"""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: "Track0000" — Tony Winslow's own song, recorded in 2016 and
# never finished. The emotional core of CODA: the exact kind of clip this tool
# exists to finish. lo-fi in, finished-sounding out.
DEMO_CLIP = os.path.join(os.path.dirname(__file__),
"examples", "track0000_tony_winslow.mp3")
# 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}
")
def _listening_html():
"""Instant 'CODA is reading your clip' state for the readout, shown the moment
a clip lands so the panel lights up immediately instead of sitting greyed-out
until the (CPU-bound) key/tempo analysis finishes. Replaced in-place by the
real fingerprint readout a beat later."""
return ("
"
"
"
"CODA HEARDLISTENING…
"
"
"
"
Reading key, tempo and groove from your "
"clip…
")
# 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 _mmss(seconds):
s = int(round(max(float(seconds), 0.0)))
return f"{s // 60}:{s % 60:02d}"
def _seam_html(source_seconds, total):
"""A self-contained before/after 'composition ribbon' for the finished track:
one bar split at the EXACT seam — YOUR PART on the left, CODA's continuation
on the right — with the join marked and labelled.
Rendered SERVER-SIDE from the precise numbers finish_song already computed, so
the marker is always exactly where CODA begins. The old approach measured the
WaveSurfer waveform in the browser and drifted/stuck (the 'needle in the wrong
place' bug); this can't, because the split is baked into the HTML. Purely
cosmetic — does NOT touch enhance -> fingerprint -> continue_audio -> stitch."""
src = max(float(source_seconds), 0.0)
tot = max(float(total), src + 0.001)
pct = max(0.0, min(src / tot, 1.0)) * 100.0
added = max(tot - src, 0.0)
return (
"
"
"
"
"The finished track"
f"{_mmss(tot)} total"
"
"
"
"
f"
"
"YOUR PART
"
f"
"
"CODA
"
f""
"
"
"
"
f"Your original · {_mmss(src)}"
f"CODA added · +{_mmss(added)}"
"
"
"
")
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.
A GENERATOR so the readout lights up the MOMENT a clip lands: it yields a
'listening…' state first, then replaces it with the real key/tempo readout
once the (CPU-bound) analysis completes. That kills the 'CODA heard stays
greyed until you generate' feel on slower hosts. Does NOT touch the
generation pipeline — it drives the readout panel + the Finish button's armed
state, and RESETS the result panel (player / summary / seam / stage) so a new
clip can't show the previous run's output."""
# A freshly loaded clip makes the PREVIOUS finished result stale. Wipe the
# whole result panel — player + summary + seam ribbon + any leftover stage
# overlay — on the first beat, so (a) the old output can never be mistaken for
# the new one, and (b) the next run's player starts from None and is forced to
# reload its waveform cleanly. These are the trailing 4 outputs added to this
# handler; `_clear` clears them once on the first yield, `_keep` leaves them
# untouched on later yields so we don't re-send the clear every beat.
_clear = (gr.update(value=None), gr.update(value=""),
gr.update(value=""), gr.update(value=""))
_keep = (gr.update(), gr.update(), gr.update(), gr.update())
if not audio_path:
yield (gr.update(value="", visible=False),
gr.update(interactive=False), *_clear)
return
# instant feedback — panel lights up before the analysis even starts
yield (gr.update(value=_listening_html(), visible=True),
gr.update(interactive=False), *_clear)
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.")
yield (gr.update(value=msg, visible=True),
gr.update(interactive=False), *_keep)
return
html = _readout_html(info, quality)
yield (gr.update(value=html, visible=True),
gr.update(interactive=True), *_keep)
except Exception as e:
print(f"[coda] analysis failed ({e})", flush=True)
yield (gr.update(value=_warn_html("Couldn't read that file", str(e)),
visible=True),
gr.update(interactive=False), *_keep)
@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 Track0000 demo) first.")
total_seconds = int(total_seconds)
try:
# --- BEAT: listening (CPU prep, outside the GPU window) ---
# Clear ANY previous result the moment the run starts: blank the player
# (value=None), the summary and the seam ribbon. This is what makes a
# resubmit show fresh output on the FIRST click — the player transitions
# None -> new file at the reveal, which forces WaveSurfer to tear down and
# reload instead of (sometimes) keeping the prior take's waveform mounted.
progress(0.05, desc="Listening to your clip…")
yield gr.update(value=None), gr.update(value=""), _stage_html(
"listening", "Listening to your clip",
"reading key · tempo · groove"), gr.update(value="") # +seam_data
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"
), gr.update() # 4-tuple: +seam_data
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"), gr.update() # +seam_data
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 overlay, surface seam) ---
yield out_path, summary, "", _seam_html(source_seconds, total)
except gr.Error:
# clear the overlay + any stale seam so a failure leaves a clean panel
yield gr.update(), gr.update(), "", ""
raise
def load_demo():
"""Load the Track0000 demo clip into the uploader.
Copy the bundled clip to a fresh temp file and hand THAT to the uploader.
Gradio 6 refuses to move a non-user-uploaded bundled file into its cache
("…was not uploaded by a user"), which broke the demo on the Space; a file
in a per-request temp dir is one Gradio will cache and serve normally."""
import shutil
dst = os.path.join(tempfile.mkdtemp(), os.path.basename(DEMO_CLIP))
shutil.copyfile(DEMO_CLIP, dst)
return dst
# --- 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 dark-studio design system */
@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; transition:min-height .5s var(--ease-expo); }
/* while the processing 'stage' overlay is up, give the card real vertical room so
the spinner, label, sub-line and step rail never crush together (the overlay is
position:absolute, so it can't size the card itself). Released the instant the
result lands and .coda-stage is gone — the transition above smooths the change. */
#coda-result:has(.coda-stage){ min-height:380px; }
#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