Spaces:
Running on Zero
Running on Zero
| """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 | |