File size: 9,480 Bytes
1f39aa6
0dc9d0c
67a163c
1f39aa6
3fdc7b6
143e083
 
0dc9d0c
 
143e083
0dc9d0c
1f39aa6
43b39f0
1f39aa6
0dc9d0c
c571a45
0dc9d0c
c571a45
0dc9d0c
43b39f0
1f39aa6
0dc9d0c
 
 
de4ab0d
0dc9d0c
43b39f0
c571a45
0dc9d0c
 
 
 
 
 
3fdc7b6
1f39aa6
0dc9d0c
 
 
de4ab0d
151fa6d
1f39aa6
 
e0dc6e1
0dc9d0c
 
 
 
 
1f39aa6
3fdc7b6
1f39aa6
8d0070f
 
 
 
 
 
 
0dc9d0c
 
e0dc6e1
0dc9d0c
 
 
 
1f39aa6
43b39f0
8d0070f
 
 
7d583b2
8d0070f
 
 
 
 
 
 
0dc9d0c
 
 
 
 
 
143e083
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63c966a
 
 
 
 
 
 
 
 
4b929eb
 
 
 
 
 
 
 
63c966a
4b929eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63c966a
 
4b929eb
 
613b55a
0dc9d0c
 
dde491e
02f6bf9
 
67a163c
02f6bf9
 
 
 
3872c0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9780ff0
 
 
 
 
 
3872c0b
 
 
c31dbc6
 
