bigbossmonster commited on
Commit
e02eab5
·
verified ·
1 Parent(s): d42abe1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +244 -0
app.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import json
5
+ import tempfile
6
+ import shutil
7
+ import logging
8
+ import base64
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
12
+ from fastapi.staticfiles import StaticFiles
13
+ from fastapi.responses import FileResponse
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from PIL import Image
16
+ import rarfile
17
+ import zipfile
18
+ import google.generativeai as genai
19
+
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ app = FastAPI()
25
+
26
+ # 1. Enable CORS (good practice, though less critical if serving static from same origin)
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"],
30
+ allow_credentials=True,
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ # 2. Utility Functions
36
+ def parse_srt_time_to_ms(time_str):
37
+ try:
38
+ if not time_str: return 0
39
+ time, ms = time_str.replace(',', '.').split('.')
40
+ hours, minutes, seconds = map(int, time.split(':'))
41
+ return (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + int(ms)
42
+ except Exception:
43
+ return 0
44
+
45
+ def parse_filename_to_ms(filename):
46
+ match = re.search(r'(\d{1,2})_(\d{2})_(\d{2})_(\d{3})', filename)
47
+ if not match:
48
+ return None
49
+ h, m, s, ms = map(int, match.groups())
50
+ return (h * 3600000) + (m * 60000) + (s * 1000) + ms
51
+
52
+ def parse_srt(content: str):
53
+ blocks = content.replace('\r\n', '\n').replace('\r', '\n').strip().split('\n\n')
54
+ parsed = []
55
+ for block in blocks:
56
+ lines = block.split('\n')
57
+ if len(lines) >= 2:
58
+ time_line = lines[1]
59
+ if '-->' in time_line:
60
+ start_str = time_line.split('-->')[0].strip()
61
+ text = " ".join(lines[2:]) if len(lines) > 2 else "[BLANK]"
62
+ parsed.append({
63
+ "id": lines[0],
64
+ "time": time_line,
65
+ "startTimeMs": parse_srt_time_to_ms(start_str),
66
+ "text": text
67
+ })
68
+ return parsed
69
+
70
+ def compress_image(image_bytes, quality=70, max_width=800):
71
+ try:
72
+ img = Image.open(io.BytesIO(image_bytes))
73
+ if img.mode != 'RGB':
74
+ img = img.convert('RGB')
75
+
76
+ width, height = img.size
77
+ if width > max_width:
78
+ height = int((height * max_width) / width)
79
+ width = max_width
80
+ img = img.resize((width, height), Image.Resampling.LANCZOS)
81
+
82
+ buffer = io.BytesIO()
83
+ img.save(buffer, format="JPEG", quality=quality)
84
+ return buffer.getvalue()
85
+ except Exception as e:
86
+ logger.error(f"Image compression failed: {e}")
87
+ return None
88
+
89
+ def process_batch_gemini(api_key, items):
90
+ try:
91
+ genai.configure(api_key=api_key)
92
+ model = genai.GenerativeModel('gemini-2.0-flash')
93
+
94
+ prompt_parts = [
95
+ "You are a Subtitle Quality Control (QC) bot.",
96
+ f"I will provide {len(items)} images and the EXPECTED subtitle text for each.",
97
+ "Return a JSON array strictly following this schema:",
98
+ '[{"index": <int>, "detected_text": "<string>", "match": <bool>, "reason": "<string>"}, ...]',
99
+ "Return ONLY the JSON. No markdown."
100
+ ]
101
+
102
+ for item in items:
103
+ prompt_parts.append(f"\n--- Item {item['index']} ---")
104
+ prompt_parts.append(f"Index: {item['index']}")
105
+ prompt_parts.append(f"Expected Text: \"{item['expected_text']}\"")
106
+ prompt_parts.append(f"Image:")
107
+ img = Image.open(io.BytesIO(item['image_data']))
108
+ prompt_parts.append(img)
109
+
110
+ response = model.generate_content(prompt_parts)
111
+ text = response.text.replace("```json", "").replace("```", "").strip()
112
+ return json.loads(text)
113
+ except Exception as e:
114
+ logger.error(f"Gemini API Error with key ...{api_key[-4:]}: {e}")
115
+ return None
116
+
117
+ # 3. API Endpoint
118
+ @app.post("/api/analyze")
119
+ async def analyze_subtitles(
120
+ srt_file: UploadFile = File(...),
121
+ media_files: list[UploadFile] = File(...),
122
+ api_keys: str = Form(...),
123
+ batch_size: int = Form(20)
124
+ ):
125
+ temp_dir = tempfile.mkdtemp()
126
+ try:
127
+ # Read SRT
128
+ srt_content = (await srt_file.read()).decode('utf-8', errors='ignore')
129
+ srt_data = parse_srt(srt_content)
130
+ srt_data.sort(key=lambda x: x['startTimeMs'])
131
+
132
+ # Process Media
133
+ images = []
134
+
135
+ for file in media_files:
136
+ file_path = os.path.join(temp_dir, file.filename)
137
+ with open(file_path, "wb") as f:
138
+ shutil.copyfileobj(file.file, f)
139
+
140
+ if file.filename.lower().endswith('.rar'):
141
+ # Docker guarantees 'unrar' is installed
142
+ try:
143
+ with rarfile.RarFile(file_path) as rf:
144
+ rf.extractall(temp_dir)
145
+ except rarfile.RarCannotExec:
146
+ raise HTTPException(status_code=500, detail="Unrar executable not found in container.")
147
+ elif file.filename.lower().endswith('.zip'):
148
+ with zipfile.ZipFile(file_path, 'r') as zf:
149
+ zf.extractall(temp_dir)
150
+
151
+ for root, _, files in os.walk(temp_dir):
152
+ for filename in files:
153
+ if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.webp', '.bmp')):
154
+ full_path = os.path.join(root, filename)
155
+ ms = parse_filename_to_ms(filename)
156
+ if ms is not None:
157
+ with open(full_path, "rb") as f:
158
+ raw_bytes = f.read()
159
+ compressed = compress_image(raw_bytes)
160
+ if compressed:
161
+ images.append({
162
+ "filename": filename,
163
+ "timeMs": ms,
164
+ "data": compressed
165
+ })
166
+
167
+ images.sort(key=lambda x: x['timeMs'])
168
+
169
+ pairs = []
170
+ for i in range(len(images)):
171
+ img = images[i]
172
+ srt = srt_data[i] if i < len(srt_data) else None
173
+
174
+ if srt:
175
+ thumb_bytes = compress_image(img['data'], quality=50, max_width=300)
176
+ thumb_b64 = base64.b64encode(thumb_bytes).decode('utf-8')
177
+
178
+ pairs.append({
179
+ "index": i,
180
+ "image_data": img['data'],
181
+ "expected_text": srt['text'],
182
+ "srt_id": srt['id'],
183
+ "srt_time": srt['time'],
184
+ "filename": img['filename'],
185
+ "thumb": f"data:image/jpeg;base64,{thumb_b64}",
186
+ "status": "pending"
187
+ })
188
+
189
+ if not pairs:
190
+ return {"status": "error", "message": "No valid image/subtitle pairs found."}
191
+
192
+ # Process Gemini
193
+ keys = [k.strip() for k in api_keys.split('\n') if k.strip()]
194
+ if not keys:
195
+ raise HTTPException(status_code=400, detail="No API Keys provided")
196
+
197
+ results_map = {}
198
+ batches = [pairs[i:i + batch_size] for i in range(0, len(pairs), batch_size)]
199
+
200
+ def worker(batch_idx, batch):
201
+ key = keys[batch_idx % len(keys)]
202
+ return process_batch_gemini(key, batch)
203
+
204
+ with ThreadPoolExecutor(max_workers=len(keys)) as executor:
205
+ futures = [executor.submit(worker, i, b) for i, b in enumerate(batches)]
206
+ for future in futures:
207
+ res = future.result()
208
+ if res:
209
+ for item in res:
210
+ results_map[item['index']] = item
211
+
212
+ final_output = []
213
+ for p in pairs:
214
+ analysis = results_map.get(p['index'])
215
+ status = "pending"
216
+ reason = ""
217
+ detected = ""
218
+ if analysis:
219
+ status = "match" if analysis['match'] else "mismatch"
220
+ reason = analysis.get('reason', '')
221
+ detected = analysis.get('detected_text', '')
222
+
223
+ final_output.append({
224
+ "id": p['index'],
225
+ "filename": p['filename'],
226
+ "thumb": p['thumb'],
227
+ "expected": p['expected_text'],
228
+ "detected": detected,
229
+ "status": status,
230
+ "reason": reason,
231
+ "srt_id": p['srt_id'],
232
+ "srt_time": p['srt_time']
233
+ })
234
+
235
+ return {"status": "success", "results": final_output}
236
+
237
+ except Exception as e:
238
+ logger.error(f"Server Error: {e}")
239
+ raise HTTPException(status_code=500, detail=str(e))
240
+ finally:
241
+ shutil.rmtree(temp_dir)
242
+
243
+ # 4. Serve Static Files (Frontend)
244
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")