bigbossmonster commited on
Commit
e3b282f
·
verified ·
1 Parent(s): a6d270c

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +300 -323
static/index.html CHANGED
@@ -26,144 +26,141 @@
26
  const { useState, useEffect, useRef, useCallback } = React;
27
 
28
  // --- Icons ---
29
- const IconWrapper = ({ children, className }) => (
30
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
31
- {children}
32
  </svg>
33
  );
 
 
 
 
 
 
 
 
 
 
34
 
35
- const FileText = ({ className }) => <IconWrapper className={className}><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></IconWrapper>;
36
- const LucideImage = ({ className }) => <IconWrapper className={className}><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></IconWrapper>;
37
- const CheckCircle = ({ className }) => <IconWrapper className={className}><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></IconWrapper>;
38
- const XCircle = ({ className }) => <IconWrapper className={className}><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></IconWrapper>;
39
- const Play = ({ className }) => <IconWrapper className={className}><polygon points="5 3 19 12 5 21 5 3"/></IconWrapper>;
40
- const AlertCircle = ({ className }) => <IconWrapper className={className}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></IconWrapper>;
41
- const Loader2 = ({ className }) => <IconWrapper className={className}><path d="M21 12a9 9 0 1 1-6.219-8.56"/></IconWrapper>;
42
- const Trash2 = ({ className }) => <IconWrapper className={className}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></IconWrapper>;
43
- const Clock = ({ className }) => <IconWrapper className={className}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></IconWrapper>;
44
- const ArrowRightLeft = ({ className }) => <IconWrapper className={className}><path d="m16 21 5-5-5-5"/><path d="M21 16H3"/><path d="m8 3-5 5 5 5"/><path d="M3 8h18"/></IconWrapper>;
45
- const SettingsIcon = ({ className }) => <IconWrapper className={className}><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></IconWrapper>;
46
- const Zap = ({ className }) => <IconWrapper className={className}><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></IconWrapper>;
47
- const Download = ({ className }) => <IconWrapper className={className}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></IconWrapper>;
48
- const Package = ({ className }) => <IconWrapper className={className}><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></IconWrapper>;
49
-
50
- // --- Components ---
51
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  const StatusBadge = ({ status }) => {
53
- if (status === 'match') return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1"/> Match</span>;
54
- if (status === 'mismatch') return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle className="w-3 h-3 mr-1"/> Mismatch</span>;
55
  return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">Pending</span>;
56
  };
57
 
