Spaces:
Sleeping
Sleeping
File size: 5,884 Bytes
25ddb7c 933c367 0b488c2 25ddb7c ca70841 25ddb7c e9f1515 25ddb7c 0b488c2 25ddb7c 0b488c2 9167eb0 0b488c2 25ddb7c c07343b 25ddb7c 0b488c2 25ddb7c 0b488c2 25ddb7c 0b488c2 25ddb7c e9f1515 25ddb7c ca70841 0b488c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """ZeroGPU strip-pass benchmark — throwaway test app.
Standalone Gradio app, NOT part of the production site. Runs the exact same
vocal-isolation model the live Voice Remover's Step 1 uses
(mel_band_roformer_vocals_becruily) and times it on this Space's ZeroGPU
hardware, so we can decide with real numbers whether ZeroGPU is viable as a
backend before touching anything production-facing.
Deploy: create/point a ZeroGPU-hardware Space at this folder (sdk: gradio,
see README.md). Upload a song, click "Run strip pass".
"""
import os
import shutil
import subprocess
import tempfile
import time
import traceback
from pathlib import Path
import spaces
import gradio as gr
import torch
import torchaudio
from audio_separator.separator import Separator
MODELS_DIR = Path(os.environ.get("MODELS_DIR", "/tmp/models"))
MODELS_DIR.mkdir(parents=True, exist_ok=True)
# Same model the live site's Step 1 ("Isolate vocals — becruily") uses.
VOCAL_MODEL = "mel_band_roformer_vocals_becruily.ckpt"
# Fetch the checkpoint once at Space startup — CPU-only, no GPU needed — so the
# timed run below measures disk->GPU load, not an internet download. Wrapped in
# try/except so a transient network hiccup at boot can't crash the whole Space;
# worst case the first real run below pays the download cost instead.
try:
print(f"[startup] fetching {VOCAL_MODEL} ...", flush=True)
_warm = Separator(output_dir="/tmp", model_file_dir=str(MODELS_DIR))
_warm.load_model(model_filename=VOCAL_MODEL)
del _warm
print("[startup] model cached on disk.", flush=True)
except Exception:
print("[startup] warm-up download failed, will retry on first run:", flush=True)
print(traceback.format_exc(), flush=True)
def _normalize(src: str) -> str:
"""Approximates production's _normalize_for_sep (stereo / 32-bit float /
min 30s) so the timed input shape matches what RoFormer sees live.
Goes through ffmpeg first — same as production's load_audio — because
libsndfile (torchaudio/soundfile's backend) doesn't reliably decode mp3/m4a
uploads, only wav/flac. Falls back to torchaudio directly if ffmpeg isn't
on this Space's image (covers wav/flac uploads either way)."""
out = tempfile.mktemp(suffix=".wav")
ff = shutil.which("ffmpeg")
if ff:
# -c:a pcm_f32le (not just -sample_fmt flt): ffmpeg's default codec for a
# .wav target is pcm_s16le, which only accepts s16 and hard-fails if you
# ask it for float samples. Naming the float encoder directly sidesteps
# that mismatch instead of relying on ffmpeg to pick a compatible one.
try:
subprocess.run(
[ff, "-y", "-i", src,
"-ac", "2", "-c:a", "pcm_f32le",
"-af", "apad=whole_dur=30",
out],
check=True, capture_output=True)
except subprocess.CalledProcessError as e:
err = e.stderr.decode("utf-8", "replace")[-1000:] if e.stderr else "(no stderr captured)"
raise RuntimeError(f"ffmpeg failed on the uploaded file:\n{err}") from e
return out
wf, sr = torchaudio.load(src)
if wf.shape[0] == 1:
wf = wf.repeat(2, 1)
elif wf.shape[0] > 2:
wf = wf[:2]
min_samples = sr * 30
if wf.shape[1] < min_samples:
pad = torch.zeros(wf.shape[0], min_samples - wf.shape[1], dtype=wf.dtype)
wf = torch.cat([wf, pad], dim=1)
torchaudio.save(out, wf, sr, encoding="PCM_F", bits_per_sample=32)
return out
# Quota is billed on actual seconds used, not this ceiling — but ZeroGPU ranks
# shorter declared durations higher in its queue, so keep this close to real
# observed usage rather than padded to the 120s max. 60s covers up to roughly a
# 6-7 min song at the ~0.13s-inference-per-audio-second rate we measured on a
# ~3 min test song; bump it back up if you test something much longer.
@spaces.GPU(duration=60)
def run_benchmark(song_path):
if song_path is None:
return "Upload a song first."
work_dir = tempfile.mkdtemp(prefix="bench_")
try:
gpu_name = torch.cuda.get_device_name(0) if torch.cuda.is_available() else "no GPU visible"
t0 = time.perf_counter()
sep = Separator(output_dir=work_dir, output_format="WAV", model_file_dir=str(MODELS_DIR))
sep.load_model(model_filename=VOCAL_MODEL)
t1 = time.perf_counter()
sep_input = _normalize(song_path)
out_names = sep.separate(sep_input)
t2 = time.perf_counter()
load_s, sep_s, total_s = t1 - t0, t2 - t1, t2 - t0
runs_per_day = max(1, int(120 // total_s)) if total_s > 0 else "?"
return (
f"GPU: {gpu_name}\n\n"
f"Model load + move-to-device: {load_s:.1f}s\n"
f"Separation (inference): {sep_s:.1f}s\n"
f"Total: {total_s:.1f}s\n\n"
f"Output stems: {out_names}\n\n"
f"2 min/day free quota ÷ this total ≈ {runs_per_day} run(s)/day per visitor "
f"(before counting any region splits on top of this strip pass)."
)
except Exception:
return "Error:\n" + traceback.format_exc()
finally:
shutil.rmtree(work_dir, ignore_errors=True)
with gr.Blocks(title="ZeroGPU strip-pass benchmark") as demo:
gr.Markdown(
"### ZeroGPU strip-pass benchmark\n"
"Throwaway test app — not the production site. Upload a song and run "
"the same vocal-isolation model the live site's Step 1 uses, timed on "
"this Space's GPU, to get real numbers on whether ZeroGPU is viable."
)
audio_in = gr.Audio(label="Song", type="filepath")
run_btn = gr.Button("Run strip pass", variant="primary")
result = gr.Textbox(label="Timing", lines=9)
run_btn.click(fn=run_benchmark, inputs=audio_in, outputs=result)
demo.queue().launch()
|