Athspi commited on
Commit
94df3fc
·
verified ·
1 Parent(s): 7cc4829

Create static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +1012 -0
static/index.html ADDED
@@ -0,0 +1,1012 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI Audio Translator</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6366f1;
10
+ --primary-light: #818cf8;
11
+ --primary-dark: #4f46e5;
12
+ --secondary-color: #f472b6;
13
+ --accent-color: #34d399;
14
+ --dark-color: #1f2937;
15
+ --light-color: #f9fafb;
16
+ --gray-color: #9ca3af;
17
+ --danger-color: #ef4444;
18
+ --success-color: #10b981;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ body {
29
+ background-color: var(--light-color);
30
+ color: var(--dark-color);
31
+ min-height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ padding: 2rem;
37
+ overflow-x: hidden;
38
+ }
39
+
40
+ .container {
41
+ width: 100%;
42
+ max-width: 1000px;
43
+ background-color: white;
44
+ border-radius: 16px;
45
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
46
+ overflow: hidden;
47
+ position: relative;
48
+ }
49
+
50
+ header {
51
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
52
+ color: white;
53
+ padding: 2rem;
54
+ text-align: center;
55
+ position: relative;
56
+ overflow: hidden;
57
+ }
58
+
59
+ header h1 {
60
+ font-size: 2.5rem;
61
+ margin-bottom: 0.5rem;
62
+ font-weight: 700;
63
+ animation: fadeInDown 0.8s ease-out;
64
+ }
65
+
66
+ header p {
67
+ font-size: 1.1rem;
68
+ opacity: 0.9;
69
+ max-width: 600px;
70
+ margin: 0 auto;
71
+ animation: fadeInUp 0.8s ease-out 0.2s both;
72
+ }
73
+
74
+ .bubbles {
75
+ position: absolute;
76
+ width: 100%;
77
+ height: 100%;
78
+ top: 0;
79
+ left: 0;
80
+ pointer-events: none;
81
+ z-index: 0;
82
+ }
83
+
84
+ .bubble {
85
+ position: absolute;
86
+ border-radius: 50%;
87
+ background: rgba(255, 255, 255, 0.1);
88
+ animation: bubble 15s linear infinite;
89
+ }
90
+
91
+ @keyframes bubble {
92
+ 0% {
93
+ transform: translateY(0) rotate(0deg);
94
+ opacity: 1;
95
+ border-radius: 0;
96
+ }
97
+ 100% {
98
+ transform: translateY(-1000px) rotate(720deg);
99
+ opacity: 0;
100
+ border-radius: 50%;
101
+ }
102
+ }
103
+
104
+ .main-content {
105
+ padding: 2rem;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 2rem;
109
+ position: relative;
110
+ z-index: 1;
111
+ }
112
+
113
+ .card {
114
+ background-color: white;
115
+ border-radius: 12px;
116
+ padding: 1.5rem;
117
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
118
+ transition: all 0.3s ease;
119
+ }
120
+
121
+ .card:hover {
122
+ transform: translateY(-5px);
123
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
124
+ }
125
+
126
+ .card h2 {
127
+ color: var(--primary-dark);
128
+ margin-bottom: 1rem;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.5rem;
132
+ }
133
+
134
+ .audio-input {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 1rem;
138
+ }
139
+
140
+ .dropzone {
141
+ border: 2px dashed var(--gray-color);
142
+ border-radius: 8px;
143
+ padding: 2rem;
144
+ text-align: center;
145
+ cursor: pointer;
146
+ transition: all 0.3s ease;
147
+ background-color: rgba(99, 102, 241, 0.05);
148
+ }
149
+
150
+ .dropzone:hover, .dropzone.dragover {
151
+ border-color: var(--primary-color);
152
+ background-color: rgba(99, 102, 241, 0.1);
153
+ }
154
+
155
+ .dropzone p {
156
+ color: var(--gray-color);
157
+ margin: 0.5rem 0;
158
+ }
159
+
160
+ .dropzone .icon {
161
+ font-size: 2.5rem;
162
+ color: var(--primary-light);
163
+ margin-bottom: 0.5rem;
164
+ }
165
+
166
+ .custom-file-upload {
167
+ display: inline-block;
168
+ font-weight: 500;
169
+ cursor: pointer;
170
+ background-color: var(--primary-color);
171
+ color: white;
172
+ padding: 0.75rem 1.5rem;
173
+ border-radius: 6px;
174
+ transition: all 0.2s ease;
175
+ border: none;
176
+ font-size: 1rem;
177
+ }
178
+
179
+ .custom-file-upload:hover {
180
+ background-color: var(--primary-dark);
181
+ transform: translateY(-2px);
182
+ }
183
+
184
+ #audioFileInput {
185
+ display: none;
186
+ }
187
+
188
+ .record-option {
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ margin-top: 1rem;
193
+ gap: 1rem;
194
+ }
195
+
196
+ .record-btn {
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ gap: 0.5rem;
201
+ background-color: white;
202
+ border: 1px solid var(--primary-color);
203
+ color: var(--primary-color);
204
+ padding: 0.75rem 1.5rem;
205
+ border-radius: 6px;
206
+ cursor: pointer;
207
+ transition: all 0.2s ease;
208
+ font-weight: 500;
209
+ font-size: 1rem;
210
+ }
211
+
212
+ .record-btn:hover {
213
+ background-color: rgba(99, 102, 241, 0.1);
214
+ }
215
+
216
+ .record-btn.recording {
217
+ background-color: var(--danger-color);
218
+ color: white;
219
+ border-color: var(--danger-color);
220
+ animation: pulse 1.5s infinite;
221
+ }
222
+
223
+ @keyframes pulse {
224
+ 0% {
225
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
226
+ }
227
+ 70% {
228
+ box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
229
+ }
230
+ 100% {
231
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
232
+ }
233
+ }
234
+
235
+ .language-selection {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 1rem;
239
+ }
240
+
241
+ .language-dropdown {
242
+ position: relative;
243
+ }
244
+
245
+ .language-dropdown select {
246
+ width: 100%;
247
+ padding: 0.75rem 1rem;
248
+ border-radius: 6px;
249
+ border: 1px solid var(--gray-color);
250
+ background-color: white;
251
+ font-size: 1rem;
252
+ cursor: pointer;
253
+ appearance: none;
254
+ outline: none;
255
+ transition: all 0.2s ease;
256
+ }
257
+
258
+ .language-dropdown select:focus {
259
+ border-color: var(--primary-color);
260
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
261
+ }
262
+
263
+ .language-dropdown::after {
264
+ content: '▾';
265
+ position: absolute;
266
+ right: 1rem;
267
+ top: 50%;
268
+ transform: translateY(-50%);
269
+ color: var(--gray-color);
270
+ pointer-events: none;
271
+ }
272
+
273
+ .results-container {
274
+ display: grid;
275
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
276
+ gap: 1.5rem;
277
+ }
278
+
279
+ .result-card {
280
+ background-color: white;
281
+ border-radius: 12px;
282
+ padding: 1.5rem;
283
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
284
+ transition: all 0.3s ease;
285
+ height: 100%;
286
+ display: flex;
287
+ flex-direction: column;
288
+ }
289
+
290
+ .result-card h3 {
291
+ color: var(--primary-dark);
292
+ margin-bottom: 1rem;
293
+ padding-bottom: 0.5rem;
294
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
295
+ }
296
+
297
+ .result-content {
298
+ flex: 1;
299
+ overflow-y: auto;
300
+ max-height: 200px;
301
+ padding-right: 0.5rem;
302
+ margin-bottom: 1rem;
303
+ line-height: 1.6;
304
+ }
305
+
306
+ .result-content::-webkit-scrollbar {
307
+ width: 6px;
308
+ }
309
+
310
+ .result-content::-webkit-scrollbar-track {
311
+ background: rgba(0, 0, 0, 0.05);
312
+ border-radius: 10px;
313
+ }
314
+
315
+ .result-content::-webkit-scrollbar-thumb {
316
+ background: var(--primary-light);
317
+ border-radius: 10px;
318
+ }
319
+
320
+ .audio-player {
321
+ width: 100%;
322
+ height: 40px;
323
+ outline: none;
324
+ }
325
+
326
+ .translate-btn {
327
+ display: block;
328
+ width: 100%;
329
+ padding: 1rem;
330
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
331
+ color: white;
332
+ border: none;
333
+ border-radius: 8px;
334
+ font-size: 1.1rem;
335
+ font-weight: 600;
336
+ cursor: pointer;
337
+ transition: all 0.3s ease;
338
+ margin-top: 1rem;
339
+ position: relative;
340
+ overflow: hidden;
341
+ }
342
+
343
+ .translate-btn:hover {
344
+ transform: translateY(-2px);
345
+ box-shadow: 0 6px 15px rgba(79, 70, 229, 0.3);
346
+ }
347
+
348
+ .translate-btn:active {
349
+ transform: translateY(0);
350
+ }
351
+
352
+ .translate-btn .btn-content {
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ gap: 0.5rem;
357
+ position: relative;
358
+ z-index: 2;
359
+ }
360
+
361
+ .translate-btn::before {
362
+ content: '';
363
+ position: absolute;
364
+ top: 0;
365
+ left: -100%;
366
+ width: 100%;
367
+ height: 100%;
368
+ background: linear-gradient(135deg, var(--primary-dark), var(--secondary-color));
369
+ transition: all 0.5s ease;
370
+ z-index: 1;
371
+ }
372
+
373
+ .translate-btn:hover::before {
374
+ left: 0;
375
+ }
376
+
377
+ .loading {
378
+ display: none;
379
+ align-items: center;
380
+ justify-content: center;
381
+ flex-direction: column;
382
+ gap: 1rem;
383
+ padding: 2rem;
384
+ text-align: center;
385
+ }
386
+
387
+ .spinner {
388
+ width: 60px;
389
+ height: 60px;
390
+ border: 5px solid rgba(99, 102, 241, 0.2);
391
+ border-radius: 50%;
392
+ border-top-color: var(--primary-color);
393
+ animation: spin 1s linear infinite;
394
+ }
395
+
396
+ @keyframes spin {
397
+ to {
398
+ transform: rotate(360deg);
399
+ }
400
+ }
401
+
402
+ .hidden {
403
+ display: none;
404
+ }
405
+
406
+ .output-container {
407
+ display: none;
408
+ animation: fadeIn 0.5s ease-out;
409
+ }
410
+
411
+ .audio-visualization {
412
+ height: 60px;
413
+ background-color: rgba(99, 102, 241, 0.1);
414
+ border-radius: 8px;
415
+ overflow: hidden;
416
+ margin: 1rem 0;
417
+ position: relative;
418
+ }
419
+
420
+ .visualizer-bars {
421
+ display: flex;
422
+ align-items: flex-end;
423
+ height: 100%;
424
+ padding: 0 5px;
425
+ gap: 2px;
426
+ }
427
+
428
+ .visualizer-bar {
429
+ flex: 1;
430
+ background: linear-gradient(to top, var(--primary-color), var(--primary-light));
431
+ max-width: 4px;
432
+ border-radius: 2px;
433
+ height: 0%;
434
+ transition: height 0.05s ease;
435
+ }
436
+
437
+ .controls {
438
+ display: flex;
439
+ gap: 0.5rem;
440
+ margin-top: 1rem;
441
+ }
442
+
443
+ .control-btn {
444
+ flex: 1;
445
+ padding: 0.75rem;
446
+ border: none;
447
+ border-radius: 6px;
448
+ font-weight: 500;
449
+ cursor: pointer;
450
+ transition: all 0.2s ease;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ gap: 0.5rem;
455
+ }
456
+
457
+ .download-btn {
458
+ background-color: var(--success-color);
459
+ color: white;
460
+ }
461
+
462
+ .download-btn:hover {
463
+ background-color: #0b9e6e;
464
+ }
465
+
466
+ .copy-btn {
467
+ background-color: var(--accent-color);
468
+ color: white;
469
+ }
470
+
471
+ .copy-btn:hover {
472
+ background-color: #2bbb89;
473
+ }
474
+
475
+ .toast {
476
+ position: fixed;
477
+ bottom: 2rem;
478
+ left: 50%;
479
+ transform: translateX(-50%) translateY(100px);
480
+ background-color: var(--dark-color);
481
+ color: white;
482
+ padding: 0.75rem 1.5rem;
483
+ border-radius: 6px;
484
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
485
+ z-index: 1000;
486
+ opacity: 0;
487
+ transition: all 0.3s ease;
488
+ }
489
+
490
+ .toast.show {
491
+ transform: translateX(-50%) translateY(0);
492
+ opacity: 1;
493
+ }
494
+
495
+ @keyframes fadeIn {
496
+ from {
497
+ opacity: 0;
498
+ transform: translateY(20px);
499
+ }
500
+ to {
501
+ opacity: 1;
502
+ transform: translateY(0);
503
+ }
504
+ }
505
+
506
+ @keyframes fadeInDown {
507
+ from {
508
+ opacity: 0;
509
+ transform: translateY(-20px);
510
+ }
511
+ to {
512
+ opacity: 1;
513
+ transform: translateY(0);
514
+ }
515
+ }
516
+
517
+ @keyframes fadeInUp {
518
+ from {
519
+ opacity: 0;
520
+ transform: translateY(20px);
521
+ }
522
+ to {
523
+ opacity: 1;
524
+ transform: translateY(0);
525
+ }
526
+ }
527
+
528
+ @media (max-width: 768px) {
529
+ .main-content {
530
+ padding: 1.5rem;
531
+ }
532
+
533
+ .results-container {
534
+ grid-template-columns: 1fr;
535
+ }
536
+
537
+ header h1 {
538
+ font-size: 2rem;
539
+ }
540
+
541
+ header p {
542
+ font-size: 1rem;
543
+ }
544
+ }
545
+ </style>
546
+ </head>
547
+ <body>
548
+ <div class="container">
549
+ <header>
550
+ <div class="bubbles">
551
+ <!-- Bubbles generated by JS -->
552
+ </div>
553
+ <h1>AI Audio Translator</h1>
554
+ <p>Upload an audio file or record via microphone, select a target language, and get the transcription, translation, and translated audio!</p>
555
+ </header>
556
+
557
+ <div class="main-content">
558
+ <div class="audio-input card">
559
+ <h2>
560
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
561
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path>
562
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
563
+ <line x1="12" y1="19" x2="12" y2="22"></line>
564
+ </svg>
565
+ Audio Input
566
+ </h2>
567
+ <div class="dropzone" id="dropzone">
568
+ <div class="icon">
569
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
570
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
571
+ <polyline points="17 8 12 3 7 8"></polyline>
572
+ <line x1="12" y1="3" x2="12" y2="15"></line>
573
+ </svg>
574
+ </div>
575
+ <p>Drag and drop your audio file here</p>
576
+ <p>or</p>
577
+ <label for="audioFileInput" class="custom-file-upload">
578
+ Choose File
579
+ </label>
580
+ <input type="file" id="audioFileInput" accept="audio/*">
581
+ <p id="fileInfo" class="hidden"></p>
582
+ </div>
583
+
584
+ <div class="record-option">
585
+ <button id="recordBtn" class="record-btn">
586
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
587
+ <circle cx="12" cy="12" r="10"></circle>
588
+ <circle cx="12" cy="12" r="3"></circle>
589
+ </svg>
590
+ Start Recording
591
+ </button>
592
+ </div>
593
+
594
+ <div id="audioVisualization" class="audio-visualization hidden">
595
+ <div class="visualizer-bars" id="visualizerBars">
596
+ <!-- Bars will be generated dynamically -->
597
+ </div>
598
+ </div>
599
+ </div>
600
+
601
+ <div class="language-selection card">
602
+ <h2>
603
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
604
+ <path d="M5 8l6 6"></path>
605
+ <path d="M4 14l6-6 2-3"></path>
606
+ <path d="M2 5h12"></path>
607
+ <path d="M7 2h1"></path>
608
+ <path d="M22 22l-5-10-5 10"></path>
609
+ <path d="M14 18h6"></path>
610
+ </svg>
611
+ Target Language
612
+ </h2>
613
+ <div class="language-dropdown">
614
+ <select id="languageSelect">
615
+ <option value="" disabled selected>Select language</option>
616
+ <!-- Languages will be populated dynamically -->
617
+ </select>
618
+ </div>
619
+
620
+ <button id="translateBtn" class="translate-btn" disabled>
621
+ <span class="btn-content">
622
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
623
+ <path d="M5 8l6 6"></path>
624
+ <path d="M4 14l6-6 2-3"></path>
625
+ <path d="M2 5h12"></path>
626
+ <path d="M7 2h1"></path>
627
+ <path d="M22 22l-5-10-5 10"></path>
628
+ <path d="M14 18h6"></path>
629
+ </svg>
630
+ Translate Audio
631
+ </span>
632
+ </button>
633
+ </div>
634
+
635
+ <div id="loadingSection" class="loading">
636
+ <div class="spinner"></div>
637
+ <p>Processing your audio... This may take a moment.</p>
638
+ </div>
639
+
640
+ <div id="outputContainer" class="output-container">
641
+ <div class="results-container">
642
+ <div class="result-card">
643
+ <h3>Original Transcription</h3>
644
+ <div id="originalText" class="result-content">
645
+ <!-- Transcription will appear here -->
646
+ </div>
647
+ <audio id="originalAudio" class="audio-player" controls></audio>
648
+ <div class="controls">
649
+ <button id="copyOriginal" class="control-btn copy-btn">
650
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
651
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
652
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
653
+ </svg>
654
+ Copy Text
655
+ </button>
656
+ </div>
657
+ </div>
658
+
659
+ <div class="result-card">
660
+ <h3>Translated Text</h3>
661
+ <div id="translatedText" class="result-content">
662
+ <!-- Translation will appear here -->
663
+ </div>
664
+ <audio id="translatedAudio" class="audio-player" controls></audio>
665
+ <div class="controls">
666
+ <button id="copyTranslated" class="control-btn copy-btn">
667
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
668
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
669
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
670
+ </svg>
671
+ Copy Text
672
+ </button>
673
+ <button id="downloadTranslated" class="control-btn download-btn">
674
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
675
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
676
+ <polyline points="7 10 12 15 17 10"></polyline>
677
+ <line x1="12" y1="15" x2="12" y2="3"></line>
678
+ </svg>
679
+ Download
680
+ </button>
681
+ </div>
682
+ </div>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+
688
+ <div id="toast" class="toast">Copied to clipboard!</div>
689
+
690
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.4/axios.min.js"></script>
691
+ <script>
692
+ // Create bubble animation in header
693
+ const createBubbles = () => {
694
+ const bubblesContainer = document.querySelector('.bubbles');
695
+ const bubbleCount = 10;
696
+
697
+ for (let i = 0; i < bubbleCount; i++) {
698
+ const bubble = document.createElement('div');
699
+ bubble.classList.add('bubble');
700
+
701
+ // Random sizes
702
+ const size = Math.random() * 60 + 20;
703
+ bubble.style.width = `${size}px`;
704
+ bubble.style.height = `${size}px`;
705
+
706
+ // Random positions
707
+ bubble.style.left = `${Math.random() * 100}%`;
708
+ bubble.style.top = `${Math.random() * 100}%`;
709
+
710
+ // Random animation delay and duration
711
+ const animationDuration = Math.random() * 10 + 5;
712
+ const animationDelay = Math.random() * 5;
713
+
714
+ bubble.style.animationDuration = `${animationDuration}s`;
715
+ bubble.style.animationDelay = `${animationDelay}s`;
716
+
717
+ bubblesContainer.appendChild(bubble);
718
+ }
719
+ };
720
+
721
+ // DOM Elements
722
+ const dropzone = document.getElementById('dropzone');
723
+ const fileInput = document.getElementById('audioFileInput');
724
+ const fileInfo = document.getElementById('fileInfo');
725
+ const recordBtn = document.getElementById('recordBtn');
726
+ const translateBtn = document.getElementById('translateBtn');
727
+ const languageSelect = document.getElementById('languageSelect');
728
+ const loadingSection = document.getElementById('loadingSection');
729
+ const outputContainer = document.getElementById('outputContainer');
730
+ const originalText = document.getElementById('originalText');
731
+ const translatedText = document.getElementById('translatedText');
732
+ const originalAudio = document.getElementById('originalAudio');
733
+ const translatedAudio = document.getElementById('translatedAudio');
734
+ const copyOriginal = document.getElementById('copyOriginal');
735
+ const copyTranslated = document.getElementById('copyTranslated');
736
+ const downloadTranslated = document.getElementById('downloadTranslated');
737
+ const toast = document.getElementById('toast');
738
+ const audioVisualization = document.getElementById('audioVisualization');
739
+ const visualizerBars = document.getElementById('visualizerBars');
740
+
741
+ // Variables
742
+ let audioFile = null;
743
+ let mediaRecorder = null;
744
+ let audioChunks = [];
745
+ let isRecording = false;
746
+ let audioContext = null;
747
+ let analyser = null;
748
+ let visualizationInterval = null;
749
+
750
+ // Create visualizer bars
751
+ const createVisualizerBars = () => {
752
+ visualizerBars.innerHTML = '';
753
+ const barCount = 50;
754
+
755
+ for (let i = 0; i < barCount; i++) {
756
+ const bar = document.createElement('div');
757
+ bar.classList.add('visualizer-bar');
758
+ visualizerBars.appendChild(bar);
759
+ }
760
+ };
761
+
762
+ // Initialize audio context
763
+ const initAudioContext = () => {
764
+ if (!audioContext) {
765
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
766
+ analyser = audioContext.createAnalyser();
767
+ analyser.fftSize = 256;
768
+ }
769
+ };
770
+
771
+ // Update visualization
772
+ const updateVisualization = (dataArray) => {
773
+ const bars = visualizerBars.querySelectorAll('.visualizer-bar');
774
+ const bufferLength = analyser.frequencyBinCount;
775
+
776
+ analyser.getByteFrequencyData(dataArray);
777
+
778
+ for (let i = 0; i < bars.length; i++) {
779
+ const index = Math.floor(i * (bufferLength / bars.length));
780
+ const value = dataArray[index] / 255;
781
+ const height = value * 100;
782
+ bars[i].style.height = `${height}%`;
783
+ }
784
+ };
785
+
786
+ // Start visualization
787
+ const startVisualization = (stream) => {
788
+ initAudioContext();
789
+
790
+ const source = audioContext.createMediaStreamSource(stream);
791
+ source.connect(analyser);
792
+
793
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
794
+
795
+ createVisualizerBars();
796
+ audioVisualization.classList.remove('hidden');
797
+
798
+ visualizationInterval = setInterval(() => {
799
+ updateVisualization(dataArray);
800
+ }, 50);
801
+ };
802
+
803
+ // Stop visualization
804
+ const stopVisualization = () => {
805
+ if (visualizationInterval) {
806
+ clearInterval(visualizationInterval);
807
+ visualizationInterval = null;
808
+ }
809
+
810
+ audioVisualization.classList.add('hidden');
811
+ };
812
+
813
+ // Initialize
814
+ const init = () => {
815
+ createBubbles();
816
+
817
+ // Drag and drop functionality
818
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
819
+ dropzone.addEventListener(eventName, preventDefaults, false);
820
+ });
821
+
822
+ function preventDefaults(e) {
823
+ e.preventDefault();
824
+ e.stopPropagation();
825
+ }
826
+
827
+ ['dragenter', 'dragover'].forEach(eventName => {
828
+ dropzone.addEventListener(eventName, highlight, false);
829
+ });
830
+
831
+ ['dragleave', 'drop'].forEach(eventName => {
832
+ dropzone.addEventListener(eventName, unhighlight, false);
833
+ });
834
+
835
+ function highlight() {
836
+ dropzone.classList.add('dragover');
837
+ }
838
+
839
+ function unhighlight() {
840
+ dropzone.classList.remove('dragover');
841
+ }
842
+
843
+ dropzone.addEventListener('drop', handleDrop, false);
844
+
845
+ function handleDrop(e) {
846
+ const dt = e.dataTransfer;
847
+ const files = dt.files;
848
+ if (files.length > 0) {
849
+ handleFile(files[0]);
850
+ }
851
+ }
852
+
853
+ // File input change handler
854
+ fileInput.addEventListener('change', (e) => {
855
+ if (e.target.files.length > 0) {
856
+ handleFile(e.target.files[0]);
857
+ }
858
+ });
859
+
860
+ // Handle file selection
861
+ const handleFile = (file) => {
862
+ if (file.type.startsWith('audio/')) {
863
+ audioFile = file;
864
+ fileInfo.textContent = `Selected file: ${file.name}`;
865
+ fileInfo.classList.remove('hidden');
866
+ translateBtn.disabled = false;
867
+ } else {
868
+ alert('Please upload a valid audio file.');
869
+ }
870
+ };
871
+
872
+ // Record button click handler
873
+ recordBtn.addEventListener('click', () => {
874
+ if (!isRecording) {
875
+ startRecording();
876
+ } else {
877
+ stopRecording();
878
+ }
879
+ });
880
+
881
+ // Start recording
882
+ const startRecording = async () => {
883
+ try {
884
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
885
+ mediaRecorder = new MediaRecorder(stream);
886
+ mediaRecorder.start();
887
+ isRecording = true;
888
+ recordBtn.classList.add('recording');
889
+ recordBtn.innerHTML = `
890
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
891
+ <circle cx="12" cy="12" r="10"></circle>
892
+ <rect x="6" y="6" width="12" height="12" rx="2"></rect>
893
+ </svg>
894
+ Stop Recording
895
+ `;
896
+ startVisualization(stream);
897
+
898
+ mediaRecorder.ondataavailable = (e) => {
899
+ audioChunks.push(e.data);
900
+ };
901
+
902
+ mediaRecorder.onstop = () => {
903
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
904
+ audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' });
905
+ fileInfo.textContent = `Recording saved: ${audioFile.name}`;
906
+ fileInfo.classList.remove('hidden');
907
+ translateBtn.disabled = false;
908
+ stopVisualization();
909
+ };
910
+ } catch (err) {
911
+ console.error('Error starting recording:', err);
912
+ alert('Error starting recording. Please ensure you have a microphone connected and permissions granted.');
913
+ }
914
+ };
915
+
916
+ // Stop recording
917
+ const stopRecording = () => {
918
+ mediaRecorder.stop();
919
+ isRecording = false;
920
+ recordBtn.classList.remove('recording');
921
+ recordBtn.innerHTML = `
922
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
923
+ <circle cx="12" cy="12" r="10"></circle>
924
+ <circle cx="12" cy="12" r="3"></circle>
925
+ </svg>
926
+ Start Recording
927
+ `;
928
+ audioChunks = [];
929
+ };
930
+
931
+ // Translate button click handler
932
+ translateBtn.addEventListener('click', async () => {
933
+ if (!audioFile || !languageSelect.value) {
934
+ alert('Please select an audio file and a target language.');
935
+ return;
936
+ }
937
+
938
+ loadingSection.style.display = 'flex';
939
+ outputContainer.style.display = 'none';
940
+
941
+ const formData = new FormData();
942
+ formData.append('audio', audioFile);
943
+ formData.append('language', languageSelect.value);
944
+
945
+ try {
946
+ const response = await axios.post('/translate', formData);
947
+
948
+ if (response.data.error) {
949
+ throw new Error(response.data.error);
950
+ }
951
+
952
+ const { transcription, translation, audio_url } = response.data;
953
+
954
+ originalText.textContent = transcription;
955
+ translatedText.textContent = translation;
956
+ originalAudio.src = URL.createObjectURL(audioFile);
957
+ translatedAudio.src = audio_url;
958
+
959
+ loadingSection.style.display = 'none';
960
+ outputContainer.style.display = 'block';
961
+ } catch (err) {
962
+ console.error('Error:', err);
963
+ showToast(err.message || 'An error occurred');
964
+ loadingSection.style.display = 'none';
965
+ }
966
+ });
967
+
968
+ // Copy original text to clipboard
969
+ copyOriginal.addEventListener('click', () => {
970
+ navigator.clipboard.writeText(originalText.textContent).then(() => {
971
+ showToast('Original text copied to clipboard!');
972
+ });
973
+ });
974
+
975
+ // Copy translated text to clipboard
976
+ copyTranslated.addEventListener('click', () => {
977
+ navigator.clipboard.writeText(translatedText.textContent).then(() => {
978
+ showToast('Translated text copied to clipboard!');
979
+ });
980
+ });
981
+
982
+ // Download translated audio
983
+ downloadTranslated.addEventListener('click', () => {
984
+ const link = document.createElement('a');
985
+ link.href = translatedAudio.src;
986
+ link.download = `translated_audio.${translatedAudio.src.split('.').pop()}`;
987
+ link.click();
988
+ });
989
+
990
+ // Show toast message
991
+ const showToast = (message) => {
992
+ toast.textContent = message;
993
+ toast.classList.add('show');
994
+ setTimeout(() => {
995
+ toast.classList.remove('show');
996
+ }, 3000);
997
+ };
998
+
999
+ // Load languages
1000
+ fetch('/languages')
1001
+ .then(response => response.json())
1002
+ .then(languages => {
1003
+ languageSelect.innerHTML = '<option value="" disabled selected>Select language</option>' +
1004
+ languages.map(lang => `<option value="${lang}">${lang}</option>`).join('');
1005
+ });
1006
+ };
1007
+
1008
+ // Initialize the app
1009
+ init();
1010
+ </script>
1011
+ </body>
1012
+ </html>