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()