File size: 9,121 Bytes
88a619b
 
 
 
 
 
36fc86c
88a619b
 
 
 
04721fb
88a619b
 
 
 
 
 
 
 
 
 
 
04721fb
88a619b
 
 
 
 
 
 
 
 
c75b0b8
 
 
 
 
 
 
448481b
 
 
 
 
23eb152
 
 
 
 
04721fb
 
c75b0b8
88a619b
 
 
 
 
 
 
 
 
 
 
 
 
 
04721fb
 
 
 
88a619b
36fc86c
 
 
88a619b
 
 
36fc86c
 
 
 
 
 
88a619b
 
 
 
36fc86c
88a619b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f944367
88a619b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36fc86c
88a619b
 
04721fb
88a619b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36fc86c
88a619b
 
 
04721fb
 
 
 
 
 
88a619b
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""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