harp-dev commited on
Commit
d0b7dfd
·
verified ·
1 Parent(s): 340c97a

Move to the Gradio SDK, run inference in-process, and return every stem.

Browse files
Files changed (5) hide show
  1. Dockerfile +0 -24
  2. README.md +9 -12
  3. app.py +97 -64
  4. packages.txt +1 -0
  5. requirements.txt +8 -2
Dockerfile DELETED
@@ -1,24 +0,0 @@
1
- FROM python:3.10-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && \
6
- apt-get install -y --no-install-recommends git gcc libc6-dev ffmpeg && \
7
- rm -rf /var/lib/apt/lists/*
8
-
9
- COPY requirements.txt /app/requirements.txt
10
- RUN pip install --no-cache-dir -r /app/requirements.txt
11
-
12
- # Pre-download model weights at build time so first inference is fast
13
- RUN bs-roformer-download \
14
- --model roformer-model-bs-roformer-sw-by-jarredou \
15
- --output-dir /app/models
16
-
17
- COPY . /app
18
-
19
- ENV PORT=7860
20
- EXPOSE 7860
21
- ENV PYTHONUNBUFFERED=1
22
- ENV PYTHONIOENCODING=UTF-8
23
-
24
- CMD ["python", "-u", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,12 +3,13 @@ title: BS-RoFormer
3
  emoji: ✂️
4
  colorFrom: red
5
  colorTo: purple
6
- sdk: docker
7
  sdk_version: 6.24.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Band-split RoPE transformer stem separation
12
  tags:
13
  - category:separation
14
  - subcategory:stem-separation
@@ -18,17 +19,13 @@ tags:
18
  - vocals
19
  - drums
20
  - bass
21
- app_port: 7860
22
  ---
23
 
24
- # BS-RoFormer — HARP Endpoint
25
 
26
- Deploys [BS-RoFormer](https://github.com/lucidrains/BS-RoFormer) as a [PyHARP](https://github.com/TEAMuP-dev/pyharp) endpoint.
 
27
 
28
- ## What it does
29
-
30
- Separates audio into stems (vocals, drums, bass, guitar, piano, other, instrumental) using the Band-Split RoPE Transformer — state-of-the-art music source separation from ByteDance AI Labs.
31
-
32
- ## License
33
-
34
- MIT
 
3
  emoji: ✂️
4
  colorFrom: red
5
  colorTo: purple
6
+ sdk: gradio
7
  sdk_version: 6.24.0
8
+ python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ short_description: Band-split RoPE transformer, six stems at once
13
  tags:
14
  - category:separation
15
  - subcategory:stem-separation
 
19
  - vocals
20
  - drums
21
  - bass
 
22
  ---
23
 
24
+ # BS-RoFormer
25
 
26
+ Deploys [BS-RoFormer](https://github.com/lucidrains/BS-RoFormer) as a
27
+ [PyHARP](https://github.com/TEAMuP-dev/pyharp) endpoint.
28
 
29
+ Separates a mixture into vocals, drums, bass, guitar, piano, and other with the SW checkpoint,
30
+ which estimates all six in one pass, and returns each as its own track along with an instrumental
31
+ (the mixture without its vocals). The output keeps the input's sample rate and channel count.
 
 
 
 
app.py CHANGED
@@ -1,99 +1,133 @@
1
- import os
2
- import shutil
3
- import subprocess
 
 
 
 
 
 
4
  import tempfile
5
  from pathlib import Path
6
 
7
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
8
  from pyharp import ModelCard, build_endpoint
9
  from pyharp.tags import Subcategory
10
 
11
- MODELS_DIR = Path("/app/models")
12
  MODEL_SLUG = "roformer-model-bs-roformer-sw-by-jarredou"
13
- MODEL_DIR = MODELS_DIR / MODEL_SLUG
 
 
 
14
 
15
- STEMS = ["vocals", "drums", "bass", "guitar", "piano", "other", "instrumental"]
16
 
17
- def get_model_paths():
18
- configs = list(MODEL_DIR.glob("*.yaml"))
19
- checkpoints = list(MODEL_DIR.glob("*.ckpt"))
20
- if not configs or not checkpoints:
21
- raise FileNotFoundError(f"Model files not found in {MODEL_DIR}")
22
- return str(configs[0]), str(checkpoints[0])
 
 
 
 
 
 
 
 
 
23
 
24
  model_card = ModelCard(
25
  name="BS-RoFormer",
26
  description=(
27
- "Separate audio into stems (vocals, drums, bass, guitar, piano, other) "
28
- "using the Band-Split RoPE Transformer. Repository: "
29
- "https://github.com/lucidrains/BS-RoFormer Paper: "
30
- "https://arxiv.org/abs/2309.02612"
31
  ),
32
  author="Wei-Tsung Lu, Ju-Chiang Wang, Qiuqiang Kong, Yun-Ning Hung (ByteDance)",
33
- tags=[Subcategory.STEM_SEPARATION, 'stems', 'vocals', 'drums', 'bass'],
34
  )
35
 
36
- def process_fn(input_audio_path: str, stem: str) -> str:
37
- print(f"Separating stem: {stem}...", flush=True)
38
 
39
- input_dir = Path(tempfile.mkdtemp())
40
- output_dir = Path(tempfile.mkdtemp())
 
 
 
 
 
 
41
 
42
- input_path = Path(input_audio_path)
43
- shutil.copy(input_path, input_dir / input_path.name)
 
44
 
45
- config_path, model_path = get_model_paths()
46
- print(f"Config: {config_path}", flush=True)
47
- print(f"Model: {model_path}", flush=True)
48
- print(f"Input dir contents: {list(input_dir.iterdir())}", flush=True)
49
 
50
- result = subprocess.run([
51
- "bs-roformer-infer",
52
- "--config_path", config_path,
53
- "--model_path", model_path,
54
- "--input_folder", str(input_dir),
55
- "--store_dir", str(output_dir),
56
- ], capture_output=True, text=True)
57
 
58
- print(f"STDOUT:\n{result.stdout}", flush=True)
59
- print(f"STDERR:\n{result.stderr}", flush=True)
 
60
 
61
- if result.returncode != 0:
62
- raise RuntimeError(f"bs-roformer-infer failed:\n{result.stderr}")
 
63
 
64
- stem_files = list(output_dir.glob(f"*_{stem}.wav"))
65
- if not stem_files:
66
- available = [f.name for f in output_dir.glob("*.wav")]
67
- raise ValueError(f"Stem '{stem}' not found. Available: {available}")
68
 
69
- output_path = tempfile.mktemp(suffix=".wav")
70
- shutil.copy(stem_files[0], output_path)
71
- print("Done.", flush=True)
72
- return output_path
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  if __name__ == "__main__":
75
  with gr.Blocks() as demo:
76
  input_components = [
77
- gr.Audio(
78
- type="filepath",
79
- label="Input Audio",
80
- ).set_info("The mixture to separate."),
81
- gr.Dropdown(
82
- choices=STEMS,
83
- value="vocals",
84
- label="Stem to Extract",
85
- info=(
86
- "Which part to pull out. Each stem uses its own trained model, so "
87
- "only one comes back per run."
88
- ),
89
  ),
90
  ]
91
  output_components = [
92
- gr.Audio(
93
- type="filepath",
94
- label="Separated Stem",
95
- ).set_info("Extracted stem audio."),
 
 
 
 
 
 
96
  ]
 
97
  app = build_endpoint(
98
  model_card=model_card,
99
  input_components=input_components,
@@ -101,5 +135,4 @@ if __name__ == "__main__":
101
  process_fn=process_fn,
102
  )
103
 
104
- print("Launching Gradio...", flush=True)
105
- demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True, pwa=True)
 
1
+ """
2
+ BS-RoFormer: separates a mixture into six stems with the Band-Split RoPE Transformer.
3
+
4
+ The SW checkpoint estimates vocals, drums, bass, guitar, piano, and other in a single pass, so
5
+ every stem is returned at once rather than one chosen ahead of time.
6
+ """
7
+
8
+ import spaces # imported before torch, as ZeroGPU requires
9
+
10
  import tempfile
11
  from pathlib import Path
12
 
13
  import gradio as gr
14
+ import numpy as np
15
+ import soundfile as sf
16
+ import torch
17
+ import torchaudio
18
+ import yaml
19
+ from bs_roformer.download import default_models_dir, ensure_model_assets
20
+ from bs_roformer.inference import SafeLoaderWithTuple
21
+ from bs_roformer.utils import demix_track, get_model_from_config
22
+ from filelock import FileLock
23
+ from ml_collections import ConfigDict
24
  from pyharp import ModelCard, build_endpoint
25
  from pyharp.tags import Subcategory
26
 
 
27
  MODEL_SLUG = "roformer-model-bs-roformer-sw-by-jarredou"
28
+ # The checkpoint was trained on 44.1 kHz stereo, whatever rate the input arrives at
29
+ MODEL_SR = 44100
30
+ # Presentation order. The checkpoint emits them as bass, drums, other, vocals, guitar, piano.
31
+ STEMS = ["vocals", "drums", "bass", "guitar", "piano", "other"]
32
 
33
+ device = "cuda" if torch.cuda.is_available() else "cpu"
34
 
35
+ # pyharp's worker process imports this file as well, so both processes reach this point at
36
+ # startup. The downloader writes straight to the final path, which would let the second one
37
+ # find a partial checkpoint and load it, so only one downloads at a time.
38
+ models_dir = default_models_dir()
39
+ models_dir.mkdir(parents=True, exist_ok=True)
40
+ with FileLock(str(models_dir / ".download.lock")):
41
+ checkpoint_path, config_path = ensure_model_assets(MODEL_SLUG, models_dir)
42
+
43
+ with open(config_path) as f:
44
+ config = ConfigDict(yaml.load(f, Loader=SafeLoaderWithTuple))
45
+
46
+ model = get_model_from_config("bs_roformer", config)
47
+ model.load_state_dict(torch.load(checkpoint_path, map_location="cpu"))
48
+ model.eval()
49
+ model.to(device)
50
 
51
  model_card = ModelCard(
52
  name="BS-RoFormer",
53
  description=(
54
+ "Separates a mixture into vocals, drums, bass, guitar, piano, and other with the "
55
+ "Band-Split RoPE Transformer, returning every stem along with an instrumental, which is "
56
+ "the mixture without its vocals. Repository: https://github.com/lucidrains/BS-RoFormer "
57
+ "Paper: https://arxiv.org/abs/2309.02612"
58
  ),
59
  author="Wei-Tsung Lu, Ju-Chiang Wang, Qiuqiang Kong, Yun-Ning Hung (ByteDance)",
60
+ tags=[Subcategory.STEM_SEPARATION, "stems", "vocals", "drums", "bass"],
61
  )
62
 
 
 
63
 
64
+ @spaces.GPU(duration=120)
65
+ @torch.inference_mode()
66
+ def process_fn(input_audio_path: str):
67
+ mix, sr = sf.read(input_audio_path, always_2d=True)
68
+ num_samples, num_channels = mix.shape
69
+
70
+ if num_channels > 2:
71
+ raise gr.Error(f"Expected mono or stereo audio, but the input has {num_channels} channels.")
72
 
73
+ # The model only accepts stereo, so a mono input is duplicated and the stems folded back
74
+ stereo = np.repeat(mix, 2, axis=1) if num_channels == 1 else mix
75
+ mixture = torch.from_numpy(np.ascontiguousarray(stereo.T, dtype=np.float32))
76
 
77
+ if sr != MODEL_SR:
78
+ mixture = torchaudio.functional.resample(mixture, sr, MODEL_SR)
 
 
79
 
80
+ estimates, _ = demix_track(config, model, mixture, device)
 
 
 
 
 
 
81
 
82
+ # The library derives its own instrumental from the first instrument, which for this
83
+ # checkpoint is the bass rather than the vocals, so it is computed here instead
84
+ estimates["instrumental"] = mixture.numpy() - estimates["vocals"]
85
 
86
+ out_dir = Path(tempfile.mkdtemp())
87
+ stem = Path(input_audio_path).stem
88
+ output_paths = []
89
 
90
+ for name in STEMS + ["instrumental"]:
91
+ audio = torch.from_numpy(np.ascontiguousarray(estimates[name], dtype=np.float32))
 
 
92
 
93
+ if sr != MODEL_SR:
94
+ audio = torchaudio.functional.resample(audio, MODEL_SR, sr)
 
 
95
 
96
+ # Resampling can shift the length by a sample or two
97
+ audio = audio[:, :num_samples]
98
+ if audio.shape[1] < num_samples:
99
+ audio = torch.nn.functional.pad(audio, (0, num_samples - audio.shape[1]))
100
+
101
+ audio = audio.mean(dim=0, keepdim=True) if num_channels == 1 else audio
102
+
103
+ path = out_dir / f"{stem}_{name}.wav"
104
+ sf.write(path, audio.numpy().T, sr, subtype="FLOAT")
105
+ output_paths.append(str(path))
106
+
107
+ return tuple(output_paths)
108
+
109
+
110
+ # The processing worker imports this file, so the app must not be built there
111
  if __name__ == "__main__":
112
  with gr.Blocks() as demo:
113
  input_components = [
114
+ gr.Audio(type="filepath", label="Input Audio").set_info(
115
+ "The mixture to separate, in mono or stereo."
 
 
 
 
 
 
 
 
 
 
116
  ),
117
  ]
118
  output_components = [
119
+ gr.Audio(type="filepath", label=f"{name.capitalize()}").set_info(info)
120
+ for name, info in [
121
+ ("vocals", "The vocals."),
122
+ ("drums", "The drums and percussion."),
123
+ ("bass", "The bass."),
124
+ ("guitar", "The guitar."),
125
+ ("piano", "The piano."),
126
+ ("other", "Everything not assigned to another stem."),
127
+ ("instrumental", "The mixture without its vocals."),
128
+ ]
129
  ]
130
+
131
  app = build_endpoint(
132
  model_card=model_card,
133
  input_components=input_components,
 
135
  process_fn=process_fn,
136
  )
137
 
138
+ demo.queue().launch(show_error=True, pwa=True)
 
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt CHANGED
@@ -1,3 +1,9 @@
1
- git+https://github.com/TEAMuP-dev/pyharp.git@v0.4.1
2
- bs-roformer-infer
3
  spaces>=0.51.1
 
 
 
 
 
 
 
1
+ # pyharp v0.4.1
2
+ git+https://github.com/TEAMuP-dev/pyharp.git@9a31acb35563c0bcf5a93f968d8285c7c3180aba
3
  spaces>=0.51.1
4
+ # ZeroGPU requires torch 2.8 or newer
5
+ torch==2.8.0
6
+ torchaudio==2.8.0
7
+ bs-roformer-infer==0.1.5
8
+ soundfile
9
+ filelock