58
- const Header = ({ onOpenSettings, activeWorkers }) => (
59
- <header className="bg-slate-900 text-white p-6 shadow-lg">
60
- <div className="max-w-6xl mx-auto flex items-center justify-between">
61
- <div className="flex items-center space-x-3">
62
- <div className="bg-blue-600 p-2 rounded-lg">
63
- <FileText className="w-6 h-6 text-white" />
64
- </div>
65
- <div>
66
- <h1 className="text-xl font-bold tracking-tight">Subtitle QC Assistant</h1>
67
- <p className="text-slate-400 text-sm">Verify image subtitles against SRT files using Gemini</p>
68
- </div>
69
- </div>
70
- <div className="flex items-center space-x-4">
71
- <div className="hidden md:block text-xs text-slate-500 text-right">
72
- {activeWorkers > 0 ? (
73
- <span className="text-green-400 font-bold flex items-center justify-end gap-1">
74
- <Zap className="w-3 h-3 animate-pulse"/>
75
- {activeWorkers} Parallel Key(s) Active
76
- </span>
77
- ) : (
78
- "Mode: Sequential Sort (Time Ascending)"
79
- )}
80
- <br/>
81
- Supports Multi-Key Parallel Batching
82
- </div>
83
- <button
84
- onClick={onOpenSettings}
85
- className="p-2 bg-slate-800 hover:bg-slate-700 rounded-full transition-colors text-slate-300 hover:text-white"
86
- title="API Key Settings"
87
- >
88
- <SettingsIcon className="w-5 h-5" />
89
- </button>
90
- </div>
91
- </div>
92
- </header>
93
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  const SettingsModal = ({ isOpen, onClose, config, setConfig }) => {
96
  if (!isOpen) return null;
97
  return (
98
- <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
99
- <div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6 animate-in fade-in zoom-in duration-200">
100
  <div className="flex justify-between items-center mb-4">
101
  <h3 className="text-lg font-bold text-slate-800">Configuration</h3>
102
- <button onClick={onClose} className="text-slate-400 hover:text-slate-600">
103
- <XCircle className="w-6 h-6" />
104
- </button>
105
  </div>
106
  <div className="space-y-4">
107
  <div>
108
  <label className="block text-sm font-medium text-slate-700 mb-1">Gemini API Keys</label>
109
- <p className="text-xs text-slate-500 mb-2">Enter one key per line. Multiple keys will run in parallel.</p>
110
- <textarea
111
- value={config.apiKeys}
112
- onChange={(e) => setConfig({...config, apiKeys: e.target.value})}
113
- placeholder="AIzaSy...&#10;AIzaSy..."
114
- rows={3}
115
- className="w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none font-mono text-xs"
116
- />
117
  </div>
118
-
119
  <div className="grid grid-cols-2 gap-4">
120
  <div>
121
- <label className="block text-sm font-medium text-slate-700 mb-1">Model Name</label>
122
- <select
123
- value={config.modelName}
124
- onChange={(e) => setConfig({...config, modelName: e.target.value})}
125
- className="w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
126
- >
127
- <option value="gemini-3-flash-preview">Gemini 3 Flash</option>
128
- <option value="gemini-3-pro-preview">Gemini 3 Pro</option>
129
  </select>
130
  </div>
131
  <div>
132
  <label className="block text-sm font-medium text-slate-700 mb-1">Batch Size</label>
133
- <input
134
- type="number"
135
- min="1" max="50"
136
- value={config.batchSize}
137
- onChange={(e) => setConfig({...config, batchSize: Number(e.target.value)})}
138
- className="w-full p-2 border border-slate-300 rounded-lg text-sm"
139
- />
140
  </div>
141
  </div>
142
-
143
  <div>
144
- <label className="block text-sm font-medium text-slate-700 mb-1">
145
- Compression Quality: <span className="text-blue-600 font-bold">{config.quality}</span>
146
- </label>
147
- <input
148
- type="range"
149
- min="0.1"
150
- max="1.0"
151
- step="0.1"
152
- value={config.quality}
153
- onChange={(e) => setConfig({...config, quality: Number(e.target.value)})}
154
- className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer"
155
- />
156
- <div className="flex justify-between text-xs text-slate-400 mt-1">
157
- <span>0.1 (Smallest)</span>
158
- <span>0.7 (Default)</span>
159
- <span>1.0 (Best)</span>
160
- </div>
161
  </div>
162
-
163
  <div className="flex justify-end pt-2">
164
- <button onClick={onClose} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
165
- Save & Close
166
- </button>
167
  </div>
168
  </div>
169
  </div>
@@ -172,53 +169,105 @@
172
  };
173
 
174
  const App = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  const [srtFile, setSrtFile] = useState(null);
176
  const [mediaFiles, setMediaFiles] = useState([]);
177
- const [pairs, setPairs] = useState([]);
178
 
 
 
 
 
 
179
  const [isProcessing, setIsProcessing] = useState(false);
180
- const [activeWorkers, setActiveWorkers] = useState(0);
181
  const [error, setError] = useState(null);
182
- const [loadingMessage, setLoadingMessage] = useState('');
183
  const [showSettings, setShowSettings] = useState(false);
184
-
185
- // Centralized Config State
186
- const [config, setConfig] = useState(() => ({
187
- apiKeys: localStorage.getItem('gemini_api_keys_multi') || '',
188
- batchSize: Number(localStorage.getItem('gemini_batch_size')) || 20,
189
- modelName: localStorage.getItem('gemini_model_name') || 'gemini-3-flash-preview',
190
- quality: Number(localStorage.getItem('gemini_quality')) || 0.7
191
- }));
192
-
193
  useEffect(() => {
194
- localStorage.setItem('gemini_api_keys_multi', config.apiKeys);
195
- localStorage.setItem('gemini_batch_size', config.batchSize);
196
- localStorage.setItem('gemini_model_name', config.modelName);
197
- localStorage.setItem('gemini_quality', config.quality);
198
- }, [config]);
199
 
200
- const [isDraggingSrt, setIsDraggingSrt] = useState(false);
201
- const [isDraggingImages, setIsDraggingImages] = useState(false);
 
 
202
 
203
- const handleAnalyze = async () => {
204
- if (!srtFile || mediaFiles.length === 0) {
205
- setError("Please upload both SRT file and Media files.");
206
- return;
207
- }
208
- if (!config.apiKeys.trim()) {
209
- setError("API Key missing. Please check settings.");
210
- setShowSettings(true);
211
- return;
212
- }
 
 
 
 
 
 
 
 
 
 
 
 
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  setIsProcessing(true);
215
  setError(null);
216
- setPairs([]);
217
- setLoadingMessage("Uploading and analyzing on backend...");
218
-
219
- // Count workers simply by keys present
220
- const keyCount = config.apiKeys.split('\n').filter(k => k.trim()).length;
221
- setActiveWorkers(keyCount);
222
 
223
  const formData = new FormData();
224
  formData.append("srt_file", srtFile);
@@ -231,233 +280,161 @@
231
  formData.append("compression_quality", config.quality);
232
 
233
  try {
234
- const response = await fetch('/api/analyze', {
235
- method: 'POST',
236
- body: formData
237
- });
238
-
239
- if (!response.ok) {
240
- const errText = await response.text();
241
- throw new Error(`Backend Error (${response.status}): ${errText}`);
242
- }
243
-
244
  const data = await response.json();
 
245
  if (data.status === 'success') {
246
- // The backend returns a unified list of results
247
- // We map this to the structure expected by the UI renderer
248
- const mappedPairs = data.results.map((res, idx) => ({
249
- id: res.id,
250
- image: { originalFile: { name: res.filename }, dataUrl: res.thumb },
251
- subtitle: { text: res.expected, time: res.srt_time },
252
- status: res.status,
253
- analysis: { detected_text: res.detected, reason: res.reason },
254
- matchNote: res.status === 'match' ? 'Match' : 'Mismatch' // simplified for remote logic
255
- }));
256
- setPairs(mappedPairs);
257
  } else {
258
- throw new Error(data.message || "Unknown server error");
259
  }
260
  } catch (err) {
261
  setError(err.message);
262
  } finally {
263
  setIsProcessing(false);
264
- setLoadingMessage('');
265
- setActiveWorkers(0);
266
  }
267
  };
268
 
269
- const handleDownloadSRT = () => {
270
- if (pairs.length === 0) return;
271
- let srtContent = "";
272
- pairs.forEach((pair) => {
273
- const text = (pair.status === 'mismatch' && pair.analysis?.detected_text)
274
- ? pair.analysis.detected_text
275
- : pair.subtitle.text;
276
- // Note: Backend simplified passing back SRT IDs, ensuring they exist
277
- srtContent += `${pair.id + 1}\n${pair.subtitle.time}\n${text}\n\n`;
278
  });
279
- const blob = new Blob([srtContent], { type: 'text/plain' });
280
- const url = URL.createObjectURL(blob);
281
  const a = document.createElement('a');
282
  a.href = url;
283
- a.download = "corrected_subtitles.srt";
284
  document.body.appendChild(a);
285
  a.click();
286
  document.body.removeChild(a);
287
- URL.revokeObjectURL(url);
288
  };
289
 
290
  const handleReset = () => {
291
  setSrtFile(null);
292
  setMediaFiles([]);
293
- setPairs([]);
 
294
  setError(null);
295
  };
296
 
297
  return (
298
- <div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
299
- <Header onOpenSettings={() => setShowSettings(true)} activeWorkers={activeWorkers} />
300
-
301
- <SettingsModal
302
- isOpen={showSettings}
303
- onClose={() => setShowSettings(false)}
304
- config={config}
305
- setConfig={setConfig}
306
- />
307
 
308
- <main className="max-w-6xl mx-auto p-6 space-y-8">
309
- {/* Upload Section */}
310
- <section className="grid md:grid-cols-2 gap-6">
311
- {/* SRT Uploader */}
312
- <div
313
- className={`relative border-2 border-dashed rounded-xl p-8 flex flex-col items-center justify-center transition-all duration-200
314
- ${isDraggingSrt ? 'border-blue-500 bg-blue-50 scale-[1.02] shadow-xl' : 'border-slate-300 hover:border-blue-400 bg-white'}
315
- ${srtFile ? 'border-green-400 bg-green-50' : ''}`}
316
- onDragOver={(e) => { e.preventDefault(); setIsDraggingSrt(true); }}
317
- onDragLeave={(e) => { e.preventDefault(); setIsDraggingSrt(false); }}
318
- onDrop={(e) => {
319
- e.preventDefault(); setIsDraggingSrt(false);
320
- if(e.dataTransfer.files.length) setSrtFile(e.dataTransfer.files[0]);
321
- }}
322
- >
323
- <input type="file" accept=".srt" onChange={(e) => setSrtFile(e.target.files[0])} className="hidden" id="srt-upload" />
324
- <label htmlFor="srt-upload" className="cursor-pointer flex flex-col items-center text-center w-full z-10">
325
- <FileText className={`w-12 h-12 mb-4 transition-colors ${srtFile ? 'text-green-600' : 'text-slate-400'}`} />
326
- <span className="font-semibold text-lg mb-1">
327
- {srtFile ? srtFile.name : 'Drag & Drop SRT File'}
328
- </span>
329
- </label>
330
- </div>
331
-
332
- {/* Image Uploader */}
333
- <div
334
- className={`relative border-2 border-dashed rounded-xl p-8 flex flex-col items-center justify-center transition-all duration-200
335
- ${isDraggingImages ? 'border-blue-500 bg-blue-50 scale-[1.02] shadow-xl' : 'border-slate-300 hover:border-blue-400 bg-white'}
336
- ${mediaFiles.length > 0 ? 'border-green-400 bg-green-50' : ''}`}
337
- onDragOver={(e) => { e.preventDefault(); setIsDraggingImages(true); }}
338
- onDragLeave={(e) => { e.preventDefault(); setIsDraggingImages(false); }}
339
- onDrop={(e) => {
340
- e.preventDefault(); setIsDraggingImages(false);
341
- if(e.dataTransfer.files.length) setMediaFiles(e.dataTransfer.files);
342
- }}
343
- >
344
- <input type="file" accept="image/*,.rar,.zip" multiple onChange={(e) => setMediaFiles(e.target.files)} className="hidden" id="img-upload" />
345
- <label htmlFor="img-upload" className="cursor-pointer flex flex-col items-center text-center w-full z-10">
346
- <div className="flex justify-center space-x-2">
347
- <LucideImage className={`w-12 h-12 mb-4 transition-colors ${mediaFiles.length > 0 ? 'text-green-600' : 'text-slate-400'}`} />
348
- <Package className={`w-12 h-12 mb-4 transition-colors ${mediaFiles.length > 0 ? 'text-green-600' : 'text-slate-400'}`} />
349
- </div>
350
- <span className="font-semibold text-lg mb-1">
351
- {mediaFiles.length > 0 ? `${mediaFiles.length} Files Selected` : 'Drag & Drop Images/RAR/ZIP'}
352
- </span>
353
- </label>
354
  </div>
355
- </section>
356
-
357
- {/* Processing & Error State */}
358
- {isProcessing && (
359
- <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center justify-center space-x-3 text-blue-800 animate-pulse">
360
- <Loader2 className="w-5 h-5 animate-spin" />
361
- <span className="font-medium">{loadingMessage}</span>
362
  </div>
363
- )}
364
 
365
- {error && (
366
- <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-3 text-red-800">
367
- <AlertCircle className="w-5 h-5" />
368
- <span>{error}</span>
369
- </div>
370
- )}
371
 
372
- {/* Actions & Results */}
373
- {(pairs.length > 0 || (srtFile && mediaFiles.length > 0 && !isProcessing)) && (
374
- <section className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
375
- <div className="p-4 border-b border-slate-200 bg-slate-50 flex items-center justify-between sticky top-0 z-10">
376
- <div className="flex items-center space-x-4">
377
- <h2 className="font-bold text-lg">Results ({pairs.length})</h2>
378
- {pairs.length > 0 && (
379
- <div className="flex space-x-2 text-sm">
380
- <span className="px-2 py-1 bg-green-100 text-green-700 rounded-md">
381
- {pairs.filter(p => p.status === 'match').length} Match
382
- </span>
383
- <span className="px-2 py-1 bg-red-100 text-red-700 rounded-md">
384
- {pairs.filter(p => p.status === 'mismatch').length} Mismatch
385
- </span>
386
- </div>
387
- )}
388
- </div>
389
- <div className="flex space-x-3">
390
- {pairs.length > 0 && (
391
- <button
392
- onClick={handleDownloadSRT}
393
- className="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
394
- >
395
- <Download className="w-4 h-4" />
396
- <span>Corrected SRT</span>
397
- </button>
398
- )}
399
- <button
400
- onClick={handleReset}
401
- className="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-600 hover:text-red-600 transition-colors"
402
- >
403
- <Trash2 className="w-4 h-4" />
404
- <span>Clear All</span>
405
  </button>
 
406
  <button
407
- onClick={handleAnalyze}
408
  disabled={isProcessing}
409
- className={`flex items-center space-x-2 px-6 py-2 rounded-lg font-semibold text-white transition-all shadow-md ${isProcessing ? 'bg-slate-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 hover:shadow-lg'}`}
410
  >
411
- {isProcessing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
412
- <span>{pairs.length > 0 ? 'Re-Analyze' : 'Run Analysis'}</span>
413
  </button>
414
- </div>
415
  </div>
 
 
416
 
417
- <div className="divide-y divide-slate-100">
418
- {pairs.map((pair, index) => (
419
- <div key={index} className={`grid grid-cols-12 gap-4 p-4 hover:bg-slate-50 transition-colors ${pair.status === 'mismatch' ? 'bg-red-50/50' : ''}`}>
420
- <div className="col-span-12 md:col-span-3">
421
- <div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
422
- {pair.image.dataUrl ? (
423
- <img src={pair.image.dataUrl} alt={`Frame ${index}`} className="w-full h-full object-contain" />
424
- ) : (
425
- <div className="w-full h-full flex items-center justify-center text-xs text-gray-400">No Image</div>
426
- )}
427
- <div className="absolute top-0 left-0 right-0 bg-black/70 p-1">
428
- <div className="text-white text-[10px] truncate font-mono">{pair.image.originalFile?.name}</div>
 
 
429
  </div>
430
- </div>
 
431
  </div>
432
-
433
- <div className="col-span-12 md:col-span-4 flex flex-col justify-center">
434
- <div className="text-xs font-bold text-slate-400 uppercase mb-1">Expected</div>
435
- <div className="p-3 bg-slate-100 rounded-lg text-sm font-medium">{pair.subtitle.text}</div>
436
- <div className="mt-1 text-xs text-slate-400 font-mono flex items-center">
437
- <Clock className="w-3 h-3 mr-1"/> {pair.subtitle.time}
438
- </div>
439
  </div>
 
 
 
 
 
 
440
 
441
- <div className="col-span-12 md:col-span-5 flex flex-col justify-center">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  <div className="flex justify-between items-center mb-1">
443
- <div className="text-xs font-bold text-slate-400 uppercase">Actual (Gemini)</div>
444
- <StatusBadge status={pair.status} />
445
  </div>
446
- <div className={`p-3 rounded-lg text-sm border min-h-[60px] flex items-center ${
447
- pair.status === 'match' ? 'bg-green-50 border-green-200 text-green-800' :
448
- pair.status === 'mismatch' ? 'bg-red-50 border-red-200 text-red-800' :
449
- 'bg-white border-slate-200 text-slate-400 italic'
450
- }`}>
451
- <div>
452
- <span className="font-semibold block mb-1">Detected: "{pair.analysis.detected_text}"</span>
453
- {pair.analysis.reason && <span className="text-xs opacity-75 block">{pair.analysis.reason}</span>}
454
- </div>
455
  </div>
 
456
  </div>
457
  </div>
458
- ))}
459
- </div>
460
- </section>
461
  )}
462
 
463
  {pairs.length === 0 && !srtFile && (
 
26
  const { useState, useEffect, useRef, useCallback } = React;
27
 
28
  // --- Icons ---
29
+ const Icon = ({ d, className }) => (
30
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
31
+ <path d={d}/>
32
  </svg>
33
  );
34
+ const Icons = {
35
+ Upload: ({className}) => <Icon className={className} d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>,
36
+ Check: ({className}) => <Icon className={className} d="M22 11.08V12a10 10 0 1 1-5.93-9.14 22 4 12 14.01 9 11.01"/>,
37
+ X: ({className}) => <Icon className={className} d="M18 6 6 18M6 6l12 12"/>,
38
+ Download: ({className}) => <Icon className={className} d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>,
39
+ Settings: ({className}) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
40
+ Zap: ({className}) => <Icon className={className} d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />,
41
+ Clock: ({className}) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>,
42
+ Archive: ({className}) => <Icon className={className} d="M21 8v13H3V8M1 3h22v5H1zM10 12h4" />
43
+ };
44
 
45
+ const IconWrapper = ({ children, className }) => (
46
+ <div className={className}>{children}</div>
47
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ // Alias for compatibility
50
+ const FileText = ({ className }) => <IconWrapper className={className}><Icons.Upload className="w-full h-full" /></IconWrapper>;
51
+ const LucideImage = ({ className }) => <IconWrapper className={className}><Icons.Archive className="w-full h-full" /></IconWrapper>;
52
+ const CheckCircle = ({ className }) => <IconWrapper className={className}><Icons.Check className="w-full h-full" /></IconWrapper>;
53
+ const XCircle = ({ className }) => <IconWrapper className={className}><Icons.X className="w-full h-full" /></IconWrapper>;
54
+ const Play = ({ className }) => <IconWrapper className={className}><Icons.Zap className="w-full h-full" /></IconWrapper>;
55
+ const AlertCircle = ({ className }) => <IconWrapper className={className}><Icons.X className="w-full h-full" /></IconWrapper>;
56
+ const Loader2 = ({ className }) => <IconWrapper className={className}><svg className="animate-spin w-full h-full" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z" opacity=".3"/><path d="M12 4a8 8 0 0 1 7.8 6.6L22 10a10 10 0 0 0-19.6 0L4.2 10.6A8 8 0 0 1 12 4z"/></svg></IconWrapper>;
57
+ const Trash2 = ({ className }) => <IconWrapper className={className}><Icons.X className="w-full h-full" /></IconWrapper>;
58
+ const SettingsIcon = ({ className }) => <IconWrapper className={className}><Icons.Settings className="w-full h-full" /></IconWrapper>;
59
+ const Package = ({ className }) => <IconWrapper className={className}><Icons.Archive className="w-full h-full" /></IconWrapper>;
60
+ const Download = ({ className }) => <IconWrapper className={className}><Icons.Download className="w-full h-full" /></IconWrapper>;
61
  const StatusBadge = ({ status }) => {
62
+ if (status === 'match') return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><Icons.Check className="w-3 h-3 mr-1"/> Match</span>;
63
+ if (status === 'mismatch') return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><Icons.X className="w-3 h-3 mr-1"/> Mismatch</span>;
64
  return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">Pending</span>;
65
  };
66
 
67
+ // --- Client Side Parsing Logic for Preview ---
68
+ const timeStringToMs = (timeStr) => {
69
+ if (!timeStr) return 0;
70
+ const [time, ms] = timeStr.replace(',', '.').split('.');
71
+ const [hours, minutes, seconds] = time.split(':').map(Number);
72
+ return (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + (Number(ms) || 0);
73
+ };
74
+
75
+ const filenameToMs = (filename) => {
76
+ const match = filename.match(/(\d{1,2})_(\d{2})_(\d{2})_(\d{3})/);
77
+ if (!match) return null;
78
+ const [_, h, m, s, ms] = match;
79
+ return (Number(h) * 3600000) + (Number(m) * 60000) + (Number(s) * 1000) + Number(ms);
80
+ };
81
+
82
+ const parseSRT = (data) => {
83
+ if (!data) return [];
84
+ const normalized = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
85
+
86
+ // Robust splitting by double newlines (or more) to handle inconsistent files
87
+ // This regex handles \n\n, \n\n\n, or \n \n etc.
88
+ const blocks = normalized.trim().split(/\n\s*\n/);
89
+
90
+ const parsed = [];
91
+
92
+ blocks.forEach(block => {
93
+ const lines = block.split('\n').map(l => l.trim()).filter(l => l);
94
+
95
+ if (lines.length < 2) return;
96
+
97
+ // Find timestamp line
98
+ let timeLineIndex = -1;
99
+ for (let i = 0; i < lines.length; i++) {
100
+ if (lines[i].includes('-->')) {
101
+ timeLineIndex = i;
102
+ break;
103
+ }
104
+ }
105
+
106
+ if (timeLineIndex !== -1) {
107
+ const timeLine = lines[timeLineIndex];
108
+ // ID is likely the line before timestamp, otherwise guess sequence
109
+ const id = timeLineIndex > 0 ? lines[timeLineIndex - 1] : String(parsed.length + 1);
110
+
111
+ // Text is everything after timestamp
112
+ const textLines = lines.slice(timeLineIndex + 1);
113
+ const text = textLines.length > 0 ? textLines.join(' ') : "[BLANK SUBTITLE]";
114
+
115
+ const startTimeStr = timeLine.split('-->')[0].trim();
116
+
117
+ parsed.push({
118
+ id: id,
119
+ time: timeLine,
120
+ startTimeMs: timeStringToMs(startTimeStr),
121
+ text: text
122
+ });
123
+ }
124
+ });
125
+
126
+ return parsed;
127
+ };
128
 
129
  const SettingsModal = ({ isOpen, onClose, config, setConfig }) => {
130
  if (!isOpen) return null;
131
  return (
132
+ <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
133
+ <div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6 animate-in zoom-in duration-200">
134
  <div className="flex justify-between items-center mb-4">
135
  <h3 className="text-lg font-bold text-slate-800">Configuration</h3>
136
+ <button onClick={onClose} className="text-slate-400 hover:text-slate-600"><Icons.X className="w-6 h-6" /></button>
 
 
137
  </div>
138
  <div className="space-y-4">
139
  <div>
140
  <label className="block text-sm font-medium text-slate-700 mb-1">Gemini API Keys</label>
141
+ <textarea value={config.apiKeys} onChange={(e) => setConfig({...config, apiKeys: e.target.value})} placeholder="AIzaSy...&#10;AIzaSy..." rows={3} className="w-full p-2 border border-slate-300 rounded-lg text-xs font-mono" />
 
 
 
 
 
 
 
142
  </div>
 
143
  <div className="grid grid-cols-2 gap-4">
144
  <div>
145
+ <label className="block text-sm font-medium text-slate-700 mb-1">Model</label>
146
+ <select value={config.modelName} onChange={(e) => setConfig({...config, modelName: e.target.value})} className="w-full p-2 border border-slate-300 rounded-lg text-sm">
147
+ <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
148
+ <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
149
+ <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
150
+ <option value="gemini-3-flash-preview">Gemini 3 Flash</option>
 
 
151
  </select>
152
  </div>
153
  <div>
154
  <label className="block text-sm font-medium text-slate-700 mb-1">Batch Size</label>
155
+ <input type="number" min="1" max="50" value={config.batchSize} onChange={(e) => setConfig({...config, batchSize: Number(e.target.value)})} className="w-full p-2 border border-slate-300 rounded-lg text-sm" />
 
 
 
 
 
 
156
  </div>
157
  </div>
 
158
  <div>
159
+ <label className="block text-sm font-medium text-slate-700 mb-1">Compression Quality: <span className="text-blue-600 font-bold">{config.quality}</span></label>
160
+ <input type="range" min="0.1" max="1.0" step="0.1" value={config.quality} onChange={(e) => setConfig({...config, quality: Number(e.target.value)})} className="w-full h-2 bg-slate-200 rounded-lg cursor-pointer" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  </div>
 
162
  <div className="flex justify-end pt-2">
163
+ <button onClick={onClose} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">Save & Close</button>
 
 
164
  </div>
165
  </div>
166
  </div>
 
169
  };
170
 
171
  const App = () => {
172
+ // Configuration
173
+ const [config, setConfig] = useState(() => ({
174
+ apiKeys: localStorage.getItem('qc_api_keys') || '',
175
+ batchSize: Number(localStorage.getItem('qc_batch_size')) || 20,
176
+ modelName: localStorage.getItem('qc_model') || 'gemini-2.0-flash',
177
+ quality: Number(localStorage.getItem('qc_quality')) || 0.7
178
+ }));
179
+
180
+ useEffect(() => {
181
+ localStorage.setItem('qc_api_keys', config.apiKeys);
182
+ localStorage.setItem('qc_batch_size', config.batchSize);
183
+ localStorage.setItem('qc_model', config.modelName);
184
+ localStorage.setItem('qc_quality', config.quality);
185
+ }, [config]);
186
+
187
+ // File State
188
  const [srtFile, setSrtFile] = useState(null);
189
  const [mediaFiles, setMediaFiles] = useState([]);
 
190
 
191
+ // Preview State
192
+ const [previewItems, setPreviewItems] = useState([]);
193
+ const [archiveMode, setArchiveMode] = useState(false);
194
+
195
+ // Process State
196
  const [isProcessing, setIsProcessing] = useState(false);
197
+ const [results, setResults] = useState([]);
198
  const [error, setError] = useState(null);
 
199
  const [showSettings, setShowSettings] = useState(false);
200
+
201
+ // 1. Logic to Generate Preview
 
 
 
 
 
 
 
202
  useEffect(() => {
203
+ const generatePreview = async () => {
204
+ if (!srtFile) {
205
+ setPreviewItems([]);
206
+ return;
207
+ }
208
 
209
+ // Parse SRT
210
+ const text = await srtFile.text();
211
+ const parsedSrt = parseSRT(text);
212
+ parsedSrt.sort((a, b) => a.startTimeMs - b.startTimeMs);
213
 
214
+ let displayItems = [];
215
+
216
+ // Check for Archives
217
+ const hasArchive = Array.from(mediaFiles).some(f => f.name.toLowerCase().endsWith('.rar') || f.name.toLowerCase().endsWith('.zip'));
218
+ setArchiveMode(hasArchive);
219
+
220
+ if (hasArchive) {
221
+ // Archive Mode: Just show SRT with placeholders
222
+ displayItems = parsedSrt.map((srt, idx) => ({
223
+ id: idx,
224
+ srt: srt,
225
+ imgName: "Inside Archive",
226
+ imgUrl: null, // No preview for archive content client-side
227
+ isArchive: true
228
+ }));
229
+ } else if (mediaFiles.length > 0) {
230
+ // Image Mode: Sort and Pair locally
231
+ const images = Array.from(mediaFiles)
232
+ .filter(f => /\.(jpg|jpeg|png|webp|bmp)$/i.test(f.name))
233
+ .map(f => ({ file: f, time: filenameToMs(f.name) }))
234
+ .filter(f => f.time !== null)
235
+ .sort((a, b) => a.time - b.time);
236
 
237
+ displayItems = parsedSrt.map((srt, idx) => {
238
+ const matchedImg = images[idx];
239
+ return {
240
+ id: idx,
241
+ srt: srt,
242
+ imgName: matchedImg ? matchedImg.file.name : "Missing Image",
243
+ imgUrl: matchedImg ? URL.createObjectURL(matchedImg.file) : null,
244
+ isArchive: false
245
+ };
246
+ });
247
+ } else {
248
+ // SRT Only Mode (just waiting for images)
249
+ displayItems = parsedSrt.map((srt, idx) => ({
250
+ id: idx,
251
+ srt: srt,
252
+ imgName: "Waiting for media...",
253
+ imgUrl: null,
254
+ isArchive: false
255
+ }));
256
+ }
257
+
258
+ setPreviewItems(displayItems);
259
+ };
260
+
261
+ generatePreview();
262
+ }, [srtFile, mediaFiles]);
263
+
264
+ // 2. Analyze
265
+ const handleAnalyze = async () => {
266
+ if (!config.apiKeys.trim()) { setError("API Key missing."); setShowSettings(true); return; }
267
+
268
  setIsProcessing(true);
269
  setError(null);
270
+ setResults([]);
 
 
 
 
 
271
 
272
  const formData = new FormData();
273
  formData.append("srt_file", srtFile);
 
280
  formData.append("compression_quality", config.quality);
281
 
282
  try {
283
+ const response = await fetch('/api/analyze', { method: 'POST', body: formData });
284
+ if (!response.ok) throw new Error(await response.text());
 
 
 
 
 
 
 
 
285
  const data = await response.json();
286
+
287
  if (data.status === 'success') {
288
+ setResults(data.results);
289
+ // Clear preview to show results
290
+ setPreviewItems([]);
 
 
 
 
 
 
 
 
291
  } else {
292
+ throw new Error(data.message);
293
  }
294
  } catch (err) {
295
  setError(err.message);
296
  } finally {
297
  setIsProcessing(false);
 
 
298
  }
299
  };
300
 
301
+ // 3. Download Logic
302
+ const handleDownload = () => {
303
+ let content = "";
304
+ results.forEach(r => {
305
+ const text = (r.status === 'mismatch' && r.detected) ? r.detected : r.expected;
306
+ content += `${r.srt_id}\n${r.srt_time}\n${text}\n\n`;
 
 
 
307
  });
308
+ const url = URL.createObjectURL(new Blob([content], { type: 'text/plain' }));
 
309
  const a = document.createElement('a');
310
  a.href = url;
311
+ a.download = "corrected.srt";
312
  document.body.appendChild(a);
313
  a.click();
314
  document.body.removeChild(a);
 
315
  };
316
 
317
  const handleReset = () => {
318
  setSrtFile(null);
319
  setMediaFiles([]);
320
+ setPreviewItems([]);
321
+ setResults([]);
322
  setError(null);
323
  };
324
 
325
  return (
326
+ <div className="min-h-screen bg-slate-50 text-slate-900 font-sans pb-20">
327
+ <Header onOpenSettings={() => setShowSettings(true)} activeWorkers={isProcessing ? config.apiKeys.split('\n').length : 0} />
328
+ <SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} config={config} setConfig={setConfig} />
 
 
 
 
 
 
329
 
330
+ <main className="max-w-6xl mx-auto p-6 space-y-6">
331
+
332
+ {/* Upload Area */}
333
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200 grid md:grid-cols-2 gap-6">
334
+ <div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:bg-slate-50 transition-colors relative">
335
+ <input type="file" accept=".srt" onChange={e => setSrtFile(e.target.files[0])} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
336
+ <div className="pointer-events-none">
337
+ <div className="flex justify-center mb-2"><Icons.Upload className={`w-8 h-8 ${srtFile ? 'text-green-500' : 'text-slate-400'}`} /></div>
338
+ <div className="font-semibold text-sm">{srtFile ? srtFile.name : "Upload SRT File"}</div>
339
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  </div>
341
+ <div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:bg-slate-50 transition-colors relative">
342
+ <input type="file" accept=".rar,.zip,image/*" multiple onChange={e => setMediaFiles(e.target.files)} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
343
+ <div className="pointer-events-none">
344
+ <div className="flex justify-center mb-2"><Icons.Upload className={`w-8 h-8 ${mediaFiles.length > 0 ? 'text-green-500' : 'text-slate-400'}`} /></div>
345
+ <div className="font-semibold text-sm">{mediaFiles.length > 0 ? `${mediaFiles.length} Files` : "Upload Media (Img/RAR/ZIP)"}</div>
346
+ </div>
 
347
  </div>
348
+ </div>
349
 
350
+ {/* Error Banner */}
351
+ {error && <div className="bg-red-50 text-red-700 p-4 rounded-lg border border-red-200 flex items-center gap-2"><Icons.X className="w-5 h-5"/>{error}</div>}
 
 
 
 
352
 
353
+ {/* Action Bar (Only shows when content is loaded) */}
354
+ {(previewItems.length > 0 || results.length > 0) && (
355
+ <div className="flex justify-between items-center bg-white p-4 rounded-xl shadow-sm border border-slate-200 sticky top-4 z-40">
356
+ <div className="font-bold text-lg">
357
+ {results.length > 0 ? "Analysis Results" : "Preview Alignment"}
358
+ </div>
359
+ <div className="flex gap-3">
360
+ <button onClick={handleReset} className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg text-sm font-medium">Reset</button>
361
+ {results.length > 0 ? (
362
+ <button onClick={handleDownload} className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-bold shadow-sm transition-all">
363
+ <Icons.Download className="w-4 h-4" /> Download SRT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </button>
365
+ ) : (
366
  <button
367
+ onClick={handleAnalyze}
368
  disabled={isProcessing}
369
+ className={`flex items-center gap-2 px-6 py-2 rounded-lg font-bold text-white shadow-sm transition-all ${isProcessing ? 'bg-slate-400' : 'bg-green-600 hover:bg-green-700'}`}
370
  >
371
+ {isProcessing ? <Loader2 className="w-4 h-4 animate-spin"/> : <Icons.Zap className="w-4 h-4"/>}
372
+ {isProcessing ? "Analyzing..." : "Analyze with Gemini"}
373
  </button>
374
+ )}
375
  </div>
376
+ </div>
377
+ )}
378
 
379
+ {/* Preview List (Before Analysis) */}
380
+ {previewItems.length > 0 && results.length === 0 && (
381
+ <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden divide-y divide-slate-100">
382
+ {archiveMode && <div className="p-3 bg-amber-50 text-amber-800 text-sm text-center border-b border-amber-100">📦 Archive mode active. Images will be extracted and paired on server.</div>}
383
+ {previewItems.map((item) => (
384
+ <div key={item.id} className="grid grid-cols-12 gap-4 p-4 hover:bg-slate-50 transition-colors group">
385
+ <div className="col-span-3">
386
+ <div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
387
+ {item.imgUrl ? (
388
+ <img src={item.imgUrl} className="w-full h-full object-contain" />
389
+ ) : (
390
+ <div className="w-full h-full flex items-center justify-center text-slate-400 text-xs flex-col">
391
+ {item.isArchive ? <Icons.Archive className="w-6 h-6 mb-1"/> : <Icons.X className="w-6 h-6 mb-1"/>}
392
+ {item.isArchive ? "Server Side" : "No Image"}
393
  </div>
394
+ )}
395
+ <div className="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-[10px] p-1 truncate font-mono">{item.imgName}</div>
396
  </div>
397
+ </div>
398
+ <div className="col-span-9 flex flex-col justify-center">
399
+ <div className="flex items-center justify-between mb-1">
400
+ <span className="text-xs font-bold text-slate-400 uppercase">Line #{item.id}</span>
401
+ <span className="text-xs font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded">{item.srt.time.split('-->')[0].trim()}</span>
 
 
402
  </div>
403
+ <div className="text-sm font-medium text-slate-800 p-2 bg-slate-50 rounded border border-slate-100 group-hover:bg-white">{item.srt.text}</div>
404
+ </div>
405
+ </div>
406
+ ))}
407
+ </div>
408
+ )}
409
 
410
+ {/* Results List (After Analysis) */}
411
+ {results.length > 0 && (
412
+ <div className="space-y-4">
413
+ {results.map((res, idx) => (
414
+ <div key={idx} className={`bg-white rounded-xl shadow-sm border p-4 flex gap-4 ${res.status === 'mismatch' ? 'border-red-200 bg-red-50' : 'border-slate-200'}`}>
415
+ <div className="w-40 bg-slate-100 rounded-lg overflow-hidden flex-shrink-0 border border-slate-200">
416
+ <img src={res.thumb} className="w-full h-full object-cover" />
417
+ </div>
418
+ <div className="flex-1 grid md:grid-cols-2 gap-6">
419
+ <div>
420
+ <div className="text-xs font-bold text-slate-400 uppercase mb-1">Original Text</div>
421
+ <div className="p-3 bg-slate-100/50 rounded-lg text-sm">{res.expected}</div>
422
+ <div className="mt-2 text-xs text-slate-400 font-mono">{res.filename}</div>
423
+ </div>
424
+ <div>
425
  <div className="flex justify-between items-center mb-1">
426
+ <span className="text-xs font-bold text-slate-400 uppercase">AI Verified</span>
427
+ <StatusBadge status={res.status} />
428
  </div>
429
+ <div className={`p-3 rounded-lg text-sm border ${res.status === 'mismatch' ? 'bg-white border-red-200 text-red-700' : 'bg-green-50 border-green-200 text-green-800'}`}>
430
+ {res.detected || "No Text Detected"}
 
 
 
 
 
 
 
431
  </div>
432
+ {res.reason && <div className="text-xs text-slate-500 mt-2 italic">{res.reason}</div>}
433
  </div>
434
  </div>
435
+ </div>
436
+ ))}
437
+ </div>
438
  )}
439
 
440
  {pairs.length === 0 && !srtFile && (