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
, ,
,
,
, ,
, 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 , ,
,
,
, ,
, 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 , ,
,
,
, ,
, 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'' if icon_base64 else ''
-
- gr.HTML(f"""
-
-
-
-
- {icon_img_tag}
-
Glossarion - AI-Powered Translation
-
- """)
-
- 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 , ,
,
,
, ,
, 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 , ,
,
,
, ,
, 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 , ,
,
,
, ,
, 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'' if icon_base64 else ''
+
+ gr.HTML(f"""
+
+
+
+
+ {icon_img_tag}
+
Glossarion - AI-Powered Translation
+
+ """)
+
+ 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