blackboxanalytics commited on
Commit
5eef7f2
·
1 Parent(s): 8694d13

Fix result-panel UI: accurate seam ribbon, full waveform, instant readout, intro replays

Browse files

- Seam 'needle' was drifting/stuck: it measured WaveSurfer's waveform in the
browser. Replace with a self-contained before/after ribbon rendered server-side
(YOUR PART | CODA split + join), so the marker is always exactly at src/total.
- Output waveform was clipped (~15px bleeding into the timestamps): give the
waveform container real height (min 90px, centered) so it shows in full.
- 'CODA heard' sat greyed until generation on slow hosts: make analyze_on_upload
stream a 'listening...' state the instant a clip lands, then the key/tempo readout.
- Intro only played on first load (localStorage gate): remove the gate so the
2016 intro replays on every page load.
- Polish: easing/transition refinements, lighter front-end (dropped the fragile
seam-overlay JS and its observers).

Files changed (1) hide show
  1. app.py +196 -165
app.py CHANGED
@@ -121,6 +121,19 @@ def _warn_html(title, body):
121
  f"<div class='coda-hud-note'>{body}</div></div>")
122
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # the live "what is CODA doing right now" overlay shown over the result panel
125
  # during generation. finish_song yields these at each real pipeline milestone.
126
  _STAGE_STEPS = ["listening", "composing", "splicing"]
@@ -146,20 +159,60 @@ def _stage_html(phase, label, sub=""):
146
  "</div>")
147
 
148
 
 
 
 
 
 
149
  def _seam_html(source_seconds, total):
150
- """Hidden data carrier the Feature-B (seam-reveal) JS reads via data-* on a
151
- stable id. Purely cosmetic surfacing of values finish_song already computed;
152
- does NOT touch enhance -> fingerprint -> continue_audio -> stitch."""
153
- return (f"<div id='coda-seam-data' hidden "
154
- f"data-seam='{float(source_seconds):.4f}' "
155
- f"data-total='{float(total):.4f}'></div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
 
158
  def analyze_on_upload(audio_path):
159
  """Fast CPU-only pass the instant a clip loads, so the user sees what CODA
160
- heard immediately instead of a dead screen."""
 
 
 
 
 
 
 
161
  if not audio_path:
162
- return gr.update(value="", visible=False), gr.update(interactive=False)
 
 
 
163
  try:
164
  listen_path = enhance_to_tempfile(audio_path)
165
  info = fingerprint(listen_path)
@@ -174,14 +227,15 @@ def analyze_on_upload(audio_path):
174
  f"{engine.MAX_SOURCE_SECONDS:.0f}s (Stable Audio 3's "
175
  f"{engine.MAX_TOTAL_SECONDS:.0f}s total cap). Trim it shorter and "
176
  f"re-upload.")
177
- return gr.update(value=msg, visible=True), gr.update(interactive=False)
 
178
  html = _readout_html(info, quality)
179
- return gr.update(value=html, visible=True), gr.update(interactive=True)
180
  except Exception as e:
181
  print(f"[coda] analysis failed ({e})", flush=True)
182
- return (gr.update(value=_warn_html("Couldn't read that file", str(e)),
183
- visible=True),
184
- gr.update(interactive=False))
185
 
186
 
187
  @spaces.GPU(duration=120)
@@ -1163,6 +1217,118 @@ body.coda-intro-lock{ overflow:hidden !important; }
1163
  .coda-intro-beat, #coda-intro-eq{ opacity:1 !important; transform:none !important; filter:none !important; }
1164
  #coda-intro-overlay{ transition:opacity .2s linear; }
1165
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1166
  """
1167
 
1168
  # ethereal hero spectrum: a frequency-spectrum bar field with a centered
@@ -1351,141 +1517,12 @@ CODA_INIT_JS = """
1351
  W.heroSync = sync;
1352
  }
1353
 