3872c0b
 
 
c31dbc6
3872c0b
0dc9d0c
613b55a
 
 
 
 
 
 
dde491e
0dc9d0c
 
 
 
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""
Translation interface using the MADLAD-400 3B model.
Translates English text to 22 production-ready languages from the MADLAD-400 paper.
"""

import csv
import tempfile
import warnings
from functools import lru_cache
from pathlib import Path

import gradio as gr
import spaces
import torch
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer

from langmap.langid_mapping import langid_to_language

MODEL_NAME = "google/madlad400-3b-mt"


def _get_device() -> torch.device:
    if torch.cuda.is_available():
        return torch.device("cuda")
    warnings.warn("No GPU available. Running on CPU — translation will be slow.", stacklevel=2)
    return torch.device("cpu")


@lru_cache(maxsize=1)
def _load_tokenizer() -> AutoTokenizer:
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=True)
    if tokenizer is None:
        raise RuntimeError(f"Failed to load tokenizer for {MODEL_NAME}")
    return tokenizer


@lru_cache(maxsize=1)
def _load_model() -> AutoModelForSeq2SeqLM:
    device = _get_device()
    dtype = torch.float16 if device.type == "cuda" else torch.float32
    return AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME, dtype=dtype).to(device)


@lru_cache(maxsize=1)
def _build_language_mappings() -> tuple[dict[str, str], list[str]]:
    tokenizer = _load_tokenizer()
    vocab = tokenizer.get_vocab()
    name_to_code = {v: k for k, v in langid_to_language.items() if k in vocab}
    return name_to_code, sorted(name_to_code.keys())


@spaces.GPU
def translate(
    text: str,
    target_language_name: str,
    max_new_tokens: int = 512,
    num_beams: int = 1,
    temperature: float = 1.0,
) -> str:
    tokenizer = _load_tokenizer()
    model = _load_model()
    device = model.device

    name_to_code, _ = _build_language_mappings()
    target_code = name_to_code.get(target_language_name)
    if target_code is None:
        raise ValueError(f"Unsupported language: {target_language_name}")

    if num_beams > 1 and temperature != 1.0:
        gr.Info("Temperature has no effect when beam search is enabled (num_beams > 1).")

    input_ids = tokenizer(target_code + " " + text, return_tensors="pt").input_ids.to(device)

    generate_kwargs: dict = {"input_ids": input_ids, "max_new_tokens": max_new_tokens, "num_beams": num_beams}
    if num_beams == 1:
        generate_kwargs["do_sample"] = True
        generate_kwargs["temperature"] = temperature

    outputs = model.generate(**generate_kwargs)
    result = tokenizer.decode(outputs[0], skip_special_tokens=True)
    if not isinstance(result, str):
        raise TypeError(f"Expected str from decode, got {type(result)}")
    return result


def _parse_input_file(file_path: str) -> tuple[list[str], list[dict] | None]:
    """Parse a .txt or .csv file into a list of texts to translate.

    Returns (texts, original_data). original_data is None for .txt files,
    or a list of row dicts for .csv files (used to preserve extra columns in output).
    """
    path = Path(file_path)
    ext = path.suffix.lower()

    if ext == ".txt":
        texts = [line for line in path.read_text().splitlines() if line.strip()]
        return texts, None

    if ext == ".csv":
        with open(path, newline="") as f:
            reader = csv.DictReader(f)
            if reader.fieldnames is None or "text" not in reader.fieldnames:
                raise ValueError("CSV file must have a 'text' column.")
            rows = list(reader)
        texts = [row["text"] for row in rows]
        return texts, rows

    raise ValueError(f"Unsupported file type: {ext}. Use .txt or .csv.")


def _write_output_file(translations: list[str], original_data: list[dict] | None, ext: str) -> str:
    """Write translation results to a temp file and return its path."""
    if ext == ".txt":
        f = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False)
        f.write("\n".join(translations))
        f.close()
        return f.name

    # CSV: append translation column to original data
    f = tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="")
    assert original_data is not None
    fieldnames = list(original_data[0].keys()) + ["translation"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    for row, translation in zip(original_data, translations):
        row["translation"] = translation
        writer.writerow(row)
    f.close()
    return f.name


def _build_table_data(texts: list[str], translations: list[str], original_data: list[dict] | None) -> dict:
    """Build a dict with headers and data for gr.Dataframe display."""
    if original_data is None:
        return {"headers": ["source", "translation"], "data": [[t, tr] for t, tr in zip(texts, translations)]}
    headers = list(original_data[0].keys()) + ["translation"]
    data = [[row[k] for k in original_data[0].keys()] + [tr] for row, tr in zip(original_data, translations)]
    return {"headers": headers, "data": data}


@spaces.GPU
def translate_batch(
    file,
    target_language_name: str,
    max_new_tokens: int = 512,
    num_beams: int = 1,
    temperature: float = 1.0,
    progress=gr.Progress(),
) -> tuple[dict, str]:
    if file is None:
        raise gr.Error("Please upload a file first.")

    file_path = file.name if hasattr(file, "name") else str(file)
    ext = Path(file_path).suffix.lower()
    texts, original_data = _parse_input_file(file_path)

    if not texts:
        raise gr.Error("File is empty or contains no text to translate.")

    if num_beams > 1 and temperature != 1.0:
        gr.Info("Temperature has no effect when beam search is enabled (num_beams > 1).")

    translations = []
    for i, text in enumerate(progress.tqdm(texts, desc="Translating")):
        result = translate(text, target_language_name, max_new_tokens, num_beams, temperature)
        translations.append(result)

    output_path = _write_output_file(translations, original_data, ext)
    table_data = _build_table_data(texts, translations, original_data)
    return table_data, output_path


def _build_demo() -> gr.Blocks:
    _, language_names = _build_language_mappings()

    with gr.Blocks(title="MADLAD-400 Translation") as demo:
        gr.Markdown(
            "# MADLAD-400 Translation\n"
            "Translate English into 22 production-ready languages using Google's MADLAD-400 3B model. "
            "[Paper](https://arxiv.org/pdf/2309.04662)"
        )

        with gr.Row():
            # --- Sidebar ---
            with gr.Column(scale=1, min_width=250):
                gr.Markdown("### Settings")

                target_language = gr.Dropdown(
                    choices=language_names,
                    value="French",
                    label="Target language",
                )
                max_new_tokens = gr.Slider(minimum=1, maximum=1024, value=512, step=1, label="Max new tokens")
                num_beams = gr.Slider(minimum=1, maximum=10, value=1, step=1, label="Num beams")
                temperature = gr.Slider(minimum=0.1, maximum=2.0, value=1.0, step=0.1, label="Temperature")

            # --- Main area ---
            with gr.Column(scale=3):
                with gr.Tab("Single"):
                    with gr.Row():
                        input_text = gr.Textbox(
                            label="English",
                            placeholder="Enter English text here",
                            lines=4,
                        )
                        output_text = gr.Textbox(
                            label="Translation",
                            lines=4,
                            buttons=["copy"],
                            interactive=False,
                        )

                    single_btn = gr.Button("Translate", variant="primary")

                    single_btn.click(
                        fn=translate,
                        inputs=[input_text, target_language, max_new_tokens, num_beams, temperature],
                        outputs=output_text,
                    )

                    gr.Examples(
                        examples=[
                            ["Hello, how are you today?", "French"],
                            ["The weather is beautiful.", "Spanish"],
                            ["Thank you very much.", "German"],
                            ["Where is the train station?", "Portuguese"],
                        ],
                        inputs=[input_text, target_language],
                    )

                with gr.Tab("Batch"):
                    with gr.Row():
                        batch_input = gr.File(
                            label="Upload file (.txt, one sentence per line, or .csv with 'text' column)",
                            file_types=[".txt", ".csv"],
                        )
                        batch_output = gr.File(label="Download translations", interactive=False)

                    batch_btn = gr.Button("Translate", variant="primary")

                    batch_preview = gr.Dataframe(label="Preview", interactive=False)

                    batch_btn.click(
                        fn=translate_batch,
                        inputs=[batch_input, target_language, max_new_tokens, num_beams, temperature],
                        outputs=[batch_preview, batch_output],
                    )

    return demo


demo = _build_demo()


def main() -> None:
    demo.launch(theme=gr.themes.Soft())


if __name__ == "__main__":
    main()