akhaliq HF Staff Claude commited on
Commit
f108f27
·
1 Parent(s): d7a4755

Prune UI to studio essentials

Browse files

Dropped:
- Top bar connection dot / busy indicator (state shows on the
generate button itself)
- "TRACK · INPUT" / "MASTER · OUTPUT" / "CHANNEL 01" subheadings
- Status footer (queue / ETA / latency / codec bar at the bottom)
- Live-generating animated waveform bars (the button spinner covers it)
- Stop / loop transport buttons
- Solo button (mute is enough)
- "Library" panel for examples (moved inline under generate)
- Sample-rate / channels metadata (decoding clutter)
- Radial gradient body background

Kept (the studio signature):
- 3-column work area: Track | Master | Channel
- Functional channel strip: vertical fader → GainNode, pan knob →
StereoPannerNode, real LED meter via AnalyserNode
- RMS waveform with click-to-seek playhead
- DAW-style sliders with mono right-aligned values
- Segmented mode control

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. index.html +123 -328
index.html CHANGED
@@ -6,7 +6,7 @@
6
  <title>GEPARD STUDIO</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
10
  <style>
11
  :root {
12
  --bg: #0b0d12;
@@ -16,7 +16,6 @@
16
  --bg-meter: #05070a;
17
  --line: #20242e;
18
  --line-strong: #2b303c;
19
- --line-hairline: #191c25;
20
  --text: #e7e9f0;
21
  --text-dim: #9aa3b8;
22
  --text-faint: #5e6577;
@@ -25,10 +24,6 @@
25
  --orange: #f97316;
26
  --red: #dc2626;
27
  --green: #22c55e;
28
- --cyan: #06b6d4;
29
- --good: #22c55e;
30
- --bad: #ef4444;
31
- --warn: #f59e0b;
32
  --radius-sm: 3px;
33
  --radius: 5px;
34
  --radius-lg: 8px;
@@ -43,16 +38,13 @@
43
  min-height: 100vh;
44
  -webkit-font-smoothing: antialiased;
45
  }
46
- body {
47
- display: flex; flex-direction: column; min-height: 100vh;
48
- background: radial-gradient(1400px 700px at 50% -220px, rgba(245,165,36,0.035), transparent 60%), var(--bg);
49
- }
50
  .mono, .num { font-family: "JetBrains Mono", ui-monospace, monospace; font-variant-numeric: tabular-nums; }
51
 
52
  /* ===== Top bar ===== */
53
  .topbar {
54
- display: flex; align-items: center; justify-content: space-between;
55
- height: 46px; padding: 0 16px;
56
  background: var(--bg-elev);
57
  border-bottom: 1px solid var(--line);
58
  }
@@ -62,43 +54,29 @@
62
  background: linear-gradient(135deg, #eab308, var(--orange), var(--red));
63
  border-radius: 3px;
64
  display: inline-flex; align-items: center; justify-content: center;
65
- font-size: 12px; color: #fff; font-weight: 800;
66
  }
67
  .brand .name { font-weight: 700; letter-spacing: 0.18em; font-size: 12px; text-transform: uppercase; }
68
  .brand .name .accent { color: var(--amber); }
69
- .brand .tag {
70
- font-size: 10px; color: var(--text-faint); letter-spacing: 0.1em; text-transform: uppercase;
71
- padding-left: 10px; margin-left: 6px; border-left: 1px solid var(--line);
72
- }
73
- .topbar-right { display: flex; align-items: center; gap: 14px; }
74
- .conn-dot {
75
- width: 7px; height: 7px; border-radius: 50%; background: var(--text-faint);
76
- }
77
- .conn-dot.live { background: var(--good); box-shadow: 0 0 8px rgba(34,197,94,0.55); }
78
- .conn-dot.busy { background: var(--warn); box-shadow: 0 0 8px rgba(245,158,11,0.55); animation: pulse 1.2s ease-in-out infinite; }
79
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
80
- .conn-text { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.1em; }
81
 
82
  /* ===== Work area ===== */
83
  .workarea {
84
  flex: 1;
85
- max-width: 1360px;
86
  margin: 0 auto;
87
  width: 100%;
88
- padding: 16px;
89
  display: grid;
90
- grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.35fr) minmax(0, 200px);
91
- gap: 14px;
92
- align-items: start;
93
  }
94
  @media (max-width: 1080px) {
95
  .workarea { grid-template-columns: 1fr 1fr; }
96
- .channel-strip { grid-column: 1 / -1; display: flex; gap: 14px; align-items: stretch; height: 220px; }
97
- .fader-bank, .meter-bank, .knob-bank { flex: 1; }
98
  }
99
  @media (max-width: 680px) {
100
  .workarea { grid-template-columns: 1fr; }
101
- .channel-strip { flex-direction: row; height: 180px; }
102
  }
103
 
104
  .panel {
@@ -106,26 +84,25 @@
106
  border: 1px solid var(--line);
107
  border-radius: var(--radius-lg);
108
  overflow: hidden;
 
109
  }
110
  .panel-head {
111
  display: flex; align-items: center; justify-content: space-between;
112
- padding: 9px 12px;
113
  border-bottom: 1px solid var(--line);
114
  background: var(--bg-elev-2);
115
  }
116
  .panel-head .title {
117
  font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
118
  }
119
- .panel-head .meta { font-size: 10px; color: var(--text-faint); letter-spacing: 0.1em; text-transform: uppercase; }
120
- .panel-body { padding: 12px; }
121
 
122
  .section { margin-bottom: 14px; }
123
  .section:last-child { margin-bottom: 0; }
124
  .section-label {
125
  font-size: 9px; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-faint);
126
- margin-bottom: 7px; display: flex; align-items: center; gap: 8px;
127
  }
128
- .section-label::after { content: ""; flex: 1; height: 1px; background: var(--line-hairline); }
129
 
130
  /* Inputs */
131
  select, textarea, input[type="text"] {
@@ -133,7 +110,7 @@
133
  color: var(--text); font: inherit; padding: 8px 10px; outline: none; transition: border-color 0.15s;
134
  }
135
  select:focus, textarea:focus { border-color: var(--amber); }
136
- textarea { resize: vertical; min-height: 100px; line-height: 1.5; }
137
 
138
  /* Segmented control */
139
  .seg {
@@ -180,14 +157,9 @@
180
  input[type="range"]::-moz-range-track { height: 3px; background: var(--bg-input); border: 1px solid var(--line); border-radius: 2px; }
181
  input[type="range"]::-moz-range-thumb { width: 11px; height: 11px; border: 0; border-radius: 2px; background: var(--amber); cursor: pointer; }
182
 
183
- /* ===== Master panel ===== */
184
- .master { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; }
185
- .master-head { display: flex; align-items: center; justify-content: space-between; padding: 9px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev-2); }
186
- .master-head .title { font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim); }
187
- .master-body { padding: 14px 12px; }
188
-
189
  .waveform-wrap {
190
- height: 110px; background: var(--bg-input); border: 1px solid var(--line); border-radius: var(--radius);
191
  position: relative; overflow: hidden; cursor: pointer;
192
  }
193
  canvas.wave { width: 100%; height: 100%; display: block; }
@@ -196,168 +168,117 @@
196
  color: var(--text-faint); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
197
  }
198
  .wave-progress { position: absolute; top: 0; left: 0; bottom: 0; width: 0; background: rgba(245,165,36,0.05); border-right: 1px solid var(--amber); pointer-events: none; }
199
- .waveform-wrap.generating canvas.wave { opacity: 0; }
200
- .wave-live {
201
- position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
202
- background: repeating-linear-gradient(90deg, transparent 0 6px, rgba(245,165,36,0.04) 6px 12px);
203
- }
204
- .wave-live-bars { display: flex; align-items: center; gap: 3px; height: 55%; }
205
- .wave-live-bars span { width: 3px; background: var(--amber); border-radius: 1px; animation: liveBar 0.85s ease-in-out infinite; }
206
- .wave-live-bars span:nth-child(1){animation-delay:-0.1s}
207
- .wave-live-bars span:nth-child(2){animation-delay:-0.2s}
208
- .wave-live-bars span:nth-child(3){animation-delay:-0.3s}
209
- .wave-live-bars span:nth-child(4){animation-delay:-0.4s}
210
- .wave-live-bars span:nth-child(5){animation-delay:-0.5s}
211
- .wave-live-bars span:nth-child(6){animation-delay:-0.6s}
212
- .wave-live-bars span:nth-child(7){animation-delay:-0.7s}
213
- .wave-live-bars span:nth-child(8){animation-delay:-0.8s}
214
- @keyframes liveBar { 0%,100%{height:10%} 50%{height:90%} }
215
 
