Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| Scan2Doc Pro β Professional GUI Application | |
| ============================================ | |
| Modern CustomTkinter-based desktop application for OCR-to-Word conversion. | |
| Features: image preview, real-time logging, batch processing, quality scoring. | |
| """ | |
| import os | |
| import sys | |
| import threading | |
| import time | |
| import traceback | |
| from pathlib import Path | |
| # ββ Headless-safe import for customtkinter ββ | |
| os.environ.setdefault("DISPLAY", ":0") | |
| try: | |
| import customtkinter as ctk | |
| except ImportError: | |
| print("ERROR: customtkinter not installed. Run: pip install customtkinter") | |
| sys.exit(1) | |
| # Set theme | |
| ctk.set_appearance_mode("dark") | |
| ctk.set_default_color_theme("blue") | |
| try: | |
| from PIL import Image, ImageTk | |
| PIL_AVAILABLE = True | |
| except ImportError: | |
| PIL_AVAILABLE = False | |
| # Import OCR engine | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| try: | |
| from scan2doc_pro_v6 import BatchProcessor, QualityScorer, PageStatistics | |
| except ImportError: | |
| # Try alternate path | |
| try: | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from scan2doc_pro_v6 import BatchProcessor, QualityScorer, PageStatistics | |
| except ImportError: | |
| print("ERROR: Cannot import scan2doc_pro_v5.py") | |
| print("Make sure scan2doc_pro_v5.py is in the same directory as this script.") | |
| sys.exit(1) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Color Palette | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COLORS = { | |
| "bg_dark": "#1a1a2e", | |
| "bg_medium": "#16213e", | |
| "bg_light": "#0f3460", | |
| "accent": "#e94560", | |
| "accent2": "#533483", | |
| "text": "#eaeaea", | |
| "text_dim": "#8899aa", | |
| "success": "#00c853", | |
| "warning": "#ffab00", | |
| "error": "#ff1744", | |
| "sidebar": "#0f0f23", | |
| "card": "#1a1a3e", | |
| "blue": "#2979ff", | |
| "blue_dark": "#1565c0", | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # File List Item Widget | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class FileListItem(ctk.CTkFrame): | |
| """Individual file item in the file list.""" | |
| def __init__(self, master, filepath, on_remove=None, **kwargs): | |
| super().__init__(master, fg_color=COLORS["card"], corner_radius=6, **kwargs) | |
| self.filepath = filepath | |
| self.on_remove = on_remove | |
| self.grid_columnconfigure(0, weight=1) | |
| # Filename label | |
| name = os.path.basename(filepath) | |
| if len(name) > 30: | |
| name = name[:27] + "..." | |
| self.label = ctk.CTkLabel( | |
| self, text=name, anchor="w", | |
| text_color=COLORS["text"], font=ctk.CTkFont(size=11)) | |
| self.label.grid(row=0, column=0, padx=(8, 4), pady=4, sticky="ew") | |
| # Remove button | |
| self.remove_btn = ctk.CTkButton( | |
| self, text="β", width=24, height=24, | |
| fg_color="transparent", hover_color=COLORS["error"], | |
| text_color=COLORS["text_dim"], font=ctk.CTkFont(size=12, weight="bold"), | |
| command=self._remove) | |
| self.remove_btn.grid(row=0, column=1, padx=(0, 6), pady=4) | |
| def _remove(self): | |
| if self.on_remove: | |
| self.on_remove(self.filepath) | |
| self.destroy() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Sidebar Panel | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Sidebar(ctk.CTkFrame): | |
| """Left sidebar with file management and settings.""" | |
| def __init__(self, master, app, **kwargs): | |
| super().__init__(master, fg_color=COLORS["sidebar"], corner_radius=0, width=280, **kwargs) | |
| self.app = app | |
| self.pack_propagate(False) | |
| self.grid_propagate(False) | |
| self.files = [] | |
| self._build_ui() | |
| def _build_ui(self): | |
| # ββ Logo / Title ββ | |
| title_frame = ctk.CTkFrame(self, fg_color="transparent") | |
| title_frame.pack(fill="x", padx=15, pady=(15, 5)) | |
| ctk.CTkLabel( | |
| title_frame, text="π Scan2Doc Pro", | |
| font=ctk.CTkFont(size=20, weight="bold"), | |
| text_color=COLORS["blue"] | |
| ).pack(anchor="w") | |
| ctk.CTkLabel( | |
| title_frame, text="Professional OCR β Word", | |
| font=ctk.CTkFont(size=11), | |
| text_color=COLORS["text_dim"] | |
| ).pack(anchor="w") | |
| # Separator | |
| ctk.CTkFrame(self, fg_color=COLORS["bg_light"], height=1).pack(fill="x", padx=15, pady=10) | |
| # ββ File Selection ββ | |
| ctk.CTkLabel(self, text="π Input Files", font=ctk.CTkFont(size=13, weight="bold"), | |
| text_color=COLORS["text"]).pack(anchor="w", padx=15, pady=(5, 5)) | |
| btn_frame = ctk.CTkFrame(self, fg_color="transparent") | |
| btn_frame.pack(fill="x", padx=15) | |
| ctk.CTkButton( | |
| btn_frame, text="π Select Folder", height=32, | |
| fg_color=COLORS["blue_dark"], hover_color=COLORS["blue"], | |
| font=ctk.CTkFont(size=12), command=self._select_folder | |
| ).pack(side="left", expand=True, fill="x", padx=(0, 4)) | |
| ctk.CTkButton( | |
| btn_frame, text="π Add Files", height=32, | |
| fg_color=COLORS["accent2"], hover_color=COLORS["accent"], | |
| font=ctk.CTkFont(size=12), command=self._select_files | |
| ).pack(side="left", expand=True, fill="x", padx=(4, 0)) | |
| # ββ File List ββ | |
| self.file_list_frame = ctk.CTkScrollableFrame( | |
| self, fg_color=COLORS["bg_medium"], corner_radius=8, | |
| height=120) | |
| self.file_list_frame.pack(fill="x", padx=15, pady=(8, 5)) | |
| self.file_count_label = ctk.CTkLabel( | |
| self, text="No files selected", font=ctk.CTkFont(size=10), | |
| text_color=COLORS["text_dim"]) | |
| self.file_count_label.pack(anchor="w", padx=15) | |
| # Clear all button | |
| ctk.CTkButton( | |
| self, text="π Clear All", height=24, width=80, | |
| fg_color="transparent", border_width=1, border_color=COLORS["error"], | |
| text_color=COLORS["error"], hover_color="#330000", | |
| font=ctk.CTkFont(size=10), command=self._clear_files | |
| ).pack(anchor="e", padx=15, pady=(2, 8)) | |
| # Separator | |
| ctk.CTkFrame(self, fg_color=COLORS["bg_light"], height=1).pack(fill="x", padx=15, pady=5) | |
| # ββ Settings ββ | |
| ctk.CTkLabel(self, text="βοΈ Settings", font=ctk.CTkFont(size=13, weight="bold"), | |
| text_color=COLORS["text"]).pack(anchor="w", padx=15, pady=(5, 8)) | |
| # DPI | |
| self._setting_label("DPI:") | |
| self.dpi_var = ctk.StringVar(value="300") | |
| ctk.CTkOptionMenu( | |
| self, variable=self.dpi_var, | |
| values=["150", "200", "300", "400", "500", "600"], | |
| fg_color=COLORS["bg_medium"], button_color=COLORS["blue_dark"], | |
| width=120, height=28, font=ctk.CTkFont(size=11) | |
| ).pack(anchor="w", padx=15, pady=(0, 6)) | |
| # Language | |
| self._setting_label("Language:") | |
| self.lang_var = ctk.StringVar(value="Persian + English") | |
| ctk.CTkOptionMenu( | |
| self, variable=self.lang_var, | |
| values=["Persian + English", "Persian", "English", "Arabic", "All Languages"], | |
| fg_color=COLORS["bg_medium"], button_color=COLORS["blue_dark"], | |
| width=160, height=28, font=ctk.CTkFont(size=11) | |
| ).pack(anchor="w", padx=15, pady=(0, 6)) | |
| # Title | |
| self._setting_label("Document Title:") | |
| self.title_entry = ctk.CTkEntry( | |
| self, placeholder_text="Optional title...", | |
| fg_color=COLORS["bg_medium"], border_color=COLORS["blue_dark"], | |
| height=28, font=ctk.CTkFont(size=11)) | |
| self.title_entry.pack(fill="x", padx=15, pady=(0, 6)) | |
| # Checkboxes | |
| self.tables_var = ctk.BooleanVar(value=True) | |
| self.lines_var = ctk.BooleanVar(value=True) | |
| self.images_var = ctk.BooleanVar(value=False) | |
| checks_frame = ctk.CTkFrame(self, fg_color="transparent") | |
| checks_frame.pack(fill="x", padx=15, pady=(0, 4)) | |
| ctk.CTkCheckBox( | |
| checks_frame, text="Tables", variable=self.tables_var, | |
| text_color=COLORS["text"], font=ctk.CTkFont(size=11), | |
| fg_color=COLORS["blue_dark"], hover_color=COLORS["blue"], | |
| checkmark_color=COLORS["text"] | |
| ).pack(side="left", padx=(0, 8)) | |
| ctk.CTkCheckBox( | |
| checks_frame, text="Lines", variable=self.lines_var, | |
| text_color=COLORS["text"], font=ctk.CTkFont(size=11), | |
| fg_color=COLORS["blue_dark"], hover_color=COLORS["blue"], | |
| checkmark_color=COLORS["text"] | |
| ).pack(side="left", padx=(0, 8)) | |
| ctk.CTkCheckBox( | |
| checks_frame, text="Images", variable=self.images_var, | |
| text_color=COLORS["text"], font=ctk.CTkFont(size=11), | |
| fg_color=COLORS["blue_dark"], hover_color=COLORS["blue"], | |
| checkmark_color=COLORS["text"] | |
| ).pack(side="left") | |
| # Separator | |
| ctk.CTkFrame(self, fg_color=COLORS["bg_light"], height=1).pack(fill="x", padx=15, pady=8) | |
| # ββ Output ββ | |
| ctk.CTkLabel(self, text="πΎ Output", font=ctk.CTkFont(size=13, weight="bold"), | |
| text_color=COLORS["text"]).pack(anchor="w", padx=15, pady=(0, 5)) | |
| self.output_var = ctk.StringVar(value="output.docx") | |
| self.output_entry = ctk.CTkEntry( | |
| self, textvariable=self.output_var, | |
| fg_color=COLORS["bg_medium"], border_color=COLORS["blue_dark"], | |
| height=28, font=ctk.CTkFont(size=11)) | |
| self.output_entry.pack(fill="x", padx=15, pady=(0, 4)) | |
| ctk.CTkButton( | |
| self, text="π Browse...", height=26, | |
| fg_color=COLORS["bg_medium"], border_width=1, | |
| border_color=COLORS["blue_dark"], hover_color=COLORS["blue_dark"], | |
| font=ctk.CTkFont(size=11), command=self._select_output | |
| ).pack(fill="x", padx=15, pady=(0, 10)) | |
| # ββ Start Button ββ | |
| self.start_btn = ctk.CTkButton( | |
| self, text="βΆ START PROCESSING", height=44, | |
| fg_color=COLORS["blue"], hover_color=COLORS["blue_dark"], | |
| font=ctk.CTkFont(size=15, weight="bold"), | |
| corner_radius=10, command=self._start_processing) | |
| self.start_btn.pack(fill="x", padx=15, pady=(0, 15)) | |
| def _setting_label(self, text): | |
| ctk.CTkLabel(self, text=text, font=ctk.CTkFont(size=11), | |
| text_color=COLORS["text_dim"]).pack(anchor="w", padx=15, pady=(0, 2)) | |
| def _select_folder(self): | |
| try: | |
| from tkinter import filedialog | |
| folder = filedialog.askdirectory(title="Select Image Folder") | |
| if folder: | |
| exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp', '.gif'} | |
| files = [] | |
| for f in sorted(Path(folder).iterdir()): | |
| if f.suffix.lower() in exts: | |
| files.append(str(f)) | |
| if files: | |
| self.files.extend(files) | |
| self._refresh_file_list() | |
| self.app.log(f"Added {len(files)} files from folder") | |
| else: | |
| self.app.log("No image files found in selected folder", "warning") | |
| except Exception as e: | |
| self.app.log(f"Error selecting folder: {e}", "error") | |
| def _select_files(self): | |
| try: | |
| from tkinter import filedialog | |
| files = filedialog.askopenfilenames( | |
| title="Select Image Files", | |
| filetypes=[ | |
| ("Image files", "*.jpg *.jpeg *.png *.bmp *.tiff *.tif *.webp *.gif"), | |
| ("All files", "*.*") | |
| ]) | |
| if files: | |
| self.files.extend(list(files)) | |
| self._refresh_file_list() | |
| self.app.log(f"Added {len(files)} file(s)") | |
| except Exception as e: | |
| self.app.log(f"Error selecting files: {e}", "error") | |
| def _select_output(self): | |
| try: | |
| from tkinter import filedialog | |
| path = filedialog.asksaveasfilename( | |
| title="Save Output As", | |
| defaultextension=".docx", | |
| filetypes=[("Word Document", "*.docx")]) | |
| if path: | |
| self.output_var.set(path) | |
| except Exception as e: | |
| self.app.log(f"Error: {e}", "error") | |
| def _clear_files(self): | |
| self.files.clear() | |
| self._refresh_file_list() | |
| self.app.log("File list cleared") | |
| def _remove_file(self, filepath): | |
| if filepath in self.files: | |
| self.files.remove(filepath) | |
| self._refresh_file_list() | |
| def _refresh_file_list(self): | |
| # Clear existing items | |
| for widget in self.file_list_frame.winfo_children(): | |
| widget.destroy() | |
| for fp in self.files: | |
| item = FileListItem(self.file_list_frame, fp, on_remove=self._remove_file) | |
| item.pack(fill="x", padx=2, pady=2) | |
| count = len(self.files) | |
| self.file_count_label.configure( | |
| text=f"{count} file(s) selected" if count > 0 else "No files selected") | |
| # Auto-set output filename | |
| if count == 1: | |
| stem = Path(self.files[0]).stem | |
| self.output_var.set(f"{stem}.docx") | |
| elif count > 1: | |
| parent = os.path.basename(os.path.dirname(self.files[0])) | |
| if parent: | |
| self.output_var.set(f"{parent}.docx") | |
| def get_settings(self): | |
| """Return current settings as a dict.""" | |
| lang_map = { | |
| "Persian + English": "fas+eng", | |
| "Persian": "fas", | |
| "English": "eng", | |
| "Arabic": "ara", | |
| "All Languages": "fas+ara+eng" | |
| } | |
| return { | |
| "files": self.files[:], | |
| "dpi": int(self.dpi_var.get()), | |
| "lang": lang_map.get(self.lang_var.get(), "fas+eng"), | |
| "title": self.title_entry.get().strip() or None, | |
| "output": self.output_var.get().strip() or "output.docx", | |
| "tables": self.tables_var.get(), | |
| "lines": self.lines_var.get(), | |
| "images": self.images_var.get(), | |
| } | |
| def set_processing_state(self, processing): | |
| """Enable/disable UI during processing.""" | |
| state = "disabled" if processing else "normal" | |
| self.start_btn.configure( | |
| state=state, | |
| text="β³ Processing..." if processing else "βΆ START PROCESSING") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main Content Area | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MainArea(ctk.CTkFrame): | |
| """Right main area with preview, log, and progress.""" | |
| def __init__(self, master, app, **kwargs): | |
| super().__init__(master, fg_color=COLORS["bg_dark"], corner_radius=0, **kwargs) | |
| self.app = app | |
| self.grid_rowconfigure(1, weight=1) | |
| self.grid_columnconfigure(0, weight=1) | |
| self._build_ui() | |
| def _build_ui(self): | |
| # ββ Preview Panel ββ | |
| preview_label = ctk.CTkLabel( | |
| self, text="πΌοΈ Preview", font=ctk.CTkFont(size=13, weight="bold"), | |
| text_color=COLORS["text"]) | |
| preview_label.grid(row=0, column=0, padx=15, pady=(10, 5), sticky="w") | |
| self.preview_frame = ctk.CTkFrame( | |
| self, fg_color=COLORS["bg_medium"], corner_radius=10, | |
| height=250) | |
| self.preview_frame.grid(row=0, column=0, padx=15, pady=(30, 5), sticky="nsew") | |
| self.preview_frame.grid_propagate(False) | |
| self.preview_label = ctk.CTkLabel( | |
| self.preview_frame, text="Select files to preview", | |
| text_color=COLORS["text_dim"], font=ctk.CTkFont(size=12)) | |
| self.preview_label.pack(expand=True) | |
| # ββ Processing Log ββ | |
| log_label = ctk.CTkLabel( | |
| self, text="π Processing Log", font=ctk.CTkFont(size=13, weight="bold"), | |
| text_color=COLORS["text"]) | |
| log_label.grid(row=1, column=0, padx=15, pady=(10, 5), sticky="nw") | |
| self.log_text = ctk.CTkTextbox( | |
| self, fg_color=COLORS["bg_medium"], text_color=COLORS["text"], | |
| font=ctk.CTkFont(family="Consolas", size=11), | |
| corner_radius=10, wrap="word") | |
| self.log_text.grid(row=1, column=0, padx=15, pady=(30, 5), sticky="nsew") | |
| self.log_text.configure(state="disabled") | |
| # ββ Footer: Progress + Status ββ | |
| footer_frame = ctk.CTkFrame(self, fg_color=COLORS["bg_medium"], corner_radius=10) | |
| footer_frame.grid(row=2, column=0, padx=15, pady=(5, 10), sticky="ew") | |
| footer_frame.grid_columnconfigure(0, weight=1) | |
| # Progress bar | |
| self.progress_bar = ctk.CTkProgressBar( | |
| footer_frame, progress_color=COLORS["blue"], | |
| fg_color=COLORS["bg_dark"], height=12) | |
| self.progress_bar.grid(row=0, column=0, padx=10, pady=(8, 4), sticky="ew") | |
| self.progress_bar.set(0) | |
| # Status labels | |
| status_frame = ctk.CTkFrame(footer_frame, fg_color="transparent") | |
| status_frame.grid(row=1, column=0, padx=10, pady=(0, 8), sticky="ew") | |
| status_frame.grid_columnconfigure(0, weight=1) | |
| status_frame.grid_columnconfigure(1, weight=1) | |
| status_frame.grid_columnconfigure(2, weight=1) | |
| self.progress_label = ctk.CTkLabel( | |
| status_frame, text="0%", font=ctk.CTkFont(size=12, weight="bold"), | |
| text_color=COLORS["blue"]) | |
| self.progress_label.grid(row=0, column=0, sticky="w") | |
| self.page_label = ctk.CTkLabel( | |
| status_frame, text="0 / 0 pages", font=ctk.CTkFont(size=11), | |
| text_color=COLORS["text_dim"]) | |
| self.page_label.grid(row=0, column=1, sticky="w") | |
| self.quality_label = ctk.CTkLabel( | |
| status_frame, text="Quality: --", font=ctk.CTkFont(size=11, weight="bold"), | |
| text_color=COLORS["text_dim"]) | |
| self.quality_label.grid(row=0, column=2, sticky="e") | |
| def log(self, message, level="info"): | |
| """Thread-safe log message.""" | |
| prefix = {"info": "βΉοΈ", "warning": "β οΈ", "error": "β", "success": "β "}.get(level, "βΉοΈ") | |
| color_map = {"info": COLORS["text"], "warning": COLORS["warning"], | |
| "error": COLORS["error"], "success": COLORS["success"]} | |
| timestamp = time.strftime("%H:%M:%S") | |
| line = f"[{timestamp}] {prefix} {message}\n" | |
| def _append(): | |
| self.log_text.configure(state="normal") | |
| self.log_text.insert("end", line) | |
| self.log_text.see("end") | |
| self.log_text.configure(state="disabled") | |
| try: | |
| self.after(0, _append) | |
| except Exception: | |
| pass | |
| def update_preview(self, filepath): | |
| """Update the preview panel with an image.""" | |
| if not PIL_AVAILABLE: | |
| return | |
| def _update(): | |
| try: | |
| for widget in self.preview_frame.winfo_children(): | |
| widget.destroy() | |
| img = Image.open(filepath) | |
| # Calculate fit size | |
| frame_w = self.preview_frame.winfo_width() - 20 | |
| frame_h = self.preview_frame.winfo_height() - 20 | |
| if frame_w < 10: frame_w = 400 | |
| if frame_h < 10: frame_h = 200 | |
| img.thumbnail((frame_w, frame_h), Image.Resampling.LANCZOS) | |
| photo = ImageTk.PhotoImage(img) | |
| label = ctk.CTkLabel(self.preview_frame, image=photo, text="") | |
| label.image = photo # Keep reference | |
| label.pack(expand=True) | |
| except Exception as e: | |
| self.log(f"Preview error: {e}", "warning") | |
| try: | |
| self.after(0, _update) | |
| except Exception: | |
| pass | |
| def update_progress(self, current, total, message=""): | |
| """Update progress bar and labels.""" | |
| def _update(): | |
| pct = (current / total * 100) if total > 0 else 0 | |
| self.progress_bar.set(current / total if total > 0 else 0) | |
| self.progress_label.configure(text=f"{pct:.0f}%") | |
| self.page_label.configure(text=f"{current} / {total} pages") | |
| try: | |
| self.after(0, _update) | |
| except Exception: | |
| pass | |
| def update_quality(self, score): | |
| """Update quality score display.""" | |
| def _update(): | |
| if score >= 9: | |
| color = COLORS["success"] | |
| elif score >= 7: | |
| color = COLORS["blue"] | |
| elif score >= 5: | |
| color = COLORS["warning"] | |
| else: | |
| color = COLORS["error"] | |
| self.quality_label.configure(text=f"Quality: {score}/10", text_color=color) | |
| try: | |
| self.after(0, _update) | |
| except Exception: | |
| pass | |
| def reset(self): | |
| """Reset progress display.""" | |
| self.progress_bar.set(0) | |
| self.progress_label.configure(text="0%", text_color=COLORS["blue"]) | |
| self.page_label.configure(text="0 / 0 pages") | |
| self.quality_label.configure(text="Quality: --", text_color=COLORS["text_dim"]) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main Application Window | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Scan2DocApp(ctk.CTk): | |
| """Main application window.""" | |
| def __init__(self): | |
| super().__init__() | |
| self.title("Scan2Doc Pro β Professional OCR to Word") | |
| self.geometry("1280x800") | |
| self.minsize(900, 600) | |
| self.configure(fg_color=COLORS["bg_dark"]) | |
| self.processing = False | |
| self._build_layout() | |
| def _build_layout(self): | |
| # Grid layout: sidebar (fixed) + main area (expandable) | |
| self.grid_columnconfigure(1, weight=1) | |
| self.grid_rowconfigure(0, weight=1) | |
| # Sidebar | |
| self.sidebar = Sidebar(self, self) | |
| self.sidebar.grid(row=0, column=0, sticky="ns") | |
| # Main area | |
| self.main_area = MainArea(self, self) | |
| self.main_area.grid(row=0, column=1, sticky="nsew") | |
| def log(self, message, level="info"): | |
| """Log a message to the processing log.""" | |
| self.main_area.log(message, level) | |
| def _start_processing(self): | |
| """Start OCR processing in a background thread.""" | |
| settings = self.sidebar.get_settings() | |
| if not settings["files"]: | |
| self.log("No files selected! Please add images first.", "error") | |
| return | |
| if self.processing: | |
| return | |
| self.processing = True | |
| self.sidebar.set_processing_state(True) | |
| self.main_area.reset() | |
| # Show first image preview | |
| if settings["files"]: | |
| self.main_area.update_preview(settings["files"][0]) | |
| thread = threading.Thread(target=self._process_worker, args=(settings,), daemon=True) | |
| thread.start() | |
| def _process_worker(self, settings): | |
| """Background processing worker.""" | |
| try: | |
| self.log(f"Starting processing: {len(settings['files'])} file(s)", "info") | |
| self.log(f"Settings: DPI={settings['dpi']}, Lang={settings['lang']}", "info") | |
| t0 = time.time() | |
| def progress_callback(current, total, msg): | |
| self.main_area.update_progress(current, total, msg) | |
| self.log(f"Page {current}/{total}: {msg}", "info") | |
| processor = BatchProcessor( | |
| lang=settings["lang"], | |
| dpi=settings["dpi"], | |
| title=settings["title"], | |
| progress_callback=progress_callback | |
| ) | |
| # Suppress print output, capture via log | |
| import io | |
| old_stdout = sys.stdout | |
| sys.stdout = io.StringIO() | |
| ok = processor.process_batch( | |
| settings["files"], | |
| settings["output"], | |
| title=settings["title"] | |
| ) | |
| sys.stdout = old_stdout | |
| # Calculate quality score | |
| if processor.stats: | |
| ac = sum(s.overall_confidence for s in processor.stats) / len(processor.stats) | |
| tb = sum(s.text_blocks for s in processor.stats) | |
| tt = sum(s.tables for s in processor.stats) | |
| tl = sum(s.lines for s in processor.stats) | |
| tw = sum(s.total_words for s in processor.stats) | |
| lc = sum(s.low_confidence_words for s in processor.stats) | |
| agg_stats = { | |
| 'tables_found': tt, 'confidence': ac, 'blocks': tb, | |
| 'low_conf_words': lc, 'lines_found': tl, | |
| 'columns': max((s.columns for s in processor.stats), default=1), | |
| 'word_count': tw, | |
| } | |
| quality = QualityScorer.calculate(agg_stats) | |
| self.main_area.update_quality(quality) | |
| self.log(f"Quality Score: {quality}/10", "success") | |
| self.log(f"Blocks: {tb}, Tables: {tt}, Words: {tw}, Confidence: {ac:.1f}%", "info") | |
| elapsed = time.time() - t0 | |
| if ok: | |
| self.log(f"Done! Output saved to: {settings['output']}", "success") | |
| self.log(f"Total time: {elapsed:.1f}s", "info") | |
| self.main_area.update_progress(len(settings["files"]), len(settings["files"])) | |
| else: | |
| self.log("Processing failed. Check the log for errors.", "error") | |
| except Exception as e: | |
| self.log(f"Processing error: {e}", "error") | |
| self.log(traceback.format_exc(), "error") | |
| sys.stdout = sys.__stdout__ | |
| finally: | |
| self.processing = False | |
| try: | |
| self.after(0, lambda: self.sidebar.set_processing_state(False)) | |
| except Exception: | |
| pass | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Entry Point | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| """Launch the application.""" | |
| app = Scan2DocApp() | |
| app.mainloop() | |
| if __name__ == "__main__": | |
| main() | |