1354
- /* ===== FEATURE B — BEFORE/AFTER SEAM REVEAL ===== */
1355
- (function featureB() {
1356
- if (W.seam) return;
1357
- var S = (W.seam = { booted: true, _seam: null });
1358
-
1359
- function readSeam() {
1360
- try {
1361
- var el = doc.getElementById('coda-seam-data');
1362
- if (!el) return null;
1363
- var src = parseFloat(el.getAttribute('data-seam'));
1364
- var tot = parseFloat(el.getAttribute('data-total'));
1365
- if (!isFinite(src) || !isFinite(tot) || tot <= 0 || src < 0) return null;
1366
- if (src >= tot) return null;
1367
- return { src: src, total: tot, frac: clamp(src / tot, 0, 1) };
1368
- } catch (e) { return null; }
1369
- }
1370
- /* visible .coda-player audio DOES carry a real src in this build (verified
1371
- * C.src=url), so it's the cleanest predictable target. */
1372
- function findPlayable(result) {
1373
- try {
1374
- var vis = result.querySelector('.coda-player audio');
1375
- if (vis && ((vis.currentSrc && vis.currentSrc.length) || (vis.src && vis.src.length))) return vis;
1376
- var auds = result.querySelectorAll('audio');
1377
- for (var i = 0; i < auds.length; i++) if (auds[i].currentSrc && auds[i].currentSrc.length) return auds[i];
1378
- for (var j = 0; j < auds.length; j++) if (auds[j].src && auds[j].src.length) return auds[j];
1379
- return vis || auds[0] || null;
1380
- } catch (e) { return null; }
1381
- }
1382
- function ensureOverlay(wc) {
1383
- var ov = wc.querySelector('#coda-seam-overlay');
1384
- if (!ov) {
1385
- ov = doc.createElement('div');
1386
- ov.id = 'coda-seam-overlay';
1387
- ov.setAttribute('aria-hidden', 'true');
1388
- ov.innerHTML =
1389
- '<div class="coda-seam-line"></div>' +
1390
- '<div class="coda-seam-tag coda-seam-yours">YOUR PART</div>' +
1391
- '<div class="coda-seam-tag coda-seam-coda">CODA</div>';
1392
- if (getComputedStyle(wc).position === 'static') wc.style.position = 'relative';
1393
- wc.appendChild(ov);
1394
- if (!REDUCED) ov.classList.add('coda-seam-in');
1395
- }
1396
- return ov;
1397
- }
1398
- function ensureButton(result, seam) {
1399
- var btn = result.querySelector('#coda-seam-btn');
1400
- if (!btn) {
1401
- var head = result.querySelector('.coda-result-head');
1402
- btn = doc.createElement('button');
1403
- btn.id = 'coda-seam-btn'; btn.type = 'button'; btn.className = 'coda-seam-btn';
1404
- btn.innerHTML = '<span class="coda-seam-ico">&#9654;</span>' +
1405
- '<span class="coda-seam-txt">Play from the seam</span>';
1406
- btn.addEventListener('click', function () {
1407
- try {
1408
- var s = S._seam; if (!s) return;
1409
- var a = findPlayable(result); if (!a) return;
1410
- AUDIO.pauseOthers(a); /* avoid double audio; drive the el Feature A taps */
1411
- var seekAndPlay = function () {
1412
- try {
1413
- var dur = (a.duration && isFinite(a.duration)) ? a.duration : 0;
1414
- var t = dur > 0 ? clamp(s.src, 0, Math.max(0, dur - 0.05)) : s.src;
1415
- a.currentTime = t;
1416
- var p = a.play(); if (p && p.catch) p.catch(function () {});
1417
- } catch (e) {}
1418
- };
1419
- if (a.readyState >= 1 && isFinite(a.duration) && a.duration > 0) seekAndPlay();
1420
- else a.addEventListener('loadedmetadata', seekAndPlay, { once: true });
1421
- /* do NOT call a.load() — src already set by Gradio; load() races it */
1422
- } catch (e) {}
1423
- });
1424
- if (head) head.appendChild(btn); else result.insertBefore(btn, result.firstChild);
1425
- }
1426
- var txt = btn.querySelector('.coda-seam-txt');
1427
- if (txt) txt.textContent = 'Play from the seam · ' + fmtTime(seam.src);
1428
- btn.title = 'Hear the join: your original ends at ' + fmtTime(seam.src) + ', CODA takes over';
1429
- btn.style.display = '';
1430
- return btn;
1431
- }
1432
- function teardown(result) {
1433
- try {
1434
- var ov = result.querySelector('#coda-seam-overlay'); if (ov) ov.style.display = 'none';
1435
- var btn = result.querySelector('#coda-seam-btn'); if (btn) btn.style.display = 'none';
1436
- } catch (e) {}
1437
- }
1438
- function place(seam) {
1439
- var result = doc.getElementById('coda-result'); if (!result) return;
1440
- var wc = result.querySelector('.coda-player .waveform-container'); if (!wc) return;
1441
- if (!seam) { teardown(result); return; }
1442
- var ov = ensureOverlay(wc);
1443
- ov.style.setProperty('--seam', (seam.frac * 100).toFixed(3) + '%');
1444
- ov.style.display = 'block';
1445
- ov.classList.toggle('coda-seam-near-end', seam.frac > 0.88);
1446
- ov.classList.toggle('coda-seam-near-start', seam.frac < 0.12);
1447
- /* play-from-seam button removed: WaveSurfer playback is not controllable from app JS. The gold seam marker + labels carry the before/after reveal. */
1448
- }
1449
- function sync() {
1450
- try {
1451
- var seam = readSeam(); var prev = S._seam; S._seam = seam;
1452
- var changed = !prev || !seam || prev.src !== seam.src || prev.total !== seam.total;
1453
- var result = doc.getElementById('coda-result');
1454
- var ovEl = result && result.querySelector('#coda-seam-overlay');
1455
- var hasOverlay = ovEl && ovEl.style.display !== 'none';
1456
- if (changed || (seam && !hasOverlay)) place(seam);
1457
- } catch (e) {}
1458
- }
1459
- var rafPending = false;
1460
- function onResize() {
1461
- if (rafPending) return; rafPending = true;
1462
- requestAnimationFrame(function () { rafPending = false; if (S._seam) place(S._seam); });
1463
- }
1464
- if ('ResizeObserver' in window) {
1465
- try {
1466
- S._ro = new ResizeObserver(onResize);
1467
- S._observeWC = function () {
1468
- try {
1469
- var wc = doc.querySelector('#coda-result .coda-player .waveform-container');
1470
- if (wc && wc !== S._wc) { if (S._wc) S._ro.unobserve(S._wc); S._ro.observe(wc); S._wc = wc; }
1471
- } catch (e) {}
1472
- };
1473
- } catch (e) { S._observeWC = function () {}; }
1474
- } else { window.addEventListener('resize', onResize, { passive: true }); S._observeWC = function () {}; }
1475
- function boot() {
1476
- var result = doc.getElementById('coda-result');
1477
- if (!result) { setTimeout(boot, 250); return; }
1478
- if (S._mo) return;
1479
- var t = null;
1480
- S._mo = new MutationObserver(function () {
1481
- if (t) return; t = setTimeout(function () { t = null; S._observeWC(); sync(); }, 60);
1482
- });
1483
- S._mo.observe(result, { childList: true, subtree: true, attributes: true,
1484
- attributeFilter: ['data-seam', 'data-total'] });
1485
- S._observeWC(); sync();
1486
- }
1487
- boot();
1488
- })();
1489
 
