Spaces:
Running on Zero
Running on Zero
Build all three models in the worker from a Hub mirror, declare the format, and pin requirements
Browse filesBuilds htdemucs, mdx_extra, and mdx_extra_q once, in the worker, from checkpoints in teamup-tech/Demucs-weights rather than inside every GPU call from Meta's servers, declares 44.1 kHz stereo, rewrites the description, removes a dead branch, writes to a temporary directory, drops the unused ffmpeg and scipy requirements, and pins the rest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README.md +2 -0
- app.py +34 -44
- requirements.txt +4 -6
README.md
CHANGED
|
@@ -15,6 +15,8 @@ tags:
|
|
| 15 |
- subcategory:stem-separation
|
| 16 |
- input:audio
|
| 17 |
- output:audio
|
|
|
|
|
|
|
| 18 |
- stems
|
| 19 |
---
|
| 20 |
|
|
|
|
| 15 |
- subcategory:stem-separation
|
| 16 |
- input:audio
|
| 17 |
- output:audio
|
| 18 |
+
- sample-rate:44100
|
| 19 |
+
- channels:stereo
|
| 20 |
- stems
|
| 21 |
---
|
| 22 |
|
app.py
CHANGED
|
@@ -1,57 +1,59 @@
|
|
| 1 |
import spaces
|
| 2 |
from pyharp.core import ModelCard, build_endpoint
|
| 3 |
from pyharp.media.audio import save_audio
|
| 4 |
-
from pyharp.
|
| 5 |
-
from pyharp.tags import Subcategory
|
| 6 |
|
| 7 |
from audiotools import AudioSignal
|
| 8 |
from demucs import pretrained
|
| 9 |
from demucs.apply import apply_model
|
|
|
|
| 10 |
|
|
|
|
| 11 |
import gradio as gr
|
| 12 |
import soundfile as sf
|
|
|
|
| 13 |
import torch
|
| 14 |
from pathlib import Path
|
| 15 |
-
import numpy as np
|
| 16 |
|
| 17 |
# ModelCard
|
| 18 |
model_card = ModelCard(
|
| 19 |
name="Demucs",
|
| 20 |
description=(
|
| 21 |
-
"Separates a full music track into four
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"quality
|
| 26 |
-
"
|
| 27 |
"https://github.com/facebookresearch/demucs Paper: "
|
| 28 |
"https://arxiv.org/abs/2111.03600"
|
| 29 |
),
|
| 30 |
author="Simon Rouard, Francisco Massa, Alexandre Défossez",
|
| 31 |
-
tags=[Subcategory.STEM_SEPARATION, 'stems']
|
| 32 |
)
|
| 33 |
|
| 34 |
DEMUX_MODELS = ["htdemucs", "mdx_extra", "mdx_extra_q"]
|
| 35 |
STEM_NAMES = ["Drums", "Bass", "Vocals", "Other"]
|
| 36 |
LOSSLESS = {".wav", ".flac", ".aiff", ".aif"}
|
|
|
|
| 37 |
|
| 38 |
-
#
|
|
|
|
|
|
|
| 39 |
LOADED_MODELS = {}
|
| 40 |
|
| 41 |
-
# Each model's weights are fetched in the worker, which runs process_fn and starts as the server
|
| 42 |
-
# comes up, rather than inside the GPU call the first time a request picks the model
|
| 43 |
if __name__ != "__main__":
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
model = pretrained.get_model(model_name)
|
| 50 |
-
model.eval()
|
| 51 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 52 |
-
model.to(device)
|
| 53 |
-
LOADED_MODELS[model_name] = model
|
| 54 |
-
return LOADED_MODELS[model_name]
|
| 55 |
|
| 56 |
# The mixture is separated in overlapping segments, so the cost grows with its length
|
| 57 |
# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
|
|
@@ -69,33 +71,21 @@ def gpu_seconds(audio_file_path: str, model_name: str) -> float:
|
|
| 69 |
# Separation Logic
|
| 70 |
@spaces.GPU(duration=gpu_seconds)
|
| 71 |
def separate_all_stems(audio_file_path: str, model_name: str):
|
| 72 |
-
model =
|
| 73 |
|
| 74 |
signal = AudioSignal(audio_file_path)
|
| 75 |
original_sr = signal.sample_rate
|
| 76 |
signal = signal.resample(44100) # expects 44.1kHz
|
| 77 |
|
|
|
|
| 78 |
is_mono = signal.num_channels == 1
|
| 79 |
if is_mono:
|
| 80 |
signal.audio_data = signal.audio_data.repeat(1, 2, 1) # [batch, channels, samples]
|
| 81 |
-
signal._num_channels = 2
|
| 82 |
-
|
| 83 |
-
sr = signal.sample_rate
|
| 84 |
-
|
| 85 |
-
# Ensure audio_data is a torch.Tensor
|
| 86 |
-
audio = signal.audio_data
|
| 87 |
-
if isinstance(audio, np.ndarray):
|
| 88 |
-
audio = torch.from_numpy(audio)
|
| 89 |
-
|
| 90 |
-
audio = audio.float() # [channels, samples] or [channels, samples, ?]
|
| 91 |
-
|
| 92 |
-
# Remove extra trailing dimensions
|
| 93 |
-
if audio.ndim > 2:
|
| 94 |
-
audio = audio.squeeze()
|
| 95 |
|
| 96 |
-
|
| 97 |
-
waveform = audio.unsqueeze(0)
|
| 98 |
|
|
|
|
|
|
|
| 99 |
|
| 100 |
with torch.no_grad():
|
| 101 |
stems_batch = apply_model(
|
|
@@ -124,7 +114,7 @@ def separate_all_stems(audio_file_path: str, model_name: str):
|
|
| 124 |
def process_fn(audio_file_path, model_name):
|
| 125 |
try:
|
| 126 |
if model_name not in DEMUX_MODELS:
|
| 127 |
-
raise
|
| 128 |
# Checked before the GPU is reserved, so a request the model cannot take spends none of
|
| 129 |
# the caller's ZeroGPU allowance
|
| 130 |
if not audio_file_path:
|
|
@@ -140,13 +130,13 @@ def process_fn(audio_file_path, model_name):
|
|
| 140 |
)
|
| 141 |
output_signals = separate_all_stems(audio_file_path, model_name)
|
| 142 |
# A lossless input keeps its container. A lossy one comes back as WAV, since encoding the
|
| 143 |
-
# stems lossily again would expose the first encode
|
| 144 |
input_ext = Path(audio_file_path).suffix.lower()
|
| 145 |
extension = input_ext.lstrip(".") if input_ext in LOSSLESS else "wav"
|
|
|
|
| 146 |
outputs = []
|
| 147 |
for stem_name, signal in zip(STEM_NAMES, output_signals):
|
| 148 |
-
|
| 149 |
-
output_path = Path(filename)
|
| 150 |
save_audio(signal, output_path)
|
| 151 |
outputs.append(str(output_path))
|
| 152 |
return tuple(outputs)
|
|
|
|
| 1 |
import spaces
|
| 2 |
from pyharp.core import ModelCard, build_endpoint
|
| 3 |
from pyharp.media.audio import save_audio
|
| 4 |
+
from pyharp.tags import Channels, SampleRate, Subcategory
|
|
|
|
| 5 |
|
| 6 |
from audiotools import AudioSignal
|
| 7 |
from demucs import pretrained
|
| 8 |
from demucs.apply import apply_model
|
| 9 |
+
from huggingface_hub import snapshot_download
|
| 10 |
|
| 11 |
+
import contextlib
|
| 12 |
import gradio as gr
|
| 13 |
import soundfile as sf
|
| 14 |
+
import tempfile
|
| 15 |
import torch
|
| 16 |
from pathlib import Path
|
|
|
|
| 17 |
|
| 18 |
# ModelCard
|
| 19 |
model_card = ModelCard(
|
| 20 |
name="Demucs",
|
| 21 |
description=(
|
| 22 |
+
"Separates a full music track into four stems: drums, bass, vocals, and "
|
| 23 |
+
"everything else. Three models are available. htdemucs, the Hybrid Transformer "
|
| 24 |
+
"Demucs, gives the best overall fidelity. mdx_extra is faster with a small drop in "
|
| 25 |
+
"quality, and mdx_extra_q is mdx_extra quantized, the lightest of the three, with a "
|
| 26 |
+
"noticeable loss of quality. Separation runs at 44.1 kHz in stereo, and the stems "
|
| 27 |
+
"come back at the rate and channel count of the input. Repository: "
|
| 28 |
"https://github.com/facebookresearch/demucs Paper: "
|
| 29 |
"https://arxiv.org/abs/2111.03600"
|
| 30 |
),
|
| 31 |
author="Simon Rouard, Francisco Massa, Alexandre Défossez",
|
| 32 |
+
tags=[Subcategory.STEM_SEPARATION, SampleRate(44100), Channels(2), 'stems']
|
| 33 |
)
|
| 34 |
|
| 35 |
DEMUX_MODELS = ["htdemucs", "mdx_extra", "mdx_extra_q"]
|
| 36 |
STEM_NAMES = ["Drums", "Bass", "Vocals", "Other"]
|
| 37 |
LOSSLESS = {".wav", ".flac", ".aiff", ".aif"}
|
| 38 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 39 |
|
| 40 |
+
# Every model is built and placed in the worker, which runs process_fn and starts as the server
|
| 41 |
+
# comes up, so that no GPU call builds one. demucs would download the checkpoints from Meta's own
|
| 42 |
+
# server, so they are read from a pinned mirror on the Hub, linked where torch.hub looks first.
|
| 43 |
LOADED_MODELS = {}
|
| 44 |
|
|
|
|
|
|
|
| 45 |
if __name__ != "__main__":
|
| 46 |
+
_weights = Path(snapshot_download(
|
| 47 |
+
"teamup-tech/Demucs-weights", revision="e12e76af128e0935a0ad1734cb97974e5d27663d"
|
| 48 |
+
))
|
| 49 |
+
_hub = Path(torch.hub.get_dir()) / "checkpoints"
|
| 50 |
+
_hub.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
for _checkpoint in _weights.glob("*/*.th"):
|
| 52 |
+
with contextlib.suppress(FileExistsError):
|
| 53 |
+
(_hub / _checkpoint.name).symlink_to(_checkpoint)
|
| 54 |
|
| 55 |
+
for _model_name in DEMUX_MODELS:
|
| 56 |
+
LOADED_MODELS[_model_name] = pretrained.get_model(_model_name).eval().to(DEVICE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
# The mixture is separated in overlapping segments, so the cost grows with its length
|
| 59 |
# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
|
|
|
|
| 71 |
# Separation Logic
|
| 72 |
@spaces.GPU(duration=gpu_seconds)
|
| 73 |
def separate_all_stems(audio_file_path: str, model_name: str):
|
| 74 |
+
model = LOADED_MODELS[model_name]
|
| 75 |
|
| 76 |
signal = AudioSignal(audio_file_path)
|
| 77 |
original_sr = signal.sample_rate
|
| 78 |
signal = signal.resample(44100) # expects 44.1kHz
|
| 79 |
|
| 80 |
+
# The model separates in stereo, so a mono input is duplicated to feed it
|
| 81 |
is_mono = signal.num_channels == 1
|
| 82 |
if is_mono:
|
| 83 |
signal.audio_data = signal.audio_data.repeat(1, 2, 1) # [batch, channels, samples]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
sr = signal.sample_rate
|
|
|
|
| 86 |
|
| 87 |
+
# [batch, channels, samples], with the one item in the batch
|
| 88 |
+
waveform = signal.audio_data.float()
|
| 89 |
|
| 90 |
with torch.no_grad():
|
| 91 |
stems_batch = apply_model(
|
|
|
|
| 114 |
def process_fn(audio_file_path, model_name):
|
| 115 |
try:
|
| 116 |
if model_name not in DEMUX_MODELS:
|
| 117 |
+
raise gr.Error(f"Unsupported model selected: {model_name}")
|
| 118 |
# Checked before the GPU is reserved, so a request the model cannot take spends none of
|
| 119 |
# the caller's ZeroGPU allowance
|
| 120 |
if not audio_file_path:
|
|
|
|
| 130 |
)
|
| 131 |
output_signals = separate_all_stems(audio_file_path, model_name)
|
| 132 |
# A lossless input keeps its container. A lossy one comes back as WAV, since encoding the
|
| 133 |
+
# stems lossily again would expose the artifacts of the first encode and add a second set.
|
| 134 |
input_ext = Path(audio_file_path).suffix.lower()
|
| 135 |
extension = input_ext.lstrip(".") if input_ext in LOSSLESS else "wav"
|
| 136 |
+
output_dir = Path(tempfile.mkdtemp())
|
| 137 |
outputs = []
|
| 138 |
for stem_name, signal in zip(STEM_NAMES, output_signals):
|
| 139 |
+
output_path = output_dir / f"demucs_{model_name}_{stem_name.lower()}.{extension}"
|
|
|
|
| 140 |
save_audio(signal, output_path)
|
| 141 |
outputs.append(str(output_path))
|
| 142 |
return tuple(outputs)
|
requirements.txt
CHANGED
|
@@ -4,15 +4,13 @@ git+https://github.com/TEAMuP-dev/pyharp.git@v0.4.2
|
|
| 4 |
demucs==4.0.0
|
| 5 |
dora-search==0.1.11
|
| 6 |
einops==0.6.1
|
| 7 |
-
julius
|
| 8 |
-
lameenc
|
| 9 |
openunmix==1.2.1
|
| 10 |
|
| 11 |
# Audio & ML
|
| 12 |
torch==2.8.0
|
| 13 |
torchaudio==2.8.0
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
scipy
|
| 17 |
-
numpy<2
|
| 18 |
spaces>=0.51.1
|
|
|
|
| 4 |
demucs==4.0.0
|
| 5 |
dora-search==0.1.11
|
| 6 |
einops==0.6.1
|
| 7 |
+
julius==0.2.8
|
| 8 |
+
lameenc==1.9.0
|
| 9 |
openunmix==1.2.1
|
| 10 |
|
| 11 |
# Audio & ML
|
| 12 |
torch==2.8.0
|
| 13 |
torchaudio==2.8.0
|
| 14 |
+
soundfile==0.14.0
|
| 15 |
+
numpy==1.26.4
|
|
|
|
|
|
|
| 16 |
spaces>=0.51.1
|