Sithum Sathsara commited on
Commit
22b97e4
Β·
1 Parent(s): 4fa615e
Files changed (1) hide show
  1. app.py +553 -30
app.py CHANGED
@@ -1,9 +1,11 @@
1
  import os
2
  import tempfile
3
  import pysrt
 
4
  from fastapi import FastAPI, File, UploadFile, HTTPException
5
- from fastapi.responses import FileResponse, HTMLResponse
6
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
 
7
 
8
  # Use environment-defined cache or fallback to app-relative path
9
  CACHE = os.getenv("HF_HOME", os.getenv("TRANSFORMERS_CACHE", "/app/.cache/huggingface"))
@@ -14,59 +16,580 @@ model_name = "zaanind/nllb-ensi-v1-tuning"
14
  tokenizer = AutoTokenizer.from_pretrained(model_name)
15
  model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
16
 
17
-
18
-
19
- # ... rest of your code (homepage and translate endpoints) remains unchanged ...
20
- # ─── FastAPI App ──────────────────────────────────────────────────────────
21
  app = FastAPI(title="Sinhala Subtitle Translator")
22
 
23
- # ─── UI Endpoint ──────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  @app.get("/", response_class=HTMLResponse)
25
  async def homepage():
26
  return """
27
- <html>
28
- <head>
 
 
 
29
  <title>Sinhala Subtitle Translator</title>
30
- </head>
31
- <body style="font-family: sans-serif; max-width: 600px; margin: auto; padding: 2rem;">
32
- <h1>🎬 Sinhala Subtitle Translator</h1>
33
- <p>Upload an English <code>.srt</code> file to translate it into Sinhala.</p>
34
- <form action="/translate/" enctype="multipart/form-data" method="post">
35
- <input name="file" type="file" accept=".srt" required />
36
- <button type="submit" style="margin-top:1rem; padding:0.5rem 1rem;">Translate</button>
37
- </form>
38
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </html>
40
  """
41
 
42
- # ─── Translation Endpoint ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  @app.post("/translate/")
44
  async def translate_srt(file: UploadFile = File(...)):
45
  if not file.filename.lower().endswith(".srt"):
46
  raise HTTPException(status_code=400, detail="Please upload a .srt file")
47
 
 
 
 
 
 
48
  # Save upload
49
  tmp_in = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
50
- tmp_in.write(await file.read())
 
51
  tmp_in.flush()
52
 
53
- # Read & translate
54
  subs = pysrt.open(tmp_in.name)
55
- for sub in subs:
56
- inputs = tokenizer(sub.text, return_tensors="pt")
57
- out = model.generate(
58
- **inputs,
59
- forced_bos_token_id=tokenizer.lang_code_to_id["sin_Sinh"]
60
- )
61
- sub.text = tokenizer.decode(out[0], skip_special_tokens=True)
 
 
 
 
62
 
63
  # Write translated file
64
  tmp_out = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
65
  subs.save(tmp_out.name, encoding="utf-8")
66
-
67
- # Return it so browser auto-downloads
68
  return FileResponse(
69
  tmp_out.name,
70
  media_type="text/plain",
71
  filename="translated_si.srt"
72
- )
 
1
  import os
2
  import tempfile
3
  import pysrt
4
+ import time
5
  from fastapi import FastAPI, File, UploadFile, HTTPException
6
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
7
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
8
+ from typing import List, Dict
9
 
10
  # Use environment-defined cache or fallback to app-relative path
11
  CACHE = os.getenv("HF_HOME", os.getenv("TRANSFORMERS_CACHE", "/app/.cache/huggingface"))
 
16
  tokenizer = AutoTokenizer.from_pretrained(model_name)
17
  model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
18
 
 
 
 
 
19
  app = FastAPI(title="Sinhala Subtitle Translator")
20
 
21
+ # In-memory store for translation progress
22
+ translation_progress: Dict[str, int] = {}
23
+ translation_status: Dict[str, str] = {}
24
+
25
+ def translate_subtitle(sub: pysrt.SubRipItem, task_id: str):
26
+ """Translate a single subtitle line and update progress"""
27
+ inputs = tokenizer(sub.text, return_tensors="pt")
28
+ out = model.generate(
29
+ **inputs,
30
+ forced_bos_token_id=tokenizer.lang_code_to_id["sin_Sinh"]
31
+ )
32
+ sub.text = tokenizer.decode(out[0], skip_special_tokens=True)
33
+
34
+ # Update progress
35
+ if task_id in translation_progress:
36
+ translation_progress[task_id] += 1
37
+ return sub
38
+
39
+ # ─── Modern UI Endpoint ───────────────────────────────────────────────────
40
  @app.get("/", response_class=HTMLResponse)
