Spaces:
Running
Running
| # manga_settings_dialog.py | |
| """ | |
| Enhanced settings dialog for manga translation with all settings visible | |
| Properly integrated with TranslatorGUI's WindowManager and UIHelper | |
| """ | |
| import os | |
| import json | |
| import tkinter as tk | |
| from tkinter import ttk, filedialog, messagebox | |
| import ttkbootstrap as tb | |
| from typing import Dict, Any, Optional, Callable | |
| from bubble_detector import BubbleDetector | |
| import logging | |
| import time | |
| import copy | |
| # Use the same logging infrastructure initialized by translator_gui | |
| logger = logging.getLogger(__name__) | |
| class MangaSettingsDialog: | |
| """Settings dialog for manga translation""" | |
| def __init__(self, parent, main_gui, config: Dict[str, Any], callback: Optional[Callable] = None): | |
| """Initialize settings dialog | |
| Args: | |
| parent: Parent window | |
| main_gui: Reference to TranslatorGUI instance | |
| config: Configuration dictionary | |
| callback: Function to call after saving | |
| """ | |
| self.parent = parent | |
| self.main_gui = main_gui | |
| self.config = config | |
| self.callback = callback | |
| self.dialog = None | |
| # Enhanced default settings structure with all options | |
| self.default_settings = { | |
| 'preprocessing': { | |
| 'enabled': False, | |
| 'auto_detect_quality': True, | |
| 'contrast_threshold': 0.4, | |
| 'sharpness_threshold': 0.3, | |
| 'noise_threshold': 20, | |
| 'enhancement_strength': 1.5, | |
| 'denoise_strength': 10, | |
| 'max_image_dimension': 2000, | |
| 'max_image_pixels': 2000000, | |
| 'chunk_height': 2000, | |
| 'chunk_overlap': 100, | |
| # Inpainting tiling | |
| 'inpaint_tiling_enabled': False, # Off by default | |
| 'inpaint_tile_size': 512, # Default tile size | |
| 'inpaint_tile_overlap': 64 # Overlap to avoid seams | |
| }, | |
| 'compression': { | |
| 'enabled': False, | |
| 'format': 'jpeg', | |
| 'jpeg_quality': 85, | |
| 'png_compress_level': 6, | |
| 'webp_quality': 85 | |
| }, | |
| 'ocr': { | |
| 'language_hints': ['ja', 'ko', 'zh'], | |
| 'confidence_threshold': 0.7, | |
| 'merge_nearby_threshold': 20, | |
| 'azure_merge_multiplier': 3.0, | |
| 'text_detection_mode': 'document', | |
| 'enable_rotation_correction': True, | |
| 'bubble_detection_enabled': True, | |
| 'roi_locality_enabled': False, | |
| 'bubble_model_path': '', | |
| 'bubble_confidence': 0.5, | |
| 'detector_type': 'rtdetr_onnx', | |
| 'rtdetr_confidence': 0.3, | |
| 'detect_empty_bubbles': True, | |
| 'detect_text_bubbles': True, | |
| 'detect_free_text': True, | |
| 'rtdetr_model_url': '', | |
| 'azure_reading_order': 'natural', | |
| 'azure_model_version': 'latest', | |
| 'azure_max_wait': 60, | |
| 'azure_poll_interval': 0.5, | |
| 'min_text_length': 0, | |
| 'exclude_english_text': False, | |
| 'english_exclude_threshold': 0.7, | |
| 'english_exclude_min_chars': 4, | |
| 'english_exclude_short_tokens': False | |
| }, | |
| 'advanced': { | |
| 'format_detection': True, | |
| 'webtoon_mode': 'auto', | |
| 'debug_mode': False, | |
| 'save_intermediate': False, | |
| 'parallel_processing': True, | |
| 'max_workers': 2, | |
| 'parallel_panel_translation': False, | |
| 'panel_max_workers': 2, | |
| 'use_singleton_models': False, | |
| 'auto_cleanup_models': False, | |
| 'unload_models_after_translation': False, | |
| 'auto_convert_to_onnx': False, # Disabled by default | |
| 'auto_convert_to_onnx_background': True, | |
| 'quantize_models': False, | |
| 'onnx_quantize': False, | |
| 'torch_precision': 'fp16', | |
| # HD strategy defaults (mirrors comic-translate) | |
| 'hd_strategy': 'resize', # 'original' | 'resize' | 'crop' | |
| 'hd_strategy_resize_limit': 1536, # long-edge cap for resize | |
| 'hd_strategy_crop_margin': 16, # pixels padding around cropped ROIs | |
| 'hd_strategy_crop_trigger_size': 1024, # only crop if long edge exceeds this | |
| # RAM cap defaults | |
| 'ram_cap_enabled': False, | |
| 'ram_cap_mb': 4096, | |
| 'ram_cap_mode': 'soft', | |
| 'ram_gate_timeout_sec': 15.0, | |
| 'ram_min_floor_over_baseline_mb': 256 | |
| }, | |
| 'inpainting': { | |
| 'batch_size': 10, | |
| 'enable_cache': True, | |
| 'method': 'local', | |
| 'local_method': 'anime' | |
| }, | |
| 'font_sizing': { | |
| 'algorithm': 'smart', # 'smart', 'conservative', 'aggressive' | |
| 'prefer_larger': True, # Prefer larger readable text | |
| 'max_lines': 10, # Maximum lines before forcing smaller | |
| 'line_spacing': 1.3, # Line height multiplier | |
| 'bubble_size_factor': True # Scale font based on bubble size | |
| }, | |
| # Mask dilation settings with new iteration controls | |
| 'mask_dilation': 0, | |
| 'use_all_iterations': True, # Master control - use same for all by default | |
| 'all_iterations': 2, # Value when using same for all | |
| 'text_bubble_dilation_iterations': 2, # Text-filled speech bubbles | |
| 'empty_bubble_dilation_iterations': 3, # Empty speech bubbles | |
| 'free_text_dilation_iterations': 0, # Free text (0 for clean B&W) | |
| 'bubble_dilation_iterations': 2, # Legacy support | |
| 'dilation_iterations': 2, # Legacy support | |
| # Cloud inpainting settings | |
| 'cloud_inpaint_model': 'ideogram-v2', | |
| 'cloud_custom_version': '', | |
| 'cloud_inpaint_prompt': 'clean background, smooth surface', | |
| 'cloud_negative_prompt': 'text, writing, letters', | |
| 'cloud_inference_steps': 20, | |
| 'cloud_timeout': 60 | |
| } | |
| # Merge with existing config | |
| self.settings = self._merge_settings(config.get('manga_settings', {})) | |
| # Show dialog | |
| self.show_dialog() | |
| def _disable_spinbox_scroll(self, widget): | |
| """Disable mouse wheel scrolling on a spinbox or combobox""" | |
| def dummy_scroll(event): | |
| # Return "break" to prevent the default scroll behavior | |
| return "break" | |
| # Bind mouse wheel events to the dummy handler | |
| widget.bind("<MouseWheel>", dummy_scroll) # Windows | |
| widget.bind("<Button-4>", dummy_scroll) # Linux scroll up | |
| widget.bind("<Button-5>", dummy_scroll) # Linux scroll down | |
| def _disable_all_spinbox_scrolling(self, parent): | |
| """Recursively find and disable scrolling on all spinboxes and comboboxes""" | |
| for widget in parent.winfo_children(): | |
| # Check if it's a Spinbox (both ttk and tk versions) | |
| if isinstance(widget, (tb.Spinbox, tk.Spinbox, ttk.Spinbox)): | |
| self._disable_spinbox_scroll(widget) | |
| # Check if it's a Combobox (ttk and ttkbootstrap versions) | |
| elif isinstance(widget, (ttk.Combobox, tb.Combobox)): | |
| self._disable_spinbox_scroll(widget) | |
| # Recursively check frames and other containers | |
| elif hasattr(widget, 'winfo_children'): | |
| self._disable_all_spinbox_scrolling(widget) | |
| def _create_font_size_controls(self, parent_frame): | |
| """Create improved font size controls with presets""" | |
| # Font size frame | |
| font_frame = tk.Frame(parent_frame) | |
| font_frame.pack(fill=tk.X, pady=5) | |
| tk.Label(font_frame, text="Font Size:", width=20, anchor='w').pack(side=tk.LEFT) | |
| # Font size mode selection | |
| mode_frame = tk.Frame(font_frame) | |
| mode_frame.pack(side=tk.LEFT, padx=10) | |
| # Radio buttons for mode | |
| self.font_size_mode_var = tk.StringVar(value='auto') | |
| modes = [ | |
| ("Auto", "auto", "Automatically fit text to bubble size"), | |
| ("Fixed", "fixed", "Use a specific font size"), | |
| ("Scale", "scale", "Scale auto size by percentage") | |
| ] | |
| for text, value, tooltip in modes: | |
| rb = ttk.Radiobutton( | |
| mode_frame, | |
| text=text, | |
| variable=self.font_size_mode_var, | |
| value=value, | |
| command=self._on_font_mode_change | |
| ) | |
| rb.pack(side=tk.LEFT, padx=5) | |
| # Add tooltip | |
| self._create_tooltip(rb, tooltip) | |
| # Controls frame (changes based on mode) | |
| self.font_controls_frame = tk.Frame(parent_frame) | |
| self.font_controls_frame.pack(fill=tk.X, pady=5, padx=(20, 0)) | |
| # Fixed size controls | |
| self.fixed_size_frame = tk.Frame(self.font_controls_frame) | |
| tk.Label(self.fixed_size_frame, text="Size:").pack(side=tk.LEFT) | |
| self.fixed_font_size_var = tk.IntVar(value=16) | |
| fixed_spin = tb.Spinbox( | |
| self.fixed_size_frame, | |
| from_=8, | |
| to=72, | |
| textvariable=self.fixed_font_size_var, | |
| width=10, | |
| command=self._save_rendering_settings | |
| ) | |
| fixed_spin.pack(side=tk.LEFT, padx=5) | |
| # Quick presets for fixed size | |
| tk.Label(self.fixed_size_frame, text="Presets:").pack(side=tk.LEFT, padx=(10, 5)) | |
| presets = [ | |
| ("Small", 12), | |
| ("Medium", 16), | |
| ("Large", 20), | |
| ("XL", 24) | |
| ] | |
| for text, size in presets: | |
| ttk.Button( | |
| self.fixed_size_frame, | |
| text=text, | |
| command=lambda s=size: self._set_fixed_size(s), | |
| width=6 | |
| ).pack(side=tk.LEFT, padx=2) | |
| # Scale controls | |
| self.scale_frame = tk.Frame(self.font_controls_frame) | |
| tk.Label(self.scale_frame, text="Scale:").pack(side=tk.LEFT) | |
| self.font_scale_var = tk.DoubleVar(value=1.0) | |
| scale_slider = tk.Scale( | |
| self.scale_frame, | |
| from_=0.5, | |
| to=2.0, | |
| resolution=0.01, | |
| orient=tk.HORIZONTAL, | |
| variable=self.font_scale_var, | |
| length=200, | |
| command=lambda v: self._update_scale_label() | |
| ) | |
| scale_slider.pack(side=tk.LEFT, padx=5) | |
| self.scale_label = tk.Label(self.scale_frame, text="100%", width=5) | |
| self.scale_label.pack(side=tk.LEFT) | |
| # Quick scale presets | |
| tk.Label(self.scale_frame, text="Quick:").pack(side=tk.LEFT, padx=(10, 5)) | |
| scale_presets = [ | |
| ("75%", 0.75), | |
| ("100%", 1.0), | |
| ("125%", 1.25), | |
| ("150%", 1.5) | |
| ] | |
| for text, scale in scale_presets: | |
| ttk.Button( | |
| self.scale_frame, | |
| text=text, | |
| command=lambda s=scale: self._set_scale(s), | |
| width=5 | |
| ).pack(side=tk.LEFT, padx=2) | |
| # Auto size settings | |
| self.auto_frame = tk.Frame(self.font_controls_frame) | |
| # Min/Max size constraints for auto mode | |
| constraints_frame = tk.Frame(self.auto_frame) | |
| constraints_frame.pack(fill=tk.X) | |
| tk.Label(constraints_frame, text="Size Range:").pack(side=tk.LEFT) | |
| tk.Label(constraints_frame, text="Min:").pack(side=tk.LEFT, padx=(10, 2)) | |
| self.min_font_size_var = tk.IntVar(value=10) | |
| tb.Spinbox( | |
| constraints_frame, | |
| from_=6, | |
| to=20, | |
| textvariable=self.min_font_size_var, | |
| width=8, | |
| command=self._save_rendering_settings | |
| ).pack(side=tk.LEFT) | |
| tk.Label(constraints_frame, text="Max:").pack(side=tk.LEFT, padx=(10, 2)) | |
| self.max_font_size_var = tk.IntVar(value=28) | |
| tb.Spinbox( | |
| constraints_frame, | |
| from_=16, | |
| to=48, | |
| textvariable=self.max_font_size_var, | |
| width=8, | |
| command=self._save_rendering_settings | |
| ).pack(side=tk.LEFT) | |
| # Auto fit quality | |
| quality_frame = tk.Frame(self.auto_frame) | |
| quality_frame.pack(fill=tk.X, pady=(5, 0)) | |
| tk.Label(quality_frame, text="Fit Style:").pack(side=tk.LEFT) | |
| self.auto_fit_style_var = tk.StringVar(value='balanced') | |
| fit_styles = [ | |
| ("Compact", "compact", "Fit more text, smaller size"), | |
| ("Balanced", "balanced", "Balance readability and fit"), | |
| ("Readable", "readable", "Prefer larger, more readable text") | |
| ] | |
| for text, value, tooltip in fit_styles: | |
| rb = ttk.Radiobutton( | |
| quality_frame, | |
| text=text, | |
| variable=self.auto_fit_style_var, | |
| value=value, | |
| command=self._save_rendering_settings | |
| ) | |
| rb.pack(side=tk.LEFT, padx=5) | |
| self._create_tooltip(rb, tooltip) | |
| # Initialize the correct frame | |
| self._on_font_mode_change() | |
| def _on_font_mode_change(self): | |
| """Show/hide appropriate font controls based on mode""" | |
| # Hide all frames | |
| for frame in [self.fixed_size_frame, self.scale_frame, self.auto_frame]: | |
| frame.pack_forget() | |
| # Show the appropriate frame | |
| mode = self.font_size_mode_var.get() | |
| if mode == 'fixed': | |
| self.fixed_size_frame.pack(fill=tk.X) | |
| elif mode == 'scale': | |
| self.scale_frame.pack(fill=tk.X) | |
| else: # auto | |
| self.auto_frame.pack(fill=tk.X) | |
| self._save_rendering_settings() | |
| def _set_fixed_size(self, size): | |
| """Set fixed font size from preset""" | |
| self.fixed_font_size_var.set(size) | |
| self._save_rendering_settings() | |
| def _set_scale(self, scale): | |
| """Set font scale from preset""" | |
| self.font_scale_var.set(scale) | |
| self._update_scale_label() | |
| self._save_rendering_settings() | |
| def _update_scale_label(self): | |
| """Update the scale percentage label""" | |
| scale = self.font_scale_var.get() | |
| self.scale_label.config(text=f"{int(scale * 100)}%") | |
| self._save_rendering_settings() | |
| def _create_tooltip(self, widget, text): | |
| """Create a tooltip for a widget""" | |
| def on_enter(event): | |
| tooltip = tk.Toplevel() | |
| tooltip.wm_overrideredirect(True) | |
| tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}") | |
| label = tk.Label( | |
| tooltip, | |
| text=text, | |
| background="#ffffe0", | |
| relief=tk.SOLID, | |
| borderwidth=1, | |
| font=('Arial', 9) | |
| ) | |
| label.pack() | |
| widget.tooltip = tooltip | |
| def on_leave(event): | |
| if hasattr(widget, 'tooltip'): | |
| widget.tooltip.destroy() | |
| del widget.tooltip | |
| widget.bind('<Enter>', on_enter) | |
| widget.bind('<Leave>', on_leave) | |
| def _merge_settings(self, existing: Dict) -> Dict: | |
| """Merge existing settings with defaults""" | |
| result = self.default_settings.copy() | |
| def deep_merge(base: Dict, update: Dict) -> Dict: | |
| for key, value in update.items(): | |
| if key in base and isinstance(base[key], dict) and isinstance(value, dict): | |
| base[key] = deep_merge(base[key], value) | |
| else: | |
| base[key] = value | |
| return base | |
| return deep_merge(result, existing) | |
| def show_dialog(self): | |
| """Display the settings dialog using WindowManager""" | |
| # Set initialization flag to prevent auto-saves during setup | |
| self._initializing = True | |
| # Use WindowManager to create scrollable dialog | |
| if self.main_gui.wm._force_safe_ratios: | |
| max_width_ratio = 0.5 | |
| max_height_ratio = 0.85 | |
| else: | |
| max_width_ratio = 0.5 | |
| max_height_ratio = 1.05 | |
| self.dialog, scrollable_frame, canvas = self.main_gui.wm.setup_scrollable( | |
| self.parent, | |
| "Manga Translation Settings", | |
| width=None, | |
| height=None, | |
| max_width_ratio=max_width_ratio, | |
| max_height_ratio=max_height_ratio | |
| ) | |
| # Store canvas reference for potential cleanup | |
| self.canvas = canvas | |
| # Create main content frame (that will scroll) | |
| content_frame = tk.Frame(scrollable_frame) | |
| content_frame.pack(fill='both', expand=True, padx=10, pady=10) | |
| # Create notebook for tabs inside the content frame | |
| notebook = ttk.Notebook(content_frame) | |
| notebook.pack(fill='both', expand=True) | |
| # Create all tabs | |
| self._create_preprocessing_tab(notebook) | |
| self._create_ocr_tab(notebook) | |
| self._create_inpainting_tab(notebook) | |
| self._create_advanced_tab(notebook) | |
| # NOTE: Font Sizing tab removed; controls are now in Manga Integration UI | |
| # Cloud API tab | |
| self.cloud_tab = ttk.Frame(notebook) | |
| notebook.add(self.cloud_tab, text="Cloud API") | |
| self._create_cloud_api_tab(self.cloud_tab) | |
| # DISABLE SCROLL WHEEL ON ALL SPINBOXES | |
| self.dialog.after(10, lambda: self._disable_all_spinbox_scrolling(self.dialog)) | |
| # Clear initialization flag after setup is complete | |
| self._initializing = False | |
| # Create fixed button frame at bottom of dialog (not inside scrollable content) | |
| button_frame = tk.Frame(self.dialog) | |
| button_frame.pack(fill='x', padx=10, pady=(5, 10), side='bottom') | |
| # Buttons | |
| tb.Button( | |
| button_frame, | |
| text="Save", | |
| command=self._save_settings, | |
| bootstyle="success" | |
| ).pack(side='right', padx=(5, 0)) | |
| tb.Button( | |
| button_frame, | |
| text="Cancel", | |
| command=self._cancel, | |
| bootstyle="secondary" | |
| ).pack(side='right', padx=(5, 0)) | |
| tb.Button( | |
| button_frame, | |
| text="Reset to Defaults", | |
| command=self._reset_defaults, | |
| bootstyle="warning" | |
| ).pack(side='left') | |
| # Initialize preprocessing state | |
| self._toggle_preprocessing() | |
| # Handle window close protocol | |
| self.dialog.protocol("WM_DELETE_WINDOW", self._cancel) | |
| def _create_preprocessing_tab(self, notebook): | |
| """Create preprocessing settings tab with all options""" | |
| frame = ttk.Frame(notebook) | |
| notebook.add(frame, text="Preprocessing") | |
| # Main scrollable content | |
| content_frame = tk.Frame(frame) | |
| content_frame.pack(fill='both', expand=True, padx=5, pady=5) | |
| # Enable preprocessing with command | |
| enable_frame = tk.Frame(content_frame) | |
| enable_frame.pack(fill='x', padx=20, pady=(20, 10)) | |
| self.preprocess_enabled = tk.BooleanVar(value=self.settings['preprocessing']['enabled']) | |
| tb.Checkbutton( | |
| enable_frame, | |
| text="Enable Image Preprocessing", | |
| variable=self.preprocess_enabled, | |
| bootstyle="round-toggle", | |
| command=self._toggle_preprocessing | |
| ).pack(anchor='w') | |
| # Store all preprocessing controls for enable/disable | |
| self.preprocessing_controls = [] | |
| # Auto quality detection | |
| self.auto_detect = tk.BooleanVar(value=self.settings['preprocessing']['auto_detect_quality']) | |
| auto_cb = tb.Checkbutton( | |
| enable_frame, | |
| text="Auto-detect image quality issues", | |
| variable=self.auto_detect, | |
| bootstyle="round-toggle" | |
| ) | |
| auto_cb.pack(anchor='w', pady=(10, 0)) | |
| self.preprocessing_controls.append(auto_cb) | |
| # Quality thresholds section | |
| threshold_frame = tk.LabelFrame(content_frame, text="Image Enhancement", padx=15, pady=10) | |
| threshold_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.preprocessing_controls.append(threshold_frame) | |
| # Contrast threshold | |
| contrast_frame = tk.Frame(threshold_frame) | |
| contrast_frame.pack(fill='x', pady=5) | |
| contrast_label = tk.Label(contrast_frame, text="Contrast Adjustment:", width=20, anchor='w') | |
| contrast_label.pack(side='left') | |
| self.preprocessing_controls.append(contrast_label) | |
| self.contrast_threshold = tk.DoubleVar(value=self.settings['preprocessing']['contrast_threshold']) | |
| contrast_scale = tk.Scale( | |
| contrast_frame, | |
| from_=0.0, to=1.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.contrast_threshold, | |
| length=250 | |
| ) | |
| contrast_scale.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(contrast_scale) | |
| contrast_value = tk.Label(contrast_frame, textvariable=self.contrast_threshold, width=5) | |
| contrast_value.pack(side='left') | |
| self.preprocessing_controls.append(contrast_value) | |
| # Sharpness threshold | |
| sharpness_frame = tk.Frame(threshold_frame) | |
| sharpness_frame.pack(fill='x', pady=5) | |
| sharpness_label = tk.Label(sharpness_frame, text="Sharpness Enhancement:", width=20, anchor='w') | |
| sharpness_label.pack(side='left') | |
| self.preprocessing_controls.append(sharpness_label) | |
| self.sharpness_threshold = tk.DoubleVar(value=self.settings['preprocessing']['sharpness_threshold']) | |
| sharpness_scale = tk.Scale( | |
| sharpness_frame, | |
| from_=0.0, to=1.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.sharpness_threshold, | |
| length=250 | |
| ) | |
| sharpness_scale.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(sharpness_scale) | |
| sharpness_value = tk.Label(sharpness_frame, textvariable=self.sharpness_threshold, width=5) | |
| sharpness_value.pack(side='left') | |
| self.preprocessing_controls.append(sharpness_value) | |
| # Enhancement strength | |
| enhance_frame = tk.Frame(threshold_frame) | |
| enhance_frame.pack(fill='x', pady=5) | |
| enhance_label = tk.Label(enhance_frame, text="Overall Enhancement:", width=20, anchor='w') | |
| enhance_label.pack(side='left') | |
| self.preprocessing_controls.append(enhance_label) | |
| self.enhancement_strength = tk.DoubleVar(value=self.settings['preprocessing']['enhancement_strength']) | |
| enhance_scale = tk.Scale( | |
| enhance_frame, | |
| from_=0.0, to=3.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.enhancement_strength, | |
| length=250 | |
| ) | |
| enhance_scale.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(enhance_scale) | |
| enhance_value = tk.Label(enhance_frame, textvariable=self.enhancement_strength, width=5) | |
| enhance_value.pack(side='left') | |
| self.preprocessing_controls.append(enhance_value) | |
| # Noise reduction section | |
| noise_frame = tk.LabelFrame(content_frame, text="Noise Reduction", padx=15, pady=10) | |
| noise_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.preprocessing_controls.append(noise_frame) | |
| # Noise threshold | |
| noise_threshold_frame = tk.Frame(noise_frame) | |
| noise_threshold_frame.pack(fill='x', pady=5) | |
| noise_label = tk.Label(noise_threshold_frame, text="Noise Threshold:", width=20, anchor='w') | |
| noise_label.pack(side='left') | |
| self.preprocessing_controls.append(noise_label) | |
| self.noise_threshold = tk.IntVar(value=self.settings['preprocessing']['noise_threshold']) | |
| noise_scale = tk.Scale( | |
| noise_threshold_frame, | |
| from_=0, to=50, | |
| orient='horizontal', | |
| variable=self.noise_threshold, | |
| length=250 | |
| ) | |
| noise_scale.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(noise_scale) | |
| noise_value = tk.Label(noise_threshold_frame, textvariable=self.noise_threshold, width=5) | |
| noise_value.pack(side='left') | |
| self.preprocessing_controls.append(noise_value) | |
| # Denoise strength | |
| denoise_frame = tk.Frame(noise_frame) | |
| denoise_frame.pack(fill='x', pady=5) | |
| denoise_label = tk.Label(denoise_frame, text="Denoise Strength:", width=20, anchor='w') | |
| denoise_label.pack(side='left') | |
| self.preprocessing_controls.append(denoise_label) | |
| self.denoise_strength = tk.IntVar(value=self.settings['preprocessing']['denoise_strength']) | |
| denoise_scale = tk.Scale( | |
| denoise_frame, | |
| from_=0, to=30, | |
| orient='horizontal', | |
| variable=self.denoise_strength, | |
| length=250 | |
| ) | |
| denoise_scale.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(denoise_scale) | |
| denoise_value = tk.Label(denoise_frame, textvariable=self.denoise_strength, width=5) | |
| denoise_value.pack(side='left') | |
| self.preprocessing_controls.append(denoise_value) | |
| # Size limits section | |
| size_frame = tk.LabelFrame(content_frame, text="Image Size Limits", padx=15, pady=10) | |
| size_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.preprocessing_controls.append(size_frame) | |
| # Max dimension | |
| dimension_frame = tk.Frame(size_frame) | |
| dimension_frame.pack(fill='x', pady=5) | |
| dimension_label = tk.Label(dimension_frame, text="Max Dimension:", width=20, anchor='w') | |
| dimension_label.pack(side='left') | |
| self.preprocessing_controls.append(dimension_label) | |
| self.max_dimension = tk.IntVar(value=self.settings['preprocessing']['max_image_dimension']) | |
| self.dimension_spinbox = tb.Spinbox( | |
| dimension_frame, | |
| from_=500, | |
| to=4000, | |
| textvariable=self.max_dimension, | |
| increment=100, | |
| width=10 | |
| ) | |
| self.dimension_spinbox.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(self.dimension_spinbox) | |
| tk.Label(dimension_frame, text="pixels").pack(side='left') | |
| # Max pixels | |
| pixels_frame = tk.Frame(size_frame) | |
| pixels_frame.pack(fill='x', pady=5) | |
| pixels_label = tk.Label(pixels_frame, text="Max Total Pixels:", width=20, anchor='w') | |
| pixels_label.pack(side='left') | |
| self.preprocessing_controls.append(pixels_label) | |
| self.max_pixels = tk.IntVar(value=self.settings['preprocessing']['max_image_pixels']) | |
| self.pixels_spinbox = tb.Spinbox( | |
| pixels_frame, | |
| from_=1000000, | |
| to=10000000, | |
| textvariable=self.max_pixels, | |
| increment=100000, | |
| width=10 | |
| ) | |
| self.pixels_spinbox.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(self.pixels_spinbox) | |
| tk.Label(pixels_frame, text="pixels").pack(side='left') | |
| # Compression section | |
| compression_frame = tk.LabelFrame(content_frame, text="Image Compression (applies to OCR uploads)", padx=15, pady=10) | |
| compression_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Do NOT add compression controls to preprocessing_controls; keep independent of preprocessing toggle | |
| # Enable compression toggle | |
| self.compression_enabled_var = tk.BooleanVar(value=self.settings.get('compression', {}).get('enabled', False)) | |
| compression_toggle = tb.Checkbutton( | |
| compression_frame, | |
| text="Enable compression for OCR uploads", | |
| variable=self.compression_enabled_var, | |
| bootstyle="round-toggle", | |
| ) | |
| compression_toggle.pack(anchor='w') | |
| self.compression_toggle = compression_toggle | |
| # Hook toggle to enable/disable compression fields | |
| def _toggle_compression_enabled(): | |
| enabled = bool(self.compression_enabled_var.get()) | |
| state = 'normal' if enabled else 'disabled' | |
| try: | |
| self.compression_format_combo.config(state='readonly' if enabled else 'disabled') | |
| except Exception: | |
| pass | |
| for w in [getattr(self, 'jpeg_quality_spin', None), getattr(self, 'png_level_spin', None), getattr(self, 'webp_quality_spin', None)]: | |
| try: | |
| if w is not None: | |
| w.config(state=state) | |
| except Exception: | |
| pass | |
| compression_toggle.config(command=_toggle_compression_enabled) | |
| # Format selection | |
| format_row = tk.Frame(compression_frame) | |
| format_row.pack(fill='x', pady=5) | |
| tk.Label(format_row, text="Format:", width=20, anchor='w').pack(side='left') | |
| self.compression_format_var = tk.StringVar(value=self.settings.get('compression', {}).get('format', 'jpeg')) | |
| self.compression_format_combo = ttk.Combobox( | |
| format_row, | |
| textvariable=self.compression_format_var, | |
| values=['jpeg', 'png', 'webp'], | |
| state='readonly', | |
| width=10 | |
| ) | |
| self.compression_format_combo.pack(side='left', padx=10) | |
| # JPEG quality | |
| self.jpeg_row = tk.Frame(compression_frame) | |
| self.jpeg_row.pack(fill='x', pady=5) | |
| tk.Label(self.jpeg_row, text="JPEG Quality:", width=20, anchor='w').pack(side='left') | |
| self.jpeg_quality_var = tk.IntVar(value=self.settings.get('compression', {}).get('jpeg_quality', 85)) | |
| self.jpeg_quality_spin = tb.Spinbox( | |
| self.jpeg_row, | |
| from_=1, | |
| to=95, | |
| textvariable=self.jpeg_quality_var, | |
| width=10 | |
| ) | |
| self.jpeg_quality_spin.pack(side='left', padx=10) | |
| tk.Label(self.jpeg_row, text="(higher = better quality, larger size)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # PNG compression level | |
| self.png_row = tk.Frame(compression_frame) | |
| self.png_row.pack(fill='x', pady=5) | |
| tk.Label(self.png_row, text="PNG Compression:", width=20, anchor='w').pack(side='left') | |
| self.png_level_var = tk.IntVar(value=self.settings.get('compression', {}).get('png_compress_level', 6)) | |
| self.png_level_spin = tb.Spinbox( | |
| self.png_row, | |
| from_=0, | |
| to=9, | |
| textvariable=self.png_level_var, | |
| width=10 | |
| ) | |
| self.png_level_spin.pack(side='left', padx=10) | |
| tk.Label(self.png_row, text="(0 = fastest, 9 = smallest)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # WEBP quality | |
| self.webp_row = tk.Frame(compression_frame) | |
| self.webp_row.pack(fill='x', pady=5) | |
| tk.Label(self.webp_row, text="WEBP Quality:", width=20, anchor='w').pack(side='left') | |
| self.webp_quality_var = tk.IntVar(value=self.settings.get('compression', {}).get('webp_quality', 85)) | |
| self.webp_quality_spin = tb.Spinbox( | |
| self.webp_row, | |
| from_=1, | |
| to=100, | |
| textvariable=self.webp_quality_var, | |
| width=10 | |
| ) | |
| self.webp_quality_spin.pack(side='left', padx=10) | |
| tk.Label(self.webp_row, text="(higher = better quality, larger size)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Hook to toggle visibility based on format | |
| self.compression_format_combo.bind('<<ComboboxSelected>>', lambda e: self._toggle_compression_format()) | |
| self._toggle_compression_format() | |
| # Apply enabled/disabled state for compression fields initially | |
| try: | |
| _toggle_compression_enabled() | |
| except Exception: | |
| pass | |
| # Chunk settings for large images (moved above compression) | |
| chunk_frame = tk.LabelFrame(content_frame, text="Large Image Processing", padx=15, pady=10) | |
| chunk_frame.pack(fill='x', padx=20, pady=(10, 0), before=compression_frame) | |
| self.preprocessing_controls.append(chunk_frame) | |
| # HD Strategy (Inpainting acceleration) | |
| hd_frame = tk.LabelFrame(chunk_frame, text="Inpainting HD Strategy", padx=10, pady=8) | |
| hd_frame.pack(fill='x', pady=(5, 10)) | |
| # Strategy selector | |
| strat_row = tk.Frame(hd_frame) | |
| strat_row.pack(fill='x', pady=4) | |
| tk.Label(strat_row, text="Strategy:", width=20, anchor='w').pack(side='left') | |
| self.hd_strategy_var = tk.StringVar(value=self.settings.get('advanced', {}).get('hd_strategy', 'resize')) | |
| self.hd_strategy_combo = ttk.Combobox( | |
| strat_row, | |
| textvariable=self.hd_strategy_var, | |
| values=['original', 'resize', 'crop'], | |
| state='readonly', | |
| width=12 | |
| ) | |
| self.hd_strategy_combo.pack(side='left', padx=10) | |
| tk.Label(strat_row, text="(original = legacy full-image; resize/crop = faster)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Resize limit row | |
| self.hd_resize_row = tk.Frame(hd_frame) | |
| self.hd_resize_row.pack(fill='x', pady=4) | |
| tk.Label(self.hd_resize_row, text="Resize limit (long edge):", width=20, anchor='w').pack(side='left') | |
| self.hd_resize_limit_var = tk.IntVar(value=int(self.settings.get('advanced', {}).get('hd_strategy_resize_limit', 1536))) | |
| self.hd_resize_limit_spin = tb.Spinbox( | |
| self.hd_resize_row, | |
| from_=512, | |
| to=4096, | |
| textvariable=self.hd_resize_limit_var, | |
| increment=64, | |
| width=10 | |
| ) | |
| self.hd_resize_limit_spin.pack(side='left', padx=10) | |
| tk.Label(self.hd_resize_row, text="px").pack(side='left') | |
| # Crop params rows | |
| self.hd_crop_margin_row = tk.Frame(hd_frame) | |
| self.hd_crop_margin_row.pack(fill='x', pady=4) | |
| tk.Label(self.hd_crop_margin_row, text="Crop margin:", width=20, anchor='w').pack(side='left') | |
| self.hd_crop_margin_var = tk.IntVar(value=int(self.settings.get('advanced', {}).get('hd_strategy_crop_margin', 16))) | |
| self.hd_crop_margin_spin = tb.Spinbox( | |
| self.hd_crop_margin_row, | |
| from_=0, | |
| to=256, | |
| textvariable=self.hd_crop_margin_var, | |
| increment=2, | |
| width=10 | |
| ) | |
| self.hd_crop_margin_spin.pack(side='left', padx=10) | |
| tk.Label(self.hd_crop_margin_row, text="px").pack(side='left') | |
| self.hd_crop_trigger_row = tk.Frame(hd_frame) | |
| self.hd_crop_trigger_row.pack(fill='x', pady=4) | |
| tk.Label(self.hd_crop_trigger_row, text="Crop trigger size:", width=20, anchor='w').pack(side='left') | |
| self.hd_crop_trigger_var = tk.IntVar(value=int(self.settings.get('advanced', {}).get('hd_strategy_crop_trigger_size', 1024))) | |
| self.hd_crop_trigger_spin = tb.Spinbox( | |
| self.hd_crop_trigger_row, | |
| from_=256, | |
| to=4096, | |
| textvariable=self.hd_crop_trigger_var, | |
| increment=64, | |
| width=10 | |
| ) | |
| self.hd_crop_trigger_spin.pack(side='left', padx=10) | |
| tk.Label(self.hd_crop_trigger_row, text="px (apply crop only if long edge > trigger)").pack(side='left') | |
| # Toggle rows based on current selection | |
| def _on_hd_strategy_change(*_): | |
| strat = self.hd_strategy_var.get() | |
| try: | |
| if strat == 'resize': | |
| self.hd_resize_row.pack(fill='x', pady=4) | |
| self.hd_crop_margin_row.pack_forget() | |
| self.hd_crop_trigger_row.pack_forget() | |
| elif strat == 'crop': | |
| self.hd_resize_row.pack_forget() | |
| self.hd_crop_margin_row.pack(fill='x', pady=4) | |
| self.hd_crop_trigger_row.pack(fill='x', pady=4) | |
| else: # original | |
| self.hd_resize_row.pack_forget() | |
| self.hd_crop_margin_row.pack_forget() | |
| self.hd_crop_trigger_row.pack_forget() | |
| except Exception: | |
| pass | |
| self.hd_strategy_combo.bind('<<ComboboxSelected>>', _on_hd_strategy_change) | |
| _on_hd_strategy_change() | |
| # Clarifying note about precedence with tiling | |
| try: | |
| tk.Label( | |
| hd_frame, | |
| text="Note: HD Strategy (resize/crop) takes precedence over Inpainting Tiling when it triggers.\nSet strategy to 'original' if you want tiling to control large-image behavior.", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ).pack(anchor='w', pady=(2, 2)) | |
| except Exception: | |
| pass | |
| # Chunk height | |
| self.chunk_frame = chunk_frame | |
| chunk_height_frame = tk.Frame(chunk_frame) | |
| chunk_height_frame.pack(fill='x', pady=5) | |
| self.chunk_height_label = tk.Label(chunk_height_frame, text="Chunk Height:", width=20, anchor='w') | |
| self.chunk_height_label.pack(side='left') | |
| self.preprocessing_controls.append(self.chunk_height_label) | |
| self.chunk_height = tk.IntVar(value=self.settings['preprocessing']['chunk_height']) | |
| self.chunk_height_spinbox = tb.Spinbox( | |
| chunk_height_frame, | |
| from_=500, | |
| to=2000, | |
| textvariable=self.chunk_height, | |
| increment=100, | |
| width=10 | |
| ) | |
| self.chunk_height_spinbox.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(self.chunk_height_spinbox) | |
| self.chunk_height_unit_label = tk.Label(chunk_height_frame, text="pixels") | |
| self.chunk_height_unit_label.pack(side='left') | |
| self.preprocessing_controls.append(self.chunk_height_unit_label) | |
| # Chunk overlap | |
| chunk_overlap_frame = tk.Frame(chunk_frame) | |
| chunk_overlap_frame.pack(fill='x', pady=5) | |
| self.chunk_overlap_label = tk.Label(chunk_overlap_frame, text="Chunk Overlap:", width=20, anchor='w') | |
| self.chunk_overlap_label.pack(side='left') | |
| self.preprocessing_controls.append(self.chunk_overlap_label) | |
| self.chunk_overlap = tk.IntVar(value=self.settings['preprocessing']['chunk_overlap']) | |
| self.chunk_overlap_spinbox = tb.Spinbox( | |
| chunk_overlap_frame, | |
| from_=0, | |
| to=200, | |
| textvariable=self.chunk_overlap, | |
| increment=10, | |
| width=10 | |
| ) | |
| self.chunk_overlap_spinbox.pack(side='left', padx=10) | |
| self.preprocessing_controls.append(self.chunk_overlap_spinbox) | |
| self.chunk_overlap_unit_label = tk.Label(chunk_overlap_frame, text="pixels") | |
| self.chunk_overlap_unit_label.pack(side='left') | |
| self.preprocessing_controls.append(self.chunk_overlap_unit_label) | |
| # Inpainting Tiling section (add after the "Large Image Processing" section) | |
| self.tiling_frame = tk.LabelFrame(content_frame, text="Inpainting Tiling", padx=15, pady=10) | |
| self.tiling_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| tiling_frame = self.tiling_frame | |
| self.preprocessing_controls.append(self.tiling_frame) | |
| # Enable tiling | |
| # Prefer values from legacy 'tiling' section if present, otherwise use 'preprocessing' | |
| tiling_enabled_value = self.settings['preprocessing'].get('inpaint_tiling_enabled', False) | |
| if 'tiling' in self.settings and isinstance(self.settings['tiling'], dict) and 'enabled' in self.settings['tiling']: | |
| tiling_enabled_value = self.settings['tiling']['enabled'] | |
| self.inpaint_tiling_enabled = tk.BooleanVar(value=tiling_enabled_value) | |
| tiling_enable_cb = tb.Checkbutton( | |
| tiling_frame, | |
| text="Enable automatic tiling for inpainting (processes large images in tiles)", | |
| variable=self.inpaint_tiling_enabled, | |
| command=lambda: self._toggle_tiling_controls(), | |
| bootstyle="round-toggle" | |
| ) | |
| tiling_enable_cb.pack(anchor='w', pady=(5, 10)) | |
| # Tile size | |
| tile_size_frame = tk.Frame(tiling_frame) | |
| tile_size_frame.pack(fill='x', pady=5) | |
| tile_size_label = tk.Label(tile_size_frame, text="Tile Size:", width=20, anchor='w') | |
| tile_size_label.pack(side='left') | |
| tile_size_value = self.settings['preprocessing'].get('inpaint_tile_size', 512) | |
| if 'tiling' in self.settings and isinstance(self.settings['tiling'], dict) and 'tile_size' in self.settings['tiling']: | |
| tile_size_value = self.settings['tiling']['tile_size'] | |
| self.inpaint_tile_size = tk.IntVar(value=tile_size_value) | |
| self.tile_size_spinbox = tb.Spinbox( | |
| tile_size_frame, | |
| from_=256, | |
| to=2048, | |
| textvariable=self.inpaint_tile_size, | |
| increment=128, | |
| width=10 | |
| ) | |
| self.tile_size_spinbox.pack(side='left', padx=10) | |
| tk.Label(tile_size_frame, text="pixels").pack(side='left') | |
| # Initial tiling fields state | |
| try: | |
| self._toggle_tiling_controls() | |
| except Exception: | |
| pass | |
| # Tile overlap | |
| tile_overlap_frame = tk.Frame(tiling_frame) | |
| tile_overlap_frame.pack(fill='x', pady=5) | |
| tile_overlap_label = tk.Label(tile_overlap_frame, text="Tile Overlap:", width=20, anchor='w') | |
| tile_overlap_label.pack(side='left') | |
| tile_overlap_value = self.settings['preprocessing'].get('inpaint_tile_overlap', 64) | |
| if 'tiling' in self.settings and isinstance(self.settings['tiling'], dict) and 'tile_overlap' in self.settings['tiling']: | |
| tile_overlap_value = self.settings['tiling']['tile_overlap'] | |
| self.inpaint_tile_overlap = tk.IntVar(value=tile_overlap_value) | |
| self.tile_overlap_spinbox = tb.Spinbox( | |
| tile_overlap_frame, | |
| from_=0, | |
| to=256, | |
| textvariable=self.inpaint_tile_overlap, | |
| increment=16, | |
| width=10 | |
| ) | |
| self.tile_overlap_spinbox.pack(side='left', padx=10) | |
| tk.Label(tile_overlap_frame, text="pixels").pack(side='left') | |
| def _create_inpainting_tab(self, notebook): | |
| """Create inpainting settings tab with comprehensive per-text-type dilation controls""" | |
| frame = ttk.Frame(notebook) | |
| notebook.add(frame, text="Inpainting") | |
| content_frame = tk.Frame(frame) | |
| content_frame.pack(fill='both', expand=True, padx=5, pady=5) | |
| # General Mask Settings (applies to all inpainting methods) | |
| mask_frame = tk.LabelFrame(content_frame, text="Mask Settings", padx=15, pady=10) | |
| mask_frame.pack(fill='x', padx=20, pady=(20, 10)) | |
| # Auto toggle (affects both mask dilation and iterations) | |
| auto_global_frame = tk.Frame(mask_frame) | |
| auto_global_frame.pack(fill='x', pady=(0, 5)) | |
| if not hasattr(self, 'auto_iterations_var'): | |
| self.auto_iterations_var = tk.BooleanVar(value=self.settings.get('auto_iterations', True)) | |
| tb.Checkbutton( | |
| auto_global_frame, | |
| text="Auto (affects mask dilation and iterations)", | |
| variable=self.auto_iterations_var, | |
| command=self._toggle_iteration_controls, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # Mask dilation size | |
| dilation_frame = tk.Frame(mask_frame) | |
| dilation_frame.pack(fill='x', pady=5) | |
| tk.Label(dilation_frame, text="Mask Dilation:", width=15, anchor='w').pack(side='left') | |
| self.mask_dilation_var = tk.IntVar(value=self.settings.get('mask_dilation', 15)) | |
| self.mask_dilation_spinbox = tb.Spinbox( | |
| dilation_frame, | |
| from_=0, | |
| to=50, | |
| textvariable=self.mask_dilation_var, | |
| increment=5, | |
| width=10 | |
| ) | |
| self.mask_dilation_spinbox.pack(side='left', padx=10) | |
| tk.Label(dilation_frame, text="pixels (expand mask beyond text)").pack(side='left') | |
| # Per-Text-Type Iterations - EXPANDED SECTION | |
| iterations_label_frame = tk.LabelFrame(mask_frame, text="Dilation Iterations Control", padx=10, pady=5) | |
| iterations_label_frame.pack(fill='x', pady=(10, 5)) | |
| # All Iterations Master Control (NEW) | |
| all_iter_frame = tk.Frame(iterations_label_frame) | |
| all_iter_frame.pack(fill='x', pady=5) | |
| # Auto-iterations toggle (secondary control reflects the same setting) | |
| if not hasattr(self, 'auto_iterations_var'): | |
| self.auto_iterations_var = tk.BooleanVar(value=self.settings.get('auto_iterations', True)) | |
| auto_iter_checkbox = tb.Checkbutton( | |
| all_iter_frame, | |
| text="Auto (set by image: B&W vs Color)", | |
| variable=self.auto_iterations_var, | |
| command=self._toggle_iteration_controls, | |
| bootstyle="round-toggle" | |
| ) | |
| auto_iter_checkbox.pack(side='left', padx=(0, 10)) | |
| # Checkbox to enable/disable uniform iterations | |
| self.use_all_iterations_var = tk.BooleanVar(value=self.settings.get('use_all_iterations', True)) | |
| all_iter_checkbox = tb.Checkbutton( | |
| all_iter_frame, | |
| text="Use Same For All:", | |
| variable=self.use_all_iterations_var, | |
| command=self._toggle_iteration_controls, | |
| bootstyle="round-toggle" | |
| ) | |
| all_iter_checkbox.pack(side='left', padx=(0, 10)) | |
| self.use_all_iterations_checkbox = all_iter_checkbox | |
| self.all_iterations_var = tk.IntVar(value=self.settings.get('all_iterations', 2)) | |
| self.all_iterations_spinbox = tb.Spinbox( | |
| all_iter_frame, | |
| from_=0, | |
| to=5, | |
| textvariable=self.all_iterations_var, | |
| width=10, | |
| state='disabled' if not self.use_all_iterations_var.get() else 'normal' | |
| ) | |
| self.all_iterations_spinbox.pack(side='left', padx=10) | |
| tk.Label(all_iter_frame, text="iterations (applies to all text types)").pack(side='left') | |
| # Separator | |
| ttk.Separator(iterations_label_frame, orient='horizontal').pack(fill='x', pady=(10, 5)) | |
| # Individual Controls Label | |
| tk.Label( | |
| iterations_label_frame, | |
| text="Individual Text Type Controls:", | |
| font=('Arial', 9, 'bold') | |
| ).pack(anchor='w', pady=(5, 5)) | |
| # Text Bubble iterations (modified from original bubble iterations) | |
| text_bubble_iter_frame = tk.Frame(iterations_label_frame) | |
| text_bubble_iter_frame.pack(fill='x', pady=5) | |
| text_bubble_label = tk.Label(text_bubble_iter_frame, text="Text Bubbles:", width=15, anchor='w') | |
| text_bubble_label.pack(side='left') | |
| self.text_bubble_iterations_var = tk.IntVar(value=self.settings.get('text_bubble_dilation_iterations', | |
| self.settings.get('bubble_dilation_iterations', 2))) | |
| self.text_bubble_iter_spinbox = tb.Spinbox( | |
| text_bubble_iter_frame, | |
| from_=0, | |
| to=5, | |
| textvariable=self.text_bubble_iterations_var, | |
| width=10 | |
| ) | |
| self.text_bubble_iter_spinbox.pack(side='left', padx=10) | |
| tk.Label(text_bubble_iter_frame, text="iterations (speech/dialogue bubbles)").pack(side='left') | |
| # Empty Bubble iterations (NEW) | |
| empty_bubble_iter_frame = tk.Frame(iterations_label_frame) | |
| empty_bubble_iter_frame.pack(fill='x', pady=5) | |
| empty_bubble_label = tk.Label(empty_bubble_iter_frame, text="Empty Bubbles:", width=15, anchor='w') | |
| empty_bubble_label.pack(side='left') | |
| self.empty_bubble_iterations_var = tk.IntVar(value=self.settings.get('empty_bubble_dilation_iterations', 3)) | |
| self.empty_bubble_iter_spinbox = tb.Spinbox( | |
| empty_bubble_iter_frame, | |
| from_=0, | |
| to=5, | |
| textvariable=self.empty_bubble_iterations_var, | |
| width=10 | |
| ) | |
| self.empty_bubble_iter_spinbox.pack(side='left', padx=10) | |
| tk.Label(empty_bubble_iter_frame, text="iterations (empty speech bubbles)").pack(side='left') | |
| # Free text iterations | |
| free_text_iter_frame = tk.Frame(iterations_label_frame) | |
| free_text_iter_frame.pack(fill='x', pady=5) | |
| free_text_label = tk.Label(free_text_iter_frame, text="Free Text:", width=15, anchor='w') | |
| free_text_label.pack(side='left') | |
| self.free_text_iterations_var = tk.IntVar(value=self.settings.get('free_text_dilation_iterations', 0)) | |
| self.free_text_iter_spinbox = tb.Spinbox( | |
| free_text_iter_frame, | |
| from_=0, | |
| to=5, | |
| textvariable=self.free_text_iterations_var, | |
| width=10 | |
| ) | |
| self.free_text_iter_spinbox.pack(side='left', padx=10) | |
| tk.Label(free_text_iter_frame, text="iterations (0 = perfect for B&W panels)").pack(side='left') | |
| # Store individual control widgets for enable/disable | |
| self.individual_iteration_controls = [ | |
| (text_bubble_label, self.text_bubble_iter_spinbox), | |
| (empty_bubble_label, self.empty_bubble_iter_spinbox), | |
| (free_text_label, self.free_text_iter_spinbox) | |
| ] | |
| # Apply initial state | |
| self._toggle_iteration_controls() | |
| # Legacy iterations (backwards compatibility) | |
| self.bubble_iterations_var = self.text_bubble_iterations_var # Link to text bubble for legacy | |
| self.dilation_iterations_var = self.text_bubble_iterations_var # Legacy support | |
| # Quick presets - UPDATED VERSION | |
| preset_frame = tk.Frame(mask_frame) | |
| preset_frame.pack(fill='x', pady=(10, 5)) | |
| tk.Label(preset_frame, text="Quick Presets:").pack(side='left', padx=(0, 10)) | |
| tb.Button( | |
| preset_frame, | |
| text="B&W Manga", | |
| command=lambda: self._set_mask_preset(15, False, 2, 2, 3, 0), | |
| bootstyle="secondary", | |
| width=12 | |
| ).pack(side='left', padx=2) | |
| tb.Button( | |
| preset_frame, | |
| text="Colored", | |
| command=lambda: self._set_mask_preset(15, False, 2, 2, 3, 3), | |
| bootstyle="secondary", | |
| width=12 | |
| ).pack(side='left', padx=2) | |
| tb.Button( | |
| preset_frame, | |
| text="Uniform", | |
| command=lambda: self._set_mask_preset(0, True, 2, 2, 2, 0), | |
| bootstyle="secondary", | |
| width=12 | |
| ).pack(side='left', padx=2) | |
| # Help text - UPDATED | |
| tk.Label( | |
| mask_frame, | |
| text="💡 B&W Manga: Optimized for black & white panels with clean bubbles\n" | |
| "💡 Colored: For colored manga with complex backgrounds\n" | |
| "💡 Aggressive: For difficult text removal cases\n" | |
| "💡 Uniform: Good for Manga-OCR\n" | |
| "ℹ️ Empty bubbles often need more iterations than text bubbles\n" | |
| "ℹ️ Set Free Text to 0 for crisp B&W panels without bleeding", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ).pack(anchor='w', pady=(10, 0)) | |
| # Note about method selection | |
| info_frame = tk.Frame(content_frame) | |
| info_frame.pack(fill='x', padx=20, pady=(20, 0)) | |
| tk.Label( | |
| info_frame, | |
| text="ℹ️ Note: Inpainting method (Cloud/Local) and model selection are configured\n" | |
| " in the Manga tab when you select images for translation.", | |
| font=('Arial', 10), | |
| fg='#4a9eff', | |
| justify='left' | |
| ).pack(anchor='w') | |
| def _toggle_iteration_controls(self): | |
| """Enable/disable iteration controls based on Auto and 'Use Same For All' toggles""" | |
| auto_on = getattr(self, 'auto_iterations_var', tk.BooleanVar(value=True)).get() | |
| use_all = self.use_all_iterations_var.get() | |
| if auto_on: | |
| # Disable everything when auto is on | |
| try: | |
| self.all_iterations_spinbox.config(state='disabled') | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'use_all_iterations_checkbox'): | |
| self.use_all_iterations_checkbox.config(state='disabled') | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'mask_dilation_spinbox'): | |
| self.mask_dilation_spinbox.config(state='disabled') | |
| except Exception: | |
| pass | |
| for label, spinbox in getattr(self, 'individual_iteration_controls', []): | |
| try: | |
| spinbox.config(state='disabled') | |
| label.config(fg='gray') | |
| except Exception: | |
| pass | |
| return | |
| # Auto off -> respect 'use all' | |
| try: | |
| self.all_iterations_spinbox.config(state='normal' if use_all else 'disabled') | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'use_all_iterations_checkbox'): | |
| self.use_all_iterations_checkbox.config(state='normal') | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'mask_dilation_spinbox'): | |
| self.mask_dilation_spinbox.config(state='normal') | |
| except Exception: | |
| pass | |
| for label, spinbox in getattr(self, 'individual_iteration_controls', []): | |
| state = 'disabled' if use_all else 'normal' | |
| try: | |
| spinbox.config(state=state) | |
| label.config(fg='gray' if use_all else 'white') | |
| except Exception: | |
| pass | |
| def _set_mask_preset(self, dilation, use_all, all_iter, text_bubble_iter, empty_bubble_iter, free_text_iter): | |
| """Set mask dilation preset values with comprehensive iteration controls""" | |
| self.mask_dilation_var.set(dilation) | |
| self.use_all_iterations_var.set(use_all) | |
| self.all_iterations_var.set(all_iter) | |
| self.text_bubble_iterations_var.set(text_bubble_iter) | |
| self.empty_bubble_iterations_var.set(empty_bubble_iter) | |
| self.free_text_iterations_var.set(free_text_iter) | |
| self._toggle_iteration_controls() | |
| def _create_cloud_api_tab(self, parent): | |
| """Create cloud API settings tab""" | |
| # NO CANVAS - JUST USE PARENT DIRECTLY | |
| frame = parent | |
| # API Model Selection | |
| model_frame = tk.LabelFrame(frame, text="Inpainting Model", padx=15, pady=10) | |
| model_frame.pack(fill='x', padx=20, pady=(20, 0)) | |
| tk.Label(model_frame, text="Select the Replicate model to use for inpainting:").pack(anchor='w', pady=(0, 10)) | |
| # Model options | |
| self.cloud_model_var = tk.StringVar(value=self.settings.get('cloud_inpaint_model', 'ideogram-v2')) | |
| models = [ | |
| ('ideogram-v2', 'Ideogram V2 (Best quality, with prompts)', 'ideogram-ai/ideogram-v2'), | |
| ('sd-inpainting', 'Stable Diffusion Inpainting (Classic, fast)', 'stability-ai/stable-diffusion-inpainting'), | |
| ('flux-inpainting', 'FLUX Dev Inpainting (High quality)', 'zsxkib/flux-dev-inpainting'), | |
| ('custom', 'Custom Model (Enter model identifier)', '') | |
| ] | |
| for value, text, model_id in models: | |
| row_frame = tk.Frame(model_frame) | |
| row_frame.pack(fill='x', pady=2) | |
| rb = tb.Radiobutton( | |
| row_frame, | |
| text=text, | |
| variable=self.cloud_model_var, | |
| value=value, | |
| command=self._on_cloud_model_change | |
| ) | |
| rb.pack(side='left') | |
| if model_id: | |
| tk.Label(row_frame, text=f"({model_id})", font=('Arial', 8), fg='gray').pack(side='left', padx=(10, 0)) | |
| # Custom version ID (now model identifier) | |
| self.custom_version_frame = tk.Frame(model_frame) | |
| self.custom_version_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(self.custom_version_frame, text="Model ID:", width=15, anchor='w').pack(side='left') | |
| self.custom_version_var = tk.StringVar(value=self.settings.get('cloud_custom_version', '')) | |
| self.custom_version_entry = tk.Entry(self.custom_version_frame, textvariable=self.custom_version_var, width=50) | |
| self.custom_version_entry.pack(side='left', padx=10) | |
| # Add helper text for custom model | |
| helper_text = tk.Label( | |
| self.custom_version_frame, | |
| text="Format: owner/model-name (e.g. stability-ai/stable-diffusion-inpainting)", | |
| font=('Arial', 8), | |
| fg='gray' | |
| ) | |
| helper_text.pack(anchor='w', padx=(70, 0), pady=(2, 0)) | |
| # Initially hide custom version entry | |
| if self.cloud_model_var.get() != 'custom': | |
| self.custom_version_frame.pack_forget() | |
| # Performance Settings | |
| perf_frame = tk.LabelFrame(frame, text="Performance Settings", padx=15, pady=10) | |
| perf_frame.pack(fill='x', padx=20, pady=(20, 0)) | |
| # Timeout | |
| timeout_frame = tk.Frame(perf_frame) | |
| timeout_frame.pack(fill='x', pady=5) | |
| tk.Label(timeout_frame, text="API Timeout:", width=15, anchor='w').pack(side='left') | |
| self.cloud_timeout_var = tk.IntVar(value=self.settings.get('cloud_timeout', 60)) | |
| timeout_spinbox = tb.Spinbox( | |
| timeout_frame, | |
| from_=30, | |
| to=300, | |
| textvariable=self.cloud_timeout_var, | |
| width=10 | |
| ) | |
| timeout_spinbox.pack(side='left', padx=10) | |
| tk.Label(timeout_frame, text="seconds", font=('Arial', 9)).pack(side='left') | |
| # Help text | |
| help_frame = tk.Frame(frame) | |
| help_frame.pack(fill='x', padx=20, pady=20) | |
| help_text = tk.Label( | |
| help_frame, | |
| text="💡 Tips:\n" | |
| "• Ideogram V2 is currently the best quality option\n" | |
| "• SD inpainting is fast and supports prompts\n" | |
| "• FLUX inpainting offers high quality results\n" | |
| "• Find more models at replicate.com/collections/inpainting", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ) | |
| help_text.pack(anchor='w') | |
| # Prompt Settings (for all models except custom) | |
| self.prompt_frame = tk.LabelFrame(frame, text="Prompt Settings", padx=15, pady=10) | |
| self.prompt_frame.pack(fill='x', padx=20, pady=(0, 20)) | |
| # Positive prompt | |
| tk.Label(self.prompt_frame, text="Inpainting Prompt:").pack(anchor='w', pady=(0, 5)) | |
| self.cloud_prompt_var = tk.StringVar(value=self.settings.get('cloud_inpaint_prompt', 'clean background, smooth surface')) | |
| prompt_entry = tk.Entry(self.prompt_frame, textvariable=self.cloud_prompt_var, width=60) | |
| prompt_entry.pack(fill='x', padx=(20, 20)) | |
| # Add note about prompts | |
| tk.Label( | |
| self.prompt_frame, | |
| text="Tip: Describe what you want in the inpainted area (e.g., 'white wall', 'wooden floor')", | |
| font=('Arial', 8), | |
| fg='gray' | |
| ).pack(anchor='w', padx=(20, 0), pady=(2, 10)) | |
| # Negative prompt (mainly for SD) | |
| self.negative_prompt_label = tk.Label(self.prompt_frame, text="Negative Prompt (SD only):") | |
| self.negative_prompt_label.pack(anchor='w', pady=(0, 5)) | |
| self.cloud_negative_prompt_var = tk.StringVar(value=self.settings.get('cloud_negative_prompt', 'text, writing, letters')) | |
| self.negative_entry = tk.Entry(self.prompt_frame, textvariable=self.cloud_negative_prompt_var, width=60) | |
| self.negative_entry.pack(fill='x', padx=(20, 20)) | |
| # Inference steps (for SD) | |
| self.steps_frame = tk.Frame(self.prompt_frame) | |
| self.steps_frame.pack(fill='x', pady=(10, 5)) | |
| self.steps_label = tk.Label(self.steps_frame, text="Inference Steps (SD only):", width=20, anchor='w') | |
| self.steps_label.pack(side='left', padx=(20, 0)) | |
| self.cloud_steps_var = tk.IntVar(value=self.settings.get('cloud_inference_steps', 20)) | |
| self.steps_spinbox = tb.Spinbox( | |
| self.steps_frame, | |
| from_=10, | |
| to=50, | |
| textvariable=self.cloud_steps_var, | |
| width=10 | |
| ) | |
| self.steps_spinbox.pack(side='left', padx=10) | |
| tk.Label(self.steps_frame, text="(Higher = better quality, slower)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Initially hide prompt frame if not using appropriate model | |
| if self.cloud_model_var.get() == 'custom': | |
| self.prompt_frame.pack_forget() | |
| # Show/hide SD-specific options based on model | |
| self._on_cloud_model_change() | |
| def _on_cloud_model_change(self): | |
| """Handle cloud model selection change""" | |
| model = self.cloud_model_var.get() | |
| # Show/hide custom version entry | |
| if model == 'custom': | |
| self.custom_version_frame.pack(fill='x', pady=(10, 0)) | |
| # DON'T HIDE THE PROMPT FRAME FOR CUSTOM MODELS | |
| self.prompt_frame.pack(fill='x', padx=20, pady=(20, 0)) | |
| else: | |
| self.custom_version_frame.pack_forget() | |
| self.prompt_frame.pack(fill='x', padx=20, pady=(20, 0)) | |
| # Show/hide SD-specific options | |
| if model == 'sd-inpainting': | |
| # Show negative prompt and steps | |
| self.negative_prompt_label.pack(anchor='w', pady=(10, 5)) | |
| self.negative_entry.pack(fill='x', padx=(20, 0)) | |
| self.steps_frame.pack(fill='x', pady=(10, 0)) | |
| else: | |
| # Hide SD-specific options | |
| self.negative_prompt_label.pack_forget() | |
| self.negative_entry.pack_forget() | |
| self.steps_frame.pack_forget() | |
| def _toggle_preprocessing(self): | |
| """Enable/disable preprocessing controls based on main toggle""" | |
| enabled = self.preprocess_enabled.get() | |
| # Widgets that must remain enabled regardless of toggle (widgets only, not Tk variables) | |
| always_on = [] | |
| for name in [ | |
| 'tiling_frame', | |
| 'tile_size_spinbox', 'tile_overlap_spinbox', | |
| 'chunk_frame', 'chunk_height_spinbox', 'chunk_overlap_spinbox', | |
| 'chunk_height_label', 'chunk_overlap_label', | |
| 'chunk_height_unit_label', 'chunk_overlap_unit_label', | |
| # Compression controls should always be active (separate from preprocessing) | |
| 'compression_frame', 'compression_toggle', 'compression_format_combo', 'jpeg_quality_spin', 'png_level_spin', 'webp_quality_spin' | |
| ]: | |
| if hasattr(self, name): | |
| always_on.append(getattr(self, name)) | |
| for control in self.preprocessing_controls: | |
| try: | |
| if control in always_on: | |
| # Ensure enabled | |
| if isinstance(control, (tk.Scale, tb.Spinbox, tb.Checkbutton)): | |
| control.config(state='normal') | |
| elif isinstance(control, tk.LabelFrame): | |
| control.config(fg='white') | |
| self._toggle_frame_children(control, True) | |
| elif isinstance(control, tk.Label): | |
| control.config(fg='white') | |
| elif isinstance(control, tk.Frame): | |
| self._toggle_frame_children(control, True) | |
| continue | |
| except Exception: | |
| pass | |
| # Normal enable/disable logic for other controls | |
| if isinstance(control, (tk.Scale, tb.Spinbox, tb.Checkbutton)): | |
| control.config(state='normal' if enabled else 'disabled') | |
| elif isinstance(control, tk.LabelFrame): | |
| control.config(fg='white' if enabled else 'gray') | |
| elif isinstance(control, tk.Label): | |
| control.config(fg='white' if enabled else 'gray') | |
| elif isinstance(control, tk.Frame): | |
| self._toggle_frame_children(control, enabled) | |
| # Final enforcement for always-on widgets (in case they were not in list) | |
| try: | |
| if hasattr(self, 'chunk_height_spinbox'): | |
| self.chunk_height_spinbox.config(state='normal') | |
| if hasattr(self, 'chunk_overlap_spinbox'): | |
| self.chunk_overlap_spinbox.config(state='normal') | |
| if hasattr(self, 'chunk_height_label'): | |
| self.chunk_height_label.config(fg='white') | |
| if hasattr(self, 'chunk_overlap_label'): | |
| self.chunk_overlap_label.config(fg='white') | |
| if hasattr(self, 'chunk_height_unit_label'): | |
| self.chunk_height_unit_label.config(fg='white') | |
| if hasattr(self, 'chunk_overlap_unit_label'): | |
| self.chunk_overlap_unit_label.config(fg='white') | |
| except Exception: | |
| pass | |
| # Ensure tiling fields respect their own toggle regardless of preprocessing state | |
| try: | |
| if hasattr(self, '_toggle_tiling_controls'): | |
| self._toggle_tiling_controls() | |
| except Exception: | |
| pass | |
| def _toggle_frame_children(self, frame, enabled): | |
| """Recursively enable/disable all children of a frame""" | |
| for child in frame.winfo_children(): | |
| if isinstance(child, (tk.Scale, tb.Spinbox, tb.Checkbutton, ttk.Combobox)): | |
| try: | |
| child.config(state='readonly' if (enabled and isinstance(child, ttk.Combobox)) else ('normal' if enabled else 'disabled')) | |
| except Exception: | |
| child.config(state='normal' if enabled else 'disabled') | |
| elif isinstance(child, tk.Label): | |
| child.config(fg='white' if enabled else 'gray') | |
| elif isinstance(child, tk.Frame): | |
| self._toggle_frame_children(child, enabled) | |
| def _toggle_roi_locality_controls(self): | |
| """Show/hide ROI locality controls based on toggle.""" | |
| try: | |
| enabled = self.roi_locality_var.get() | |
| except Exception: | |
| enabled = False | |
| # Rows to manage | |
| rows = [ | |
| getattr(self, 'roi_pad_row', None), | |
| getattr(self, 'roi_min_row', None), | |
| getattr(self, 'roi_area_row', None), | |
| getattr(self, 'roi_max_row', None) | |
| ] | |
| for row in rows: | |
| try: | |
| if row is None: continue | |
| if enabled: | |
| # Only pack if not already managed | |
| row.pack(fill='x', pady=5) | |
| else: | |
| row.pack_forget() | |
| except Exception: | |
| pass | |
| def _toggle_tiling_controls(self): | |
| """Enable/disable tiling size/overlap fields based on tiling toggle.""" | |
| try: | |
| enabled = bool(self.inpaint_tiling_enabled.get()) | |
| except Exception: | |
| enabled = False | |
| state = 'normal' if enabled else 'disabled' | |
| try: | |
| self.tile_size_spinbox.config(state=state) | |
| except Exception: | |
| pass | |
| try: | |
| self.tile_overlap_spinbox.config(state=state) | |
| except Exception: | |
| pass | |
| def _toggle_compression_format(self): | |
| """Show only the controls relevant to the selected format (hide others).""" | |
| fmt = getattr(self, 'compression_format_var', tk.StringVar(value='jpeg')).get() | |
| try: | |
| # Hide all rows first | |
| for row in [getattr(self, 'jpeg_row', None), getattr(self, 'png_row', None), getattr(self, 'webp_row', None)]: | |
| try: | |
| if row is not None: | |
| row.pack_forget() | |
| except Exception: | |
| pass | |
| # Show the selected one | |
| if fmt == 'jpeg': | |
| if hasattr(self, 'jpeg_row') and self.jpeg_row is not None: | |
| self.jpeg_row.pack(fill='x', pady=5) | |
| elif fmt == 'png': | |
| if hasattr(self, 'png_row') and self.png_row is not None: | |
| self.png_row.pack(fill='x', pady=5) | |
| else: # webp | |
| if hasattr(self, 'webp_row') and self.webp_row is not None: | |
| self.webp_row.pack(fill='x', pady=5) | |
| except Exception: | |
| pass | |
| def _toggle_ocr_batching_controls(self): | |
| """Show/hide OCR batching rows based on enable toggle.""" | |
| try: | |
| enabled = bool(self.ocr_batch_enabled_var.get()) | |
| except Exception: | |
| enabled = False | |
| try: | |
| if hasattr(self, 'ocr_bs_row') and self.ocr_bs_row: | |
| (self.ocr_bs_row.pack if enabled else self.ocr_bs_row.pack_forget)() | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'ocr_cc_row') and self.ocr_cc_row: | |
| (self.ocr_cc_row.pack if enabled else self.ocr_cc_row.pack_forget)() | |
| except Exception: | |
| pass | |
| def _create_ocr_tab(self, notebook): | |
| """Create OCR settings tab with all options""" | |
| frame = ttk.Frame(notebook) | |
| notebook.add(frame, text="OCR Settings") | |
| # Main content | |
| content_frame = tk.Frame(frame) | |
| content_frame.pack(fill='both', expand=True, padx=5, pady=5) | |
| # Language hints | |
| lang_frame = tk.LabelFrame(content_frame, text="Language Detection", padx=15, pady=10) | |
| lang_frame.pack(fill='x', padx=20, pady=20) | |
| tk.Label( | |
| lang_frame, | |
| text="Select languages to prioritize during OCR:", | |
| font=('Arial', 10) | |
| ).pack(anchor='w', pady=(0, 10)) | |
| # Language checkboxes | |
| self.lang_vars = {} | |
| languages = [ | |
| ('ja', 'Japanese'), | |
| ('ko', 'Korean'), | |
| ('zh', 'Chinese (Simplified)'), | |
| ('zh-TW', 'Chinese (Traditional)'), | |
| ('en', 'English') | |
| ] | |
| lang_grid = tk.Frame(lang_frame) | |
| lang_grid.pack(fill='x') | |
| for i, (code, name) in enumerate(languages): | |
| var = tk.BooleanVar(value=code in self.settings['ocr']['language_hints']) | |
| self.lang_vars[code] = var | |
| tb.Checkbutton( | |
| lang_grid, | |
| text=name, | |
| variable=var, | |
| bootstyle="round-toggle" | |
| ).grid(row=i//2, column=i%2, sticky='w', padx=10, pady=5) | |
| # OCR parameters | |
| ocr_frame = tk.LabelFrame(content_frame, text="OCR Parameters", padx=15, pady=10) | |
| ocr_frame.pack(fill='x', padx=20) | |
| # Confidence threshold | |
| conf_frame = tk.Frame(ocr_frame) | |
| conf_frame.pack(fill='x', pady=5) | |
| tk.Label(conf_frame, text="Confidence Threshold:", width=20, anchor='w').pack(side='left') | |
| self.confidence_threshold = tk.DoubleVar(value=self.settings['ocr']['confidence_threshold']) | |
| conf_scale = tk.Scale( | |
| conf_frame, | |
| from_=0.0, to=1.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.confidence_threshold, | |
| length=250 | |
| ) | |
| conf_scale.pack(side='left', padx=10) | |
| tk.Label(conf_frame, textvariable=self.confidence_threshold, width=5).pack(side='left') | |
| # Detection mode | |
| mode_frame = tk.Frame(ocr_frame) | |
| mode_frame.pack(fill='x', pady=5) | |
| tk.Label(mode_frame, text="Detection Mode:", width=20, anchor='w').pack(side='left') | |
| self.detection_mode = tk.StringVar(value=self.settings['ocr']['text_detection_mode']) | |
| mode_combo = ttk.Combobox( | |
| mode_frame, | |
| textvariable=self.detection_mode, | |
| values=['document', 'text'], | |
| state='readonly', | |
| width=15 | |
| ) | |
| mode_combo.pack(side='left', padx=10) | |
| tk.Label( | |
| mode_frame, | |
| text="(document = better for manga, text = simple layouts)", | |
| font=('Arial', 9), | |
| fg='gray' | |
| ).pack(side='left', padx=5) | |
| # Text merging settings | |
| merge_frame = tk.LabelFrame(content_frame, text="Text Region Merging", padx=15, pady=10) | |
| merge_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Merge nearby threshold | |
| nearby_frame = tk.Frame(merge_frame) | |
| nearby_frame.pack(fill='x', pady=5) | |
| tk.Label(nearby_frame, text="Merge Distance:", width=20, anchor='w').pack(side='left') | |
| self.merge_nearby_threshold = tk.IntVar(value=self.settings['ocr']['merge_nearby_threshold']) | |
| nearby_spinbox = tb.Spinbox( | |
| nearby_frame, | |
| from_=0, | |
| to=200, | |
| textvariable=self.merge_nearby_threshold, | |
| increment=10, | |
| width=10 | |
| ) | |
| nearby_spinbox.pack(side='left', padx=10) | |
| tk.Label(nearby_frame, text="pixels").pack(side='left') | |
| # Text Filtering Setting | |
| filter_frame = tk.LabelFrame(content_frame, text="Text Filtering", padx=15, pady=10) | |
| filter_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Minimum text length | |
| min_length_frame = tk.Frame(filter_frame) | |
| min_length_frame.pack(fill='x', pady=5) | |
| tk.Label(min_length_frame, text="Min Text Length:", width=20, anchor='w').pack(side='left') | |
| self.min_text_length_var = tk.IntVar( | |
| value=self.settings['ocr'].get('min_text_length', 0) | |
| ) | |
| min_length_spinbox = tb.Spinbox( | |
| min_length_frame, | |
| from_=1, | |
| to=10, | |
| textvariable=self.min_text_length_var, | |
| increment=1, | |
| width=10 | |
| ) | |
| min_length_spinbox.pack(side='left', padx=10) | |
| tk.Label(min_length_frame, text="characters").pack(side='left') | |
| tk.Label( | |
| min_length_frame, | |
| text="(skip text shorter than this)", | |
| font=('Arial', 9), | |
| fg='gray' | |
| ).pack(side='left', padx=10) | |
| # Exclude English text checkbox | |
| exclude_english_frame = tk.Frame(filter_frame) | |
| exclude_english_frame.pack(fill='x', pady=(5, 0)) | |
| self.exclude_english_var = tk.BooleanVar( | |
| value=self.settings['ocr'].get('exclude_english_text', False) | |
| ) | |
| tb.Checkbutton( | |
| exclude_english_frame, | |
| text="Exclude primarily English text (tunable threshold)", | |
| variable=self.exclude_english_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # Threshold slider | |
| english_threshold_frame = tk.Frame(filter_frame) | |
| english_threshold_frame.pack(fill='x', pady=5) | |
| tk.Label(english_threshold_frame, text="English Exclude Threshold:", width=28, anchor='w').pack(side='left') | |
| self.english_exclude_threshold = tk.DoubleVar( | |
| value=self.settings['ocr'].get('english_exclude_threshold', 0.7) | |
| ) | |
| threshold_scale = tk.Scale( | |
| english_threshold_frame, | |
| from_=0.6, to=0.99, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.english_exclude_threshold, | |
| length=250, | |
| command=lambda v: self.english_threshold_label.config(text=f"{float(v)*100:.0f}%") | |
| ) | |
| threshold_scale.pack(side='left', padx=10) | |
| self.english_threshold_label = tk.Label(english_threshold_frame, text=f"{int(self.english_exclude_threshold.get()*100)}%", width=5) | |
| self.english_threshold_label.pack(side='left') | |
| # Minimum character count | |
| min_chars_frame = tk.Frame(filter_frame) | |
| min_chars_frame.pack(fill='x', pady=5) | |
| tk.Label(min_chars_frame, text="Min chars to exclude as English:", width=28, anchor='w').pack(side='left') | |
| self.english_exclude_min_chars = tk.IntVar( | |
| value=self.settings['ocr'].get('english_exclude_min_chars', 4) | |
| ) | |
| min_chars_spinbox = tb.Spinbox( | |
| min_chars_frame, | |
| from_=1, | |
| to=10, | |
| textvariable=self.english_exclude_min_chars, | |
| increment=1, | |
| width=10 | |
| ) | |
| min_chars_spinbox.pack(side='left', padx=10) | |
| tk.Label(min_chars_frame, text="characters").pack(side='left') | |
| # Legacy aggressive short-token filter | |
| exclude_short_frame = tk.Frame(filter_frame) | |
| exclude_short_frame.pack(fill='x', pady=(5, 0)) | |
| self.english_exclude_short_tokens = tk.BooleanVar( | |
| value=self.settings['ocr'].get('english_exclude_short_tokens', False) | |
| ) | |
| tb.Checkbutton( | |
| exclude_short_frame, | |
| text="Aggressively drop very short ASCII tokens (legacy)", | |
| variable=self.english_exclude_short_tokens, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # Help text | |
| tk.Label( | |
| filter_frame, | |
| text="💡 Text filtering helps skip:\n" | |
| " • UI elements and watermarks\n" | |
| " • Page numbers and copyright text\n" | |
| " • Single characters or symbols\n" | |
| " • Non-target language text", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ).pack(anchor='w', pady=(10, 0)) | |
| # Azure-specific OCR settings (existing code continues here) | |
| azure_ocr_frame = tk.LabelFrame(content_frame, text="Azure OCR Settings", padx=15, pady=10) | |
| # Azure-specific OCR settings | |
| azure_ocr_frame = tk.LabelFrame(content_frame, text="Azure OCR Settings", padx=15, pady=10) | |
| azure_ocr_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Azure merge multiplier | |
| merge_mult_frame = tk.Frame(azure_ocr_frame) | |
| merge_mult_frame.pack(fill='x', pady=5) | |
| tk.Label(merge_mult_frame, text="Merge Multiplier:", width=20, anchor='w').pack(side='left') | |
| self.azure_merge_multiplier = tk.DoubleVar( | |
| value=self.settings['ocr'].get('azure_merge_multiplier', 2.0) | |
| ) | |
| azure_scale = tk.Scale( | |
| merge_mult_frame, | |
| from_=1.0, | |
| to=5.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.azure_merge_multiplier, | |
| length=200, | |
| command=lambda v: self._update_azure_label() | |
| ) | |
| azure_scale.pack(side='left', padx=10) | |
| self.azure_label = tk.Label(merge_mult_frame, text="2.0x", width=5) | |
| self.azure_label.pack(side='left') | |
| self._update_azure_label() | |
| tk.Label( | |
| merge_mult_frame, | |
| text="(multiplies merge distance for Azure lines)", | |
| font=('Arial', 9), | |
| fg='gray' | |
| ).pack(side='left', padx=5) | |
| # Reading order | |
| reading_order_frame = tk.Frame(azure_ocr_frame) | |
| reading_order_frame.pack(fill='x', pady=5) | |
| tk.Label(reading_order_frame, text="Reading Order:", width=20, anchor='w').pack(side='left') | |
| self.azure_reading_order = tk.StringVar( | |
| value=self.settings['ocr'].get('azure_reading_order', 'natural') | |
| ) | |
| order_combo = ttk.Combobox( | |
| reading_order_frame, | |
| textvariable=self.azure_reading_order, | |
| values=['basic', 'natural'], | |
| state='readonly', | |
| width=15 | |
| ) | |
| order_combo.pack(side='left', padx=10) | |
| tk.Label( | |
| reading_order_frame, | |
| text="(natural = better for complex layouts)", | |
| font=('Arial', 9), | |
| fg='gray' | |
| ).pack(side='left', padx=5) | |
| # Model version | |
| model_version_frame = tk.Frame(azure_ocr_frame) | |
| model_version_frame.pack(fill='x', pady=5) | |
| tk.Label(model_version_frame, text="Model Version:", width=20, anchor='w').pack(side='left') | |
| self.azure_model_version = tk.StringVar( | |
| value=self.settings['ocr'].get('azure_model_version', 'latest') | |
| ) | |
| version_combo = ttk.Combobox( | |
| model_version_frame, | |
| textvariable=self.azure_model_version, | |
| values=['latest', '2022-04-30', '2022-01-30', '2021-09-30'], | |
| width=15 | |
| ) | |
| version_combo.pack(side='left', padx=10) | |
| tk.Label( | |
| model_version_frame, | |
| text="(use 'latest' for newest features)", | |
| font=('Arial', 9), | |
| fg='gray' | |
| ).pack(side='left', padx=5) | |
| # Timeout settings | |
| timeout_frame = tk.Frame(azure_ocr_frame) | |
| timeout_frame.pack(fill='x', pady=5) | |
| tk.Label(timeout_frame, text="Max Wait Time:", width=20, anchor='w').pack(side='left') | |
| self.azure_max_wait = tk.IntVar( | |
| value=self.settings['ocr'].get('azure_max_wait', 60) | |
| ) | |
| wait_spinbox = tb.Spinbox( | |
| timeout_frame, | |
| from_=10, | |
| to=120, | |
| textvariable=self.azure_max_wait, | |
| increment=5, | |
| width=10 | |
| ) | |
| wait_spinbox.pack(side='left', padx=10) | |
| tk.Label(timeout_frame, text="seconds").pack(side='left') | |
| # Poll interval | |
| poll_frame = tk.Frame(azure_ocr_frame) | |
| poll_frame.pack(fill='x', pady=5) | |
| tk.Label(poll_frame, text="Poll Interval:", width=20, anchor='w').pack(side='left') | |
| self.azure_poll_interval = tk.DoubleVar( | |
| value=self.settings['ocr'].get('azure_poll_interval', 0.5) | |
| ) | |
| poll_scale = tk.Scale( | |
| poll_frame, | |
| from_=0.0, | |
| to=2.0, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.azure_poll_interval, | |
| length=200 | |
| ) | |
| poll_scale.pack(side='left', padx=10) | |
| tk.Label(poll_frame, textvariable=self.azure_poll_interval, width=5).pack(side='left') | |
| tk.Label(poll_frame, text="sec").pack(side='left') | |
| # Help text | |
| tk.Label( | |
| azure_ocr_frame, | |
| text="💡 Azure Read API auto-detects language well\n" | |
| "💡 Natural reading order works better for manga panels", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ).pack(anchor='w', pady=(10, 0)) | |
| # Rotation correction | |
| rotation_frame = tk.Frame(merge_frame) | |
| rotation_frame.pack(fill='x', pady=5) | |
| self.enable_rotation = tk.BooleanVar(value=self.settings['ocr']['enable_rotation_correction']) | |
| tb.Checkbutton( | |
| rotation_frame, | |
| text="Enable automatic rotation correction for tilted text", | |
| variable=self.enable_rotation, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # OCR batching and locality settings | |
| ocr_batch_frame = tk.LabelFrame(content_frame, text="OCR Batching & Concurrency", padx=15, pady=10) | |
| ocr_batch_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Enable OCR batching | |
| self.ocr_batch_enabled_var = tk.BooleanVar(value=self.settings['ocr'].get('ocr_batch_enabled', True)) | |
| tb.Checkbutton( | |
| ocr_batch_frame, | |
| text="Enable OCR batching (independent of translation batching)", | |
| variable=self.ocr_batch_enabled_var, | |
| command=lambda: self._toggle_ocr_batching_controls(), | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # OCR batch size | |
| ocr_bs_row = tk.Frame(ocr_batch_frame) | |
| self.ocr_bs_row = ocr_bs_row | |
| ocr_bs_row.pack(fill='x', pady=5) | |
| tk.Label(ocr_bs_row, text="OCR Batch Size:", width=20, anchor='w').pack(side='left') | |
| self.ocr_batch_size_var = tk.IntVar(value=int(self.settings['ocr'].get('ocr_batch_size', 8))) | |
| self.ocr_batch_size_spin = tb.Spinbox( | |
| ocr_bs_row, | |
| from_=1, | |
| to=32, | |
| textvariable=self.ocr_batch_size_var, | |
| width=10 | |
| ) | |
| self.ocr_batch_size_spin.pack(side='left', padx=10) | |
| tk.Label(ocr_bs_row, text="(Google: items/request; Azure: drives concurrency)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # OCR Max Concurrency | |
| ocr_cc_row = tk.Frame(ocr_batch_frame) | |
| self.ocr_cc_row = ocr_cc_row | |
| ocr_cc_row.pack(fill='x', pady=5) | |
| tk.Label(ocr_cc_row, text="OCR Max Concurrency:", width=20, anchor='w').pack(side='left') | |
| self.ocr_max_conc_var = tk.IntVar(value=int(self.settings['ocr'].get('ocr_max_concurrency', 2))) | |
| self.ocr_max_conc_spin = tb.Spinbox( | |
| ocr_cc_row, | |
| from_=1, | |
| to=8, | |
| textvariable=self.ocr_max_conc_var, | |
| width=10 | |
| ) | |
| self.ocr_max_conc_spin.pack(side='left', padx=10) | |
| tk.Label(ocr_cc_row, text="(Google: concurrent requests; Azure: workers, capped at 4)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Apply initial visibility for OCR batching controls | |
| try: | |
| self._toggle_ocr_batching_controls() | |
| except Exception: | |
| pass | |
| # ROI sizing | |
| roi_frame_local = tk.LabelFrame(content_frame, text="ROI Locality Controls", padx=15, pady=10) | |
| roi_frame_local.pack(fill='x', padx=20, pady=(10, 0)) | |
| # ROI locality toggle (now inside this section) | |
| self.roi_locality_var = tk.BooleanVar(value=self.settings['ocr'].get('roi_locality_enabled', False)) | |
| tb.Checkbutton( | |
| roi_frame_local, | |
| text="Enable ROI-based OCR locality and batching (uses bubble detection)", | |
| variable=self.roi_locality_var, | |
| command=self._toggle_roi_locality_controls, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w', pady=(0,5)) | |
| # ROI padding ratio | |
| roi_pad_row = tk.Frame(roi_frame_local) | |
| roi_pad_row.pack(fill='x', pady=5) | |
| self.roi_pad_row = roi_pad_row | |
| tk.Label(roi_pad_row, text="ROI Padding Ratio:", width=20, anchor='w').pack(side='left') | |
| self.roi_padding_ratio_var = tk.DoubleVar(value=float(self.settings['ocr'].get('roi_padding_ratio', 0.08))) | |
| roi_pad_scale = tk.Scale( | |
| roi_pad_row, | |
| from_=0.0, | |
| to=0.30, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.roi_padding_ratio_var, | |
| length=200 | |
| ) | |
| roi_pad_scale.pack(side='left', padx=10) | |
| tk.Label(roi_pad_row, textvariable=self.roi_padding_ratio_var, width=5).pack(side='left') | |
| # ROI min side / area | |
| roi_min_row = tk.Frame(roi_frame_local) | |
| roi_min_row.pack(fill='x', pady=5) | |
| self.roi_min_row = roi_min_row | |
| tk.Label(roi_min_row, text="Min ROI Side:", width=20, anchor='w').pack(side='left') | |
| self.roi_min_side_var = tk.IntVar(value=int(self.settings['ocr'].get('roi_min_side_px', 12))) | |
| self.roi_min_side_spin = tb.Spinbox( | |
| roi_min_row, | |
| from_=1, | |
| to=64, | |
| textvariable=self.roi_min_side_var, | |
| width=10 | |
| ) | |
| self.roi_min_side_spin.pack(side='left', padx=10) | |
| tk.Label(roi_min_row, text="px").pack(side='left') | |
| roi_area_row = tk.Frame(roi_frame_local) | |
| roi_area_row.pack(fill='x', pady=5) | |
| self.roi_area_row = roi_area_row | |
| tk.Label(roi_area_row, text="Min ROI Area:", width=20, anchor='w').pack(side='left') | |
| self.roi_min_area_var = tk.IntVar(value=int(self.settings['ocr'].get('roi_min_area_px', 100))) | |
| self.roi_min_area_spin = tb.Spinbox( | |
| roi_area_row, | |
| from_=1, | |
| to=5000, | |
| textvariable=self.roi_min_area_var, | |
| width=10 | |
| ) | |
| self.roi_min_area_spin.pack(side='left', padx=10) | |
| tk.Label(roi_area_row, text="px^2").pack(side='left') | |
| # ROI max side (0 disables) | |
| roi_max_row = tk.Frame(roi_frame_local) | |
| roi_max_row.pack(fill='x', pady=5) | |
| self.roi_max_row = roi_max_row | |
| tk.Label(roi_max_row, text="ROI Max Side (0=off):", width=20, anchor='w').pack(side='left') | |
| self.roi_max_side_var = tk.IntVar(value=int(self.settings['ocr'].get('roi_max_side', 0))) | |
| self.roi_max_side_spin = tb.Spinbox( | |
| roi_max_row, | |
| from_=0, | |
| to=2048, | |
| textvariable=self.roi_max_side_var, | |
| width=10 | |
| ) | |
| self.roi_max_side_spin.pack(side='left', padx=10) | |
| # Apply initial visibility based on toggle | |
| self._toggle_roi_locality_controls() | |
| # AI Bubble Detection Settings | |
| bubble_frame = tk.LabelFrame(content_frame, text="AI Bubble Detection", padx=15, pady=10) | |
| bubble_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Enable bubble detection | |
| self.bubble_detection_enabled = tk.BooleanVar( | |
| value=self.settings['ocr'].get('bubble_detection_enabled', False) | |
| ) | |
| bubble_enable_cb = tb.Checkbutton( | |
| bubble_frame, | |
| text="Enable AI-powered bubble detection (overrides traditional merging)", | |
| variable=self.bubble_detection_enabled, | |
| bootstyle="round-toggle", | |
| command=self._toggle_bubble_controls | |
| ) | |
| bubble_enable_cb.pack(anchor='w') | |
| # Detector type dropdown - PUT THIS DIRECTLY IN bubble_frame | |
| detector_type_frame = tk.Frame(bubble_frame) | |
| detector_type_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(detector_type_frame, text="Detector:", width=15, anchor='w').pack(side='left') | |
| # Model mapping | |
| self.detector_models = { | |
| 'RTEDR_onnx': 'ogkalu/comic-text-and-bubble-detector', | |
| 'RT-DETR': 'ogkalu/comic-text-and-bubble-detector', | |
| 'YOLOv8 Speech': 'ogkalu/comic-speech-bubble-detector-yolov8m', | |
| 'YOLOv8 Text': 'ogkalu/comic-text-segmenter-yolov8m', | |
| 'YOLOv8 Manga': 'ogkalu/manga-text-detector-yolov8s', | |
| 'Custom Model': '' | |
| } | |
| # Get saved detector type (default to ONNX backend) | |
| saved_type = self.settings['ocr'].get('detector_type', 'rtdetr_onnx') | |
| if saved_type == 'rtdetr_onnx': | |
| initial_selection = 'RTEDR_onnx' | |
| elif saved_type == 'rtdetr': | |
| initial_selection = 'RT-DETR' | |
| elif saved_type == 'yolo': | |
| initial_selection = 'YOLOv8 Speech' | |
| elif saved_type == 'custom': | |
| initial_selection = 'Custom Model' | |
| else: | |
| initial_selection = 'RTEDR_onnx' | |
| self.detector_type = tk.StringVar(value=initial_selection) | |
| detector_combo = ttk.Combobox( | |
| detector_type_frame, | |
| textvariable=self.detector_type, | |
| values=list(self.detector_models.keys()), | |
| state='readonly', | |
| width=20 | |
| ) | |
| detector_combo.pack(side='left', padx=(10, 0)) | |
| detector_combo.bind('<<ComboboxSelected>>', lambda e: self._on_detector_type_changed()) | |
| # NOW create the settings frame | |
| self.yolo_settings_frame = tk.LabelFrame(bubble_frame, text="Model Settings", padx=10, pady=5) | |
| self.rtdetr_settings_frame = self.yolo_settings_frame # Alias | |
| # NOW you can create model_frame inside yolo_settings_frame | |
| model_frame = tk.Frame(self.yolo_settings_frame) | |
| model_frame.pack(fill='x', pady=(5, 0)) | |
| tk.Label(model_frame, text="Model:", width=12, anchor='w').pack(side='left') | |
| self.bubble_model_path = tk.StringVar( | |
| value=self.settings['ocr'].get('bubble_model_path', '') | |
| ) | |
| self.rtdetr_model_url = self.bubble_model_path # Alias | |
| # Style the entry to match GUI theme | |
| self.bubble_model_entry = tk.Entry( | |
| model_frame, | |
| textvariable=self.bubble_model_path, | |
| width=35, | |
| state='readonly', | |
| bg='#2b2b2b', # Dark background | |
| fg='#ffffff', # White text | |
| insertbackground='#ffffff', # White cursor | |
| readonlybackground='#1e1e1e', # Even darker when readonly | |
| relief='flat', | |
| bd=1 | |
| ) | |
| self.bubble_model_entry.pack(side='left', padx=(0, 10)) | |
| self.rtdetr_url_entry = self.bubble_model_entry # Alias | |
| # Store for compatibility | |
| self.detector_radio_widgets = [detector_combo] | |
| # Settings frames | |
| self.yolo_settings_frame = tk.LabelFrame(bubble_frame, text="Model Settings", padx=10, pady=5) | |
| self.rtdetr_settings_frame = self.yolo_settings_frame # Alias | |
| # Model path/URL | |
| model_frame = tk.Frame(self.yolo_settings_frame) | |
| model_frame.pack(fill='x', pady=(5, 0)) | |
| tk.Label(model_frame, text="Model:", width=12, anchor='w').pack(side='left') | |
| self.bubble_model_path = tk.StringVar( | |
| value=self.settings['ocr'].get('bubble_model_path', '') | |
| ) | |
| self.rtdetr_model_url = self.bubble_model_path # Alias | |
| self.bubble_model_entry = tk.Entry( | |
| model_frame, | |
| textvariable=self.bubble_model_path, | |
| width=35, | |
| state='readonly' | |
| ) | |
| self.bubble_model_entry.pack(side='left', padx=(0, 10)) | |
| self.rtdetr_url_entry = self.bubble_model_entry # Alias | |
| self.bubble_browse_btn = tb.Button( | |
| model_frame, | |
| text="Browse", | |
| command=self._browse_bubble_model, | |
| bootstyle="primary" | |
| ) | |
| self.bubble_browse_btn.pack(side='left') | |
| self.bubble_clear_btn = tb.Button( | |
| model_frame, | |
| text="Clear", | |
| command=self._clear_bubble_model, | |
| bootstyle="secondary" | |
| ) | |
| self.bubble_clear_btn.pack(side='left', padx=(5, 0)) | |
| # Download and Load buttons | |
| button_frame = tk.Frame(self.yolo_settings_frame) | |
| button_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(button_frame, text="Actions:", width=12, anchor='w').pack(side='left') | |
| self.rtdetr_download_btn = tb.Button( | |
| button_frame, | |
| text="Download", | |
| command=self._download_rtdetr_model, | |
| bootstyle="success" | |
| ) | |
| self.rtdetr_download_btn.pack(side='left', padx=(0, 5)) | |
| self.rtdetr_load_btn = tb.Button( | |
| button_frame, | |
| text="Load Model", | |
| command=self._load_rtdetr_model, | |
| bootstyle="primary" | |
| ) | |
| self.rtdetr_load_btn.pack(side='left') | |
| self.rtdetr_status_label = tk.Label( | |
| button_frame, | |
| text="", | |
| font=('Arial', 9) | |
| ) | |
| self.rtdetr_status_label.pack(side='left', padx=(15, 0)) | |
| # RT-DETR Detection classes | |
| rtdetr_classes_frame = tk.Frame(self.yolo_settings_frame) | |
| rtdetr_classes_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(rtdetr_classes_frame, text="Detect:", width=12, anchor='w').pack(side='left') | |
| self.detect_empty_bubbles = tk.BooleanVar( | |
| value=self.settings['ocr'].get('detect_empty_bubbles', True) | |
| ) | |
| empty_cb = tk.Checkbutton( | |
| rtdetr_classes_frame, | |
| text="Empty Bubbles", | |
| variable=self.detect_empty_bubbles | |
| ) | |
| empty_cb.pack(side='left', padx=(0, 10)) | |
| self.detect_text_bubbles = tk.BooleanVar( | |
| value=self.settings['ocr'].get('detect_text_bubbles', True) | |
| ) | |
| text_cb = tk.Checkbutton( | |
| rtdetr_classes_frame, | |
| text="Text Bubbles", | |
| variable=self.detect_text_bubbles | |
| ) | |
| text_cb.pack(side='left', padx=(0, 10)) | |
| self.detect_free_text = tk.BooleanVar( | |
| value=self.settings['ocr'].get('detect_free_text', True) | |
| ) | |
| free_cb = tk.Checkbutton( | |
| rtdetr_classes_frame, | |
| text="Free Text", | |
| variable=self.detect_free_text | |
| ) | |
| free_cb.pack(side='left') | |
| self.rtdetr_classes_frame = rtdetr_classes_frame | |
| # Confidence | |
| conf_frame = tk.Frame(self.yolo_settings_frame) | |
| conf_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(conf_frame, text="Confidence:", width=12, anchor='w').pack(side='left') | |
| detector_label = self.detector_type.get() | |
| default_conf = 0.3 if ('RT-DETR' in detector_label or 'RTEDR_onnx' in detector_label or 'onnx' in detector_label.lower()) else 0.5 | |
| self.bubble_confidence = tk.DoubleVar( | |
| value=self.settings['ocr'].get('bubble_confidence', default_conf) | |
| ) | |
| self.rtdetr_confidence = self.bubble_confidence | |
| self.bubble_conf_scale = tk.Scale( | |
| conf_frame, | |
| from_=0.0, | |
| to=0.99, | |
| resolution=0.01, | |
| orient='horizontal', | |
| variable=self.bubble_confidence, | |
| length=200, | |
| command=lambda v: self.bubble_conf_label.config(text=f"{float(v):.2f}") | |
| ) | |
| self.bubble_conf_scale.pack(side='left', padx=(0, 10)) | |
| self.rtdetr_conf_scale = self.bubble_conf_scale | |
| self.bubble_conf_label = tk.Label(conf_frame, text=f"{self.bubble_confidence.get():.2f}", width=5) | |
| self.bubble_conf_label.pack(side='left') | |
| self.rtdetr_conf_label = self.bubble_conf_label | |
| # Status label | |
| # YOLO-specific: Max detections (only visible for YOLO) | |
| self.yolo_maxdet_row = tk.Frame(self.yolo_settings_frame) | |
| self.yolo_maxdet_row.pack_forget() | |
| tk.Label(self.yolo_maxdet_row, text="Max detections:", width=12, anchor='w').pack(side='left') | |
| self.bubble_max_det_yolo_var = tk.IntVar( | |
| value=self.settings['ocr'].get('bubble_max_detections_yolo', 100) | |
| ) | |
| tb.Spinbox( | |
| self.yolo_maxdet_row, | |
| from_=1, | |
| to=2000, | |
| textvariable=self.bubble_max_det_yolo_var, | |
| width=10 | |
| ).pack(side='left', padx=(0,10)) | |
| self.bubble_status_label = tk.Label( | |
| bubble_frame, | |
| text="", | |
| font=('Arial', 9) | |
| ) | |
| self.bubble_status_label.pack(anchor='w', pady=(10, 0)) | |
| # Store controls | |
| self.bubble_controls = [ | |
| detector_combo, | |
| self.bubble_model_entry, | |
| self.bubble_browse_btn, | |
| self.bubble_clear_btn, | |
| self.bubble_conf_scale, | |
| self.rtdetr_download_btn, | |
| self.rtdetr_load_btn | |
| ] | |
| self.rtdetr_controls = [ | |
| self.rtdetr_url_entry, | |
| self.rtdetr_load_btn, | |
| self.rtdetr_download_btn, | |
| self.rtdetr_conf_scale, | |
| empty_cb, | |
| text_cb, | |
| free_cb | |
| ] | |
| self.yolo_controls = [ | |
| self.bubble_model_entry, | |
| self.bubble_browse_btn, | |
| self.bubble_clear_btn, | |
| self.bubble_conf_scale, | |
| self.yolo_maxdet_row | |
| ] | |
| # Initialize control states | |
| self._toggle_bubble_controls() | |
| # Only call detector change after everything is initialized | |
| if self.bubble_detection_enabled.get(): | |
| try: | |
| self._on_detector_type_changed() | |
| self._update_bubble_status() | |
| except AttributeError: | |
| # Frames not yet created, skip initialization | |
| pass | |
| # Check status after dialog ready | |
| self.dialog.after(500, self._check_rtdetr_status) | |
| def _on_detector_type_changed(self): | |
| """Handle detector type change""" | |
| if not hasattr(self, 'bubble_detection_enabled'): | |
| return | |
| if not self.bubble_detection_enabled.get(): | |
| self.yolo_settings_frame.pack_forget() | |
| return | |
| detector = self.detector_type.get() | |
| # Handle different detector types | |
| if detector == 'Custom Model': | |
| # Custom model - enable manual entry | |
| self.bubble_model_path.set(self.settings['ocr'].get('custom_model_path', '')) | |
| self.bubble_model_entry.config( | |
| state='normal', | |
| bg='#2b2b2b', | |
| readonlybackground='#2b2b2b' | |
| ) | |
| # Show browse/clear buttons for custom | |
| self.bubble_browse_btn.pack(side='left') | |
| self.bubble_clear_btn.pack(side='left', padx=(5, 0)) | |
| # Hide download button | |
| self.rtdetr_download_btn.pack_forget() | |
| elif detector in self.detector_models: | |
| # HuggingFace model | |
| url = self.detector_models[detector] | |
| self.bubble_model_path.set(url) | |
| # Make entry read-only for HuggingFace models | |
| self.bubble_model_entry.config( | |
| state='readonly', | |
| readonlybackground='#1e1e1e' | |
| ) | |
| # Hide browse/clear buttons for HuggingFace models | |
| self.bubble_browse_btn.pack_forget() | |
| self.bubble_clear_btn.pack_forget() | |
| # Show download button | |
| self.rtdetr_download_btn.pack(side='left', padx=(0, 5)) | |
| # Show/hide RT-DETR specific controls | |
| if 'RT-DETR' in detector or 'RTEDR_onnx' in detector: | |
| self.rtdetr_classes_frame.pack(fill='x', pady=(10, 0), after=self.rtdetr_load_btn.master) | |
| # Hide YOLO-only max det row | |
| self.yolo_maxdet_row.pack_forget() | |
| else: | |
| self.rtdetr_classes_frame.pack_forget() | |
| # Show YOLO-only max det row for YOLO models | |
| if 'YOLO' in detector or 'Yolo' in detector or 'yolo' in detector or detector == 'Custom Model': | |
| self.yolo_maxdet_row.pack(fill='x', pady=(6,0)) | |
| else: | |
| self.yolo_maxdet_row.pack_forget() | |
| # Always show settings frame | |
| self.yolo_settings_frame.pack(fill='x', pady=(10, 0)) | |
| # Update status | |
| self._update_bubble_status() | |
| def _download_rtdetr_model(self): | |
| """Download selected model""" | |
| try: | |
| detector = self.detector_type.get() | |
| model_url = self.bubble_model_path.get() | |
| self.rtdetr_status_label.config(text="Downloading...", fg='orange') | |
| self.dialog.update_idletasks() | |
| if 'RTEDR_onnx' in detector: | |
| from bubble_detector import BubbleDetector | |
| bd = BubbleDetector() | |
| if bd.load_rtdetr_onnx_model(model_id=model_url): | |
| self.rtdetr_status_label.config(text="✅ Downloaded", fg='green') | |
| messagebox.showinfo("Success", f"RTEDR_onnx model downloaded successfully!") | |
| else: | |
| self.rtdetr_status_label.config(text="❌ Failed", fg='red') | |
| messagebox.showerror("Error", f"Failed to download RTEDR_onnx model") | |
| elif 'RT-DETR' in detector: | |
| # RT-DETR handling (works fine) | |
| from bubble_detector import BubbleDetector | |
| bd = BubbleDetector() | |
| if bd.load_rtdetr_model(model_id=model_url): | |
| self.rtdetr_status_label.config(text="✅ Downloaded", fg='green') | |
| messagebox.showinfo("Success", f"RT-DETR model downloaded successfully!") | |
| else: | |
| self.rtdetr_status_label.config(text="❌ Failed", fg='red') | |
| messagebox.showerror("Error", f"Failed to download RT-DETR model") | |
| else: | |
| # FIX FOR YOLO: Download to a simpler local path | |
| from huggingface_hub import hf_hub_download | |
| import os | |
| # Create models directory | |
| models_dir = "models" | |
| os.makedirs(models_dir, exist_ok=True) | |
| # Define simple local filenames | |
| filename_map = { | |
| 'ogkalu/comic-speech-bubble-detector-yolov8m': 'comic-speech-bubble-detector.pt', | |
| 'ogkalu/comic-text-segmenter-yolov8m': 'comic-text-segmenter.pt', | |
| 'ogkalu/manga-text-detector-yolov8s': 'manga-text-detector.pt' | |
| } | |
| filename = filename_map.get(model_url, 'model.pt') | |
| # Download to cache first | |
| cached_path = hf_hub_download(repo_id=model_url, filename=filename) | |
| # Copy to local models directory with simple path | |
| import shutil | |
| local_path = os.path.join(models_dir, filename) | |
| shutil.copy2(cached_path, local_path) | |
| # Set the simple local path instead of the cache path | |
| self.bubble_model_path.set(local_path) | |
| self.rtdetr_status_label.config(text="✅ Downloaded", fg='green') | |
| messagebox.showinfo("Success", f"Model downloaded to:\n{local_path}") | |
| except ImportError: | |
| self.rtdetr_status_label.config(text="❌ Missing deps", fg='red') | |
| messagebox.showerror("Error", "Install: pip install huggingface-hub transformers") | |
| except Exception as e: | |
| self.rtdetr_status_label.config(text="❌ Error", fg='red') | |
| messagebox.showerror("Error", f"Download failed: {e}") | |
| def _check_rtdetr_status(self): | |
| """Check if model is already loaded""" | |
| try: | |
| from bubble_detector import BubbleDetector | |
| if hasattr(self.main_gui, 'manga_tab') and hasattr(self.main_gui.manga_tab, 'translator'): | |
| translator = self.main_gui.manga_tab.translator | |
| if hasattr(translator, 'bubble_detector') and translator.bubble_detector: | |
| if getattr(translator.bubble_detector, 'rtdetr_onnx_loaded', False): | |
| self.rtdetr_status_label.config(text="✅ Loaded", fg='green') | |
| return True | |
| if getattr(translator.bubble_detector, 'rtdetr_loaded', False): | |
| self.rtdetr_status_label.config(text="✅ Loaded", fg='green') | |
| return True | |
| elif getattr(translator.bubble_detector, 'model_loaded', False): | |
| self.rtdetr_status_label.config(text="✅ Loaded", fg='green') | |
| return True | |
| self.rtdetr_status_label.config(text="Not loaded", fg='gray') | |
| return False | |
| except ImportError: | |
| self.rtdetr_status_label.config(text="❌ Missing deps", fg='red') | |
| return False | |
| except Exception: | |
| self.rtdetr_status_label.config(text="Not loaded", fg='gray') | |
| return False | |
| def _load_rtdetr_model(self): | |
| """Load selected model""" | |
| try: | |
| from bubble_detector import BubbleDetector | |
| self.rtdetr_status_label.config(text="Loading...", fg='orange') | |
| self.dialog.update_idletasks() | |
| bd = BubbleDetector() | |
| detector = self.detector_type.get() | |
| model_path = self.bubble_model_path.get() | |
| if 'RTEDR_onnx' in detector: | |
| # RT-DETR (ONNX) uses repo id directly | |
| if bd.load_rtdetr_onnx_model(model_id=model_path): | |
| self.rtdetr_status_label.config(text="✅ Ready", fg='green') | |
| messagebox.showinfo("Success", f"RTEDR_onnx model loaded successfully!") | |
| else: | |
| self.rtdetr_status_label.config(text="❌ Failed", fg='red') | |
| elif 'RT-DETR' in detector: | |
| # RT-DETR uses model_id directly | |
| if bd.load_rtdetr_model(model_id=model_path): | |
| self.rtdetr_status_label.config(text="✅ Ready", fg='green') | |
| messagebox.showinfo("Success", f"RT-DETR model loaded successfully!") | |
| else: | |
| self.rtdetr_status_label.config(text="❌ Failed", fg='red') | |
| else: | |
| # YOLOv8 - CHECK LOCAL MODELS FOLDER FIRST | |
| if model_path.startswith('ogkalu/'): | |
| # It's a HuggingFace ID - check if already downloaded | |
| filename_map = { | |
| 'ogkalu/comic-speech-bubble-detector-yolov8m': 'comic-speech-bubble-detector.pt', | |
| 'ogkalu/comic-text-segmenter-yolov8m': 'comic-text-segmenter.pt', | |
| 'ogkalu/manga-text-detector-yolov8s': 'manga-text-detector.pt' | |
| } | |
| filename = filename_map.get(model_path, 'model.pt') | |
| local_path = os.path.join('models', filename) | |
| # Check if it exists locally | |
| if os.path.exists(local_path): | |
| # Use the local file | |
| model_path = local_path | |
| self.bubble_model_path.set(local_path) # Update the field | |
| else: | |
| # Not downloaded yet | |
| messagebox.showwarning("Download Required", | |
| f"Model not found locally.\nPlease download it first using the Download button.") | |
| self.rtdetr_status_label.config(text="❌ Not downloaded", fg='orange') | |
| return | |
| # Now model_path should be a local file | |
| if not os.path.exists(model_path): | |
| messagebox.showerror("Error", f"Model file not found: {model_path}") | |
| self.rtdetr_status_label.config(text="❌ File not found", fg='red') | |
| return | |
| # Load the YOLOv8 model from local file | |
| if bd.load_model(model_path): | |
| self.rtdetr_status_label.config(text="✅ Ready", fg='green') | |
| messagebox.showinfo("Success", f"YOLOv8 model loaded successfully!") | |
| # Auto-convert to ONNX if enabled | |
| if os.environ.get('AUTO_CONVERT_TO_ONNX', 'true').lower() == 'true': | |
| onnx_path = model_path.replace('.pt', '.onnx') | |
| if not os.path.exists(onnx_path): | |
| if bd.convert_to_onnx(model_path, onnx_path): | |
| logger.info(f"✅ Converted to ONNX: {onnx_path}") | |
| else: | |
| self.rtdetr_status_label.config(text="❌ Failed", fg='red') | |
| except ImportError: | |
| self.rtdetr_status_label.config(text="❌ Missing deps", fg='red') | |
| messagebox.showerror("Error", "Install transformers: pip install transformers") | |
| except Exception as e: | |
| self.rtdetr_status_label.config(text="❌ Error", fg='red') | |
| messagebox.showerror("Error", f"Failed to load: {e}") | |
| def _toggle_bubble_controls(self): | |
| """Enable/disable bubble detection controls""" | |
| enabled = self.bubble_detection_enabled.get() | |
| if enabled: | |
| # Enable controls | |
| for widget in self.bubble_controls: | |
| try: | |
| widget.config(state='normal') | |
| except: | |
| pass | |
| # Show/hide frames based on detector type | |
| self._on_detector_type_changed() | |
| else: | |
| # Disable controls | |
| for widget in self.bubble_controls: | |
| try: | |
| widget.config(state='disabled') | |
| except: | |
| pass | |
| # Hide frames | |
| self.yolo_settings_frame.pack_forget() | |
| self.bubble_status_label.config(text="") | |
| def _browse_bubble_model(self): | |
| """Browse for model file""" | |
| from tkinter import filedialog | |
| path = filedialog.askopenfilename( | |
| title="Select Model File", | |
| filetypes=[ | |
| ("Model files", "*.pt;*.pth;*.bin;*.safetensors"), | |
| ("All files", "*.*") | |
| ] | |
| ) | |
| if path: | |
| self.bubble_model_path.set(path) | |
| self._update_bubble_status() | |
| def _clear_bubble_model(self): | |
| """Clear selected model""" | |
| self.bubble_model_path.set("") | |
| self._update_bubble_status() | |
| def _update_bubble_status(self): | |
| """Update bubble model status label""" | |
| if not self.bubble_detection_enabled.get(): | |
| self.bubble_status_label.config(text="") | |
| return | |
| detector = self.detector_type.get() | |
| model_path = self.bubble_model_path.get() | |
| if not model_path: | |
| self.bubble_status_label.config(text="⚠️ No model selected", fg='orange') | |
| return | |
| if model_path.startswith("ogkalu/"): | |
| self.bubble_status_label.config(text=f"📥 {detector} ready to download", fg='blue') | |
| elif os.path.exists(model_path): | |
| self.bubble_status_label.config(text="✅ Model file ready", fg='green') | |
| else: | |
| self.bubble_status_label.config(text="❌ Model file not found", fg='red') | |
| def _update_azure_label(self): | |
| """Update Azure multiplier label""" | |
| value = self.azure_merge_multiplier.get() | |
| self.azure_label.config(text=f"{value:.1f}x") | |
| def _set_azure_multiplier(self, value): | |
| """Set Azure multiplier from preset""" | |
| self.azure_merge_multiplier.set(value) | |
| self._update_azure_label() | |
| def _create_advanced_tab(self, notebook): | |
| """Create advanced settings tab with all options""" | |
| frame = ttk.Frame(notebook) | |
| notebook.add(frame, text="Advanced") | |
| # Main content | |
| content_frame = tk.Frame(frame) | |
| content_frame.pack(fill='both', expand=True, padx=5, pady=5) | |
| # Format detection | |
| detect_frame = tk.LabelFrame(content_frame, text="Format Detection", padx=15, pady=10) | |
| detect_frame.pack(fill='x', padx=20, pady=20) | |
| self.format_detection = tk.IntVar(value=1 if self.settings['advanced']['format_detection'] else 0) | |
| tb.Checkbutton( | |
| detect_frame, | |
| text="Enable automatic manga format detection (reading direction)", | |
| variable=self.format_detection, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # Webtoon mode | |
| webtoon_frame = tk.Frame(detect_frame) | |
| webtoon_frame.pack(fill='x', pady=(10, 0)) | |
| tk.Label(webtoon_frame, text="Webtoon Mode:", width=20, anchor='w').pack(side='left') | |
| self.webtoon_mode = tk.StringVar(value=self.settings['advanced']['webtoon_mode']) | |
| webtoon_combo = ttk.Combobox( | |
| webtoon_frame, | |
| textvariable=self.webtoon_mode, | |
| values=['auto', 'enabled', 'disabled'], | |
| state='readonly', | |
| width=15 | |
| ) | |
| webtoon_combo.pack(side='left', padx=10) | |
| # Debug settings | |
| debug_frame = tk.LabelFrame(content_frame, text="Debug Options", padx=15, pady=10) | |
| debug_frame.pack(fill='x', padx=20, pady=(0, 20)) | |
| self.debug_mode = tk.IntVar(value=1 if self.settings['advanced']['debug_mode'] else 0) | |
| tb.Checkbutton( | |
| debug_frame, | |
| text="Enable debug mode (verbose logging)", | |
| variable=self.debug_mode, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # New: Concise pipeline logs (reduce noise) | |
| self.concise_logs_var = tk.BooleanVar(value=bool(self.settings.get('advanced', {}).get('concise_logs', True))) | |
| def _save_concise(): | |
| try: | |
| self.settings.setdefault('advanced', {})['concise_logs'] = bool(self.concise_logs_var.get()) | |
| if hasattr(self, 'config'): | |
| self.config['manga_settings'] = self.settings | |
| if hasattr(self.main_gui, 'save_config'): | |
| self.main_gui.save_config(show_message=False) | |
| except Exception: | |
| pass | |
| tb.Checkbutton( | |
| debug_frame, | |
| text="Concise pipeline logs (reduce noise)", | |
| variable=self.concise_logs_var, | |
| command=_save_concise, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w', pady=(5, 0)) | |
| self.save_intermediate = tk.IntVar(value=1 if self.settings['advanced']['save_intermediate'] else 0) | |
| tb.Checkbutton( | |
| debug_frame, | |
| text="Save intermediate images (preprocessed, detection overlays)", | |
| variable=self.save_intermediate, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w', pady=(5, 0)) | |
| # Performance settings | |
| perf_frame = tk.LabelFrame(content_frame, text="Performance", padx=15, pady=10) | |
| # Defer packing until after memory_frame so this section appears below it | |
| # New: Parallel rendering (per-region overlays) | |
| self.render_parallel_var = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('render_parallel', True) | |
| ) | |
| tb.Checkbutton( | |
| perf_frame, | |
| text="Enable parallel rendering (per-region overlays)", | |
| variable=self.render_parallel_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| self.parallel_processing = tk.IntVar(value=1 if self.settings['advanced']['parallel_processing'] else 0) | |
| parallel_cb = tb.Checkbutton( | |
| perf_frame, | |
| text="Enable parallel processing (experimental)", | |
| variable=self.parallel_processing, | |
| bootstyle="round-toggle", | |
| command=self._toggle_workers | |
| ) | |
| parallel_cb.pack(anchor='w') | |
| # Max workers | |
| workers_frame = tk.Frame(perf_frame) | |
| workers_frame.pack(fill='x', pady=(10, 0)) | |
| self.workers_label = tk.Label(workers_frame, text="Max Workers:", width=20, anchor='w') | |
| self.workers_label.pack(side='left') | |
| self.max_workers = tk.IntVar(value=self.settings['advanced']['max_workers']) | |
| self.workers_spinbox = tb.Spinbox( | |
| workers_frame, | |
| from_=1, | |
| to=8, | |
| textvariable=self.max_workers, | |
| increment=1, | |
| width=10 | |
| ) | |
| self.workers_spinbox.pack(side='left', padx=10) | |
| tk.Label(workers_frame, text="(threads for parallel processing)").pack(side='left') | |
| # Initialize workers state | |
| self._toggle_workers() | |
| # Memory management section | |
| memory_frame = tk.LabelFrame(content_frame, text="Memory Management", padx=15, pady=10) | |
| memory_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # Now pack performance BELOW memory management | |
| perf_frame.pack(fill='x', padx=20) | |
| # Singleton mode for model instances | |
| self.use_singleton_models = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('use_singleton_models', True) | |
| ) | |
| def _toggle_singleton_mode(): | |
| """Disable LOCAL parallel processing options when singleton mode is enabled. | |
| Note: This does NOT affect parallel API calls (batch translation). | |
| """ | |
| # Update settings immediately to avoid background preloads | |
| try: | |
| if 'advanced' not in self.settings: | |
| self.settings['advanced'] = {} | |
| if self.use_singleton_models.get(): | |
| # Turn off local parallelism and panel preloads | |
| self.settings['advanced']['parallel_processing'] = False | |
| self.settings['advanced']['parallel_panel_translation'] = False | |
| self.settings['advanced']['preload_local_inpainting_for_panels'] = False | |
| # Persist to config if available | |
| if hasattr(self, 'config'): | |
| self.config['manga_settings'] = self.settings | |
| if hasattr(self.main_gui, 'save_config'): | |
| self.main_gui.save_config(show_message=False) | |
| except Exception: | |
| pass | |
| if self.use_singleton_models.get(): | |
| # Disable LOCAL parallel processing toggles (but NOT API batch translation) | |
| self.parallel_processing.set(0) | |
| self.parallel_panel_var.set(False) | |
| # Disable the UI elements for LOCAL parallel processing | |
| parallel_cb.config(state='disabled') | |
| panel_cb.config(state='disabled') | |
| # Also disable the spinboxes | |
| self.workers_spinbox.config(state='disabled') | |
| panel_workers_spinbox.config(state='disabled') | |
| panel_stagger_spinbox.config(state='disabled') | |
| else: | |
| # Re-enable the UI elements | |
| parallel_cb.config(state='normal') | |
| panel_cb.config(state='normal') | |
| # Re-enable spinboxes based on their toggle states | |
| self._toggle_workers() | |
| _toggle_panel_controls() | |
| singleton_cb = tb.Checkbutton( | |
| memory_frame, | |
| text="Use single model instances (saves RAM, only affects local models)", | |
| variable=self.use_singleton_models, | |
| bootstyle="round-toggle", | |
| command=_toggle_singleton_mode | |
| ) | |
| singleton_cb.pack(anchor='w') | |
| singleton_note = tk.Label( | |
| memory_frame, | |
| text="When enabled: One bubble detector & one inpainter shared across all images.\n" | |
| "When disabled: Each thread/image can have its own models (uses more RAM).\n" | |
| "✅ Batch API translation remains fully functional with singleton mode enabled.", | |
| font=('Arial', 9), | |
| fg='gray', | |
| justify='left' | |
| ) | |
| singleton_note.pack(anchor='w', pady=(2, 10), padx=(20, 0)) | |
| self.auto_cleanup_models = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('auto_cleanup_models', False) | |
| ) | |
| cleanup_cb = tb.Checkbutton( | |
| memory_frame, | |
| text="Automatically cleanup models after translation to free RAM", | |
| variable=self.auto_cleanup_models, | |
| bootstyle="round-toggle" | |
| ) | |
| cleanup_cb.pack(anchor='w') | |
| # Unload models after translation (disabled by default) | |
| self.unload_models_var = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('unload_models_after_translation', False) | |
| ) | |
| unload_cb = tb.Checkbutton( | |
| memory_frame, | |
| text="Unload models after translation (reset translator instance)", | |
| variable=self.unload_models_var, | |
| bootstyle="round-toggle" | |
| ) | |
| unload_cb.pack(anchor='w', pady=(4,0)) | |
| # Add a note about parallel processing | |
| note_label = tk.Label( | |
| memory_frame, | |
| text="Note: When parallel panel translation is enabled, cleanup happens after ALL panels complete.", | |
| font=('Arial', 9), | |
| fg='gray', | |
| wraplength=450 | |
| ) | |
| note_label.pack(anchor='w', pady=(5, 0), padx=(20, 0)) | |
| # Panel-level parallel translation | |
| panel_frame = tk.LabelFrame(content_frame, text="Parallel Panel Translation", padx=15, pady=10) | |
| panel_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| # New: Preload local inpainting for panels (default ON) | |
| preload_row = tk.Frame(panel_frame) | |
| preload_row.pack(fill='x', pady=5) | |
| self.preload_local_panels_var = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('preload_local_inpainting_for_panels', True) | |
| ) | |
| tb.Checkbutton( | |
| preload_row, | |
| text="Preload local inpainting instances for panel-parallel runs", | |
| variable=self.preload_local_panels_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| self.parallel_panel_var = tk.BooleanVar( | |
| value=self.settings.get('advanced', {}).get('parallel_panel_translation', False) | |
| ) | |
| def _toggle_panel_controls(): | |
| """Enable/disable panel spinboxes based on panel parallel toggle""" | |
| if self.parallel_panel_var.get() and not self.use_singleton_models.get(): | |
| panel_workers_spinbox.config(state='normal') | |
| panel_stagger_spinbox.config(state='normal') | |
| else: | |
| panel_workers_spinbox.config(state='disabled') | |
| panel_stagger_spinbox.config(state='disabled') | |
| panel_cb = tb.Checkbutton( | |
| panel_frame, | |
| text="Enable parallel panel translation (process multiple images concurrently)", | |
| variable=self.parallel_panel_var, | |
| bootstyle="round-toggle", | |
| command=_toggle_panel_controls | |
| ) | |
| panel_cb.pack(anchor='w') | |
| # Inpainting Performance (moved from Inpainting tab) | |
| inpaint_perf = tk.LabelFrame(perf_frame, text="Inpainting Performance", padx=15, pady=10) | |
| inpaint_perf.pack(fill='x', padx=0, pady=(10,0)) | |
| inpaint_bs_row = tk.Frame(inpaint_perf) | |
| inpaint_bs_row.pack(fill='x', pady=5) | |
| tk.Label(inpaint_bs_row, text="Batch Size:", width=20, anchor='w').pack(side='left') | |
| self.inpaint_batch_size = getattr(self, 'inpaint_batch_size', tk.IntVar(value=self.settings.get('inpainting', {}).get('batch_size', 10))) | |
| tb.Spinbox( | |
| inpaint_bs_row, | |
| from_=1, | |
| to=32, | |
| textvariable=self.inpaint_batch_size, | |
| width=10 | |
| ).pack(side='left', padx=10) | |
| tk.Label(inpaint_bs_row, text="(process multiple regions at once)", font=('Arial',9), fg='gray').pack(side='left') | |
| cache_row = tk.Frame(inpaint_perf) | |
| cache_row.pack(fill='x', pady=5) | |
| self.enable_cache_var = getattr(self, 'enable_cache_var', tk.BooleanVar(value=self.settings.get('inpainting', {}).get('enable_cache', True))) | |
| tb.Checkbutton( | |
| cache_row, | |
| text="Enable inpainting cache (speeds up repeated processing)", | |
| variable=self.enable_cache_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| panels_row = tk.Frame(panel_frame) | |
| panels_row.pack(fill='x', pady=5) | |
| tk.Label(panels_row, text="Max concurrent panels:", width=20, anchor='w').pack(side='left') | |
| self.panel_max_workers_var = tk.IntVar( | |
| value=self.settings.get('advanced', {}).get('panel_max_workers', 2) | |
| ) | |
| panel_workers_spinbox = tb.Spinbox( | |
| panels_row, | |
| from_=1, | |
| to=12, | |
| textvariable=self.panel_max_workers_var, | |
| width=10 | |
| ) | |
| panel_workers_spinbox.pack(side='left', padx=10) | |
| # Panel start stagger (ms) | |
| stagger_row = tk.Frame(panel_frame) | |
| stagger_row.pack(fill='x', pady=5) | |
| tk.Label(stagger_row, text="Panel start stagger:", width=20, anchor='w').pack(side='left') | |
| self.panel_stagger_ms_var = tk.IntVar( | |
| value=self.settings.get('advanced', {}).get('panel_start_stagger_ms', 30) | |
| ) | |
| panel_stagger_spinbox = tb.Spinbox( | |
| stagger_row, | |
| from_=0, | |
| to=1000, | |
| textvariable=self.panel_stagger_ms_var, | |
| width=10 | |
| ) | |
| panel_stagger_spinbox.pack(side='left', padx=10) | |
| tk.Label(stagger_row, text="ms").pack(side='left') | |
| # Initialize control states | |
| _toggle_panel_controls() # Initialize panel spinbox states | |
| _toggle_singleton_mode() # Initialize singleton mode state (may override above) | |
| # ONNX conversion settings | |
| onnx_frame = tk.LabelFrame(content_frame, text="ONNX Conversion", padx=15, pady=10) | |
| onnx_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.auto_convert_onnx_var = tk.BooleanVar(value=self.settings['advanced'].get('auto_convert_to_onnx', False)) | |
| self.auto_convert_onnx_bg_var = tk.BooleanVar(value=self.settings['advanced'].get('auto_convert_to_onnx_background', True)) | |
| def _toggle_onnx_controls(): | |
| # If auto-convert is off, background toggle should be disabled | |
| state = 'normal' if self.auto_convert_onnx_var.get() else 'disabled' | |
| try: | |
| bg_cb.config(state=state) | |
| except Exception: | |
| pass | |
| auto_cb = tb.Checkbutton( | |
| onnx_frame, | |
| text="Auto-convert local models to ONNX for faster inference (recommended)", | |
| variable=self.auto_convert_onnx_var, | |
| bootstyle="round-toggle", | |
| command=_toggle_onnx_controls | |
| ) | |
| auto_cb.pack(anchor='w') | |
| bg_cb = tb.Checkbutton( | |
| onnx_frame, | |
| text="Convert in background (non-blocking; switches to ONNX when ready)", | |
| variable=self.auto_convert_onnx_bg_var, | |
| bootstyle="round-toggle" | |
| ) | |
| bg_cb.pack(anchor='w', pady=(5, 0)) | |
| _toggle_onnx_controls() | |
| # Model memory optimization (quantization) | |
| quant_frame = tk.LabelFrame(content_frame, text="Model Memory Optimization", padx=15, pady=10) | |
| quant_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.quantize_models_var = tk.BooleanVar(value=self.settings['advanced'].get('quantize_models', False)) | |
| tb.Checkbutton( | |
| quant_frame, | |
| text="Reduce RAM with quantized models (global switch)", | |
| variable=self.quantize_models_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # ONNX quantize sub-toggle | |
| onnx_row = tk.Frame(quant_frame) | |
| onnx_row.pack(fill='x', pady=(6, 0)) | |
| self.onnx_quantize_var = tk.BooleanVar(value=self.settings['advanced'].get('onnx_quantize', False)) | |
| tb.Checkbutton( | |
| onnx_row, | |
| text="Quantize ONNX models to INT8 (dynamic)", | |
| variable=self.onnx_quantize_var, | |
| bootstyle="round-toggle" | |
| ).pack(side='left') | |
| tk.Label(onnx_row, text="(lower RAM/CPU; slight accuracy trade-off)", font=('Arial', 9), fg='gray').pack(side='left', padx=8) | |
| # Torch precision dropdown | |
| precision_row = tk.Frame(quant_frame) | |
| precision_row.pack(fill='x', pady=(6, 0)) | |
| tk.Label(precision_row, text="Torch precision:", width=20, anchor='w').pack(side='left') | |
| self.torch_precision_var = tk.StringVar(value=self.settings['advanced'].get('torch_precision', 'fp16')) | |
| ttk.Combobox( | |
| precision_row, | |
| textvariable=self.torch_precision_var, | |
| values=['fp16', 'fp32', 'auto'], | |
| state='readonly', | |
| width=10 | |
| ).pack(side='left', padx=10) | |
| tk.Label(precision_row, text="(fp16 only, since fp32 is currently bugged)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Aggressive memory cleanup | |
| cleanup_frame = tk.LabelFrame(content_frame, text="Memory & Cleanup", padx=15, pady=10) | |
| cleanup_frame.pack(fill='x', padx=20, pady=(10, 0)) | |
| self.force_deep_cleanup_var = tk.BooleanVar(value=self.settings.get('advanced', {}).get('force_deep_cleanup_each_image', False)) | |
| tb.Checkbutton( | |
| cleanup_frame, | |
| text="Force deep model cleanup after every image (slowest, lowest RAM)", | |
| variable=self.force_deep_cleanup_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| tk.Label(cleanup_frame, text="Also clears shared caches at batch end.", font=('Arial', 9), fg='gray').pack(anchor='w', padx=(0,0), pady=(2,0)) | |
| # RAM cap controls | |
| ramcap_frame = tk.Frame(cleanup_frame) | |
| ramcap_frame.pack(fill='x', pady=(10, 0)) | |
| self.ram_cap_enabled_var = tk.BooleanVar(value=self.settings.get('advanced', {}).get('ram_cap_enabled', False)) | |
| tb.Checkbutton( | |
| ramcap_frame, | |
| text="Enable RAM cap", | |
| variable=self.ram_cap_enabled_var, | |
| bootstyle="round-toggle" | |
| ).pack(anchor='w') | |
| # RAM cap value | |
| ramcap_value_row = tk.Frame(cleanup_frame) | |
| ramcap_value_row.pack(fill='x', pady=5) | |
| tk.Label(ramcap_value_row, text="Max RAM (MB):", width=20, anchor='w').pack(side='left') | |
| self.ram_cap_mb_var = tk.IntVar(value=int(self.settings.get('advanced', {}).get('ram_cap_mb', 0) or 0)) | |
| tb.Spinbox( | |
| ramcap_value_row, | |
| from_=512, | |
| to=131072, | |
| textvariable=self.ram_cap_mb_var, | |
| width=12 | |
| ).pack(side='left', padx=10) | |
| tk.Label(ramcap_value_row, text="(0 = disabled)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # RAM cap mode | |
| ramcap_mode_row = tk.Frame(cleanup_frame) | |
| ramcap_mode_row.pack(fill='x', pady=(5, 0)) | |
| tk.Label(ramcap_mode_row, text="Cap mode:", width=20, anchor='w').pack(side='left') | |
| self.ram_cap_mode_var = tk.StringVar(value=self.settings.get('advanced', {}).get('ram_cap_mode', 'soft')) | |
| ttk.Combobox( | |
| ramcap_mode_row, | |
| textvariable=self.ram_cap_mode_var, | |
| values=['soft', 'hard (Windows only)'], | |
| state='readonly', | |
| width=20 | |
| ).pack(side='left', padx=10) | |
| tk.Label(ramcap_mode_row, text="Soft = clean/trim, Hard = OS-enforced (may OOM)", font=('Arial', 9), fg='gray').pack(side='left') | |
| # Advanced RAM gate tuning | |
| gate_row = tk.Frame(cleanup_frame) | |
| gate_row.pack(fill='x', pady=(5, 0)) | |
| tk.Label(gate_row, text="Gate timeout (sec):", width=20, anchor='w').pack(side='left') | |
| self.ram_gate_timeout_var = tk.DoubleVar(value=float(self.settings.get('advanced', {}).get('ram_gate_timeout_sec', 10.0))) | |
| tb.Spinbox( | |
| gate_row, | |
| from_=2.0, | |
| to=60.0, | |
| increment=0.5, | |
| textvariable=self.ram_gate_timeout_var, | |
| width=12 | |
| ).pack(side='left', padx=10) | |
| floor_row = tk.Frame(cleanup_frame) | |
| floor_row.pack(fill='x', pady=(5, 0)) | |
| tk.Label(floor_row, text="Gate floor over baseline (MB):", width=25, anchor='w').pack(side='left') | |
| self.ram_gate_floor_var = tk.IntVar(value=int(self.settings.get('advanced', {}).get('ram_min_floor_over_baseline_mb', 128))) | |
| tb.Spinbox( | |
| floor_row, | |
| from_=64, | |
| to=2048, | |
| textvariable=self.ram_gate_floor_var, | |
| width=12 | |
| ).pack(side='left', padx=10) | |
| def _toggle_workers(self): | |
| """Enable/disable worker settings based on parallel processing toggle""" | |
| enabled = bool(self.parallel_processing.get()) | |
| self.workers_spinbox.config(state='normal' if enabled else 'disabled') | |
| self.workers_label.config(fg='white' if enabled else 'gray') | |
| def _apply_defaults_to_controls(self): | |
| """Apply default values to all visible Tk variables/controls across tabs without rebuilding the dialog.""" | |
| try: | |
| # Use current in-memory settings (which we set to defaults above) | |
| s = self.settings if isinstance(getattr(self, 'settings', None), dict) else self.default_settings | |
| pre = s.get('preprocessing', {}) | |
| comp = s.get('compression', {}) | |
| ocr = s.get('ocr', {}) | |
| adv = s.get('advanced', {}) | |
| inp = s.get('inpainting', {}) | |
| font = s.get('font_sizing', {}) | |
| # Preprocessing | |
| if hasattr(self, 'preprocess_enabled'): self.preprocess_enabled.set(bool(pre.get('enabled', False))) | |
| if hasattr(self, 'auto_detect'): self.auto_detect.set(bool(pre.get('auto_detect_quality', True))) | |
| if hasattr(self, 'contrast_threshold'): self.contrast_threshold.set(float(pre.get('contrast_threshold', 0.4))) | |
| if hasattr(self, 'sharpness_threshold'): self.sharpness_threshold.set(float(pre.get('sharpness_threshold', 0.3))) | |
| if hasattr(self, 'enhancement_strength'): self.enhancement_strength.set(float(pre.get('enhancement_strength', 1.5))) | |
| if hasattr(self, 'noise_threshold'): self.noise_threshold.set(int(pre.get('noise_threshold', 20))) | |
| if hasattr(self, 'denoise_strength'): self.denoise_strength.set(int(pre.get('denoise_strength', 10))) | |
| if hasattr(self, 'max_dimension'): self.max_dimension.set(int(pre.get('max_image_dimension', 2000))) | |
| if hasattr(self, 'max_pixels'): self.max_pixels.set(int(pre.get('max_image_pixels', 2000000))) | |
| if hasattr(self, 'chunk_height'): self.chunk_height.set(int(pre.get('chunk_height', 1000))) | |
| if hasattr(self, 'chunk_overlap'): self.chunk_overlap.set(int(pre.get('chunk_overlap', 100))) | |
| # Compression | |
| if hasattr(self, 'compression_enabled_var'): self.compression_enabled_var.set(bool(comp.get('enabled', False))) | |
| if hasattr(self, 'compression_format_var'): self.compression_format_var.set(str(comp.get('format', 'jpeg'))) | |
| if hasattr(self, 'jpeg_quality_var'): self.jpeg_quality_var.set(int(comp.get('jpeg_quality', 85))) | |
| if hasattr(self, 'png_level_var'): self.png_level_var.set(int(comp.get('png_compress_level', 6))) | |
| if hasattr(self, 'webp_quality_var'): self.webp_quality_var.set(int(comp.get('webp_quality', 85))) | |
| # Tiling | |
| if hasattr(self, 'inpaint_tiling_enabled'): self.inpaint_tiling_enabled.set(bool(pre.get('inpaint_tiling_enabled', False))) | |
| if hasattr(self, 'inpaint_tile_size'): self.inpaint_tile_size.set(int(pre.get('inpaint_tile_size', 512))) | |
| if hasattr(self, 'inpaint_tile_overlap'): self.inpaint_tile_overlap.set(int(pre.get('inpaint_tile_overlap', 64))) | |
| # OCR basic | |
| if hasattr(self, 'confidence_threshold'): self.confidence_threshold.set(float(ocr.get('confidence_threshold', 0.7))) | |
| if hasattr(self, 'detection_mode'): self.detection_mode.set(str(ocr.get('text_detection_mode', 'document'))) | |
| if hasattr(self, 'merge_nearby_threshold'): self.merge_nearby_threshold.set(int(ocr.get('merge_nearby_threshold', 20))) | |
| if hasattr(self, 'enable_rotation'): self.enable_rotation.set(bool(ocr.get('enable_rotation_correction', True))) | |
| # Language checkboxes | |
| try: | |
| if hasattr(self, 'lang_vars') and isinstance(self.lang_vars, dict): | |
| langs = set(ocr.get('language_hints', ['ja', 'ko', 'zh'])) | |
| for code, var in self.lang_vars.items(): | |
| var.set(code in langs) | |
| except Exception: | |
| pass | |
| # OCR batching/locality | |
| if hasattr(self, 'ocr_batch_enabled_var'): self.ocr_batch_enabled_var.set(bool(ocr.get('ocr_batch_enabled', True))) | |
| if hasattr(self, 'ocr_batch_size_var'): self.ocr_batch_size_var.set(int(ocr.get('ocr_batch_size', 8))) | |
| if hasattr(self, 'ocr_max_conc_var'): self.ocr_max_conc_var.set(int(ocr.get('ocr_max_concurrency', 2))) | |
| if hasattr(self, 'roi_locality_var'): self.roi_locality_var.set(bool(ocr.get('roi_locality_enabled', False))) | |
| if hasattr(self, 'roi_padding_ratio_var'): self.roi_padding_ratio_var.set(float(ocr.get('roi_padding_ratio', 0.08))) | |
| if hasattr(self, 'roi_min_side_var'): self.roi_min_side_var.set(int(ocr.get('roi_min_side_px', 12))) | |
| if hasattr(self, 'roi_min_area_var'): self.roi_min_area_var.set(int(ocr.get('roi_min_area_px', 100))) | |
| if hasattr(self, 'roi_max_side_var'): self.roi_max_side_var.set(int(ocr.get('roi_max_side', 0))) | |
| # English filters | |
| if hasattr(self, 'exclude_english_var'): self.exclude_english_var.set(bool(ocr.get('exclude_english_text', False))) | |
| if hasattr(self, 'english_exclude_threshold'): self.english_exclude_threshold.set(float(ocr.get('english_exclude_threshold', 0.7))) | |
| if hasattr(self, 'english_exclude_min_chars'): self.english_exclude_min_chars.set(int(ocr.get('english_exclude_min_chars', 4))) | |
| if hasattr(self, 'english_exclude_short_tokens'): self.english_exclude_short_tokens.set(bool(ocr.get('english_exclude_short_tokens', False))) | |
| # Azure | |
| if hasattr(self, 'azure_merge_multiplier'): self.azure_merge_multiplier.set(float(ocr.get('azure_merge_multiplier', 3.0))) | |
| if hasattr(self, 'azure_reading_order'): self.azure_reading_order.set(str(ocr.get('azure_reading_order', 'natural'))) | |
| if hasattr(self, 'azure_model_version'): self.azure_model_version.set(str(ocr.get('azure_model_version', 'latest'))) | |
| if hasattr(self, 'azure_max_wait'): self.azure_max_wait.set(int(ocr.get('azure_max_wait', 60))) | |
| if hasattr(self, 'azure_poll_interval'): self.azure_poll_interval.set(float(ocr.get('azure_poll_interval', 0.5))) | |
| try: | |
| self._update_azure_label() | |
| except Exception: | |
| pass | |
| # Bubble detector | |
| if hasattr(self, 'bubble_detection_enabled'): self.bubble_detection_enabled.set(bool(ocr.get('bubble_detection_enabled', False))) | |
| # Detector type mapping to UI labels | |
| if hasattr(self, 'detector_type'): | |
| dt = str(ocr.get('detector_type', 'rtdetr_onnx')) | |
| if dt == 'rtdetr_onnx': self.detector_type.set('RTEDR_onnx') | |
| elif dt == 'rtdetr': self.detector_type.set('RT-DETR') | |
| elif dt == 'yolo': self.detector_type.set('YOLOv8 Speech') | |
| elif dt == 'custom': self.detector_type.set('Custom Model') | |
| else: self.detector_type.set('RTEDR_onnx') | |
| if hasattr(self, 'bubble_model_path'): self.bubble_model_path.set(str(ocr.get('bubble_model_path', ''))) | |
| if hasattr(self, 'bubble_confidence'): self.bubble_confidence.set(float(ocr.get('bubble_confidence', 0.5))) | |
| if hasattr(self, 'detect_empty_bubbles'): self.detect_empty_bubbles.set(bool(ocr.get('detect_empty_bubbles', True))) | |
| if hasattr(self, 'detect_text_bubbles'): self.detect_text_bubbles.set(bool(ocr.get('detect_text_bubbles', True))) | |
| if hasattr(self, 'detect_free_text'): self.detect_free_text.set(bool(ocr.get('detect_free_text', True))) | |
| if hasattr(self, 'bubble_max_det_yolo_var'): self.bubble_max_det_yolo_var.set(int(ocr.get('bubble_max_detections_yolo', 100))) | |
| # Inpainting | |
| if hasattr(self, 'inpaint_batch_size'): self.inpaint_batch_size.set(int(inp.get('batch_size', 1))) | |
| if hasattr(self, 'enable_cache_var'): self.enable_cache_var.set(bool(inp.get('enable_cache', True))) | |
| if hasattr(self, 'mask_dilation_var'): self.mask_dilation_var.set(int(s.get('mask_dilation', 0))) | |
| if hasattr(self, 'use_all_iterations_var'): self.use_all_iterations_var.set(bool(s.get('use_all_iterations', True))) | |
| if hasattr(self, 'all_iterations_var'): self.all_iterations_var.set(int(s.get('all_iterations', 2))) | |
| if hasattr(self, 'text_bubble_iterations_var'): self.text_bubble_iterations_var.set(int(s.get('text_bubble_dilation_iterations', 2))) | |
| if hasattr(self, 'empty_bubble_iterations_var'): self.empty_bubble_iterations_var.set(int(s.get('empty_bubble_dilation_iterations', 3))) | |
| if hasattr(self, 'free_text_iterations_var'): self.free_text_iterations_var.set(int(s.get('free_text_dilation_iterations', 0))) | |
| # Advanced | |
| if hasattr(self, 'format_detection'): self.format_detection.set(1 if adv.get('format_detection', True) else 0) | |
| if hasattr(self, 'webtoon_mode'): self.webtoon_mode.set(str(adv.get('webtoon_mode', 'auto'))) | |
| if hasattr(self, 'debug_mode'): self.debug_mode.set(1 if adv.get('debug_mode', False) else 0) | |
| if hasattr(self, 'save_intermediate'): self.save_intermediate.set(1 if adv.get('save_intermediate', False) else 0) | |
| if hasattr(self, 'parallel_processing'): self.parallel_processing.set(1 if adv.get('parallel_processing', False) else 0) | |
| if hasattr(self, 'max_workers'): self.max_workers.set(int(adv.get('max_workers', 4))) | |
| if hasattr(self, 'use_singleton_models'): self.use_singleton_models.set(bool(adv.get('use_singleton_models', True))) | |
| if hasattr(self, 'auto_cleanup_models'): self.auto_cleanup_models.set(bool(adv.get('auto_cleanup_models', False))) | |
| if hasattr(self, 'unload_models_var'): self.unload_models_var.set(bool(adv.get('unload_models_after_translation', False))) | |
| if hasattr(self, 'parallel_panel_var'): self.parallel_panel_var.set(bool(adv.get('parallel_panel_translation', False))) | |
| if hasattr(self, 'panel_max_workers_var'): self.panel_max_workers_var.set(int(adv.get('panel_max_workers', 2))) | |
| if hasattr(self, 'panel_stagger_ms_var'): self.panel_stagger_ms_var.set(int(adv.get('panel_start_stagger_ms', 30))) | |
| # New: preload local inpainting for parallel panels (default True) | |
| if hasattr(self, 'preload_local_panels_var'): self.preload_local_panels_var.set(bool(adv.get('preload_local_inpainting_for_panels', True))) | |
| if hasattr(self, 'auto_convert_onnx_var'): self.auto_convert_onnx_var.set(bool(adv.get('auto_convert_to_onnx', False))) | |
| if hasattr(self, 'auto_convert_onnx_bg_var'): self.auto_convert_onnx_bg_var.set(bool(adv.get('auto_convert_to_onnx_background', True))) | |
| if hasattr(self, 'quantize_models_var'): self.quantize_models_var.set(bool(adv.get('quantize_models', False))) | |
| if hasattr(self, 'onnx_quantize_var'): self.onnx_quantize_var.set(bool(adv.get('onnx_quantize', False))) | |
| if hasattr(self, 'torch_precision_var'): self.torch_precision_var.set(str(adv.get('torch_precision', 'auto'))) | |
| # Font sizing tab | |
| if hasattr(self, 'font_algorithm_var'): self.font_algorithm_var.set(str(font.get('algorithm', 'smart'))) | |
| if hasattr(self, 'min_font_size_var'): self.min_font_size_var.set(int(font.get('min_size', 10))) | |
| if hasattr(self, 'max_font_size_var'): self.max_font_size_var.set(int(font.get('max_size', 40))) | |
| if hasattr(self, 'min_readable_var'): self.min_readable_var.set(int(font.get('min_readable', 14))) | |
| if hasattr(self, 'prefer_larger_var'): self.prefer_larger_var.set(bool(font.get('prefer_larger', True))) | |
| if hasattr(self, 'bubble_size_factor_var'): self.bubble_size_factor_var.set(bool(font.get('bubble_size_factor', True))) | |
| if hasattr(self, 'line_spacing_var'): self.line_spacing_var.set(float(font.get('line_spacing', 1.3))) | |
| if hasattr(self, 'max_lines_var'): self.max_lines_var.set(int(font.get('max_lines', 10))) | |
| try: | |
| if hasattr(self, '_on_font_mode_change'): | |
| self._on_font_mode_change() | |
| except Exception: | |
| pass | |
| # Rendering controls (if present in this dialog) | |
| if hasattr(self, 'font_size_mode_var'): self.font_size_mode_var.set(str(s.get('rendering', {}).get('font_size_mode', 'auto'))) | |
| if hasattr(self, 'fixed_font_size_var'): self.fixed_font_size_var.set(int(s.get('rendering', {}).get('fixed_font_size', 16))) | |
| if hasattr(self, 'font_scale_var'): self.font_scale_var.set(float(s.get('rendering', {}).get('font_scale', 1.0))) | |
| if hasattr(self, 'auto_fit_style_var'): self.auto_fit_style_var.set(str(s.get('rendering', {}).get('auto_fit_style', 'balanced'))) | |
| # Cloud API tab | |
| if hasattr(self, 'cloud_model_var'): self.cloud_model_var.set(str(s.get('cloud_inpaint_model', 'ideogram-v2'))) | |
| if hasattr(self, 'custom_version_var'): self.custom_version_var.set(str(s.get('cloud_custom_version', ''))) | |
| if hasattr(self, 'cloud_prompt_var'): self.cloud_prompt_var.set(str(s.get('cloud_inpaint_prompt', 'clean background, smooth surface'))) | |
| if hasattr(self, 'cloud_negative_prompt_var'): self.cloud_negative_prompt_var.set(str(s.get('cloud_negative_prompt', 'text, writing, letters'))) | |
| if hasattr(self, 'cloud_steps_var'): self.cloud_steps_var.set(int(s.get('cloud_inference_steps', 20))) | |
| if hasattr(self, 'cloud_timeout_var'): self.cloud_timeout_var.set(int(s.get('cloud_timeout', 60))) | |
| # Trigger dependent UI updates | |
| try: | |
| self._toggle_preprocessing() | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, '_on_cloud_model_change'): | |
| self._on_cloud_model_change() | |
| except Exception: | |
| pass | |
| try: | |
| self._toggle_iteration_controls() | |
| except Exception: | |
| pass | |
| try: | |
| self._toggle_roi_locality_controls() | |
| except Exception: | |
| pass | |
| try: | |
| self._toggle_workers() | |
| except Exception: | |
| pass | |
| # Build/attach advanced control for local inpainting preload if not present | |
| try: | |
| if not hasattr(self, 'preload_local_panels_var') and hasattr(self, '_create_advanced_tab_ui'): | |
| # If there is a helper to build advanced UI, we rely on it. Otherwise, attach to existing advanced frame if available. | |
| pass | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'compression_format_combo'): | |
| self._toggle_compression_format() | |
| except Exception: | |
| pass | |
| try: | |
| if hasattr(self, 'detector_type'): | |
| self._on_detector_type_changed() | |
| except Exception: | |
| pass | |
| try: | |
| self.dialog.update_idletasks() | |
| except Exception: | |
| pass | |
| except Exception: | |
| # Best-effort application only | |
| pass | |
| def _set_font_preset(self, preset: str): | |
| """Apply font sizing preset""" | |
| if preset == 'small': | |
| # For manga with small bubbles | |
| self.font_algorithm_var.set('conservative') | |
| self.min_font_size_var.set(8) | |
| self.max_font_size_var.set(24) | |
| self.min_readable_var.set(12) | |
| self.prefer_larger_var.set(False) | |
| self.bubble_size_factor_var.set(True) | |
| self.line_spacing_var.set(1.2) | |
| self.max_lines_var.set(8) | |
| elif preset == 'balanced': | |
| # Default balanced settings | |
| self.font_algorithm_var.set('smart') | |
| self.min_font_size_var.set(10) | |
| self.max_font_size_var.set(40) | |
| self.min_readable_var.set(14) | |
| self.prefer_larger_var.set(True) | |
| self.bubble_size_factor_var.set(True) | |
| self.line_spacing_var.set(1.3) | |
| self.max_lines_var.set(10) | |
| elif preset == 'large': | |
| # For maximum readability | |
| self.font_algorithm_var.set('aggressive') | |
| self.min_font_size_var.set(14) | |
| self.max_font_size_var.set(50) | |
| self.min_readable_var.set(16) | |
| self.prefer_larger_var.set(True) | |
| self.bubble_size_factor_var.set(False) | |
| self.line_spacing_var.set(1.4) | |
| self.max_lines_var.set(12) | |
| def _save_rendering_settings(self, *args): | |
| """Auto-save font and rendering settings when controls change""" | |
| # Don't save during initialization | |
| if hasattr(self, '_initializing') and self._initializing: | |
| return | |
| try: | |
| # Ensure rendering section exists in settings | |
| if 'rendering' not in self.settings: | |
| self.settings['rendering'] = {} | |
| # Save font size controls if they exist | |
| if hasattr(self, 'font_size_mode_var'): | |
| self.settings['rendering']['font_size_mode'] = self.font_size_mode_var.get() | |
| self.settings['rendering']['fixed_font_size'] = self.fixed_font_size_var.get() | |
| self.settings['rendering']['font_scale'] = self.font_scale_var.get() | |
| self.settings['rendering']['auto_fit_style'] = self.auto_fit_style_var.get() | |
| # Save min/max for auto mode | |
| if hasattr(self, 'min_font_size_var'): | |
| self.settings['rendering']['auto_min_size'] = self.min_font_size_var.get() | |
| if hasattr(self, 'max_font_size_var'): | |
| self.settings['rendering']['auto_max_size'] = self.max_font_size_var.get() | |
| # Update config | |
| self.config['manga_settings'] = self.settings | |
| # Mirror only auto max to top-level config for backward compatibility; keep min nested | |
| try: | |
| auto_max = self.settings.get('rendering', {}).get('auto_max_size', None) | |
| if auto_max is not None: | |
| self.config['manga_max_font_size'] = int(auto_max) | |
| except Exception: | |
| pass | |
| # Save to file immediately | |
| if hasattr(self.main_gui, 'save_config'): | |
| self.main_gui.save_config() | |
| print(f"Auto-saved rendering settings") | |
| time.sleep(0.1) # Brief pause for stability | |
| print("💤 Auto-save pausing briefly for stability") | |
| except Exception as e: | |
| print(f"Error auto-saving rendering settings: {e}") | |
| def _save_settings(self): | |
| """Save all settings including expanded iteration controls""" | |
| try: | |
| # Collect all preprocessing settings | |
| self.settings['preprocessing']['enabled'] = self.preprocess_enabled.get() | |
| self.settings['preprocessing']['auto_detect_quality'] = self.auto_detect.get() | |
| self.settings['preprocessing']['contrast_threshold'] = self.contrast_threshold.get() | |
| self.settings['preprocessing']['sharpness_threshold'] = self.sharpness_threshold.get() | |
| self.settings['preprocessing']['enhancement_strength'] = self.enhancement_strength.get() | |
| self.settings['preprocessing']['noise_threshold'] = self.noise_threshold.get() | |
| self.settings['preprocessing']['denoise_strength'] = self.denoise_strength.get() | |
| self.settings['preprocessing']['max_image_dimension'] = self.max_dimension.get() | |
| self.settings['preprocessing']['max_image_pixels'] = self.max_pixels.get() | |
| self.settings['preprocessing']['chunk_height'] = self.chunk_height.get() | |
| self.settings['preprocessing']['chunk_overlap'] = self.chunk_overlap.get() | |
| # Compression (saved separately from preprocessing) | |
| if 'compression' not in self.settings: | |
| self.settings['compression'] = {} | |
| self.settings['compression']['enabled'] = bool(self.compression_enabled_var.get()) if hasattr(self, 'compression_enabled_var') else False | |
| self.settings['compression']['format'] = str(self.compression_format_var.get()) if hasattr(self, 'compression_format_var') else 'jpeg' | |
| self.settings['compression']['jpeg_quality'] = int(self.jpeg_quality_var.get()) if hasattr(self, 'jpeg_quality_var') else 85 | |
| self.settings['compression']['png_compress_level'] = int(self.png_level_var.get()) if hasattr(self, 'png_level_var') else 6 | |
| self.settings['compression']['webp_quality'] = int(self.webp_quality_var.get()) if hasattr(self, 'webp_quality_var') else 85 | |
| # TILING SETTINGS - save under preprocessing (primary) and mirror under 'tiling' for backward compatibility | |
| self.settings['preprocessing']['inpaint_tiling_enabled'] = self.inpaint_tiling_enabled.get() | |
| self.settings['preprocessing']['inpaint_tile_size'] = self.inpaint_tile_size.get() | |
| self.settings['preprocessing']['inpaint_tile_overlap'] = self.inpaint_tile_overlap.get() | |
| # Back-compat mirror | |
| self.settings['tiling'] = { | |
| 'enabled': self.inpaint_tiling_enabled.get(), | |
| 'tile_size': self.inpaint_tile_size.get(), | |
| 'tile_overlap': self.inpaint_tile_overlap.get() | |
| } | |
| # OCR settings | |
| self.settings['ocr']['language_hints'] = [code for code, var in self.lang_vars.items() if var.get()] | |
| self.settings['ocr']['confidence_threshold'] = self.confidence_threshold.get() | |
| self.settings['ocr']['text_detection_mode'] = self.detection_mode.get() | |
| self.settings['ocr']['merge_nearby_threshold'] = self.merge_nearby_threshold.get() | |
| self.settings['ocr']['enable_rotation_correction'] = self.enable_rotation.get() | |
| self.settings['ocr']['azure_merge_multiplier'] = self.azure_merge_multiplier.get() | |
| self.settings['ocr']['azure_reading_order'] = self.azure_reading_order.get() | |
| self.settings['ocr']['azure_model_version'] = self.azure_model_version.get() | |
| self.settings['ocr']['azure_max_wait'] = self.azure_max_wait.get() | |
| self.settings['ocr']['azure_poll_interval'] = self.azure_poll_interval.get() | |
| self.settings['ocr']['min_text_length'] = self.min_text_length_var.get() | |
| self.settings['ocr']['exclude_english_text'] = self.exclude_english_var.get() | |
| self.settings['ocr']['roi_locality_enabled'] = bool(self.roi_locality_var.get()) if hasattr(self, 'roi_locality_var') else True | |
| # OCR batching & locality | |
| self.settings['ocr']['ocr_batch_enabled'] = bool(self.ocr_batch_enabled_var.get()) if hasattr(self, 'ocr_batch_enabled_var') else True | |
| self.settings['ocr']['ocr_batch_size'] = int(self.ocr_batch_size_var.get()) if hasattr(self, 'ocr_batch_size_var') else 8 | |
| self.settings['ocr']['ocr_max_concurrency'] = int(self.ocr_max_conc_var.get()) if hasattr(self, 'ocr_max_conc_var') else 2 | |
| self.settings['ocr']['roi_padding_ratio'] = float(self.roi_padding_ratio_var.get()) if hasattr(self, 'roi_padding_ratio_var') else 0.08 | |
| self.settings['ocr']['roi_min_side_px'] = int(self.roi_min_side_var.get()) if hasattr(self, 'roi_min_side_var') else 12 | |
| self.settings['ocr']['roi_min_area_px'] = int(self.roi_min_area_var.get()) if hasattr(self, 'roi_min_area_var') else 100 | |
| self.settings['ocr']['roi_max_side'] = int(self.roi_max_side_var.get()) if hasattr(self, 'roi_max_side_var') else 0 | |
| self.settings['ocr']['english_exclude_threshold'] = self.english_exclude_threshold.get() | |
| self.settings['ocr']['english_exclude_min_chars'] = self.english_exclude_min_chars.get() | |
| self.settings['ocr']['english_exclude_short_tokens'] = self.english_exclude_short_tokens.get() | |
| # Bubble detection settings | |
| self.settings['ocr']['bubble_detection_enabled'] = self.bubble_detection_enabled.get() | |
| self.settings['ocr']['bubble_model_path'] = self.bubble_model_path.get() | |
| self.settings['ocr']['bubble_confidence'] = self.bubble_confidence.get() | |
| self.settings['ocr']['rtdetr_confidence'] = self.bubble_confidence.get() | |
| self.settings['ocr']['detect_empty_bubbles'] = self.detect_empty_bubbles.get() | |
| self.settings['ocr']['detect_text_bubbles'] = self.detect_text_bubbles.get() | |
| self.settings['ocr']['detect_free_text'] = self.detect_free_text.get() | |
| self.settings['ocr']['rtdetr_model_url'] = self.bubble_model_path.get() | |
| self.settings['ocr']['bubble_max_detections_yolo'] = int(self.bubble_max_det_yolo_var.get()) | |
| # Save the detector type properly | |
| if hasattr(self, 'detector_type'): | |
| detector_display = self.detector_type.get() | |
| if 'RTEDR_onnx' in detector_display or 'ONNX' in detector_display.upper(): | |
| self.settings['ocr']['detector_type'] = 'rtdetr_onnx' | |
| elif 'RT-DETR' in detector_display: | |
| self.settings['ocr']['detector_type'] = 'rtdetr' | |
| elif 'YOLOv8' in detector_display: | |
| self.settings['ocr']['detector_type'] = 'yolo' | |
| elif detector_display == 'Custom Model': | |
| self.settings['ocr']['detector_type'] = 'custom' | |
| self.settings['ocr']['custom_model_path'] = self.bubble_model_path.get() | |
| else: | |
| self.settings['ocr']['detector_type'] = 'rtdetr_onnx' | |
| # Inpainting settings | |
| if hasattr(self, 'inpaint_batch_size'): | |
| if 'inpainting' not in self.settings: | |
| self.settings['inpainting'] = {} | |
| self.settings['inpainting']['batch_size'] = self.inpaint_batch_size.get() | |
| self.settings['inpainting']['enable_cache'] = self.enable_cache_var.get() | |
| # Save all dilation settings | |
| self.settings['mask_dilation'] = self.mask_dilation_var.get() | |
| self.settings['use_all_iterations'] = self.use_all_iterations_var.get() | |
| self.settings['all_iterations'] = self.all_iterations_var.get() | |
| self.settings['text_bubble_dilation_iterations'] = self.text_bubble_iterations_var.get() | |
| self.settings['empty_bubble_dilation_iterations'] = self.empty_bubble_iterations_var.get() | |
| self.settings['free_text_dilation_iterations'] = self.free_text_iterations_var.get() | |
| self.settings['auto_iterations'] = self.auto_iterations_var.get() | |
| # Legacy support | |
| self.settings['bubble_dilation_iterations'] = self.text_bubble_iterations_var.get() | |
| self.settings['dilation_iterations'] = self.text_bubble_iterations_var.get() | |
| # Advanced settings | |
| self.settings['advanced']['format_detection'] = bool(self.format_detection.get()) | |
| self.settings['advanced']['webtoon_mode'] = self.webtoon_mode.get() | |
| self.settings['advanced']['debug_mode'] = bool(self.debug_mode.get()) | |
| self.settings['advanced']['save_intermediate'] = bool(self.save_intermediate.get()) | |
| self.settings['advanced']['parallel_processing'] = bool(self.parallel_processing.get()) | |
| self.settings['advanced']['max_workers'] = self.max_workers.get() | |
| # Save HD strategy settings | |
| try: | |
| self.settings['advanced']['hd_strategy'] = str(self.hd_strategy_var.get()) | |
| self.settings['advanced']['hd_strategy_resize_limit'] = int(self.hd_resize_limit_var.get()) | |
| self.settings['advanced']['hd_strategy_crop_margin'] = int(self.hd_crop_margin_var.get()) | |
| self.settings['advanced']['hd_strategy_crop_trigger_size'] = int(self.hd_crop_trigger_var.get()) | |
| # Also reflect into environment for immediate effect in this session | |
| os.environ['HD_STRATEGY'] = self.settings['advanced']['hd_strategy'] | |
| os.environ['HD_RESIZE_LIMIT'] = str(self.settings['advanced']['hd_strategy_resize_limit']) | |
| os.environ['HD_CROP_MARGIN'] = str(self.settings['advanced']['hd_strategy_crop_margin']) | |
| os.environ['HD_CROP_TRIGGER'] = str(self.settings['advanced']['hd_strategy_crop_trigger_size']) | |
| except Exception: | |
| pass | |
| # Save parallel rendering toggle | |
| if hasattr(self, 'render_parallel_var'): | |
| self.settings['advanced']['render_parallel'] = bool(self.render_parallel_var.get()) | |
| # Panel-level parallel translation settings | |
| self.settings['advanced']['parallel_panel_translation'] = bool(self.parallel_panel_var.get()) | |
| self.settings['advanced']['panel_max_workers'] = int(self.panel_max_workers_var.get()) | |
| self.settings['advanced']['panel_start_stagger_ms'] = int(self.panel_stagger_ms_var.get()) | |
| # New: preload local inpainting for panels | |
| if hasattr(self, 'preload_local_panels_var'): | |
| self.settings['advanced']['preload_local_inpainting_for_panels'] = bool(self.preload_local_panels_var.get()) | |
| # Memory management settings | |
| self.settings['advanced']['use_singleton_models'] = bool(self.use_singleton_models.get()) | |
| self.settings['advanced']['auto_cleanup_models'] = bool(self.auto_cleanup_models.get()) | |
| self.settings['advanced']['unload_models_after_translation'] = bool(getattr(self, 'unload_models_var', tk.BooleanVar(value=False)).get()) | |
| # ONNX auto-convert settings (persist and apply to environment) | |
| if hasattr(self, 'auto_convert_onnx_var'): | |
| self.settings['advanced']['auto_convert_to_onnx'] = bool(self.auto_convert_onnx_var.get()) | |
| os.environ['AUTO_CONVERT_TO_ONNX'] = 'true' if self.auto_convert_onnx_var.get() else 'false' | |
| if hasattr(self, 'auto_convert_onnx_bg_var'): | |
| self.settings['advanced']['auto_convert_to_onnx_background'] = bool(self.auto_convert_onnx_bg_var.get()) | |
| os.environ['AUTO_CONVERT_TO_ONNX_BACKGROUND'] = 'true' if self.auto_convert_onnx_bg_var.get() else 'false' | |
| # Quantization toggles and precision | |
| if hasattr(self, 'quantize_models_var'): | |
| self.settings['advanced']['quantize_models'] = bool(self.quantize_models_var.get()) | |
| os.environ['MODEL_QUANTIZE'] = 'true' if self.quantize_models_var.get() else 'false' | |
| if hasattr(self, 'onnx_quantize_var'): | |
| self.settings['advanced']['onnx_quantize'] = bool(self.onnx_quantize_var.get()) | |
| os.environ['ONNX_QUANTIZE'] = 'true' if self.onnx_quantize_var.get() else 'false' | |
| if hasattr(self, 'torch_precision_var'): | |
| self.settings['advanced']['torch_precision'] = str(self.torch_precision_var.get()) | |
| os.environ['TORCH_PRECISION'] = self.settings['advanced']['torch_precision'] | |
| # Memory cleanup toggle | |
| if hasattr(self, 'force_deep_cleanup_var'): | |
| if 'advanced' not in self.settings: | |
| self.settings['advanced'] = {} | |
| self.settings['advanced']['force_deep_cleanup_each_image'] = bool(self.force_deep_cleanup_var.get()) | |
| # RAM cap settings | |
| if hasattr(self, 'ram_cap_enabled_var'): | |
| self.settings['advanced']['ram_cap_enabled'] = bool(self.ram_cap_enabled_var.get()) | |
| if hasattr(self, 'ram_cap_mb_var'): | |
| try: | |
| self.settings['advanced']['ram_cap_mb'] = int(self.ram_cap_mb_var.get()) | |
| except Exception: | |
| self.settings['advanced']['ram_cap_mb'] = 0 | |
| if hasattr(self, 'ram_cap_mode_var'): | |
| mode = self.ram_cap_mode_var.get() | |
| if mode not in ['soft', 'hard (Windows only)']: | |
| mode = 'soft' | |
| # Normalize to 'soft' or 'hard' | |
| self.settings['advanced']['ram_cap_mode'] = 'hard' if mode.startswith('hard') else 'soft' | |
| if hasattr(self, 'ram_gate_timeout_var'): | |
| try: | |
| self.settings['advanced']['ram_gate_timeout_sec'] = float(self.ram_gate_timeout_var.get()) | |
| except Exception: | |
| self.settings['advanced']['ram_gate_timeout_sec'] = 10.0 | |
| if hasattr(self, 'ram_gate_floor_var'): | |
| try: | |
| self.settings['advanced']['ram_min_floor_over_baseline_mb'] = int(self.ram_gate_floor_var.get()) | |
| except Exception: | |
| self.settings['advanced']['ram_min_floor_over_baseline_mb'] = 128 | |
| # Cloud API settings | |
| if hasattr(self, 'cloud_model_var'): | |
| self.settings['cloud_inpaint_model'] = self.cloud_model_var.get() | |
| self.settings['cloud_custom_version'] = self.custom_version_var.get() | |
| self.settings['cloud_inpaint_prompt'] = self.cloud_prompt_var.get() | |
| self.settings['cloud_negative_prompt'] = self.cloud_negative_prompt_var.get() | |
| self.settings['cloud_inference_steps'] = self.cloud_steps_var.get() | |
| self.settings['cloud_timeout'] = self.cloud_timeout_var.get() | |
| # Font sizing settings from Font Sizing tab | |
| if hasattr(self, 'font_algorithm_var'): | |
| if 'font_sizing' not in self.settings: | |
| self.settings['font_sizing'] = {} | |
| self.settings['font_sizing']['algorithm'] = self.font_algorithm_var.get() | |
| self.settings['font_sizing']['min_size'] = self.min_font_size_var.get() | |
| self.settings['font_sizing']['max_size'] = self.max_font_size_var.get() | |
| self.settings['font_sizing']['min_readable'] = self.min_readable_var.get() | |
| self.settings['font_sizing']['prefer_larger'] = self.prefer_larger_var.get() | |
| self.settings['font_sizing']['bubble_size_factor'] = self.bubble_size_factor_var.get() | |
| self.settings['font_sizing']['line_spacing'] = self.line_spacing_var.get() | |
| self.settings['font_sizing']['max_lines'] = self.max_lines_var.get() | |
| # SAVE FONT SIZE CONTROLS FROM RENDERING (if they exist) | |
| if hasattr(self, 'font_size_mode_var'): | |
| if 'rendering' not in self.settings: | |
| self.settings['rendering'] = {} | |
| self.settings['rendering']['font_size_mode'] = self.font_size_mode_var.get() | |
| self.settings['rendering']['fixed_font_size'] = self.fixed_font_size_var.get() | |
| self.settings['rendering']['font_scale'] = self.font_scale_var.get() | |
| self.settings['rendering']['auto_min_size'] = self.min_font_size_var.get() if hasattr(self, 'min_font_size_var') else 10 | |
| self.settings['rendering']['auto_max_size'] = self.max_font_size_var.get() if hasattr(self, 'max_font_size_var') else 28 | |
| self.settings['rendering']['auto_fit_style'] = self.auto_fit_style_var.get() | |
| # Clear bubble detector cache to force reload with new settings | |
| if hasattr(self.main_gui, 'manga_tab') and hasattr(self.main_gui.manga_tab, 'translator'): | |
| if hasattr(self.main_gui.manga_tab.translator, 'bubble_detector'): | |
| self.main_gui.manga_tab.translator.bubble_detector = None | |
| # Save to config | |
| self.config['manga_settings'] = self.settings | |
| # Save to file - using the correct method name | |
| try: | |
| if hasattr(self.main_gui, 'save_config'): | |
| self.main_gui.save_config() | |
| print("Settings saved successfully via save_config") | |
| time.sleep(0.1) # Brief pause for stability | |
| print("💤 Main settings save pausing briefly for stability") | |
| elif hasattr(self.main_gui, 'save_configuration'): | |
| self.main_gui.save_configuration() | |
| print("Settings saved successfully via save_configuration") | |
| else: | |
| print("Warning: No save method found on main_gui") | |
| # Try direct save as fallback | |
| if hasattr(self.main_gui, 'config_file'): | |
| import json | |
| with open(self.main_gui.config_file, 'w') as f: | |
| json.dump(self.config, f, indent=2) | |
| print("Settings saved directly to config file") | |
| except Exception as e: | |
| print(f"Error saving configuration: {e}") | |
| from tkinter import messagebox | |
| messagebox.showerror("Save Error", f"Failed to save settings: {e}") | |
| # Call callback if provided | |
| if self.callback: | |
| try: | |
| self.callback(self.settings) | |
| except Exception as e: | |
| print(f"Error in callback: {e}") | |
| # Close dialog with cleanup | |
| try: | |
| if hasattr(self.dialog, '_cleanup_scrolling'): | |
| self.dialog._cleanup_scrolling() | |
| self.dialog.destroy() | |
| except Exception as e: | |
| print(f"Error closing dialog: {e}") | |
| self.dialog.destroy() | |
| except Exception as e: | |
| print(f"Critical error in _save_settings: {e}") | |
| from tkinter import messagebox | |
| messagebox.showerror("Save Error", f"Failed to save settings: {e}") | |
| def _reset_defaults(self): | |
| """Reset by removing manga_settings from config and reinitializing the dialog.""" | |
| from tkinter import messagebox | |
| if not messagebox.askyesno("Reset Settings", "Reset all manga settings to defaults?\nThis will remove custom manga settings from config.json."): | |
| return | |
| # Remove manga_settings key to force defaults | |
| try: | |
| if isinstance(self.config, dict) and 'manga_settings' in self.config: | |
| del self.config['manga_settings'] | |
| except Exception: | |
| pass | |
| # Persist changes | |
| try: | |
| if hasattr(self.main_gui, 'save_config'): | |
| self.main_gui.save_config() | |
| elif hasattr(self.main_gui, 'save_configuration'): | |
| self.main_gui.save_configuration() | |
| elif hasattr(self.main_gui, 'config_file') and isinstance(self.main_gui.config_file, str): | |
| with open(self.main_gui.config_file, 'w', encoding='utf-8') as f: | |
| import json | |
| json.dump(self.config, f, ensure_ascii=False, indent=2) | |
| except Exception: | |
| try: | |
| if hasattr(self.main_gui, 'CONFIG_FILE') and isinstance(self.main_gui.CONFIG_FILE, str): | |
| with open(self.main_gui.CONFIG_FILE, 'w', encoding='utf-8') as f: | |
| import json | |
| json.dump(self.config, f, ensure_ascii=False, indent=2) | |
| except Exception: | |
| pass | |
| # Close and reopen dialog so defaults apply | |
| try: | |
| if hasattr(self.dialog, '_cleanup_scrolling'): | |
| self.dialog._cleanup_scrolling() | |
| except Exception: | |
| pass | |
| try: | |
| self.dialog.destroy() | |
| except Exception: | |
| pass | |
| try: | |
| MangaSettingsDialog(parent=self.parent, main_gui=self.main_gui, config=self.config, callback=self.callback) | |
| except Exception: | |
| try: | |
| messagebox.showinfo("Reset", "Settings reset. Please reopen the dialog.") | |
| except Exception: | |
| pass | |
| def _cancel(self): | |
| """Cancel without saving""" | |
| if hasattr(self.dialog, '_cleanup_scrolling'): | |
| self.dialog._cleanup_scrolling() | |
| self.dialog.destroy() | |