Sada8888 commited on
Commit
ffe118a
·
verified ·
1 Parent(s): 4c7f5ad

Update templates/matnbesada.html

Browse files
Files changed (1) hide show
  1. templates/matnbesada.html +863 -70
templates/matnbesada.html CHANGED
@@ -3,99 +3,892 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>تبدیل صدا به متن آلفا</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;900&display=swap" rel="stylesheet">
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
- :root { --p: #7C4DFF; --bg: #09090e; --glass: rgba(255,255,255,0.05); }
11
- body { font-family: 'Vazirmatn'; background: var(--bg); color: white; padding: 20px; display: flex; justify-content: center; }
12
- .card { max-width: 650px; width: 100%; background: var(--glass); backdrop-filter: blur(15px); border: 1px solid rgba(255,255,255,0.1); border-radius: 25px; padding: 30px; }
13
- h1 { text-align: center; font-size: 1.5rem; color: var(--p); margin-bottom: 25px; }
14
- .upload-zone { border: 2px dashed #333; padding: 40px; text-align: center; border-radius: 20px; cursor: pointer; transition: 0.3s; }
15
- .upload-zone:hover { border-color: var(--p); background: rgba(124, 77, 255, 0.05); }
16
- .btn { width: 100%; padding: 16px; border-radius: 15px; border: none; background: var(--p); color: white; font-weight: bold; cursor: pointer; margin-top: 20px; font-size: 1rem; }
17
- .btn:disabled { background: #222; color: #555; }
18
- #status { text-align: center; margin-top: 15px; font-size: 0.9rem; color: #aaa; }
19
- .results { display: none; margin-top: 30px; border-top: 1px solid #222; padding-top: 20px; }
20
- pre { background: rgba(0,0,0,0.4); padding: 15px; border-radius: 12px; white-space: pre-wrap; font-size: 0.85rem; max-height: 200px; overflow-y: auto; color: #ddd; border: 1px solid #222; }
21
- .dl-group { display: flex; gap: 10px; margin-top: 15px; }
22
- .dl-group a { flex: 1; text-align: center; background: #1a1a1a; padding: 12px; border-radius: 10px; color: white; text-decoration: none; font-size: 0.8rem; border: 1px solid #333; }
23
- .dl-group a:hover { background: #252525; border-color: var(--p); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </style>
25
  </head>
26
  <body>
27
 
28
- <div class="card">
29
- <h1><i class="fa-solid fa-wand-magic-sparkles"></i> تبدیل هوشمند صوت به متن</h1>
30
-
31
- <div class="upload-zone" onclick="document.getElementById('fileIn').click()">
32
- <i class="fa-solid fa-file-audio" style="font-size: 3rem; color: #444; margin-bottom: 15px;"></i>
33
- <p id="fileMsg">فایل صوتی یا ویدیویی را انتخاب کنید</p>
34
- <input type="file" id="fileIn" hidden accept="audio/*,video/*">
35
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- <button id="runBtn" class="btn">🚀 شروع استخراج متن و زیرنویس</button>
38
- <div id="status">آماده برای پردازش</div>
 
 
 
 
39
 
40
- <div id="results" class="results">
41
- <p style="margin-bottom: 8px; font-size: 0.9rem; color: var(--p); font-weight: bold;">متن شناسایی شده:</p>
42
- <pre id="txtView"></pre>
43
- <div class="dl-group">
44
- <a id="linkTxt" href="#" download="text.txt"><i class="fa-solid fa-download"></i> دانلود متن</a>
45
- <a id="linkSrt" href="#" download="subtitle.srt"><i class="fa-solid fa-closed-captioning"></i> دانلود زیرنویس</a>
 
 
 
 
 
 
 
 
 
46
  </div>
47
  </div>
 
 
 
 
48
  </div>
49
 
50
  <script>
51
- const runBtn = document.getElementById('runBtn'), fileIn = document.getElementById('fileIn'), status = document.getElementById('status'), results = document.getElementById('results');
 
 
 
 
 
52
 
53
- fileIn.onchange = () => { if(fileIn.files[0]) document.getElementById('fileMsg').innerText = fileIn.files[0].name; };
 
 
54
 
55
- runBtn.onclick = async () => {
56
- if(!fileIn.files[0]) return alert('لطفاً ابتدا فایل را انتخاب کنید.');
57
-
58
- runBtn.disabled = true;
59
- results.style.display = 'none';
60
- status.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> در حال ارسال به سرور گیت‌هاب...';
 
 
61
 
62
- const fd = new FormData();
63
- // استفاده از فیلد تصویر برای ارسال فایل (چون api/edit یا api/generate-flux2 فایل میگیرند)
64
- // اما چون api/generate کلا فایل نمیگیرد، ما از api/chat استفاده میکنیم ولی با یک ترفند
 
65
 
66
- // بهترین راه برای سیستم شما: استفاده از api/chat و تغییر نام اکشن
67
- fd.append('file', fileIn.files[0]);
68
- fd.append('prompt', ''); // متن خالی برای اینکه چت متوجه شود فقط فایل است
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  try {
71
- // نکته: درخواست به api/chat میرود اما در گیت‌هاب اسکریپت matnbesada.py اجرا می‌شود
72
- const res = await fetch('/api/chat', { method: 'POST', body: fd });
73
- const data = await res.json();
74
- if(data.status === 'success') {
75
- wait(data.run_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
- } catch(e) { status.innerText = "خطا در آپلود"; runBtn.disabled = false; }
78
- };
79
 
80
- async function wait(id) {
81
- status.innerText = "در حال پردازش در اکشن (A100)... لطفاً صبور باشید.";
82
- const check = setInterval(async () => {
83
- try {
84
- const res = await fetch(`/api/status/${id}`);
85
- const data = await res.json();
86
- if(data.status === 'ready') {
87
- clearInterval(check);
88
- status.innerHTML = '<span style="color:#00e676">✅ با موفقیت انجام شد!</span>';
89
- const text = await (await fetch(data.url)).text();
90
- document.getElementById('txtView').innerText = text;
91
- document.getElementById('linkTxt').href = data.url;
92
- document.getElementById('linkSrt').href = `/static/images/${id}_srt.srt`;
93
- results.style.display = 'block';
94
- runBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
- } catch(e){}
97
- }, 5000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
  </script>
 
100
  </body>
101
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>استودیو هوشمند تبدیل صدا به متن و زیرنویس</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700;900&display=swap" rel="stylesheet">
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
+ :root {
11
+ --bg: #09090e;
12
+ --glass-bg: rgba(20, 20, 30, 0.6);
13
+ --glass-border: rgba(255, 255, 255, 0.08);
14
+ --primary: #7C4DFF;
15
+ --primary-glow: rgba(124, 77, 255, 0.35);
16
+ --secondary: #ff7043;
17
+ --text-main: #f3f4f6;
18
+ --text-sub: #9ca3af;
19
+ --success: #00e676;
20
+ --danger: #ff5252;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ font-family: 'Vazirmatn', sans-serif;
28
+ user-select: none;
29
+ }
30
+
31
+ body {
32
+ background-color: var(--bg);
33
+ background-image: radial-gradient(circle at 10% 20%, rgba(124, 77, 255, 0.08) 0%, transparent 40%),
34
+ radial-gradient(circle at 90% 80%, rgba(255, 112, 67, 0.06) 0%, transparent 40%);
35
+ background-attachment: fixed;
36
+ color: var(--text-main);
37
+ min-height: 100vh;
38
+ display: flex;
39
+ flex-direction: column;
40
+ padding: 20px;
41
+ }
42
+
43
+ .container {
44
+ max-width: 1400px;
45
+ width: 100%;
46
+ margin: 0 auto;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 20px;
50
+ flex: 1;
51
+ }
52
+
53
+ header {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: space-between;
57
+ padding: 15px 25px;
58
+ background: var(--glass-bg);
59
+ border: 1px solid var(--glass-border);
60
+ border-radius: 20px;
61
+ backdrop-filter: blur(16px);
62
+ -webkit-backdrop-filter: blur(16px);
63
+ }
64
+
65
+ .logo-area {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 15px;
69
+ }
70
+
71
+ .logo-icon {
72
+ width: 45px;
73
+ height: 45px;
74
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
75
+ border-radius: 12px;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ color: #fff;
80
+ font-size: 1.3rem;
81
+ box-shadow: 0 4px 15px var(--primary-glow);
82
+ }
83
+
84
+ h1 {
85
+ font-size: 1.25rem;
86
+ font-weight: 900;
87
+ background: linear-gradient(to left, #fff, var(--text-sub));
88
+ -webkit-background-clip: text;
89
+ -webkit-text-fill-color: transparent;
90
+ }
91
+
92
+ .status-badge {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 8px;
96
+ background: rgba(255, 255, 255, 0.05);
97
+ border: 1px solid var(--glass-border);
98
+ padding: 6px 14px;
99
+ border-radius: 999px;
100
+ font-size: 0.8rem;
101
+ font-weight: bold;
102
+ color: var(--text-sub);
103
+ }
104
+
105
+ .status-dot {
106
+ width: 8px;
107
+ height: 8px;
108
+ background: var(--success);
109
+ border-radius: 50%;
110
+ box-shadow: 0 0 8px var(--success);
111
+ }
112
+
113
+ .workspace {
114
+ display: grid;
115
+ grid-template-columns: 1fr;
116
+ gap: 20px;
117
+ flex: 1;
118
+ }
119
+
120
+ @media (min-width: 1024px) {
121
+ .workspace {
122
+ grid-template-columns: 420px 1fr;
123
+ }
124
+ }
125
+
126
+ .input-panel {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 20px;
130
+ }
131
+
132
+ .card {
133
+ background: var(--glass-bg);
134
+ border: 1px solid var(--glass-border);
135
+ border-radius: 24px;
136
+ padding: 25px;
137
+ backdrop-filter: blur(16px);
138
+ -webkit-backdrop-filter: blur(16px);
139
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
140
+ }
141
+
142
+ .card-title {
143
+ font-size: 1rem;
144
+ font-weight: 700;
145
+ margin-bottom: 20px;
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 10px;
149
+ color: #fff;
150
+ }
151
+
152
+ .card-title i {
153
+ color: var(--primary);
154
+ }
155
+
156
+ .dropzone {
157
+ border: 2px dashed rgba(255, 255, 255, 0.15);
158
+ border-radius: 18px;
159
+ padding: 30px 20px;
160
+ text-align: center;
161
+ cursor: pointer;
162
+ transition: all 0.2s ease;
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: center;
166
+ gap: 15px;
167
+ background: rgba(255, 255, 255, 0.01);
168
+ }
169
+
170
+ .dropzone:hover, .dropzone.dragover {
171
+ border-color: var(--primary);
172
+ background: rgba(124, 77, 255, 0.03);
173
+ }
174
+
175
+ .dropzone-icon {
176
+ font-size: 2.5rem;
177
+ color: var(--text-sub);
178
+ transition: transform 0.2s;
179
+ }
180
+
181
+ .dropzone:hover .dropzone-icon {
182
+ transform: translateY(-5px);
183
+ color: var(--primary);
184
+ }
185
+
186
+ .mic-section {
187
+ border: 1px solid rgba(255, 255, 255, 0.05);
188
+ background: rgba(0, 0, 0, 0.15);
189
+ border-radius: 18px;
190
+ padding: 20px;
191
+ display: flex;
192
+ flex-direction: column;
193
+ align-items: center;
194
+ gap: 15px;
195
+ }
196
+
197
+ .mic-btn {
198
+ width: 60px;
199
+ height: 60px;
200
+ border-radius: 50%;
201
+ border: none;
202
+ background: linear-gradient(135deg, var(--primary), #a885ff);
203
+ color: #fff;
204
+ font-size: 1.4rem;
205
+ cursor: pointer;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ box-shadow: 0 4px 15px var(--primary-glow);
210
+ position: relative;
211
+ }
212
+
213
+ .mic-btn.recording {
214
+ background: var(--danger);
215
+ box-shadow: 0 0 20px rgba(255, 82, 82, 0.4);
216
+ animation: pulse 1.5s infinite;
217
+ }
218
+
219
+ @keyframes pulse {
220
+ 0% { transform: scale(1); }
221
+ 50% { transform: scale(1.08); }
222
+ 100% { transform: scale(1); }
223
+ }
224
+
225
+ .wave-container {
226
+ display: none;
227
+ align-items: center;
228
+ gap: 3px;
229
+ height: 20px;
230
+ }
231
+
232
+ .wave-container.active {
233
+ display: flex;
234
+ }
235
+
236
+ .wave-bar {
237
+ width: 3px;
238
+ height: 100%;
239
+ background: var(--danger);
240
+ border-radius: 3px;
241
+ animation: jump 0.8s ease-in-out infinite alternate;
242
+ }
243
+
244
+ .wave-bar:nth-child(2) { animation-delay: 0.1s; }
245
+ .wave-bar:nth-child(3) { animation-delay: 0.2s; }
246
+ .wave-bar:nth-child(4) { animation-delay: 0.3s; }
247
+ .wave-bar:nth-child(5) { animation-delay: 0.15s; }
248
+
249
+ @keyframes jump {
250
+ 0% { height: 5px; }
251
+ 100% { height: 25px; }
252
+ }
253
+
254
+ .form-group {
255
+ margin-bottom: 15px;
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 8px;
259
+ }
260
+
261
+ .form-group label {
262
+ font-size: 0.85rem;
263
+ font-weight: bold;
264
+ color: var(--text-sub);
265
+ }
266
+
267
+ .form-control {
268
+ background: rgba(255, 255, 255, 0.05);
269
+ border: 1px solid var(--glass-border);
270
+ color: #fff;
271
+ padding: 12px 16px;
272
+ border-radius: 12px;
273
+ font-size: 0.9rem;
274
+ width: 100%;
275
+ outline: none;
276
+ transition: all 0.2s;
277
+ }
278
+
279
+ .form-control:focus {
280
+ border-color: var(--primary);
281
+ background: rgba(255, 255, 255, 0.08);
282
+ }
283
+
284
+ select.form-control option {
285
+ background-color: #12121c;
286
+ color: #fff;
287
+ }
288
+
289
+ .checkbox-container {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 10px;
293
+ cursor: pointer;
294
+ padding: 5px 0;
295
+ }
296
+
297
+ .checkbox-box {
298
+ width: 20px;
299
+ height: 20px;
300
+ border: 1px solid var(--glass-border);
301
+ background: rgba(255, 255, 255, 0.05);
302
+ border-radius: 6px;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ color: transparent;
307
+ font-size: 0.75rem;
308
+ transition: all 0.15s;
309
+ }
310
+
311
+ .checkbox-container input[type="checkbox"] {
312
+ display: none;
313
+ }
314
+
315
+ .checkbox-container input[type="checkbox"]:checked + .checkbox-box {
316
+ background: var(--primary);
317
+ border-color: var(--primary);
318
+ color: #fff;
319
+ box-shadow: 0 0 8px var(--primary-glow);
320
+ }
321
+
322
+ .btn-submit {
323
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
324
+ border: none;
325
+ color: #fff;
326
+ padding: 14px;
327
+ border-radius: 14px;
328
+ font-weight: 700;
329
+ font-size: 1rem;
330
+ cursor: pointer;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ gap: 10px;
335
+ box-shadow: 0 4px 20px rgba(124, 77, 255, 0.25);
336
+ transition: all 0.2s;
337
+ width: 100%;
338
+ }
339
+
340
+ .btn-submit:hover {
341
+ transform: translateY(-2px);
342
+ box-shadow: 0 6px 25px rgba(124, 77, 255, 0.35);
343
+ }
344
+
345
+ .btn-submit:disabled {
346
+ background: rgba(255, 255, 255, 0.1);
347
+ color: var(--text-sub);
348
+ box-shadow: none;
349
+ cursor: not-allowed;
350
+ transform: none;
351
+ }
352
+
353
+ .output-panel {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: 20px;
357
+ }
358
+
359
+ .tabs-header {
360
+ display: flex;
361
+ gap: 10px;
362
+ background: rgba(255, 255, 255, 0.03);
363
+ border: 1px solid var(--glass-border);
364
+ padding: 6px;
365
+ border-radius: 16px;
366
+ }
367
+
368
+ .tab-btn {
369
+ flex: 1;
370
+ padding: 10px;
371
+ background: transparent;
372
+ border: none;
373
+ color: var(--text-sub);
374
+ font-size: 0.85rem;
375
+ font-weight: 700;
376
+ border-radius: 10px;
377
+ cursor: pointer;
378
+ transition: all 0.2s;
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ gap: 8px;
383
+ }
384
+
385
+ .tab-btn.active {
386
+ background: rgba(255, 255, 255, 0.07);
387
+ color: #fff;
388
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
389
+ }
390
+
391
+ .tab-content {
392
+ display: none;
393
+ position: relative;
394
+ }
395
+
396
+ .tab-content.active {
397
+ display: block;
398
+ }
399
+
400
+ .text-viewer {
401
+ width: 100%;
402
+ height: 450px;
403
+ background: rgba(0, 0, 0, 0.2);
404
+ border: 1px solid var(--glass-border);
405
+ border-radius: 18px;
406
+ padding: 20px;
407
+ color: #fff;
408
+ font-size: 0.95rem;
409
+ line-height: 1.8;
410
+ overflow-y: auto;
411
+ white-space: pre-wrap;
412
+ user-select: text;
413
+ text-align: justify;
414
+ }
415
+
416
+ .action-overlay {
417
+ position: absolute;
418
+ top: 15px;
419
+ left: 15px;
420
+ display: flex;
421
+ gap: 8px;
422
+ z-index: 10;
423
+ }
424
+
425
+ .action-btn {
426
+ background: rgba(20, 20, 30, 0.8);
427
+ border: 1px solid var(--glass-border);
428
+ color: var(--text-sub);
429
+ width: 40px;
430
+ height: 40px;
431
+ border-radius: 10px;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ cursor: pointer;
436
+ transition: all 0.2s;
437
+ backdrop-filter: blur(5px);
438
+ }
439
+
440
+ .action-btn:hover {
441
+ border-color: var(--primary);
442
+ color: #fff;
443
+ transform: scale(1.05);
444
+ }
445
+
446
+ .processing-overlay {
447
+ display: none;
448
+ flex-direction: column;
449
+ align-items: center;
450
+ justify-content: center;
451
+ gap: 20px;
452
+ height: 450px;
453
+ background: rgba(9, 9, 14, 0.85);
454
+ border: 1px solid var(--glass-border);
455
+ border-radius: 18px;
456
+ position: absolute;
457
+ top: 0;
458
+ left: 0;
459
+ width: 100%;
460
+ z-index: 100;
461
+ }
462
+
463
+ .spinner {
464
+ width: 60px;
465
+ height: 60px;
466
+ border: 4px solid rgba(124, 77, 255, 0.1);
467
+ border-top: 4px solid var(--primary);
468
+ border-right: 4px solid var(--primary);
469
+ border-radius: 50%;
470
+ animation: spin 1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
471
+ }
472
+
473
+ @keyframes spin {
474
+ 0% { transform: rotate(0deg); }
475
+ 100% { transform: rotate(360deg); }
476
+ }
477
+
478
+ .loading-text {
479
+ font-size: 1rem;
480
+ font-weight: 700;
481
+ color: #fff;
482
+ animation: pulseText 1.5s infinite alternate;
483
+ }
484
+
485
+ @keyframes pulseText {
486
+ from { opacity: 0.6; }
487
+ to { opacity: 1; }
488
+ }
489
+
490
+ .error-logger {
491
+ display: none;
492
+ background: rgba(255, 82, 82, 0.08);
493
+ border: 1px solid rgba(255, 82, 82, 0.2);
494
+ border-radius: 16px;
495
+ padding: 15px 20px;
496
+ color: #ff8a8a;
497
+ font-size: 0.85rem;
498
+ line-height: 1.6;
499
+ }
500
+
501
+ .audio-player-container {
502
+ display: none;
503
+ width: 100%;
504
+ background: rgba(255, 255, 255, 0.02);
505
+ border: 1px solid var(--glass-border);
506
+ border-radius: 14px;
507
+ padding: 12px;
508
+ align-items: center;
509
+ gap: 10px;
510
+ }
511
+
512
+ audio {
513
+ width: 100%;
514
+ height: 35px;
515
+ outline: none;
516
+ }
517
+
518
+ footer {
519
+ text-align: center;
520
+ padding: 20px;
521
+ font-size: 0.8rem;
522
+ color: var(--text-sub);
523
+ border-top: 1px solid rgba(255,255,255,0.03);
524
+ margin-top: auto;
525
+ }
526
  </style>
527
  </head>
528
  <body>
529
 
530
+ <div class="container">
531
+ <header>
532
+ <div class="logo-area">
533
+ <div class="logo-icon"><i class="fa-solid fa-microphone-lines"></i></div>
534
+ <div>
535
+ <h1ستودیو تبدیل صدا به متن و زیرنویس</h1>
536
+ <p style="font-size: 0.75rem; color: var(--text-sub); margin-top: 3px;">پردازش از طریق شبکه یکپارچه گیت‌هاب اکشن</p>
537
+ </div>
538
+ </div>
539
+ <div class="status-badge">
540
+ <div class="status-dot"></div>
541
+ <span>آماده دریافت فرمان پردازش</span>
542
+ </div>
543
+ </header>
544
+
545
+ <div class="workspace">
546
+ <div class="input-panel">
547
+ <div class="card">
548
+ <div class="card-title"><i class="fa-solid fa-audio-description"></i> ورودی صدا یا ویدیو</div>
549
+
550
+ <div style="display: flex; flex-direction: column; gap: 15px;">
551
+ <div class="dropzone" id="dropzone" onclick="document.getElementById('fileIn').click()">
552
+ <div class="dropzone-icon"><i class="fa-solid fa-cloud-arrow-up"></i></div>
553
+ <div>
554
+ <p style="font-size: 0.85rem; font-weight: bold; margin-bottom: 5px;">فایل صوتی یا ویدیویی خود را بکشید یا انتخاب کنید</p>
555
+ <p style="font-size: 0.75rem; color: var(--text-sub);">پشتیبانی از MP3, WAV, M4A, MP4</p>
556
+ </div>
557
+ <input type="file" id="fileIn" accept="audio/*,video/*" style="display: none;">
558
+ </div>
559
+
560
+ <div style="text-align: center; color: var(--text-sub); font-size: 0.8rem; font-weight: bold;">یا</div>
561
+
562
+ <div class="mic-section">
563
+ <p style="font-size: 0.8rem; font-weight: bold; color: var(--text-sub);">ضبط زنده با میکروفون مرورگر</p>
564
+ <button class="mic-btn" id="micBtn" type="button"><i class="fa-solid fa-microphone"></i></button>
565
+ <div class="wave-container" id="waveContainer">
566
+ <div class="wave-bar"></div>
567
+ <div class="wave-bar"></div>
568
+ <div class="wave-bar"></div>
569
+ <div class="wave-bar"></div>
570
+ <div class="wave-bar"></div>
571
+ </div>
572
+ <span id="micTimer" style="font-size: 0.8rem; color: var(--text-sub); display: none; font-family: monospace;">00:00</span>
573
+ </div>
574
+
575
+ <div class="audio-player-container" id="playerContainer">
576
+ <audio id="audioPlayback" controls></audio>
577
+ </div>
578
+ </div>
579
+ </div>
580
+
581
+ <div class="card">
582
+ <div class="card-title"><i class="fa-solid fa-sliders"></i> تنظیمات پردازش</div>
583
+
584
+ <div class="form-group">
585
+ <label for="langSelection">زبان گفتار:</label>
586
+ <select id="langSelection" class="form-control">
587
+ <option value="Auto-Detect">تشخیص خودکار هوشمند</option>
588
+ <option value="Persian" selected>Persian / فارسی</option>
589
+ <option value="English">English / انگلیسی</option>
590
+ <option value="Arabic">Arabic / عربی</option>
591
+ <option value="Turkish">Turkish / ترکی</option>
592
+ <option value="French">French / فرانسوی</option>
593
+ <option value="German">German / آلمانی</option>
594
+ </select>
595
+ </div>
596
+
597
+ <div class="form-group" style="margin-bottom: 25px;">
598
+ <label class="checkbox-container" id="lblOptimize">
599
+ <input type="checkbox" id="chkOptimize" checked>
600
+ <div class="checkbox-box"><i class="fa-solid fa-check"></i></div>
601
+ <span style="font-size: 0.85rem; color: var(--text-sub); font-weight: bold;">بهینه‌سازی کلمات عامیانه و محاوره‌ای فارسی</span>
602
+ </label>
603
+ </div>
604
+
605
+ <button class="btn-submit" id="btnSubmit" disabled>
606
+ <i class="fa-solid fa-wand-magic-sparkles"></i>
607
+ <span>شروع فرآیند تبدیل به متن</span>
608
+ </button>
609
+ </div>
610
+ </div>
611
+
612
+ <div class="output-panel">
613
+ <div class="error-logger" id="errorLogger">
614
+ <i class="fa-solid fa-circle-exclamation"></i>
615
+ <span id="errorText">خطایی رخ داد.</span>
616
+ </div>
617
+
618
+ <div class="card" style="flex: 1; display: flex; flex-direction: column; gap: 15px; position: relative;">
619
+ <div class="tabs-header">
620
+ <button class="tab-btn active" onclick="switchTab('text', this)"><i class="fa-solid fa-align-right"></i> متن کامل</button>
621
+ <button class="tab-btn" onclick="switchTab('srt', this)"><i class="fa-regular fa-closed-captioning"></i> زیرنویس (SRT)</button>
622
+ <button class="tab-btn" onclick="switchTab('json', this)"><i class="fa-solid fa-code"></i> زمان‌بندی خام کلمات</button>
623
+ </div>
624
+
625
+ <div class="processing-overlay" id="loader">
626
+ <div class="spinner"></div>
627
+ <div class="loading-text" id="loaderText">در حال آپلود فایل صوتی به سرور...</div>
628
+ <p style="font-size: 0.8rem; color: var(--text-sub);">لطفاً تا پایان پردازش صبور باشید.</p>
629
+ </div>
630
 
631
+ <div id="content-text" class="tab-content active">
632
+ <div class="action-overlay">
633
+ <button class="action-btn" onclick="copyText('viewer-text')" title="کپی متن کامل"><i class="fa-regular fa-copy"></i></button>
634
+ </div>
635
+ <div class="text-viewer" id="viewer-text" placeholder="متن تبدیل شده در اینجا نمایش داده می‌شود...">جهت شروع پردازش، ابتدا یک فایل صوتی یا ویدیویی انتخاب کنید و یا دکمه ضبط میکروفون را فشار دهید.</div>
636
+ </div>
637
 
638
+ <div id="content-srt" class="tab-content">
639
+ <div class="action-overlay">
640
+ <button class="action-btn" onclick="downloadSRT()" title="دانلود فایل SRT"><i class="fa-solid fa-download"></i></button>
641
+ <button class="action-btn" onclick="copyText('viewer-srt')" title="کپی کردن زیرنویس"><i class="fa-regular fa-copy"></i></button>
642
+ </div>
643
+ <div class="text-viewer" id="viewer-srt" style="font-family: monospace;" placeholder="فایل زیرنویس استاندارد در این قسمت ساخته می‌شود..."></div>
644
+ </div>
645
+
646
+ <div id="content-json" class="tab-content">
647
+ <div class="action-overlay">
648
+ <button class="action-btn" onclick="copyText('viewer-json')" title="کپی کدهای JSON"><i class="fa-regular fa-copy"></i></button>
649
+ </div>
650
+ <div class="text-viewer" id="viewer-json" style="font-family: monospace;" placeholder="داده‌های زمانی کلمات به صورت JSON..."></div>
651
+ </div>
652
+ </div>
653
  </div>
654
  </div>
655
+
656
+ <footer>
657
+ <p>یکپارچه‌سازی شده با شبکه‌ی توزیع بار اکشن‌های گیت‌هاب جهت دور زدن قطعی و محدودیت‌ها</p>
658
+ </footer>
659
  </div>
660
 
661
  <script>
662
+ let activeFile = null;
663
+ let mediaRecorder = null;
664
+ let audioChunks = [];
665
+ let isRecording = false;
666
+ let recordTimer = null;
667
+ let secondsRecorded = 0;
668
 
669
+ document.getElementById('fileIn').addEventListener('change', function(e) {
670
+ if (e.target.files[0]) handleSelectedFile(e.target.files[0]);
671
+ });
672
 
673
+ const dropzone = document.getElementById('dropzone');
674
+ dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); });
675
+ dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
676
+ dropzone.addEventListener('drop', (e) => {
677
+ e.preventDefault();
678
+ dropzone.classList.remove('dragover');
679
+ if (e.dataTransfer.files[0]) handleSelectedFile(e.dataTransfer.files[0]);
680
+ });
681
 
682
+ function handleSelectedFile(file) {
683
+ activeFile = file;
684
+ const playerContainer = document.getElementById('playerContainer');
685
+ const playback = document.getElementById('audioPlayback');
686
 
687
+ if (file.type.startsWith('audio/')) {
688
+ playback.src = URL.createObjectURL(file);
689
+ playerContainer.style.display = 'flex';
690
+ } else {
691
+ playerContainer.style.display = 'none';
692
+ }
693
+
694
+ document.getElementById('viewer-text').textContent = `فایل انتخاب شده: ${file.name}\nآماده برای شروع فرآیند پردازش با هوش مصنوعی.`;
695
+ document.getElementById('btnSubmit').disabled = false;
696
+ }
697
+
698
+ const micBtn = document.getElementById('micBtn');
699
+ micBtn.addEventListener('click', async () => {
700
+ if (!isRecording) {
701
+ try {
702
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
703
+ mediaRecorder = new MediaRecorder(stream);
704
+ audioChunks = [];
705
+ mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);
706
+ mediaRecorder.onstop = () => {
707
+ const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' });
708
+ const file = new File([audioBlob], `recorded_voice_${Date.now()}.mp3`, { type: 'audio/mp3' });
709
+ handleSelectedFile(file);
710
+ };
711
+ mediaRecorder.start();
712
+ isRecording = true;
713
+ micBtn.classList.add('recording');
714
+ document.getElementById('waveContainer').classList.add('active');
715
+
716
+ secondsRecorded = 0;
717
+ const timerEl = document.getElementById('micTimer');
718
+ timerEl.style.display = 'block';
719
+ recordTimer = setInterval(() => {
720
+ secondsRecorded++;
721
+ const m = String(Math.floor(secondsRecorded / 60)).padStart(2, '0');
722
+ const s = String(secondsRecorded % 60).padStart(2, '0');
723
+ timerEl.textContent = `${m}:${s}`;
724
+ }, 1000);
725
+ } catch (err) {
726
+ showError("مجوز دسترسی به میکروفون صادر نشد یا میکروفون متصل نیست.");
727
+ }
728
+ } else {
729
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
730
+ mediaRecorder.stop();
731
+ mediaRecorder.stream.getTracks().forEach(track => track.stop());
732
+ }
733
+ isRecording = false;
734
+ micBtn.classList.remove('recording');
735
+ document.getElementById('waveContainer').classList.remove('active');
736
+ clearInterval(recordTimer);
737
+ document.getElementById('micTimer').style.display = 'none';
738
+ }
739
+ });
740
+
741
+ document.getElementById('btnSubmit').addEventListener('click', async () => {
742
+ if (!activeFile) return;
743
+
744
+ showLoader(true, "۱. در حال آپلود امن فایل به سرور موقت داکر...");
745
+ hideError();
746
 
747
  try {
748
+ // ایجاد یک کد پیگیری انحصاری
749
+ const runId = 'wx_' + Math.random().toString(36).substr(2, 9);
750
+
751
+ // مرحله اول: آپلود فایل صوتی به سرور با مکانیزم webhook
752
+ const formData = new FormData();
753
+ formData.append('run_id', runId);
754
+ formData.append('ext', 'audio');
755
+ formData.append('file', activeFile);
756
+
757
+ const uploadRes = await fetch('/api/webhook/upload', {
758
+ method: 'POST',
759
+ body: formData
760
+ });
761
+
762
+ if (!uploadRes.ok) throw new Error("آپلود فایل صوتی با خطا مواجه شد.");
763
+
764
+ showLoader(true, "۲. در حال آماده‌سازی و ارسال سیگنال پردازش به گیت‌هاب اکشن...");
765
+
766
+ const lang = document.getElementById('langSelection').value;
767
+ const optimize = document.getElementById('chkOptimize').checked;
768
+
769
+ // جا زدن (Masquerade): اطلاعات را در قالب فیلد Prompt به سرور می‌فرستیم
770
+ // ترکیب رشته برای جلوگیری از آسیب ترجمه
771
+ const safePayload = `${runId}___${lang}___${optimize}`;
772
+
773
+ // فراخوانی API ژنراتور اصلی و هدایت به فایل matnbesada
774
+ const genRes = await fetch('/api/generate', {
775
+ method: 'POST',
776
+ headers: { 'Content-Type': 'application/json' },
777
+ body: JSON.stringify({
778
+ action_name: "matnbesada",
779
+ prompt: safePayload,
780
+ width: 1024,
781
+ height: 1024
782
+ })
783
+ });
784
+
785
+ const genData = await genRes.json();
786
+ if (genData.status !== 'success') {
787
+ throw new Error(genData.message || "خطا در برقراری ارتباط با توزیع‌کننده بار گیت‌هاب.");
788
  }
 
 
789
 
790
+ showLoader(true, "۳. دستور در صف قرار گرفت! در حال پردازش صوت در موتور WhisperX...");
791
+
792
+ // مرحله سوم: پولینگ (Polling) برای دریافت نتایج
793
+ const pollTimer = setInterval(async () => {
794
+ try {
795
+ const statusRes = await fetch(`/api/status/${runId}`);
796
+ const statusData = await statusRes.json();
797
+
798
+ if (statusData.status === 'ready') {
799
+ clearInterval(pollTimer);
800
+ showLoader(false);
801
+
802
+ // واکشی متن کامل
803
+ const txtRes = await fetch(`/static/images/${runId}.txt`);
804
+ if (txtRes.ok) {
805
+ const full_text = await txtRes.text();
806
+ document.getElementById('viewer-text').textContent = full_text;
807
+ }
808
+
809
+ // واکشی فایل SRT
810
+ try {
811
+ const srtRes = await fetch(`/static/images/${runId}_srt.srt`);
812
+ if (srtRes.ok) {
813
+ const srt_content = await srtRes.text();
814
+ document.getElementById('viewer-srt').textContent = srt_content;
815
+ }
816
+ } catch(e) {}
817
+
818
+ // واکشی زمان‌بندی جیسون
819
+ try {
820
+ const jsonRes = await fetch(`/static/images/${runId}_json.json`);
821
+ if (jsonRes.ok) {
822
+ const json_content = await jsonRes.text();
823
+ document.getElementById('viewer-json').textContent = json_content;
824
+ }
825
+ } catch(e) {}
826
+
827
+ // پرش به تب اول در پایان
828
+ switchTab('text', document.querySelector('.tab-btn'));
829
+ }
830
+ } catch (e) {
831
+ // در صورت خطای شبکه در پولینگ، ادامه می‌دهیم
832
  }
833
+ }, 4500);
834
+
835
+ } catch (err) {
836
+ showLoader(false);
837
+ showError(`توقف عملیات: ${err.message}`);
838
+ }
839
+ });
840
+
841
+ function showLoader(show, text = "") {
842
+ const loader = document.getElementById('loader');
843
+ if (show) {
844
+ document.getElementById('loaderText').textContent = text;
845
+ loader.style.display = 'flex';
846
+ } else {
847
+ loader.style.display = 'none';
848
+ }
849
+ }
850
+
851
+ function showError(text) {
852
+ document.getElementById('errorText').textContent = text;
853
+ document.getElementById('errorLogger').style.display = 'block';
854
+ }
855
+
856
+ function hideError() {
857
+ document.getElementById('errorLogger').style.display = 'none';
858
+ }
859
+
860
+ function switchTab(tabName, btn) {
861
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
862
+ document.querySelectorAll('.tab-btn').forEach(button => button.classList.remove('active'));
863
+ document.getElementById(`content-${tabName}`).classList.add('active');
864
+ btn.classList.add('active');
865
+ }
866
+
867
+ function copyText(elementId) {
868
+ const text = document.getElementById(elementId).textContent;
869
+ navigator.clipboard.writeText(text).then(() => {
870
+ alert("محتوای متنی با موفقیت در کلیپ‌بورد کپی شد.");
871
+ }).catch(err => {
872
+ alert("خطا در کپی متن: " + err);
873
+ });
874
+ }
875
+
876
+ function downloadSRT() {
877
+ const srtContent = document.getElementById('viewer-srt').textContent;
878
+ if (!srtContent || srtContent.trim() === "" || srtContent.startsWith("فایل زیرنویس")) {
879
+ alert("ابتدا باید یک فایل صوتی را پردازش کنید تا زیرنویس ساخته شود.");
880
+ return;
881
+ }
882
+
883
+ const blob = new Blob([srtContent], { type: 'text/srt;charset=utf-8;' });
884
+ const link = document.createElement("a");
885
+ link.href = URL.createObjectURL(blob);
886
+ link.setAttribute("download", `subtitle_${Date.now()}.srt`);
887
+ document.body.appendChild(link);
888
+ link.click();
889
+ document.body.removeChild(link);
890
  }
891
  </script>
892
+
893
  </body>
894
  </html>