harp-dev Claude Opus 5.5 commited on
Commit
7fc77b7
·
verified ·
1 Parent(s): bbe7c60

Build all three models in the worker from a Hub mirror, declare the format, and pin requirements

Browse files

Builds 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>

Files changed (3) hide show
  1. README.md +2 -0
  2. app.py +34 -44
  3. 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.labels import LabelList
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 individual audio stems: Drums, "
22
- "Bass, Vocals, Other-instrumental using the Demucs model. Available "
23
- "models are - htdemucs: Best overall fidelity and natural sound, "
24
- "mdx_extra: Balanced trade-off, faster processing with a small drop in "
25
- "quality, mdx_extra_q: Fastest & lightest, noticeable quality loss, "
26
- "ideal for quick previews. Repository: "
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
- # Global model cache
 
 
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
- for _model_name in DEMUX_MODELS:
45
- pretrained.get_model(_model_name)
 
 
 
 
 
 
46
 
47
- def get_cached_model(model_name: str):
48
- if model_name not in LOADED_MODELS:
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 = get_cached_model(model_name)
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
- # Final shape: [1, channels, samples]
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 ValueError(f"Unsupported model selected: {model_name}")
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's artifacts and add a second set.
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
- filename = f"demucs_{model_name}_{stem_name.lower().replace(' ', '_')}.{extension}"
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>=0.2.3
8
- lameenc>=1.2
9
  openunmix==1.2.1
10
 
11
  # Audio & ML
12
  torch==2.8.0
13
  torchaudio==2.8.0
14
- ffmpeg
15
- soundfile
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