216
  /* Transport */
217
  .transport {
218
  display: flex; align-items: center; gap: 12px; margin-top: 12px;
219
  }
220
- .transport-btns { display: flex; align-items: center; gap: 6px; }
221
- .transport-btns button {
222
- appearance: none; border: 0; background: var(--bg-elev-2); color: var(--text-dim);
223
- width: 34px; height: 34px; border-radius: var(--radius-sm); cursor: pointer;
224
- display: flex; align-items: center; justify-content: center; font-size: 12px;
225
- border: 1px solid var(--line); transition: all 0.12s;
226
- }
227
- .transport-btns button:hover:not(:disabled) { color: var(--text); border-color: var(--line-strong); }
228
- .transport-btns button.primary { background: var(--amber); color: #1a1300; border-color: transparent; }
229
- .transport-btns button:disabled { color: var(--text-faint); cursor: not-allowed; }
 
 
 
 
 
 
230
  .time-display { display: flex; align-items: baseline; gap: 6px; font-family: "JetBrains Mono", monospace; }
231
- .time-display .cur { font-size: 24px; font-weight: 500; color: var(--text); }
232
- .time-display .sep { color: var(--text-faint); font-size: 16px; }
233
  .time-display .dur { font-size: 14px; color: var(--text-dim); }
234
- .transport-meta { margin-left: auto; text-align: right; font-size: 10px; color: var(--text-faint); letter-spacing: 0.1em; text-transform: uppercase; line-height: 1.5; }
235
- .transport-meta .v { color: var(--text-dim); }
236
 
237
  /* ===== Channel strip ===== */
238
  .channel-strip {
239
- background: var(--bg-elev);
240
- border: 1px solid var(--line);
241
- border-radius: var(--radius-lg);
242
- overflow: hidden;
243
- display: grid;
244
- grid-template-rows: auto 1fr;
245
- min-height: 460px;
246
- }
247
- .channel-strip-head {
248
- padding: 9px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev-2);
249
- font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
250
- display: flex; justify-content: space-between;
251
  }
252
  .strip-body {
253
- padding: 12px;
254
  display: grid;
255
- grid-template-columns: 1fr 40px 1fr;
256
- gap: 10px;
257
  align-items: end;
258
- min-height: 0;
259
- }
260
- @media (max-width: 1080px) {
261
- .strip-body { grid-template-columns: 1fr 60px 1fr; }
262
  }
 
263
 
264
- /* Strip buttons */
265
- .strip-col { display: flex; flex-direction: column; align-items: center; gap: 10px; }
266
  .strip-btn {
267
- width: 100%; padding: 6px 0; border: 1px solid var(--line); border-radius: var(--radius-sm);
268
- background: var(--bg-elev-2); color: var(--text-dim); font-size: 10px; font-weight: 700;
269
- letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; transition: all 0.12s;
270
  }
271
  .strip-btn:hover { border-color: var(--line-strong); color: var(--text); }
