billingsmoore's picture
Log translate model/prompt, link OpenRouter key signup and leaderboard
23eb152
Raw
History Blame Contribute Delete
9.12 kB
"""SimpleTranslationUI — plain-text Tibetan Buddhist translation editor."""
from dotenv import load_dotenv
load_dotenv()
import gradio as gr
from engine import DEFAULT_MODEL, list_model_choices
from styles import CSS
from handlers import (
MAX_SLOTS,
_handle_cancel_translate,
_handle_download_click,
_handle_get_files,
_handle_next,
_handle_prev,
_handle_save,
_load_resume_json,
_load_source,
_make_state,
_read_prompt,
_reset_prompt,
_save_prompt,
_set_usage_tracking,
_translate_all,
_translate_one,
)
def build_app() -> gr.Blocks:
with gr.Blocks(title="SimpleTranslationUI") as demo:
app_state = gr.State(_make_state())
gr.Markdown(
"# SimpleTranslationUI\nTibetan Buddhist Translation Editor\n\n"
"**How to use:** Upload a source document below — it's automatically split into "
"segments (by paragraph, then by sentence-ending shad marks or whitespace for long "
"paragraphs). Click **Translate All** to translate every segment, or translate one "
"segment at a time with its own **Translate** button. Edit any translation inline, "
"then **Download** the result as text, Word, or JSON.\n\n"
"**Choosing a model:** By default (no API key) translation runs on a local CPU "
"model ([billingsmoore/mlotsawa-ground-base]"
"(https://huggingface.co/billingsmoore/mlotsawa-ground-base)) — free, runs on this "
"machine, but a plain translation model, not an instruction-following AI (it ignores "
"the translation prompt below) and generally lower translation quality. For higher-quality, instruction-"
"following translation, add an [**OpenRouter API key**](https://openrouter.ai/keys) in "
"Settings and pick a model from the dropdown — this calls OpenRouter's API (usage is "
"billed to your OpenRouter account) and uses the editable translation prompt. See the "
"[Tibetan AI Leaderboard](https://huggingface.co/spaces/khyentsevision/tibetan-ai-leaderboard) "
"for how different models perform on Tibetan translation evaluations.\n\n"
"Usage of this space may be monitored for research purposes. You can opt out of this "
"in Settings."
)
with gr.Row():
source_input = gr.File(
label="Source Document (.txt, .docx, .pdf)",
file_types=[".txt", ".docx", ".pdf"],
scale=1,
)
resume_input = gr.File(
label="Resume: Previously downloaded JSON (optional)",
file_types=[".json"],
scale=1,
)
with gr.Accordion("Settings", open=False):
usage_tracking_checkbox = gr.Checkbox(
label="Allow usage tracking for research purposes",
value=True,
)
with gr.Row():
openrouter_api_key = gr.Textbox(
label="OpenRouter API Key (optional)",
placeholder="sk-or-… — leave blank to use the local CPU model instead",
type="password",
scale=2,
)
openrouter_model = gr.Dropdown(
choices=list_model_choices(), value=DEFAULT_MODEL,
label="OpenRouter Model (only used if a key is given)",
info="A few recommended models are pinned at the top; the rest of "
"OpenRouter's catalog is below — type to filter.",
scale=1,
)
gr.Markdown(
"### Translation Prompt\n"
"Only used when translating with OpenRouter — the local CPU fallback "
"(billingsmoore/mlotsawa-ground-base) is a plain translation model and ignores this. "
"Saved directly to `translation_prompt.txt` in this app's folder."
)
prompt_box = gr.Textbox(label="Translation Prompt", lines=12, value=_read_prompt())
with gr.Row():
save_prompt_btn = gr.Button("Save", variant="primary", size="sm", scale=0, min_width=80)
reset_prompt_btn = gr.Button("Reset to Default", size="sm", scale=0, min_width=130)
prompt_status = gr.Markdown("")
with gr.Row():
translate_all_btn = gr.Button("Translate All", variant="primary", scale=2)
cancel_btn = gr.Button("Cancel", variant="stop", scale=1)
status_output = gr.Textbox(label="Status", lines=2, interactive=False, visible=False)
with gr.Column(visible=False) as editor_col:
gr.Markdown("### Segments")
with gr.Row():
prev_btn = gr.Button("◀ Previous", interactive=False)
nav_label = gr.HTML("")
next_btn = gr.Button("Next ▶", interactive=False)
with gr.Row():
save_btn = gr.Button("Save", variant="secondary")
download_btn = gr.Button("Download", variant="secondary")
with gr.Row():
download_checklist = gr.CheckboxGroup(
label="Files to include", choices=[], value=[], visible=False, scale=2,
)
get_files_btn = gr.Button("Get Files", variant="primary", scale=0, min_width=100, visible=False)
download_zip = gr.File(label="Download", visible=False, interactive=False)
slot_groups, slot_sources, slot_targets, slot_translate_btns = [], [], [], []
for _ in range(MAX_SLOTS):
with gr.Group(visible=False) as grp:
with gr.Row():
tlb = gr.Button("Translate", size="sm", scale=0, min_width=90)
with gr.Row(equal_height=True):
src = gr.Textbox(label="Source", lines=3, scale=1, elem_classes=["src-box"])
tgt = gr.Textbox(label="Translation", lines=3, scale=1, elem_classes=["tgt-box"])
slot_groups.append(grp)
slot_sources.append(src)
slot_targets.append(tgt)
slot_translate_btns.append(tlb)
page_outputs = [
app_state, nav_label, prev_btn, next_btn,
*slot_groups, *slot_sources, *slot_targets,
]
edit_inputs = [app_state, *slot_sources, *slot_targets]
load_outputs = [*page_outputs, editor_col]
translate_all_outputs = [*page_outputs, status_output]
source_input.change(fn=_load_source, inputs=[source_input, app_state], outputs=load_outputs)
resume_input.change(fn=_load_resume_json, inputs=[resume_input, app_state], outputs=page_outputs)
prev_btn.click(_handle_prev, inputs=edit_inputs, outputs=page_outputs)
next_btn.click(_handle_next, inputs=edit_inputs, outputs=page_outputs)
translate_event = translate_all_btn.click(
_translate_all,
inputs=[app_state, openrouter_api_key, openrouter_model, *slot_sources, *slot_targets],
outputs=translate_all_outputs,
)
cancel_btn.click(_handle_cancel_translate, inputs=[app_state], outputs=[app_state], cancels=[translate_event])
save_btn.click(_handle_save, inputs=edit_inputs, outputs=[app_state, status_output])
download_btn.click(
_handle_download_click,
inputs=edit_inputs,
outputs=[app_state, download_checklist, get_files_btn, download_zip],
)
get_files_btn.click(
_handle_get_files,
inputs=[app_state, download_checklist],
outputs=[download_zip, download_checklist, get_files_btn],
)
for i in range(MAX_SLOTS):
slot_sources[i].blur(_handle_save, inputs=edit_inputs, outputs=[app_state, status_output])
slot_targets[i].blur(_handle_save, inputs=edit_inputs, outputs=[app_state, status_output])
for i in range(MAX_SLOTS):
slot_translate_btns[i].click(
fn=lambda: gr.update(value="", placeholder="Translating…"),
inputs=[],
outputs=[slot_targets[i]],
).then(
fn=lambda state, source, key, model, i=i: _translate_one(state, i, source, key, model),
inputs=[app_state, slot_sources[i], openrouter_api_key, openrouter_model],
outputs=[app_state, slot_targets[i]],
)
save_prompt_btn.click(_save_prompt, inputs=[app_state, prompt_box], outputs=[app_state, prompt_status])
reset_prompt_btn.click(_reset_prompt, inputs=[app_state], outputs=[app_state, prompt_box, prompt_status])
usage_tracking_checkbox.change(
_set_usage_tracking, inputs=[app_state, usage_tracking_checkbox], outputs=[app_state],
)
return demo
demo = build_app()
if __name__ == "__main__":
demo.launch(css=CSS, ssr_mode=False, server_name="0.0.0.0", server_port=7860)
else:
demo.css = CSS