41
  async def homepage():
42
  return """
43
+ <!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
48
  <title>Sinhala Subtitle Translator</title>
49
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
50
+ <style>
51
+ * {
52
+ box-sizing: border-box;
53
+ margin: 0;
54
+ padding: 0;
55
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
56
+ }
57
+
58
+ body {
59
+ background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
60
+ min-height: 100vh;
61
+ display: flex;
62
+ justify-content: center;
63
+ align-items: center;
64
+ padding: 20px;
65
+ }
66
+
67
+ .container {
68
+ background: rgba(255, 255, 255, 0.92);
69
+ backdrop-filter: blur(10px);
70
+ border-radius: 20px;
71
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
72
+ width: 100%;
73
+ max-width: 800px;
74
+ padding: 40px;
75
+ text-align: center;
76
+ }
77
+
78
+ h1 {
79
+ color: #1a2a6c;
80
+ margin-bottom: 20px;
81
+ font-size: 2.5rem;
82
+ }
83
+
84
+ .description {
85
+ color: #333;
86
+ margin-bottom: 30px;
87
+ font-size: 1.1rem;
88
+ line-height: 1.6;
89
+ }
90
+
91
+ .card {
92
+ background: white;
93
+ border-radius: 15px;
94
+ padding: 30px;
95
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
96
+ margin-bottom: 30px;
97
+ }
98
+
99
+ .upload-area {
100
+ border: 3px dashed #4a69bd;
101
+ border-radius: 12px;
102
+ padding: 40px 20px;
103
+ cursor: pointer;
104
+ transition: all 0.3s ease;
105
+ position: relative;
106
+ background: #f8f9fa;
107
+ }
108
+
109
+ .upload-area:hover {
110
+ background: #edf2ff;
111
+ border-color: #1a2a6c;
112
+ }
113
+
114
+ .upload-icon {
115
+ font-size: 60px;
116
+ color: #4a69bd;
117
+ margin-bottom: 15px;
118
+ }
119
+
120
+ .upload-text {
121
+ font-size: 18px;
122
+ margin-bottom: 10px;
123
+ color: #333;
124
+ }
125
+
126
+ .file-input {
127
+ position: absolute;
128
+ width: 100%;
129
+ height: 100%;
130
+ top: 0;
131
+ left: 0;
132
+ opacity: 0;
133
+ cursor: pointer;
134
+ }
135
+
136
+ .file-name {
137
+ margin-top: 15px;
138
+ font-size: 16px;
139
+ color: #666;
140
+ }
141
+
142
+ .btn {
143
+ background: linear-gradient(to right, #1a2a6c, #4a69bd);
144
+ color: white;
145
+ border: none;
146
+ padding: 15px 40px;
147
+ font-size: 18px;
148
+ border-radius: 50px;
149
+ cursor: pointer;
150
+ transition: all 0.3s ease;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: 10px;
155
+ box-shadow: 0 5px 15px rgba(26, 42, 108, 0.4);
156
+ }
157
+
158
+ .btn:hover {
159
+ transform: translateY(-3px);
160
+ box-shadow: 0 8px 20px rgba(26, 42, 108, 0.6);
161
+ }
162
+
163
+ .btn:disabled {
164
+ background: #cccccc;
165
+ cursor: not-allowed;
166
+ transform: none;
167
+ box-shadow: none;
168
+ }
169
+
170
+ .progress-container {
171
+ display: none;
172
+ margin: 30px 0;
173
+ text-align: left;
174
+ }
175
+
176
+ .progress-header {
177
+ display: flex;
178
+ justify-content: space-between;
179
+ margin-bottom: 10px;
180
+ font-weight: 600;
181
+ color: #333;
182
+ }
183
+
184
+ .progress-bar {
185
+ height: 20px;
186
+ background: #e9ecef;
187
+ border-radius: 10px;
188
+ overflow: hidden;
189
+ }
190
+
191
+ .progress-fill {
192
+ height: 100%;
193
+ background: linear-gradient(to right, #4a69bd, #1a2a6c);
194
+ border-radius: 10px;
195
+ width: 0%;
196
+ transition: width 0.4s ease;
197
+ }
198
+
199
+ .result-container {
200
+ display: none;
201
+ margin-top: 30px;
202
+ padding: 20px;
203
+ background: #e3f2fd;
204
+ border-radius: 12px;
205
+ animation: fadeIn 0.5s ease;
206
+ }
207
+
208
+ .preview-area {
209
+ display: flex;
210
+ justify-content: space-between;
211
+ gap: 20px;
212
+ margin: 25px 0;
213
+ text-align: left;
214
+ }
215
+
216
+ .preview-box {
217
+ flex: 1;
218
+ background: white;
219
+ padding: 20px;
220
+ border-radius: 12px;
221
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
222
+ }
223
+
224
+ .preview-title {
225
+ color: #1a2a6c;
226
+ border-bottom: 2px solid #4a69bd;
227
+ padding-bottom: 10px;
228
+ margin-bottom: 15px;
229
+ font-size: 1.2rem;
230
+ }
231
+
232
+ .preview-content {
233
+ max-height: 200px;
234
+ overflow-y: auto;
235
+ font-size: 0.9rem;
236
+ line-height: 1.6;
237
+ color: #333;
238
+ }
239
+
240
+ .stats {
241
+ display: flex;
242
+ justify-content: space-around;
243
+ margin: 20px 0;
244
+ font-size: 1.1rem;
245
+ }
246
+
247
+ .stat-box {
248
+ text-align: center;
249
+ padding: 15px;
250
+ }
251
+
252
+ .stat-value {
253
+ font-size: 2rem;
254
+ font-weight: 700;
255
+ color: #1a2a6c;
256
+ }
257
+
258
+ .stat-label {
259
+ color: #666;
260
+ }
261
+
262
+ .download-btn {
263
+ background: linear-gradient(to right, #00b09b, #96c93d);
264
+ margin-top: 20px;
265
+ }
266
+
267
+ footer {
268
+ margin-top: 30px;
269
+ color: #666;
270
+ font-size: 0.9rem;
271
+ }
272
+
273
+ @keyframes fadeIn {
274
+ from { opacity: 0; transform: translateY(20px); }
275
+ to { opacity: 1; transform: translateY(0); }
276
+ }
277
+
278
+ @media (max-width: 768px) {
279
+ .container {
280
+ padding: 20px;
281
+ }
282
+
283
+ .preview-area {
284
+ flex-direction: column;
285
+ }
286
+
287
+ .stats {
288
+ flex-direction: column;
289
+ gap: 10px;
290
+ }
291
+ }
292
+ </style>
293
+ </head>
294
+ <body>
295
+ <div class="container">
296
+ <h1><i class="fas fa-closed-captioning"></i> Sinhala Subtitle Translator</h1>
297
+ <p class="description">
298
+ Upload an English subtitle file (.srt) and get an instant translation to Sinhala.
299
+ Perfect for movies, TV shows, and educational content.
300
+ </p>
301
+
302
+ <div class="card">
303
+ <div class="upload-area" id="uploadArea">
304
+ <div class="upload-icon">
305
+ <i class="fas fa-cloud-upload-alt"></i>
306
+ </div>
307
+ <div class="upload-text">
308
+ <strong>Click to upload</strong> or drag & drop your .srt file
309
+ </div>
310
+ <div class="file-name" id="fileName">No file selected</div>
311
+ <input type="file" id="fileInput" class="file-input" accept=".srt">
312
+ </div>
313
+
314
+ <button id="translateBtn" class="btn" disabled>
315
+ <i class="fas fa-language"></i> Translate to Sinhala
316
+ </button>
317
+
318
+ <div class="progress-container" id="progressContainer">
319
+ <div class="progress-header">
320
+ <span>Processing...</span>
321
+ <span id="progressPercent">0%</span>
322
+ </div>
323
+ <div class="progress-bar">
324
+ <div class="progress-fill" id="progressFill"></div>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="result-container" id="resultContainer">
329
+ <div class="stats">
330
+ <div class="stat-box">
331
+ <div class="stat-value" id="lineCount">0</div>
332
+ <div class="stat-label">Lines Translated</div>
333
+ </div>
334
+ <div class="stat-box">
335
+ <div class="stat-value" id="timeTaken">0s</div>
336
+ <div class="stat-label">Processing Time</div>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="preview-area">
341
+ <div class="preview-box">
342
+ <div class="preview-title">Original English</div>
343
+ <div class="preview-content" id="originalPreview"></div>
344
+ </div>
345
+ <div class="preview-box">
346
+ <div class="preview-title">Translated Sinhala</div>
347
+ <div class="preview-content" id="translatedPreview"></div>
348
+ </div>
349
+ </div>
350
+
351
+ <button id="downloadBtn" class="btn download-btn">
352
+ <i class="fas fa-download"></i> Download Sinhala Subtitles
353
+ </button>
354
+ </div>
355
+ </div>
356
+
357
+ <footer>
358
+ <p>Powered by NLLB translation model & FastAPI | Real-time Sinhala subtitle conversion</p>
359
+ </footer>
360
+ </div>
361
+
362
+ <script>
363
+ // DOM Elements
364
+ const fileInput = document.getElementById('fileInput');
365
+ const fileName = document.getElementById('fileName');
366
+ const uploadArea = document.getElementById('uploadArea');
367
+ const translateBtn = document.getElementById('translateBtn');
368
+ const progressContainer = document.getElementById('progressContainer');
369
+ const progressFill = document.getElementById('progressFill');
370
+ const progressPercent = document.getElementById('progressPercent');
371
+ const resultContainer = document.getElementById('resultContainer');
372
+ const originalPreview = document.getElementById('originalPreview');
373
+ const translatedPreview = document.getElementById('translatedPreview');
374
+ const downloadBtn = document.getElementById('downloadBtn');
375
+ const lineCount = document.getElementById('lineCount');
376
+ const timeTaken = document.getElementById('timeTaken');
377
+
378
+ // File selection handling
379
+ fileInput.addEventListener('change', function() {
380
+ if (this.files.length > 0) {
381
+ fileName.textContent = this.files[0].name;
382
+ translateBtn.disabled = false;
383
+
384
+ // Preview first few lines
385
+ const file = this.files[0];
386
+ const reader = new FileReader();
387
+ reader.onload = function(e) {
388
+ const content = e.target.result;
389
+ const lines = content.split('\n').slice(0, 10);
390
+ originalPreview.innerHTML = lines.map(line =>
391
+ `<div>${line}</div>`).join('');
392
+ };
393
+ reader.readAsText(file);
394
+ } else {
395
+ fileName.textContent = 'No file selected';
396
+ translateBtn.disabled = true;
397
+ }
398
+ });
399
+
400
+ // Drag and drop handling
401
+ uploadArea.addEventListener('dragover', (e) => {
402
+ e.preventDefault();
403
+ uploadArea.style.background = '#e6eeff';
404
+ });
405
+
406
+ uploadArea.addEventListener('dragleave', () => {
407
+ uploadArea.style.background = '#f8f9fa';
408
+ });
409
+
410
+ uploadArea.addEventListener('drop', (e) => {
411
+ e.preventDefault();
412
+ uploadArea.style.background = '#f8f9fa';
413
+
414
+ if (e.dataTransfer.files.length > 0) {
415
+ fileInput.files = e.dataTransfer.files;
416
+ const event = new Event('change', { bubbles: true });
417
+ fileInput.dispatchEvent(event);
418
+ }
419
+ });
420
+
421
+ // Translation process
422
+ translateBtn.addEventListener('click', async () => {
423
+ if (!fileInput.files.length) return;
424
+
425
+ const file = fileInput.files[0];
426
+ const formData = new FormData();
427
+ formData.append('file', file);
428
+
429
+ // Show progress
430
+ progressContainer.style.display = 'block';
431
+ translateBtn.disabled = true;
432
+
433
+ // Get task ID
434
+ const taskResponse = await fetch('/start-translation', {
435
+ method: 'POST'
436
+ });
437
+ const taskData = await taskResponse.json();
438
+ const taskId = taskData.task_id;
439
+
440
+ // Start upload and translation
441
+ try {
442
+ const startTime = Date.now();
443
+
444
+ const response = await fetch('/translate/', {
445
+ method: 'POST',
446
+ body: formData,
447
+ headers: {
448
+ 'X-Task-ID': taskId
449
+ }
450
+ });
451
+
452
+ if (!response.ok) {
453
+ throw new Error('Translation failed');
454
+ }
455
+
456
+ const blob = await response.blob();
457
+ const downloadUrl = URL.createObjectURL(blob);
458
+
459
+ // Update stats
460
+ const endTime = Date.now();
461
+ const duration = (endTime - startTime) / 1000;
462
+ timeTaken.textContent = `${duration.toFixed(1)}s`;
463
+
464
+ // Show result
465
+ resultContainer.style.display = 'block';
466
+
467
+ // Set download link
468
+ downloadBtn.onclick = () => {
469
+ const a = document.createElement('a');
470
+ a.href = downloadUrl;
471
+ a.download = 'translated_si.srt';
472
+ document.body.appendChild(a);
473
+ a.click();
474
+ document.body.removeChild(a);
475
+ };
476
+
477
+ // Preview translated content
478
+ const translatedText = await blob.text();
479
+ const translatedLines = translatedText.split('\n').slice(0, 10);
480
+ translatedPreview.innerHTML = translatedLines.map(line =>
481
+ `<div>${line}</div>`).join('');
482
+
483
+ // Update line count
484
+ const lineCountValue = translatedText.split('\n').filter(line =>
485
+ line.trim() !== '' && !line.trim().match(/^\d+$/) &&
486
+ !line.trim().match(/^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/)
487
+ ).length;
488
+ lineCount.textContent = lineCountValue;
489
+
490
+ } catch (error) {
491
+ progressPercent.textContent = 'Error';
492
+ progressFill.style.background = '#ff6b6b';
493
+ alert(`Translation failed: ${error.message}`);
494
+ } finally {
495
+ // Clean up task
496
+ await fetch(`/complete-translation/${taskId}`, {
497
+ method: 'POST'
498
+ });
499
+ }
500
+ });
501
+
502
+ // Progress polling
503
+ async function pollProgress(taskId) {
504
+ const response = await fetch(`/progress/${taskId}`);
505
+ const data = await response.json();
506
+
507
+ if (data.progress !== undefined) {
508
+ const percent = Math.min(100, Math.max(0, data.progress));
509
+ progressFill.style.width = `${percent}%`;
510
+ progressPercent.textContent = `${percent}%`;
511
+
512
+ if (percent < 100) {
513
+ setTimeout(() => pollProgress(taskId), 500);
514
+ }
515
+ }
516
+ }
517
+
518
+ // Initialize task progress endpoint
519
+ async function initTaskProgress() {
520
+ const taskResponse = await fetch('/start-translation', {
521
+ method: 'POST'
522
+ });
523
+ const taskData = await taskResponse.json();
524
+ return taskData.task_id;
525
+ }
526
+ </script>
527
+ </body>
528
  </html>
529
  """
