Glossarion / glossarion_web.py
Shirochi's picture
Update glossarion_web.py
fa7c5b7 verified
raw
history blame
122 kB
#!/usr/bin/env python3
"""
Glossarion Web - Gradio Web Interface
AI-powered translation in your browser
"""
import gradio as gr
import os
import sys
import json
import tempfile
import base64
from pathlib import Path
# Import API key encryption/decryption
try:
from api_key_encryption import APIKeyEncryption
API_KEY_ENCRYPTION_AVAILABLE = True
# Create web-specific encryption handler with its own key file
_web_encryption_handler = None
def get_web_encryption_handler():
global _web_encryption_handler
if _web_encryption_handler is None:
_web_encryption_handler = APIKeyEncryption()
# Use web-specific key file
from pathlib import Path
_web_encryption_handler.key_file = Path('.glossarion_web_key')
_web_encryption_handler.cipher = _web_encryption_handler._get_or_create_cipher()
# Add web-specific fields to encrypt
_web_encryption_handler.api_key_fields.extend([
'azure_vision_key',
'google_vision_credentials'
])
return _web_encryption_handler
def decrypt_config(config):
return get_web_encryption_handler().decrypt_config(config)
def encrypt_config(config):
return get_web_encryption_handler().encrypt_config(config)
except ImportError:
API_KEY_ENCRYPTION_AVAILABLE = False
def decrypt_config(config):
return config # Fallback: return config as-is
def encrypt_config(config):
return config # Fallback: return config as-is
# Import your existing translation modules
try:
import TransateKRtoEN
from model_options import get_model_options
TRANSLATION_AVAILABLE = True
except ImportError:
TRANSLATION_AVAILABLE = False
print("⚠️ Translation modules not found")
# Import manga translation modules
try:
from manga_translator import MangaTranslator
from unified_api_client import UnifiedClient
MANGA_TRANSLATION_AVAILABLE = True
except ImportError as e:
MANGA_TRANSLATION_AVAILABLE = False
print(f"⚠️ Manga translation modules not found: {e}")
class GlossarionWeb:
"""Web interface for Glossarion translator"""
def __init__(self):
self.config_file = "config_web.json"
self.config = self.load_config()
# Decrypt API keys for use
if API_KEY_ENCRYPTION_AVAILABLE:
self.config = decrypt_config(self.config)
self.models = get_model_options() if TRANSLATION_AVAILABLE else ["gpt-4", "claude-3-5-sonnet"]
# Default prompts from the GUI (same as translator_gui.py)
self.default_prompts = {
"korean": (
"You are a professional Korean to English novel translator, you must strictly output only English text and HTML tags while following these rules:\n"
"- Use a natural, comedy-friendly English translation style that captures both humor and readability without losing any original meaning.\n"
"- Include 100% of the source text - every word, phrase, and sentence must be fully translated without exception.\n"
"- Retain Korean honorifics and respectful speech markers in romanized form, including but not limited to: -nim, -ssi, -yang, -gun, -isiyeo, -hasoseo. For archaic/classical Korean honorific forms (like 이시여/isiyeo, 하소서/hasoseo), preserve them as-is rather than converting to modern equivalents.\n"
"- Always localize Korean terminology to proper English equivalents instead of literal translations (examples: 마왕 = Demon King; 마술 = magic).\n"
"- When translating Korean's pronoun-dropping style, insert pronouns in English only where needed for clarity: prioritize original pronouns as implied or according to the glossary, and only use they/them as a last resort, use I/me for first-person narration, and maintain natural English flow without overusing pronouns just because they're omitted in Korean.\n"
"- All Korean profanity must be translated to English profanity.\n"
"- Preserve original intent, and speech tone.\n"
"- Retain onomatopoeia in Romaji.\n"
"- Keep original Korean quotation marks (" ", ' ', 「」, 『』) as-is without converting to English quotes.\n"
"- Every Korean/Chinese/Japanese character must be converted to its English meaning. Examples: The character 생 means 'life/living', 활 means 'active', 관 means 'hall/building' - together 생활관 means Dormitory.\n"
"- Preserve ALL HTML tags exactly as they appear in the source, including <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n"
),
"japanese": (
"You are a professional Japanese to English novel translator, you must strictly output only English text and HTML tags while following these rules:\n"
"- Use a natural, comedy-friendly English translation style that captures both humor and readability without losing any original meaning.\n"
"- Include 100% of the source text - every word, phrase, and sentence must be fully translated without exception.\n"
"- Retain Japanese honorifics and respectful speech markers in romanized form, including but not limited to: -san, -sama, -chan, -kun, -dono, -sensei, -senpai, -kouhai. For archaic/classical Japanese honorific forms, preserve them as-is rather than converting to modern equivalents.\n"
"- Always localize Japanese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔術 = magic).\n"
"- When translating Japanese's pronoun-dropping style, insert pronouns in English only where needed for clarity: prioritize original pronouns as implied or according to the glossary, and only use they/them as a last resort, use I/me for first-person narration while reflecting the Japanese pronoun's nuance (私/僕/俺/etc.) through speech patterns rather than the pronoun itself, and maintain natural English flow without overusing pronouns just because they're omitted in Japanese.\n"
"- All Japanese profanity must be translated to English profanity.\n"
"- Preserve original intent, and speech tone.\n"
"- Retain onomatopoeia in Romaji.\n"
"- Keep original Japanese quotation marks (「」, 『』) as-is without converting to English quotes.\n"
"- Every Korean/Chinese/Japanese character must be converted to its English meaning. Examples: The character 生 means 'life/living', 活 means 'active', 館 means 'hall/building' - together 生活館 means Dormitory.\n"
"- Preserve ALL HTML tags exactly as they appear in the source, including <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n"
),
"chinese": (
"You are a professional Chinese to English novel translator, you must strictly output only English text and HTML tags while following these rules:\n"
"- Use a natural, comedy-friendly English translation style that captures both humor and readability without losing any original meaning.\n"
"- Include 100% of the source text - every word, phrase, and sentence must be fully translated without exception.\n"
"- Always localize Chinese terminology to proper English equivalents instead of literal translations (examples: 魔王 = Demon King; 魔法 = magic).\n"
"- When translating Chinese's pronoun-dropping style, insert pronouns in English only where needed for clarity while maintaining natural English flow.\n"
"- All Chinese profanity must be translated to English profanity.\n"
"- Preserve original intent, and speech tone.\n"
"- Retain onomatopoeia in Pinyin.\n"
"- Keep original Chinese quotation marks (「」, 『』) as-is without converting to English quotes.\n"
"- Every Korean/Chinese/Japanese character must be converted to its English meaning. Examples: The character 生 means 'life/living', 活 means 'active', 館 means 'hall/building' - together 生活館 means Dormitory.\n"
"- Preserve ALL HTML tags exactly as they appear in the source, including <head>, <title>, <h1>, <h2>, <p>, <br>, <div>, etc.\n"
),
"Manga_JP": (
"You are a professional Japanese to English Manga translator.\n"
"You have both the image of the Manga panel and the extracted text to work with.\n"
"Output only English text while following these rules: \n\n"
"VISUAL CONTEXT:\n"
"- Analyze the character's facial expressions and body language in the image.\n"
"- Consider the scene's mood and atmosphere.\n"
"- Note any action or movement depicted.\n"
"- Use visual cues to determine the appropriate tone and emotion.\n"
"- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n"
"DIALOGUE REQUIREMENTS:\n"
"- Match the translation tone to the character's expression.\n"
"- If a character looks angry, use appropriately intense language.\n"
"- If a character looks shy or embarrassed, reflect that in the translation.\n"
"- Keep speech patterns consistent with the character's appearance and demeanor.\n"
"- Retain honorifics and onomatopoeia in Romaji.\n\n"
"IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n"
),
"Manga_KR": (
"You are a professional Korean to English Manhwa translator.\n"
"You have both the image of the Manhwa panel and the extracted text to work with.\n"
"Output only English text while following these rules: \n\n"
"VISUAL CONTEXT:\n"
"- Analyze the character's facial expressions and body language in the image.\n"
"- Consider the scene's mood and atmosphere.\n"
"- Note any action or movement depicted.\n"
"- Use visual cues to determine the appropriate tone and emotion.\n"
"- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n\n"
"DIALOGUE REQUIREMENTS:\n"
"- Match the translation tone to the character's expression.\n"
"- If a character looks angry, use appropriately intense language.\n"
"- If a character looks shy or embarrassed, reflect that in the translation.\n"
"- Keep speech patterns consistent with the character's appearance and demeanor.\n"
"- Retain honorifics and onomatopoeia in Romaji.\n\n"
"IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n"
),
"Manga_CN": (
"You are a professional Chinese to English Manga translator.\n"
"You have both the image of the Manga panel and the extracted text to work with.\n"
"Output only English text while following these rules: \n\n"
"VISUAL CONTEXT:\n"
"- Analyze the character's facial expressions and body language in the image.\n"
"- Consider the scene's mood and atmosphere.\n"
"- Note any action or movement depicted.\n"
"- Use visual cues to determine the appropriate tone and emotion.\n"
"- USE THE IMAGE to inform your translation choices. The image is not decorative - it contains essential context for accurate translation.\n"
"DIALOGUE REQUIREMENTS:\n"
"- Match the translation tone to the character's expression.\n"
"- If a character looks angry, use appropriately intense language.\n"
"- If a character looks shy or embarrassed, reflect that in the translation.\n"
"- Keep speech patterns consistent with the character's appearance and demeanor.\n"
"- Retain honorifics and onomatopoeia in Romaji.\n\n"
"IMPORTANT: Use both the visual context and text to create the most accurate and natural-sounding translation.\n"
),
"Original": "Return everything exactly as seen on the source."
}
# Load profiles from config, fallback to defaults
self.profiles = self.config.get('prompt_profiles', self.default_prompts.copy())
if not self.profiles:
self.profiles = self.default_prompts.copy()
def load_config(self):
"""Load configuration"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Warning: Failed to load config: {e}")
return {}
def save_config(self, config):
"""Save configuration with encryption"""
try:
# Encrypt sensitive fields before saving
encrypted_config = config.copy()
if API_KEY_ENCRYPTION_AVAILABLE:
encrypted_config = encrypt_config(encrypted_config)
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(encrypted_config, f, ensure_ascii=False, indent=2)
# Reload config to ensure consistency and decrypt it
self.config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
self.config = decrypt_config(self.config)
except Exception as e:
return f"❌ Failed to save config: {e}"
return "✅ Configuration saved"
def translate_epub(
self,
epub_file,
model,
api_key,
profile_name,
system_prompt,
temperature,
max_tokens,
glossary_file=None,
progress=gr.Progress()
):
"""Translate EPUB file"""
if not TRANSLATION_AVAILABLE:
return None, "❌ Translation modules not loaded"
if not epub_file:
return None, "❌ Please upload an EPUB file"
if not api_key:
return None, "❌ Please provide an API key"
if not profile_name:
return None, "❌ Please select a translation profile"
try:
# Progress tracking
progress(0, desc="Starting translation...")
# Save uploaded file to temp location if needed
input_path = epub_file.name if hasattr(epub_file, 'name') else epub_file
epub_base = os.path.splitext(os.path.basename(input_path))[0]
# Use the provided system prompt (user may have edited it)
translation_prompt = system_prompt if system_prompt else self.profiles.get(profile_name, "")
# Set the input path as a command line argument simulation
# TransateKRtoEN.main() reads from sys.argv if config doesn't have it
import sys
original_argv = sys.argv.copy()
sys.argv = ['glossarion_web.py', input_path]
# Set environment variables for TransateKRtoEN.main()
os.environ['INPUT_PATH'] = input_path
os.environ['MODEL'] = model
os.environ['TRANSLATION_TEMPERATURE'] = str(temperature)
os.environ['MAX_OUTPUT_TOKENS'] = str(max_tokens)
# Set API key environment variable
if 'gpt' in model.lower() or 'openai' in model.lower():
os.environ['OPENAI_API_KEY'] = api_key
os.environ['API_KEY'] = api_key
elif 'claude' in model.lower():
os.environ['ANTHROPIC_API_KEY'] = api_key
os.environ['API_KEY'] = api_key
elif 'gemini' in model.lower():
os.environ['GOOGLE_API_KEY'] = api_key
os.environ['API_KEY'] = api_key
else:
os.environ['API_KEY'] = api_key
# Set the system prompt
if translation_prompt:
# Save to temp profile
temp_config = self.config.copy()
temp_config['prompt_profiles'] = temp_config.get('prompt_profiles', {})
temp_config['prompt_profiles'][profile_name] = translation_prompt
temp_config['active_profile'] = profile_name
# Save temporarily
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(temp_config, f, ensure_ascii=False, indent=2)
progress(0.1, desc="Initializing translation...")
# Create a thread-safe queue for capturing logs
import queue
import threading
log_queue = queue.Queue()
last_log = ""
def log_callback(msg):
"""Capture log messages without recursion"""
nonlocal last_log
if msg and msg.strip():
last_log = msg.strip()
log_queue.put(msg.strip())
# Monitor logs in a separate thread
def update_progress():
while True:
try:
msg = log_queue.get(timeout=0.5)
# Extract progress if available
if '✅' in msg or '✓' in msg:
progress(0.5, desc=msg[:100]) # Limit message length
elif '🔄' in msg or 'Translating' in msg:
progress(0.3, desc=msg[:100])
else:
progress(0.2, desc=msg[:100])
except queue.Empty:
if last_log:
progress(0.2, desc=last_log[:100])
continue
except:
break
progress_thread = threading.Thread(target=update_progress, daemon=True)
progress_thread.start()
# Call translation function (it reads from environment and config)
try:
result = TransateKRtoEN.main(
log_callback=log_callback,
stop_callback=None
)
finally:
# Restore original sys.argv
sys.argv = original_argv
# Stop progress thread
log_queue.put(None)
progress(1.0, desc="Translation complete!")
# Check for output EPUB in the output directory
output_dir = epub_base
if os.path.exists(output_dir):
# Look for compiled EPUB
compiled_epub = os.path.join(output_dir, f"{epub_base}_translated.epub")
if os.path.exists(compiled_epub):
return compiled_epub, f"✅ Translation successful!\n\nTranslated: {os.path.basename(compiled_epub)}"
return None, "❌ Translation failed - output file not created"
except Exception as e:
import traceback
error_msg = f"❌ Error during translation:\n{str(e)}\n\n{traceback.format_exc()}"
return None, error_msg
def extract_glossary(
self,
epub_file,
model,
api_key,
min_frequency,
max_names,
progress=gr.Progress()
):
"""Extract glossary from EPUB"""
if not epub_file:
return None, "❌ Please upload an EPUB file"
try:
import extract_glossary_from_epub
progress(0, desc="Starting glossary extraction...")
input_path = epub_file.name
output_path = input_path.replace('.epub', '_glossary.csv')
# Set API key
if 'gpt' in model.lower():
os.environ['OPENAI_API_KEY'] = api_key
elif 'claude' in model.lower():
os.environ['ANTHROPIC_API_KEY'] = api_key
progress(0.2, desc="Extracting text...")
# Set environment variables for glossary extraction
os.environ['MODEL'] = model
os.environ['GLOSSARY_MIN_FREQUENCY'] = str(min_frequency)
os.environ['GLOSSARY_MAX_NAMES'] = str(max_names)
# Call with proper arguments (check the actual signature)
result = extract_glossary_from_epub.main(
log_callback=None,
stop_callback=None
)
progress(1.0, desc="Glossary extraction complete!")
if os.path.exists(output_path):
return output_path, f"✅ Glossary extracted!\n\nSaved to: {os.path.basename(output_path)}"
else:
return None, "❌ Glossary extraction failed"
except Exception as e:
return None, f"❌ Error: {str(e)}"
def translate_manga(
self,
image_files,
model,
api_key,
profile_name,
system_prompt,
ocr_provider,
google_creds_path,
azure_key,
azure_endpoint,
enable_bubble_detection,
enable_inpainting,
font_size_mode,
font_size,
font_multiplier,
min_font_size,
max_font_size,
text_color,
shadow_enabled,
shadow_color,
shadow_offset_x,
shadow_offset_y,
shadow_blur,
bg_opacity,
bg_style,
parallel_panel_translation=False,
panel_max_workers=10
):
"""Translate manga images - GENERATOR that yields (logs, image, cbz_file, status) updates"""
if not MANGA_TRANSLATION_AVAILABLE:
yield "❌ Manga translation modules not loaded", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
if not image_files:
yield "❌ Please upload at least one image", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
if not api_key:
yield "❌ Please provide an API key", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
if ocr_provider == "google":
# Check if credentials are provided or saved in config
if not google_creds_path and not self.config.get('google_vision_credentials'):
yield "❌ Please provide Google Cloud credentials JSON file", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
if ocr_provider == "azure":
# Ensure azure credentials are strings
azure_key_str = str(azure_key) if azure_key else ''
azure_endpoint_str = str(azure_endpoint) if azure_endpoint else ''
if not azure_key_str.strip() or not azure_endpoint_str.strip():
yield "❌ Please provide Azure API key and endpoint", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
try:
# Set API key environment variable
if 'gpt' in model.lower() or 'openai' in model.lower():
os.environ['OPENAI_API_KEY'] = api_key
elif 'claude' in model.lower():
os.environ['ANTHROPIC_API_KEY'] = api_key
elif 'gemini' in model.lower():
os.environ['GOOGLE_API_KEY'] = api_key
# Set Google Cloud credentials if provided and save to config
if ocr_provider == "google":
if google_creds_path:
# New file provided - save it
creds_path = google_creds_path.name if hasattr(google_creds_path, 'name') else google_creds_path
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = creds_path
# Auto-save to config
self.config['google_vision_credentials'] = creds_path
self.save_config(self.config)
elif self.config.get('google_vision_credentials'):
# Use saved credentials from config
creds_path = self.config.get('google_vision_credentials')
if os.path.exists(creds_path):
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = creds_path
else:
yield f"❌ Saved Google credentials not found: {creds_path}", gr.update(visible=False), gr.update(visible=False), gr.update(value="❌ Error", visible=True)
return
# Set Azure credentials if provided and save to config
if ocr_provider == "azure":
# Convert to strings and strip whitespace
azure_key_str = str(azure_key).strip() if azure_key else ''
azure_endpoint_str = str(azure_endpoint).strip() if azure_endpoint else ''
os.environ['AZURE_VISION_KEY'] = azure_key_str
os.environ['AZURE_VISION_ENDPOINT'] = azure_endpoint_str
# Auto-save to config
self.config['azure_vision_key'] = azure_key_str
self.config['azure_vision_endpoint'] = azure_endpoint_str
self.save_config(self.config)
# Apply text visibility settings to config
# Convert hex color to RGB tuple
def hex_to_rgb(hex_color):
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
text_rgb = hex_to_rgb(text_color)
shadow_rgb = hex_to_rgb(shadow_color)
self.config['manga_font_size_mode'] = font_size_mode
self.config['manga_font_size'] = int(font_size)
self.config['manga_font_size_multiplier'] = float(font_multiplier)
self.config['manga_max_font_size'] = int(max_font_size)
self.config['manga_text_color'] = list(text_rgb)
self.config['manga_shadow_enabled'] = bool(shadow_enabled)
self.config['manga_shadow_color'] = list(shadow_rgb)
self.config['manga_shadow_offset_x'] = int(shadow_offset_x)
self.config['manga_shadow_offset_y'] = int(shadow_offset_y)
self.config['manga_shadow_blur'] = int(shadow_blur)
self.config['manga_bg_opacity'] = int(bg_opacity)
self.config['manga_bg_style'] = bg_style
# Also update nested manga_settings structure
if 'manga_settings' not in self.config:
self.config['manga_settings'] = {}
if 'rendering' not in self.config['manga_settings']:
self.config['manga_settings']['rendering'] = {}
if 'font_sizing' not in self.config['manga_settings']:
self.config['manga_settings']['font_sizing'] = {}
self.config['manga_settings']['rendering']['auto_min_size'] = int(min_font_size)
self.config['manga_settings']['font_sizing']['min_size'] = int(min_font_size)
self.config['manga_settings']['rendering']['auto_max_size'] = int(max_font_size)
self.config['manga_settings']['font_sizing']['max_size'] = int(max_font_size)
# Prepare output directory
output_dir = tempfile.mkdtemp(prefix="manga_translated_")
translated_files = []
cbz_mode = False
cbz_output_path = None
# Initialize translation logs early (needed for CBZ processing)
translation_logs = []
# Check if any file is a CBZ/ZIP archive
import zipfile
files_to_process = image_files if isinstance(image_files, list) else [image_files]
extracted_images = []
for file in files_to_process:
file_path = file.name if hasattr(file, 'name') else file
if file_path.lower().endswith(('.cbz', '.zip')):
# Extract CBZ
cbz_mode = True
translation_logs.append(f"📚 Extracting CBZ: {os.path.basename(file_path)}")
extract_dir = tempfile.mkdtemp(prefix="cbz_extract_")
try:
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
# Find all image files in extracted directory
import glob
for ext in ['*.png', '*.jpg', '*.jpeg', '*.webp', '*.bmp', '*.gif']:
extracted_images.extend(glob.glob(os.path.join(extract_dir, '**', ext), recursive=True))
# Sort naturally (by filename)
extracted_images.sort()
translation_logs.append(f"✅ Extracted {len(extracted_images)} images from CBZ")
# Prepare CBZ output path
cbz_output_path = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(file_path))[0]}_translated.cbz")
except Exception as e:
translation_logs.append(f"❌ Error extracting CBZ: {str(e)}")
else:
# Regular image file
extracted_images.append(file_path)
# Use extracted images if CBZ was processed, otherwise use original files
if extracted_images:
# Create mock file objects for extracted images
class MockFile:
def __init__(self, path):
self.name = path
files_to_process = [MockFile(img) for img in extracted_images]
total_images = len(files_to_process)
# Merge web app config with SimpleConfig for MangaTranslator
# This includes all the text visibility settings we just set
merged_config = self.config.copy()
# Override with web-specific settings
merged_config['model'] = model
merged_config['active_profile'] = profile_name
# Update manga_settings
if 'manga_settings' not in merged_config:
merged_config['manga_settings'] = {}
if 'ocr' not in merged_config['manga_settings']:
merged_config['manga_settings']['ocr'] = {}
if 'inpainting' not in merged_config['manga_settings']:
merged_config['manga_settings']['inpainting'] = {}
if 'advanced' not in merged_config['manga_settings']:
merged_config['manga_settings']['advanced'] = {}
merged_config['manga_settings']['ocr']['provider'] = ocr_provider
merged_config['manga_settings']['ocr']['bubble_detection_enabled'] = enable_bubble_detection
merged_config['manga_settings']['inpainting']['method'] = 'local' if enable_inpainting else 'none'
# Make sure local_method is set from config (defaults to anime_onnx)
if 'local_method' not in merged_config['manga_settings']['inpainting']:
merged_config['manga_settings']['inpainting']['local_method'] = self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx')
# Set parallel panel translation settings from UI parameters
merged_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_panel_translation
merged_config['manga_settings']['advanced']['panel_max_workers'] = int(panel_max_workers)
# CRITICAL: Set skip_inpainting flag to False when inpainting is enabled
merged_config['manga_skip_inpainting'] = not enable_inpainting
# Create a simple config object for MangaTranslator
class SimpleConfig:
def __init__(self, cfg):
self.config = cfg
def get(self, key, default=None):
return self.config.get(key, default)
# Create mock GUI object with necessary attributes
class MockGUI:
def __init__(self, config, profile_name, system_prompt, max_output_tokens, api_key, model):
self.config = config
# Add profile_var mock for MangaTranslator compatibility
class ProfileVar:
def __init__(self, profile):
self.profile = str(profile) if profile else ''
def get(self):
return self.profile
self.profile_var = ProfileVar(profile_name)
# Add prompt_profiles BOTH to config AND as attribute (manga_translator checks both)
if 'prompt_profiles' not in self.config:
self.config['prompt_profiles'] = {}
self.config['prompt_profiles'][profile_name] = system_prompt
# Also set as direct attribute for line 4653 check
self.prompt_profiles = self.config['prompt_profiles']
# Add max_output_tokens as direct attribute (line 299 check)
self.max_output_tokens = max_output_tokens
# Add mock GUI attributes that MangaTranslator expects
class MockVar:
def __init__(self, val):
# Ensure val is properly typed
self.val = val
def get(self):
return self.val
self.delay_entry = MockVar(float(config.get('delay', 2.0)))
self.trans_temp = MockVar(float(config.get('translation_temperature', 0.3)))
self.contextual_var = MockVar(bool(config.get('contextual', False)))
self.trans_history = MockVar(int(config.get('translation_history_limit', 2)))
self.translation_history_rolling_var = MockVar(bool(config.get('translation_history_rolling', False)))
self.token_limit_disabled = bool(config.get('token_limit_disabled', False))
# IMPORTANT: token_limit_entry must return STRING because manga_translator calls .strip() on it
self.token_limit_entry = MockVar(str(config.get('token_limit', 200000)))
# Add API key and model for custom-api OCR provider - ensure strings
self.api_key_entry = MockVar(str(api_key) if api_key else '')
self.model_var = MockVar(str(model) if model else '')
simple_config = SimpleConfig(merged_config)
# Get max_output_tokens from config or use from web app config
web_max_tokens = merged_config.get('max_output_tokens', 16000)
mock_gui = MockGUI(simple_config.config, profile_name, system_prompt, web_max_tokens, api_key, model)
# Ensure model path is in config for local inpainting
if enable_inpainting:
local_method = merged_config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx')
# Set the model path key that MangaTranslator expects
model_path_key = f'manga_{local_method}_model_path'
if model_path_key not in merged_config:
# Use default model path or empty string
default_model_path = self.config.get(model_path_key, '')
merged_config[model_path_key] = default_model_path
print(f"Set {model_path_key} to: {default_model_path}")
# Setup OCR configuration
ocr_config = {
'provider': ocr_provider
}
if ocr_provider == 'google':
ocr_config['google_credentials_path'] = google_creds_path.name if google_creds_path else None
elif ocr_provider == 'azure':
# Use string versions
azure_key_str = str(azure_key).strip() if azure_key else ''
azure_endpoint_str = str(azure_endpoint).strip() if azure_endpoint else ''
ocr_config['azure_key'] = azure_key_str
ocr_config['azure_endpoint'] = azure_endpoint_str
# Create UnifiedClient for translation API calls
try:
unified_client = UnifiedClient(
api_key=api_key,
model=model,
output_dir=output_dir
)
except Exception as e:
error_log = f"❌ Failed to initialize API client: {str(e)}"
yield error_log, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_log, visible=True)
return
# Log storage - will be yielded as live updates
last_yield_log_count = [0] # Track when we last yielded
last_yield_time = [0] # Track last yield time
# Track current image being processed
current_image_idx = [0]
import time
def should_yield_logs():
"""Check if we should yield log updates (every 2 logs or 1 second)"""
current_time = time.time()
log_count_diff = len(translation_logs) - last_yield_log_count[0]
time_diff = current_time - last_yield_time[0]
# Yield if 2+ new logs OR 1+ seconds passed
return log_count_diff >= 2 or time_diff >= 1.0
def capture_log(msg, level="info"):
"""Capture logs - caller will yield periodically"""
if msg and msg.strip():
log_msg = msg.strip()
translation_logs.append(log_msg)
# Initialize timing
last_yield_time[0] = time.time()
# Create MangaTranslator instance
try:
# Debug: Log inpainting config
inpaint_cfg = merged_config.get('manga_settings', {}).get('inpainting', {})
print(f"\n=== INPAINTING CONFIG DEBUG ===")
print(f"Inpainting enabled checkbox: {enable_inpainting}")
print(f"Inpainting method: {inpaint_cfg.get('method')}")
print(f"Local method: {inpaint_cfg.get('local_method')}")
print(f"Full inpainting config: {inpaint_cfg}")
print("=== END DEBUG ===\n")
translator = MangaTranslator(
ocr_config=ocr_config,
unified_client=unified_client,
main_gui=mock_gui,
log_callback=capture_log
)
# CRITICAL: Set skip_inpainting flag directly on translator instance
translator.skip_inpainting = not enable_inpainting
print(f"Set translator.skip_inpainting = {translator.skip_inpainting}")
# Explicitly initialize local inpainting if enabled
if enable_inpainting:
print(f"🎨 Initializing local inpainting...")
try:
# Force initialization of the inpainter
init_result = translator._initialize_local_inpainter()
if init_result:
print(f"✅ Local inpainter initialized successfully")
else:
print(f"⚠️ Local inpainter initialization returned False")
except Exception as init_error:
print(f"❌ Failed to initialize inpainter: {init_error}")
import traceback
traceback.print_exc()
except Exception as e:
import traceback
full_error = traceback.format_exc()
print(f"\n\n=== MANGA TRANSLATOR INIT ERROR ===")
print(full_error)
print(f"\nocr_config: {ocr_config}")
print(f"\nmock_gui.model_var.get(): {mock_gui.model_var.get()}")
print(f"\nmock_gui.api_key_entry.get(): {type(mock_gui.api_key_entry.get())}")
print("=== END ERROR ===")
error_log = f"❌ Failed to initialize manga translator: {str(e)}\n\nCheck console for full traceback"
yield error_log, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_log, visible=True)
return
# Process each image with real progress tracking
for idx, img_file in enumerate(files_to_process, 1):
try:
# Update current image index for log capture
current_image_idx[0] = idx
# Calculate progress range for this image
start_progress = (idx - 1) / total_images
end_progress = idx / total_images
input_path = img_file.name if hasattr(img_file, 'name') else img_file
output_path = os.path.join(output_dir, f"translated_{os.path.basename(input_path)}")
filename = os.path.basename(input_path)
# Log start of processing and YIELD update
start_msg = f"🎨 [{idx}/{total_images}] Starting: {filename}"
translation_logs.append(start_msg)
translation_logs.append(f"Image path: {input_path}")
translation_logs.append(f"Processing with OCR: {ocr_provider}, Model: {model}")
translation_logs.append("-" * 60)
# Yield initial log update
last_yield_log_count[0] = len(translation_logs)
last_yield_time[0] = time.time()
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
# Start processing in a thread so we can yield logs periodically
import threading
processing_complete = [False]
result_container = [None]
def process_wrapper():
result_container[0] = translator.process_image(
image_path=input_path,
output_path=output_path,
batch_index=idx,
batch_total=total_images
)
processing_complete[0] = True
# Start processing in background
process_thread = threading.Thread(target=process_wrapper, daemon=True)
process_thread.start()
# Poll for log updates while processing
while not processing_complete[0]:
time.sleep(0.5) # Check every 0.5 seconds
if should_yield_logs():
last_yield_log_count[0] = len(translation_logs)
last_yield_time[0] = time.time()
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
# Wait for thread to complete
process_thread.join(timeout=1)
result = result_container[0]
if result.get('success'):
# Use the output path from the result
final_output = result.get('output_path', output_path)
if os.path.exists(final_output):
translated_files.append(final_output)
translation_logs.append(f"✅ Image {idx}/{total_images} COMPLETE: {filename} | Total: {len(translated_files)}/{total_images} done")
translation_logs.append("")
# Yield progress update with completed image
yield "\n".join(translation_logs), gr.update(value=final_output, visible=True), gr.update(visible=False), gr.update(visible=False)
else:
translation_logs.append(f"⚠️ Image {idx}/{total_images}: Output file missing for {filename}")
translation_logs.append(f"⚠️ Warning: Output file not found for image {idx}")
translation_logs.append("")
# Yield progress update
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
else:
errors = result.get('errors', [])
error_msg = errors[0] if errors else 'Unknown error'
translation_logs.append(f"❌ Image {idx}/{total_images} FAILED: {error_msg[:50]}")
translation_logs.append(f"⚠️ Error on image {idx}: {error_msg}")
translation_logs.append("")
# Yield progress update
yield "\n".join(translation_logs), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
# If translation failed, save original with error overlay
from PIL import Image as PILImage, ImageDraw, ImageFont
img = PILImage.open(input_path)
draw = ImageDraw.Draw(img)
# Add error message
draw.text((10, 10), f"Translation Error: {error_msg[:50]}", fill="red")
img.save(output_path)
translated_files.append(output_path)
except Exception as e:
import traceback
error_trace = traceback.format_exc()
translation_logs.append(f"❌ Image {idx}/{total_images} ERROR: {str(e)[:60]}")
translation_logs.append(f"❌ Exception on image {idx}: {str(e)}")
print(f"Manga translation error for {input_path}:\n{error_trace}")
# Save original on error
try:
from PIL import Image as PILImage
img = PILImage.open(input_path)
img.save(output_path)
translated_files.append(output_path)
except:
pass
continue
# Add completion message
translation_logs.append("\n" + "="*60)
translation_logs.append(f"✅ ALL COMPLETE! Successfully translated {len(translated_files)}/{total_images} images")
translation_logs.append("="*60)
# If CBZ mode, compile translated images into CBZ archive
final_output_for_display = None
if cbz_mode and cbz_output_path and translated_files:
translation_logs.append("\n📦 Compiling translated images into CBZ archive...")
try:
with zipfile.ZipFile(cbz_output_path, 'w', zipfile.ZIP_DEFLATED) as cbz:
for img_path in translated_files:
# Preserve original filename structure
arcname = os.path.basename(img_path).replace("translated_", "")
cbz.write(img_path, arcname)
translation_logs.append(f"✅ CBZ archive created: {os.path.basename(cbz_output_path)}")
translation_logs.append(f"📁 Archive location: {cbz_output_path}")
final_output_for_display = cbz_output_path
except Exception as e:
translation_logs.append(f"❌ Error creating CBZ: {str(e)}")
# Build final status
final_status_lines = []
if translated_files:
final_status_lines.append(f"✅ Successfully translated {len(translated_files)}/{total_images} image(s)!")
if cbz_mode and cbz_output_path:
final_status_lines.append(f"\n📦 CBZ Output: {cbz_output_path}")
else:
final_status_lines.append(f"\nOutput directory: {output_dir}")
else:
final_status_lines.append("❌ Translation failed - no images were processed")
final_status_text = "\n".join(final_status_lines)
# Final yield with complete logs, image, CBZ, and final status
# Format: (logs_textbox, output_image, cbz_file, status_textbox)
if translated_files:
# If CBZ mode, show CBZ file for download; otherwise show first image
if cbz_mode and cbz_output_path and os.path.exists(cbz_output_path):
yield (
"\n".join(translation_logs),
gr.update(value=translated_files[0], visible=True),
gr.update(value=cbz_output_path, visible=True), # CBZ file for download with visibility
gr.update(value=final_status_text, visible=True)
)
else:
yield (
"\n".join(translation_logs),
gr.update(value=translated_files[0], visible=True),
gr.update(visible=False), # Hide CBZ component
gr.update(value=final_status_text, visible=True)
)
else:
yield (
"\n".join(translation_logs),
gr.update(visible=False),
gr.update(visible=False), # Hide CBZ component
gr.update(value=final_status_text, visible=True)
)
except Exception as e:
import traceback
error_msg = f"❌ Error during manga translation:\n{str(e)}\n\n{traceback.format_exc()}"
yield error_msg, gr.update(visible=False), gr.update(visible=False), gr.update(value=error_msg, visible=True)
def create_interface(self):
"""Create Gradio interface"""
# Load and encode icon as base64
icon_base64 = ""
icon_path = "Halgakos.png" if os.path.exists("Halgakos.png") else "Halgakos.ico"
if os.path.exists(icon_path):
with open(icon_path, "rb") as f:
icon_base64 = base64.b64encode(f.read()).decode()
# Custom CSS to hide Gradio footer and add favicon
custom_css = """
footer {display: none !important;}
.gradio-container {min-height: 100vh;}
"""
with gr.Blocks(
title="Glossarion - AI Translation",
theme=gr.themes.Soft(),
css=custom_css
) as app:
# Add custom HTML with favicon link and title with icon
icon_img_tag = f'<img src="data:image/png;base64,{icon_base64}" alt="Glossarion">' if icon_base64 else ''
gr.HTML(f"""
<link rel="icon" type="image/x-icon" href="file/Halgakos.ico">
<link rel="shortcut icon" type="image/x-icon" href="file/Halgakos.ico">
<style>
.title-with-icon {{
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}}
.title-with-icon img {{
width: 48px;
height: 48px;
}}
</style>
<div class="title-with-icon">
{icon_img_tag}
<h1>Glossarion - AI-Powered Translation</h1>
</div>
""")
gr.Markdown("""
Translate novels and books using advanced AI models (GPT-5, Claude, etc.)
""")
with gr.Tabs():
# Manga Translation Tab - DEFAULT/FIRST
with gr.Tab("🎨 Manga Translation"):
with gr.Row():
with gr.Column():
manga_images = gr.File(
label="🖼️ Upload Manga Images or CBZ",
file_types=[".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".cbz", ".zip"],
file_count="multiple"
)
translate_manga_btn = gr.Button(
"🚀 Translate Manga",
variant="primary",
size="lg"
)
manga_model = gr.Dropdown(
choices=self.models,
value=self.config.get('model', 'gpt-4-turbo'),
label="🤖 AI Model"
)
manga_api_key = gr.Textbox(
label="🔑 API Key",
type="password",
placeholder="Enter your API key",
value=self.config.get('api_key', '') # Pre-fill from config
)
# Filter manga-specific profiles
manga_profile_choices = [k for k in self.profiles.keys() if k.startswith('Manga_')]
if not manga_profile_choices:
manga_profile_choices = list(self.profiles.keys()) # Fallback to all
default_manga_profile = "Manga_JP" if "Manga_JP" in self.profiles else manga_profile_choices[0] if manga_profile_choices else ""
manga_profile = gr.Dropdown(
choices=manga_profile_choices,
value=default_manga_profile,
label="📝 Translation Profile"
)
# Editable manga system prompt
manga_system_prompt = gr.Textbox(
label="Manga System Prompt (Translation Instructions)",
lines=8,
max_lines=15,
interactive=True,
placeholder="Select a manga profile to load translation instructions...",
value=self.profiles.get(default_manga_profile, '') if default_manga_profile else ''
)
with gr.Accordion("⚙️ OCR Settings", open=False):
gr.Markdown("🔒 **Credentials are auto-saved** to your config (encrypted) after first use.")
ocr_provider = gr.Radio(
choices=["google", "azure", "custom-api"],
value=self.config.get('ocr_provider', 'custom-api'),
label="OCR Provider"
)
# Show saved Google credentials path if available
saved_google_path = self.config.get('google_vision_credentials', '')
if saved_google_path and os.path.exists(saved_google_path):
gr.Markdown(f"✅ **Saved credentials found:** `{os.path.basename(saved_google_path)}`")
gr.Markdown("💡 *Using saved credentials. Upload a new file only if you want to change them.*")
else:
gr.Markdown("⚠️ No saved Google credentials found. Please upload your JSON file.")
# Note: File component doesn't support pre-filling paths due to browser security
google_creds = gr.File(
label="Google Cloud Credentials JSON (upload to update)",
file_types=[".json"]
)
azure_key = gr.Textbox(
label="Azure Vision API Key (if using Azure)",
type="password",
placeholder="Enter Azure API key",
value=self.config.get('azure_vision_key', '')
)
azure_endpoint = gr.Textbox(
label="Azure Vision Endpoint (if using Azure)",
placeholder="https://your-resource.cognitiveservices.azure.com/",
value=self.config.get('azure_vision_endpoint', '')
)
bubble_detection = gr.Checkbox(
label="Enable Bubble Detection",
value=self.config.get('bubble_detection_enabled', True)
)
inpainting = gr.Checkbox(
label="Enable Text Removal (Inpainting)",
value=self.config.get('inpainting_enabled', True)
)
with gr.Accordion("✨ Text Visibility Settings", open=False):
gr.Markdown("### Font Settings")
font_size_mode = gr.Radio(
choices=["auto", "fixed", "multiplier"],
value=self.config.get('manga_font_size_mode', 'auto'),
label="Font Size Mode"
)
font_size = gr.Slider(
minimum=0,
maximum=72,
value=self.config.get('manga_font_size', 24),
step=1,
label="Fixed Font Size (0=auto, used when mode=fixed)"
)
font_multiplier = gr.Slider(
minimum=0.5,
maximum=2.0,
value=self.config.get('manga_font_size_multiplier', 1.0),
step=0.1,
label="Font Size Multiplier (when mode=multiplier)"
)
min_font_size = gr.Slider(
minimum=0,
maximum=100,
value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_min_size', 12),
step=1,
label="Minimum Font Size (0=no limit)"
)
max_font_size = gr.Slider(
minimum=20,
maximum=100,
value=self.config.get('manga_max_font_size', 48),
step=1,
label="Maximum Font Size"
)
gr.Markdown("### Text Color")
text_color_rgb = gr.ColorPicker(
label="Font Color",
value="#000000" # Default black
)
gr.Markdown("### Shadow Settings")
shadow_enabled = gr.Checkbox(
label="Enable Text Shadow",
value=self.config.get('manga_shadow_enabled', True)
)
shadow_color = gr.ColorPicker(
label="Shadow Color",
value="#FFFFFF" # Default white
)
shadow_offset_x = gr.Slider(
minimum=-10,
maximum=10,
value=self.config.get('manga_shadow_offset_x', 2),
step=1,
label="Shadow Offset X"
)
shadow_offset_y = gr.Slider(
minimum=-10,
maximum=10,
value=self.config.get('manga_shadow_offset_y', 2),
step=1,
label="Shadow Offset Y"
)
shadow_blur = gr.Slider(
minimum=0,
maximum=10,
value=self.config.get('manga_shadow_blur', 0),
step=1,
label="Shadow Blur"
)
gr.Markdown("### Background Settings")
bg_opacity = gr.Slider(
minimum=0,
maximum=255,
value=self.config.get('manga_bg_opacity', 130),
step=1,
label="Background Opacity"
)
bg_style = gr.Radio(
choices=["box", "circle", "wrap"],
value=self.config.get('manga_bg_style', 'circle'),
label="Background Style"
)
# Hidden parallel processing controls (managed in Manga Settings tab)
parallel_panel_translation = gr.Checkbox(
label="Parallel Panel Translation",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False),
visible=False
)
panel_max_workers = gr.Slider(
minimum=1,
maximum=20,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10),
step=1,
label="Panel Max Workers",
visible=False
)
with gr.Column():
# Add logo and loading message at top
with gr.Row():
gr.Image(
value="Halgakos.png",
label=None,
show_label=False,
width=80,
height=80,
interactive=False,
show_download_button=False,
container=False
)
status_message = gr.Markdown(
value="### Ready to translate\nUpload an image and click 'Translate Manga' to begin.",
visible=True
)
manga_logs = gr.Textbox(
label="📋 Translation Logs",
lines=20,
max_lines=30,
value="Ready to translate. Click 'Translate Manga' to begin.",
visible=True,
interactive=False
)
manga_output_image = gr.Image(label="📷 Translated Image Preview", visible=False)
manga_cbz_output = gr.File(label="📦 Download Translated CBZ", visible=False)
manga_status = gr.Textbox(
label="Final Status",
lines=8,
max_lines=15,
visible=False
)
# Auto-save model and API key
def save_manga_credentials(model, api_key):
"""Save model and API key to config"""
try:
current_config = self.load_config()
current_config['model'] = model
if api_key: # Only save if not empty
current_config['api_key'] = api_key
self.save_config(current_config)
return None # No output needed
except Exception as e:
print(f"Failed to save manga credentials: {e}")
return None
# Update manga system prompt when profile changes
def update_manga_system_prompt(profile_name):
return self.profiles.get(profile_name, "")
# Auto-save on model change
manga_model.change(
fn=lambda m, k: save_manga_credentials(m, k),
inputs=[manga_model, manga_api_key],
outputs=None
)
# Auto-save on API key change
manga_api_key.change(
fn=lambda m, k: save_manga_credentials(m, k),
inputs=[manga_model, manga_api_key],
outputs=None
)
# Auto-save Azure credentials on change
def save_azure_credentials(key, endpoint):
"""Save Azure credentials to config"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
if key and key.strip():
current_config['azure_vision_key'] = str(key).strip()
if endpoint and endpoint.strip():
current_config['azure_vision_endpoint'] = str(endpoint).strip()
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save Azure credentials: {e}")
return None
azure_key.change(
fn=lambda k, e: save_azure_credentials(k, e),
inputs=[azure_key, azure_endpoint],
outputs=None
)
azure_endpoint.change(
fn=lambda k, e: save_azure_credentials(k, e),
inputs=[azure_key, azure_endpoint],
outputs=None
)
# Auto-save OCR provider on change
def save_ocr_provider(provider):
"""Save OCR provider to config"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
current_config['ocr_provider'] = provider
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save OCR provider: {e}")
return None
ocr_provider.change(
fn=save_ocr_provider,
inputs=[ocr_provider],
outputs=None
)
# Auto-save bubble detection and inpainting on change
def save_detection_settings(bubble_det, inpaint):
"""Save bubble detection and inpainting settings"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
current_config['bubble_detection_enabled'] = bubble_det
current_config['inpainting_enabled'] = inpaint
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save detection settings: {e}")
return None
bubble_detection.change(
fn=lambda b, i: save_detection_settings(b, i),
inputs=[bubble_detection, inpainting],
outputs=None
)
inpainting.change(
fn=lambda b, i: save_detection_settings(b, i),
inputs=[bubble_detection, inpainting],
outputs=None
)
# Auto-save font size mode on change
def save_font_mode(mode):
"""Save font size mode to config"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
current_config['manga_font_size_mode'] = mode
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save font mode: {e}")
return None
font_size_mode.change(
fn=save_font_mode,
inputs=[font_size_mode],
outputs=None
)
# Auto-save background style on change
def save_bg_style(style):
"""Save background style to config"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
current_config['manga_bg_style'] = style
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save bg style: {e}")
return None
bg_style.change(
fn=save_bg_style,
inputs=[bg_style],
outputs=None
)
manga_profile.change(
fn=update_manga_system_prompt,
inputs=[manga_profile],
outputs=[manga_system_prompt]
)
translate_manga_btn.click(
fn=self.translate_manga,
inputs=[
manga_images,
manga_model,
manga_api_key,
manga_profile,
manga_system_prompt,
ocr_provider,
google_creds,
azure_key,
azure_endpoint,
bubble_detection,
inpainting,
font_size_mode,
font_size,
font_multiplier,
min_font_size,
max_font_size,
text_color_rgb,
shadow_enabled,
shadow_color,
shadow_offset_x,
shadow_offset_y,
shadow_blur,
bg_opacity,
bg_style,
parallel_panel_translation,
panel_max_workers
],
outputs=[manga_logs, manga_output_image, manga_cbz_output, manga_status]
)
# Manga Settings Tab - NEW
with gr.Tab("🎬 Manga Settings"):
gr.Markdown("### Advanced Manga Translation Settings")
gr.Markdown("Configure bubble detection, inpainting, preprocessing, and rendering options.")
with gr.Accordion("🕹️ Bubble Detection & Inpainting", open=True):
gr.Markdown("#### Bubble Detection")
detector_type = gr.Radio(
choices=["rtdetr_onnx", "rtdetr", "yolo"],
value=self.config.get('manga_settings', {}).get('ocr', {}).get('detector_type', 'rtdetr_onnx'),
label="Detector Type",
interactive=True
)
rtdetr_confidence = gr.Slider(
minimum=0.0,
maximum=1.0,
value=self.config.get('manga_settings', {}).get('ocr', {}).get('rtdetr_confidence', 0.3),
step=0.05,
label="RT-DETR Confidence Threshold",
interactive=True
)
bubble_confidence = gr.Slider(
minimum=0.0,
maximum=1.0,
value=self.config.get('manga_settings', {}).get('ocr', {}).get('bubble_confidence', 0.3),
step=0.05,
label="YOLO Bubble Confidence Threshold",
interactive=True
)
detect_text_bubbles = gr.Checkbox(
label="Detect Text Bubbles",
value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_text_bubbles', True)
)
detect_empty_bubbles = gr.Checkbox(
label="Detect Empty Bubbles",
value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_empty_bubbles', True)
)
detect_free_text = gr.Checkbox(
label="Detect Free Text (outside bubbles)",
value=self.config.get('manga_settings', {}).get('ocr', {}).get('detect_free_text', True)
)
gr.Markdown("#### Inpainting")
local_inpaint_method = gr.Radio(
choices=["anime_onnx", "anime", "lama", "lama_onnx", "aot", "aot_onnx"],
value=self.config.get('manga_settings', {}).get('inpainting', {}).get('local_method', 'anime_onnx'),
label="Local Inpainting Model",
interactive=True
)
with gr.Row():
download_models_btn = gr.Button(
"📥 Download Models",
variant="secondary",
size="sm"
)
load_models_btn = gr.Button(
"📂 Load Models",
variant="secondary",
size="sm"
)
gr.Markdown("#### Mask Dilation")
auto_iterations = gr.Checkbox(
label="Auto Iterations (Recommended)",
value=self.config.get('manga_settings', {}).get('auto_iterations', True)
)
mask_dilation = gr.Slider(
minimum=0,
maximum=20,
value=self.config.get('manga_settings', {}).get('mask_dilation', 0),
step=1,
label="General Mask Dilation",
interactive=True
)
text_bubble_dilation = gr.Slider(
minimum=0,
maximum=20,
value=self.config.get('manga_settings', {}).get('text_bubble_dilation_iterations', 2),
step=1,
label="Text Bubble Dilation Iterations",
interactive=True
)
empty_bubble_dilation = gr.Slider(
minimum=0,
maximum=20,
value=self.config.get('manga_settings', {}).get('empty_bubble_dilation_iterations', 3),
step=1,
label="Empty Bubble Dilation Iterations",
interactive=True
)
free_text_dilation = gr.Slider(
minimum=0,
maximum=20,
value=self.config.get('manga_settings', {}).get('free_text_dilation_iterations', 3),
step=1,
label="Free Text Dilation Iterations",
interactive=True
)
with gr.Accordion("🖌️ Image Preprocessing", open=False):
preprocessing_enabled = gr.Checkbox(
label="Enable Preprocessing",
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enabled', False)
)
auto_detect_quality = gr.Checkbox(
label="Auto Detect Image Quality",
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('auto_detect_quality', True)
)
enhancement_strength = gr.Slider(
minimum=1.0,
maximum=3.0,
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('enhancement_strength', 1.5),
step=0.1,
label="Enhancement Strength",
interactive=True
)
denoise_strength = gr.Slider(
minimum=0,
maximum=50,
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('denoise_strength', 10),
step=1,
label="Denoise Strength",
interactive=True
)
max_image_dimension = gr.Number(
label="Max Image Dimension (pixels)",
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('max_image_dimension', 2000),
minimum=500
)
chunk_height = gr.Number(
label="Chunk Height for Large Images",
value=self.config.get('manga_settings', {}).get('preprocessing', {}).get('chunk_height', 1000),
minimum=500
)
gr.Markdown("#### HD Strategy for Inpainting")
gr.Markdown("*Controls how large images are processed during inpainting*")
hd_strategy = gr.Radio(
choices=["original", "resize", "crop"],
value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy', 'resize'),
label="HD Strategy",
interactive=True,
info="original = legacy full-image; resize/crop = faster"
)
hd_strategy_resize_limit = gr.Slider(
minimum=512,
maximum=4096,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_resize_limit', 1536),
step=64,
label="Resize Limit (long edge, px)",
info="For resize strategy",
interactive=True
)
hd_strategy_crop_margin = gr.Slider(
minimum=0,
maximum=256,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_margin', 16),
step=2,
label="Crop Margin (px)",
info="For crop strategy",
interactive=True
)
hd_strategy_crop_trigger = gr.Slider(
minimum=256,
maximum=4096,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('hd_strategy_crop_trigger_size', 1024),
step=64,
label="Crop Trigger Size (px)",
info="Apply crop only if long edge exceeds this",
interactive=True
)
gr.Markdown("#### Image Tiling")
gr.Markdown("*Alternative tiling strategy (note: HD Strategy takes precedence)*")
tiling_enabled = gr.Checkbox(
label="Enable Tiling",
value=self.config.get('manga_settings', {}).get('tiling', {}).get('enabled', False)
)
tiling_tile_size = gr.Slider(
minimum=256,
maximum=1024,
value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_size', 480),
step=64,
label="Tile Size (px)",
interactive=True
)
tiling_tile_overlap = gr.Slider(
minimum=0,
maximum=128,
value=self.config.get('manga_settings', {}).get('tiling', {}).get('tile_overlap', 64),
step=16,
label="Tile Overlap (px)",
interactive=True
)
with gr.Accordion("🎨 Font & Text Rendering", open=False):
gr.Markdown("#### Font Sizing Algorithm")
font_algorithm = gr.Radio(
choices=["smart", "simple"],
value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('algorithm', 'smart'),
label="Font Sizing Algorithm",
interactive=True
)
prefer_larger = gr.Checkbox(
label="Prefer Larger Fonts",
value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('prefer_larger', True)
)
max_lines = gr.Slider(
minimum=1,
maximum=20,
value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('max_lines', 10),
step=1,
label="Maximum Lines Per Bubble",
interactive=True
)
line_spacing = gr.Slider(
minimum=0.5,
maximum=3.0,
value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('line_spacing', 1.3),
step=0.1,
label="Line Spacing Multiplier",
interactive=True
)
bubble_size_factor = gr.Checkbox(
label="Use Bubble Size Factor",
value=self.config.get('manga_settings', {}).get('font_sizing', {}).get('bubble_size_factor', True)
)
auto_fit_style = gr.Radio(
choices=["balanced", "aggressive", "conservative"],
value=self.config.get('manga_settings', {}).get('rendering', {}).get('auto_fit_style', 'balanced'),
label="Auto Fit Style",
interactive=True
)
with gr.Accordion("⚙️ Advanced Options", open=False):
gr.Markdown("#### Format Detection")
format_detection = gr.Checkbox(
label="Enable Format Detection (manga/webtoon)",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('format_detection', True)
)
webtoon_mode = gr.Radio(
choices=["auto", "force_manga", "force_webtoon"],
value=self.config.get('manga_settings', {}).get('advanced', {}).get('webtoon_mode', 'auto'),
label="Webtoon Mode",
interactive=True
)
gr.Markdown("#### Performance")
parallel_processing = gr.Checkbox(
label="Enable Parallel Processing",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_processing', True)
)
max_workers = gr.Slider(
minimum=1,
maximum=8,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('max_workers', 2),
step=1,
label="Max Worker Threads",
interactive=True
)
parallel_panel_translation = gr.Checkbox(
label="Parallel Panel Translation",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('parallel_panel_translation', False)
)
panel_max_workers = gr.Slider(
minimum=1,
maximum=20,
value=self.config.get('manga_settings', {}).get('advanced', {}).get('panel_max_workers', 10),
step=1,
label="Panel Max Workers",
interactive=True
)
gr.Markdown("#### Model Optimization")
torch_precision = gr.Radio(
choices=["fp32", "fp16"],
value=self.config.get('manga_settings', {}).get('advanced', {}).get('torch_precision', 'fp16'),
label="Torch Precision",
interactive=True
)
auto_cleanup_models = gr.Checkbox(
label="Auto Cleanup Models from Memory",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('auto_cleanup_models', False)
)
gr.Markdown("#### Debug Options")
debug_mode = gr.Checkbox(
label="Enable Debug Mode",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('debug_mode', False)
)
save_intermediate = gr.Checkbox(
label="Save Intermediate Files",
value=self.config.get('manga_settings', {}).get('advanced', {}).get('save_intermediate', False)
)
concise_pipeline_logs = gr.Checkbox(
label="Concise Pipeline Logs",
value=self.config.get('concise_pipeline_logs', True)
)
# Button handlers for model management
def download_models_handler(detector_type_val, inpaint_method_val):
"""Download selected models"""
messages = []
try:
# Download bubble detection model
if detector_type_val:
messages.append(f"📥 Downloading {detector_type_val} bubble detector...")
try:
from bubble_detector import BubbleDetector
bd = BubbleDetector()
if detector_type_val == "rtdetr_onnx":
if bd.load_rtdetr_onnx_model():
messages.append("✅ RT-DETR ONNX model downloaded successfully")
else:
messages.append("❌ Failed to download RT-DETR ONNX model")
elif detector_type_val == "rtdetr":
if bd.load_rtdetr_model():
messages.append("✅ RT-DETR model downloaded successfully")
else:
messages.append("❌ Failed to download RT-DETR model")
elif detector_type_val == "yolo":
messages.append("ℹ️ YOLO models are downloaded automatically on first use")
except Exception as e:
messages.append(f"❌ Error downloading detector: {str(e)}")
# Download inpainting model
if inpaint_method_val:
messages.append(f"\n📥 Downloading {inpaint_method_val} inpainting model...")
try:
from local_inpainter import LocalInpainter, LAMA_JIT_MODELS
inpainter = LocalInpainter({})
# Map method names to download keys
method_map = {
'anime_onnx': 'anime_onnx',
'anime': 'anime',
'lama': 'lama',
'lama_onnx': 'lama_onnx',
'aot': 'aot',
'aot_onnx': 'aot_onnx'
}
method_key = method_map.get(inpaint_method_val)
if method_key and method_key in LAMA_JIT_MODELS:
model_info = LAMA_JIT_MODELS[method_key]
messages.append(f"Downloading {model_info['name']}...")
model_path = inpainter.download_jit_model(method_key)
if model_path:
messages.append(f"✅ {model_info['name']} downloaded to: {model_path}")
else:
messages.append(f"❌ Failed to download {model_info['name']}")
else:
messages.append(f"ℹ️ {inpaint_method_val} is downloaded automatically on first use")
except Exception as e:
messages.append(f"❌ Error downloading inpainting model: {str(e)}")
if not messages:
messages.append("ℹ️ No models selected for download")
except Exception as e:
messages.append(f"❌ Error during download: {str(e)}")
return gr.Info("\n".join(messages))
def load_models_handler(detector_type_val, inpaint_method_val):
"""Load selected models into memory"""
messages = []
try:
# Load bubble detection model
if detector_type_val:
messages.append(f"📦 Loading {detector_type_val} bubble detector...")
try:
from bubble_detector import BubbleDetector
bd = BubbleDetector()
if detector_type_val == "rtdetr_onnx":
if bd.load_rtdetr_onnx_model():
messages.append("✅ RT-DETR ONNX model loaded successfully")
else:
messages.append("❌ Failed to load RT-DETR ONNX model")
elif detector_type_val == "rtdetr":
if bd.load_rtdetr_model():
messages.append("✅ RT-DETR model loaded successfully")
else:
messages.append("❌ Failed to load RT-DETR model")
elif detector_type_val == "yolo":
messages.append("ℹ️ YOLO models are loaded automatically when needed")
except Exception as e:
messages.append(f"❌ Error loading detector: {str(e)}")
# Load inpainting model
if inpaint_method_val:
messages.append(f"\n📦 Loading {inpaint_method_val} inpainting model...")
try:
from local_inpainter import LocalInpainter, LAMA_JIT_MODELS
import os
inpainter = LocalInpainter({})
# Map method names to model keys
method_map = {
'anime_onnx': 'anime_onnx',
'anime': 'anime',
'lama': 'lama',
'lama_onnx': 'lama_onnx',
'aot': 'aot',
'aot_onnx': 'aot_onnx'
}
method_key = method_map.get(inpaint_method_val)
if method_key:
# First check if model exists, download if not
if method_key in LAMA_JIT_MODELS:
model_info = LAMA_JIT_MODELS[method_key]
cache_dir = os.path.expanduser('~/.cache/inpainting')
model_filename = os.path.basename(model_info['url'])
model_path = os.path.join(cache_dir, model_filename)
if not os.path.exists(model_path):
messages.append(f"Model not found, downloading first...")
model_path = inpainter.download_jit_model(method_key)
if not model_path:
messages.append(f"❌ Failed to download model")
return gr.Info("\n".join(messages))
# Now load the model
if inpainter.load_model(method_key, model_path):
messages.append(f"✅ {model_info['name']} loaded successfully")
else:
messages.append(f"❌ Failed to load {model_info['name']}")
else:
messages.append(f"ℹ️ {inpaint_method_val} will be loaded automatically when needed")
else:
messages.append(f"ℹ️ Unknown method: {inpaint_method_val}")
except Exception as e:
messages.append(f"❌ Error loading inpainting model: {str(e)}")
if not messages:
messages.append("ℹ️ No models selected for loading")
except Exception as e:
messages.append(f"❌ Error during loading: {str(e)}")
return gr.Info("\n".join(messages))
download_models_btn.click(
fn=download_models_handler,
inputs=[detector_type, local_inpaint_method],
outputs=None
)
load_models_btn.click(
fn=load_models_handler,
inputs=[detector_type, local_inpaint_method],
outputs=None
)
# Auto-save parallel panel translation settings
def save_parallel_settings(parallel_enabled, max_workers):
"""Save parallel panel translation settings to config"""
try:
current_config = self.load_config()
if API_KEY_ENCRYPTION_AVAILABLE:
current_config = decrypt_config(current_config)
# Initialize nested structure if not exists
if 'manga_settings' not in current_config:
current_config['manga_settings'] = {}
if 'advanced' not in current_config['manga_settings']:
current_config['manga_settings']['advanced'] = {}
current_config['manga_settings']['advanced']['parallel_panel_translation'] = parallel_enabled
current_config['manga_settings']['advanced']['panel_max_workers'] = int(max_workers)
self.save_config(current_config)
return None
except Exception as e:
print(f"Failed to save parallel panel settings: {e}")
return None
parallel_panel_translation.change(
fn=lambda p, w: save_parallel_settings(p, w),
inputs=[parallel_panel_translation, panel_max_workers],
outputs=None
)
panel_max_workers.change(
fn=lambda p, w: save_parallel_settings(p, w),
inputs=[parallel_panel_translation, panel_max_workers],
outputs=None
)
gr.Markdown("\n---\n**Note:** These settings will be saved to your config and applied to all manga translations.")
# Glossary Extraction Tab - TEMPORARILY HIDDEN
with gr.Tab("📝 Glossary Extraction", visible=False):
with gr.Row():
with gr.Column():
glossary_epub = gr.File(
label="📖 Upload EPUB File",
file_types=[".epub"]
)
glossary_model = gr.Dropdown(
choices=self.models,
value="gpt-4-turbo",
label="🤖 AI Model"
)
glossary_api_key = gr.Textbox(
label="🔑 API Key",
type="password",
placeholder="Enter API key"
)
min_freq = gr.Slider(
minimum=1,
maximum=10,
value=2,
step=1,
label="Minimum Frequency"
)
max_names_slider = gr.Slider(
minimum=10,
maximum=200,
value=50,
step=10,
label="Max Character Names"
)
extract_btn = gr.Button(
"🔍 Extract Glossary",
variant="primary"
)
with gr.Column():
glossary_output = gr.File(label="📥 Download Glossary CSV")
glossary_status = gr.Textbox(
label="Status",
lines=10
)
extract_btn.click(
fn=self.extract_glossary,
inputs=[
glossary_epub,
glossary_model,
glossary_api_key,
min_freq,
max_names_slider
],
outputs=[glossary_output, glossary_status]
)
# Settings Tab
with gr.Tab("⚙️ Settings"):
gr.Markdown("### Configuration")
gr.Markdown("#### Translation Profiles")
gr.Markdown("Profiles are loaded from your `config_web.json` file. The web interface has its own separate configuration.")
with gr.Accordion("View All Profiles", open=False):
profiles_text = "\n\n".join(
[f"**{name}**:\n```\n{prompt[:200]}...\n```"
for name, prompt in self.profiles.items()]
)
gr.Markdown(profiles_text if profiles_text else "No profiles found")
gr.Markdown("---")
gr.Markdown("#### Advanced Translation Settings")
with gr.Row():
with gr.Column():
thread_delay = gr.Slider(
minimum=0,
maximum=5,
value=self.config.get('thread_submission_delay', 0.5),
step=0.1,
label="Threading delay (s)"
)
api_delay = gr.Slider(
minimum=0,
maximum=10,
value=self.config.get('delay', 2),
step=0.5,
label="API call delay (s)"
)
chapter_range = gr.Textbox(
label="Chapter range (e.g., 5-10)",
value=self.config.get('chapter_range', ''),
placeholder="Leave empty for all chapters"
)
token_limit = gr.Number(
label="Input Token limit",
value=self.config.get('token_limit', 200000),
minimum=0
)
disable_token_limit = gr.Checkbox(
label="Disable Input Token Limit",
value=self.config.get('token_limit_disabled', False)
)
output_token_limit = gr.Number(
label="Output Token limit",
value=self.config.get('max_output_tokens', 16000),
minimum=0
)
with gr.Column():
contextual = gr.Checkbox(
label="Contextual Translation",
value=self.config.get('contextual', False)
)
history_limit = gr.Number(
label="Translation History Limit",
value=self.config.get('translation_history_limit', 2),
minimum=0
)
rolling_history = gr.Checkbox(
label="Rolling History Window",
value=self.config.get('translation_history_rolling', False)
)
batch_translation = gr.Checkbox(
label="Batch Translation",
value=self.config.get('batch_translation', False)
)
batch_size = gr.Number(
label="Batch Size",
value=self.config.get('batch_size', 3),
minimum=1
)
gr.Markdown("---")
gr.Markdown("🔒 **API keys are encrypted** when saved to config using AES encryption.")
save_api_key = gr.Checkbox(
label="Save API Key (Encrypted)",
value=True
)
save_status = gr.Textbox(label="Settings Status", value="Settings auto-save on change", interactive=False)
def save_settings(save_key, t_delay, a_delay, ch_range, tok_limit, disable_tok_limit, out_tok_limit, ctx, hist_lim, roll_hist, batch, b_size):
"""Auto-save settings when changed"""
try:
# Reload latest config first to avoid overwriting other changes
current_config = self.load_config()
# Update only the fields we're managing
current_config.update({
'save_api_key': save_key,
'thread_submission_delay': float(t_delay),
'delay': float(a_delay),
'chapter_range': str(ch_range),
'token_limit': int(tok_limit) if tok_limit else 200000,
'token_limit_disabled': bool(disable_tok_limit),
'max_output_tokens': int(out_tok_limit) if out_tok_limit else 16000,
'contextual': bool(ctx),
'translation_history_limit': int(hist_lim) if hist_lim else 2,
'translation_history_rolling': bool(roll_hist),
'batch_translation': bool(batch),
'batch_size': int(b_size) if b_size else 3
})
# Save with the merged config
result = self.save_config(current_config)
return f"✅ {result}"
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"Settings save error:\n{error_trace}")
return f"❌ Save failed: {str(e)}"
# Auto-save on any change
for component in [save_api_key, thread_delay, api_delay, chapter_range, token_limit, disable_token_limit,
output_token_limit, contextual, history_limit, rolling_history, batch_translation, batch_size]:
component.change(
fn=save_settings,
inputs=[
save_api_key,
thread_delay,
api_delay,
chapter_range,
token_limit,
disable_token_limit,
output_token_limit,
contextual,
history_limit,
rolling_history,
batch_translation,
batch_size
],
outputs=[save_status]
)
# Help Tab
with gr.Tab("❓ Help"):
gr.Markdown("""
## How to Use Glossarion
### Translation
1. Upload an EPUB file
2. Select AI model (GPT-4, Claude, etc.)
3. Enter your API key
4. Click "Translate"
5. Download the translated EPUB
### Manga Translation
1. Upload manga image(s) (PNG, JPG, etc.)
2. Select AI model and enter API key
3. Choose translation profile (e.g., Manga_JP, Manga_KR)
4. Configure OCR settings (Google Cloud Vision recommended)
5. Enable bubble detection and inpainting for best results
6. Click "Translate Manga"
### Glossary Extraction
1. Upload an EPUB file
2. Configure extraction settings
3. Click "Extract Glossary"
4. Use the CSV in future translations
### API Keys
- **OpenAI**: Get from https://platform.openai.com/api-keys
- **Anthropic**: Get from https://console.anthropic.com/
### Translation Profiles
Profiles contain detailed translation instructions and rules.
Select a profile that matches your source language and style preferences.
You can create and edit profiles in the desktop application.
### Tips
- Use glossaries for consistent character name translation
- Lower temperature (0.1-0.3) for more literal translations
- Higher temperature (0.5-0.7) for more creative translations
""")
return app
def main():
"""Launch Gradio web app"""
print("🚀 Starting Glossarion Web Interface...")
# Check if running on Hugging Face Spaces
is_spaces = os.getenv('HF_SPACES') == 'true' or os.getenv('Shirochi/Glossarion') is not None
if is_spaces:
print("🤗 Running on Hugging Face Spaces")
web_app = GlossarionWeb()
app = web_app.create_interface()
# Set favicon with absolute path if available (skip for Spaces)
favicon_path = None
if not is_spaces and os.path.exists("Halgakos.ico"):
favicon_path = os.path.abspath("Halgakos.ico")
print(f"✅ Using favicon: {favicon_path}")
elif not is_spaces:
print("⚠️ Halgakos.ico not found")
# Launch with options appropriate for environment
launch_args = {
"server_name": "0.0.0.0", # Allow external access
"server_port": 7860,
"share": False,
"show_error": True,
}
# Only add favicon for non-Spaces environments
if not is_spaces and favicon_path:
launch_args["favicon_path"] = favicon_path
app.launch(**launch_args)
if __name__ == "__main__":
main()