Damonar commited on
Commit
5fb554d
·
verified ·
1 Parent(s): c90d3fc

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +237 -0
app.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import time
4
+
5
+ import spaces
6
+ import torch
7
+ import gradio as gr
8
+ import pysubs2
9
+ from transformers import AutoTokenizer, AutoModelForCausalLM
10
+
11
+ # ── تنظیمات ──────────────────────────────────────────────
12
+ MODEL_ID = "google/translategemma-4b-it"
13
+ SRC_LANG = "en"
14
+ TGT_LANG = "fa"
15
+ BATCH_SIZE = 12
16
+ MAX_NEW_TOKENS = 1536
17
+
18
+ HF_TOKEN = os.environ.get("HF_TOKEN")
19
+
20
+ # ── بارگذاری مدل (سطح global، طبق الزام ZeroGPU) ────────
21
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
22
+ model = AutoModelForCausalLM.from_pretrained(
23
+ MODEL_ID,
24
+ torch_dtype=torch.bfloat16,
25
+ token=HF_TOKEN,
26
+ )
27
+ model.to("cuda")
28
+ model.eval()
29
+
30
+
31
+ # ── ساخت پرامپت و پارس خروجی ─────────────────────────────
32
+ def build_batch(subs, start, end):
33
+ chunks = []
34
+ for n, idx in enumerate(range(start, end), 1):
35
+ chunks.append(f"SEGMENT {n}:\n{subs[idx].text.strip()}")
36
+ return "\n\n".join(chunks)
37
+
38
+
39
+ def build_prompt(batch_text, count):
40
+ return f"""<start_of_turn>user
41
+ شما یک مترجم حرفه‌ای زیرنویس انگلیسی به فارسی هستید.
42
+
43
+ متن زیر شامل دقیقاً {count} بخش (SEGMENT) پیاپی از یک گفتگوی طبیعی است. هر بخش را با توجه به بافت کل گفتگو، به فارسی محاوره‌ای، خودمونی و روان ترجمه کن — مثل زبان صحبت روزمره، نه فارسی رسمی و کتابی.
44
+
45
+ قوانین مهم:
46
+ - هر SEGMENT را جداگانه و دقیقاً به همان ترتیب ترجمه کن.
47
+ - هرگز دو یا چند SEGMENT را با هم ادغام نکن و هیچ SEGMENT را نصف نکن.
48
+ - برچسب‌های SEGMENT فقط برای هم‌ترازی هستند؛ آن‌ها را ترجمه نکن.
49
+ - نام‌های خاص، اصطلاحات فنی و کلمات انگلیسی رایج (مثل اسم نرم‌افزارها) را به همون شکل انگلیسی نگه دار.
50
+ - حتی اگر یک SEGMENT وسط جمله (بدون نقطه‌ی پایان) قطع شده باشد، فقط همان کلمات را ترجمه کن؛ هرگز ادامه‌ی جمله یا محتوای بعدی را حدس نزن یا اضافه نکن.
51
+ - فقط خروجی خواسته‌شده را بده، بدون توضیح اضافه.
52
+
53
+ خروجی را دقیقاً به این شکل بده:
54
+ SEGMENT 1: <ترجمه فارسی>
55
+ SEGMENT 2: <ترجمه فارسی>
56
+ ...
57
+ SEGMENT {count}: <ترجمه فارسی>
58
+
59
+ گفتگو:
60
+ {batch_text}<end_of_turn>
61
+ <start_of_turn>model
62
+ """
63
+
64
+
65
+ def parse_output(text, count):
66
+ text = (text or "").strip()
67
+ text = re.sub(r"```(?:text|plaintext|srt)?", "", text, flags=re.I).replace("```", "").strip()
68
+ text = re.sub(
69
+ r"(?im)^\s*\[?\s*SEGMENT\s+(\d+)\s*\]?\s*[::-]\s*",
70
+ lambda m: f"\n@@SEGMENT_{m.group(1)}@@\n",
71
+ text
72
+ )
73
+ parts = re.split(r"\n@@SEGMENT_(\d+)@@\n", "\n" + text + "\n")
74
+ found = {}
75
+ for i in range(1, len(parts), 2):
76
+ try:
77
+ n = int(parts[i])
78
+ value = parts[i + 1].strip()
79
+ if value:
80
+ found[n] = value
81
+ except Exception:
82
+ pass
83
+
84
+ if count == 1 and not found:
85
+ return [text]
86
+
87
+ if len(found) != count or any(i not in found for i in range(1, count + 1)):
88
+ raise ValueError(f"Expected {count} segments, got {len(found)}")
89
+ return [found[i] for i in range(1, count + 1)]
90
+
91
+
92
+ # ── تابع GPU (هر batch یک فراخوانی کوتاه است) ────────────
93
+ def compute_max_new_tokens(batch_text):
94
+ # سقف پویا: حدود ۸ توکن به‌ازای هر کلمه‌ی ورودی (کافی برای ترجمه + یک حاشیه‌ی امن)
95
+ # این کار مانع می‌شود که مدل برای یک تکه‌ی ناقص (بدون نقطه‌ی پایان)
96
+ # به‌جای ترجمه‌ی همان تکه، ادامه‌ی کل جمله/متن را حدس بزند و تولید کند.
97
+ word_count = max(1, len(batch_text.split()))
98
+ return max(48, min(MAX_NEW_TOKENS, word_count * 8))
99
+
100
+
101
+ @spaces.GPU(duration=60)
102
+ def translate_batch(batch_text, count):
103
+ prompt = build_prompt(batch_text, count)
104
+ inputs = tokenizer(prompt, return_tensors="pt")
105
+ inputs = {k: v.to(model.device) for k, v in inputs.items()}
106
+
107
+ dynamic_max_tokens = compute_max_new_tokens(batch_text)
108
+
109
+ with torch.inference_mode():
110
+ output = model.generate(
111
+ **inputs,
112
+ max_new_tokens=dynamic_max_tokens,
113
+ do_sample=False,
114
+ pad_token_id=tokenizer.eos_token_id,
115
+ )
116
+
117
+ generated = output[0][inputs["input_ids"].shape[-1]:]
118
+ answer = tokenizer.decode(generated, skip_special_tokens=True)
119
+ return parse_output(answer, count)
120
+
121
+
122
+ def translate_batch_retry(subs, start, end):
123
+ batch_text = build_batch(subs, start, end)
124
+ count = end - start
125
+ try:
126
+ return translate_batch(batch_text, count)
127
+ except Exception as e:
128
+ print(f"⚠️ Batch alignment failed: {e}")
129
+ print("🔁 Retrying segment-by-segment...")
130
+ result = []
131
+ for idx in range(start, end):
132
+ try:
133
+ single = build_batch(subs, idx, idx + 1)
134
+ result.extend(translate_batch(single, 1))
135
+ except Exception as e2:
136
+ print(f"❌ Segment {idx + 1}: {e2}")
137
+ result.append(subs[idx].text.strip())
138
+ return result
139
+
140
+
141
+ def fix_bidi(text):
142
+ """دور کلمات/عبارات لاتین و عددی، کاراکتر Unicode Isolate می‌ذاره
143
+ تا در متن راست‌چین، جهت‌شون به‌هم نریزه."""
144
+ LRI = "\u2066" # Left-to-Right Isolate
145
+ PDI = "\u2069" # Pop Directional Isolate
146
+ pattern = re.compile(r'[A-Za-z0-9][A-Za-z0-9 _\-\.]*[A-Za-z0-9]|[A-Za-z0-9]')
147
+ return pattern.sub(lambda m: f"{LRI}{m.group(0)}{PDI}", text)
148
+
149
+
150
+ def translate_srt_file(input_path, progress=gr.Progress()):
151
+ subs = pysubs2.load(input_path, encoding="utf-8")
152
+ total = len(subs)
153
+ translations = [""] * total
154
+ started = time.time()
155
+
156
+ for start in range(0, total, BATCH_SIZE):
157
+ end = min(total, start + BATCH_SIZE)
158
+ translations[start:end] = translate_batch_retry(subs, start, end)
159
+
160
+ elapsed = time.time() - started
161
+ print(f"✅ {end}/{total} | {end/elapsed:.2f} segments/sec", flush=True)
162
+ if progress:
163
+ progress(end / total, desc=f"ترجمه {end}/{total}")
164
+
165
+ output = pysubs2.SSAFile()
166
+ preview = []
167
+
168
+ for i, sub in enumerate(subs):
169
+ original = sub.text.strip()
170
+ translated = fix_bidi(translations[i].strip()) if original else ""
171
+
172
+ output.append(pysubs2.SSAEvent(
173
+ start=sub.start,
174
+ end=sub.end,
175
+ text=translated
176
+ ))
177
+
178
+ preview.append({
179
+ "number": i + 1,
180
+ "english": original,
181
+ "persian": translated
182
+ })
183
+
184
+ base = os.path.splitext(os.path.basename(input_path))[0]
185
+ output_path = f"/tmp/{base}_FA.srt"
186
+ output.save(output_path, encoding="utf-8")
187
+
188
+ print(f"✅ Finished in {(time.time()-started)/60:.1f} minutes")
189
+ return output_path, preview
190
+
191
+
192
+ def ui_translate(file, progress=gr.Progress()):
193
+ if file is None:
194
+ raise gr.Error("لطفاً یک فایل SRT انتخاب کنید.")
195
+
196
+ output_path, data = translate_srt_file(file, progress)
197
+
198
+ blocks = []
199
+ for x in data[:50]:
200
+ blocks.append(
201
+ f"### #{x['number']}\n\n"
202
+ f"🇬🇧 **English**\n\n{x['english']}\n\n"
203
+ f"🇮🇷 **فارسی**\n\n{x['persian']}\n\n---"
204
+ )
205
+ preview = "\n\n".join(blocks)
206
+ if len(data) > 50:
207
+ preview += f"\n\n> نمایش ۵۰ مورد اول از {len(data)} subtitle."
208
+ return output_path, preview
209
+
210
+
211
+ with gr.Blocks(title="TranslateGemma 4B SRT Translator") as demo:
212
+ gr.Markdown("""
213
+ # 🎬 English → Persian SRT Translator
214
+ ### Powered by TranslateGemma 4B
215
+
216
+ مدل timestamp و شماره SRT را نمی‌بیند؛ فقط متن تمیز و segmentهای context-aware را ترجمه می‌کند.
217
+ """)
218
+
219
+ with gr.Row():
220
+ input_file = gr.File(
221
+ label="📂 English SRT",
222
+ file_types=[".srt"],
223
+ type="filepath"
224
+ )
225
+ output_file = gr.File(label="📥 Persian SRT")
226
+
227
+ translate_button = gr.Button("🚀 شروع ترجمه", variant="primary", size="lg")
228
+ gr.Markdown("## 👁️ پیش‌نمایش")
229
+ preview = gr.Markdown()
230
+
231
+ translate_button.click(
232
+ fn=ui_translate,
233
+ inputs=input_file,
234
+ outputs=[output_file, preview]
235
+ )
236
+
237
+ demo.queue(max_size=5).launch()