Spaces:
Paused
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).
|
@@ -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 |
-
"""
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 178 |
html = _readout_html(info, quality)
|
| 179 |
-
|
| 180 |
except Exception as e:
|
| 181 |
print(f"[coda] analysis failed ({e})", flush=True)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 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 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 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">▶</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 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 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 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 · {_mmss(src)}</span>"
|
| 196 |
+
f"<span class='coda-seam-foot-c'>CODA added · +{_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 "
|