Spaces:
Running on Zero
Fix ZeroGPU audio quality and stuck-processing UI
Browse filesQuality: stop pinning seed=7. That seed reproduced the verified lab take
on torch 2.7.1, but ZeroGPU runs torch 2.8-2.11, where the same seed is
just an arbitrary (bad) draw -- the 'sporadic loud random synth noise'
take. engine.continue_audio now draws best-of-N candidates and keeps the
cleanest by a blind artifact score that rejects both loud random bursts
and silence collapse. Early-accept + an 85s wall-clock budget keep it
inside the 120s GPU window. Stay at the distilled 8-step pingpong regime
(bumping steps adds artifacts, not quality).
UI: collapse the finish_btn .then chain into a single event so one
completion signal clears the spinner -- the chained second event could
hang the 'processing' state if the SSE stream blipped (ClientDisconnect).
Build the summary markdown visible (empty) instead of toggling it. Queue
one job at a time and launch with show_error so a real failure surfaces
in the UI instead of a silently stuck spinner.
|
@@ -137,13 +137,15 @@ def _continue_on_gpu(listen_path, total_seconds, vibe):
|
|
| 137 |
starts and finishes fast instead of sitting through a slow analysis preamble
|
| 138 |
until the browser aborts the stream.
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
"""
|
| 144 |
return engine.continue_audio(
|
| 145 |
listen_path, total_seconds=int(total_seconds),
|
| 146 |
-
prompt=(vibe or "").strip()
|
| 147 |
|
| 148 |
|
| 149 |
def finish_song(audio_path, total_seconds, vibe, remaster,
|
|
@@ -295,7 +297,11 @@ with gr.Blocks(title="CODA") as app:
|
|
| 295 |
gr.Markdown("### Your finished song")
|
| 296 |
output_audio = gr.Audio(label="", type="filepath",
|
| 297 |
interactive=False)
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
gr.Markdown(PUSHBACK_CREDIT, elem_id="coda-foot")
|
| 301 |
|
|
@@ -303,17 +309,21 @@ with gr.Blocks(title="CODA") as app:
|
|
| 303 |
outputs=[info_md, finish_btn])
|
| 304 |
demo_btn.click(fn=load_demo, inputs=[], outputs=[audio_input])
|
| 305 |
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
| 309 |
finish_btn.click(
|
| 310 |
fn=finish_song,
|
| 311 |
inputs=[audio_input, total_slider, vibe, remaster],
|
| 312 |
-
outputs=[output_audio, summary_md])
|
| 313 |
-
fn=_show_summary, inputs=[], outputs=[summary_md])
|
| 314 |
|
| 315 |
|
| 316 |
if __name__ == "__main__":
|
| 317 |
# Gradio 6 moved theme/css to launch(); pass them here (and they remain on
|
| 318 |
# Blocks above) so the dark DAW theme applies however the Space serves it.
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
starts and finishes fast instead of sitting through a slow analysis preamble
|
| 138 |
until the browser aborts the stream.
|
| 139 |
|
| 140 |
+
Do NOT pin a magic seed. The lab's verified seed=7 take was made on torch
|
| 141 |
+
2.7.1; ZeroGPU runs a different torch (2.8–2.11), so the same seed reproduces
|
| 142 |
+
a *different* draw — on the deployed build, the bad "loud random synth noise"
|
| 143 |
+
one. Instead, leave the seed unset (engine draws several candidates and keeps
|
| 144 |
+
the cleanest by an artifact score), which is robust across torch builds.
|
| 145 |
"""
|
| 146 |
return engine.continue_audio(
|
| 147 |
listen_path, total_seconds=int(total_seconds),
|
| 148 |
+
prompt=(vibe or "").strip())
|
| 149 |
|
| 150 |
|
| 151 |
def finish_song(audio_path, total_seconds, vibe, remaster,
|
|
|
|
| 297 |
gr.Markdown("### Your finished song")
|
| 298 |
output_audio = gr.Audio(label="", type="filepath",
|
| 299 |
interactive=False)
|
| 300 |
+
# built visible with an empty value (an empty Markdown renders
|
| 301 |
+
# nothing). finish_song fills it in the SAME event that sets the
|
| 302 |
+
# audio, so there's no second `.then` to toggle visibility — one
|
| 303 |
+
# event = one completion signal, so the spinner always clears.
|
| 304 |
+
summary_md = gr.Markdown()
|
| 305 |
|
| 306 |
gr.Markdown(PUSHBACK_CREDIT, elem_id="coda-foot")
|
| 307 |
|
|
|
|
| 309 |
outputs=[info_md, finish_btn])
|
| 310 |
demo_btn.click(fn=load_demo, inputs=[], outputs=[audio_input])
|
| 311 |
|
| 312 |
+
# Single event (no chained `.then`): finish_song returns BOTH the audio path
|
| 313 |
+
# and the summary markdown, so one completion message clears the spinner.
|
| 314 |
+
# A chained second event was a place the "stuck processing" state could hang
|
| 315 |
+
# if the SSE stream blipped (ClientDisconnect) between the two events.
|
| 316 |
finish_btn.click(
|
| 317 |
fn=finish_song,
|
| 318 |
inputs=[audio_input, total_slider, vibe, remaster],
|
| 319 |
+
outputs=[output_audio, summary_md])
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
if __name__ == "__main__":
|
| 323 |
# Gradio 6 moved theme/css to launch(); pass them here (and they remain on
|
| 324 |
# Blocks above) so the dark DAW theme applies however the Space serves it.
|
| 325 |
+
# queue: one heavy job at a time (single GPU), others wait rather than
|
| 326 |
+
# contend. show_error surfaces a real error in the UI instead of a silently
|
| 327 |
+
# stuck spinner — the failure mode we just chased on the deployed Space.
|
| 328 |
+
app.queue(default_concurrency_limit=1, max_size=10).launch(
|
| 329 |
+
theme=THEME, css=CSS, show_error=True)
|
|
@@ -5,8 +5,17 @@ into a finished-sounding track in the same key, tempo and feel. SA3 does that in
|
|
| 5 |
a SINGLE call. Its `generate_diffusion_cond_inpaint` is a native audio-inpainting
|
| 6 |
diffusion sampler: place the user's clip at the front of the buffer, mask the
|
| 7 |
region after it, and the model fills the masked region conditioned on the kept
|
| 8 |
-
audio — true long-form continuation, 44.1 kHz stereo, no multi-pass chaining
|
| 9 |
-
no energy guards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
This module is the whole generation core. It returns ONLY the newly generated
|
| 12 |
tail plus the source length in seconds; `stitch.py` joins that tail onto the
|
|
@@ -28,16 +37,30 @@ Mask convention (verified against the installed library source):
|
|
| 28 |
at the front and mask [lead, lead+new], so SA3 keeps the lead and generates a
|
| 29 |
fresh `new`-second tail that continues from the clip's end.
|
| 30 |
"""
|
|
|
|
|
|
|
| 31 |
import numpy as np
|
| 32 |
import torch
|
| 33 |
|
| 34 |
MODEL_ID = "stabilityai/stable-audio-3-small-music"
|
| 35 |
|
| 36 |
SR = 44100 # SA3 native sample rate (model_config: sample_rate)
|
| 37 |
-
STEPS = 8 # SA3 Small is an 8-step adversarially-distilled model
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
SAMPLER = "pingpong" # the sampler the distilled model was tuned for
|
| 39 |
DEFAULT_CFG = 1.0 # distilled-model guidance; the prompt still conditions
|
| 40 |
# at 1.0 (CFG amplification off, conditional path on)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
MAX_TOTAL_SECONDS = 120 # SA3 Small duration cap (sample_size / sample_rate)
|
| 42 |
MIN_NEW_SECONDS = 5 # below this a "continuation" isn't worth a GPU call
|
| 43 |
MAX_LEAD_SECONDS = 30 # how much of the clip's TAIL to feed SA3 as run-up.
|
|
@@ -144,9 +167,47 @@ def plan_continuation(source_seconds, total_seconds):
|
|
| 144 |
return lead, new_seconds, buffer_seconds
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
| 148 |
-
seed=-1, progress=None):
|
| 149 |
-
"""Continue `clip_path` up to `total_seconds`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
Returns (new_tail, source_seconds, SR) where:
|
| 152 |
new_tail : (2, M) float32 @44.1k — ONLY the generated region
|
|
@@ -155,7 +216,7 @@ def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
|
| 155 |
SR : 44100
|
| 156 |
|
| 157 |
`progress(stage_name)` is called (best-effort) at each stage so the UI can
|
| 158 |
-
paint a live status.
|
| 159 |
"""
|
| 160 |
from einops import rearrange
|
| 161 |
from stable_audio_tools.inference.generation import (
|
|
@@ -172,11 +233,21 @@ def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
|
| 172 |
model = _ensure_on_device()
|
| 173 |
dev = _device()
|
| 174 |
|
| 175 |
-
# The library does `np.random.randint(0, 2**32-1)` when
|
| 176 |
-
# overflows int32 on Windows/numpy<2
|
| 177 |
-
#
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
_notify("reading")
|
| 182 |
source = _load_source(clip_path)
|
|
@@ -202,43 +273,66 @@ def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
|
| 202 |
f"(+{new_seconds:.1f}s new), mask=[{mask_start:.1f}s, {mask_end:.1f}s], "
|
| 203 |
f"steps={STEPS}, cfg={cfg_scale}, prompt={prompt!r}", flush=True)
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
_notify("composing")
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
_notify("finalizing")
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
if peak > 1e-9:
|
| 228 |
-
audio = audio / peak
|
| 229 |
-
|
| 230 |
-
if audio.shape[0] == 1: # safety: ensure stereo
|
| 231 |
-
audio = np.repeat(audio, 2, axis=0)
|
| 232 |
-
|
| 233 |
-
# the generated region is [lead, buffer]; lead ends at the clip's true end,
|
| 234 |
-
# so this slice is the continuation that follows the source.
|
| 235 |
-
start = int(round(lead * SR))
|
| 236 |
-
end = min(int(round(buffer_seconds * SR)), audio.shape[-1])
|
| 237 |
-
new_tail = audio[:, start:end]
|
| 238 |
-
new_tail = np.ascontiguousarray(new_tail.astype(np.float32))
|
| 239 |
-
|
| 240 |
-
print(f"[coda] generated tail: shape={new_tail.shape} "
|
| 241 |
-
f"({new_tail.shape[-1] / SR:.1f}s), peak after norm "
|
| 242 |
-
f"{float(np.abs(new_tail).max()):.3f}, "
|
| 243 |
-
f"rms {float(np.sqrt(np.mean(new_tail ** 2))):.3f}", flush=True)
|
| 244 |
-
return new_tail, source_seconds, SR
|
|
|
|
| 5 |
a SINGLE call. Its `generate_diffusion_cond_inpaint` is a native audio-inpainting
|
| 6 |
diffusion sampler: place the user's clip at the front of the buffer, mask the
|
| 7 |
region after it, and the model fills the masked region conditioned on the kept
|
| 8 |
+
audio — true long-form continuation, 44.1 kHz stereo, no multi-pass chaining
|
| 9 |
+
and no energy guards.
|
| 10 |
+
|
| 11 |
+
Candidate selection (the deployed quality fix): the lab verified a take with a
|
| 12 |
+
pinned seed on torch 2.7.1, but ZeroGPU runs a different torch (2.8–2.11), so the
|
| 13 |
+
same seed no longer reproduces it — it just freezes one arbitrary draw, which on
|
| 14 |
+
the deployed build was the bad "sporadic loud synth noise" take. Instead of
|
| 15 |
+
trusting a magic seed, CODA draws a few candidates and keeps the cleanest by a
|
| 16 |
+
cheap artifact score (it rejects both failure modes: loud random bursts AND
|
| 17 |
+
silence collapse). A wall-clock budget + early-accept keep this inside the
|
| 18 |
+
ZeroGPU window, so it costs at most a couple of extra fast 8-step draws.
|
| 19 |
|
| 20 |
This module is the whole generation core. It returns ONLY the newly generated
|
| 21 |
tail plus the source length in seconds; `stitch.py` joins that tail onto the
|
|
|
|
| 37 |
at the front and mask [lead, lead+new], so SA3 keeps the lead and generates a
|
| 38 |
fresh `new`-second tail that continues from the clip's end.
|
| 39 |
"""
|
| 40 |
+
import time
|
| 41 |
+
|
| 42 |
import numpy as np
|
| 43 |
import torch
|
| 44 |
|
| 45 |
MODEL_ID = "stabilityai/stable-audio-3-small-music"
|
| 46 |
|
| 47 |
SR = 44100 # SA3 native sample rate (model_config: sample_rate)
|
| 48 |
+
STEPS = 8 # SA3 Small is an 8-step adversarially-distilled model.
|
| 49 |
+
# It was tuned for 8-step pingpong; pushing it to 16/25
|
| 50 |
+
# steps is OFF its distilled regime and tends to ADD
|
| 51 |
+
# artifacts, not remove them. We stay at 8 and fix
|
| 52 |
+
# quality by picking the best of a few draws instead.
|
| 53 |
SAMPLER = "pingpong" # the sampler the distilled model was tuned for
|
| 54 |
DEFAULT_CFG = 1.0 # distilled-model guidance; the prompt still conditions
|
| 55 |
# at 1.0 (CFG amplification off, conditional path on)
|
| 56 |
+
|
| 57 |
+
# Best-of-N: with a random seed each draw differs, so we generate a few and keep
|
| 58 |
+
# the cleanest. Bounded so it never blows the ZeroGPU window.
|
| 59 |
+
DEFAULT_CANDIDATES = 3 # how many draws to consider when no seed is pinned
|
| 60 |
+
GPU_BUDGET_SECONDS = 85.0 # stop drawing once this much wall-clock is spent
|
| 61 |
+
# (the @spaces.GPU window is 120s; leave slack)
|
| 62 |
+
EARLY_ACCEPT_SCORE = 4.0 # a draw this clean is taken immediately, no re-draw
|
| 63 |
+
# (heuristic — see _tail_artifact_score; tune live)
|
| 64 |
MAX_TOTAL_SECONDS = 120 # SA3 Small duration cap (sample_size / sample_rate)
|
| 65 |
MIN_NEW_SECONDS = 5 # below this a "continuation" isn't worth a GPU call
|
| 66 |
MAX_LEAD_SECONDS = 30 # how much of the clip's TAIL to feed SA3 as run-up.
|
|
|
|
| 167 |
return lead, new_seconds, buffer_seconds
|
| 168 |
|
| 169 |
|
| 170 |
+
def _tail_artifact_score(tail, sr=SR):
|
| 171 |
+
"""Lower is better. A blind, ear-free quality score for a generated tail,
|
| 172 |
+
used to pick the cleanest of several candidate draws.
|
| 173 |
+
|
| 174 |
+
It targets the two ways an SA3 draw goes bad:
|
| 175 |
+
* "sporadic loud random synth noises" — even a FEW short windows far louder
|
| 176 |
+
than the body push the loudest window way above the median. (After the
|
| 177 |
+
whole-buffer peak-normalize, a burst that set the peak crushes the body,
|
| 178 |
+
making the gap larger still.) Sustained dynamics rarely make any single
|
| 179 |
+
50 ms window many times the median, so musical loudness doesn't trip it.
|
| 180 |
+
* silence collapse — a near-silent tail (the other known failure) is caught
|
| 181 |
+
by the explicit loudness floor below.
|
| 182 |
+
|
| 183 |
+
Score = max(window RMS) / median(window RMS) + silence penalty.
|
| 184 |
+
Computed on a mono mix over short (~50 ms) windows. A flat, steady signal
|
| 185 |
+
scores ~1; isolated loud bursts or a crushed body score high.
|
| 186 |
+
"""
|
| 187 |
+
mono = tail.mean(axis=0) if tail.ndim == 2 else np.asarray(tail)
|
| 188 |
+
mono = np.asarray(mono, dtype=np.float64)
|
| 189 |
+
win = max(1, int(0.05 * sr))
|
| 190 |
+
if mono.size < win * 4:
|
| 191 |
+
return float("inf") # too short to judge — avoid it
|
| 192 |
+
n = mono.size // win
|
| 193 |
+
energies = np.sqrt(
|
| 194 |
+
np.mean(mono[:n * win].reshape(n, win) ** 2, axis=1) + 1e-12)
|
| 195 |
+
median = float(np.median(energies)) + 1e-9
|
| 196 |
+
loudest = float(np.max(energies))
|
| 197 |
+
spikiness = loudest / median
|
| 198 |
+
overall = float(np.sqrt(np.mean(mono ** 2)) + 1e-12)
|
| 199 |
+
silence_penalty = 0.0 if overall > 0.02 else (0.02 - overall) * 200.0
|
| 200 |
+
return spikiness + silence_penalty
|
| 201 |
+
|
| 202 |
+
|
| 203 |
def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
| 204 |
+
seed=-1, candidates=None, progress=None):
|
| 205 |
+
"""Continue `clip_path` up to `total_seconds` with SA3 inpainting.
|
| 206 |
+
|
| 207 |
+
With the default `seed` (< 0) this draws up to `candidates` SA3 inpaint
|
| 208 |
+
takes and returns the cleanest by `_tail_artifact_score` (early-accepting a
|
| 209 |
+
clean draw and respecting a GPU wall-clock budget). Pin `seed` >= 0 for a
|
| 210 |
+
single deterministic draw (debug/repro).
|
| 211 |
|
| 212 |
Returns (new_tail, source_seconds, SR) where:
|
| 213 |
new_tail : (2, M) float32 @44.1k — ONLY the generated region
|
|
|
|
| 216 |
SR : 44100
|
| 217 |
|
| 218 |
`progress(stage_name)` is called (best-effort) at each stage so the UI can
|
| 219 |
+
paint a live status. Progress is stage-based (read / compose / finalize).
|
| 220 |
"""
|
| 221 |
from einops import rearrange
|
| 222 |
from stable_audio_tools.inference.generation import (
|
|
|
|
| 233 |
model = _ensure_on_device()
|
| 234 |
dev = _device()
|
| 235 |
|
| 236 |
+
# Seed policy. The library does `np.random.randint(0, 2**32-1)` when
|
| 237 |
+
# seed == -1, which overflows int32 on Windows/numpy<2, so we always draw
|
| 238 |
+
# our own safe seeds. A caller that pins a seed (>= 0) gets exactly one
|
| 239 |
+
# deterministic draw (reproducibility/debug paths). The default path
|
| 240 |
+
# (seed < 0) draws several candidates and keeps the cleanest — that's what
|
| 241 |
+
# the app uses, because a single pinned seed doesn't survive a torch change.
|
| 242 |
+
pinned = seed is not None and seed >= 0
|
| 243 |
+
n_candidates = 1 if pinned else max(1, int(candidates or DEFAULT_CANDIDATES))
|
| 244 |
+
if pinned:
|
| 245 |
+
seeds = [int(seed)]
|
| 246 |
+
else:
|
| 247 |
+
base = int(np.random.randint(0, 2 ** 31 - 1))
|
| 248 |
+
# spread the seeds far apart so the draws are genuinely different
|
| 249 |
+
seeds = [(base + i * 0x9E3779B1) % (2 ** 31 - 1)
|
| 250 |
+
for i in range(n_candidates)]
|
| 251 |
|
| 252 |
_notify("reading")
|
| 253 |
source = _load_source(clip_path)
|
|
|
|
| 273 |
f"(+{new_seconds:.1f}s new), mask=[{mask_start:.1f}s, {mask_end:.1f}s], "
|
| 274 |
f"steps={STEPS}, cfg={cfg_scale}, prompt={prompt!r}", flush=True)
|
| 275 |
|
| 276 |
+
def _draw(draw_seed):
|
| 277 |
+
"""One full SA3 inpaint draw -> normalized generated tail (2, M)."""
|
| 278 |
+
with torch.no_grad():
|
| 279 |
+
output = generate_diffusion_cond_inpaint(
|
| 280 |
+
model,
|
| 281 |
+
steps=STEPS,
|
| 282 |
+
cfg_scale=cfg_scale,
|
| 283 |
+
conditioning=[{"prompt": prompt, "seconds_total": buffer_seconds}],
|
| 284 |
+
sample_size=_sample_size,
|
| 285 |
+
sampler_type=SAMPLER,
|
| 286 |
+
inpaint_audio=(SR, lead_audio),
|
| 287 |
+
inpaint_mask_start_seconds=mask_start,
|
| 288 |
+
inpaint_mask_end_seconds=mask_end,
|
| 289 |
+
seed=int(draw_seed),
|
| 290 |
+
device=dev,
|
| 291 |
+
)
|
| 292 |
+
# (b, d, n) -> (d, b*n); peak-normalize like Stability's reference Space
|
| 293 |
+
# (unchanged from the verified-good local path).
|
| 294 |
+
output = rearrange(output, "b d n -> d (b n)")
|
| 295 |
+
audio = output.to(torch.float32).cpu().numpy()
|
| 296 |
+
peak = float(np.abs(audio).max())
|
| 297 |
+
if peak > 1e-9:
|
| 298 |
+
audio = audio / peak
|
| 299 |
+
if audio.shape[0] == 1: # safety: ensure stereo
|
| 300 |
+
audio = np.repeat(audio, 2, axis=0)
|
| 301 |
+
# the generated region is [lead, buffer]; lead ends at the clip's true
|
| 302 |
+
# end, so this slice is the continuation that follows the source.
|
| 303 |
+
start = int(round(lead * SR))
|
| 304 |
+
end = min(int(round(buffer_seconds * SR)), audio.shape[-1])
|
| 305 |
+
return np.ascontiguousarray(audio[:, start:end].astype(np.float32))
|
| 306 |
+
|
| 307 |
_notify("composing")
|
| 308 |
+
# Best-of-N: draw, score, keep the cleanest. Early-accept a clean draw and
|
| 309 |
+
# stop if the GPU wall-clock budget runs low, so this never blows the window.
|
| 310 |
+
best_tail, best_score, best_seed = None, float("inf"), None
|
| 311 |
+
t0 = time.time()
|
| 312 |
+
for i, draw_seed in enumerate(seeds):
|
| 313 |
+
tail = _draw(draw_seed)
|
| 314 |
+
score = _tail_artifact_score(tail, SR)
|
| 315 |
+
elapsed = time.time() - t0
|
| 316 |
+
print(f"[coda] candidate {i + 1}/{len(seeds)} seed={draw_seed} "
|
| 317 |
+
f"shape={tail.shape} ({tail.shape[-1] / SR:.1f}s) "
|
| 318 |
+
f"artifact_score={score:.2f} "
|
| 319 |
+
f"rms={float(np.sqrt(np.mean(tail ** 2))):.3f} "
|
| 320 |
+
f"elapsed={elapsed:.1f}s", flush=True)
|
| 321 |
+
if score < best_score:
|
| 322 |
+
best_tail, best_score, best_seed = tail, score, draw_seed
|
| 323 |
+
if best_score <= EARLY_ACCEPT_SCORE:
|
| 324 |
+
print(f"[coda] candidate {i + 1} clean enough "
|
| 325 |
+
f"(score {best_score:.2f} <= {EARLY_ACCEPT_SCORE}); accepting",
|
| 326 |
+
flush=True)
|
| 327 |
+
break
|
| 328 |
+
if elapsed > GPU_BUDGET_SECONDS and i + 1 < len(seeds):
|
| 329 |
+
print(f"[coda] GPU budget {GPU_BUDGET_SECONDS:.0f}s reached after "
|
| 330 |
+
f"{i + 1} draw(s); keeping best so far", flush=True)
|
| 331 |
+
break
|
| 332 |
|
| 333 |
_notify("finalizing")
|
| 334 |
+
print(f"[coda] selected seed={best_seed} artifact_score={best_score:.2f} "
|
| 335 |
+
f"tail={best_tail.shape[-1] / SR:.1f}s peak after norm "
|
| 336 |
+
f"{float(np.abs(best_tail).max()):.3f} "
|
| 337 |
+
f"rms {float(np.sqrt(np.mean(best_tail ** 2))):.3f}", flush=True)
|
| 338 |
+
return best_tail, source_seconds, SR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|