diff --git "a/glossarion_web.py" "b/glossarion_web.py" --- "a/glossarion_web.py" +++ "b/glossarion_web.py" @@ -1,2257 +1,2288 @@ -#!/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 - -# 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 -except ImportError as e: - MANGA_TRANSLATION_AVAILABLE = False - print(f"⚠️ Manga translation modules not found: {e}") - - -class GlossarionWeb: - """Web interface for Glossarion translator""" - - def __init__(self): - self.config_file = "config_web.json" - self.config = self.load_config() - # Decrypt API keys for use - if API_KEY_ENCRYPTION_AVAILABLE: - self.config = decrypt_config(self.config) - self.models = get_model_options() if TRANSLATION_AVAILABLE else ["gpt-4", "claude-3-5-sonnet"] - - # 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 , , <h1>, <h2>, <p>, <br>, <div>, etc.\n" - ), - "japanese": ( - "You are a professional Japanese 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 Japanese honorifics and respectful speech markers in romanized form, including but not limited to: -san, -sama, -chan, -kun, -dono, -sensei, -senpai, -kouhai. For archaic/classical Japanese honorific forms, preserve them as-is rather than converting to modern equivalents.\n" - "- Always localize Japanese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔術 = magic).\n" - "- When translating Japanese'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 while reflecting the Japanese pronoun's nuance (私/僕/俺/etc.) through speech patterns rather than the pronoun itself, and maintain natural English flow without overusing pronouns just because they're omitted in Japanese.\n" - "- All Japanese profanity must be translated to English profanity.\n" - "- Preserve original intent, and speech tone.\n" - "- Retain onomatopoeia in Romaji.\n" - "- Keep original Japanese 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 <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n" - ), - "chinese": ( - "You are a professional Chinese 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" - "- Always localize Chinese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔法 = magic).\n" - "- When translating Chinese's pronoun-dropping style, insert pronouns in English only where needed for clarity while maintaining natural English flow.\n" - "- All Chinese profanity must be translated to English profanity.\n" - "- Preserve original intent, and speech tone.\n" - "- Retain onomatopoeia in Pinyin.\n" - "- Keep original Chinese 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 <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n" - ), - "Manga_JP": ( - "You are a professional Japanese to English Manga translator.\n" - "You have both the image of the Manga panel and the extracted text to work with.\n" - "Output only English text while following these rules: \n\n" - - "VISUAL CONTEXT:\n" - "- Analyze the character's facial expressions and body language in the image.\n" - "- Consider the scene's mood and atmosphere.\n" - "- Note any action or movement depicted.\n" - "- Use visual cues to determine the appropriate tone and emotion.\n" - "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n" - - "DIALOGUE REQUIREMENTS:\n" - "- Match the translation tone to the character's expression.\n" - "- If a character looks angry, use appropriately intense language.\n" - "- If a character looks shy or embarrassed, reflect that in the translation.\n" - "- Keep speech patterns consistent with the character's appearance and demeanor.\n" - "- Retain honorifics and onomatopoeia in Romaji.\n\n" - - "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" - ), - "Manga_KR": ( - "You are a professional Korean to English Manhwa translator.\n" - "You have both the image of the Manhwa panel and the extracted text to work with.\n" - "Output only English text while following these rules: \n\n" - - "VISUAL CONTEXT:\n" - "- Analyze the character's facial expressions and body language in the image.\n" - "- Consider the scene's mood and atmosphere.\n" - "- Note any action or movement depicted.\n" - "- Use visual cues to determine the appropriate tone and emotion.\n" - "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n" - - "DIALOGUE REQUIREMENTS:\n" - "- Match the translation tone to the character's expression.\n" - "- If a character looks angry, use appropriately intense language.\n" - "- If a character looks shy or embarrassed, reflect that in the translation.\n" - "- Keep speech patterns consistent with the character's appearance and demeanor.\n" - "- Retain honorifics and onomatopoeia in Romaji.\n\n" - - "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" - ), - "Manga_CN": ( - "You are a professional Chinese to English Manga translator.\n" - "You have both the image of the Manga panel and the extracted text to work with.\n" - "Output only English text while following these rules: \n\n" - - "VISUAL CONTEXT:\n" - "- Analyze the character's facial expressions and body language in the image.\n" - "- Consider the scene's mood and atmosphere.\n" - "- Note any action or movement depicted.\n" - "- Use visual cues to determine the appropriate tone and emotion.\n" - "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n" - - "DIALOGUE REQUIREMENTS:\n" - "- Match the translation tone to the character's expression.\n" - "- If a character looks angry, use appropriately intense language.\n" - "- If a character looks shy or embarrassed, reflect that in the translation.\n" - "- Keep speech patterns consistent with the character's appearance and demeanor.\n" - "- Retain honorifics and onomatopoeia in Romaji.\n\n" - - "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" - ), - "Original": "Return everything exactly as seen on the source." - } - - # Load profiles from config, fallback to defaults - self.profiles = self.config.get('prompt_profiles', self.default_prompts.copy()) - if not self.profiles: - self.profiles = self.default_prompts.copy() - - def load_config(self): - """Load configuration""" - try: - if os.path.exists(self.config_file): - with open(self.config_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"Warning: Failed to load config: {e}") - return {} - - def save_config(self, config): - """Save configuration with encryption""" - try: - # Encrypt sensitive fields before saving - encrypted_config = config.copy() - if API_KEY_ENCRYPTION_AVAILABLE: - encrypted_config = encrypt_config(encrypted_config) - - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(encrypted_config, f, ensure_ascii=False, indent=2) - # Reload config to ensure consistency and decrypt it - self.config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - self.config = decrypt_config(self.config) - except Exception as e: - return f"❌ Failed to save config: {e}" - return "✅ Configuration saved" - - def translate_epub( - self, - epub_file, - model, - api_key, - profile_name, - system_prompt, - temperature, - max_tokens, - glossary_file=None, - progress=gr.Progress() - ): - """Translate EPUB file""" - - if not TRANSLATION_AVAILABLE: - return None, "❌ Translation modules not loaded" - - if not epub_file: - return None, "❌ Please upload an EPUB file" - - if not api_key: - return None, "❌ Please provide an API key" - - if not profile_name: - return None, "❌ Please select a translation profile" - - try: - # Progress tracking - progress(0, desc="Starting translation...") - - # Save uploaded file to temp location if needed - input_path = epub_file.name if hasattr(epub_file, 'name') else epub_file - epub_base = os.path.splitext(os.path.basename(input_path))[0] - - # Use the provided system prompt (user may have edited it) - translation_prompt = system_prompt if system_prompt else self.profiles.get(profile_name, "") - - # Set the input path as a command line argument simulation - # TransateKRtoEN.main() reads from sys.argv if config doesn't have it - import sys - original_argv = sys.argv.copy() - sys.argv = ['glossarion_web.py', input_path] - - # Set environment variables for TransateKRtoEN.main() - os.environ['INPUT_PATH'] = input_path - os.environ['MODEL'] = model - os.environ['TRANSLATION_TEMPERATURE'] = str(temperature) - os.environ['MAX_OUTPUT_TOKENS'] = str(max_tokens) - - # Set API key environment variable - if 'gpt' in model.lower() or 'openai' in model.lower(): - os.environ['OPENAI_API_KEY'] = api_key - os.environ['API_KEY'] = api_key - elif 'claude' in model.lower(): - os.environ['ANTHROPIC_API_KEY'] = api_key - os.environ['API_KEY'] = api_key - elif 'gemini' in model.lower(): - os.environ['GOOGLE_API_KEY'] = api_key - os.environ['API_KEY'] = api_key - else: - os.environ['API_KEY'] = api_key - - # Set the system prompt - if translation_prompt: - # Save to temp profile - temp_config = self.config.copy() - temp_config['prompt_profiles'] = temp_config.get('prompt_profiles', {}) - temp_config['prompt_profiles'][profile_name] = translation_prompt - temp_config['active_profile'] = profile_name - - # Save temporarily - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(temp_config, f, ensure_ascii=False, indent=2) - - progress(0.1, desc="Initializing translation...") - - # Create a thread-safe queue for capturing logs - import queue - import threading - log_queue = queue.Queue() - last_log = "" - - def log_callback(msg): - """Capture log messages without recursion""" - nonlocal last_log - if msg and msg.strip(): - last_log = msg.strip() - log_queue.put(msg.strip()) - - # Monitor logs in a separate thread - def update_progress(): - while True: - try: - msg = log_queue.get(timeout=0.5) - # Extract progress if available - if '✅' in msg or '✓' in msg: - progress(0.5, desc=msg[:100]) # Limit message length - elif '🔄' in msg or 'Translating' in msg: - progress(0.3, desc=msg[:100]) - else: - progress(0.2, desc=msg[:100]) - except queue.Empty: - if last_log: - progress(0.2, desc=last_log[:100]) - continue - except: - break - - progress_thread = threading.Thread(target=update_progress, daemon=True) - progress_thread.start() - - # Call translation function (it reads from environment and config) - try: - result = TransateKRtoEN.main( - log_callback=log_callback, - stop_callback=None - ) - finally: - # Restore original sys.argv - sys.argv = original_argv - # Stop progress thread - log_queue.put(None) - - progress(1.0, desc="Translation complete!") - - # Check for output EPUB in the output directory - output_dir = epub_base - if os.path.exists(output_dir): - # Look for compiled EPUB - compiled_epub = os.path.join(output_dir, f"{epub_base}_translated.epub") - if os.path.exists(compiled_epub): - return compiled_epub, f"✅ Translation successful!\n\nTranslated: {os.path.basename(compiled_epub)}" - - return None, "❌ Translation failed - output file not created" - - except Exception as e: - import traceback - error_msg = f"❌ Error during translation:\n{str(e)}\n\n{traceback.format_exc()}" - return None, error_msg - - def extract_glossary( - self, - epub_file, - model, - api_key, - min_frequency, - max_names, - progress=gr.Progress() - ): - """Extract glossary from EPUB""" - - if not epub_file: - return None, "❌ Please upload an EPUB file" - - try: - import extract_glossary_from_epub - - progress(0, desc="Starting glossary extraction...") - - input_path = epub_file.name - output_path = input_path.replace('.epub', '_glossary.csv') - - # Set API key - if 'gpt' in model.lower(): - os.environ['OPENAI_API_KEY'] = api_key - elif 'claude' in model.lower(): - os.environ['ANTHROPIC_API_KEY'] = api_key - - progress(0.2, desc="Extracting text...") - - # Set environment variables for glossary extraction - os.environ['MODEL'] = model - os.environ['GLOSSARY_MIN_FREQUENCY'] = str(min_frequency) - os.environ['GLOSSARY_MAX_NAMES'] = str(max_names) - - # Call with proper arguments (check the actual signature) - result = extract_glossary_from_epub.main( - log_callback=None, - stop_callback=None - ) - - progress(1.0, desc="Glossary extraction complete!") - - if os.path.exists(output_path): - return output_path, f"✅ Glossary extracted!\n\nSaved to: {os.path.basename(output_path)}" - else: - return None, "❌ Glossary extraction failed" - - except Exception as e: - return None, f"❌ Error: {str(e)}" - - 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) updates""" - - if not MANGA_TRANSLATION_AVAILABLE: - yield "❌ Manga translation modules not loaded", None, None, gr.update(value="❌ Error", visible=True) - return - - if not image_files: - yield "❌ Please upload at least one image", None, None, gr.update(value="❌ Error", visible=True) - return - - if not api_key: - yield "❌ Please provide an API key", None, None, gr.update(value="❌ Error", visible=True) - return - - if ocr_provider == "google": - # Check if credentials are provided or saved in config - if not google_creds_path and not self.config.get('google_vision_credentials'): - yield "❌ Please provide Google Cloud credentials JSON file", None, None, gr.update(value="❌ Error", visible=True) - 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", None, None, gr.update(value="❌ Error", visible=True) - return - - try: - - # 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.config.get('google_vision_credentials'): - # Use saved credentials from config - creds_path = self.config.get('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}", None, None, gr.update(value="❌ Error", visible=True) - 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): - hex_color = hex_color.lstrip('#') - return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - - text_rgb = hex_to_rgb(text_color) - shadow_rgb = hex_to_rgb(shadow_color) - - 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_onnx) - if 'local_method' not in merged_config['manga_settings']['inpainting']: - merged_config['manga_settings']['inpainting']['local_method'] = self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx') - - # Set parallel panel translation settings from UI parameters - merged_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_panel_translation - merged_config['manga_settings']['advanced']['panel_max_workers'] = int(panel_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 - self.delay_entry = MockVar(float(config.get('delay', 2.0))) - 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))) - # 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) - - # 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_onnx') - # 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.config.get(model_path_key, '') - merged_config[model_path_key] = default_model_path - print(f"Set {model_path_key} to: {default_model_path}") - - # 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 - ) - except Exception as e: - error_log = f"❌ Failed to initialize API client: {str(e)}" - yield error_log, None, None, gr.update(value=error_log, visible=True) - 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 - ) - - # 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, None, None, gr.update(value=error_log, visible=True) - return - - # Process each image with real progress tracking - for idx, img_file in enumerate(files_to_process, 1): - try: - # 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 - last_yield_log_count[0] = len(translation_logs) - last_yield_time[0] = time.time() - yield "\n".join(translation_logs), None, None, gr.update(visible=False) - - # 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 - if should_yield_logs(): - last_yield_log_count[0] = len(translation_logs) - last_yield_time[0] = time.time() - yield "\n".join(translation_logs), None, None, gr.update(visible=False) - - # 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 completed image - yield "\n".join(translation_logs), gr.update(value=final_output, visible=True), None, gr.update(visible=False) - 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 - yield "\n".join(translation_logs), None, None, gr.update(visible=False) - 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 - yield "\n".join(translation_logs), None, None, gr.update(visible=False) - - # 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 - - # 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 - final_status_lines = [] - if translated_files: - final_status_lines.append(f"✅ Successfully translated {len(translated_files)}/{total_images} image(s)!") - if cbz_mode and cbz_output_path: - final_status_lines.append(f"\n📦 CBZ Output: {cbz_output_path}") - else: - final_status_lines.append(f"\nOutput directory: {output_dir}") - 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) - if translated_files: - # If CBZ mode, show CBZ file for download; otherwise show first image - if cbz_mode and cbz_output_path and os.path.exists(cbz_output_path): - yield "\n".join(translation_logs), gr.update(value=translated_files[0], visible=True), gr.update(value=cbz_output_path, visible=True), gr.update(value=final_status_text, visible=True) - else: - yield "\n".join(translation_logs), gr.update(value=translated_files[0], visible=True), gr.update(visible=False), gr.update(value=final_status_text, visible=True) - else: - yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(value=final_status_text, visible=True) - - except Exception as e: - import traceback - error_msg = f"❌ Error during manga translation:\n{str(e)}\n\n{traceback.format_exc()}" - yield error_msg, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_msg, visible=True) - - def create_interface(self): - """Create Gradio interface""" - - # Load and encode icon as base64 - icon_base64 = "" - icon_path = "Halgakos.png" if os.path.exists("Halgakos.png") 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;} - """ - - 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'<img src="data:image/png;base64,{icon_base64}" alt="Glossarion">' if icon_base64 else '' - - gr.HTML(f""" - <link rel="icon" type="image/x-icon" href="file/Halgakos.ico"> - <link rel="shortcut icon" type="image/x-icon" href="file/Halgakos.ico"> - <style> - .title-with-icon {{ - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 10px; - }} - .title-with-icon img {{ - width: 48px; - height: 48px; - }} - </style> - <div class="title-with-icon"> - {icon_img_tag} - <h1>Glossarion - AI-Powered Translation</h1> - </div> - """) - - gr.Markdown(""" - Translate novels and books using advanced AI models (GPT-5, Claude, etc.) - """) - - with gr.Tabs(): - # Manga Translation Tab - DEFAULT/FIRST - with gr.Tab("🎨 Manga Translation"): - with gr.Row(): - with gr.Column(): - manga_images = gr.File( - label="🖼️ Upload Manga Images or CBZ", - file_types=[".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".cbz", ".zip"], - file_count="multiple" - ) - - translate_manga_btn = gr.Button( - "🚀 Translate Manga", - variant="primary", - size="lg" - ) - - manga_model = gr.Dropdown( - choices=self.models, - value=self.config.get('model', 'gpt-4-turbo'), - label="🤖 AI Model" - ) - - manga_api_key = gr.Textbox( - label="🔑 API Key", - type="password", - placeholder="Enter your API key", - value=self.config.get('api_key', '') # Pre-fill from config - ) - - # Filter manga-specific profiles - manga_profile_choices = [k for k in self.profiles.keys() if k.startswith('Manga_')] - if not manga_profile_choices: - manga_profile_choices = list(self.profiles.keys()) # Fallback to all - - default_manga_profile = "Manga_JP" if "Manga_JP" in self.profiles else manga_profile_choices[0] if manga_profile_choices else "" - - manga_profile = gr.Dropdown( - choices=manga_profile_choices, - value=default_manga_profile, - label="📝 Translation Profile" - ) - - # Editable manga system prompt - manga_system_prompt = gr.Textbox( - label="Manga System Prompt (Translation Instructions)", - lines=8, - max_lines=15, - interactive=True, - placeholder="Select a manga profile to load translation instructions...", - value=self.profiles.get(default_manga_profile, '') if default_manga_profile else '' - ) - - with gr.Accordion("⚙️ OCR Settings", open=False): - gr.Markdown("🔒 **Credentials are auto-saved** to your config (encrypted) after first use.") - - ocr_provider = gr.Radio( - choices=["google", "azure", "custom-api"], - value=self.config.get('ocr_provider', 'custom-api'), - label="OCR Provider" - ) - - # Show saved Google credentials path if available - saved_google_path = self.config.get('google_vision_credentials', '') - if saved_google_path and os.path.exists(saved_google_path): - gr.Markdown(f"✅ **Saved credentials found:** `{os.path.basename(saved_google_path)}`") - gr.Markdown("💡 *Using saved credentials. Upload a new file only if you want to change them.*") - else: - gr.Markdown("⚠️ No saved Google credentials found. Please upload your JSON file.") - - # Note: File component doesn't support pre-filling paths due to browser security - google_creds = gr.File( - label="Google Cloud Credentials JSON (upload to update)", - file_types=[".json"] - ) - - azure_key = gr.Textbox( - label="Azure Vision API Key (if using Azure)", - type="password", - placeholder="Enter Azure API key", - value=self.config.get('azure_vision_key', '') - ) - - azure_endpoint = gr.Textbox( - label="Azure Vision Endpoint (if using Azure)", - placeholder="https://your-resource.cognitiveservices.azure.com/", - value=self.config.get('azure_vision_endpoint', '') - ) - - bubble_detection = gr.Checkbox( - label="Enable Bubble Detection", - value=self.config.get('bubble_detection_enabled', True) - ) - - inpainting = gr.Checkbox( - label="Enable Text Removal (Inpainting)", - value=self.config.get('inpainting_enabled', True) - ) - - with gr.Accordion("✨ Text Visibility Settings", open=False): - gr.Markdown("### Font Settings") - - font_size_mode = gr.Radio( - choices=["auto", "fixed", "multiplier"], - value=self.config.get('manga_font_size_mode', 'auto'), - label="Font Size Mode" - ) - - font_size = gr.Slider( - minimum=0, - maximum=72, - value=self.config.get('manga_font_size', 24), - step=1, - label="Fixed Font Size (0=auto, used when mode=fixed)" - ) - - font_multiplier = gr.Slider( - minimum=0.5, - maximum=2.0, - value=self.config.get('manga_font_size_multiplier', 1.0), - step=0.1, - label="Font Size Multiplier (when mode=multiplier)" - ) - - min_font_size = gr.Slider( - minimum=0, - maximum=100, - value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_min_size', 12), - step=1, - label="Minimum Font Size (0=no limit)" - ) - - max_font_size = gr.Slider( - minimum=20, - maximum=100, - value=self.config.get('manga_max_font_size', 48), - step=1, - label="Maximum Font Size" - ) - - gr.Markdown("### Text Color") - - text_color_rgb = gr.ColorPicker( - label="Font Color", - value="#000000" # Default black - ) - - gr.Markdown("### Shadow Settings") - - shadow_enabled = gr.Checkbox( - label="Enable Text Shadow", - value=self.config.get('manga_shadow_enabled', True) - ) - - shadow_color = gr.ColorPicker( - label="Shadow Color", - value="#FFFFFF" # Default white - ) - - shadow_offset_x = gr.Slider( - minimum=-10, - maximum=10, - value=self.config.get('manga_shadow_offset_x', 2), - step=1, - label="Shadow Offset X" - ) - - shadow_offset_y = gr.Slider( - minimum=-10, - maximum=10, - value=self.config.get('manga_shadow_offset_y', 2), - step=1, - label="Shadow Offset Y" - ) - - shadow_blur = gr.Slider( - minimum=0, - maximum=10, - value=self.config.get('manga_shadow_blur', 0), - step=1, - label="Shadow Blur" - ) - - gr.Markdown("### Background Settings") - - bg_opacity = gr.Slider( - minimum=0, - maximum=255, - value=self.config.get('manga_bg_opacity', 130), - step=1, - label="Background Opacity" - ) - - bg_style = gr.Radio( - choices=["box", "circle", "wrap"], - value=self.config.get('manga_bg_style', 'circle'), - label="Background Style" - ) - - with gr.Column(): - # Add logo and loading message 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 - ) - status_message = gr.Markdown( - value="### Ready to translate\nUpload an image and click 'Translate Manga' to begin.", - visible=True - ) - manga_logs = gr.Textbox( - label="📋 Translation Logs", - lines=20, - max_lines=30, - value="Ready to translate. Click 'Translate Manga' to begin.", - visible=True, - interactive=False - ) - - manga_output_image = gr.Image(label="📷 Translated Image Preview", visible=False) - manga_cbz_output = gr.File(label="📦 Download Translated CBZ", visible=False) - manga_status = gr.Textbox( - label="Final Status", - lines=8, - max_lines=15, - visible=False - ) - - # Auto-save model and API key - def save_manga_credentials(model, api_key): - """Save model and API key to config""" - try: - current_config = self.load_config() - current_config['model'] = model - if api_key: # Only save if not empty - current_config['api_key'] = api_key - self.save_config(current_config) - return None # No output needed - except Exception as e: - print(f"Failed to save manga credentials: {e}") - return None - - # Update manga system prompt when profile changes - def update_manga_system_prompt(profile_name): - return self.profiles.get(profile_name, "") - - # Auto-save on model change - manga_model.change( - fn=lambda m, k: save_manga_credentials(m, k), - inputs=[manga_model, manga_api_key], - outputs=None - ) - - # Auto-save on API key change - manga_api_key.change( - fn=lambda m, k: save_manga_credentials(m, k), - inputs=[manga_model, manga_api_key], - outputs=None - ) - - # Auto-save Azure credentials on change - def save_azure_credentials(key, endpoint): - """Save Azure credentials to config""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - if key and key.strip(): - current_config['azure_vision_key'] = str(key).strip() - if endpoint and endpoint.strip(): - current_config['azure_vision_endpoint'] = str(endpoint).strip() - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save Azure credentials: {e}") - return None - - azure_key.change( - fn=lambda k, e: save_azure_credentials(k, e), - inputs=[azure_key, azure_endpoint], - outputs=None - ) - - azure_endpoint.change( - fn=lambda k, e: save_azure_credentials(k, e), - inputs=[azure_key, azure_endpoint], - outputs=None - ) - - # Auto-save OCR provider on change - def save_ocr_provider(provider): - """Save OCR provider to config""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - current_config['ocr_provider'] = provider - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save OCR provider: {e}") - return None - - ocr_provider.change( - fn=save_ocr_provider, - inputs=[ocr_provider], - outputs=None - ) - - # Auto-save bubble detection and inpainting on change - def save_detection_settings(bubble_det, inpaint): - """Save bubble detection and inpainting settings""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - current_config['bubble_detection_enabled'] = bubble_det - current_config['inpainting_enabled'] = inpaint - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save detection settings: {e}") - return None - - bubble_detection.change( - fn=lambda b, i: save_detection_settings(b, i), - inputs=[bubble_detection, inpainting], - outputs=None - ) - - inpainting.change( - fn=lambda b, i: save_detection_settings(b, i), - inputs=[bubble_detection, inpainting], - outputs=None - ) - - # Auto-save font size mode on change - def save_font_mode(mode): - """Save font size mode to config""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - current_config['manga_font_size_mode'] = mode - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save font mode: {e}") - return None - - font_size_mode.change( - fn=save_font_mode, - inputs=[font_size_mode], - outputs=None - ) - - # Auto-save background style on change - def save_bg_style(style): - """Save background style to config""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - current_config['manga_bg_style'] = style - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save bg style: {e}") - return None - - bg_style.change( - fn=save_bg_style, - inputs=[bg_style], - outputs=None - ) - - manga_profile.change( - fn=update_manga_system_prompt, - inputs=[manga_profile], - outputs=[manga_system_prompt] - ) - - translate_manga_btn.click( - fn=self.translate_manga, - inputs=[ - manga_images, - manga_model, - manga_api_key, - manga_profile, - manga_system_prompt, - ocr_provider, - google_creds, - 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 - ], - outputs=[manga_logs, manga_output_image, manga_cbz_output, manga_status] - ) - - # Manga Settings Tab - NEW - with gr.Tab("🎬 Manga Settings"): - gr.Markdown("### Advanced Manga Translation Settings") - gr.Markdown("Configure bubble detection, inpainting, preprocessing, and rendering options.") - - with gr.Accordion("🕹️ Bubble Detection & Inpainting", open=True): - gr.Markdown("#### Bubble Detection") - - detector_type = gr.Radio( - choices=["rtdetr_onnx", "rtdetr", "yolo"], - value=self.config.get('manga_settings', {}).get('ocr', {}).get('detector_type', 'rtdetr_onnx'), - label="Detector Type", - interactive=True - ) - - rtdetr_confidence = gr.Slider( - minimum=0.0, - maximum=1.0, - value=self.config.get('manga_settings', {}).get('ocr', {}).get('rtdetr_confidence', 0.3), - step=0.05, - label="RT-DETR Confidence Threshold", - interactive=True - ) - - bubble_confidence = gr.Slider( - minimum=0.0, - maximum=1.0, - value=self.config.get('manga_settings', {}).get('ocr', {}).get('bubble_confidence', 0.3), - step=0.05, - label="YOLO Bubble Confidence Threshold", - interactive=True - ) - - detect_text_bubbles = gr.Checkbox( - label="Detect Text Bubbles", - value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_text_bubbles', True) - ) - - detect_empty_bubbles = gr.Checkbox( - label="Detect Empty Bubbles", - value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_empty_bubbles', True) - ) - - detect_free_text = gr.Checkbox( - label="Detect Free Text (outside bubbles)", - value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_free_text', True) - ) - - gr.Markdown("#### Inpainting") - - local_inpaint_method = gr.Radio( - choices=["anime_onnx", "anime", "lama", "lama_onnx", "aot", "aot_onnx"], - value=self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx'), - label="Local Inpainting Model", - interactive=True - ) - - with gr.Row(): - download_models_btn = gr.Button( - "📥 Download Models", - variant="secondary", - size="sm" - ) - load_models_btn = gr.Button( - "📂 Load Models", - variant="secondary", - size="sm" - ) - - gr.Markdown("#### Mask Dilation") - - auto_iterations = gr.Checkbox( - label="Auto Iterations (Recommended)", - value=self.config.get('manga_settings', {}).get('auto_iterations', True) - ) - - mask_dilation = gr.Slider( - minimum=0, - maximum=20, - value=self.config.get('manga_settings', {}).get('mask_dilation', 0), - step=1, - label="General Mask Dilation", - interactive=True - ) - - text_bubble_dilation = gr.Slider( - minimum=0, - maximum=20, - value=self.config.get('manga_settings', {}).get('text_bubble_dilation_iterations', 2), - step=1, - label="Text Bubble Dilation Iterations", - interactive=True - ) - - empty_bubble_dilation = gr.Slider( - minimum=0, - maximum=20, - value=self.config.get('manga_settings', {}).get('empty_bubble_dilation_iterations', 3), - step=1, - label="Empty Bubble Dilation Iterations", - interactive=True - ) - - free_text_dilation = gr.Slider( - minimum=0, - maximum=20, - value=self.config.get('manga_settings', {}).get('free_text_dilation_iterations', 3), - step=1, - label="Free Text Dilation Iterations", - interactive=True - ) - - with gr.Accordion("🖌️ Image Preprocessing", open=False): - preprocessing_enabled = gr.Checkbox( - label="Enable Preprocessing", - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enabled', False) - ) - - auto_detect_quality = gr.Checkbox( - label="Auto Detect Image Quality", - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('auto_detect_quality', True) - ) - - enhancement_strength = gr.Slider( - minimum=1.0, - maximum=3.0, - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enhancement_strength', 1.5), - step=0.1, - label="Enhancement Strength", - interactive=True - ) - - denoise_strength = gr.Slider( - minimum=0, - maximum=50, - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('denoise_strength', 10), - step=1, - label="Denoise Strength", - interactive=True - ) - - max_image_dimension = gr.Number( - label="Max Image Dimension (pixels)", - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('max_image_dimension', 2000), - minimum=500 - ) - - chunk_height = gr.Number( - label="Chunk Height for Large Images", - value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('chunk_height', 1000), - minimum=500 - ) - - gr.Markdown("#### HD Strategy for Inpainting") - gr.Markdown("*Controls how large images are processed during inpainting*") - - hd_strategy = gr.Radio( - choices=["original", "resize", "crop"], - value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy', 'resize'), - label="HD Strategy", - interactive=True, - info="original = legacy full-image; resize/crop = faster" - ) - - hd_strategy_resize_limit = gr.Slider( - minimum=512, - maximum=4096, - value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_resize_limit', 1536), - step=64, - label="Resize Limit (long edge, px)", - info="For resize strategy", - interactive=True - ) - - hd_strategy_crop_margin = gr.Slider( - minimum=0, - maximum=256, - value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_margin', 16), - step=2, - label="Crop Margin (px)", - info="For crop strategy", - interactive=True - ) - - hd_strategy_crop_trigger = gr.Slider( - minimum=256, - maximum=4096, - value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_trigger_size', 1024), - step=64, - label="Crop Trigger Size (px)", - info="Apply crop only if long edge exceeds this", - interactive=True - ) - - gr.Markdown("#### Image Tiling") - gr.Markdown("*Alternative tiling strategy (note: HD Strategy takes precedence)*") - - tiling_enabled = gr.Checkbox( - label="Enable Tiling", - value=self.config.get('manga_settings', {}).get('tiling', {}).get('enabled', False) - ) - - tiling_tile_size = gr.Slider( - minimum=256, - maximum=1024, - value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_size', 480), - step=64, - label="Tile Size (px)", - interactive=True - ) - - tiling_tile_overlap = gr.Slider( - minimum=0, - maximum=128, - value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_overlap', 64), - step=16, - label="Tile Overlap (px)", - interactive=True - ) - - with gr.Accordion("🎨 Font & Text Rendering", open=False): - gr.Markdown("#### Font Sizing Algorithm") - - font_algorithm = gr.Radio( - choices=["smart", "simple"], - value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('algorithm', 'smart'), - label="Font Sizing Algorithm", - interactive=True - ) - - prefer_larger = gr.Checkbox( - label="Prefer Larger Fonts", - value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('prefer_larger', True) - ) - - max_lines = gr.Slider( - minimum=1, - maximum=20, - value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('max_lines', 10), - step=1, - label="Maximum Lines Per Bubble", - interactive=True - ) - - line_spacing = gr.Slider( - minimum=0.5, - maximum=3.0, - value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('line_spacing', 1.3), - step=0.1, - label="Line Spacing Multiplier", - interactive=True - ) - - bubble_size_factor = gr.Checkbox( - label="Use Bubble Size Factor", - value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('bubble_size_factor', True) - ) - - auto_fit_style = gr.Radio( - choices=["balanced", "aggressive", "conservative"], - value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_fit_style', 'balanced'), - label="Auto Fit Style", - interactive=True - ) - - with gr.Accordion("⚙️ Advanced Options", open=False): - gr.Markdown("#### Format Detection") - - format_detection = gr.Checkbox( - label="Enable Format Detection (manga/webtoon)", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('format_detection', True) - ) - - webtoon_mode = gr.Radio( - choices=["auto", "force_manga", "force_webtoon"], - value=self.config.get('manga_settings', {}).get('advanced', {}).get('webtoon_mode', 'auto'), - label="Webtoon Mode", - interactive=True - ) - - gr.Markdown("#### Performance") - - parallel_processing = gr.Checkbox( - label="Enable Parallel Processing", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_processing', True) - ) - - max_workers = gr.Slider( - minimum=1, - maximum=8, - value=self.config.get('manga_settings', {}).get('advanced', {}).get('max_workers', 2), - step=1, - label="Max Worker Threads", - interactive=True - ) - - parallel_panel_translation = gr.Checkbox( - label="Parallel Panel Translation", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False) - ) - - panel_max_workers = gr.Slider( - minimum=1, - maximum=20, - value=self.config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10), - step=1, - label="Panel Max Workers", - interactive=True - ) - - gr.Markdown("#### Model Optimization") - - torch_precision = gr.Radio( - choices=["fp32", "fp16"], - value=self.config.get('manga_settings', {}).get('advanced', {}).get('torch_precision', 'fp16'), - label="Torch Precision", - interactive=True - ) - - auto_cleanup_models = gr.Checkbox( - label="Auto Cleanup Models from Memory", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('auto_cleanup_models', False) - ) - - gr.Markdown("#### Debug Options") - - debug_mode = gr.Checkbox( - label="Enable Debug Mode", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('debug_mode', False) - ) - - save_intermediate = gr.Checkbox( - label="Save Intermediate Files", - value=self.config.get('manga_settings', {}).get('advanced', {}).get('save_intermediate', False) - ) - - concise_pipeline_logs = gr.Checkbox( - label="Concise Pipeline Logs", - value=self.config.get('concise_pipeline_logs', True) - ) - - # Button handlers for model management - def download_models_handler(detector_type_val, inpaint_method_val): - """Download selected models""" - messages = [] - - try: - # Download bubble detection model - if detector_type_val: - messages.append(f"📥 Downloading {detector_type_val} bubble detector...") - try: - from bubble_detector import BubbleDetector - bd = BubbleDetector() - - if detector_type_val == "rtdetr_onnx": - if bd.load_rtdetr_onnx_model(): - messages.append("✅ RT-DETR ONNX model downloaded successfully") - else: - messages.append("❌ Failed to download RT-DETR ONNX model") - elif detector_type_val == "rtdetr": - if bd.load_rtdetr_model(): - messages.append("✅ RT-DETR model downloaded successfully") - else: - messages.append("❌ Failed to download RT-DETR model") - elif detector_type_val == "yolo": - messages.append("ℹ️ YOLO models are downloaded automatically on first use") - except Exception as e: - messages.append(f"❌ Error downloading detector: {str(e)}") - - # Download inpainting model - if inpaint_method_val: - messages.append(f"\n📥 Downloading {inpaint_method_val} inpainting model...") - try: - from local_inpainter import LocalInpainter, LAMA_JIT_MODELS - - inpainter = LocalInpainter({}) - - # Map method names to download keys - method_map = { - 'anime_onnx': 'anime_onnx', - 'anime': 'anime', - 'lama': 'lama', - 'lama_onnx': 'lama_onnx', - 'aot': 'aot', - 'aot_onnx': 'aot_onnx' - } - - method_key = method_map.get(inpaint_method_val) - if method_key and method_key in LAMA_JIT_MODELS: - model_info = LAMA_JIT_MODELS[method_key] - messages.append(f"Downloading {model_info['name']}...") - - model_path = inpainter.download_jit_model(method_key) - if model_path: - messages.append(f"✅ {model_info['name']} downloaded to: {model_path}") - else: - messages.append(f"❌ Failed to download {model_info['name']}") - else: - messages.append(f"ℹ️ {inpaint_method_val} is downloaded automatically on first use") - - except Exception as e: - messages.append(f"❌ Error downloading inpainting model: {str(e)}") - - if not messages: - messages.append("ℹ️ No models selected for download") - - except Exception as e: - messages.append(f"❌ Error during download: {str(e)}") - - return gr.Info("\n".join(messages)) - - def load_models_handler(detector_type_val, inpaint_method_val): - """Load selected models into memory""" - messages = [] - - try: - # Load bubble detection model - if detector_type_val: - messages.append(f"📦 Loading {detector_type_val} bubble detector...") - try: - from bubble_detector import BubbleDetector - bd = BubbleDetector() - - if detector_type_val == "rtdetr_onnx": - if bd.load_rtdetr_onnx_model(): - messages.append("✅ RT-DETR ONNX model loaded successfully") - else: - messages.append("❌ Failed to load RT-DETR ONNX model") - elif detector_type_val == "rtdetr": - if bd.load_rtdetr_model(): - messages.append("✅ RT-DETR model loaded successfully") - else: - messages.append("❌ Failed to load RT-DETR model") - elif detector_type_val == "yolo": - messages.append("ℹ️ YOLO models are loaded automatically when needed") - except Exception as e: - messages.append(f"❌ Error loading detector: {str(e)}") - - # Load inpainting model - if inpaint_method_val: - messages.append(f"\n📦 Loading {inpaint_method_val} inpainting model...") - try: - from local_inpainter import LocalInpainter, LAMA_JIT_MODELS - import os - - inpainter = LocalInpainter({}) - - # Map method names to model keys - method_map = { - 'anime_onnx': 'anime_onnx', - 'anime': 'anime', - 'lama': 'lama', - 'lama_onnx': 'lama_onnx', - 'aot': 'aot', - 'aot_onnx': 'aot_onnx' - } - - method_key = method_map.get(inpaint_method_val) - if method_key: - # First check if model exists, download if not - if method_key in LAMA_JIT_MODELS: - model_info = LAMA_JIT_MODELS[method_key] - cache_dir = os.path.expanduser('~/.cache/inpainting') - model_filename = os.path.basename(model_info['url']) - model_path = os.path.join(cache_dir, model_filename) - - if not os.path.exists(model_path): - messages.append(f"Model not found, downloading first...") - model_path = inpainter.download_jit_model(method_key) - if not model_path: - messages.append(f"❌ Failed to download model") - return gr.Info("\n".join(messages)) - - # Now load the model - if inpainter.load_model(method_key, model_path): - messages.append(f"✅ {model_info['name']} loaded successfully") - else: - messages.append(f"❌ Failed to load {model_info['name']}") - else: - messages.append(f"ℹ️ {inpaint_method_val} will be loaded automatically when needed") - else: - messages.append(f"ℹ️ Unknown method: {inpaint_method_val}") - - except Exception as e: - messages.append(f"❌ Error loading inpainting model: {str(e)}") - - if not messages: - messages.append("ℹ️ No models selected for loading") - - except Exception as e: - messages.append(f"❌ Error during loading: {str(e)}") - - return gr.Info("\n".join(messages)) - - download_models_btn.click( - fn=download_models_handler, - inputs=[detector_type, local_inpaint_method], - outputs=None - ) - - load_models_btn.click( - fn=load_models_handler, - inputs=[detector_type, local_inpaint_method], - outputs=None - ) - - # Auto-save parallel panel translation settings - def save_parallel_settings(parallel_enabled, max_workers): - """Save parallel panel translation settings to config""" - try: - current_config = self.load_config() - if API_KEY_ENCRYPTION_AVAILABLE: - current_config = decrypt_config(current_config) - - # Initialize nested structure if not exists - if 'manga_settings' not in current_config: - current_config['manga_settings'] = {} - if 'advanced' not in current_config['manga_settings']: - current_config['manga_settings']['advanced'] = {} - - current_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_enabled - current_config['manga_settings']['advanced']['panel_max_workers'] = int(max_workers) - - self.save_config(current_config) - return None - except Exception as e: - print(f"Failed to save parallel panel settings: {e}") - return None - - parallel_panel_translation.change( - fn=lambda p, w: save_parallel_settings(p, w), - inputs=[parallel_panel_translation, panel_max_workers], - outputs=None - ) - - panel_max_workers.change( - fn=lambda p, w: save_parallel_settings(p, w), - inputs=[parallel_panel_translation, panel_max_workers], - outputs=None - ) - - gr.Markdown("\n---\n**Note:** These settings will be saved to your config and applied to all manga translations.") - - # Glossary Extraction Tab - TEMPORARILY HIDDEN - with gr.Tab("📝 Glossary Extraction", visible=False): - with gr.Row(): - with gr.Column(): - glossary_epub = gr.File( - label="📖 Upload EPUB File", - file_types=[".epub"] - ) - - glossary_model = gr.Dropdown( - choices=self.models, - value="gpt-4-turbo", - label="🤖 AI Model" - ) - - glossary_api_key = gr.Textbox( - label="🔑 API Key", - type="password", - placeholder="Enter API key" - ) - - min_freq = gr.Slider( - minimum=1, - maximum=10, - value=2, - step=1, - label="Minimum Frequency" - ) - - max_names_slider = gr.Slider( - minimum=10, - maximum=200, - value=50, - step=10, - label="Max Character Names" - ) - - extract_btn = gr.Button( - "🔍 Extract Glossary", - variant="primary" - ) - - with gr.Column(): - glossary_output = gr.File(label="📥 Download Glossary CSV") - glossary_status = gr.Textbox( - label="Status", - lines=10 - ) - - extract_btn.click( - fn=self.extract_glossary, - inputs=[ - glossary_epub, - glossary_model, - glossary_api_key, - min_freq, - max_names_slider - ], - outputs=[glossary_output, glossary_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.config.get('thread_submission_delay', 0.5), - step=0.1, - label="Threading delay (s)" - ) - - api_delay = gr.Slider( - minimum=0, - maximum=10, - value=self.config.get('delay', 2), - step=0.5, - label="API call delay (s)" - ) - - chapter_range = gr.Textbox( - label="Chapter range (e.g., 5-10)", - value=self.config.get('chapter_range', ''), - placeholder="Leave empty for all chapters" - ) - - token_limit = gr.Number( - label="Input Token limit", - value=self.config.get('token_limit', 200000), - minimum=0 - ) - - disable_token_limit = gr.Checkbox( - label="Disable Input Token Limit", - value=self.config.get('token_limit_disabled', False) - ) - - output_token_limit = gr.Number( - label="Output Token limit", - value=self.config.get('max_output_tokens', 16000), - minimum=0 - ) - - with gr.Column(): - contextual = gr.Checkbox( - label="Contextual Translation", - value=self.config.get('contextual', False) - ) - - history_limit = gr.Number( - label="Translation History Limit", - value=self.config.get('translation_history_limit', 2), - minimum=0 - ) - - rolling_history = gr.Checkbox( - label="Rolling History Window", - value=self.config.get('translation_history_rolling', False) - ) - - batch_translation = gr.Checkbox( - label="Batch Translation", - value=self.config.get('batch_translation', False) - ) - - batch_size = gr.Number( - label="Batch Size", - value=self.config.get('batch_size', 3), - minimum=1 - ) - - 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="Settings auto-save on change", interactive=False) - - def save_settings(save_key, t_delay, a_delay, ch_range, tok_limit, disable_tok_limit, out_tok_limit, ctx, hist_lim, roll_hist, batch, b_size): - """Auto-save settings when changed""" - try: - # Reload latest config first to avoid overwriting other changes - current_config = self.load_config() - - # Update only the fields we're managing - current_config.update({ - 'save_api_key': save_key, - 'thread_submission_delay': float(t_delay), - 'delay': float(a_delay), - 'chapter_range': str(ch_range), - 'token_limit': int(tok_limit) if tok_limit else 200000, - 'token_limit_disabled': bool(disable_tok_limit), - 'max_output_tokens': int(out_tok_limit) if out_tok_limit else 16000, - 'contextual': bool(ctx), - 'translation_history_limit': int(hist_lim) if hist_lim else 2, - 'translation_history_rolling': bool(roll_hist), - 'batch_translation': bool(batch), - 'batch_size': int(b_size) if b_size else 3 - }) - - # Save with the merged config - result = self.save_config(current_config) - return f"✅ {result}" - except Exception as e: - import traceback - error_trace = traceback.format_exc() - print(f"Settings save error:\n{error_trace}") - return f"❌ Save failed: {str(e)}" - - # Auto-save on any change - for component in [save_api_key, thread_delay, api_delay, chapter_range, token_limit, disable_token_limit, - output_token_limit, contextual, history_limit, rolling_history, batch_translation, batch_size]: - component.change( - fn=save_settings, - inputs=[ - save_api_key, - thread_delay, - api_delay, - chapter_range, - token_limit, - disable_token_limit, - output_token_limit, - contextual, - history_limit, - rolling_history, - batch_translation, - batch_size - ], - outputs=[save_status] - ) - - # 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 - """) - - return app - - -def main(): - """Launch Gradio web app""" - print("🚀 Starting Glossarion Web Interface...") - - # Check if running on Hugging Face Spaces - is_spaces = os.getenv('HF_SPACES') == 'true' or os.getenv('Shirochi/Glossarion') is not None - if is_spaces: - print("🤗 Running on Hugging Face Spaces") - - 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__": +#!/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 + +# 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 +except ImportError as e: + MANGA_TRANSLATION_AVAILABLE = False + print(f"⚠️ Manga translation modules not found: {e}") + + +class GlossarionWeb: + """Web interface for Glossarion translator""" + + def __init__(self): + self.config_file = "config_web.json" + self.config = self.load_config() + # Decrypt API keys for use + if API_KEY_ENCRYPTION_AVAILABLE: + self.config = decrypt_config(self.config) + self.models = get_model_options() if TRANSLATION_AVAILABLE else ["gpt-4", "claude-3-5-sonnet"] + + # 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 <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n" + ), + "japanese": ( + "You are a professional Japanese 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 Japanese honorifics and respectful speech markers in romanized form, including but not limited to: -san, -sama, -chan, -kun, -dono, -sensei, -senpai, -kouhai. For archaic/classical Japanese honorific forms, preserve them as-is rather than converting to modern equivalents.\n" + "- Always localize Japanese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔術 = magic).\n" + "- When translating Japanese'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 while reflecting the Japanese pronoun's nuance (私/僕/俺/etc.) through speech patterns rather than the pronoun itself, and maintain natural English flow without overusing pronouns just because they're omitted in Japanese.\n" + "- All Japanese profanity must be translated to English profanity.\n" + "- Preserve original intent, and speech tone.\n" + "- Retain onomatopoeia in Romaji.\n" + "- Keep original Japanese 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 <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n" + ), + "chinese": ( + "You are a professional Chinese 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" + "- Always localize Chinese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔法 = magic).\n" + "- When translating Chinese's pronoun-dropping style, insert pronouns in English only where needed for clarity while maintaining natural English flow.\n" + "- All Chinese profanity must be translated to English profanity.\n" + "- Preserve original intent, and speech tone.\n" + "- Retain onomatopoeia in Pinyin.\n" + "- Keep original Chinese 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 <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n" + ), + "Manga_JP": ( + "You are a professional Japanese to English Manga translator.\n" + "You have both the image of the Manga panel and the extracted text to work with.\n" + "Output only English text while following these rules: \n\n" + + "VISUAL CONTEXT:\n" + "- Analyze the character's facial expressions and body language in the image.\n" + "- Consider the scene's mood and atmosphere.\n" + "- Note any action or movement depicted.\n" + "- Use visual cues to determine the appropriate tone and emotion.\n" + "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n" + + "DIALOGUE REQUIREMENTS:\n" + "- Match the translation tone to the character's expression.\n" + "- If a character looks angry, use appropriately intense language.\n" + "- If a character looks shy or embarrassed, reflect that in the translation.\n" + "- Keep speech patterns consistent with the character's appearance and demeanor.\n" + "- Retain honorifics and onomatopoeia in Romaji.\n\n" + + "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" + ), + "Manga_KR": ( + "You are a professional Korean to English Manhwa translator.\n" + "You have both the image of the Manhwa panel and the extracted text to work with.\n" + "Output only English text while following these rules: \n\n" + + "VISUAL CONTEXT:\n" + "- Analyze the character's facial expressions and body language in the image.\n" + "- Consider the scene's mood and atmosphere.\n" + "- Note any action or movement depicted.\n" + "- Use visual cues to determine the appropriate tone and emotion.\n" + "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n" + + "DIALOGUE REQUIREMENTS:\n" + "- Match the translation tone to the character's expression.\n" + "- If a character looks angry, use appropriately intense language.\n" + "- If a character looks shy or embarrassed, reflect that in the translation.\n" + "- Keep speech patterns consistent with the character's appearance and demeanor.\n" + "- Retain honorifics and onomatopoeia in Romaji.\n\n" + + "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" + ), + "Manga_CN": ( + "You are a professional Chinese to English Manga translator.\n" + "You have both the image of the Manga panel and the extracted text to work with.\n" + "Output only English text while following these rules: \n\n" + + "VISUAL CONTEXT:\n" + "- Analyze the character's facial expressions and body language in the image.\n" + "- Consider the scene's mood and atmosphere.\n" + "- Note any action or movement depicted.\n" + "- Use visual cues to determine the appropriate tone and emotion.\n" + "- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n" + + "DIALOGUE REQUIREMENTS:\n" + "- Match the translation tone to the character's expression.\n" + "- If a character looks angry, use appropriately intense language.\n" + "- If a character looks shy or embarrassed, reflect that in the translation.\n" + "- Keep speech patterns consistent with the character's appearance and demeanor.\n" + "- Retain honorifics and onomatopoeia in Romaji.\n\n" + + "IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n" + ), + "Original": "Return everything exactly as seen on the source." + } + + # Load profiles from config, fallback to defaults + self.profiles = self.config.get('prompt_profiles', self.default_prompts.copy()) + if not self.profiles: + self.profiles = self.default_prompts.copy() + + def load_config(self): + """Load configuration""" + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Warning: Failed to load config: {e}") + return {} + + def save_config(self, config): + """Save configuration with encryption""" + try: + # Encrypt sensitive fields before saving + encrypted_config = config.copy() + if API_KEY_ENCRYPTION_AVAILABLE: + encrypted_config = encrypt_config(encrypted_config) + + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(encrypted_config, f, ensure_ascii=False, indent=2) + # Reload config to ensure consistency and decrypt it + self.config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + self.config = decrypt_config(self.config) + except Exception as e: + return f"❌ Failed to save config: {e}" + return "✅ Configuration saved" + + def translate_epub( + self, + epub_file, + model, + api_key, + profile_name, + system_prompt, + temperature, + max_tokens, + glossary_file=None, + progress=gr.Progress() + ): + """Translate EPUB file""" + + if not TRANSLATION_AVAILABLE: + return None, "❌ Translation modules not loaded" + + if not epub_file: + return None, "❌ Please upload an EPUB file" + + if not api_key: + return None, "❌ Please provide an API key" + + if not profile_name: + return None, "❌ Please select a translation profile" + + try: + # Progress tracking + progress(0, desc="Starting translation...") + + # Save uploaded file to temp location if needed + input_path = epub_file.name if hasattr(epub_file, 'name') else epub_file + epub_base = os.path.splitext(os.path.basename(input_path))[0] + + # Use the provided system prompt (user may have edited it) + translation_prompt = system_prompt if system_prompt else self.profiles.get(profile_name, "") + + # Set the input path as a command line argument simulation + # TransateKRtoEN.main() reads from sys.argv if config doesn't have it + import sys + original_argv = sys.argv.copy() + sys.argv = ['glossarion_web.py', input_path] + + # Set environment variables for TransateKRtoEN.main() + os.environ['INPUT_PATH'] = input_path + os.environ['MODEL'] = model + os.environ['TRANSLATION_TEMPERATURE'] = str(temperature) + os.environ['MAX_OUTPUT_TOKENS'] = str(max_tokens) + + # Set API key environment variable + if 'gpt' in model.lower() or 'openai' in model.lower(): + os.environ['OPENAI_API_KEY'] = api_key + os.environ['API_KEY'] = api_key + elif 'claude' in model.lower(): + os.environ['ANTHROPIC_API_KEY'] = api_key + os.environ['API_KEY'] = api_key + elif 'gemini' in model.lower(): + os.environ['GOOGLE_API_KEY'] = api_key + os.environ['API_KEY'] = api_key + else: + os.environ['API_KEY'] = api_key + + # Set the system prompt + if translation_prompt: + # Save to temp profile + temp_config = self.config.copy() + temp_config['prompt_profiles'] = temp_config.get('prompt_profiles', {}) + temp_config['prompt_profiles'][profile_name] = translation_prompt + temp_config['active_profile'] = profile_name + + # Save temporarily + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(temp_config, f, ensure_ascii=False, indent=2) + + progress(0.1, desc="Initializing translation...") + + # Create a thread-safe queue for capturing logs + import queue + import threading + log_queue = queue.Queue() + last_log = "" + + def log_callback(msg): + """Capture log messages without recursion""" + nonlocal last_log + if msg and msg.strip(): + last_log = msg.strip() + log_queue.put(msg.strip()) + + # Monitor logs in a separate thread + def update_progress(): + while True: + try: + msg = log_queue.get(timeout=0.5) + # Extract progress if available + if '✅' in msg or '✓' in msg: + progress(0.5, desc=msg[:100]) # Limit message length + elif '🔄' in msg or 'Translating' in msg: + progress(0.3, desc=msg[:100]) + else: + progress(0.2, desc=msg[:100]) + except queue.Empty: + if last_log: + progress(0.2, desc=last_log[:100]) + continue + except: + break + + progress_thread = threading.Thread(target=update_progress, daemon=True) + progress_thread.start() + + # Call translation function (it reads from environment and config) + try: + result = TransateKRtoEN.main( + log_callback=log_callback, + stop_callback=None + ) + finally: + # Restore original sys.argv + sys.argv = original_argv + # Stop progress thread + log_queue.put(None) + + progress(1.0, desc="Translation complete!") + + # Check for output EPUB in the output directory + output_dir = epub_base + if os.path.exists(output_dir): + # Look for compiled EPUB + compiled_epub = os.path.join(output_dir, f"{epub_base}_translated.epub") + if os.path.exists(compiled_epub): + return compiled_epub, f"✅ Translation successful!\n\nTranslated: {os.path.basename(compiled_epub)}" + + return None, "❌ Translation failed - output file not created" + + except Exception as e: + import traceback + error_msg = f"❌ Error during translation:\n{str(e)}\n\n{traceback.format_exc()}" + return None, error_msg + + def extract_glossary( + self, + epub_file, + model, + api_key, + min_frequency, + max_names, + progress=gr.Progress() + ): + """Extract glossary from EPUB""" + + if not epub_file: + return None, "❌ Please upload an EPUB file" + + try: + import extract_glossary_from_epub + + progress(0, desc="Starting glossary extraction...") + + input_path = epub_file.name + output_path = input_path.replace('.epub', '_glossary.csv') + + # Set API key + if 'gpt' in model.lower(): + os.environ['OPENAI_API_KEY'] = api_key + elif 'claude' in model.lower(): + os.environ['ANTHROPIC_API_KEY'] = api_key + + progress(0.2, desc="Extracting text...") + + # Set environment variables for glossary extraction + os.environ['MODEL'] = model + os.environ['GLOSSARY_MIN_FREQUENCY'] = str(min_frequency) + os.environ['GLOSSARY_MAX_NAMES'] = str(max_names) + + # Call with proper arguments (check the actual signature) + result = extract_glossary_from_epub.main( + log_callback=None, + stop_callback=None + ) + + progress(1.0, desc="Glossary extraction complete!") + + if os.path.exists(output_path): + return output_path, f"✅ Glossary extracted!\n\nSaved to: {os.path.basename(output_path)}" + else: + return None, "❌ Glossary extraction failed" + + except Exception as e: + return None, f"❌ Error: {str(e)}" + + 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) updates""" + + if not MANGA_TRANSLATION_AVAILABLE: + yield "❌ Manga translation modules not loaded", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True) + return + + if not image_files: + yield "❌ Please upload at least one image", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True) + return + + if not api_key: + yield "❌ Please provide an API key", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True) + return + + if ocr_provider == "google": + # Check if credentials are provided or saved in config + if not google_creds_path and not self.config.get('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) + 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) + return + + try: + + # 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.config.get('google_vision_credentials'): + # Use saved credentials from config + creds_path = self.config.get('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) + 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): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + text_rgb = hex_to_rgb(text_color) + shadow_rgb = hex_to_rgb(shadow_color) + + 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_onnx) + if 'local_method' not in merged_config['manga_settings']['inpainting']: + merged_config['manga_settings']['inpainting']['local_method'] = self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx') + + # Set parallel panel translation settings from UI parameters + merged_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_panel_translation + merged_config['manga_settings']['advanced']['panel_max_workers'] = int(panel_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 + self.delay_entry = MockVar(float(config.get('delay', 2.0))) + 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))) + # 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) + + # 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_onnx') + # 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.config.get(model_path_key, '') + merged_config[model_path_key] = default_model_path + print(f"Set {model_path_key} to: {default_model_path}") + + # 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 + ) + 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) + 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 + ) + + # 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) + return + + # Process each image with real progress tracking + for idx, img_file in enumerate(files_to_process, 1): + try: + # 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 + 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) + + # 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 + if should_yield_logs(): + 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) + + # 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 completed image + yield "\n".join(translation_logs), gr.update(value=final_output, visible=True), gr.update(visible=False), gr.update(visible=False) + 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 + yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) + 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 + yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) + + # 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 + + # 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 + final_status_lines = [] + if translated_files: + final_status_lines.append(f"✅ Successfully translated {len(translated_files)}/{total_images} image(s)!") + if cbz_mode and cbz_output_path: + final_status_lines.append(f"\n📦 CBZ Output: {cbz_output_path}") + else: + final_status_lines.append(f"\nOutput directory: {output_dir}") + 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) + if translated_files: + # If CBZ mode, show CBZ file for download; otherwise show first image + if cbz_mode and cbz_output_path and os.path.exists(cbz_output_path): + yield ( + "\n".join(translation_logs), + gr.update(value=translated_files[0], visible=True), + gr.update(value=cbz_output_path, visible=True), # CBZ file for download with visibility + gr.update(value=final_status_text, visible=True) + ) + else: + yield ( + "\n".join(translation_logs), + gr.update(value=translated_files[0], visible=True), + gr.update(visible=False), # Hide CBZ component + gr.update(value=final_status_text, visible=True) + ) + 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) + ) + + except Exception as e: + import traceback + error_msg = f"❌ Error during manga translation:\n{str(e)}\n\n{traceback.format_exc()}" + yield error_msg, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_msg, visible=True) + + def create_interface(self): + """Create Gradio interface""" + + # Load and encode icon as base64 + icon_base64 = "" + icon_path = "Halgakos.png" if os.path.exists("Halgakos.png") 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;} + """ + + 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'<img src="data:image/png;base64,{icon_base64}" alt="Glossarion">' if icon_base64 else '' + + gr.HTML(f""" + <link rel="icon" type="image/x-icon" href="file/Halgakos.ico"> + <link rel="shortcut icon" type="image/x-icon" href="file/Halgakos.ico"> + <style> + .title-with-icon {{ + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; + }} + .title-with-icon img {{ + width: 48px; + height: 48px; + }} + </style> + <div class="title-with-icon"> + {icon_img_tag} + <h1>Glossarion - AI-Powered Translation</h1> + </div> + """) + + gr.Markdown(""" + Translate novels and books using advanced AI models (GPT-5, Claude, etc.) + """) + + with gr.Tabs(): + # Manga Translation Tab - DEFAULT/FIRST + with gr.Tab("🎨 Manga Translation"): + with gr.Row(): + with gr.Column(): + manga_images = gr.File( + label="🖼️ Upload Manga Images or CBZ", + file_types=[".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".cbz", ".zip"], + file_count="multiple" + ) + + translate_manga_btn = gr.Button( + "🚀 Translate Manga", + variant="primary", + size="lg" + ) + + manga_model = gr.Dropdown( + choices=self.models, + value=self.config.get('model', 'gpt-4-turbo'), + label="🤖 AI Model" + ) + + manga_api_key = gr.Textbox( + label="🔑 API Key", + type="password", + placeholder="Enter your API key", + value=self.config.get('api_key', '') # Pre-fill from config + ) + + # Filter manga-specific profiles + manga_profile_choices = [k for k in self.profiles.keys() if k.startswith('Manga_')] + if not manga_profile_choices: + manga_profile_choices = list(self.profiles.keys()) # Fallback to all + + default_manga_profile = "Manga_JP" if "Manga_JP" in self.profiles else manga_profile_choices[0] if manga_profile_choices else "" + + manga_profile = gr.Dropdown( + choices=manga_profile_choices, + value=default_manga_profile, + label="📝 Translation Profile" + ) + + # Editable manga system prompt + manga_system_prompt = gr.Textbox( + label="Manga System Prompt (Translation Instructions)", + lines=8, + max_lines=15, + interactive=True, + placeholder="Select a manga profile to load translation instructions...", + value=self.profiles.get(default_manga_profile, '') if default_manga_profile else '' + ) + + with gr.Accordion("⚙️ OCR Settings", open=False): + gr.Markdown("🔒 **Credentials are auto-saved** to your config (encrypted) after first use.") + + ocr_provider = gr.Radio( + choices=["google", "azure", "custom-api"], + value=self.config.get('ocr_provider', 'custom-api'), + label="OCR Provider" + ) + + # Show saved Google credentials path if available + saved_google_path = self.config.get('google_vision_credentials', '') + if saved_google_path and os.path.exists(saved_google_path): + gr.Markdown(f"✅ **Saved credentials found:** `{os.path.basename(saved_google_path)}`") + gr.Markdown("💡 *Using saved credentials. Upload a new file only if you want to change them.*") + else: + gr.Markdown("⚠️ No saved Google credentials found. Please upload your JSON file.") + + # Note: File component doesn't support pre-filling paths due to browser security + google_creds = gr.File( + label="Google Cloud Credentials JSON (upload to update)", + file_types=[".json"] + ) + + azure_key = gr.Textbox( + label="Azure Vision API Key (if using Azure)", + type="password", + placeholder="Enter Azure API key", + value=self.config.get('azure_vision_key', '') + ) + + azure_endpoint = gr.Textbox( + label="Azure Vision Endpoint (if using Azure)", + placeholder="https://your-resource.cognitiveservices.azure.com/", + value=self.config.get('azure_vision_endpoint', '') + ) + + bubble_detection = gr.Checkbox( + label="Enable Bubble Detection", + value=self.config.get('bubble_detection_enabled', True) + ) + + inpainting = gr.Checkbox( + label="Enable Text Removal (Inpainting)", + value=self.config.get('inpainting_enabled', True) + ) + + with gr.Accordion("✨ Text Visibility Settings", open=False): + gr.Markdown("### Font Settings") + + font_size_mode = gr.Radio( + choices=["auto", "fixed", "multiplier"], + value=self.config.get('manga_font_size_mode', 'auto'), + label="Font Size Mode" + ) + + font_size = gr.Slider( + minimum=0, + maximum=72, + value=self.config.get('manga_font_size', 24), + step=1, + label="Fixed Font Size (0=auto, used when mode=fixed)" + ) + + font_multiplier = gr.Slider( + minimum=0.5, + maximum=2.0, + value=self.config.get('manga_font_size_multiplier', 1.0), + step=0.1, + label="Font Size Multiplier (when mode=multiplier)" + ) + + min_font_size = gr.Slider( + minimum=0, + maximum=100, + value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_min_size', 12), + step=1, + label="Minimum Font Size (0=no limit)" + ) + + max_font_size = gr.Slider( + minimum=20, + maximum=100, + value=self.config.get('manga_max_font_size', 48), + step=1, + label="Maximum Font Size" + ) + + gr.Markdown("### Text Color") + + text_color_rgb = gr.ColorPicker( + label="Font Color", + value="#000000" # Default black + ) + + gr.Markdown("### Shadow Settings") + + shadow_enabled = gr.Checkbox( + label="Enable Text Shadow", + value=self.config.get('manga_shadow_enabled', True) + ) + + shadow_color = gr.ColorPicker( + label="Shadow Color", + value="#FFFFFF" # Default white + ) + + shadow_offset_x = gr.Slider( + minimum=-10, + maximum=10, + value=self.config.get('manga_shadow_offset_x', 2), + step=1, + label="Shadow Offset X" + ) + + shadow_offset_y = gr.Slider( + minimum=-10, + maximum=10, + value=self.config.get('manga_shadow_offset_y', 2), + step=1, + label="Shadow Offset Y" + ) + + shadow_blur = gr.Slider( + minimum=0, + maximum=10, + value=self.config.get('manga_shadow_blur', 0), + step=1, + label="Shadow Blur" + ) + + gr.Markdown("### Background Settings") + + bg_opacity = gr.Slider( + minimum=0, + maximum=255, + value=self.config.get('manga_bg_opacity', 130), + step=1, + label="Background Opacity" + ) + + bg_style = gr.Radio( + choices=["box", "circle", "wrap"], + value=self.config.get('manga_bg_style', 'circle'), + label="Background Style" + ) + + # Hidden parallel processing controls (managed in Manga Settings tab) + parallel_panel_translation = gr.Checkbox( + label="Parallel Panel Translation", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False), + visible=False + ) + + panel_max_workers = gr.Slider( + minimum=1, + maximum=20, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10), + step=1, + label="Panel Max Workers", + visible=False + ) + + with gr.Column(): + # Add logo and loading message 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 + ) + status_message = gr.Markdown( + value="### Ready to translate\nUpload an image and click 'Translate Manga' to begin.", + visible=True + ) + manga_logs = gr.Textbox( + label="📋 Translation Logs", + lines=20, + max_lines=30, + value="Ready to translate. Click 'Translate Manga' to begin.", + visible=True, + interactive=False + ) + + manga_output_image = gr.Image(label="📷 Translated Image Preview", visible=False) + manga_cbz_output = gr.File(label="📦 Download Translated CBZ", visible=False) + manga_status = gr.Textbox( + label="Final Status", + lines=8, + max_lines=15, + visible=False + ) + + # Auto-save model and API key + def save_manga_credentials(model, api_key): + """Save model and API key to config""" + try: + current_config = self.load_config() + current_config['model'] = model + if api_key: # Only save if not empty + current_config['api_key'] = api_key + self.save_config(current_config) + return None # No output needed + except Exception as e: + print(f"Failed to save manga credentials: {e}") + return None + + # Update manga system prompt when profile changes + def update_manga_system_prompt(profile_name): + return self.profiles.get(profile_name, "") + + # Auto-save on model change + manga_model.change( + fn=lambda m, k: save_manga_credentials(m, k), + inputs=[manga_model, manga_api_key], + outputs=None + ) + + # Auto-save on API key change + manga_api_key.change( + fn=lambda m, k: save_manga_credentials(m, k), + inputs=[manga_model, manga_api_key], + outputs=None + ) + + # Auto-save Azure credentials on change + def save_azure_credentials(key, endpoint): + """Save Azure credentials to config""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + if key and key.strip(): + current_config['azure_vision_key'] = str(key).strip() + if endpoint and endpoint.strip(): + current_config['azure_vision_endpoint'] = str(endpoint).strip() + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save Azure credentials: {e}") + return None + + azure_key.change( + fn=lambda k, e: save_azure_credentials(k, e), + inputs=[azure_key, azure_endpoint], + outputs=None + ) + + azure_endpoint.change( + fn=lambda k, e: save_azure_credentials(k, e), + inputs=[azure_key, azure_endpoint], + outputs=None + ) + + # Auto-save OCR provider on change + def save_ocr_provider(provider): + """Save OCR provider to config""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + current_config['ocr_provider'] = provider + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save OCR provider: {e}") + return None + + ocr_provider.change( + fn=save_ocr_provider, + inputs=[ocr_provider], + outputs=None + ) + + # Auto-save bubble detection and inpainting on change + def save_detection_settings(bubble_det, inpaint): + """Save bubble detection and inpainting settings""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + current_config['bubble_detection_enabled'] = bubble_det + current_config['inpainting_enabled'] = inpaint + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save detection settings: {e}") + return None + + bubble_detection.change( + fn=lambda b, i: save_detection_settings(b, i), + inputs=[bubble_detection, inpainting], + outputs=None + ) + + inpainting.change( + fn=lambda b, i: save_detection_settings(b, i), + inputs=[bubble_detection, inpainting], + outputs=None + ) + + # Auto-save font size mode on change + def save_font_mode(mode): + """Save font size mode to config""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + current_config['manga_font_size_mode'] = mode + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save font mode: {e}") + return None + + font_size_mode.change( + fn=save_font_mode, + inputs=[font_size_mode], + outputs=None + ) + + # Auto-save background style on change + def save_bg_style(style): + """Save background style to config""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + current_config['manga_bg_style'] = style + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save bg style: {e}") + return None + + bg_style.change( + fn=save_bg_style, + inputs=[bg_style], + outputs=None + ) + + manga_profile.change( + fn=update_manga_system_prompt, + inputs=[manga_profile], + outputs=[manga_system_prompt] + ) + + translate_manga_btn.click( + fn=self.translate_manga, + inputs=[ + manga_images, + manga_model, + manga_api_key, + manga_profile, + manga_system_prompt, + ocr_provider, + google_creds, + 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 + ], + outputs=[manga_logs, manga_output_image, manga_cbz_output, manga_status] + ) + + # Manga Settings Tab - NEW + with gr.Tab("🎬 Manga Settings"): + gr.Markdown("### Advanced Manga Translation Settings") + gr.Markdown("Configure bubble detection, inpainting, preprocessing, and rendering options.") + + with gr.Accordion("🕹️ Bubble Detection & Inpainting", open=True): + gr.Markdown("#### Bubble Detection") + + detector_type = gr.Radio( + choices=["rtdetr_onnx", "rtdetr", "yolo"], + value=self.config.get('manga_settings', {}).get('ocr', {}).get('detector_type', 'rtdetr_onnx'), + label="Detector Type", + interactive=True + ) + + rtdetr_confidence = gr.Slider( + minimum=0.0, + maximum=1.0, + value=self.config.get('manga_settings', {}).get('ocr', {}).get('rtdetr_confidence', 0.3), + step=0.05, + label="RT-DETR Confidence Threshold", + interactive=True + ) + + bubble_confidence = gr.Slider( + minimum=0.0, + maximum=1.0, + value=self.config.get('manga_settings', {}).get('ocr', {}).get('bubble_confidence', 0.3), + step=0.05, + label="YOLO Bubble Confidence Threshold", + interactive=True + ) + + detect_text_bubbles = gr.Checkbox( + label="Detect Text Bubbles", + value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_text_bubbles', True) + ) + + detect_empty_bubbles = gr.Checkbox( + label="Detect Empty Bubbles", + value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_empty_bubbles', True) + ) + + detect_free_text = gr.Checkbox( + label="Detect Free Text (outside bubbles)", + value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_free_text', True) + ) + + gr.Markdown("#### Inpainting") + + local_inpaint_method = gr.Radio( + choices=["anime_onnx", "anime", "lama", "lama_onnx", "aot", "aot_onnx"], + value=self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx'), + label="Local Inpainting Model", + interactive=True + ) + + with gr.Row(): + download_models_btn = gr.Button( + "📥 Download Models", + variant="secondary", + size="sm" + ) + load_models_btn = gr.Button( + "📂 Load Models", + variant="secondary", + size="sm" + ) + + gr.Markdown("#### Mask Dilation") + + auto_iterations = gr.Checkbox( + label="Auto Iterations (Recommended)", + value=self.config.get('manga_settings', {}).get('auto_iterations', True) + ) + + mask_dilation = gr.Slider( + minimum=0, + maximum=20, + value=self.config.get('manga_settings', {}).get('mask_dilation', 0), + step=1, + label="General Mask Dilation", + interactive=True + ) + + text_bubble_dilation = gr.Slider( + minimum=0, + maximum=20, + value=self.config.get('manga_settings', {}).get('text_bubble_dilation_iterations', 2), + step=1, + label="Text Bubble Dilation Iterations", + interactive=True + ) + + empty_bubble_dilation = gr.Slider( + minimum=0, + maximum=20, + value=self.config.get('manga_settings', {}).get('empty_bubble_dilation_iterations', 3), + step=1, + label="Empty Bubble Dilation Iterations", + interactive=True + ) + + free_text_dilation = gr.Slider( + minimum=0, + maximum=20, + value=self.config.get('manga_settings', {}).get('free_text_dilation_iterations', 3), + step=1, + label="Free Text Dilation Iterations", + interactive=True + ) + + with gr.Accordion("🖌️ Image Preprocessing", open=False): + preprocessing_enabled = gr.Checkbox( + label="Enable Preprocessing", + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enabled', False) + ) + + auto_detect_quality = gr.Checkbox( + label="Auto Detect Image Quality", + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('auto_detect_quality', True) + ) + + enhancement_strength = gr.Slider( + minimum=1.0, + maximum=3.0, + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enhancement_strength', 1.5), + step=0.1, + label="Enhancement Strength", + interactive=True + ) + + denoise_strength = gr.Slider( + minimum=0, + maximum=50, + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('denoise_strength', 10), + step=1, + label="Denoise Strength", + interactive=True + ) + + max_image_dimension = gr.Number( + label="Max Image Dimension (pixels)", + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('max_image_dimension', 2000), + minimum=500 + ) + + chunk_height = gr.Number( + label="Chunk Height for Large Images", + value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('chunk_height', 1000), + minimum=500 + ) + + gr.Markdown("#### HD Strategy for Inpainting") + gr.Markdown("*Controls how large images are processed during inpainting*") + + hd_strategy = gr.Radio( + choices=["original", "resize", "crop"], + value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy', 'resize'), + label="HD Strategy", + interactive=True, + info="original = legacy full-image; resize/crop = faster" + ) + + hd_strategy_resize_limit = gr.Slider( + minimum=512, + maximum=4096, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_resize_limit', 1536), + step=64, + label="Resize Limit (long edge, px)", + info="For resize strategy", + interactive=True + ) + + hd_strategy_crop_margin = gr.Slider( + minimum=0, + maximum=256, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_margin', 16), + step=2, + label="Crop Margin (px)", + info="For crop strategy", + interactive=True + ) + + hd_strategy_crop_trigger = gr.Slider( + minimum=256, + maximum=4096, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_trigger_size', 1024), + step=64, + label="Crop Trigger Size (px)", + info="Apply crop only if long edge exceeds this", + interactive=True + ) + + gr.Markdown("#### Image Tiling") + gr.Markdown("*Alternative tiling strategy (note: HD Strategy takes precedence)*") + + tiling_enabled = gr.Checkbox( + label="Enable Tiling", + value=self.config.get('manga_settings', {}).get('tiling', {}).get('enabled', False) + ) + + tiling_tile_size = gr.Slider( + minimum=256, + maximum=1024, + value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_size', 480), + step=64, + label="Tile Size (px)", + interactive=True + ) + + tiling_tile_overlap = gr.Slider( + minimum=0, + maximum=128, + value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_overlap', 64), + step=16, + label="Tile Overlap (px)", + interactive=True + ) + + with gr.Accordion("🎨 Font & Text Rendering", open=False): + gr.Markdown("#### Font Sizing Algorithm") + + font_algorithm = gr.Radio( + choices=["smart", "simple"], + value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('algorithm', 'smart'), + label="Font Sizing Algorithm", + interactive=True + ) + + prefer_larger = gr.Checkbox( + label="Prefer Larger Fonts", + value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('prefer_larger', True) + ) + + max_lines = gr.Slider( + minimum=1, + maximum=20, + value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('max_lines', 10), + step=1, + label="Maximum Lines Per Bubble", + interactive=True + ) + + line_spacing = gr.Slider( + minimum=0.5, + maximum=3.0, + value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('line_spacing', 1.3), + step=0.1, + label="Line Spacing Multiplier", + interactive=True + ) + + bubble_size_factor = gr.Checkbox( + label="Use Bubble Size Factor", + value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('bubble_size_factor', True) + ) + + auto_fit_style = gr.Radio( + choices=["balanced", "aggressive", "conservative"], + value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_fit_style', 'balanced'), + label="Auto Fit Style", + interactive=True + ) + + with gr.Accordion("⚙️ Advanced Options", open=False): + gr.Markdown("#### Format Detection") + + format_detection = gr.Checkbox( + label="Enable Format Detection (manga/webtoon)", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('format_detection', True) + ) + + webtoon_mode = gr.Radio( + choices=["auto", "force_manga", "force_webtoon"], + value=self.config.get('manga_settings', {}).get('advanced', {}).get('webtoon_mode', 'auto'), + label="Webtoon Mode", + interactive=True + ) + + gr.Markdown("#### Performance") + + parallel_processing = gr.Checkbox( + label="Enable Parallel Processing", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_processing', True) + ) + + max_workers = gr.Slider( + minimum=1, + maximum=8, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('max_workers', 2), + step=1, + label="Max Worker Threads", + interactive=True + ) + + parallel_panel_translation = gr.Checkbox( + label="Parallel Panel Translation", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False) + ) + + panel_max_workers = gr.Slider( + minimum=1, + maximum=20, + value=self.config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10), + step=1, + label="Panel Max Workers", + interactive=True + ) + + gr.Markdown("#### Model Optimization") + + torch_precision = gr.Radio( + choices=["fp32", "fp16"], + value=self.config.get('manga_settings', {}).get('advanced', {}).get('torch_precision', 'fp16'), + label="Torch Precision", + interactive=True + ) + + auto_cleanup_models = gr.Checkbox( + label="Auto Cleanup Models from Memory", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('auto_cleanup_models', False) + ) + + gr.Markdown("#### Debug Options") + + debug_mode = gr.Checkbox( + label="Enable Debug Mode", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('debug_mode', False) + ) + + save_intermediate = gr.Checkbox( + label="Save Intermediate Files", + value=self.config.get('manga_settings', {}).get('advanced', {}).get('save_intermediate', False) + ) + + concise_pipeline_logs = gr.Checkbox( + label="Concise Pipeline Logs", + value=self.config.get('concise_pipeline_logs', True) + ) + + # Button handlers for model management + def download_models_handler(detector_type_val, inpaint_method_val): + """Download selected models""" + messages = [] + + try: + # Download bubble detection model + if detector_type_val: + messages.append(f"📥 Downloading {detector_type_val} bubble detector...") + try: + from bubble_detector import BubbleDetector + bd = BubbleDetector() + + if detector_type_val == "rtdetr_onnx": + if bd.load_rtdetr_onnx_model(): + messages.append("✅ RT-DETR ONNX model downloaded successfully") + else: + messages.append("❌ Failed to download RT-DETR ONNX model") + elif detector_type_val == "rtdetr": + if bd.load_rtdetr_model(): + messages.append("✅ RT-DETR model downloaded successfully") + else: + messages.append("❌ Failed to download RT-DETR model") + elif detector_type_val == "yolo": + messages.append("ℹ️ YOLO models are downloaded automatically on first use") + except Exception as e: + messages.append(f"❌ Error downloading detector: {str(e)}") + + # Download inpainting model + if inpaint_method_val: + messages.append(f"\n📥 Downloading {inpaint_method_val} inpainting model...") + try: + from local_inpainter import LocalInpainter, LAMA_JIT_MODELS + + inpainter = LocalInpainter({}) + + # Map method names to download keys + method_map = { + 'anime_onnx': 'anime_onnx', + 'anime': 'anime', + 'lama': 'lama', + 'lama_onnx': 'lama_onnx', + 'aot': 'aot', + 'aot_onnx': 'aot_onnx' + } + + method_key = method_map.get(inpaint_method_val) + if method_key and method_key in LAMA_JIT_MODELS: + model_info = LAMA_JIT_MODELS[method_key] + messages.append(f"Downloading {model_info['name']}...") + + model_path = inpainter.download_jit_model(method_key) + if model_path: + messages.append(f"✅ {model_info['name']} downloaded to: {model_path}") + else: + messages.append(f"❌ Failed to download {model_info['name']}") + else: + messages.append(f"ℹ️ {inpaint_method_val} is downloaded automatically on first use") + + except Exception as e: + messages.append(f"❌ Error downloading inpainting model: {str(e)}") + + if not messages: + messages.append("ℹ️ No models selected for download") + + except Exception as e: + messages.append(f"❌ Error during download: {str(e)}") + + return gr.Info("\n".join(messages)) + + def load_models_handler(detector_type_val, inpaint_method_val): + """Load selected models into memory""" + messages = [] + + try: + # Load bubble detection model + if detector_type_val: + messages.append(f"📦 Loading {detector_type_val} bubble detector...") + try: + from bubble_detector import BubbleDetector + bd = BubbleDetector() + + if detector_type_val == "rtdetr_onnx": + if bd.load_rtdetr_onnx_model(): + messages.append("✅ RT-DETR ONNX model loaded successfully") + else: + messages.append("❌ Failed to load RT-DETR ONNX model") + elif detector_type_val == "rtdetr": + if bd.load_rtdetr_model(): + messages.append("✅ RT-DETR model loaded successfully") + else: + messages.append("❌ Failed to load RT-DETR model") + elif detector_type_val == "yolo": + messages.append("ℹ️ YOLO models are loaded automatically when needed") + except Exception as e: + messages.append(f"❌ Error loading detector: {str(e)}") + + # Load inpainting model + if inpaint_method_val: + messages.append(f"\n📦 Loading {inpaint_method_val} inpainting model...") + try: + from local_inpainter import LocalInpainter, LAMA_JIT_MODELS + import os + + inpainter = LocalInpainter({}) + + # Map method names to model keys + method_map = { + 'anime_onnx': 'anime_onnx', + 'anime': 'anime', + 'lama': 'lama', + 'lama_onnx': 'lama_onnx', + 'aot': 'aot', + 'aot_onnx': 'aot_onnx' + } + + method_key = method_map.get(inpaint_method_val) + if method_key: + # First check if model exists, download if not + if method_key in LAMA_JIT_MODELS: + model_info = LAMA_JIT_MODELS[method_key] + cache_dir = os.path.expanduser('~/.cache/inpainting') + model_filename = os.path.basename(model_info['url']) + model_path = os.path.join(cache_dir, model_filename) + + if not os.path.exists(model_path): + messages.append(f"Model not found, downloading first...") + model_path = inpainter.download_jit_model(method_key) + if not model_path: + messages.append(f"❌ Failed to download model") + return gr.Info("\n".join(messages)) + + # Now load the model + if inpainter.load_model(method_key, model_path): + messages.append(f"✅ {model_info['name']} loaded successfully") + else: + messages.append(f"❌ Failed to load {model_info['name']}") + else: + messages.append(f"ℹ️ {inpaint_method_val} will be loaded automatically when needed") + else: + messages.append(f"ℹ️ Unknown method: {inpaint_method_val}") + + except Exception as e: + messages.append(f"❌ Error loading inpainting model: {str(e)}") + + if not messages: + messages.append("ℹ️ No models selected for loading") + + except Exception as e: + messages.append(f"❌ Error during loading: {str(e)}") + + return gr.Info("\n".join(messages)) + + download_models_btn.click( + fn=download_models_handler, + inputs=[detector_type, local_inpaint_method], + outputs=None + ) + + load_models_btn.click( + fn=load_models_handler, + inputs=[detector_type, local_inpaint_method], + outputs=None + ) + + # Auto-save parallel panel translation settings + def save_parallel_settings(parallel_enabled, max_workers): + """Save parallel panel translation settings to config""" + try: + current_config = self.load_config() + if API_KEY_ENCRYPTION_AVAILABLE: + current_config = decrypt_config(current_config) + + # Initialize nested structure if not exists + if 'manga_settings' not in current_config: + current_config['manga_settings'] = {} + if 'advanced' not in current_config['manga_settings']: + current_config['manga_settings']['advanced'] = {} + + current_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_enabled + current_config['manga_settings']['advanced']['panel_max_workers'] = int(max_workers) + + self.save_config(current_config) + return None + except Exception as e: + print(f"Failed to save parallel panel settings: {e}") + return None + + parallel_panel_translation.change( + fn=lambda p, w: save_parallel_settings(p, w), + inputs=[parallel_panel_translation, panel_max_workers], + outputs=None + ) + + panel_max_workers.change( + fn=lambda p, w: save_parallel_settings(p, w), + inputs=[parallel_panel_translation, panel_max_workers], + outputs=None + ) + + gr.Markdown("\n---\n**Note:** These settings will be saved to your config and applied to all manga translations.") + + # Glossary Extraction Tab - TEMPORARILY HIDDEN + with gr.Tab("📝 Glossary Extraction", visible=False): + with gr.Row(): + with gr.Column(): + glossary_epub = gr.File( + label="📖 Upload EPUB File", + file_types=[".epub"] + ) + + glossary_model = gr.Dropdown( + choices=self.models, + value="gpt-4-turbo", + label="🤖 AI Model" + ) + + glossary_api_key = gr.Textbox( + label="🔑 API Key", + type="password", + placeholder="Enter API key" + ) + + min_freq = gr.Slider( + minimum=1, + maximum=10, + value=2, + step=1, + label="Minimum Frequency" + ) + + max_names_slider = gr.Slider( + minimum=10, + maximum=200, + value=50, + step=10, + label="Max Character Names" + ) + + extract_btn = gr.Button( + "🔍 Extract Glossary", + variant="primary" + ) + + with gr.Column(): + glossary_output = gr.File(label="📥 Download Glossary CSV") + glossary_status = gr.Textbox( + label="Status", + lines=10 + ) + + extract_btn.click( + fn=self.extract_glossary, + inputs=[ + glossary_epub, + glossary_model, + glossary_api_key, + min_freq, + max_names_slider + ], + outputs=[glossary_output, glossary_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.config.get('thread_submission_delay', 0.5), + step=0.1, + label="Threading delay (s)" + ) + + api_delay = gr.Slider( + minimum=0, + maximum=10, + value=self.config.get('delay', 2), + step=0.5, + label="API call delay (s)" + ) + + chapter_range = gr.Textbox( + label="Chapter range (e.g., 5-10)", + value=self.config.get('chapter_range', ''), + placeholder="Leave empty for all chapters" + ) + + token_limit = gr.Number( + label="Input Token limit", + value=self.config.get('token_limit', 200000), + minimum=0 + ) + + disable_token_limit = gr.Checkbox( + label="Disable Input Token Limit", + value=self.config.get('token_limit_disabled', False) + ) + + output_token_limit = gr.Number( + label="Output Token limit", + value=self.config.get('max_output_tokens', 16000), + minimum=0 + ) + + with gr.Column(): + contextual = gr.Checkbox( + label="Contextual Translation", + value=self.config.get('contextual', False) + ) + + history_limit = gr.Number( + label="Translation History Limit", + value=self.config.get('translation_history_limit', 2), + minimum=0 + ) + + rolling_history = gr.Checkbox( + label="Rolling History Window", + value=self.config.get('translation_history_rolling', False) + ) + + batch_translation = gr.Checkbox( + label="Batch Translation", + value=self.config.get('batch_translation', False) + ) + + batch_size = gr.Number( + label="Batch Size", + value=self.config.get('batch_size', 3), + minimum=1 + ) + + 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="Settings auto-save on change", interactive=False) + + def save_settings(save_key, t_delay, a_delay, ch_range, tok_limit, disable_tok_limit, out_tok_limit, ctx, hist_lim, roll_hist, batch, b_size): + """Auto-save settings when changed""" + try: + # Reload latest config first to avoid overwriting other changes + current_config = self.load_config() + + # Update only the fields we're managing + current_config.update({ + 'save_api_key': save_key, + 'thread_submission_delay': float(t_delay), + 'delay': float(a_delay), + 'chapter_range': str(ch_range), + 'token_limit': int(tok_limit) if tok_limit else 200000, + 'token_limit_disabled': bool(disable_tok_limit), + 'max_output_tokens': int(out_tok_limit) if out_tok_limit else 16000, + 'contextual': bool(ctx), + 'translation_history_limit': int(hist_lim) if hist_lim else 2, + 'translation_history_rolling': bool(roll_hist), + 'batch_translation': bool(batch), + 'batch_size': int(b_size) if b_size else 3 + }) + + # Save with the merged config + result = self.save_config(current_config) + return f"✅ {result}" + except Exception as e: + import traceback + error_trace = traceback.format_exc() + print(f"Settings save error:\n{error_trace}") + return f"❌ Save failed: {str(e)}" + + # Auto-save on any change + for component in [save_api_key, thread_delay, api_delay, chapter_range, token_limit, disable_token_limit, + output_token_limit, contextual, history_limit, rolling_history, batch_translation, batch_size]: + component.change( + fn=save_settings, + inputs=[ + save_api_key, + thread_delay, + api_delay, + chapter_range, + token_limit, + disable_token_limit, + output_token_limit, + contextual, + history_limit, + rolling_history, + batch_translation, + batch_size + ], + outputs=[save_status] + ) + + # 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 + """) + + return app + + +def main(): + """Launch Gradio web app""" + print("🚀 Starting Glossarion Web Interface...") + + # Check if running on Hugging Face Spaces + is_spaces = os.getenv('HF_SPACES') == 'true' or os.getenv('Shirochi/Glossarion') is not None + if is_spaces: + print("🤗 Running on Hugging Face Spaces") + + 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() \ No newline at end of file