Glossarion / manga_settings_dialog.py
Shirochi's picture
Upload 7 files
f66ccd1 verified
# 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,
'min_region_size': 50, # Minimum dimension for cloud OCR regions (0 = disabled)
'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, # On by default for best accuracy
# Azure settings removed - new API is synchronous, no polling/version settings needed
'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,
'dilation_kernel_size': 5, # Kernel size for dilation operations
'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()
# Kernel size
kernel_frame = QWidget()
kernel_layout = QHBoxLayout(kernel_frame)
kernel_layout.setContentsMargins(0, 0, 0, 0)
mask_dilation_layout.addWidget(kernel_frame)
self.kernel_size_label = QLabel("Kernel Size:")
self.kernel_size_label.setMinimumWidth(150)
kernel_layout.addWidget(self.kernel_size_label)
self.kernel_size_spinbox = QSpinBox()
self.kernel_size_spinbox.setRange(3, 15)
self.kernel_size_spinbox.setSingleStep(2) # Only odd numbers
self.kernel_size_spinbox.setValue(self.settings.get('dilation_kernel_size', 5))
kernel_layout.addWidget(self.kernel_size_spinbox)
self.kernel_size_unit_label = QLabel("pixels (dilation kernel size, must be odd)")
kernel_layout.addWidget(self.kernel_size_unit_label)
kernel_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
# Kernel size controls
try:
if hasattr(self, 'kernel_size_spinbox'):
self.kernel_size_spinbox.setEnabled(False)
except Exception:
pass
try:
if hasattr(self, 'kernel_size_label'):
self.kernel_size_label.setEnabled(False)
except Exception:
pass
try:
if hasattr(self, 'kernel_size_unit_label'):
self.kernel_size_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
# Kernel size is always enabled when auto is off
try:
if hasattr(self, 'kernel_size_spinbox'):
self.kernel_size_spinbox.setEnabled(True)
except Exception:
pass
try:
if hasattr(self, 'kernel_size_label'):
self.kernel_size_label.setEnabled(True)
except Exception:
pass
try:
if hasattr(self, 'kernel_size_unit_label'):
self.kernel_size_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)
# Cloud OCR Confidence threshold (for Google/Azure only)
conf_widget = QWidget()
conf_layout = QHBoxLayout(conf_widget)
conf_layout.setContentsMargins(0, 0, 0, 0)
ocr_layout.addWidget(conf_widget)
conf_label = QLabel("☁️ Cloud OCR Confidence:")
conf_label.setMinimumWidth(180)
conf_label.setToolTip("Applies to Google Cloud Vision and Azure OCR only.\nLocal OCR (RapidOCR, PaddleOCR, etc.) uses RT-DETR confidence only (comic-translate approach).")
conf_layout.addWidget(conf_label)
# Get cloud OCR confidence (fallback to old setting for migration)
cloud_conf = self.settings['ocr'].get('cloud_ocr_confidence', self.settings['ocr'].get('confidence_threshold', 0.0))
self.confidence_threshold_slider = QSlider(Qt.Orientation.Horizontal)
self.confidence_threshold_slider.setRange(0, 100)
self.confidence_threshold_slider.setValue(int(cloud_conf * 100))
self.confidence_threshold_slider.setMinimumWidth(250)
self.confidence_threshold_slider.setToolTip("0 = accept all (recommended, like comic-translate)\nHigher values filter low-confidence cloud OCR results")
conf_layout.addWidget(self.confidence_threshold_slider)
self.confidence_threshold_label = QLabel(f"{cloud_conf:.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()
# Add info label below slider
conf_info = QLabel("ℹ️ Local OCR providers (RapidOCR, PaddleOCR, EasyOCR, DocTR) don't use this - they rely on RT-DETR confidence only")
conf_info_font = QFont('Arial', 9)
conf_info.setFont(conf_info_font)
conf_info.setStyleSheet("color: gray; font-style: italic; padding-left: 10px;")
conf_info.setWordWrap(True)
ocr_layout.addWidget(conf_info)
# 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()
# Minimum region size for cloud OCR (Google/Azure)
min_region_widget = QWidget()
min_region_layout = QHBoxLayout(min_region_widget)
min_region_layout.setContentsMargins(0, 0, 0, 0)
ocr_layout.addWidget(min_region_widget)
min_region_label = QLabel("☁️ Min Region Size:")
min_region_label.setMinimumWidth(180)
min_region_label.setToolTip(
"Minimum dimension for cloud OCR regions (Google/Azure).\n"
"Regions smaller than this will be resized before OCR.\n\n"
"• 50px = Default (safer, ensures good OCR quality)\n"
"• 32px = Smaller minimum (like some implementations)\n"
"• 0px = Disabled (send regions as-is, may fail on very small text)"
)
min_region_layout.addWidget(min_region_label)
self.min_region_size_spinbox = QSpinBox()
self.min_region_size_spinbox.setRange(0, 100)
self.min_region_size_spinbox.setSingleStep(1)
self.min_region_size_spinbox.setValue(self.settings['ocr'].get('min_region_size', 50))
self.min_region_size_spinbox.setToolTip("0 = disabled, 32-50 = recommended range")
min_region_layout.addWidget(self.min_region_size_spinbox)
min_region_unit = QLabel("pixels")
min_region_layout.addWidget(min_region_unit)
min_region_desc = QLabel("(0 = no resize, comic-translate style)")
min_region_desc_font = QFont('Arial', 9)
min_region_desc.setFont(min_region_desc_font)
min_region_desc.setStyleSheet("color: gray;")
min_region_layout.addWidget(min_region_desc)
min_region_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 (simplified - new API is synchronous)
azure_ocr_group = QGroupBox("Azure OCR Settings")
azure_ocr_layout = QVBoxLayout(azure_ocr_group)
content_layout.addWidget(azure_ocr_group)
# Azure merge multiplier (kept for backward compatibility)
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()
# Help text
azure_help = QLabel(
"💡 Azure uses new Image Analysis API (synchronous, no polling)\n"
"💡 Language auto-detection works well for manga"
)
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 for best accuracy
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, 999)
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 (simplified - only merge multiplier remains)
if hasattr(self, 'azure_merge_multiplier'): self.azure_merge_multiplier.set(float(ocr.get('azure_merge_multiplier', 3.0)))
try:
if hasattr(self, '_update_azure_label'):
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()]
# Save as cloud_ocr_confidence (applies to Google/Azure only)
self.settings['ocr']['cloud_ocr_confidence'] = self.confidence_threshold_slider.value() / 100.0
# Keep old setting for backward compatibility
self.settings['ocr']['confidence_threshold'] = self.settings['ocr']['cloud_ocr_confidence']
self.settings['ocr']['text_detection_mode'] = self.detection_mode_combo.currentText()
self.settings['ocr']['min_region_size'] = self.min_region_size_spinbox.value()
self.settings['ocr']['merge_nearby_threshold'] = self.merge_nearby_threshold_spinbox.value()
self.settings['ocr']['enable_rotation_correction'] = self.enable_rotation_checkbox.isChecked()
# Azure settings - only merge multiplier remains (new API is synchronous)
self.settings['ocr']['azure_merge_multiplier'] = self.azure_merge_multiplier_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['dilation_kernel_size'] = self.kernel_size_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()