""" app.py — SubSync: Myanmar Subtitle Translator Complete implementation — Steps 1-6. HF Space Secrets: SUBTITLE_USER login username (default: admin) SUBTITLE_PASS login password (default: subtitle2024) GEMINI_API_KEY optional pre-fill OPENROUTER_API_KEY optional pre-fill """ import io import os import base64 import traceback import pandas as pd import streamlit as st import time from auth import is_authenticated, render_login_page, logout from job_store import (create_job, update_job, save_result, load_result, load_job, list_jobs, delete_job, clear_all_jobs, job_age_str, status_icon, status_color, save_glossary, load_glossary, get_latest_translate_job_with_glossary) from audio_transcriber import (run_transcription_bg, SUPPORTED_LANGUAGES, MODEL) from translation import (extract_glossary, translate_all, TranslationPaused, load_checkpoint, clear_checkpoint, checkpoint_progress, run_translation_bg, build_system_prompt, PROMPT_PRESETS, SOURCE_CULTURES, PROPER_NOUN_STYLES) from api_handler import fetch_models, validate_api_key, call_ai from subtitle_parser import ( parse_subtitle, get_plain_lines, replace_lines, write_subtitle, output_filename, clean_workspace, workspace_size_mb, ) # ── Page config ─────────────────────────────────────────────────────────────── st.set_page_config( page_title="SubSync · Myanmar Translator", page_icon="🎬", layout="wide", initial_sidebar_state="collapsed", ) # ───────────────────────────────────────────────────────────────────────────── # Global CSS # ───────────────────────────────────────────────────────────────────────────── def _inject_css() -> None: st.markdown(""" """, unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # Session state # ───────────────────────────────────────────────────────────────────────────── def _init_session() -> None: defaults = { "authenticated": False, "current_user": None, # Multi-key: list of Gemini API keys; active index tracks which is current "gemini_api_keys": [""], # list of keys; index 0 = Key 1 "gemini_key_idx": 0, # active key index "openrouter_api_key": "", "selected_provider": "gemini", "selected_model": None, "available_models": [], # Limit-hit dialog state "show_limit_dialog": False, "limit_hit_info": None, # {reason, provider, key_masked, model} "uploaded_file_name": None, "subtitle_raw": None, "subtitle_parsed": None, "subtitle_format": None, "plain_lines": None, "glossary_raw": None, "glossary_approved": False, "glossary_edited": None, "translation_result": None, "translation_done": False, "wizard_step": 1, "active_page": "translate", "current_job_id": None, # active job being processed "audio_job_id": None, # current audio transcription job "audio_srt_result": None, "audio_srt_filename": None, "audio_srt_to_translate": None, "audio_srt_orig_name": None, "tx_checkpoint": None, "last_error": None, # Prompt preset & source culture "prompt_preset": "adult_18", "source_culture": "western", "custom_prompt": "", "proper_noun_style": "english", } for k, v in defaults.items(): if k not in st.session_state: st.session_state[k] = v # ───────────────────────────────────────────────────────────────────────────── # Sidebar # ───────────────────────────────────────────────────────────────────────────── def _render_header(title: str, sub: str = "") -> None: """ Renders a page header row: LEFT — page title + subtitle RIGHT — ⚙️ Settings | ℹ️ About | 🗑 Clean | 🚪 Out All buttons are real st.button calls — no HTML nav needed. Active page is tracked via st.session_state["active_page"]. """ # ── CSS for the small action buttons ────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── Layout: title col (wide) + buttons col (narrow) ─────────────── col_title, col_btns = st.columns([3, 2]) with col_title: st.markdown(f'
{title}
', unsafe_allow_html=True) if sub: st.markdown(f'
{sub}
', unsafe_allow_html=True) with col_btns: # Push buttons to the right by using nested columns # Row 1: main nav buttons st.markdown('
', unsafe_allow_html=True) b1, b2, b3, b4, b5 = st.columns(5) with b1: if st.button("🎬 Translate", key="hb_home", use_container_width=True): st.session_state["active_page"] = "translate" st.rerun() with b2: if st.button("🎙 Audio→SRT", key="hb_audio", use_container_width=True): st.session_state["active_page"] = "audio_srt" st.rerun() with b3: if st.button("📋 History", key="hb_history", use_container_width=True): st.session_state["active_page"] = "history" st.rerun() with b4: if st.button("⚙️ Settings", key="hb_settings", use_container_width=True): st.session_state["active_page"] = "settings" st.rerun() with b5: if st.button("ℹ️ About", key="hb_about", use_container_width=True): st.session_state["active_page"] = "about" st.rerun() st.markdown('
', unsafe_allow_html=True) # Row 2: clean + sign out st.markdown('
', unsafe_allow_html=True) d1, d2 = st.columns(2) with d1: if st.button("🗑 Clean", key="hb_clean", use_container_width=True): try: # 1. Delete subtitle temp files clean_workspace() # 2. Delete ALL job history + result files (_jobs/ dir) clear_all_jobs() # 3. Wipe every workflow-related session key _clean_keys = [ # Subtitle workflow "uploaded_file_name", "subtitle_raw", "subtitle_parsed", "subtitle_format", "plain_lines", # Glossary — clear completely so no cross-job bleed "glossary_raw", "glossary_edited", "last_error", # Translation "translation_result", "tx_checkpoint", # Job tracking "current_job_id", # Audio SRT "audio_job_id", "audio_srt_result", "audio_srt_filename", "audio_srt_to_translate", "audio_srt_orig_name", # Limit dialog "limit_hit_info", ] for k in _clean_keys: st.session_state[k] = None st.session_state["glossary_approved"] = False st.session_state["translation_done"] = False st.session_state["show_limit_dialog"] = False st.session_state["wizard_step"] = 1 st.rerun() except Exception as e: st.error(str(e)) with d2: if st.button("🚪 Out", key="hb_signout", use_container_width=True): logout() st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) def _wizard_strip(current: int) -> None: steps = [(1,"Upload"),(2,"Model"),(3,"Glossary"),(4,"Review"),(5,"Translate")] html = '
' for i, (n, label) in enumerate(steps): cls = "done" if n < current else ("active" if n == current else "") icon = "✓ " if n < current else "" html += f'{icon}{n}. {label}' if i < len(steps) - 1: html += '' html += "
" st.markdown(html, unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # Helper — active API key # ───────────────────────────────────────────────────────────────────────────── def _active_key() -> str: """Return the currently-active API key string.""" p = st.session_state.get("selected_provider", "gemini") if p == "gemini": keys = st.session_state.get("gemini_api_keys", [""]) idx = st.session_state.get("gemini_key_idx", 0) if not keys: return "" idx = max(0, min(idx, len(keys) - 1)) return keys[idx] or "" return st.session_state.get("openrouter_api_key", "") def _active_key_label() -> str: """Return display label for the active key (e.g. 'Gemini Key 2').""" p = st.session_state.get("selected_provider", "gemini") if p == "gemini": idx = st.session_state.get("gemini_key_idx", 0) return f"Gemini Key {idx + 1}" return "OpenRouter" def _mask_key(key: str) -> str: """Return masked key for display: AIza...1234""" k = key.strip() if len(k) > 8: return k[:4] + "..." + k[-4:] return "****" if k else "(empty)" # ───────────────────────────────────────────────────────────────────────────── # STEP 1 — File Upload # ───────────────────────────────────────────────────────────────────────────── def _step1_upload() -> None: st.markdown('
', unsafe_allow_html=True) st.markdown('
📁 Step 1 — Subtitle ဖိုင် တင်ရန်
', unsafe_allow_html=True) # ── Auto-populate from Audio→SRT page "Translate this SRT" button ──── auto_srt = st.session_state.get("audio_srt_to_translate") auto_name = st.session_state.get("audio_srt_orig_name", "audio.srt") if auto_srt and st.session_state.get("uploaded_file_name") != auto_name: try: subs, fmt = parse_subtitle(auto_srt, auto_name) plain = get_plain_lines(subs) st.session_state["uploaded_file_name"] = auto_name st.session_state["subtitle_raw"] = auto_srt st.session_state["subtitle_parsed"] = subs st.session_state["subtitle_format"] = fmt st.session_state["plain_lines"] = plain # Clear audio_srt_to_translate so it doesn't re-trigger st.session_state["audio_srt_to_translate"] = None st.session_state["audio_srt_orig_name"] = None st.info(f"Audio→SRT ဖိုင် '{auto_name}' ကို auto-load ပြီး", icon="🎙") except Exception as e: st.error(f"Auto-load မအောင်မြင်: {e}", icon="❌") st.session_state["audio_srt_to_translate"] = None uploaded = st.file_uploader( "Subtitle ဖိုင် ရွေးပါ", type=["srt", "vtt", "ass", "ssa"], key="file_uploader", label_visibility="collapsed", help="SRT, VTT, ASS/SSA format လက်ခံသည်", ) if uploaded is not None: # Read bytes once and cache — avoid double-read on rerun file_bytes = uploaded.read() # Only re-parse if this is a new file if st.session_state.get("uploaded_file_name") != uploaded.name: st.session_state["uploaded_file_name"] = None # clear while parsing parse_error = None is_new_file = (st.session_state.get("uploaded_file_name") != uploaded.name) if st.session_state.get("subtitle_parsed") is None or is_new_file: if is_new_file: # New file uploaded — reset ALL glossary & job state # Prevents old glossary bleeding into new translation for _k in ["glossary_raw", "glossary_edited", "translation_result", "tx_checkpoint", "current_job_id", "limit_hit_info"]: st.session_state[_k] = None st.session_state["glossary_approved"] = False st.session_state["translation_done"] = False st.session_state["wizard_step"] = 1 try: with st.spinner("ဖိုင် ဖတ်နေသည်…"): subs, fmt = parse_subtitle(file_bytes, uploaded.name) plain = get_plain_lines(subs) st.session_state["uploaded_file_name"] = uploaded.name st.session_state["subtitle_raw"] = file_bytes st.session_state["subtitle_parsed"] = subs st.session_state["subtitle_format"] = fmt st.session_state["plain_lines"] = plain except Exception as e: parse_error = str(e) st.session_state["last_error"] = traceback.format_exc() if parse_error: st.error( f"**Subtitle ဖိုင် ဖတ်၍ မရပါ**\n\n{parse_error}", icon="❌", ) with st.expander("🐛 Full error details", expanded=False): st.code(st.session_state.get("last_error",""), language="python") else: subs = st.session_state.get("subtitle_parsed") fmt = st.session_state.get("subtitle_format", "srt") plain = st.session_state.get("plain_lines") or [] if subs is not None and plain: c1, c2, c3 = st.columns(3) c1.metric("ဖိုင်", uploaded.name) c2.metric("Format", fmt.upper()) c3.metric("Lines", f"{len(plain):,}") st.markdown("
", unsafe_allow_html=True) with st.expander("📋 Preview (ပထမ 8 ကြောင်း)", expanded=True): for i, ln in enumerate(plain[:8]): display = ln if ln else "empty" st.markdown( f'
' + f'{i+1:03d}' + f"  {display}
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) if st.button("Step 2 သို့ ဆက်သွားမည် →", key="step1_next"): st.session_state["wizard_step"] = 2 st.rerun() else: prev = st.session_state.get("uploaded_file_name") if prev: plain = st.session_state.get("plain_lines") or [] st.info( f"ဖိုင် '{prev}' ({len(plain):,} lines) တင်ထားပြီး — ဆက်သွားနိုင်သည်", icon="📎", ) if st.button("Step 2 သို့ ဆက်သွားမည် →", key="step1_next_prev"): st.session_state["wizard_step"] = 2 st.rerun() else: st.markdown( '
' + 'SRT · VTT · ASS/SSA format လက်ခံသည်
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) def _step2_model() -> None: st.markdown('
', unsafe_allow_html=True) st.markdown('
🤖 Step 2 — AI Model & Translation Settings
', unsafe_allow_html=True) provider = st.session_state.get("selected_provider", "") model = st.session_state.get("selected_model", "") key = _active_key() prov_lbl = "Google Gemini" if provider == "gemini" else "OpenRouter" if key.strip() and model: st.success(f"**{prov_lbl}** · `{model}`", icon="✅") else: st.warning("⚙️ Settings page တွင် API key နှင့် model ရွေးချယ်ပါ", icon="⚠️") if st.button("⚙️ Settings သို့ →", key="s2_go"): st.session_state["active_page"] = "settings" st.rerun() st.markdown("
", unsafe_allow_html=True) # ── Prompt Preset selector ───────────────────────────────────────── st.markdown("#### 🎭 Translation Style") preset_keys = list(PROMPT_PRESETS.keys()) preset_labels = [PROMPT_PRESETS[k]["label"] for k in preset_keys] cur_preset = st.session_state.get("prompt_preset", "adult_18") if cur_preset not in preset_keys: cur_preset = "adult_18" sel_preset = st.selectbox( "Preset ရွေးပါ:", preset_keys, index=preset_keys.index(cur_preset), format_func=lambda k: PROMPT_PRESETS[k]["label"], key="s2_preset_sel", ) st.caption(PROMPT_PRESETS[sel_preset]["desc"]) st.session_state["prompt_preset"] = sel_preset # Custom prompt text area if sel_preset == "custom": custom_txt = st.text_area( "System prompt ရေးထည့်ပါ:", value=st.session_state.get("custom_prompt", ""), height=200, placeholder="You are a subtitle translator...\n\nOutput JSON array only.", key="s2_custom_prompt", ) st.session_state["custom_prompt"] = custom_txt else: # Preview toggle with st.expander("Prompt preview ကြည့်ရန်"): from translation import build_system_prompt as _bsp st.code(_bsp(sel_preset, st.session_state.get("source_culture", "western")), language="text") st.markdown("
", unsafe_allow_html=True) # ── Source Culture selector ──────────────────────────────────────── st.markdown("#### 🌏 Source Content Origin") culture_keys = list(SOURCE_CULTURES.keys()) cur_culture = st.session_state.get("source_culture", "western") if cur_culture not in culture_keys: cur_culture = "western" sel_culture = st.selectbox( "ဇာတ်ကားဘယ်နိုင်ငံကဆိုတာ ရွေးပါ:", culture_keys, index=culture_keys.index(cur_culture), format_func=lambda k: f"{SOURCE_CULTURES[k]['label']} — {SOURCE_CULTURES[k]['desc']}", key="s2_culture_sel", ) st.session_state["source_culture"] = sel_culture st.markdown("
", unsafe_allow_html=True) # ── Proper Noun Style selector ───────────────────────────────────── st.markdown("#### 🔤 လူနာမည် / နေရာနာမည် ပြပုံ") cur_noun_style = st.session_state.get("proper_noun_style", "english") noun_style_keys = list(PROPER_NOUN_STYLES.keys()) sel_noun_style = st.radio( "Proper noun ကို ဘယ်လိုပြမည်:", noun_style_keys, index=noun_style_keys.index(cur_noun_style) if cur_noun_style in noun_style_keys else 0, format_func=lambda k: PROPER_NOUN_STYLES[k]["label"], key="s2_noun_style", horizontal=True, ) st.caption(PROPER_NOUN_STYLES[sel_noun_style]["desc"]) st.session_state["proper_noun_style"] = sel_noun_style st.markdown("
", unsafe_allow_html=True) if key.strip() and model: c1, c2 = st.columns([1, 1]) with c1: if st.button("✓ Confirm & Continue →", key="s2_confirm"): st.session_state["wizard_step"] = 3 st.rerun() with c2: st.markdown('
', unsafe_allow_html=True) if st.button("⚙ Settings ပြောင်းရန်", key="s2_settings"): st.session_state["active_page"] = "settings" st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) if st.button("← Upload ပြန်သွားမည်", key="s2_back"): st.session_state["wizard_step"] = 1 st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # STEP 3 — Glossary Extraction # ───────────────────────────────────────────────────────────────────────────── def _step3_glossary() -> None: st.markdown('
', unsafe_allow_html=True) st.markdown('
📖 Step 3 — Glossary ထုတ်ယူရန်
', unsafe_allow_html=True) lines = st.session_state.get("plain_lines") or [] if not lines: st.error("Subtitle ဖိုင် မတင်ရသေးပါ", icon="❌") if st.button("← Step 1 ပြန်သွားမည်", key="s3_back_err"): st.session_state["wizard_step"] = 1 st.rerun() st.markdown('
', unsafe_allow_html=True) return existing_glossary = st.session_state.get("glossary_raw") if existing_glossary is not None: n = len(existing_glossary) st.success(f"Glossary ထုတ်ပြီးသား ({n} terms) — Step 4 တွင် စစ်ဆေးပါ", icon="✅") c1, c2 = st.columns([1, 1]) with c1: if st.button("Step 4 Review သို့ →", key="s3_next_done"): st.session_state["wizard_step"] = 4 st.rerun() with c2: st.markdown('
', unsafe_allow_html=True) if st.button("🔄 ထပ်ထုတ်မည်", key="s3_reextract"): st.session_state["glossary_raw"] = None st.session_state["glossary_approved"] = False st.session_state["glossary_edited"] = None st.rerun() st.markdown('
', unsafe_allow_html=True) else: total_lines = len(lines) sample_n = min(300, total_lines) fname = st.session_state.get("uploaded_file_name", "file") model = st.session_state.get("selected_model", "") prov = st.session_state.get("selected_provider", "") prov_lbl = "Google Gemini" if prov == "gemini" else "OpenRouter" st.markdown( f'
{fname} · {total_lines:,} lines · ' f'Sample {sample_n} lines ကို {prov_lbl}/{model} ဖြင့် စစ်မည်
', unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) if st.button("🔍 Glossary ထုတ်ယူမည်", key="s3_extract"): key = _active_key() if not key.strip() or not model: st.error("Settings တွင် API key / model ရွေးပါ", icon="❌") else: with st.spinner("AI မှ glossary ထုတ်နေသည်… (15-30 sec)"): try: glossary = extract_glossary( lines, provider=prov, model=model, api_key=key, culture_key=st.session_state.get("source_culture", "western"), proper_noun_style=st.session_state.get("proper_noun_style", "english"), ) st.session_state["glossary_raw"] = glossary st.rerun() except Exception as e: st.error(f"Glossary extraction မအောင်မြင်: {e}", icon="❌") st.session_state["last_error"] = traceback.format_exc() st.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) if st.button("← Step 2 ပြန်သွားမည်", key="s3_back"): st.session_state["wizard_step"] = 2 st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('', unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # STEP 4 — Glossary Review & Edit # ───────────────────────────────────────────────────────────────────────────── def _step4_review() -> None: st.markdown('
', unsafe_allow_html=True) st.markdown('
✏️ Step 4 — Glossary စစ်ဆေး / ပြင်ဆင်ရန်
', unsafe_allow_html=True) raw_glossary = st.session_state.get("glossary_raw") if raw_glossary is None: st.warning("Glossary မရှိသေးပါ — Step 3 တွင် ထုတ်ပါ", icon="⚠️") if st.button("← Step 3 ပြန်သွားမည်", key="s4_back_err"): st.session_state["wizard_step"] = 3 st.rerun() st.markdown('
', unsafe_allow_html=True) return st.markdown( '
' 'Term များကို တိုက်ရိုက် ပြင်ဆင်နိုင်သည်။ ' 'Row ထပ်ထည့် သို့မဟုတ် ဖျက်နိုင်သည်။
', unsafe_allow_html=True) # Convert to DataFrame for editing if raw_glossary: df_init = pd.DataFrame( list(raw_glossary.items()), columns=["မူရင်း (Original)", "မြန်မာဘာသာပြန်"], ) else: df_init = pd.DataFrame(columns=["မူရင်း (Original)", "မြန်မာဘာသာပြန်"]) edited_df = st.data_editor( df_init, num_rows="dynamic", use_container_width=True, key="glossary_editor", column_config={ "မူရင်း (Original)": st.column_config.TextColumn(width="medium"), "မြန်မာဘာသာပြန်": st.column_config.TextColumn(width="medium"), }, ) st.markdown("
", unsafe_allow_html=True) # Show count valid_rows = edited_df.dropna(subset=["မူရင်း (Original)"]) valid_rows = valid_rows[valid_rows["မူရင်း (Original)"].str.strip() != ""] st.markdown( f'
' f'{len(valid_rows)} terms · ' f'Approve လုပ်မှသာ translation စတင်နိုင်မည်
', unsafe_allow_html=True) c1, c2 = st.columns([1.5, 1]) with c1: if st.button("✅ Approve Glossary & Continue →", key="s4_approve"): # Convert edited df back to dict approved = {} for _, row in valid_rows.iterrows(): orig = str(row["မူရင်း (Original)"]).strip() tran = str(row["မြန်မာဘာသာပြန်"]).strip() if pd.notna(row["မြန်မာဘာသာပြန်"]) else orig if orig: approved[orig] = tran st.session_state["glossary_edited"] = approved st.session_state["glossary_approved"] = True st.session_state["wizard_step"] = 5 # ── Persist glossary to job_store ────────────────────────── # Saved separately from checkpoint so it survives model/provider # changes. Resume always reuses this approved glossary. _jid = st.session_state.get("current_job_id") if not _jid or not load_job(_jid): fname_tmp = st.session_state.get("uploaded_file_name", "file") _jid = create_job( "translate", fname_tmp, model=st.session_state.get("selected_model",""), provider=st.session_state.get("selected_provider",""), ) st.session_state["current_job_id"] = _jid save_glossary(_jid, approved) st.rerun() with c2: st.markdown('
', unsafe_allow_html=True) if st.button("← Glossary ထပ်ထုတ်မည်", key="s4_back"): st.session_state["wizard_step"] = 3 st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('', unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # STEP 5 — Translation & Download # ───────────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────────── # Limit-hit dialog (@st.dialog — Streamlit ≥ 1.35) # ───────────────────────────────────────────────────────────────────────────── @st.dialog("⏸ API Limit ထိသည် — Provider / Key / Model ပြောင်းမည်") def _show_limit_dialog() -> None: """ Popup modal shown when translation hits rate/quota limit. User can: A) Switch to another Gemini API key B) Switch to OpenRouter C) Change model only (same provider/key) Then Resume continues from checkpoint with same glossary → consistent translation. """ info = st.session_state.get("limit_hit_info") or {} reason = info.get("reason", "Rate/Quota limit") hit_label = info.get("key_label", "?") hit_masked = info.get("key_masked", "****") hit_prov = info.get("provider", "gemini") hit_model = info.get("model", "") st.markdown(f"""
⚠ {reason}
Hit on: {hit_label}  ·  {hit_masked}  ·  {hit_model}
""", unsafe_allow_html=True) done_n, total_n = checkpoint_progress( st.session_state, len(st.session_state.get("plain_lines") or []) ) st.markdown( f'
✓ Checkpoint: {done_n}/{total_n} lines saved
', unsafe_allow_html=True) st.markdown("##### ဘာပြောင်းမလဲ ရွေးပါ") choice = st.radio( "action", [ "🔑 Gemini Key ပြောင်းမည်", "🌐 OpenRouter သို့ ပြောင်းမည်", "🤖 Model သာ ပြောင်းမည် (same provider/key)", ], key="limit_action", label_visibility="collapsed", ) st.markdown('
', unsafe_allow_html=True) # ── Option A: Switch Gemini key ──────────────────────────────────── if "Gemini Key" in choice: keys = st.session_state.get("gemini_api_keys", [""]) cur_idx = st.session_state.get("gemini_key_idx", 0) # Show all keys with current status st.markdown("**Available Gemini Keys:**") for i, k in enumerate(keys): is_cur = (i == cur_idx) masked = _mask_key(k) status = "🔴 (limit hit)" if is_cur else ("✓" if k.strip() else "empty") col_r, col_k, col_use = st.columns([0.4, 3, 1]) with col_r: st.markdown( f'
Key {i+1}
', unsafe_allow_html=True) with col_k: st.markdown( f'
' + f'{masked} {status}
', unsafe_allow_html=True) with col_use: if not is_cur and k.strip(): if st.button(f"Use", key=f"dlg_use_{i}"): st.session_state["gemini_key_idx"] = i st.session_state["selected_provider"] = "gemini" st.session_state["show_limit_dialog"] = False st.rerun() st.markdown("
", unsafe_allow_html=True) # Add new key inline new_key = st.text_input("New Gemini Key ထည့်ရန်:", type="password", placeholder="AIza...", key="dlg_new_key") if st.button("+ Add & Use", key="dlg_add_key"): if new_key.strip(): keys.append(new_key.strip()) new_idx = len(keys) - 1 st.session_state["gemini_api_keys"] = keys st.session_state["gemini_key_idx"] = new_idx st.session_state["selected_provider"] = "gemini" st.session_state["show_limit_dialog"] = False st.rerun() # ── Option B: Switch to OpenRouter ──────────────────────────────── elif "OpenRouter" in choice: or_key = st.session_state.get("openrouter_api_key", "") new_or = st.text_input("OpenRouter API Key:", value=or_key, type="password", placeholder="sk-or-...", key="dlg_or_key") if new_or != or_key: st.session_state["openrouter_api_key"] = new_or # Fetch OR models if st.button("🔄 Models ဆွဲယူ", key="dlg_or_fetch"): if new_or.strip(): with st.spinner("Fetching…"): mods = fetch_models("openrouter", new_or.strip()) st.session_state["available_models"] = mods st.session_state["selected_model"] = mods[0] if mods else None avail = st.session_state.get("available_models") or [ "google/gemini-2.0-flash-exp:free", "meta-llama/llama-3.3-70b-instruct:free", "deepseek/deepseek-chat-v3-0324:free", ] sel = st.selectbox("Model ရွေးပါ:", avail, key="dlg_or_model") st.session_state["selected_model"] = sel if st.button("✓ Switch to OpenRouter", key="dlg_or_confirm"): if new_or.strip(): st.session_state["openrouter_api_key"] = new_or.strip() st.session_state["selected_provider"] = "openrouter" st.session_state["selected_model"] = sel st.session_state["show_limit_dialog"] = False st.rerun() else: st.error("OpenRouter API key ထည့်ပါ") # ── Option C: Model only ─────────────────────────────────────────── elif "Model" in choice: st.markdown(f"Provider/Key မပြောင်း — {_active_key_label()} / {_mask_key(_active_key())}") cur_prov = st.session_state.get("selected_provider","gemini") if st.button("🔄 Models ဆွဲယူ", key="dlg_fetch_models"): k = _active_key() if k.strip(): with st.spinner("Fetching…"): mods = fetch_models(cur_prov, k) st.session_state["available_models"] = mods avail = st.session_state.get("available_models") or ( ["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash"] if cur_prov=="gemini" else ["google/gemini-2.0-flash-exp:free","meta-llama/llama-3.3-70b-instruct:free"] ) cur_m = st.session_state.get("selected_model") or avail[0] if cur_m not in avail: cur_m = avail[0] sel = st.selectbox("Model ရွေးပါ:", avail, index=avail.index(cur_m), key="dlg_model_sel") if st.button("✓ Model ပြောင်းပြီး Resume", key="dlg_model_confirm"): st.session_state["selected_model"] = sel st.session_state["available_models"] = avail st.session_state["show_limit_dialog"] = False st.rerun() # ── Resume button (bottom) ───────────────────────────────────────── st.markdown('
', unsafe_allow_html=True) if st.button("▶ Resume — ဘာသာပြန်ဆက်မည်", key="dlg_resume", use_container_width=True, type="primary"): st.session_state["show_limit_dialog"] = False st.rerun() if st.button("✕ Cancel", key="dlg_cancel"): st.session_state["show_limit_dialog"] = False st.rerun() def _step5_translate() -> None: st.markdown('
', unsafe_allow_html=True) st.markdown('
🚀 Step 5 — ဘာသာပြန်ခြင်း & ရယူခြင်း
', unsafe_allow_html=True) # Guards lines = st.session_state.get("plain_lines") or [] subs = st.session_state.get("subtitle_parsed") fmt = st.session_state.get("subtitle_format", "srt") fname = st.session_state.get("uploaded_file_name", "output.srt") glossary = st.session_state.get("glossary_edited") or {} model = st.session_state.get("selected_model", "") prov = st.session_state.get("selected_provider", "gemini") key = _active_key() prov_lbl = "Google Gemini" if prov == "gemini" else "OpenRouter" if not lines or subs is None: st.error("Subtitle ဖိုင် မတင်ရသေးပါ", icon="❌") if st.button("← Step 1 ပြန်သွားမည်", key="s5_back_err1"): st.session_state["wizard_step"] = 1 st.rerun() st.markdown('
', unsafe_allow_html=True) return if not st.session_state.get("glossary_approved"): st.warning("Glossary approve မလုပ်ရသေးပါ", icon="⚠️") if st.button("← Step 4 ပြန်သွားမည်", key="s5_back_err2"): st.session_state["wizard_step"] = 4 st.rerun() st.markdown('', unsafe_allow_html=True) return # ── Show limit dialog if triggered ─────────────────────────────────── if st.session_state.get("show_limit_dialog"): _show_limit_dialog() # ── Translation settings summary ───────────────────────────────────── cur_preset = st.session_state.get("prompt_preset", "adult_18") cur_culture = st.session_state.get("source_culture", "western") cur_noun = st.session_state.get("proper_noun_style", "english") preset_lbl = PROMPT_PRESETS.get(cur_preset, {}).get("label", cur_preset) culture_lbl = SOURCE_CULTURES.get(cur_culture, {}).get("label", cur_culture) noun_lbl = PROPER_NOUN_STYLES.get(cur_noun, {}).get("label", cur_noun) st.markdown( f'
' f'Style: {preset_lbl}  ·  Origin: {culture_lbl}  ·  ' f'Nouns: {noun_lbl}' f'
', unsafe_allow_html=True, ) # ── If already translated — show download ──────────────────────────── if st.session_state.get("translation_done") and \ st.session_state.get("translation_result") is not None: result_bytes = st.session_state["translation_result"] out_name = output_filename(fname) st.success( f"✅ ဘာသာပြန်မှု ပြီးစီးသည် — **{len(lines):,} lines** ပြောင်းပြီး", icon="🎉", ) # Stats c1, c2, c3 = st.columns(3) c1.metric("Lines", f"{len(lines):,}") c2.metric("Glossary Terms", len(glossary)) c3.metric("Output", out_name) st.markdown("
", unsafe_allow_html=True) st.download_button( label=f"⬇️ {out_name} ရယူမည်", data=result_bytes, file_name=out_name, mime="text/plain", use_container_width=True, key="download_btn", ) st.markdown("
", unsafe_allow_html=True) # Preview first 10 translated lines with st.expander("📋 ဘာသာပြန် Preview (ပထမ 10 ကြောင်း)", expanded=False): try: preview_text = result_bytes.decode("utf-8", errors="replace") # Show first ~50 lines of the raw file raw_lines = preview_text.splitlines()[:50] st.code("\n".join(raw_lines), language=None) except Exception: st.info("Preview ပြနိုင်ခြင်း မရှိပါ") st.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) if st.button("🔄 ထပ်ပြောင်းမည် (ဖျက်ပြီး စတင်)", key="s5_retranslate"): st.session_state["translation_result"] = None st.session_state["translation_done"] = False st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('', unsafe_allow_html=True) return # ── Translation UI ──────────────────────────────────────────────────── import math from translation import _get_chunk_size CHUNK_SIZE = _get_chunk_size(model) total_chunks = math.ceil(len(lines) / CHUNK_SIZE) st.markdown(f"""
ဖိုင် {fname}
Lines {len(lines):,}
Chunks {total_chunks} × {CHUNK_SIZE} lines
Model {prov_lbl} / {model}
Glossary {len(glossary)} terms
""", unsafe_allow_html=True) # ── Poll background job if already running ─────────────────────────── _jid = st.session_state.get("current_job_id") if _jid: _bgjob = load_job(_jid) if _bgjob and _bgjob["status"] == "running": _pct = _bgjob.get("progress", 0.0) _msg = _bgjob.get("status_msg", "ဘာသာပြန်နေသည်…") st.markdown('
', unsafe_allow_html=True) st.markdown( '
🔄 RUNNING — Background ဘာသာပြန်နေသည်
', unsafe_allow_html=True) st.progress(_pct, text=_msg) st.markdown( f'
' + f'Job: {_jid} · Browser ပိတ်ထားနိုင်သည် — History page မှ ကြည့်ပါ
', unsafe_allow_html=True) # Auto-refresh via HTML meta tag (no sleep — does not block Streamlit) st.markdown( '', unsafe_allow_html=True) col_ref, _ = st.columns([1, 3]) with col_ref: if st.button("🔄 Status စစ်ဆေးမည်", key="s5_poll_refresh"): st.rerun() st.markdown('
', unsafe_allow_html=True) return if _bgjob and _bgjob["status"] == "completed": result_bytes = load_result(_jid) if result_bytes and not st.session_state.get("translation_done"): st.session_state["translation_result"] = result_bytes st.session_state["translation_done"] = True st.rerun() if _bgjob and _bgjob["status"] == "failed": st.error( f"❌ ဘာသာပြန်မှု မအောင်မြင်ပါ\n{_bgjob.get('error','Unknown error')}", icon="❌") st.markdown('
', unsafe_allow_html=True) if st.button("🔄 ထပ်ကြိုးစားမည်", key="s5_retry_failed"): st.session_state["current_job_id"] = None st.rerun() st.markdown('
', unsafe_allow_html=True) if _bgjob and _bgjob["status"] == "paused": cp = _bgjob.get("checkpoint", {}) done_n2 = len(cp.get("lines", {})) st.warning( f"⏸ Limit hit · {done_n2}/{len(lines)} lines saved\n" f"{_bgjob.get('status_msg','')[:200]}", icon="⏸") if st.button("🔔 Key/Provider ပြောင်းမည် (Dialog)", key="s5_open_dialog"): st.session_state["limit_hit_info"] = { "reason": _bgjob.get("status_msg",""), "key_label": _active_key_label(), "key_masked": _mask_key(_active_key()), "provider": prov, "model": model, } st.session_state["show_limit_dialog"] = True st.session_state["tx_checkpoint"] = cp st.rerun() if st.button("▶ ဘာသာပြန်ခြင်း စတင်မည် (Background)", key="s5_start", use_container_width=True): if not key.strip() or not model: st.error("API key / model မရှိပါ — Settings စစ်ဆေးပါ", icon="❌") else: # Create job — save subtitle data so History resume can restore it _jid = st.session_state.get("current_job_id") if not _jid or not load_job(_jid): raw_bytes = st.session_state.get("subtitle_raw", b"") or b"" _jid = create_job("translate", fname, language="my", model=model, provider=prov, extra={ "subtitle_b64": base64.b64encode(raw_bytes).decode(), "subtitle_format": fmt, "subtitle_lines": lines, "prompt_preset": st.session_state.get("prompt_preset", "adult_18"), "source_culture": st.session_state.get("source_culture", "western"), "custom_prompt": st.session_state.get("custom_prompt", ""), "proper_noun_style": st.session_state.get("proper_noun_style", "english"), }) st.session_state["current_job_id"] = _jid # Check for existing checkpoint (resume) cp_data = st.session_state.get("tx_checkpoint", {}) # Build system prompt from selected preset + culture sys_prompt = build_system_prompt( preset_key=st.session_state.get("prompt_preset", "adult_18"), culture_key=st.session_state.get("source_culture", "western"), custom_text=st.session_state.get("custom_prompt", ""), proper_noun_style=st.session_state.get("proper_noun_style", "english"), ) # Mark as running immediately update_job(_jid, status="running", progress=0.01, status_msg="Thread စတင်နေသည်…") # Launch background thread run_translation_bg( _jid, lines, glossary, provider=prov, model=model, api_key=key, fmt=fmt, ssafile=subs, filename=fname, system_prompt=sys_prompt, preset_key=st.session_state.get("prompt_preset", "adult_18"), ) # Stash checkpoint into job so thread can resume if cp_data: update_job(_jid, checkpoint=cp_data) st.info( "🔄 Background တွင် စတင်ပြီ — History page တွင် progress ကြည့်ပါ", icon="🔄") st.rerun() if st.button("← Glossary Review ပြန်သွားမည်", key="s5_back"): st.session_state["wizard_step"] = 4 st.rerun() st.markdown('', unsafe_allow_html=True) st.markdown('', unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # Translate page — wizard dispatcher # ───────────────────────────────────────────────────────────────────────────── def _page_translate() -> None: _render_header("Subtitle Translation", "Upload · Model · Glossary · Review · Translate") step = st.session_state.get("wizard_step", 1) _wizard_strip(step) # Last error display (collapsible) if st.session_state.get("last_error"): with st.expander("🐛 Last error (debug)", expanded=False): st.code(st.session_state["last_error"], language="python") if step == 1: _step1_upload() elif step == 2: _step2_model() elif step == 3: _step3_glossary() elif step == 4: _step4_review() elif step == 5: _step5_translate() else: st.error(f"Unknown step: {step}") st.session_state["wizard_step"] = 1 st.rerun() # ───────────────────────────────────────────────────────────────────────────── # Settings page # ───────────────────────────────────────────────────────────────────────────── def _page_settings() -> None: _render_header("Settings", "API Keys · Provider · Model Selection") st.markdown(""" """, unsafe_allow_html=True) # ── Auto-prefill from HF Secrets (once) ─────────────────────────── env_g = os.environ.get("GEMINI_API_KEY", "") env_or = os.environ.get("OPENROUTER_API_KEY", "") keys = st.session_state.get("gemini_api_keys", [""]) if env_g and keys == [""]: st.session_state["gemini_api_keys"] = [env_g] if env_or and not st.session_state.get("openrouter_api_key"): st.session_state["openrouter_api_key"] = env_or # ── Provider selector ────────────────────────────────────────────── st.markdown("#### Provider") c1, c2, _ = st.columns([1.2, 1.2, 3]) with c1: if st.button("🔵 Google Gemini", key="prov_g", use_container_width=True): st.session_state["selected_provider"] = "gemini" st.session_state["selected_model"] = None st.session_state["available_models"] = [] with c2: if st.button("⚫ OpenRouter", key="prov_or", use_container_width=True): st.session_state["selected_provider"] = "openrouter" st.session_state["selected_model"] = None st.session_state["available_models"] = [] provider = st.session_state["selected_provider"] prov_lbl = "Google Gemini" if provider == "gemini" else "OpenRouter" st.markdown( f'
● Active: {prov_lbl}
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # ── Gemini multi-key management ──────────────────────────────────── if provider == "gemini": st.markdown("#### Gemini API Keys") st.caption("aistudio.google.com/apikey — key များ ထပ်ထည့် နိုင်သည်") keys = list(st.session_state.get("gemini_api_keys", [""])) active_idx = st.session_state.get("gemini_key_idx", 0) active_idx = max(0, min(active_idx, len(keys)-1)) keys_changed = False for i, key_val in enumerate(keys): is_active = (i == active_idx) label_cls = "key-active" if is_active else "" col_lbl, col_input, col_act, col_del = st.columns([1, 4, 1.2, 0.7]) with col_lbl: badge = "●" if is_active else "○" st.markdown( f'
' + f'{badge} Key {i+1}
', unsafe_allow_html=True) with col_input: new_val = st.text_input( f"key_{i}", value=key_val, type="password", placeholder="AIza...", key=f"gkey_{i}", label_visibility="collapsed") if new_val != key_val: keys[i] = new_val keys_changed = True with col_act: if not is_active: if st.button("Use", key=f"gkey_use_{i}", use_container_width=True): st.session_state["gemini_key_idx"] = i st.session_state["available_models"] = [] st.session_state["selected_model"] = None st.rerun() else: st.markdown( '
Active
', unsafe_allow_html=True) with col_del: if len(keys) > 1: if st.button("✕", key=f"gkey_del_{i}", use_container_width=True): keys.pop(i) if active_idx >= len(keys): st.session_state["gemini_key_idx"] = len(keys) - 1 st.session_state["gemini_api_keys"] = keys st.rerun() if keys_changed: st.session_state["gemini_api_keys"] = keys if st.button("+ Gemini Key ထပ်ထည့်ရန်", key="gkey_add"): keys.append("") st.session_state["gemini_api_keys"] = keys st.rerun() cur_key = keys[active_idx] if keys else "" else: # OpenRouter single key st.markdown("#### OpenRouter API Key") st.caption("openrouter.ai/keys") ki = st.text_input("OR key", value=st.session_state.get("openrouter_api_key",""), type="password", placeholder="sk-or-…", key="or_key_in", label_visibility="collapsed") st.session_state["openrouter_api_key"] = ki cur_key = ki.strip() active_idx = 0 # ── Validate + Fetch ─────────────────────────────────────────────── st.markdown("
", unsafe_allow_html=True) b1, b2, _ = st.columns([1.4, 1.6, 3]) with b1: val_clicked = st.button("🔑 Key စစ်ဆေး", key="btn_val", use_container_width=True) with b2: fetch_clicked = st.button("🔄 Models ဆွဲယူ", key="btn_fetch", use_container_width=True) if val_clicked: if not cur_key.strip(): st.warning("API key ထည့်ပါ", icon="⚠️") else: with st.spinner("Validating…"): ok, msg = validate_api_key(provider, cur_key) (st.success if ok else st.error)(msg, icon="✅" if ok else "❌") if fetch_clicked: if not cur_key.strip(): st.warning("API key ထည့်ပါ", icon="⚠️") else: with st.spinner(f"{prov_lbl} models ဆွဲယူနေသည်…"): models = fetch_models(provider, cur_key) st.session_state["available_models"] = models st.session_state["selected_model"] = models[0] if models else None st.success(f"{len(models)} models", icon="✅") # ── Model selector ───────────────────────────────────────────────── st.markdown('
', unsafe_allow_html=True) st.markdown("#### Model ရွေးချယ်ရန်") avail = st.session_state.get("available_models") or ( ["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash","gemini-1.5-flash-8b"] if provider == "gemini" else ["google/gemini-2.0-flash-exp:free","meta-llama/llama-3.3-70b-instruct:free", "deepseek/deepseek-chat-v3-0324:free","mistralai/mistral-7b-instruct:free"] ) cur_model = st.session_state.get("selected_model") or avail[0] if cur_model not in avail: cur_model = avail[0] sel = st.selectbox("Model", avail, index=avail.index(cur_model), key="model_sel", label_visibility="collapsed") st.session_state["selected_model"] = sel # ── Summary ──────────────────────────────────────────────────────── st.markdown('
', unsafe_allow_html=True) masked = _mask_key(cur_key) kstat = ('✓ Set' if cur_key.strip() else '✗ Not set') key_count_note = "" if provider == "gemini": n_keys = len([k for k in st.session_state.get("gemini_api_keys",[]) if k.strip()]) key_count_note = f" ({n_keys} keys total)" st.markdown(f"""
Current Configuration
Provider {prov_lbl}{key_count_note}
Active Key {kstat} {masked} ({_active_key_label()})
Model {sel or "—"}
""", unsafe_allow_html=True) if cur_key.strip() and sel: st.success("Settings ပြည့်စုံပြီ — Translate page သို့ သွားနိုင်သည်", icon="✅") # ───────────────────────────────────────────────────────────────────────────── # Audio → SRT page # ───────────────────────────────────────────────────────────────────────────── def _page_audio_srt() -> None: _render_header("Audio → SRT", "Audio / Video ဖိုင်မှ Subtitle ထုတ်ရန်") st.markdown(""" """, unsafe_allow_html=True) st.markdown( '
' '🧠 Model: Whisper large-v3' '  ·  Browser ပိတ်/refresh ဖြစ်ပေမဲ့ background တွင် ဆက်လုပ်သည်' '
', unsafe_allow_html=True) # ── Active job polling (running in background) ───────────────────── active_jid = st.session_state.get("audio_job_id") if active_jid: job = load_job(active_jid) if job and job["status"] == "running": st.markdown(f'
', unsafe_allow_html=True) st.markdown( f'
🔄 Transcribing: {job["filename"]}
', unsafe_allow_html=True) pct = job.get("progress", 0.0) msg = job.get("status_msg", "") st.progress(pct, text=msg) st.markdown( f'
' f'Job ID: {active_jid} · Browser ပိတ်ထားနိုင်သည် — background တွင် ဆက်လုပ်မည်
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # Auto-refresh via HTML meta (no sleep — non-blocking) st.markdown('', unsafe_allow_html=True) if st.button("🔄 Status စစ်ဆေးမည်", key="audio_poll_refresh"): st.rerun() return if job and job["status"] == "completed": srt_bytes = load_result(active_jid) if srt_bytes: st.session_state["audio_srt_result"] = srt_bytes st.session_state["audio_srt_filename"] = job["filename"] # ── Show result if available ─────────────────────────────────────── srt_result = st.session_state.get("audio_srt_result") srt_fname = st.session_state.get("audio_srt_filename", "audio") if srt_result: out_name = os.path.splitext(srt_fname)[0] + ".srt" st.success(f"✅ Transcription ပြီးသည် — {out_name}", icon="🎉") st.download_button( label=f"⬇️ {out_name} Download", data=srt_result, file_name=out_name, mime="text/plain", use_container_width=True, key="audio_dl", ) with st.expander("📋 SRT Preview (ပထမ 30 ကြောင်း)", expanded=False): preview = srt_result.decode("utf-8", errors="replace") preview_text = "\n".join(preview.splitlines()[:60]) st.code(preview_text, language=None) st.markdown("
", unsafe_allow_html=True) if st.button("🌐 ဒီ SRT ကို မြန်မာဘာသာပြန်မည်", key="audio_to_translate"): st.session_state["audio_srt_to_translate"] = srt_result st.session_state["audio_srt_orig_name"] = out_name st.session_state["active_page"] = "translate" st.session_state["wizard_step"] = 1 st.rerun() col_x, _ = st.columns([1, 3]) with col_x: st.markdown('
', unsafe_allow_html=True) if st.button("🗑 Clear Result", key="audio_clear"): st.session_state["audio_srt_result"] = None st.session_state["audio_srt_filename"] = None st.session_state["audio_job_id"] = None st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # ── File upload + start ──────────────────────────────────────────── st.markdown("#### 📂 Audio / Video ဖိုင် တင်ရန်") uploaded = st.file_uploader( "audio", type=["mp3","mp4","wav","m4a","flac","ogg","webm","mkv","mka","aac","opus","wma"], label_visibility="collapsed", key="audio_uploader", ) lang_options = list(SUPPORTED_LANGUAGES.keys()) lang_labels = list(SUPPORTED_LANGUAGES.values()) lang_idx = st.selectbox( "ဘာသာစကား", options=range(len(lang_options)), format_func=lambda i: lang_labels[i], key="audio_lang_sel", ) selected_lang = lang_options[lang_idx] if uploaded is not None: file_bytes = uploaded.read() st.markdown( f'
' f'{uploaded.name} · {len(file_bytes)/1024/1024:.1f} MB
', unsafe_allow_html=True) if st.button("▶ Transcribe မည် (Background)", key="audio_start", use_container_width=True): job_id = create_job( job_type="audio_srt", filename=uploaded.name, language=selected_lang, model=MODEL, ) st.session_state["audio_job_id"] = job_id st.session_state["audio_srt_result"] = None st.session_state["audio_srt_filename"] = None # Launch background thread — survives browser disconnect run_transcription_bg(job_id, file_bytes, uploaded.name, selected_lang) st.info( "🔄 Background တွင် စတင်ပြီ — Browser ပိတ်ထားနိုင်သည်\\n" "Progress ကို History page တွင် ကြည့်ရှုနိုင်သည်", icon="🔄") st.rerun() st.markdown( '
' '💡 large-v3 model ပထမဆုံးအကြိမ် download ~1.5GB ကြာနိုင်သည်
', unsafe_allow_html=True) def _page_history() -> None: _render_header("History", "Translation & Transcription Jobs") st.markdown(""" """, unsafe_allow_html=True) # ── Controls row ─────────────────────────────────────────────────── col_filter, col_clear, _ = st.columns([1.5, 1.2, 3]) with col_filter: filter_type = st.selectbox( "Filter", ["all", "audio_srt", "translate"], format_func=lambda x: { "all": "All Jobs", "audio_srt": "🎙 Audio→SRT", "translate": "🌐 Translate", }[x], key="hist_filter", label_visibility="collapsed", ) with col_clear: st.markdown('
', unsafe_allow_html=True) if st.button("🗑 Clear All", key="hist_clear_all", use_container_width=True): n = clear_all_jobs() st.success(f"{n} jobs deleted") st.rerun() st.markdown('
', unsafe_allow_html=True) # ── Job list ─────────────────────────────────────────────────────── jobs = list_jobs(job_type=None if filter_type == "all" else filter_type) if not jobs: st.markdown( '
' + 'Jobs မရှိသေးပါ
', unsafe_allow_html=True) return st.markdown( f'
' + f'{len(jobs)} jobs
', unsafe_allow_html=True) for job in jobs: job_id = job["id"] status = job.get("status","?") jtype = job.get("type","?") fname = job.get("filename","?") model = job.get("model","") lang = job.get("language","") age = job_age_str(job) sicon = status_icon(status) scolor = status_color(status) pct = int(job.get("progress",0)*100) msg = job.get("status_msg","") err = job.get("error","") type_label = "🎙 Audio→SRT" if jtype == "audio_srt" else "🌐 Translate" with st.container(): st.markdown(f'
', unsafe_allow_html=True) # Title row col_info, col_actions = st.columns([4, 2]) with col_info: _gloss_count = len(job.get("extra",{}).get("approved_glossary",{})) _gloss_badge = ( f' · 📖 {_gloss_count} glossary terms' if _gloss_count > 0 else "" ) st.markdown( f'
{fname}
' + f'
{type_label} · {model} · {lang} · {age}{_gloss_badge}
' + f'
' + f'{sicon} {status.upper()} {pct}%
', unsafe_allow_html=True) if msg: st.markdown( f'
{msg}
', unsafe_allow_html=True) if err and status == "failed": st.markdown( f'
{err[:120]}
', unsafe_allow_html=True) with col_actions: # Download if complete if status == "completed" and job.get("has_result"): result_bytes = load_result(job_id) if result_bytes: # Preserve original format (ass/srt/vtt) out_name = output_filename(fname) st.download_button( label="⬇️ Download", data=result_bytes, file_name=out_name, mime="text/plain", key=f"dl_{job_id}", use_container_width=True, ) # Resume if paused (translate jobs) if status == "paused" and jtype == "translate": if st.button("▶ Resume", key=f"res_{job_id}", use_container_width=True): cp = job.get("checkpoint", {}) # ── Restore approved glossary (survives model/provider change) saved_glossary = load_glossary(job_id) if saved_glossary: st.session_state["glossary_edited"] = saved_glossary st.session_state["glossary_approved"] = True else: # Fallback: try to read from checkpoint cp_gloss = cp.get("glossary", {}) if cp_gloss: st.session_state["glossary_edited"] = cp_gloss st.session_state["glossary_approved"] = True # ── Restore checkpoint for translate_all resume st.session_state["tx_checkpoint"] = cp # ── Restore subtitle data (fixes "Subtitle မတင်ရသေးပါ" error) job_extra = job.get("extra", {}) sub_b64 = job_extra.get("subtitle_b64", "") sub_fmt = job_extra.get("subtitle_format", "srt") sub_lines = job_extra.get("subtitle_lines", []) sub_fname = job.get("filename", "output.srt") if sub_b64 and sub_lines: try: raw = base64.b64decode(sub_b64) subs_obj, _ = parse_subtitle(raw, sub_fname) st.session_state["subtitle_raw"] = raw st.session_state["subtitle_parsed"] = subs_obj st.session_state["subtitle_format"] = sub_fmt st.session_state["uploaded_file_name"] = sub_fname st.session_state["plain_lines"] = sub_lines except Exception as _e: st.warning(f"Subtitle restore မရပါ — ဖိုင်ပြန်တင်ပါ: {_e}") # ── Restore prompt preset / culture if job_extra.get("prompt_preset"): st.session_state["prompt_preset"] = job_extra["prompt_preset"] if job_extra.get("source_culture"): st.session_state["source_culture"] = job_extra["source_culture"] if job_extra.get("custom_prompt"): st.session_state["custom_prompt"] = job_extra["custom_prompt"] if job_extra.get("proper_noun_style"): st.session_state["proper_noun_style"] = job_extra["proper_noun_style"] # ── Set current job and navigate to step 5 st.session_state["current_job_id"] = job_id st.session_state["active_page"] = "translate" st.session_state["wizard_step"] = 5 st.rerun() # Delete button st.markdown('
', unsafe_allow_html=True) if st.button("✕", key=f"del_{job_id}", use_container_width=True): delete_job(job_id) st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) def _page_about() -> None: _render_header("About SubSync", "Myanmar Subtitle Translator · v1.0.0") st.markdown("""
SubSync ဆိုတာဘာလဲ

