akhaliq HF Staff commited on
Commit
5a6c23f
·
1 Parent(s): acfc1da

Enhance UI with a highly responsive, clean, and mobile-friendly design

Browse files
Files changed (1) hide show
  1. index.html +686 -629
index.html CHANGED
@@ -7,7 +7,7 @@
7
 
8
  <!-- Meta tags for premium look and SEO -->
9
  <meta name="description" content="Generate highly expressive speech with voice cloning. Powered by LTX-2.3 and Resemble Perth watermarking.">
10
- <meta name="theme-color" content="#0d0f17">
11
 
12
  <!-- Google Fonts: Outfit and Inter -->
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -16,19 +16,22 @@
16
 
17
  <style>
18
  :root {
19
- --bg-color: #080a10;
20
- --panel-bg: rgba(18, 22, 38, 0.7);
21
- --border-color: rgba(255, 255, 255, 0.08);
22
- --text-primary: #f1f3f9;
23
- --text-secondary: #9aa0b9;
24
  --accent-orange: #ff6b35;
25
- --accent-orange-glow: rgba(255, 107, 53, 0.4);
 
26
  --accent-purple: #8b5cf6;
27
- --accent-purple-glow: rgba(139, 92, 246, 0.4);
 
28
  --accent-green: #10b981;
29
- --radius-lg: 16px;
30
- --radius-md: 10px;
31
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
32
  }
33
 
34
  * {
@@ -42,86 +45,107 @@
42
  color: var(--text-primary);
43
  font-family: 'Inter', sans-serif;
44
  min-height: 100vh;
45
- line-height: 1.6;
46
  overflow-x: hidden;
47
  position: relative;
 
 
48
  }
49
 
50
- /* Animated glowing background patterns */
51
  body::before {
52
  content: '';
53
  position: absolute;
54
- top: -20%;
55
- left: -10%;
56
- width: 60%;
57
- height: 60%;
58
- background: radial-gradient(circle, var(--accent-orange-glow) 0%, transparent 70%);
59
  z-index: -1;
60
- filter: blur(100px);
61
  pointer-events: none;
62
- opacity: 0.6;
63
  }
64
 
65
  body::after {
66
  content: '';
67
  position: absolute;
68
- bottom: -10%;
69
- right: -10%;
70
- width: 50%;
71
- height: 50%;
72
- background: radial-gradient(circle, var(--accent-purple-glow) 0%, transparent 75%);
73
  z-index: -1;
74
- filter: blur(100px);
75
  pointer-events: none;
76
- opacity: 0.4;
77
  }
78
 
