File size: 2,775 Bytes
791f996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
---
license: mit
library_name: onnx
tags:
  - audio-to-audio
  - audio-source-separation
  - onnx
  - onnxruntime-web
  - webgpu
  - demucs
---

# htdemucs — ONNX graphs for onnxruntime-web (WebGPU)

Hybrid Transformer Demucs, exported to ONNX and prepared for **in-browser**
inference with [onnxruntime-web](https://onnxruntime.ai/) on the **WebGPU**
backend. Used by [stemstudio.jp](https://www.stemstudio.jp) for local,
upload-free stem separation.

Derived from [facebookresearch/demucs](https://github.com/facebookresearch/demucs)
(MIT). See `LICENSE` — the upstream notice travels with these files.

## Files

| File | Transfer (gzip) | Inflated | Stems |
|---|---|---|---|
| `htdemucs_ft.opt.fp16.istft.onnx.gzbin` | 141,591,461 | 172,746,888 | vocals / instrumental (fine-tuned) |
| `htdemucs_4s.opt.fp16.istft.onnx.gzbin` | 141,601,329 | 172,746,888 | vocals / drums / bass / other |
| `htdemucs_6s.opt.fp16.istft.onnx.gzbin` | 114,203,693 | 142,573,723 | + guitar / piano |

## Two things that are easy to get wrong

**1. The `.gzbin` extension is deliberate.** These are raw gzip streams stored
as ordinary files, and the browser inflates them with
`DecompressionStream('gzip')`. They are *not* named `.gz`, because some hosts
and dev servers see that extension and add `content-encoding: gzip`, at which
point the transport decompresses them and the application decompresses them
again — which fails with `incorrect header check`. Detect gzip by the leading
magic bytes (`1f 8b`), not by header or extension.

The compression is in the file rather than in the transport because model hosts
generally serve `.onnx` uncompressed regardless of `Accept-Encoding`.

**2. Build order matters: optimize *before* fp16, not after.** Running the
graph optimizer on an fp16 graph overflows folded constants to `inf` and every
output becomes `NaN`. Worse, `nan > tolerance` is false, so a naive comparison
reports "max diff 0.000e+00 / OK" on a completely broken model. The order used
here is: fp32 export → graph optimization → fp16 weights → inverse STFT folded
into the graph.

## Why the optimized graph is WebGPU-only

Measured on one 7.8 s segment, same model, same host:

| | wasm heap | wasm infer | WebGPU session build |
|---|---|---|---|
| unoptimized | 1,223 MB | 16.8 s | 15.6 s |
| optimized | **3,251 MB** | 18.1 s | **1.4 s** |

On WebGPU, session build gets ~11× faster. On wasm, heap grows 2.7× and
inference gets *slower*. "Ship the optimized one and everyone gets faster" is
false — pick per execution provider.

## Training data

The upstream weights were trained on MUSDB18-HQ and additional sources. The
MIT license covers the code and these derived graphs; the dataset carries its
own terms, which are worth checking for your own use.