bigbossmonster commited on
Commit
0f1842c
·
verified ·
1 Parent(s): 9294e24

Upload index.html

Browse files
Files changed (1) hide show
  1. static/index.html +218 -0
static/index.html ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Subtitle QC Client</title>
7
+
8
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
9
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
10
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+
13
+ <style>
14
+ body { font-family: 'Segoe UI', sans-serif; }
15
+ </style>
16
+ </head>
17
+ <body class="bg-slate-50 text-slate-900">
18
+ <div id="root"></div>
19
+
20
+ <script type="text/babel">
21
+ const { useState, useEffect } = React;
22
+
23
+ const Icon = ({ d, className }) => (
24
+ <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}><path d={d}/></svg>
25
+ );
26
+ const Icons = {
27
+ 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"/>,
28
+ 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"/>,
29
+ X: ({className}) => <Icon className={className} d="M18 6 6 18M6 6l12 12"/>,
30
+ 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"/>,
31
+ 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>
32
+ };
33
+
34
+ const App = () => {
35
+ const [apiKeys, setApiKeys] = useState(() => localStorage.getItem('qc_api_keys') || '');
36
+ const [srtFile, setSrtFile] = useState(null);
37
+ const [mediaFiles, setMediaFiles] = useState([]);
38
+
39
+ const [isUploading, setIsUploading] = useState(false);
40
+ const [results, setResults] = useState([]);
41
+ const [error, setError] = useState(null);
42
+ const [showSettings, setShowSettings] = useState(false);
43
+
44
+ useEffect(() => {
45
+ localStorage.setItem('qc_api_keys', apiKeys);
46
+ }, [apiKeys]);
47
+
48
+ const handleAnalyze = async () => {
49
+ if (!srtFile || mediaFiles.length === 0 || !apiKeys) {
50
+ setError("Please provide SRT, Media files, and API Key.");
51
+ return;
52
+ }
53
+
54
+ setIsUploading(true);
55
+ setError(null);
56
+ setResults([]);
57
+
58
+ const formData = new FormData();
59
+ formData.append("srt_file", srtFile);
60
+ for (let i = 0; i < mediaFiles.length; i++) {
61
+ formData.append("media_files", mediaFiles[i]);
62
+ }
63
+ formData.append("api_keys", apiKeys);
64
+ formData.append("batch_size", 20);
65
+
66
+ try {
67
+ // Send to current origin /api/analyze
68
+ const response = await fetch('/api/analyze', {
69
+ method: 'POST',
70
+ body: formData
71
+ });
72
+
73
+ if (!response.ok) {
74
+ const errText = await response.text();
75
+ throw new Error(`Backend Error (${response.status}): ${errText}`);
76
+ }
77
+
78
+ const data = await response.json();
79
+ if (data.status === 'success') {
80
+ setResults(data.results);
81
+ } else {
82
+ throw new Error(data.message || "Unknown error");
83
+ }
84
+ } catch (err) {
85
+ setError(err.message);
86
+ } finally {
87
+ setIsUploading(false);
88
+ }
89
+ };
90
+
91
+ const downloadCorrectedSRT = () => {
92
+ if (results.length === 0) return;
93
+ let content = "";
94
+ results.forEach(r => {
95
+ content += `${r.srt_id}\n${r.srt_time}\n${r.status === 'mismatch' ? r.detected : r.expected}\n\n`;
96
+ });
97
+ const blob = new Blob([content], {type: "text/plain"});
98
+ const url = URL.createObjectURL(blob);
99
+ const a = document.createElement("a");
100
+ a.href = url;
101
+ a.download = "corrected_subtitles.srt";
102
+ document.body.appendChild(a);
103
+ a.click();
104
+ document.body.removeChild(a);
105
+ };
106
+
107
+ return (
108
+ <div className="min-h-screen bg-slate-100 font-sans text-slate-900 pb-20">
109
+ <header className="bg-slate-900 text-white p-4 shadow-lg sticky top-0 z-50">
110
+ <div className="max-w-6xl mx-auto flex justify-between items-center">
111
+ <h1 className="text-xl font-bold">Subtitle QC <span className="text-slate-400 font-normal">Docker</span></h1>
112
+ <button onClick={() => setShowSettings(!showSettings)} className="p-2 hover:bg-slate-800 rounded-full">
113
+ <Icons.Settings className="w-5 h-5" />
114
+ </button>
115
+ </div>
116
+ </header>
117
+
118
+ {showSettings && (
119
+ <div className="bg-slate-800 text-white p-6 shadow-inner animate-in slide-in-from-top-4">
120
+ <div className="max-w-6xl mx-auto">
121
+ <label className="block text-sm font-medium mb-1 text-blue-300">Gemini API Keys (One per line)</label>
122
+ <textarea
123
+ value={apiKeys}
124
+ onChange={e => setApiKeys(e.target.value)}
125
+ className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm h-20 font-mono"
126
+ placeholder="AIzaSy..."
127
+ />
128
+ </div>
129
+ </div>
130
+ )}
131
+
132
+ <main className="max-w-6xl mx-auto p-6 space-y-6">
133
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
134
+ <div className="grid md:grid-cols-2 gap-6">
135
+ <div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:bg-slate-50 transition-colors">
136
+ <input type="file" accept=".srt" id="srt" className="hidden" onChange={e => setSrtFile(e.target.files[0])} />
137
+ <label htmlFor="srt" className="cursor-pointer block">
138
+ <div className="text-lg font-semibold text-slate-700">{srtFile ? srtFile.name : "Select SRT File"}</div>
139
+ <div className="text-xs text-slate-400 mt-2">.srt subtitle file</div>
140
+ </label>
141
+ </div>
142
+ <div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:bg-slate-50 transition-colors">
143
+ <input type="file" accept=".rar,.zip,image/*" multiple id="media" className="hidden" onChange={e => setMediaFiles(e.target.files)} />
144
+ <label htmlFor="media" className="cursor-pointer block">
145
+ <div className="text-lg font-semibold text-slate-700">{mediaFiles.length > 0 ? `${mediaFiles.length} Files Selected` : "Select Media"}</div>
146
+ <div className="text-xs text-slate-400 mt-2">Images, .rar or .zip archives</div>
147
+ </label>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="mt-6 flex justify-end gap-3">
152
+ {results.length > 0 && (
153
+ <button onClick={downloadCorrectedSRT} className="flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-bold transition-all">
154
+ <Icons.Download className="w-5 h-5" /> Download Corrected
155
+ </button>
156
+ )}
157
+ <button
158
+ onClick={handleAnalyze}
159
+ disabled={isUploading}
160
+ className={`flex items-center gap-2 px-8 py-3 rounded-lg font-bold text-white transition-all ${isUploading ? 'bg-slate-400 cursor-wait' : 'bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg'}`}
161
+ >
162
+ {isUploading ? "Processing..." : "Analyze"}
163
+ {!isUploading && <Icons.Upload className="w-5 h-5" />}
164
+ </button>
165
+ </div>
166
+
167
+ {error && (
168
+ <div className="mt-4 p-4 bg-red-50 text-red-700 rounded-lg border border-red-200 flex items-center gap-2">
169
+ <Icons.X className="w-5 h-5" /> {error}
170
+ </div>
171
+ )}
172
+ </div>
173
+
174
+ {results.length > 0 && (
175
+ <div className="space-y-4 animate-in fade-in duration-500">
176
+ <div className="flex justify-between items-center">
177
+ <h2 className="text-xl font-bold">Analysis Results ({results.length})</h2>
178
+ <div className="flex gap-2 text-sm">
179
+ <span className="px-2 py-1 bg-green-100 text-green-700 rounded">Match: {results.filter(r => r.status === 'match').length}</span>
180
+ <span className="px-2 py-1 bg-red-100 text-red-700 rounded">Mismatch: {results.filter(r => r.status === 'mismatch').length}</span>
181
+ </div>
182
+ </div>
183
+
184
+ {results.map((item, idx) => (
185
+ <div key={idx} className={`bg-white rounded-lg shadow-sm border p-4 flex gap-4 ${item.status === 'mismatch' ? 'border-red-200 bg-red-50' : 'border-slate-200'}`}>
186
+ <div className="w-32 h-20 bg-slate-200 rounded overflow-hidden flex-shrink-0">
187
+ <img src={item.thumb} alt="thumb" className="w-full h-full object-cover" />
188
+ </div>
189
+ <div className="flex-1 grid md:grid-cols-2 gap-4">
190
+ <div>
191
+ <div className="text-xs text-slate-500 font-mono mb-1">{item.srt_id} | {item.filename}</div>
192
+ <div className="font-medium text-slate-800">{item.expected}</div>
193
+ </div>
194
+ <div className="border-l pl-4 border-slate-200">
195
+ <div className="flex items-center gap-2 mb-1">
196
+ <span className="text-xs font-bold uppercase text-slate-400">Detected</span>
197
+ {item.status === 'match' ? <Icons.Check className="w-4 h-4 text-green-600" /> : <Icons.X className="w-4 h-4 text-red-600" />}
198
+ </div>
199
+ <div className={`text-sm ${item.status === 'mismatch' ? 'text-red-700 font-semibold' : 'text-slate-600'}`}>
200
+ {item.detected || "(No text detected)"}
201
+ </div>
202
+ {item.reason && <div className="text-xs text-slate-400 mt-1 italic">{item.reason}</div>}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ ))}
207
+ </div>
208
+ )}
209
+ </main>
210
+ </div>
211
+ );
212
+ };
213
+
214
+ const root = ReactDOM.createRoot(document.getElementById('root'));
215
+ root.render(<App />);
216
+ </script>
217
+ </body>
218
+ </html>