/
grenki70
/
practice
Обзор
Документация
Войти
/
grenki70
/
practice
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app.py
341 строка
14 KB
grenki70
Initial commit: детектор рукописных дат в PDF
22 июн 2026, 22:23
22 июн 2026, 22:23
f4901f0
Код
Авторство
О чём код?
import os import subprocess import sys import threading from pathlib import Path import tkinter as tk from tkinter import filedialog, font, ttk from signature_detector import detect_dates_in_pdf DEFAULT_MODEL = str(Path("yolov8n-16") / "weights" / "best.pt") DEFAULT_DEBUG_DIR = str(Path("pdf_results") / "debug") BG = "#1e1f29" CARD = "#282a36" FIELD = "#1e1f29" TEXT = "#f8f8f2" MUTED = "#9aa0b4" ACCENT = "#7c6cff" ACCENT_ACTIVE = "#9384ff" PROGRESS = "#ffffff" BORDER = "#3a3d4d" TOP_COLOR = "#8be9fd" BOTTOM_COLOR = "#ffb86c" ERROR_COLOR = "#ff5c7a" ROW_ALT = "#2f3140" def find_pdfs(folder): return sorted(p for p in Path(folder).rglob("*") if p.suffix.lower() == ".pdf") class App: def __init__(self, root): self.root = root root.title("Детектор рукописных дат в PDF") root.geometry("960x660") root.minsize(820, 560) root.configure(bg=BG) self.folder = tk.StringVar() self.model_path = tk.StringVar(value=DEFAULT_MODEL) self.conf = tk.DoubleVar(value=0.50) self.debug = tk.BooleanVar(value=False) self.status = tk.StringVar(value="Выберите папку с PDF") self.running = False self.item_paths = {} self.folder_nodes = {} self._fonts() self._theme() self._build() def _fonts(self): self.f_title = font.Font(family="Segoe UI", size=18, weight="bold") self.f_sub = font.Font(family="Segoe UI", size=10) self.f_label = font.Font(family="Segoe UI", size=10) self.f_body = font.Font(family="Segoe UI", size=10) self.f_bold = font.Font(family="Segoe UI", size=10, weight="bold") self.f_badge = font.Font(family="Segoe UI", size=11, weight="bold") def _theme(self): style = ttk.Style() style.theme_use("clam") style.configure("App.TFrame", background=BG) style.configure("Card.TFrame", background=CARD) style.configure("Header.TFrame", background=BG) style.configure("Card.TLabel", background=CARD, foreground=TEXT, font=self.f_label) style.configure("Muted.TLabel", background=CARD, foreground=MUTED, font=self.f_sub) style.configure("Title.TLabel", background=BG, foreground=TEXT, font=self.f_title) style.configure("Subtitle.TLabel", background=BG, foreground=MUTED, font=self.f_sub) style.configure("Status.TLabel", background=BG, foreground=MUTED, font=self.f_sub) style.configure("Badge.TLabel", background=BG, foreground=ACCENT, font=self.f_badge) style.configure( "App.TEntry", fieldbackground=FIELD, foreground=TEXT, bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER, insertcolor=TEXT, padding=6, ) style.map("App.TEntry", bordercolor=[("focus", ACCENT)], lightcolor=[("focus", ACCENT)]) style.configure( "Accent.TButton", background=ACCENT, foreground="#ffffff", font=self.f_bold, borderwidth=0, focuscolor=ACCENT, padding=(16, 8), ) style.map("Accent.TButton", background=[("active", ACCENT_ACTIVE), ("disabled", BORDER)]) style.configure( "Ghost.TButton", background=CARD, foreground=TEXT, font=self.f_body, borderwidth=1, bordercolor=BORDER, focuscolor=CARD, padding=(12, 6), ) style.map("Ghost.TButton", background=[("active", ROW_ALT)], bordercolor=[("active", ACCENT)]) style.configure( "App.TCheckbutton", background=CARD, foreground=TEXT, font=self.f_body, focuscolor=CARD, ) style.map("App.TCheckbutton", background=[("active", CARD)], foreground=[("active", TEXT)]) style.configure( "App.Horizontal.TScale", background=CARD, troughcolor=FIELD, bordercolor=CARD, ) style.configure( "App.Horizontal.TProgressbar", background=PROGRESS, troughcolor=FIELD, bordercolor=BG, lightcolor=PROGRESS, darkcolor=PROGRESS, ) style.configure( "App.Treeview", background=CARD, fieldbackground=CARD, foreground=TEXT, rowheight=30, borderwidth=0, font=self.f_body, ) style.configure( "App.Treeview.Heading", background=BG, foreground=MUTED, font=self.f_bold, borderwidth=0, relief="flat", padding=8, ) style.map("App.Treeview.Heading", background=[("active", BG)]) style.map("App.Treeview", background=[("selected", ACCENT)], foreground=[("selected", "#ffffff")]) def _build(self): header = ttk.Frame(self.root, style="Header.TFrame", padding=(20, 18, 20, 6)) header.pack(fill="x") ttk.Label(header, text=" ", style="Title.TLabel").pack(anchor="w") ttk.Label( header, text="V.1", style="Subtitle.TLabel", ).pack(anchor="w", pady=(2, 0)) card = ttk.Frame(self.root, style="Card.TFrame", padding=18) card.pack(fill="x", padx=20, pady=10) card.columnconfigure(1, weight=1) ttk.Label(card, text="Папка", style="Card.TLabel").grid(row=0, column=0, sticky="w") ttk.Entry(card, textvariable=self.folder, style="App.TEntry").grid( row=0, column=1, sticky="ew", padx=10, pady=4 ) ttk.Button(card, text="Обзор", style="Ghost.TButton", command=self.pick_folder).grid(row=0, column=2) ttk.Label(card, text="Модель", style="Card.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 0)) ttk.Entry(card, textvariable=self.model_path, style="App.TEntry").grid( row=1, column=1, sticky="ew", padx=10, pady=(8, 4) ) ttk.Button(card, text="Обзор", style="Ghost.TButton", command=self.pick_model).grid( row=1, column=2, pady=(8, 0) ) ttk.Label(card, text="Уверенность", style="Card.TLabel").grid(row=2, column=0, sticky="w", pady=(12, 0)) slider = ttk.Frame(card, style="Card.TFrame") slider.grid(row=2, column=1, sticky="ew", padx=10, pady=(12, 0)) slider.columnconfigure(0, weight=1) ttk.Scale( slider, from_=0.05, to=0.95, variable=self.conf, style="App.Horizontal.TScale", command=self.on_conf, ).grid(row=0, column=0, sticky="ew") self.conf_badge = ttk.Label(card, text=f"{self.conf.get():.2f}", style="Badge.TLabel") self.conf_badge.grid(row=2, column=2, pady=(12, 0)) ttk.Checkbutton( card, text="Сохранять дебаг-изображения", variable=self.debug, style="App.TCheckbutton", ).grid(row=3, column=1, sticky="w", padx=10, pady=(12, 0)) actions = ttk.Frame(self.root, style="App.TFrame", padding=(20, 0)) actions.pack(fill="x") self.run_btn = ttk.Button(actions, text="Сканировать", style="Accent.TButton", command=self.start) self.run_btn.pack(side="left") self.progress = ttk.Progressbar( actions, style="App.Horizontal.TProgressbar", mode="determinate", length=180, ) self.progress.pack(side="right") result_wrap = ttk.Frame(self.root, style="Card.TFrame", padding=2) result_wrap.pack(fill="both", expand=True, padx=20, pady=(12, 4)) columns = ("count", "position", "conf") self.tree = ttk.Treeview(result_wrap, columns=columns, style="App.Treeview", show="tree headings") self.tree.heading("#0", text=" Документ / Страница") self.tree.heading("count", text="Дат") self.tree.heading("position", text="Позиция") self.tree.heading("conf", text="Conf") self.tree.column("#0", width=480, anchor="w") self.tree.column("count", width=90, anchor="center") self.tree.column("position", width=140, anchor="center") self.tree.column("conf", width=90, anchor="center") scrollbar = ttk.Scrollbar(result_wrap, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") self.tree.pack(side="left", fill="both", expand=True) self.tree.tag_configure("folder", font=self.f_bold, foreground=TOP_COLOR) self.tree.tag_configure("doc", font=self.f_bold) self.tree.tag_configure("doc_empty", foreground=MUTED) self.tree.tag_configure("odd", background=ROW_ALT) self.tree.tag_configure("сверху", foreground=TOP_COLOR) self.tree.tag_configure("снизу", foreground=BOTTOM_COLOR) self.tree.tag_configure("error", foreground=ERROR_COLOR) self.tree.bind("<Double-1>", self.on_open_document) status_bar = ttk.Frame(self.root, style="Header.TFrame", padding=(20, 4, 20, 12)) status_bar.pack(fill="x") ttk.Label(status_bar, textvariable=self.status, style="Status.TLabel").pack(side="left") self._zebra = 0 def on_conf(self, _value): self.conf_badge.config(text=f"{self.conf.get():.2f}") def on_open_document(self, event): item = self.tree.identify_row(event.y) pdf = self.item_paths.get(item) if not pdf: return if not Path(pdf).exists(): self.status.set(f"Файл не найден: {pdf}") return try: if sys.platform.startswith("win"): os.startfile(str(pdf)) elif sys.platform == "darwin": subprocess.Popen(["open", str(pdf)]) else: subprocess.Popen(["xdg-open", str(pdf)]) except Exception as error: self.status.set(f"Не удалось открыть документ: {error}") def pick_folder(self): path = filedialog.askdirectory() if path: self.folder.set(path) def pick_model(self): path = filedialog.askopenfilename(filetypes=[("Модель YOLO", "*.pt"), ("Все файлы", "*.*")]) if path: self.model_path.set(path) def start(self): if self.running: return folder = self.folder.get().strip() if not folder or not Path(folder).is_dir(): self.status.set("Папка не выбрана или не существует") return self.tree.delete(*self.tree.get_children()) self.item_paths.clear() self.folder_nodes.clear() self._zebra = 0 self.running = True self.run_btn.config(state="disabled") threading.Thread(target=self.scan, args=(folder,), daemon=True).start() def scan(self, folder): model_path = self.model_path.get().strip() conf = round(self.conf.get(), 2) debug_dir = DEFAULT_DEBUG_DIR if self.debug.get() else None pdfs = find_pdfs(folder) if not pdfs: self.ui(lambda: self.status.set("PDF не найдено")) self.ui(self.finish) return total = len(pdfs) self.ui(lambda: self.progress.configure(maximum=total, value=0)) found = 0 for i, pdf in enumerate(pdfs, start=1): self.ui(lambda i=i, p=pdf: self.status.set(f"[{i}/{total}] {p.name}")) try: detections = detect_dates_in_pdf(str(pdf), model_path, conf=conf, debug_dir=debug_dir) except Exception as error: self.ui(lambda p=pdf, e=error: self.add_error(folder, p, e)) else: found += len(detections) self.ui(lambda p=pdf, d=detections: self.add_result(folder, p, d)) self.ui(lambda i=i: self.progress.configure(value=i)) self.ui(lambda: self.status.set(f"Готово · документов: {total} · найдено дат: {found}")) self.ui(self.finish) def _row_tag(self): self._zebra += 1 return "odd" if self._zebra % 2 else "even" def _ensure_folder(self, rel_dir): if str(rel_dir) in (".", ""): return "" parent = "" key = "" for part in rel_dir.parts: key = part if not key else key + "/" + part node = self.folder_nodes.get(key) if node is None: node = self.tree.insert( parent, "end", text=" " + part, open=True, values=("", "", ""), tags=("folder", self._row_tag()), ) self.folder_nodes[key] = node parent = node return parent def add_result(self, folder, pdf, detections): rel = pdf.relative_to(folder) folder_node = self._ensure_folder(rel.parent) count = len(detections) doc_tag = "doc" if count else "doc_empty" parent = self.tree.insert( folder_node, "end", text=" " + rel.name, open=True, values=(count if count else "—", "", ""), tags=(doc_tag, self._row_tag()), ) self.item_paths[parent] = pdf for det in detections: child = self.tree.insert( parent, "end", text=f' {det["page_label"]} · стр. {det["page_number"]}', values=("", det["position"], f'{det["conf"]:.2f}'), tags=(det["position"], self._row_tag()), ) self.item_paths[child] = pdf def add_error(self, folder, pdf, error): rel = pdf.relative_to(folder) folder_node = self._ensure_folder(rel.parent) item = self.tree.insert( folder_node, "end", text=" " + rel.name, values=("ошибка", str(error), ""), tags=("error", self._row_tag()), ) self.item_paths[item] = pdf def finish(self): self.running = False self.run_btn.config(state="normal") def ui(self, func): self.root.after(0, func) if __name__ == "__main__": root = tk.Tk() App(root) root.mainloop()