79
- header {
80
- max-width: 1280px;
 
81
  margin: 0 auto;
82
- padding: 30px 20px 10px 20px;
 
 
 
 
 
 
83
  display: flex;
84
  flex-direction: column;
85
  align-items: center;
86
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  h1 {
90
  font-family: 'Outfit', sans-serif;
91
- font-size: 2.8rem;
92
  font-weight: 800;
93
- background: linear-gradient(135deg, #ffffff 40%, #ff8e53 70%, #d49aff 100%);
94
  -webkit-background-clip: text;
95
  -webkit-text-fill-color: transparent;
96
- margin-bottom: 8px;
97
- letter-spacing: -1px;
98
- display: flex;
99
- align-items: center;
100
- gap: 12px;
101
  }
102
 
103
  .subtitle {
104
- font-size: 1.1rem;
105
  color: var(--text-secondary);
106
  font-weight: 400;
107
- max-width: 600px;
108
- margin-bottom: 20px;
109
  }
110
 
111
  .ltx-banner {
112
- background: linear-gradient(90deg, rgba(26, 31, 58, 0.8) 0%, rgba(42, 31, 58, 0.8) 100%);
113
- border-left: 4px solid var(--accent-orange);
 
114
  border-radius: var(--radius-md);
115
- padding: 12px 20px;
116
- color: #e8e8f0;
117
- font-size: 0.9rem;
118
- max-width: 900px;
119
- margin: 0 auto 30px auto;
120
- backdrop-filter: blur(10px);
121
- border-top: 1px solid var(--border-color);
122
- border-right: 1px solid var(--border-color);
123
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
124
  text-align: left;
 
 
125
  }
126
 
127
  .ltx-banner a {
@@ -132,57 +156,78 @@
132
  }
133
 
134
  .ltx-banner a:hover {
135
- text-decoration: underline;
136
- color: #ffffff;
137
- }
138
-
139
- .ltx-banner strong {
140
  color: #ffffff;
 
141
  }
142
 
 
143
  main {
144
- max-width: 1280px;
145
- margin: 0 auto;
146
- padding: 0 20px 60px 20px;
147
  display: grid;
148
  grid-template-columns: 1.2fr 1fr;
149
- gap: 30px;
 
150
  }
151
 
152
- @media (max-width: 992px) {
153
  main {
154
  grid-template-columns: 1fr;
155
  }
156
  h1 {
157
- font-size: 2.2rem;
 
 
 
158
  }
159
  }
160
 
161
- .panel {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  background: var(--panel-bg);
163
- border: 1px solid var(--border-color);
164
  border-radius: var(--radius-lg);
165
- padding: 30px;
166
- backdrop-filter: blur(16px);
167
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
168
  display: flex;
169
  flex-direction: column;
170
- gap: 24px;
171
  transition: var(--transition);
172
  }
173
 
174
- .panel:hover {
175
- border-color: rgba(255, 255, 255, 0.12);
176
  }
177
 
178
- .panel-title {
179
- font-family: 'Outfit', sans-serif;
180
- font-size: 1.4rem;
181
- font-weight: 700;
 
 
 
 
182
  display: flex;
183
  align-items: center;
184
  gap: 10px;
 
 
 
185
  color: #ffffff;
 
 
186
  }
187
 
188
  .form-group {
@@ -192,115 +237,121 @@
192
  }
193
 
194
  .form-label {
195
- font-size: 0.85rem;
196
  font-weight: 600;
197
  text-transform: uppercase;
198
- letter-spacing: 0.5px;
199
  color: var(--text-secondary);
200
  display: flex;
201
  justify-content: space-between;
 
202
  }
203
 
204
- .form-label .label-info {
205
- font-size: 0.8rem;
206
  color: var(--accent-orange);
207
  text-transform: none;
208
- font-weight: normal;
 
209
  }
210
 
211
  .textarea-custom {
212
  width: 100%;
213
- background: rgba(8, 10, 16, 0.6);
214
- border: 1px solid var(--border-color);
215
  border-radius: var(--radius-md);
216
- padding: 16px;
217
  color: var(--text-primary);
218
  font-family: 'Inter', sans-serif;
219
  font-size: 0.95rem;
220
  line-height: 1.5;
221
  resize: vertical;
222
- min-height: 150px;
223
  outline: none;
224
  transition: var(--transition);
225
  }
226
 
227
  .textarea-custom:focus {
228
  border-color: var(--accent-orange);
229
- box-shadow: 0 0 15px rgba(255, 107, 53, 0.15);
230
- background: rgba(8, 10, 16, 0.85);
231
  }
232
 
233
- /* Drag & Drop Audio Upload Uploader */
234
- .upload-container {
235
- border: 2px dashed rgba(255, 255, 255, 0.15);
236
  border-radius: var(--radius-md);
237
- padding: 24px;
238
  text-align: center;
239
  cursor: pointer;
240
- background: rgba(255, 255, 255, 0.02);
241
- transition: var(--transition);
242
  display: flex;
243
  flex-direction: column;
244
  align-items: center;
 
 
 
245
  justify-content: center;
246
- gap: 10px;
247
- min-height: 120px;
248
  }
249
 
250
- .upload-container:hover, .upload-container.dragover {
251
  border-color: var(--accent-purple);
252
- background: rgba(139, 92, 246, 0.05);
253
  }
254
 
255
  .upload-icon {
256
- font-size: 2rem;
257
  color: var(--text-secondary);
258
  transition: var(--transition);
259
  }
260
 
261
- .upload-container:hover .upload-icon {
262
- transform: translateY(-4px);
263
  color: var(--accent-purple);
264
  }
265
 
266
  .upload-text {
267
- font-size: 0.9rem;
268
  color: var(--text-secondary);
269
  }
270
 
271
  .upload-text strong {
272
- color: var(--text-primary);
 
273
  }
274
 
275
  .hidden-input {
276
  display: none;
277
  }
278
 
279
- .uploaded-file-info {
 
280
  display: flex;
281
  align-items: center;
282
- gap: 12px;
283
- background: rgba(139, 92, 246, 0.1);
284
  border: 1px solid rgba(139, 92, 246, 0.2);
285
- padding: 10px 16px;
286
  border-radius: var(--radius-md);
287
  width: 100%;
288
- justify-content: space-between;
289
  }
290
 
291
- .uploaded-file-details {
292
  display: flex;
293
  align-items: center;
294
  gap: 10px;
295
- font-size: 0.9rem;
296
  font-weight: 500;
 
 
 
297
  }
298
 
299
- .clear-upload {
300
  background: none;
301
  border: none;
302
  color: var(--text-secondary);
303
- font-size: 1.1rem;
304
  cursor: pointer;
305
  transition: var(--transition);
306
  padding: 2px;
@@ -309,52 +360,60 @@
309
  justify-content: center;
310
  }
311
 
312
- .clear-upload:hover {
313
  color: #ffffff;
314
  transform: scale(1.1);
315
  }
316
 
317
- /* Sliders / Advanced Settings */
318
- .accordion-btn {
319
- background: rgba(255, 255, 255, 0.03);
320
- border: 1px solid var(--border-color);
321
  border-radius: var(--radius-md);
322
- padding: 12px 18px;
323
- color: var(--text-primary);
 
 
 
 
 
 
324
  font-weight: 600;
325
  font-size: 0.9rem;
 
326
  display: flex;
327
  justify-content: space-between;
328
  align-items: center;
329
  cursor: pointer;
330
- transition: var(--transition);
331
- width: 100%;
332
  outline: none;
 
333
  }
334
 
335
- .accordion-btn:hover {
336
- background: rgba(255, 255, 255, 0.06);
337
- border-color: rgba(255, 255, 255, 0.15);
338
  }
339
 
340
  .accordion-icon {
341
- font-size: 0.8rem;
342
  transition: var(--transition);
 
343
  }
344
 
345
- .accordion-content {
346
  max-height: 0;
347
  overflow: hidden;
348
- transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 
349
  display: flex;
350
  flex-direction: column;
351
- gap: 18px;
352
- padding: 0 4px;
353
  }
354
 
355
- .accordion-content.open {
356
- max-height: 500px;
357
- margin-top: 15px;
 
 
358
  }
359
 
360
  .slider-group {
@@ -363,37 +422,38 @@
363
  gap: 6px;
364
  }
365
 
366
- .slider-header {
367
  display: flex;
368
  justify-content: space-between;
369
- font-size: 0.85rem;
370
  color: var(--text-secondary);
371
  font-weight: 500;
372
  }
373
 
374
- .slider-val {
375
- color: var(--text-primary);
376
  font-weight: 700;
 
377
  }
378
 
 
379
  input[type="range"] {
380
  -webkit-appearance: none;
381
  width: 100%;
382
- height: 6px;
383
- background: rgba(255, 255, 255, 0.08);
384
  border-radius: 3px;
385
  outline: none;
386
- transition: var(--transition);
387
  }
388
 
389
  input[type="range"]::-webkit-slider-thumb {
390
  -webkit-appearance: none;
391
- width: 16px;
392
- height: 16px;
393
  border-radius: 50%;
394
  background: var(--accent-orange);
395
- cursor: pointer;
396
- box-shadow: 0 0 10px var(--accent-orange-glow);
397
  transition: var(--transition);
398
  }
399
 
@@ -402,133 +462,136 @@
402
  background: #ffffff;
403
  }
404
 
405
- .seed-input-container {
 
406
  display: flex;
407
- gap: 10px;
408
  align-items: center;
409
  }
410
 
411
- .input-number {
412
  flex: 1;
413
- background: rgba(8, 10, 16, 0.6);
414
- border: 1px solid var(--border-color);
415
- border-radius: var(--radius-md);
416
- padding: 10px;
417
  color: var(--text-primary);
418
- font-family: inherit;
419
  outline: none;
 
420
  transition: var(--transition);
 
421
  }
422
 
423
- .input-number:focus {
424
  border-color: var(--accent-orange);
425
  }
426
 
427
- .btn-icon {
428
- background: rgba(255, 255, 255, 0.05);
429
- border: 1px solid var(--border-color);
430
- border-radius: var(--radius-md);
431
  color: var(--text-primary);
432
- height: 42px;
433
- width: 42px;
434
  display: flex;
435
  align-items: center;
436
  justify-content: center;
437
  cursor: pointer;
438
  transition: var(--transition);
 
439
  }
440
 
441
- .btn-icon:hover {
442
- background: rgba(255, 255, 255, 0.1);
443
- border-color: rgba(255, 255, 255, 0.2);
444
  }
445
 
446
- /* Generate Button */
447
- .btn-primary {
448
  background: linear-gradient(135deg, var(--accent-orange) 0%, #ff8e53 100%);
449
  border: none;
450
  border-radius: var(--radius-md);
451
  color: #ffffff;
452
  font-family: 'Outfit', sans-serif;
453
- font-size: 1.1rem;
454
  font-weight: 700;
455
- padding: 16px;
456
  cursor: pointer;
457
  transition: var(--transition);
458
  display: flex;
459
  align-items: center;
460
  justify-content: center;
461
- gap: 12px;
462
- box-shadow: 0 8px 24px rgba(255, 107, 53, 0.25);
463
- margin-top: 10px;
464
- position: relative;
465
- overflow: hidden;
466
  }
467
 
468
- .btn-primary:hover:not(:disabled) {
469
- transform: translateY(-2px);
470
- box-shadow: 0 12px 30px rgba(255, 107, 53, 0.4);
471
- filter: brightness(1.05);
472
  }
473
 
474
- .btn-primary:active:not(:disabled) {
475
  transform: translateY(0);
476
  }
477
 
478
- .btn-primary:disabled {
479
- background: #252836;
480
- color: #636882;
481
  cursor: not-allowed;
482
  box-shadow: none;
483
  }
484
 
485
  /* Right Panel: Output & Controls */
486
- .output-card {
487
- background: rgba(8, 10, 16, 0.4);
488
- border: 1px solid var(--border-color);
489
  border-radius: var(--radius-md);
490
- padding: 24px;
491
  display: flex;
492
  flex-direction: column;
493
  align-items: center;
494
  justify-content: center;
495
- min-height: 200px;
496
  position: relative;
497
- gap: 16px;
498
  }
499
 
500
- .output-empty {
501
  color: var(--text-secondary);
502
  text-align: center;
503
  display: flex;
504
  flex-direction: column;
505
  align-items: center;
506
- gap: 12px;
507
  }
508
 
509
- .output-empty-icon {
510
- font-size: 2.5rem;
511
- opacity: 0.5;
512
  }
513
 
514
- /* Beautiful Custom Audio Player */
515
- .custom-player {
 
 
 
 
516
  width: 100%;
517
  display: flex;
518
  flex-direction: column;
519
- gap: 16px;
520
  }
521
 
522
- .visualizer-container {
523
  width: 100%;
524
- height: 60px;
525
- background: linear-gradient(90deg, rgba(139, 92, 246, 0.05) 0%, rgba(255, 107, 53, 0.05) 100%);
526
- border-radius: var(--radius-md);
527
  display: flex;
528
  align-items: center;
529
  justify-content: center;
530
- border: 1px solid rgba(255, 255, 255, 0.03);
531
- position: relative;
532
  overflow: hidden;
533
  }
534
 
@@ -541,20 +604,19 @@
541
 
542
  .wave-bar {
543
  width: 3px;
544
- height: 8px;
545
  background: var(--accent-purple);
546
  border-radius: 1px;
547
  transition: var(--transition);
548
  }
549
 
550
- .custom-player.playing .wave-bar {
551
- animation: bounce 1.2s infinite ease-in-out alternate;
552
  }
553
 
554
- /* Animation for visualizer bars */
555
- @keyframes bounce {
556
- 0% { height: 8px; }
557
- 100% { height: 40px; }
558
  }
559
 
560
  .wave-bar:nth-child(2n) { background: var(--accent-orange); animation-delay: 0.15s; }
@@ -562,273 +624,266 @@
562
  .wave-bar:nth-child(4n) { animation-delay: 0.45s; }
563
  .wave-bar:nth-child(5n) { background: var(--accent-purple); animation-delay: 0.6s; }
564
 
565
- .player-controls {
566
  display: flex;
567
  align-items: center;
568
- gap: 16px;
569
  width: 100%;
570
  }
571
 
572
- .play-btn {
573
  background: #ffffff;
574
  border: none;
575
  color: var(--bg-color);
576
- width: 44px;
577
- height: 44px;
578
  border-radius: 50%;
579
  display: flex;
580
  align-items: center;
581
  justify-content: center;
582
- font-size: 1.1rem;
583
  cursor: pointer;
584
  transition: var(--transition);
585
- box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
586
  flex-shrink: 0;
587
  }
588
 
589
- .play-btn:hover {
590
  transform: scale(1.05);
591
- box-shadow: 0 6px 20px rgba(255, 255, 255, 0.4);
592
  }
593
 
594
- .time-slider-container {
595
  flex: 1;
596
  display: flex;
597
  align-items: center;
598
- gap: 10px;
599
  }
600
 
601
- .time-label {
602
- font-size: 0.8rem;
603
  color: var(--text-secondary);
604
  font-family: monospace;
605
- min-width: 35px;
606
  }
607
 
608
- .extra-player-controls {
609
  display: flex;
610
  align-items: center;
611
  justify-content: space-between;
612
  width: 100%;
613
- border-top: 1px solid rgba(255, 255, 255, 0.05);
614
- padding-top: 14px;
 
 
615
  }
616
 
617
- .volume-container {
618
  display: flex;
619
  align-items: center;
620
- gap: 8px;
621
- max-width: 120px;
 
622
  }
623
 
624
- .volume-icon {
625
- font-size: 0.9rem;
626
  color: var(--text-secondary);
627
  }
628
 
629
- .speed-control {
630
  display: flex;
631
  align-items: center;
632
- gap: 6px;
633
  }
634
 
635
- .speed-btn {
636
- background: rgba(255, 255, 255, 0.03);
637
- border: 1px solid var(--border-color);
638
  border-radius: 6px;
639
  color: var(--text-secondary);
640
- font-size: 0.75rem;
641
  font-weight: 600;
642
- padding: 4px 8px;
643
  cursor: pointer;
644
  transition: var(--transition);
645
  }
646
 
647
- .speed-btn.active, .speed-btn:hover {
648
- background: rgba(255, 255, 255, 0.1);
649
  color: #ffffff;
650
- border-color: rgba(255, 255, 255, 0.25);
651
  }
652
 
653
- .btn-download {
654
- background: rgba(255, 255, 255, 0.06);
655
- border: 1px solid var(--border-color);
656
- border-radius: var(--radius-md);
657
  color: #ffffff;
658
- font-size: 0.85rem;
659
  font-weight: 600;
660
- padding: 8px 16px;
661
  text-decoration: none;
662
  display: flex;
663
  align-items: center;
664
- gap: 8px;
665
  transition: var(--transition);
666
  }
667
 
668
- .btn-download:hover {
669
  background: #ffffff;
670
  color: var(--bg-color);
671
  transform: translateY(-1px);
672
  }
673
 
674
- /* Queue & Status Indicator */
675
- .status-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  display: flex;
677
  align-items: center;
678
- gap: 12px;
679
- font-size: 0.9rem;
680
  font-weight: 500;
681
- padding: 12px 18px;
682
  border-radius: var(--radius-md);
683
  border: 1px solid transparent;
684
- display: none; /* Shown dynamically */
685
  }
686
 
687
- .status-container.success {
688
- background: rgba(16, 185, 129, 0.1);
689
- border-color: rgba(16, 185, 129, 0.2);
690
  color: var(--accent-green);
691
  }
692
 
693
- .status-container.info {
694
- background: rgba(139, 92, 246, 0.1);
695
- border-color: rgba(139, 92, 246, 0.2);
696
  color: #a78bfa;
697
  }
698
 
699
- .status-container.error {
700
- background: rgba(239, 68, 68, 0.1);
701
- border-color: rgba(239, 68, 68, 0.2);
702
  color: #f87171;
703
  }
704
 
705
- .spinner {
706
- width: 18px;
707
- height: 18px;
708
  border: 2px solid rgba(255, 255, 255, 0.1);
709
  border-top: 2px solid currentColor;
710
  border-radius: 50%;
711
  animation: spin 0.8s linear infinite;
712
  }
713
 
714
- @keyframes spin {
715
- 0% { transform: rotate(0deg); }
716
- 100% { transform: rotate(360deg); }
717
- }
718
-
719
- /* Examples Section */
720
- .examples-section {
721
  display: flex;
722
  flex-direction: column;
723
- gap: 14px;
724
- }
725
-
726
- .examples-title {
727
- font-family: 'Outfit', sans-serif;
728
- font-size: 1.1rem;
729
- font-weight: 700;
730
- color: #ffffff;
731
- display: flex;
732
- align-items: center;
733
- gap: 8px;
734
  }
735
 
736
- .examples-list {
737
  display: flex;
738
  flex-direction: column;
739
- gap: 10px;
740
- max-height: 380px;
741
  overflow-y: auto;
742
- padding-right: 4px;
743
  }
744
 
745
- /* Custom scrollbar for examples */
746
- .examples-list::-webkit-scrollbar {
747
- width: 6px;
748
  }
749
 
750
- .examples-list::-webkit-scrollbar-track {
751
- background: rgba(255, 255, 255, 0.02);
752
- border-radius: 3px;
753
  }
754
 
755
- .examples-list::-webkit-scrollbar-thumb {
756
- background: rgba(255, 255, 255, 0.1);
757
- border-radius: 3px;
758
  }
759
 
760
- .example-item {
761
- background: rgba(255, 255, 255, 0.02);
762
- border: 1px solid var(--border-color);
763
  border-radius: var(--radius-md);
764
- padding: 12px 16px;
765
  cursor: pointer;
766
  transition: var(--transition);
767
  display: flex;
768
- flex-direction: column;
769
- gap: 6px;
 
770
  text-align: left;
771
  }
772
 
773
- .example-item:hover {
774
- background: rgba(255, 255, 255, 0.06);
775
- border-color: rgba(255, 255, 255, 0.15);
776
- transform: translateX(2px);
777
  }
778
 
779
- .example-item.active {
780
  border-color: var(--accent-orange);
781
- background: rgba(255, 107, 53, 0.04);
782
  }
783
 
784
- .example-header {
785
- display: flex;
786
- justify-content: space-between;
787
- align-items: center;
788
- }
789
-
790
- .example-name {
791
  font-weight: 600;
792
- font-size: 0.9rem;
793
  color: #ffffff;
 
 
 
 
794
  }
795
 
796
- .example-badges {
797
  display: flex;
798
- gap: 6px;
 
799
  }
800
 
801
- .badge {
802
- font-size: 0.7rem;
803
  font-weight: 700;
804
- padding: 2px 6px;
805
  border-radius: 4px;
806
  text-transform: uppercase;
807
  }
808
 
809
- .badge-male { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
810
- .badge-female { background: rgba(236, 72, 153, 0.15); color: #f472b6; }
811
- .badge-long { background: rgba(139, 92, 246, 0.15); color: #a78bfa; }
812
 
813
- .example-preview {
814
- font-size: 0.8rem;
815
- color: var(--text-secondary);
816
- overflow: hidden;
817
- text-overflow: ellipsis;
818
- white-space: nowrap;
819
- }
820
-
821
- /* Prompt Writing Guide Styles */
822
- .guide-container {
823
- border-top: 1px solid rgba(255, 255, 255, 0.08);
824
- padding-top: 24px;
825
- display: flex;
826
- flex-direction: column;
827
- gap: 14px;
828
  }
829
 
830
  .guide-header {
831
- font-size: 0.95rem;
832
  font-weight: 600;
833
  color: #ffffff;
834
  cursor: pointer;
@@ -841,287 +896,302 @@
841
  .guide-body {
842
  max-height: 0;
843
  overflow: hidden;
844
- transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
845
- font-size: 0.85rem;
846
  color: var(--text-secondary);
847
  display: flex;
848
  flex-direction: column;
849
- gap: 12px;
850
  line-height: 1.5;
851
  }
852
 
853
  .guide-body.open {
854
- max-height: 400px;
855
  margin-top: 10px;
856
  }
857
 
858
- .guide-section-title {
859
  color: #ffffff;
860
  font-weight: 600;
861
  margin-bottom: 2px;
862
- font-size: 0.85rem;
863
  }
864
 
865
  .guide-list {
866
- padding-left: 18px;
867
  display: flex;
868
  flex-direction: column;
869
- gap: 4px;
 
 
 
 
 
 
 
 
 
870
  }
871
  </style>
872
  </head>
873
  <body>
874
 
875
- <header>
876
- <h1>🎭 DramaBox</h1>
877
- <div class="subtitle">Expressive TTS with Voice Cloning</div>
878
- <div class="ltx-banner">
879
- 🏗️&nbsp; Built on <a href="https://github.com/Lightricks/LTX-2" target="_blank">LTX-2</a> by
880
- <a href="https://huggingface.co/Lightricks" target="_blank">Lightricks</a>.
881
- <strong>DramaBox</strong> is <strong>Resemble AI's</strong> expressive TTS,
882
- trained on top of the LTX-2.3 audio branch under the LTX-2 Community License.
883
- Huge thanks to the Lightricks team for open-sourcing the base.
884
- </div>
885
- </header>
886
-
887
- <main>
888
- <!-- Left Panel: Form Inputs -->
889
- <section class="panel">
890
- <div class="panel-title">
891
- <span>🪄</span> Voice Generator
892
  </div>
893
-
894
- <!-- Scene Prompt Input -->
895
- <div class="form-group">
896
- <div class="form-label">
897
- <span>Scene Prompt</span>
898
- <span class="label-info">Put speech in "quotes", directions outside</span>
899
- </div>
900
- <textarea
901
- id="scene-prompt"
902
- class="textarea-custom"
903
- placeholder='A shadowy villain speaks with cold menace, "You have entered my domain, mortal." He chuckles darkly, "Such arrogance will be your undoing."'
904
- ></textarea>
905
  </div>
 
906
 
907
- <!-- Voice Reference Uploader -->
908
- <div class="form-group">
909
- <div class="form-label">
910
- <span>Voice Reference (Optional)</span>
911
- <span>10+ seconds recommended</span>
912
  </div>
913
-
914
- <div id="dropzone" class="upload-container">
915
- <span class="upload-icon">📤</span>
916
- <div class="upload-text">
917
- <strong>Click to upload</strong> or drag & drop<br>
918
- <span>Supports MP3, WAV, M4A, etc.</span>
919
  </div>
 
 
 
 
 
920
  </div>
921
- <input type="file" id="audio-file" class="hidden-input" accept="audio/*">
922
- </div>
923
 
924
- <!-- Inference Settings Accordion -->
925
- <div>
926
- <button type="button" class="accordion-btn" id="accordion-toggle">
927
- <span>⚙️ Advanced Settings</span>
928
- <span class="accordion-icon" id="accordion-arrow"></span>
929
- </button>
930
-
931
- <div class="accordion-content" id="accordion-panel">
932
- <!-- CFG Scale -->
933
- <div class="slider-group">
934
- <div class="slider-header">
935
- <span>CFG Scale</span>
936
- <span class="slider-val" id="val-cfg">2.5</span>
937
  </div>
938
- <input type="range" id="cfg" min="1.0" max="10.0" step="0.5" value="2.5">
939
  </div>
 
 
940
 
941
- <!-- STG Scale -->
942
- <div class="slider-group">
943
- <div class="slider-header">
944
- <span>STG Scale</span>
945
- <span class="slider-val" id="val-stg">1.5</span>
 
 
 
 
 
 
 
 
 
 
946
  </div>
947
- <input type="range" id="stg" min="0.0" max="5.0" step="0.5" value="1.5">
948
- </div>
949
 
950
- <!-- Duration Multiplier -->
951
- <div class="slider-group">
952
- <div class="slider-header">
953
- <span>Duration Multiplier</span>
954
- <span class="slider-val" id="val-dur">1.10</span>
 
 
955
  </div>
956
- <input type="range" id="dur" min="0.8" max="2.0" step="0.05" value="1.1">
957
- </div>
958
 
959
- <!-- Target Duration -->
960
- <div class="slider-group">
961
- <div class="slider-header">
962
- <span>Target Duration (s) — 0 = Auto</span>
963
- <span class="slider-val" id="val-gendur">0.0</span>
 
 
964
  </div>
965
- <input type="range" id="gendur" min="0.0" max="60.0" step="1.0" value="0.0">
966
- </div>
967
 
968
- <!-- Reference Duration -->
969
- <div class="slider-group">
970
- <div class="slider-header">
971
- <span>Reference Duration (s)</span>
972
- <span class="slider-val" id="val-refdur">10.0</span>
 
 
973
  </div>
974
- <input type="range" id="refdur" min="3.0" max="30.0" step="1.0" value="10.0">
975
- </div>
976
 
977
- <!-- Seed Input -->
978
- <div class="form-group">
979
- <span class="form-label">Generation Seed</span>
980
- <div class="seed-input-container">
981
- <input type="number" id="seed" class="input-number" value="42">
982
- <button type="button" id="btn-random-seed" class="btn-icon" title="Randomize Seed">🎲</button>
 
 
 
 
 
 
 
 
 
 
983
  </div>
984
  </div>
985
  </div>
986
- </div>
987
 
988
- <!-- Generate Button -->
989
- <button id="btn-generate" class="btn-primary">
990
- <span>⚡</span> Generate Speech
991
- </button>
992
- </section>
993
-
994
- <!-- Right Panel: Output & Examples -->
995
- <section class="panel">
996
- <div class="panel-title">
997
- <span>🔊</span> Output Room
998
- </div>
999
 
1000
- <!-- Status Indicator -->
1001
- <div id="status-box" class="status-container">
1002
- <div class="spinner"></div>
1003
- <span id="status-text">Connecting to DramaBox engine...</span>
1004
- </div>
1005
 
1006
- <!-- Output Container -->
1007
- <div class="output-card">
1008
- <audio id="audio-element" style="display:none;"></audio>
1009
-
1010
- <!-- Empty State -->
1011
- <div id="output-empty-state" class="output-empty">
1012
- <span class="output-empty-state-icon" style="font-size: 3rem;">🎛️</span>
1013
- <p>Enter a prompt and hit generate to voice your script</p>
1014
  </div>
1015
 
1016
- <!-- Custom Elegant Audio Player (Initially hidden) -->
1017
- <div id="custom-player" class="custom-player" style="display: none;">
1018
- <div class="visualizer-container">
1019
- <div class="visualizer-wave">
1020
- <span class="wave-bar"></span>
1021
- <span class="wave-bar"></span>
1022
- <span class="wave-bar"></span>
1023
- <span class="wave-bar"></span>
1024
- <span class="wave-bar"></span>
1025
- <span class="wave-bar"></span>
1026
- <span class="wave-bar"></span>
1027
- <span class="wave-bar"></span>
1028
- <span class="wave-bar"></span>
1029
- <span class="wave-bar"></span>
1030
- <span class="wave-bar"></span>
1031
- <span class="wave-bar"></span>
1032
- <span class="wave-bar"></span>
1033
- <span class="wave-bar"></span>
1034
- <span class="wave-bar"></span>
1035
- <span class="wave-bar"></span>
1036
- <span class="wave-bar"></span>
1037
- <span class="wave-bar"></span>
1038
- <span class="wave-bar"></span>
1039
- <span class="wave-bar"></span>
1040
- </div>
1041
- </div>
1042
 
1043
- <div class="player-controls">
1044
- <button type="button" id="player-play" class="play-btn">▶</button>
1045
- <div class="time-slider-container">
1046
- <span id="player-current-time" class="time-label">00:00</span>
1047
- <input type="range" id="player-progress" min="0" max="100" value="0">
1048
- <span id="player-duration" class="time-label">00:00</span>
1049
- </div>
1050
  </div>
1051
 
1052
- <div class="extra-player-controls">
1053
- <!-- Volume -->
1054
- <div class="volume-container">
1055
- <span class="volume-icon">🔊</span>
1056
- <input type="range" id="player-volume" min="0" max="1" step="0.1" value="0.8">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
  </div>
1058
-
1059
- <!-- Speed Controls -->
1060
- <div class="speed-control">
1061
- <button type="button" class="speed-btn" data-speed="0.8">0.8x</button>
1062
- <button type="button" class="speed-btn active" data-speed="1.0">1.0x</button>
1063
- <button type="button" class="speed-btn" data-speed="1.2">1.2x</button>
1064
- <button type="button" class="speed-btn" data-speed="1.5">1.5x</button>
 
1065
  </div>
1066
 
1067
- <!-- Download Link -->
1068
- <a id="player-download" class="btn-download" href="#" download="dramabox_voice.wav">
1069
- <span>📥</span> Download
1070
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1071
  </div>
1072
  </div>
1073
- </div>
1074
-
1075
- <!-- Example Section -->
1076
- <div class="examples-section">
1077
- <div class="examples-title">
1078
- <span>🎬</span> Quick Demos
1079
- </div>
1080
- <div class="examples-list" id="examples-container">
1081
- <!-- Loaded dynamically in JavaScript -->
1082
- </div>
1083
- </div>
1084
 
1085
- <!-- Prompt Writing Guide -->
1086
- <div class="guide-container">
1087
- <div class="guide-header" id="guide-toggle">
1088
- <span>📖 Prompt Writing Guide</span>
1089
- <span class="accordion-icon" id="guide-arrow">▼</span>
1090
- </div>
1091
-
1092
- <div class="guide-body" id="guide-body">
1093
- <div>
1094
- <div class="guide-section-title">Structure Pattern</div>
1095
- <p><code>&lt;speaker description&gt;, "&lt;dialogue&gt;" &lt;action&gt; "&lt;more dialogue&gt;"</code></p>
1096
  </div>
1097
- <div>
1098
- <div class="guide-section-title">Inside Double Quotes (Spoken)</div>
1099
- <ul class="guide-list">
1100
- <li>Standard speech dialogue: <code>"Hello, how are you today?"</code></li>
1101
- <li>Phonetic sounds and laughs: <code>"Hahaha"</code>, <code>"Hehehe"</code>, <code>"Mmmmm"</code>, <code>"Ugh"</code>, <code>"Argh"</code></li>
1102
- </ul>
1103
  </div>
1104
- <div>
1105
- <div class="guide-section-title">Outside Double Quotes (Stage Directions)</div>
1106
- <ul class="guide-list">
1107
- <li>Physical movements and breaths: <code>She sighs deeply.</code>, <code>He gulps nervously.</code>, <code>A long pause.</code></li>
1108
- <li>Voice adjustments: <code>Her voice cracks.</code>, <code>He clears his throat.</code></li>
1109
- </ul>
 
1110
  </div>
1111
- <div>
1112
- <div class="guide-section-title">Common Pitfalls</div>
1113
- <p>Avoid putting explicit words like <i>Sigh</i>, <i>Cough</i>, or <i>Gasp</i> inside quotes. The model will literally pronounce them. Put them outside quotes as directions!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  </div>
1115
  </div>
1116
- </div>
1117
- </section>
1118
- </main>
 
 
 
 
1119
 
1120
- <!-- Connect Gradio Client via Module Script -->
1121
  <script type="module">
1122
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
1123
 
1124
- // Examples Dataset
1125
  const EXAMPLES = [
1126
  {
1127
  name: "Villain monologue",
@@ -1217,7 +1287,7 @@
1217
  let selectedAudioFile = null;
1218
  let selectedAudioFilename = "";
1219
 
1220
- // Elements
1221
  const statusBox = document.getElementById("status-box");
1222
  const statusText = document.getElementById("status-text");
1223
  const btnGenerate = document.getElementById("btn-generate");
@@ -1226,12 +1296,12 @@
1226
  const dropzone = document.getElementById("dropzone");
1227
  const audioFileInput = document.getElementById("audio-file");
1228
 
1229
- // Advanced Accordion
1230
  const accordionToggle = document.getElementById("accordion-toggle");
1231
  const accordionPanel = document.getElementById("accordion-panel");
1232
  const accordionArrow = document.getElementById("accordion-arrow");
1233
 
1234
- // Sliders
1235
  const sliderCfg = document.getElementById("cfg");
1236
  const valCfg = document.getElementById("val-cfg");
1237
  const sliderStg = document.getElementById("stg");
@@ -1246,7 +1316,7 @@
1246
  const inputSeed = document.getElementById("seed");
1247
  const btnRandomSeed = document.getElementById("btn-random-seed");
1248
 
1249
- // Player Elements
1250
  const audioElement = document.getElementById("audio-element");
1251
  const outputEmptyState = document.getElementById("output-empty-state");
1252
  const customPlayer = document.getElementById("custom-player");
@@ -1256,21 +1326,21 @@
1256
  const playerDuration = document.getElementById("player-duration");
1257
  const playerVolume = document.getElementById("player-volume");
1258
  const playerDownload = document.getElementById("player-download");
1259
- const speedButtons = document.querySelectorAll(".speed-btn");
1260
 
1261
- // Guide Accordion
1262
  const guideToggle = document.getElementById("guide-toggle");
1263
  const guideBody = document.getElementById("guide-body");
1264
  const guideArrow = document.getElementById("guide-arrow");
1265
 
1266
- // Status Updater
1267
- function updateStatus(message, type = "info", showLoading = true) {
1268
  statusBox.style.display = "flex";
1269
- statusBox.className = `status-container ${type}`;
1270
  statusText.innerText = message;
1271
 
1272
- const spinner = statusBox.querySelector(".spinner");
1273
- if (showLoading) {
1274
  spinner.style.display = "block";
1275
  } else {
1276
  spinner.style.display = "none";
@@ -1281,20 +1351,20 @@
1281
  statusBox.style.display = "none";
1282
  }
1283
 
1284
- // Initialize Gradio JS Client
1285
  async function connectClient() {
1286
  try {
1287
- updateStatus("Connecting to DramaBox server...", "info");
1288
  client = await Client.connect(window.location.origin);
1289
- updateStatus("Engine connected and ready", "success", false);
1290
- setTimeout(hideStatus, 3000);
1291
  } catch (err) {
1292
  console.error(err);
1293
- updateStatus("Engine connection failed. Please reload.", "error", false);
1294
  }
1295
  }
1296
 
1297
- // Accordion Management
1298
  accordionToggle.addEventListener("click", () => {
1299
  const isOpen = accordionPanel.classList.toggle("open");
1300
  accordionArrow.innerText = isOpen ? "▲" : "▼";
@@ -1305,22 +1375,22 @@
1305
  guideArrow.innerText = isOpen ? "▲" : "▼";
1306
  });
1307
 
1308
- // Sliders Realtime Output updates
1309
  sliderCfg.addEventListener("input", (e) => valCfg.innerText = parseFloat(e.target.value).toFixed(1));
1310
  sliderStg.addEventListener("input", (e) => valStg.innerText = parseFloat(e.target.value).toFixed(1));
1311
  sliderDur.addEventListener("input", (e) => valDur.innerText = parseFloat(e.target.value).toFixed(2));
1312
  sliderGenDur.addEventListener("input", (e) => valGenDur.innerText = parseFloat(e.target.value).toFixed(1));
1313
  sliderRefDur.addEventListener("input", (e) => valRefDur.innerText = parseFloat(e.target.value).toFixed(1));
1314
 
1315
- // Seed Randomizer
1316
  btnRandomSeed.addEventListener("click", () => {
1317
- const randomSeed = Math.floor(Math.random() * 99999999);
1318
- inputSeed.value = randomSeed;
1319
  btnRandomSeed.style.transform = "rotate(360deg)";
1320
- setTimeout(() => btnRandomSeed.style.transform = "none", 400);
1321
  });
1322
 
1323
- // Uploader Handlers
1324
  dropzone.addEventListener("click", () => audioFileInput.click());
1325
 
1326
  dropzone.addEventListener("dragover", (e) => {
@@ -1355,12 +1425,12 @@
1355
 
1356
  function renderUploadedUI(filename) {
1357
  dropzone.innerHTML = `
1358
- <div class="uploaded-file-info">
1359
- <div class="uploaded-file-details">
1360
  <span>🎵</span>
1361
- <span style="word-break: break-all; text-align: left;">${filename}</span>
1362
  </div>
1363
- <button type="button" class="clear-upload" id="btn-clear-upload" title="Remove voice file">✕</button>
1364
  </div>
1365
  `;
1366
  document.getElementById("btn-clear-upload").addEventListener("click", (e) => {
@@ -1376,87 +1446,78 @@
1376
  dropzone.innerHTML = `
1377
  <span class="upload-icon">📤</span>
1378
  <div class="upload-text">
1379
- <strong>Click to upload</strong> or drag & drop<br>
1380
- <span>Supports MP3, WAV, M4A, etc.</span>
1381
  </div>
1382
  `;
1383
  }
1384
 
1385
- // Fetch Example voice and load it into file variable
1386
  async function loadExampleVoice(voicePath, originalFilename) {
1387
  try {
1388
- updateStatus("Cloning example voice reference...", "info");
1389
  const response = await fetch(voicePath);
1390
- if (!response.ok) throw new Error("Failed to fetch audio resource.");
1391
 
1392
  const blob = await response.blob();
1393
  selectedAudioFile = new File([blob], originalFilename, { type: blob.type || "audio/mpeg" });
1394
  selectedAudioFilename = originalFilename;
1395
 
1396
  renderUploadedUI(originalFilename);
1397
- updateStatus("Voice reference loaded successfully", "success", false);
1398
- setTimeout(hideStatus, 2000);
1399
  } catch (err) {
1400
  console.error(err);
1401
- updateStatus("Could not fetch the example voice reference.", "error", false);
1402
  }
1403
  }
1404
 
1405
- // Populate Examples in dynamic Grid
1406
  const examplesContainer = document.getElementById("examples-container");
1407
- EXAMPLES.forEach((ex, idx) => {
1408
- const item = document.createElement("div");
1409
- item.className = "example-item";
1410
 
1411
  const isLong = ex.name.startsWith("30s");
1412
  const badgeHtml = `
1413
- <span class="badge ${ex.gender === "male" ? "badge-male" : "badge-female"}">${ex.gender}</span>
1414
- ${isLong ? '<span class="badge badge-long">30s scenes</span>' : ''}
1415
  `;
1416
 
1417
- item.innerHTML = `
1418
- <div class="example-header">
1419
- <span class="example-name">${ex.name}</span>
1420
- <div class="example-badges">${badgeHtml}</div>
1421
- </div>
1422
- <div class="example-preview">${ex.prompt}</div>
1423
  `;
1424
 
1425
- item.addEventListener("click", () => {
1426
- // Highlight example
1427
- document.querySelectorAll(".example-item").forEach(el => el.classList.remove("active"));
1428
- item.classList.add("active");
1429
 
1430
- // Populate Fields
1431
  scenePrompt.value = ex.prompt;
1432
  sliderGenDur.value = ex.duration;
1433
  valGenDur.innerText = ex.duration.toFixed(1);
1434
 
1435
- // Extrapolate simple voice name
1436
  const filename = ex.voice.substring(ex.voice.lastIndexOf('/') + 1);
1437
  loadExampleVoice(ex.voice, filename);
1438
  });
1439
 
1440
- examplesContainer.appendChild(item);
1441
  });
1442
 
1443
- // HTML5 Audio Custom Player controls
1444
- let isAudioLoaded = false;
1445
 
1446
- function setupAudioElement(src) {
1447
  audioElement.src = src;
1448
  audioElement.load();
1449
- isAudioLoaded = true;
1450
 
1451
  outputEmptyState.style.display = "none";
1452
  customPlayer.style.display = "flex";
1453
 
1454
- // Set Player Initial state
1455
  playerPlay.innerText = "▶";
1456
  customPlayer.classList.remove("playing");
1457
  playerProgress.value = 0;
1458
-
1459
- // Download button URL update
1460
  playerDownload.href = src;
1461
  }
1462
 
@@ -1480,7 +1541,7 @@
1480
  });
1481
 
1482
  playerPlay.addEventListener("click", () => {
1483
- if (!isAudioLoaded) return;
1484
 
1485
  if (audioElement.paused) {
1486
  audioElement.play();
@@ -1494,7 +1555,7 @@
1494
  });
1495
 
1496
  playerProgress.addEventListener("input", (e) => {
1497
- if (!isAudioLoaded || !audioElement.duration) return;
1498
  const newTime = (e.target.value / 100) * audioElement.duration;
1499
  audioElement.currentTime = newTime;
1500
  });
@@ -1514,25 +1575,22 @@
1514
  function formatTime(secs) {
1515
  const minutes = Math.floor(secs / 60);
1516
  const seconds = Math.floor(secs % 60);
1517
- const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
1518
- const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
1519
- return `${returnedMinutes}:${returnedSeconds}`;
1520
  }
1521
 
1522
- // Action Trigger: Generator Click
1523
  btnGenerate.addEventListener("click", async () => {
1524
- const promptVal = scenePrompt.value.trim();
1525
- if (!promptVal) {
1526
- updateStatus("Please write a scene prompt first.", "error", false);
1527
  return;
1528
  }
1529
 
1530
  if (!client) {
1531
- updateStatus("DramaBox engine is still connecting, please wait.", "error", false);
1532
  return;
1533
  }
1534
 
1535
- // Gather values
1536
  const cfg = parseFloat(sliderCfg.value);
1537
  const stg = parseFloat(sliderStg.value);
1538
  const durMult = parseFloat(sliderDur.value);
@@ -1541,19 +1599,18 @@
1541
  const seed = parseInt(inputSeed.value);
1542
 
1543
  try {
1544
- // UI Disable State
1545
  btnGenerate.disabled = true;
1546
- btnGenerate.innerHTML = `<div class="spinner"></div> Generating Voice...`;
1547
- updateStatus("Preparing models & queue slot...", "info");
1548
 
1549
  let uploadedFileData = null;
1550
  if (selectedAudioFile) {
1551
  uploadedFileData = handle_file(selectedAudioFile);
1552
  }
1553
 
1554
- // Call Gradio queued api
1555
  const predictResponse = await client.predict("/generate_audio", {
1556
- prompt: promptVal,
1557
  audio_ref: uploadedFileData,
1558
  cfg: cfg,
1559
  stg: stg,
@@ -1565,9 +1622,9 @@
1565
 
1566
  if (predictResponse && predictResponse.data && predictResponse.data.length > 0) {
1567
  const audioUrl = predictResponse.data[0].url;
1568
- setupAudioElement(audioUrl);
1569
  updateStatus("Speech generation complete!", "success", false);
1570
- setTimeout(hideStatus, 3000);
1571
  } else {
1572
  throw new Error("No output returned from the generation server.");
1573
  }
@@ -1581,7 +1638,7 @@
1581
  }
1582
  });
1583
 
1584
- // Auto Connect on Load
1585
  connectClient();
1586
  </script>
1587
  </body>
 
7
 
8
  <!-- Meta tags for premium look and SEO -->
9
  <meta name="description" content="Generate highly expressive speech with voice cloning. Powered by LTX-2.3 and Resemble Perth watermarking.">
10
+ <meta name="theme-color" content="#0a0b10">
11
 
12
  <!-- Google Fonts: Outfit and Inter -->
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
 
16
 
17
  <style>
18
  :root {
19
+ --bg-color: #06070a;
20
+ --panel-bg: rgba(13, 15, 24, 0.7);
21
+ --panel-border: rgba(255, 255, 255, 0.05);
22
+ --text-primary: #f8fafc;
23
+ --text-secondary: #94a3b8;
24
  --accent-orange: #ff6b35;
25
+ --accent-orange-hover: #ff8554;
26
+ --accent-orange-glow: rgba(255, 107, 53, 0.25);
27
  --accent-purple: #8b5cf6;
28
+ --accent-purple-hover: #a78bfa;
29
+ --accent-purple-glow: rgba(139, 92, 246, 0.2);
30
  --accent-green: #10b981;
31
+ --radius-lg: 20px;
32
+ --radius-md: 12px;
33
+ --radius-sm: 8px;
34
+ --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
35
  }
36
 
37
  * {
 
45
  color: var(--text-primary);
46
  font-family: 'Inter', sans-serif;
47
  min-height: 100vh;
48
+ line-height: 1.5;
49
  overflow-x: hidden;
50
  position: relative;
51
+ display: flex;
52
+ flex-direction: column;
53
  }
54
 
55
+ /* Modern background radial glows */
56
  body::before {
57
  content: '';
58
  position: absolute;
59
+ top: -10%;
60
+ left: 5%;
61
+ width: 50%;
62
+ height: 50%;
63
+ background: radial-gradient(circle, var(--accent-orange-glow) 0%, transparent 60%);
64
  z-index: -1;
65
+ filter: blur(80px);
66
  pointer-events: none;
67
+ opacity: 0.8;
68
  }
69
 
70
  body::after {
71
  content: '';
72
  position: absolute;
73
+ bottom: 5%;
74
+ right: 5%;
75
+ width: 45%;
76
+ height: 45%;
77
+ background: radial-gradient(circle, var(--accent-purple-glow) 0%, transparent 60%);
78
  z-index: -1;
79
+ filter: blur(80px);
80
  pointer-events: none;
81
+ opacity: 0.6;
82
  }
83
 
84
+ .container {
85
+ width: 100%;
86
+ max-width: 1200px;
87
  margin: 0 auto;
88
+ padding: 20px;
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 24px;
92
+ }
93
+
94
+ header {
95
  display: flex;
96
  flex-direction: column;
97
  align-items: center;
98
  text-align: center;
99
+ padding: 20px 0 10px 0;
100
+ gap: 12px;
101
+ }
102
+
103
+ .logo-area {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 12px;
107
+ }
108
+
109
+ .logo-emoji {
110
+ font-size: 2.2rem;
111
+ animation: float 3s ease-in-out infinite;
112
+ }
113
+
114
+ @keyframes float {
115
+ 0%, 100% { transform: translateY(0); }
116
+ 50% { transform: translateY(-5px); }
117
  }
118
 
119
  h1 {
120
  font-family: 'Outfit', sans-serif;
121
+ font-size: 2.5rem;
122
  font-weight: 800;
123
+ background: linear-gradient(135deg, #ffffff 40%, #ffa580 80%, #cfa0f7 100%);
124
  -webkit-background-clip: text;
125
  -webkit-text-fill-color: transparent;
126
+ letter-spacing: -0.5px;
 
 
 
 
127
  }
128
 
129
  .subtitle {
130
+ font-size: 1rem;
131
  color: var(--text-secondary);
132
  font-weight: 400;
133
+ max-width: 500px;
 
134
  }
135
 
136
  .ltx-banner {
137
+ background: rgba(22, 28, 45, 0.4);
138
+ border: 1px solid var(--panel-border);
139
+ border-left: 3px solid var(--accent-orange);
140
  border-radius: var(--radius-md);
141
+ padding: 12px 18px;
142
+ color: #cbd5e1;
143
+ font-size: 0.85rem;
144
+ line-height: 1.6;
145
+ max-width: 800px;
 
 
 
 
146
  text-align: left;
147
+ backdrop-filter: blur(12px);
148
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
149
  }
150
 
151
  .ltx-banner a {
 
156
  }
157
 
158
  .ltx-banner a:hover {
 
 
 
 
 
159
  color: #ffffff;
160
+ text-decoration: underline;
161
  }
162
 
163
+ /* 2-Column Responsive Workspace */
164
  main {
 
 
 
165
  display: grid;
166
  grid-template-columns: 1.2fr 1fr;
167
+ gap: 24px;
168
+ align-items: start;
169
  }
170
 
171
+ @media (max-width: 960px) {
172
  main {
173
  grid-template-columns: 1fr;
174
  }
175
  h1 {
176
+ font-size: 2rem;
177
+ }
178
+ .logo-emoji {
179
+ font-size: 1.8rem;
180
  }
181
  }
182
 
183
+ @media (max-width: 576px) {
184
+ .container {
185
+ padding: 12px;
186
+ gap: 16px;
187
+ }
188
+ header {
189
+ padding: 10px 0;
190
+ }
191
+ .ltx-banner {
192
+ font-size: 0.8rem;
193
+ padding: 10px 14px;
194
+ }
195
+ }
196
+
197
+ .card {
198
  background: var(--panel-bg);
199
+ border: 1px solid var(--panel-border);
200
  border-radius: var(--radius-lg);
201
+ padding: 24px;
202
+ backdrop-filter: blur(20px);
203
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
204
  display: flex;
205
  flex-direction: column;
206
+ gap: 20px;
207
  transition: var(--transition);
208
  }
209
 
210
+ .card:hover {
211
+ border-color: rgba(255, 255, 255, 0.08);
212
  }
213
 
214
+ @media (max-width: 576px) {
215
+ .card {
216
+ padding: 18px;
217
+ gap: 16px;
218
+ }
219
+ }
220
+
221
+ .card-header {
222
  display: flex;
223
  align-items: center;
224
  gap: 10px;
225
+ font-family: 'Outfit', sans-serif;
226
+ font-size: 1.25rem;
227
+ font-weight: 600;
228
  color: #ffffff;
229
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
230
+ padding-bottom: 12px;
231
  }
232
 
233
  .form-group {
 
237
  }
238
 
239
  .form-label {
240
+ font-size: 0.8rem;
241
  font-weight: 600;
242
  text-transform: uppercase;
243
+ letter-spacing: 0.8px;
244
  color: var(--text-secondary);
245
  display: flex;
246
  justify-content: space-between;
247
+ align-items: center;
248
  }
249
 
250
+ .label-badge {
251
+ font-size: 0.75rem;
252
  color: var(--accent-orange);
253
  text-transform: none;
254
+ font-weight: 400;
255
+ letter-spacing: 0;
256
  }
257
 
258
  .textarea-custom {
259
  width: 100%;
260
+ background: rgba(10, 11, 18, 0.7);
261
+ border: 1px solid var(--panel-border);
262
  border-radius: var(--radius-md);
263
+ padding: 14px 16px;
264
  color: var(--text-primary);
265
  font-family: 'Inter', sans-serif;
266
  font-size: 0.95rem;
267
  line-height: 1.5;
268
  resize: vertical;
269
+ min-height: 120px;
270
  outline: none;
271
  transition: var(--transition);
272
  }
273
 
274
  .textarea-custom:focus {
275
  border-color: var(--accent-orange);
276
+ box-shadow: 0 0 12px var(--accent-orange-glow);
277
+ background: rgba(10, 11, 18, 0.9);
278
  }
279
 
280
+ /* Modernized Flat Upload Block */
281
+ .upload-zone {
282
+ border: 2px dashed rgba(255, 255, 255, 0.1);
283
  border-radius: var(--radius-md);
284
+ padding: 20px;
285
  text-align: center;
286
  cursor: pointer;
287
+ background: rgba(255, 255, 255, 0.01);
 
288
  display: flex;
289
  flex-direction: column;
290
  align-items: center;
291
+ gap: 8px;
292
+ transition: var(--transition);
293
+ min-height: 100px;
294
  justify-content: center;
 
 
295
  }
296
 
297
+ .upload-zone:hover, .upload-zone.dragover {
298
  border-color: var(--accent-purple);
299
+ background: rgba(139, 92, 246, 0.04);
300
  }
301
 
302
  .upload-icon {
303
+ font-size: 1.8rem;
304
  color: var(--text-secondary);
305
  transition: var(--transition);
306
  }
307
 
308
+ .upload-zone:hover .upload-icon {
309
+ transform: translateY(-2px);
310
  color: var(--accent-purple);
311
  }
312
 
313
  .upload-text {
314
+ font-size: 0.85rem;
315
  color: var(--text-secondary);
316
  }
317
 
318
  .upload-text strong {
319
+ color: #ffffff;
320
+ font-weight: 500;
321
  }
322
 
323
  .hidden-input {
324
  display: none;
325
  }
326
 
327
+ /* Styled Uploaded Capsule */
328
+ .file-capsule {
329
  display: flex;
330
  align-items: center;
331
+ justify-content: space-between;
332
+ background: rgba(139, 92, 246, 0.08);
333
  border: 1px solid rgba(139, 92, 246, 0.2);
334
+ padding: 10px 14px;
335
  border-radius: var(--radius-md);
336
  width: 100%;
 
337
  }
338
 
339
+ .file-info {
340
  display: flex;
341
  align-items: center;
342
  gap: 10px;
343
+ font-size: 0.85rem;
344
  font-weight: 500;
345
+ overflow: hidden;
346
+ white-space: nowrap;
347
+ text-overflow: ellipsis;
348
  }
349
 
350
+ .btn-clear {
351
  background: none;
352
  border: none;
353
  color: var(--text-secondary);
354
+ font-size: 1rem;
355
  cursor: pointer;
356
  transition: var(--transition);
357
  padding: 2px;
 
360
  justify-content: center;
361
  }
362
 
363
+ .btn-clear:hover {
364
  color: #ffffff;
365
  transform: scale(1.1);
366
  }
367
 
368
+ /* Modern Slider Accoridon Panels */
369
+ .settings-accordion {
370
+ background: rgba(255, 255, 255, 0.01);
371
+ border: 1px solid var(--panel-border);
372
  border-radius: var(--radius-md);
373
+ overflow: hidden;
374
+ }
375
+
376
+ .accordion-header {
377
+ background: none;
378
+ border: none;
379
+ padding: 14px 18px;
380
+ color: #ffffff;
381
  font-weight: 600;
382
  font-size: 0.9rem;
383
+ width: 100%;
384
  display: flex;
385
  justify-content: space-between;
386
  align-items: center;
387
  cursor: pointer;
 
 
388
  outline: none;
389
+ transition: var(--transition);
390
  }
391
 
392
+ .accordion-header:hover {
393
+ background: rgba(255, 255, 255, 0.03);
 
394
  }
395
 
396
  .accordion-icon {
397
+ font-size: 0.75rem;
398
  transition: var(--transition);
399
+ color: var(--text-secondary);
400
  }
401
 
402
+ .accordion-body {
403
  max-height: 0;
404
  overflow: hidden;
405
+ transition: max-height 0.3s ease-out;
406
+ padding: 0 18px;
407
  display: flex;
408
  flex-direction: column;
409
+ gap: 16px;
 
410
  }
411
 
412
+ .accordion-body.open {
413
+ max-height: 520px;
414
+ padding-bottom: 20px;
415
+ border-top: 1px solid rgba(255, 255, 255, 0.03);
416
+ padding-top: 16px;
417
  }
418
 
419
  .slider-group {
 
422
  gap: 6px;
423
  }
424
 
425
+ .slider-meta {
426
  display: flex;
427
  justify-content: space-between;
428
+ font-size: 0.8rem;
429
  color: var(--text-secondary);
430
  font-weight: 500;
431
  }
432
 
433
+ .slider-value {
434
+ color: #ffffff;
435
  font-weight: 700;
436
+ font-family: monospace;
437
  }
438
 
439
+ /* Custom Slider Tracks */
440
  input[type="range"] {
441
  -webkit-appearance: none;
442
  width: 100%;
443
+ height: 5px;
444
+ background: rgba(255, 255, 255, 0.06);
445
  border-radius: 3px;
446
  outline: none;
447
+ cursor: pointer;
448
  }
449
 
450
  input[type="range"]::-webkit-slider-thumb {
451
  -webkit-appearance: none;
452
+ width: 14px;
453
+ height: 14px;
454
  border-radius: 50%;
455
  background: var(--accent-orange);
456
+ box-shadow: 0 0 8px var(--accent-orange-glow);
 
457
  transition: var(--transition);
458
  }
459
 
 
462
  background: #ffffff;
463
  }
464
 
465
+ /* Seed Controls */
466
+ .seed-row {
467
  display: flex;
468
+ gap: 8px;
469
  align-items: center;
470
  }
471
 
472
+ .input-seed {
473
  flex: 1;
474
+ background: rgba(10, 11, 18, 0.7);
475
+ border: 1px solid var(--panel-border);
476
+ border-radius: var(--radius-sm);
477
+ padding: 10px 14px;
478
  color: var(--text-primary);
479
+ font-family: monospace;
480
  outline: none;
481
+ font-size: 0.9rem;
482
  transition: var(--transition);
483
+ height: 40px;
484
  }
485
 
486
+ .input-seed:focus {
487
  border-color: var(--accent-orange);
488
  }
489
 
490
+ .btn-seed-random {
491
+ background: rgba(255, 255, 255, 0.03);
492
+ border: 1px solid var(--panel-border);
493
+ border-radius: var(--radius-sm);
494
  color: var(--text-primary);
495
+ height: 40px;
496
+ width: 40px;
497
  display: flex;
498
  align-items: center;
499
  justify-content: center;
500
  cursor: pointer;
501
  transition: var(--transition);
502
+ font-size: 0.95rem;
503
  }
504
 
505
+ .btn-seed-random:hover {
506
+ background: rgba(255, 255, 255, 0.08);
507
+ border-color: rgba(255, 255, 255, 0.15);
508
  }
509
 
510
+ /* Premium Floating Generate Button */
511
+ .btn-generate {
512
  background: linear-gradient(135deg, var(--accent-orange) 0%, #ff8e53 100%);
513
  border: none;
514
  border-radius: var(--radius-md);
515
  color: #ffffff;
516
  font-family: 'Outfit', sans-serif;
517
+ font-size: 1.05rem;
518
  font-weight: 700;
519
+ padding: 14px;
520
  cursor: pointer;
521
  transition: var(--transition);
522
  display: flex;
523
  align-items: center;
524
  justify-content: center;
525
+ gap: 10px;
526
+ box-shadow: 0 6px 20px rgba(255, 107, 53, 0.2);
527
+ min-height: 48px;
 
 
528
  }
529
 
530
+ .btn-generate:hover:not(:disabled) {
531
+ transform: translateY(-1.5px);
532
+ box-shadow: 0 10px 25px rgba(255, 107, 53, 0.35);
 
533
  }
534
 
535
+ .btn-generate:active:not(:disabled) {
536
  transform: translateY(0);
537
  }
538
 
539
+ .btn-generate:disabled {
540
+ background: #1b1c26;
541
+ color: #4e526a;
542
  cursor: not-allowed;
543
  box-shadow: none;
544
  }
545
 
546
  /* Right Panel: Output & Controls */
547
+ .output-wrapper {
548
+ background: rgba(10, 11, 18, 0.5);
549
+ border: 1px solid var(--panel-border);
550
  border-radius: var(--radius-md);
551
+ padding: 20px;
552
  display: flex;
553
  flex-direction: column;
554
  align-items: center;
555
  justify-content: center;
556
+ min-height: 160px;
557
  position: relative;
 
558
  }
559
 
560
+ .empty-placeholder {
561
  color: var(--text-secondary);
562
  text-align: center;
563
  display: flex;
564
  flex-direction: column;
565
  align-items: center;
566
+ gap: 10px;
567
  }
568
 
569
+ .empty-icon {
570
+ font-size: 2.2rem;
571
+ opacity: 0.4;
572
  }
573
 
574
+ .empty-text {
575
+ font-size: 0.85rem;
576
+ }
577
+
578
+ /* Highly Responsive Audio Player Layout */
579
+ .audio-player {
580
  width: 100%;
581
  display: flex;
582
  flex-direction: column;
583
+ gap: 14px;
584
  }
585
 
586
+ .visualizer-box {
587
  width: 100%;
588
+ height: 50px;
589
+ background: linear-gradient(90deg, rgba(139, 92, 246, 0.04) 0%, rgba(255, 107, 53, 0.04) 100%);
590
+ border-radius: var(--radius-sm);
591
  display: flex;
592
  align-items: center;
593
  justify-content: center;
594
+ border: 1px solid rgba(255, 255, 255, 0.02);
 
595
  overflow: hidden;
596
  }
597
 
 
604
 
605
  .wave-bar {
606
  width: 3px;
607
+ height: 6px;
608
  background: var(--accent-purple);
609
  border-radius: 1px;
610
  transition: var(--transition);
611
  }
612
 
613
+ .audio-player.playing .wave-bar {
614
+ animation: playWave 1.2s infinite ease-in-out alternate;
615
  }
616
 
617
+ @keyframes playWave {
618
+ 0% { height: 6px; }
619
+ 100% { height: 32px; }
 
620
  }
621
 
622
  .wave-bar:nth-child(2n) { background: var(--accent-orange); animation-delay: 0.15s; }
 
624
  .wave-bar:nth-child(4n) { animation-delay: 0.45s; }
625
  .wave-bar:nth-child(5n) { background: var(--accent-purple); animation-delay: 0.6s; }
626
 
627
+ .player-row {
628
  display: flex;
629
  align-items: center;
630
+ gap: 12px;
631
  width: 100%;
632
  }
633
 
634
+ .btn-play {
635
  background: #ffffff;
636
  border: none;
637
  color: var(--bg-color);
638
+ width: 40px;
639
+ height: 40px;
640
  border-radius: 50%;
641
  display: flex;
642
  align-items: center;
643
  justify-content: center;
644
+ font-size: 1rem;
645
  cursor: pointer;
646
  transition: var(--transition);
647
+ box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15);
648
  flex-shrink: 0;
649
  }
650
 
651
+ .btn-play:hover {
652
  transform: scale(1.05);
653
+ box-shadow: 0 6px 18px rgba(255, 255, 255, 0.3);
654
  }
655
 
656
+ .progress-box {
657
  flex: 1;
658
  display: flex;
659
  align-items: center;
660
+ gap: 8px;
661
  }
662
 
663
+ .time-lbl {
664
+ font-size: 0.75rem;
665
  color: var(--text-secondary);
666
  font-family: monospace;
667
+ min-width: 32px;
668
  }
669
 
670
+ .deck-footer {
671
  display: flex;
672
  align-items: center;
673
  justify-content: space-between;
674
  width: 100%;
675
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
676
+ padding-top: 12px;
677
+ gap: 12px;
678
+ flex-wrap: wrap;
679
  }
680
 
681
+ .vol-slider {
682
  display: flex;
683
  align-items: center;
684
+ gap: 6px;
685
+ max-width: 100px;
686
+ flex-shrink: 0;
687
  }
688
 
689
+ .vol-icon {
690
+ font-size: 0.85rem;
691
  color: var(--text-secondary);
692
  }
693
 
694
+ .speed-deck {
695
  display: flex;
696
  align-items: center;
697
+ gap: 4px;
698
  }
699
 
700
+ .btn-speed {
701
+ background: rgba(255, 255, 255, 0.02);
702
+ border: 1px solid var(--panel-border);
703
  border-radius: 6px;
704
  color: var(--text-secondary);
705
+ font-size: 0.7rem;
706
  font-weight: 600;
707
+ padding: 3px 6px;
708
  cursor: pointer;
709
  transition: var(--transition);
710
  }
711
 
712
+ .btn-speed.active, .btn-speed:hover {
713
+ background: rgba(255, 255, 255, 0.08);
714
  color: #ffffff;
715
+ border-color: rgba(255, 255, 255, 0.2);
716
  }
717
 
718
+ .btn-download-wav {
719
+ background: rgba(255, 255, 255, 0.04);
720
+ border: 1px solid var(--panel-border);
721
+ border-radius: var(--radius-sm);
722
  color: #ffffff;
723
+ font-size: 0.8rem;
724
  font-weight: 600;
725
+ padding: 6px 12px;
726
  text-decoration: none;
727
  display: flex;
728
  align-items: center;
729
+ gap: 6px;
730
  transition: var(--transition);
731
  }
732
 
733
+ .btn-download-wav:hover {
734
  background: #ffffff;
735
  color: var(--bg-color);
736
  transform: translateY(-1px);
737
  }
738
 
739
+ @media (max-width: 480px) {
740
+ .deck-footer {
741
+ flex-direction: column;
742
+ align-items: stretch;
743
+ gap: 12px;
744
+ }
745
+ .vol-slider {
746
+ max-width: 100%;
747
+ }
748
+ .speed-deck {
749
+ justify-content: space-between;
750
+ }
751
+ .btn-download-wav {
752
+ justify-content: center;
753
+ }
754
+ }
755
+
756
+ /* Elegant Alert & Queues */
757
+ .status-alert {
758
  display: flex;
759
  align-items: center;
760
+ gap: 10px;
761
+ font-size: 0.85rem;
762
  font-weight: 500;
763
+ padding: 10px 14px;
764
  border-radius: var(--radius-md);
765
  border: 1px solid transparent;
766
+ display: none;
767
  }
768
 
769
+ .status-alert.success {
770
+ background: rgba(16, 185, 129, 0.06);
771
+ border-color: rgba(16, 185, 129, 0.15);
772
  color: var(--accent-green);
773
  }
774
 
775
+ .status-alert.info {
776
+ background: rgba(139, 92, 246, 0.06);
777
+ border-color: rgba(139, 92, 246, 0.15);
778
  color: #a78bfa;
779
  }
780
 
781
+ .status-alert.error {
782
+ background: rgba(239, 68, 68, 0.06);
783
+ border-color: rgba(239, 68, 68, 0.15);
784
  color: #f87171;
785
  }
786
 
787
+ .alert-spinner {
788
+ width: 14px;
789
+ height: 14px;
790
  border: 2px solid rgba(255, 255, 255, 0.1);
791
  border-top: 2px solid currentColor;
792
  border-radius: 50%;
793
  animation: spin 0.8s linear infinite;
794
  }
795
 
796
+ /* Clean Selector Capsules for Quick Demos */
797
+ .demo-deck {
 
 
 
 
 
798
  display: flex;
799
  flex-direction: column;
800
+ gap: 10px;
 
 
 
 
 
 
 
 
 
 
801
  }
802
 
803
+ .demo-capsules {
804
  display: flex;
805
  flex-direction: column;
806
+ gap: 8px;
807
+ max-height: 240px;
808
  overflow-y: auto;
809
+ padding-right: 2px;
810
  }
811
 
812
+ .demo-capsules::-webkit-scrollbar {
813
+ width: 4px;
 
814
  }
815
 
816
+ .demo-capsules::-webkit-scrollbar-track {
817
+ background: rgba(255, 255, 255, 0.01);
818
+ border-radius: 2px;
819
  }
820
 
821
+ .demo-capsules::-webkit-scrollbar-thumb {
822
+ background: rgba(255, 255, 255, 0.08);
823
+ border-radius: 2px;
824
  }
825
 
826
+ .demo-pill {
827
+ background: rgba(255, 255, 255, 0.01);
828
+ border: 1px solid var(--panel-border);
829
  border-radius: var(--radius-md);
830
+ padding: 10px 14px;
831
  cursor: pointer;
832
  transition: var(--transition);
833
  display: flex;
834
+ justify-content: space-between;
835
+ align-items: center;
836
+ gap: 12px;
837
  text-align: left;
838
  }
839
 
840
+ .demo-pill:hover {
841
+ background: rgba(255, 255, 255, 0.03);
842
+ border-color: rgba(255, 255, 255, 0.12);
843
+ transform: translateX(1px);
844
  }
845
 
846
+ .demo-pill.active {
847
  border-color: var(--accent-orange);
848
+ background: rgba(255, 107, 53, 0.03);
849
  }
850
 
851
+ .demo-pill-title {
852
+ font-size: 0.85rem;
 
 
 
 
 
853
  font-weight: 600;
 
854
  color: #ffffff;
855
+ white-space: nowrap;
856
+ overflow: hidden;
857
+ text-overflow: ellipsis;
858
+ max-width: 180px;
859
  }
860
 
861
+ .pill-labels {
862
  display: flex;
863
+ gap: 4px;
864
+ flex-shrink: 0;
865
  }
866
 
867
+ .pill-badge {
868
+ font-size: 0.65rem;
869
  font-weight: 700;
870
+ padding: 1px 5px;
871
  border-radius: 4px;
872
  text-transform: uppercase;
873
  }
874
 
875
+ .pill-badge-male { background: rgba(59, 130, 246, 0.1); color: #60a5fa; }
876
+ .pill-badge-female { background: rgba(236, 72, 153, 0.1); color: #f472b6; }
877
+ .pill-badge-long { background: rgba(139, 92, 246, 0.1); color: #c084fc; }
878
 
879
+ /* Minimalist Accordion Guide */
880
+ .guide-deck {
881
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
882
+ padding-top: 16px;
 
 
 
 
 
 
 
 
 
 
 
883
  }
884
 
885
  .guide-header {
886
+ font-size: 0.85rem;
887
  font-weight: 600;
888
  color: #ffffff;
889
  cursor: pointer;
 
896
  .guide-body {
897
  max-height: 0;
898
  overflow: hidden;
899
+ transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
900
+ font-size: 0.8rem;
901
  color: var(--text-secondary);
902
  display: flex;
903
  flex-direction: column;
904
+ gap: 10px;
905
  line-height: 1.5;
906
  }
907
 
908
  .guide-body.open {
909
+ max-height: 380px;
910
  margin-top: 10px;
911
  }
912
 
913
+ .guide-block-title {
914
  color: #ffffff;
915
  font-weight: 600;
916
  margin-bottom: 2px;
917
+ font-size: 0.8rem;
918
  }
919
 
920
  .guide-list {
921
+ padding-left: 14px;
922
  display: flex;
923
  flex-direction: column;
924
+ gap: 3px;
925
+ }
926
+
927
+ footer {
928
+ margin-top: auto;
929
+ text-align: center;
930
+ padding: 30px 20px;
931
+ font-size: 0.75rem;
932
+ color: var(--text-secondary);
933
+ border-top: 1px solid rgba(255, 255, 255, 0.03);
934
  }
935
  </style>
936
  </head>
937
  <body>
938
 
939
+ <div class="container">
940
+ <header>
941
+ <div class="logo-area">
942
+ <span class="logo-emoji">🎭</span>
943
+ <h1>DramaBox</h1>
 
 
 
 
 
 
 
 
 
 
 
 
944
  </div>
945
+ <div class="subtitle">Expressive TTS with Voice Cloning</div>
946
+
947
+ <div class="ltx-banner">
948
+ 🏗️&nbsp; Built on <a href="https://github.com/Lightricks/LTX-2" target="_blank">LTX-2</a> by
949
+ <a href="https://huggingface.co/Lightricks" target="_blank">Lightricks</a>.
950
+ <strong>DramaBox</strong> is <strong>Resemble AI's</strong> expressive TTS,
951
+ trained on top of the LTX-2.3 audio branch under the LTX-2 Community License.
952
+ Huge thanks to the Lightricks team for open-sourcing the base.
 
 
 
 
953
  </div>
954
+ </header>
955
 
956
+ <main>
957
+ <!-- Left Side: Custom Configuration Card -->
958
+ <section class="card">
959
+ <div class="card-header">
960
+ <span>🪄</span> Input Board
961
  </div>
962
+
963
+ <!-- Textarea Script -->
964
+ <div class="form-group">
965
+ <div class="form-label">
966
+ <span>Script Prompt</span>
967
+ <span class="label-badge">Double quotes for dialogue, standard text for style</span>
968
  </div>
969
+ <textarea
970
+ id="scene-prompt"
971
+ class="textarea-custom"
972
+ placeholder='A shadowy villain speaks with cold menace, "You have entered my domain, mortal." He chuckles darkly, "Such arrogance will be your undoing."'
973
+ ></textarea>
974
  </div>
 
 
975
 
976
+ <!-- Modern Reference File Uploader -->
977
+ <div class="form-group">
978
+ <div class="form-label">
979
+ <span>Timbre Reference (Optional)</span>
980
+ <span>10s+ file</span>
981
+ </div>
982
+
983
+ <div id="dropzone" class="upload-zone">
984
+ <span class="upload-icon">📤</span>
985
+ <div class="upload-text">
986
+ <strong>Drop file</strong> or click to choose<br>
987
+ <span>Supports WAV, MP3, etc.</span>
 
988
  </div>
 
989
  </div>
990
+ <input type="file" id="audio-file" class="hidden-input" accept="audio/*">
991
+ </div>
992
 
993
+ <!-- Dynamic Accordion Panel -->
994
+ <div class="settings-accordion">
995
+ <button type="button" class="accordion-header" id="accordion-toggle">
996
+ <span>⚙️ Settings Dashboard</span>
997
+ <span class="accordion-icon" id="accordion-arrow"></span>
998
+ </button>
999
+
1000
+ <div class="accordion-body" id="accordion-panel">
1001
+ <!-- CFG Scale -->
1002
+ <div class="slider-group">
1003
+ <div class="slider-meta">
1004
+ <span>CFG Scale</span>
1005
+ <span class="slider-value" id="val-cfg">2.5</span>
1006
+ </div>
1007
+ <input type="range" id="cfg" min="1.0" max="10.0" step="0.5" value="2.5">
1008
  </div>
 
 
1009
 
1010
+ <!-- STG Scale -->
1011
+ <div class="slider-group">
1012
+ <div class="slider-meta">
1013
+ <span>STG Scale</span>
1014
+ <span class="slider-value" id="val-stg">1.5</span>
1015
+ </div>
1016
+ <input type="range" id="stg" min="0.0" max="5.0" step="0.5" value="1.5">
1017
  </div>
 
 
1018
 
1019
+ <!-- Duration factor -->
1020
+ <div class="slider-group">
1021
+ <div class="slider-meta">
1022
+ <span>Breathing Room Factor</span>
1023
+ <span class="slider-value" id="val-dur">1.10</span>
1024
+ </div>
1025
+ <input type="range" id="dur" min="0.8" max="2.0" step="0.05" value="1.1">
1026
  </div>
 
 
1027
 
1028
+ <!-- Target Length -->
1029
+ <div class="slider-group">
1030
+ <div class="slider-meta">
1031
+ <span>Fixed duration (s) - 0 = Auto</span>
1032
+ <span class="slider-value" id="val-gendur">0.0</span>
1033
+ </div>
1034
+ <input type="range" id="gendur" min="0.0" max="60.0" step="1.0" value="0.0">
1035
  </div>
 
 
1036
 
1037
+ <!-- Reference window -->
1038
+ <div class="slider-group">
1039
+ <div class="slider-meta">
1040
+ <span>Reference window (s)</span>
1041
+ <span class="slider-value" id="val-refdur">10.0</span>
1042
+ </div>
1043
+ <input type="range" id="refdur" min="3.0" max="30.0" step="1.0" value="10.0">
1044
+ </div>
1045
+
1046
+ <!-- Seed Input -->
1047
+ <div class="form-group">
1048
+ <span class="form-label">Seed Value</span>
1049
+ <div class="seed-row">
1050
+ <input type="number" id="seed" class="input-seed" value="42">
1051
+ <button type="button" id="btn-random-seed" class="btn-seed-random" title="Randomize Seed">🎲</button>
1052
+ </div>
1053
  </div>
1054
  </div>
1055
  </div>
 
1056
 
1057
+ <!-- Generate Click -->
1058
+ <button id="btn-generate" class="btn-generate">
1059
+ <span>⚡</span> Generate Speech
1060
+ </button>
1061
+ </section>
 
 
 
 
 
 
1062
 
1063
+ <!-- Right Side: Status, Player, Demos & Guides -->
1064
+ <section class="card">
1065
+ <div class="card-header">
1066
+ <span>🔊</span> Output Lounge
1067
+ </div>
1068
 
1069
+ <!-- State alerts -->
1070
+ <div id="status-box" class="status-alert">
1071
+ <div class="alert-spinner"></div>
1072
+ <span id="status-text">Synchronizing Engine...</span>
 
 
 
 
1073
  </div>
1074
 
1075
+ <!-- Audio visualizer & controller -->
1076
+ <div class="output-wrapper">
1077
+ <audio id="audio-element" style="display:none;"></audio>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
+ <!-- Default Screen -->
1080
+ <div id="output-empty-state" class="empty-placeholder">
1081
+ <span class="empty-icon">🎚️</span>
1082
+ <span class="empty-text">Formulate a script above to voice speech</span>
 
 
 
1083
  </div>
1084
 
1085
+ <!-- Audio Player Deck -->
1086
+ <div id="custom-player" class="audio-player" style="display: none;">
1087
+ <div class="visualizer-box">
1088
+ <div class="visualizer-wave">
1089
+ <span class="wave-bar"></span>
1090
+ <span class="wave-bar"></span>
1091
+ <span class="wave-bar"></span>
1092
+ <span class="wave-bar"></span>
1093
+ <span class="wave-bar"></span>
1094
+ <span class="wave-bar"></span>
1095
+ <span class="wave-bar"></span>
1096
+ <span class="wave-bar"></span>
1097
+ <span class="wave-bar"></span>
1098
+ <span class="wave-bar"></span>
1099
+ <span class="wave-bar"></span>
1100
+ <span class="wave-bar"></span>
1101
+ <span class="wave-bar"></span>
1102
+ <span class="wave-bar"></span>
1103
+ <span class="wave-bar"></span>
1104
+ <span class="wave-bar"></span>
1105
+ <span class="wave-bar"></span>
1106
+ <span class="wave-bar"></span>
1107
+ <span class="wave-bar"></span>
1108
+ <span class="wave-bar"></span>
1109
+ </div>
1110
  </div>
1111
+
1112
+ <div class="player-row">
1113
+ <button type="button" id="player-play" class="btn-play">▶</button>
1114
+ <div class="progress-box">
1115
+ <span id="player-current-time" class="time-lbl">00:00</span>
1116
+ <input type="range" id="player-progress" min="0" max="100" value="0">
1117
+ <span id="player-duration" class="time-lbl">00:00</span>
1118
+ </div>
1119
  </div>
1120
 
1121
+ <div class="deck-footer">
1122
+ <!-- Volume slider -->
1123
+ <div class="vol-slider">
1124
+ <span class="vol-icon">🔊</span>
1125
+ <input type="range" id="player-volume" min="0" max="1" step="0.1" value="0.8">
1126
+ </div>
1127
+
1128
+ <!-- Speeds selector -->
1129
+ <div class="speed-deck">
1130
+ <button type="button" class="btn-speed" data-speed="0.8">0.8x</button>
1131
+ <button type="button" class="btn-speed active" data-speed="1.0">1.0x</button>
1132
+ <button type="button" class="btn-speed" data-speed="1.2">1.2x</button>
1133
+ <button type="button" class="btn-speed" data-speed="1.5">1.5x</button>
1134
+ </div>
1135
+
1136
+ <!-- Direct WAV file downloading -->
1137
+ <a id="player-download" class="btn-download-wav" href="#" download="dramabox_audio.wav">
1138
+ <span>📥</span> Download
1139
+ </a>
1140
+ </div>
1141
  </div>
1142
  </div>
 
 
 
 
 
 
 
 
 
 
 
1143
 
1144
+ <!-- Custom Selector Capsules for Quick Demos -->
1145
+ <div class="demo-deck">
1146
+ <div class="form-label" style="margin-bottom: 2px;">
1147
+ <span>Quick Demos</span>
1148
+ <span>Click to pre-populate</span>
 
 
 
 
 
 
1149
  </div>
1150
+ <div class="demo-capsules" id="examples-container">
1151
+ <!-- Loaded dynamically -->
 
 
 
 
1152
  </div>
1153
+ </div>
1154
+
1155
+ <!-- Minimalist Guide Accordion -->
1156
+ <div class="guide-deck">
1157
+ <div class="guide-header" id="guide-toggle">
1158
+ <span>📖 Prompt Structure Guide</span>
1159
+ <span class="accordion-icon" id="guide-arrow">▼</span>
1160
  </div>
1161
+
1162
+ <div class="guide-body" id="guide-body">
1163
+ <div>
1164
+ <div class="guide-block-title">Format Structure</div>
1165
+ <p><code>&lt;description&gt;, "&lt;dialogue&gt;" &lt;movement/breath&gt; "&lt;more dialogue&gt;"</code></p>
1166
+ </div>
1167
+ <div>
1168
+ <div class="guide-block-title">Inside Quotes (Model pronounces)</div>
1169
+ <ul class="guide-list">
1170
+ <li>Spoken transcript: <code>"We have achieved full takeoff."</code></li>
1171
+ <li>Phonetic expressions: <code>"Hahaha"</code>, <code>"Mmmm"</code>, <code>"Ugh"</code>, <code>"Argh"</code></li>
1172
+ </ul>
1173
+ </div>
1174
+ <div>
1175
+ <div class="guide-block-title">Outside Quotes (Stage Directions)</div>
1176
+ <ul class="guide-list">
1177
+ <li>Pacing/acting styles: <code>She sighs deeply.</code>, <code>A long pause.</code></li>
1178
+ <li>Tonal delivery: <code>His voice cracks.</code>, <code>He clears his throat.</code></li>
1179
+ </ul>
1180
+ </div>
1181
  </div>
1182
  </div>
1183
+ </section>
1184
+ </main>
1185
+ </div>
1186
+
1187
+ <footer>
1188
+ &copy; 2026 DramaBox. Generative speech outputs are invisibly watermarked with Resemble Perth.
1189
+ </footer>
1190
 
1191
+ <!-- Gradio Connection Module -->
1192
  <script type="module">
1193
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
1194
 
 
1195
  const EXAMPLES = [
1196
  {
1197
  name: "Villain monologue",
 
1287
  let selectedAudioFile = null;
1288
  let selectedAudioFilename = "";
1289
 
1290
+ // UI nodes
1291
  const statusBox = document.getElementById("status-box");
1292
  const statusText = document.getElementById("status-text");
1293
  const btnGenerate = document.getElementById("btn-generate");
 
1296
  const dropzone = document.getElementById("dropzone");
1297
  const audioFileInput = document.getElementById("audio-file");
1298
 
1299
+ // Accordion Nodes
1300
  const accordionToggle = document.getElementById("accordion-toggle");
1301
  const accordionPanel = document.getElementById("accordion-panel");
1302
  const accordionArrow = document.getElementById("accordion-arrow");
1303
 
1304
+ // Settings sliders
1305
  const sliderCfg = document.getElementById("cfg");
1306
  const valCfg = document.getElementById("val-cfg");
1307
  const sliderStg = document.getElementById("stg");
 
1316
  const inputSeed = document.getElementById("seed");
1317
  const btnRandomSeed = document.getElementById("btn-random-seed");
1318
 
1319
+ // Player elements
1320
  const audioElement = document.getElementById("audio-element");
1321
  const outputEmptyState = document.getElementById("output-empty-state");
1322
  const customPlayer = document.getElementById("custom-player");
 
1326
  const playerDuration = document.getElementById("player-duration");
1327
  const playerVolume = document.getElementById("player-volume");
1328
  const playerDownload = document.getElementById("player-download");
1329
+ const speedButtons = document.querySelectorAll(".btn-speed");
1330
 
1331
+ // Guide togglers
1332
  const guideToggle = document.getElementById("guide-toggle");
1333
  const guideBody = document.getElementById("guide-body");
1334
  const guideArrow = document.getElementById("guide-arrow");
1335
 
1336
+ // Simple alert helper
1337
+ function updateStatus(message, type = "info", showSpinner = true) {
1338
  statusBox.style.display = "flex";
1339
+ statusBox.className = `status-alert ${type}`;
1340
  statusText.innerText = message;
1341
 
1342
+ const spinner = statusBox.querySelector(".alert-spinner");
1343
+ if (showSpinner) {
1344
  spinner.style.display = "block";
1345
  } else {
1346
  spinner.style.display = "none";
 
1351
  statusBox.style.display = "none";
1352
  }
1353
 
1354
+ // Establish Gradio Connection
1355
  async function connectClient() {
1356
  try {
1357
+ updateStatus("Connecting to DramaBox API...", "info");
1358
  client = await Client.connect(window.location.origin);
1359
+ updateStatus("Engine online and ready", "success", false);
1360
+ setTimeout(hideStatus, 2500);
1361
  } catch (err) {
1362
  console.error(err);
1363
+ updateStatus("API synchronization failed. Please refresh.", "error", false);
1364
  }
1365
  }
1366
 
1367
+ // Accordion animations
1368
  accordionToggle.addEventListener("click", () => {
1369
  const isOpen = accordionPanel.classList.toggle("open");
1370
  accordionArrow.innerText = isOpen ? "▲" : "▼";
 
1375
  guideArrow.innerText = isOpen ? "▲" : "▼";
1376
  });
1377
 
1378
+ // Sliders updates
1379
  sliderCfg.addEventListener("input", (e) => valCfg.innerText = parseFloat(e.target.value).toFixed(1));
1380
  sliderStg.addEventListener("input", (e) => valStg.innerText = parseFloat(e.target.value).toFixed(1));
1381
  sliderDur.addEventListener("input", (e) => valDur.innerText = parseFloat(e.target.value).toFixed(2));
1382
  sliderGenDur.addEventListener("input", (e) => valGenDur.innerText = parseFloat(e.target.value).toFixed(1));
1383
  sliderRefDur.addEventListener("input", (e) => valRefDur.innerText = parseFloat(e.target.value).toFixed(1));
1384
 
1385
+ // Seed randomizer
1386
  btnRandomSeed.addEventListener("click", () => {
1387
+ const randomVal = Math.floor(Math.random() * 99999999);
1388
+ inputSeed.value = randomVal;
1389
  btnRandomSeed.style.transform = "rotate(360deg)";
1390
+ setTimeout(() => btnRandomSeed.style.transform = "none", 300);
1391
  });
1392
 
1393
+ // Upload triggers
1394
  dropzone.addEventListener("click", () => audioFileInput.click());
1395
 
1396
  dropzone.addEventListener("dragover", (e) => {
 
1425
 
1426
  function renderUploadedUI(filename) {
1427
  dropzone.innerHTML = `
1428
+ <div class="file-capsule">
1429
+ <div class="file-info">
1430
  <span>🎵</span>
1431
+ <span style="overflow: hidden; text-overflow: ellipsis;">${filename}</span>
1432
  </div>
1433
+ <button type="button" class="btn-clear" id="btn-clear-upload" title="Remove voice file">✕</button>
1434
  </div>
1435
  `;
1436
  document.getElementById("btn-clear-upload").addEventListener("click", (e) => {
 
1446
  dropzone.innerHTML = `
1447
  <span class="upload-icon">📤</span>
1448
  <div class="upload-text">
1449
+ <strong>Drop file</strong> or click to choose<br>
1450
+ <span>Supports WAV, MP3, etc.</span>
1451
  </div>
1452
  `;
1453
  }
1454
 
1455
+ // Fetch preloaded voices as File
1456
  async function loadExampleVoice(voicePath, originalFilename) {
1457
  try {
1458
+ updateStatus("Buffering reference voice...", "info");
1459
  const response = await fetch(voicePath);
1460
+ if (!response.ok) throw new Error("Could not fetch remote resource.");
1461
 
1462
  const blob = await response.blob();
1463
  selectedAudioFile = new File([blob], originalFilename, { type: blob.type || "audio/mpeg" });
1464
  selectedAudioFilename = originalFilename;
1465
 
1466
  renderUploadedUI(originalFilename);
1467
+ updateStatus("Reference voice mapped", "success", false);
1468
+ setTimeout(hideStatus, 1500);
1469
  } catch (err) {
1470
  console.error(err);
1471
+ updateStatus("Unable to map the voice example.", "error", false);
1472
  }
1473
  }
1474
 
1475
+ // Dynamically build Example Capsules
1476
  const examplesContainer = document.getElementById("examples-container");
1477
+ EXAMPLES.forEach((ex) => {
1478
+ const pill = document.createElement("div");
1479
+ pill.className = "demo-pill";
1480
 
1481
  const isLong = ex.name.startsWith("30s");
1482
  const badgeHtml = `
1483
+ <span class="pill-badge ${ex.gender === "male" ? "pill-badge-male" : "pill-badge-female"}">${ex.gender}</span>
1484
+ ${isLong ? '<span class="pill-badge pill-badge-long">30s</span>' : ''}
1485
  `;
1486
 
1487
+ pill.innerHTML = `
1488
+ <span class="demo-pill-title">${ex.name}</span>
1489
+ <div class="pill-labels">${badgeHtml}</div>
 
 
 
1490
  `;
1491
 
1492
+ pill.addEventListener("click", () => {
1493
+ document.querySelectorAll(".demo-pill").forEach(el => el.classList.remove("active"));
1494
+ pill.classList.add("active");
 
1495
 
 
1496
  scenePrompt.value = ex.prompt;
1497
  sliderGenDur.value = ex.duration;
1498
  valGenDur.innerText = ex.duration.toFixed(1);
1499
 
 
1500
  const filename = ex.voice.substring(ex.voice.lastIndexOf('/') + 1);
1501
  loadExampleVoice(ex.voice, filename);
1502
  });
1503
 
1504
+ examplesContainer.appendChild(pill);
1505
  });
1506
 
1507
+ // Custom HTML5 Audio controllers
1508
+ let isAudioReady = false;
1509
 
1510
+ function setupAudioSource(src) {
1511
  audioElement.src = src;
1512
  audioElement.load();
1513
+ isAudioReady = true;
1514
 
1515
  outputEmptyState.style.display = "none";
1516
  customPlayer.style.display = "flex";
1517
 
 
1518
  playerPlay.innerText = "▶";
1519
  customPlayer.classList.remove("playing");
1520
  playerProgress.value = 0;
 
 
1521
  playerDownload.href = src;
1522
  }
1523
 
 
1541
  });
1542
 
1543
  playerPlay.addEventListener("click", () => {
1544
+ if (!isAudioReady) return;
1545
 
1546
  if (audioElement.paused) {
1547
  audioElement.play();
 
1555
  });
1556
 
1557
  playerProgress.addEventListener("input", (e) => {
1558
+ if (!isAudioReady || !audioElement.duration) return;
1559
  const newTime = (e.target.value / 100) * audioElement.duration;
1560
  audioElement.currentTime = newTime;
1561
  });
 
1575
  function formatTime(secs) {
1576
  const minutes = Math.floor(secs / 60);
1577
  const seconds = Math.floor(secs % 60);
1578
+ return `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
 
 
1579
  }
1580
 
1581
+ // Process Speech Trigger
1582
  btnGenerate.addEventListener("click", async () => {
1583
+ const prompt = scenePrompt.value.trim();
1584
+ if (!prompt) {
1585
+ updateStatus("Please enter a script prompt.", "error", false);
1586
  return;
1587
  }
1588
 
1589
  if (!client) {
1590
+ updateStatus("DramaBox is still configuring, please wait.", "error", false);
1591
  return;
1592
  }
1593
 
 
1594
  const cfg = parseFloat(sliderCfg.value);
1595
  const stg = parseFloat(sliderStg.value);
1596
  const durMult = parseFloat(sliderDur.value);
 
1599
  const seed = parseInt(inputSeed.value);
1600
 
1601
  try {
 
1602
  btnGenerate.disabled = true;
1603
+ btnGenerate.innerHTML = `<div class="alert-spinner"></div> Voice Synthesis...`;
1604
+ updateStatus("Checking models & processing queues...", "info");
1605
 
1606
  let uploadedFileData = null;
1607
  if (selectedAudioFile) {
1608
  uploadedFileData = handle_file(selectedAudioFile);
1609
  }
1610
 
1611
+ // Execute Gradio Client request
1612
  const predictResponse = await client.predict("/generate_audio", {
1613
+ prompt: prompt,
1614
  audio_ref: uploadedFileData,
1615
  cfg: cfg,
1616
  stg: stg,
 
1622
 
1623
  if (predictResponse && predictResponse.data && predictResponse.data.length > 0) {
1624
  const audioUrl = predictResponse.data[0].url;
1625
+ setupAudioSource(audioUrl);
1626
  updateStatus("Speech generation complete!", "success", false);
1627
+ setTimeout(hideStatus, 2500);
1628
  } else {
1629
  throw new Error("No output returned from the generation server.");
1630
  }
 
1638
  }
1639
  });
1640
 
1641
+ // Initialize connection
1642
  connectClient();
1643
  </script>
1644
  </body>