SubSync သည် .srt · .vtt · .ass subtitle files များကို သဘာဝကျသော မြန်မာဘာသာ သို့ ပြောင်းပေးသည့် tool ဖြစ်သည်။ AI + Human two-step glossary workflow ဖြင့် ကျွမ်းကျင်သော လူသားဘာသာပြန်နှင့် အနီးစပ်ဆုံး quality ရရှိရန် ဒီဇိုင်းဆွဲထားသည်။

Workflow
  1. Subtitle ဖိုင် (.srt / .vtt / .ass) upload တင်ရန်
  2. AI Provider (Gemini / OpenRouter) နှင့် Model ရွေးချယ်ရန်
  3. AI က Glossary (ဇာတ်ကောင်အမည်၊ နေရာ၊ jargon) auto-extract ထုတ်မည်
  4. Glossary ကို စစ်ဆေး၊ ပြင်ဆင်ပြီး Approve လုပ်ရန်
  5. 500-line chunks ဖြင့် context-aware translation လုပ်မည်
  6. မူရင်း format ပြောင်းလဲမှု မရှိဘဲ ဘာသာပြန် ဖိုင် download ရယူရန်
Supported Providers
HF Space Secrets
SUBTITLE_USER Login username
SUBTITLE_PASS Login password
GEMINI_API_KEY Gemini key (optional pre-fill)
OPENROUTER_API_KEY OpenRouter key (optional pre-fill)
""", unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # Entry point # ───────────────────────────────────────────────────────────────────────────── def main() -> None: _init_session() _inject_css() if not is_authenticated(): render_login_page() return page = st.session_state.get("active_page", "translate") if page == "translate": _page_translate() elif page == "audio_srt": _page_audio_srt() elif page == "history": _page_history() elif page == "settings": _page_settings() elif page == "about": _page_about() if __name__ == "__main__": main()