"""
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
- Subtitle ဖိုင် (.srt / .vtt / .ass) upload တင်ရန်
- AI Provider (Gemini / OpenRouter) နှင့် Model ရွေးချယ်ရန်
- AI က Glossary (ဇာတ်ကောင်အမည်၊ နေရာ၊ jargon) auto-extract ထုတ်မည်
- Glossary ကို စစ်ဆေး၊ ပြင်ဆင်ပြီး Approve လုပ်ရန်
- 500-line chunks ဖြင့် context-aware translation လုပ်မည်
- မူရင်း format ပြောင်းလဲမှု မရှိဘဲ ဘာသာပြန် ဖိုင် download ရယူရန်
Supported Providers
- Google Gemini
— gemini-2.0-flash / 1.5-pro / 1.5-flash series
- OpenRouter
— GPT-4o, Claude 3.5, Llama 3, DeepSeek နှင့် 300+ models
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()