# 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("", dummy_scroll) # Windows widget.bind("", dummy_scroll) # Linux scroll up widget.bind("", 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('', on_enter) widget.bind('', 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('<>', 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('<>', _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('<>', 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()