1490
  /* ===== FEATURE C — CINEMATIC ONBOARDING ===== */
1491
  (function featureC() {
@@ -1506,14 +1543,9 @@ CODA_INIT_JS = """
1506
  host.setAttribute('aria-hidden', 'true'); host.remove();
1507
  } catch (e) {}
1508
  }
1509
- var seenFlag = false;
1510
- try { seenFlag = hasLS && localStorage.getItem(KEY) === '1'; } catch (e) {}
1511
- if (seenFlag) {
1512
- var existing = doc.getElementById('coda-intro-overlay');
1513
- if (existing) hardRemove(existing);
1514
- wireReplay();
1515
- return;
1516
- }
1517
  var tries = 0;
1518
  function boot() {
1519
  var host = doc.getElementById('coda-intro-overlay');
@@ -1521,9 +1553,7 @@ CODA_INIT_JS = """
1521
  init(host);
1522
  }
1523
  function init(host) {
1524
- var seen = false;
1525
- try { seen = hasLS && localStorage.getItem(KEY) === '1'; } catch (e) {}
1526
- if (seen) { hardRemove(host); wireReplay(); return; }
1527
  try { if (host.parentNode !== doc.body) doc.body.appendChild(host); } catch (e) {}
1528
  try { doc.body.classList.add('coda-intro-lock'); } catch (e) {}
1529
  host.classList.add('coda-intro-on');
@@ -1543,7 +1573,7 @@ CODA_INIT_JS = """
1543
  if (done) return; done = true;
1544
  hardRemove(host);
1545
  try { doc.body.classList.remove('coda-intro-lock'); } catch (e) {}
1546
- try { if (hasLS) localStorage.setItem(KEY, '1'); } catch (e) {}
1547
  }
1548
  host.addEventListener('transitionend', finish, { once: true });
1549
  setTimeout(finish, 750);
@@ -1713,16 +1743,17 @@ with gr.Blocks(title="CODA", head=HEAD_SCRIPT) as app:
1713
  waveform_options=gr.WaveformOptions(
1714
  waveform_color="#3a3060",
1715
  waveform_progress_color="#a98bff"))
 
 
 
 
 
 
1716
  # built visible with an empty value (an empty Markdown renders
1717
  # nothing). finish_song fills it in the SAME event that sets the
1718
  # audio, so there's no second `.then` to toggle visibility — one
1719
  # event = one completion signal, so the spinner always clears.
1720
  summary_md = gr.Markdown(elem_classes="coda-summary")
1721
- # hidden seam-data carrier: finish_song fills this with a
1722
- # <div id='coda-seam-data' data-seam data-total> on reveal; the
1723
- # Feature-B JS (MutationObserver on #coda-result) reads it to draw
1724
- # the before/after seam marker + "play from the seam" control.
1725
- seam_data = gr.HTML("", elem_id="coda-seam-host")
1726
 
1727
  gr.HTML(
1728
  "<div id='coda-foot'>Demo clip: <strong>Track0000</strong> — recorded "
 
121
  f"<div class='coda-hud-note'>{body}</div></div>")
122
 
123
 
124
+ def _listening_html():
125
+ """Instant 'CODA is reading your clip' state for the readout, shown the moment
126
+ a clip lands so the panel lights up immediately instead of sitting greyed-out
127
+ until the (CPU-bound) key/tempo analysis finishes. Replaced in-place by the
128
+ real fingerprint readout a beat later."""
129
+ return ("<div class='coda-hud coda-hud-listening'>"
130
+ "<div class='coda-hud-head'><span class='coda-hud-dot'></span>"
131
+ "CODA&nbsp;HEARD<span class='coda-hud-live'>LISTENING…</span></div>"
132
+ "<div class='coda-hud-scan'><span></span></div>"
133
+ "<div class='coda-hud-note'>Reading key, tempo and groove from your "
134
+ "clip…</div></div>")
135
+
136
+
137
  # the live "what is CODA doing right now" overlay shown over the result panel
138
  # during generation. finish_song yields these at each real pipeline milestone.
139
  _STAGE_STEPS = ["listening", "composing", "splicing"]
 
159
  "</div>")
160
 
161
 
162
+ def _mmss(seconds):
163
+ s = int(round(max(float(seconds), 0.0)))
164
+ return f"{s // 60}:{s % 60:02d}"
165
+
166
+
167
  def _seam_html(source_seconds, total):
168
+ """A self-contained before/after 'composition ribbon' for the finished track:
169
+ one bar split at the EXACT seam YOUR PART on the left, CODA's continuation
170
+ on the right with the join marked and labelled.
171
+
172
+ Rendered SERVER-SIDE from the precise numbers finish_song already computed, so
173
+ the marker is always exactly where CODA begins. The old approach measured the
174
+ WaveSurfer waveform in the browser and drifted/stuck (the 'needle in the wrong
175
+ place' bug); this can't, because the split is baked into the HTML. Purely
176
+ cosmetic — does NOT touch enhance -> fingerprint -> continue_audio -> stitch."""
177
+ src = max(float(source_seconds), 0.0)
178
+ tot = max(float(total), src + 0.001)
179
+ pct = max(0.0, min(src / tot, 1.0)) * 100.0
180
+ added = max(tot - src, 0.0)
181
+ return (
182
+ "<div class='coda-seam-ribbon'>"
183
+ "<div class='coda-seam-cap'>"
184
+ "<span>The finished track</span>"
185
+ f"<span class='coda-seam-cap-tot'>{_mmss(tot)} total</span>"
186
+ "</div>"
187
+ "<div class='coda-seam-track'>"
188
+ f"<div class='coda-seam-seg coda-seam-seg-yours' style='width:{pct:.3f}%'>"
189
+ "<span class='coda-seam-seg-lab'>YOUR&nbsp;PART</span></div>"
190
+ f"<div class='coda-seam-seg coda-seam-seg-coda' style='width:{100.0 - pct:.3f}%'>"
191
+ "<span class='coda-seam-seg-lab'>CODA</span></div>"
192
+ f"<div class='coda-seam-join' style='left:{pct:.3f}%'></div>"
193
+ "</div>"
194
+ "<div class='coda-seam-foot'>"
195
+ f"<span class='coda-seam-foot-y'>Your original&nbsp;·&nbsp;{_mmss(src)}</span>"
196
+ f"<span class='coda-seam-foot-c'>CODA added&nbsp;·&nbsp;+{_mmss(added)}</span>"
197
+ "</div>"
198
+ "</div>")
199
 
200
 
201
  def analyze_on_upload(audio_path):
202
  """Fast CPU-only pass the instant a clip loads, so the user sees what CODA
203
+ heard immediately instead of a dead screen.
204
+
205
+ A GENERATOR so the readout lights up the MOMENT a clip lands: it yields a
206
+ 'listening…' state first, then replaces it with the real key/tempo readout
207
+ once the (CPU-bound) analysis completes. That kills the 'CODA heard stays
208
+ greyed until you generate' feel on slower hosts. Does NOT touch the
209
+ generation pipeline — it only drives the readout panel + the Finish button's
210
+ armed state."""
211
  if not audio_path:
212
+ yield gr.update(value="", visible=False), gr.update(interactive=False)
213
+ return
214
+ # instant feedback — panel lights up before the analysis even starts
215
+ yield gr.update(value=_listening_html(), visible=True), gr.update(interactive=False)
216
  try:
217
  listen_path = enhance_to_tempfile(audio_path)
218
  info = fingerprint(listen_path)
 
227
  f"{engine.MAX_SOURCE_SECONDS:.0f}s (Stable Audio 3's "
228
  f"{engine.MAX_TOTAL_SECONDS:.0f}s total cap). Trim it shorter and "
229
  f"re-upload.")
230
+ yield gr.update(value=msg, visible=True), gr.update(interactive=False)
231
+ return
232
  html = _readout_html(info, quality)
233
+ yield gr.update(value=html, visible=True), gr.update(interactive=True)
234
  except Exception as e:
235
  print(f"[coda] analysis failed ({e})", flush=True)
236
+ yield (gr.update(value=_warn_html("Couldn't read that file", str(e)),
237
+ visible=True),
238
+ gr.update(interactive=False))
239
 
240
 
241
  @spaces.GPU(duration=120)
 
1217
  .coda-intro-beat, #coda-intro-eq{ opacity:1 !important; transform:none !important; filter:none !important; }
1218
  #coda-intro-overlay{ transition:opacity .2s linear; }
1219
  }
1220
+
1221
+ /* ===================================================================== */
1222
+ /* CODA UI FIXES (additive) — server-rendered before/after ribbon, full */
1223
+ /* waveform, instant 'listening' readout, and elegance polish. */
1224
+ /* ===================================================================== */
1225
+
1226
+ /* ---------- before/after COMPOSITION RIBBON (server-rendered, exact) ----------
1227
+ replaces the old client-side seam overlay that drifted/stuck on the waveform.
1228
+ The split is baked in as inline width/left %, so the join is ALWAYS exactly
1229
+ where CODA begins. */
1230
+ .coda-seam-ribbon{
1231
+ margin:16px 2px 2px; font-family:'Space Grotesk',sans-serif;
1232
+ animation:codaSeamRibbonIn .7s var(--ease-expo) both;
1233
+ }
1234
+ @keyframes codaSeamRibbonIn{ from{opacity:0; transform:translateY(7px)} to{opacity:1; transform:none} }
1235
+ .coda-seam-cap{
1236
+ display:flex; justify-content:space-between; align-items:baseline;
1237
+ font-size:.6rem; letter-spacing:.18em; text-transform:uppercase;
1238
+ color:var(--sub); margin:0 2px 8px; font-weight:600;
1239
+ }
1240
+ .coda-seam-cap-tot{
1241
+ font-family:'JetBrains Mono',ui-monospace,monospace; color:#cfd6f5;
1242
+ letter-spacing:.05em; font-variant-numeric:tabular-nums;
1243
+ }
1244
+ .coda-seam-track{
1245
+ position:relative; display:flex; height:38px; border-radius:11px; overflow:hidden;
1246
+ border:1px solid rgba(150,160,220,.16);
1247
+ box-shadow:inset 0 1px 4px rgba(0,0,0,.55), inset 0 -1px 0 rgba(255,255,255,.04);
1248
+ }
1249
+ .coda-seam-seg{
1250
+ display:flex; align-items:center; min-width:0; padding:0 13px; overflow:hidden;
1251
+ position:relative; transition:width .5s var(--ease-expo);
1252
+ }
1253
+ .coda-seam-seg-yours{
1254
+ justify-content:flex-start;
1255
+ background:linear-gradient(180deg, rgba(111,224,245,.13), rgba(111,224,245,.04));
1256
+ }
1257
+ .coda-seam-seg-coda{
1258
+ justify-content:flex-end;
1259
+ background:linear-gradient(180deg, rgba(246,214,138,.18), rgba(246,214,138,.06));
1260
+ }
1261
+ .coda-seam-seg-lab{ font-size:.62rem; font-weight:600; letter-spacing:.16em; white-space:nowrap; }
1262
+ .coda-seam-seg-yours .coda-seam-seg-lab{ color:var(--cyan); text-shadow:0 0 10px rgba(111,224,245,.3); }
1263
+ .coda-seam-seg-coda .coda-seam-seg-lab{ color:var(--gold); text-shadow:0 0 10px rgba(246,214,138,.35); }
1264
+ .coda-seam-join{
1265
+ position:absolute; top:0; bottom:0; width:2px; transform:translateX(-1px);
1266
+ background:linear-gradient(180deg, rgba(246,214,138,.25), var(--gold) 22%, var(--gold) 78%, rgba(246,214,138,.25));
1267
+ box-shadow:0 0 10px rgba(246,214,138,.7), 0 0 2px rgba(246,214,138,.9);
1268
+ transform-origin:center; animation:codaSeamWipe .7s var(--ease-expo) .25s both;
1269
+ }
1270
+ .coda-seam-join::before, .coda-seam-join::after{
1271
+ content:''; position:absolute; left:50%; width:7px; height:7px;
1272
+ transform:translateX(-50%) rotate(45deg); background:var(--gold); border-radius:1px;
1273
+ box-shadow:0 0 8px rgba(246,214,138,.8);
1274
+ }
1275
+ .coda-seam-join::before{ top:-3px; }
1276
+ .coda-seam-join::after{ bottom:-3px; }
1277
+ .coda-seam-foot{
1278
+ display:flex; justify-content:space-between; margin:9px 2px 0;
1279
+ font-family:'JetBrains Mono',ui-monospace,monospace; font-size:.66rem;
1280
+ font-variant-numeric:tabular-nums; letter-spacing:.02em;
1281
+ }
1282
+ .coda-seam-foot-y{ color:var(--cyan); }
1283
+ .coda-seam-foot-c{ color:var(--gold); }
1284
+
1285
+ /* ---------- OUTPUT WAVEFORM: show it in full (was clipped ~15px) ----------
1286
+ WaveSurfer draws taller than Gradio's default container, so the lower part of
1287
+ the waveform was bleeding into the timestamps and looking 'cut off'. Give it
1288
+ real vertical room and let it breathe. */
1289
+ .coda-player .component-wrapper{ overflow:visible; }
1290
+ .coda-player .waveform-container{
1291
+ min-height:90px !important; align-items:center !important; overflow:visible !important;
1292
+ }
1293
+
1294
+ /* ---------- 'CODA HEARD' instant LISTENING state ----------
1295
+ the readout lights up the moment a clip lands, before key/tempo analysis is
1296
+ done — so it never sits greyed-out waiting on generation. */
1297
+ .coda-hud-listening .coda-hud-live{
1298
+ color:var(--gold); border-color:rgba(246,214,138,.3); background:rgba(246,214,138,.06);
1299
+ }
1300
+ .coda-hud-scan{
1301
+ height:6px; margin:2px 0 12px; border-radius:3px; overflow:hidden;
1302
+ background:rgba(0,0,0,.42); box-shadow:inset 0 1px 2px rgba(0,0,0,.6);
1303
+ }
1304
+ .coda-hud-scan span{
1305
+ display:block; height:100%; width:36%; border-radius:3px;
1306
+ background:linear-gradient(90deg, transparent, var(--cyan), transparent);
1307
+ animation:codaHudScan 1.15s var(--ease-expo) infinite;
1308
+ }
1309
+ @keyframes codaHudScan{ 0%{transform:translateX(-120%)} 100%{transform:translateX(380%)} }
1310
+
1311
+ /* ---------- ELEGANCE POLISH ---------- */
1312
+ .gradio-container *{ -webkit-tap-highlight-color:transparent; }
1313
+ /* one consistent, soft focus ring instead of the browser default */
1314
+ .gradio-container button:focus-visible,
1315
+ .gradio-container [tabindex]:focus-visible,
1316
+ .coda-vibe textarea:focus-visible{
1317
+ outline:none !important;
1318
+ box-shadow:0 0 0 2px rgba(111,224,245,.5), 0 0 18px rgba(111,224,245,.25) !important;
1319
+ }
1320
+ /* quiet utility key easing (was a generic 'all .25s ease') */
1321
+ .coda-demo{ transition:color .25s var(--ease-expo), border-color .25s var(--ease-expo),
1322
+ background .25s var(--ease-expo), box-shadow .25s var(--ease-expo) !important; }
1323
+ /* the finished summary settles in gently with the reveal rather than popping */
1324
+ .coda-summary h3, .coda-summary p{ animation:codaSeamRibbonIn .6s var(--ease-expo) both; }
1325
+ @media (prefers-reduced-motion: reduce){
1326
+ .coda-seam-ribbon, .coda-seam-join, .coda-seam-seg, .coda-hud-scan span,
1327
+ .coda-summary h3, .coda-summary p{
1328
+ animation:none !important; transition:none !important;
1329
+ }
1330
+ .coda-hud-scan{ display:none; }
1331
+ }
1332
  """
1333
 
1334
  # ethereal hero spectrum: a frequency-spectrum bar field with a centered
 
1517
  W.heroSync = sync;
1518
  }
1519
 
1520
+ /* ===== FEATURE B — BEFORE/AFTER SEAM REVEAL =====
1521
+ * The before/after split (YOUR PART | CODA + the join marker) is now rendered
1522
+ * SERVER-SIDE as a self-contained ribbon (see _seam_html in app.py) with the
1523
+ * exact proportions baked into inline styles. That removes the old, fragile
1524
+ * client-side path that measured WaveSurfer's waveform and could leave the
1525
+ * marker stuck in the wrong place. No JS needed here anymore. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1526
 
1527
  /* ===== FEATURE C — CINEMATIC ONBOARDING ===== */
1528
  (function featureC() {
 
1543
  host.setAttribute('aria-hidden', 'true'); host.remove();
1544
  } catch (e) {}
1545
  }
1546
+ /* Tony's call: the 2016 intro replays on EVERY page load, not just the
1547
+ * first. So there is NO localStorage "seen" gate we always boot it.
1548
+ * Skip / Esc / click-outside / auto-advance still dismiss it per load. */
 
 
 
 
 
1549
  var tries = 0;
1550
  function boot() {
1551
  var host = doc.getElementById('coda-intro-overlay');
 
1553
  init(host);
1554
  }
1555
  function init(host) {
1556
+ /* no seen-gate: the intro plays every load (see featureC note above) */
 
 
1557
  try { if (host.parentNode !== doc.body) doc.body.appendChild(host); } catch (e) {}
1558
  try { doc.body.classList.add('coda-intro-lock'); } catch (e) {}
1559
  host.classList.add('coda-intro-on');
 
1573
  if (done) return; done = true;
1574
  hardRemove(host);
1575
  try { doc.body.classList.remove('coda-intro-lock'); } catch (e) {}
1576
+ /* intentionally do NOT persist a 'seen' flag replay every load */
1577
  }
1578
  host.addEventListener('transitionend', finish, { once: true });
1579
  setTimeout(finish, 750);
 
1743
  waveform_options=gr.WaveformOptions(
1744
  waveform_color="#3a3060",
1745
  waveform_progress_color="#a98bff"))
1746
+ # before/after composition ribbon, sat right under the player.
1747
+ # finish_song fills it on reveal with a fully self-contained,
1748
+ # server-computed YOUR PART | CODA split (see _seam_html) — the
1749
+ # marker is always exactly where CODA begins, no client-side
1750
+ # waveform measuring. Empty until a result lands.
1751
+ seam_data = gr.HTML("", elem_id="coda-seam-host")
1752
  # built visible with an empty value (an empty Markdown renders
1753
  # nothing). finish_song fills it in the SAME event that sets the
1754
  # audio, so there's no second `.then` to toggle visibility — one
1755
  # event = one completion signal, so the spinner always clears.
1756
  summary_md = gr.Markdown(elem_classes="coda-summary")
 
 
 
 
 
1757
 
1758
  gr.HTML(
1759
  "<div id='coda-foot'>Demo clip: <strong>Track0000</strong> — recorded "