Add htdemucs ONNX model card, inference script, and requirements
Browse files- README.md +265 -0
- infer.py +141 -0
- requirements.txt +3 -0
README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: en
|
| 3 |
+
license: mit
|
| 4 |
+
library_name: onnxruntime
|
| 5 |
+
pipeline_tag: audio-to-audio
|
| 6 |
+
tags:
|
| 7 |
+
- onnx
|
| 8 |
+
- onnxruntime
|
| 9 |
+
- stem-separation
|
| 10 |
+
- source-separation
|
| 11 |
+
- vocal-remover
|
| 12 |
+
- karaoke
|
| 13 |
+
- acapella
|
| 14 |
+
- demucs
|
| 15 |
+
- htdemucs
|
| 16 |
+
- music
|
| 17 |
+
- audio-to-audio
|
| 18 |
+
- mobile
|
| 19 |
+
- ios
|
| 20 |
+
- android
|
| 21 |
+
- coreml
|
| 22 |
+
- directml
|
| 23 |
+
- production-ready
|
| 24 |
+
datasets:
|
| 25 |
+
- StemSplitio/stem-separation-benchmark-2026
|
| 26 |
+
inference: false
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
# HT-Demucs (single-file 4-stem) — ONNX
|
| 30 |
+
|
| 31 |
+
The **first ONNX export of the standard `htdemucs` (non-FT) model** on
|
| 32 |
+
the Hugging Face Hub. Runs in `onnxruntime` on CPU out of the box, and
|
| 33 |
+
on CoreML / CUDA / DirectML with a one-line provider change.
|
| 34 |
+
**No PyTorch required at inference.**
|
| 35 |
+
|
| 36 |
+
This repo is the single-file companion to
|
| 37 |
+
[`StemSplitio/htdemucs-ft-onnx`](https://huggingface.co/StemSplitio/htdemucs-ft-onnx).
|
| 38 |
+
You get all 4 stems out of one 316 MB `.onnx` file (`htdemucs.onnx`),
|
| 39 |
+
or 166 MB if you grab the fp16weights variant. The FT bag is higher
|
| 40 |
+
quality; this single model is ~30% faster and uses 1 session instead of 4.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## TL;DR
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
# 316 MB fp32 model:
|
| 48 |
+
pip install onnxruntime numpy soundfile
|
| 49 |
+
python infer.py your-song.mp3 ./out/ --write-all-stems
|
| 50 |
+
# writes ./out/{drums,bass,other,vocals}.wav at 44.1 kHz stereo
|
| 51 |
+
|
| 52 |
+
# 166 MB fp16weights variant (same runtime cost):
|
| 53 |
+
python infer.py your-song.mp3 ./out/ --small --write-all-stems
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
The repo contains:
|
| 57 |
+
|
| 58 |
+
- `htdemucs.onnx` — 316 MB, opset 17, parity-verified vs PyTorch fp32.
|
| 59 |
+
- `htdemucs_fp16weights.onnx` — 166 MB, fp16-stored weights, same runtime memory / latency.
|
| 60 |
+
- `infer.py` — pure-numpy reference inference (~200 lines, no torch).
|
| 61 |
+
- `requirements.txt` — three small packages, no PyTorch.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## Quality
|
| 66 |
+
|
| 67 |
+
The official `htdemucs` model is the precursor to `htdemucs_ft` — same
|
| 68 |
+
architecture, single set of weights instead of 4 specialist sub-models.
|
| 69 |
+
On MUSDB18-HQ:
|
| 70 |
+
|
| 71 |
+
| Metric | `htdemucs` (this) | `htdemucs_ft` (4-bag) |
|
| 72 |
+
|---|---:|---:|
|
| 73 |
+
| Median vocals SDR | ~8.8 dB | **9.19 dB** |
|
| 74 |
+
| Median drums SDR | ~9.5 dB | **10.11 dB** |
|
| 75 |
+
| Total model size | **316 MB** | 1.26 GB |
|
| 76 |
+
| Sessions to load | **1** | 4 |
|
| 77 |
+
| Speed vs the bag | **~1.4× faster** | baseline |
|
| 78 |
+
|
| 79 |
+
Parity vs PyTorch fp32 (random input, 7.8 s segment):
|
| 80 |
+
|
| 81 |
+
- `htdemucs.onnx` max abs diff: **6.62 × 10⁻⁴**
|
| 82 |
+
- `htdemucs_fp16weights.onnx` max abs diff (vs fp32 weights): **4.6 × 10⁻⁵**
|
| 83 |
+
|
| 84 |
+
Both well within the 1e-3 publish threshold.
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Performance
|
| 89 |
+
|
| 90 |
+
Single 7.8 s segment, Apple M4 Pro CPU:
|
| 91 |
+
|
| 92 |
+
| Variant | RAM | Latency | RTF |
|
| 93 |
+
|---|---:|---:|---:|
|
| 94 |
+
| `htdemucs.onnx` (fp32) | ~1.1 GB | ~1.6 s | 0.20 |
|
| 95 |
+
| `htdemucs_fp16weights.onnx` | ~1.1 GB | ~1.6 s | 0.20 |
|
| 96 |
+
| For comparison: `htdemucs_ft` (4-session bag) | ~4.0 GB | ~6.4 s | 0.49 |
|
| 97 |
+
|
| 98 |
+
CUDA / DirectML / CoreML EPs are typically ≥ 5× faster on real GPUs.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## Quick start
|
| 103 |
+
|
| 104 |
+
### Python
|
| 105 |
+
|
| 106 |
+
```python
|
| 107 |
+
import soundfile as sf
|
| 108 |
+
import infer
|
| 109 |
+
|
| 110 |
+
audio, sr = sf.read("your-song.mp3", dtype="float32", always_2d=True)
|
| 111 |
+
stems = infer.separate(audio.T, sr,
|
| 112 |
+
model_path=infer.DEFAULT_MODEL,
|
| 113 |
+
providers=["CPUExecutionProvider"])
|
| 114 |
+
for stem, arr in stems.items():
|
| 115 |
+
sf.write(f"{stem}.wav", arr.T, sr)
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### CLI
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
python infer.py your-song.mp3 ./out/ --write-all-stems
|
| 122 |
+
python infer.py your-song.mp3 ./out/ --providers coreml # macOS arm64
|
| 123 |
+
python infer.py your-song.mp3 ./out/ --providers cuda # Linux + NVIDIA
|
| 124 |
+
python infer.py your-song.mp3 ./out/ --providers dml # Windows + DX12
|
| 125 |
+
python infer.py your-song.mp3 ./out/ --small # 166 MB variant
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### Mobile / Web (after pip install `onnxruntime-mobile` or `onnxruntime-web`)
|
| 129 |
+
|
| 130 |
+
```swift
|
| 131 |
+
// iOS / Swift
|
| 132 |
+
import onnxruntime_objc
|
| 133 |
+
let opts = try ORTSessionOptions()
|
| 134 |
+
try opts.appendCoreMLExecutionProvider(with: ORTCoreMLExecutionProviderOptions())
|
| 135 |
+
let session = try ORTSession(env: env,
|
| 136 |
+
modelPath: Bundle.main.path(forResource: "htdemucs", ofType: "onnx")!,
|
| 137 |
+
sessionOptions: opts)
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
```js
|
| 141 |
+
// Browser / web
|
| 142 |
+
import * as ort from "onnxruntime-web";
|
| 143 |
+
const sess = await ort.InferenceSession.create("htdemucs_fp16weights.onnx", {
|
| 144 |
+
executionProviders: ["wasm"],
|
| 145 |
+
});
|
| 146 |
+
const t = new ort.Tensor("float32", audioBuffer, [1, 2, 343980]);
|
| 147 |
+
const out = await sess.run({ mix: t }); // out.stems is (1, 4, 2, 343980)
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
For a turnkey browser demo with file-picker + chunked overlap-add, see
|
| 151 |
+
[`demucs-onnx browser-demo`](https://github.com/StemSplit/demucs-onnx#browser-demos).
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## Input / output spec
|
| 156 |
+
|
| 157 |
+
| Tensor | Name | Shape | Dtype | Notes |
|
| 158 |
+
|---|---|---|---|---|
|
| 159 |
+
| Input | `mix` | `(1, 2, 343980)` | float32 | Stereo, 44.1 kHz, 7.8 s segment. Values in [-1, 1]. |
|
| 160 |
+
| Output | `stems` | `(1, 4, 2, 343980)` | float32 | Stems in order `[drums, bass, other, vocals]`. All 4 are real predictions (unlike the FT specialists). |
|
| 161 |
+
|
| 162 |
+
For longer audio, chunk with overlap-add — see `infer.py::separate` for
|
| 163 |
+
a working 60-line implementation.
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## Tooling — `demucs-onnx` Python package
|
| 168 |
+
|
| 169 |
+
This model can be run (and re-exported from PyTorch) via the open-source
|
| 170 |
+
[`demucs-onnx`](https://github.com/StemSplit/demucs-onnx) Python package
|
| 171 |
+
on PyPI. It auto-downloads from this repo on first use, so you don't
|
| 172 |
+
have to clone or wrangle file paths.
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
pip install demucs-onnx
|
| 176 |
+
|
| 177 |
+
# Single-file 4-stem flavor (this repo):
|
| 178 |
+
demucs-onnx separate song.mp3 stems/ --model htdemucs
|
| 179 |
+
|
| 180 |
+
# Python API:
|
| 181 |
+
python -c "from demucs_onnx import separate; \
|
| 182 |
+
print(separate('song.mp3', model='htdemucs').keys())"
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
To re-export your own fine-tune:
|
| 186 |
+
|
| 187 |
+
```bash
|
| 188 |
+
pip install 'demucs-onnx[export]'
|
| 189 |
+
demucs-onnx export htdemucs out/htdemucs.onnx
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## How it was built
|
| 195 |
+
|
| 196 |
+
The export pipeline lives in the open-source
|
| 197 |
+
[`demucs-onnx`](https://github.com/StemSplit/demucs-onnx) package at
|
| 198 |
+
[`demucs_onnx/export/`](https://github.com/StemSplit/demucs-onnx/tree/main/src/demucs_onnx/export).
|
| 199 |
+
It applies four patches to make `torch.onnx.export` work on htdemucs:
|
| 200 |
+
|
| 201 |
+
1. Complex-typed `torch.stft` outputs → `Conv1d` with sin/cos kernels.
|
| 202 |
+
2. `model.segment` `fractions.Fraction` → plain `float`.
|
| 203 |
+
3. `random.randrange` in transformer pos-embedding → hardcoded `shift=0`.
|
| 204 |
+
4. `aten::_native_multi_head_attention` (no ONNX symbolic) → drop-in
|
| 205 |
+
`nn.MultiheadAttention.forward` built from `Linear`/`bmm`/`softmax`.
|
| 206 |
+
|
| 207 |
+
These are the four blockers every previous community attempt at "demucs
|
| 208 |
+
onnx" stalled on. See the [README of the demucs-onnx package](https://github.com/StemSplit/demucs-onnx#the-4-blockers-explained)
|
| 209 |
+
for the full write-up with code references.
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
## Related work
|
| 214 |
+
|
| 215 |
+
Sibling ONNX repos from the same export pipeline:
|
| 216 |
+
|
| 217 |
+
| Repo | Format | Stems | Use when |
|
| 218 |
+
|---|---|---|---|
|
| 219 |
+
| `htdemucs-onnx` *(this)* | Single file | 4 | Faster startup, fewer sessions, ~30% lower latency than the FT bag. |
|
| 220 |
+
| [`htdemucs-ft-onnx`](https://huggingface.co/StemSplitio/htdemucs-ft-onnx) | Bag of 4 files | 4 | Best SDR, especially on vocals. The default in StemSplit production. |
|
| 221 |
+
| [`htdemucs-6s-onnx`](https://huggingface.co/StemSplitio/htdemucs-6s-onnx) | Single file | 6 | Need guitar + piano stems on top of the standard 4. |
|
| 222 |
+
| [`htdemucs-ft-{drums,bass,other,vocals}-onnx`](https://huggingface.co/StemSplitio) | Single specialist | 1 | Fastest single-stem inference; 4× faster than the bag. |
|
| 223 |
+
|
| 224 |
+
Full benchmark across every popular open-source separator:
|
| 225 |
+
[StemSplitio/stem-separation-benchmark-2026](https://huggingface.co/datasets/StemSplitio/stem-separation-benchmark-2026).
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## Skip the infrastructure — use the StemSplit API
|
| 230 |
+
|
| 231 |
+
Don't want to bundle a 316 MB model in your app, manage a GPU pool, or
|
| 232 |
+
write overlap-add chunking? Use the **[StemSplit API](https://stemsplit.io/developers)**
|
| 233 |
+
instead — same model under the hood, hosted for you, with credits and a
|
| 234 |
+
dashboard.
|
| 235 |
+
|
| 236 |
+
- 🌐 [stemsplit.io](https://stemsplit.io)
|
| 237 |
+
- 📘 [Developer docs](https://stemsplit.io/developers/docs)
|
| 238 |
+
- 🔌 [API reference](https://stemsplit.io/developers/reference)
|
| 239 |
+
|
| 240 |
+
Or use the no-code tools that ship the same model family:
|
| 241 |
+
|
| 242 |
+
- 🎤 [Vocal Remover](https://stemsplit.io/vocal-remover)
|
| 243 |
+
- 🎶 [Karaoke Maker](https://stemsplit.io/karaoke-maker)
|
| 244 |
+
- 🎙️ [Acapella Maker](https://stemsplit.io/acapella-maker)
|
| 245 |
+
- 📺 [YouTube Stem Splitter](https://stemsplit.io/youtube-stem-splitter)
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## License & attribution
|
| 250 |
+
|
| 251 |
+
This repo is **MIT-licensed**, matching the original HT-Demucs.
|
| 252 |
+
|
| 253 |
+
```bibtex
|
| 254 |
+
@inproceedings{rouard2023hybrid,
|
| 255 |
+
title = {Hybrid Transformers for Music Source Separation},
|
| 256 |
+
author = {Rouard, Simon and Massa, Francisco and D{\'e}fossez, Alexandre},
|
| 257 |
+
booktitle = {ICASSP},
|
| 258 |
+
year = {2023}
|
| 259 |
+
}
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
- Original PyTorch model: [`facebookresearch/demucs`](https://github.com/facebookresearch/demucs)
|
| 263 |
+
- ONNX export, parity verification, and packaging by [StemSplit](https://stemsplit.io)
|
| 264 |
+
- Search keywords: **htdemucs onnx**, **demucs onnx single file**, **demucs ios**,
|
| 265 |
+
**demucs android**, **music source separation onnx**, **stem separation mobile**.
|
infer.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure numpy + onnxruntime inference for htdemucs (single 4-stem ONNX model).
|
| 2 |
+
|
| 3 |
+
Reference implementation, no torch. ~200 lines, MIT-licensed.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python infer.py your-song.mp3 ./out/
|
| 7 |
+
python infer.py your-song.mp3 ./out/ --providers coreml
|
| 8 |
+
|
| 9 |
+
The pre-trained ONNX file (``htdemucs.onnx`` fp32 or
|
| 10 |
+
``htdemucs_fp16weights.onnx`` half-storage) lives next to this script.
|
| 11 |
+
On first run we load the local file directly; no Hugging Face Hub
|
| 12 |
+
download required.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import onnxruntime as ort
|
| 22 |
+
import soundfile as sf
|
| 23 |
+
|
| 24 |
+
HERE = Path(__file__).resolve().parent
|
| 25 |
+
DEFAULT_MODEL = HERE / "htdemucs.onnx"
|
| 26 |
+
FP16_MODEL = HERE / "htdemucs_fp16weights.onnx"
|
| 27 |
+
|
| 28 |
+
SOURCES = ("drums", "bass", "other", "vocals")
|
| 29 |
+
SAMPLE_RATE = 44100
|
| 30 |
+
SEGMENT_S = 7.8
|
| 31 |
+
N_SAMPLES = int(SEGMENT_S * SAMPLE_RATE) # 343,980
|
| 32 |
+
N_CHANNELS = 2
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _make_window(n: int, overlap: int) -> np.ndarray:
|
| 36 |
+
w = np.ones(n, dtype=np.float32)
|
| 37 |
+
fade = np.linspace(0, 1, overlap, dtype=np.float32)
|
| 38 |
+
w[:overlap] = fade
|
| 39 |
+
w[-overlap:] = fade[::-1]
|
| 40 |
+
return w
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def separate(mix: np.ndarray, sr: int, *,
|
| 44 |
+
model_path: Path = DEFAULT_MODEL,
|
| 45 |
+
providers: list[str] | None = None,
|
| 46 |
+
verbose: bool = False) -> dict[str, np.ndarray]:
|
| 47 |
+
"""Run htdemucs on ``mix`` (shape ``(channels, samples)``).
|
| 48 |
+
|
| 49 |
+
Returns ``{stem: (channels, samples) float32}`` for all 4 stems.
|
| 50 |
+
Caller is responsible for resampling if ``sr != 44100``.
|
| 51 |
+
"""
|
| 52 |
+
if mix.ndim != 2 or mix.shape[0] not in (1, 2):
|
| 53 |
+
raise ValueError(f"expected (1|2, samples), got {mix.shape}")
|
| 54 |
+
if mix.shape[0] == 1:
|
| 55 |
+
mix = np.repeat(mix, 2, axis=0)
|
| 56 |
+
if sr != SAMPLE_RATE:
|
| 57 |
+
raise ValueError(
|
| 58 |
+
f"input sample rate {sr} != model rate {SAMPLE_RATE}; "
|
| 59 |
+
"resample first (e.g. soxr.resample).",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
providers = providers or ["CPUExecutionProvider"]
|
| 63 |
+
sess_opts = ort.SessionOptions()
|
| 64 |
+
sess_opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 65 |
+
sess = ort.InferenceSession(str(model_path), sess_options=sess_opts,
|
| 66 |
+
providers=providers)
|
| 67 |
+
if verbose:
|
| 68 |
+
print(f" loaded {model_path.name} on {sess.get_providers()[0]}")
|
| 69 |
+
|
| 70 |
+
total = mix.shape[1]
|
| 71 |
+
overlap = N_SAMPLES // 4
|
| 72 |
+
stride = N_SAMPLES - overlap
|
| 73 |
+
n_chunks = max(1, (total + stride - 1) // stride)
|
| 74 |
+
window = _make_window(N_SAMPLES, overlap)
|
| 75 |
+
out = np.zeros((len(SOURCES), N_CHANNELS, total), dtype=np.float32)
|
| 76 |
+
weight = np.zeros(total, dtype=np.float32)
|
| 77 |
+
|
| 78 |
+
for i in range(n_chunks):
|
| 79 |
+
start = i * stride
|
| 80 |
+
end = min(start + N_SAMPLES, total)
|
| 81 |
+
chunk = mix[:, start:end]
|
| 82 |
+
if chunk.shape[1] < N_SAMPLES:
|
| 83 |
+
chunk = np.pad(chunk, ((0, 0), (0, N_SAMPLES - chunk.shape[1])),
|
| 84 |
+
mode="constant")
|
| 85 |
+
x = chunk[np.newaxis, ...].astype(np.float32, copy=False)
|
| 86 |
+
stems = sess.run(["stems"], {"mix": x})[0][0] # (4, 2, N)
|
| 87 |
+
clen = end - start
|
| 88 |
+
w = window[:clen]
|
| 89 |
+
out[:, :, start:end] += stems[:, :, :clen] * w
|
| 90 |
+
weight[start:end] += w
|
| 91 |
+
if verbose:
|
| 92 |
+
print(f" chunk {i + 1}/{n_chunks}")
|
| 93 |
+
|
| 94 |
+
out /= np.maximum(weight, 1e-8)
|
| 95 |
+
return {src: out[i] for i, src in enumerate(SOURCES)}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def main() -> int:
|
| 99 |
+
p = argparse.ArgumentParser()
|
| 100 |
+
p.add_argument("input", type=Path, help="Input audio file (44.1 kHz stereo recommended)")
|
| 101 |
+
p.add_argument("output_dir", type=Path, help="Directory to write {stem}.wav files")
|
| 102 |
+
p.add_argument("--providers", default="cpu",
|
| 103 |
+
choices=["cpu", "coreml", "cuda", "dml"],
|
| 104 |
+
help="ONNX Runtime execution provider")
|
| 105 |
+
p.add_argument("--small", action="store_true",
|
| 106 |
+
help=f"Use {FP16_MODEL.name} (half the disk size, same runtime).")
|
| 107 |
+
p.add_argument("--write-all-stems", action="store_true",
|
| 108 |
+
help="Write all 4 stems instead of just vocals.")
|
| 109 |
+
args = p.parse_args()
|
| 110 |
+
|
| 111 |
+
provider_map = {
|
| 112 |
+
"cpu": ["CPUExecutionProvider"],
|
| 113 |
+
"coreml": ["CoreMLExecutionProvider", "CPUExecutionProvider"],
|
| 114 |
+
"cuda": ["CUDAExecutionProvider", "CPUExecutionProvider"],
|
| 115 |
+
"dml": ["DmlExecutionProvider", "CPUExecutionProvider"],
|
| 116 |
+
}
|
| 117 |
+
providers = provider_map[args.providers]
|
| 118 |
+
|
| 119 |
+
audio, sr = sf.read(str(args.input), dtype="float32", always_2d=True)
|
| 120 |
+
audio = audio.T # (channels, samples)
|
| 121 |
+
if sr != SAMPLE_RATE:
|
| 122 |
+
print(f"ERROR: input sample rate is {sr} Hz; resample to {SAMPLE_RATE} first.",
|
| 123 |
+
file=sys.stderr)
|
| 124 |
+
return 2
|
| 125 |
+
|
| 126 |
+
model_path = FP16_MODEL if args.small else DEFAULT_MODEL
|
| 127 |
+
if not model_path.exists():
|
| 128 |
+
print(f"ERROR: model file missing: {model_path}", file=sys.stderr)
|
| 129 |
+
return 2
|
| 130 |
+
stems = separate(audio, sr, model_path=model_path, providers=providers, verbose=True)
|
| 131 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 132 |
+
out_targets = SOURCES if args.write_all_stems else ("vocals",)
|
| 133 |
+
for s in out_targets:
|
| 134 |
+
path = args.output_dir / f"{s}.wav"
|
| 135 |
+
sf.write(str(path), stems[s].T, sr, subtype="PCM_16")
|
| 136 |
+
print(f"wrote {path}")
|
| 137 |
+
return 0
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
raise SystemExit(main())
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
onnxruntime>=1.17
|
| 2 |
+
numpy>=1.24
|
| 3 |
+
soundfile>=0.12
|