@audio/neural-separate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +36 -0
- package/README.md +149 -0
- package/index.d.ts +163 -0
- package/package.json +61 -0
- package/scripts/export-htdemucs.py +230 -0
- package/scripts/export-openunmix.py +219 -0
- package/scripts/reference.py +34 -0
- package/separate.js +702 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@audio/neural-separate
|
|
2
|
+
Copyright 2026 Dmitry Iv. and audiojs contributors
|
|
3
|
+
|
|
4
|
+
This product includes a JavaScript translation of parts of open-unmix-pytorch
|
|
5
|
+
(https://github.com/sigsep/open-unmix-pytorch) and norbert (https://github.com/sigsep/norbert):
|
|
6
|
+
filtering.py's multichannel Wiener filter and its expectation-maximization, with their defaults,
|
|
7
|
+
and model.Separator's 300-frame windowing, into separate.js. Both under the MIT License:
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2019 Inria (Fabian-Robert Stöter, Antoine Liutkus)
|
|
10
|
+
|
|
11
|
+
This product includes a JavaScript translation of parts of Demucs
|
|
12
|
+
(https://github.com/facebookresearch/demucs): apply.apply_model's segment overlap-add and
|
|
13
|
+
htdemucs.py's spectrogram framing, into separate.js. Under the MIT License:
|
|
14
|
+
|
|
15
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
16
|
+
|
|
17
|
+
The MIT License, for each notice above:
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
20
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
21
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
22
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
23
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
26
|
+
substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
29
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
30
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
31
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
32
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
33
|
+
DEALINGS IN THE SOFTWARE.
|
|
34
|
+
|
|
35
|
+
No model weights are included. The export scripts write them to the local cache; their terms are
|
|
36
|
+
their authors' (Open-Unmix umxhq: MIT; Demucs htdemucs weights: see the README).
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @audio/neural-separate
|
|
2
|
+
|
|
3
|
+
> Source separation (stems): Open-Unmix and Hybrid Transformer Demucs through ONNX, matching the original PyTorch implementations.
|
|
4
|
+
|
|
5
|
+
The ML upgrade to [`@audio/vocals`](https://github.com/audiojs/vocals)'s classical center-cancel: instead of one M/S trick, a trained model estimates each target (vocals, drums, bass, other), and the pipeline around it (STFT, Wiener refinement, chunking, overlap-add) is ported from the reference implementations and checked against them.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import separate from '@audio/neural-separate'
|
|
9
|
+
|
|
10
|
+
let { stems, residual } = await separate([left, right], { sampleRate: 44100, model: 'umxhq' })
|
|
11
|
+
stems.vocals // Float32Array[2]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npm install @audio/neural-separate onnxruntime-node
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In the browser `@audio/neural-runtime` runs `onnxruntime-web` instead (`device: 'wasm'` or `'webgpu'`).
|
|
21
|
+
|
|
22
|
+
## Models
|
|
23
|
+
|
|
24
|
+
| `model` | Architecture | Files | Weights | SDR, MUSDB18 previews: vocals · drums · bass · other | Real-time factor: Node CPU · browser wasm · WebGPU |
|
|
25
|
+
|---|---|---|---|---|---|
|
|
26
|
+
| `'umxhq'` | Open-Unmix: bi-LSTM on magnitude spectrograms, multichannel Wiener EM | 4 × 35.6 MB (fp16: 4 × 17.8 MB) | MIT | 6.75 · 6.11 · 5.00 · 3.36 | 0.19 (vocals only: 0.11) · 0.12 · 0.29 |
|
|
27
|
+
| `'htdemucs'` | Hybrid Transformer Demucs: waveform and spectrogram U-Nets joined by a cross-domain transformer | 174 MB | research only | 8.86 · 9.55 · 9.30 · 5.69 | 1.4 (vocals only: 1.2) · 0.70 · 0.21 |
|
|
28
|
+
| `'htdemucs_ft'` | four fine-tuned HTDemucs, one per source | 4 × 174 MB | research only | not measured | vocals only: 1.1 · not measured · not measured |
|
|
29
|
+
|
|
30
|
+
SDR as measured below (Verification), identical to the Python originals'. The previews are 7 s excerpts, so these numbers sit apart from full-track results in scale, not in order: the Demucs README reports overall SDR 9.0 for fine-tuned HT Demucs against 5.3 for Open-Unmix on the MUSDB18-HQ test set.
|
|
31
|
+
|
|
32
|
+
Real-time factor: processing time over duration, lower is faster. 27 s of music; onnxruntime-node and onnxruntime-web 1.30, the web one in headless Chromium (wasm, Metal WebGPU); the best of two to four runs spread over a day on a 14-core M4 Max shared with other jobs (load averages 45 to 180), so upper bounds. The Python originals' best there: 0.43 (open-unmix) and 2.8 (demucs). umxhq's time is mostly JS (ONNX Runtime takes 6 to 12 %: STFT, Wiener EM, iSTFT around it), and it runs faster on wasm than on WebGPU; htdemucs's is about 80 % inference, where WebGPU pays. Memory: umxhq 1.3 GB, hybrid models 3 GB at peak in Node.
|
|
33
|
+
|
|
34
|
+
`targets` picks a subset: `targets: ['vocals']` runs only the vocals graph of `umxhq` (against the residual, see Algorithm: 2.4× faster, vocals SDR 6.50 instead of 6.75) and of `htdemucs_ft`, and skips the other sources' iSTFT for `htdemucs`. Presets resample to their 44.1 kHz and back.
|
|
35
|
+
|
|
36
|
+
## Weights
|
|
37
|
+
|
|
38
|
+
Nothing ships in this package and nothing is fetched by default. Export the weights once from the original checkpoints:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install torch openunmix onnx onnxruntime onnxscript
|
|
42
|
+
python3 node_modules/@audio/neural-separate/scripts/export-openunmix.py --model umxhq --verify
|
|
43
|
+
|
|
44
|
+
pip install demucs
|
|
45
|
+
python3 node_modules/@audio/neural-separate/scripts/export-htdemucs.py --model htdemucs --verify
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
They land in `$AUDIO_NEURAL_CACHE/<model>/`, default `~/.cache/audiojs/neural/<model>/`, where a preset looks in Node; a missing file throws, naming itself and the script. Elsewhere, serve that directory and pass its URL: `separate(audio, { model: 'umxhq', weights: 'https://…/' })`; `@audio/neural-runtime` fetches each file once and caches it (Cache API in the browser). `--verify` compares the ONNX graphs with the PyTorch modules and writes the reference separation `test.js` checks the JS pipeline against. `export-openunmix.py --fp16` also writes float16 weights with float32 I/O, half the size.
|
|
49
|
+
|
|
50
|
+
- **`export-openunmix.py`** exports each target's `OpenUnmix` through a wrapper that takes the frame count from the traced tensor (upstream reads it off `x.data.shape`, which export freezes), with the TorchScript exporter (the `torch.export` one bakes the LSTM's sequence length into a reshape). Magnitude `[1, C, F, T]` in and out, T dynamic. `umxhq`'s bandwidth restriction (`max_bin`, 1487 of 2049 bins for `n_fft=4096`@44.1kHz) crops the network's *input* below 16 kHz for efficiency; the final dense layer regresses the **full** bin range from that reduced representation (a learned extrapolation, not a literal zero-fill).
|
|
51
|
+
- **`export-htdemucs.py`** follows [sevagh/demucs.onnx](https://github.com/sevagh/demucs.onnx): the STFT and iSTFT, which ONNX export cannot carry, move out of the graph. Rather than a vendored copy of `htdemucs.py`, it runs upstream's own `forward` with its four transform methods swapped on the instance. One graph per 7.8 s segment: `mix [1, 2, 343980]` and `mix_spec [1, 4, 2048, 336]` (complex as channels) in, `stems_spec [1, 4, 4, 2048, 336]` and `stems_wave [1, 4, 2, 343980]` out; a source is the iSTFT of the first plus the second.
|
|
52
|
+
|
|
53
|
+
## Algorithm
|
|
54
|
+
|
|
55
|
+
**`'openunmix'`, `'mask'`** (spectrogram models):
|
|
56
|
+
|
|
57
|
+
1. **STFT** each channel — `n_fft` 4096, hop 1024, periodic Hann, `center=True` (torch.stft-compatible reflect padding) — own implementation on top of [`fourier-transform`](https://github.com/scijs/fourier-transform)'s raw FFT (not its bundled `stft` submodule, which zero-pads instead of centering — Open-Unmix's own filterbank is `torch.stft(..., center=True, pad_mode="reflect")`, so this package matches that framing exactly rather than reusing a differently-conventioned STFT).
|
|
58
|
+
2. **Magnitude** per channel → **one ONNX run per target** (or one multi-target graph) → estimated magnitude (`modelType: 'openunmix'`) or a `[0,1]` mask multiplied by the mixture magnitude (`modelType: 'mask'`).
|
|
59
|
+
3. **Multichannel Wiener EM** (`wiener` option, default 1 iteration) refines the per-target magnitude estimates into full complex spectra, using the mixture's spatial (inter-channel) structure — see below. It runs in windows of 300 frames, as open-unmix's `Separator` does (`wiener_win_len=300`), each window estimating its own spatial covariances; a single target runs against a residual (open-unmix's `residual=True`), since EM needs two sources.
|
|
60
|
+
4. **iSTFT** each target's channel spectra back to waveforms.
|
|
61
|
+
5. Long files are **chunked** (`chunk` seconds, default 30, with `overlap` seconds crossfade, default 2) so a bi-LSTM-class model's memory stays bounded; chunk stems are stitched back with a linear crossfade. Trade-off: the model only sees `chunk` seconds of context at once, same reasoning as Open-Unmix's own reference `Separator` batching its Wiener step into `wiener_win_len=300`-frame windows (`openunmix/model.py`) — coarser here (whole pipeline per chunk, not just the Wiener step), chosen for a simple, uniform memory bound across both spectral and waveform model families.
|
|
62
|
+
|
|
63
|
+
**`'hybrid'`** (Hybrid Transformer Demucs): `demucs.apply.apply_model` with `split=True, overlap=0.25, shifts=0`, and `demucs.api`'s normalization by the whole input's mean and standard deviation. Segments of 7.8 s start every 5.85 s; each is centered in its window with the neighbouring input as context, zeros past the ends. Per segment: HTDemucs's framing (reflect re-padding by ¾ hop, `normalized=True`, the Nyquist bin and two edge frames dropped) → ONNX → frequency branch through the iSTFT plus time branch. Segments overlap-add under a triangular window. `shifts` (averaging randomly time-shifted runs, "up to 0.2 points" of SDR per demucs's docstring) is not implemented; the CLI's default single shift averages nothing.
|
|
64
|
+
|
|
65
|
+
`modelType: 'waveform'` (Demucs v2-class) skips steps 1–4 entirely: chunked raw audio in `[1, C, N]`, stacked stems out `[1, S, C, N]`, no STFT or Wiener step — Demucs v2 operates in the time domain by design.
|
|
66
|
+
|
|
67
|
+
## Verification
|
|
68
|
+
|
|
69
|
+
| Check | Result |
|
|
70
|
+
|---|---|
|
|
71
|
+
| ONNX vs PyTorch module, random input (`--verify`) | umxhq max \|diff\| ≤ 5.7e-6 of max \|y\| (fp16: ≤ 5.3e-3); stems of htdemucs ≤ 1.6e-4, of htdemucs_ft ≤ 2.5e-4 |
|
|
72
|
+
| Pipeline vs `openunmix.Separator` in float64, 9 s reference mix (`test.js`) | 112–134 dB SNR per stem |
|
|
73
|
+
| Pipeline vs `demucs.apply.apply_model`, same mix (`test.js`) | htdemucs 80–84 dB, htdemucs_ft 78–89 dB SNR per stem |
|
|
74
|
+
| `wienerFilter` vs open-unmix's `wiener()`, float64 fixture (`test.js`) | ratio mask: 3e-12; mixture phase: 1.7e-7, upstream's `atan2` adds a float32 π |
|
|
75
|
+
|
|
76
|
+
The 50 test tracks of the MUSDB18 7 s previews (`musdb.DB(download=True)`; its terms are educational and non-commercial, so it serves measurement only), SDR by [museval](https://github.com/sigsep/sigsep-mus-eval) (BSSEval v4, 1 s windows, median over windows, then over tracks: the SiSEC 2018 aggregation):
|
|
77
|
+
|
|
78
|
+
| | vocals | drums | bass | other | this package vs Python, waveform SNR per track (median / lowest) |
|
|
79
|
+
|---|---|---|---|---|---|
|
|
80
|
+
| `umxhq`, open-unmix `Separator` | 6.75 | 6.11 | 5.00 | 3.36 | |
|
|
81
|
+
| `umxhq`, this package | 6.75 | 6.11 | 5.00 | 3.36 | 116–125 / 72–83 dB |
|
|
82
|
+
| `htdemucs`, demucs `apply_model` | 8.86 | 9.55 | 9.30 | 5.69 | |
|
|
83
|
+
| `htdemucs`, this package | 8.86 | 9.55 | 9.30 | 5.69 | 78–88 / 56–75 dB |
|
|
84
|
+
|
|
85
|
+
Per track, the SDRs differ by at most 0.003 dB.
|
|
86
|
+
|
|
87
|
+
Upstream runs its Wiener EM in float32. On the reference mix that alone moves its stems by 71 to 93 dB SNR against a float64 run of the same code, which this port matches to 112 to 134 dB; the float64 run is the reference.
|
|
88
|
+
|
|
89
|
+
## Wiener filter
|
|
90
|
+
|
|
91
|
+
`wienerFilter(mixStft, estimates, opts)` is a from-scratch JS port of the algorithm in [norbert](https://github.com/sigsep/norbert) (Liutkus & Stöter), with [open-unmix-pytorch](https://github.com/sigsep/open-unmix-pytorch)'s `openunmix/filtering.py` defaults (`eps=1e-10`, `softmask=False`, `scale_factor=10`) — softmask off by default, matching upstream's own recommendation ("`softmask=False` is recommended... once the model estimates are themselves good"). The core update — re-estimate each source's power spectral density and spatial covariance matrix, rebuild the modelled mixture covariance, apply the resulting multichannel Wiener gain, iterate — is the local Gaussian model from **Duong, Vincent, Gribonval, "Under-determined reverberant audio source separation using a full-rank spatial covariance model," IEEE TASLP 18(7), 2010**. The gain applies as `v_j R_j (Cxx⁻¹ x)`: one inverse times the mixture per bin, then one C×C product per source. The covariances are Hermitian, so stereo takes the closed-form 2×2 inverse, other channel counts a Gauss-Jordan solver.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { stft, wienerFilter, istft } from '@audio/neural-separate'
|
|
95
|
+
|
|
96
|
+
let mixStft = [stft(left, {}), stft(right, {})] // per channel
|
|
97
|
+
let estimates = { vocals: vocalsMag, drums: drumsMag } // per target: magnitude[channel][frame] (Float64Array(bins))
|
|
98
|
+
let complex = wienerFilter(mixStft, estimates, { iterations: 1 }) // per target: complex STFT per channel
|
|
99
|
+
let vocals = complex.vocals.map(ch => istft(ch, { length: left.length }))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`iterations: 0` returns the initial estimate untouched ("raw masks"). `softmask: true` uses a ratio mask that sums to the mixture exactly, by construction; `residual: true` appends a `'residual'` target (mixture minus the other targets) computed before EM. **Known limitation**: multichannel Wiener EM assumes genuine inter-channel diversity — mono content duplicated to stereo (identical L≡R) gives a rank-1 spatial covariance matrix per bin, a degenerate case where EM iterations can occasionally underperform `iterations: 0` on an already-good separation (real stereo mixes, with any actual left/right difference, don't hit this — verified in `test.js`).
|
|
103
|
+
|
|
104
|
+
## `opts`
|
|
105
|
+
|
|
106
|
+
| Option | Default | |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `sampleRate` | — | required unless `audio` is `{ channelData, sampleRate }` |
|
|
109
|
+
| `model` | — | required. Preset `'umxhq' \| 'htdemucs' \| 'htdemucs_ft'` · `url \| bytes` (single target, named `'stem'`) · `{ target: url \| { url, targets }, ... }` (one graph per target, Open-Unmix's own layout; a multi-source graph contributes its own target) · `{ url, targets: [...] }` (one multi-target graph, stacks a target axis) |
|
|
110
|
+
| `targets` | all | the targets to return |
|
|
111
|
+
| `weights` | `$AUDIO_NEURAL_CACHE` or `~/.cache/audiojs/neural` (Node) | where a preset's files are: URL, or a directory in Node |
|
|
112
|
+
| `modelType` | `'openunmix'` | `'openunmix'` (magnitude out) · `'mask'` (`[0,1]` mask out, multiplied by mixture magnitude) · `'hybrid'` (demucs.onnx contract) · `'waveform'` (Demucs v2-class); presets know theirs |
|
|
113
|
+
| `wiener` | `1` | EM iterations; `0` = raw masks. Ignored for `'hybrid'` and `'waveform'` |
|
|
114
|
+
| `wienerWindow` | `300` | frames per EM window |
|
|
115
|
+
| `chunk` / `overlap` | `30` / `2` (seconds) | `overlap` must be `< chunk`; not used by `'hybrid'`, which segments as demucs does |
|
|
116
|
+
| `segment` | `343980` | `'hybrid'`: segment length in samples when the graph does not declare its input length |
|
|
117
|
+
| `targetRate` | the preset's rate, else the input rate | resample to the model's rate for inference; stems are resampled back to the input rate |
|
|
118
|
+
| `device` | — | passed through to `@audio/neural-runtime`'s `load()` as `backend` |
|
|
119
|
+
| `dtype` | `'float32'` | only `'float32'` tensor marshalling is implemented; anything else throws |
|
|
120
|
+
| `progress` | — | `({ chunk, totalChunks }) => {}` |
|
|
121
|
+
| `session` | — | overrides `@audio/neural-runtime`'s `load()` — for tests, or a custom ORT setup |
|
|
122
|
+
|
|
123
|
+
Mono input is duplicated to stereo internally (matching `openunmix.utils.preprocess`'s own "if we have mono, we duplicate it to get stereo"), so stems always come back stereo. `separate()` returns `{ stems, sampleRate, residual }` — `residual` is the mixture minus the sum of all stems, per channel, at the input rate.
|
|
124
|
+
|
|
125
|
+
## Precedence and licenses
|
|
126
|
+
|
|
127
|
+
Open-source stem separation has three lineages; this package runs the first two.
|
|
128
|
+
|
|
129
|
+
| Project | Code | Weights | |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| **Open-Unmix** (`umx`, `umxhq`) | MIT | MIT ([Zenodo](https://zenodo.org/records/3370489)-declared) | trained on MUSDB18(-HQ); this package's primary target |
|
|
132
|
+
| **Open-Unmix** (`umxl`) | MIT | **CC BY-NC-SA 4.0 — non-commercial only** | despite being the `openunmix` package's own default variant name; trained on a private stems dataset (see the project [README](https://github.com/sigsep/open-unmix-pytorch#pre-trained-models)) |
|
|
133
|
+
| **Demucs** (Meta; `htdemucs`, `htdemucs_ft`) | MIT | **research only**: "The model weights are not covered by the MIT license, and are provided only for scientific purposes" (the author in [#327](https://github.com/facebookresearch/demucs/issues/327), 2022; again in [#508](https://github.com/facebookresearch/demucs/issues/508), 2023) | trained on MUSDB18-HQ plus 800 songs; the repository is archived, maintained at [adefossez/demucs](https://github.com/adefossez/demucs) |
|
|
134
|
+
| **Spleeter** (Deezer) | MIT | **undocumented** | the README licenses only "the code of Spleeter"; the pretrained weights' license is an open, unresolved question ([deezer/spleeter#898](https://github.com/deezer/spleeter/issues/898)) — do not assume MIT |
|
|
135
|
+
|
|
136
|
+
Audit any weight source yourself before shipping it — this table reflects what each project states as of this writing, not a guarantee. `scripts/export-openunmix.py` defaults to `umxhq` (not `umxl`) for exactly this reason.
|
|
137
|
+
|
|
138
|
+
## Reference
|
|
139
|
+
|
|
140
|
+
Stöter, Uhlich, Liutkus, Mitsufuji, "Open-Unmix - A Reference Implementation for Music Source Separation," *JOSS* 4(41), 2019. · Rouard, Massa, Défossez, "Hybrid Transformers for Music Source Separation," *ICASSP* 2023. · Duong, Vincent, Gribonval, "Under-determined reverberant audio source separation using a full-rank spatial covariance model," *IEEE TASLP* 18(7), 2010. · Rafii, Liutkus, Stöter, Mimilakis, Bittner, "The MUSDB18 corpus for music separation," 2017. · [norbert](https://github.com/sigsep/norbert) (Liutkus & Stöter) · [open-unmix-pytorch](https://github.com/sigsep/open-unmix-pytorch) · [Demucs](https://github.com/facebookresearch/demucs) · [demucs.onnx](https://github.com/sevagh/demucs.onnx) · [museval](https://github.com/sigsep/sigsep-mus-eval) · [Spleeter](https://github.com/deezer/spleeter).
|
|
141
|
+
|
|
142
|
+
**Use when:** you have (or can license) an ONNX-exported spectrogram-mask, Hybrid-Demucs or waveform separation model and want to run it — with proper multichannel Wiener refinement, not just the raw mask — dependency-free, in Node or the browser; `umxhq` where the weights must be MIT, `htdemucs` for the higher SDR under research-only terms.<br>
|
|
143
|
+
**Not for:** the classical, model-free case — reach for [`@audio/vocals`](https://github.com/audiojs/vocals) when a center-panned M/S trick is all you need; training a model (this is inference-only); real-time streaming (neither the bi-LSTM Open-Unmix nor Hybrid Transformer Demucs is causal — offline/chunked only, same as upstream).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
Part of the [@audio/neural](https://github.com/audiojs/neural) lane.
|
|
148
|
+
|
|
149
|
+
MIT © [audiojs](https://github.com/audiojs)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source separation (stems) through @audio/neural-runtime's ONNX adapter: Open-Unmix-class
|
|
3
|
+
* spectrogram models (Stöter, Uhlich, Liutkus, Mitsufuji, JOSS 2019), Hybrid Transformer Demucs
|
|
4
|
+
* (Rouard, Massa, Défossez, ICASSP 2023) with STFT and iSTFT outside the graph, and waveform graphs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Mono duplicates internally; multichannel arrays must share the same length. */
|
|
8
|
+
export type AudioInput = Float32Array[] | { channelData: Float32Array[]; sampleRate: number }
|
|
9
|
+
|
|
10
|
+
/** A model spec resolvable by @audio/neural-runtime's load(): URL string or raw ONNX bytes. */
|
|
11
|
+
export type ModelSpec = string | Uint8Array
|
|
12
|
+
|
|
13
|
+
/** Presets whose files the export scripts write: <weights>/<name>/<target>.onnx or <weights>/<name>/<name>.onnx */
|
|
14
|
+
export type ModelName = 'umxhq' | 'htdemucs' | 'htdemucs_ft'
|
|
15
|
+
|
|
16
|
+
/** One graph whose output stacks several sources on its S axis */
|
|
17
|
+
export type MultiGraph = { url: ModelSpec; targets: string[] }
|
|
18
|
+
|
|
19
|
+
export type ModelOption =
|
|
20
|
+
| ModelName
|
|
21
|
+
| ModelSpec // single target, named 'stem'
|
|
22
|
+
| Record<string, ModelSpec | MultiGraph> // one graph per target (Open-Unmix's layout); a multi-source graph contributes its own target
|
|
23
|
+
| MultiGraph
|
|
24
|
+
|
|
25
|
+
export type ModelType =
|
|
26
|
+
| 'openunmix' // target model outputs the estimated magnitude directly
|
|
27
|
+
| 'mask' // target model outputs a [0,1] mask; multiplied by the mixture magnitude
|
|
28
|
+
| 'hybrid' // demucs.onnx contract: mix [1,C,L] + CaC spectrogram [1,2C,F,T] in; [1,S,2C,F,T] + [1,S,C,L] out
|
|
29
|
+
| 'waveform' // Demucs v2-class: [1,C,N] waveform in, [1,S,C,N] stacked waveforms out
|
|
30
|
+
|
|
31
|
+
export interface ModelPreset {
|
|
32
|
+
modelType: ModelType
|
|
33
|
+
sampleRate: number
|
|
34
|
+
/** in the order stems come back */
|
|
35
|
+
targets: string[]
|
|
36
|
+
/** one graph per target (<target>.onnx), else one graph for all (<name>.onnx) */
|
|
37
|
+
perTarget?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Model presets by name */
|
|
41
|
+
export const models: Record<ModelName, ModelPreset>
|
|
42
|
+
|
|
43
|
+
/** A neural-runtime-shaped session — enough of it to drive separate() with a test double. */
|
|
44
|
+
export interface Session {
|
|
45
|
+
run(feeds: Record<string, { data: Float32Array; dims: number[]; type: string }>): Promise<Record<string, { data: Float32Array; dims: number[] }>>
|
|
46
|
+
/** 'hybrid' reads its segment length off inputs[0].dims[2] when declared */
|
|
47
|
+
inputs?: { name: string; dims?: number[] }[]
|
|
48
|
+
outputs?: { name: string }[]
|
|
49
|
+
free?(): void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SeparateOptions {
|
|
53
|
+
/** required unless audio is the { channelData, sampleRate } form */
|
|
54
|
+
sampleRate?: number
|
|
55
|
+
model: ModelOption
|
|
56
|
+
/** ignored for presets, which know theirs (default 'openunmix') */
|
|
57
|
+
modelType?: ModelType
|
|
58
|
+
/** subset of the model's targets to return; spectral models skip the other graphs, and one target runs Wiener EM against the residual */
|
|
59
|
+
targets?: string[]
|
|
60
|
+
/** where a preset's files are: URL, or a directory in Node (default $AUDIO_NEURAL_CACHE or ~/.cache/audiojs/neural) */
|
|
61
|
+
weights?: string
|
|
62
|
+
/** iterations of multichannel Wiener EM refinement (default 1); 0 = raw masks. Ignored for modelType 'hybrid' and 'waveform'. */
|
|
63
|
+
wiener?: number
|
|
64
|
+
/** wienerFilter's softmask option (default false, matching open-unmix-pytorch) */
|
|
65
|
+
softmask?: boolean
|
|
66
|
+
/** wienerFilter's eps (default 1e-10) */
|
|
67
|
+
eps?: number
|
|
68
|
+
/** frames per Wiener EM window (default 300, open-unmix Separator's wiener_win_len) */
|
|
69
|
+
wienerWindow?: number
|
|
70
|
+
/** 'hybrid': segment length in samples when the graph does not declare it (default 343980, htdemucs's 7.8 s) */
|
|
71
|
+
segment?: number
|
|
72
|
+
/** 'openunmix' | 'mask' | 'waveform': chunk length in seconds (default 30) */
|
|
73
|
+
chunk?: number
|
|
74
|
+
/** crossfade overlap in seconds between chunks (default 2); must be < chunk */
|
|
75
|
+
overlap?: number
|
|
76
|
+
/** resample to this rate for model inference, stems back to the input rate (default: the preset's rate, else the input's) */
|
|
77
|
+
targetRate?: number
|
|
78
|
+
/** STFT size for the spectral pipeline (default 4096, Open-Unmix's n_fft) */
|
|
79
|
+
n?: number
|
|
80
|
+
/** STFT hop for the spectral pipeline (default 1024, Open-Unmix's n_hop) */
|
|
81
|
+
hop?: number
|
|
82
|
+
/** passed through to @audio/neural-runtime's load() as `backend` */
|
|
83
|
+
device?: string
|
|
84
|
+
/** only 'float32' is implemented; anything else throws */
|
|
85
|
+
dtype?: 'float32'
|
|
86
|
+
progress?: (p: { chunk: number; totalChunks: number }) => void
|
|
87
|
+
/** overrides @audio/neural-runtime's load() — for tests, or a custom ORT setup */
|
|
88
|
+
session?: (model: ModelSpec, opts: SeparateOptions) => Session | Promise<Session>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SeparateResult {
|
|
92
|
+
/** keys are the model's target names ('stem' for a single bare model) */
|
|
93
|
+
stems: Record<string, Float32Array[]>
|
|
94
|
+
sampleRate: number
|
|
95
|
+
/** mixture minus the sum of all stems, per channel, at the input rate */
|
|
96
|
+
residual: Float32Array[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Separate a mixture into stems. See README for the ONNX I/O contract per modelType. */
|
|
100
|
+
export default function separate(audio: AudioInput, opts: SeparateOptions): Promise<SeparateResult>
|
|
101
|
+
|
|
102
|
+
// ------------------------------------------------------------- STFT / iSTFT
|
|
103
|
+
|
|
104
|
+
export interface StftOptions {
|
|
105
|
+
/** FFT size, power of 2 (default 4096) */
|
|
106
|
+
n?: number
|
|
107
|
+
/** hop size (default 1024) */
|
|
108
|
+
hop?: number
|
|
109
|
+
/** analysis window, length n (default: periodic Hann) */
|
|
110
|
+
window?: Float64Array
|
|
111
|
+
/** torch.stft-compatible reflect-padding by n/2 on both ends (default true) */
|
|
112
|
+
center?: boolean
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ComplexStft {
|
|
116
|
+
/** re[frame] is a Float64Array(bins), bins = n/2+1 */
|
|
117
|
+
re: Float64Array[]
|
|
118
|
+
im: Float64Array[]
|
|
119
|
+
n: number
|
|
120
|
+
hop: number
|
|
121
|
+
bins: number
|
|
122
|
+
center: boolean
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Mono complex STFT. Frame count = 1 + floor(x.length / hop) when center=true (torch.stft-compatible). */
|
|
126
|
+
export function stft(x: Float32Array | Float64Array, opts?: StftOptions): ComplexStft
|
|
127
|
+
|
|
128
|
+
export interface IstftOptions {
|
|
129
|
+
/** exact output length (torch.istft-compatible crop/pad); default: paddedLength − 2·(n/2) when center */
|
|
130
|
+
length?: number
|
|
131
|
+
/** synthesis window (default: the same periodic Hann as stft()) */
|
|
132
|
+
window?: Float64Array
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Exact inverse of stft() via squared-window-normalized overlap-add (WOLA). */
|
|
136
|
+
export function istft(frames: ComplexStft, opts?: IstftOptions): Float32Array
|
|
137
|
+
|
|
138
|
+
// ------------------------------------------------------------- Wiener filter
|
|
139
|
+
|
|
140
|
+
export interface WienerOptions {
|
|
141
|
+
/** EM refinement steps (default 1); 0 returns the initial softmask/phase-substituted estimate untouched */
|
|
142
|
+
iterations?: number
|
|
143
|
+
/** true = ratio mask (sums to the mixture exactly); false = magnitude with the mixture's phase (default, open-unmix's recommendation) */
|
|
144
|
+
softmask?: boolean
|
|
145
|
+
/** appends a 'residual' target = mixture − Σ(other targets), computed before EM */
|
|
146
|
+
residual?: boolean
|
|
147
|
+
/** regularization floor (default 1e-10) */
|
|
148
|
+
eps?: number
|
|
149
|
+
/** EM numerical-stability rescaling divisor (default 10, open-unmix-pytorch's scale_factor) */
|
|
150
|
+
scaleFactor?: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Multichannel Wiener EM (Liutkus & Stöter, github.com/sigsep/norbert; Duong,
|
|
155
|
+
* Vincent, Gribonval, IEEE TASLP 2010) with open-unmix-pytorch's defaults.
|
|
156
|
+
* estimates: per-target magnitude, one Float64Array(bins) per frame per
|
|
157
|
+
* channel — a single-channel estimate broadcasts to all mixture channels.
|
|
158
|
+
*/
|
|
159
|
+
export function wienerFilter(
|
|
160
|
+
mixStft: ComplexStft[],
|
|
161
|
+
estimates: Record<string, Float64Array[][]>,
|
|
162
|
+
opts?: WienerOptions
|
|
163
|
+
): Record<string, ComplexStft[]>
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/neural-separate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Source separation (stems): Open-Unmix-class spectrogram masking + multichannel Wiener refinement, Hybrid Transformer Demucs, model-agnostic ONNX",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "separate.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"default": "./separate.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"separate.js",
|
|
18
|
+
"index.d.ts",
|
|
19
|
+
"scripts/export-openunmix.py",
|
|
20
|
+
"scripts/export-htdemucs.py",
|
|
21
|
+
"scripts/reference.py",
|
|
22
|
+
"NOTICE"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@audio/neural-runtime": "^0.1.0",
|
|
26
|
+
"@audio/resample-sinc": "^1.1.2",
|
|
27
|
+
"fourier-transform": "^2.4.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"onnxruntime-node": "^1.29.0",
|
|
31
|
+
"tst": "^9.4.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node test.js"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"audio",
|
|
38
|
+
"ml",
|
|
39
|
+
"neural",
|
|
40
|
+
"separate",
|
|
41
|
+
"stems",
|
|
42
|
+
"open-unmix",
|
|
43
|
+
"demucs",
|
|
44
|
+
"wiener",
|
|
45
|
+
"onnx"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/audiojs/neural.git",
|
|
55
|
+
"directory": "packages/neural-separate"
|
|
56
|
+
},
|
|
57
|
+
"funding": "https://github.com/sponsors/audiojs",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export Demucs v4 (Hybrid Transformer Demucs) to the ONNX contract
|
|
3
|
+
@audio/neural-separate expects for modelType 'hybrid': the network without its
|
|
4
|
+
STFT and iSTFT, which ONNX export cannot carry. The split is sevagh's
|
|
5
|
+
demucs.onnx (github.com/sevagh/demucs.onnx, MIT); JS computes both transforms.
|
|
6
|
+
|
|
7
|
+
pip install torch demucs onnx onnxruntime
|
|
8
|
+
python3 export-htdemucs.py --model htdemucs --verify
|
|
9
|
+
python3 export-htdemucs.py --model htdemucs_ft --verify # four source-specialist graphs
|
|
10
|
+
|
|
11
|
+
One 7.8 s training segment (343980 samples at 44.1 kHz) per run:
|
|
12
|
+
inputs mix [1, 2, 343980] waveform
|
|
13
|
+
mix_spec [1, 4, 2048, 336] STFT as complex-as-channels (L re, L im, R re, R im)
|
|
14
|
+
outputs stems_spec [1, S, 4, 2048, 336] frequency branch, same layout
|
|
15
|
+
stems_wave [1, S, 2, 343980] time branch
|
|
16
|
+
stem = iSTFT(stems_spec) + stems_wave
|
|
17
|
+
|
|
18
|
+
Writes <out-dir>/<model>.onnx (htdemucs_ft: <out-dir>/<source>.onnx per
|
|
19
|
+
specialist), weights embedded. Default out-dir: $AUDIO_NEURAL_CACHE/<model>, else
|
|
20
|
+
~/.cache/audiojs/neural/<model>. Last run: torch 2.14, demucs 4.1.0,
|
|
21
|
+
onnxruntime 1.30.
|
|
22
|
+
|
|
23
|
+
How: HTDemucs.forward runs unmodified with its four transform methods swapped
|
|
24
|
+
on the instance: _spec and _magnitude return the given spectrogram, _mask keeps
|
|
25
|
+
the frequency-branch estimate, _ispec returns 0 so forward returns the time
|
|
26
|
+
branch alone. sevagh edits a vendored copy of htdemucs.py to the same effect;
|
|
27
|
+
patching keeps upstream's forward as the single source. --verify rebuilds the
|
|
28
|
+
stems from the ONNX outputs with demucs's own _mask and _ispec and compares them
|
|
29
|
+
with model(mix) on the same segment.
|
|
30
|
+
|
|
31
|
+
Weights: Demucs's checkpoints "are not covered by the MIT license, and are
|
|
32
|
+
provided only for scientific purposes" (the author,
|
|
33
|
+
github.com/facebookresearch/demucs/issues/327). Memory: an export peaks near
|
|
34
|
+
3.2 GB; htdemucs_ft exports and checks each specialist in its own process.
|
|
35
|
+
"""
|
|
36
|
+
import argparse
|
|
37
|
+
import gc
|
|
38
|
+
import os
|
|
39
|
+
import subprocess
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_core(model):
|
|
45
|
+
import torch
|
|
46
|
+
import torch.nn as nn
|
|
47
|
+
|
|
48
|
+
class HybridCore(nn.Module):
|
|
49
|
+
def __init__(self, m):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.m = m
|
|
52
|
+
|
|
53
|
+
def forward(self, mix, mix_spec):
|
|
54
|
+
m, box = self.m, {}
|
|
55
|
+
m._spec = lambda x: mix_spec
|
|
56
|
+
m._magnitude = lambda z: z
|
|
57
|
+
m._mask = lambda z, x: box.setdefault("spec", x)
|
|
58
|
+
m._ispec = lambda z, length=None, scale=0: torch.zeros((), dtype=mix.dtype)
|
|
59
|
+
try:
|
|
60
|
+
wave = m(mix)
|
|
61
|
+
finally:
|
|
62
|
+
for k in ("_spec", "_magnitude", "_mask", "_ispec"):
|
|
63
|
+
del m.__dict__[k]
|
|
64
|
+
return box["spec"], wave
|
|
65
|
+
|
|
66
|
+
return HybridCore(model).eval()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cac_spec(model, mix):
|
|
70
|
+
"""The mix's STFT as HTDemucs sees it (its own _spec and _magnitude)."""
|
|
71
|
+
return model._magnitude(model._spec(mix))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def export_one(model, out_path, opset, verify):
|
|
75
|
+
import torch
|
|
76
|
+
|
|
77
|
+
L = int(model.segment * model.samplerate)
|
|
78
|
+
mix = torch.randn(1, model.audio_channels, L)
|
|
79
|
+
with torch.no_grad():
|
|
80
|
+
spec = cac_spec(model, mix)
|
|
81
|
+
# traced with grad enabled: under no_grad, nn.MultiheadAttention takes its fused
|
|
82
|
+
# fast path, aten::_native_multi_head_attention, which has no ONNX export
|
|
83
|
+
torch.onnx.export(
|
|
84
|
+
build_core(model),
|
|
85
|
+
(mix, spec),
|
|
86
|
+
str(out_path),
|
|
87
|
+
input_names=["mix", "mix_spec"],
|
|
88
|
+
output_names=["stems_spec", "stems_wave"],
|
|
89
|
+
opset_version=opset,
|
|
90
|
+
dynamo=False,
|
|
91
|
+
)
|
|
92
|
+
print(f"wrote {out_path} ({out_path.stat().st_size / 1e6:.1f} MB), segment {L} samples")
|
|
93
|
+
gc.collect() # the traced graph: export peaks near 3 GB
|
|
94
|
+
if verify:
|
|
95
|
+
verify_export(model, out_path, L)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def verify_export(model, onnx_path, L):
|
|
99
|
+
"""Stems rebuilt from onnxruntime's outputs against model(mix): a random
|
|
100
|
+
segment and a harmonic one, relative max |diff| < 1e-3 (float32 through a
|
|
101
|
+
transformer; a transform mismatch would show at order 1)."""
|
|
102
|
+
import math
|
|
103
|
+
import numpy as np
|
|
104
|
+
import onnxruntime as ort
|
|
105
|
+
import torch
|
|
106
|
+
|
|
107
|
+
opts = ort.SessionOptions()
|
|
108
|
+
opts.enable_cpu_mem_arena = False # give memory back between runs
|
|
109
|
+
opts.enable_mem_pattern = False
|
|
110
|
+
session = ort.InferenceSession(str(onnx_path), opts, providers=["CPUExecutionProvider"])
|
|
111
|
+
t = torch.arange(L) / model.samplerate
|
|
112
|
+
tone = sum(torch.sin(2 * math.pi * f * t) / k for k, f in enumerate((110, 220, 440, 880, 1760), 1))
|
|
113
|
+
for name, mix in (("noise", 0.1 * torch.randn(1, 2, L)), ("tones", 0.2 * torch.stack([tone, 0.8 * tone])[None])):
|
|
114
|
+
with torch.no_grad():
|
|
115
|
+
expected = model(mix).numpy()
|
|
116
|
+
spec = cac_spec(model, mix)
|
|
117
|
+
s, w = session.run(None, {"mix": mix.numpy(), "mix_spec": spec.numpy()})
|
|
118
|
+
actual = (model._ispec(model._mask(None, torch.from_numpy(s)), L) + torch.from_numpy(w)).numpy()
|
|
119
|
+
rel = np.abs(actual - expected).max() / max(np.abs(expected).max(), 1e-12)
|
|
120
|
+
ok = rel < 1e-3
|
|
121
|
+
print(f" verify {onnx_path.name} {name}: max|diff|/max|y| = {rel:.2e} " + ("OK" if ok else "FAIL"))
|
|
122
|
+
if not ok:
|
|
123
|
+
raise SystemExit(f"{onnx_path}: verification failed ({rel:.2e} >= 1e-3)")
|
|
124
|
+
del session
|
|
125
|
+
gc.collect()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_reference(bag, out_dir):
|
|
129
|
+
"""demucs.api.Separator's path: whole-input normalization, apply_model with split=True,
|
|
130
|
+
overlap=0.25 and shifts=0 (the CLI's one random shift is not reproducible)."""
|
|
131
|
+
import torch
|
|
132
|
+
from demucs.apply import apply_model
|
|
133
|
+
|
|
134
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
135
|
+
sys.dont_write_bytecode = True # no __pycache__ beside the package's scripts
|
|
136
|
+
import reference
|
|
137
|
+
|
|
138
|
+
def separate(mix):
|
|
139
|
+
wav = torch.from_numpy(mix)
|
|
140
|
+
ref = wav.mean(0)
|
|
141
|
+
mean, std = ref.mean(), ref.std() + 1e-8
|
|
142
|
+
with torch.no_grad():
|
|
143
|
+
est = apply_model(bag, ((wav - mean) / std)[None], shifts=0, split=True, overlap=0.25)[0] * std + mean
|
|
144
|
+
return {s: est[k].numpy() for k, s in enumerate(bag.sources)}
|
|
145
|
+
|
|
146
|
+
reference.write(out_dir, separate)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def bag_of(name):
|
|
150
|
+
"""(signature, source) per model of a bag of source specialists (htdemucs_ft), read off the
|
|
151
|
+
bag's YAML in the demucs package without importing torch; None for a single model."""
|
|
152
|
+
import importlib.util
|
|
153
|
+
import yaml
|
|
154
|
+
|
|
155
|
+
spec = importlib.util.find_spec("demucs")
|
|
156
|
+
f = Path(spec.origin).parent / "remote" / f"{name}.yaml" if spec else None
|
|
157
|
+
bag = yaml.safe_load(f.read_text()) if f and f.exists() else {}
|
|
158
|
+
if len(bag.get("models", [])) < 2 or not bag.get("weights"):
|
|
159
|
+
return None
|
|
160
|
+
sources = ["drums", "bass", "other", "vocals"] # demucs.pretrained.SOURCES, the weights' column order
|
|
161
|
+
return [(sig, sources[max(range(len(w)), key=lambda k: w[k])]) for sig, w in zip(bag["models"], bag["weights"])]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
166
|
+
ap.add_argument("--model", default="htdemucs", help="htdemucs | htdemucs_ft | htdemucs_6s")
|
|
167
|
+
ap.add_argument("--out-dir", default=None, help="default: $AUDIO_NEURAL_CACHE/<model> or ~/.cache/audiojs/neural/<model>")
|
|
168
|
+
ap.add_argument("--opset", type=int, default=17)
|
|
169
|
+
ap.add_argument("--verify", action="store_true", help="compare the rebuilt stems with model(mix), and write the "
|
|
170
|
+
"reference separation test.js compares against (scripts/reference.py)")
|
|
171
|
+
ap.add_argument("--source", default=None, help=argparse.SUPPRESS) # a bag's specialist, in its own process
|
|
172
|
+
ap.add_argument("--reference", action="store_true", help=argparse.SUPPRESS) # a bag's reference, in its own process
|
|
173
|
+
ap.add_argument("--check", action="store_true", help=argparse.SUPPRESS) # verify a specialist's export, in its own process
|
|
174
|
+
args = ap.parse_args()
|
|
175
|
+
|
|
176
|
+
cache = os.environ.get("AUDIO_NEURAL_CACHE") or Path.home() / ".cache" / "audiojs" / "neural"
|
|
177
|
+
out_dir = Path(args.out_dir) if args.out_dir else Path(cache) / args.model
|
|
178
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
bag = bag_of(args.model)
|
|
182
|
+
except ImportError:
|
|
183
|
+
raise SystemExit("torch and demucs are required: pip install torch demucs onnx onnxruntime")
|
|
184
|
+
if bag and not (args.reference or args.source):
|
|
185
|
+
# a bag of source specialists: one process each, since an export peaks near 3 GB and is
|
|
186
|
+
# not handed back; this one stays free of torch
|
|
187
|
+
run = lambda *extra: subprocess.run([sys.executable, __file__, "--model", args.model, "--out-dir", str(out_dir), "--opset", str(args.opset), *extra], check=True)
|
|
188
|
+
if args.verify:
|
|
189
|
+
run("--reference")
|
|
190
|
+
for _, src in bag:
|
|
191
|
+
run("--source", src)
|
|
192
|
+
if args.verify:
|
|
193
|
+
run("--source", src, "--check")
|
|
194
|
+
print(f"done: {args.model} in {out_dir}")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
from demucs.pretrained import get_model
|
|
199
|
+
from demucs.htdemucs import HTDemucs
|
|
200
|
+
except ImportError:
|
|
201
|
+
raise SystemExit("torch and demucs are required: pip install torch demucs onnx onnxruntime")
|
|
202
|
+
|
|
203
|
+
def load(name):
|
|
204
|
+
model = get_model(name)
|
|
205
|
+
for m in getattr(model, "models", [model]):
|
|
206
|
+
if not isinstance(m, HTDemucs):
|
|
207
|
+
raise SystemExit(f"{name}: not a Hybrid Transformer Demucs model")
|
|
208
|
+
m.eval()
|
|
209
|
+
return model
|
|
210
|
+
|
|
211
|
+
if args.reference:
|
|
212
|
+
write_reference(load(args.model), out_dir)
|
|
213
|
+
elif args.source:
|
|
214
|
+
# the one specialist, by its signature: a single HTDemucs; exported, or checked in a fresh process
|
|
215
|
+
model, path = load(dict((s, g) for g, s in bag)[args.source]), out_dir / f"{args.source}.onnx"
|
|
216
|
+
if args.check:
|
|
217
|
+
verify_export(model, path, int(model.segment * model.samplerate))
|
|
218
|
+
else:
|
|
219
|
+
export_one(model, path, args.opset, False)
|
|
220
|
+
else:
|
|
221
|
+
model = load(args.model)
|
|
222
|
+
if args.verify:
|
|
223
|
+
write_reference(model, out_dir) # first, while memory is fresh
|
|
224
|
+
gc.collect()
|
|
225
|
+
export_one(getattr(model, "models", [model])[0], out_dir / f"{args.model}.onnx", args.opset, args.verify)
|
|
226
|
+
print(f"done: {args.model} in {out_dir}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
sys.exit(main())
|