272
- .strip-btn.active { background: var(--amber); color: #1a1300; border-color: transparent; }
273
- .strip-btn.mute.active { background: var(--red); color: #fff; }
274
- .strip-btn.solo.active { background: var(--cyan); color: #001a1c; }
275
 
276
  /* Fader */
277
  .fader-rail {
278
- flex: 1;
279
- width: 34px;
280
- position: relative;
281
- background: var(--bg-input);
282
- border: 1px solid var(--line);
283
- border-radius: 999px;
284
- cursor: pointer;
285
- touch-action: none;
286
  }
287
  .fader-rail::before {
288
  content: ""; position: absolute; left: 50%; top: 8px; bottom: 8px; width: 2px;
289
  background: var(--line); transform: translateX(-50%);
290
  }
291
- .fader-tick {
292
- position: absolute; left: 50%; width: 6px; height: 1px; background: var(--line-strong);
293
- transform: translateX(-50%);
294
- }
295
  .fader-cap {
296
- position: absolute; left: 50%; width: 42px; height: 18px;
297
  background: linear-gradient(180deg, #2a2f3a, #1a1e27);
298
- border: 1px solid var(--line-strong);
299
- border-radius: 3px;
300
  transform: translate(-50%, -50%);
301
  box-shadow: 0 2px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06);
302
  cursor: grab;
303
  }
304
  .fader-cap:active { cursor: grabbing; }
305
  .fader-cap::after {
306
- content: ""; position: absolute; left: 50%; top: 50%; width: 28px; height: 2px;
307
  background: var(--amber); transform: translate(-50%, -50%); border-radius: 1px;
308
  }
309
- .fader-value {
310
- margin-top: 8px; font-family: "JetBrains Mono", monospace; font-size: 11px;
311
- color: var(--text-dim); text-align: center;
312
- }
313
 
314
  /* LED meter */
315
  .meter-rail {
316
- flex: 1;
317
- width: 22px;
318
- position: relative;
319
- background: var(--bg-meter);
320
- border: 1px solid var(--line);
321
- border-radius: var(--radius-sm);
322
- overflow: hidden;
323
  }
324
  .meter-led {
325
  position: absolute; left: 2px; right: 2px; height: 3px;
326
  background: var(--line-strong); border-radius: 1px;
327
  }
328
  .meter-led.on { background: var(--green); }
329
- .meter-led.on.warn { background: var(--warn); }
330
  .meter-led.on.clip { background: var(--red); box-shadow: 0 0 4px var(--red); }
331
- .meter-labels {
332
- position: absolute; right: -22px; top: 0; bottom: 0;
333
- display: flex; flex-direction: column; justify-content: space-between;
334
- font-size: 8px; color: var(--text-faint); font-family: "JetBrains Mono", monospace;
335
- }
336
 
337
  /* Pan knob */
338
- .knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
339
  .knob-ring {
340
- width: 54px; height: 54px; border-radius: 50%;
341
  background: var(--bg-input); border: 1px solid var(--line);
342
  position: relative; cursor: pointer; touch-action: none;
343
  }
344
  .knob-ring::before {
345
  content: ""; position: absolute; inset: 0; border-radius: 50%;
346
  background: conic-gradient(var(--amber) 0deg, var(--amber) var(--knob-deg, 0deg), transparent var(--knob-deg, 0deg));
347
- mask: radial-gradient(transparent 58%, black 60%); -webkit-mask: radial-gradient(transparent 58%, black 60%);
348
  }
349
  .knob-cap {
350
- position: absolute; inset: 6px; border-radius: 50%;
351
  background: linear-gradient(180deg, #2a2f3a, #1a1e27);
352
  border: 1px solid var(--line-strong);
353
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
354
  }
355
  .knob-cap::after {
356
- content: ""; position: absolute; top: 6px; left: 50%; width: 2px; height: 10px;
357
  background: var(--amber); transform: translateX(-50%); border-radius: 1px;
358
  }
359
- .knob-label { font-size: 10px; color: var(--text-faint); letter-spacing: 0.12em; text-transform: uppercase; }
360
- .knob-value { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--text-dim); }
361
 
362
  /* Generate */
363
  .generate-btn {
@@ -381,54 +302,32 @@
381
  }
382
  .toast.hidden { display: none; }
383
 
384
- /* Examples */
385
- .examples { display: flex; flex-wrap: wrap; gap: 6px; }
386
  .example-chip {
387
  appearance: none; display: inline-flex; align-items: center; gap: 6px;
388
- padding: 6px 10px; background: var(--bg-elev-2); border: 1px solid var(--line); border-radius: 999px;
389
  color: var(--text-dim); font: inherit; font-size: 11px; cursor: pointer; transition: all 0.12s;
390
  }
391
  .example-chip:hover { border-color: var(--amber); color: var(--text); }
392
  .example-chip .tag { font-family: "JetBrains Mono", monospace; color: var(--amber); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
393
  .example-chip .txt { max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
394
-
395
- /* Status bar */
396
- .statusbar {
397
- display: flex; align-items: center; gap: 18px;
398
- padding: 8px 16px; background: var(--bg-elev); border-top: 1px solid var(--line);
399
- font-size: 10px; color: var(--text-faint); letter-spacing: 0.12em; text-transform: uppercase;
400
- }
401
- .statusbar .item { display: flex; align-items: center; gap: 6px; }
402
- .statusbar .item .k { color: var(--text-faint); }
403
- .statusbar .item .v { color: var(--text-dim); font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums; }
404
- .statusbar .item .v.live { color: var(--amber); }
405
- .statusbar .spacer { flex: 1; }
406
  </style>
407
  </head>
408
  <body>
409
 
410
- <!-- ===== Top bar ===== -->
411
  <div class="topbar">
412
  <div class="brand">
413
  <span class="logo">G</span>
414
  <span class="name">GEPARD<span class="accent">·</span>STUDIO</span>
415
- <span class="tag">v1.1 · Text-to-Speech</span>
416
- </div>
417
- <div class="topbar-right">
418
- <span class="conn-dot" id="conn-dot"></span>
419
- <span class="conn-text" id="conn-text">Disconnected</span>
420
  </div>
421
  </div>
422
 
423
- <!-- ===== Work area ===== -->
424
  <div class="workarea">
425
 
426
- <!-- LEFT: TRACK -->
427
  <section class="panel">
428
- <div class="panel-head">
429
- <div class="title">Track · Input</div>
430
- <div class="meta">Source · Script · Mix</div>
431
- </div>
432
  <div class="panel-body">
433
  <div class="section">
434
  <div class="section-label">Source</div>
@@ -442,7 +341,7 @@
442
  <select id="speaker"></select>
443
  </div>
444
  <div class="section hidden" id="ref-section">
445
- <div class="section-label">Reference Audio · ≤ 60s</div>
446
  <div class="file-drop" id="drop">
447
  <div class="ico">🎙️</div>
448
  <div class="lbl">Drop clip · click to upload</div>
@@ -455,74 +354,49 @@
455
  <textarea id="text" placeholder="Type what the cheetah should say…"></textarea>
456
  </div>
457
  <div class="section">
458
- <div class="section-label">Mix · Parameters</div>
459
  <div id="sliders"></div>
460
  </div>
461
  </div>
462
  </section>
463
 
464
- <!-- CENTER: MASTER -->
465
- <section class="master">
466
- <div class="master-head">
467
- <div class="title">Master · Output</div>
468
- <div class="meta" id="stage-meta">Idle</div>
469
- </div>
470
- <div class="master-body">
471
  <div class="waveform-wrap" id="wave-wrap">
472
- <div class="wave-empty" id="wave-empty">No signal · awaiting input</div>
473
  <canvas class="wave" id="wave"></canvas>
474
  <div class="wave-progress" id="wave-progress"></div>
475
- <div class="wave-live" id="wave-live" style="display:none">
476
- <div class="wave-live-bars">
477
- <span></span><span></span><span></span><span></span>
478
- <span></span><span></span><span></span><span></span>
479
- </div>
480
- </div>
481
  </div>
482
  <div class="transport">
483
- <div class="transport-btns">
484
- <button id="stop-btn" type="button" title="Stop">■</button>
485
- <button class="primary" id="play-btn" type="button" title="Play / Pause" disabled>▶</button>
486
- <button id="loop-btn" type="button" title="Loop">⟳</button>
487
- </div>
488
  <div class="time-display">
489
  <span class="cur" id="cur-time">0:00</span>
490
  <span class="sep">/</span>
491
  <span class="dur" id="dur-time">0:00</span>
492
  </div>
493
- <div class="transport-meta">
494
- <div>SR <span class="v" id="sr-meta">—</span></div>
495
- <div>CH <span class="v" id="ch-meta">—</span></div>
496
- </div>
497
  </div>
 
 
 
498
  </div>
499
  </section>
500
 
501
- <!-- RIGHT: CHANNEL STRIP -->
502
- <section class="channel-strip" id="channel-strip">
503
- <div class="channel-strip-head">
504
- <span>Channel 01</span>
505
- <span>Master</span>
506
- </div>
507
  <div class="strip-body">
508
  <div class="strip-col">
509
- <button class="strip-btn mute" id="mute-btn" type="button">Mute</button>
510
- <button class="strip-btn solo" id="solo-btn" type="button">Solo</button>
511
- <div style="flex:1"></div>
512
- <div class="knob-wrap">
513
- <div class="knob-ring" id="pan-ring" style="--knob-deg:0deg">
514
- <div class="knob-cap" id="pan-cap"></div>
515
- </div>
516
- <span class="knob-label">Pan</span>
517
- <span class="knob-value mono" id="pan-val">C</span>
518
- </div>
519
  </div>
520
- <div class="strip-col" style="justify-content:flex-end">
521
  <div class="meter-rail" id="meter-rail"></div>
522
- <div class="meter-labels" id="meter-labels"></div>
523
  </div>
524
  <div class="strip-col">
525
- <div style="flex:1"></div>
526
  <div class="fader-rail" id="fader-rail"><div class="fader-cap" id="fader-cap"></div></div>
527
  <div class="fader-value mono" id="fader-val">0.0 dB</div>
528
  </div>
@@ -531,54 +405,23 @@
531
 
532
  </div>
533
 
534
- <!-- ===== Generate + Examples ===== -->
535
- <div class="workarea" style="padding-top:0">
536
- <div style="grid-column:1 / -1">
537
- <button class="generate-btn" id="generate-btn" type="button"><span class="lbl">▶ Generate</span></button>
538
- <div class="toast hidden" id="toast"></div>
539
- <section class="panel" style="margin-top:14px">
540
- <div class="panel-head">
541
- <div class="title">Library · Examples</div>
542
- <div class="meta">Click to load</div>
543
- </div>
544
- <div class="panel-body"><div class="examples" id="examples"></div></div>
545
- </section>
546
- </div>
547
- </div>
548
-
549
- <!-- ===== Status footer ===== -->
550
- <div class="statusbar">
551
- <div class="item"><span class="k">Stage</span> <span class="v" id="f-stage">Idle</span></div>
552
- <div class="item"><span class="k">Queue</span> <span class="v" id="f-queue">—</span></div>
553
- <div class="item"><span class="k">ETA</span> <span class="v" id="f-eta">—</span></div>
554
- <div class="item"><span class="k">Latency</span> <span class="v" id="f-latency">—</span></div>
555
- <div class="spacer"></div>
556
- <div class="item"><span class="k">Codec</span> <span class="v">NeMo Nano · 22.05kHz</span></div>
557
- </div>
558
-
559
  <script type="module">
560
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
561
 
562
  const $ = (id) => document.getElementById(id);
563
  const els = {
564
- connDot: $("conn-dot"), connText: $("conn-text"),
565
  modePreset: $("mode-preset"), modeClone: $("mode-clone"),
566
- speaker: $("speaker"),
567
- speakerSection: $("speaker-section"), refSection: $("ref-section"),
568
  drop: $("drop"), fileInput: $("file-input"), fileName: $("file-name"),
569
  text: $("text"), sliders: $("sliders"),
570
  waveWrap: $("wave-wrap"), wave: $("wave"), waveEmpty: $("wave-empty"),
571
- waveProgress: $("wave-progress"), waveLive: $("wave-live"),
572
- playBtn: $("play-btn"), stopBtn: $("stop-btn"), loopBtn: $("loop-btn"),
573
- curTime: $("cur-time"), durTime: $("dur-time"),
574
- srMeta: $("sr-meta"), chMeta: $("ch-meta"),
575
  generateBtn: $("generate-btn"), toast: $("toast"), examples: $("examples"),
576
- stageMeta: $("stage-meta"),
577
- fStage: $("f-stage"), fQueue: $("f-queue"), fEta: $("f-eta"), fLatency: $("f-latency"),
578
  faderRail: $("fader-rail"), faderCap: $("fader-cap"), faderVal: $("fader-val"),
579
- meterRail: $("meter-rail"), meterLabels: $("meter-labels"),
580
  panRing: $("pan-ring"), panCap: $("pan-cap"), panVal: $("pan-val"),
581
- muteBtn: $("mute-btn"), soloBtn: $("solo-btn"),
582
  };
583
 
584
  let cfg = null;
@@ -589,19 +432,16 @@ let audioUrl = null;
589
  let audioEl = new Audio();
590
  let currentSubmission = null;
591
  let startedAt = 0;
592
- let loop = false;
593
 
594
- // Audio graph for level meter + fader + pan
595
  let audioCtx = null;
596
  let sourceNode = null;
597
  let gainNode = null;
598
  let panNode = null;
599
  let analyser = null;
600
  let meterRAF = null;
601
- let faderValueDb = 0; // -60..+12
602
- let panValue = 0; // -1..0..+1
603
  let isMuted = false;
604
- let isSolo = false;
605
 
606
  const sliderSpec = [
607
  { key: "temperature", label: "Temperature", fmt: v => Number(v).toFixed(2), step: 0.05 },
@@ -611,26 +451,19 @@ const sliderSpec = [
611
  { key: "repetition_window", label: "Rep. Window", fmt: v => String(Math.round(v)), step: 1 },
612
  ];
613
 
614
- // ---------- Helpers ----------
615
- const setStage = (t) => { els.stageMeta.textContent = t; els.fStage.textContent = t; };
616
- const setConn = (state, text) => {
617
- els.connDot.classList.remove("live", "busy");
618
- if (state) els.connDot.classList.add(state);
619
- els.connText.textContent = text;
620
- };
621
- const setBusy = (busy, label) => {
622
  els.generateBtn.disabled = busy;
623
  els.generateBtn.innerHTML = busy
624
- ? '<span class="spinner"></span> Processing…'
625
- : `<span class="lbl">${label || "▶ Generate"}</span>`;
626
  };
627
  const showError = (msg) => { els.toast.textContent = msg; els.toast.classList.remove("hidden"); };
628
  const clearError = () => { els.toast.classList.add("hidden"); els.toast.textContent = ""; };
629
  const fmtTime = (s) => { if (!isFinite(s)) return "0:00"; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${String(r).padStart(2, "0")}`; };
630
- const fmtEta = (s) => { if (!s || s < 0) return "—"; if (s < 60) return `${Math.round(s)}s`; return `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`; };
631
- const fmtQueue = (n) => (n == null || n <= 0) ? "0" : `#${n}`;
632
 
633
- // ---------- Mode / file / sliders ----------
634
  const setMode = (m) => {
635
  mode = m;
636
  els.modePreset.classList.toggle("active", m === "preset");
@@ -645,9 +478,10 @@ els.drop.addEventListener("click", () => els.fileInput.click());
645
  els.fileInput.addEventListener("change", (e) => { const f = e.target.files[0]; if (f) setRefFile(f); });
646
  ["dragenter", "dragover"].forEach(ev => els.drop.addEventListener(ev, (e) => { e.preventDefault(); els.drop.classList.add("drag"); }));
647
  ["dragleave", "drop"].forEach(ev => els.drop.addEventListener(ev, (e) => { e.preventDefault(); els.drop.classList.remove("drag"); }));
648
- els.drop.addEventListener("drop", (e) => { const f = e.target.files[0]; if (f) setRefFile(f); });
649
  const setRefFile = (f) => { refFile = f; els.fileName.textContent = f.name; };
650
 
 
651
  const buildSliders = () => {
652
  els.sliders.innerHTML = "";
653
  sliderSpec.forEach(spec => {
@@ -665,67 +499,56 @@ const sliderValue = (key) => Number($(`sl-${key}`).value);
665
 
666
  // ---------- Channel strip ----------
667
  const buildMeter = () => {
668
- const labels = ["+6", "0", "-6", "-12", "-18", "-24", "-36", "-60"];
669
- const ledCount = 32;
670
  const step = 100 / ledCount;
671
  els.meterRail.innerHTML = "";
672
  for (let i = 0; i < ledCount; i++) {
673
  const led = document.createElement("div");
674
  led.className = "meter-led";
675
  led.style.top = `${i * step}%`;
676
- led.style.height = `${Math.max(2, step * 0.65)}%`;
677
  led.dataset.level = 1 - (i / ledCount);
678
  els.meterRail.appendChild(led);
679
  }
680
- els.meterLabels.innerHTML = labels.map(l => `<span>${l}</span>`).join("");
681
  };
682
-
683
  const updateFaderUI = () => {
684
  const pct = (1 - (faderValueDb - (-60)) / (12 - (-60))) * 100;
685
  els.faderCap.style.top = `${Math.max(4, Math.min(96, pct))}%`;
686
  els.faderVal.textContent = faderValueDb <= -58 ? "-∞ dB" : `${faderValueDb >= 0 ? "+" : ""}${faderValueDb.toFixed(1)} dB`;
687
  if (gainNode) gainNode.gain.value = isMuted ? 0 : dbToGain(faderValueDb);
688
  };
689
- const dbToGain = (db) => Math.pow(10, db / 20);
690
  const updatePanUI = () => {
691
- const deg = (panValue + 1) * 180; // 0..360 for conic
692
  els.panRing.style.setProperty("--knob-deg", `${deg}deg`);
693
  els.panCap.style.transform = `rotate(${deg}deg)`;
694
- if (panValue === 0) els.panVal.textContent = "C";
695
- else els.panVal.textContent = `${panValue > 0 ? "R" : "L"}${Math.round(Math.abs(panValue) * 100)}`;
696
  if (panNode) panNode.pan.value = panValue;
697
  };
698
-
699
- // Fader interaction
700
  let draggingFader = false;
701
  els.faderRail.addEventListener("pointerdown", (e) => { draggingFader = true; els.faderRail.setPointerCapture(e.pointerId); moveFader(e); });
702
  els.faderRail.addEventListener("pointermove", (e) => { if (draggingFader) moveFader(e); });
703
- els.faderRail.addEventListener("pointerup", (e) => { draggingFader = false; });
704
  const moveFader = (e) => {
705
  const rect = els.faderRail.getBoundingClientRect();
706
  const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
707
  faderValueDb = 12 - y * (12 - (-60));
708
  updateFaderUI();
709
  };
710
-
711
- // Pan interaction
712
  let draggingPan = false;
713
  els.panRing.addEventListener("pointerdown", (e) => { draggingPan = true; els.panRing.setPointerCapture(e.pointerId); movePan(e); });
714
  els.panRing.addEventListener("pointermove", (e) => { if (draggingPan) movePan(e); });
715
- els.panRing.addEventListener("pointerup", (e) => { draggingPan = false; });
716
  const movePan = (e) => {
717
  const rect = els.panRing.getBoundingClientRect();
718
  const cx = rect.left + rect.width / 2;
719
  const cy = rect.top + rect.height / 2;
720
  const angle = Math.atan2(e.clientY - cy, e.clientX - cx);
721
- let deg = (angle * 180 / Math.PI) + 90; // 0 top
722
  if (deg < 0) deg += 360;
723
  panValue = Math.max(-1, Math.min(1, (deg / 180) - 1));
724
  updatePanUI();
725
  };
726
-
727
  els.muteBtn.addEventListener("click", () => { isMuted = !isMuted; els.muteBtn.classList.toggle("active", isMuted); updateFaderUI(); });
728
- els.soloBtn.addEventListener("click", () => { isSolo = !isSolo; els.soloBtn.classList.toggle("active", isSolo); });
729
 
730
  // ---------- Audio graph ----------
731
  const initAudioGraph = () => {
@@ -754,20 +577,13 @@ const meterLoop = () => {
754
  if (!analyser) return;
755
  const data = new Uint8Array(analyser.frequencyBinCount);
756
  analyser.getByteFrequencyData(data);
757
- // Convert peak freq energy to dB-ish 0..1
758
- let peak = 0;
759
- for (let i = 0; i < data.length; i++) peak = Math.max(peak, data[i] / 255);
760
- // also use time-domain for more motion
761
  const time = new Uint8Array(analyser.fftSize);
762
  analyser.getByteTimeDomainData(time);
763
- let tpeak = 0;
764
- for (let i = 0; i < time.length; i++) {
765
- const v = Math.abs((time[i] - 128) / 128);
766
- if (v > tpeak) tpeak = v;
767
- }
768
  const level = Math.max(peak * 0.7, tpeak * 0.9);
769
- const leds = els.meterRail.querySelectorAll(".meter-led");
770
- leds.forEach(led => {
771
  const thr = parseFloat(led.dataset.level);
772
  const on = level >= thr;
773
  led.classList.toggle("on", on);
@@ -783,8 +599,6 @@ const drawWaveform = async (audioUrl) => {
783
  const buf = await fetch(audioUrl).then(r => r.arrayBuffer());
784
  const audio = await ctx.decodeAudioData(buf);
785
  ctx.close();
786
- els.srMeta.textContent = `${audio.sampleRate} Hz`;
787
- els.chMeta.textContent = audio.numberOfChannels === 1 ? "Mono" : "Stereo";
788
  const canvas = els.wave;
789
  const dpr = window.devicePixelRatio || 1;
790
  const rect = els.waveWrap.getBoundingClientRect();
@@ -808,17 +622,11 @@ const drawWaveform = async (audioUrl) => {
808
  c.fillRect(i, mid - h / 2, 1, h);
809
  }
810
  };
811
- const setGenerating = (on) => {
812
- els.waveWrap.classList.toggle("generating", on);
813
- els.waveLive.style.display = on ? "flex" : "none";
814
- els.waveEmpty.style.display = on ? "none" : (audioUrl ? "none" : "flex");
815
- };
816
  const loadAudio = (url) => {
817
  if (audioUrl) URL.revokeObjectURL(audioUrl);
818
  audioUrl = url;
819
  audioEl = new Audio(url);
820
  audioEl.preload = "auto";
821
- audioEl.loop = loop;
822
  audioEl.addEventListener("loadedmetadata", () => {
823
  els.durTime.textContent = fmtTime(audioEl.duration);
824
  els.waveEmpty.style.display = "none";
@@ -831,19 +639,16 @@ const loadAudio = (url) => {
831
  const pct = audioEl.duration ? (audioEl.currentTime / audioEl.duration) * 100 : 0;
832
  els.waveProgress.style.width = `${pct}%`;
833
  });
834
- audioEl.addEventListener("ended", () => { if (!loop) els.playBtn.textContent = "▶"; });
835
  };
836
-
837
  els.playBtn.addEventListener("click", () => {
838
  if (!audioCtx) initAudioGraph();
839
  if (audioCtx.state === "suspended") audioCtx.resume();
840
  if (audioEl.paused) { audioEl.play(); els.playBtn.textContent = "❚❚"; }
841
  else { audioEl.pause(); els.playBtn.textContent = "▶"; }
842
  });
843
- els.stopBtn.addEventListener("click", () => { audioEl.pause(); audioEl.currentTime = 0; els.playBtn.textContent = "▶"; });
844
- els.loopBtn.addEventListener("click", () => { loop = !loop; audioEl.loop = loop; els.loopBtn.classList.toggle("active", loop); });
845
  els.waveWrap.addEventListener("click", (e) => {
846
- if (!audioEl.duration || els.waveWrap.classList.contains("generating")) return;
847
  const rect = els.waveWrap.getBoundingClientRect();
848
  const t = ((e.clientX - rect.left) / rect.width) * audioEl.duration;
849
  audioEl.currentTime = Math.max(0, Math.min(audioEl.duration, t));
@@ -855,7 +660,7 @@ const renderExamples = (items) => {
855
  items.forEach(ex => {
856
  const chip = document.createElement("button");
857
  chip.type = "button"; chip.className = "example-chip";
858
- chip.innerHTML = `<span class="tag">${ex.label}</span><span class="txt">${ex.text.slice(0,70)}${ex.text.length > 70 ? "…" : ""}</span>`;
859
  chip.addEventListener("click", () => { els.text.value = ex.text; setMode("preset"); els.speaker.value = ex.speaker; });
860
  els.examples.appendChild(chip);
861
  });
@@ -879,21 +684,13 @@ els.generateBtn.addEventListener("click", async () => {
879
  repetition_penalty: sliderValue("repetition_penalty"),
880
  repetition_window: sliderValue("repetition_window"),
881
  };
882
- setBusy(true); setGenerating(true); setStage("Submitting");
883
- els.fQueue.textContent = "—"; els.fEta.textContent = "—"; els.fLatency.textContent = "—";
884
  startedAt = Date.now();
885
  try {
886
  currentSubmission = await client.submit("/synthesize", args);
887
  for await (const event of currentSubmission) {
888
- if (event.type === "status") {
889
- const stage = event.stage || "pending";
890
- const label = stage === "generating" ? "Generating" : stage === "pending" ? "Queued" : stage === "complete" ? "Complete" : stage === "error" ? "Error" : stage === "streaming" ? "Streaming" : stage;
891
- setStage(label);
892
- els.fQueue.textContent = fmtQueue(event.position);
893
- els.fEta.textContent = fmtEta(event.eta);
894
- setConn(stage === "generating" ? "busy" : "live", stage === "generating" ? "Generating" : "Ready");
895
- } else if (event.type === "data") {
896
- els.fLatency.textContent = `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
897
  const data = event.data?.[0];
898
  if (data?.url) {
899
  const resp = await fetch(data.url); const blob = await resp.blob();
@@ -901,23 +698,21 @@ els.generateBtn.addEventListener("click", async () => {
901
  }
902
  }
903
  }
904
- setStage("Idle"); setConn("live", "Ready");
905
  } catch (err) {
906
- console.error(err); showError(err?.message || String(err)); setStage("Error"); setConn("live", "Ready");
907
  } finally {
908
- setBusy(false); setGenerating(false); currentSubmission = null;
909
  }
910
  });
911
 
912
  // ---------- Boot ----------
913
  (async () => {
914
- setConn(null, "Connecting"); setStage("Idle"); buildMeter();
915
  cfg = await fetch("/config.json").then(r => r.json());
916
  cfg.speakers.forEach(s => { const o = document.createElement("option"); o.value = s.key; o.textContent = s.label; els.speaker.appendChild(o); });
917
  buildSliders();
 
918
  renderExamples(cfg.examples);
919
  client = await Client.connect(window.location.origin);
920
- setConn("live", "Ready");
921
  })();
922
  </script>
923
  </body>
 
6
  <title>GEPARD STUDIO</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
10
  <style>
11
  :root {
12
  --bg: #0b0d12;
 
16
  --bg-meter: #05070a;
17
  --line: #20242e;
18
  --line-strong: #2b303c;
 
19
  --text: #e7e9f0;
20
  --text-dim: #9aa3b8;
21
  --text-faint: #5e6577;
 
24
  --orange: #f97316;
25
  --red: #dc2626;
26
  --green: #22c55e;
 
 
 
 
27
  --radius-sm: 3px;
28
  --radius: 5px;
29
  --radius-lg: 8px;
 
38
  min-height: 100vh;
39
  -webkit-font-smoothing: antialiased;
40
  }
41
+ body { display: flex; flex-direction: column; min-height: 100vh; }
 
 
 
42
  .mono, .num { font-family: "JetBrains Mono", ui-monospace, monospace; font-variant-numeric: tabular-nums; }
43
 
44
  /* ===== Top bar ===== */
45
  .topbar {
46
+ display: flex; align-items: center;
47
+ height: 44px; padding: 0 16px;
48
  background: var(--bg-elev);
49
  border-bottom: 1px solid var(--line);
50
  }
 
54
  background: linear-gradient(135deg, #eab308, var(--orange), var(--red));
55
  border-radius: 3px;
56
  display: inline-flex; align-items: center; justify-content: center;
57
+ font-size: 12px; color: #fff; font-weight: 700;
58
  }
59
  .brand .name { font-weight: 700; letter-spacing: 0.18em; font-size: 12px; text-transform: uppercase; }
60
  .brand .name .accent { color: var(--amber); }
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  /* ===== Work area ===== */
63
  .workarea {
64
  flex: 1;
65
+ max-width: 1320px;
66
  margin: 0 auto;
67
  width: 100%;
68
+ padding: 14px;
69
  display: grid;
70
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.4fr) 180px;
71
+ gap: 12px;
72
+ align-items: stretch;
73
  }
74
  @media (max-width: 1080px) {
75
  .workarea { grid-template-columns: 1fr 1fr; }
76
+ .channel-strip { grid-column: 1 / -1; height: 200px; }
 
77
  }
78
  @media (max-width: 680px) {
79
  .workarea { grid-template-columns: 1fr; }
 
80
  }
81
 
82
  .panel {
 
84
  border: 1px solid var(--line);
85
  border-radius: var(--radius-lg);
86
  overflow: hidden;
87
+ display: flex; flex-direction: column;
88
  }
89
  .panel-head {
90
  display: flex; align-items: center; justify-content: space-between;
91
+ padding: 8px 12px;
92
  border-bottom: 1px solid var(--line);
93
  background: var(--bg-elev-2);
94
  }
95
  .panel-head .title {
96
  font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
97
  }
98
+ .panel-body { padding: 12px; flex: 1; }
 
99
 
100
  .section { margin-bottom: 14px; }
101
  .section:last-child { margin-bottom: 0; }
102
  .section-label {
103
  font-size: 9px; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-faint);
104
+ margin-bottom: 6px;
105
  }
 
106
 
107
  /* Inputs */
108
  select, textarea, input[type="text"] {
 
110
  color: var(--text); font: inherit; padding: 8px 10px; outline: none; transition: border-color 0.15s;
111
  }
112
  select:focus, textarea:focus { border-color: var(--amber); }
113
+ textarea { resize: vertical; min-height: 96px; line-height: 1.5; }
114
 
115
  /* Segmented control */
116
  .seg {
 
157
  input[type="range"]::-moz-range-track { height: 3px; background: var(--bg-input); border: 1px solid var(--line); border-radius: 2px; }
158
  input[type="range"]::-moz-range-thumb { width: 11px; height: 11px; border: 0; border-radius: 2px; background: var(--amber); cursor: pointer; }
159
 
160
+ /* ===== Master ===== */
 
 
 
 
 
161
  .waveform-wrap {
162
+ height: 100px; background: var(--bg-input); border: 1px solid var(--line); border-radius: var(--radius);
163
  position: relative; overflow: hidden; cursor: pointer;
164
  }
165
  canvas.wave { width: 100%; height: 100%; display: block; }
 
168
  color: var(--text-faint); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
169
  }
170
  .wave-progress { position: absolute; top: 0; left: 0; bottom: 0; width: 0; background: rgba(245,165,36,0.05); border-right: 1px solid var(--amber); pointer-events: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  /* Transport */
173
  .transport {
174
  display: flex; align-items: center; gap: 12px; margin-top: 12px;
175
  }
176
+ .play-btn {
177
+ appearance: none; border: 0;
178
+ width: 44px; height: 44px;
179
+ border-radius: 50%;
180
+ background: var(--amber);
181
+ color: #1a1300;
182
+ font-size: 16px;
183
+ cursor: pointer;
184
+ flex-shrink: 0;
185
+ display: flex; align-items: center; justify-content: center;
186
+ transition: filter 0.12s, transform 0.05s;
187
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.3), 0 4px 12px rgba(245,165,36,0.18);
188
+ }
189
+ .play-btn:hover:not(:disabled) { filter: brightness(1.08); }
190
+ .play-btn:active:not(:disabled) { transform: translateY(1px); }
191
+ .play-btn:disabled { background: var(--bg-elev-2); color: var(--text-faint); cursor: not-allowed; box-shadow: 0 0 0 1px var(--line); }
192
  .time-display { display: flex; align-items: baseline; gap: 6px; font-family: "JetBrains Mono", monospace; }
193
+ .time-display .cur { font-size: 26px; font-weight: 500; color: var(--text); }
194
+ .time-display .sep { color: var(--text-faint); font-size: 18px; }
195
  .time-display .dur { font-size: 14px; color: var(--text-dim); }
 
 
196
 
197
  /* ===== Channel strip ===== */
198
  .channel-strip {
199
+ background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius-lg);
200
+ overflow: hidden; display: flex; flex-direction: column;
 
 
 
 
 
 
 
 
 
 
201
  }
202
  .strip-body {
203
+ padding: 14px 10px 12px;
204
  display: grid;
205
+ grid-template-columns: 1fr 22px 1fr;
206
+ gap: 8px;
207
  align-items: end;
208
+ flex: 1;
 
 
 
209
  }
210
+ .strip-col { display: flex; flex-direction: column; align-items: center; gap: 10px; min-height: 0; }
211
 
 
 
212
  .strip-btn {
213
+ width: 100%; padding: 5px 0; border: 1px solid var(--line); border-radius: var(--radius-sm);
214
+ background: var(--bg-elev-2); color: var(--text-dim); font-size: 9px; font-weight: 700;
215
+ letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; transition: all 0.12s;
216
  }
217
  .strip-btn:hover { border-color: var(--line-strong); color: var(--text); }
218
+ .strip-btn.active { background: var(--red); color: #fff; border-color: transparent; }
 
 
219
 
220
  /* Fader */
221
  .fader-rail {
222
+ flex: 1; width: 30px; min-height: 100px;
223
+ position: relative; background: var(--bg-input);
224
+ border: 1px solid var(--line); border-radius: 999px; cursor: pointer; touch-action: none;
 
 
 
 
 
225
  }
226
  .fader-rail::before {
227
  content: ""; position: absolute; left: 50%; top: 8px; bottom: 8px; width: 2px;
228
  background: var(--line); transform: translateX(-50%);
229
  }
 
 
 
 
230
  .fader-cap {
231
+ position: absolute; left: 50%; width: 38px; height: 16px;
232
  background: linear-gradient(180deg, #2a2f3a, #1a1e27);
233
+ border: 1px solid var(--line-strong); border-radius: 3px;
 
234
  transform: translate(-50%, -50%);
235
  box-shadow: 0 2px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06);
236
  cursor: grab;
237
  }
238
  .fader-cap:active { cursor: grabbing; }
239
  .fader-cap::after {
240
+ content: ""; position: absolute; left: 50%; top: 50%; width: 26px; height: 2px;
241
  background: var(--amber); transform: translate(-50%, -50%); border-radius: 1px;
242
  }
243
+ .fader-value { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--text-dim); text-align: center; margin-top: 6px; }
 
 
 
244
 
245
  /* LED meter */
246
  .meter-rail {
247
+ flex: 1; width: 22px; min-height: 100px;
248
+ position: relative; background: var(--bg-meter);
249
+ border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden;
 
 
 
 
250
  }
251
  .meter-led {
252
  position: absolute; left: 2px; right: 2px; height: 3px;
253
  background: var(--line-strong); border-radius: 1px;
254
  }
255
  .meter-led.on { background: var(--green); }
256
+ .meter-led.on.warn { background: var(--amber); }
257
  .meter-led.on.clip { background: var(--red); box-shadow: 0 0 4px var(--red); }
 
 
 
 
 
258
 
259
  /* Pan knob */
 
260
  .knob-ring {
261
+ width: 44px; height: 44px; border-radius: 50%;
262
  background: var(--bg-input); border: 1px solid var(--line);
263
  position: relative; cursor: pointer; touch-action: none;
264
  }
265
  .knob-ring::before {
266
  content: ""; position: absolute; inset: 0; border-radius: 50%;
267
  background: conic-gradient(var(--amber) 0deg, var(--amber) var(--knob-deg, 0deg), transparent var(--knob-deg, 0deg));
268
+ mask: radial-gradient(transparent 56%, black 60%); -webkit-mask: radial-gradient(transparent 56%, black 60%);
269
  }
270
  .knob-cap {
271
+ position: absolute; inset: 5px; border-radius: 50%;
272
  background: linear-gradient(180deg, #2a2f3a, #1a1e27);
273
  border: 1px solid var(--line-strong);
274
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
275
  }
276
  .knob-cap::after {
277
+ content: ""; position: absolute; top: 5px; left: 50%; width: 2px; height: 8px;
278
  background: var(--amber); transform: translateX(-50%); border-radius: 1px;
279
  }
280
+ .knob-label { font-size: 9px; color: var(--text-faint); letter-spacing: 0.14em; text-transform: uppercase; margin-top: 4px; }
281
+ .knob-value { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--text-dim); margin-top: 2px; }
282
 
283
  /* Generate */
284
  .generate-btn {
 
302
  }
303
  .toast.hidden { display: none; }
304
 
305
+ /* Examples inline */
306
+ .examples { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
307
  .example-chip {
308
  appearance: none; display: inline-flex; align-items: center; gap: 6px;
309
+ padding: 5px 9px; background: var(--bg-elev-2); border: 1px solid var(--line); border-radius: 999px;
310
  color: var(--text-dim); font: inherit; font-size: 11px; cursor: pointer; transition: all 0.12s;
311
  }
312
  .example-chip:hover { border-color: var(--amber); color: var(--text); }
313
  .example-chip .tag { font-family: "JetBrains Mono", monospace; color: var(--amber); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
314
  .example-chip .txt { max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
 
 
 
 
 
 
 
 
 
 
315
  </style>
316
  </head>
317
  <body>
318
 
 
319
  <div class="topbar">
320
  <div class="brand">
321
  <span class="logo">G</span>
322
  <span class="name">GEPARD<span class="accent">·</span>STUDIO</span>
 
 
 
 
 
323
  </div>
324
  </div>
325
 
 
326
  <div class="workarea">
327
 
328
+ <!-- Track -->
329
  <section class="panel">
330
+ <div class="panel-head"><div class="title">Track</div></div>
 
 
 
331
  <div class="panel-body">
332
  <div class="section">
333
  <div class="section-label">Source</div>
 
341
  <select id="speaker"></select>
342
  </div>
343
  <div class="section hidden" id="ref-section">
344
+ <div class="section-label">Reference · ≤ 60s</div>
345
  <div class="file-drop" id="drop">
346
  <div class="ico">🎙️</div>
347
  <div class="lbl">Drop clip · click to upload</div>
 
354
  <textarea id="text" placeholder="Type what the cheetah should say…"></textarea>
355
  </div>
356
  <div class="section">
357
+ <div class="section-label">Mix</div>
358
  <div id="sliders"></div>
359
  </div>
360
  </div>
361
  </section>
362
 
363
+ <!-- Master -->
364
+ <section class="panel">
365
+ <div class="panel-head"><div class="title">Master</div></div>
366
+ <div class="panel-body">
 
 
 
367
  <div class="waveform-wrap" id="wave-wrap">
368
+ <div class="wave-empty" id="wave-empty">No signal</div>
369
  <canvas class="wave" id="wave"></canvas>
370
  <div class="wave-progress" id="wave-progress"></div>
 
 
 
 
 
 
371
  </div>
372
  <div class="transport">
373
+ <button class="play-btn" id="play-btn" type="button" disabled>▶</button>
 
 
 
 
374
  <div class="time-display">
375
  <span class="cur" id="cur-time">0:00</span>
376
  <span class="sep">/</span>
377
  <span class="dur" id="dur-time">0:00</span>
378
  </div>
 
 
 
 
379
  </div>
380
+ <button class="generate-btn" id="generate-btn" type="button"><span class="lbl">▶ Generate</span></button>
381
+ <div class="toast hidden" id="toast"></div>
382
+ <div class="examples" id="examples"></div>
383
  </div>
384
  </section>
385
 
386
+ <!-- Channel strip -->
387
+ <section class="channel-strip">
388
+ <div class="panel-head"><div class="title">Channel</div></div>
 
 
 
389
  <div class="strip-body">
390
  <div class="strip-col">
391
+ <button class="strip-btn" id="mute-btn" type="button">Mute</button>
392
+ <div class="knob-ring" id="pan-ring" style="--knob-deg:0deg"><div class="knob-cap" id="pan-cap"></div></div>
393
+ <div class="knob-label">Pan</div>
394
+ <div class="knob-value mono" id="pan-val">C</div>
 
 
 
 
 
 
395
  </div>
396
+ <div class="strip-col">
397
  <div class="meter-rail" id="meter-rail"></div>
 
398
  </div>
399
  <div class="strip-col">
 
400
  <div class="fader-rail" id="fader-rail"><div class="fader-cap" id="fader-cap"></div></div>
401
  <div class="fader-value mono" id="fader-val">0.0 dB</div>
402
  </div>
 
405
 
406
  </div>
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  <script type="module">
409
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
410
 
411
  const $ = (id) => document.getElementById(id);
412
  const els = {
 
413
  modePreset: $("mode-preset"), modeClone: $("mode-clone"),
414
+ speaker: $("speaker"), speakerSection: $("speaker-section"), refSection: $("ref-section"),
 
415
  drop: $("drop"), fileInput: $("file-input"), fileName: $("file-name"),
416
  text: $("text"), sliders: $("sliders"),
417
  waveWrap: $("wave-wrap"), wave: $("wave"), waveEmpty: $("wave-empty"),
418
+ waveProgress: $("wave-progress"),
419
+ playBtn: $("play-btn"), curTime: $("cur-time"), durTime: $("dur-time"),
 
 
420
  generateBtn: $("generate-btn"), toast: $("toast"), examples: $("examples"),
 
 
421
  faderRail: $("fader-rail"), faderCap: $("fader-cap"), faderVal: $("fader-val"),
422
+ meterRail: $("meter-rail"),
423
  panRing: $("pan-ring"), panCap: $("pan-cap"), panVal: $("pan-val"),
424
+ muteBtn: $("mute-btn"),
425
  };
426
 
427
  let cfg = null;
 
432
  let audioEl = new Audio();
433
  let currentSubmission = null;
434
  let startedAt = 0;
 
435
 
 
436
  let audioCtx = null;
437
  let sourceNode = null;
438
  let gainNode = null;
439
  let panNode = null;
440
  let analyser = null;
441
  let meterRAF = null;
442
+ let faderValueDb = 0;
443
+ let panValue = 0;
444
  let isMuted = false;
 
445
 
446
  const sliderSpec = [
447
  { key: "temperature", label: "Temperature", fmt: v => Number(v).toFixed(2), step: 0.05 },
 
451
  { key: "repetition_window", label: "Rep. Window", fmt: v => String(Math.round(v)), step: 1 },
452
  ];
453
 
454
+ const dbToGain = (db) => Math.pow(10, db / 20);
455
+
456
+ const setBusy = (busy) => {
 
 
 
 
 
457
  els.generateBtn.disabled = busy;
458
  els.generateBtn.innerHTML = busy
459
+ ? '<span class="spinner"></span> Generating…'
460
+ : '<span class="lbl">▶ Generate</span>';
461
  };
462
  const showError = (msg) => { els.toast.textContent = msg; els.toast.classList.remove("hidden"); };
463
  const clearError = () => { els.toast.classList.add("hidden"); els.toast.textContent = ""; };
464
  const fmtTime = (s) => { if (!isFinite(s)) return "0:00"; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${String(r).padStart(2, "0")}`; };
 
 
465
 
466
+ // ---------- Mode / file ----------
467
  const setMode = (m) => {
468
  mode = m;
469
  els.modePreset.classList.toggle("active", m === "preset");
 
478
  els.fileInput.addEventListener("change", (e) => { const f = e.target.files[0]; if (f) setRefFile(f); });
479
  ["dragenter", "dragover"].forEach(ev => els.drop.addEventListener(ev, (e) => { e.preventDefault(); els.drop.classList.add("drag"); }));
480
  ["dragleave", "drop"].forEach(ev => els.drop.addEventListener(ev, (e) => { e.preventDefault(); els.drop.classList.remove("drag"); }));
481
+ els.drop.addEventListener("drop", (e) => { const f = e.dataTransfer.files[0]; if (f) setRefFile(f); });
482
  const setRefFile = (f) => { refFile = f; els.fileName.textContent = f.name; };
483
 
484
+ // ---------- Sliders ----------
485
  const buildSliders = () => {
486
  els.sliders.innerHTML = "";
487
  sliderSpec.forEach(spec => {
 
499
 
500
  // ---------- Channel strip ----------
501
  const buildMeter = () => {
502
+ const ledCount = 28;
 
503
  const step = 100 / ledCount;
504
  els.meterRail.innerHTML = "";
505
  for (let i = 0; i < ledCount; i++) {
506
  const led = document.createElement("div");
507
  led.className = "meter-led";
508
  led.style.top = `${i * step}%`;
509
+ led.style.height = `${Math.max(2, step * 0.7)}%`;
510
  led.dataset.level = 1 - (i / ledCount);
511
  els.meterRail.appendChild(led);
512
  }
 
513
  };
 
514
  const updateFaderUI = () => {
515
  const pct = (1 - (faderValueDb - (-60)) / (12 - (-60))) * 100;
516
  els.faderCap.style.top = `${Math.max(4, Math.min(96, pct))}%`;
517
  els.faderVal.textContent = faderValueDb <= -58 ? "-∞ dB" : `${faderValueDb >= 0 ? "+" : ""}${faderValueDb.toFixed(1)} dB`;
518
  if (gainNode) gainNode.gain.value = isMuted ? 0 : dbToGain(faderValueDb);
519
  };
 
520
  const updatePanUI = () => {
521
+ const deg = (panValue + 1) * 180;
522
  els.panRing.style.setProperty("--knob-deg", `${deg}deg`);
523
  els.panCap.style.transform = `rotate(${deg}deg)`;
524
+ els.panVal.textContent = panValue === 0 ? "C" : `${panValue > 0 ? "R" : "L"}${Math.round(Math.abs(panValue) * 100)}`;
 
525
  if (panNode) panNode.pan.value = panValue;
526
  };
 
 
527
  let draggingFader = false;
528
  els.faderRail.addEventListener("pointerdown", (e) => { draggingFader = true; els.faderRail.setPointerCapture(e.pointerId); moveFader(e); });
529
  els.faderRail.addEventListener("pointermove", (e) => { if (draggingFader) moveFader(e); });
530
+ els.faderRail.addEventListener("pointerup", () => draggingFader = false);
531
  const moveFader = (e) => {
532
  const rect = els.faderRail.getBoundingClientRect();
533
  const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
534
  faderValueDb = 12 - y * (12 - (-60));
535
  updateFaderUI();
536
  };
 
 
537
  let draggingPan = false;
538
  els.panRing.addEventListener("pointerdown", (e) => { draggingPan = true; els.panRing.setPointerCapture(e.pointerId); movePan(e); });
539
  els.panRing.addEventListener("pointermove", (e) => { if (draggingPan) movePan(e); });
540
+ els.panRing.addEventListener("pointerup", () => draggingPan = false);
541
  const movePan = (e) => {
542
  const rect = els.panRing.getBoundingClientRect();
543
  const cx = rect.left + rect.width / 2;
544
  const cy = rect.top + rect.height / 2;
545
  const angle = Math.atan2(e.clientY - cy, e.clientX - cx);
546
+ let deg = (angle * 180 / Math.PI) + 90;
547
  if (deg < 0) deg += 360;
548
  panValue = Math.max(-1, Math.min(1, (deg / 180) - 1));
549
  updatePanUI();
550
  };
 
551
  els.muteBtn.addEventListener("click", () => { isMuted = !isMuted; els.muteBtn.classList.toggle("active", isMuted); updateFaderUI(); });
 
552
 
553
  // ---------- Audio graph ----------
554
  const initAudioGraph = () => {
 
577
  if (!analyser) return;
578
  const data = new Uint8Array(analyser.frequencyBinCount);
579
  analyser.getByteFrequencyData(data);
 
 
 
 
580
  const time = new Uint8Array(analyser.fftSize);
581
  analyser.getByteTimeDomainData(time);
582
+ let peak = 0, tpeak = 0;
583
+ for (let i = 0; i < data.length; i++) peak = Math.max(peak, data[i] / 255);
584
+ for (let i = 0; i < time.length; i++) { const v = Math.abs((time[i] - 128) / 128); if (v > tpeak) tpeak = v; }
 
 
585
  const level = Math.max(peak * 0.7, tpeak * 0.9);
586
+ els.meterRail.querySelectorAll(".meter-led").forEach(led => {
 
587
  const thr = parseFloat(led.dataset.level);
588
  const on = level >= thr;
589
  led.classList.toggle("on", on);
 
599
  const buf = await fetch(audioUrl).then(r => r.arrayBuffer());
600
  const audio = await ctx.decodeAudioData(buf);
601
  ctx.close();
 
 
602
  const canvas = els.wave;
603
  const dpr = window.devicePixelRatio || 1;
604
  const rect = els.waveWrap.getBoundingClientRect();
 
622
  c.fillRect(i, mid - h / 2, 1, h);
623
  }
624
  };
 
 
 
 
 
625
  const loadAudio = (url) => {
626
  if (audioUrl) URL.revokeObjectURL(audioUrl);
627
  audioUrl = url;
628
  audioEl = new Audio(url);
629
  audioEl.preload = "auto";
 
630
  audioEl.addEventListener("loadedmetadata", () => {
631
  els.durTime.textContent = fmtTime(audioEl.duration);
632
  els.waveEmpty.style.display = "none";
 
639
  const pct = audioEl.duration ? (audioEl.currentTime / audioEl.duration) * 100 : 0;
640
  els.waveProgress.style.width = `${pct}%`;
641
  });
642
+ audioEl.addEventListener("ended", () => { els.playBtn.textContent = "▶"; });
643
  };
 
644
  els.playBtn.addEventListener("click", () => {
645
  if (!audioCtx) initAudioGraph();
646
  if (audioCtx.state === "suspended") audioCtx.resume();
647
  if (audioEl.paused) { audioEl.play(); els.playBtn.textContent = "❚❚"; }
648
  else { audioEl.pause(); els.playBtn.textContent = "▶"; }
649
  });
 
 
650
  els.waveWrap.addEventListener("click", (e) => {
651
+ if (!audioEl.duration) return;
652
  const rect = els.waveWrap.getBoundingClientRect();
653
  const t = ((e.clientX - rect.left) / rect.width) * audioEl.duration;
654
  audioEl.currentTime = Math.max(0, Math.min(audioEl.duration, t));
 
660
  items.forEach(ex => {
661
  const chip = document.createElement("button");
662
  chip.type = "button"; chip.className = "example-chip";
663
+ chip.innerHTML = `<span class="tag">${ex.label}</span><span class="txt">${ex.text.slice(0,60)}${ex.text.length > 60 ? "…" : ""}</span>`;
664
  chip.addEventListener("click", () => { els.text.value = ex.text; setMode("preset"); els.speaker.value = ex.speaker; });
665
  els.examples.appendChild(chip);
666
  });
 
684
  repetition_penalty: sliderValue("repetition_penalty"),
685
  repetition_window: sliderValue("repetition_window"),
686
  };
687
+ setBusy(true);
 
688
  startedAt = Date.now();
689
  try {
690
  currentSubmission = await client.submit("/synthesize", args);
691
  for await (const event of currentSubmission) {
692
+ if (event.type === "data") {
693
+ const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
 
 
 
 
 
 
 
694
  const data = event.data?.[0];
695
  if (data?.url) {
696
  const resp = await fetch(data.url); const blob = await resp.blob();
 
698
  }
699
  }
700
  }
 
701
  } catch (err) {
702
+ console.error(err); showError(err?.message || String(err));
703
  } finally {
704
+ setBusy(false); currentSubmission = null;
705
  }
706
  });
707
 
708
  // ---------- Boot ----------
709
  (async () => {
 
710
  cfg = await fetch("/config.json").then(r => r.json());
711
  cfg.speakers.forEach(s => { const o = document.createElement("option"); o.value = s.key; o.textContent = s.label; els.speaker.appendChild(o); });
712
  buildSliders();
713
+ buildMeter();
714
  renderExamples(cfg.examples);
715
  client = await Client.connect(window.location.origin);
 
716
  })();
717
  </script>
718
  </body>