#!/usr/bin/env python3 """ Glossarion Web - Gradio Web Interface AI-powered translation in your browser """ import gradio as gr import os import sys import json import tempfile import base64 from pathlib import Path # CRITICAL: Set API delay IMMEDIATELY at module level before any other imports # This ensures unified_api_client reads the correct value when it's imported if 'SEND_INTERVAL_SECONDS' not in os.environ: os.environ['SEND_INTERVAL_SECONDS'] = '0.5' print(f"π§ Module-level API delay initialized: {os.environ['SEND_INTERVAL_SECONDS']}s") # Import API key encryption/decryption try: from api_key_encryption import APIKeyEncryption API_KEY_ENCRYPTION_AVAILABLE = True # Create web-specific encryption handler with its own key file _web_encryption_handler = None def get_web_encryption_handler(): global _web_encryption_handler if _web_encryption_handler is None: _web_encryption_handler = APIKeyEncryption() # Use web-specific key file from pathlib import Path _web_encryption_handler.key_file = Path('.glossarion_web_key') _web_encryption_handler.cipher = _web_encryption_handler._get_or_create_cipher() # Add web-specific fields to encrypt _web_encryption_handler.api_key_fields.extend([ 'azure_vision_key', 'google_vision_credentials' ]) return _web_encryption_handler def decrypt_config(config): return get_web_encryption_handler().decrypt_config(config) def encrypt_config(config): return get_web_encryption_handler().encrypt_config(config) except ImportError: API_KEY_ENCRYPTION_AVAILABLE = False def decrypt_config(config): return config # Fallback: return config as-is def encrypt_config(config): return config # Fallback: return config as-is # Import your existing translation modules try: import TransateKRtoEN from model_options import get_model_options TRANSLATION_AVAILABLE = True except ImportError: TRANSLATION_AVAILABLE = False print("β οΈ Translation modules not found") # Import manga translation modules try: from manga_translator import MangaTranslator from unified_api_client import UnifiedClient MANGA_TRANSLATION_AVAILABLE = True print("β Manga translation modules loaded successfully") except ImportError as e: MANGA_TRANSLATION_AVAILABLE = False print(f"β οΈ Manga translation modules not found: {e}") print(f"β οΈ Current working directory: {os.getcwd()}") print(f"β οΈ Python path: {sys.path[:3]}...") # Check if files exist files_to_check = ['manga_translator.py', 'unified_api_client.py', 'bubble_detector.py', 'local_inpainter.py'] for file in files_to_check: if os.path.exists(file): print(f"β Found: {file}") else: print(f"β Missing: {file}") class GlossarionWeb: """Web interface for Glossarion translator""" def __init__(self): # Determine config file path based on environment is_hf_spaces = os.getenv('SPACE_ID') is not None or os.getenv('HF_SPACES') == 'true' if is_hf_spaces: # Use /data directory for Hugging Face Spaces persistent storage data_dir = '/data' if not os.path.exists(data_dir): # Fallback to current directory if /data doesn't exist data_dir = '.' self.config_file = os.path.join(data_dir, 'config_web.json') print(f"π€ HF Spaces detected - using config path: {self.config_file}") print(f"π Directory exists: {os.path.exists(os.path.dirname(self.config_file))}") else: # Local mode - use current directory self.config_file = "config_web.json" print(f"π Local mode - using config path: {self.config_file}") # Load raw config first self.config = self.load_config() # Create a decrypted version for display/use in the UI # but keep the original for saving self.decrypted_config = self.config.copy() if API_KEY_ENCRYPTION_AVAILABLE: self.decrypted_config = decrypt_config(self.decrypted_config) # CRITICAL: Initialize environment variables IMMEDIATELY after loading config # This must happen before any UnifiedClient is created # Set API call delay api_call_delay = self.decrypted_config.get('api_call_delay', 0.5) if 'api_call_delay' not in self.config: self.config['api_call_delay'] = 0.5 self.decrypted_config['api_call_delay'] = 0.5 os.environ['SEND_INTERVAL_SECONDS'] = str(api_call_delay) print(f"π§ Initialized API call delay: {api_call_delay}s") # Set batch translation settings if 'batch_translation' not in self.config: self.config['batch_translation'] = True self.decrypted_config['batch_translation'] = True if 'batch_size' not in self.config: self.config['batch_size'] = 10 self.decrypted_config['batch_size'] = 10 print(f"π¦ Initialized batch translation: {self.config['batch_translation']}, batch size: {self.config['batch_size']}") # CRITICAL: Ensure extraction method and filtering level are initialized if 'text_extraction_method' not in self.config: self.config['text_extraction_method'] = 'standard' self.decrypted_config['text_extraction_method'] = 'standard' if 'file_filtering_level' not in self.config: self.config['file_filtering_level'] = 'smart' self.decrypted_config['file_filtering_level'] = 'smart' if 'indefinitely_retry_rate_limit' not in self.config: self.config['indefinitely_retry_rate_limit'] = False self.decrypted_config['indefinitely_retry_rate_limit'] = False if 'thread_submission_delay' not in self.config: self.config['thread_submission_delay'] = 0.1 self.decrypted_config['thread_submission_delay'] = 0.1 if 'enhanced_preserve_structure' not in self.config: self.config['enhanced_preserve_structure'] = True self.decrypted_config['enhanced_preserve_structure'] = True if 'force_bs_for_traditional' not in self.config: self.config['force_bs_for_traditional'] = True self.decrypted_config['force_bs_for_traditional'] = True print(f"π Initialized extraction method: {self.config['text_extraction_method']}") print(f"π Initialized filtering level: {self.config['file_filtering_level']}") print(f"π Initialized rate limit retry: {self.config['indefinitely_retry_rate_limit']}") print(f"β±οΈ Initialized threading delay: {self.config['thread_submission_delay']}s") print(f"π§ Enhanced preserve structure: {self.config['enhanced_preserve_structure']}") print(f"π§ Force BS for traditional: {self.config['force_bs_for_traditional']}") # Set font algorithm and auto fit style if not present if 'manga_settings' not in self.config: self.config['manga_settings'] = {} if 'font_sizing' not in self.config['manga_settings']: self.config['manga_settings']['font_sizing'] = {} if 'rendering' not in self.config['manga_settings']: self.config['manga_settings']['rendering'] = {} if 'algorithm' not in self.config['manga_settings']['font_sizing']: self.config['manga_settings']['font_sizing']['algorithm'] = 'smart' if 'auto_fit_style' not in self.config['manga_settings']['rendering']: self.config['manga_settings']['rendering']['auto_fit_style'] = 'balanced' # Also ensure they're in decrypted_config if 'manga_settings' not in self.decrypted_config: self.decrypted_config['manga_settings'] = {} if 'font_sizing' not in self.decrypted_config['manga_settings']: self.decrypted_config['manga_settings']['font_sizing'] = {} if 'rendering' not in self.decrypted_config['manga_settings']: self.decrypted_config['manga_settings']['rendering'] = {} if 'algorithm' not in self.decrypted_config['manga_settings']['font_sizing']: self.decrypted_config['manga_settings']['font_sizing']['algorithm'] = 'smart' if 'auto_fit_style' not in self.decrypted_config['manga_settings']['rendering']: self.decrypted_config['manga_settings']['rendering']['auto_fit_style'] = 'balanced' print(f"π¨ Initialized font algorithm: {self.config['manga_settings']['font_sizing']['algorithm']}") print(f"π¨ Initialized auto fit style: {self.config['manga_settings']['rendering']['auto_fit_style']}") self.models = get_model_options() if TRANSLATION_AVAILABLE else ["gpt-4", "claude-3-5-sonnet"] print(f"π€ Loaded {len(self.models)} models: {self.models[:5]}{'...' if len(self.models) > 5 else ''}") # Translation state management import threading self.is_translating = False self.stop_flag = threading.Event() self.translation_thread = None self.current_unified_client = None # Track active client to allow cancellation self.current_translator = None # Track active translator to allow shutdown # Add stop flags for different translation types self.epub_translation_stop = False self.epub_translation_thread = None self.glossary_extraction_stop = False self.glossary_extraction_thread = None # Default prompts from the GUI (same as translator_gui.py) self.default_prompts = { "korean": ( "You are a professional Korean to English novel translator, you must strictly output only English text and HTML tags while following these rules:\n" "- Use a natural, comedy-friendly English translation style that captures both humor and readability without losing any original meaning.\n" "- Include 100% of the source text - every word, phrase, and sentence must be fully translated without exception.\n" "- Retain Korean honorifics and respectful speech markers in romanized form, including but not limited to: -nim, -ssi, -yang, -gun, -isiyeo, -hasoseo. For archaic/classical Korean honorific forms (like μ΄μμ¬/isiyeo, νμμ/hasoseo), preserve them as-is rather than converting to modern equivalents.\n" "- Always localize Korean terminology to proper English equivalents instead of literal translations (examples: λ§μ = Demon King; λ§μ = magic).\n" "- When translating Korean's pronoun-dropping style, insert pronouns in English only where needed for clarity: prioritize original pronouns as implied or according to the glossary, and only use they/them as a last resort, use I/me for first-person narration, and maintain natural English flow without overusing pronouns just because they're omitted in Korean.\n" "- All Korean profanity must be translated to English profanity.\n" "- Preserve original intent, and speech tone.\n" "- Retain onomatopoeia in Romaji.\n" "- Keep original Korean quotation marks (" ", ' ', γγ, γγ) as-is without converting to English quotes.\n" "- Every Korean/Chinese/Japanese character must be converted to its English meaning. Examples: The character μ means 'life/living', ν means 'active', κ΄ means 'hall/building' - together μνκ΄ means Dormitory.\n" "- Preserve ALL HTML tags exactly as they appear in the source, including
,,
,
,
,
,
,
') + content.count('
]+>', '', content)) if text_length > 0: p_text = re.findall(r'
]*>(.*?)
', content, re.DOTALL) p_text_length = sum(len(t) for t in p_text) percentage = (p_text_length / text_length) * 100 if percentage < min_paragraph_percentage: file_issues.append(f"Only {percentage:.1f}% text in tags")
# Simulated additional checks
if check_repetition and random.random() > 0.85:
file_issues.append("Excessive repetition detected")
if check_glossary_leakage and random.random() > 0.9:
file_issues.append("Glossary leakage detected")
# Report issues found
if file_issues:
for issue in file_issues:
issues_found.append(f" β οΈ {file_name}: {issue}")
scan_logs.append(f" β οΈ Issue: {issue}")
else:
scan_logs.append(f" β
No issues found")
except Exception as e:
scan_logs.append(f" β Error reading file: {str(e)}")
# Update logs periodically
if len(scan_logs) > 100:
scan_logs = scan_logs[-100:] # Keep only last 100 logs
yield gr.update(visible=False), None, gr.update(visible=True), "\n".join(scan_logs), gr.update(visible=False), f"Scanning {file_name}...", progress
# Generate report
scan_logs.append("\nπ Generating report...")
yield gr.update(visible=False), None, gr.update(visible=True), "\n".join(scan_logs), gr.update(visible=False), "Generating report...", 95
# Create report content based on selected format
if report_format == "summary":
# Summary format - brief overview only
report_content = "QA SCAN REPORT - SUMMARY\n"
report_content += "=" * 50 + "\n\n"
report_content += f"Total files scanned: {total_files}\n"
report_content += f"Issues found: {len(issues_found)}\n\n"
if issues_found:
report_content += f"Files with issues: {min(len(issues_found), 10)} (showing first 10)\n"
report_content += "\n".join(issues_found[:10])
else:
report_content += "β
No issues detected."
elif report_format == "verbose":
# Verbose format - all data including passed files
report_content = "QA SCAN REPORT - VERBOSE (ALL DATA)\n"
report_content += "=" * 50 + "\n\n"
from datetime import datetime
report_content += f"Scan Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
report_content += f"Folder Scanned: {folder_path}\n"
report_content += f"Total files scanned: {total_files}\n"
report_content += f"Issues found: {len(issues_found)}\n"
report_content += f"Settings used:\n"
report_content += f" - Min foreign chars: {min_foreign_chars}\n"
report_content += f" - Check repetition: {check_repetition}\n"
report_content += f" - Check glossary leakage: {check_glossary_leakage}\n"
report_content += f" - Min file length: {min_file_length}\n"
report_content += f" - Check multiple headers: {check_multiple_headers}\n"
report_content += f" - Check missing HTML: {check_missing_html}\n"
report_content += f" - Check insufficient paragraphs: {check_insufficient_paragraphs}\n"
report_content += f" - Min paragraph percentage: {min_paragraph_percentage}%\n\n"
report_content += "ALL FILES PROCESSED:\n"
report_content += "-" * 30 + "\n"
for file in html_files:
rel_path = os.path.relpath(file, folder_path)
report_content += f" {rel_path}\n"
if issues_found:
report_content += "\n\nISSUES DETECTED (DETAILED):\n"
report_content += "\n".join(issues_found)
else:
report_content += "\n\nβ
No issues detected. All files passed scan."
else: # detailed (default/recommended)
# Detailed format - recommended balance
report_content = "QA SCAN REPORT - DETAILED\n"
report_content += "=" * 50 + "\n\n"
report_content += f"Total files scanned: {total_files}\n"
report_content += f"Issues found: {len(issues_found)}\n\n"
if issues_found:
report_content += "ISSUES DETECTED:\n"
report_content += "\n".join(issues_found)
else:
report_content += "No issues detected. All files passed quick scan."
# Always save report to file for download
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_filename = f"qa_scan_report_{timestamp}.txt"
report_path = os.path.join(os.getcwd(), report_filename)
# Always write the report file
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report_content)
if auto_save_report:
scan_logs.append(f"πΎ Report auto-saved to: {report_filename}")
else:
scan_logs.append(f"π Report ready for download: {report_filename}")
scan_logs.append(f"\nβ
QA Scan completed!")
scan_logs.append(f"π Summary: {total_files} files scanned, {len(issues_found)} issues found")
scan_logs.append(f"\nπ₯ Click 'Download QA Report' below to save the report")
# Always return the report path and make File component visible
final_status = f"β
Scan complete!\n{total_files} files scanned\n{len(issues_found)} issues found"
yield gr.update(value=report_path, visible=True), gr.update(value=f"### {final_status}", visible=True), gr.update(visible=False), "\n".join(scan_logs), gr.update(value=final_status, visible=True), "Scan complete!", 100
except Exception as e:
import traceback
error_msg = f"β Error during QA scan:\n{str(e)}\n\n{traceback.format_exc()}"
scan_logs.append(error_msg)
yield gr.update(visible=False), gr.update(value="### β Error occurred", visible=True), gr.update(visible=False), "\n".join(scan_logs), gr.update(visible=True), "Error occurred", 0
def run_qa_scan_with_stop(self, *args):
"""Wrapper for run_qa_scan that includes button visibility control"""
self.qa_scan_stop = False
# Show stop button, hide scan button at start
for result in self.run_qa_scan(*args):
if self.qa_scan_stop:
# Scan was stopped
yield result[0], result[1], result[2], result[3] + "\n\nβ οΈ Scan stopped by user", result[4], "Stopped", 0, gr.update(visible=True), gr.update(visible=False)
return
# Add button visibility updates to the yields
yield result[0], result[1], result[2], result[3], result[4], result[5], result[6], gr.update(visible=False), gr.update(visible=True)
# Reset buttons at the end
yield result[0], result[1], result[2], result[3], result[4], result[5], result[6], gr.update(visible=True), gr.update(visible=False)
def stop_qa_scan(self):
"""Stop the ongoing QA scan"""
self.qa_scan_stop = True
return gr.update(visible=True), gr.update(visible=False), "Scan stopped"
def stop_translation(self):
"""Stop the ongoing translation process"""
print(f"DEBUG: stop_translation called, was_translating={self.is_translating}")
if self.is_translating:
print("DEBUG: Setting stop flag and cancellation")
self.stop_flag.set()
self.is_translating = False
# Best-effort: cancel any in-flight API operation on the active client
try:
if getattr(self, 'current_unified_client', None):
self.current_unified_client.cancel_current_operation()
print("DEBUG: Requested UnifiedClient cancellation")
except Exception as e:
print(f"DEBUG: UnifiedClient cancel failed: {e}")
# Also propagate to MangaTranslator class if available
try:
if MANGA_TRANSLATION_AVAILABLE:
from manga_translator import MangaTranslator
MangaTranslator.set_global_cancellation(True)
print("DEBUG: Set MangaTranslator global cancellation")
except ImportError:
pass
# Also propagate to UnifiedClient if available
try:
if MANGA_TRANSLATION_AVAILABLE:
from unified_api_client import UnifiedClient
UnifiedClient.set_global_cancellation(True)
print("DEBUG: Set UnifiedClient global cancellation")
except ImportError:
pass
# Kick off translator shutdown to free resources quickly
try:
tr = getattr(self, 'current_translator', None)
if tr and hasattr(tr, 'shutdown'):
import threading as _th
_th.Thread(target=tr.shutdown, name="WebMangaTranslatorShutdown", daemon=True).start()
print("DEBUG: Initiated translator shutdown thread")
# Clear reference so a new start creates a fresh instance
self.current_translator = None
except Exception as e:
print(f"DEBUG: Failed to start translator shutdown: {e}")
else:
print("DEBUG: stop_translation called but not translating")
def _reset_translation_flags(self):
"""Reset all translation flags for new translation"""
self.is_translating = False
self.stop_flag.clear()
# Reset global cancellation flags
try:
if MANGA_TRANSLATION_AVAILABLE:
from manga_translator import MangaTranslator
MangaTranslator.set_global_cancellation(False)
except ImportError:
pass
try:
if MANGA_TRANSLATION_AVAILABLE:
from unified_api_client import UnifiedClient
UnifiedClient.set_global_cancellation(False)
except ImportError:
pass
def translate_manga(
self,
image_files,
model,
api_key,
profile_name,
system_prompt,
ocr_provider,
google_creds_path,
azure_key,
azure_endpoint,
enable_bubble_detection,
enable_inpainting,
font_size_mode,
font_size,
font_multiplier,
min_font_size,
max_font_size,
text_color,
shadow_enabled,
shadow_color,
shadow_offset_x,
shadow_offset_y,
shadow_blur,
bg_opacity,
bg_style,
parallel_panel_translation=False,
panel_max_workers=10
):
"""Translate manga images - GENERATOR that yields (logs, image, cbz_file, status, progress_group, progress_text, progress_bar) updates"""
# Reset translation flags and set running state
self._reset_translation_flags()
self.is_translating = True
if not MANGA_TRANSLATION_AVAILABLE:
self.is_translating = False
yield "β Manga translation modules not loaded", None, None, gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
if not image_files:
self.is_translating = False
yield "β Please upload at least one image", gr.update(visible=False), gr.update(visible=False), gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
if not api_key:
self.is_translating = False
yield "β Please provide an API key", gr.update(visible=False), gr.update(visible=False), gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
# Check for stop request
if self.stop_flag.is_set():
self.is_translating = False
yield "βΉοΈ Translation stopped by user", gr.update(visible=False), gr.update(visible=False), gr.update(value="βΉοΈ Stopped", visible=True), gr.update(visible=False), gr.update(value="Stopped"), gr.update(value=0)
return
if ocr_provider == "google":
# Check if credentials are provided or saved in config
if not google_creds_path and not self.get_config_value('google_vision_credentials'):
yield "β Please provide Google Cloud credentials JSON file", gr.update(visible=False), gr.update(visible=False), gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
if ocr_provider == "azure":
# Ensure azure credentials are strings
azure_key_str = str(azure_key) if azure_key else ''
azure_endpoint_str = str(azure_endpoint) if azure_endpoint else ''
if not azure_key_str.strip() or not azure_endpoint_str.strip():
yield "β Please provide Azure API key and endpoint", gr.update(visible=False), gr.update(visible=False), gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
try:
# Set all environment variables from config
self.set_all_environment_variables()
# Set API key environment variable
if 'gpt' in model.lower() or 'openai' in model.lower():
os.environ['OPENAI_API_KEY'] = api_key
elif 'claude' in model.lower():
os.environ['ANTHROPIC_API_KEY'] = api_key
elif 'gemini' in model.lower():
os.environ['GOOGLE_API_KEY'] = api_key
# Set Google Cloud credentials if provided and save to config
if ocr_provider == "google":
if google_creds_path:
# New file provided - save it
creds_path = google_creds_path.name if hasattr(google_creds_path, 'name') else google_creds_path
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = creds_path
# Auto-save to config
self.config['google_vision_credentials'] = creds_path
self.save_config(self.config)
elif self.get_config_value('google_vision_credentials'):
# Use saved credentials from config
creds_path = self.get_config_value('google_vision_credentials')
if os.path.exists(creds_path):
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = creds_path
else:
yield f"β Saved Google credentials not found: {creds_path}", gr.update(visible=False), gr.update(visible=False), gr.update(value="β Error", visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
# Set Azure credentials if provided and save to config
if ocr_provider == "azure":
# Convert to strings and strip whitespace
azure_key_str = str(azure_key).strip() if azure_key else ''
azure_endpoint_str = str(azure_endpoint).strip() if azure_endpoint else ''
os.environ['AZURE_VISION_KEY'] = azure_key_str
os.environ['AZURE_VISION_ENDPOINT'] = azure_endpoint_str
# Auto-save to config
self.config['azure_vision_key'] = azure_key_str
self.config['azure_vision_endpoint'] = azure_endpoint_str
self.save_config(self.config)
# Apply text visibility settings to config
# Convert hex color to RGB tuple
def hex_to_rgb(hex_color):
# Handle different color formats
if isinstance(hex_color, (list, tuple)):
# Already RGB format
return tuple(hex_color[:3])
elif isinstance(hex_color, str):
# Remove any brackets or spaces if present
hex_color = hex_color.strip().strip('[]').strip()
if hex_color.startswith('#'):
# Hex format
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
elif len(hex_color) == 3:
# Short hex format like #FFF
return tuple(int(hex_color[i]*2, 16) for i in range(3))
elif ',' in hex_color:
# RGB string format like "255, 0, 0"
try:
parts = hex_color.split(',')
return tuple(int(p.strip()) for p in parts[:3])
except:
pass
# Default to black if parsing fails
return (0, 0, 0)
# Debug logging for color values
print(f"DEBUG: text_color type: {type(text_color)}, value: {text_color}")
print(f"DEBUG: shadow_color type: {type(shadow_color)}, value: {shadow_color}")
try:
text_rgb = hex_to_rgb(text_color)
shadow_rgb = hex_to_rgb(shadow_color)
except Exception as e:
print(f"WARNING: Error converting colors: {e}")
print(f"WARNING: Using default colors - text: black, shadow: white")
text_rgb = (0, 0, 0) # Default to black text
shadow_rgb = (255, 255, 255) # Default to white shadow
self.config['manga_font_size_mode'] = font_size_mode
self.config['manga_font_size'] = int(font_size)
self.config['manga_font_size_multiplier'] = float(font_multiplier)
self.config['manga_max_font_size'] = int(max_font_size)
self.config['manga_text_color'] = list(text_rgb)
self.config['manga_shadow_enabled'] = bool(shadow_enabled)
self.config['manga_shadow_color'] = list(shadow_rgb)
self.config['manga_shadow_offset_x'] = int(shadow_offset_x)
self.config['manga_shadow_offset_y'] = int(shadow_offset_y)
self.config['manga_shadow_blur'] = int(shadow_blur)
self.config['manga_bg_opacity'] = int(bg_opacity)
self.config['manga_bg_style'] = bg_style
# Also update nested manga_settings structure
if 'manga_settings' not in self.config:
self.config['manga_settings'] = {}
if 'rendering' not in self.config['manga_settings']:
self.config['manga_settings']['rendering'] = {}
if 'font_sizing' not in self.config['manga_settings']:
self.config['manga_settings']['font_sizing'] = {}
self.config['manga_settings']['rendering']['auto_min_size'] = int(min_font_size)
self.config['manga_settings']['font_sizing']['min_size'] = int(min_font_size)
self.config['manga_settings']['rendering']['auto_max_size'] = int(max_font_size)
self.config['manga_settings']['font_sizing']['max_size'] = int(max_font_size)
# Prepare output directory
output_dir = tempfile.mkdtemp(prefix="manga_translated_")
translated_files = []
cbz_mode = False
cbz_output_path = None
# Initialize translation logs early (needed for CBZ processing)
translation_logs = []
# Check if any file is a CBZ/ZIP archive
import zipfile
files_to_process = image_files if isinstance(image_files, list) else [image_files]
extracted_images = []
for file in files_to_process:
file_path = file.name if hasattr(file, 'name') else file
if file_path.lower().endswith(('.cbz', '.zip')):
# Extract CBZ
cbz_mode = True
translation_logs.append(f"π Extracting CBZ: {os.path.basename(file_path)}")
extract_dir = tempfile.mkdtemp(prefix="cbz_extract_")
try:
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
# Find all image files in extracted directory
import glob
for ext in ['*.png', '*.jpg', '*.jpeg', '*.webp', '*.bmp', '*.gif']:
extracted_images.extend(glob.glob(os.path.join(extract_dir, '**', ext), recursive=True))
# Sort naturally (by filename)
extracted_images.sort()
translation_logs.append(f"β
Extracted {len(extracted_images)} images from CBZ")
# Prepare CBZ output path
cbz_output_path = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(file_path))[0]}_translated.cbz")
except Exception as e:
translation_logs.append(f"β Error extracting CBZ: {str(e)}")
else:
# Regular image file
extracted_images.append(file_path)
# Use extracted images if CBZ was processed, otherwise use original files
if extracted_images:
# Create mock file objects for extracted images
class MockFile:
def __init__(self, path):
self.name = path
files_to_process = [MockFile(img) for img in extracted_images]
total_images = len(files_to_process)
# Merge web app config with SimpleConfig for MangaTranslator
# This includes all the text visibility settings we just set
merged_config = self.config.copy()
# Override with web-specific settings
merged_config['model'] = model
merged_config['active_profile'] = profile_name
# Update manga_settings
if 'manga_settings' not in merged_config:
merged_config['manga_settings'] = {}
if 'ocr' not in merged_config['manga_settings']:
merged_config['manga_settings']['ocr'] = {}
if 'inpainting' not in merged_config['manga_settings']:
merged_config['manga_settings']['inpainting'] = {}
if 'advanced' not in merged_config['manga_settings']:
merged_config['manga_settings']['advanced'] = {}
merged_config['manga_settings']['ocr']['provider'] = ocr_provider
merged_config['manga_settings']['ocr']['bubble_detection_enabled'] = enable_bubble_detection
merged_config['manga_settings']['inpainting']['method'] = 'local' if enable_inpainting else 'none'
# Make sure local_method is set from config (defaults to anime)
if 'local_method' not in merged_config['manga_settings']['inpainting']:
merged_config['manga_settings']['inpainting']['local_method'] = self.get_config_value('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime')
# Set parallel panel translation settings from config (Manga Settings tab)
# These are controlled in the Manga Settings tab, so reload config to get latest values
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
config_parallel = current_config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False)
config_max_workers = current_config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10)
# Map web UI settings to MangaTranslator expected names
merged_config['manga_settings']['advanced']['parallel_panel_translation'] = config_parallel
merged_config['manga_settings']['advanced']['panel_max_workers'] = int(config_max_workers)
# CRITICAL: Also set the setting names that MangaTranslator actually checks
merged_config['manga_settings']['advanced']['parallel_processing'] = config_parallel
merged_config['manga_settings']['advanced']['max_workers'] = int(config_max_workers)
# Log the parallel settings being used
print(f"π§ Reloaded config - Using parallel panel translation: {config_parallel}")
print(f"π§ Reloaded config - Using panel max workers: {config_max_workers}")
# CRITICAL: Set skip_inpainting flag to False when inpainting is enabled
merged_config['manga_skip_inpainting'] = not enable_inpainting
# Create a simple config object for MangaTranslator
class SimpleConfig:
def __init__(self, cfg):
self.config = cfg
def get(self, key, default=None):
return self.config.get(key, default)
# Create mock GUI object with necessary attributes
class MockGUI:
def __init__(self, config, profile_name, system_prompt, max_output_tokens, api_key, model):
self.config = config
# Add profile_var mock for MangaTranslator compatibility
class ProfileVar:
def __init__(self, profile):
self.profile = str(profile) if profile else ''
def get(self):
return self.profile
self.profile_var = ProfileVar(profile_name)
# Add prompt_profiles BOTH to config AND as attribute (manga_translator checks both)
if 'prompt_profiles' not in self.config:
self.config['prompt_profiles'] = {}
self.config['prompt_profiles'][profile_name] = system_prompt
# Also set as direct attribute for line 4653 check
self.prompt_profiles = self.config['prompt_profiles']
# Add max_output_tokens as direct attribute (line 299 check)
self.max_output_tokens = max_output_tokens
# Add mock GUI attributes that MangaTranslator expects
class MockVar:
def __init__(self, val):
# Ensure val is properly typed
self.val = val
def get(self):
return self.val
# CRITICAL: delay_entry must read from api_call_delay (not 'delay')
self.delay_entry = MockVar(float(config.get('api_call_delay', 0.5)))
self.trans_temp = MockVar(float(config.get('translation_temperature', 0.3)))
self.contextual_var = MockVar(bool(config.get('contextual', False)))
self.trans_history = MockVar(int(config.get('translation_history_limit', 2)))
self.translation_history_rolling_var = MockVar(bool(config.get('translation_history_rolling', False)))
self.token_limit_disabled = bool(config.get('token_limit_disabled', False))
# IMPORTANT: token_limit_entry must return STRING because manga_translator calls .strip() on it
self.token_limit_entry = MockVar(str(config.get('token_limit', 200000)))
# Batch translation settings
self.batch_translation_var = MockVar(bool(config.get('batch_translation', True)))
self.batch_size_var = MockVar(str(config.get('batch_size', '10')))
# Add API key and model for custom-api OCR provider - ensure strings
self.api_key_entry = MockVar(str(api_key) if api_key else '')
self.model_var = MockVar(str(model) if model else '')
simple_config = SimpleConfig(merged_config)
# Get max_output_tokens from config or use from web app config
web_max_tokens = merged_config.get('max_output_tokens', 16000)
mock_gui = MockGUI(simple_config.config, profile_name, system_prompt, web_max_tokens, api_key, model)
# CRITICAL: Set SYSTEM_PROMPT environment variable for manga translation
os.environ['SYSTEM_PROMPT'] = system_prompt if system_prompt else ''
if system_prompt:
print(f"β
System prompt set ({len(system_prompt)} characters)")
else:
print("β οΈ No system prompt provided")
# CRITICAL: Set batch environment variables from mock_gui variables
os.environ['BATCH_TRANSLATION'] = '1' if mock_gui.batch_translation_var.get() else '0'
os.environ['BATCH_SIZE'] = str(mock_gui.batch_size_var.get())
print(f"π¦ Set BATCH_TRANSLATION={os.environ['BATCH_TRANSLATION']}, BATCH_SIZE={os.environ['BATCH_SIZE']}")
# Ensure model path is in config for local inpainting
if enable_inpainting:
local_method = merged_config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime')
# Set the model path key that MangaTranslator expects
model_path_key = f'manga_{local_method}_model_path'
if model_path_key not in merged_config:
# Use default model path or empty string
default_model_path = self.get_config_value(model_path_key, '')
merged_config[model_path_key] = default_model_path
print(f"Set {model_path_key} to: {default_model_path}")
# CRITICAL: Explicitly set environment variables before creating UnifiedClient
api_call_delay = merged_config.get('api_call_delay', 0.5)
os.environ['SEND_INTERVAL_SECONDS'] = str(api_call_delay)
print(f"π§ Manga translation: Set SEND_INTERVAL_SECONDS = {api_call_delay}s")
# Set batch translation and batch size from MockGUI variables (after MockGUI is created)
# Will be set after mock_gui is created below
# Also ensure font algorithm and auto fit style are in config for manga_translator
if 'manga_settings' not in merged_config:
merged_config['manga_settings'] = {}
if 'font_sizing' not in merged_config['manga_settings']:
merged_config['manga_settings']['font_sizing'] = {}
if 'rendering' not in merged_config['manga_settings']:
merged_config['manga_settings']['rendering'] = {}
if 'algorithm' not in merged_config['manga_settings']['font_sizing']:
merged_config['manga_settings']['font_sizing']['algorithm'] = 'smart'
if 'auto_fit_style' not in merged_config['manga_settings']['rendering']:
merged_config['manga_settings']['rendering']['auto_fit_style'] = 'balanced'
print(f"π¦ Batch: BATCH_TRANSLATION={os.environ.get('BATCH_TRANSLATION')}, BATCH_SIZE={os.environ.get('BATCH_SIZE')}")
print(f"π¨ Font: algorithm={merged_config['manga_settings']['font_sizing']['algorithm']}, auto_fit_style={merged_config['manga_settings']['rendering']['auto_fit_style']}")
# Setup OCR configuration
ocr_config = {
'provider': ocr_provider
}
if ocr_provider == 'google':
ocr_config['google_credentials_path'] = google_creds_path.name if google_creds_path else None
elif ocr_provider == 'azure':
# Use string versions
azure_key_str = str(azure_key).strip() if azure_key else ''
azure_endpoint_str = str(azure_endpoint).strip() if azure_endpoint else ''
ocr_config['azure_key'] = azure_key_str
ocr_config['azure_endpoint'] = azure_endpoint_str
# Create UnifiedClient for translation API calls
try:
unified_client = UnifiedClient(
api_key=api_key,
model=model,
output_dir=output_dir
)
# Store reference for stop() cancellation support
self.current_unified_client = unified_client
except Exception as e:
error_log = f"β Failed to initialize API client: {str(e)}"
yield error_log, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_log, visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
# Log storage - will be yielded as live updates
last_yield_log_count = [0] # Track when we last yielded
last_yield_time = [0] # Track last yield time
# Track current image being processed
current_image_idx = [0]
import time
def should_yield_logs():
"""Check if we should yield log updates (every 2 logs or 1 second)"""
current_time = time.time()
log_count_diff = len(translation_logs) - last_yield_log_count[0]
time_diff = current_time - last_yield_time[0]
# Yield if 2+ new logs OR 1+ seconds passed
return log_count_diff >= 2 or time_diff >= 1.0
def capture_log(msg, level="info"):
"""Capture logs - caller will yield periodically"""
if msg and msg.strip():
log_msg = msg.strip()
translation_logs.append(log_msg)
# Initialize timing
last_yield_time[0] = time.time()
# Create MangaTranslator instance
try:
# Debug: Log inpainting config
inpaint_cfg = merged_config.get('manga_settings', {}).get('inpainting', {})
print(f"\n=== INPAINTING CONFIG DEBUG ===")
print(f"Inpainting enabled checkbox: {enable_inpainting}")
print(f"Inpainting method: {inpaint_cfg.get('method')}")
print(f"Local method: {inpaint_cfg.get('local_method')}")
print(f"Full inpainting config: {inpaint_cfg}")
print("=== END DEBUG ===\n")
translator = MangaTranslator(
ocr_config=ocr_config,
unified_client=unified_client,
main_gui=mock_gui,
log_callback=capture_log
)
# Keep a reference for stop/shutdown support
self.current_translator = translator
# Connect stop flag so translator can react immediately to stop requests
if hasattr(translator, 'set_stop_flag'):
try:
translator.set_stop_flag(self.stop_flag)
except Exception:
pass
# CRITICAL: Set skip_inpainting flag directly on translator instance
translator.skip_inpainting = not enable_inpainting
print(f"Set translator.skip_inpainting = {translator.skip_inpainting}")
# Explicitly initialize local inpainting if enabled
if enable_inpainting:
print(f"π¨ Initializing local inpainting...")
try:
# Force initialization of the inpainter
init_result = translator._initialize_local_inpainter()
if init_result:
print(f"β
Local inpainter initialized successfully")
else:
print(f"β οΈ Local inpainter initialization returned False")
except Exception as init_error:
print(f"β Failed to initialize inpainter: {init_error}")
import traceback
traceback.print_exc()
except Exception as e:
import traceback
full_error = traceback.format_exc()
print(f"\n\n=== MANGA TRANSLATOR INIT ERROR ===")
print(full_error)
print(f"\nocr_config: {ocr_config}")
print(f"\nmock_gui.model_var.get(): {mock_gui.model_var.get()}")
print(f"\nmock_gui.api_key_entry.get(): {type(mock_gui.api_key_entry.get())}")
print("=== END ERROR ===")
error_log = f"β Failed to initialize manga translator: {str(e)}\n\nCheck console for full traceback"
yield error_log, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_log, visible=True), gr.update(visible=False), gr.update(value="Error"), gr.update(value=0)
return
# Process each image with real progress tracking
for idx, img_file in enumerate(files_to_process, 1):
try:
# Check for stop request before processing each image
if self.stop_flag.is_set():
translation_logs.append(f"\nβΉοΈ Translation stopped by user before image {idx}/{total_images}")
self.is_translating = False
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(value="βΉοΈ Translation stopped", visible=True), gr.update(visible=True), gr.update(value="Stopped"), gr.update(value=0)
return
# Update current image index for log capture
current_image_idx[0] = idx
# Calculate progress range for this image
start_progress = (idx - 1) / total_images
end_progress = idx / total_images
input_path = img_file.name if hasattr(img_file, 'name') else img_file
output_path = os.path.join(output_dir, f"translated_{os.path.basename(input_path)}")
filename = os.path.basename(input_path)
# Log start of processing and YIELD update
start_msg = f"π¨ [{idx}/{total_images}] Starting: {filename}"
translation_logs.append(start_msg)
translation_logs.append(f"Image path: {input_path}")
translation_logs.append(f"Processing with OCR: {ocr_provider}, Model: {model}")
translation_logs.append("-" * 60)
# Yield initial log update with progress
progress_percent = int(((idx - 1) / total_images) * 100)
status_text = f"Processing {idx}/{total_images}: {filename}"
last_yield_log_count[0] = len(translation_logs)
last_yield_time[0] = time.time()
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value=status_text), gr.update(value=progress_percent)
# Start processing in a thread so we can yield logs periodically
import threading
processing_complete = [False]
result_container = [None]
def process_wrapper():
result_container[0] = translator.process_image(
image_path=input_path,
output_path=output_path,
batch_index=idx,
batch_total=total_images
)
processing_complete[0] = True
# Start processing in background
process_thread = threading.Thread(target=process_wrapper, daemon=True)
process_thread.start()
# Poll for log updates while processing
while not processing_complete[0]:
time.sleep(0.5) # Check every 0.5 seconds
# Check for stop request during processing
if self.stop_flag.is_set():
translation_logs.append(f"\nβΉοΈ Translation stopped by user while processing image {idx}/{total_images}")
self.is_translating = False
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(value="βΉοΈ Translation stopped", visible=True), gr.update(visible=True), gr.update(value="Stopped"), gr.update(value=0)
return
if should_yield_logs():
progress_percent = int(((idx - 0.5) / total_images) * 100) # Mid-processing
status_text = f"Processing {idx}/{total_images}: {filename} (in progress...)"
last_yield_log_count[0] = len(translation_logs)
last_yield_time[0] = time.time()
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value=status_text), gr.update(value=progress_percent)
# Wait for thread to complete
process_thread.join(timeout=1)
result = result_container[0]
if result.get('success'):
# Use the output path from the result
final_output = result.get('output_path', output_path)
if os.path.exists(final_output):
translated_files.append(final_output)
translation_logs.append(f"β
Image {idx}/{total_images} COMPLETE: {filename} | Total: {len(translated_files)}/{total_images} done")
translation_logs.append("")
# Yield progress update with all translated images so far
progress_percent = int((idx / total_images) * 100)
status_text = f"Completed {idx}/{total_images}: {filename}"
# Show all translated files as gallery
yield "\n".join(translation_logs), gr.update(value=translated_files, visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value=status_text), gr.update(value=progress_percent)
else:
translation_logs.append(f"β οΈ Image {idx}/{total_images}: Output file missing for {filename}")
translation_logs.append(f"β οΈ Warning: Output file not found for image {idx}")
translation_logs.append("")
# Yield progress update
progress_percent = int((idx / total_images) * 100)
status_text = f"Warning: {idx}/{total_images} - Output missing for {filename}"
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value=status_text), gr.update(value=progress_percent)
else:
errors = result.get('errors', [])
error_msg = errors[0] if errors else 'Unknown error'
translation_logs.append(f"β Image {idx}/{total_images} FAILED: {error_msg[:50]}")
translation_logs.append(f"β οΈ Error on image {idx}: {error_msg}")
translation_logs.append("")
# Yield progress update
progress_percent = int((idx / total_images) * 100)
status_text = f"Failed: {idx}/{total_images} - {filename}"
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value=status_text), gr.update(value=progress_percent)
# If translation failed, save original with error overlay
from PIL import Image as PILImage, ImageDraw, ImageFont
img = PILImage.open(input_path)
draw = ImageDraw.Draw(img)
# Add error message
draw.text((10, 10), f"Translation Error: {error_msg[:50]}", fill="red")
img.save(output_path)
translated_files.append(output_path)
except Exception as e:
import traceback
error_trace = traceback.format_exc()
translation_logs.append(f"β Image {idx}/{total_images} ERROR: {str(e)[:60]}")
translation_logs.append(f"β Exception on image {idx}: {str(e)}")
print(f"Manga translation error for {input_path}:\n{error_trace}")
# Save original on error
try:
from PIL import Image as PILImage
img = PILImage.open(input_path)
img.save(output_path)
translated_files.append(output_path)
except:
pass
continue
# Check for stop request before final processing
if self.stop_flag.is_set():
translation_logs.append("\nβΉοΈ Translation stopped by user")
self.is_translating = False
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(value="βΉοΈ Translation stopped", visible=True), gr.update(visible=True), gr.update(value="Stopped"), gr.update(value=0)
return
# Add completion message
translation_logs.append("\n" + "="*60)
translation_logs.append(f"β
ALL COMPLETE! Successfully translated {len(translated_files)}/{total_images} images")
translation_logs.append("="*60)
# If CBZ mode, compile translated images into CBZ archive
final_output_for_display = None
if cbz_mode and cbz_output_path and translated_files:
translation_logs.append("\nπ¦ Compiling translated images into CBZ archive...")
try:
with zipfile.ZipFile(cbz_output_path, 'w', zipfile.ZIP_DEFLATED) as cbz:
for img_path in translated_files:
# Preserve original filename structure
arcname = os.path.basename(img_path).replace("translated_", "")
cbz.write(img_path, arcname)
translation_logs.append(f"β
CBZ archive created: {os.path.basename(cbz_output_path)}")
translation_logs.append(f"π Archive location: {cbz_output_path}")
final_output_for_display = cbz_output_path
except Exception as e:
translation_logs.append(f"β Error creating CBZ: {str(e)}")
# Build final status with detailed panel information
final_status_lines = []
if translated_files:
final_status_lines.append(f"β
Successfully translated {len(translated_files)}/{total_images} image(s)!")
final_status_lines.append("")
final_status_lines.append("πΌοΈ **Translated Panels:**")
for i, file_path in enumerate(translated_files, 1):
filename = os.path.basename(file_path)
final_status_lines.append(f" {i}. {filename}")
final_status_lines.append("")
final_status_lines.append("π **Download Options:**")
if cbz_mode and cbz_output_path:
final_status_lines.append(f" π¦ CBZ Archive: {os.path.basename(cbz_output_path)}")
final_status_lines.append(f" π Location: {cbz_output_path}")
else:
# Create ZIP file for all images
zip_path = os.path.join(output_dir, "translated_images.zip")
try:
import zipfile
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for img_path in translated_files:
arcname = os.path.basename(img_path)
zipf.write(img_path, arcname)
final_status_lines.append(f" π¦ Download all images: translated_images.zip")
final_status_lines.append(f" π Output directory: {output_dir}")
final_output_for_display = zip_path # Set this so it can be downloaded
except Exception as e:
final_status_lines.append(f" β Failed to create ZIP: {str(e)}")
final_status_lines.append(f" π Output directory: {output_dir}")
final_status_lines.append(" πΌοΈ Images saved individually in output directory")
else:
final_status_lines.append("β Translation failed - no images were processed")
final_status_text = "\n".join(final_status_lines)
# Final yield with complete logs, image, CBZ, and final status
# Format: (logs_textbox, output_image, cbz_file, status_textbox, progress_group, progress_text, progress_bar)
final_progress_text = f"Complete! Processed {len(translated_files)}/{total_images} images"
if translated_files:
# Show all translated images in gallery
if cbz_mode and cbz_output_path and os.path.exists(cbz_output_path):
yield (
"\n".join(translation_logs),
gr.update(value=translated_files, visible=True), # Show all images in gallery
gr.update(value=cbz_output_path, visible=True), # CBZ file for download with visibility
gr.update(value=final_status_text, visible=True),
gr.update(visible=True),
gr.update(value=final_progress_text),
gr.update(value=100)
)
else:
# Show ZIP file for download if it was created
if final_output_for_display and os.path.exists(final_output_for_display):
yield (
"\n".join(translation_logs),
gr.update(value=translated_files, visible=True), # Show all images in gallery
gr.update(value=final_output_for_display, visible=True), # ZIP file for download
gr.update(value=final_status_text, visible=True),
gr.update(visible=True),
gr.update(value=final_progress_text),
gr.update(value=100)
)
else:
yield (
"\n".join(translation_logs),
gr.update(value=translated_files, visible=True), # Show all images in gallery
gr.update(visible=False), # Hide download component if ZIP failed
gr.update(value=final_status_text, visible=True),
gr.update(visible=True),
gr.update(value=final_progress_text),
gr.update(value=100)
)
else:
yield (
"\n".join(translation_logs),
gr.update(visible=False),
gr.update(visible=False), # Hide CBZ component
gr.update(value=final_status_text, visible=True),
gr.update(visible=True),
gr.update(value=final_progress_text),
gr.update(value=0) # 0% if nothing was processed
)
except Exception as e:
import traceback
error_msg = f"β Error during manga translation:\n{str(e)}\n\n{traceback.format_exc()}"
self.is_translating = False
yield error_msg, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_msg, visible=True), gr.update(visible=False), gr.update(value="Error occurred"), gr.update(value=0)
finally:
# Always reset translation state when done
self.is_translating = False
# Clear active references on full completion
try:
self.current_translator = None
self.current_unified_client = None
except Exception:
pass
def stop_manga_translation(self):
"""Simple function to stop manga translation"""
print("DEBUG: Stop button clicked")
if self.is_translating:
print("DEBUG: Stopping active translation")
self.stop_translation()
# Return UI updates for button visibility and status
return (
gr.update(visible=True), # translate button - show
gr.update(visible=False), # stop button - hide
"βΉοΈ Translation stopped by user"
)
else:
print("DEBUG: No active translation to stop")
return (
gr.update(visible=True), # translate button - show
gr.update(visible=False), # stop button - hide
"No active translation to stop"
)
def start_manga_translation(self, *args):
"""Simple function to start manga translation - GENERATOR FUNCTION"""
print("DEBUG: Translate button clicked")
# Reset flags for new translation and mark as translating BEFORE first yield
self._reset_translation_flags()
self.is_translating = True
# Initial yield to update button visibility
yield (
"π Starting translation...",
gr.update(visible=False), # manga_output_gallery - hide initially
gr.update(visible=False), # manga_cbz_output
gr.update(value="Starting...", visible=True), # manga_status
gr.update(visible=False), # manga_progress_group
gr.update(value="Initializing..."), # manga_progress_text
gr.update(value=0), # manga_progress_bar
gr.update(visible=False), # translate button - hide during translation
gr.update(visible=True) # stop button - show during translation
)
# Call the translate function and yield all its results
last_result = None
try:
for result in self.translate_manga(*args):
# Check if stop was requested during iteration
if self.stop_flag.is_set():
print("DEBUG: Stop flag detected, breaking translation loop")
break
last_result = result
# Pad result to include button states (translate_visible=False, stop_visible=True)
if len(result) >= 7:
yield result + (gr.update(visible=False), gr.update(visible=True))
else:
# Pad result to match expected length (7 values) then add button states
padded_result = list(result) + [gr.update(visible=False)] * (7 - len(result))
yield tuple(padded_result) + (gr.update(visible=False), gr.update(visible=True))
except GeneratorExit:
print("DEBUG: Translation generator was closed")
self.is_translating = False
return
except Exception as e:
print(f"DEBUG: Exception during translation: {e}")
self.is_translating = False
# Show error and reset buttons
error_msg = f"β Error during translation: {str(e)}"
yield (
error_msg,
gr.update(visible=False),
gr.update(visible=False),
gr.update(value=error_msg, visible=True),
gr.update(visible=False),
gr.update(value="Error occurred"),
gr.update(value=0),
gr.update(visible=True), # translate button - show after error
gr.update(visible=False) # stop button - hide after error
)
return
finally:
# Clear active references when the loop exits
self.is_translating = False
try:
self.current_translator = None
self.current_unified_client = None
except Exception:
pass
# Check if we stopped early
if self.stop_flag.is_set():
yield (
"βΉοΈ Translation stopped by user",
gr.update(visible=False),
gr.update(visible=False),
gr.update(value="βΉοΈ Translation stopped", visible=True),
gr.update(visible=False),
gr.update(value="Stopped"),
gr.update(value=0),
gr.update(visible=True), # translate button - show after stop
gr.update(visible=False) # stop button - hide after stop
)
return
# Final yield to reset buttons after successful completion
print("DEBUG: Translation completed normally, resetting buttons")
if last_result is None:
last_result = ("", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="Complete"), gr.update(value=100))
if len(last_result) >= 7:
yield last_result[:7] + (gr.update(visible=True), gr.update(visible=False))
else:
# Pad result to match expected length then add button states
padded_result = list(last_result) + [gr.update(visible=False)] * (7 - len(last_result))
yield tuple(padded_result) + (gr.update(visible=True), gr.update(visible=False))
def create_interface(self):
"""Create and return the Gradio interface"""
# Reload config before creating interface to get latest values
self.config = self.load_config()
self.decrypted_config = decrypt_config(self.config.copy()) if API_KEY_ENCRYPTION_AVAILABLE else self.config.copy()
# Load and encode icon as base64
icon_base64 = ""
icon_path = "Halgakos.ico" if os.path.exists("Halgakos.ico") else "Halgakos.ico"
if os.path.exists(icon_path):
with open(icon_path, "rb") as f:
icon_base64 = base64.b64encode(f.read()).decode()
# Custom CSS to hide Gradio footer and add favicon
custom_css = """
footer {display: none !important;}
.gradio-container {min-height: 100vh;}
/* Stop button styling */
.gr-button[data-variant="stop"] {
background-color: #dc3545 !important;
border-color: #dc3545 !important;
color: white !important;
}
.gr-button[data-variant="stop"]:hover {
background-color: #c82333 !important;
border-color: #bd2130 !important;
color: white !important;
}
"""
# JavaScript for localStorage persistence - SIMPLE VERSION
localStorage_js = """
"""
with gr.Blocks(
title="Glossarion - AI Translation",
theme=gr.themes.Soft(),
css=custom_css
) as app:
# Add custom HTML with favicon link and title with icon
icon_img_tag = f'' if icon_base64 else ''
gr.HTML(f"""
tags (%)", info="Files with less than this percentage will be flagged" ) # Report Settings gr.Markdown("#### Report Settings") report_format = gr.Radio( choices=["summary", "detailed", "verbose"], value=self.get_config_value('qa_report_format', 'detailed'), label="Report format", info="Summary = brief overview, Detailed = recommended, Verbose = all data" ) auto_save_report = gr.Checkbox( label="Automatically save report after scan", value=self.get_config_value('qa_auto_save_report', True) ) with gr.Column(): # Add logo and status at top with gr.Row(): gr.Image( value="Halgakos.png", label=None, show_label=False, width=80, height=80, interactive=False, show_download_button=False, container=False ) qa_status_message = gr.Markdown( value="### Ready to scan\nEnter the path to your output folder and click 'Quick Scan' to begin.", visible=True ) # Progress section with gr.Group(visible=False) as qa_progress_group: gr.Markdown("### Progress") qa_progress_text = gr.Textbox( label="π¨ Current Status", value="Ready to start", interactive=False, lines=1 ) qa_progress_bar = gr.Slider( minimum=0, maximum=100, value=0, step=1, label="π Scan Progress", interactive=False, show_label=True ) qa_logs = gr.Textbox( label="π Scan Logs", lines=20, max_lines=30, value="Ready to scan. Enter output folder path and configure settings.", visible=True, interactive=False ) qa_report = gr.File( label="π Download QA Report", visible=False ) qa_status = gr.Textbox( label="Final Status", lines=3, max_lines=5, visible=False, interactive=False ) # QA Scan button handler qa_scan_btn.click( fn=self.run_qa_scan_with_stop, inputs=[ qa_folder_path, min_foreign_chars, check_repetition, check_glossary_leakage, min_file_length, check_multiple_headers, check_missing_html, check_insufficient_paragraphs, min_paragraph_percentage, report_format, auto_save_report ], outputs=[ qa_report, qa_status_message, qa_progress_group, qa_logs, qa_status, qa_progress_text, qa_progress_bar, qa_scan_btn, stop_qa_btn ] ) # Stop button handler stop_qa_btn.click( fn=self.stop_qa_scan, inputs=[], outputs=[qa_scan_btn, stop_qa_btn, qa_status] ) # Settings Tab with gr.Tab("βοΈ Settings"): gr.Markdown("### Configuration") gr.Markdown("#### Translation Profiles") gr.Markdown("Profiles are loaded from your `config_web.json` file. The web interface has its own separate configuration.") with gr.Accordion("View All Profiles", open=False): profiles_text = "\n\n".join( [f"**{name}**:\n```\n{prompt[:200]}...\n```" for name, prompt in self.profiles.items()] ) gr.Markdown(profiles_text if profiles_text else "No profiles found") gr.Markdown("---") gr.Markdown("#### Advanced Translation Settings") with gr.Row(): with gr.Column(): thread_delay = gr.Slider( minimum=0, maximum=5, value=self.get_config_value('thread_submission_delay', 0.1), step=0.1, label="Threading delay (s)", interactive=True ) api_delay = gr.Slider( minimum=0, maximum=10, value=self.get_config_value('api_call_delay', 0.5), step=0.1, label="API call delay (s) [SEND_INTERVAL_SECONDS]", interactive=True, info="Delay between API calls to avoid rate limits" ) chapter_range = gr.Textbox( label="Chapter range (e.g., 5-10)", value=self.get_config_value('chapter_range', ''), placeholder="Leave empty for all chapters" ) token_limit = gr.Number( label="Input Token limit", value=self.get_config_value('token_limit', 200000), minimum=0 ) disable_token_limit = gr.Checkbox( label="Disable Input Token Limit", value=self.get_config_value('token_limit_disabled', False) ) output_token_limit = gr.Number( label="Output Token limit", value=self.get_config_value('max_output_tokens', 16000), minimum=0 ) with gr.Column(): contextual = gr.Checkbox( label="Contextual Translation", value=self.get_config_value('contextual', False) ) history_limit = gr.Number( label="Translation History Limit", value=self.get_config_value('translation_history_limit', 2), minimum=0 ) rolling_history = gr.Checkbox( label="Rolling History Window", value=self.get_config_value('translation_history_rolling', False) ) batch_translation = gr.Checkbox( label="Batch Translation", value=self.get_config_value('batch_translation', True) ) batch_size = gr.Number( label="Batch Size", value=self.get_config_value('batch_size', 10), minimum=1 ) gr.Markdown("---") gr.Markdown("#### Chapter Processing Options") with gr.Row(): with gr.Column(): # Chapter Header Translation batch_translate_headers = gr.Checkbox( label="Batch Translate Headers", value=self.get_config_value('batch_translate_headers', False) ) headers_per_batch = gr.Number( label="Headers per batch", value=self.get_config_value('headers_per_batch', 400), minimum=1 ) # NCX and CSS options use_ncx_navigation = gr.Checkbox( label="Use NCX-only Navigation (Compatibility Mode)", value=self.get_config_value('use_ncx_navigation', False) ) attach_css_to_chapters = gr.Checkbox( label="Attach CSS to Chapters (Fixes styling issues)", value=self.get_config_value('attach_css_to_chapters', False) ) retain_source_extension = gr.Checkbox( label="Retain source extension (no 'response_' prefix)", value=self.get_config_value('retain_source_extension', True) ) with gr.Column(): # Conservative Batching use_conservative_batching = gr.Checkbox( label="Use Conservative Batching", value=self.get_config_value('use_conservative_batching', False), info="Groups chapters in batches of 3x batch size for memory management" ) # Gemini API Safety disable_gemini_safety = gr.Checkbox( label="Disable Gemini API Safety Filters", value=self.get_config_value('disable_gemini_safety', False), info="β οΈ Disables ALL content safety filters for Gemini models (BLOCK_NONE)" ) # OpenRouter Options use_http_openrouter = gr.Checkbox( label="Use HTTP-only for OpenRouter (bypass SDK)", value=self.get_config_value('use_http_openrouter', False), info="Direct HTTP POST with explicit headers" ) disable_openrouter_compression = gr.Checkbox( label="Disable compression for OpenRouter (Accept-Encoding)", value=self.get_config_value('disable_openrouter_compression', False), info="Sends Accept-Encoding: identity for uncompressed responses" ) gr.Markdown("---") gr.Markdown("#### Chapter Extraction Settings") with gr.Row(): with gr.Column(): gr.Markdown("**Text Extraction Method:**") text_extraction_method = gr.Radio( choices=["standard", "enhanced"], value=self.get_config_value('text_extraction_method', 'standard'), label="", info="Standard uses BeautifulSoup, Enhanced uses html2text", interactive=True ) gr.Markdown("β’ **Standard (BeautifulSoup)** - Traditional HTML parsing, fast and reliable") gr.Markdown("β’ **Enhanced (html2text)** - Superior Unicode handling, cleaner text extraction") with gr.Column(): gr.Markdown("**File Filtering Level:**") file_filtering_level = gr.Radio( choices=["smart", "comprehensive", "full"], value=self.get_config_value('file_filtering_level', 'smart'), label="", info="Controls which files are extracted from EPUBs", interactive=True ) gr.Markdown("β’ **Smart (Aggressive Filtering)** - Skips navigation, TOC, copyright files") gr.Markdown("β’ **Moderate** - Only skips obvious navigation files") gr.Markdown("β’ **Full (No Filtering)** - Extracts ALL HTML/XHTML files") gr.Markdown("---") gr.Markdown("#### Response Handling & Retry Logic") with gr.Row(): with gr.Column(): gr.Markdown("**GPT-5 Thinking (OpenRouter/OpenAI-style)**") enable_gpt_thinking = gr.Checkbox( label="Enable GPT / OR Thinking", value=self.get_config_value('enable_gpt_thinking', True), info="Controls GPT-5 and OpenRouter reasoning" ) with gr.Row(): gpt_thinking_effort = gr.Dropdown( choices=["low", "medium", "high"], value=self.get_config_value('gpt_thinking_effort', 'medium'), label="Effort", interactive=True ) or_thinking_tokens = gr.Number( label="OR Thinking Tokens", value=self.get_config_value('or_thinking_tokens', 2000), minimum=0, maximum=50000, info="tokens" ) gr.Markdown("*Provide Tokens to force a max token budget for other models; GPT-5 only uses Effort (low/medium/high)*", elem_classes=["markdown-small"]) with gr.Column(): gr.Markdown("**Gemini Thinking Mode**") enable_gemini_thinking = gr.Checkbox( label="Enable Gemini Thinking", value=self.get_config_value('enable_gemini_thinking', False), info="Control Gemini's thinking process", interactive=True ) gemini_thinking_budget = gr.Number( label="Budget", value=self.get_config_value('gemini_thinking_budget', 0), minimum=0, maximum=50000, info="tokens (0 = disabled)", interactive=True ) gr.Markdown("*0 = disabled, 512-24576 = limited thinking*", elem_classes=["markdown-small"]) gr.Markdown("---") gr.Markdown("π **API keys are encrypted** when saved to config using AES encryption.") save_api_key = gr.Checkbox( label="Save API Key (Encrypted)", value=True ) save_status = gr.Textbox(label="Settings Status", value="Use the 'Save Config' button to save changes", interactive=False) # Hidden HTML component for JavaScript execution js_executor = gr.HTML("", visible=False) # Auto-save function for settings tab def save_settings_tab(thread_delay_val, api_delay_val, chapter_range_val, token_limit_val, disable_token_limit_val, output_token_limit_val, contextual_val, history_limit_val, rolling_history_val, batch_translation_val, batch_size_val, save_api_key_val): """Save settings from the Settings tab""" try: current_config = self.get_current_config_for_update() # Don't decrypt - just update non-encrypted fields # Update settings current_config['thread_submission_delay'] = float(thread_delay_val) current_config['api_call_delay'] = float(api_delay_val) current_config['chapter_range'] = str(chapter_range_val) current_config['token_limit'] = int(token_limit_val) current_config['token_limit_disabled'] = bool(disable_token_limit_val) current_config['max_output_tokens'] = int(output_token_limit_val) current_config['contextual'] = bool(contextual_val) current_config['translation_history_limit'] = int(history_limit_val) current_config['translation_history_rolling'] = bool(rolling_history_val) current_config['batch_translation'] = bool(batch_translation_val) current_config['batch_size'] = int(batch_size_val) # CRITICAL: Update environment variables immediately os.environ['SEND_INTERVAL_SECONDS'] = str(api_delay_val) os.environ['THREAD_SUBMISSION_DELAY'] = str(thread_delay_val) print(f"β Updated SEND_INTERVAL_SECONDS = {api_delay_val}s") print(f"β Updated THREAD_SUBMISSION_DELAY = {thread_delay_val}s") # Save to file self.save_config(current_config) # JavaScript to save to localStorage js_code = """ """ % ( thread_delay_val, api_delay_val, chapter_range_val, token_limit_val, str(disable_token_limit_val).lower(), output_token_limit_val, str(contextual_val).lower(), history_limit_val, str(rolling_history_val).lower(), str(batch_translation_val).lower(), batch_size_val ) return "β Settings saved successfully", js_code except Exception as e: return f"β Failed to save: {str(e)}", "" # Settings tab auto-save handlers removed - use manual Save Config button # Token sync handlers removed - use manual Save Config button # Help Tab with gr.Tab("β Help"): gr.Markdown(""" ## How to Use Glossarion ### Translation 1. Upload an EPUB file 2. Select AI model (GPT-4, Claude, etc.) 3. Enter your API key 4. Click "Translate" 5. Download the translated EPUB ### Manga Translation 1. Upload manga image(s) (PNG, JPG, etc.) 2. Select AI model and enter API key 3. Choose translation profile (e.g., Manga_JP, Manga_KR) 4. Configure OCR settings (Google Cloud Vision recommended) 5. Enable bubble detection and inpainting for best results 6. Click "Translate Manga" ### Glossary Extraction 1. Upload an EPUB file 2. Configure extraction settings 3. Click "Extract Glossary" 4. Use the CSV in future translations ### API Keys - **OpenAI**: Get from https://platform.openai.com/api-keys - **Anthropic**: Get from https://console.anthropic.com/ ### Translation Profiles Profiles contain detailed translation instructions and rules. Select a profile that matches your source language and style preferences. You can create and edit profiles in the desktop application. ### Tips - Use glossaries for consistent character name translation - Lower temperature (0.1-0.3) for more literal translations - Higher temperature (0.5-0.7) for more creative translations """) # Create a comprehensive load function that refreshes ALL values def load_all_settings(): """Load all settings from config file on page refresh""" # Reload config to get latest values self.config = self.load_config() self.decrypted_config = decrypt_config(self.config.copy()) if API_KEY_ENCRYPTION_AVAILABLE else self.config.copy() # CRITICAL: Reload profiles from config after reloading config self.profiles = self.default_prompts.copy() config_profiles = self.config.get('prompt_profiles', {}) if config_profiles: self.profiles.update(config_profiles) # Helper function to convert RGB arrays to hex def to_hex_color(color_value, default='#000000'): if isinstance(color_value, (list, tuple)) and len(color_value) >= 3: return '#{:02x}{:02x}{:02x}'.format(int(color_value[0]), int(color_value[1]), int(color_value[2])) elif isinstance(color_value, str): return color_value if color_value.startswith('#') else default return default # Return values for all tracked components return [ self.get_config_value('model', 'gpt-4-turbo'), # epub_model self.get_config_value('api_key', ''), # epub_api_key self.get_config_value('active_profile', list(self.profiles.keys())[0] if self.profiles else ''), # epub_profile self.profiles.get(self.get_config_value('active_profile', ''), ''), # epub_system_prompt self.get_config_value('temperature', 0.3), # epub_temperature self.get_config_value('max_output_tokens', 16000), # epub_max_tokens self.get_config_value('enable_image_translation', False), # enable_image_translation self.get_config_value('enable_auto_glossary', False), # enable_auto_glossary self.get_config_value('append_glossary_to_prompt', True), # append_glossary # Auto glossary settings self.get_config_value('glossary_min_frequency', 2), # auto_glossary_min_freq self.get_config_value('glossary_max_names', 50), # auto_glossary_max_names self.get_config_value('glossary_max_titles', 30), # auto_glossary_max_titles self.get_config_value('glossary_batch_size', 50), # auto_glossary_batch_size self.get_config_value('glossary_filter_mode', 'all'), # auto_glossary_filter_mode self.get_config_value('glossary_fuzzy_threshold', 0.90), # auto_glossary_fuzzy_threshold # Manual glossary extraction settings self.get_config_value('manual_glossary_min_frequency', self.get_config_value('glossary_min_frequency', 2)), # min_freq self.get_config_value('manual_glossary_max_names', self.get_config_value('glossary_max_names', 50)), # max_names_slider self.get_config_value('manual_glossary_max_titles', self.get_config_value('glossary_max_titles', 30)), # max_titles self.get_config_value('glossary_max_text_size', 50000), # max_text_size self.get_config_value('glossary_max_sentences', 200), # max_sentences self.get_config_value('manual_glossary_batch_size', self.get_config_value('glossary_batch_size', 50)), # translation_batch self.get_config_value('glossary_chapter_split_threshold', 8192), # chapter_split_threshold self.get_config_value('manual_glossary_filter_mode', self.get_config_value('glossary_filter_mode', 'all')), # filter_mode self.get_config_value('strip_honorifics', True), # strip_honorifics self.get_config_value('manual_glossary_fuzzy_threshold', self.get_config_value('glossary_fuzzy_threshold', 0.90)), # fuzzy_threshold # Chapter processing options self.get_config_value('batch_translate_headers', False), # batch_translate_headers self.get_config_value('headers_per_batch', 400), # headers_per_batch self.get_config_value('use_ncx_navigation', False), # use_ncx_navigation self.get_config_value('attach_css_to_chapters', False), # attach_css_to_chapters self.get_config_value('retain_source_extension', True), # retain_source_extension self.get_config_value('use_conservative_batching', False), # use_conservative_batching self.get_config_value('disable_gemini_safety', False), # disable_gemini_safety self.get_config_value('use_http_openrouter', False), # use_http_openrouter self.get_config_value('disable_openrouter_compression', False), # disable_openrouter_compression self.get_config_value('text_extraction_method', 'standard'), # text_extraction_method self.get_config_value('file_filtering_level', 'smart'), # file_filtering_level # QA report format self.get_config_value('qa_report_format', 'detailed'), # report_format # Thinking mode settings self.get_config_value('enable_gpt_thinking', True), # enable_gpt_thinking self.get_config_value('gpt_thinking_effort', 'medium'), # gpt_thinking_effort self.get_config_value('or_thinking_tokens', 2000), # or_thinking_tokens self.get_config_value('enable_gemini_thinking', False), # enable_gemini_thinking - disabled by default self.get_config_value('gemini_thinking_budget', 0), # gemini_thinking_budget - 0 = disabled # Manga settings self.get_config_value('model', 'gpt-4-turbo'), # manga_model self.get_config_value('api_key', ''), # manga_api_key self.get_config_value('active_profile', list(self.profiles.keys())[0] if self.profiles else ''), # manga_profile self.profiles.get(self.get_config_value('active_profile', ''), ''), # manga_system_prompt self.get_config_value('ocr_provider', 'custom-api'), # ocr_provider self.get_config_value('azure_vision_key', ''), # azure_key self.get_config_value('azure_vision_endpoint', ''), # azure_endpoint self.get_config_value('bubble_detection_enabled', True), # bubble_detection self.get_config_value('inpainting_enabled', True), # inpainting self.get_config_value('manga_font_size_mode', 'auto'), # font_size_mode self.get_config_value('manga_font_size', 24), # font_size self.get_config_value('manga_font_multiplier', 1.0), # font_multiplier self.get_config_value('manga_min_font_size', 12), # min_font_size self.get_config_value('manga_max_font_size', 48), # max_font_size # Convert colors to hex format if they're stored as RGB arrays (white text, black shadow like manga integration) to_hex_color(self.get_config_value('manga_text_color', [255, 255, 255]), '#FFFFFF'), # text_color_rgb - default white self.get_config_value('manga_shadow_enabled', True), # shadow_enabled to_hex_color(self.get_config_value('manga_shadow_color', [0, 0, 0]), '#000000'), # shadow_color - default black self.get_config_value('manga_shadow_offset_x', 2), # shadow_offset_x self.get_config_value('manga_shadow_offset_y', 2), # shadow_offset_y self.get_config_value('manga_shadow_blur', 0), # shadow_blur self.get_config_value('manga_bg_opacity', 130), # bg_opacity self.get_config_value('manga_bg_style', 'circle'), # bg_style self.get_config_value('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False), # parallel_panel_translation self.get_config_value('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 7), # panel_max_workers ] # SECURITY: Save Config button DISABLED to prevent API keys from being saved to persistent storage on HF Spaces # This is a critical security measure to prevent API key leakage in shared environments # save_config_btn.click( # fn=save_all_config, # inputs=[ # # EPUB tab fields # epub_model, epub_api_key, epub_profile, epub_temperature, epub_max_tokens, # enable_image_translation, enable_auto_glossary, append_glossary, # # Auto glossary settings # auto_glossary_min_freq, auto_glossary_max_names, auto_glossary_max_titles, # auto_glossary_batch_size, auto_glossary_filter_mode, auto_glossary_fuzzy_threshold, # enable_post_translation_scan, # # Manual glossary extraction settings # min_freq, max_names_slider, max_titles, # max_text_size, max_sentences, translation_batch, # chapter_split_threshold, filter_mode, strip_honorifics, # fuzzy_threshold, extraction_prompt, format_instructions, # use_legacy_csv, # # QA Scanner settings # min_foreign_chars, check_repetition, check_glossary_leakage, # min_file_length, check_multiple_headers, check_missing_html, # check_insufficient_paragraphs, min_paragraph_percentage, # report_format, auto_save_report, # # Chapter processing options # batch_translate_headers, headers_per_batch, use_ncx_navigation, # attach_css_to_chapters, retain_source_extension, # use_conservative_batching, disable_gemini_safety, # use_http_openrouter, disable_openrouter_compression, # text_extraction_method, file_filtering_level, # # Thinking mode settings # enable_gpt_thinking, gpt_thinking_effort, or_thinking_tokens, # enable_gemini_thinking, gemini_thinking_budget, # # Manga tab fields # manga_model, manga_api_key, manga_profile, # ocr_provider, azure_key, azure_endpoint, # bubble_detection, inpainting, # font_size_mode, font_size, font_multiplier, min_font_size, max_font_size, # text_color_rgb, shadow_enabled, shadow_color, # shadow_offset_x, shadow_offset_y, shadow_blur, # bg_opacity, bg_style, # parallel_panel_translation, panel_max_workers, # # Advanced Settings fields # detector_type, rtdetr_confidence, bubble_confidence, # detect_text_bubbles, detect_empty_bubbles, detect_free_text, bubble_max_detections, # local_inpaint_method, webtoon_mode, # inpaint_batch_size, inpaint_cache_enabled, # parallel_processing, max_workers, # preload_local_inpainting, panel_start_stagger, # torch_precision, auto_cleanup_models, # debug_mode, save_intermediate, concise_pipeline_logs # ], # outputs=[save_status_text] # ) # Add load handler to restore settings on page load app.load( fn=load_all_settings, inputs=[], outputs=[ epub_model, epub_api_key, epub_profile, epub_system_prompt, epub_temperature, epub_max_tokens, enable_image_translation, enable_auto_glossary, append_glossary, # Auto glossary settings auto_glossary_min_freq, auto_glossary_max_names, auto_glossary_max_titles, auto_glossary_batch_size, auto_glossary_filter_mode, auto_glossary_fuzzy_threshold, # Manual glossary extraction settings min_freq, max_names_slider, max_titles, max_text_size, max_sentences, translation_batch, chapter_split_threshold, filter_mode, strip_honorifics, fuzzy_threshold, # Chapter processing options batch_translate_headers, headers_per_batch, use_ncx_navigation, attach_css_to_chapters, retain_source_extension, use_conservative_batching, disable_gemini_safety, use_http_openrouter, disable_openrouter_compression, text_extraction_method, file_filtering_level, report_format, # Thinking mode settings enable_gpt_thinking, gpt_thinking_effort, or_thinking_tokens, enable_gemini_thinking, gemini_thinking_budget, # Manga settings manga_model, manga_api_key, manga_profile, manga_system_prompt, ocr_provider, azure_key, azure_endpoint, bubble_detection, inpainting, font_size_mode, font_size, font_multiplier, min_font_size, max_font_size, text_color_rgb, shadow_enabled, shadow_color, shadow_offset_x, shadow_offset_y, shadow_blur, bg_opacity, bg_style, parallel_panel_translation, panel_max_workers ] ) return app def main(): """Launch Gradio web app""" print("π Starting Glossarion Web Interface...") # Check if running on Hugging Face Spaces is_spaces = os.getenv('SPACE_ID') is not None or os.getenv('HF_SPACES') == 'true' if is_spaces: print("π€ Running on Hugging Face Spaces") print(f"π Space ID: {os.getenv('SPACE_ID', 'Unknown')}") print(f"π Files in current directory: {len(os.listdir('.'))} items") print(f"π Working directory: {os.getcwd()}") print(f"π Available manga modules: {MANGA_TRANSLATION_AVAILABLE}") else: print("π Running locally") web_app = GlossarionWeb() app = web_app.create_interface() # Set favicon with absolute path if available (skip for Spaces) favicon_path = None if not is_spaces and os.path.exists("Halgakos.ico"): favicon_path = os.path.abspath("Halgakos.ico") print(f"β Using favicon: {favicon_path}") elif not is_spaces: print("β οΈ Halgakos.ico not found") # Launch with options appropriate for environment launch_args = { "server_name": "0.0.0.0", # Allow external access "server_port": 7860, "share": False, "show_error": True, } # Only add favicon for non-Spaces environments if not is_spaces and favicon_path: launch_args["favicon_path"] = favicon_path app.launch(**launch_args) if __name__ == "__main__": main()