530
 
531
+ # ─── Task Management Endpoints ────────────────────────────────────────────
532
+ @app.post("/start-translation")
533
+ async def start_translation():
534
+ """Create a new translation task"""
535
+ task_id = str(int(time.time() * 1000))
536
+ translation_progress[task_id] = 0
537
+ translation_status[task_id] = "processing"
538
+ return JSONResponse({"task_id": task_id})
539
+
540
+ @app.post("/complete-translation/{task_id}")
541
+ async def complete_translation(task_id: str):
542
+ """Clean up translation task"""
543
+ if task_id in translation_progress:
544
+ del translation_progress[task_id]
545
+ if task_id in translation_status:
546
+ del translation_status[task_id]
547
+ return {"status": "cleaned"}
548
+
549
+ @app.get("/progress/{task_id}")
550
+ async def get_progress(task_id: str):
551
+ """Get current translation progress"""
552
+ progress = translation_progress.get(task_id, 0)
553
+ return JSONResponse({"progress": progress})
554
+
555
+ # ─── Translation Endpoint ────────────────────────────────────────────────
556
  @app.post("/translate/")
557
  async def translate_srt(file: UploadFile = File(...)):
558
  if not file.filename.lower().endswith(".srt"):
559
  raise HTTPException(status_code=400, detail="Please upload a .srt file")
560
 
561
+ # Get task ID from header
562
+ task_id = None
563
+ if "x-task-id" in request.headers:
564
+ task_id = request.headers["x-task-id"]
565
+
566
  # Save upload
567
  tmp_in = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
568
+ content = await file.read()
569
+ tmp_in.write(content)
570
  tmp_in.flush()
571
 
572
+ # Read subtitles
573
  subs = pysrt.open(tmp_in.name)
574
+ total_lines = len(subs)
575
+ os.unlink(tmp_in.name) # Clean up input file
576
+
577
+ # Translate each line with progress tracking
578
+ for i, sub in enumerate(subs):
579
+ sub = translate_subtitle(sub, task_id)
580
+
581
+ # Update progress percentage
582
+ if task_id and total_lines > 0:
583
+ progress = int((i + 1) / total_lines * 100)
584
+ translation_progress[task_id] = progress
585
 
586
  # Write translated file
587
  tmp_out = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
588
  subs.save(tmp_out.name, encoding="utf-8")
589
+
590
+ # Return file response
591
  return FileResponse(
592
  tmp_out.name,
593
  media_type="text/plain",
594
  filename="translated_si.srt"
595
+ )