#!/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__": main()