aavi21458 commited on
Commit
f42cd9d
·
verified ·
1 Parent(s): 84e4ffe

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1405 -19
index.html CHANGED
@@ -1,19 +1,1405 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Audio-Video Lip Sync Studio</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --primary: #6366f1;
12
+ --primary-dark: #4f46e5;
13
+ --primary-light: #818cf8;
14
+ --secondary: #ec4899;
15
+ --secondary-dark: #db2777;
16
+ --bg-dark: #0f0f1a;
17
+ --bg-card: #1a1a2e;
18
+ --bg-card-hover: #252542;
19
+ --text-primary: #ffffff;
20
+ --text-secondary: #a1a1aa;
21
+ --text-muted: #71717a;
22
+ --border: #27272a;
23
+ --success: #10b981;
24
+ --warning: #f59e0b;
25
+ --error: #ef4444;
26
+ --gradient-1: linear-gradient(135deg, #6366f1, #ec4899);
27
+ --gradient-2: linear-gradient(135deg, #8b5cf6, #06b6d4);
28
+ }
29
+
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ body {
37
+ font-family: 'Inter', sans-serif;
38
+ background: var(--bg-dark);
39
+ color: var(--text-primary);
40
+ min-height: 100vh;
41
+ overflow-x: hidden;
42
+ }
43
+
44
+ /* Animated Background */
45
+ .bg-animation {
46
+ position: fixed;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ pointer-events: none;
52
+ z-index: 0;
53
+ overflow: hidden;
54
+ }
55
+
56
+ .bg-animation::before {
57
+ content: '';
58
+ position: absolute;
59
+ top: -50%;
60
+ left: -50%;
61
+ width: 200%;
62
+ height: 200%;
63
+ background:
64
+ radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
65
+ radial-gradient(ellipse at 80% 80%, rgba(236, 72, 153, 0.1) 0%, transparent 50%);
66
+ animation: bgPulse 15s ease-in-out infinite;
67
+ }
68
+
69
+ @keyframes bgPulse {
70
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
71
+ 33% { transform: translate(2%, 2%) rotate(1deg); }
72
+ 66% { transform: translate(-1%, 1%) rotate(-1deg); }
73
+ }
74
+
75
+ /* Header */
76
+ header {
77
+ position: relative;
78
+ z-index: 10;
79
+ padding: 1.5rem 2rem;
80
+ background: rgba(15, 15, 26, 0.8);
81
+ backdrop-filter: blur(20px);
82
+ border-bottom: 1px solid var(--border);
83
+ display: flex;
84
+ justify-content: space-between;
85
+ align-items: center;
86
+ }
87
+
88
+ .logo {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 0.75rem;
92
+ }
93
+
94
+ .logo-icon {
95
+ width: 42px;
96
+ height: 42px;
97
+ background: var(--gradient-1);
98
+ border-radius: 12px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ font-size: 1.25rem;
103
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
104
+ }
105
+
106
+ .logo h1 {
107
+ font-size: 1.25rem;
108
+ font-weight: 700;
109
+ background: var(--gradient-1);
110
+ -webkit-background-clip: text;
111
+ -webkit-text-fill-color: transparent;
112
+ background-clip: text;
113
+ }
114
+
115
+ .brand-link {
116
+ color: var(--text-secondary);
117
+ text-decoration: none;
118
+ font-size: 0.75rem;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 0.5rem;
122
+ padding: 0.5rem 1rem;
123
+ background: var(--bg-card);
124
+ border-radius: 20px;
125
+ transition: all 0.3s ease;
126
+ }
127
+
128
+ .brand-link:hover {
129
+ background: var(--bg-card-hover);
130
+ color: var(--primary-light);
131
+ }
132
+
133
+ /* Main Content */
134
+ main {
135
+ position: relative;
136
+ z-index: 1;
137
+ padding: 2rem;
138
+ max-width: 1600px;
139
+ margin: 0 auto;
140
+ }
141
+
142
+ /* Upload Section */
143
+ .upload-section {
144
+ display: grid;
145
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
146
+ gap: 1.5rem;
147
+ margin-bottom: 2rem;
148
+ }
149
+
150
+ .upload-card {
151
+ background: var(--bg-card);
152
+ border-radius: 20px;
153
+ padding: 2rem;
154
+ border: 2px dashed var(--border);
155
+ transition: all 0.3s ease;
156
+ position: relative;
157
+ overflow: hidden;
158
+ }
159
+
160
+ .upload-card::before {
161
+ content: '';
162
+ position: absolute;
163
+ top: 0;
164
+ left: 0;
165
+ right: 0;
166
+ height: 4px;
167
+ background: var(--gradient-1);
168
+ opacity: 0;
169
+ transition: opacity 0.3s ease;
170
+ }
171
+
172
+ .upload-card:hover {
173
+ border-color: var(--primary);
174
+ transform: translateY(-4px);
175
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
176
+ }
177
+
178
+ .upload-card:hover::before {
179
+ opacity: 1;
180
+ }
181
+
182
+ .upload-card.video::before {
183
+ background: var(--gradient-2);
184
+ }
185
+
186
+ .upload-card.has-file {
187
+ border-style: solid;
188
+ border-color: var(--success);
189
+ }
190
+
191
+ .upload-icon {
192
+ width: 80px;
193
+ height: 80px;
194
+ background: rgba(99, 102, 241, 0.1);
195
+ border-radius: 50%;
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ margin: 0 auto 1.5rem;
200
+ font-size: 2rem;
201
+ color: var(--primary);
202
+ transition: all 0.3s ease;
203
+ }
204
+
205
+ .video .upload-icon {
206
+ background: rgba(139, 92, 246, 0.1);
207
+ color: #8b5cf6;
208
+ }
209
+
210
+ .upload-card:hover .upload-icon {
211
+ transform: scale(1.1) rotate(5deg);
212
+ }
213
+
214
+ .upload-title {
215
+ font-size: 1.25rem;
216
+ font-weight: 600;
217
+ margin-bottom: 0.5rem;
218
+ text-align: center;
219
+ }
220
+
221
+ .upload-subtitle {
222
+ color: var(--text-secondary);
223
+ font-size: 0.875rem;
224
+ text-align: center;
225
+ margin-bottom: 1.5rem;
226
+ }
227
+
228
+ .file-input {
229
+ display: none;
230
+ }
231
+
232
+ .upload-btn {
233
+ width: 100%;
234
+ padding: 1rem;
235
+ background: var(--gradient-1);
236
+ border: none;
237
+ border-radius: 12px;
238
+ color: white;
239
+ font-size: 1rem;
240
+ font-weight: 600;
241
+ cursor: pointer;
242
+ transition: all 0.3s ease;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ gap: 0.5rem;
247
+ }
248
+
249
+ .video .upload-btn {
250
+ background: var(--gradient-2);
251
+ }
252
+
253
+ .upload-btn:hover {
254
+ transform: translateY(-2px);
255
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
256
+ }
257
+
258
+ .file-info {
259
+ display: none;
260
+ margin-top: 1rem;
261
+ padding: 1rem;
262
+ background: rgba(16, 185, 129, 0.1);
263
+ border-radius: 12px;
264
+ text-align: center;
265
+ }
266
+
267
+ .file-info.visible {
268
+ display: block;
269
+ }
270
+
271
+ .file-info i {
272
+ color: var(--success);
273
+ margin-right: 0.5rem;
274
+ }
275
+
276
+ .file-name {
277
+ font-weight: 600;
278
+ color: var(--success);
279
+ word-break: break-all;
280
+ }
281
+
282
+ /* Sync Controls */
283
+ .sync-section {
284
+ background: var(--bg-card);
285
+ border-radius: 24px;
286
+ padding: 2rem;
287
+ margin-bottom: 2rem;
288
+ border: 1px solid var(--border);
289
+ }
290
+
291
+ .section-header {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 1rem;
295
+ margin-bottom: 2rem;
296
+ }
297
+
298
+ .section-icon {
299
+ width: 48px;
300
+ height: 48px;
301
+ background: var(--gradient-1);
302
+ border-radius: 14px;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ font-size: 1.25rem;
307
+ }
308
+
309
+ .section-title {
310
+ font-size: 1.5rem;
311
+ font-weight: 700;
312
+ }
313
+
314
+ .sync-controls {
315
+ display: grid;
316
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
317
+ gap: 1.5rem;
318
+ }
319
+
320
+ .control-group {
321
+ background: rgba(255, 255, 255, 0.03);
322
+ padding: 1.5rem;
323
+ border-radius: 16px;
324
+ border: 1px solid var(--border);
325
+ }
326
+
327
+ .control-label {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.75rem;
331
+ font-weight: 600;
332
+ margin-bottom: 1rem;
333
+ color: var(--text-secondary);
334
+ }
335
+
336
+ .control-label i {
337
+ color: var(--primary);
338
+ }
339
+
340
+ .slider-container {
341
+ position: relative;
342
+ }
343
+
344
+ .time-slider {
345
+ width: 100%;
346
+ height: 8px;
347
+ -webkit-appearance: none;
348
+ background: var(--border);
349
+ border-radius: 4px;
350
+ outline: none;
351
+ cursor: pointer;
352
+ }
353
+
354
+ .time-slider::-webkit-slider-thumb {
355
+ -webkit-appearance: none;
356
+ width: 24px;
357
+ height: 24px;
358
+ background: var(--gradient-1);
359
+ border-radius: 50%;
360
+ cursor: pointer;
361
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.5);
362
+ transition: transform 0.2s ease;
363
+ }
364
+
365
+ .time-slider::-webkit-slider-thumb:hover {
366
+ transform: scale(1.2);
367
+ }
368
+
369
+ .time-value {
370
+ text-align: center;
371
+ font-size: 1.5rem;
372
+ font-weight: 700;
373
+ color: var(--primary);
374
+ margin-top: 1rem;
375
+ font-variant-numeric: tabular-nums;
376
+ }
377
+
378
+ .btn-group {
379
+ display: flex;
380
+ gap: 0.75rem;
381
+ margin-top: 1rem;
382
+ }
383
+
384
+ .btn {
385
+ flex: 1;
386
+ padding: 0.875rem 1.5rem;
387
+ border: none;
388
+ border-radius: 12px;
389
+ font-size: 0.875rem;
390
+ font-weight: 600;
391
+ cursor: pointer;
392
+ transition: all 0.3s ease;
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ gap: 0.5rem;
397
+ }
398
+
399
+ .btn-primary {
400
+ background: var(--primary);
401
+ color: white;
402
+ }
403
+
404
+ .btn-primary:hover {
405
+ background: var(--primary-dark);
406
+ transform: translateY(-2px);
407
+ }
408
+
409
+ .btn-secondary {
410
+ background: var(--bg-card-hover);
411
+ color: var(--text-primary);
412
+ border: 1px solid var(--border);
413
+ }
414
+
415
+ .btn-secondary:hover {
416
+ background: var(--border);
417
+ }
418
+
419
+ /* Preview Section */
420
+ .preview-section {
421
+ display: grid;
422
+ grid-template-columns: 2fr 1fr;
423
+ gap: 2rem;
424
+ margin-bottom: 2rem;
425
+ }
426
+
427
+ @media (max-width: 1024px) {
428
+ .preview-section {
429
+ grid-template-columns: 1fr;
430
+ }
431
+ }
432
+
433
+ .video-container {
434
+ background: var(--bg-card);
435
+ border-radius: 24px;
436
+ padding: 1.5rem;
437
+ border: 1px solid var(--border);
438
+ }
439
+
440
+ .video-wrapper {
441
+ position: relative;
442
+ width: 100%;
443
+ background: #000;
444
+ border-radius: 16px;
445
+ overflow: hidden;
446
+ aspect-ratio: 16/9;
447
+ }
448
+
449
+ .video-wrapper video {
450
+ width: 100%;
451
+ height: 100%;
452
+ object-fit: contain;
453
+ }
454
+
455
+ .video-placeholder {
456
+ position: absolute;
457
+ top: 0;
458
+ left: 0;
459
+ width: 100%;
460
+ height: 100%;
461
+ display: flex;
462
+ flex-direction: column;
463
+ align-items: center;
464
+ justify-content: center;
465
+ background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
466
+ color: var(--text-muted);
467
+ }
468
+
469
+ .video-placeholder i {
470
+ font-size: 4rem;
471
+ margin-bottom: 1rem;
472
+ opacity: 0.5;
473
+ }
474
+
475
+ /* Waveform */
476
+ .waveform-container {
477
+ background: var(--bg-card);
478
+ border-radius: 24px;
479
+ padding: 1.5rem;
480
+ border: 1px solid var(--border);
481
+ }
482
+
483
+ .waveform-header {
484
+ display: flex;
485
+ justify-content: space-between;
486
+ align-items: center;
487
+ margin-bottom: 1rem;
488
+ }
489
+
490
+ .waveform-title {
491
+ font-weight: 600;
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 0.5rem;
495
+ }
496
+
497
+ .waveform-title i {
498
+ color: var(--primary);
499
+ }
500
+
501
+ #waveform-canvas {
502
+ width: 100%;
503
+ height: 150px;
504
+ background: rgba(0, 0, 0, 0.3);
505
+ border-radius: 12px;
506
+ }
507
+
508
+ /* Playback Controls */
509
+ .playback-controls {
510
+ background: var(--bg-card);
511
+ border-radius: 24px;
512
+ padding: 2rem;
513
+ border: 1px solid var(--border);
514
+ margin-bottom: 2rem;
515
+ }
516
+
517
+ .main-controls {
518
+ display: flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ gap: 1rem;
522
+ margin-bottom: 1.5rem;
523
+ }
524
+
525
+ .control-btn {
526
+ width: 56px;
527
+ height: 56px;
528
+ border-radius: 50%;
529
+ border: none;
530
+ background: var(--bg-card-hover);
531
+ color: var(--text-primary);
532
+ font-size: 1.25rem;
533
+ cursor: pointer;
534
+ transition: all 0.3s ease;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ }
539
+
540
+ .control-btn:hover {
541
+ background: var(--primary);
542
+ transform: scale(1.1);
543
+ }
544
+
545
+ .control-btn.play {
546
+ width: 72px;
547
+ height: 72px;
548
+ background: var(--gradient-1);
549
+ font-size: 1.5rem;
550
+ box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4);
551
+ }
552
+
553
+ .control-btn.play:hover {
554
+ transform: scale(1.15);
555
+ }
556
+
557
+ .progress-container {
558
+ position: relative;
559
+ margin-bottom: 1rem;
560
+ }
561
+
562
+ .progress-bar {
563
+ width: 100%;
564
+ height: 10px;
565
+ background: var(--border);
566
+ border-radius: 5px;
567
+ cursor: pointer;
568
+ position: relative;
569
+ overflow: hidden;
570
+ }
571
+
572
+ .progress-fill {
573
+ height: 100%;
574
+ background: var(--gradient-1);
575
+ border-radius: 5px;
576
+ width: 0%;
577
+ transition: width 0.1s linear;
578
+ }
579
+
580
+ .time-display {
581
+ display: flex;
582
+ justify-content: space-between;
583
+ font-size: 0.875rem;
584
+ color: var(--text-secondary);
585
+ font-variant-numeric: tabular-nums;
586
+ }
587
+
588
+ .volume-control {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 1rem;
592
+ justify-content: center;
593
+ }
594
+
595
+ .volume-slider {
596
+ width: 120px;
597
+ height: 6px;
598
+ -webkit-appearance: none;
599
+ background: var(--border);
600
+ border-radius: 3px;
601
+ cursor: pointer;
602
+ }
603
+
604
+ .volume-slider::-webkit-slider-thumb {
605
+ -webkit-appearance: none;
606
+ width: 16px;
607
+ height: 16px;
608
+ background: var(--primary);
609
+ border-radius: 50%;
610
+ cursor: pointer;
611
+ }
612
+
613
+ /* Sync Status */
614
+ .sync-status {
615
+ display: grid;
616
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
617
+ gap: 1rem;
618
+ margin-top: 1.5rem;
619
+ }
620
+
621
+ .status-card {
622
+ background: rgba(255, 255, 255, 0.03);
623
+ padding: 1.25rem;
624
+ border-radius: 12px;
625
+ border: 1px solid var(--border);
626
+ text-align: center;
627
+ }
628
+
629
+ .status-value {
630
+ font-size: 1.5rem;
631
+ font-weight: 700;
632
+ color: var(--primary);
633
+ margin-bottom: 0.25rem;
634
+ }
635
+
636
+ .status-label {
637
+ font-size: 0.75rem;
638
+ color: var(--text-secondary);
639
+ text-transform: uppercase;
640
+ letter-spacing: 0.5px;
641
+ }
642
+
643
+ .status-card.synced .status-value {
644
+ color: var(--success);
645
+ }
646
+
647
+ .status-card.warning .status-value {
648
+ color: var(--warning);
649
+ }
650
+
651
+ /* Export Section */
652
+ .export-section {
653
+ background: var(--bg-card);
654
+ border-radius: 24px;
655
+ padding: 2rem;
656
+ border: 1px solid var(--border);
657
+ text-align: center;
658
+ }
659
+
660
+ .export-info {
661
+ margin-bottom: 1.5rem;
662
+ color: var(--text-secondary);
663
+ }
664
+
665
+ .export-actions {
666
+ display: flex;
667
+ gap: 1rem;
668
+ justify-content: center;
669
+ flex-wrap: wrap;
670
+ }
671
+
672
+ .btn-export {
673
+ padding: 1rem 2rem;
674
+ border-radius: 12px;
675
+ font-weight: 600;
676
+ cursor: pointer;
677
+ transition: all 0.3s ease;
678
+ display: flex;
679
+ align-items: center;
680
+ gap: 0.75rem;
681
+ border: none;
682
+ }
683
+
684
+ .btn-export.sync {
685
+ background: var(--gradient-1);
686
+ color: white;
687
+ }
688
+
689
+ .btn-export.sync:hover {
690
+ transform: translateY(-3px);
691
+ box-shadow: 0 15px 40px rgba(99, 102, 241, 0.4);
692
+ }
693
+
694
+ .btn-export.reset {
695
+ background: transparent;
696
+ color: var(--text-secondary);
697
+ border: 2px solid var(--border);
698
+ }
699
+
700
+ .btn-export.reset:hover {
701
+ border-color: var(--error);
702
+ color: var(--error);
703
+ }
704
+
705
+ /* Toast Notifications */
706
+ .toast-container {
707
+ position: fixed;
708
+ top: 2rem;
709
+ right: 2rem;
710
+ z-index: 1000;
711
+ display: flex;
712
+ flex-direction: column;
713
+ gap: 0.75rem;
714
+ }
715
+
716
+ .toast {
717
+ padding: 1rem 1.5rem;
718
+ background: var(--bg-card);
719
+ border-radius: 12px;
720
+ border-left: 4px solid var(--primary);
721
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
722
+ animation: slideIn 0.3s ease;
723
+ display: flex;
724
+ align-items: center;
725
+ gap: 0.75rem;
726
+ }
727
+
728
+ .toast.success {
729
+ border-color: var(--success);
730
+ }
731
+
732
+ .toast.error {
733
+ border-color: var(--error);
734
+ }
735
+
736
+ .toast.warning {
737
+ border-color: var(--warning);
738
+ }
739
+
740
+ @keyframes slideIn {
741
+ from {
742
+ transform: translateX(100%);
743
+ opacity: 0;
744
+ }
745
+ to {
746
+ transform: translateX(0);
747
+ opacity: 1;
748
+ }
749
+ }
750
+
751
+ /* Responsive */
752
+ @media (max-width: 768px) {
753
+ header {
754
+ padding: 1rem;
755
+ flex-direction: column;
756
+ gap: 1rem;
757
+ }
758
+
759
+ main {
760
+ padding: 1rem;
761
+ }
762
+
763
+ .upload-section {
764
+ grid-template-columns: 1fr;
765
+ }
766
+
767
+ .sync-section,
768
+ .playback-controls {
769
+ padding: 1.5rem;
770
+ }
771
+
772
+ .control-btn {
773
+ width: 48px;
774
+ height: 48px;
775
+ }
776
+
777
+ .control-btn.play {
778
+ width: 64px;
779
+ height: 64px;
780
+ }
781
+ }
782
+
783
+ /* Loading Animation */
784
+ .loading {
785
+ display: inline-block;
786
+ width: 20px;
787
+ height: 20px;
788
+ border: 2px solid var(--border);
789
+ border-top-color: var(--primary);
790
+ border-radius: 50%;
791
+ animation: spin 1s linear infinite;
792
+ }
793
+
794
+ @keyframes spin {
795
+ to { transform: rotate(360deg); }
796
+ }
797
+
798
+ /* Visual Feedback for Sync */
799
+ .sync-indicator {
800
+ display: inline-flex;
801
+ align-items: center;
802
+ gap: 0.5rem;
803
+ padding: 0.5rem 1rem;
804
+ background: rgba(16, 185, 129, 0.1);
805
+ border-radius: 20px;
806
+ font-size: 0.875rem;
807
+ color: var(--success);
808
+ }
809
+
810
+ .sync-indicator.unsynced {
811
+ background: rgba(239, 68, 68, 0.1);
812
+ color: var(--error);
813
+ }
814
+
815
+ .sync-dot {
816
+ width: 8px;
817
+ height: 8px;
818
+ border-radius: 50%;
819
+ background: currentColor;
820
+ animation: pulse 2s ease-in-out infinite;
821
+ }
822
+
823
+ @keyframes pulse {
824
+ 0%, 100% { opacity: 1; }
825
+ 50% { opacity: 0.5; }
826
+ }
827
+ </style>
828
+ </head>
829
+ <body>
830
+ <div class="bg-animation"></div>
831
+
832
+ <header>
833
+ <div class="logo">
834
+ <div class="logo-icon">
835
+ <i class="fas fa-wave-square"></i>
836
+ </div>
837
+ <h1>Lip Sync Studio</h1>
838
+ </div>
839
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link">
840
+ <i class="fas fa-robot"></i>
841
+ Built with anycoder
842
+ </a>
843
+ </header>
844
+
845
+ <main>
846
+ <!-- Upload Section -->
847
+ <section class="upload-section">
848
+ <div class="upload-card video" id="video-upload-card">
849
+ <div class="upload-icon">
850
+ <i class="fas fa-video"></i>
851
+ </div>
852
+ <h3 class="upload-title">Upload Video</h3>
853
+ <p class="upload-subtitle">Select an MP4 file to use as the video source</p>
854
+ <input type="file" id="video-input" class="file-input" accept="video/mp4,video/webm">
855
+ <button class="upload-btn" onclick="document.getElementById('video-input').click()">
856
+ <i class="fas fa-folder-open"></i>
857
+ Choose Video File
858
+ </button>
859
+ <div class="file-info" id="video-file-info">
860
+ <i class="fas fa-check-circle"></i>
861
+ <span class="file-name" id="video-filename"></span>
862
+ </div>
863
+ </div>
864
+
865
+ <div class="upload-card" id="audio-upload-card">
866
+ <div class="upload-icon">
867
+ <i class="fas fa-music"></i>
868
+ </div>
869
+ <h3 class="upload-title">Upload Audio</h3>
870
+ <p class="upload-subtitle">Select an MP3 file to synchronize</p>
871
+ <input type="file" id="audio-input" class="file-input" accept="audio/mp3,audio/wav,audio/mpeg">
872
+ <button class="upload-btn" onclick="document.getElementById('audio-input').click()">
873
+ <i class="fas fa-folder-open"></i>
874
+ Choose Audio File
875
+ </button>
876
+ <div class="file-info" id="audio-file-info">
877
+ <i class="fas fa-check-circle"></i>
878
+ <span class="file-name" id="audio-filename"></span>
879
+ </div>
880
+ </div>
881
+ </section>
882
+
883
+ <!-- Preview Section -->
884
+ <section class="preview-section">
885
+ <div class="video-container">
886
+ <div class="waveform-header">
887
+ <span class="waveform-title">
888
+ <i class="fas fa-desktop"></i>
889
+ Video Preview
890
+ </span>
891
+ <span class="sync-indicator unsynced" id="sync-indicator">
892
+ <span class="sync-dot"></span>
893
+ <span id="sync-text">Not Synced</span>
894
+ </span>
895
+ </div>
896
+ <div class="video-wrapper">
897
+ <video id="video-player" playsinline></video>
898
+ <div class="video-placeholder" id="video-placeholder">
899
+ <i class="fas fa-film"></i>
900
+ <span>Video will appear here</span>
901
+ </div>
902
+ </div>
903
+ </div>
904
+
905
+ <div class="waveform-container">
906
+ <div class="waveform-header">
907
+ <span class="waveform-title">
908
+ <i class="fas fa-wave-square"></i>
909
+ Audio Waveform
910
+ </span>
911
+ </div>
912
+ <canvas id="waveform-canvas"></canvas>
913
+ </div>
914
+ </section>
915
+
916
+ <!-- Sync Controls -->
917
+ <section class="sync-section">
918
+ <div class="section-header">
919
+ <div class="section-icon">
920
+ <i class="fas fa-sliders-h"></i>
921
+ </div>
922
+ <h2 class="section-title">Synchronization Controls</h2>
923
+ </div>
924
+
925
+ <div class="sync-controls">
926
+ <div class="control-group">
927
+ <div class="control-label">
928
+ <i class="fas fa-clock"></i>
929
+ Audio Offset (Delay)
930
+ </div>
931
+ <div class="slider-container">
932
+ <input type="range" class="time-slider" id="offset-slider" min="-3000" max="3000" value="0" step="10">
933
+ </div>
934
+ <div class="time-value" id="offset-value">0ms</div>
935
+ <div class="btn-group">
936
+ <button class="btn btn-secondary" onclick="adjustOffset(-100)">
937
+ <i class="fas fa-minus"></i> -100ms
938
+ </button>
939
+ <button class="btn btn-secondary" onclick="adjustOffset(100)">
940
+ <i class="fas fa-plus"></i> +100ms
941
+ </button>
942
+ </div>
943
+ </div>
944
+
945
+ <div class="control-group">
946
+ <div class="control-label">
947
+ <i class="fas fa-play"></i>
948
+ Playback Rate
949
+ </div>
950
+ <div class="slider-container">
951
+ <input type="range" class="time-slider" id="rate-slider" min="0.5" max="2" value="1" step="0.05">
952
+ </div >
953
+ <div class="time-value" id="rate-value">1.0x</div>
954
+ <div class="btn-group">
955
+ <button class="btn btn-secondary" onclick="adjustRate(-0.1)">
956
+ <i class="fas fa-minus"></i> Slower
957
+ </button>
958
+ <button class="btn btn-secondary" onclick="adjustRate(0.1)">
959
+ <i class="fas fa-plus"></i> Faster
960
+ </button>
961
+ </div>
962
+ </div>
963
+
964
+ <div class="control-group">
965
+ <div class="control-label">
966
+ <i class="fas fa-volume-up"></i>
967
+ Audio Volume
968
+ </div>
969
+ <div class="slider-container">
970
+ <input type="range" class="time-slider" id="volume-slider" min="0" max="2" value="1" step="0.1">
971
+ </div>
972
+ <div class="time-value" id="volume-value">100%</div>
973
+ <div class="btn-group">
974
+ <button class="btn btn-secondary" onclick="setVolume(0)">
975
+ <i class="fas fa-volume-mute"></i> Mute
976
+ </button>
977
+ <button class="btn btn-secondary" onclick="setVolume(1)">
978
+ <i class="fas fa-volume-up"></i> Normal
979
+ </button>
980
+ </div>
981
+ </div>
982
+ </div>
983
+ </section>
984
+
985
+ <!-- Playback Controls -->
986
+ <section class="playback-controls">
987
+ <div class="main-controls">
988
+ <button class="control-btn" id="skip-back-btn" title="Skip Back 5s">
989
+ <i class="fas fa-backward"></i>
990
+ </button>
991
+ <button class="control-btn" id="rewind-btn" title="Rewind 1s">
992
+ <i class="fas fa-step-backward"></i>
993
+ </button>
994
+ <button class="control-btn play" id="play-btn" title="Play/Pause">
995
+ <i class="fas fa-play"></i>
996
+ </button>
997
+ <button class="control-btn" id="forward-btn" title="Forward 1s">
998
+ <i class="fas fa-step-forward"></i>
999
+ </button>
1000
+ <button class="control-btn" id="skip-forward-btn" title="Skip Forward 5s">
1001
+ <i class="fas fa-forward"></i>
1002
+ </button>
1003
+ </div>
1004
+
1005
+ <div class="progress-container">
1006
+ <div class="progress-bar" id="progress-bar">
1007
+ <div class="progress-fill" id="progress-fill"></div>
1008
+ </div>
1009
+ <div class="time-display">
1010
+ <span id="current-time">00:00.000</span>
1011
+ <span id="duration">00:00.000</span>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ <div class="volume-control">
1016
+ <i class="fas fa-volume-down" style="color: var(--text-secondary);"></i>
1017
+ <input type="range" class="volume-slider" id="main-volume" min="0" max="1" value="1" step="0.01">
1018
+ <i class="fas fa-volume-up" style="color: var(--text-secondary);"></i>
1019
+ </div>
1020
+
1021
+ <div class="sync-status">
1022
+ <div class="status-card" id="offset-status">
1023
+ <div class="status-value" id="status-offset">0ms</div>
1024
+ <div class="status-label">Audio Offset</div>
1025
+ </div>
1026
+ <div class="status-card" id="rate-status">
1027
+ <div class="status-value" id="status-rate">1.0x</div>
1028
+ <div class="status-label">Playback Rate</div>
1029
+ </div>
1030
+ <div class="status-card" id="video-status">
1031
+ <div class="status-value" id="status-video">--</div>
1032
+ <div class="status-label">Video Loaded</div>
1033
+ </div>
1034
+ <div class="status-card" id="audio-status">
1035
+ <div class="status-value" id="status-audio">--</div>
1036
+ <div class="status-label">Audio Loaded</div>
1037
+ </div>
1038
+ </div>
1039
+ </section>
1040
+
1041
+ <!-- Export Section -->
1042
+ <section class="export-section">
1043
+ <div class="section-header" style="justify-content: center;">
1044
+ <div class="section-icon">
1045
+ <i class="fas fa-download"></i>
1046
+ </div>
1047
+ <h2 class="section-title">Export Settings</h2>
1048
+ </div>
1049
+ <p class="export-info">Save your synchronization settings to apply them later or share with others.</p>
1050
+ <div class="export-actions">
1051
+ <button class="btn-export sync" onclick="exportSettings()">
1052
+ <i class="fas fa-save"></i>
1053
+ Save Sync Settings
1054
+ </button>
1055
+ <button class="btn-export reset" onclick="resetAll()">
1056
+ <i class="fas fa-undo"></i>
1057
+ Reset All
1058
+ </button>
1059
+ </div>
1060
+ </section>
1061
+ </main>
1062
+
1063
+ <!-- Toast Container -->
1064
+ <div class="toast-container" id="toast-container"></div>
1065
+
1066
+ <script>
1067
+ // Global state
1068
+ const state = {
1069
+ videoFile: null,
1070
+ audioFile: null,
1071
+ audioContext: null,
1072
+ audioBuffer: null,
1073
+ audioSource: null,
1074
+ isPlaying: false,
1075
+ audioOffset: 0,
1076
+ playbackRate: 1,
1077
+ volume: 1,
1078
+ startTime: 0,
1079
+ pausedAt: 0,
1080
+ videoElement: null,
1081
+ audioElement: null
1082
+ };
1083
+
1084
+ // DOM Elements
1085
+ const elements = {
1086
+ videoInput: document.getElementById('video-input'),
1087
+ audioInput: document.getElementById('audio-input'),
1088
+ videoPlayer: document.getElementById('video-player'),
1089
+ videoPlaceholder: document.getElementById('video-placeholder'),
1090
+ waveformCanvas: document.getElementById('waveform-canvas'),
1091
+ playBtn: document.getElementById('play-btn'),
1092
+ progressBar: document.getElementById('progress-bar'),
1093
+ progressFill: document.getElementById('progress-fill'),
1094
+ currentTime: document.getElementById('current-time'),
1095
+ duration: document.getElementById('duration'),
1096
+ offsetSlider: document.getElementById('offset-slider'),
1097
+ offsetValue: document.getElementById('offset-value'),
1098
+ rateSlider: document.getElementById('rate-slider'),
1099
+ rateValue: document.getElementById('rate-value'),
1100
+ volumeSlider: document.getElementById('volume-slider'),
1101
+ volumeValue: document.getElementById('volume-value'),
1102
+ mainVolume: document.getElementById('main-volume'),
1103
+ syncIndicator: document.getElementById('sync-indicator'),
1104
+ syncText: document.getElementById('sync-text')
1105
+ };
1106
+
1107
+ // Initialize
1108
+ document.addEventListener('DOMContentLoaded', () => {
1109
+ initializeEventListeners();
1110
+ drawEmptyWaveform();
1111
+ });
1112
+
1113
+ function initializeEventListeners() {
1114
+ // File inputs
1115
+ elements.videoInput.addEventListener('change', handleVideoUpload);
1116
+ elements.audioInput.addEventListener('change', handleAudioUpload);
1117
+
1118
+ // Playback controls
1119
+ elements.playBtn.addEventListener('click', togglePlay);
1120
+ document.getElementById('skip-back-btn').addEventListener('click', () => skip(-5));
1121
+ document.getElementById('rewind-btn').addEventListener('click', () => skip(-1));
1122
+ document.getElementById('forward-btn').addEventListener('click', () => skip(1));
1123
+ document.getElementById('skip-forward-btn').addEventListener('click', () => skip(5));
1124
+
1125
+ // Progress bar
1126
+ elements.progressBar.addEventListener('click', seek);
1127
+
1128
+ // Sliders
1129
+ elements.offsetSlider.addEventListener('input', updateOffset);
1130
+ elements.rateSlider.addEventListener('input', updateRate);
1131
+ elements.volumeSlider.addEventListener('input', updateVolume);
1132
+ elements.mainVolume.addEventListener('input', (e) => {
1133
+ const vol = parseFloat(e.target.value);
1134
+ setVolume(vol);
1135
+ });
1136
+
1137
+ // Video events
1138
+ elements.videoPlayer.addEventListener('loadedmetadata', updateDuration);
1139
+ elements.videoPlayer.addEventListener('timeupdate', updateProgress);
1140
+ elements.videoPlayer.addEventListener('ended', () => {
1141
+ state.isPlaying = false;
1142
+ updatePlayButton();
1143
+ });
1144
+ }
1145
+
1146
+ // File Upload Handlers
1147
+ function handleVideoUpload(e) {
1148
+ const file = e.target.files[0];
1149
+ if (!file) return;
1150
+
1151
+ state.videoFile = file;
1152
+ const url = URL.createObjectURL(file);
1153
+
1154
+ elements.videoPlayer.src = url;
1155
+ elements.videoPlayer.load();
1156
+ elements.videoPlaceholder.style.display = 'none';
1157
+
1158
+ // Update UI
1159
+ document.getElementById('video-upload-card').classList.add('has-file');
1160
+ document.getElementById('video-filename').textContent = file.name;
1161
+ document.getElementById('video-file-info').classList.add('visible');
1162
+ document.getElementById('status-video').textContent = '✓ Loaded';
1163
+
1164
+ showToast('Video loaded successfully!', 'success');
1165
+ checkSyncStatus();
1166
+ }
1167
+
1168
+ async function handleAudioUpload(e) {
1169
+ const file = e.target.files[0];
1170
+ if (!file) return;
1171
+
1172
+ state.audioFile = file;
1173
+
1174
+ try {
1175
+ // Create audio element for playback
1176
+ if (state.audioElement) {
1177
+ state.audioElement.pause();
1178
+ state.audioElement = null;
1179
+ }
1180
+
1181
+ const audioUrl = URL.createObjectURL(file);
1182
+ state.audioElement = new Audio(audioUrl);
1183
+
1184
+ // Setup Web Audio API for visualization
1185
+ await setupAudioContext(file);
1186
+
1187
+ // Update UI
1188
+ document.getElementById('audio-upload-card').classList.add('has-file');
1189
+ document.getElementById('audio-filename').textContent = file.name;
1190
+ document.getElementById('audio-file-info').classList.add('visible');
1191
+ document.getElementById('status-audio').textContent = '✓ Loaded';
1192
+
1193
+ showToast('Audio loaded successfully!', 'success');
1194
+ checkSyncStatus();
1195
+ } catch (error) {
1196
+ showToast('Error loading audio: ' + error.message, 'error');
1197
+ }
1198
+ }
1199
+
1200
+ async function setupAudioContext(file) {
1201
+ if (!state.audioContext) {
1202
+ state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
1203
+ }
1204
+
1205
+ const arrayBuffer = await file.arrayBuffer();
1206
+ state.audioBuffer = await state.audioContext.decodeAudioData(arrayBuffer);
1207
+
1208
+ drawWaveform();
1209
+ }
1210
+
1211
+ // Waveform Drawing
1212
+ function drawEmptyWaveform() {
1213
+ const canvas = elements.waveformCanvas;
1214
+ const ctx = canvas.getContext('2d');
1215
+ const dpr = window.devicePixelRatio || 1;
1216
+
1217
+ canvas.width = canvas.offsetWidth * dpr;
1218
+ canvas.height = canvas.offsetHeight * dpr;
1219
+ ctx.scale(dpr, dpr);
1220
+
1221
+ const width = canvas.offsetWidth;
1222
+ const height = canvas.offsetHeight;
1223
+
1224
+ // Background
1225
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
1226
+ ctx.fillRect(0, 0, width, height);
1227
+
1228
+ // Center line
1229
+ ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
1230
+ ctx.lineWidth = 1;
1231
+ ctx.beginPath();
1232
+ ctx.moveTo(0, height / 2);
1233
+ ctx.lineTo(width, height / 2);
1234
+ ctx.stroke();
1235
+
1236
+ // Text
1237
+ ctx.fillStyle = 'rgba(161, 161, 170, 0.5)';
1238
+ ctx.font = '14px Inter';
1239
+ ctx.textAlign = 'center';
1240
+ ctx.fillText('Upload audio to see waveform', width / 2, height / 2 + 5);
1241
+ }
1242
+
1243
+ function drawWaveform() {
1244
+ if (!state.audioBuffer) return;
1245
+
1246
+ const canvas = elements.waveformCanvas;
1247
+ const ctx = canvas.getContext('2d');
1248
+ const dpr = window.devicePixelRatio || 1;
1249
+
1250
+ canvas.width = canvas.offsetWidth * dpr;
1251
+ canvas.height = canvas.offsetHeight * dpr;
1252
+ ctx.scale(dpr, dpr);
1253
+
1254
+ const width = canvas.offsetWidth;
1255
+ const height = canvas.offsetHeight;
1256
+ const data = state.audioBuffer.getChannelData(0);
1257
+ const step = Math.ceil(data.length / width);
1258
+ const amp = height / 2;
1259
+
1260
+ // Clear
1261
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
1262
+ ctx.fillRect(0, 0, width, height);
1263
+
1264
+ // Draw waveform
1265
+ const gradient = ctx.createLinearGradient(0, 0, width, 0);
1266
+ gradient.addColorStop(0, '#6366f1');
1267
+ gradient.addColorStop(0.5, '#8b5cf6');
1268
+ gradient.addColorStop(1, '#ec4899');
1269
+
1270
+ ctx.fillStyle = gradient;
1271
+ ctx.beginPath();
1272
+ ctx.moveTo(0, amp);
1273
+
1274
+ for (let i = 0; i < width; i++) {
1275
+ let min = 1.0;
1276
+ let max = -1.0;
1277
+
1278
+ for (let j = 0; j < step; j++) {
1279
+ const datum = data[(i * step) + j];
1280
+ if (datum < min) min = datum;
1281
+ if (datum > max) max = datum;
1282
+ }
1283
+
1284
+ ctx.lineTo(i, (1 + min) * amp);
1285
+ ctx.lineTo(i, (1 + max) * amp);
1286
+ }
1287
+
1288
+ ctx.closePath();
1289
+ ctx.fill();
1290
+
1291
+ // Center line
1292
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
1293
+ ctx.lineWidth = 1;
1294
+ ctx.beginPath();
1295
+ ctx.moveTo(0, height / 2);
1296
+ ctx.lineTo(width, height / 2);
1297
+ ctx.stroke();
1298
+ }
1299
+
1300
+ // Playback Controls
1301
+ function togglePlay() {
1302
+ if (!state.videoFile && !state.audioFile) {
1303
+ showToast('Please upload a video or audio file first', 'warning');
1304
+ return;
1305
+ }
1306
+
1307
+ if (state.isPlaying) {
1308
+ pause();
1309
+ } else {
1310
+ play();
1311
+ }
1312
+ }
1313
+
1314
+ function play() {
1315
+ state.isPlaying = true;
1316
+
1317
+ if (state.videoFile) {
1318
+ elements.videoPlayer.play();
1319
+ }
1320
+
1321
+ if (state.audioFile && state.audioElement) {
1322
+ if (state.audioContext && state.audioContext.state === 'suspended') {
1323
+ state.audioContext.resume();
1324
+ }
1325
+ state.audioElement.currentTime = elements.videoPlayer.currentTime + (state.audioOffset / 1000);
1326
+ state.audioElement.playbackRate = state.playbackRate;
1327
+ state.audioElement.volume = state.volume;
1328
+ state.audioElement.play();
1329
+ }
1330
+
1331
+ updatePlayButton();
1332
+ showToast('Playback started', 'success');
1333
+ }
1334
+
1335
+ function pause() {
1336
+ state.isPlaying = false;
1337
+
1338
+ if (state.videoFile) {
1339
+ elements.videoPlayer.pause();
1340
+ }
1341
+
1342
+ if (state.audioElement) {
1343
+ state.audioElement.pause();
1344
+ }
1345
+
1346
+ updatePlayButton();
1347
+ }
1348
+
1349
+ function updatePlayButton() {
1350
+ const icon = elements.playBtn.querySelector('i');
1351
+ icon.className = state.isPlaying ? 'fas fa-pause' : 'fas fa-play';
1352
+ }
1353
+
1354
+ function skip(seconds) {
1355
+ if (state.videoFile) {
1356
+ elements.videoPlayer.currentTime = Math.max(0, elements.videoPlayer.currentTime + seconds);
1357
+ }
1358
+ }
1359
+
1360
+ function seek(e) {
1361
+ const rect = elements.progressBar.getBoundingClientRect();
1362
+ const pos = (e.clientX - rect.left) / rect.width;
1363
+ const duration = elements.videoPlayer.duration || 0;
1364
+
1365
+ if (state.videoFile) {
1366
+ elements.videoPlayer.currentTime = pos * duration;
1367
+ }
1368
+
1369
+ if (state.audioElement) {
1370
+ state.audioElement.currentTime = pos * duration;
1371
+ }
1372
+ }
1373
+
1374
+ function updateProgress() {
1375
+ if (!state.videoFile) return;
1376
+
1377
+ const current = elements.videoPlayer.currentTime;
1378
+ const duration = elements.videoPlayer.duration || 1;
1379
+ const percent = (current / duration) * 100;
1380
+
1381
+ elements.progressFill.style.width = percent + '%';
1382
+ elements.currentTime.textContent = formatTime(current);
1383
+
1384
+ // Sync audio position
1385
+ if (state.audioElement && state.isPlaying) {
1386
+ const audioTime = current + (state.audioOffset / 1000);
1387
+ if (Math.abs(state.audioElement.currentTime - audioTime) > 0.1) {
1388
+ state.audioElement.currentTime = audioTime;
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ function updateDuration() {
1394
+ const duration = elements.videoPlayer.duration;
1395
+ elements.duration.textContent = formatTime(duration);
1396
+ }
1397
+
1398
+ function formatTime(seconds) {
1399
+ if (isNaN(seconds)) return '00:00.000';
1400
+
1401
+ const mins = Math.floor(seconds / 60);
1402
+ const secs = Math.floor(seconds % 60);
1403
+ const ms = Math.floor((seconds % 1) * 1000);
1404
+
1405
+ return `${mins.toString().padStart(2,