"""CODA — finish the song you quit on.
Upload a short, unfinished music clip. CODA listens to it (key, tempo, meter),
then continues it into a longer, finished-sounding track in the same feel using
Stable Audio 3 Small Music — a single native audio-continuation call, 44.1 kHz
stereo — and splices the new part seamlessly onto your pristine original with a
level-matched crossfade and a clean closing fade.
Deliberately one job, done well. No lyrics generator, no cover-art printer — just
a real, listenable continuation of your clip.
"""
# stable-audio-tools hard-pins torch==2.7.1 in its pyproject.toml; ZeroGPU only
# accepts torch 2.8.0/2.9.1/2.10.0/2.11.0. We install the package without its
# dependency tree so the ZeroGPU-managed torch in the env is used instead.
import subprocess as _sp, sys as _sys
try:
import stable_audio_tools as _ # noqa: F401
del _
except ImportError:
_sp.check_call([_sys.executable, "-m", "pip", "install",
"--no-cache-dir", "--no-deps", "-q",
"git+https://github.com/Stability-AI/stable-audio-tools.git"])
del _sp, _sys
import os
import tempfile
import gradio as gr
import librosa
import numpy as np
import soundfile as sf
import engine
import stitch
from analyze import fingerprint
from enhance import enhance_audio, enhance_to_tempfile, input_quality
# ZeroGPU shim: on a Space `spaces` exists and `@spaces.GPU` attaches a GPU for
# the duration of the call. Locally we no-op so the app still runs.
try:
import spaces
except ImportError:
class _FakeSpaces:
def GPU(self, fn=None, **kw):
return fn if fn else (lambda f: f)
spaces = _FakeSpaces()
# bundled demo: a low-quality phone capture of an unfinished song by the band
# PUSHBACK (shared via TikTok). lo-fi in, finished-sounding out.
PUSHBACK_DEMO = os.path.join(os.path.dirname(__file__),
"examples", "pushback_demo.mp3")
PUSHBACK_CREDIT = ("Demo clip: **PUSHBACK** (via TikTok), used with thanks. "
"Bring your own clip to finish your own song.")
# total finished length. SA3 Small generates up to 120 s in one call, so this is
# a *total length* control (clip + continuation), not a "seconds to add" knob.
MIN_TOTAL, MAX_TOTAL, DEFAULT_TOTAL = 30, 120, 60
# SA3's weights are gated under the Stability Community License, so the download
# 401s unless the request is authenticated. Log in with the HF_TOKEN secret
# (which must come from an account that accepted the licence on the model page)
# BEFORE preload() pulls the weights. No-op when unset, so local runs that have
# already done `huggingface-cli login` still work.
_HF_TOKEN = os.environ.get("HF_TOKEN")
if _HF_TOKEN:
try:
from huggingface_hub import login as _hf_login
_hf_login(token=_HF_TOKEN)
print("[coda] authenticated to the HF Hub via HF_TOKEN", flush=True)
except Exception as _e:
print(f"[coda] HF login failed ({_e}); gated model download may 401",
flush=True)
elif os.environ.get("SPACE_ID"):
print("[coda] WARNING: no HF_TOKEN secret set — the gated SA3 weights will "
"401. Add HF_TOKEN in the Space settings (Settings -> Variables and "
"secrets) using a token from an account that accepted the licence.",
flush=True)
# On a Space, pull weights into CPU RAM at boot so the GPU window is spent
# generating, not reading 3 GB off disk. `spaces` defers CUDA placement until
# the first @spaces.GPU call.
if os.environ.get("SPACE_ID"):
try:
engine.preload()
except Exception as _e:
print(f"[coda] preload failed ({_e}); will lazy-load", flush=True)
def _readout_html(info, quality):
"""The 'CODA heard' fingerprint as a premium audio-tool readout (a HUD of
labelled value cells), not a text dump. Rendered into a gr.HTML panel."""
cells = [
("KEY", str(info["key"]), ""),
("TEMPO", str(info["bpm"]), "BPM"),
("METER", str(info["time_signature"]), ""),
("LENGTH", f"{info['duration']}", "S"),
]
cell_html = "".join(
f"
"
f"
{k}
"
f"
{v}{u}
"
f"
"
for (k, v, u) in cells)
note = ""
if quality and quality.get("lofi"):
note = (f"
"
f"LO-FI ~{quality['bandwidth_hz']/1000:.0f}KHZ"
f"CODA cleans a copy before it listens, so it follows the "
f"song, not the hiss
")
return (
"
"
"
"
"CODA HEARDANALYZED
"
f"
{cell_html}
"
f"{note}
")
def _warn_html(title, body):
"""A clip-rejected / read-error message styled to match the readout HUD."""
return ("
"
f"
{title}
"
f"
{body}
")
# the live "what is CODA doing right now" overlay shown over the result panel
# during generation. finish_song yields these at each real pipeline milestone.
_STAGE_STEPS = ["listening", "composing", "splicing"]
def _stage_html(phase, label, sub=""):
bars = "".join("" for _ in range(9))
steps = "".join(
f"= i else ''}"
f"{' now' if _STAGE_STEPS.index(phase) == i else ''}'>"
if phase in _STAGE_STEPS else ""
for i in range(len(_STAGE_STEPS)))
return (
f"
"
"
"
""
""
f"
{bars}
"
"
"
f"
{label}
"
f"
{sub}
"
f"
{steps}
"
"
")
def analyze_on_upload(audio_path):
"""Fast CPU-only pass the instant a clip loads, so the user sees what CODA
heard immediately instead of a dead screen."""
if not audio_path:
return gr.update(value="", visible=False), gr.update(interactive=False)
try:
listen_path = enhance_to_tempfile(audio_path)
info = fingerprint(listen_path)
quality = input_quality(audio_path)
# SA3 continues up to a 120s total, so a clip needs headroom for at
# least MIN_NEW seconds of new audio. Block over-long clips here, with a
# clear message, instead of failing at generation time.
if info["duration"] > engine.MAX_SOURCE_SECONDS:
msg = _warn_html(
"Clip too long",
f"That clip is {info['duration']:.0f}s. CODA continues clips up to "
f"{engine.MAX_SOURCE_SECONDS:.0f}s (Stable Audio 3's "
f"{engine.MAX_TOTAL_SECONDS:.0f}s total cap). Trim it shorter and "
f"re-upload.")
return gr.update(value=msg, visible=True), gr.update(interactive=False)
html = _readout_html(info, quality)
return gr.update(value=html, visible=True), gr.update(interactive=True)
except Exception as e:
print(f"[coda] analysis failed ({e})", flush=True)
return (gr.update(value=_warn_html("Couldn't read that file", str(e)),
visible=True),
gr.update(interactive=False))
@spaces.GPU(duration=120)
def _continue_on_gpu(listen_path, total_seconds, vibe):
"""ONLY the SA3 diffusion call runs inside the GPU window. All CPU work —
enhancement, key/tempo analysis, decode, splice — happens OUTSIDE it (in
`finish_song`), so the scarce ZeroGPU allocation is spent generating instead
of decoding/analyzing audio. That's the stream-abort fix: the GPU task now
starts and finishes fast instead of sitting through a slow analysis preamble
until the browser aborts the stream.
Do NOT pin a magic seed. The lab's verified seed=7 take was made on torch
2.7.1; ZeroGPU runs a different torch (2.8–2.11), so the same seed reproduces
a *different* draw — on the deployed build, the bad "loud random synth noise"
one. Instead, leave the seed unset (engine draws several candidates and keeps
the cleanest by an artifact score), which is robust across torch builds.
"""
return engine.continue_audio(
listen_path, total_seconds=int(total_seconds),
prompt=(vibe or "").strip())
def finish_song(audio_path, total_seconds, vibe, remaster,
progress=gr.Progress()):
"""Orchestrate the job: CPU prep -> GPU continuation -> CPU splice.
A GENERATOR so the UI gets a real, dramatic, server-synced wait state: it
yields a live 'stage' overlay (gr.HTML) at each genuine pipeline milestone —
listening / composing / splicing — and finally yields the finished audio +
summary while clearing the overlay. This is still ONE event (the spinner
clears on StopIteration), so it keeps the single-completion property; the
earlier 'stuck spinner' risk was a CHAINED second `.then` event, which this
is not. The exact pipeline contract and call order are unchanged:
enhance_to_tempfile -> fingerprint -> engine.continue_audio -> stitch.stitch.
Yields/returns 3-tuples for (output_audio, summary_md, stage_html)."""
if not audio_path:
raise gr.Error("Upload a clip (or load the PUSHBACK demo) first.")
total_seconds = int(total_seconds)
try:
# --- BEAT: listening (CPU prep, outside the GPU window) ---
progress(0.05, desc="Listening to your clip…")
yield gr.update(), gr.update(), _stage_html(
"listening", "Listening to your clip",
"reading key · tempo · groove")
listen_path = enhance_to_tempfile(audio_path)
info = fingerprint(listen_path)
# the pristine original — what the listener hears for the first stretch
original, sr = librosa.load(audio_path, sr=None, mono=False)
if remaster:
progress(0.15, desc="Remastering your part…")
original = enhance_audio(original, sr)
# --- BEAT: composing (holds during the ONLY @spaces.GPU call) ---
progress(0.35, desc="Composing the continuation…")
yield gr.update(), gr.update(), _stage_html(
"composing", "Composing the continuation",
f"Stable Audio 3 · extending in {info['key']} at {info['bpm']} BPM")
try:
new_tail, source_seconds, SR = _continue_on_gpu(
listen_path, total_seconds, vibe)
except ValueError as e:
# e.g. the clip is a full-length track, not a clip to continue
raise gr.Error(str(e))
# --- BEAT: splicing (CPU splice + write, outside the GPU window) ---
progress(0.9, desc="Splicing onto your original…")
yield gr.update(), gr.update(), _stage_html(
"splicing", "Splicing onto your original",
"level-matched crossfade · clean closing fade")
out = stitch.stitch(original, sr, new_tail, source_seconds)
out_path = os.path.join(tempfile.mkdtemp(), "coda_finished.wav")
sf.write(out_path, out.T, SR, subtype="PCM_16")
progress(1.0, desc="Done.")
total = out.shape[-1] / SR
added = total - source_seconds
vibe_note = f" guided by *“{vibe.strip()}”*" if (vibe or "").strip() else ""
summary = (
f"### Finished — {total:.0f}s\n"
f"Your **{source_seconds:.0f}s** clip in **{info['key']}** at "
f"**{info['bpm']} BPM** continued for **~{added:.0f}s** more{vibe_note}, "
f"then crossfaded onto your original and faded to a clean close.\n\n"
f"*Stable Audio 3 generated the continuation as 44.1 kHz stereo in a "
f"single pass; your original recording plays untouched up to the seam.*"
)
# --- BEAT: reveal (set audio + summary, clear the overlay) ---
yield out_path, summary, ""
except gr.Error:
# clear the overlay so a failure never leaves a stuck 'composing' animation
yield gr.update(), gr.update(), ""
raise
def load_demo():
"""Load the PUSHBACK demo clip into the uploader."""
return PUSHBACK_DEMO
# --- ethereal "dark studio" theme --------------------------------------------
THEME = gr.themes.Base(
primary_hue=gr.themes.colors.cyan,
secondary_hue=gr.themes.colors.purple,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Space Grotesk"),
gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
).set(
body_background_fill="#070710",
body_background_fill_dark="#070710",
background_fill_primary="rgba(255,255,255,0)",
background_fill_secondary="rgba(255,255,255,0)",
block_background_fill="rgba(255,255,255,0.025)",
block_border_color="rgba(150,160,220,0.10)",
block_border_width="1px",
block_radius="16px",
block_label_background_fill="rgba(0,0,0,0)",
block_label_text_color="#9aa3bd",
block_title_text_color="#9aa3bd",
body_text_color="#e9ecf8",
body_text_color_subdued="#9aa3bd",
button_primary_background_fill="linear-gradient(100deg,#6fe0f5,#a98bff)",
button_primary_background_fill_hover="linear-gradient(100deg,#7be8ff,#b89bff)",
button_primary_text_color="#0a0a16",
color_accent_soft="rgba(111,224,245,0.10)",
border_color_accent="#6fe0f5",
input_background_fill="rgba(10,11,22,0.55)",
input_border_color="rgba(150,160,220,0.12)",
slider_color="#6fe0f5",
)
CSS = """
/* ===================================================================== CODA — premium audio-instrument treatment. This is a COMPLETE replacement for the CSS string in app.py. It keeps every existing ambient layer / hero / HUD / stage rule that already works, retargets only REAL class names (verified against the app.py source + the Gradio 6.17 StaticAudio/Upload compiled assets), and adds the milled-metal light model, the LCD power-on, the milled controls, the illuminated key states, and the gold MASTER reveal. No new font import (theme already loads Space Grotesk + Inter + JetBrains Mono). ===================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap');
:root{
--cyan:#6fe0f5; --violet:#a98bff; --gold:#f6d68a;
--text:#e9ecf8; --sub:#9aa3bd;
--glass:rgba(18,20,36,0.55);
--glass-brd:rgba(150,160,220,0.14);
--mx:50%; --my:28%;
/* milled-metal light model (cheap, no textures) */
--bevel-hi:rgba(255,255,255,.06);
--bevel-lo:rgba(0,0,0,.55);
--well:linear-gradient(180deg,rgba(0,0,0,.34),rgba(0,0,0,.16));
--ring:rgba(150,160,220,.14);
--ring-hot:rgba(111,224,245,.45);
--ease-expo:cubic-bezier(.16,1,.3,1);
--ink:#0a0a16;
}
/* ---------- ambient background layers (UNCHANGED — already GOOD) ---------- */
.coda-aurora,.coda-glow,.coda-particles{ position:fixed; inset:0; pointer-events:none; overflow:hidden; }
.coda-aurora{
z-index:-3;
background:
radial-gradient(38% 48% at 18% 22%, rgba(124,92,255,.20), transparent 62%),
radial-gradient(44% 52% at 84% 28%, rgba(64,200,235,.16), transparent 62%),
radial-gradient(52% 46% at 50% 96%, rgba(246,214,138,.10), transparent 60%),
radial-gradient(40% 42% at 72% 78%, rgba(169,139,255,.14), transparent 62%);
filter:saturate(125%);
animation:codaAurora 26s ease-in-out infinite alternate;
}
@keyframes codaAurora{
0%{transform:translate3d(0,0,0) scale(1)}
50%{transform:translate3d(2.5%,-1.8%,0) scale(1.07)}
100%{transform:translate3d(-1.8%,1.4%,0) scale(1.03)}
}
.coda-glow{
z-index:-2;
background:radial-gradient(420px 420px at var(--mx) var(--my),
rgba(111,224,245,.08), transparent 68%);
transition:background .25s ease;
}
.coda-particles{ z-index:-1; }
.coda-particles span{
position:absolute; bottom:-12px; border-radius:50%;
background:radial-gradient(circle, rgba(190,205,255,.95), rgba(190,205,255,0) 70%);
opacity:0; animation-name:codaFloat; animation-timing-function:linear;
animation-iteration-count:infinite;
}
@keyframes codaFloat{
0%{transform:translateY(0) scale(.5); opacity:0}
12%{opacity:.7} 88%{opacity:.45}
100%{transform:translateY(-108vh) scale(1.05); opacity:0}
}
/* ---------- shell ---------- */
.gradio-container{ max-width:1080px !important; margin:0 auto !important; }
.gradio-container, .gradio-container *{ font-family:'Inter',ui-sans-serif,system-ui; }
/* ---------- hero (wordmark + spectrum power-on) ---------- */
#coda-head{ text-align:center; padding:40px 0 4px; position:relative; }
#coda-title{
font-family:'Space Grotesk',sans-serif; font-weight:700;
font-size:clamp(3rem,9vw,4.6rem); letter-spacing:.20em; margin:0 0 .2rem;
background:linear-gradient(100deg,#6fe0f5 0%,#a98bff 46%,#f6d68a 100%);
background-size:220% auto; -webkit-background-clip:text; background-clip:text;
-webkit-text-fill-color:transparent; color:transparent;
filter:drop-shadow(0 0 38px rgba(150,140,255,.30));
animation:codaShine 9s linear infinite;
}
@keyframes codaShine{ to{ background-position:220% center } }
#coda-tagline{
color:var(--sub); text-transform:uppercase; letter-spacing:.34em;
font-size:.78rem; margin:.2rem 0 0; font-weight:500;
}
#coda-eq{
display:flex; gap:3px; align-items:center; justify-content:center;
height:46px; max-width:440px; margin:22px auto 4px; opacity:.92;
}
#coda-eq span{
flex:1 1 0; max-width:5px; height:100%; border-radius:3px;
transform-origin:center; transform:scaleY(.2);
background:linear-gradient(180deg,#7be8ff,#a98bff);
box-shadow:0 0 9px rgba(111,224,245,.45);
/* one-shot power-on, THEN the infinite breathing bar (additive list) */
animation:codaPowerOn .7s var(--ease-expo) both, codaBar 1.2s ease-in-out infinite .7s;
animation-name:codaPowerOn, codaBar;
}
@keyframes codaPowerOn{ from{transform:scaleY(.04); opacity:.25} to{opacity:.92} }
@keyframes codaBar{ 0%,100%{transform:scaleY(.16)} 50%{transform:scaleY(1)} }
#coda-rule{
height:1px; border:0; max-width:260px; margin:18px auto 6px;
background:linear-gradient(90deg,transparent,var(--cyan),var(--violet),transparent);
opacity:.6;
}
#coda-intro{ text-align:center; color:var(--sub); max-width:680px;
margin:0 auto 6px; font-size:1rem; line-height:1.6; }
#coda-intro strong{ color:#dfe4ff; font-weight:600; }
/* ---------- glass panels -> milled faceplates ---------- */
.coda-glass, .coda-card{
background:var(--glass) !important;
border:1px solid var(--glass-brd) !important;
border-radius:20px !important;
backdrop-filter:blur(16px) saturate(135%);
-webkit-backdrop-filter:blur(16px) saturate(135%);
/* light model: top-bevel lip + recessed body + outer contact drop */
box-shadow:
inset 0 1px 0 var(--bevel-hi),
inset 0 -1px 0 var(--bevel-lo),
0 12px 40px rgba(0,0,0,.5) !important;
padding:22px !important; position:relative;
}
.coda-glass{ animation:codaRise .85s var(--ease-expo) .55s both; }
.coda-card{ animation:codaRise .85s var(--ease-expo) .63s both; }
@keyframes codaRise{ from{opacity:0; transform:translateY(16px) scale(.99)} to{opacity:1; transform:none} }
/* section micro-labels (engraved legend + live status lamp) */
.coda-label{
color:var(--cyan); text-transform:uppercase; letter-spacing:.18em;
font-size:.72rem; font-weight:600; margin:0 0 10px; display:flex;
align-items:center; gap:.5rem;
}
.coda-label::before{
content:''; width:7px; height:7px; border-radius:50%;
background:var(--cyan); box-shadow:0 0 10px var(--cyan);
animation:codaPulse 2.4s ease-in-out infinite;
}
@keyframes codaPulse{ 0%,100%{opacity:.5; transform:scale(.85)} 50%{opacity:1; transform:scale(1.15)} }
/* blend inner gradio blocks into the glass; mute redundant helper text */
.coda-glass .block, .coda-card .block{ box-shadow:none !important; background:transparent !important; }
.coda-glass .info, .coda-card .info{ color:var(--sub) !important; font-size:.78rem !important; opacity:.85; }
.coda-card h3{
color:var(--cyan); text-transform:uppercase; letter-spacing:.16em;
font-size:.74rem; font-weight:600; margin:.1rem 0 .7rem;
}
/* ---------- primary FINISH key: disarmed -> armed -> held ---------- */
.coda-go, .coda-go button{
background:linear-gradient(100deg,#6fe0f5,#a98bff 62%,#cf9bff) !important;
background-size:180% auto !important;
color:var(--ink) !important; font-weight:600 !important; letter-spacing:.03em;
border:0 !important; border-radius:14px !important; min-height:48px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,.42),
inset 0 -2px 4px rgba(0,0,0,.25),
0 0 0 1px rgba(255,255,255,.08),
0 8px 26px rgba(124,100,255,.34) !important;
transition:transform .25s var(--ease-expo), box-shadow .25s,
filter .25s, background-position .6s !important;
}
/* IGNITE on arm (the real interactive flip removes [disabled]) */
.coda-go:not([disabled]){ animation:codaCtaSheen .85s ease-out 1; }
@keyframes codaCtaSheen{ 0%{background-position:120% center} 100%{background-position:0% center} }
.coda-go:hover:not([disabled]){
transform:translateY(-2px); filter:brightness(1.06);
background-position:right center !important;
box-shadow:
inset 0 1px 0 rgba(255,255,255,.5),
0 0 0 1px rgba(255,255,255,.16),
0 14px 40px rgba(124,100,255,.5),
0 0 34px rgba(111,224,245,.38) !important;
}
.coda-go:active:not([disabled]){
transform:translateY(0) scale(.99);
box-shadow:
inset 0 2px 6px rgba(0,0,0,.4),
0 0 0 1px rgba(255,255,255,.12),
0 0 30px rgba(124,100,255,.45) !important;
}
.coda-go[disabled]{
filter:grayscale(.6) brightness(.6); opacity:.5;
box-shadow:inset 0 1px 2px rgba(0,0,0,.5) !important;
}
/* held/lit while the engine runs — keyed off the live stage overlay via :has(),
so it needs no JS to set/clear and is correct for the whole run + after. */
.gradio-container:has(.coda-stage) .coda-go,
.gradio-container:has(.coda-stage) .coda-go button{
filter:brightness(.94);
box-shadow:inset 0 2px 8px rgba(0,0,0,.45),0 0 26px rgba(169,139,255,.45) !important;
}
/* ---------- demo button (quiet utility key) ---------- */
.coda-demo{
background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,.02)) !important;
color:var(--sub) !important;
border:1px solid var(--glass-brd) !important; border-radius:11px !important;
font-weight:500 !important; letter-spacing:.02em;
box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -1px 2px rgba(0,0,0,.4) !important;
transition:all .25s ease !important;
}
.coda-demo:hover{
color:#eaf6ff !important; border-color:var(--ring-hot) !important;
background:linear-gradient(180deg,rgba(111,224,245,.10),rgba(111,224,245,.04)) !important;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 0 22px rgba(111,224,245,.20) !important;
}
/* ---------- vibe textbox (carved + arming focus) ---------- */
.coda-vibe textarea, .coda-vibe input[type=text]{
background:var(--well) !important;
border:1px solid var(--ring) !important; border-radius:10px !important;
color:var(--text) !important; caret-color:var(--cyan);
box-shadow:inset 0 2px 6px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04) !important;
transition:box-shadow .2s, border-color .2s !important;
}
.coda-vibe textarea::placeholder, .coda-vibe input[type=text]::placeholder{
color:#6b7388 !important; letter-spacing:.02em; opacity:.9;
}
.coda-vibe textarea:focus, .coda-vibe input[type=text]:focus{
border-color:var(--cyan) !important;
box-shadow:
inset 0 0 0 1px rgba(111,224,245,.35),
inset 0 2px 6px rgba(0,0,0,.45),
0 0 22px rgba(111,224,245,.18) !important;
}
/* ---------- length slider (milled fader, webkit + moz) ---------- */
.coda-slider input[type=range]{
-webkit-appearance:none; appearance:none; height:6px; border-radius:4px;
background:rgba(10,11,22,.7);
box-shadow:inset 0 1px 3px rgba(0,0,0,.7), inset 0 -1px 0 rgba(255,255,255,.04);
accent-color:var(--cyan); /* native fill is always correct — no JS --val */
}
.coda-slider input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none; width:20px; height:20px; border-radius:50%;
background:radial-gradient(circle at 50% 35%,#eafcff,#6fe0f5 58%,#3f8fa8);
border:1px solid rgba(255,255,255,.16);
box-shadow:0 2px 4px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.22),
inset 0 -1px 2px rgba(0,0,0,.55), 0 0 0 0 rgba(111,224,245,0);
transition:transform .14s, box-shadow .14s; cursor:grab;
}
.coda-slider input[type=range]:active::-webkit-slider-thumb{
transform:scale(1.1); cursor:grabbing;
box-shadow:0 2px 6px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.28),
0 0 0 4px rgba(111,224,245,.18), 0 0 16px rgba(111,224,245,.55);
}
.coda-slider input[type=range]::-moz-range-thumb{
width:20px; height:20px; border-radius:50%; border:1px solid rgba(255,255,255,.16);
background:radial-gradient(circle at 50% 35%,#eafcff,#6fe0f5 58%,#3f8fa8);
box-shadow:0 2px 4px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.22);
transition:transform .14s, box-shadow .14s;
}
.coda-slider input[type=range]:active::-moz-range-thumb{
transform:scale(1.1);
box-shadow:0 0 0 4px rgba(111,224,245,.18), 0 0 16px rgba(111,224,245,.55);
}
.coda-slider input[type=range]::-moz-range-track{ height:6px; border-radius:4px; background:rgba(10,11,22,.7); }
/* the companion number box = honest mono value chip */
.coda-slider input[type=number]{
background:rgba(10,11,22,.6) !important; border:1px solid var(--ring) !important;
box-shadow:inset 0 1px 3px rgba(0,0,0,.5) !important;
color:#dfe4ff !important; border-radius:8px !important;
font-family:'JetBrains Mono',ui-monospace,monospace !important;
font-variant-numeric:tabular-nums;
}
/* ---------- remaster checkbox (backlit toggle) ---------- */
.coda-check input[type=checkbox]{
-webkit-appearance:none; appearance:none; width:18px; height:18px;
border-radius:5px; position:relative; cursor:pointer; vertical-align:middle;
background:linear-gradient(180deg,rgba(0,0,0,.42),rgba(0,0,0,.22));
border:1px solid rgba(150,160,220,.2);
box-shadow:inset 0 1px 2px rgba(0,0,0,.6), inset 0 -1px 0 rgba(255,255,255,.04);
transition:background .2s, box-shadow .2s, border-color .2s;
}
.coda-check input[type=checkbox]:hover{ border-color:var(--ring-hot); }
.coda-check input[type=checkbox]:checked{
background:linear-gradient(135deg,#7be8ff,#a98bff);
border-color:transparent;
box-shadow:inset 0 1px 0 rgba(255,255,255,.3), 0 0 14px rgba(111,224,245,.45);
}
.coda-check input[type=checkbox]:checked::after{
content:''; position:absolute; left:5px; top:1px; width:5px; height:10px;
border:solid var(--ink); border-width:0 2px 2px 0;
transform:rotate(42deg) scale(0);
animation:codaCheck .18s cubic-bezier(.34,1.56,.64,1) forwards;
}
@keyframes codaCheck{ to{ transform:rotate(42deg) scale(1) } }
/* generic range accent (covers any non-scoped range, e.g. player seek) */
input[type=range]{ accent-color:var(--cyan); }
/* ---------- upload dropzone -> recessed INPUT channel ---------- */
.coda-drop{
border-radius:16px !important;
background:radial-gradient(120% 120% at 50% 0%, rgba(111,224,245,.05), rgba(10,11,22,.5)) !important;
box-shadow:inset 0 2px 10px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.05) !important;
transition:box-shadow .3s, transform .3s var(--ease-expo);
}
/* Gradio's Upload inner empty-state = .wrap (verified). Tame the stark frame. */
.coda-drop .wrap{
border:1.5px dashed rgba(111,224,245,.22) !important; border-radius:13px !important;
background:transparent !important; transition:border-color .3s, background .3s;
}
.coda-drop .wrap:hover{ border-color:var(--ring-hot) !important; background:rgba(111,224,245,.04) !important; }
.coda-drop:hover{ box-shadow:inset 0 2px 10px rgba(0,0,0,.5), 0 0 30px rgba(124,100,255,.18) !important; }
.coda-drop .wrap *{ color:var(--sub) !important; letter-spacing:.03em; }
.coda-drop svg{ filter:drop-shadow(0 0 8px rgba(111,224,245,.3)); opacity:.8; }
/* loaded/hot channel */
.coda-has-clip .coda-drop, .coda-drop:has(audio){
box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 0 22px rgba(111,224,245,.14) !important;
}
.coda-has-clip .coda-drop .wrap, .coda-drop:has(audio) .wrap{
border-style:solid !important; border-color:rgba(111,224,245,.3) !important;
}
/* ---------- output player -> integrated deck (REAL StaticAudio classes) ---------- */
.coda-player{
border:1px solid rgba(150,160,220,0.16) !important; border-radius:14px !important;
background:linear-gradient(180deg, rgba(111,224,245,.04), rgba(169,139,255,.04)) !important;
box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -1px 0 rgba(0,0,0,.5),
inset 0 0 30px rgba(0,0,0,.3) !important;
}
.coda-player .standard-player, .coda-player .component-wrapper{ padding:14px !important; }
/* recess the waveform + scrubber into a well (canvas color is locked — frame, don't recolor) */
.coda-player .waveform-container{
background:rgba(10,11,22,.4) !important; border-radius:10px !important;
box-shadow:inset 0 1px 4px rgba(0,0,0,.5);
}
.coda-player .timestamps{
font-family:'JetBrains Mono',ui-monospace,monospace !important;
font-variant-numeric:tabular-nums; color:var(--sub) !important; letter-spacing:.02em;
}
.coda-player .play-pause-button{ color:var(--cyan) !important; fill:var(--cyan) !important;
transition:filter .2s, transform .12s; }
.coda-player .play-pause-button:hover{ filter:drop-shadow(0 0 8px rgba(111,224,245,.5)); transform:scale(1.06); }
/* ---------- finished-song card: living glow (UNCHANGED base) ---------- */
#coda-result{ position:relative; }
#coda-result::after{
content:''; position:absolute; inset:-1px; border-radius:20px; pointer-events:none;
box-shadow:0 0 0 1px rgba(111,224,245,.10), 0 0 40px rgba(111,224,245,.06) inset;
animation:codaBreath 4.5s ease-in-out infinite;
}
@keyframes codaBreath{ 0%,100%{opacity:.4} 50%{opacity:.9} }
/* ---------- REVEAL: gold MASTER escalation (only on a real finished result) ---------- */
/* keyed off the output