# 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 from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QCheckBox, QSpinBox, QDoubleSpinBox, QSlider, QComboBox, QLineEdit, QGroupBox, QTabWidget, QWidget, QScrollArea, QFrame, QRadioButton, QButtonGroup, QMessageBox, QFileDialog, QSizePolicy, QApplication) from PySide6.QtCore import Qt, Signal, QTimer, QEvent, QObject from PySide6.QtGui import QFont, QIcon 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(QDialog): """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 (should be QWidget or None) main_gui: Reference to TranslatorGUI instance config: Configuration dictionary callback: Function to call after saving """ # Ensure parent is a QWidget or None for proper PySide6 initialization if parent is not None and not hasattr(parent, 'windowTitle'): # If parent is not a QWidget, use None parent = None super().__init__(parent) self.parent = parent self.main_gui = main_gui self.config = config self.callback = callback # Make dialog non-modal so it doesn't block the manga integration GUI self.setModal(False) # 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.3, 'detector_type': 'rtdetr_onnx', 'rtdetr_confidence': 0.3, 'detect_empty_bubbles': True, 'detect_text_bubbles': True, 'detect_free_text': True, 'rtdetr_model_url': '', 'use_rtdetr_for_ocr_regions': True, '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 _create_styled_checkbox(self, text): """Create a checkbox with proper checkmark using text overlay (same as manga_integration.py)""" checkbox = QCheckBox(text) checkbox.setStyleSheet(""" QCheckBox { color: white; spacing: 6px; } QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid #5a9fd4; border-radius: 2px; background-color: #2d2d2d; } QCheckBox::indicator:checked { background-color: #5a9fd4; border-color: #5a9fd4; } QCheckBox::indicator:hover { border-color: #7bb3e0; } QCheckBox:disabled { color: #666666; } QCheckBox::indicator:disabled { background-color: #1a1a1a; border-color: #3a3a3a; } """) # Create checkmark overlay checkmark = QLabel("✓", checkbox) checkmark.setStyleSheet(""" QLabel { color: white; background: transparent; font-weight: bold; font-size: 11px; } """) checkmark.setAlignment(Qt.AlignCenter) checkmark.hide() checkmark.setAttribute(Qt.WA_TransparentForMouseEvents) # Make checkmark click-through # Position checkmark properly after widget is shown def position_checkmark(): # Position over the checkbox indicator checkmark.setGeometry(2, 1, 14, 14) # Show/hide checkmark based on checked state def update_checkmark(): if checkbox.isChecked(): position_checkmark() checkmark.show() else: checkmark.hide() checkbox.stateChanged.connect(update_checkmark) # Delay initial positioning to ensure widget is properly rendered QTimer.singleShot(0, lambda: (position_checkmark(), update_checkmark())) return checkbox def _disable_spinbox_scroll(self, widget): """Disable mouse wheel scrolling on a spinbox, combobox, or slider (PySide6 version)""" # Install event filter to block wheel events class WheelEventFilter(QObject): def eventFilter(self, obj, event): if event.type() == QEvent.Wheel: event.ignore() return True return False filter_obj = WheelEventFilter(widget) # Parent it to the widget widget.installEventFilter(filter_obj) # Store the filter so it doesn't get garbage collected if not hasattr(widget, '_wheel_filter'): widget._wheel_filter = filter_obj def _disable_all_spinbox_scrolling(self, parent): """Recursively find and disable scrolling on all spinboxes, comboboxes, and sliders (PySide6 version)""" # Check if the parent itself is a spinbox, combobox, or slider if isinstance(parent, (QSpinBox, QDoubleSpinBox, QComboBox, QSlider)): self._disable_spinbox_scroll(parent) # Check all children recursively if hasattr(parent, 'children'): for child in parent.children(): if isinstance(child, QWidget): self._disable_all_spinbox_scrolling(child) def _create_font_size_controls(self, parent_layout): """Create improved font size controls with presets""" # Font size frame font_frame = QWidget() font_layout = QHBoxLayout(font_frame) font_layout.setContentsMargins(0, 0, 0, 0) parent_layout.addWidget(font_frame) label = QLabel("Font Size:") label.setMinimumWidth(150) font_layout.addWidget(label) # Font size mode selection mode_frame = QWidget() mode_layout = QHBoxLayout(mode_frame) mode_layout.setContentsMargins(0, 0, 0, 0) font_layout.addWidget(mode_frame) # Radio buttons for mode - using QButtonGroup self.font_size_mode_group = QButtonGroup() self.font_size_mode = 'auto' # Store mode as string attribute 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 = QRadioButton(text) rb.setChecked(value == 'auto') rb.setToolTip(tooltip) rb.toggled.connect(lambda checked, v=value: self._on_font_mode_change(v) if checked else None) mode_layout.addWidget(rb) self.font_size_mode_group.addButton(rb) # Controls frame (changes based on mode) self.font_controls_frame = QWidget() self.font_controls_layout = QVBoxLayout(self.font_controls_frame) self.font_controls_layout.setContentsMargins(20, 5, 0, 5) parent_layout.addWidget(self.font_controls_frame) # Fixed size controls self.fixed_size_frame = QWidget() fixed_layout = QHBoxLayout(self.fixed_size_frame) fixed_layout.setContentsMargins(0, 0, 0, 0) fixed_layout.addWidget(QLabel("Size:")) self.fixed_font_size_spin = QSpinBox() self.fixed_font_size_spin.setRange(8, 72) self.fixed_font_size_spin.setValue(16) self.fixed_font_size_spin.valueChanged.connect(self._save_rendering_settings) fixed_layout.addWidget(self.fixed_font_size_spin) # Quick presets for fixed size fixed_layout.addWidget(QLabel("Presets:")) presets = [ ("Small", 12), ("Medium", 16), ("Large", 20), ("XL", 24) ] for text, size in presets: btn = QPushButton(text) btn.setMaximumWidth(60) btn.clicked.connect(lambda checked, s=size: self._set_fixed_size(s)) fixed_layout.addWidget(btn) fixed_layout.addStretch() # Scale controls self.scale_frame = QWidget() scale_layout = QHBoxLayout(self.scale_frame) scale_layout.setContentsMargins(0, 0, 0, 0) scale_layout.addWidget(QLabel("Scale:")) # QSlider uses integers, so we'll use 50-200 to represent 0.5-2.0 self.font_scale_slider = QSlider(Qt.Horizontal) self.font_scale_slider.setRange(50, 200) self.font_scale_slider.setValue(100) self.font_scale_slider.setMinimumWidth(200) self.font_scale_slider.valueChanged.connect(self._update_scale_label) scale_layout.addWidget(self.font_scale_slider) self.scale_label = QLabel("100%") self.scale_label.setMinimumWidth(50) scale_layout.addWidget(self.scale_label) # Quick scale presets scale_layout.addWidget(QLabel("Quick:")) scale_presets = [ ("75%", 0.75), ("100%", 1.0), ("125%", 1.25), ("150%", 1.5) ] for text, scale in scale_presets: btn = QPushButton(text) btn.setMaximumWidth(50) btn.clicked.connect(lambda checked, s=scale: self._set_scale(s)) scale_layout.addWidget(btn) scale_layout.addStretch() # Auto size settings self.auto_frame = QWidget() auto_layout = QVBoxLayout(self.auto_frame) auto_layout.setContentsMargins(0, 0, 0, 0) # Min/Max size constraints for auto mode constraints_frame = QWidget() constraints_layout = QHBoxLayout(constraints_frame) constraints_layout.setContentsMargins(0, 0, 0, 0) auto_layout.addWidget(constraints_frame) constraints_layout.addWidget(QLabel("Size Range:")) constraints_layout.addWidget(QLabel("Min:")) self.min_font_size_spin = QSpinBox() self.min_font_size_spin.setRange(6, 20) self.min_font_size_spin.setValue(10) self.min_font_size_spin.valueChanged.connect(self._save_rendering_settings) constraints_layout.addWidget(self.min_font_size_spin) constraints_layout.addWidget(QLabel("Max:")) self.max_font_size_spin = QSpinBox() self.max_font_size_spin.setRange(16, 48) self.max_font_size_spin.setValue(28) self.max_font_size_spin.valueChanged.connect(self._save_rendering_settings) constraints_layout.addWidget(self.max_font_size_spin) constraints_layout.addStretch() # Auto fit quality quality_frame = QWidget() quality_layout = QHBoxLayout(quality_frame) quality_layout.setContentsMargins(0, 0, 0, 0) auto_layout.addWidget(quality_frame) quality_layout.addWidget(QLabel("Fit Style:")) self.auto_fit_style_group = QButtonGroup() self.auto_fit_style = 'balanced' # Store as string attribute 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 = QRadioButton(text) rb.setChecked(value == 'balanced') rb.setToolTip(tooltip) rb.toggled.connect(lambda checked, v=value: self._on_fit_style_change(v) if checked else None) quality_layout.addWidget(rb) self.auto_fit_style_group.addButton(rb) quality_layout.addStretch() # Initialize the correct frame self._on_font_mode_change('auto') def _on_font_mode_change(self, mode): """Show/hide appropriate font controls based on mode""" # Update the stored mode self.font_size_mode = mode # Remove all frames from layout self.font_controls_layout.removeWidget(self.fixed_size_frame) self.font_controls_layout.removeWidget(self.scale_frame) self.font_controls_layout.removeWidget(self.auto_frame) self.fixed_size_frame.hide() self.scale_frame.hide() self.auto_frame.hide() # Show the appropriate frame if mode == 'fixed': self.font_controls_layout.addWidget(self.fixed_size_frame) self.fixed_size_frame.show() elif mode == 'scale': self.font_controls_layout.addWidget(self.scale_frame) self.scale_frame.show() else: # auto self.font_controls_layout.addWidget(self.auto_frame) self.auto_frame.show() self._save_rendering_settings() def _set_fixed_size(self, size): """Set fixed font size from preset""" self.fixed_font_size_spin.setValue(size) self._save_rendering_settings() def _set_scale(self, scale): """Set font scale from preset""" # Scale is 0.5-2.0, slider uses 50-200 self.font_scale_slider.setValue(int(scale * 100)) self._update_scale_label() self._save_rendering_settings() def _update_scale_label(self): """Update the scale percentage label""" # Get value from slider (50-200) and convert to percentage scale_value = self.font_scale_slider.value() self.scale_label.setText(f"{scale_value}%") self._save_rendering_settings() def _on_fit_style_change(self, style): """Handle fit style change""" self.auto_fit_style = style self._save_rendering_settings() def _create_tooltip(self, widget, text): """Create a tooltip for a widget - PySide6 version""" # In PySide6, tooltips are much simpler - just set the toolTip property widget.setToolTip(text) 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 PySide6""" # Set initialization flag to prevent auto-saves during setup self._initializing = True # Set dialog properties self.setWindowTitle("Manga Translation Settings") # Dialog is already non-modal from __init__, don't override it # Set the halgakos.ico icon try: icon_path = os.path.join(os.path.dirname(__file__), 'Halgakos.ico') if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) except Exception: pass # Fail silently if icon can't be loaded # Apply overall dark theme styling self.setStyleSheet(""" QDialog { background-color: #1e1e1e; color: white; font-family: Arial; } QGroupBox { font-family: Arial; font-size: 10pt; font-weight: bold; color: white; border: 1px solid #555; border-radius: 5px; margin-top: 10px; padding-top: 5px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; } QLabel { color: white; font-family: Arial; font-size: 9pt; } QLineEdit { background-color: #2d2d2d; color: white; border: 1px solid #555; border-radius: 3px; padding: 3px; font-family: Arial; font-size: 9pt; } QSpinBox, QDoubleSpinBox { background-color: #2d2d2d; color: white; border: 1px solid #555; border-radius: 3px; padding: 3px; font-family: Arial; font-size: 9pt; } QComboBox { background-color: #2d2d2d; color: white; border: 1px solid #555; border-radius: 3px; padding: 3px 5px; padding-right: 25px; font-family: Arial; font-size: 9pt; min-height: 20px; } QComboBox:hover { border: 1px solid #7bb3e0; } QComboBox:focus { border: 1px solid #5a9fd4; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: right center; width: 20px; border-left: 1px solid #555; background-color: #3c3c3c; border-top-right-radius: 3px; border-bottom-right-radius: 3px; } QComboBox::drop-down:hover { background-color: #4a4a4a; } QComboBox::down-arrow { image: none; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #aaa; width: 0; height: 0; margin-right: 5px; } QComboBox::down-arrow:hover { border-top: 5px solid #fff; } QComboBox QAbstractItemView { background-color: #2d2d2d; color: white; selection-background-color: #5a9fd4; selection-color: white; border: 1px solid #555; outline: none; } QPushButton { font-family: Arial; font-size: 9pt; padding: 5px 15px; border-radius: 3px; border: none; } QSlider::groove:horizontal { border: 1px solid #555; height: 6px; background: #2d2d2d; border-radius: 3px; } QSlider::handle:horizontal { background: #5a9fd4; border: 1px solid #5a9fd4; width: 18px; border-radius: 9px; margin: -6px 0; } QSlider::handle:horizontal:hover { background: #7bb3e0; border: 1px solid #7bb3e0; } QRadioButton { color: white; spacing: 6px; font-family: Arial; font-size: 9pt; } QRadioButton::indicator { width: 16px; height: 16px; border: 2px solid #5a9fd4; border-radius: 8px; background-color: #2d2d2d; } QRadioButton::indicator:checked { background-color: #5a9fd4; border: 2px solid #5a9fd4; } QRadioButton::indicator:hover { border-color: #7bb3e0; } QRadioButton:disabled { color: #666666; } QRadioButton::indicator:disabled { background-color: #1a1a1a; border-color: #3a3a3a; } QCheckBox { color: white; spacing: 6px; } QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid #5a9fd4; border-radius: 2px; background-color: #2d2d2d; } QCheckBox::indicator:checked { background-color: #5a9fd4; border-color: #5a9fd4; } QCheckBox::indicator:hover { border-color: #7bb3e0; } QCheckBox:disabled { color: #666666; } QCheckBox::indicator:disabled { background-color: #1a1a1a; border-color: #3a3a3a; } QLineEdit:disabled, QComboBox:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled { background-color: #1a1a1a; color: #666666; border: 1px solid #3a3a3a; } QLabel:disabled { color: #666666; } QScrollArea { background-color: #1e1e1e; border: none; } QWidget { background-color: #1e1e1e; color: white; } """) # Calculate size based on screen dimensions # Use availableGeometry to exclude taskbar and other system UI app = QApplication.instance() if app: screen = app.primaryScreen().availableGeometry() else: screen = self.parent.screen().availableGeometry() if self.parent else self.screen().availableGeometry() dialog_width = min(800, int(screen.width() * 0.5)) dialog_height = int(screen.height() * 0.90) # Use 90% of available height for more screen space with safety margin self.resize(dialog_width, dialog_height) # Center the dialog within available screen space dialog_x = screen.x() + (screen.width() - dialog_width) // 2 dialog_y = screen.y() + (screen.height() - dialog_height) // 2 self.move(dialog_x, dialog_y) # Create main layout main_layout = QVBoxLayout(self) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(10) # Create scroll area for the content scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Create content widget that will go inside scroll area content_widget = QWidget() content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(5, 5, 5, 5) # Create tab widget with enhanced styling self.tab_widget = QTabWidget() self.tab_widget.setStyleSheet(""" QTabWidget::pane { border: 1px solid #555; background-color: #2b2b2b; } QTabBar::tab { background-color: #3c3c3c; color: #cccccc; border: 1px solid #555; border-bottom: none; padding: 8px 16px; margin-right: 2px; font-family: Arial; font-size: 10pt; font-weight: bold; } QTabBar::tab:selected { background-color: #5a9fd4; color: white; border-color: #7bb3e0; margin-bottom: -1px; } QTabBar::tab:hover:!selected { background-color: #4a4a4a; color: white; border-color: #7bb3e0; } QTabBar::tab:first { margin-left: 0; } """) content_layout.addWidget(self.tab_widget) # Create all tabs self._create_preprocessing_tab() self._create_ocr_tab() self._create_inpainting_tab() self._create_advanced_tab() self._create_cloud_api_tab() # NOTE: Font Sizing tab removed; controls are now in Manga Integration UI # Set content widget in scroll area scroll_area.setWidget(content_widget) main_layout.addWidget(scroll_area) # Create button frame at bottom button_frame = QWidget() button_layout = QHBoxLayout(button_frame) button_layout.setContentsMargins(0, 5, 0, 0) # Reset button on left reset_button = QPushButton("Reset to Defaults") reset_button.clicked.connect(self._reset_defaults) reset_button.setMinimumWidth(120) reset_button.setMinimumHeight(32) reset_button.setStyleSheet(""" QPushButton { background-color: #ffc107; color: #1a1a1a; font-weight: bold; font-size: 10pt; border: none; border-radius: 4px; padding: 6px 16px; } QPushButton:hover { background-color: #ffcd38; } QPushButton:pressed { background-color: #e0a800; } """) button_layout.addWidget(reset_button) button_layout.addStretch() # Push other buttons to the right # Cancel and Save buttons on right cancel_button = QPushButton("Cancel") cancel_button.clicked.connect(self._cancel) cancel_button.setMinimumWidth(90) cancel_button.setMinimumHeight(32) cancel_button.setStyleSheet(""" QPushButton { background-color: #6c757d; color: white; font-weight: bold; font-size: 10pt; border: none; border-radius: 4px; padding: 6px 16px; } QPushButton:hover { background-color: #7d8a96; } QPushButton:pressed { background-color: #5a6268; } """) button_layout.addWidget(cancel_button) save_button = QPushButton("Save") save_button.clicked.connect(self._save_settings) save_button.setDefault(True) # Make it the default button save_button.setMinimumWidth(90) save_button.setMinimumHeight(32) save_button.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; font-weight: bold; font-size: 10pt; border: none; border-radius: 4px; padding: 6px 16px; } QPushButton:hover { background-color: #34c759; } QPushButton:pressed { background-color: #218838; } """) button_layout.addWidget(save_button) main_layout.addWidget(button_frame) # Clear initialization flag after setup is complete self._initializing = False # Initialize preprocessing state self._toggle_preprocessing() # Initialize tiling controls state (must be after widgets are created) try: self._toggle_tiling_controls() except Exception as e: print(f"Warning: Failed to initialize tiling controls: {e}") # Initialize iteration controls state try: self._toggle_iteration_controls() except Exception: pass # Disable mouse wheel scrolling on all spinboxes and comboboxes self._disable_all_spinbox_scrolling(self) # Show the dialog self.show() def _create_preprocessing_tab(self): """Create preprocessing settings tab with all options""" # Create tab widget and add to tab widget tab_widget = QWidget() self.tab_widget.addTab(tab_widget, "Preprocessing") # Main scrollable content main_layout = QVBoxLayout(tab_widget) main_layout.setContentsMargins(5, 5, 5, 5) main_layout.setSpacing(6) # Enable preprocessing group enable_group = QGroupBox("Image Preprocessing") main_layout.addWidget(enable_group) enable_layout = QVBoxLayout(enable_group) enable_layout.setContentsMargins(8, 8, 8, 6) enable_layout.setSpacing(4) self.preprocess_enabled = self._create_styled_checkbox("Enable Image Preprocessing") self.preprocess_enabled.setChecked(self.settings['preprocessing']['enabled']) self.preprocess_enabled.toggled.connect(self._toggle_preprocessing) enable_layout.addWidget(self.preprocess_enabled) # Store all preprocessing controls for enable/disable self.preprocessing_controls = [] # Auto quality detection self.auto_detect = self._create_styled_checkbox("Auto-detect image quality issues") self.auto_detect.setChecked(self.settings['preprocessing']['auto_detect_quality']) enable_layout.addWidget(self.auto_detect) self.preprocessing_controls.append(self.auto_detect) # Quality thresholds section threshold_group = QGroupBox("Image Enhancement") main_layout.addWidget(threshold_group) threshold_layout = QVBoxLayout(threshold_group) threshold_layout.setContentsMargins(8, 8, 8, 6) threshold_layout.setSpacing(4) self.preprocessing_controls.append(threshold_group) # Contrast threshold contrast_frame = QWidget() contrast_layout = QHBoxLayout(contrast_frame) contrast_layout.setContentsMargins(0, 0, 0, 0) threshold_layout.addWidget(contrast_frame) contrast_label = QLabel("Contrast Adjustment:") contrast_label.setMinimumWidth(150) contrast_layout.addWidget(contrast_label) self.preprocessing_controls.append(contrast_label) self.contrast_threshold = QDoubleSpinBox() self.contrast_threshold.setRange(0.0, 1.0) self.contrast_threshold.setSingleStep(0.01) self.contrast_threshold.setDecimals(2) self.contrast_threshold.setValue(self.settings['preprocessing']['contrast_threshold']) contrast_layout.addWidget(self.contrast_threshold) self.preprocessing_controls.append(self.contrast_threshold) contrast_layout.addStretch() # Sharpness threshold sharpness_frame = QWidget() sharpness_layout = QHBoxLayout(sharpness_frame) sharpness_layout.setContentsMargins(0, 0, 0, 0) threshold_layout.addWidget(sharpness_frame) sharpness_label = QLabel("Sharpness Enhancement:") sharpness_label.setMinimumWidth(150) sharpness_layout.addWidget(sharpness_label) self.preprocessing_controls.append(sharpness_label) self.sharpness_threshold = QDoubleSpinBox() self.sharpness_threshold.setRange(0.0, 1.0) self.sharpness_threshold.setSingleStep(0.01) self.sharpness_threshold.setDecimals(2) self.sharpness_threshold.setValue(self.settings['preprocessing']['sharpness_threshold']) sharpness_layout.addWidget(self.sharpness_threshold) self.preprocessing_controls.append(self.sharpness_threshold) sharpness_layout.addStretch() # Enhancement strength enhance_frame = QWidget() enhance_layout = QHBoxLayout(enhance_frame) enhance_layout.setContentsMargins(0, 0, 0, 0) threshold_layout.addWidget(enhance_frame) enhance_label = QLabel("Overall Enhancement:") enhance_label.setMinimumWidth(150) enhance_layout.addWidget(enhance_label) self.preprocessing_controls.append(enhance_label) self.enhancement_strength = QDoubleSpinBox() self.enhancement_strength.setRange(0.0, 3.0) self.enhancement_strength.setSingleStep(0.01) self.enhancement_strength.setDecimals(2) self.enhancement_strength.setValue(self.settings['preprocessing']['enhancement_strength']) enhance_layout.addWidget(self.enhancement_strength) self.preprocessing_controls.append(self.enhancement_strength) enhance_layout.addStretch() # Noise reduction section noise_group = QGroupBox("Noise Reduction") main_layout.addWidget(noise_group) noise_layout = QVBoxLayout(noise_group) noise_layout.setContentsMargins(8, 8, 8, 6) noise_layout.setSpacing(4) self.preprocessing_controls.append(noise_group) # Noise threshold noise_threshold_frame = QWidget() noise_threshold_layout = QHBoxLayout(noise_threshold_frame) noise_threshold_layout.setContentsMargins(0, 0, 0, 0) noise_layout.addWidget(noise_threshold_frame) noise_label = QLabel("Noise Threshold:") noise_label.setMinimumWidth(150) noise_threshold_layout.addWidget(noise_label) self.preprocessing_controls.append(noise_label) self.noise_threshold = QSpinBox() self.noise_threshold.setRange(0, 50) self.noise_threshold.setValue(self.settings['preprocessing']['noise_threshold']) noise_threshold_layout.addWidget(self.noise_threshold) self.preprocessing_controls.append(self.noise_threshold) noise_threshold_layout.addStretch() # Denoise strength denoise_frame = QWidget() denoise_layout = QHBoxLayout(denoise_frame) denoise_layout.setContentsMargins(0, 0, 0, 0) noise_layout.addWidget(denoise_frame) denoise_label = QLabel("Denoise Strength:") denoise_label.setMinimumWidth(150) denoise_layout.addWidget(denoise_label) self.preprocessing_controls.append(denoise_label) self.denoise_strength = QSpinBox() self.denoise_strength.setRange(0, 30) self.denoise_strength.setValue(self.settings['preprocessing']['denoise_strength']) denoise_layout.addWidget(self.denoise_strength) self.preprocessing_controls.append(self.denoise_strength) denoise_layout.addStretch() # Size limits section size_group = QGroupBox("Image Size Limits") main_layout.addWidget(size_group) size_layout = QVBoxLayout(size_group) size_layout.setContentsMargins(8, 8, 8, 6) size_layout.setSpacing(4) self.preprocessing_controls.append(size_group) # Max dimension dimension_frame = QWidget() dimension_layout = QHBoxLayout(dimension_frame) dimension_layout.setContentsMargins(0, 0, 0, 0) size_layout.addWidget(dimension_frame) dimension_label = QLabel("Max Dimension:") dimension_label.setMinimumWidth(150) dimension_layout.addWidget(dimension_label) self.preprocessing_controls.append(dimension_label) self.dimension_spinbox = QSpinBox() self.dimension_spinbox.setRange(500, 4000) self.dimension_spinbox.setSingleStep(100) self.dimension_spinbox.setValue(self.settings['preprocessing']['max_image_dimension']) dimension_layout.addWidget(self.dimension_spinbox) self.preprocessing_controls.append(self.dimension_spinbox) dimension_layout.addWidget(QLabel("pixels")) dimension_layout.addStretch() # Max pixels pixels_frame = QWidget() pixels_layout = QHBoxLayout(pixels_frame) pixels_layout.setContentsMargins(0, 0, 0, 0) size_layout.addWidget(pixels_frame) pixels_label = QLabel("Max Total Pixels:") pixels_label.setMinimumWidth(150) pixels_layout.addWidget(pixels_label) self.preprocessing_controls.append(pixels_label) self.pixels_spinbox = QSpinBox() self.pixels_spinbox.setRange(1000000, 10000000) self.pixels_spinbox.setSingleStep(100000) self.pixels_spinbox.setValue(self.settings['preprocessing']['max_image_pixels']) pixels_layout.addWidget(self.pixels_spinbox) self.preprocessing_controls.append(self.pixels_spinbox) pixels_layout.addWidget(QLabel("pixels")) pixels_layout.addStretch() # Compression section compression_group = QGroupBox("Image Compression (applies to OCR uploads)") main_layout.addWidget(compression_group) compression_layout = QVBoxLayout(compression_group) compression_layout.setContentsMargins(8, 8, 8, 6) compression_layout.setSpacing(4) # Do NOT add compression controls to preprocessing_controls; keep independent of preprocessing toggle # Enable compression toggle self.compression_enabled = self._create_styled_checkbox("Enable compression for OCR uploads") self.compression_enabled.setChecked(self.settings.get('compression', {}).get('enabled', False)) self.compression_enabled.toggled.connect(self._toggle_compression_enabled) compression_layout.addWidget(self.compression_enabled) # Format selection format_frame = QWidget() format_layout = QHBoxLayout(format_frame) format_layout.setContentsMargins(0, 0, 0, 0) compression_layout.addWidget(format_frame) self.format_label = QLabel("Format:") self.format_label.setMinimumWidth(150) format_layout.addWidget(self.format_label) self.compression_format_combo = QComboBox() self.compression_format_combo.addItems(['jpeg', 'png', 'webp']) self.compression_format_combo.setCurrentText(self.settings.get('compression', {}).get('format', 'jpeg')) self.compression_format_combo.currentTextChanged.connect(self._toggle_compression_format) format_layout.addWidget(self.compression_format_combo) format_layout.addStretch() # JPEG quality self.jpeg_frame = QWidget() jpeg_layout = QHBoxLayout(self.jpeg_frame) jpeg_layout.setContentsMargins(0, 0, 0, 0) compression_layout.addWidget(self.jpeg_frame) self.jpeg_label = QLabel("JPEG Quality:") self.jpeg_label.setMinimumWidth(150) jpeg_layout.addWidget(self.jpeg_label) self.jpeg_quality_spin = QSpinBox() self.jpeg_quality_spin.setRange(1, 95) self.jpeg_quality_spin.setValue(self.settings.get('compression', {}).get('jpeg_quality', 85)) jpeg_layout.addWidget(self.jpeg_quality_spin) self.jpeg_help = QLabel("(higher = better quality, larger size)") self.jpeg_help.setStyleSheet("color: gray; font-size: 9pt;") jpeg_layout.addWidget(self.jpeg_help) jpeg_layout.addStretch() # PNG compression level self.png_frame = QWidget() png_layout = QHBoxLayout(self.png_frame) png_layout.setContentsMargins(0, 0, 0, 0) compression_layout.addWidget(self.png_frame) self.png_label = QLabel("PNG Compression:") self.png_label.setMinimumWidth(150) png_layout.addWidget(self.png_label) self.png_level_spin = QSpinBox() self.png_level_spin.setRange(0, 9) self.png_level_spin.setValue(self.settings.get('compression', {}).get('png_compress_level', 6)) png_layout.addWidget(self.png_level_spin) self.png_help = QLabel("(0 = fastest, 9 = smallest)") self.png_help.setStyleSheet("color: gray; font-size: 9pt;") png_layout.addWidget(self.png_help) png_layout.addStretch() # WEBP quality self.webp_frame = QWidget() webp_layout = QHBoxLayout(self.webp_frame) webp_layout.setContentsMargins(0, 0, 0, 0) compression_layout.addWidget(self.webp_frame) self.webp_label = QLabel("WEBP Quality:") self.webp_label.setMinimumWidth(150) webp_layout.addWidget(self.webp_label) self.webp_quality_spin = QSpinBox() self.webp_quality_spin.setRange(1, 100) self.webp_quality_spin.setValue(self.settings.get('compression', {}).get('webp_quality', 85)) webp_layout.addWidget(self.webp_quality_spin) self.webp_help = QLabel("(higher = better quality, larger size)") self.webp_help.setStyleSheet("color: gray; font-size: 9pt;") webp_layout.addWidget(self.webp_help) webp_layout.addStretch() # Initialize format-specific visibility and enabled state self._toggle_compression_format() self._toggle_compression_enabled() # HD Strategy (Inpainting acceleration) - Independent of preprocessing toggle hd_group = QGroupBox("Inpainting HD Strategy") main_layout.addWidget(hd_group) hd_layout = QVBoxLayout(hd_group) hd_layout.setContentsMargins(8, 8, 8, 6) hd_layout.setSpacing(4) # Do NOT add to preprocessing_controls - HD Strategy should be independent # Chunk settings for large images - Independent of preprocessing toggle chunk_group = QGroupBox("Large Image Processing") main_layout.addWidget(chunk_group) chunk_layout = QVBoxLayout(chunk_group) chunk_layout.setContentsMargins(8, 8, 8, 6) chunk_layout.setSpacing(4) # Do NOT add to preprocessing_controls - Large Image Processing should be independent # Strategy selector strat_frame = QWidget() strat_layout = QHBoxLayout(strat_frame) strat_layout.setContentsMargins(0, 0, 0, 0) hd_layout.addWidget(strat_frame) strat_label = QLabel("Strategy:") strat_label.setMinimumWidth(150) strat_layout.addWidget(strat_label) self.hd_strategy_combo = QComboBox() self.hd_strategy_combo.addItems(['original', 'resize', 'crop']) self.hd_strategy_combo.setCurrentText(self.settings.get('advanced', {}).get('hd_strategy', 'resize')) self.hd_strategy_combo.currentTextChanged.connect(self._on_hd_strategy_change) strat_layout.addWidget(self.hd_strategy_combo) strat_help = QLabel("(original = legacy full-image; resize/crop = faster)") strat_help.setStyleSheet("color: gray; font-size: 9pt;") strat_layout.addWidget(strat_help) strat_layout.addStretch() # Resize limit row self.hd_resize_frame = QWidget() resize_layout = QHBoxLayout(self.hd_resize_frame) resize_layout.setContentsMargins(0, 0, 0, 0) hd_layout.addWidget(self.hd_resize_frame) resize_label = QLabel("Resize limit (long edge):") resize_label.setMinimumWidth(150) resize_layout.addWidget(resize_label) self.hd_resize_limit_spin = QSpinBox() self.hd_resize_limit_spin.setRange(512, 4096) self.hd_resize_limit_spin.setSingleStep(64) self.hd_resize_limit_spin.setValue(int(self.settings.get('advanced', {}).get('hd_strategy_resize_limit', 1536))) resize_layout.addWidget(self.hd_resize_limit_spin) resize_layout.addWidget(QLabel("px")) resize_layout.addStretch() # Crop params rows self.hd_crop_margin_frame = QWidget() margin_layout = QHBoxLayout(self.hd_crop_margin_frame) margin_layout.setContentsMargins(0, 0, 0, 0) hd_layout.addWidget(self.hd_crop_margin_frame) margin_label = QLabel("Crop margin:") margin_label.setMinimumWidth(150) margin_layout.addWidget(margin_label) self.hd_crop_margin_spin = QSpinBox() self.hd_crop_margin_spin.setRange(0, 256) self.hd_crop_margin_spin.setSingleStep(2) self.hd_crop_margin_spin.setValue(int(self.settings.get('advanced', {}).get('hd_strategy_crop_margin', 16))) margin_layout.addWidget(self.hd_crop_margin_spin) margin_layout.addWidget(QLabel("px")) margin_layout.addStretch() self.hd_crop_trigger_frame = QWidget() trigger_layout = QHBoxLayout(self.hd_crop_trigger_frame) trigger_layout.setContentsMargins(0, 0, 0, 0) hd_layout.addWidget(self.hd_crop_trigger_frame) trigger_label = QLabel("Crop trigger size:") trigger_label.setMinimumWidth(150) trigger_layout.addWidget(trigger_label) self.hd_crop_trigger_spin = QSpinBox() self.hd_crop_trigger_spin.setRange(256, 4096) self.hd_crop_trigger_spin.setSingleStep(64) self.hd_crop_trigger_spin.setValue(int(self.settings.get('advanced', {}).get('hd_strategy_crop_trigger_size', 1024))) trigger_layout.addWidget(self.hd_crop_trigger_spin) trigger_help = QLabel("px (apply crop only if long edge > trigger)") trigger_layout.addWidget(trigger_help) trigger_layout.addStretch() # Initialize strategy-specific visibility self._on_hd_strategy_change() # Clarifying note about precedence with tiling note_label = QLabel( "Note: HD Strategy (resize/crop) takes precedence over Inpainting Tiling when it triggers.\n" "Set strategy to 'original' if you want tiling to control large-image behavior." ) note_label.setStyleSheet("color: gray; font-size: 9pt;") note_label.setWordWrap(True) hd_layout.addWidget(note_label) # Chunk height chunk_height_frame = QWidget() chunk_height_layout = QHBoxLayout(chunk_height_frame) chunk_height_layout.setContentsMargins(0, 0, 0, 0) chunk_layout.addWidget(chunk_height_frame) chunk_height_label = QLabel("Chunk Height:") chunk_height_label.setMinimumWidth(150) chunk_height_layout.addWidget(chunk_height_label) # Do NOT add to preprocessing_controls - chunk settings should be independent self.chunk_height_spinbox = QSpinBox() self.chunk_height_spinbox.setRange(500, 2000) self.chunk_height_spinbox.setSingleStep(100) self.chunk_height_spinbox.setValue(self.settings['preprocessing']['chunk_height']) chunk_height_layout.addWidget(self.chunk_height_spinbox) # Do NOT add to preprocessing_controls - chunk settings should be independent chunk_height_layout.addWidget(QLabel("pixels")) chunk_height_layout.addStretch() # Chunk overlap chunk_overlap_frame = QWidget() chunk_overlap_layout = QHBoxLayout(chunk_overlap_frame) chunk_overlap_layout.setContentsMargins(0, 0, 0, 0) chunk_layout.addWidget(chunk_overlap_frame) chunk_overlap_label = QLabel("Chunk Overlap:") chunk_overlap_label.setMinimumWidth(150) chunk_overlap_layout.addWidget(chunk_overlap_label) # Do NOT add to preprocessing_controls - chunk settings should be independent self.chunk_overlap_spinbox = QSpinBox() self.chunk_overlap_spinbox.setRange(0, 200) self.chunk_overlap_spinbox.setSingleStep(10) self.chunk_overlap_spinbox.setValue(self.settings['preprocessing']['chunk_overlap']) chunk_overlap_layout.addWidget(self.chunk_overlap_spinbox) # Do NOT add to preprocessing_controls - chunk settings should be independent chunk_overlap_layout.addWidget(QLabel("pixels")) chunk_overlap_layout.addStretch() # Inpainting Tiling section tiling_group = QGroupBox("Inpainting Tiling") main_layout.addWidget(tiling_group) tiling_layout = QVBoxLayout(tiling_group) tiling_layout.setContentsMargins(8, 8, 8, 6) tiling_layout.setSpacing(4) # Do NOT add to preprocessing_controls - tiling should be independent # 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 = self._create_styled_checkbox("Enable automatic tiling for inpainting (processes large images in tiles)") self.inpaint_tiling_enabled.setChecked(tiling_enabled_value) self.inpaint_tiling_enabled.toggled.connect(self._toggle_tiling_controls) tiling_layout.addWidget(self.inpaint_tiling_enabled) # Tile size tile_size_frame = QWidget() tile_size_layout = QHBoxLayout(tile_size_frame) tile_size_layout.setContentsMargins(0, 0, 0, 0) tiling_layout.addWidget(tile_size_frame) self.tile_size_label = QLabel("Tile Size:") self.tile_size_label.setMinimumWidth(150) tile_size_layout.addWidget(self.tile_size_label) 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.tile_size_spinbox = QSpinBox() self.tile_size_spinbox.setRange(256, 2048) self.tile_size_spinbox.setSingleStep(128) self.tile_size_spinbox.setValue(tile_size_value) tile_size_layout.addWidget(self.tile_size_spinbox) self.tile_size_unit_label = QLabel("pixels") tile_size_layout.addWidget(self.tile_size_unit_label) tile_size_layout.addStretch() # Tile overlap tile_overlap_frame = QWidget() tile_overlap_layout = QHBoxLayout(tile_overlap_frame) tile_overlap_layout.setContentsMargins(0, 0, 0, 0) tiling_layout.addWidget(tile_overlap_frame) self.tile_overlap_label = QLabel("Tile Overlap:") self.tile_overlap_label.setMinimumWidth(150) tile_overlap_layout.addWidget(self.tile_overlap_label) 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.tile_overlap_spinbox = QSpinBox() self.tile_overlap_spinbox.setRange(0, 256) self.tile_overlap_spinbox.setSingleStep(16) self.tile_overlap_spinbox.setValue(tile_overlap_value) tile_overlap_layout.addWidget(self.tile_overlap_spinbox) self.tile_overlap_unit_label = QLabel("pixels") tile_overlap_layout.addWidget(self.tile_overlap_unit_label) tile_overlap_layout.addStretch() # Don't initialize here - will be done after dialog is shown # ELIMINATE ALL EMPTY SPACE - Add stretch at the end main_layout.addStretch() def _create_inpainting_tab(self): """Create inpainting settings tab with comprehensive per-text-type dilation controls""" # Create tab widget and add to tab widget tab_widget = QWidget() self.tab_widget.addTab(tab_widget, "Inpainting") # Main scrollable content main_layout = QVBoxLayout(tab_widget) main_layout.setContentsMargins(5, 5, 5, 5) main_layout.setSpacing(6) # General Mask Settings (applies to all inpainting methods) mask_group = QGroupBox("Mask Settings") main_layout.addWidget(mask_group) mask_layout = QVBoxLayout(mask_group) mask_layout.setContentsMargins(8, 8, 8, 6) mask_layout.setSpacing(4) # Auto toggle (affects both mask dilation AND iterations) self.auto_iterations_checkbox = self._create_styled_checkbox("Auto Iterations (automatically set values based on OCR provider and B&W vs Color)") self.auto_iterations_checkbox.setChecked(self.settings.get('auto_iterations', True)) self.auto_iterations_checkbox.toggled.connect(self._toggle_iteration_controls) self.auto_iterations_checkbox.toggled.connect(self._on_primary_auto_toggle) # Sync with "Use Same For All" mask_layout.addWidget(self.auto_iterations_checkbox) # Mask Dilation frame (affected by auto setting) mask_dilation_group = QGroupBox("Mask Dilation") mask_layout.addWidget(mask_dilation_group) mask_dilation_layout = QVBoxLayout(mask_dilation_group) mask_dilation_layout.setContentsMargins(8, 8, 8, 6) mask_dilation_layout.setSpacing(4) # Note about dilation importance note_label = QLabel( "Mask dilation is critical for avoiding white spots in final images.\n" "Adjust per text type for optimal results." ) note_label.setStyleSheet("color: gray; font-style: italic;") note_label.setWordWrap(True) mask_dilation_layout.addWidget(note_label) # Keep all three dilation controls in a list for easy access if not hasattr(self, 'mask_dilation_controls'): self.mask_dilation_controls = [] # Mask dilation size dilation_frame = QWidget() dilation_layout = QHBoxLayout(dilation_frame) dilation_layout.setContentsMargins(0, 0, 0, 0) mask_dilation_layout.addWidget(dilation_frame) self.dilation_label = QLabel("Mask Dilation:") self.dilation_label.setMinimumWidth(150) dilation_layout.addWidget(self.dilation_label) self.mask_dilation_spinbox = QSpinBox() self.mask_dilation_spinbox.setRange(0, 50) self.mask_dilation_spinbox.setSingleStep(5) self.mask_dilation_spinbox.setValue(self.settings.get('mask_dilation', 15)) dilation_layout.addWidget(self.mask_dilation_spinbox) self.dilation_unit_label = QLabel("pixels (expand mask beyond text)") dilation_layout.addWidget(self.dilation_unit_label) dilation_layout.addStretch() # Per-Text-Type Iterations - EXPANDED SECTION iterations_group = QGroupBox("Dilation Iterations Control") iterations_layout = QVBoxLayout(iterations_group) mask_dilation_layout.addWidget(iterations_group) # All Iterations Master Control (NEW) all_iter_widget = QWidget() all_iter_layout = QHBoxLayout(all_iter_widget) all_iter_layout.setContentsMargins(0, 0, 0, 0) iterations_layout.addWidget(all_iter_widget) # Checkbox to enable/disable uniform iterations self.use_all_iterations_checkbox = self._create_styled_checkbox("Use Same For All:") self.use_all_iterations_checkbox.setChecked(self.settings.get('use_all_iterations', True)) self.use_all_iterations_checkbox.toggled.connect(self._toggle_iteration_controls) all_iter_layout.addWidget(self.use_all_iterations_checkbox) all_iter_layout.addSpacing(10) self.all_iterations_spinbox = QSpinBox() self.all_iterations_spinbox.setRange(0, 5) self.all_iterations_spinbox.setValue(self.settings.get('all_iterations', 2)) self.all_iterations_spinbox.setEnabled(self.use_all_iterations_checkbox.isChecked()) all_iter_layout.addWidget(self.all_iterations_spinbox) self.all_iter_label = QLabel("iterations (applies to all text types)") all_iter_layout.addWidget(self.all_iter_label) all_iter_layout.addStretch() # Separator separator1 = QFrame() separator1.setFrameShape(QFrame.Shape.HLine) separator1.setFrameShadow(QFrame.Shadow.Sunken) iterations_layout.addWidget(separator1) # Individual Controls Label self.individual_controls_header_label = QLabel("Individual Text Type Controls:") individual_label_font = QFont('Arial', 9) individual_label_font.setBold(True) self.individual_controls_header_label.setFont(individual_label_font) iterations_layout.addWidget(self.individual_controls_header_label) # Text Bubble iterations (modified from original bubble iterations) text_bubble_iter_widget = QWidget() text_bubble_iter_layout = QHBoxLayout(text_bubble_iter_widget) text_bubble_iter_layout.setContentsMargins(0, 0, 0, 0) iterations_layout.addWidget(text_bubble_iter_widget) self.text_bubble_label = QLabel("Text Bubbles:") self.text_bubble_label.setMinimumWidth(120) text_bubble_iter_layout.addWidget(self.text_bubble_label) self.text_bubble_iter_spinbox = QSpinBox() self.text_bubble_iter_spinbox.setRange(0, 5) self.text_bubble_iter_spinbox.setValue(self.settings.get('text_bubble_dilation_iterations', self.settings.get('bubble_dilation_iterations', 2))) text_bubble_iter_layout.addWidget(self.text_bubble_iter_spinbox) self.text_bubble_desc = QLabel("iterations (speech/dialogue bubbles)") text_bubble_iter_layout.addWidget(self.text_bubble_desc) text_bubble_iter_layout.addStretch() # Empty Bubble iterations (NEW) empty_bubble_iter_widget = QWidget() empty_bubble_iter_layout = QHBoxLayout(empty_bubble_iter_widget) empty_bubble_iter_layout.setContentsMargins(0, 0, 0, 0) iterations_layout.addWidget(empty_bubble_iter_widget) self.empty_bubble_label = QLabel("Empty Bubbles:") self.empty_bubble_label.setMinimumWidth(120) empty_bubble_iter_layout.addWidget(self.empty_bubble_label) self.empty_bubble_iter_spinbox = QSpinBox() self.empty_bubble_iter_spinbox.setRange(0, 5) self.empty_bubble_iter_spinbox.setValue(self.settings.get('empty_bubble_dilation_iterations', 3)) empty_bubble_iter_layout.addWidget(self.empty_bubble_iter_spinbox) self.empty_bubble_desc = QLabel("iterations (empty speech bubbles)") empty_bubble_iter_layout.addWidget(self.empty_bubble_desc) empty_bubble_iter_layout.addStretch() # Free text iterations free_text_iter_widget = QWidget() free_text_iter_layout = QHBoxLayout(free_text_iter_widget) free_text_iter_layout.setContentsMargins(0, 0, 0, 0) iterations_layout.addWidget(free_text_iter_widget) self.free_text_label = QLabel("Free Text:") self.free_text_label.setMinimumWidth(120) free_text_iter_layout.addWidget(self.free_text_label) self.free_text_iter_spinbox = QSpinBox() self.free_text_iter_spinbox.setRange(0, 5) self.free_text_iter_spinbox.setValue(self.settings.get('free_text_dilation_iterations', 0)) free_text_iter_layout.addWidget(self.free_text_iter_spinbox) self.free_text_desc = QLabel("iterations (0 = perfect for B&W panels)") free_text_iter_layout.addWidget(self.free_text_desc) free_text_iter_layout.addStretch() # Store individual control widgets for enable/disable (includes descriptive labels) self.individual_iteration_controls = [ (self.text_bubble_label, self.text_bubble_iter_spinbox, self.text_bubble_desc), (self.empty_bubble_label, self.empty_bubble_iter_spinbox, self.empty_bubble_desc), (self.free_text_label, self.free_text_iter_spinbox, self.free_text_desc) ] # Apply initial state self._toggle_iteration_controls() # Quick presets - UPDATED VERSION preset_widget = QWidget() preset_layout = QHBoxLayout(preset_widget) preset_layout.setContentsMargins(0, 0, 0, 0) mask_dilation_layout.addWidget(preset_widget) preset_label = QLabel("Quick Presets:") preset_layout.addWidget(preset_label) preset_layout.addSpacing(10) bw_manga_btn = QPushButton("B&W Manga") bw_manga_btn.setStyleSheet(""" QPushButton { background-color: #3a7ca5; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #4a8cb5; } QPushButton:pressed { background-color: #2a6c95; } """) bw_manga_btn.clicked.connect(lambda: self._set_mask_preset(15, False, 2, 2, 3, 0)) preset_layout.addWidget(bw_manga_btn) colored_btn = QPushButton("Colored") colored_btn.setStyleSheet(""" QPushButton { background-color: #3a7ca5; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #4a8cb5; } QPushButton:pressed { background-color: #2a6c95; } """) colored_btn.clicked.connect(lambda: self._set_mask_preset(15, False, 2, 2, 3, 3)) preset_layout.addWidget(colored_btn) uniform_btn = QPushButton("Uniform") uniform_btn.setStyleSheet(""" QPushButton { background-color: #3a7ca5; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #4a8cb5; } QPushButton:pressed { background-color: #2a6c95; } """) uniform_btn.clicked.connect(lambda: self._set_mask_preset(0, True, 2, 2, 2, 0)) preset_layout.addWidget(uniform_btn) preset_layout.addStretch() # Help text - UPDATED help_text = QLabel( "💡 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" ) help_text_font = QFont('Arial', 9) help_text.setFont(help_text_font) help_text.setStyleSheet("color: gray;") help_text.setWordWrap(True) mask_dilation_layout.addWidget(help_text) main_layout.addStretch() def _toggle_iteration_controls(self): """Enable/disable iteration controls based on Auto and 'Use Same For All' toggles""" # Get auto checkbox state auto_on = False if hasattr(self, 'auto_iterations_checkbox'): auto_on = self.auto_iterations_checkbox.isChecked() # Get use_all checkbox state use_all = False if hasattr(self, 'use_all_iterations_checkbox'): use_all = self.use_all_iterations_checkbox.isChecked() # Also update the auto_iterations_enabled attribute self.auto_iterations_enabled = auto_on if auto_on: # Disable ALL mask dilation and iteration controls when auto is on # Mask dilation controls try: if hasattr(self, 'mask_dilation_spinbox'): self.mask_dilation_spinbox.setEnabled(False) except Exception: pass try: if hasattr(self, 'dilation_label'): self.dilation_label.setEnabled(False) except Exception: pass try: if hasattr(self, 'dilation_unit_label'): self.dilation_unit_label.setEnabled(False) except Exception: pass # Iteration controls try: self.all_iterations_spinbox.setEnabled(False) except Exception: pass try: if hasattr(self, 'all_iter_label'): self.all_iter_label.setEnabled(False) except Exception: pass try: if hasattr(self, 'use_all_iterations_checkbox'): self.use_all_iterations_checkbox.setEnabled(False) except Exception: pass try: if hasattr(self, 'individual_controls_header_label'): self.individual_controls_header_label.setEnabled(False) except Exception: pass # Disable individual controls and their description labels for control_tuple in getattr(self, 'individual_iteration_controls', []): try: if len(control_tuple) == 3: label, spinbox, desc_label = control_tuple spinbox.setEnabled(False) label.setEnabled(False) desc_label.setEnabled(False) elif len(control_tuple) == 2: label, spinbox = control_tuple spinbox.setEnabled(False) label.setEnabled(False) except Exception: pass return # Auto off -> enable mask dilation (always) and "Use Same For All" (respect its state) # Mask dilation is always enabled when auto is off try: if hasattr(self, 'mask_dilation_spinbox'): self.mask_dilation_spinbox.setEnabled(True) except Exception: pass try: if hasattr(self, 'dilation_label'): self.dilation_label.setEnabled(True) except Exception: pass try: if hasattr(self, 'dilation_unit_label'): self.dilation_unit_label.setEnabled(True) except Exception: pass try: if hasattr(self, 'use_all_iterations_checkbox'): self.use_all_iterations_checkbox.setEnabled(True) except Exception: pass try: self.all_iterations_spinbox.setEnabled(use_all) except Exception: pass try: if hasattr(self, 'all_iter_label'): self.all_iter_label.setEnabled(use_all) except Exception: pass try: if hasattr(self, 'individual_controls_header_label'): self.individual_controls_header_label.setEnabled(not use_all) except Exception: pass # Individual controls respect the "Use Same For All" state for control_tuple in getattr(self, 'individual_iteration_controls', []): enabled = not use_all try: if len(control_tuple) == 3: label, spinbox, desc_label = control_tuple spinbox.setEnabled(enabled) label.setEnabled(enabled) desc_label.setEnabled(enabled) elif len(control_tuple) == 2: label, spinbox = control_tuple spinbox.setEnabled(enabled) label.setEnabled(enabled) except Exception: pass def _on_primary_auto_toggle(self, checked): """When primary Auto toggle changes, disable/enable 'Use Same For All' checkbox""" if hasattr(self, 'use_all_iterations_checkbox'): self.use_all_iterations_checkbox.setEnabled(not checked) 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_spinbox.setValue(dilation) self.use_all_iterations_checkbox.setChecked(use_all) self.all_iterations_spinbox.setValue(all_iter) self.text_bubble_iter_spinbox.setValue(text_bubble_iter) self.empty_bubble_iter_spinbox.setValue(empty_bubble_iter) self.free_text_iter_spinbox.setValue(free_text_iter) self._toggle_iteration_controls() def _create_cloud_api_tab(self): """Create cloud API settings tab""" # Create tab widget and add to tab widget tab_widget = QWidget() self.tab_widget.addTab(tab_widget, "Cloud API") # Create scroll area for content scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFrameShape(QFrame.Shape.NoFrame) content_widget = QWidget() content_layout = QVBoxLayout(content_widget) content_layout.setSpacing(10) content_layout.setContentsMargins(20, 20, 20, 20) scroll_area.setWidget(content_widget) # Add scroll area to parent layout parent_layout = QVBoxLayout(tab_widget) parent_layout.setContentsMargins(0, 0, 0, 0) parent_layout.addWidget(scroll_area) # API Model Selection model_group = QGroupBox("Inpainting Model") model_layout = QVBoxLayout(model_group) content_layout.addWidget(model_group) model_desc = QLabel("Select the Replicate model to use for inpainting:") model_layout.addWidget(model_desc) model_layout.addSpacing(10) # Model options - use button group for radio buttons self.cloud_model_button_group = QButtonGroup() self.cloud_model_selected = 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_widget = QWidget() row_layout = QHBoxLayout(row_widget) row_layout.setContentsMargins(0, 0, 0, 0) model_layout.addWidget(row_widget) rb = QRadioButton(text) rb.setChecked(value == self.cloud_model_selected) rb.toggled.connect(lambda checked, v=value: self._on_cloud_model_change(v) if checked else None) self.cloud_model_button_group.addButton(rb) row_layout.addWidget(rb) if model_id: model_id_label = QLabel(f"({model_id})") model_id_font = QFont('Arial', 8) model_id_label.setFont(model_id_font) model_id_label.setStyleSheet("color: gray;") row_layout.addWidget(model_id_label) row_layout.addStretch() # Custom version ID (now model identifier) self.custom_version_widget = QWidget() custom_version_layout = QVBoxLayout(self.custom_version_widget) custom_version_layout.setContentsMargins(0, 10, 0, 0) model_layout.addWidget(self.custom_version_widget) custom_id_row = QWidget() custom_id_layout = QHBoxLayout(custom_id_row) custom_id_layout.setContentsMargins(0, 0, 0, 0) custom_version_layout.addWidget(custom_id_row) custom_id_label = QLabel("Model ID:") custom_id_label.setMinimumWidth(120) custom_id_layout.addWidget(custom_id_label) self.custom_version_entry = QLineEdit() self.custom_version_entry.setText(self.settings.get('cloud_custom_version', '')) custom_id_layout.addWidget(self.custom_version_entry) # Add helper text for custom model helper_text = QLabel("Format: owner/model-name (e.g. stability-ai/stable-diffusion-inpainting)") helper_font = QFont('Arial', 8) helper_text.setFont(helper_font) helper_text.setStyleSheet("color: gray;") helper_text.setContentsMargins(120, 0, 0, 0) custom_version_layout.addWidget(helper_text) # Initially hide custom version entry if self.cloud_model_selected != 'custom': self.custom_version_widget.setVisible(False) # Performance Settings perf_group = QGroupBox("Performance Settings") perf_layout = QVBoxLayout(perf_group) content_layout.addWidget(perf_group) # Timeout timeout_widget = QWidget() timeout_layout = QHBoxLayout(timeout_widget) timeout_layout.setContentsMargins(0, 0, 0, 0) perf_layout.addWidget(timeout_widget) timeout_label = QLabel("API Timeout:") timeout_label.setMinimumWidth(120) timeout_layout.addWidget(timeout_label) self.cloud_timeout_spinbox = QSpinBox() self.cloud_timeout_spinbox.setRange(30, 300) self.cloud_timeout_spinbox.setValue(self.settings.get('cloud_timeout', 60)) timeout_layout.addWidget(self.cloud_timeout_spinbox) timeout_unit = QLabel("seconds") timeout_unit_font = QFont('Arial', 9) timeout_unit.setFont(timeout_unit_font) timeout_layout.addWidget(timeout_unit) timeout_layout.addStretch() # Help text help_text = QLabel( "💡 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" ) help_font = QFont('Arial', 9) help_text.setFont(help_font) help_text.setStyleSheet("color: gray;") help_text.setWordWrap(True) content_layout.addWidget(help_text) # Prompt Settings (for all models except custom) self.prompt_group = QGroupBox("Prompt Settings") prompt_layout = QVBoxLayout(self.prompt_group) content_layout.addWidget(self.prompt_group) # Positive prompt prompt_label = QLabel("Inpainting Prompt:") prompt_layout.addWidget(prompt_label) self.cloud_prompt_entry = QLineEdit() self.cloud_prompt_entry.setText(self.settings.get('cloud_inpaint_prompt', 'clean background, smooth surface')) prompt_layout.addWidget(self.cloud_prompt_entry) # Add note about prompts prompt_tip = QLabel("Tip: Describe what you want in the inpainted area (e.g., 'white wall', 'wooden floor')") prompt_tip_font = QFont('Arial', 8) prompt_tip.setFont(prompt_tip_font) prompt_tip.setStyleSheet("color: gray;") prompt_tip.setWordWrap(True) prompt_tip.setContentsMargins(0, 2, 0, 10) prompt_layout.addWidget(prompt_tip) # Negative prompt (mainly for SD) self.negative_prompt_label = QLabel("Negative Prompt (SD only):") prompt_layout.addWidget(self.negative_prompt_label) self.negative_entry = QLineEdit() self.negative_entry.setText(self.settings.get('cloud_negative_prompt', 'text, writing, letters')) prompt_layout.addWidget(self.negative_entry) # Inference steps (for SD) self.steps_widget = QWidget() steps_layout = QHBoxLayout(self.steps_widget) steps_layout.setContentsMargins(0, 10, 0, 5) prompt_layout.addWidget(self.steps_widget) self.steps_label = QLabel("Inference Steps (SD only):") self.steps_label.setMinimumWidth(180) steps_layout.addWidget(self.steps_label) self.steps_spinbox = QSpinBox() self.steps_spinbox.setRange(10, 50) self.steps_spinbox.setValue(self.settings.get('cloud_inference_steps', 20)) steps_layout.addWidget(self.steps_spinbox) steps_desc = QLabel("(Higher = better quality, slower)") steps_desc_font = QFont('Arial', 9) steps_desc.setFont(steps_desc_font) steps_desc.setStyleSheet("color: gray;") steps_layout.addWidget(steps_desc) steps_layout.addStretch() # Add stretch at end content_layout.addStretch() # Initially hide prompt frame if not using appropriate model if self.cloud_model_selected == 'custom': self.prompt_group.setVisible(False) # Show/hide SD-specific options based on model self._on_cloud_model_change(self.cloud_model_selected) def _on_cloud_model_change(self, model): """Handle cloud model selection change""" # Store the selected model self.cloud_model_selected = model # Show/hide custom version entry if model == 'custom': self.custom_version_widget.setVisible(True) # DON'T HIDE THE PROMPT FRAME FOR CUSTOM MODELS self.prompt_group.setVisible(True) else: self.custom_version_widget.setVisible(False) self.prompt_group.setVisible(True) # Show/hide SD-specific options if model == 'sd-inpainting': # Show negative prompt and steps self.negative_prompt_label.setVisible(True) self.negative_entry.setVisible(True) self.steps_widget.setVisible(True) else: # Hide SD-specific options self.negative_prompt_label.setVisible(False) self.negative_entry.setVisible(False) self.steps_widget.setVisible(False) def _toggle_preprocessing(self): """Enable/disable preprocessing controls based on main toggle""" enabled = self.preprocess_enabled.isChecked() # Process each control in preprocessing_controls list for control in self.preprocessing_controls: try: if isinstance(control, QGroupBox): # Enable/disable entire group box children self._toggle_frame_children(control, enabled) elif isinstance(control, (QSlider, QSpinBox, QCheckBox, QDoubleSpinBox, QComboBox, QLabel)): # Just use setEnabled() - the global stylesheet handles the visual state control.setEnabled(enabled) except Exception as e: 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, widget, enabled): """Recursively enable/disable all children of a widget""" # Handle all controls including labels - just use setEnabled() for child in widget.findChildren(QWidget): if isinstance(child, (QSlider, QSpinBox, QCheckBox, QDoubleSpinBox, QComboBox, QLineEdit, QLabel)): try: child.setEnabled(enabled) except Exception: pass def _toggle_roi_locality_controls(self): """Show/hide ROI locality controls based on toggle.""" try: enabled = self.roi_locality_checkbox.isChecked() 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 row.setVisible(enabled) 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.isChecked()) except Exception: enabled = False # Enable/disable tiling widgets and their labels widgets_to_toggle = [ ('tile_size_spinbox', 'tile_size_label', 'tile_size_unit_label'), ('tile_overlap_spinbox', 'tile_overlap_label', 'tile_overlap_unit_label') ] for widget_names in widgets_to_toggle: for widget_name in widget_names: try: widget = getattr(self, widget_name, None) if widget is not None: # Just use setEnabled() for everything - stylesheet handles visuals widget.setEnabled(enabled) except Exception: pass def _on_hd_strategy_change(self): """Show/hide HD strategy controls based on selected strategy.""" try: strategy = self.hd_strategy_combo.currentText() except Exception: strategy = 'original' # Show/hide resize limit based on strategy if hasattr(self, 'hd_resize_frame'): self.hd_resize_frame.setVisible(strategy == 'resize') # Show/hide crop params based on strategy if hasattr(self, 'hd_crop_margin_frame'): self.hd_crop_margin_frame.setVisible(strategy == 'crop') if hasattr(self, 'hd_crop_trigger_frame'): self.hd_crop_trigger_frame.setVisible(strategy == 'crop') def _toggle_compression_enabled(self): """Enable/disable compression controls based on compression toggle.""" try: enabled = bool(self.compression_enabled.isChecked()) except Exception: enabled = False # Enable/disable all compression format controls compression_widgets = [ getattr(self, 'format_label', None), getattr(self, 'compression_format_combo', None), getattr(self, 'jpeg_frame', None), getattr(self, 'jpeg_label', None), getattr(self, 'jpeg_quality_spin', None), getattr(self, 'jpeg_help', None), getattr(self, 'png_frame', None), getattr(self, 'png_label', None), getattr(self, 'png_level_spin', None), getattr(self, 'png_help', None), getattr(self, 'webp_frame', None), getattr(self, 'webp_label', None), getattr(self, 'webp_quality_spin', None), getattr(self, 'webp_help', None), ] for widget in compression_widgets: try: if widget is not None: widget.setEnabled(enabled) except Exception: pass def _toggle_compression_format(self): """Show only the controls relevant to the selected format (hide others).""" fmt = self.compression_format_combo.currentText().lower() if hasattr(self, 'compression_format_combo') else 'jpeg' try: # Hide all rows first for row in [getattr(self, 'jpeg_frame', None), getattr(self, 'png_frame', None), getattr(self, 'webp_frame', None)]: try: if row is not None: row.setVisible(False) except Exception: pass # Show the selected one if fmt == 'jpeg': if hasattr(self, 'jpeg_frame') and self.jpeg_frame is not None: self.jpeg_frame.setVisible(True) elif fmt == 'png': if hasattr(self, 'png_frame') and self.png_frame is not None: self.png_frame.setVisible(True) else: # webp if hasattr(self, 'webp_frame') and self.webp_frame is not None: self.webp_frame.setVisible(True) 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_checkbox.isChecked()) except Exception: enabled = False try: if hasattr(self, 'ocr_bs_row') and self.ocr_bs_row: self.ocr_bs_row.setVisible(enabled) except Exception: pass try: if hasattr(self, 'ocr_cc_row') and self.ocr_cc_row: self.ocr_cc_row.setVisible(enabled) except Exception: pass def _create_ocr_tab(self): """Create OCR settings tab with all options""" # Create tab widget and add to tab widget tab_widget = QWidget() self.tab_widget.addTab(tab_widget, "OCR") # Create scroll area for OCR settings scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFrameShape(QFrame.Shape.NoFrame) content_widget = QWidget() content_layout = QVBoxLayout(content_widget) content_layout.setSpacing(10) content_layout.setContentsMargins(20, 20, 20, 20) scroll_area.setWidget(content_widget) # Add scroll area to parent layout parent_layout = QVBoxLayout(tab_widget) parent_layout.setContentsMargins(0, 0, 0, 0) parent_layout.addWidget(scroll_area) # Language hints lang_group = QGroupBox("Language Detection") lang_layout = QVBoxLayout(lang_group) content_layout.addWidget(lang_group) lang_desc = QLabel("Select languages to prioritize during OCR:") lang_desc_font = QFont('Arial', 10) lang_desc.setFont(lang_desc_font) lang_layout.addWidget(lang_desc) lang_layout.addSpacing(10) # Language checkboxes self.lang_checkboxes = {} languages = [ ('ja', 'Japanese'), ('ko', 'Korean'), ('zh', 'Chinese (Simplified)'), ('zh-TW', 'Chinese (Traditional)'), ('en', 'English') ] lang_grid_widget = QWidget() lang_grid_layout = QGridLayout(lang_grid_widget) lang_layout.addWidget(lang_grid_widget) for i, (code, name) in enumerate(languages): checkbox = self._create_styled_checkbox(name) checkbox.setChecked(code in self.settings['ocr']['language_hints']) self.lang_checkboxes[code] = checkbox lang_grid_layout.addWidget(checkbox, i//2, i%2) # OCR parameters ocr_group = QGroupBox("OCR Parameters") ocr_layout = QVBoxLayout(ocr_group) content_layout.addWidget(ocr_group) # Confidence threshold conf_widget = QWidget() conf_layout = QHBoxLayout(conf_widget) conf_layout.setContentsMargins(0, 0, 0, 0) ocr_layout.addWidget(conf_widget) conf_label = QLabel("Confidence Threshold:") conf_label.setMinimumWidth(180) conf_layout.addWidget(conf_label) self.confidence_threshold_slider = QSlider(Qt.Orientation.Horizontal) self.confidence_threshold_slider.setRange(0, 100) self.confidence_threshold_slider.setValue(int(self.settings['ocr']['confidence_threshold'] * 100)) self.confidence_threshold_slider.setMinimumWidth(250) conf_layout.addWidget(self.confidence_threshold_slider) self.confidence_threshold_label = QLabel(f"{self.settings['ocr']['confidence_threshold']:.2f}") self.confidence_threshold_label.setMinimumWidth(50) self.confidence_threshold_slider.valueChanged.connect( lambda v: self.confidence_threshold_label.setText(f"{v/100:.2f}") ) conf_layout.addWidget(self.confidence_threshold_label) conf_layout.addStretch() # Detection mode mode_widget = QWidget() mode_layout = QHBoxLayout(mode_widget) mode_layout.setContentsMargins(0, 0, 0, 0) ocr_layout.addWidget(mode_widget) mode_label = QLabel("Detection Mode:") mode_label.setMinimumWidth(180) mode_layout.addWidget(mode_label) self.detection_mode_combo = QComboBox() self.detection_mode_combo.addItems(['document', 'text']) self.detection_mode_combo.setCurrentText(self.settings['ocr']['text_detection_mode']) mode_layout.addWidget(self.detection_mode_combo) mode_desc = QLabel("(document = better for manga, text = simple layouts)") mode_desc_font = QFont('Arial', 9) mode_desc.setFont(mode_desc_font) mode_desc.setStyleSheet("color: gray;") mode_layout.addWidget(mode_desc) mode_layout.addStretch() # Text merging settings merge_group = QGroupBox("Text Region Merging") merge_layout = QVBoxLayout(merge_group) content_layout.addWidget(merge_group) # Merge nearby threshold nearby_widget = QWidget() nearby_layout = QHBoxLayout(nearby_widget) nearby_layout.setContentsMargins(0, 0, 0, 0) merge_layout.addWidget(nearby_widget) nearby_label = QLabel("Merge Distance:") nearby_label.setMinimumWidth(180) nearby_layout.addWidget(nearby_label) self.merge_nearby_threshold_spinbox = QSpinBox() self.merge_nearby_threshold_spinbox.setRange(0, 200) self.merge_nearby_threshold_spinbox.setSingleStep(10) self.merge_nearby_threshold_spinbox.setValue(self.settings['ocr']['merge_nearby_threshold']) nearby_layout.addWidget(self.merge_nearby_threshold_spinbox) nearby_unit = QLabel("pixels") nearby_layout.addWidget(nearby_unit) nearby_layout.addStretch() # Text Filtering Setting filter_group = QGroupBox("Text Filtering") filter_layout = QVBoxLayout(filter_group) content_layout.addWidget(filter_group) # Minimum text length min_length_widget = QWidget() min_length_layout = QHBoxLayout(min_length_widget) min_length_layout.setContentsMargins(0, 0, 0, 0) filter_layout.addWidget(min_length_widget) min_length_label = QLabel("Min Text Length:") min_length_label.setMinimumWidth(180) min_length_layout.addWidget(min_length_label) self.min_text_length_spinbox = QSpinBox() self.min_text_length_spinbox.setRange(1, 10) self.min_text_length_spinbox.setValue(self.settings['ocr'].get('min_text_length', 0)) min_length_layout.addWidget(self.min_text_length_spinbox) min_length_unit = QLabel("characters") min_length_layout.addWidget(min_length_unit) min_length_desc = QLabel("(skip text shorter than this)") min_length_desc_font = QFont('Arial', 9) min_length_desc.setFont(min_length_desc_font) min_length_desc.setStyleSheet("color: gray;") min_length_layout.addWidget(min_length_desc) min_length_layout.addStretch() # Exclude English text checkbox self.exclude_english_checkbox = self._create_styled_checkbox("Exclude primarily English text (tunable threshold)") self.exclude_english_checkbox.setChecked(self.settings['ocr'].get('exclude_english_text', False)) filter_layout.addWidget(self.exclude_english_checkbox) # Threshold slider english_threshold_widget = QWidget() english_threshold_layout = QHBoxLayout(english_threshold_widget) english_threshold_layout.setContentsMargins(0, 0, 0, 0) filter_layout.addWidget(english_threshold_widget) threshold_label = QLabel("English Exclude Threshold:") threshold_label.setMinimumWidth(240) english_threshold_layout.addWidget(threshold_label) self.english_exclude_threshold_slider = QSlider(Qt.Orientation.Horizontal) self.english_exclude_threshold_slider.setRange(60, 99) self.english_exclude_threshold_slider.setValue(int(self.settings['ocr'].get('english_exclude_threshold', 0.7) * 100)) self.english_exclude_threshold_slider.setMinimumWidth(250) english_threshold_layout.addWidget(self.english_exclude_threshold_slider) self.english_threshold_label = QLabel(f"{int(self.settings['ocr'].get('english_exclude_threshold', 0.7)*100)}%") self.english_threshold_label.setMinimumWidth(50) self.english_exclude_threshold_slider.valueChanged.connect( lambda v: self.english_threshold_label.setText(f"{v}%") ) english_threshold_layout.addWidget(self.english_threshold_label) english_threshold_layout.addStretch() # Minimum character count min_chars_widget = QWidget() min_chars_layout = QHBoxLayout(min_chars_widget) min_chars_layout.setContentsMargins(0, 0, 0, 0) filter_layout.addWidget(min_chars_widget) min_chars_label = QLabel("Min chars to exclude as English:") min_chars_label.setMinimumWidth(240) min_chars_layout.addWidget(min_chars_label) self.english_exclude_min_chars_spinbox = QSpinBox() self.english_exclude_min_chars_spinbox.setRange(1, 10) self.english_exclude_min_chars_spinbox.setValue(self.settings['ocr'].get('english_exclude_min_chars', 4)) min_chars_layout.addWidget(self.english_exclude_min_chars_spinbox) min_chars_unit = QLabel("characters") min_chars_layout.addWidget(min_chars_unit) min_chars_layout.addStretch() # Legacy aggressive short-token filter self.english_exclude_short_tokens_checkbox = self._create_styled_checkbox("Aggressively drop very short ASCII tokens (legacy)") self.english_exclude_short_tokens_checkbox.setChecked(self.settings['ocr'].get('english_exclude_short_tokens', False)) filter_layout.addWidget(self.english_exclude_short_tokens_checkbox) # Help text filter_help = QLabel( "💡 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" ) filter_help_font = QFont('Arial', 9) filter_help.setFont(filter_help_font) filter_help.setStyleSheet("color: gray;") filter_help.setWordWrap(True) filter_help.setContentsMargins(0, 10, 0, 0) filter_layout.addWidget(filter_help) # Azure-specific OCR settings azure_ocr_group = QGroupBox("Azure OCR Settings") azure_ocr_layout = QVBoxLayout(azure_ocr_group) content_layout.addWidget(azure_ocr_group) # Azure merge multiplier merge_mult_widget = QWidget() merge_mult_layout = QHBoxLayout(merge_mult_widget) merge_mult_layout.setContentsMargins(0, 0, 0, 0) azure_ocr_layout.addWidget(merge_mult_widget) merge_mult_label = QLabel("Merge Multiplier:") merge_mult_label.setMinimumWidth(180) merge_mult_layout.addWidget(merge_mult_label) self.azure_merge_multiplier_slider = QSlider(Qt.Orientation.Horizontal) self.azure_merge_multiplier_slider.setRange(100, 500) self.azure_merge_multiplier_slider.setValue(int(self.settings['ocr'].get('azure_merge_multiplier', 2.0) * 100)) self.azure_merge_multiplier_slider.setMinimumWidth(200) merge_mult_layout.addWidget(self.azure_merge_multiplier_slider) self.azure_label = QLabel(f"{self.settings['ocr'].get('azure_merge_multiplier', 2.0):.2f}x") self.azure_label.setMinimumWidth(50) self.azure_merge_multiplier_slider.valueChanged.connect( lambda v: self.azure_label.setText(f"{v/100:.2f}x") ) merge_mult_layout.addWidget(self.azure_label) merge_mult_desc = QLabel("(multiplies merge distance for Azure lines)") merge_mult_desc_font = QFont('Arial', 9) merge_mult_desc.setFont(merge_mult_desc_font) merge_mult_desc.setStyleSheet("color: gray;") merge_mult_layout.addWidget(merge_mult_desc) merge_mult_layout.addStretch() # Reading order reading_order_widget = QWidget() reading_order_layout = QHBoxLayout(reading_order_widget) reading_order_layout.setContentsMargins(0, 0, 0, 0) azure_ocr_layout.addWidget(reading_order_widget) reading_order_label = QLabel("Reading Order:") reading_order_label.setMinimumWidth(180) reading_order_layout.addWidget(reading_order_label) self.azure_reading_order_combo = QComboBox() self.azure_reading_order_combo.addItems(['basic', 'natural']) self.azure_reading_order_combo.setCurrentText(self.settings['ocr'].get('azure_reading_order', 'natural')) reading_order_layout.addWidget(self.azure_reading_order_combo) reading_order_desc = QLabel("(natural = better for complex layouts)") reading_order_desc_font = QFont('Arial', 9) reading_order_desc.setFont(reading_order_desc_font) reading_order_desc.setStyleSheet("color: gray;") reading_order_layout.addWidget(reading_order_desc) reading_order_layout.addStretch() # Model version model_version_widget = QWidget() model_version_layout = QHBoxLayout(model_version_widget) model_version_layout.setContentsMargins(0, 0, 0, 0) azure_ocr_layout.addWidget(model_version_widget) model_version_label = QLabel("Model Version:") model_version_label.setMinimumWidth(180) model_version_layout.addWidget(model_version_label) self.azure_model_version_combo = QComboBox() self.azure_model_version_combo.addItems(['latest', '2022-04-30', '2022-01-30', '2021-09-30']) self.azure_model_version_combo.setCurrentText(self.settings['ocr'].get('azure_model_version', 'latest')) self.azure_model_version_combo.setEditable(True) model_version_layout.addWidget(self.azure_model_version_combo) model_version_desc = QLabel("(use 'latest' for newest features)") model_version_desc_font = QFont('Arial', 9) model_version_desc.setFont(model_version_desc_font) model_version_desc.setStyleSheet("color: gray;") model_version_layout.addWidget(model_version_desc) model_version_layout.addStretch() # Timeout settings timeout_widget = QWidget() timeout_layout = QHBoxLayout(timeout_widget) timeout_layout.setContentsMargins(0, 0, 0, 0) azure_ocr_layout.addWidget(timeout_widget) timeout_label = QLabel("Max Wait Time:") timeout_label.setMinimumWidth(180) timeout_layout.addWidget(timeout_label) self.azure_max_wait_spinbox = QSpinBox() self.azure_max_wait_spinbox.setRange(10, 120) self.azure_max_wait_spinbox.setSingleStep(5) self.azure_max_wait_spinbox.setValue(self.settings['ocr'].get('azure_max_wait', 60)) timeout_layout.addWidget(self.azure_max_wait_spinbox) timeout_unit = QLabel("seconds") timeout_layout.addWidget(timeout_unit) timeout_layout.addStretch() # Poll interval poll_widget = QWidget() poll_layout = QHBoxLayout(poll_widget) poll_layout.setContentsMargins(0, 0, 0, 0) azure_ocr_layout.addWidget(poll_widget) poll_label = QLabel("Poll Interval:") poll_label.setMinimumWidth(180) poll_layout.addWidget(poll_label) self.azure_poll_interval_slider = QSlider(Qt.Orientation.Horizontal) self.azure_poll_interval_slider.setRange(0, 200) self.azure_poll_interval_slider.setValue(int(self.settings['ocr'].get('azure_poll_interval', 0.5) * 100)) self.azure_poll_interval_slider.setMinimumWidth(200) poll_layout.addWidget(self.azure_poll_interval_slider) self.azure_poll_interval_label = QLabel(f"{self.settings['ocr'].get('azure_poll_interval', 0.5):.2f}") self.azure_poll_interval_label.setMinimumWidth(50) self.azure_poll_interval_slider.valueChanged.connect( lambda v: self.azure_poll_interval_label.setText(f"{v/100:.2f}") ) poll_layout.addWidget(self.azure_poll_interval_label) poll_unit = QLabel("sec") poll_layout.addWidget(poll_unit) poll_layout.addStretch() # Help text azure_help = QLabel( "💡 Azure Read API auto-detects language well\n" "💡 Natural reading order works better for manga panels" ) azure_help_font = QFont('Arial', 9) azure_help.setFont(azure_help_font) azure_help.setStyleSheet("color: gray;") azure_help.setWordWrap(True) azure_help.setContentsMargins(0, 10, 0, 0) azure_ocr_layout.addWidget(azure_help) # Rotation correction self.enable_rotation_checkbox = self._create_styled_checkbox("Enable automatic rotation correction for tilted text") self.enable_rotation_checkbox.setChecked(self.settings['ocr']['enable_rotation_correction']) merge_layout.addWidget(self.enable_rotation_checkbox) # OCR batching and locality settings ocr_batch_group = QGroupBox("OCR Batching & Concurrency") ocr_batch_layout = QVBoxLayout(ocr_batch_group) content_layout.addWidget(ocr_batch_group) # Enable OCR batching self.ocr_batch_enabled_checkbox = self._create_styled_checkbox("Enable OCR batching (independent of translation batching)") self.ocr_batch_enabled_checkbox.setChecked(self.settings['ocr'].get('ocr_batch_enabled', True)) self.ocr_batch_enabled_checkbox.stateChanged.connect(self._toggle_ocr_batching_controls) ocr_batch_layout.addWidget(self.ocr_batch_enabled_checkbox) # OCR batch size ocr_bs_widget = QWidget() ocr_bs_layout = QHBoxLayout(ocr_bs_widget) ocr_bs_layout.setContentsMargins(0, 0, 0, 0) ocr_batch_layout.addWidget(ocr_bs_widget) self.ocr_bs_row = ocr_bs_widget ocr_bs_label = QLabel("OCR Batch Size:") ocr_bs_label.setMinimumWidth(180) ocr_bs_layout.addWidget(ocr_bs_label) self.ocr_batch_size_spinbox = QSpinBox() self.ocr_batch_size_spinbox.setRange(1, 32) self.ocr_batch_size_spinbox.setValue(int(self.settings['ocr'].get('ocr_batch_size', 8))) ocr_bs_layout.addWidget(self.ocr_batch_size_spinbox) ocr_bs_desc = QLabel("(Google: items/request; Azure: drives concurrency)") ocr_bs_desc_font = QFont('Arial', 9) ocr_bs_desc.setFont(ocr_bs_desc_font) ocr_bs_desc.setStyleSheet("color: gray;") ocr_bs_layout.addWidget(ocr_bs_desc) ocr_bs_layout.addStretch() # OCR Max Concurrency ocr_cc_widget = QWidget() ocr_cc_layout = QHBoxLayout(ocr_cc_widget) ocr_cc_layout.setContentsMargins(0, 0, 0, 0) ocr_batch_layout.addWidget(ocr_cc_widget) self.ocr_cc_row = ocr_cc_widget ocr_cc_label = QLabel("OCR Max Concurrency:") ocr_cc_label.setMinimumWidth(180) ocr_cc_layout.addWidget(ocr_cc_label) self.ocr_max_conc_spinbox = QSpinBox() self.ocr_max_conc_spinbox.setRange(1, 8) self.ocr_max_conc_spinbox.setValue(int(self.settings['ocr'].get('ocr_max_concurrency', 2))) ocr_cc_layout.addWidget(self.ocr_max_conc_spinbox) ocr_cc_desc = QLabel("(Google: concurrent requests; Azure: workers, capped at 4)") ocr_cc_desc_font = QFont('Arial', 9) ocr_cc_desc.setFont(ocr_cc_desc_font) ocr_cc_desc.setStyleSheet("color: gray;") ocr_cc_layout.addWidget(ocr_cc_desc) ocr_cc_layout.addStretch() # Apply initial visibility for OCR batching controls try: self._toggle_ocr_batching_controls() except Exception: pass # ROI sizing roi_group = QGroupBox("ROI Locality Controls") roi_layout = QVBoxLayout(roi_group) content_layout.addWidget(roi_group) # ROI locality toggle (now inside this section) self.roi_locality_checkbox = self._create_styled_checkbox("Enable ROI-based OCR locality and batching (uses bubble detection)") self.roi_locality_checkbox.setChecked(self.settings['ocr'].get('roi_locality_enabled', False)) self.roi_locality_checkbox.stateChanged.connect(self._toggle_roi_locality_controls) roi_layout.addWidget(self.roi_locality_checkbox) # ROI padding ratio roi_pad_widget = QWidget() roi_pad_layout = QHBoxLayout(roi_pad_widget) roi_pad_layout.setContentsMargins(0, 0, 0, 0) roi_layout.addWidget(roi_pad_widget) self.roi_pad_row = roi_pad_widget roi_pad_label = QLabel("ROI Padding Ratio:") roi_pad_label.setMinimumWidth(180) roi_pad_layout.addWidget(roi_pad_label) self.roi_padding_ratio_slider = QSlider(Qt.Orientation.Horizontal) self.roi_padding_ratio_slider.setRange(0, 30) self.roi_padding_ratio_slider.setValue(int(float(self.settings['ocr'].get('roi_padding_ratio', 0.08)) * 100)) self.roi_padding_ratio_slider.setMinimumWidth(200) roi_pad_layout.addWidget(self.roi_padding_ratio_slider) self.roi_padding_ratio_label = QLabel(f"{float(self.settings['ocr'].get('roi_padding_ratio', 0.08)):.2f}") self.roi_padding_ratio_label.setMinimumWidth(50) self.roi_padding_ratio_slider.valueChanged.connect( lambda v: self.roi_padding_ratio_label.setText(f"{v/100:.2f}") ) roi_pad_layout.addWidget(self.roi_padding_ratio_label) roi_pad_layout.addStretch() # ROI min side / area roi_min_widget = QWidget() roi_min_layout = QHBoxLayout(roi_min_widget) roi_min_layout.setContentsMargins(0, 0, 0, 0) roi_layout.addWidget(roi_min_widget) self.roi_min_row = roi_min_widget roi_min_label = QLabel("Min ROI Side:") roi_min_label.setMinimumWidth(180) roi_min_layout.addWidget(roi_min_label) self.roi_min_side_spinbox = QSpinBox() self.roi_min_side_spinbox.setRange(1, 64) self.roi_min_side_spinbox.setValue(int(self.settings['ocr'].get('roi_min_side_px', 12))) roi_min_layout.addWidget(self.roi_min_side_spinbox) roi_min_unit = QLabel("px") roi_min_layout.addWidget(roi_min_unit) roi_min_layout.addStretch() roi_area_widget = QWidget() roi_area_layout = QHBoxLayout(roi_area_widget) roi_area_layout.setContentsMargins(0, 0, 0, 0) roi_layout.addWidget(roi_area_widget) self.roi_area_row = roi_area_widget roi_area_label = QLabel("Min ROI Area:") roi_area_label.setMinimumWidth(180) roi_area_layout.addWidget(roi_area_label) self.roi_min_area_spinbox = QSpinBox() self.roi_min_area_spinbox.setRange(1, 5000) self.roi_min_area_spinbox.setValue(int(self.settings['ocr'].get('roi_min_area_px', 100))) roi_area_layout.addWidget(self.roi_min_area_spinbox) roi_area_unit = QLabel("px^2") roi_area_layout.addWidget(roi_area_unit) roi_area_layout.addStretch() # ROI max side (0 disables) roi_max_widget = QWidget() roi_max_layout = QHBoxLayout(roi_max_widget) roi_max_layout.setContentsMargins(0, 0, 0, 0) roi_layout.addWidget(roi_max_widget) self.roi_max_row = roi_max_widget roi_max_label = QLabel("ROI Max Side (0=off):") roi_max_label.setMinimumWidth(180) roi_max_layout.addWidget(roi_max_label) self.roi_max_side_spinbox = QSpinBox() self.roi_max_side_spinbox.setRange(0, 2048) self.roi_max_side_spinbox.setValue(int(self.settings['ocr'].get('roi_max_side', 0))) roi_max_layout.addWidget(self.roi_max_side_spinbox) roi_max_layout.addStretch() # Apply initial visibility based on toggle self._toggle_roi_locality_controls() # AI Bubble Detection Settings bubble_group = QGroupBox("AI Bubble Detection") bubble_layout = QVBoxLayout(bubble_group) content_layout.addWidget(bubble_group) # Enable bubble detection self.bubble_detection_enabled_checkbox = self._create_styled_checkbox("Enable AI-powered bubble detection (overrides traditional merging)") # IMPORTANT: Default to True for optimal text detection (especially for Chinese/Japanese text) self.bubble_detection_enabled_checkbox.setChecked(self.settings['ocr'].get('bubble_detection_enabled', True)) self.bubble_detection_enabled_checkbox.stateChanged.connect(self._toggle_bubble_controls) bubble_layout.addWidget(self.bubble_detection_enabled_checkbox) # Use RT-DETR for text region detection (not just bubble detection) self.use_rtdetr_for_ocr_checkbox = self._create_styled_checkbox("Use RT-DETR to guide OCR (Google/Azure only - others already do this)") self.use_rtdetr_for_ocr_checkbox.setChecked(self.settings['ocr'].get('use_rtdetr_for_ocr_regions', True)) # Default: True self.use_rtdetr_for_ocr_checkbox.setToolTip( "When enabled, RT-DETR first detects all text regions (text bubbles + free text), \n" "then your OCR provider reads each region separately.\n\n" "🎯 Applies to: Google Cloud Vision, Azure Computer Vision\n" "✓ Already enabled: Qwen2-VL, Custom API, EasyOCR, PaddleOCR, DocTR, manga-ocr\n\n" "Benefits:\n" "• More accurate text detection (trained specifically for manga/comics)\n" "• Better separation of overlapping text\n" "• Improved handling of different text types (bubbles vs. free text)\n" "• Focused OCR on actual text regions (faster, more accurate)\n\n" "Note: Requires bubble detection to be enabled and uses the selected detector above." ) bubble_layout.addWidget(self.use_rtdetr_for_ocr_checkbox) # Detector type dropdown detector_type_widget = QWidget() detector_type_layout = QHBoxLayout(detector_type_widget) detector_type_layout.setContentsMargins(0, 10, 0, 0) bubble_layout.addWidget(detector_type_widget) detector_type_label = QLabel("Detector:") detector_type_label.setMinimumWidth(120) detector_type_layout.addWidget(detector_type_label) # 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_combo = QComboBox() self.detector_type_combo.addItems(list(self.detector_models.keys())) self.detector_type_combo.setCurrentText(initial_selection) self.detector_type_combo.currentTextChanged.connect(self._on_detector_type_changed) detector_type_layout.addWidget(self.detector_type_combo) detector_type_layout.addStretch() # NOW create the settings frame self.yolo_settings_group = QGroupBox("Model Settings") yolo_settings_layout = QVBoxLayout(self.yolo_settings_group) bubble_layout.addWidget(self.yolo_settings_group) self.rtdetr_settings_frame = self.yolo_settings_group # Alias for compatibility # Model path/URL row model_widget = QWidget() model_layout = QHBoxLayout(model_widget) model_layout.setContentsMargins(0, 5, 0, 0) yolo_settings_layout.addWidget(model_widget) model_label = QLabel("Model:") model_label.setMinimumWidth(100) model_layout.addWidget(model_label) self.bubble_model_entry = QLineEdit() self.bubble_model_entry.setText(self.settings['ocr'].get('bubble_model_path', '')) self.bubble_model_entry.setReadOnly(True) self.bubble_model_entry.setStyleSheet( "QLineEdit { background-color: #1e1e1e; color: #ffffff; border: 1px solid #3a3a3a; }" ) model_layout.addWidget(self.bubble_model_entry) self.rtdetr_url_entry = self.bubble_model_entry # Alias # Store for compatibility self.detector_radio_widgets = [self.detector_type_combo] # Browse and Clear buttons (initially hidden for HuggingFace models) self.bubble_browse_btn = QPushButton("Browse") self.bubble_browse_btn.clicked.connect(self._browse_bubble_model) model_layout.addWidget(self.bubble_browse_btn) self.bubble_clear_btn = QPushButton("Clear") self.bubble_clear_btn.clicked.connect(self._clear_bubble_model) model_layout.addWidget(self.bubble_clear_btn) model_layout.addStretch() # Download and Load buttons button_widget = QWidget() button_layout = QHBoxLayout(button_widget) button_layout.setContentsMargins(0, 10, 0, 0) yolo_settings_layout.addWidget(button_widget) button_label = QLabel("Actions:") button_label.setMinimumWidth(100) button_layout.addWidget(button_label) self.rtdetr_download_btn = QPushButton("Download") self.rtdetr_download_btn.clicked.connect(self._download_rtdetr_model) self.rtdetr_download_btn.setStyleSheet(""" QPushButton { background-color: #5a9fd4; color: white; font-weight: bold; border: none; border-radius: 3px; padding: 5px 15px; } QPushButton:hover { background-color: #7bb3e0; } QPushButton:pressed { background-color: #4a8fc4; } """) button_layout.addWidget(self.rtdetr_download_btn) self.rtdetr_load_btn = QPushButton("Load Model") self.rtdetr_load_btn.clicked.connect(self._load_rtdetr_model) self.rtdetr_load_btn.setStyleSheet(""" QPushButton { background-color: #5a9fd4; color: white; font-weight: bold; border: none; border-radius: 3px; padding: 5px 15px; } QPushButton:hover { background-color: #7bb3e0; } QPushButton:pressed { background-color: #4a8fc4; } """) button_layout.addWidget(self.rtdetr_load_btn) self.rtdetr_status_label = QLabel("") rtdetr_status_font = QFont('Arial', 9) self.rtdetr_status_label.setFont(rtdetr_status_font) button_layout.addWidget(self.rtdetr_status_label) button_layout.addStretch() # RT-DETR Detection classes rtdetr_classes_widget = QWidget() rtdetr_classes_layout = QHBoxLayout(rtdetr_classes_widget) rtdetr_classes_layout.setContentsMargins(0, 10, 0, 0) yolo_settings_layout.addWidget(rtdetr_classes_widget) self.rtdetr_classes_frame = rtdetr_classes_widget classes_label = QLabel("Detect:") classes_label.setMinimumWidth(100) rtdetr_classes_layout.addWidget(classes_label) self.detect_empty_bubbles_checkbox = self._create_styled_checkbox("Empty Bubbles") self.detect_empty_bubbles_checkbox.setChecked(self.settings['ocr'].get('detect_empty_bubbles', True)) rtdetr_classes_layout.addWidget(self.detect_empty_bubbles_checkbox) self.detect_text_bubbles_checkbox = self._create_styled_checkbox("Text Bubbles") self.detect_text_bubbles_checkbox.setChecked(self.settings['ocr'].get('detect_text_bubbles', True)) rtdetr_classes_layout.addWidget(self.detect_text_bubbles_checkbox) self.detect_free_text_checkbox = self._create_styled_checkbox("Free Text") self.detect_free_text_checkbox.setChecked(self.settings['ocr'].get('detect_free_text', True)) rtdetr_classes_layout.addWidget(self.detect_free_text_checkbox) rtdetr_classes_layout.addStretch() # Confidence conf_widget = QWidget() conf_layout = QHBoxLayout(conf_widget) conf_layout.setContentsMargins(0, 10, 0, 0) yolo_settings_layout.addWidget(conf_widget) conf_label = QLabel("Confidence:") conf_label.setMinimumWidth(100) conf_layout.addWidget(conf_label) detector_label = self.detector_type_combo.currentText() 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_conf_slider = QSlider(Qt.Orientation.Horizontal) self.bubble_conf_slider.setRange(0, 99) self.bubble_conf_slider.setValue(int(self.settings['ocr'].get('bubble_confidence', default_conf) * 100)) self.bubble_conf_slider.setMinimumWidth(200) conf_layout.addWidget(self.bubble_conf_slider) self.rtdetr_conf_scale = self.bubble_conf_slider # Alias self.bubble_conf_label = QLabel(f"{self.settings['ocr'].get('bubble_confidence', default_conf):.2f}") self.bubble_conf_label.setMinimumWidth(50) self.bubble_conf_slider.valueChanged.connect( lambda v: self.bubble_conf_label.setText(f"{v/100:.2f}") ) conf_layout.addWidget(self.bubble_conf_label) self.rtdetr_conf_label = self.bubble_conf_label # Alias conf_layout.addStretch() # YOLO-specific: Max detections (only visible for YOLO) self.yolo_maxdet_widget = QWidget() yolo_maxdet_layout = QHBoxLayout(self.yolo_maxdet_widget) yolo_maxdet_layout.setContentsMargins(0, 6, 0, 0) yolo_settings_layout.addWidget(self.yolo_maxdet_widget) self.yolo_maxdet_row = self.yolo_maxdet_widget # Alias self.yolo_maxdet_widget.setVisible(False) # Hidden initially maxdet_label = QLabel("Max detections:") maxdet_label.setMinimumWidth(100) yolo_maxdet_layout.addWidget(maxdet_label) self.bubble_max_det_yolo_spinbox = QSpinBox() self.bubble_max_det_yolo_spinbox.setRange(1, 2000) self.bubble_max_det_yolo_spinbox.setValue(self.settings['ocr'].get('bubble_max_detections_yolo', 100)) yolo_maxdet_layout.addWidget(self.bubble_max_det_yolo_spinbox) yolo_maxdet_layout.addStretch() # Status label at the bottom of bubble group self.bubble_status_label = QLabel("") bubble_status_font = QFont('Arial', 9) self.bubble_status_label.setFont(bubble_status_font) bubble_status_label_container = QWidget() bubble_status_label_layout = QVBoxLayout(bubble_status_label_container) bubble_status_label_layout.setContentsMargins(0, 10, 0, 0) bubble_status_label_layout.addWidget(self.bubble_status_label) bubble_layout.addWidget(bubble_status_label_container) # Store controls for enable/disable self.bubble_controls = [ self.detector_type_combo, self.bubble_model_entry, self.bubble_browse_btn, self.bubble_clear_btn, self.bubble_conf_slider, self.rtdetr_download_btn, self.rtdetr_load_btn ] self.rtdetr_controls = [ self.bubble_model_entry, self.rtdetr_load_btn, self.rtdetr_download_btn, self.bubble_conf_slider, self.detect_empty_bubbles_checkbox, self.detect_text_bubbles_checkbox, self.detect_free_text_checkbox ] self.yolo_controls = [ self.bubble_model_entry, self.bubble_browse_btn, self.bubble_clear_btn, self.bubble_conf_slider, self.yolo_maxdet_widget ] # Add stretch to end of OCR tab content content_layout.addStretch() # Initialize control states self._toggle_bubble_controls() # Only call detector change after everything is initialized if self.bubble_detection_enabled_checkbox.isChecked(): try: self._on_detector_type_changed() self._update_bubble_status() except AttributeError: # Frames not yet created, skip initialization pass # Check status after dialog ready QTimer.singleShot(500, self._check_rtdetr_status) def _on_detector_type_changed(self, detector=None): """Handle detector type change""" if not hasattr(self, 'bubble_detection_enabled_checkbox'): return if not self.bubble_detection_enabled_checkbox.isChecked(): self.yolo_settings_group.setVisible(False) return if detector is None: detector = self.detector_type_combo.currentText() # Handle different detector types if detector == 'Custom Model': # Custom model - enable manual entry self.bubble_model_entry.setText(self.settings['ocr'].get('custom_model_path', '')) self.bubble_model_entry.setReadOnly(False) self.bubble_model_entry.setStyleSheet( "QLineEdit { background-color: #2b2b2b; color: #ffffff; border: 1px solid #3a3a3a; }" ) # Show browse/clear buttons for custom self.bubble_browse_btn.setVisible(True) self.bubble_clear_btn.setVisible(True) # Hide download button self.rtdetr_download_btn.setVisible(False) elif detector in self.detector_models: # HuggingFace model url = self.detector_models[detector] self.bubble_model_entry.setText(url) # Make entry read-only for HuggingFace models self.bubble_model_entry.setReadOnly(True) self.bubble_model_entry.setStyleSheet( "QLineEdit { background-color: #1e1e1e; color: #ffffff; border: 1px solid #3a3a3a; }" ) # Hide browse/clear buttons for HuggingFace models self.bubble_browse_btn.setVisible(False) self.bubble_clear_btn.setVisible(False) # Show download button self.rtdetr_download_btn.setVisible(True) # Show/hide RT-DETR specific controls is_rtdetr = 'RT-DETR' in detector or 'RTEDR_onnx' in detector if is_rtdetr: self.rtdetr_classes_frame.setVisible(True) # Hide YOLO-only max det row self.yolo_maxdet_widget.setVisible(False) else: self.rtdetr_classes_frame.setVisible(False) # 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_widget.setVisible(True) else: self.yolo_maxdet_widget.setVisible(False) # Show/hide RT-DETR concurrency control in Performance section (Advanced tab) # Only update if the widget has been created (Advanced tab may not be loaded yet) if hasattr(self, 'rtdetr_conc_frame'): self.rtdetr_conc_frame.setVisible(is_rtdetr) # Always show settings frame self.yolo_settings_group.setVisible(True) # Update status self._update_bubble_status() def _download_rtdetr_model(self): """Download selected model""" try: detector = self.detector_type_combo.currentText() model_url = self.bubble_model_entry.text() self.rtdetr_status_label.setText("Downloading...") self.rtdetr_status_label.setStyleSheet("color: orange;") QApplication.processEvents() 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.setText("✅ Downloaded") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "Success", f"RTEDR_onnx model downloaded successfully!") else: self.rtdetr_status_label.setText("❌ Failed") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "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.setText("✅ Downloaded") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "Success", f"RT-DETR model downloaded successfully!") else: self.rtdetr_status_label.setText("❌ Failed") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "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_entry.setText(local_path) self.rtdetr_status_label.setText("✅ Downloaded") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "Success", f"Model downloaded to:\n{local_path}") except ImportError: self.rtdetr_status_label.setText("❌ Missing deps") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "Error", "Install: pip install huggingface-hub transformers") except Exception as e: self.rtdetr_status_label.setText("❌ Error") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "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.setText("✅ Loaded") self.rtdetr_status_label.setStyleSheet("color: green;") return True if getattr(translator.bubble_detector, 'rtdetr_loaded', False): self.rtdetr_status_label.setText("✅ Loaded") self.rtdetr_status_label.setStyleSheet("color: green;") return True elif getattr(translator.bubble_detector, 'model_loaded', False): self.rtdetr_status_label.setText("✅ Loaded") self.rtdetr_status_label.setStyleSheet("color: green;") return True self.rtdetr_status_label.setText("Not loaded") self.rtdetr_status_label.setStyleSheet("color: gray;") return False except ImportError: self.rtdetr_status_label.setText("❌ Missing deps") self.rtdetr_status_label.setStyleSheet("color: red;") return False except Exception: self.rtdetr_status_label.setText("Not loaded") self.rtdetr_status_label.setStyleSheet("color: gray;") return False def _load_rtdetr_model(self): """Load selected model""" try: from bubble_detector import BubbleDetector from PySide6.QtWidgets import QApplication self.rtdetr_status_label.setText("Loading...") self.rtdetr_status_label.setStyleSheet("color: orange;") QApplication.processEvents() bd = BubbleDetector() detector = self.detector_type_combo.currentText() model_path = self.bubble_model_entry.text() 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.setText("✅ Ready") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "Success", f"RTEDR_onnx model loaded successfully!") else: self.rtdetr_status_label.setText("❌ Failed") self.rtdetr_status_label.setStyleSheet("color: 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.setText("✅ Ready") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "Success", f"RT-DETR model loaded successfully!") else: self.rtdetr_status_label.setText("❌ Failed") self.rtdetr_status_label.setStyleSheet("color: 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_entry.setText(local_path) # Update the field else: # Not downloaded yet QMessageBox.warning(self, "Download Required", f"Model not found locally.\nPlease download it first using the Download button.") self.rtdetr_status_label.setText("❌ Not downloaded") self.rtdetr_status_label.setStyleSheet("color: orange;") return # Now model_path should be a local file if not os.path.exists(model_path): QMessageBox.critical(self, "Error", f"Model file not found: {model_path}") self.rtdetr_status_label.setText("❌ File not found") self.rtdetr_status_label.setStyleSheet("color: red;") return # Load the YOLOv8 model from local file if bd.load_model(model_path): self.rtdetr_status_label.setText("✅ Ready") self.rtdetr_status_label.setStyleSheet("color: green;") QMessageBox.information(self, "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.setText("❌ Failed") self.rtdetr_status_label.setStyleSheet("color: red;") except ImportError: self.rtdetr_status_label.setText("❌ Missing deps") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "Error", "Install transformers: pip install transformers") except Exception as e: self.rtdetr_status_label.setText("❌ Error") self.rtdetr_status_label.setStyleSheet("color: red;") QMessageBox.critical(self, "Error", f"Failed to load: {e}") def _toggle_bubble_controls(self): """Enable/disable bubble detection controls""" enabled = self.bubble_detection_enabled_checkbox.isChecked() if enabled: # Enable controls for widget in self.bubble_controls: try: widget.setEnabled(True) 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.setEnabled(False) except: pass # Hide frames self.yolo_settings_group.setVisible(False) self.bubble_status_label.setText("") def _browse_bubble_model(self): """Browse for model file""" path, _ = QFileDialog.getOpenFileName( self, "Select Model File", "", "Model files (*.pt *.pth *.bin *.safetensors);;All files (*.*)" ) if path: self.bubble_model_entry.setText(path) self._update_bubble_status() def _clear_bubble_model(self): """Clear selected model""" self.bubble_model_entry.setText("") self._update_bubble_status() def _update_bubble_status(self): """Update bubble model status label""" if not self.bubble_detection_enabled_checkbox.isChecked(): self.bubble_status_label.setText("") return detector = self.detector_type_combo.currentText() model_path = self.bubble_model_entry.text() if not model_path: self.bubble_status_label.setText("⚠️ No model selected") self.bubble_status_label.setStyleSheet("color: orange;") return if model_path.startswith("ogkalu/"): self.bubble_status_label.setText(f"📥 {detector} ready to download") self.bubble_status_label.setStyleSheet("color: blue;") elif os.path.exists(model_path): self.bubble_status_label.setText("✅ Model file ready") self.bubble_status_label.setStyleSheet("color: green;") else: self.bubble_status_label.setText("❌ Model file not found") self.bubble_status_label.setStyleSheet("color: red;") def _update_azure_label(self): """Update Azure multiplier label""" # This method is deprecated - Azure multiplier UI was removed pass def _set_azure_multiplier(self, value): """Set Azure multiplier from preset""" # This method is deprecated - Azure multiplier UI was removed pass def _create_advanced_tab(self): """Create advanced settings tab with all options""" # Create tab widget and add to tab widget tab_widget = QWidget() self.tab_widget.addTab(tab_widget, "Advanced") # Main scrollable content main_layout = QVBoxLayout(tab_widget) main_layout.setContentsMargins(5, 5, 5, 5) main_layout.setSpacing(6) # Format detection detect_group = QGroupBox("Format Detection") main_layout.addWidget(detect_group) detect_layout = QVBoxLayout(detect_group) detect_layout.setContentsMargins(8, 8, 8, 6) detect_layout.setSpacing(4) self.format_detection_checkbox = self._create_styled_checkbox("Enable automatic manga format detection (reading direction)") self.format_detection_checkbox.setChecked(self.settings['advanced']['format_detection']) detect_layout.addWidget(self.format_detection_checkbox) # Webtoon mode webtoon_frame = QWidget() webtoon_layout = QHBoxLayout(webtoon_frame) webtoon_layout.setContentsMargins(0, 0, 0, 0) detect_layout.addWidget(webtoon_frame) webtoon_label = QLabel("Webtoon Mode:") webtoon_label.setMinimumWidth(150) webtoon_layout.addWidget(webtoon_label) self.webtoon_mode_combo = QComboBox() self.webtoon_mode_combo.addItems(['auto', 'enabled', 'disabled']) self.webtoon_mode_combo.setCurrentText(self.settings['advanced']['webtoon_mode']) webtoon_layout.addWidget(self.webtoon_mode_combo) webtoon_layout.addStretch() # Debug settings debug_group = QGroupBox("Debug Options") main_layout.addWidget(debug_group) debug_layout = QVBoxLayout(debug_group) debug_layout.setContentsMargins(8, 8, 8, 6) debug_layout.setSpacing(4) self.debug_mode_checkbox = self._create_styled_checkbox("Enable debug mode (verbose logging)") self.debug_mode_checkbox.setChecked(self.settings['advanced']['debug_mode']) debug_layout.addWidget(self.debug_mode_checkbox) # New: Concise pipeline logs (reduce noise) self.concise_logs_checkbox = self._create_styled_checkbox("Concise pipeline logs (reduce noise)") self.concise_logs_checkbox.setChecked(bool(self.settings.get('advanced', {}).get('concise_logs', True))) def _save_concise(): try: if 'advanced' not in self.settings: self.settings['advanced'] = {} self.settings['advanced']['concise_logs'] = bool(self.concise_logs_checkbox.isChecked()) 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 self.concise_logs_checkbox.toggled.connect(_save_concise) debug_layout.addWidget(self.concise_logs_checkbox) self.save_intermediate_checkbox = self._create_styled_checkbox("Save intermediate images (preprocessed, detection overlays)") self.save_intermediate_checkbox.setChecked(self.settings['advanced']['save_intermediate']) debug_layout.addWidget(self.save_intermediate_checkbox) # Performance settings perf_group = QGroupBox("Performance") main_layout.addWidget(perf_group) perf_layout = QVBoxLayout(perf_group) perf_layout.setContentsMargins(8, 8, 8, 6) perf_layout.setSpacing(4) # New: Parallel rendering (per-region overlays) self.render_parallel_checkbox = self._create_styled_checkbox("Enable parallel rendering (per-region overlays)") self.render_parallel_checkbox.setChecked(self.settings.get('advanced', {}).get('render_parallel', True)) perf_layout.addWidget(self.render_parallel_checkbox) self.parallel_processing_checkbox = self._create_styled_checkbox("Enable parallel processing (experimental)") self.parallel_processing_checkbox.setChecked(self.settings['advanced']['parallel_processing']) self.parallel_processing_checkbox.toggled.connect(self._toggle_workers) perf_layout.addWidget(self.parallel_processing_checkbox) # Max workers workers_frame = QWidget() workers_layout = QHBoxLayout(workers_frame) workers_layout.setContentsMargins(0, 0, 0, 0) perf_layout.addWidget(workers_frame) self.workers_label = QLabel("Max Workers:") self.workers_label.setMinimumWidth(150) workers_layout.addWidget(self.workers_label) self.max_workers_spinbox = QSpinBox() self.max_workers_spinbox.setRange(1, 999) self.max_workers_spinbox.setValue(self.settings['advanced']['max_workers']) workers_layout.addWidget(self.max_workers_spinbox) self.workers_desc_label = QLabel("(threads for parallel processing)") workers_layout.addWidget(self.workers_desc_label) workers_layout.addStretch() # Initialize workers state self._toggle_workers() # Memory management section memory_group = QGroupBox("Memory Management") main_layout.addWidget(memory_group) memory_layout = QVBoxLayout(memory_group) memory_layout.setContentsMargins(8, 8, 8, 6) memory_layout.setSpacing(4) # Singleton mode checkbox - will connect handler later after panel widgets created self.use_singleton_models_checkbox = self._create_styled_checkbox("Use single model instances (saves RAM, only affects local models)") self.use_singleton_models_checkbox.setChecked(self.settings.get('advanced', {}).get('use_singleton_models', True)) self.use_singleton_models_checkbox.toggled.connect(self._toggle_singleton_controls) memory_layout.addWidget(self.use_singleton_models_checkbox) # Singleton note singleton_note = QLabel( "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." ) singleton_note_font = QFont('Arial', 9) singleton_note.setFont(singleton_note_font) singleton_note.setStyleSheet("color: gray;") singleton_note.setWordWrap(True) memory_layout.addWidget(singleton_note) self.auto_cleanup_models_checkbox = self._create_styled_checkbox("Automatically cleanup models after translation to free RAM") self.auto_cleanup_models_checkbox.setChecked(self.settings.get('advanced', {}).get('auto_cleanup_models', False)) memory_layout.addWidget(self.auto_cleanup_models_checkbox) # Unload models after translation (disabled by default) self.unload_models_checkbox = self._create_styled_checkbox("Unload models after translation (reset translator instance)") self.unload_models_checkbox.setChecked(self.settings.get('advanced', {}).get('unload_models_after_translation', False)) memory_layout.addWidget(self.unload_models_checkbox) # Add a note about parallel processing note_label = QLabel("Note: When parallel panel translation is enabled, cleanup happens after ALL panels complete.") note_font = QFont('Arial', 9) note_label.setFont(note_font) note_label.setStyleSheet("color: gray;") note_label.setWordWrap(True) memory_layout.addWidget(note_label) # Panel-level parallel translation panel_group = QGroupBox("Parallel Panel Translation") main_layout.addWidget(panel_group) panel_layout = QVBoxLayout(panel_group) panel_layout.setContentsMargins(8, 8, 8, 6) panel_layout.setSpacing(4) # New: Preload local inpainting for panels (default ON) self.preload_local_panels_checkbox = self._create_styled_checkbox("Preload local inpainting instances for panel-parallel runs") self.preload_local_panels_checkbox.setChecked(self.settings.get('advanced', {}).get('preload_local_inpainting_for_panels', True)) panel_layout.addWidget(self.preload_local_panels_checkbox) self.parallel_panel_checkbox = self._create_styled_checkbox("Enable parallel panel translation (process multiple images concurrently)") self.parallel_panel_checkbox.setChecked(self.settings.get('advanced', {}).get('parallel_panel_translation', False)) self.parallel_panel_checkbox.toggled.connect(self._toggle_panel_controls) panel_layout.addWidget(self.parallel_panel_checkbox) # Local LLM Performance (add to performance group) inpaint_perf_group = QGroupBox("Local LLM Performance") perf_layout.addWidget(inpaint_perf_group) inpaint_perf_layout = QVBoxLayout(inpaint_perf_group) inpaint_perf_layout.setContentsMargins(8, 8, 8, 6) inpaint_perf_layout.setSpacing(4) # RT-DETR Concurrency (for memory optimization) rtdetr_conc_widget = QWidget() rtdetr_conc_layout = QHBoxLayout(rtdetr_conc_widget) rtdetr_conc_layout.setContentsMargins(0, 0, 0, 0) inpaint_perf_layout.addWidget(rtdetr_conc_widget) self.rtdetr_conc_frame = rtdetr_conc_widget rtdetr_conc_label = QLabel("RT-DETR Concurrency:") rtdetr_conc_label.setMinimumWidth(150) rtdetr_conc_layout.addWidget(rtdetr_conc_label) self.rtdetr_max_concurrency_spinbox = QSpinBox() self.rtdetr_max_concurrency_spinbox.setRange(1, 999) self.rtdetr_max_concurrency_spinbox.setValue(self.settings['ocr'].get('rtdetr_max_concurrency', 12)) self.rtdetr_max_concurrency_spinbox.setToolTip("Maximum concurrent RT-DETR region OCR calls (rate limiting handled via delays)") rtdetr_conc_layout.addWidget(self.rtdetr_max_concurrency_spinbox) rtdetr_conc_desc = QLabel("parallel OCR calls (lower = less RAM)") rtdetr_conc_desc_font = QFont('Arial', 9) rtdetr_conc_desc.setFont(rtdetr_conc_desc_font) rtdetr_conc_desc.setStyleSheet("color: gray;") rtdetr_conc_layout.addWidget(rtdetr_conc_desc) rtdetr_conc_layout.addStretch() # Initially hide RT-DETR concurrency control until we check detector type self.rtdetr_conc_frame.setVisible(False) # Inpainting Concurrency inpaint_bs_frame = QWidget() inpaint_bs_layout = QHBoxLayout(inpaint_bs_frame) inpaint_bs_layout.setContentsMargins(0, 0, 0, 0) inpaint_perf_layout.addWidget(inpaint_bs_frame) inpaint_bs_label = QLabel("Inpainting Concurrency:") inpaint_bs_label.setMinimumWidth(150) inpaint_bs_layout.addWidget(inpaint_bs_label) self.inpaint_batch_size_spinbox = QSpinBox() self.inpaint_batch_size_spinbox.setRange(1, 32) self.inpaint_batch_size_spinbox.setValue(self.settings.get('inpainting', {}).get('batch_size', 10)) inpaint_bs_layout.addWidget(self.inpaint_batch_size_spinbox) inpaint_bs_help = QLabel("(process multiple regions at once)") inpaint_bs_help_font = QFont('Arial', 9) inpaint_bs_help.setFont(inpaint_bs_help_font) inpaint_bs_help.setStyleSheet("color: gray;") inpaint_bs_layout.addWidget(inpaint_bs_help) inpaint_bs_layout.addStretch() self.enable_cache_checkbox = self._create_styled_checkbox("Enable inpainting cache (speeds up repeated processing)") self.enable_cache_checkbox.setChecked(self.settings.get('inpainting', {}).get('enable_cache', True)) inpaint_perf_layout.addWidget(self.enable_cache_checkbox) # Max concurrent panels panels_frame = QWidget() panels_layout = QHBoxLayout(panels_frame) panels_layout.setContentsMargins(0, 0, 0, 0) panel_layout.addWidget(panels_frame) self.panels_label = QLabel("Max concurrent panels:") self.panels_label.setMinimumWidth(150) panels_layout.addWidget(self.panels_label) self.panel_max_workers_spinbox = QSpinBox() self.panel_max_workers_spinbox.setRange(1, 12) self.panel_max_workers_spinbox.setValue(self.settings.get('advanced', {}).get('panel_max_workers', 2)) panels_layout.addWidget(self.panel_max_workers_spinbox) panels_layout.addStretch() # Panel start stagger (ms) stagger_frame = QWidget() stagger_layout = QHBoxLayout(stagger_frame) stagger_layout.setContentsMargins(0, 0, 0, 0) panel_layout.addWidget(stagger_frame) self.stagger_label = QLabel("Panel start stagger:") self.stagger_label.setMinimumWidth(150) stagger_layout.addWidget(self.stagger_label) self.panel_stagger_ms_spinbox = QSpinBox() self.panel_stagger_ms_spinbox.setRange(0, 1000) self.panel_stagger_ms_spinbox.setValue(self.settings.get('advanced', {}).get('panel_start_stagger_ms', 30)) stagger_layout.addWidget(self.panel_stagger_ms_spinbox) self.stagger_unit_label = QLabel("ms") stagger_layout.addWidget(self.stagger_unit_label) stagger_layout.addStretch() # Initialize panel controls state self._toggle_panel_controls() self._toggle_singleton_controls() # ONNX conversion settings onnx_group = QGroupBox("ONNX Conversion") main_layout.addWidget(onnx_group) onnx_layout = QVBoxLayout(onnx_group) onnx_layout.setContentsMargins(8, 8, 8, 6) onnx_layout.setSpacing(4) self.auto_convert_onnx_checkbox = self._create_styled_checkbox("Auto-convert local models to ONNX for faster inference (recommended)") self.auto_convert_onnx_checkbox.setChecked(self.settings['advanced'].get('auto_convert_to_onnx', False)) onnx_layout.addWidget(self.auto_convert_onnx_checkbox) self.auto_convert_onnx_bg_checkbox = self._create_styled_checkbox("Convert in background (non-blocking; switches to ONNX when ready)") self.auto_convert_onnx_bg_checkbox.setChecked(self.settings['advanced'].get('auto_convert_to_onnx_background', True)) onnx_layout.addWidget(self.auto_convert_onnx_bg_checkbox) # Connect toggle handler def _toggle_onnx_controls(): self.auto_convert_onnx_bg_checkbox.setEnabled(self.auto_convert_onnx_checkbox.isChecked()) self.auto_convert_onnx_checkbox.toggled.connect(_toggle_onnx_controls) _toggle_onnx_controls() # Model memory optimization (quantization) quant_group = QGroupBox("Model Memory Optimization") main_layout.addWidget(quant_group) quant_layout = QVBoxLayout(quant_group) quant_layout.setContentsMargins(8, 8, 8, 6) quant_layout.setSpacing(4) self.quantize_models_checkbox = self._create_styled_checkbox("Reduce RAM with quantized models (global switch)") self.quantize_models_checkbox.setChecked(self.settings['advanced'].get('quantize_models', False)) quant_layout.addWidget(self.quantize_models_checkbox) # ONNX quantize sub-toggle onnx_quant_frame = QWidget() onnx_quant_layout = QHBoxLayout(onnx_quant_frame) onnx_quant_layout.setContentsMargins(0, 0, 0, 0) quant_layout.addWidget(onnx_quant_frame) self.onnx_quantize_checkbox = self._create_styled_checkbox("Quantize ONNX models to INT8 (dynamic)") self.onnx_quantize_checkbox.setChecked(self.settings['advanced'].get('onnx_quantize', False)) onnx_quant_layout.addWidget(self.onnx_quantize_checkbox) onnx_quant_help = QLabel("(lower RAM/CPU; slight accuracy trade-off)") onnx_quant_help_font = QFont('Arial', 9) onnx_quant_help.setFont(onnx_quant_help_font) onnx_quant_help.setStyleSheet("color: gray;") onnx_quant_layout.addWidget(onnx_quant_help) onnx_quant_layout.addStretch() # Torch precision dropdown precision_frame = QWidget() precision_layout = QHBoxLayout(precision_frame) precision_layout.setContentsMargins(0, 0, 0, 0) quant_layout.addWidget(precision_frame) precision_label = QLabel("Torch precision:") precision_label.setMinimumWidth(150) precision_layout.addWidget(precision_label) self.torch_precision_combo = QComboBox() self.torch_precision_combo.addItems(['fp16', 'fp32', 'auto']) self.torch_precision_combo.setCurrentText(self.settings['advanced'].get('torch_precision', 'fp16')) precision_layout.addWidget(self.torch_precision_combo) precision_help = QLabel("(fp16 only, since fp32 is currently bugged)") precision_help_font = QFont('Arial', 9) precision_help.setFont(precision_help_font) precision_help.setStyleSheet("color: gray;") precision_layout.addWidget(precision_help) precision_layout.addStretch() # Aggressive memory cleanup cleanup_group = QGroupBox("Memory & Cleanup") main_layout.addWidget(cleanup_group) cleanup_layout = QVBoxLayout(cleanup_group) cleanup_layout.setContentsMargins(8, 8, 8, 6) cleanup_layout.setSpacing(4) self.force_deep_cleanup_checkbox = self._create_styled_checkbox("Force deep model cleanup after every image (slowest, lowest RAM)") self.force_deep_cleanup_checkbox.setChecked(self.settings.get('advanced', {}).get('force_deep_cleanup_each_image', False)) cleanup_layout.addWidget(self.force_deep_cleanup_checkbox) cleanup_help = QLabel("Also clears shared caches at batch end.") cleanup_help_font = QFont('Arial', 9) cleanup_help.setFont(cleanup_help_font) cleanup_help.setStyleSheet("color: gray;") cleanup_layout.addWidget(cleanup_help) # RAM cap controls self.ram_cap_enabled_checkbox = self._create_styled_checkbox("Enable RAM cap") self.ram_cap_enabled_checkbox.setChecked(self.settings.get('advanced', {}).get('ram_cap_enabled', False)) cleanup_layout.addWidget(self.ram_cap_enabled_checkbox) # RAM cap value ramcap_value_frame = QWidget() ramcap_value_layout = QHBoxLayout(ramcap_value_frame) ramcap_value_layout.setContentsMargins(0, 0, 0, 0) cleanup_layout.addWidget(ramcap_value_frame) ramcap_value_label = QLabel("Max RAM (MB):") ramcap_value_label.setMinimumWidth(150) ramcap_value_layout.addWidget(ramcap_value_label) self.ram_cap_mb_spinbox = QSpinBox() self.ram_cap_mb_spinbox.setRange(512, 131072) self.ram_cap_mb_spinbox.setValue(int(self.settings.get('advanced', {}).get('ram_cap_mb', 0) or 0)) ramcap_value_layout.addWidget(self.ram_cap_mb_spinbox) ramcap_value_help = QLabel("(0 = disabled)") ramcap_value_help_font = QFont('Arial', 9) ramcap_value_help.setFont(ramcap_value_help_font) ramcap_value_help.setStyleSheet("color: gray;") ramcap_value_layout.addWidget(ramcap_value_help) ramcap_value_layout.addStretch() # RAM cap mode ramcap_mode_frame = QWidget() ramcap_mode_layout = QHBoxLayout(ramcap_mode_frame) ramcap_mode_layout.setContentsMargins(0, 0, 0, 0) cleanup_layout.addWidget(ramcap_mode_frame) ramcap_mode_label = QLabel("Cap mode:") ramcap_mode_label.setMinimumWidth(150) ramcap_mode_layout.addWidget(ramcap_mode_label) self.ram_cap_mode_combo = QComboBox() self.ram_cap_mode_combo.addItems(['soft', 'hard (Windows only)']) self.ram_cap_mode_combo.setCurrentText(self.settings.get('advanced', {}).get('ram_cap_mode', 'soft')) ramcap_mode_layout.addWidget(self.ram_cap_mode_combo) ramcap_mode_help = QLabel("Soft = clean/trim, Hard = OS-enforced (may OOM)") ramcap_mode_help_font = QFont('Arial', 9) ramcap_mode_help.setFont(ramcap_mode_help_font) ramcap_mode_help.setStyleSheet("color: gray;") ramcap_mode_layout.addWidget(ramcap_mode_help) ramcap_mode_layout.addStretch() # Advanced RAM gate tuning gate_frame = QWidget() gate_layout = QHBoxLayout(gate_frame) gate_layout.setContentsMargins(0, 0, 0, 0) cleanup_layout.addWidget(gate_frame) gate_label = QLabel("Gate timeout (sec):") gate_label.setMinimumWidth(150) gate_layout.addWidget(gate_label) self.ram_gate_timeout_spinbox = QDoubleSpinBox() self.ram_gate_timeout_spinbox.setRange(2.0, 60.0) self.ram_gate_timeout_spinbox.setSingleStep(0.5) self.ram_gate_timeout_spinbox.setValue(float(self.settings.get('advanced', {}).get('ram_gate_timeout_sec', 10.0))) gate_layout.addWidget(self.ram_gate_timeout_spinbox) gate_layout.addStretch() # Gate floor floor_frame = QWidget() floor_layout = QHBoxLayout(floor_frame) floor_layout.setContentsMargins(0, 0, 0, 0) cleanup_layout.addWidget(floor_frame) floor_label = QLabel("Gate floor over baseline (MB):") floor_label.setMinimumWidth(180) floor_layout.addWidget(floor_label) self.ram_gate_floor_spinbox = QSpinBox() self.ram_gate_floor_spinbox.setRange(64, 2048) self.ram_gate_floor_spinbox.setValue(int(self.settings.get('advanced', {}).get('ram_min_floor_over_baseline_mb', 128))) floor_layout.addWidget(self.ram_gate_floor_spinbox) floor_layout.addStretch() # Update RT-DETR concurrency control visibility based on current detector type # This is called after the Advanced tab is fully created to sync with OCR tab state QTimer.singleShot(0, self._sync_rtdetr_concurrency_visibility) def _sync_rtdetr_concurrency_visibility(self): """Sync RT-DETR concurrency control visibility with detector type selection""" if hasattr(self, 'detector_type_combo') and hasattr(self, 'rtdetr_conc_frame'): detector = self.detector_type_combo.currentText() is_rtdetr = 'RT-DETR' in detector or 'RTEDR_onnx' in detector self.rtdetr_conc_frame.setVisible(is_rtdetr) def _toggle_workers(self): """Enable/disable worker settings based on parallel processing toggle""" if hasattr(self, 'parallel_processing_checkbox'): enabled = bool(self.parallel_processing_checkbox.isChecked()) if hasattr(self, 'max_workers_spinbox'): self.max_workers_spinbox.setEnabled(enabled) if hasattr(self, 'workers_label'): self.workers_label.setEnabled(enabled) self.workers_label.setStyleSheet("color: white;" if enabled else "color: gray;") if hasattr(self, 'workers_desc_label'): self.workers_desc_label.setEnabled(enabled) self.workers_desc_label.setStyleSheet("color: white;" if enabled else "color: gray;") def _toggle_singleton_controls(self): """Enable/disable parallel panel translation based on singleton toggle.""" # When singleton mode is ENABLED, parallel panel translation should be DISABLED try: singleton_enabled = bool(self.use_singleton_models_checkbox.isChecked()) except Exception: singleton_enabled = True # Default # Disable parallel panel checkbox when singleton is enabled if hasattr(self, 'parallel_panel_checkbox'): self.parallel_panel_checkbox.setEnabled(not singleton_enabled) if singleton_enabled: # Also gray out the label when disabled pass # The checkbox itself shows as disabled def _toggle_panel_controls(self): """Enable/disable panel control fields based on parallel panel toggle.""" try: enabled = bool(self.parallel_panel_checkbox.isChecked()) except Exception: enabled = False # Enable/disable panel control widgets and their labels panel_widgets = [ ('panel_max_workers_spinbox', 'panels_label'), ('panel_stagger_ms_spinbox', 'stagger_label', 'stagger_unit_label'), ('preload_local_panels_checkbox',) # Add preload checkbox ] for widget_names in panel_widgets: for widget_name in widget_names: try: widget = getattr(self, widget_name, None) if widget is not None: # Just use setEnabled() - stylesheet handles visuals widget.setEnabled(enabled) except Exception: pass 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.isChecked() self.settings['preprocessing']['auto_detect_quality'] = self.auto_detect.isChecked() self.settings['preprocessing']['contrast_threshold'] = self.contrast_threshold.value() self.settings['preprocessing']['sharpness_threshold'] = self.sharpness_threshold.value() self.settings['preprocessing']['enhancement_strength'] = self.enhancement_strength.value() self.settings['preprocessing']['noise_threshold'] = self.noise_threshold.value() self.settings['preprocessing']['denoise_strength'] = self.denoise_strength.value() self.settings['preprocessing']['max_image_dimension'] = self.dimension_spinbox.value() self.settings['preprocessing']['max_image_pixels'] = self.pixels_spinbox.value() self.settings['preprocessing']['chunk_height'] = self.chunk_height_spinbox.value() self.settings['preprocessing']['chunk_overlap'] = self.chunk_overlap_spinbox.value() # Compression (saved separately from preprocessing) if 'compression' not in self.settings: self.settings['compression'] = {} self.settings['compression']['enabled'] = bool(self.compression_enabled.isChecked()) self.settings['compression']['format'] = str(self.compression_format_combo.currentText()) self.settings['compression']['jpeg_quality'] = int(self.jpeg_quality_spin.value()) self.settings['compression']['png_compress_level'] = int(self.png_level_spin.value()) self.settings['compression']['webp_quality'] = int(self.webp_quality_spin.value()) # TILING SETTINGS - save under preprocessing (primary) and mirror under 'tiling' for backward compatibility self.settings['preprocessing']['inpaint_tiling_enabled'] = self.inpaint_tiling_enabled.isChecked() self.settings['preprocessing']['inpaint_tile_size'] = self.tile_size_spinbox.value() self.settings['preprocessing']['inpaint_tile_overlap'] = self.tile_overlap_spinbox.value() # Back-compat mirror self.settings['tiling'] = { 'enabled': self.inpaint_tiling_enabled.isChecked(), 'tile_size': self.tile_size_spinbox.value(), 'tile_overlap': self.tile_overlap_spinbox.value() } # OCR settings self.settings['ocr']['language_hints'] = [code for code, checkbox in self.lang_checkboxes.items() if checkbox.isChecked()] self.settings['ocr']['confidence_threshold'] = self.confidence_threshold_slider.value() / 100.0 self.settings['ocr']['text_detection_mode'] = self.detection_mode_combo.currentText() self.settings['ocr']['merge_nearby_threshold'] = self.merge_nearby_threshold_spinbox.value() self.settings['ocr']['enable_rotation_correction'] = self.enable_rotation_checkbox.isChecked() self.settings['ocr']['azure_merge_multiplier'] = self.azure_merge_multiplier_slider.value() / 100.0 self.settings['ocr']['azure_reading_order'] = self.azure_reading_order_combo.currentText() self.settings['ocr']['azure_model_version'] = self.azure_model_version_combo.currentText() self.settings['ocr']['azure_max_wait'] = self.azure_max_wait_spinbox.value() self.settings['ocr']['azure_poll_interval'] = self.azure_poll_interval_slider.value() / 100.0 self.settings['ocr']['min_text_length'] = self.min_text_length_spinbox.value() self.settings['ocr']['exclude_english_text'] = self.exclude_english_checkbox.isChecked() # OCR batching & locality self.settings['ocr']['ocr_batch_enabled'] = bool(self.ocr_batch_enabled_checkbox.isChecked()) self.settings['ocr']['ocr_batch_size'] = int(self.ocr_batch_size_spinbox.value()) self.settings['ocr']['ocr_max_concurrency'] = int(self.ocr_max_conc_spinbox.value()) self.settings['ocr']['roi_locality_enabled'] = bool(self.roi_locality_checkbox.isChecked()) self.settings['ocr']['roi_padding_ratio'] = float(self.roi_padding_ratio_slider.value() / 100.0) self.settings['ocr']['roi_min_side_px'] = int(self.roi_min_side_spinbox.value()) self.settings['ocr']['roi_min_area_px'] = int(self.roi_min_area_spinbox.value()) self.settings['ocr']['roi_max_side'] = int(self.roi_max_side_spinbox.value()) self.settings['ocr']['english_exclude_threshold'] = self.english_exclude_threshold_slider.value() / 100.0 self.settings['ocr']['english_exclude_min_chars'] = self.english_exclude_min_chars_spinbox.value() self.settings['ocr']['english_exclude_short_tokens'] = self.english_exclude_short_tokens_checkbox.isChecked() # Bubble detection settings self.settings['ocr']['bubble_detection_enabled'] = self.bubble_detection_enabled_checkbox.isChecked() self.settings['ocr']['use_rtdetr_for_ocr_regions'] = self.use_rtdetr_for_ocr_checkbox.isChecked() # NEW: RT-DETR for OCR guidance self.settings['ocr']['bubble_model_path'] = self.bubble_model_entry.text() self.settings['ocr']['bubble_confidence'] = self.bubble_conf_slider.value() / 100.0 self.settings['ocr']['rtdetr_confidence'] = self.bubble_conf_slider.value() / 100.0 self.settings['ocr']['detect_empty_bubbles'] = self.detect_empty_bubbles_checkbox.isChecked() self.settings['ocr']['detect_text_bubbles'] = self.detect_text_bubbles_checkbox.isChecked() self.settings['ocr']['detect_free_text'] = self.detect_free_text_checkbox.isChecked() self.settings['ocr']['rtdetr_model_url'] = self.bubble_model_entry.text() self.settings['ocr']['bubble_max_detections_yolo'] = int(self.bubble_max_det_yolo_spinbox.value()) self.settings['ocr']['rtdetr_max_concurrency'] = int(self.rtdetr_max_concurrency_spinbox.value()) # Save the detector type properly detector_display = self.detector_type_combo.currentText() 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_entry.text() else: self.settings['ocr']['detector_type'] = 'rtdetr_onnx' # Inpainting settings if 'inpainting' not in self.settings: self.settings['inpainting'] = {} self.settings['inpainting']['batch_size'] = self.inpaint_batch_size_spinbox.value() self.settings['inpainting']['enable_cache'] = self.enable_cache_checkbox.isChecked() # Save all dilation settings self.settings['mask_dilation'] = self.mask_dilation_spinbox.value() self.settings['use_all_iterations'] = self.use_all_iterations_checkbox.isChecked() self.settings['all_iterations'] = self.all_iterations_spinbox.value() self.settings['text_bubble_dilation_iterations'] = self.text_bubble_iter_spinbox.value() self.settings['empty_bubble_dilation_iterations'] = self.empty_bubble_iter_spinbox.value() self.settings['free_text_dilation_iterations'] = self.free_text_iter_spinbox.value() self.settings['auto_iterations'] = self.auto_iterations_checkbox.isChecked() # Legacy support self.settings['bubble_dilation_iterations'] = self.text_bubble_iter_spinbox.value() self.settings['dilation_iterations'] = self.text_bubble_iter_spinbox.value() # Advanced settings self.settings['advanced']['format_detection'] = bool(self.format_detection_checkbox.isChecked()) self.settings['advanced']['webtoon_mode'] = self.webtoon_mode_combo.currentText() self.settings['advanced']['debug_mode'] = bool(self.debug_mode_checkbox.isChecked()) self.settings['advanced']['save_intermediate'] = bool(self.save_intermediate_checkbox.isChecked()) self.settings['advanced']['parallel_processing'] = bool(self.parallel_processing_checkbox.isChecked()) self.settings['advanced']['max_workers'] = self.max_workers_spinbox.value() # Save HD strategy settings self.settings['advanced']['hd_strategy'] = str(self.hd_strategy_combo.currentText()) self.settings['advanced']['hd_strategy_resize_limit'] = int(self.hd_resize_limit_spin.value()) self.settings['advanced']['hd_strategy_crop_margin'] = int(self.hd_crop_margin_spin.value()) self.settings['advanced']['hd_strategy_crop_trigger_size'] = int(self.hd_crop_trigger_spin.value()) # 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']) # Save parallel rendering toggle if hasattr(self, 'render_parallel_checkbox'): self.settings['advanced']['render_parallel'] = bool(self.render_parallel_checkbox.isChecked()) # Panel-level parallel translation settings self.settings['advanced']['parallel_panel_translation'] = bool(self.parallel_panel_checkbox.isChecked()) self.settings['advanced']['panel_max_workers'] = int(self.panel_max_workers_spinbox.value()) self.settings['advanced']['panel_start_stagger_ms'] = int(self.panel_stagger_ms_spinbox.value()) # New: preload local inpainting for panels if hasattr(self, 'preload_local_panels_checkbox'): self.settings['advanced']['preload_local_inpainting_for_panels'] = bool(self.preload_local_panels_checkbox.isChecked()) # Memory management settings self.settings['advanced']['use_singleton_models'] = bool(self.use_singleton_models_checkbox.isChecked()) self.settings['advanced']['auto_cleanup_models'] = bool(self.auto_cleanup_models_checkbox.isChecked()) self.settings['advanced']['unload_models_after_translation'] = bool(self.unload_models_checkbox.isChecked() if hasattr(self, 'unload_models_checkbox') else False) # ONNX auto-convert settings (persist and apply to environment) if hasattr(self, 'auto_convert_onnx_checkbox'): self.settings['advanced']['auto_convert_to_onnx'] = bool(self.auto_convert_onnx_checkbox.isChecked()) os.environ['AUTO_CONVERT_TO_ONNX'] = 'true' if self.auto_convert_onnx_checkbox.isChecked() else 'false' if hasattr(self, 'auto_convert_onnx_bg_checkbox'): self.settings['advanced']['auto_convert_to_onnx_background'] = bool(self.auto_convert_onnx_bg_checkbox.isChecked()) os.environ['AUTO_CONVERT_TO_ONNX_BACKGROUND'] = 'true' if self.auto_convert_onnx_bg_checkbox.isChecked() else 'false' # Quantization toggles and precision if hasattr(self, 'quantize_models_checkbox'): self.settings['advanced']['quantize_models'] = bool(self.quantize_models_checkbox.isChecked()) os.environ['MODEL_QUANTIZE'] = 'true' if self.quantize_models_checkbox.isChecked() else 'false' if hasattr(self, 'onnx_quantize_checkbox'): self.settings['advanced']['onnx_quantize'] = bool(self.onnx_quantize_checkbox.isChecked()) os.environ['ONNX_QUANTIZE'] = 'true' if self.onnx_quantize_checkbox.isChecked() else 'false' if hasattr(self, 'torch_precision_combo'): self.settings['advanced']['torch_precision'] = str(self.torch_precision_combo.currentText()) os.environ['TORCH_PRECISION'] = self.settings['advanced']['torch_precision'] # Memory cleanup toggle if hasattr(self, 'force_deep_cleanup_checkbox'): if 'advanced' not in self.settings: self.settings['advanced'] = {} self.settings['advanced']['force_deep_cleanup_each_image'] = bool(self.force_deep_cleanup_checkbox.isChecked()) # RAM cap settings if hasattr(self, 'ram_cap_enabled_checkbox'): self.settings['advanced']['ram_cap_enabled'] = bool(self.ram_cap_enabled_checkbox.isChecked()) if hasattr(self, 'ram_cap_mb_spinbox'): self.settings['advanced']['ram_cap_mb'] = int(self.ram_cap_mb_spinbox.value()) if hasattr(self, 'ram_cap_mode_combo'): mode = self.ram_cap_mode_combo.currentText() self.settings['advanced']['ram_cap_mode'] = 'hard' if mode.startswith('hard') else 'soft' if hasattr(self, 'ram_gate_timeout_spinbox'): self.settings['advanced']['ram_gate_timeout_sec'] = float(self.ram_gate_timeout_spinbox.value()) if hasattr(self, 'ram_gate_floor_spinbox'): self.settings['advanced']['ram_min_floor_over_baseline_mb'] = int(self.ram_gate_floor_spinbox.value()) # Cloud API settings if hasattr(self, 'cloud_model_selected'): self.settings['cloud_inpaint_model'] = self.cloud_model_selected self.settings['cloud_custom_version'] = self.custom_version_entry.text() self.settings['cloud_inpaint_prompt'] = self.cloud_prompt_entry.text() self.settings['cloud_negative_prompt'] = self.negative_entry.text() self.settings['cloud_inference_steps'] = self.steps_spinbox.value() self.settings['cloud_timeout'] = self.cloud_timeout_spinbox.value() # 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(show_message=False) print("Settings saved successfully") elif hasattr(self.main_gui, 'save_configuration'): self.main_gui.save_configuration() print("Settings saved successfully") else: # Try direct save as fallback if hasattr(self.main_gui, 'config_file'): 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}") QMessageBox.critical(self, "Save Error", f"Failed to save settings: {e}") return # Call callback if provided if self.callback: try: self.callback(self.settings) except Exception as e: print(f"Error in callback: {e}") # Close dialog self.accept() except Exception as e: import traceback print(f"Critical error in _save_settings: {e}") print(traceback.format_exc()) QMessageBox.critical(self, "Save Error", f"Failed to save settings: {e}") def _reset_defaults(self): """Reset by removing manga_settings from config and reinitializing the dialog.""" reply = QMessageBox.question(self, "Reset Settings", "Reset all manga settings to defaults?\nThis will remove custom manga settings from config.json.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply != QMessageBox.StandardButton.Yes: 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 WITHOUT showing message try: if hasattr(self.main_gui, 'save_config'): self.main_gui.save_config(show_message=False) 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: 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: json.dump(self.config, f, ensure_ascii=False, indent=2) except Exception: pass # Close and reopen dialog so defaults apply self.close() try: MangaSettingsDialog(parent=self.parent, main_gui=self.main_gui, config=self.config, callback=self.callback) except Exception: pass # Don't show any message def _cancel(self): """Cancel without saving""" self.reject()