diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d6dc432..429f14d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,21 @@ jobs:
- run: npm run acestep:check
- run: npm run acestep:test
+ # Vendored DiCoSe stem-separation runtime (packages/dicose): typecheck +
+ # weight-free vitest suite (the model-contract test self-excludes without
+ # the gitignored public/model package).
+ dicose:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci --ignore-scripts
+ - run: npm run dicose:check
+ - run: npm run dicose:test
+
# Prettier — style is enforced, not suggested.
format:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index cd1a3c6..6bfe258 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@ demo page.
| `eou-parakeet` | Parakeet EOU 120M | **297×** browser-verified (1hr in 12.1s; worker-overlapped wasm decode + linear-cost stream-batch encode) | transcript + end-of-utterance events; TRUE streaming push()/finish() (bit-exact cache-carrying encode) + wasm-SIMD RNNT decode; whole-clip batch runs through the same linear-cost encoder |
| `asr-voicechat` | VoiceChat-11B STT (609M encoder) | 34.6× (1hr file) | the speech-recognition slice of NVIDIA's full-duplex VoiceChat-11B; fully-causal per-frame streaming, parity byte-identical to the torch reference; weights hosted at [`FluidInference/fluidaudio-web`](https://huggingface.co/FluidInference/fluidaudio-web) like the other engines |
| `musicgen-acestep` | ACE-Step 1.5 Turbo (3.5B + VAE) | ~1.9× (180s song in ~95s, M3, warm) | full text-to-music on [`/music`](music.html): 8-step DiT + Oobleck VAE in pure WGSL (`packages/acestep`); ~5.7 GB one-time download; requires `shader-f16`; direct mode (optional planner LLM path exists upstream, still being optimized) |
+| `stem-dicose` | DiCoSe stem separation (BS-RoFormer + 1-step CD) | ~3× fast mode (30s / 48 kHz song in ~9.8s, M5 Pro); refined ~0.45× | 5 stems — drums/bass/other/vocals + derived instrumental — on [`/analyze.html`](analyze.html) and as **Split stems** on [`/music`](music.html); vendored `packages/dicose` (DiCoSe.wgsl by Hamza Qayyum); 623 MB f16 weight package; requires `shader-f16` + fixed 32-wide subgroups; mix-reconstruction NRMSE 1.5% (4-stem sum) / 9e-5 (vocals + instrumental) |
| `tts-voicechat` | VoiceChat-11B TTS “Aria” (595M backbone + 159M MoG + 763M codec) | 23.7 GPU-ms per 80 ms frame + ~16 ms host (timestamp-query, M5 Pro dawn; est. ~1.6× in-browser) — node wall is poll-bound at 0.12× (dawn ~100 ms/sync × 5 syncs/frame); WASM 0.19× | the speech-decoder slice of NVIDIA's full-duplex VoiceChat-11B as a standalone TTS voice; GPU-resident decode loop (backbone/MoG-MLP batched submits, GPU KV caches, 5 readbacks/frame for the host-side f64 PRVQ decisions); audio codes bit-exact vs the torch reference ON BOTH BACKENDS (1550/1550), waveform NRMSE 1.1e-6; codec GPU decode ~24 GPU-ms/s of audio; local-only weights (`scripts/extract-voicechat-tts.py`, ~3.5 GB) — hidden from the picker unless exported |
First (cold) run is several× slower — WebGPU compiles pipelines and weights
@@ -134,6 +135,13 @@ exact tuple). The optional 0.6B planner ("thinking") path is excluded from
the served manifest until its pending optimization experiments
(OPT-0084/0085/0087) are integrated.
+A finished song offers **Split stems**: DiCoSe (also by Hamza Qayyum,
+vendored at [`packages/dicose`](packages/dicose/)) separates the generated
+WAV into drums, bass, other, vocals, and a derived instrumental, right in
+the result panel — playable and downloadable per stem. Fast deterministic
+mode by default (~3× realtime); the 623 MB weight package downloads on
+first use and is released with the result panel.
+
## Text processing (WASM)
[`text-processing-rs`](https://github.com/FluidInference/text-processing-rs)
@@ -155,6 +163,7 @@ npm run build # static site → dist/
npm run sdk:pack # publishable SDK tarball (dist-sdk/ + .tgz in repo root)
npm run acestep:check && npm run acestep:test # ACE-Step runtime (packages/acestep) gates
+npm run dicose:check && npm run dicose:test # DiCoSe runtime (packages/dicose) gates
```
## Deploy
@@ -182,6 +191,8 @@ src/
packages/
acestep/ vendored ace-step-1.5.wgsl music-gen runtime (own kernels,
scheduler, tests, and optimization ledger — see its AGENTS.md)
+ dicose/ vendored DiCoSe.wgsl stem-separation runtime (own kernels,
+ tests, and optimization ledger)
scripts/ node gates: token-identity, kernel parity, per-engine smokes
rust/ parakeet RNNT decoder + kernel lib sources (wasm32+simd128)
docs/ architecture, benchmarks, PORTING.md (add-a-model checklist), the ORT removal story
@@ -235,5 +246,12 @@ over for integration here; we took it over, integrated, and are continuing
the optimization work. The `packages/acestep` runtime and the `/music` page's
backend seam are his code.
+Stem separation is likewise his: DiCoSe.wgsl (vendored at
+`packages/dicose`, MIT) ports DiCoSe — BS-RoFormer plus one-step
+consistency-distilled refinement, [karchkha/DiCoSe](https://huggingface.co/karchkha/DiCoSe)
+checkpoints — to raw WebGPU WGSL with its own correctness-audited kernel
+ledger, and powers both the `stem-dicose` engine and the `/music` page's
+Split stems feature.
+
See [THIRD-PARTY-LICENSES.md](./THIRD-PARTY-LICENSES.md) for the full
list of adapted code and licenses.
diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md
index b75ad3f..8e9ffd0 100644
--- a/THIRD-PARTY-LICENSES.md
+++ b/THIRD-PARTY-LICENSES.md
@@ -65,6 +65,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
+## DiCoSe.wgsl (MIT)
+
+Upstream repo currently ships no LICENSE file (its package.json declares
+MIT; the license text is pending from the author and will be added at
+`packages/dicose/LICENSE` when supplied). The complete vendored source is
+in this repository at `packages/dicose`.
+Copyright (c) 2026 Hamza Qayyum (Narcotic Software) — MIT License.
+
+The entire stem-separation runtime (`packages/dicose`) is a vendored
+import of DiCoSe.wgsl by **Hamza Qayyum**: a raw WebGPU WGSL port of
+DiCoSe (BS-RoFormer + one-step consistency-distilled refinement,
+arXiv 2412.06965) with its own correctness-audited optimization ledger
+(`packages/dicose/optimization/`). The `stem-dicose` engine and the
+`/music` page's Split stems feature are thin wrappers over his worker
+client. Model weights are converted from the
+[karchkha/DiCoSe](https://huggingface.co/karchkha/DiCoSe) checkpoints
+(MIT) via `packages/dicose/model/convert.py` and retain their upstream
+license.
+
## parakeet.js / ysdede (MIT)
https://github.com/ysdede/parakeet.js — the NeMo log-mel preprocessor in
diff --git a/analyze.html b/analyze.html
index 3176c18..56f9c62 100644
--- a/analyze.html
+++ b/analyze.html
@@ -47,8 +47,8 @@
FluidAudio Web Other Audio Models
- Voice activity detection and speaker diarization right here — WebGPU + WebAssembly, nothing leaves your machine. First load downloads the model
- weights (cached after).
+ Voice activity detection, speaker diarization, and music stem separation right here — WebGPU + WebAssembly, nothing leaves your machine. First
+ load downloads the model weights (cached after).
diff --git a/music.html b/music.html
index 2d55eb5..a13657b 100644
--- a/music.html
+++ b/music.html
@@ -275,9 +275,26 @@ Preparing
—
-
- Download WAV
-
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 2bee0a9..c483086 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
],
"dependencies": {
"ace-step-1.5.wgsl": "*",
+ "dicose-wgsl": "*",
"pinyin-pro": "^3.28.2"
},
"devDependencies": {
@@ -23,7 +24,7 @@
"vite": "^6.0.0"
},
"engines": {
- "node": ">=20"
+ "node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1457,6 +1458,10 @@
"node": ">=8"
}
},
+ "node_modules/dicose-wgsl": {
+ "resolved": "packages/dicose",
+ "link": true
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2635,6 +2640,96 @@
"optional": true
}
}
+ },
+ "packages/dicose": {
+ "name": "dicose-wgsl",
+ "version": "0.1.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^26.1.1",
+ "@webgpu/types": "^0.1.64",
+ "typescript": "^5.9.2",
+ "vite": "^8.1.5",
+ "vitest": "^4.1.10"
+ }
+ },
+ "packages/dicose/node_modules/vite": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
+ "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.26",
+ "rolldown": "~1.2.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index de5e924..36ad839 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,10 @@
"acestep:build": "npm run build --workspace ace-step-1.5.wgsl",
"acestep:check": "npm run check --workspace ace-step-1.5.wgsl",
"acestep:test": "npm run test --workspace ace-step-1.5.wgsl",
- "build": "npm run acestep:build && tsc --noEmit && vite build && node scripts/postbuild-strip-wasm.mjs",
+ "dicose:build": "npm run build --workspace dicose-wgsl",
+ "dicose:check": "npm run check --workspace dicose-wgsl",
+ "dicose:test": "npm run test --workspace dicose-wgsl",
+ "build": "npm run acestep:build && npm run dicose:build && tsc --noEmit && vite build && node scripts/postbuild-strip-wasm.mjs",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"gpu:verify": "node scripts/gpu-verify.mjs",
@@ -63,6 +66,7 @@
},
"dependencies": {
"ace-step-1.5.wgsl": "*",
+ "dicose-wgsl": "*",
"pinyin-pro": "^3.28.2"
},
"devDependencies": {
diff --git a/packages/dicose/.gitignore b/packages/dicose/.gitignore
new file mode 100644
index 0000000..1f3820b
--- /dev/null
+++ b/packages/dicose/.gitignore
@@ -0,0 +1,54 @@
+node_modules/
+dist/
+.vite/
+coverage/
+playwright-report/
+test-results/
+*.tsbuildinfo
+
+__pycache__/
+*.py[cod]
+.venv/
+.pytest_cache/
+.ruff_cache/
+.mypy_cache/
+.coverage
+htmlcov/
+
+.model-cache/
+.upstream-dicose/
+model/cache/
+public/model/
+model/package/
+model/downloads/
+public/.model.staging-*/
+public/.model.previous-*/
+benchmark/results/
+fixtures/generated/
+*.ckpt
+*.safetensors
+*.bin
+*.onnx
+*.pt
+*.pth
+*.wav
+!Mixture_audio_1.wav
+!public/Mixture_audio_1.wav
+
+.env
+.env.*
+!.env.example
+!.env.*.example
+.dev.vars*
+!.dev.vars.example
+
+.pnpm-store/
+.cache/
+.eslintcache
+*.log
+
+.DS_Store
+._*
+.idea/
+.*.sw[op]
+*~
diff --git a/packages/dicose/README.md b/packages/dicose/README.md
new file mode 100644
index 0000000..7f61fa1
--- /dev/null
+++ b/packages/dicose/README.md
@@ -0,0 +1,120 @@
+# DiCoSe WebGPU
+
+An interactive and automation-ready browser runtime for the released **[DiCoSe BS-RoFormer + one-step
+consistency-distilled (CD) refinement](https://arxiv.org/abs/2412.06965)**, using the
+[official model weights](https://huggingface.co/karchkha/DiCoSe). The neural graph is raw WGSL/WebGPU:
+f16 storage, f32 reductions, converter-native tile-major subgroup GEMM, fused
+online attention with producer-rotated K, RMSNorm, FiLM, Conv2d STFT
+conditioning, complex masks, and CD affine sampling.
+The CPU boundary is intentionally limited to WAV decoding, deterministic
+resampling, centered Hann STFT/ISTFT, seeded noise generation, and the final
+instrumental complement subtraction.
+
+The public API lives in `src/index.ts`. Browser inference runs in a dedicated
+worker and transfers PCM/result buffers instead of blocking the page thread.
+
+## Inference modes
+
+`new DiCoSeWorkerClient()` and `separateAudio(source)` keep the released
+full-resolution, one-step refined graph as the default. Fast is an explicit
+quality/performance tradeoff that returns the deterministic separator before
+CD refinement:
+
+```ts
+const fast = new DiCoSeWorkerClient();
+const fastResult = await fast.separateAudio(source, {
+ outputMode: "deterministic",
+});
+```
+
+Both modes return the four neural estimates under `result.stems` and a derived
+`result.instrumental`. Instrumental is computed as the decoded input mixture
+minus the vocal estimate after both have been restored to the uploaded file's
+sample rate and exact frame count. It adds no model pass and is deliberately
+not computed by summing drums, bass, and other.
+
+On the supplied WAV, deterministic-only had a 5.92-s sustained median. That
+number remains useful as a performance measurement, not quality evidence.
+Fast uses the released deterministic checkpoint but omits learned refinement.
+See `optimization/CORRECTNESS_AUDIT.md` and `optimization/LEDGER.md` for the
+current evidence and dispositions.
+
+## Model package
+
+Large checkpoints, download caches, and the generated weight blob are ignored
+by Git. Run this command manually whenever the local browser package needs to
+be prepared:
+
+```sh
+pnpm model:prepare
+```
+
+It downloads the two pinned official checkpoints, verifies them, converts the
+exact Full/Fast production package into `public/model/`, and verifies the
+canonical generated hashes. The source download is about 4.66 GB and is cached
+under `model/cache/`. The command requires `uv`; Python 3.13 and all converter
+dependencies come from the locked `model/` environment.
+
+## Run locally
+
+```sh
+pnpm dev
+```
+
+Open `http://127.0.0.1:5173/`, choose or drop a local WAV, select Full or Fast,
+and run the separation. The page shows stage timings and
+creates the four model stems plus a derived instrumental as five in-memory
+stereo WAVs with playback and download controls.
+The source file and generated outputs stay in the browser tab; they are not
+uploaded. Inputs above 12 seconds are processed as fixed 11-second model items
+with reflected context and normalized overlap-add. Full retains the upstream
+50% overlap policy. Fast overlaps only the existing 10% fade region. For the
+5,608,109-sample model-rate `trust_nobody.wav` input, that changes the plan from
+25 chunks in Full to 13 in Fast. A fresh isolated-Chrome sustained panel measured
+a 79.61-s median (71.57–113.05 s) for that Fast path; this does not meet the
+30-second target, and listening remains the quality gate. Long tracks still
+require serial model calls. File-based runs restore each output to the uploaded
+WAV's sample rate and exact frame count before playback/download.
+
+For an unattended page invocation, add `?autorun=1`; the result is published
+to `window.__DICOSE_BROWSER__.report` and `#result`. `?mode=benchmark` uses a
+single persistent worker/model package across its warmup and measured runs.
+Neither path opens a save dialog, download, or UI control.
+
+## Checks and isolated browser testing
+
+```sh
+pnpm check
+pnpm test
+pnpm test:reference-quality
+pnpm test:refined-reference-quality
+pnpm test:output-mode-quality
+pnpm test:webgpu
+pnpm test:browser
+pnpm benchmark:browser
+```
+
+The release benchmark accepts an explicit output selector:
+
+```sh
+DICOSE_BENCHMARK_OUTPUT_MODE=deterministic pnpm benchmark:browser
+```
+
+The browser scripts automatically start Vite and a new headless Chrome process
+with a freshly-created temporary `--user-data-dir`, then delete that profile,
+stop Chrome, and stop Vite in `finally`. They use CDP to await the automatic
+result; no user profile, click, permission prompt, or file save is involved.
+`test:browser` additionally enforces the fixture's f16 deterministic-output
+envelope against the upstream f32 reference. `test:reference-quality` checks
+the released deterministic graph and 30 internal tensor seams;
+`test:refined-reference-quality` checks the released one-step CD graph, 17
+internal CD seams, its raw model output, and the final refined stems against a
+fixed-noise execution of the official PyTorch implementation.
+
+`Mixture_audio_1.wav` is the supplied 22.05 kHz mono fixture. Production decode
+duplicates it to stereo and uses the Hann-windowed sinc geometry and defaults
+from torchaudio 2.0.2 before processing 1,189 centered-STFT frames. The
+deterministic model-arithmetic oracle deliberately replays its older frozen
+linear input tensor so resampler and neural-graph regressions remain separate
+gates. The CD sampler uses a fixed default noise seed, so an otherwise
+identical run is reproducible.
diff --git a/packages/dicose/index.html b/packages/dicose/index.html
new file mode 100644
index 0000000..879a4fa
--- /dev/null
+++ b/packages/dicose/index.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+ DiCoSe · Local stem separation
+
+
+ Skip to separator
+
+
+
+
+
+ Four-stem separation + instrumental
+ Pull a song apart.Right in your browser.
+
+ Turn any WAV file into vocals, drums, bass, other, and a complementary instrumental.
+ Your audio stays on this device—the model runs locally on your GPU.
+
+
+ ◆ No upload
+ ◆ No account
+ ◆ Five stereo WAV outputs
+
+
+
+
+
+
+
New separation
+
Choose your audio
+
+
WAV files only
+
+
+
+
+
+
+
+
+
+ Drop a WAV file here
+ or browse your files
+
+
+ No file selected
+ Choose a stereo or mono WAV file
+
+
+
+ Files longer than 12 seconds run as overlapping 11-second chunks. Full keeps
+ 50% overlap; Fast uses 10% overlap to reduce the number of model passes.
+
+
+
+
+
+ Separation mode
+ Choose the speed and quality balance
+
+
+
+
+
+
+
+
+ The complete deterministic model and full-resolution refinement graph.
+
+ Released full graph · 50% long-file overlap
+
+
+
+
+
+
+
+
+
+ Returns the deterministic separator output, skips refinement, and reduces long-file overlap.
+
+ Released checkpoint · 10% long-file overlap
+
+
+
+
+
+
+
+
+
+
+ Choose a WAV file to begin.
+
+
+
+ Separate stems
+ →
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DiCoSe requires a browser with WebGPU support and a compatible GPU.
+
+
+
+
+
+
+
+
diff --git a/packages/dicose/model/README.md b/packages/dicose/model/README.md
new file mode 100644
index 0000000..c52e672
--- /dev/null
+++ b/packages/dicose/model/README.md
@@ -0,0 +1,25 @@
+# Preparing the browser model
+
+Downloaded checkpoints, caches, and packed weights are not committed. The
+small audited manifest remains tracked. From the repository root, run the
+preparation explicitly:
+
+```sh
+pnpm model:prepare
+```
+
+The command downloads only these files from `karchkha/DiCoSe` at revision
+`b3e44147b96e55b08eea2dd0b6b4e017748a87a9`:
+
+- `Deterministic_model_MSST_bs_roformer/model.ckpt`
+- `CD_MSST_bs_roformer/model.ckpt`
+
+Both source files are checked by byte length and SHA-256 before PyTorch opens
+them. Conversion is staged transactionally, and the final manifest and 623 MB
+f16 blob must match the canonical production hashes before replacing
+`public/model/`. Downloads are resumable and retained in ignored
+`model/cache/`.
+
+For an already-downloaded pair, the lower-level converter accepts explicit
+`--deterministic` and `--cd` paths. Run `pnpm model:test` for the no-download
+preparation tests and `pnpm verify:package` to recheck an installed package.
diff --git a/packages/dicose/model/convert.py b/packages/dicose/model/convert.py
new file mode 100644
index 0000000..3657d2a
--- /dev/null
+++ b/packages/dicose/model/convert.py
@@ -0,0 +1,937 @@
+#!/usr/bin/env python3
+"""Download and convert the released DiCoSe checkpoints for WebGPU.
+
+With no checkpoint arguments, the exporter downloads the two pinned Hugging Face
+release files. It verifies their exact bytes *before* unpickling, exports only
+the two inference networks, and writes a transactional f16 package:
+
+ /manifest.json
+ /weights.f16.bin
+
+Run explicitly from the repository root:
+
+ pnpm model:prepare
+
+The manifest names tensors as ``det.`` and ``cd.``. All payloads live
+in one f16 file and begin at 256-byte aligned offsets. Dense matrices whose
+dimensions fit the production subgroup GEMM are stored in converter-native
+N128/N256 × K32 tiles; smaller tail shapes stay ``[in_features, out_features]``.
+Conv2d tensors remain OIHW.
+Repeated Torch storage views (notably RoPE frequency buffers) share one payload
+and are marked with ``aliasOf`` in the manifest.
+"""
+
+from __future__ import annotations
+
+import argparse
+import gc
+import hashlib
+import json
+import os
+import pickle
+import shutil
+import sys
+import uuid
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+
+ALIGNMENT_BYTES = 256
+LINEAR_TILE_INNER = 32
+LINEAR_TILE_COLUMNS = (256, 128)
+OUTPUT_MANIFEST = "manifest.json"
+OUTPUT_WEIGHTS = "weights.f16.bin"
+PACKAGE_SCHEMA = "dicose-wgsl-package-v1"
+MODEL_DIRECTORY = Path(__file__).resolve().parent
+REPOSITORY_ROOT = MODEL_DIRECTORY.parent
+UPSTREAM_REPOSITORY = "karchkha/DiCoSe"
+UPSTREAM_REVISION = "b3e44147b96e55b08eea2dd0b6b4e017748a87a9"
+CANONICAL_WEIGHTS_BYTES = 623_246_848
+CANONICAL_WEIGHTS_SHA256 = (
+ "96f65545dd3ef7aa3189a353c51cdb30c4e592c356260c87f06459f092eeb0fe"
+)
+CANONICAL_MANIFEST_SHA256 = (
+ "a280ddc377f1effac71698f85d5e12547f8e992aaceeb5efff4d07b6c5913a94"
+)
+
+# These are the byte-level LFS objects published at
+# https://huggingface.co/karchkha/DiCoSe, revision `UPSTREAM_REVISION`.
+# Checking them before torch.load() both verifies the model identity and avoids
+# unpickling an arbitrary local file by accident.
+@dataclass(frozen=True)
+class SourceSpec:
+ role: str
+ repository_path: str
+ expected_sha256: str
+ expected_bytes: int
+
+
+DETERMINISTIC_SOURCE = SourceSpec(
+ role="deterministic",
+ repository_path="Deterministic_model_MSST_bs_roformer/model.ckpt",
+ expected_sha256="8087fbdcbc63f11f3ee305ef042cf42a42a5802e8a76678997f6448cb45256f5",
+ expected_bytes=527_434_267,
+)
+CD_SOURCE = SourceSpec(
+ role="consistency_distilled",
+ repository_path="CD_MSST_bs_roformer/model.ckpt",
+ expected_sha256="d25035bed7294a227fcb0f1ea691a0d1b8452ef76bde0e411c2b75536acf13da",
+ expected_bytes=4_129_571_657,
+)
+
+
+BANDS = [2] * 24 + [4] * 12 + [12] * 8 + [24] * 8 + [48] * 8 + [128, 129]
+assert len(BANDS) == 62 and sum(BANDS) == 1025
+
+# Fixed architecture values from configs/consistency_model/bsrf_eval.yaml and
+# configs/deterministic_model/bsrf_eval.yaml in the official source repository.
+MODEL_CONFIG: dict[str, Any] = {
+ "name": "DiCoSe BS-RoFormer + CD",
+ "sampleRate": 44_100,
+ "nFft": 2_048,
+ "hopLength": 441,
+ "winLength": 2_048,
+ "stftNormalized": False,
+ "stftCenter": True,
+ "stftWindow": "hann_periodic",
+ "zeroDc": True,
+ "stereo": True,
+ "numStems": 4,
+ "stems": ["drums", "bass", "other", "vocals"],
+ "dim": 384,
+ "depth": 8,
+ "heads": 8,
+ "dimHead": 64,
+ "timeTransformerDepth": 1,
+ "freqTransformerDepth": 1,
+ "linearTransformerDepth": 0,
+ "maskEstimatorDepth": 2,
+ "mlpExpansionFactor": 2,
+ "freqsPerBands": BANDS,
+ "deterministic": {
+ "modelType": "bs_roformer",
+ "useContextTime": False,
+ },
+ "consistencyDistilled": {
+ "modelType": "bs_roformer_stems_in_out_stem_cond_random_stem",
+ "useContextTime": True,
+ "timeEmbedding": "Positional",
+ "useMixtureFeatureConditioning": True,
+ "stftAdapterType": "conv2d",
+ "stftAdapterHidden": 128,
+ "diffusionSigmaData": 0.06,
+ "sampler": "cm_multistep_cd",
+ "oneStepSigmaMax": 0.003934,
+ "sigmaMin": 0.0001,
+ "rho": 9,
+ },
+}
+
+
+@dataclass(frozen=True)
+class ComponentSpec:
+ id: str
+ state_prefix: str
+ namespace: str
+ expected_tensor_count: int
+ expected_numel: int
+
+
+DETERMINISTIC_COMPONENT = ComponentSpec(
+ id="deterministic",
+ state_prefix="model.unet.",
+ namespace="det",
+ expected_tensor_count=1_355,
+ expected_numel=131_704_612,
+)
+CD_COMPONENT = ComponentSpec(
+ id="consistency_distilled",
+ state_prefix="net.model.diffusion.net.",
+ namespace="cd",
+ expected_tensor_count=1_502,
+ expected_numel=179_866_024,
+)
+CD_EMBEDDED_DETERMINISTIC_PREFIX = "pre_trained_mixture_feature_extractor_model.model.unet."
+# Model1d registers its underlying separator both as ``model.unet`` and as the
+# internal Diffusion object's ``model.diffusion.net``. The latter is canonical
+# for this exporter; the former is a state-dict alias.
+CD_DUPLICATE_STUDENT_PREFIX = "net.model.unet."
+
+
+class ConversionError(RuntimeError):
+ """An input or output failed an intentional package invariant."""
+
+
+class IgnoredLightningMetadata:
+ """Inert replacement for unavailable Lightning/Hydra metadata classes.
+
+ The release checkpoints pickle dataset/config objects in metadata that the
+ exporter never reads. Their tensor ``state_dict`` is still reconstructed by
+ Torch's standard unpickler. Keeping these inert avoids installing the
+ upstream training stack just to unpack an inference state dictionary.
+ """
+
+ def __init__(self, *_: Any, **__: Any) -> None:
+ pass
+
+ def __setstate__(self, state: Any) -> None:
+ self.state = state
+
+
+class LightningMetadataUnpickler(pickle.Unpickler):
+ """Resolve unavailable metadata globals without executing upstream imports."""
+
+ def find_class(self, module: str, name: str) -> Any:
+ # These are training-only values in the released checkpoint's
+ # hyperparameters. Do not import the upstream project or its optional
+ # training dependencies merely to deserialize them.
+ if module == "main" or module.startswith("main."):
+ return IgnoredLightningMetadata
+ if module == "ml_collections" or module.startswith("ml_collections."):
+ return IgnoredLightningMetadata
+ try:
+ return super().find_class(module, name)
+ except (ModuleNotFoundError, ImportError, AttributeError):
+ # State-dict tensor rebuilding uses Torch globals, which resolve
+ # normally. Any remaining unavailable global can only belong to
+ # unused checkpoint metadata because the source SHA-256 was
+ # verified before this loader is entered.
+ return IgnoredLightningMetadata
+
+
+class LightningCheckpointPickleModule:
+ """pickle-module facade accepted by torch.load()."""
+
+ Unpickler = LightningMetadataUnpickler
+ load = pickle.load
+ dump = pickle.dump
+ HIGHEST_PROTOCOL = pickle.HIGHEST_PROTOCOL
+
+
+@dataclass
+class SelectedTensor:
+ component: ComponentSpec
+ suffix: str
+ source_key: str
+ name: str
+ tensor: Any
+
+
+@dataclass(frozen=True)
+class StorageView:
+ storage_id: int
+ storage_nbytes: int
+ storage_offset: int
+ dtype: str
+ shape: tuple[int, ...]
+ stride: tuple[int, ...]
+
+
+@dataclass(frozen=True)
+class Payload:
+ name: str
+ offset: int
+ byte_length: int
+ sha256: str
+ numel: int
+ shape: tuple[int, ...]
+ layout: str
+
+
+def sha256_file(path: Path) -> tuple[str, int]:
+ digest = hashlib.sha256()
+ size = 0
+ with path.open("rb") as handle:
+ while True:
+ block = handle.read(8 * 1024 * 1024)
+ if not block:
+ break
+ digest.update(block)
+ size += len(block)
+ return digest.hexdigest(), size
+
+
+def verify_source(path: Path, spec: SourceSpec) -> dict[str, Any]:
+ if not path.is_file():
+ raise ConversionError(f"{spec.role} checkpoint is not a regular file: {path}")
+
+ sha256, byte_count = sha256_file(path)
+ if byte_count != spec.expected_bytes:
+ raise ConversionError(
+ f"{spec.role} checkpoint has {byte_count:,} bytes; expected "
+ f"{spec.expected_bytes:,} for the released DiCoSe checkpoint."
+ )
+ if sha256 != spec.expected_sha256:
+ raise ConversionError(
+ f"{spec.role} checkpoint SHA-256 does not match the released DiCoSe object.\n"
+ f"expected: {spec.expected_sha256}\nactual: {sha256}"
+ )
+
+ return {
+ "role": spec.role,
+ "fileName": Path(spec.repository_path).name,
+ "byteLength": byte_count,
+ "sha256": sha256,
+ }
+
+
+def download_source_checkpoints(
+ cache_dir: Path,
+ snapshot_download: Any | None = None,
+) -> tuple[Path, Path]:
+ if snapshot_download is None:
+ from huggingface_hub import snapshot_download as hugging_face_snapshot_download
+
+ snapshot_download = hugging_face_snapshot_download
+
+ cache_dir = cache_dir.resolve()
+ cache_dir.mkdir(parents=True, exist_ok=True)
+ print(
+ f"Downloading pinned DiCoSe checkpoints from {UPSTREAM_REPOSITORY} "
+ f"at {UPSTREAM_REVISION}...",
+ flush=True,
+ )
+ try:
+ snapshot = Path(
+ snapshot_download(
+ repo_id=UPSTREAM_REPOSITORY,
+ revision=UPSTREAM_REVISION,
+ cache_dir=cache_dir / "huggingface",
+ local_dir=cache_dir / "official",
+ allow_patterns=[
+ DETERMINISTIC_SOURCE.repository_path,
+ CD_SOURCE.repository_path,
+ ],
+ )
+ )
+ except Exception as exc:
+ raise ConversionError(
+ "unable to download pinned DiCoSe checkpoints "
+ f"({type(exc).__name__}: {exc})"
+ ) from exc
+ deterministic = snapshot / DETERMINISTIC_SOURCE.repository_path
+ cd = snapshot / CD_SOURCE.repository_path
+ for path, spec in ((deterministic, DETERMINISTIC_SOURCE), (cd, CD_SOURCE)):
+ if not path.is_file():
+ raise ConversionError(
+ f"Hugging Face download omitted {spec.role} checkpoint: {path}"
+ )
+ return deterministic, cd
+
+
+def resolve_source_paths(
+ args: argparse.Namespace,
+ snapshot_download: Any | None = None,
+) -> tuple[Path, Path]:
+ deterministic = args.deterministic
+ cd = args.cd
+ if deterministic is None and cd is None:
+ return download_source_checkpoints(args.cache_dir, snapshot_download)
+ if deterministic is None or cd is None:
+ raise ConversionError(
+ "provide both --deterministic and --cd, or omit both to download the pinned release"
+ )
+ return deterministic, cd
+
+
+def require_torch() -> Any:
+ try:
+ import torch
+ except ModuleNotFoundError as exc: # pragma: no cover - depends on caller env
+ raise ConversionError(
+ "PyTorch is required. Run this script with `uv run --project model ...` "
+ "so model/pyproject.toml supplies the pinned runtime."
+ ) from exc
+ return torch
+
+
+def load_lightning_state_dict(torch: Any, path: Path, role: str) -> Mapping[str, Any]:
+ # mmap keeps the enormous CD checkpoint storage-backed instead of eagerly
+ # copying its 4.1 GB archive into Python heap memory.
+ try:
+ checkpoint = torch.load(
+ str(path),
+ map_location="cpu",
+ mmap=True,
+ weights_only=False,
+ pickle_module=LightningCheckpointPickleModule,
+ )
+ except Exception as exc: # torch errors vary by release
+ raise ConversionError(
+ f"unable to load {role} Lightning checkpoint: {path} "
+ f"({type(exc).__name__}: {exc})"
+ ) from exc
+
+ if not isinstance(checkpoint, Mapping):
+ raise ConversionError(f"{role} checkpoint root must be a mapping")
+ state_dict = checkpoint.get("state_dict")
+ if not isinstance(state_dict, Mapping):
+ raise ConversionError(f"{role} checkpoint is missing mapping key `state_dict`")
+ return state_dict
+
+
+def validate_tensor(torch: Any, tensor: Any, source_key: str) -> None:
+ if not isinstance(tensor, torch.Tensor):
+ raise ConversionError(f"{source_key} is not a Tensor")
+ if tensor.device.type != "cpu":
+ raise ConversionError(f"{source_key} did not load onto CPU")
+ if tensor.dtype != torch.float32:
+ raise ConversionError(
+ f"{source_key} has dtype {tensor.dtype}; released DiCoSe inference tensors must be float32"
+ )
+ if tensor.layout != torch.strided:
+ raise ConversionError(f"{source_key} has unsupported layout {tensor.layout}")
+ if tensor.numel() == 0:
+ raise ConversionError(f"{source_key} is empty")
+
+
+def select_component(
+ torch: Any, state_dict: Mapping[str, Any], spec: ComponentSpec
+) -> list[SelectedTensor]:
+ selected: list[SelectedTensor] = []
+ for source_key in sorted(state_dict):
+ if not source_key.startswith(spec.state_prefix):
+ continue
+ suffix = source_key.removeprefix(spec.state_prefix)
+ if not suffix:
+ raise ConversionError(f"empty tensor key below {spec.state_prefix!r}")
+ tensor = state_dict[source_key]
+ validate_tensor(torch, tensor, source_key)
+ selected.append(
+ SelectedTensor(
+ component=spec,
+ suffix=suffix,
+ source_key=source_key,
+ name=f"{spec.namespace}.{suffix}",
+ tensor=tensor,
+ )
+ )
+
+ names = [item.name for item in selected]
+ if len(names) != len(set(names)):
+ raise ConversionError(f"{spec.id} selection contains duplicate output tensor names")
+ numel = sum(item.tensor.numel() for item in selected)
+ if len(selected) != spec.expected_tensor_count or numel != spec.expected_numel:
+ raise ConversionError(
+ f"{spec.id} selection does not match the released architecture: "
+ f"got {len(selected):,} tensors / {numel:,} elements, expected "
+ f"{spec.expected_tensor_count:,} / {spec.expected_numel:,}."
+ )
+ return selected
+
+
+def storage_view(tensor: Any) -> StorageView:
+ storage = tensor.untyped_storage()
+ # _cdata is the native storage identity, unlike a Tensor's data_ptr which
+ # incorporates a possible view offset. It remains stable for this load.
+ storage_id = int(storage._cdata)
+ return StorageView(
+ storage_id=storage_id,
+ storage_nbytes=int(storage.nbytes()),
+ storage_offset=int(tensor.storage_offset()),
+ dtype=str(tensor.dtype),
+ shape=tuple(int(value) for value in tensor.shape),
+ stride=tuple(int(value) for value in tensor.stride()),
+ )
+
+
+def verify_cd_duplicate_student_aliases(
+ torch: Any, state_dict: Mapping[str, Any], selected_student: Sequence[SelectedTensor]
+) -> None:
+ """Ensure the excluded Lightning alias really aliases the canonical student."""
+
+ duplicate: dict[str, Any] = {
+ key.removeprefix(CD_DUPLICATE_STUDENT_PREFIX): value
+ for key, value in state_dict.items()
+ if key.startswith(CD_DUPLICATE_STUDENT_PREFIX)
+ }
+ canonical = {item.suffix: item.tensor for item in selected_student}
+ if set(duplicate) != set(canonical):
+ raise ConversionError(
+ "CD checkpoint's `net.model.unet.` alias does not exactly mirror "
+ "`net.model.diffusion.net.`. Refusing an ambiguous student selection."
+ )
+ for suffix, tensor in canonical.items():
+ other = duplicate[suffix]
+ validate_tensor(torch, other, f"{CD_DUPLICATE_STUDENT_PREFIX}{suffix}")
+ if storage_view(tensor) != storage_view(other):
+ raise ConversionError(
+ f"CD duplicated student storage is not an exact alias for {suffix!r}"
+ )
+
+
+def verify_embedded_deterministic(
+ torch: Any,
+ deterministic: Sequence[SelectedTensor],
+ cd_state_dict: Mapping[str, Any],
+) -> None:
+ """Fail if the CD checkpoint was conditioned on different deterministic weights."""
+
+ embedded: dict[str, Any] = {
+ key.removeprefix(CD_EMBEDDED_DETERMINISTIC_PREFIX): value
+ for key, value in cd_state_dict.items()
+ if key.startswith(CD_EMBEDDED_DETERMINISTIC_PREFIX)
+ }
+ expected = {item.suffix: item.tensor for item in deterministic}
+ if set(embedded) != set(expected):
+ raise ConversionError(
+ "CD checkpoint's embedded frozen deterministic extractor does not match "
+ "the expected DiCoSe BS-RoFormer tensor namespace."
+ )
+
+ for suffix, tensor in expected.items():
+ other = embedded[suffix]
+ validate_tensor(torch, other, f"{CD_EMBEDDED_DETERMINISTIC_PREFIX}{suffix}")
+ if tuple(tensor.shape) != tuple(other.shape) or not torch.equal(tensor, other):
+ raise ConversionError(
+ "The supplied deterministic checkpoint is not bit-identical to the "
+ f"extractor embedded in the CD checkpoint at tensor {suffix!r}."
+ )
+
+
+def packed_layout_and_shape(tensor: Any, source_key: str) -> tuple[str, tuple[int, ...]]:
+ """Return the WebGPU layout and shape for one released inference tensor.
+
+ PyTorch ``nn.Linear`` persists its matrix as ``[out_features, in_features]``.
+ The WGSL dense kernels index weights as ``[in_features, out_features]`` so
+ all released two-dimensional ``*.weight`` tensors are transposed at export,
+ except the student-conditioning lookup table. The checkpoint hash makes
+ this intentionally narrow classification safe: the only non-Linear 2-D
+ weight in these two inference namespaces is ``stem_embedding.weight``.
+ """
+
+ shape = tuple(int(value) for value in tensor.shape)
+ if tensor.ndim == 4 and source_key.endswith(".weight"):
+ # PyTorch Conv2d tensors are [out_channels, in_channels, height, width]
+ # and WGSL uses that same OIHW ordering.
+ return "conv-oihw", shape
+ if tensor.ndim == 2 and source_key.endswith(".weight"):
+ if source_key.endswith(".stem_embedding.weight"):
+ return "row-major", shape
+ packed_shape = (shape[1], shape[0])
+ inner, columns = packed_shape
+ if inner % LINEAR_TILE_INNER == 0:
+ for tile_columns in LINEAR_TILE_COLUMNS:
+ if columns % tile_columns == 0:
+ return f"linear-tile-n{tile_columns}-k{LINEAR_TILE_INNER}", packed_shape
+ return "linear-in-out", packed_shape
+ return "row-major", shape
+
+
+def f16_bytes(torch: Any, tensor: Any, source_key: str, layout: str) -> bytes:
+ source = tensor.detach()
+ if layout == "linear-in-out" or layout.startswith("linear-tile-"):
+ if source.ndim != 2:
+ raise ConversionError(f"linear layout requested for non-matrix {source_key}")
+ source = source.transpose(0, 1)
+ if layout.startswith("linear-tile-"):
+ inner, columns = (int(value) for value in source.shape)
+ tile_columns = int(layout.removeprefix("linear-tile-n").split("-", 1)[0])
+ if inner % LINEAR_TILE_INNER or columns % tile_columns:
+ raise ConversionError(f"invalid tiled linear shape for {source_key}: {source.shape}")
+ source = (
+ source.reshape(
+ inner // LINEAR_TILE_INNER,
+ LINEAR_TILE_INNER,
+ columns // tile_columns,
+ tile_columns,
+ )
+ .permute(2, 0, 1, 3)
+ )
+ converted = source.to(device="cpu", dtype=torch.float16).contiguous()
+ try:
+ raw = converted.view(torch.uint8).numpy().tobytes(order="C")
+ finally:
+ del converted
+ expected = tensor.numel() * 2
+ if len(raw) != expected:
+ raise ConversionError(
+ f"f16 conversion for {source_key} produced {len(raw)} bytes, expected {expected}"
+ )
+ return raw
+
+
+def align_file(handle: Any, digest: Any, offset: int) -> int:
+ padding = (-offset) % ALIGNMENT_BYTES
+ if padding:
+ zeros = b"\0" * padding
+ handle.write(zeros)
+ digest.update(zeros)
+ return offset + padding
+
+
+def write_weight_blob(
+ torch: Any, stage_dir: Path, tensors: Sequence[SelectedTensor]
+) -> tuple[dict[str, Any], list[dict[str, Any]]]:
+ """Write a single aligned blob and return its descriptor plus tensor entries."""
+
+ blob_path = stage_dir / OUTPUT_WEIGHTS
+ aliases: dict[StorageView, Payload] = {}
+ entries: list[dict[str, Any]] = []
+ digest = hashlib.sha256()
+ offset = 0
+ logical_elements = 0
+
+ with blob_path.open("xb") as handle:
+ for item in tensors:
+ logical_elements += item.tensor.numel()
+ view = storage_view(item.tensor)
+ layout, packed_shape = packed_layout_and_shape(item.tensor, item.source_key)
+ existing = aliases.get(view)
+ if existing is None:
+ offset = align_file(handle, digest, offset)
+ raw = f16_bytes(torch, item.tensor, item.source_key, layout)
+ payload = Payload(
+ name=item.name,
+ offset=offset,
+ byte_length=len(raw),
+ sha256=hashlib.sha256(raw).hexdigest(),
+ numel=item.tensor.numel(),
+ shape=packed_shape,
+ layout=layout,
+ )
+ handle.write(raw)
+ digest.update(raw)
+ offset += len(raw)
+ aliases[view] = payload
+ alias_of: str | None = None
+ else:
+ payload = existing
+ alias_of = existing.name
+ if (
+ payload.numel != item.tensor.numel()
+ or payload.shape != packed_shape
+ or payload.layout != layout
+ ):
+ raise ConversionError(
+ f"storage alias packing mismatch for {item.name}; refusing to "
+ "reuse a payload with a different WebGPU interpretation"
+ )
+
+ entry: dict[str, Any] = {
+ "name": item.name,
+ "sourceKey": item.source_key,
+ "sourceShape": list(item.tensor.shape),
+ "shape": list(packed_shape),
+ "dtype": "f16",
+ "layout": layout,
+ "offset": payload.offset,
+ "byteLength": payload.byte_length,
+ "sha256": payload.sha256,
+ }
+ if alias_of is not None:
+ entry["aliasOf"] = alias_of
+ entries.append(entry)
+
+ handle.flush()
+ os.fsync(handle.fileno())
+
+ file_sha256, byte_count = sha256_file(blob_path)
+ expected_sha256 = digest.hexdigest()
+ if file_sha256 != expected_sha256 or byte_count != offset:
+ raise ConversionError("weights.f16.bin changed while it was being written")
+
+ canonical_entries = [entry for entry in entries if "aliasOf" not in entry]
+ return (
+ {
+ "file": OUTPUT_WEIGHTS,
+ "byteLength": byte_count,
+ "sha256": file_sha256,
+ "dtype": "f16",
+ "endianness": "little",
+ "alignment": ALIGNMENT_BYTES,
+ "logicalTensorCount": len(entries),
+ "uniqueTensorCount": len(canonical_entries),
+ "logicalElementCount": logical_elements,
+ "uniquePayloadBytes": sum(entry["byteLength"] for entry in canonical_entries),
+ },
+ entries,
+ )
+
+
+def validate_package_layout(weights: Mapping[str, Any], tensors: Sequence[Mapping[str, Any]]) -> None:
+ by_name = {entry["name"]: entry for entry in tensors}
+ if len(by_name) != len(tensors):
+ raise ConversionError("manifest would contain duplicate tensor names")
+
+ occupied: list[tuple[int, int, str]] = []
+ for entry in tensors:
+ offset = entry["offset"]
+ byte_length = entry["byteLength"]
+ expected_length = 2
+ for dimension in entry["shape"]:
+ expected_length *= dimension
+ if byte_length != expected_length:
+ raise ConversionError(f"invalid packed length for {entry['name']}")
+ if offset % ALIGNMENT_BYTES:
+ raise ConversionError(f"unaligned packed offset for {entry['name']}")
+ alias_of = entry.get("aliasOf")
+ if alias_of is not None:
+ target = by_name.get(alias_of)
+ if target is None:
+ raise ConversionError(f"alias target missing for {entry['name']}")
+ for key in ("offset", "byteLength", "sha256", "shape", "layout"):
+ if entry[key] != target[key]:
+ raise ConversionError(f"alias payload mismatch for {entry['name']}")
+ continue
+ occupied.append((offset, offset + byte_length, entry["name"]))
+
+ previous_end = 0
+ for start, end, name in sorted(occupied):
+ if start < previous_end or end > weights["byteLength"]:
+ raise ConversionError(f"overlapping or out-of-range payload for {name}")
+ previous_end = end
+
+
+def validate_canonical_package(package_dir: Path) -> None:
+ weights_path = package_dir / OUTPUT_WEIGHTS
+ manifest_path = package_dir / OUTPUT_MANIFEST
+ weights_sha256, weights_bytes = sha256_file(weights_path)
+ if (
+ weights_bytes != CANONICAL_WEIGHTS_BYTES
+ or weights_sha256 != CANONICAL_WEIGHTS_SHA256
+ ):
+ raise ConversionError(
+ "generated weights do not match the canonical production package\n"
+ f"expected: {CANONICAL_WEIGHTS_BYTES} bytes / {CANONICAL_WEIGHTS_SHA256}\n"
+ f"actual: {weights_bytes} bytes / {weights_sha256}"
+ )
+ manifest_sha256, _ = sha256_file(manifest_path)
+ if manifest_sha256 != CANONICAL_MANIFEST_SHA256:
+ raise ConversionError(
+ "generated manifest does not match the canonical production package\n"
+ f"expected: {CANONICAL_MANIFEST_SHA256}\nactual: {manifest_sha256}"
+ )
+
+
+def write_json(path: Path, value: Mapping[str, Any]) -> None:
+ encoded = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
+ with path.open("xb") as handle:
+ handle.write(encoded)
+ handle.flush()
+ os.fsync(handle.fileno())
+
+
+def fsync_directory(path: Path) -> None:
+ """Best-effort directory durability on POSIX filesystems."""
+
+ try:
+ descriptor = os.open(path, os.O_RDONLY)
+ except OSError:
+ return
+ try:
+ os.fsync(descriptor)
+ except OSError:
+ pass
+ finally:
+ os.close(descriptor)
+
+
+def ensure_safe_output(output: Path, sources: Sequence[Path], overwrite: bool) -> Path:
+ output_parent = output.parent.resolve()
+ output_parent.mkdir(parents=True, exist_ok=True)
+ resolved = (output_parent / output.name).resolve()
+
+ for source in sources:
+ resolved_source = source.resolve()
+ if resolved_source == resolved or resolved_source.is_relative_to(resolved):
+ raise ConversionError(
+ "refusing an output directory that would contain or replace a source checkpoint"
+ )
+
+ if resolved.exists():
+ if not resolved.is_dir():
+ raise ConversionError(f"output exists and is not a directory: {resolved}")
+ if not overwrite:
+ raise ConversionError(
+ f"output already exists: {resolved}. Re-run with --overwrite to replace it."
+ )
+ return resolved
+
+
+def publish_transaction(stage: Path, output: Path, overwrite: bool) -> None:
+ backup: Path | None = None
+ try:
+ if output.exists():
+ if not overwrite: # protected earlier, retained as a race-safe guard
+ raise ConversionError(f"output appeared during conversion: {output}")
+ backup = output.with_name(f".{output.name}.previous-{uuid.uuid4().hex}")
+ os.replace(output, backup)
+ os.replace(stage, output)
+ fsync_directory(output.parent)
+ except Exception:
+ if backup is not None and backup.exists() and not output.exists():
+ os.replace(backup, output)
+ raise
+ else:
+ if backup is not None:
+ try:
+ shutil.rmtree(backup)
+ except OSError as exc:
+ # The new package was published successfully. Keeping the prior
+ # directory is safer than reporting a failed conversion or
+ # deleting it through a less reliable recovery path.
+ print(f"warning: retained previous output at {backup}: {exc}", file=sys.stderr)
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description=(
+ "Download and strictly export the released DiCoSe BS-RoFormer "
+ "deterministic and consistency-distilled checkpoints into one "
+ "aligned WebGPU f16 package."
+ ),
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ "--deterministic",
+ type=Path,
+ default=None,
+ metavar="PATH",
+ help=(
+ "local deterministic checkpoint; omit with --cd to download both "
+ "pinned files"
+ ),
+ )
+ parser.add_argument(
+ "--cd",
+ type=Path,
+ default=None,
+ metavar="PATH",
+ help=(
+ "local CD checkpoint; omit with --deterministic to download both "
+ "pinned files"
+ ),
+ )
+ parser.add_argument(
+ "--cache-dir",
+ type=Path,
+ default=MODEL_DIRECTORY / "cache",
+ metavar="DIR",
+ help="ignored directory for resumable Hugging Face downloads",
+ )
+ parser.add_argument(
+ "--output",
+ type=Path,
+ default=REPOSITORY_ROOT / "public/model",
+ metavar="DIR",
+ help="directory to publish manifest.json and weights.f16.bin",
+ )
+ parser.add_argument(
+ "--overwrite",
+ action="store_true",
+ help="transactionally replace an existing output directory",
+ )
+ return parser
+
+
+def convert(args: argparse.Namespace) -> dict[str, Any]:
+ if sys.byteorder != "little":
+ raise ConversionError("this f16 package format requires a little-endian host")
+
+ deterministic_path = args.deterministic.resolve()
+ cd_path = args.cd.resolve()
+ output = ensure_safe_output(args.output, [deterministic_path, cd_path], args.overwrite)
+
+ # Hash first: `torch.load(..., weights_only=False)` is necessary for the
+ # Lightning metadata, so only known published bytes are accepted.
+ source_manifest = [
+ verify_source(deterministic_path, DETERMINISTIC_SOURCE),
+ verify_source(cd_path, CD_SOURCE),
+ ]
+ source_by_role = {entry["role"]: entry for entry in source_manifest}
+
+ torch = require_torch()
+ deterministic_state = load_lightning_state_dict(torch, deterministic_path, "deterministic")
+ deterministic = select_component(torch, deterministic_state, DETERMINISTIC_COMPONENT)
+
+ cd_state = load_lightning_state_dict(torch, cd_path, "consistency-distilled")
+ cd_student = select_component(torch, cd_state, CD_COMPONENT)
+ verify_cd_duplicate_student_aliases(torch, cd_state, cd_student)
+ verify_embedded_deterministic(torch, deterministic, cd_state)
+
+ selected = [*deterministic, *cd_student]
+ stage = output.with_name(f".{output.name}.staging-{uuid.uuid4().hex}")
+ try:
+ stage.mkdir(parents=False, exist_ok=False)
+ weights, tensor_entries = write_weight_blob(torch, stage, selected)
+ validate_package_layout(weights, tensor_entries)
+ manifest: dict[str, Any] = {
+ "schema": PACKAGE_SCHEMA,
+ "source": {
+ "upstreamRevision": UPSTREAM_REVISION,
+ "deterministicCheckpointSha256": source_by_role["deterministic"]["sha256"],
+ "cdCheckpointSha256": source_by_role["consistency_distilled"]["sha256"],
+ },
+ "config": MODEL_CONFIG,
+ # Retain the audited source-file details beyond the small runtime
+ # source object, including their checked byte lengths.
+ "sources": source_manifest,
+ "components": [
+ {
+ "id": DETERMINISTIC_COMPONENT.id,
+ "namespace": DETERMINISTIC_COMPONENT.namespace,
+ "stateDictPrefix": DETERMINISTIC_COMPONENT.state_prefix,
+ "expectedTensorCount": DETERMINISTIC_COMPONENT.expected_tensor_count,
+ "expectedElementCount": DETERMINISTIC_COMPONENT.expected_numel,
+ },
+ {
+ "id": CD_COMPONENT.id,
+ "namespace": CD_COMPONENT.namespace,
+ "stateDictPrefix": CD_COMPONENT.state_prefix,
+ "expectedTensorCount": CD_COMPONENT.expected_tensor_count,
+ "expectedElementCount": CD_COMPONENT.expected_numel,
+ },
+ ],
+ "weights": weights,
+ "tensors": tensor_entries,
+ }
+ write_json(stage / OUTPUT_MANIFEST, manifest)
+ validate_canonical_package(stage)
+ fsync_directory(stage)
+ publish_transaction(stage, output, args.overwrite)
+ except Exception:
+ if stage.exists():
+ shutil.rmtree(stage)
+ raise
+ finally:
+ # Release the mmap-backed state dictionaries before returning to a caller
+ # that may immediately invoke a browser benchmark in the same shell.
+ del selected, deterministic, cd_student, deterministic_state, cd_state
+ gc.collect()
+
+ return {
+ "output": output,
+ "weights": weights,
+ "tensor_count": len(tensor_entries),
+ "unique_tensor_count": weights["uniqueTensorCount"],
+ }
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+ try:
+ args.deterministic, args.cd = resolve_source_paths(args)
+ result = convert(args)
+ except ConversionError as exc:
+ print(f"conversion failed: {exc}", file=sys.stderr)
+ return 2
+ except OSError as exc:
+ print(f"conversion failed: {exc}", file=sys.stderr)
+ return 2
+
+ print(
+ "wrote "
+ f"{result['output'] / OUTPUT_MANIFEST} and {result['output'] / OUTPUT_WEIGHTS} "
+ f"({result['tensor_count']:,} logical tensors; "
+ f"{result['unique_tensor_count']:,} unique f16 payloads; "
+ f"{result['weights']['byteLength']:,} bytes)"
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packages/dicose/model/pyproject.toml b/packages/dicose/model/pyproject.toml
new file mode 100644
index 0000000..0924053
--- /dev/null
+++ b/packages/dicose/model/pyproject.toml
@@ -0,0 +1,13 @@
+[project]
+name = "dicose-weight-converter"
+version = "0.1.0"
+description = "Strict DiCoSe BS-RoFormer + CD checkpoint exporter for the WebGPU demo"
+requires-python = ">=3.13,<3.14"
+dependencies = [
+ "huggingface-hub==1.24.0",
+ "numpy>=2.0",
+ "torch>=2.6,<3",
+]
+
+[tool.uv]
+package = false
diff --git a/packages/dicose/model/tests/test_convert.py b/packages/dicose/model/tests/test_convert.py
new file mode 100644
index 0000000..ff50c9b
--- /dev/null
+++ b/packages/dicose/model/tests/test_convert.py
@@ -0,0 +1,132 @@
+from __future__ import annotations
+
+import argparse
+import hashlib
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+import convert # noqa: E402
+
+
+class ModelPreparationTests(unittest.TestCase):
+ def test_default_download_is_pinned_and_selective(self) -> None:
+ with tempfile.TemporaryDirectory() as temporary:
+ cache_dir = Path(temporary) / "cache"
+ calls: list[dict[str, object]] = []
+
+ def snapshot_download(**kwargs: object) -> str:
+ calls.append(kwargs)
+ local_dir = Path(kwargs["local_dir"])
+ for spec in (convert.DETERMINISTIC_SOURCE, convert.CD_SOURCE):
+ path = local_dir / spec.repository_path
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.touch()
+ return str(local_dir)
+
+ deterministic, cd = convert.download_source_checkpoints(
+ cache_dir,
+ snapshot_download,
+ )
+
+ self.assertEqual(len(calls), 1)
+ call = calls[0]
+ self.assertEqual(call["repo_id"], convert.UPSTREAM_REPOSITORY)
+ self.assertEqual(call["revision"], convert.UPSTREAM_REVISION)
+ self.assertEqual(
+ call["allow_patterns"],
+ [
+ convert.DETERMINISTIC_SOURCE.repository_path,
+ convert.CD_SOURCE.repository_path,
+ ],
+ )
+ self.assertEqual(
+ deterministic,
+ Path(call["local_dir"])
+ / convert.DETERMINISTIC_SOURCE.repository_path,
+ )
+ self.assertEqual(
+ cd,
+ Path(call["local_dir"]) / convert.CD_SOURCE.repository_path,
+ )
+
+ def test_local_overrides_require_both_checkpoints(self) -> None:
+ args = argparse.Namespace(
+ deterministic=Path("det.ckpt"),
+ cd=None,
+ cache_dir=Path("cache"),
+ )
+ with self.assertRaisesRegex(convert.ConversionError, "provide both"):
+ convert.resolve_source_paths(args)
+
+ def test_download_failure_has_a_concise_conversion_error(self) -> None:
+ with tempfile.TemporaryDirectory() as temporary:
+
+ def fail_download(**_kwargs: object) -> str:
+ raise RuntimeError("network unavailable")
+
+ with self.assertRaisesRegex(
+ convert.ConversionError,
+ "unable to download pinned DiCoSe checkpoints",
+ ):
+ convert.download_source_checkpoints(
+ Path(temporary) / "cache",
+ fail_download,
+ )
+
+ def test_local_overrides_bypass_download(self) -> None:
+ deterministic = Path("det.ckpt")
+ cd = Path("cd.ckpt")
+ args = argparse.Namespace(
+ deterministic=deterministic,
+ cd=cd,
+ cache_dir=Path("cache"),
+ )
+ downloader = mock.Mock(side_effect=AssertionError("unexpected download"))
+ self.assertEqual(
+ convert.resolve_source_paths(args, downloader),
+ (deterministic, cd),
+ )
+ downloader.assert_not_called()
+
+ def test_default_paths_do_not_depend_on_the_calling_directory(self) -> None:
+ args = convert.build_parser().parse_args([])
+ self.assertEqual(args.cache_dir, convert.MODEL_DIRECTORY / "cache")
+ self.assertEqual(args.output, convert.REPOSITORY_ROOT / "public/model")
+
+ def test_canonical_package_gate_checks_independent_hashes(self) -> None:
+ with tempfile.TemporaryDirectory() as temporary:
+ package = Path(temporary)
+ weights = b"weights"
+ manifest = b"manifest"
+ (package / convert.OUTPUT_WEIGHTS).write_bytes(weights)
+ (package / convert.OUTPUT_MANIFEST).write_bytes(manifest)
+ with (
+ mock.patch.object(convert, "CANONICAL_WEIGHTS_BYTES", len(weights)),
+ mock.patch.object(
+ convert,
+ "CANONICAL_WEIGHTS_SHA256",
+ hashlib.sha256(weights).hexdigest(),
+ ),
+ mock.patch.object(
+ convert,
+ "CANONICAL_MANIFEST_SHA256",
+ hashlib.sha256(manifest).hexdigest(),
+ ),
+ ):
+ convert.validate_canonical_package(package)
+ (package / convert.OUTPUT_WEIGHTS).write_bytes(b"changed")
+ with self.assertRaisesRegex(
+ convert.ConversionError,
+ "canonical production",
+ ):
+ convert.validate_canonical_package(package)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/packages/dicose/model/uv.lock b/packages/dicose/model/uv.lock
new file mode 100644
index 0000000..c2b265f
--- /dev/null
+++ b/packages/dicose/model/uv.lock
@@ -0,0 +1,574 @@
+version = 1
+revision = 3
+requires-python = "==3.13.*"
+
+[options]
+exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
+exclude-newer-span = "P1W"
+
+[options.exclude-newer-package]
+yt-dlp = false
+
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cuda-bindings"
+version = "13.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-pathfinder" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" },
+]
+
+[[package]]
+name = "cuda-pathfinder"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" },
+]
+
+[[package]]
+name = "cuda-toolkit"
+version = "13.0.3.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" },
+]
+
+[package.optional-dependencies]
+cublas = [
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cudart = [
+ { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cufft = [
+ { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cufile = [
+ { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cupti = [
+ { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+curand = [
+ { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cusolver = [
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cusparse = [
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvjitlink = [
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvrtc = [
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvtx = [
+ { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+
+[[package]]
+name = "dicose-weight-converter"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "torch" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "huggingface-hub", specifier = "==1.24.0" },
+ { name = "numpy", specifier = ">=2.0" },
+ { name = "torch", specifier = ">=2.6,<3" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.32.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
+ { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "1.24.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/9b/d3bb4e7d792835daf34dd7091bbc7d7b4e0437d9388f1ea7239cce49f478/huggingface_hub-1.24.0.tar.gz", hash = "sha256:18431ff4daae0749aa9ba102fc952e314c98e1d30ebdec5319d85ca0a83e1ae5", size = 921848, upload-time = "2026-07-17T09:54:01.022Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/c3/aeaaf3911d2529614be18d1c8b5496afc185560e76568063d517283318af/huggingface_hub-1.24.0-py3-none-any.whl", hash = "sha256:6ed4120a84a6beec900640aa7e346bd766a6b7341e41526fef5dc8bd81fb7d59", size = 771904, upload-time = "2026-07-17T09:53:59.106Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" },
+ { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" },
+ { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" },
+ { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" },
+]
+
+[[package]]
+name = "nvidia-cublas"
+version = "13.1.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cuda-nvrtc" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti"
+version = "13.0.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
+ { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc"
+version = "13.0.88"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime"
+version = "13.0.96"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu13"
+version = "9.20.0.48"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" },
+]
+
+[[package]]
+name = "nvidia-cufft"
+version = "12.0.0.61"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
+]
+
+[[package]]
+name = "nvidia-cufile"
+version = "1.15.1.6"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
+]
+
+[[package]]
+name = "nvidia-curand"
+version = "10.4.0.35"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver"
+version = "12.0.4.66"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas" },
+ { name = "nvidia-cusparse" },
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse"
+version = "12.6.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu13"
+version = "0.8.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" },
+ { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu13"
+version = "2.29.7"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink"
+version = "13.3.33"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" },
+ { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu13"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx"
+version = "13.0.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "84.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-bindings", marker = "sys_platform == 'linux'" },
+ { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" },
+ { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" },
+ { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.70.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.7.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" },
+ { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
diff --git a/packages/dicose/optimization/ARITHMETIC_BUDGET.md b/packages/dicose/optimization/ARITHMETIC_BUDGET.md
new file mode 100644
index 0000000..a560ca8
--- /dev/null
+++ b/packages/dicose/optimization/ARITHMETIC_BUDGET.md
@@ -0,0 +1,89 @@
+# Production arithmetic budget
+
+This is the first-principles budget for the supplied 11.89-second WAV after it
+becomes 1,189 STFT frames and 62 learned frequency bands. The transformer sees
+`R = 1,189 × 62 = 73,718` rows. Logical FLOPs count a multiply-add as two and
+do not claim that every operation maps to identical hardware instructions.
+
+## Where the arithmetic comes from
+
+The deterministic network executes 16 transformer blocks (eight layers over
+two axes). Four consistency-distilled stem refinements execute another 64, for
+80 blocks total.
+
+| Dense projection per block | Logical GFLOP | Calls | Total TFLOP |
+| --- | ---: | ---: | ---: |
+| QKV 384→1,536 | 86.961 | 80 | 6.957 |
+| gates 384→8 | 0.453 | 80 | 0.036 |
+| attention output 512→384 | 28.987 | 80 | 2.319 |
+| FF up 384→1,536 | 86.961 | 80 | 6.957 |
+| FF down 1,536→384 | 86.961 | 80 | 6.957 |
+| **Transformer dense subtotal** | **290.324** | **80** | **23.226** |
+
+Condition adapters, band split, mapping, and mask estimators add about 1.226
+TFLOP, giving approximately **24.452 TFLOP of dense work**.
+
+Each of the five network evaluations has eight time-axis and eight
+frequency-axis attention calls. Exact all-key attention therefore contributes:
+
+| Attention geometry | Logical GFLOP/call | Calls | Total TFLOP |
+| --- | ---: | ---: | ---: |
+| 62 sequences × 1,189 tokens | 179.509 | 40 | 7.180 |
+| 1,189 sequences × 62 tokens | 9.360 | 40 | 0.374 |
+| **Attention subtotal** | | | **7.555** |
+
+The four STFT-adapter convolutions add 0.102 TFLOP. The resulting accounted
+total is about **32.11 TFLOP**, before small elementwise and DSP work.
+
+## Measured reconciliation and hard floor
+
+Retained production-shape medians predict roughly 12.65 seconds for the four
+large dense projections across 80 blocks and 8.21 seconds for Flash attention.
+Those two families alone explain about 20.86 seconds of the 23.44-second cold
+model wall, so scheduling and elementwise cleanup cannot produce another order
+of magnitude.
+
+Even the impossible best case where all 32.11 TFLOP—including softmax work—ran
+continuously at Parakeet's measured 2.7 TFLOP/s has a lower bound of **11.89
+seconds**, before ISTFT or browser overhead. Higher utilization can reach
+roughly realtime; substantially sub-realtime execution also requires fewer
+model operations.
+
+## Priority consequences after the measured branches
+
+1. Do not use whole-graph logical TFLOP/s as a contraction-utilization metric.
+ The retained exact dense kernels sustain roughly 2.05–2.13 TFLOP/s; the
+ former 0.63 figure mixed dense contractions, online softmax, DSP, dispatch,
+ and browser wall time into one denominator.
+2. Exact-kernel headroom is no longer large enough to meet a substantially
+ sub-realtime goal. Bounded-f16 dense partials saved only a projected 0.87 s,
+ native-K4 layout work missed its migration gate, and bounded-f16 Flash
+ partials regressed both production geometries. Reopen these families only
+ for a materially different algorithm with a multi-second projection.
+3. Removing evaluations has the highest demonstrated leverage. Returning the
+ already-computed deterministic separator output removes all four CD calls
+ and reaches a 5.92-s sustained median. This needs ground-truth and listening
+ qualification, not another kernel benchmark.
+4. Fast long-track scheduling now overlaps only the existing 10% fade region.
+ Relative to applying Full's overlap schedule to Fast, `trust_nobody.wav`
+ uses 13 deterministic graph calls instead of 25, a 1.92× chunk-count
+ reduction. Its isolated-Chrome sustained median is 79.61 s, including
+ 69.51 s of deterministic compute in the median sample. This is not evidence
+ that seam quality is acceptable, and it does not meet the 30-second target.
+5. Reducing only the CD temporal rows previously preserved deterministic
+ diagnostics exactly and measured 14.08 s in a controlled pair, with
+ 1.8–3.5% global waveform NRMSE. That Balanced experiment was not promoted;
+ its runtime switch was removed and OPT-0025 retains the historical evidence.
+6. If inference-time token reduction fails its listening gate, the reliable
+ fallback is a trained smaller graph: distill a lower-token deterministic
+ trunk and/or a joint/cheaper refinement policy.
+ Untrained all-network stride-2 caused 0.16–0.48 NRMSE, while optimal
+ weight-only SVD had no useful error/FLOP crossing. Batch-processing stems,
+ skinny projections, normalization, and elementwise fusion do not remove
+ enough arithmetic to change this conclusion.
+7. OPT-0027 tested a separate Extra Fast path that changed the complete
+ STFT/model/mask/ISTFT temporal grid from hop 441 to 882. It reduced a fixed
+ item from 1,101 to 551 frames and the 13-call long-track budget from 75.687
+ to 33.875 TFLOP, reaching a 28.04-s `trust_nobody.wav` sustained median.
+ Listening found the quality loss unacceptable relative to Fast, so the mode
+ and its half-rate implementation were rejected and pruned.
diff --git a/packages/dicose/optimization/BASELINE.md b/packages/dicose/optimization/BASELINE.md
new file mode 100644
index 0000000..9a21186
--- /dev/null
+++ b/packages/dicose/optimization/BASELINE.md
@@ -0,0 +1,42 @@
+# Accepted baseline
+
+> Correctness reset (2026-08-23): the timing results below remain useful as
+> performance measurements, but their old output gates did not compare against
+> upstream and allowed shared GELU and CD-time-conditioning defects to pass.
+> Do not treat them as quality evidence. See
+> [`CORRECTNESS_AUDIT.md`](CORRECTNESS_AUDIT.md) for the repaired reference
+> gates and audit findings.
+
+Fixture: `Mixture_audio_1.wav` (SHA-256
+`9e487f3a84b974b11b47442d0fd99512ab4826130d04351e8c9625d84e107bb7`),
+duplicated to stereo and linearly resampled from 22.05 kHz to 44.1 kHz.
+
+Browser: isolated headless Chrome 151.0.7922.173 on the Apple/Metal WebGPU
+adapter, with `shader-f16` and fixed 32-lane `subgroups`.
+
+The pre-optimization generic-kernel acceptance run completed with no device or
+page errors and passed deterministic fixture diagnostics:
+
+| Boundary | Time (ms) |
+| --- | ---: |
+| deterministic BS-RoFormer | 22,003.0 |
+| four CD refinements | 80,629.9 |
+| complete model timing | 104,924.6 |
+| page end-to-end timing | 105,713.2 |
+
+This is a single clean-profile acceptance sample, not a statistical benchmark.
+The retained baseline is superseded only after a quality-gated multi-run
+benchmark is recorded in `LEDGER.md`.
+
+## Current exact reference
+
+After OPT-0003 through OPT-0016, the unchanged `refined` + `full` graph was
+remeasured with the release protocol in isolated Chrome 151: one warmup and
+three measured runs. End-to-end samples were 24,665.2, 25,548.8, and 26,230.3
+ms, for a **25,548.8-ms median** (range 24,665.2–26,230.3 ms). The corresponding
+median model timing was 25,537.2 ms: 5,279.4 ms deterministic, 18,018.8 ms for
+the four refinements, and 1,559.8 ms ISTFT.
+
+This is the current exact performance reference. It is **5.34× faster** than
+the original 136,560.2-ms sustained median. The rising samples are retained as
+evidence of thermal throttling rather than collapsed into the median alone.
diff --git a/packages/dicose/optimization/CORRECTNESS_AUDIT.md b/packages/dicose/optimization/CORRECTNESS_AUDIT.md
new file mode 100644
index 0000000..d4d0373
--- /dev/null
+++ b/packages/dicose/optimization/CORRECTNESS_AUDIT.md
@@ -0,0 +1,239 @@
+# Upstream correctness audit
+
+This audit compares the browser runtime with the official DiCoSe source at
+commit `a1dc0a41ad2b1829674a60ea74e74edfd1509083`. The released checkpoints used
+by both sides are byte-identical:
+
+- deterministic: `8087fbdcbc63f11f3ee305ef042cf42a42a5802e8a76678997f6448cb45256f5`
+- consistency-distilled: `d25035bed7294a227fcb0f1ea691a0d1b8452ef76bde0e411c2b75536acf13da`
+
+The performance measurements in the optimization ledger still describe GPU
+cost, but measurements made before this audit are not output-quality evidence.
+The previous acceptance checks compared optimized WebGPU paths with other
+local paths and broad signal-energy envelopes; correlated implementation bugs
+could therefore pass.
+
+## Confirmed semantic defects
+
+### Shared GELU was too steep
+
+The WebGPU erf approximation evaluated `erf(x)` where GELU requires
+`erf(x / sqrt(2))`. This affected every deterministic feed-forward block and
+the CD time/mapping/adapter graph. The first large upstream divergence appeared
+at the first feed-forward GELU despite strong agreement immediately before it.
+
+The WGSL implementation now scales the erf input by
+`0.7071067811865476`. Against a genuine upstream float32 execution on the
+included fixture, the corrected deterministic waveforms measure:
+
+| Stem | NRMSE | SNR | Cosine |
+| --- | ---: | ---: | ---: |
+| drums | 0.000350 | 69.12 dB | 0.99999994 |
+| bass | 0.000526 | 65.57 dB | 0.99999986 |
+| other | 0.000433 | 67.27 dB | 0.99999994 |
+| vocals | 0.002745 | 51.23 dB | low-energy stem |
+
+The browser reference gate also compares 30 sampled intermediate tensors, so
+this class of shared activation error now fails close to its origin.
+
+### CD time conditioning was off by 1000×
+
+The sampler transports sigma as `250 * log(sigma)`. Upstream
+`EDMPrecond.forward` reverses that transport and passes `log(sigma) / 4` to the
+BS-RoFormer. The browser passed the transported value directly. At the released
+one-step sigma this was approximately `-1384.52` instead of `-1.38452`.
+
+The browser now supplies `log(sigma) / 4`. A static operator-by-operator audit
+found the remaining Full CD graph consistent with upstream: input/noise
+scaling, `c_in`/`c_skip`/`c_out`, condition capture and injection order, stem
+embedding and FiLM order, STFT adapter, time/frequency transformers, masks,
+complex multiplication, final affine, and clamping.
+
+### CD stem-embedding copies invalidated the mapping command encoder
+
+The packaged weight buffer was created with `STORAGE | COPY_DST`, but
+`createMappings` also uses it as the source of four `copyBufferToBuffer`
+operations for the learned stem embeddings. WebGPU therefore invalidated the
+entire CD mapping command encoder. Before explicit error scopes were added,
+Chrome did not turn that validation failure into a rejected separation, so the
+refiner continued with unusable FiLM vectors.
+
+The package buffer now includes `COPY_SRC`. The unattended Full-mode check
+reproduced the original validation error before this change and completes with
+valid per-stem mappings after it. Validation, out-of-memory, and internal GPU
+errors are now surfaced at the inference boundary instead of silently flowing
+into waveform output.
+
+### Arbitrary-length files were sent through one unbounded graph
+
+DiCoSe is trained and evaluated on 485,100-sample (11-second) items. The old
+browser runtime instead built one time-attention graph for an entire track.
+For `trust_nobody.wav` (16-kHz mono, 127.168 seconds), conversion to 44.1 kHz
+produces 5,608,109 samples and 12,717 STFT frames. At that shape:
+
+- the deterministic `wide` activation alone requires about 2.26 GiB;
+- time attention grows quadratically;
+- specialized STFT-adapter convolution dispatches exceed the WebGPU grid;
+- invalid or failed GPU work could flow into CD noise instead of producing a
+ useful error.
+
+Long Full input now uses fixed 485,100-sample model items, 50% overlap,
+reflected outer context, the upstream 10% endpoint-inclusive linear fade,
+normalized overlap-add, and exact crop/length restoration. This follows the
+generic MSST whole-track policy without reproducing its batch-dependent
+edge-window bug or its final chunk whose output is entirely cropped. The
+overlap noise is keyed by padded-track coordinate so the final CD affine does
+not crossfade independent noise fields at seams. WebGPU validation,
+out-of-memory, and internal errors are scoped and surfaced.
+
+Fast keeps the same fixed model item, fade, normalization, and exact output
+geometry, but advances by 436,590 samples so adjacent chunks overlap only the
+48,510-sample fade region. This is a deliberate long-track performance policy,
+not an upstream whole-track equivalence claim. At the model-rate length of
+`trust_nobody.wav`, Full plans 25 chunks and Fast plans 13. The 25/13, or
+approximately 1.92×, reduction is a chunk-count projection; listening quality
+and end-to-end wall time are not inferred from it.
+
+Pure numeric tests cover both long-track plans, reflection, adaptive final
+padding, positive coverage at every output sample, identity reconstruction at
+boundary lengths, asymmetric stereo, and Full overlap-noise continuity.
+
+The long-file wrappers are explicit browser policies, not a claim that upstream
+published a whole-track stochastic oracle. Full's chunk, reflection, fade,
+normalization, and crop geometry follow the generic MSST helper. The Full path
+uses one coordinate-stable noise field across overlaps to avoid crossfading
+independent CD noise at seams; upstream's sampler draws a new contiguous field
+per invocation. Fast retains the common fixed-item and overlap-add machinery
+but deliberately changes the step and reflected border described above.
+
+### Non-44.1-kHz resampling and output geometry differed from the loader
+
+Upstream uses torchaudio's default Hann-windowed sinc resampler when source and
+target rates differ. The browser used two-point linear interpolation, endpoint
+hold, and a rounded output length. Torchaudio uses a centered zero-padded
+polyphase filter and a ceiling output length. This is material for both the
+included 22.05-kHz fixture and the reported 16-kHz long file. The production
+path now uses the torchaudio 2.0.2 geometry and coefficients, with rate-pair,
+edge-impulse, identity, and output-length numeric oracles.
+
+The file API also used to return the internal 44.1-kHz timeline instead of the
+source file's timeline. Upstream restores the original sample rate and exact
+frame count after inference. The browser now retains that input geometry,
+resamples all stems back after the model, and trims or zero-pads the single
+rounding frame when needed. For `trust_nobody.wav`, output is therefore exactly
+2,034,688 frames at 16 kHz rather than 5,608,109 frames at 44.1 kHz. This
+mismatch could retain out-of-band model residue and wasted transfer/storage,
+but could not change duration or turn valid PCM into broadband noise.
+Upstream's generic CLI uses librosa `kaiser_best` for this reverse conversion;
+the browser reuses its validated Hann-sinc resampler, so native geometry is
+matched but reverse-resampler samples are not claimed bit-exact.
+
+### Over-range Fast exports were clipped
+
+The demo always encoded PCM16 and therefore hard-clipped deterministic/Fast
+samples outside `[-1, 1]`. Upstream selects float WAV when a stem peak exceeds
+one. The demo now uses the same peak rule: in-range stems remain PCM16, while
+over-range stems use IEEE-float WAV and preserve their samples. Full already
+clamps final output by design, matching the released sampler, so this was not
+the reported Full-mode noise mechanism.
+
+### A fresh manifest could be paired with stale packed weights
+
+The manifest was fetched with `no-store`, while the fixed-name 623 MB weight
+blob used `force-cache`. Only byte length was checked. Several packing layouts
+have the same total length, so an updated manifest could address a stale cached
+blob with incompatible layouts and produce arbitrary tensors without a load
+error.
+
+The weight URL is now content-addressed with the manifest's declared SHA-256 as
+its cache key. Manifest parsing rejects malformed weight digests, and the
+stream still enforces the exact declared byte count. A package revision can no
+longer reuse another revision's cached blob.
+
+## Audited matches
+
+The following paths were independently compared with upstream and did not
+contain a semantic mismatch after the model-math and mapping fixes:
+
+- centered periodic-Hann STFT/ISTFT geometry and exact requested length;
+- stereo spectrum packing and `[left real, left imag, right real, right imag]`
+ ordering;
+- 62-band split/scatter layout and stem ordering;
+- RMSNorm, RoPE frequencies and positions, gated attention, residual order,
+ feed-forward GLU, mask estimation, zero-DC handling, and complex masks;
+- deterministic-to-CD condition tensors and every condition-add location;
+- CD sampler coefficients and one-step schedule;
+- checkpoint tensor selection, names, shapes, and package hashes;
+- peak-selected PCM16/IEEE-float WAV interleaving and headers.
+
+Expected numerical differences remain from binary16 storage, WebGPU FFT
+rounding, and the browser's deterministic RNG. Those are not semantic graph
+changes.
+
+## Full/CD dynamic parity
+
+The Full oracle uses exactly the first 485,100 samples of the included WAV
+after the production browser Hann-sinc conversion, not a separately recreated
+input. Its planar input SHA-256 is
+`2f47b43d1129549916fa2cd9a70ca0dae4b650f7e1833fe811a550d5283ff3f2`.
+The official implementation and browser share fixed seed `0xd1c05e`.
+
+Final refined waveform agreement is:
+
+| Stem | NRMSE | SNR | Cosine |
+| --- | ---: | ---: | ---: |
+| drums | 0.001114 | 59.06 dB | 0.99999938 |
+| bass | 0.000858 | 61.33 dB | 0.99999963 |
+| other | 0.000766 | 62.32 dB | 0.99999971 |
+| vocals | 0.021789 | 33.24 dB | 0.99991330 |
+
+The vocal reference is nearly silent (RMS `0.0000914`); its absolute RMSE is
+`0.00000199`. Before the final consistency affine, raw CD-model NRMSE is
+0.00052–0.01187 with minimum cosine 0.999932. The gate additionally compares
+17 internal CD seams, including time embedding, mapping input/output, first
+FiLM vectors, both condition adapters, early and late transformer states,
+final normalization, and masks. Every stem/call is checked against a
+stage-specific envelope rather than an aggregate signal-energy heuristic.
+
+Waveforms were generated by upstream PyTorch float32. Internal stages were
+generated with the released float16 weights under PyTorch autocast so the
+comparison isolates graph/layout errors from expected precision differences.
+Independent oracle-generation runs were byte-identical.
+An additional reconstruction loaded both checkpoint files independently,
+recomputed the condition adapter without browser intermediates, and matched
+the fixture's condition stages at 0.00043–0.00050 NRMSE.
+
+## What the two demo modes mean
+
+- **Full** is the released deterministic graph followed by the released
+ one-step CD graph. Its long-track wrapper retains the upstream 50% overlap
+ policy.
+- **Fast** is the released deterministic checkpoint returned directly. It is
+ a valid upstream model output per item, but omits the learned CD refinement.
+ On inputs above 12 seconds it also uses 10% rather than 50% overlap, so its
+ whole-track result is an explicit approximation rather than an upstream
+ reference-equivalence claim.
+The retired Balanced experiment used a stride-2 CD temporal trunk for which
+there is no released checkpoint or upstream graph. Its measurements remain in
+OPT-0025 as historical evidence, but the mode was removed rather than retained
+as a dormant public switch. The old observation that it sounded less broken on
+one short file did not validate the then-incorrect CD graph.
+
+The retired Extra Fast experiment changed the full temporal grid to hop 882.
+It met the 30-second timing target, but listening found its degradation too
+large relative to Fast. OPT-0027 retains the measurements as historical
+evidence; the public mode, runtime geometry, shader support, and probe-only
+stride-2 primitives were removed completely.
+
+## Gates
+
+- `pnpm test` covers DSP primitives, model/package contracts, chunking, and
+ overlap-add behavior.
+- `pnpm test:reference-quality` compares Fast/deterministic waveforms and
+ intermediate tensors with the official PyTorch model and fails on envelope
+ violations.
+- `pnpm test:refined-reference-quality` compares deterministic, raw CD, and
+ final Full waveforms plus 17 internal CD seams with a frozen-noise official
+ PyTorch execution. It enforces both aggregate and per-stem/per-call limits.
+- `pnpm test:output-mode-quality` checks same-worker Full/Fast isolation and
+ verifies their deterministic diagnostics agree exactly.
diff --git a/packages/dicose/optimization/LEDGER.md b/packages/dicose/optimization/LEDGER.md
new file mode 100644
index 0000000..4af6019
--- /dev/null
+++ b/packages/dicose/optimization/LEDGER.md
@@ -0,0 +1,38 @@
+# Optimization ledger
+
+> The speed measurements remain valid, but pre-audit waveform/diagnostic gates
+> were local comparisons and did not establish upstream correctness. Quality
+> claims must now pass the reference gates described in
+> [`CORRECTNESS_AUDIT.md`](CORRECTNESS_AUDIT.md).
+
+| ID | Mechanism | Evidence | Disposition |
+| --- | --- | --- | --- |
+| OPT-0001 | 128-column subgroup GEMM ownership | Negative: correct but substantially slower on full DiCoSe. | Abandoned / reverted |
+| OPT-0002 | Four-query-per-subgroup attention scheduling | Positive: raw f16-equivalent probe, faster cold full run, and release benchmark completed. | Integrated |
+| OPT-0003 | Converter-native N128/N256 × K32 subgroup GEMM | Positive: raw-bit exact primitive and 1.64× faster full cold acceptance run. | Integrated |
+| OPT-0004 | Persistent GPU RoPE sin/cos table | Positive: raw-bit exact probe and 1.09× faster full cold acceptance run. | Integrated |
+| OPT-0005 | Native-f16 packed GEMM accumulation | Negative: changed full output but improved cold wall by only ~1%. | Abandoned / reverted |
+| OPT-0006 | Fused attention gating and projection residuals | Positive: raw-bit exact probes and 1.04× faster full cold acceptance run. | Integrated |
+| OPT-0007 | M64×N128/WG256 packed GEMM owner | Negative: exact but neutral at the full cold boundary. | Abandoned / reverted |
+| OPT-0008 | Strided time attention without layout transposes | Positive: raw-bit exact probe, unchanged full output, and 1.04× faster full cold acceptance run. | Integrated |
+| OPT-0009 | Producer-fused one-time K rotation | Positive: raw-bit exact, 1.06× faster than the immediate cold control, and final sustained median 53.67 s. | Integrated |
+| OPT-0010 | Fuse CD condition adds into FF2 | Negative: exact only with workgroup staging, then neutral at the full cold boundary. | Abandoned / reverted |
+| OPT-0011 | Production-shape GPU timestamp profiler | Positive: separated dense throughput from attention and localized ~24.6 s of baseline attention GPU time. | Integrated |
+| OPT-0012 | Q64 attention with ascending K8 shared tiles | Positive: raw-bit exact; time/frequency kernels improved by 36.0%/30.8% against Q32. | Integrated |
+| OPT-0013 | N128 FP32 owner with exact K4 source unrolling | Positive: raw-bit exact; production dense shapes reached 1.80–1.90 TFLOP/s. | Integrated |
+| OPT-0014 | Subgroup-owned STFT adapter convolutions | Positive: raw-bit exact; entry 3×3 improved 11.5× and each hidden 1×1 improved 30.6×. | Integrated |
+| OPT-0015 | Q64×K16 blockwise Flash attention | Positive: 1.65×/1.62× on time/frequency attention; full-waveform NRMSE stayed below 0.00036. | Integrated |
+| OPT-0016 | Eight-row subgroup RMSNorm owner | Positive: raw-bit exact across 90 production-width/tail/FiLM cases; 1.52–1.70× at the main C384 shape, projecting ~0.13 s transformer saving. | Integrated |
+| OPT-0017 | Bounded-f16 K2/K4 dense partials on the current K-major package layout | Mixed: K2 regressed every shape; K4 reached 2.24–2.29 TFLOP/s but only 1.082× weighted/~0.87 s projected saving. | Benchmark-only; not selected |
+| OPT-0018 | Cooperative Flash score exponentials | Negative: raw-bit exact, but two production panels projected only 0.12–0.14 s saving because added synchronization offset SFU concurrency. | Abandoned / pruned |
+| OPT-0019 | Converter-native per-output K4 dense layout | Mixed: raw-equivalent to transposed K4 and 1.153×/~1.52 s faster than exact, but only 1.068× over transposed K4, missing the declared layout-migration gate. | Abandoned / pruned |
+| OPT-0020 | Native-K4 M16/N256 owner geometries | Negative: both were exact relative to their arithmetic controls but slower than M32×N128 on every eligible shape. | Abandoned / pruned |
+| OPT-0021 | Bounded-f16 Flash QK/PV partials | Negative: all five isolated/combined arms passed narrow quality but regressed both production geometries; combined projected 0.57 s slower. | Abandoned / pruned |
+| OPT-0022 | Stride-2 full transformer trunk with residual-delta reconstruction | Mixed: primitive geometry/RoPE gates passed and paired end-to-end wall fell 23.85→11.39 s, but waveform NRMSE was 0.16–0.48 with severe local errors. | All-network arm rejected; benchmark evidence retained |
+| OPT-0023 | Weight-only truncated-SVD transformer projections | Negative preflight: all 96 dominant matrices are strongly full-rank; rank 256 saves only 16.7% of their FLOPs with a 25.2% optimal residual. | Abandoned before conversion/kernel work |
+| OPT-0024 | Deterministic-only output with CD-only setup elided | Positive fast-mode performance: cold total 5.81 s versus 24.73 s refined (4.25×), sustained median 5.92 s; intentionally omits learned refinement and needs ground-truth/listening evidence. | Explicit fast mode; refined remains default |
+| OPT-0025 | CD-only stride-2 transformer trunk with full deterministic conditioning | Positive historical evidence: deterministic diagnostics exact, refinement 2.30× and total 1.6486× in the controlled pair, 17.91-s thermally conservative sustained median, stem NRMSE 0.018–0.035. | Removed from the product; experiment evidence retained |
+| OPT-0026 | Fast long-track 10% overlap aligned to the existing fade | `trust_nobody.wav` falls from 25 to 13 calls; isolated Chrome 151 sustained median 79.61 s (71.57–113.05 s), with 69.51 s in deterministic compute at the median sample. Listening quality remains unverified. | Integrated for explicit Fast only; Full retains 50% overlap; 30 s target not met |
+| OPT-0027 | Extra Fast end-to-end hop-882 analysis, model, masks, and synthesis with original-position temporal RoPE | 1,101→551 frames and 44.76% of Fast arithmetic; supplied-WAV smoke passed, and `trust_nobody.wav` sustained median reached 28.04 s (26.62–28.82 s), but listening found the quality loss too large relative to Fast. | Rejected after listening; mode and implementation pruned; benchmark evidence retained |
+
+See `experiments/` for the reproducible commands and exact measurements.
diff --git a/packages/dicose/optimization/README.md b/packages/dicose/optimization/README.md
new file mode 100644
index 0000000..ad4921d
--- /dev/null
+++ b/packages/dicose/optimization/README.md
@@ -0,0 +1,26 @@
+# Optimization program
+
+This directory records performance experiments against the same raw-WGSL
+DiCoSe graph. It keeps quality and timing evidence together so a fast-looking
+shader does not silently replace a correct one, and a correct-looking shader
+does not silently make the browser slower.
+
+## Rules
+
+1. Start from the current accepted fixture contract and record an `OPT-NNNN`
+ entry before retaining a performance-sensitive change.
+2. Validate the narrowest affected primitive in an isolated Chrome profile.
+3. Run the supplied WAV in an isolated Chrome profile and require the f16
+ deterministic acceptance envelope before interpreting timing.
+4. Retain only improvements measured at the full inference boundary.
+5. Keep rejected experiments in the ledger; do not leave dormant public
+ switches merely to preserve them.
+
+The browser harness is intentionally unattended: each run launches Chrome with
+a disposable profile and uses CDP to collect the automatic report. Raw result
+artifacts belong in the ignored `benchmark/results/` tree when needed; the
+small, reproducible facts belong in this ledger.
+
+The current production-shape operation count and hardware floor are recorded
+in [`ARITHMETIC_BUDGET.md`](ARITHMETIC_BUDGET.md). Revisit that budget before
+allocating work to a low-impact kernel family.
diff --git a/packages/dicose/optimization/experiments/OPT-0001-subgroup-gemm.md b/packages/dicose/optimization/experiments/OPT-0001-subgroup-gemm.md
new file mode 100644
index 0000000..2d53c98
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0001-subgroup-gemm.md
@@ -0,0 +1,25 @@
+# OPT-0001 — 128-column subgroup GEMM
+
+## Hypothesis
+
+Using four 32-lane subgroups to emit a 32×128 output tile directly from
+row-major f16 weights would reduce workgroup count and beat the generic 16×16
+shared-memory GEMM for the model's 384/512/1536 projections.
+
+## Correctness gate
+
+The isolated Chrome raw-WGSL probe checked a nontrivial 4×128 projection and
+passed with no validation or uncaptured-device errors. A full fixture pass
+produced bit-identical reported output statistics.
+
+## Result
+
+The full clean-profile run regressed from 104,924.6 ms to 147,497.9 ms model
+time. Deterministic inference alone rose from 22,003.0 ms to 58,884.2 ms.
+The extra register pressure/subgroup broadcast ownership lost to the original
+16×16 tiled kernel on the target Metal adapter.
+
+## Disposition
+
+Negative. The implementation and its special raw probe were removed; generic
+linear remains the production path.
diff --git a/packages/dicose/optimization/experiments/OPT-0002-quad-attention.md b/packages/dicose/optimization/experiments/OPT-0002-quad-attention.md
new file mode 100644
index 0000000..a578217
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0002-quad-attention.md
@@ -0,0 +1,52 @@
+# OPT-0002 — quad-query attention
+
+## Hypothesis
+
+The original 256-lane attention workgroup loaded each K/V row once for eight
+queries. Four independent query streams per fixed 32-lane subgroup can reuse
+that same K/V row for 32 queries, reducing workgroup dispatches and shared
+K/V traffic by four without changing each query's arithmetic.
+
+## Correctness gate
+
+`pnpm test:webgpu` runs the prior query-8 schedule alongside the new default
+schedule on a two-sequence, 13-token tail shape. All 13,312 raw f16 output
+words matched exactly, with no WebGPU validation or uncaptured errors.
+
+The full isolated Chrome fixture run also passed the deterministic f16
+acceptance envelope and reported bit-identical stem/diagnostic statistics with
+no page, console, or device errors.
+
+## One-run evidence
+
+| Boundary | Generic query-8 (ms) | Quad query (ms) |
+| --- | ---: | ---: |
+| deterministic BS-RoFormer | 22,003.0 | 18,923.1 |
+| four CD refinements | 80,629.9 | 62,486.5 |
+| complete model timing | 104,924.6 | 83,621.5 |
+
+That clean-profile acceptance sample is a 20.3% full-model improvement.
+
+## Release benchmark
+
+The automated fresh-profile Chrome 151 benchmark completed one warmup and
+three measured full-WAV runs without page/device errors:
+
+| Statistic | End-to-end time (ms) |
+| --- | ---: |
+| min | 134,853.3 |
+| median | 136,560.2 |
+| mean | 139,202.6 |
+| max | 146,194.2 |
+
+Stage samples (`prepare / deterministic / mapping / refinement / ISTFT / total`
+in ms) were:
+
+1. `145.3 / 45,301.1 / 7.0 / 98,899.5 / 1,356.8 / 146,179.4`
+2. `126.3 / 33,650.9 / 7.4 / 99,328.2 / 1,290.9 / 134,839.9`
+3. `121.9 / 33,144.4 / 6.9 / 101,478.7 / 1,348.0 / 136,548.2`
+
+The multi-run sustained figures are slower than the isolated cold acceptance
+sample, consistent with thermal variation across repeated long Metal runs.
+They are recorded separately rather than being used to overstate a cold-run
+speedup.
diff --git a/packages/dicose/optimization/experiments/OPT-0003-packed-subgroup-gemm.md b/packages/dicose/optimization/experiments/OPT-0003-packed-subgroup-gemm.md
new file mode 100644
index 0000000..bb9be8d
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0003-packed-subgroup-gemm.md
@@ -0,0 +1,62 @@
+# OPT-0003 — converter-native packed subgroup GEMM
+
+## First-principles attribution
+
+The supplied 1,189-frame fixture creates 73,718 transformer rows. Static graph
+accounting attributes approximately 32.1 logical TFLOPs to one separation:
+
+- 23.23 TFLOPs (72.3%) are transformer dense projections;
+- 7.55 TFLOPs (23.5%) are attention; and
+- the repeated 384↔1536 projections alone account for about 20.9 TFLOPs.
+
+The prior 16×16 shared-tile GEMM therefore owned the largest credible absolute
+saving. The failed OPT-0001 changed subgroup ownership but retained row-major
+weights. This experiment instead treats layout and kernel ownership as one
+mechanism, following the successful ACE-Step/Parakeet pattern.
+
+## Mechanism
+
+The converter now stores eligible matrices as `[N tile, K tile, K32, N]`,
+choosing N256 where possible and N128 for shapes such as N384. The production
+kernel uses WG128 with four fixed-32 subgroups. Each subgroup owns eight rows;
+each lane owns one N128 or two N256 `vec4` columns. Converter-native weights are
+read directly without a workgroup panel or barriers and reused across the
+eight rows through subgroup broadcasts.
+
+Operands remain f16. Every contraction visits K in increasing source order,
+uses f32 FMA state, adds f16 bias after the contraction, applies the existing
+post-op, and rounds once to f16 output. Small or incompatible shapes retain the
+generic kernel. The package size is unchanged; 799 of 2,857 logical tensors,
+representing 569,180,160 payload bytes, select a packed layout.
+
+## Correctness gates
+
+- `pnpm test:webgpu` compares a 7×32×128 packed GELU projection against the
+ generic kernel over all 896 raw f16 output words: zero mismatches.
+- `pnpm check`, `pnpm test`, and `pnpm verify:package` pass. The package has
+ 2,857 logical tensors, 2,829 unique payloads, and 623,246,848 bytes.
+- `pnpm test:browser` passes the supplied-WAV f16 acceptance envelope with no
+ validation, device, console, or page errors.
+
+## Full-graph evidence
+
+Both samples used isolated Chrome 151 profiles, zero warmups, and one measured
+run on the same machine. They are cold acceptance evidence, not a sustained
+thermal benchmark.
+
+| Boundary | Pre-change (ms) | Packed GEMM (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 19,370.6 | 12,013.8 | 1.61× |
+| four CD refinements | 59,820.3 | 35,086.8 | 1.70× |
+| complete model timing | 81,286.9 | 49,283.0 | 1.65× |
+| page end-to-end timing | 82,260.4 | 50,064.7 | 1.64× |
+
+End-to-end wall fell 39.1%. This result is not compared directly with the
+136.56-second sustained OPT-0002 median because thermal cadence differs.
+
+## Disposition
+
+Positive and integrated. Dense math remains the largest counted family, but
+the next experiment must target a distinct mechanism rather than nearby tile
+geometry. Attention's repeatedly recomputed rotary transcendental work is the
+next structural candidate.
diff --git a/packages/dicose/optimization/experiments/OPT-0004-persistent-rope-table.md b/packages/dicose/optimization/experiments/OPT-0004-persistent-rope-table.md
new file mode 100644
index 0000000..391dae6
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0004-persistent-rope-table.md
@@ -0,0 +1,54 @@
+# OPT-0004 — persistent GPU RoPE table
+
+## Hypothesis
+
+The quad-query attention owner still evaluated `pow`, `sin`, and `cos` while
+loading every K dimension. On the 1,189-token axis, each key is revisited by
+38 query workgroups, across 62 sequences, eight heads, 16 deterministic
+attention blocks, and 64 CD attention blocks. The trigonometric result depends
+only on `(position, dimension)`, not the layer, stem, sequence, head, or QKV
+values. Materializing it once should remove repeated transcendental work with
+negligible storage.
+
+## Mechanism
+
+The first attention invocation dispatches a GPU kernel that writes one
+`vec2(cos, sin)` per position/pair. The longest table is about 304 KiB and
+persists across deterministic and CD graphs. Quad attention reads the table;
+the query8 reference retains direct transcendental evaluation. Creating the
+table on the GPU preserves the shader's f32 numerical path and avoids relying
+on host trigonometric implementations.
+
+## Correctness
+
+`pnpm test:webgpu` compares the direct-trig query8 reference with table-backed
+quad attention over 13,312 raw f16 words: zero mismatches. The full supplied-WAV
+acceptance run passed with no browser/GPU errors, and every reported stem and
+diagnostic statistic was identical to OPT-0003.
+
+## Full-graph evidence
+
+Isolated Chrome 151, zero warmups, one measured cold run per arm:
+
+| Boundary | OPT-0003 (ms) | RoPE table (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 12,013.8 | 10,828.5 | 1.11× |
+| four CD refinements | 35,086.8 | 32,383.4 | 1.08× |
+| complete model timing | 49,283.0 | 45,206.9 | 1.09× |
+| page end-to-end timing | 50,064.7 | 46,084.7 | 1.09× |
+
+This is cold acceptance evidence rather than a sustained thermal benchmark.
+
+## Rejected arm
+
+A second arm rotated every K vector once in-place in the QKV buffer, preserving
+the exact f16 K boundary and eliminating repeated rotation arithmetic as well
+as transcendental evaluation. It remained bit-identical but added a roughly
+75 MB read/write pass to every attention block. Full end-to-end wall regressed
+to 48,914.5 ms versus the 46,084.7 ms table arm, so it was removed.
+
+## Disposition
+
+The table-only arm is positive and integrated. The in-place K-hoist is
+abandoned; removing arithmetic without accounting for activation traffic was
+not a win at the full boundary.
diff --git a/packages/dicose/optimization/experiments/OPT-0005-native-f16-gemm-accumulation.md b/packages/dicose/optimization/experiments/OPT-0005-native-f16-gemm-accumulation.md
new file mode 100644
index 0000000..5eda722
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0005-native-f16-gemm-accumulation.md
@@ -0,0 +1,38 @@
+# OPT-0005 — native-f16 packed GEMM accumulation
+
+## Hypothesis
+
+Parakeet's native-f16 accumulator reaches roughly 2.8 TFLOP/s on the same
+hardware class, while the exact f16-operands/f32-accumulator path is materially
+slower. Converting the packed GEMM's running `vec4` state and FMA to f16 could
+move the dominant 23.23-TFLOP family closer to native throughput.
+
+## Risk and gate
+
+This intentionally changes the numerical contract. ACE-Step rejected native
+f16 accumulation after adversarial zero-collapse, overflow, cancellation, and
+long-K drift. The candidate was therefore eligible only if it produced a
+large full-graph wall improvement and remained healthy at the complete audio
+boundary; a marginal timing result could not justify any numerical change.
+
+## Result
+
+The small K32 packed primitive happened to match all 896 reference f16 words,
+but the complete model produced different stem and diagnostic statistics,
+confirming that long-K contractions changed the graph. The supplied WAV still
+passed the broad acceptance envelope with finite outputs and no GPU errors.
+
+Isolated Chrome 151, zero warmups, one cold run:
+
+| Boundary | Exact f32 state (ms) | Native f16 state (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| complete model timing | 45,206.9 | 44,723.6 | 1.01× |
+| page end-to-end timing | 46,084.7 | 45,555.5 | 1.01× |
+
+The difference is too small to separate from cold-run and thermal variance,
+and is nowhere near enough to compensate for the weaker numerical contract.
+
+## Disposition
+
+Negative. Native-f16 accumulation was reverted. The production kernel retains
+source-order f32 FMA state.
diff --git a/packages/dicose/optimization/experiments/OPT-0006-transformer-boundary-fusion.md b/packages/dicose/optimization/experiments/OPT-0006-transformer-boundary-fusion.md
new file mode 100644
index 0000000..afb3dc0
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0006-transformer-boundary-fusion.md
@@ -0,0 +1,52 @@
+# OPT-0006 — transformer boundary fusion
+
+## Hypothesis
+
+Each of the 80 transformer blocks materialized attention context only for a
+separate gate kernel to rewrite it, then ran two full-tensor residual-add
+kernels after the output and feed-forward projections. Across the supplied
+fixture these 240 dispatches update approximately 7.55 billion f16 elements.
+Fusing the operations at their producer stores can remove that traffic without
+changing model arithmetic.
+
+## Mechanism
+
+- The eight-value gate projection is scheduled before attention. Attention
+ rounds its context to f16, applies the same f32 sigmoid division, and rounds
+ the gated result back to f16 in its final store.
+- Packed output and feed-forward projections accept a residual binding. They
+ round the projection to f16 first, add the f16 residual in f32, and round the
+ result to f16, preserving the former two-dispatch boundary.
+- No activation-bearing projection uses residual fusion; the production fused
+ sites have no nonlinear post-op.
+
+## Correctness gates
+
+`pnpm test:webgpu` compares:
+
+- packed projection plus a standalone add against the fused residual owner over
+ 896 raw f16 words; and
+- direct-trig query8 plus standalone gating against table-backed quad attention
+ with fused gating over 13,312 raw f16 words.
+
+Both comparisons have zero mismatches. The full WAV also retained every
+reported OPT-0004 output and diagnostic statistic exactly, with no GPU or page
+errors.
+
+## Full-graph evidence
+
+Isolated Chrome 151, zero warmups, one measured cold run per arm:
+
+| Boundary | OPT-0004 (ms) | Fused (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 10,828.5 | 10,627.5 | 1.02× |
+| four CD refinements | 32,383.4 | 30,810.7 | 1.05× |
+| complete model timing | 45,206.9 | 43,595.6 | 1.04× |
+| page end-to-end timing | 46,084.7 | 44,453.4 | 1.04× |
+
+This is cold acceptance evidence rather than a sustained thermal benchmark.
+
+## Disposition
+
+Positive and integrated. The result saves a modest but repeatable structural
+floor while preserving raw primitive output and full reported behavior.
diff --git a/packages/dicose/optimization/experiments/OPT-0007-m64-n128-gemm.md b/packages/dicose/optimization/experiments/OPT-0007-m64-n128-gemm.md
new file mode 100644
index 0000000..2128861
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0007-m64-n128-gemm.md
@@ -0,0 +1,23 @@
+# OPT-0007 — M64×N128/WG256 packed GEMM owner
+
+## Hypothesis
+
+N384 projections use three N128 tiles and represent a large share of repeated
+dense work. Their M32×N128/WG128 owner emits half as many outputs per group as
+the M32×N256 owner. Doubling the N128 row tile to M64 with eight fixed-32
+subgroups would equalize output work per group and halve its workgroup count.
+
+## Result
+
+The 896-word primitive and the complete WAV remained exact. One isolated
+Chrome 151 cold run measured 44,094.5 ms end-to-end and 43,345.6 ms model time,
+versus OPT-0006's 44,453.4 ms and 43,595.6 ms. The apparent 0.6–0.8% advantage
+is within cold-run/thermal variance; refinement itself regressed from 30,810.7
+to 31,257.3 ms. There is no full-boundary evidence for retaining a larger,
+lower-occupancy owner.
+
+## Disposition
+
+Negative and reverted. The production N128 path remains M32/WG128. Do not
+continue nearby row-tile tuning without a materially different mechanism or a
+new hardware/compiler profile.
diff --git a/packages/dicose/optimization/experiments/OPT-0008-strided-time-attention.md b/packages/dicose/optimization/experiments/OPT-0008-strided-time-attention.md
new file mode 100644
index 0000000..45ab4d9
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0008-strided-time-attention.md
@@ -0,0 +1,71 @@
+# OPT-0008 — strided time attention without layout transposes
+
+## Hypothesis
+
+The model stores transformer rows physically as `[time, band, dim]`, but each
+time-axis block previously copied the complete tensor to `[band, time, dim]`
+and back solely to make attention tokens contiguous. RMS normalization, dense
+projections, residuals, activations, and adapters are all row-local and do not
+require that permutation. Teaching time attention to map logical `(band,
+time)` to physical row `time * bands + band` can remove the copies while
+leaving all model arithmetic unchanged.
+
+The complete supplied-WAV graph has 40 time-axis blocks: eight deterministic
+and eight in each of four CD evaluations. Removing both copies around each
+block eliminates 80 dispatches and approximately 9.06 GB of aggregate f16
+read/write traffic. It also removes one 56.6 MB workspace tensor.
+
+## Mechanism
+
+- Quad attention accepts an explicit strided-row mode. Contiguous frequency
+ attention retains `sequence * tokens + position`; time attention uses
+ `position * sequences + sequence` for Q, K, V, fused gates, and output.
+- RoPE still uses the logical token position, never the physical row.
+- Deterministic time-condition adapters and CD consumers now both retain the
+ same `[time, band, dim]` physical order.
+- The query8 reference rejects strided mode rather than silently using an
+ unsupported layout.
+
+The main risk was locality: successive K/V rows for one band are roughly 190
+KiB apart. This experiment therefore required a full-boundary timing rather
+than acceptance based on eliminated traffic alone.
+
+## Correctness gates
+
+The browser probe now exercises all four quad query streams and the tail with
+3 sequences and 37 tokens. It compares direct-trig query8 plus standalone
+gating with both contiguous and physically transposed strided quad attention.
+After inverse permutation, each comparison has zero mismatches across 56,832
+raw f16 output words.
+
+The full supplied WAV retained every OPT-0006 stem and diagnostic statistic
+exactly, with no validation, console, or page errors.
+
+## Full-graph evidence
+
+Isolated Chrome 151, zero warmups, one measured cold run per retained arm:
+
+| Boundary | OPT-0006 (ms) | Strided (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 10,627.5 | 10,304.8 | 1.03× |
+| four CD refinements | 30,810.7 | 29,587.5 | 1.04× |
+| complete model timing | 43,595.6 | 41,861.8 | 1.04× |
+| page end-to-end timing | 44,453.4 | 42,639.9 | 1.04× |
+
+This is cold acceptance evidence rather than a sustained thermal benchmark.
+
+## Sustained integrated benchmark
+
+The final integrated OPT-0008 stack was then run under the same release
+protocol as the retained baseline: isolated Chrome 151, one warmup, and three
+measured runs. End-to-end samples were 46,368.0, 55,192.5, and 61,277.2 ms,
+for a **55,192.5 ms median** (range 46,368.0–61,277.2 ms). Against the retained
+136,560 ms baseline median (134,850–146,190 ms), this is **2.47× faster** and a
+**59.6% wall-time reduction**. The widening thermal range is material and is
+reported rather than hidden by the median.
+
+## Disposition
+
+Positive and integrated. The strided reads do not outweigh the removed full
+tensor copies on the tested Apple Metal backend, and the change preserves raw
+primitive output and every reported full-model statistic.
diff --git a/packages/dicose/optimization/experiments/OPT-0009-producer-fused-k-rotation.md b/packages/dicose/optimization/experiments/OPT-0009-producer-fused-k-rotation.md
new file mode 100644
index 0000000..59b0dd1
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0009-producer-fused-k-rotation.md
@@ -0,0 +1,85 @@
+# OPT-0009 — producer-fused one-time K rotation
+
+## Hypothesis
+
+Quad attention rotated every K scalar again for every 32-query tile. Across
+the supplied 1,189×62 workload and 80 transformer blocks, that is about 60.390
+billion scalar K rotations. Rotating K once when the packed QKV projection
+stores it requires 3.019 billion, removing 57.370 billion evaluations (95%).
+
+The shader-level savings are roughly 172 GFLOPs, 121 GB of duplicate QKV
+reads, and 459 GB of RoPE-table reads. The table is highly cacheable, so those
+traffic totals are logical rather than DRAM estimates.
+
+## Numerical contract
+
+The former boundary first stored the raw projection as f16. Attention then
+loaded each f16 pair into f32, applied RoPE, and rounded the rotated K value to
+f16 workgroup storage. The fused producer preserves that ordering exactly:
+
+1. finish the source-order f32 GEMM and bias;
+2. explicitly round the complete QKV projection vector to f16;
+3. convert only the rounded K pair back to f32, apply the same table expression,
+ and round to f16 again; and
+4. store Q and V with only the original projection rounding.
+
+Time-axis rows derive position as `row / sequences`; contiguous frequency rows
+use `row % tokens`. Q still rotates in attention, while the pre-rotated-K
+attention variant compiles its hot K path down to a direct f16 load.
+
+## Kernel isolation
+
+A first arm put a uniform K-range branch and the rotary helper in the one QKV
+shader dispatched over all six N256 tiles. It was exact but regressed the full
+cold run to 47,729.4 ms end-to-end; the immediately following unfused control
+was 44,811.0 ms. The larger store path penalized the dominant packed GEMM even
+for Q and V.
+
+The retained arm dispatches three compile-time-specialized two-tile segments:
+
+- lean packed GEMM for Q;
+- packed GEMM plus exact f16-boundary K rotation for K; and
+- lean packed GEMM for V.
+
+This adds two dispatches per transformer block (160 total) but keeps rotary
+code and register pressure out of four of the six projection tiles.
+
+## Correctness gates
+
+The browser probe puts a real packed N256 QKV producer before attention. It
+compares the old attention-side rotation with producer-fused K rotation for
+both contiguous and physically strided layouts. Both comparisons have zero
+mismatches across 56,832 raw f16 context words. The complete WAV retained every
+stem and diagnostic statistic exactly, with no GPU, console, or page errors.
+
+## Full-graph evidence
+
+Isolated Chrome 151, zero warmups, one measured run per adjacent arm:
+
+| Boundary | Unfused control (ms) | Isolated K tiles (ms) | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 10,227.7 | 10,354.2 | 0.99× |
+| four CD refinements | 31,847.9 | 29,112.9 | 1.09× |
+| complete model timing | 44,033.6 | 41,620.7 | 1.06× |
+| page end-to-end timing | 44,811.0 | 42,364.0 | 1.06× |
+
+A second retained-arm run completed in 41,478.6 ms end-to-end and 40,742.7 ms
+total model timing. These are cold acceptance samples, not a new sustained
+thermal benchmark.
+
+## Sustained integrated benchmark
+
+The final retained stack used the release protocol: isolated Chrome 151, one
+warmup, and three measured runs. End-to-end samples were 43,004.3, 53,665.1,
+and 56,903.4 ms, for a **53,665.1 ms median** (range
+43,004.3–56,903.4 ms). Against the original 136,560 ms median
+(134,850–146,190 ms), the delivered stack is **2.54× faster** with a **60.7%
+wall-time reduction**. It is also 2.8% below OPT-0008's 55,192.5 ms sustained
+median. The broad range remains a real thermal characteristic of the tested
+Apple GPU and is not hidden by the aggregate.
+
+## Disposition
+
+Positive and integrated in its isolated-tile form. The monolithic producer
+variant is closed; any future fusion into a dominant GEMM must keep unrelated
+tiles on the lean shader path.
diff --git a/packages/dicose/optimization/experiments/OPT-0010-cd-condition-fusion.md b/packages/dicose/optimization/experiments/OPT-0010-cd-condition-fusion.md
new file mode 100644
index 0000000..af6a7a4
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0010-cd-condition-fusion.md
@@ -0,0 +1,45 @@
+# OPT-0010 — CD condition fusion into FF2
+
+## Hypothesis
+
+Each of four CD evaluations adds one time and one frequency condition after
+each of eight layers. Fusing those 64 elementwise passes into FF2's existing
+projection/residual store would remove 64 dispatches, bind groups, and uniform
+writes. Across 73,718×384 f16 elements, the standalone adds represent 10.870
+GB of logical traffic; condition reads remain necessary, so the nominal
+eliminated destination round trip is 7.247 GB (6.749 GiB).
+
+## Exactness finding
+
+This boundary contains two semantically observable f16 roundings:
+
+1. `projection16 = f16(GEMM + bias)`
+2. `residual16 = f16(f32(projection16) + f32(residual))`
+3. `output16 = f16(f32(residual16) + f32(condition))`
+
+A first fused epilogue expressed `residual16` as a local `vec4`. Its
+coarse random probe passed, but the full model changed slightly because the
+Metal compiler reassociated the local expression across the nominal f16
+boundary. An adversarial probe lane with residual and condition both 0.0006
+exposed one raw-word mismatch: the correct double-rounded result was `0xb590`,
+while the collapsed expression produced `0xb58f`.
+
+Materializing the intermediate in an 8 KiB `var` f16 stage, crossing
+a workgroup barrier, then applying the condition made the adversarial 896-word
+probe and the complete WAV bit-identical. Merely spelling a local as f16 is
+therefore not a sufficient numerical boundary on this backend.
+
+## Full-graph evidence
+
+The exact staged arm completed cold full-WAV runs in 41,276.2 and 42,224.3 ms
+end-to-end. The retained unfused OPT-0009 path completed comparable runs in
+41,478.6 and 42,364.0 ms. The distributions overlap: the apparent 0.3–0.5%
+advantage is below run-to-run thermal variance and does not justify adding
+workgroup storage and a barrier to a dominant FF2 kernel.
+
+## Disposition
+
+Neutral and reverted. The 64 standalone condition adds remain. Revisit only
+with an independently useful FF2 owner that can guarantee the intermediate
+rounding without reducing occupancy; do not retry local f16 spelling as a
+barrier.
diff --git a/packages/dicose/optimization/experiments/OPT-0011-production-shape-gpu-profiler.md b/packages/dicose/optimization/experiments/OPT-0011-production-shape-gpu-profiler.md
new file mode 100644
index 0000000..fbc6f0e
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0011-production-shape-gpu-profiler.md
@@ -0,0 +1,48 @@
+# OPT-0011 — production-shape GPU timestamp profiler
+
+## Why the aggregate was misleading
+
+The former 0.63 TFLOP/s figure divided 32.1 trillion logical multiply-add
+FLOPs by complete graph wall time. That denominator includes online softmax,
+transcendentals, workgroup barriers, elementwise kernels, command encoding,
+queue gaps, readback, and CPU DSP. It was therefore not comparable with the
+isolated GEMM calibrations reported by Parakeet or ACE-Step.
+
+The dedicated browser profiler requests `timestamp-query` and wraps exactly
+one production-shape compute pass per command buffer. Each of seven measured
+samples gets its own submission and drain. Pipeline compilation, buffer
+allocation/upload, submission, and timestamp readback are outside the GPU
+interval. Zero or reversed timestamps fail the run instead of becoming a
+plausible-looking result.
+
+Reproduce with:
+
+```sh
+pnpm profile:webgpu
+```
+
+## Baseline localization
+
+The first Chrome 151 profile, before the retained OPT-0012/0013 kernels,
+measured:
+
+| Kernel | GPU median | Effective throughput |
+| --- | ---: | ---: |
+| plain 73,718×384×1,536 dense | 64.03 ms | 1.358 TFLOP/s |
+| FF1 73,718×384×1,536 + GELU | 63.64 ms | 1.367 TFLOP/s |
+| FF2 73,718×1,536×384 + residual | 54.39 ms | 1.599 TFLOP/s |
+| output 73,718×512×384 + residual | 17.96 ms | 1.614 TFLOP/s |
+| time attention, 62×1,189 | 582.94 ms | 0.308 logical TFLOP/s |
+| frequency attention, 1,189×62 | 31.85 ms | 0.294 logical TFLOP/s |
+
+The dense kernels were already in ACE's exact-FP32 range rather than running
+at 0.63 TFLOP/s. Forty blocks of each attention axis predict 24.59 seconds of
+GPU work, which accounts for most of the unexplained end-to-end wall time.
+The key fact is not the attention "TFLOP/s" itself—softmax work is absent from
+that numerator—but that attention, not GEMM, owned the largest wall slice.
+
+## Disposition
+
+Integrated as `pnpm profile:webgpu`. The profiler retains explicit controls
+for the old and selected owners so future compiler/browser changes can be
+measured without loading the 623 MB model package.
diff --git a/packages/dicose/optimization/experiments/OPT-0012-q64-k8-attention.md b/packages/dicose/optimization/experiments/OPT-0012-q64-k8-attention.md
new file mode 100644
index 0000000..5728054
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0012-q64-k8-attention.md
@@ -0,0 +1,64 @@
+# OPT-0012 — Q64 attention with ascending K8 shared tiles
+
+## Hypothesis
+
+The Q32 owner used eight fixed-32 subgroups and four scalar query streams per
+subgroup. Every workgroup loaded the same K/V row and crossed two barriers for
+each key. Doubling ownership to eight streams keeps the 256-thread workgroup
+but halves query workgroups. Loading eight ascending K/V rows into shared
+memory then amortizes the barriers across eight numerically unchanged online
+softmax updates.
+
+The online update also observes that one of
+`exp(old_max - next_max)` and `exp(score - next_max)` is always `exp(0)`.
+The selected shader computes the nontrivial exponential and substitutes exact
+literal 1 for the other branch while preserving key order and FP32 state.
+
+## Rejected arms
+
+- The one-exponential rewrite by itself was raw-bit exact but moved the
+ 582.94 ms time kernel only to 571.80 ms in one profile; it was not the main
+ mechanism.
+- Packing four query streams into `vec4` and hoisting gate denominators
+ was exact but regressed to 583.79/32.90 ms for time/frequency attention.
+- Q96 crossed the private-state pressure cliff at 529.66/44.43 ms.
+- Q128 was substantially slower and produced an invalid zero timestamp for
+ one long-shape sample. It was rejected immediately.
+- Q64 without key blocking was positive; K4 was materially better; K8 gave a
+ smaller final step. Wider key tiles were not chased after that diminishing
+ return.
+
+## Kernel evidence
+
+One matched seven-sample Chrome 151 profile measured:
+
+| Owner | Time 62×1,189 | Frequency 1,189×62 |
+| --- | ---: | ---: |
+| Q32/K1 control | 575.67 ms | 31.33 ms |
+| Q64/K1 | 454.43 ms | 26.35 ms |
+| Q64/K4 | 376.50 ms | 21.63 ms |
+| Q64/K8 selected | 368.44 ms | 21.69 ms |
+
+The selected owner is 36.0% faster on the dominant time axis and 30.8% faster
+on the frequency axis. Across forty blocks of each axis, the isolated medians
+predict an 8.67-second GPU-time reduction.
+
+## Numerical contract
+
+Every query retains its own scalar FP32 max, denominator, and two context
+states. K tiles are consumed in exactly ascending order. The query8 control
+and selected owner have zero mismatches across 56,832 raw f16 words for
+contiguous, strided, and producer-rotated-K paths. The complete WAV output
+statistics remain unchanged.
+
+The integrated OPT-0012/0013 stack completed a cold supplied-WAV acceptance
+run in 39,168.3 ms end-to-end (38,341.1 ms model timing): 9,500.8 ms for the
+deterministic stage and 26,641.5 ms for four refinements. Every final stem and
+model-diagnostic peak/RMS statistic matched the former Q32/K1 stack exactly.
+This cold sample is an acceptance receipt, not a sustained thermal median.
+
+## Disposition
+
+Integrated. The public runtime keeps only the Q32 correctness control and the
+selected Q64/K8 owner; Q64/K1, Q64/K4, Q96, Q128, and vectorized-stream arms
+were removed rather than left as dormant switches.
diff --git a/packages/dicose/optimization/experiments/OPT-0013-exact-k4-n128-dense.md b/packages/dicose/optimization/experiments/OPT-0013-exact-k4-n128-dense.md
new file mode 100644
index 0000000..2ccba93
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0013-exact-k4-n128-dense.md
@@ -0,0 +1,66 @@
+# OPT-0013 — N128 FP32 owner with exact K4 source unrolling
+
+## Hypothesis
+
+The converter-native N256 shader combined Parakeet's wide output ownership
+with FP32 running state: sixteen `vec4` accumulators, or 64 scalar FP32
+values, per lane. Parakeet uses that footprint for native-f16 accumulation but
+deliberately narrows its FP32 owner to N128.
+
+The selected arm reads each physical N256 weight tile as two logical N128
+owners, halving accumulator pressure without repacking weights. It also loads
+four adjacent f16 activations at once and source-unrolls their four FP32 FMAs.
+The FMA sequence remains K0, K1, K2, K3, so this is load/compiler shaping, not
+K4 partial accumulation and not approximate arithmetic.
+
+## Interaction result
+
+The mechanisms had to be tested together. On 73,718×384×1,536:
+
+| Arm | GPU TFLOP/s |
+| --- | ---: |
+| N256/K1 control | 1.582 |
+| N256/K4 loads | 1.311 |
+| N128/K1 | 1.624 |
+| N128/K4 selected | 1.798 |
+
+K4 source unrolling made the high-pressure N256 owner worse, while the same
+unrolling made the bounded N128 owner substantially faster. This closes the
+tempting but incorrect idea of applying K4 mechanically to every geometry.
+
+## Production-shape evidence
+
+A matched seven-sample Chrome 151 profile measured:
+
+| Boundary | Control | Selected | Gain |
+| --- | ---: | ---: | ---: |
+| FF1 384→1,536 + GELU | 1.576 | 1.823 TFLOP/s | 15.7% |
+| QKV 384→1,536 + fused K rotation | 1.556 | 1.818 TFLOP/s | 16.8% |
+| FF2 1,536→384 + residual | 1.626 | 1.858 TFLOP/s | 14.3% |
+| attention output 512→384 + residual | 1.626 | 1.835 TFLOP/s | 12.9% |
+| adapter 384→384 | 1.684 | 1.896 TFLOP/s | 12.6% |
+
+For the four transformer projections alone, those medians predict about 1.94
+seconds less GPU time across 80 blocks. Large packed rows select N128/K4;
+tiny row-one mapping projections retain the lower-dispatch original owner.
+
+## Correctness
+
+The raw browser probe compares the physical-N256/N128/K4 owner to the generic
+source-order reference and reports zero mismatches. It also validates fused K
+rotation for contiguous and strided layouts through attention, again with zero
+mismatches across 56,832 f16 words. No native-f16 accumulator or bounded-dot
+approximation is used.
+
+Together with OPT-0012, the selected dense owner completed the full supplied
+WAV in 39,168.3 ms end-to-end (38,341.1 ms model timing), with deterministic
+and four-refinement stages of 9,500.8 and 26,641.5 ms. All final output and
+diagnostic statistics remained bit-for-bit unchanged. Sustained measurement
+is deferred until the GPU has cooled; consecutive long runs on this machine
+have already ranged from 39 to 64 seconds under thermal saturation.
+
+## Disposition
+
+Integrated for packed projections with at least one full 32-row tile. This
+brings the production dense family into the 1.80–1.90 exact-FP32 TFLOP/s range
+on the tested Chrome/Metal stack.
diff --git a/packages/dicose/optimization/experiments/OPT-0014-subgroup-stft-convolutions.md b/packages/dicose/optimization/experiments/OPT-0014-subgroup-stft-convolutions.md
new file mode 100644
index 0000000..f744461
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0014-subgroup-stft-convolutions.md
@@ -0,0 +1,54 @@
+# OPT-0014 — Subgroup-owned STFT adapter convolutions
+
+## Hypothesis
+
+The STFT adapter applies its convolutions to a 1,025×1,189 grid. The generic
+shader assigns one invocation to one output pixel/channel and dispatches the
+channel dimension as workgroups, producing 2,460,288 tiny workgroups for both
+the 4→128 entry convolution and each 128→128 hidden convolution. This is a
+work-ownership failure, not an arithmetic-throughput limit.
+
+The selected owners give each subgroup four adjacent output channels and
+eight pixel rows. Four subgroups therefore produce a 32×128 output tile per
+workgroup. Weights are staged once per workgroup and activations are broadcast
+within each subgroup. The production dispatch falls to 38,086 workgroups,
+64.6× fewer, while retaining bias-first FP32 accumulation in source order.
+
+## Production-shape evidence
+
+A matched Chrome 151 timestamp profile used two warmups and seven measured
+passes. Compilation, upload, submission, and readback were excluded.
+
+| Boundary | Generic | Selected | Speedup |
+| --- | ---: | ---: | ---: |
+| 4→128 3×3 entry | 171.442 ms / 0.065 TFLOP/s | 14.942 ms / 0.751 TFLOP/s | 11.5× |
+| 128→128 1×1 hidden | 763.167 ms / 0.052 TFLOP/s | 24.969 ms / 1.599 TFLOP/s | 30.6× |
+
+The hidden shape occurs twice. The matched medians predict 1,632.9 ms less
+GPU time for the retained entry and hidden owners together. The remaining
+128→4 exit 3×3 measured 121.307 ms in the generic shader, so it was left alone
+rather than spending effort on a sub-0.13-second ceiling.
+
+## Correctness and integrated acceptance
+
+The raw browser probe compares generic and selected outputs using a 5×7 grid,
+which covers all padding edges and corners plus a partial final 32-row tile.
+It reports zero mismatches across 4,480 raw f16 output words. The 128×128 1×1
+owner likewise reports zero mismatches across 4,736 words.
+
+The complete supplied WAV then passed in 30,458.8 ms end-to-end, with
+29,714.2 ms model timing: 6,079.3 ms deterministic, 21,498.3 ms refinement,
+and 1,474.7 ms ISTFT. Every final-stem and model-diagnostic peak/RMS statistic
+was unchanged. This is a cold acceptance receipt, not a sustained thermal
+median.
+
+On the same cool timestamp run, the exact packed dense kernels reached
+2.05–2.13 TFLOP/s. This reinforces that the former 0.63 aggregate figure was
+whole-graph logical work divided by wall time, not the machine's dense-kernel
+ceiling.
+
+## Disposition
+
+Integrated for the exact production 4→128 3×3 and 128→128 1×1 geometries.
+The generic convolution remains for the low-impact 128→4 exit and other
+shapes.
diff --git a/packages/dicose/optimization/experiments/OPT-0015-blockwise-flash-attention.md b/packages/dicose/optimization/experiments/OPT-0015-blockwise-flash-attention.md
new file mode 100644
index 0000000..49955f4
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0015-blockwise-flash-attention.md
@@ -0,0 +1,81 @@
+# OPT-0015 — Q64×K16 blockwise Flash attention
+
+## First-principles diagnosis
+
+The exact Q64/K8 owner still performed one subgroup reduction and one online
+softmax update for every query/key pair. Each of the 32 lanes redundantly held
+the same max and denominator and executed the same exponential, while large
+per-lane query/value state constrained occupancy. It sustained only about
+0.56 TFLOP/s even though the exact dense family exceeded 2.1 TFLOP/s.
+
+The selected owner maps a lane to one query/key score for four query rows. It
+stages 64 FP32-rotated queries and a K16 f16 key/value tile, evaluates complete
+64-wide QK dots as source-ordered FP32 FMAs without subgroup reductions, and
+materializes a 64×16 FP32 score tile. One lane per query computes the tile
+softmax. The same score tile is immediately consumed by P×V, with every V load
+feeding four query accumulators. Max, denominator, and output state merge once
+per K16 block.
+
+This changes floating-point association but not the model operation: every
+query attends every key; QK, softmax state, and P×V remain FP32; only storage
+and final outputs are f16. The shader uses 25,344 bytes of workgroup storage,
+so device creation explicitly requests that advertised adapter limit.
+
+## Ownership panel
+
+A matched Chrome 151 timestamp panel used two warmups and seven measured
+passes. Compilation, upload, submission, and readback were excluded.
+
+| Owner | Time 62×1,189 | Frequency 1,189×62 | Disposition |
+| --- | ---: | ---: | --- |
+| exact Q64/K8 control | 319.554 ms | 18.743 ms | Control retained |
+| exact state-owner/K8 | 261.489 ms | 15.466 ms | Positive, superseded |
+| exact state-owner/K16 | 255.984 ms | 15.401 ms | Positive, superseded |
+| Flash Q32/K16 | 232.522 ms | 14.352 ms | Rejected: occupancy did not repay doubled workgroups |
+| Flash Q64/K16 selected | 193.659 ms | 11.534 ms | Integrated |
+
+The selected owner is 1.65× faster on the dominant time axis and 1.62× on the
+frequency axis, reaching 0.927/0.812 effective TFLOP/s. Across forty blocks of
+each geometry, the matched medians predict 5.32 seconds less GPU time.
+
+The exact state-owner arms demonstrated that removing redundant softmax state
+was independently useful, but their 1.22–1.25× result was materially below the
+blockwise dataflow change. Q32 Flash fit under 16 KiB but lost to Q64 because
+duplicated K/V loads and synchronization outweighed the possible occupancy.
+Those arms were removed rather than retained as runtime switches.
+
+## Numerical and waveform quality
+
+The narrow q64-versus-Flash probe reports 20 changed words out of 56,832,
+NRMSE 9.40e-6, max absolute error 0.00048828125, and cosine
+0.9999999999558. This is deliberately a quality comparison, not a claim of
+raw-bit equivalence.
+
+The full supplied WAV was then run twice with the same decoded PCM and CD
+noise seed: exact Q64 followed by Flash. All 1,048,576 stereo samples per stem
+were compared directly.
+
+| Stem | Waveform NRMSE | SNR | Cosine | Worst 4,096-sample window NRMSE |
+| --- | ---: | ---: | ---: | ---: |
+| drums | 0.000239 | 72.44 dB | 0.999999972 | 0.001352 |
+| bass | 0.000356 | 68.98 dB | 0.999999937 | 0.009327 |
+| other | 0.000278 | 71.12 dB | 0.999999961 | 0.000831 |
+| vocals | 0.000237 | 72.52 dB | 0.999999972 | 0.000314 |
+
+Maximum absolute error was at most 0.063% of the corresponding reference
+peak. RMS drift was at most 0.0016% and peak drift at most 0.0151%. The
+candidate therefore passed both global and localized waveform gates.
+
+## Integrated acceptance
+
+A fresh-profile cold candidate run completed in 24,157.7 ms end-to-end with
+23,438.3 ms model timing: 4,998.0 ms deterministic, 16,460.9 ms refinement,
+and 1,360.7 ms ISTFT. Every output was finite and remained inside the recorded
+upstream acceptance contract. This is an acceptance sample, not a sustained
+thermal median.
+
+## Disposition
+
+Integrated as the production attention owner. Exact Q64 remains as the
+explicit waveform-control path; intermediate exact-state, Q32 Flash, and old
+Q32 grouped owners are not public runtime choices.
diff --git a/packages/dicose/optimization/experiments/OPT-0016-eight-row-rmsnorm.md b/packages/dicose/optimization/experiments/OPT-0016-eight-row-rmsnorm.md
new file mode 100644
index 0000000..65d55d7
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0016-eight-row-rmsnorm.md
@@ -0,0 +1,56 @@
+# OPT-0016 — eight-row subgroup RMSNorm
+
+## First-principles basis
+
+The original RMSNorm assigned a complete WG256 to one row. At the main
+73,718-row transformer shape this launched 73,718 workgroups per call even
+though each row has only 384 values. Across 165 transformer norms that is over
+12 million small workgroups, two workgroup barriers per row, and shared-memory
+traffic solely to combine eight fixed-32 subgroup partials.
+
+The candidate assigns one row to each of the eight subgroups in WG256. To keep
+the target-browser arithmetic contract, every subgroup emulates the original
+eight lane partitions in ascending order: it forms the same per-lane FP32 FMA
+chains, performs eight subgroup reductions, and folds those results in the same
+slot order. No workgroup storage or barrier remains. One workgroup owns eight
+rows, reducing the production C384 dispatch from 73,718 to 9,215 workgroups.
+
+## Correctness gate
+
+Chrome 151 compared the old row owner and the new eight-row owner over all
+production widths `C={8,16,48,96,192,384,512,516}`, row tails `1..9` and `17`,
+FiLM off/on at C384, dynamic-range-stressing finite f16 inputs, and a forced
+small workgroup width that exercised 2-D dispatch flattening. All 133,672
+candidate f16 words across 90 comparisons matched bit-for-bit.
+
+Reproduce with:
+
+```sh
+pnpm test:webgpu
+```
+
+## Production-shape timing
+
+The GPU timestamp profile uses two warmups and seven measured submissions at
+73,718 × 384. Compilation, upload, submission, and readback are excluded.
+
+| Shape | Row1 median | Rows8 median | Speedup | Logical bandwidth |
+| --- | ---: | ---: | ---: | ---: |
+| plain | 2.228224 ms | 1.310720 ms | 1.7000× | 172.78 GB/s |
+| FiLM mapped | 2.097152 ms | 1.376256 ms | 1.5238× | 246.82 GB/s |
+
+The 37 plain and 128 mapped transformer calls project about 126 ms of saving.
+Band-split norms add a smaller benefit not included in that projection. This
+is a valid exact structural cleanup, but the absolute result also proves that
+RMSNorm is not the route to multi-second improvement; dense contraction
+remains the next priority.
+
+Reproduce with:
+
+```sh
+DICOSE_PROFILE_FOCUS=norm pnpm profile:webgpu
+```
+
+## Disposition
+
+Integrated. The old row owner remains an explicit profiler/probe control.
diff --git a/packages/dicose/optimization/experiments/OPT-0017-bounded-f16-dense-partials.md b/packages/dicose/optimization/experiments/OPT-0017-bounded-f16-dense-partials.md
new file mode 100644
index 0000000..e2e2035
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0017-bounded-f16-dense-partials.md
@@ -0,0 +1,77 @@
+# OPT-0017 — bounded-f16 dense partials on the current package layout
+
+## First-principles basis
+
+The exact packed owner converts f16 operands to f32 and updates every f32
+accumulator once per K value. Parakeet's 2.7-TFLOP/s path instead keeps native
+f16 contraction state, which is too numerically fragile over K384–K1536.
+
+This experiment tested two bounded compromises while retaining the existing
+WG128/M32×N128 owner and f32 running state:
+
+- K2: two adjacent f16 products are reduced with `dot(vec2)`, widened,
+ and added to f32 state twice per K4 group.
+- K4: four adjacent products are reduced with `dot(vec4)`, widened once,
+ and added to f32 state once per K4 group.
+
+The current converter layout is K-major across four adjacent outputs. Both
+arms therefore transpose a 4×4 K/output block in registers before each dot.
+This isolates arithmetic without changing the package, but it is not the
+per-output K4-native layout used by ACE's faster arm.
+
+## Numerical probe
+
+Chrome 151 compared exact, K2, and K4 at M33/K384/N128 using deterministic
+signed f16 operands, a partial row tile, identical packed weights, and complete
+f16 writes.
+
+| Arm | Changed words / 4,224 | NRMSE | Maximum absolute | Cosine |
+| --- | ---: | ---: | ---: | ---: |
+| K2 | 1,826 | 0.0003333 | 0.00390625 | 0.999999948 |
+| K4 | 2,175 | 0.0004130 | 0.00390625 | 0.999999916 |
+
+Both candidates were finite and deterministic. Reproduce with:
+
+```sh
+pnpm test:webgpu
+```
+
+## Balanced production-shape panel
+
+The profiler compiled and warmed all arms, then used six balanced exact/K2/K4
+orders with one timestamped compute pass and drained submission per sample.
+
+| Projection | Exact TFLOP/s | K2 TFLOP/s | K4 TFLOP/s | Exact→K4 |
+| --- | ---: | ---: | ---: | ---: |
+| FF1 + GELU | 2.059 | 1.995 | 2.243 | 1.090× |
+| QKV + rotary | 2.080 | 2.009 | 2.253 | 1.083× |
+| FF2 + residual | 2.118 | 2.045 | 2.288 | 1.080× |
+| attention output | 2.126 | 2.015 | 2.262 | 1.064× |
+| adapter | 2.093 | 2.017 | 2.257 | 1.078× |
+
+Weighted by the actual `80/80/80/80/34` call counts:
+
+| Arm | Projected GPU time | Speedup | Saving vs exact |
+| --- | ---: | ---: | ---: |
+| exact | 11,452.35 ms | 1.0000× | — |
+| K2 | 11,869.42 ms | 0.9649× | -417.07 ms |
+| K4 | 10,582.62 ms | 1.0822× | 869.73 ms |
+
+Every K2 shape regressed. Every K4 shape won with non-overlapping ranges, but
+the weighted result missed the predeclared 1.15× and 1.5-second materiality
+gate. A full-waveform A/B was deliberately skipped because the primitive did
+not earn production escalation.
+
+Reproduce with:
+
+```sh
+DICOSE_PROFILE_FOCUS=dense pnpm profile:webgpu
+```
+
+## Disposition and next experiment
+
+K2 is rejected. Register-transposed K4 is benchmark-only and not a production
+default. The result identifies physical layout—not another loop unroll—as the
+next dense lever: pack each output's consecutive K4 operands together so the
+native f16 dot consumes direct vector loads. That materially different layout
+must beat this arm and exact before any waveform gate or runtime selection.
diff --git a/packages/dicose/optimization/experiments/OPT-0018-cooperative-flash-softmax.md b/packages/dicose/optimization/experiments/OPT-0018-cooperative-flash-softmax.md
new file mode 100644
index 0000000..71a6156
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0018-cooperative-flash-softmax.md
@@ -0,0 +1,43 @@
+# OPT-0018 — cooperative Flash score exponentials
+
+## Hypothesis
+
+The Q64×K16 Flash owner used all 256 lanes for score construction, then only
+64 lanes for softmax while each active lane evaluated 16 exponentials. Giving
+four lanes to each query could expose four times more score-exp concurrency.
+
+The benchmark arm kept the current owner's ascending block maximum and final
+ascending probability sum. It published the next maximum, let all 256 lanes
+compute four identical scalar exponentials apiece, and returned the stored
+probabilities to the original owner for state update. Workgroup storage stayed
+at 25,344 bytes. Reordering the phases required one additional barrier per K16
+block, from four to five.
+
+## Correctness
+
+The narrow Chrome 151 probe compared the current Flash output and cooperative
+arm across all 56,832 f16 words and found zero raw-word mismatches. This
+confirmed identical target-browser score, softmax, and output association.
+
+## Production-shape timing
+
+Two fresh-profile panels used two warmups and seven timestamped samples per
+arm and geometry. Compilation, upload, submission, and readback were excluded.
+
+| Run | Geometry | Current median | Cooperative median | Speedup |
+| --- | --- | ---: | ---: | ---: |
+| 1 | time 62×1,189 | 193.396736 ms | 189.988864 ms | 1.0179× |
+| 1 | frequency 1,189×62 | 11.534336 ms | 11.534336 ms | 1.0000× |
+| 2 | time 62×1,189 | 193.069056 ms | 190.251008 ms | 1.0148× |
+| 2 | frequency 1,189×62 | 11.534336 ms | 11.468800 ms | 1.0057× |
+
+Across the production 40 time and 40 frequency calls, those medians project
+only 115–136 ms of saving. The extra barrier almost completely cancels the
+benefit of parallel score exponentials, proving that the apparent 75% idle
+phase was not a multi-second standalone bottleneck.
+
+## Disposition
+
+Negative. The benchmark arm was pruned and production Flash remained
+unchanged. Any later attention work must alter a larger dataflow or arithmetic
+term rather than repeat this ownership-only softmax split.
diff --git a/packages/dicose/optimization/experiments/OPT-0019-native-k4-dense-layout.md b/packages/dicose/optimization/experiments/OPT-0019-native-k4-dense-layout.md
new file mode 100644
index 0000000..21c9963
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0019-native-k4-dense-layout.md
@@ -0,0 +1,63 @@
+# OPT-0019 — converter-native per-output K4 dense layout
+
+## Hypothesis
+
+OPT-0017's bounded-f16 K4 arm had to rebuild each output column's four K
+operands from four K-major `vec4` loads. A physical layout that stores those
+four operands together could remove the 4×4 register transpose and approach
+the 2.7-TFLOP/s Parakeet path while retaining FP32 running state.
+
+The benchmark-only layout for logical `W[K,N]` was:
+
+```text
+[N/128, K/4, output4, lane32, K4]
+```
+
+with scalar index:
+
+```text
+(((((n/128)*(K/4)+k/4)*4+(n%4))*32+((n%128)/4))*4+(k%4))
+```
+
+One direct `vec4` load therefore supplied K4 for one output, and four
+loads produced four adjacent outputs without swizzling. Synthetic buffers had
+the same bytes as production; the public converter, 594-MiB package, manifest,
+and runtime default were not changed.
+
+## Gate
+
+The layout had to win every production shape, reach at least 1.15× and save at
+least 1.5 seconds versus exact, and improve at least 1.10× over OPT-0017's
+transposed K4. The last condition prevents a 129.6-MiB package-layout migration
+when almost all gain comes from the already-marginal arithmetic arm.
+
+## Correctness
+
+The native layout's exact source-order path matched the current exact owner in
+all 4,224 f16 probe words. Its direct K4 path matched the current transposed K4
+path in all 4,224 words, proving packing/indexing and identical bounded-dot
+association.
+
+## Confirmation timing
+
+Six balanced production-shape rounds reported:
+
+| Projection | Exact | Transposed K4 | Native K4 | Native TFLOP/s |
+| --- | ---: | ---: | ---: | ---: |
+| FF up | 42.2707 ms | 38.8301 ms | 36.2086 ms | 2.402 |
+| QKV + rotary | 41.6481 ms | 38.7318 ms | 36.1759 ms | 2.404 |
+| FF down | 41.0255 ms | 38.1092 ms | 35.7499 ms | 2.432 |
+| attention output | 13.7298 ms | 12.8123 ms | 12.1242 ms | 2.391 |
+| adapter | 10.3219 ms | 9.5683 ms | 8.9457 ms | 2.430 |
+
+Weighted by `80/80/80/80/34`, exact was 11,444.88 ms, transposed K4 was
+10,603.99 ms, and native K4 was 9,924.84 ms. Native K4 won every shape, reached
+1.1532× and saved 1,520.04 ms versus exact, but improved only 1.0684× over
+transposed K4. An independent first panel reached the same decision
+(1.1566×/1,552.22 ms versus exact and 1.0680× versus transposed K4).
+
+## Disposition
+
+The layout missed its predeclared 1.10× incremental gate. No checkpoint
+conversion, package duplication/replacement, waveform A/B, or production
+selection occurred. Experimental runtime/layout code was pruned.
diff --git a/packages/dicose/optimization/experiments/OPT-0020-native-k4-owner-geometries.md b/packages/dicose/optimization/experiments/OPT-0020-native-k4-owner-geometries.md
new file mode 100644
index 0000000..d2d7d82
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0020-native-k4-owner-geometries.md
@@ -0,0 +1,40 @@
+# OPT-0020 — native-K4 M16/N256 owner geometries
+
+## Hypothesis
+
+The best OPT-0019 owner still held 32 FP32 accumulator scalars per lane. Two
+materially different geometries tested the remaining ownership tradeoff:
+
+- M16×N128: four rather than eight rows per subgroup, halving accumulator
+ state while doubling row workgroups and packed-weight requests.
+- M32×N256 on the two N1536 projections: two output vectors per lane, halving
+ column workgroups while doubling FP32 accumulator state.
+
+A per-shape selector had to beat native M32×N128 wherever selected, reach at
+least 1.22× weighted versus exact, sustain at least 2.55 TFLOP/s on the four
+major shapes, and save at least 2.0 seconds.
+
+## Correctness
+
+At M33/K384/N256, M16 exact, M16 K4, and M32×N256 K4 each matched the
+corresponding native M32×N128 arithmetic control in all 8,448 f16 words.
+
+## Result
+
+| Projection | Exact | Native M32×N128 | M16×N128 | M32×N256 |
+| --- | ---: | ---: | ---: | ---: |
+| FF up | 42.4018 ms | 36.2742 ms | 41.2221 ms | 38.3713 ms |
+| QKV + rotary | 41.8120 ms | 36.1103 ms | 41.0255 ms | 38.3713 ms |
+| FF down | 41.0583 ms | 35.6844 ms | 40.5668 ms | — |
+| attention output | 13.7298 ms | 12.0914 ms | 13.5987 ms | — |
+| adapter | 10.3547 ms | 8.9457 ms | 10.2236 ms | — |
+
+Every new arm lost to M32×N128. The forced new-arm compound projected
+10,820.26 ms, only 1.0603×/651.95 ms better than exact, with 2.13–2.27
+TFLOP/s on major shapes. It failed every declared gate.
+
+## Disposition
+
+Negative. M16 duplicated enough scheduling and weight traffic to overwhelm its
+register reduction; N256 doubled live FP32 state without reducing physical
+weight work. Both arms and the unselected native-layout scaffold were pruned.
diff --git a/packages/dicose/optimization/experiments/OPT-0021-bounded-f16-flash-partials.md b/packages/dicose/optimization/experiments/OPT-0021-bounded-f16-flash-partials.md
new file mode 100644
index 0000000..dafe1d7
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0021-bounded-f16-flash-partials.md
@@ -0,0 +1,47 @@
+# OPT-0021 — bounded-f16 Flash QK/PV partials
+
+## Hypothesis
+
+Flash attention still sustained substantially less useful arithmetic throughput
+than the dense contractions. Six paired arms isolated whether grouping adjacent
+products in f16 could reduce instruction pressure while keeping the softmax and
+persistent output state in f32:
+
+- current f32 QK and PV accumulation;
+- QK K4 partials only;
+- PV K4 partials only;
+- combined QK K4 and PV K4 partials;
+- f16 PV products with f32 accumulation;
+- QK K4 plus f16 PV products.
+
+Every candidate first had to keep narrow-shape NRMSE below 0.001 and cosine
+above 0.99999. A production candidate then had to beat the retained Flash
+kernel on the weighted 40 time-axis plus 40 frequency-axis call budget.
+
+## Correctness
+
+All five approximate arms passed the primitive gate. Across the candidates,
+NRMSE ranged from 0.0001995 to 0.0003804, maximum absolute error was at most
+0.0009766, and cosine similarity was at least 0.999999928.
+
+## Result
+
+| Arithmetic | Projected GPU time | Speedup | Change vs current |
+| --- | ---: | ---: | ---: |
+| Current Flash | 8,230.01 ms | 1.000× | — |
+| QK K4 | 8,408.27 ms | 0.979× | 178.26 ms slower |
+| PV K4 | 8,564.24 ms | 0.961× | 334.23 ms slower |
+| QK K4 + PV K4 | 8,800.17 ms | 0.935× | 570.16 ms slower |
+| f16 PV products | 8,283.75 ms | 0.994× | 53.74 ms slower |
+| QK K4 + f16 PV products | 8,465.94 ms | 0.972× | 235.93 ms slower |
+
+The current time/frequency medians were 194.281/11.469 ms. Every approximate
+arm regressed both production geometries rather than merely losing in the
+weighted aggregate.
+
+## Disposition
+
+Negative. The extra conversion and grouping instructions cost more than the
+reduced f32 arithmetic on this GPU. A full waveform run was not warranted
+because no arm passed the performance gate. All selectors, probes, profiler
+arms, and shader variants were pruned.
diff --git a/packages/dicose/optimization/experiments/OPT-0022-stride2-temporal-trunk.md b/packages/dicose/optimization/experiments/OPT-0022-stride2-temporal-trunk.md
new file mode 100644
index 0000000..88e0662
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0022-stride2-temporal-trunk.md
@@ -0,0 +1,68 @@
+# OPT-0022 — stride-2 full transformer trunk
+
+## First-principles target
+
+The supplied WAV produces 1,189 frames × 62 bands = 73,718 transformer
+rows. Dense projections account for about 24.45 TFLOP and full time attention
+for another 7.18 TFLOP, so raising contraction utilization alone cannot make
+the exact 32.11-TFLOP graph substantially sub-realtime. This arm instead
+tested reducing the temporal row count of every deterministic and CD
+transformer block.
+
+The experimental arm selected 595 original-frame anchors at positions
+`0, 2, …, 1188`. Band splitting and mask decoding remain at all 1,189 frames.
+After the low-resolution trunk, the full feature is reconstructed as:
+
+```text
+band_full + interpolate(trunk_low - sampled_band_full)
+```
+
+This bridge is the identity if the trunk is the identity and retains local
+odd-frame band features. Time-axis RoPE uses original positions rather than
+compressed positions; frequency attention remains unchanged.
+
+The accounted arithmetic falls from about 32.11 to 14.57 TFLOP, a 54.6%
+reduction.
+
+## Primitive evidence
+
+The browser WebGPU probe passed with:
+
+- 0/42 temporal-anchor f16 mismatches across even and odd endpoint layouts;
+- zero residual-bridge identity, constant-delta, and odd-tail mismatches;
+- 0/18,432 fused-QKV mismatches against selected original-position rows;
+- zero mapped fused/non-fused attention mismatches;
+- 5,846 output words changed versus the deliberately wrong compressed-RoPE
+ control, proving the position remap is active.
+
+Type checking, all 8 unit tests, and the complete WebGPU probe passed before a
+full model run.
+
+## Paired supplied-WAV result
+
+Both arms used blockwise Flash, the same decoded PCM, and seed `0xd1c05e` in a
+fresh isolated Chrome 151 process.
+
+| Stage | Full | Stride 2 | Speedup |
+| --- | ---: | ---: | ---: |
+| deterministic | 5,458.0 ms | 2,276.6 ms | 2.40× |
+| four refinements | 16,316.7 ms | 7,037.1 ms | 2.32× |
+| total | 23,845.9 ms | 11,388.2 ms | 2.09× |
+
+The 12,457.7-ms saving validates the arithmetic model. Quality does not:
+
+| Stem | NRMSE | SNR | cosine | worst-window NRMSE |
+| --- | ---: | ---: | ---: | ---: |
+| drums | 0.2164 | 13.29 dB | 0.97634 | 0.9959 |
+| bass | 0.4771 | 6.43 dB | 0.88150 | 0.9999 |
+| other | 0.4115 | 7.71 dB | 0.92521 | 1.1770 |
+| vocals | 0.1564 | 16.11 dB | 0.98820 | 0.9053 |
+
+## Disposition
+
+The all-network stride-2 arm is rejected and its public/runtime selection was
+pruned after recording this evidence. It proves that temporal row reduction
+has the required performance leverage, but untrained resampling of the
+deterministic separator changes the waveform far too much. OPT-0025 preserves
+the deterministic network exactly and applies reduced resolution only to the
+small consistency correction.
diff --git a/packages/dicose/optimization/experiments/OPT-0023-weight-only-low-rank-preflight.md b/packages/dicose/optimization/experiments/OPT-0023-weight-only-low-rank-preflight.md
new file mode 100644
index 0000000..a974b94
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0023-weight-only-low-rank-preflight.md
@@ -0,0 +1,44 @@
+# OPT-0023 — weight-only low-rank preflight
+
+## Hypothesis
+
+If the dominant transformer matrices were naturally low-rank, converter-time
+truncated SVD could replace one large contraction with two smaller ones and
+remove dense arithmetic without retraining.
+
+All 96 shipped QKV, FF-up, and FF-down matrices were decoded directly from the
+converter-native f16 package. The audit used optimal singular values, so its
+errors are lower bounds before f16 factor quantization and the additional f16
+activation boundary required by two WebGPU dispatches.
+
+For these 384↔1,536 matrices, rank `r` has both FLOP and factor-storage ratio
+`r / 307.2`.
+
+## Result
+
+| Rank | FLOP ratio | Pooled relative Frobenius residual | Worst residual |
+| ---: | ---: | ---: | ---: |
+| 64 | 20.83% | 0.6594 | 0.7772 |
+| 96 | 31.25% | 0.5679 | 0.6995 |
+| 128 | 41.67% | 0.4907 | 0.6260 |
+| 192 | 62.50% | 0.3617 | 0.4865 |
+| 256 | 83.33% | 0.2519 | 0.3526 |
+| 320 | 104.17% | 0.1493 | 0.2169 |
+
+Pooled residual reaches 20% only at rank 289, which retains 94.1% of the
+original FLOPs. It reaches 10% only at rank 349, which costs 13.6% more than
+the original contraction. Keeping every matrix below 20% requires rank 328
+and is also more expensive.
+
+Rank 256 would remove only about 3.48 TFLOP from the complete graph while its
+optimal weight residual remains 25.2%. Dispatch and intermediate-activation
+traffic would reduce the theoretical saving further. Splitting Q, K, and V
+does not create a useful crossing, and only two of 96 individual matrices have
+rank-256 residual below 15%.
+
+## Disposition
+
+Negative at preflight. Weight-only SVD does not justify package conversion,
+kernel work, or waveform testing. A useful low-rank model would require
+retraining, distillation, or activation-aware calibration rather than a free
+inference-time factorization.
diff --git a/packages/dicose/optimization/experiments/OPT-0024-deterministic-only-output.md b/packages/dicose/optimization/experiments/OPT-0024-deterministic-only-output.md
new file mode 100644
index 0000000..401fe32
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0024-deterministic-only-output.md
@@ -0,0 +1,76 @@
+# OPT-0024 — deterministic-only output
+
+## First-principles target
+
+Four consistency-distilled stem evaluations consume 64 of the graph's 80
+transformer blocks and about 16.5 seconds of the retained 23.4-second model
+wall. The runtime already computes four complete deterministic stem spectra
+before refinement. This arm exposes those released deterministic outputs
+directly rather than pretending another kernel tweak can match the leverage of
+removing four network evaluations.
+
+The per-request mode is explicit and default-off. It returns each deterministic
+spectrum through the existing zero-DC conversion and ISTFT. It does not add
+noise, scale a CD input, run CD mapping or transformer blocks, apply the
+consistency affine, or clamp the output.
+
+Condition capture is also disabled in this mode. The deterministic graph skips
+the CD STFT adapter, all 17 condition adapters, the roughly 972-MiB condition
+arena, and the roughly 634-MiB convolution intermediates because none of them
+feed the deterministic masks.
+
+## Validation
+
+The default remains refined. The output mode is carried explicitly through the
+public API and worker result; deterministic results omit CD diagnostics rather
+than fabricating them. The panel also requires exact per-stem deterministic
+peak/RMS diagnostics between standalone and CD-capturing executions. Type
+checking, all 8 unit tests, the WebGPU probe, and diff checks passed before the
+waveform panel.
+
+## Supplied-WAV result
+
+A fresh isolated Chrome 151 process initialized one full-temporal/Flash
+runtime, ran deterministic-only cold, then ran refined output with the same
+decoded PCM and seed `0xd1c05e`.
+
+| Stage | Deterministic only | Refined |
+| --- | ---: | ---: |
+| deterministic | 5,101.1 ms | 5,168.9 ms |
+| mapping | 0 ms | 17.1 ms |
+| four refinements | 0 ms | 17,555.6 ms |
+| ISTFT | 583.8 ms | 1,360.4 ms |
+| **total** | **5,814.6 ms** | **24,727.0 ms** |
+
+This is a **4.25× speedup** and **18,912.4-ms saving**. The supplied-waveform
+drift versus the refined result was:
+
+| Stem | NRMSE | SNR | cosine | RMS drift |
+| --- | ---: | ---: | ---: | ---: |
+| drums | 0.0582 | 24.71 dB | 0.99853 | 1.98% |
+| bass | 0.0588 | 24.61 dB | 0.99853 | 2.14% |
+| other | 0.0538 | 25.38 dB | 0.99868 | 1.47% |
+| vocals | 638.57 | -56.10 dB | 0.00062 | 63,757% |
+
+The vocals ratio is dominated by a near-zero deterministic vocal reference on
+this particular fixture: its peak is only about `6.8e-5`, while the refined
+vocal peak is about `0.013`. It does not establish which output is closer to a
+ground-truth stem.
+
+## Sustained benchmark
+
+The selectable release harness then ran one warmup and three measured
+deterministic-only passes in a fresh isolated Chrome 151 profile. End-to-end
+samples were 5,936.9, 5,921.2, and 5,840.2 ms, for a **5,921.2-ms median**
+(range 5,840.2–5,936.9 ms). The median model total was 5,912.8 ms, including a
+5,143.1-ms deterministic pass and 613.2-ms ISTFT. Unlike the longer refined
+panels, this short mode did not exhibit an upward thermal slope.
+
+## Disposition
+
+Retain deterministic-only as an explicit fast mode because its 5.81-second
+cold boundary and 5.92-second sustained median are the first results near the
+product's responsiveness target. Do not replace the refined default from
+same-model waveform drift alone. Promotion requires licensed ground-truth
+stems, per-stem SDR/SI-SDR and transient gates, and blind listening; the two
+model outputs are not ground truth for each other.
diff --git a/packages/dicose/optimization/experiments/OPT-0025-cd-stride2-temporal-trunk.md b/packages/dicose/optimization/experiments/OPT-0025-cd-stride2-temporal-trunk.md
new file mode 100644
index 0000000..f31cbd3
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0025-cd-stride2-temporal-trunk.md
@@ -0,0 +1,87 @@
+# OPT-0025 — CD-only stride-2 temporal trunk
+
+## First-principles target
+
+OPT-0022 established that temporal row reduction has enough arithmetic leverage
+to move the full runtime materially, but reducing both networks damaged the
+deterministic separator before refinement began. This narrower arm preserves
+the deterministic separator and all of its condition adapters at the released
+full resolution. Only the four consistency-distilled transformer trunks use
+the 595 anchors at original frame positions `0, 2, …, 1188`.
+
+The CD band split and mask estimator remain at all 1,189 frames. Each full
+deterministic condition tensor is sampled directly while it is added to the
+low-resolution CD destination, avoiding persistent low-resolution condition
+copies. Original-position RoPE is retained. The full mask feature is restored
+with the same residual-delta bridge as OPT-0022:
+
+```text
+cd_band_full + interpolate(cd_trunk_low - sampled_cd_band_full)
+```
+
+This leaves the deterministic graph and its diagnostics byte-path equivalent
+to full mode while reducing arithmetic in the four CD evaluations that
+dominate refined inference.
+
+## Validation
+
+The raw WebGPU probe covered even, odd, and clamped-tail anchor addition with
+zero mismatches across 42 f16 words. Fourteen fixture words distinguish the
+correct uploaded-f16 add from a naive full-precision reference, proving the
+rounding check is active. The existing decimation, residual bridge, and
+original-position RoPE probes also remained exact. Type checking, all 8 unit
+tests, the complete WebGPU probe, and diff checks passed before the paired
+waveform run.
+
+## Paired supplied-WAV result
+
+Both arms used blockwise Flash, the same decoded PCM, and seed `0xd1c05e` in
+the isolated-Chrome quality harness. The candidate changed only the four CD
+workspaces; deterministic diagnostics matched the full control exactly.
+
+| Stage | Full refined | CD stride 2 | Full / candidate |
+| --- | ---: | ---: | ---: |
+| deterministic | 4,947.3 ms | 4,819.1 ms | 1.03× |
+| four refinements | 16,340.6 ms | 7,103.6 ms | 2.30× |
+| ISTFT | 1,311.6 ms | 1,471.8 ms | 0.89× |
+| **total** | **23,214.6 ms** | **14,081.3 ms** | **1.6486×** |
+
+The candidate saved **9,133.3 ms** end to end. Unlike the all-network arm,
+global waveform drift stayed small:
+
+| Stem | NRMSE | SNR | cosine | worst-window NRMSE | worst-window cosine |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| drums | 0.0230327 | 32.7531 dB | 0.9997358 | 0.23294 | 0.97274 |
+| bass | 0.0222445 | 33.0555 dB | 0.9997531 | 0.54917 | 0.97686 |
+| other | 0.0353758 | 29.0259 dB | 0.9993754 | 0.09258 | 0.99572 |
+| vocals | 0.0180505 | 34.8702 dB | 0.9998604 | 0.03713 | 0.999635 |
+
+Peak-localized errors were also bounded on this fixture: maximum absolute
+error/reference peak was `0.0396217/0.07330` for drums,
+`0.0208697/0.05809` for bass, `0.0217871/0.06172` for other, and
+`0.00042005/0.03228` for vocals. The bass worst-window NRMSE of 0.54917 is the
+clearest remaining warning that low global error is not a listening or
+ground-truth quality result.
+
+## Sustained benchmark
+
+The selectable release harness also ran one warmup and three measured
+`refined` + `cd-stride2` passes in isolated Chrome 151. End-to-end samples were
+17,534.9, 17,908.1, and 18,617.0 ms, for a **17,908.1-ms median** (range
+17,534.9–18,617.0 ms). Median stages were 6,494.4 ms deterministic, 9,562.7 ms
+refinement, 1,262.8 ms ISTFT, and 17,898.3 ms model total.
+
+This panel immediately followed the longer full-resolution sustained panel;
+its nominally unchanged deterministic stage was 23% slower than the full
+panel's median and continued rising, exposing substantial device thermal
+state. Therefore 17.91 s is a conservative hot-device absolute result, while
+the adjacent-arm 14.08 s result above remains the controlled speed comparison.
+
+## Disposition
+
+Retain `cd-stride2` as an explicit experimental balanced mode. It recovers
+most of the temporal-reduction speedup while avoiding the large waveform drift
+caused by decimating the deterministic separator. Full refined inference stays
+the default until licensed ground-truth stems, per-stem SDR/SI-SDR and
+transient gates, and blind listening establish that the local errors are
+acceptable.
diff --git a/packages/dicose/optimization/experiments/OPT-0026-fast-ten-percent-overlap.md b/packages/dicose/optimization/experiments/OPT-0026-fast-ten-percent-overlap.md
new file mode 100644
index 0000000..be45e77
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0026-fast-ten-percent-overlap.md
@@ -0,0 +1,84 @@
+# OPT-0026 — Fast 10% long-track overlap
+
+## First-principles target
+
+OPT-0024 removes the four consistency-distilled evaluations from Fast, but the
+long-track wrapper still inherited Full's 50% overlap. That schedule evaluates
+almost every source coordinate twice even though Fast runs only the released
+deterministic separator. Reducing duplicate fixed-size calls has more leverage
+than another small kernel change and does not alter the neural graph within a
+model item.
+
+Full remains the released deterministic plus CD graph with the generic MSST
+50% whole-track overlap policy. Fast keeps the 485,100-sample model item and
+the existing 48,510-sample endpoint-inclusive linear fade, but advances by
+436,590 samples. Adjacent Fast chunks therefore overlap exactly the fade region:
+
+| Geometry | Full | Fast |
+| --- | ---: | ---: |
+| Model item | 485,100 samples | 485,100 samples |
+| Step | 242,550 samples / 550 STFT hops | 436,590 samples / 990 STFT hops |
+| Overlap and reflected border | 242,550 samples / 50% | 48,510 samples / 10% |
+| Linear fade | 48,510 samples / 10% | 48,510 samples / 10% |
+
+Both schedules remain aligned to the 441-sample STFT hop. In Fast, the two
+linear window ramps span the complete overlap and normalized overlap-add keeps
+every output coordinate covered.
+
+## Long-track call-count projection
+
+After input resampling, `trust_nobody.wav` has 5,608,109 model-rate samples.
+The exact schedule geometry is:
+
+| Schedule | Chunks | Evaluated model samples | Evaluated / source |
+| --- | ---: | ---: | ---: |
+| Full 50% overlap | 25 | 12,127,500 | 2.1625× |
+| Fast 10% overlap | 13 | 6,306,300 | 1.1245× |
+
+Because every item has the same size, the 25/13 ratio projects a **1.92×
+reduction in chunk calls** for this input. This is deliberately not reported as
+a 1.92× wall-time result: browser scheduling, DSP, allocation, and sustained
+thermal behavior do not scale exactly with call count.
+
+## End-to-end measurement
+
+A fresh isolated Chrome 151 process ran `trust_nobody.wav` in Fast with one
+warmup followed by three measured passes through the same worker and model
+package. End-to-end browser samples were:
+
+| Sample | Wall time |
+| ---: | ---: |
+| 1 | 71.57 s |
+| 2 | 113.05 s |
+| 3 | 79.61 s |
+
+The sustained median was **79.61 s** (range **71.57–113.05 s**). The pass at
+the median wall time reported 69.51 s deterministic model compute, 7.49 s
+ISTFT, and 1.94 s preparation. A separate fresh-Chrome, no-warmup sample took
+60.85 s end to end. The spread is large enough that the sustained panel, not
+the isolated best sample, is the decision metric.
+
+This misses the 30-second goal. At the sustained median, the deterministic
+graph alone consumes 69.51 seconds, so further overlap reduction cannot close
+the gap: the schedule is already only 13 fixed items for 127.17 seconds of
+source. The next material step must reduce or accelerate deterministic-graph
+and DSP work while preserving the checkpoint's useful quality.
+
+## Correctness and quality boundary
+
+The per-item deterministic checkpoint, STFT geometry, mask reconstruction,
+and output restoration are unchanged. Inputs at or below the existing
+single-pass threshold are also unchanged. Full retains its 50% schedule and
+reference gates.
+
+Fast's long-track context and crossfade differ from the generic upstream MSST
+policy. Numeric plan and identity-overlap tests can establish exact length,
+positive coverage, and absence of arithmetic gaps, but they cannot establish
+perceptual seam quality. No listening-quality claim follows from the call-count
+projection; long-track listening remains the acceptance gate for that tradeoff.
+
+## Disposition
+
+Retain the 10% schedule only for explicit Fast requests. Keep Full as the
+default 50%-overlap path. Do not promote the Fast schedule as upstream-
+equivalent, and do not describe it as meeting the 30-second long-track target.
diff --git a/packages/dicose/optimization/experiments/OPT-0027-extra-fast-hop882.md b/packages/dicose/optimization/experiments/OPT-0027-extra-fast-hop882.md
new file mode 100644
index 0000000..4b24239
--- /dev/null
+++ b/packages/dicose/optimization/experiments/OPT-0027-extra-fast-hop882.md
@@ -0,0 +1,99 @@
+# OPT-0027 — Extra Fast half-rate STFT
+
+## First-principles target
+
+Fast still evaluates about 75.687 logical TFLOP across the 13 fixed-size calls
+needed for `trust_nobody.wav`. Even a fictional uniform 2.7 TFLOP/s therefore
+spends 28.03 seconds on model arithmetic before STFT, ISTFT, readback, or browser
+overhead. Reaching 30 seconds requires fewer transformer rows rather than
+another isolated kernel improvement.
+
+The Extra Fast candidate was tested as a third, explicit mode while leaving
+Full and Fast unchanged. It kept the 2,048-sample FFT and released deterministic
+checkpoint, but changed the analysis/synthesis hop from 441 to 882 samples and
+evaluated temporal RoPE at original positions `0, 2, ..., 1100`.
+
+For one 485,100-sample item:
+
+| Family | Fast TFLOP | Extra Fast TFLOP |
+| --- | ---: | ---: |
+| Transformer dense | 4.3014 | 2.1526 |
+| Time attention | 1.2314 | 0.3084 |
+| Frequency attention | 0.0693 | 0.0347 |
+| Mask estimators | 0.2165 | 0.1084 |
+| Band split | 0.0035 | 0.0017 |
+| **Total** | **5.8221** | **2.6058** |
+
+The graph retains **44.76%** of Fast's accounted arithmetic. Across 13 chunks,
+the projection is 75.687 → 33.875 TFLOP.
+
+## Geometry and correctness boundary
+
+For the same centered window, hop-882 frame `j` is bit-identical to hop-441
+frame `2j`. A fixed item therefore becomes 1,101 → 551 frames. The existing
+low-overlap schedule remains exactly aligned:
+
+| Quantity | Samples | Hop-882 intervals |
+| --- | ---: | ---: |
+| Model item | 485,100 | 550 |
+| Step | 436,590 | 495 |
+| Fade and reflected border | 48,510 | 55 |
+
+The periodic Hann window with hop 882 remains an oversampled, normalized
+analysis/synthesis pair. Unit gates cover exact even-frame equivalence for both
+odd and even hop-441 frame counts, minimum and arbitrary-length round trips,
+exact output length, and overlap alignment. The output-spectrum metadata carries
+hop 882 into ISTFT; leaving the released 441 value there would silently zero-fill
+roughly half the requested waveform.
+
+Only time-axis QKV rotation and attention use `positionStride=2` with
+`positionLimit=(tokens-1)*2`. Frequency attention remains at stride 1. The
+existing raw-WebGPU probe reports zero fused-QKV and mapped-attention mismatches
+and proves that original-position RoPE changes the result versus compressed
+positions.
+
+This arm shares the even-frame transformer predictions of OPT-0022's rejected
+stride-2 trunk. Its new behavior is coherent half-rate mask estimation and
+hop-882 synthesis rather than an interpolated hidden residual feeding a
+full-rate mask head. That removes one mismatch, but it does not establish
+perceptual quality. The checkpoint was trained at hop 441, so listening remains
+the acceptance gate.
+
+## Supplied-WAV smoke
+
+A fresh isolated Chrome 151 process completed the supplied 11.89-second WAV in
+3.11 seconds end to end, including 1.76 seconds of deterministic model time and
+0.39 seconds of ISTFT. Every stem had the exact restored 262,144-sample,
+22.05-kHz timeline, finite samples, nonzero final-window energy, deterministic
+diagnostics, and zero mapping/refinement time.
+
+## Sustained long-track measurement
+
+The source was `/Users/hamza/Desktop/trust_nobody.wav`, SHA-256
+`d3c1378d287bbd0bb2b1f294806015cce572cfafe6c495b2f6dbf46432322a1b`. After
+resampling it contains 5,608,109 model-rate samples and uses the same 13 chunks
+as Fast.
+
+A fresh isolated Chrome 151 process ran one warmup followed by three measured
+passes through the same worker and model package:
+
+| Sample | Wall time |
+| ---: | ---: |
+| 1 | 26.62 s |
+| 2 | 28.04 s |
+| 3 | 28.82 s |
+
+The sustained median is **28.04 seconds** (range **26.62–28.82 seconds**),
+meeting the 30-second target. The median sample reports 21.93 seconds of
+deterministic model compute, 1.16 seconds of preparation, and 4.49 seconds of
+ISTFT. No approximate K4 accumulation, persistent graph, Wasm SIMD FFT, or
+GPU/CPU chunk pipeline is included.
+
+## Disposition
+
+Rejected after listening. Although the numeric gates passed and the candidate
+met the timing target, its perceptual degradation was too large relative to
+Fast. The public mode and all hop-882/temporal-position-remapping implementation
+and probe code were removed. Full remains the released refined default, Fast
+remains the full-temporal-resolution deterministic path, and this document is
+retained only as benchmark evidence.
diff --git a/packages/dicose/package.json b/packages/dicose/package.json
new file mode 100644
index 0000000..475bd55
--- /dev/null
+++ b/packages/dicose/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "dicose-wgsl",
+ "version": "0.1.0",
+ "private": true,
+ "description": "DiCoSe BS-RoFormer + consistency-distilled refinement in raw WebGPU WGSL",
+ "type": "module",
+ "sideEffects": ["./dist/worker.js", "./src/worker.ts"],
+ "license": "MIT",
+ "author": {
+ "name": "Hamza Qayyum",
+ "email": "hamza@narcotic.sh"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ },
+ "./worker": {
+ "types": "./dist/worker.d.ts",
+ "import": "./dist/worker.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "check": "tsc --noEmit",
+ "test": "vitest run",
+ "build": "npm run check && vite build && tsc --project tsconfig.build.json",
+ "dev": "vite --host 127.0.0.1",
+ "test:webgpu": "node scripts/browser-webgpu-probe.mjs",
+ "profile:webgpu": "node scripts/browser-kernel-profile.mjs",
+ "test:attention-quality": "node scripts/browser-attention-quality.mjs",
+ "test:reference-quality": "node scripts/browser-reference-quality.mjs",
+ "test:refined-reference-quality": "node scripts/browser-refined-reference-quality.mjs",
+ "test:output-mode-quality": "node scripts/browser-output-mode-quality.mjs",
+ "model:prepare": "uv run --frozen --python 3.13 --project model python3 model/convert.py --overwrite",
+ "model:test": "uv run --frozen --python 3.13 --project model python3 -m unittest discover -s model/tests -v",
+ "test:browser": "node scripts/browser-e2e.mjs",
+ "benchmark:browser": "node scripts/browser-benchmark.mjs",
+ "verify:package": "node scripts/verify-package.mjs"
+ },
+ "devDependencies": {
+ "@types/node": "^26.1.1",
+ "@webgpu/types": "^0.1.64",
+ "typescript": "^5.9.2",
+ "vite": "^8.1.5",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/packages/dicose/public/Mixture_audio_1.wav b/packages/dicose/public/Mixture_audio_1.wav
new file mode 100644
index 0000000..18c5b80
Binary files /dev/null and b/packages/dicose/public/Mixture_audio_1.wav differ
diff --git a/packages/dicose/scripts/browser-acceptance.mjs b/packages/dicose/scripts/browser-acceptance.mjs
new file mode 100644
index 0000000..65f2e87
--- /dev/null
+++ b/packages/dicose/scripts/browser-acceptance.mjs
@@ -0,0 +1,96 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const ROOT = resolve(import.meta.dirname, "..");
+const CONTRACT_PATH = resolve(ROOT, "test/fixtures/deterministic-reference.json");
+
+/**
+ * Fail an unattended browser run if its deterministic WebGPU checkpoint leaves
+ * the separately-recorded upstream f32/f16 acceptance envelope. Runtime
+ * completion alone is not a quality signal: a finite but silent mask would
+ * otherwise look successful to the CDP harness.
+ */
+export function assertFixtureAcceptance(harnessResult) {
+ const contract = JSON.parse(readFileSync(CONTRACT_PATH, "utf8"));
+ const report = harnessResult?.report;
+ if (report?.ok !== true || report.output === undefined || report.metrics === undefined) {
+ throw new Error("Browser report has no successful fixture output to validate");
+ }
+ const diagnostics = report.output.diagnostics?.deterministic;
+ if (diagnostics === undefined) {
+ throw new Error("Browser report omitted deterministic diagnostic statistics");
+ }
+ const expected = contract.reference?.stems;
+ const envelope = contract.webgpuF16Acceptance;
+ if (expected === undefined || envelope === undefined) {
+ throw new Error("Deterministic fixture contract is malformed");
+ }
+
+ for (const name of ["drums", "bass", "other"]) {
+ const actual = diagnostics[name];
+ const target = expected[name];
+ assertStat(
+ actual?.rms,
+ target?.rms,
+ envelope.audibleStems.rmsRelativeTolerance,
+ envelope.audibleStems.rmsAbsoluteTolerance,
+ `${name} deterministic RMS`,
+ );
+ assertStat(
+ actual?.peak,
+ target?.peak,
+ envelope.audibleStems.peakRelativeTolerance,
+ envelope.audibleStems.peakAbsoluteTolerance,
+ `${name} deterministic peak`,
+ );
+ }
+ const vocals = diagnostics.vocals;
+ if (!Number.isFinite(vocals?.rms) || vocals.rms > envelope.vocals.maxRms) {
+ throw new Error(`vocals deterministic RMS outside f16 acceptance envelope: ${vocals?.rms}`);
+ }
+ if (!Number.isFinite(vocals?.peak) || vocals.peak > envelope.vocals.maxPeak) {
+ throw new Error(`vocals deterministic peak outside f16 acceptance envelope: ${vocals?.peak}`);
+ }
+
+ const outputSampleRate = contract.fixture?.input?.sampleRate;
+ const outputFrames = contract.fixture?.input?.frames;
+ if (!Number.isSafeInteger(outputSampleRate) || !Number.isSafeInteger(outputFrames)) {
+ throw new Error("Deterministic fixture contract omits native output geometry");
+ }
+ for (const name of ["drums", "bass", "other", "vocals"]) {
+ const stem = report.output.stems?.[name];
+ if (
+ stem?.sampleRate !== outputSampleRate ||
+ stem.samples !== outputFrames ||
+ stem.finiteSamples !== outputFrames * 2 ||
+ !Number.isFinite(stem.rms) ||
+ !Number.isFinite(stem.peak) ||
+ stem.rms <= 0 ||
+ stem.peak <= 0
+ ) {
+ throw new Error(`final ${name} output does not match the finite native fixture timeline`);
+ }
+ }
+ const instrumental = report.output.instrumental;
+ if (
+ instrumental?.sampleRate !== outputSampleRate ||
+ instrumental.samples !== outputFrames ||
+ instrumental.finiteSamples !== outputFrames * 2 ||
+ !Number.isFinite(instrumental.rms) ||
+ !Number.isFinite(instrumental.peak) ||
+ instrumental.rms <= 0 ||
+ instrumental.peak <= 0
+ ) {
+ throw new Error("derived instrumental does not match the finite native fixture timeline");
+ }
+}
+
+function assertStat(actual, expected, relativeTolerance, absoluteTolerance, label) {
+ if (!Number.isFinite(actual) || !Number.isFinite(expected)) {
+ throw new Error(`${label} is non-finite`);
+ }
+ const allowed = Math.max(absoluteTolerance, Math.abs(expected) * relativeTolerance);
+ if (Math.abs(actual - expected) > allowed) {
+ throw new Error(`${label} outside f16 acceptance envelope: expected ${expected}, got ${actual}, allowed ±${allowed}`);
+ }
+}
diff --git a/packages/dicose/scripts/browser-attention-quality.mjs b/packages/dicose/scripts/browser-attention-quality.mjs
new file mode 100644
index 0000000..e7c87a4
--- /dev/null
+++ b/packages/dicose/scripts/browser-attention-quality.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe exact-q64 versus Flash waveform A/B",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: 5 * 60_000,
+ pagePath: "/test/browser-attention-quality.html",
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(`${JSON.stringify({
+ ok: false,
+ harness: "browser-attention-quality",
+ error: error instanceof Error ? error.message : String(error),
+ }, null, 2)}\n`);
+ process.exitCode = 1;
+}
diff --git a/packages/dicose/scripts/browser-benchmark.mjs b/packages/dicose/scripts/browser-benchmark.mjs
new file mode 100644
index 0000000..dd40900
--- /dev/null
+++ b/packages/dicose/scripts/browser-benchmark.mjs
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const warmupRuns = readNonNegativeIntegerEnv("DICOSE_BENCHMARK_WARMUP_RUNS", 1);
+ const measuredRuns = readPositiveIntegerEnv("DICOSE_BENCHMARK_RUNS", 3);
+ const outputMode = readChoiceEnv(
+ "DICOSE_BENCHMARK_OUTPUT_MODE",
+ ["refined", "deterministic"],
+ "refined",
+ );
+ const result = await runBrowserHarness({
+ label: `DiCoSe ${outputMode} supplied-wave benchmark`,
+ mode: "benchmark",
+ warmupRuns,
+ measuredRuns,
+ pagePath: `/?outputMode=${encodeURIComponent(outputMode)}`,
+ timeoutMs: readPositiveIntegerEnv("DICOSE_BROWSER_TIMEOUT_MS", 60 * 60 * 1000),
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(
+ `${JSON.stringify(
+ {
+ ok: false,
+ harness: "browser-benchmark",
+ error: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ process.exitCode = 1;
+}
+
+function readNonNegativeIntegerEnv(name, fallback) {
+ const source = process.env[name];
+ if (source === undefined) return fallback;
+ const parsed = Number(source);
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
+ throw new RangeError(`${name} must be a non-negative integer`);
+ }
+ return parsed;
+}
+
+function readPositiveIntegerEnv(name, fallback) {
+ const source = process.env[name];
+ if (source === undefined) return fallback;
+ const parsed = Number(source);
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
+ throw new RangeError(`${name} must be a positive integer`);
+ }
+ return parsed;
+}
+
+function readChoiceEnv(name, choices, fallback) {
+ const value = process.env[name] ?? fallback;
+ if (!choices.includes(value)) {
+ throw new RangeError(`${name} must be one of: ${choices.join(", ")}`);
+ }
+ return value;
+}
diff --git a/packages/dicose/scripts/browser-e2e.mjs b/packages/dicose/scripts/browser-e2e.mjs
new file mode 100644
index 0000000..e7ad00b
--- /dev/null
+++ b/packages/dicose/scripts/browser-e2e.mjs
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+import { assertFixtureAcceptance } from "./browser-acceptance.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe full supplied-wave acceptance",
+ mode: "e2e",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: readPositiveIntegerEnv("DICOSE_BROWSER_TIMEOUT_MS", 20 * 60 * 1000),
+ });
+ assertFixtureAcceptance(result);
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(
+ `${JSON.stringify(
+ {
+ ok: false,
+ harness: "browser-e2e",
+ error: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ process.exitCode = 1;
+}
+
+function readPositiveIntegerEnv(name, fallback) {
+ const source = process.env[name];
+ if (source === undefined) return fallback;
+ const parsed = Number(source);
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
+ throw new RangeError(`${name} must be a positive integer in milliseconds`);
+ }
+ return parsed;
+}
diff --git a/packages/dicose/scripts/browser-harness.mjs b/packages/dicose/scripts/browser-harness.mjs
new file mode 100644
index 0000000..20758d5
--- /dev/null
+++ b/packages/dicose/scripts/browser-harness.mjs
@@ -0,0 +1,635 @@
+/**
+ * Shared, fully unattended Chrome runner for the local DiCoSe WebGPU page.
+ *
+ * Every invocation starts a temporary Vite server and a Chrome process with a
+ * brand-new user-data directory. It talks to that process over its private
+ * CDP port, so there is no dependency on a user profile, extension, click, or
+ * download prompt.
+ */
+import { spawn } from "node:child_process";
+import { access, mkdtemp, readFile, rm } from "node:fs/promises";
+import { constants } from "node:fs";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+
+import { createServer } from "vite";
+
+export const repositoryRoot = resolve(import.meta.dirname, "..");
+
+const DEFAULT_CHROME_PATH =
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
+const MAX_CAPTURED_MESSAGES = 50;
+
+/**
+ * Load the local page and wait for its automatic run to publish a result.
+ *
+ * The page contract is deliberately small:
+ * `globalThis.__DICOSE_BROWSER__.report` and `#result` must contain the same
+ * JSON-serializable report, with `ok: true` on success. The page reads the
+ * query parameters written here and starts itself when `autorun=1`.
+ */
+export async function runBrowserHarness({
+ label,
+ mode,
+ warmupRuns = 0,
+ measuredRuns = 1,
+ timeoutMs = 20 * 60 * 1000,
+ sourcePath = "/Mixture_audio_1.wav",
+ pagePath = "/",
+} = {}) {
+ assertNonEmptyString(label, "label");
+ assertOneOf(mode, ["e2e", "benchmark", "probe"], "mode");
+ assertNonNegativeInteger(warmupRuns, "warmupRuns");
+ assertPositiveInteger(measuredRuns, "measuredRuns");
+ assertPositiveInteger(timeoutMs, "timeoutMs");
+ if (!sourcePath.startsWith("/")) {
+ throw new RangeError("sourcePath must be an absolute URL path");
+ }
+ if (!pagePath.startsWith("/")) {
+ throw new RangeError("pagePath must be an absolute URL path");
+ }
+
+ const profileDirectory = await mkdtemp(
+ join(tmpdir(), "dicose-wgsl-chrome-profile-"),
+ );
+ let server;
+ let browser;
+ let connection;
+ let targetId;
+ let sessionId;
+ const consoleMessages = [];
+ const pageExceptions = [];
+
+ try {
+ server = await createServer({
+ root: repositoryRoot,
+ logLevel: "error",
+ server: {
+ host: "127.0.0.1",
+ port: 0,
+ strictPort: false,
+ },
+ });
+ await server.listen();
+
+ const chromePath = await resolveChromePath();
+ const stderr = [];
+ let launchError;
+ browser = spawn(
+ chromePath,
+ [
+ `--user-data-dir=${profileDirectory}`,
+ "--headless=new",
+ "--remote-debugging-address=127.0.0.1",
+ "--remote-debugging-port=0",
+ "--remote-allow-origins=*",
+ "--no-first-run",
+ "--no-default-browser-check",
+ "--disable-background-networking",
+ "--disable-component-update",
+ "--disable-default-apps",
+ "--disable-features=MediaRouter,OptimizationHints,Translate",
+ "about:blank",
+ ],
+ { detached: true, stdio: ["ignore", "ignore", "pipe"] },
+ );
+ browser.once("error", (error) => {
+ launchError = error;
+ });
+ browser.stderr?.setEncoding("utf8");
+ browser.stderr?.on("data", (chunk) => {
+ capture(stderr, String(chunk));
+ });
+
+ const webSocketUrl = await waitForDevTools({
+ profileDirectory,
+ browser,
+ stderr,
+ launchError: () => launchError,
+ });
+ connection = await CdpConnection.connect(webSocketUrl);
+ const chromeVersion = await connection.send("Browser.getVersion");
+
+ ({ targetId } = await connection.send("Target.createTarget", {
+ url: "about:blank",
+ }));
+ ({ sessionId } = await connection.send(
+ "Target.attachToTarget",
+ { targetId, flatten: true },
+ ));
+ connection.on("Runtime.consoleAPICalled", (params, eventSessionId) => {
+ if (eventSessionId !== sessionId) return;
+ const text = (params.args ?? [])
+ .map((argument) => {
+ if (typeof argument.value === "string") return argument.value;
+ if (argument.value !== undefined) return JSON.stringify(argument.value);
+ return argument.description ?? argument.type ?? "unknown";
+ })
+ .join(" ");
+ capture(consoleMessages, `${params.type ?? "log"}: ${text}`);
+ });
+ connection.on("Runtime.exceptionThrown", (params, eventSessionId) => {
+ if (eventSessionId !== sessionId) return;
+ const details = params.exceptionDetails ?? {};
+ capture(
+ pageExceptions,
+ details.exception?.description ?? details.text ?? "Page exception",
+ );
+ });
+ await Promise.all([
+ connection.send("Page.enable", {}, sessionId),
+ connection.send("Runtime.enable", {}, sessionId),
+ connection.send("Log.enable", {}, sessionId),
+ ]);
+
+ const targetUrl = createRunUrl(serverOrigin(server), {
+ mode,
+ warmupRuns,
+ measuredRuns,
+ sourcePath,
+ pagePath,
+ });
+ const navigation = await connection.send(
+ "Page.navigate",
+ { url: targetUrl },
+ sessionId,
+ );
+ if (typeof navigation.errorText === "string") {
+ throw new Error(`Chrome could not navigate to the local test page: ${navigation.errorText}`);
+ }
+
+ const report = await waitForBrowserReport({
+ connection,
+ sessionId,
+ timeoutMs,
+ consoleMessages,
+ pageExceptions,
+ });
+ if (report.ok !== true) {
+ throw new Error(formatFailedReport(report));
+ }
+ validateSuccessfulReport(report, mode);
+
+ const page = await evaluate(connection, sessionId, `(() => ({
+ url: location.href,
+ readyState: document.readyState,
+ webgpu: "gpu" in navigator,
+ crossOriginIsolated: globalThis.crossOriginIsolated === true,
+ }))()`);
+ return {
+ ok: true,
+ label,
+ run: { mode, warmupRuns, measuredRuns, sourcePath, pagePath },
+ chrome: {
+ product: chromeVersion.product,
+ userAgent: chromeVersion.userAgent,
+ jsVersion: chromeVersion.jsVersion,
+ },
+ page,
+ report,
+ consoleMessages,
+ pageExceptions,
+ };
+ } finally {
+ if (connection !== undefined && targetId !== undefined) {
+ await connection.send("Target.closeTarget", { targetId }).catch(() => {});
+ }
+ connection?.close();
+ await terminateProcessGroup(browser);
+ await closeViteServer(server);
+ await rm(profileDirectory, {
+ recursive: true,
+ force: true,
+ maxRetries: 3,
+ retryDelay: 100,
+ }).catch(() => {});
+ }
+}
+
+function createRunUrl(origin, { mode, warmupRuns, measuredRuns, sourcePath, pagePath }) {
+ const url = new URL(pagePath, origin);
+ url.searchParams.set("autorun", "1");
+ url.searchParams.set("mode", mode);
+ url.searchParams.set("warmupRuns", String(warmupRuns));
+ url.searchParams.set("measuredRuns", String(measuredRuns));
+ url.searchParams.set("source", sourcePath);
+ return url.href;
+}
+
+async function resolveChromePath() {
+ const candidates = [
+ process.env.DICOSE_CHROME_PATH,
+ process.env.CHROME_PATH,
+ process.env.CHROME_BIN,
+ DEFAULT_CHROME_PATH,
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
+ for (const candidate of candidates) {
+ try {
+ await access(candidate, constants.X_OK);
+ return candidate;
+ } catch {
+ // Try the next explicit or conventional Chrome location.
+ }
+ }
+ throw new Error(
+ `Could not find an executable Google Chrome. Checked: ${candidates.join(", ")}`,
+ );
+}
+
+function serverOrigin(server) {
+ const address = server?.httpServer?.address();
+ if (address === null || address === undefined || typeof address === "string") {
+ throw new Error("Vite did not expose a local TCP address");
+ }
+ return `http://127.0.0.1:${address.port}`;
+}
+
+async function closeViteServer(server) {
+ if (server === undefined) return;
+ if (typeof server.close === "function") {
+ await server.close();
+ return;
+ }
+ const httpServer = server.httpServer;
+ if (httpServer === undefined || !httpServer.listening) return;
+ await new Promise((resolvePromise, rejectPromise) => {
+ httpServer.close((error) => {
+ if (error === undefined) resolvePromise();
+ else rejectPromise(error);
+ });
+ });
+}
+
+async function waitForDevTools({
+ profileDirectory,
+ browser,
+ stderr,
+ launchError,
+}) {
+ const activePort = join(profileDirectory, "DevToolsActivePort");
+ const deadline = Date.now() + 20_000;
+ while (Date.now() < deadline) {
+ const startError = launchError();
+ if (startError !== undefined) throw startError;
+ if (browser.exitCode !== null || browser.signalCode !== null) {
+ throw new Error(`Chrome exited before CDP was ready:\n${stderr.join("")}`);
+ }
+ try {
+ const [portSource, browserPath] = (await readFile(activePort, "utf8"))
+ .trim()
+ .split(/\r?\n/);
+ const port = Number(portSource);
+ if (Number.isSafeInteger(port) && port > 0 && browserPath) {
+ const response = await fetch(`http://127.0.0.1:${port}/json/version`);
+ if (response.ok) {
+ const version = await response.json();
+ return version.webSocketDebuggerUrl ?? `ws://127.0.0.1:${port}${browserPath}`;
+ }
+ }
+ } catch {
+ // Chrome has not finished writing the private CDP endpoint yet.
+ }
+ await delay(100);
+ }
+ throw new Error(`Timed out waiting for Chrome CDP:\n${stderr.join("")}`);
+}
+
+async function waitForBrowserReport({
+ connection,
+ sessionId,
+ timeoutMs,
+ consoleMessages,
+ pageExceptions,
+}) {
+ const deadline = Date.now() + timeoutMs;
+ let lastEvaluationError;
+ while (Date.now() < deadline) {
+ let report;
+ try {
+ report = await evaluate(connection, sessionId, browserReportExpression());
+ lastEvaluationError = undefined;
+ } catch (error) {
+ // Navigation can replace the execution context while the first module is loading.
+ lastEvaluationError = error;
+ }
+ if (report !== null && report !== undefined) {
+ if (typeof report !== "object" || Array.isArray(report)) {
+ throw new Error("DiCoSe browser page published a non-object report");
+ }
+ if (report.ok === false) throw new Error(formatFailedReport(report));
+ if (report.ok === true) return report;
+ }
+ await delay(150);
+ }
+ const details = [
+ "Timed out waiting for the automatic DiCoSe browser run.",
+ lastEvaluationError === undefined
+ ? undefined
+ : `Last page evaluation error: ${errorMessage(lastEvaluationError)}`,
+ pageExceptions.length === 0
+ ? undefined
+ : `Page exceptions:\n${pageExceptions.join("\n")}`,
+ consoleMessages.length === 0
+ ? undefined
+ : `Console output:\n${consoleMessages.join("\n")}`,
+ ]
+ .filter(Boolean)
+ .join("\n\n");
+ throw new Error(details);
+}
+
+function browserReportExpression() {
+ return `(() => {
+ const api = globalThis.__DICOSE_BROWSER__;
+ const candidates = [
+ api?.report,
+ api?.lastReport,
+ globalThis.__DICOSE_BROWSER_REPORT__,
+ ];
+ for (const candidate of candidates) {
+ if (candidate !== undefined && candidate !== null) return candidate;
+ }
+ const text = document.querySelector("#result")?.textContent ?? "";
+ try {
+ const parsed = JSON.parse(text);
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
+ } catch {
+ return null;
+ }
+ })()`;
+}
+
+async function evaluate(connection, sessionId, expression) {
+ const response = await connection.send(
+ "Runtime.evaluate",
+ {
+ expression,
+ awaitPromise: true,
+ returnByValue: true,
+ },
+ sessionId,
+ );
+ if (response.exceptionDetails !== undefined) {
+ throw new Error(
+ response.exceptionDetails.exception?.description ?? response.exceptionDetails.text,
+ );
+ }
+ return response.result?.value;
+}
+
+function formatFailedReport(report) {
+ const error =
+ typeof report.error === "string"
+ ? report.error
+ : report.error !== null && typeof report.error === "object" &&
+ typeof report.error.message === "string"
+ ? report.error.message
+ : "browser run failed";
+ const probeValues = Object.fromEntries(
+ ["input", "weight", "copy", "linear", "maskStyle", "validationError", "uncapturedErrors"]
+ .filter((key) => report[key] !== undefined)
+ .map((key) => [key, report[key]]),
+ );
+ const suffix = Object.keys(probeValues).length === 0
+ ? ""
+ : `; details=${JSON.stringify(probeValues)}`;
+ return `DiCoSe browser report failed: ${error}${suffix}`;
+}
+
+/**
+ * A page-level `ok: true` only proves that its control flow completed. For
+ * the bundled non-silent fixture, require the actual E2E result to contain
+ * finite stem PCM and, when the diagnostic seam is present, nonzero model
+ * contributions. This prevents a GPU validation failure from masquerading
+ * as a successful sampler that merely returns its seeded noise.
+ */
+function validateSuccessfulReport(report, mode) {
+ if (mode !== "e2e") return;
+ const output = report.output;
+ if (output === null || typeof output !== "object") {
+ throw new Error("DiCoSe E2E report omitted output summaries");
+ }
+ const stems = output.stems;
+ if (stems === null || typeof stems !== "object") {
+ throw new Error("DiCoSe E2E report omitted stem summaries");
+ }
+ for (const name of ["drums", "bass", "other", "vocals"]) {
+ const stem = stems[name];
+ if (stem === null || typeof stem !== "object") {
+ throw new Error(`DiCoSe E2E report omitted the ${name} stem`);
+ }
+ if (
+ !Number.isSafeInteger(stem.samples) || stem.samples <= 0 ||
+ stem.finiteSamples !== stem.samples * 2 ||
+ !Number.isFinite(stem.durationSeconds) || stem.durationSeconds <= 0 ||
+ !Number.isFinite(stem.peak) || stem.peak < 0 ||
+ !Number.isFinite(stem.rms) || stem.rms < 0
+ ) {
+ throw new Error(`DiCoSe E2E report has invalid ${name} PCM statistics`);
+ }
+ }
+
+ const diagnostics = output.diagnostics;
+ if (diagnostics === undefined) return;
+ if (diagnostics === null || typeof diagnostics !== "object") {
+ throw new Error("DiCoSe E2E diagnostics are malformed");
+ }
+ const deterministicOnly = report.metrics?.outputMode === "deterministic";
+ const modelStages = deterministicOnly
+ ? [diagnostics.deterministic]
+ : [diagnostics.deterministic, diagnostics.cdModelOutput];
+ let hasModelContribution = false;
+ for (const stage of modelStages) {
+ if (stage === null || typeof stage !== "object") {
+ throw new Error("DiCoSe E2E diagnostics omit a model stage");
+ }
+ for (const name of ["drums", "bass", "other", "vocals"]) {
+ const statistic = stage[name];
+ if (statistic === null || typeof statistic !== "object" ||
+ !Number.isFinite(statistic.rms) || !Number.isFinite(statistic.peak)) {
+ throw new Error(`DiCoSe E2E diagnostics are malformed for ${name}`);
+ }
+ hasModelContribution ||= statistic.rms > 1e-7 || statistic.peak > 1e-7;
+ }
+ }
+ if (!hasModelContribution) {
+ throw new Error("DiCoSe E2E model diagnostics are all zero; refusing a noise-only false pass");
+ }
+}
+
+function capture(values, value) {
+ values.push(value);
+ while (values.length > MAX_CAPTURED_MESSAGES) values.shift();
+}
+
+async function terminateProcessGroup(child) {
+ if (child === undefined || child.pid === undefined) return;
+ const signal = (name) => {
+ try {
+ process.kill(-child.pid, name);
+ } catch (error) {
+ if (error?.code !== "ESRCH") throw error;
+ }
+ };
+ signal("SIGTERM");
+ if (child.exitCode === null && child.signalCode === null) {
+ await waitForChildExit(child, 5_000);
+ }
+ try {
+ process.kill(-child.pid, 0);
+ signal("SIGKILL");
+ } catch (error) {
+ if (error?.code !== "ESRCH") throw error;
+ }
+ if (child.exitCode === null && child.signalCode === null) {
+ await waitForChildExit(child, 5_000);
+ }
+ child.stderr?.destroy();
+}
+
+function waitForChildExit(child, timeoutMs) {
+ return new Promise((resolvePromise) => {
+ let settled = false;
+ const finish = () => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timer);
+ child.off("exit", onExit);
+ resolvePromise();
+ };
+ const onExit = () => finish();
+ const timer = setTimeout(finish, timeoutMs);
+ child.once("exit", onExit);
+ if (child.exitCode !== null || child.signalCode !== null) finish();
+ });
+}
+
+function delay(milliseconds) {
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
+}
+
+function assertOneOf(value, values, label) {
+ if (!values.includes(value)) {
+ throw new RangeError(`${label} must be one of: ${values.join(", ")}`);
+ }
+}
+
+function assertNonEmptyString(value, label) {
+ if (typeof value !== "string" || value.length === 0) {
+ throw new TypeError(`${label} must be a non-empty string`);
+ }
+}
+
+function assertNonNegativeInteger(value, label) {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ throw new RangeError(`${label} must be a non-negative integer`);
+ }
+}
+
+function assertPositiveInteger(value, label) {
+ if (!Number.isSafeInteger(value) || value <= 0) {
+ throw new RangeError(`${label} must be a positive integer`);
+ }
+}
+
+function errorMessage(error) {
+ return error instanceof Error ? error.message : String(error);
+}
+
+class CdpConnection {
+ #socket;
+ #nextId = 1;
+ #pending = new Map();
+ #listeners = new Map();
+
+ static async connect(webSocketUrl) {
+ if (typeof WebSocket !== "function") {
+ throw new Error("The browser harness requires Node.js with a global WebSocket");
+ }
+ const socket = new WebSocket(webSocketUrl);
+ await new Promise((resolvePromise, rejectPromise) => {
+ const timer = setTimeout(
+ () => rejectPromise(new Error("CDP connection timed out")),
+ 10_000,
+ );
+ socket.addEventListener(
+ "open",
+ () => {
+ clearTimeout(timer);
+ resolvePromise();
+ },
+ { once: true },
+ );
+ socket.addEventListener(
+ "error",
+ (event) => {
+ clearTimeout(timer);
+ rejectPromise(new Error(event.message ?? "CDP connection failed"));
+ },
+ { once: true },
+ );
+ });
+ return new CdpConnection(socket);
+ }
+
+ constructor(socket) {
+ this.#socket = socket;
+ socket.addEventListener("message", (event) => {
+ const message = JSON.parse(String(event.data));
+ if (message.id !== undefined) {
+ const pending = this.#pending.get(message.id);
+ if (pending === undefined) return;
+ this.#pending.delete(message.id);
+ clearTimeout(pending.timer);
+ if (message.error === undefined) pending.resolve(message.result ?? {});
+ else pending.reject(new Error(`${pending.method}: ${message.error.message}`));
+ return;
+ }
+ const listeners = this.#listeners.get(message.method);
+ if (listeners === undefined) return;
+ for (const listener of listeners) listener(message.params ?? {}, message.sessionId);
+ });
+ socket.addEventListener("close", () => {
+ for (const pending of this.#pending.values()) {
+ clearTimeout(pending.timer);
+ pending.reject(new Error("CDP connection closed"));
+ }
+ this.#pending.clear();
+ });
+ }
+
+ on(method, listener) {
+ const listeners = this.#listeners.get(method) ?? new Set();
+ listeners.add(listener);
+ this.#listeners.set(method, listeners);
+ return () => listeners.delete(listener);
+ }
+
+ send(method, params = {}, sessionId, timeoutMs = 30_000) {
+ const id = this.#nextId++;
+ return new Promise((resolvePromise, rejectPromise) => {
+ const timer = setTimeout(() => {
+ this.#pending.delete(id);
+ rejectPromise(new Error(`${method} timed out`));
+ }, timeoutMs);
+ this.#pending.set(id, {
+ method,
+ resolve: resolvePromise,
+ reject: rejectPromise,
+ timer,
+ });
+ this.#socket.send(
+ JSON.stringify({
+ id,
+ method,
+ params,
+ ...(sessionId === undefined ? {} : { sessionId }),
+ }),
+ );
+ });
+ }
+
+ close() {
+ this.#socket.close();
+ }
+}
diff --git a/packages/dicose/scripts/browser-kernel-profile.mjs b/packages/dicose/scripts/browser-kernel-profile.mjs
new file mode 100644
index 0000000..f83c3a4
--- /dev/null
+++ b/packages/dicose/scripts/browser-kernel-profile.mjs
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const focus = readProfileFocus(process.env.DICOSE_PROFILE_FOCUS);
+ const result = await runBrowserHarness({
+ label: "DiCoSe production-shape GPU kernel profile",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: readPositiveIntegerEnv("DICOSE_BROWSER_TIMEOUT_MS", 3 * 60_000),
+ pagePath: `/test/browser-kernel-profile.html${focus === undefined ? "" : `?focus=${focus}`}`,
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(
+ `${JSON.stringify(
+ {
+ ok: false,
+ harness: "browser-kernel-profile",
+ error: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ process.exitCode = 1;
+}
+
+function readProfileFocus(source) {
+ if (source === undefined) return undefined;
+ if (!["all", "dense", "conv", "norm", "attention"].includes(source)) {
+ throw new RangeError("DICOSE_PROFILE_FOCUS must be all, dense, conv, norm, or attention");
+ }
+ return source;
+}
+
+function readPositiveIntegerEnv(name, fallback) {
+ const source = process.env[name];
+ if (source === undefined) return fallback;
+ const parsed = Number(source);
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
+ throw new RangeError(`${name} must be a positive integer in milliseconds`);
+ }
+ return parsed;
+}
diff --git a/packages/dicose/scripts/browser-output-mode-quality.mjs b/packages/dicose/scripts/browser-output-mode-quality.mjs
new file mode 100644
index 0000000..8dbd0a4
--- /dev/null
+++ b/packages/dicose/scripts/browser-output-mode-quality.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe deterministic versus refined waveform panel",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: 5 * 60_000,
+ pagePath: "/test/browser-output-mode-quality.html",
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(`${JSON.stringify({
+ ok: false,
+ harness: "browser-output-mode-quality",
+ error: error instanceof Error ? error.message : String(error),
+ }, null, 2)}\n`);
+ process.exitCode = 1;
+}
diff --git a/packages/dicose/scripts/browser-reference-quality.mjs b/packages/dicose/scripts/browser-reference-quality.mjs
new file mode 100644
index 0000000..104c887
--- /dev/null
+++ b/packages/dicose/scripts/browser-reference-quality.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe deterministic waveform versus upstream PyTorch",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: 5 * 60_000,
+ pagePath: "/test/browser-reference-quality.html",
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(`${JSON.stringify({
+ ok: false,
+ harness: "browser-reference-quality",
+ error: error instanceof Error ? error.message : String(error),
+ }, null, 2)}\n`);
+ process.exitCode = 1;
+}
diff --git a/packages/dicose/scripts/browser-refined-reference-quality.mjs b/packages/dicose/scripts/browser-refined-reference-quality.mjs
new file mode 100644
index 0000000..309d03b
--- /dev/null
+++ b/packages/dicose/scripts/browser-refined-reference-quality.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe Full/refined waveform versus upstream PyTorch",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: 5 * 60_000,
+ pagePath: "/test/browser-refined-reference-quality.html",
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(`${JSON.stringify({
+ ok: false,
+ harness: "browser-refined-reference-quality",
+ error: error instanceof Error ? error.message : String(error),
+ }, null, 2)}\n`);
+ process.exitCode = 1;
+}
diff --git a/packages/dicose/scripts/browser-webgpu-probe.mjs b/packages/dicose/scripts/browser-webgpu-probe.mjs
new file mode 100644
index 0000000..83060db
--- /dev/null
+++ b/packages/dicose/scripts/browser-webgpu-probe.mjs
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+try {
+ const result = await runBrowserHarness({
+ label: "DiCoSe raw WGSL linear/tanh/GLU probe",
+ mode: "probe",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: readPositiveIntegerEnv("DICOSE_BROWSER_TIMEOUT_MS", 60_000),
+ pagePath: "/test/browser-webgpu-probe.html",
+ });
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(
+ `${JSON.stringify(
+ {
+ ok: false,
+ harness: "browser-webgpu-probe",
+ error: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ process.exitCode = 1;
+}
+
+function readPositiveIntegerEnv(name, fallback) {
+ const source = process.env[name];
+ if (source === undefined) return fallback;
+ const parsed = Number(source);
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
+ throw new RangeError(`${name} must be a positive integer in milliseconds`);
+ }
+ return parsed;
+}
diff --git a/packages/dicose/scripts/run-coldrun.local.mjs b/packages/dicose/scripts/run-coldrun.local.mjs
new file mode 100644
index 0000000..a171b02
--- /dev/null
+++ b/packages/dicose/scripts/run-coldrun.local.mjs
@@ -0,0 +1,18 @@
+#!/usr/bin/env node
+// Local assessment driver: run the DiCoSe demo page unattended on a caller-
+// supplied WAV (served from the repository root) and print the raw report.
+import { runBrowserHarness } from "./browser-harness.mjs";
+
+const outputMode = process.env.MODE ?? "deterministic";
+const sourcePath = process.env.SOURCE ?? "/cold-run.wav";
+
+const result = await runBrowserHarness({
+ label: `cold-run ${outputMode}`,
+ mode: "e2e",
+ warmupRuns: 0,
+ measuredRuns: 1,
+ timeoutMs: 20 * 60 * 1000,
+ sourcePath,
+ pagePath: `/?outputMode=${encodeURIComponent(outputMode)}`,
+});
+process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
diff --git a/packages/dicose/scripts/verify-package.mjs b/packages/dicose/scripts/verify-package.mjs
new file mode 100644
index 0000000..fc24985
--- /dev/null
+++ b/packages/dicose/scripts/verify-package.mjs
@@ -0,0 +1,267 @@
+#!/usr/bin/env node
+
+import { createHash } from "node:crypto";
+import { createReadStream } from "node:fs";
+import { open, readFile, stat } from "node:fs/promises";
+import { dirname, relative, resolve } from "node:path";
+
+const repositoryRoot = resolve(import.meta.dirname, "..");
+const manifestPath = resolveManifestPath(process.argv.slice(2));
+
+try {
+ const result = await verifyPackage(manifestPath);
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+} catch (error) {
+ process.stderr.write(
+ `${JSON.stringify(
+ {
+ ok: false,
+ manifest: manifestPath,
+ error: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ process.exitCode = 1;
+}
+
+async function verifyPackage(currentManifestPath) {
+ const manifest = JSON.parse(await readFile(currentManifestPath, "utf8"));
+ assert(manifest?.schema === "dicose-wgsl-package-v1", "Unsupported package schema");
+ assert(manifest.weights !== null && typeof manifest.weights === "object", "Manifest omits weights metadata");
+ assert(Array.isArray(manifest.tensors) && manifest.tensors.length > 0, "Manifest has no tensors");
+ assert(manifest.weights.dtype === "f16", "Package weight dtype must be f16");
+ assert(manifest.weights.endianness === "little", "Package weights must be little-endian");
+ assertPositiveInteger(manifest.weights.alignment, "weights.alignment");
+ assertPositiveInteger(manifest.weights.byteLength, "weights.byteLength");
+ assertSha256(manifest.weights.sha256, "weights.sha256");
+ assertSafeRelativeFile(manifest.weights.file, "weights.file");
+
+ const weightsPath = resolve(dirname(currentManifestPath), manifest.weights.file);
+ const weightsInfo = await stat(weightsPath);
+ assert(
+ weightsInfo.size === manifest.weights.byteLength,
+ `Weight byte length mismatch: manifest=${manifest.weights.byteLength}, file=${weightsInfo.size}`,
+ );
+ const fileSha256 = await sha256File(weightsPath);
+ assert(
+ fileSha256 === manifest.weights.sha256,
+ `Weight SHA-256 mismatch: manifest=${manifest.weights.sha256}, file=${fileSha256}`,
+ );
+
+ const names = new Set();
+ const payloads = new Map();
+ let logicalElementCount = 0;
+ for (const tensor of manifest.tensors) {
+ validateTensor(tensor, manifest.weights, names);
+ const elementCount = tensor.shape.reduce((total, dimension) => total * dimension, 1);
+ logicalElementCount += elementCount;
+ const key = `${tensor.offset}:${tensor.byteLength}`;
+ const payload = payloads.get(key) ?? {
+ offset: tensor.offset,
+ byteLength: tensor.byteLength,
+ sha256: tensor.sha256,
+ tensors: [],
+ };
+ assert(
+ payload.sha256 === tensor.sha256,
+ `Aliased payload at ${key} has conflicting tensor SHA-256 values`,
+ );
+ payload.tensors.push(tensor.name);
+ payloads.set(key, payload);
+ }
+ assert(
+ Number.isSafeInteger(logicalElementCount),
+ "Logical tensor element count exceeds JavaScript safe integer precision",
+ );
+
+ const uniquePayloads = [...payloads.values()].sort((left, right) => left.offset - right.offset);
+ let previousEnd = 0;
+ for (const payload of uniquePayloads) {
+ assert(
+ payload.offset >= previousEnd,
+ `Payload ${payload.tensors[0]} overlaps a preceding payload`,
+ );
+ const end = payload.offset + payload.byteLength;
+ assert(end <= manifest.weights.byteLength, `Payload ${payload.tensors[0]} exceeds weights file`);
+ previousEnd = end;
+ }
+
+ const handle = await open(weightsPath, "r");
+ try {
+ for (const payload of uniquePayloads) {
+ const actualSha256 = await sha256Range(handle, payload.offset, payload.byteLength);
+ assert(
+ actualSha256 === payload.sha256,
+ `Tensor payload SHA-256 mismatch for ${payload.tensors.join(", ")}: expected ${payload.sha256}, got ${actualSha256}`,
+ );
+ }
+ } finally {
+ await handle.close();
+ }
+
+ validateAggregateMetadata(manifest, {
+ logicalElementCount,
+ uniquePayloads,
+ });
+ return {
+ ok: true,
+ schema: manifest.schema,
+ manifest: relative(repositoryRoot, currentManifestPath) || ".",
+ weights: {
+ file: relative(repositoryRoot, weightsPath),
+ byteLength: weightsInfo.size,
+ sha256: fileSha256,
+ },
+ tensors: {
+ logicalCount: manifest.tensors.length,
+ uniquePayloadCount: uniquePayloads.length,
+ logicalElementCount,
+ uniquePayloadBytes: uniquePayloads.reduce((total, payload) => total + payload.byteLength, 0),
+ },
+ };
+}
+
+function validateTensor(tensor, weights, names) {
+ assert(tensor !== null && typeof tensor === "object", "Tensor metadata must be an object");
+ assert(typeof tensor.name === "string" && tensor.name.length > 0, "Tensor is missing a name");
+ assert(!names.has(tensor.name), `Duplicate tensor name: ${tensor.name}`);
+ names.add(tensor.name);
+ assert(tensor.dtype === "f16", `Tensor ${tensor.name} has unsupported dtype`);
+ assert(
+ tensor.layout === "row-major" || tensor.layout === "linear-in-out" ||
+ tensor.layout === "linear-tile-n128-k32" ||
+ tensor.layout === "linear-tile-n256-k32" || tensor.layout === "conv-oihw",
+ `Tensor ${tensor.name} has unsupported layout`,
+ );
+ assert(Array.isArray(tensor.shape) && tensor.shape.length > 0, `Tensor ${tensor.name} has no shape`);
+ for (const dimension of tensor.shape) {
+ assertPositiveInteger(dimension, `Tensor ${tensor.name} shape dimension`);
+ }
+ const elementCount = tensor.shape.reduce((total, dimension) => total * dimension, 1);
+ assert(Number.isSafeInteger(elementCount), `Tensor ${tensor.name} has an unsafe element count`);
+ assertNonNegativeInteger(tensor.offset, `Tensor ${tensor.name} offset`);
+ assert(
+ tensor.offset % weights.alignment === 0,
+ `Tensor ${tensor.name} offset is not ${weights.alignment}-byte aligned`,
+ );
+ assertPositiveInteger(tensor.byteLength, `Tensor ${tensor.name} byteLength`);
+ assert(
+ tensor.byteLength === elementCount * 2,
+ `Tensor ${tensor.name} f16 byteLength does not match its shape`,
+ );
+ assertSha256(tensor.sha256, `Tensor ${tensor.name} sha256`);
+ assert(
+ tensor.offset + tensor.byteLength <= weights.byteLength,
+ `Tensor ${tensor.name} exceeds the weights file`,
+ );
+}
+
+function validateAggregateMetadata(manifest, { logicalElementCount, uniquePayloads }) {
+ const weights = manifest.weights;
+ assert(
+ weights.logicalTensorCount === manifest.tensors.length,
+ "weights.logicalTensorCount does not match tensors.length",
+ );
+ assert(
+ weights.uniqueTensorCount === uniquePayloads.length,
+ "weights.uniqueTensorCount does not match unique payload ranges",
+ );
+ assert(
+ weights.logicalElementCount === logicalElementCount,
+ "weights.logicalElementCount does not match tensor shapes",
+ );
+ const uniquePayloadBytes = uniquePayloads.reduce(
+ (total, payload) => total + payload.byteLength,
+ 0,
+ );
+ assert(
+ weights.uniquePayloadBytes === uniquePayloadBytes,
+ "weights.uniquePayloadBytes does not match unique tensor ranges",
+ );
+
+ if (Array.isArray(manifest.components)) {
+ for (const component of manifest.components) {
+ assert(typeof component.namespace === "string", "Component is missing a namespace");
+ const tensors = manifest.tensors.filter((tensor) =>
+ tensor.name.startsWith(`${component.namespace}.`),
+ );
+ const elementCount = tensors.reduce(
+ (total, tensor) => total + tensor.shape.reduce((size, dimension) => size * dimension, 1),
+ 0,
+ );
+ assert(
+ component.expectedTensorCount === tensors.length,
+ `Component ${component.namespace} tensor count does not match its manifest`,
+ );
+ assert(
+ component.expectedElementCount === elementCount,
+ `Component ${component.namespace} element count does not match its manifest`,
+ );
+ }
+ }
+}
+
+async function sha256File(path) {
+ const hash = createHash("sha256");
+ await new Promise((resolvePromise, rejectPromise) => {
+ const source = createReadStream(path);
+ source.on("data", (chunk) => hash.update(chunk));
+ source.once("end", resolvePromise);
+ source.once("error", rejectPromise);
+ });
+ return hash.digest("hex");
+}
+
+async function sha256Range(handle, offset, byteLength) {
+ const hash = createHash("sha256");
+ const buffer = Buffer.allocUnsafe(Math.min(byteLength, 4 * 1024 * 1024));
+ let position = offset;
+ let remaining = byteLength;
+ while (remaining > 0) {
+ const requested = Math.min(buffer.byteLength, remaining);
+ const { bytesRead } = await handle.read(buffer, 0, requested, position);
+ assert(bytesRead === requested, `Unexpected EOF at byte ${position}`);
+ hash.update(buffer.subarray(0, bytesRead));
+ position += bytesRead;
+ remaining -= bytesRead;
+ }
+ return hash.digest("hex");
+}
+
+function resolveManifestPath(argumentsList) {
+ if (argumentsList.length === 0) {
+ return joinPublicManifest();
+ }
+ if (argumentsList.length === 2 && argumentsList[0] === "--manifest") {
+ return resolve(repositoryRoot, argumentsList[1]);
+ }
+ throw new Error("Usage: node scripts/verify-package.mjs [--manifest public/model/manifest.json]");
+}
+
+function joinPublicManifest() {
+ return resolve(repositoryRoot, "public/model/manifest.json");
+}
+
+function assertSafeRelativeFile(value, label) {
+ assert(typeof value === "string" && value.length > 0, `${label} must be a non-empty path`);
+ assert(!value.startsWith("/") && !value.includes("\\"), `${label} must be a relative POSIX path`);
+ assert(!value.split("/").includes(".."), `${label} may not traverse out of the package`);
+}
+
+function assertSha256(value, label) {
+ assert(typeof value === "string" && /^[a-f0-9]{64}$/.test(value), `${label} must be a lowercase SHA-256`);
+}
+
+function assertNonNegativeInteger(value, label) {
+ assert(Number.isSafeInteger(value) && value >= 0, `${label} must be a non-negative safe integer`);
+}
+
+function assertPositiveInteger(value, label) {
+ assert(Number.isSafeInteger(value) && value > 0, `${label} must be a positive safe integer`);
+}
+
+function assert(condition, message) {
+ if (!condition) throw new Error(message);
+}
diff --git a/packages/dicose/src/api.ts b/packages/dicose/src/api.ts
new file mode 100644
index 0000000..343ce8d
--- /dev/null
+++ b/packages/dicose/src/api.ts
@@ -0,0 +1,294 @@
+import { decodeAudioBlob, type StereoPcm } from "./runtime/audio.js";
+import {
+ DICOSE_STEM_NAMES,
+ isDiCoSeWorkerEvent,
+ type DiCoSeProgress,
+ type DiCoSeStemName,
+ type DiCoSeWorkerError,
+ type DiCoSeWorkerInitOptions,
+ type DiCoSeWorkerResult,
+ type DiCoSeWorkerSeparateOptions,
+} from "./worker-protocol.js";
+
+export interface DiCoSeClientOptions extends DiCoSeWorkerInitOptions {
+ /** Invoked from the page thread for loading and inference progress. */
+ readonly onProgress?: (progress: DiCoSeProgress) => void;
+ /**
+ * Host-bundler seam: supply the dedicated worker instead of the default
+ * `new Worker(new URL("./worker.ts", import.meta.url))`, whose relative URL
+ * does not survive every consumer's bundling of a prebuilt dist. The worker
+ * must run this package's worker entry (`dicose-wgsl/worker`).
+ */
+ readonly createWorker?: () => Worker;
+}
+
+export interface DiCoSeSeparateOptions extends DiCoSeWorkerSeparateOptions {
+ /** Overrides the progress callback supplied when the worker was created. */
+ readonly onProgress?: (progress: DiCoSeProgress) => void;
+}
+
+export interface DiCoSeSeparation {
+ readonly outputMode: DiCoSeWorkerResult["outputMode"];
+ readonly stems: Readonly>;
+ /** Decoded input mixture minus vocals; derived after native-timeline restoration. */
+ readonly instrumental: StereoPcm;
+ readonly timing: Readonly>;
+ readonly diagnostics: DiCoSeWorkerResult["diagnostics"];
+}
+
+interface PendingRequest {
+ readonly resolve: (value: T) => void;
+ readonly reject: (reason: Error) => void;
+ readonly onProgress?: (progress: DiCoSeProgress) => void;
+}
+
+/**
+ * A page-side facade for the dedicated WebGPU worker. The weight package,
+ * device, and model execution stay off the UI thread; only decoder work and
+ * result ownership cross the boundary.
+ */
+export class DiCoSeWorkerClient {
+ private readonly worker: Worker;
+ private readonly pending = new Map>();
+ private nextId = 1;
+ private initialized: Promise | undefined;
+ private disposed = false;
+
+ constructor(private readonly options: DiCoSeClientOptions = {}) {
+ this.worker =
+ options.createWorker?.() ??
+ new Worker(new URL("./worker.ts", import.meta.url), {
+ type: "module",
+ name: "dicose-webgpu",
+ });
+ this.worker.addEventListener("message", this.handleMessage);
+ this.worker.addEventListener("messageerror", this.handleMessageError);
+ this.worker.addEventListener("error", this.handleWorkerError);
+ }
+
+ /** Create the WebGPU device and stream the model package once. */
+ async initialize(): Promise {
+ this.requireAlive();
+ if (this.initialized === undefined) {
+ const manifestUrl = resolveOptionalUrl(this.options.manifestUrl);
+ const attentionKernel = this.options.attentionKernel;
+ this.initialized = this.request(
+ "initialize",
+ {
+ options: {
+ ...(manifestUrl === undefined ? {} : { manifestUrl }),
+ ...(attentionKernel === undefined ? {} : { attentionKernel }),
+ },
+ },
+ [],
+ this.options.onProgress,
+ );
+ }
+ return await this.initialized;
+ }
+
+ /** Decode a browser-supported audio blob and restore all outputs to its native timeline. */
+ async separateAudio(
+ source: Blob | ArrayBuffer,
+ options: DiCoSeSeparateOptions = {},
+ ): Promise {
+ this.requireAlive();
+ const blob = source instanceof Blob ? source : new Blob([source]);
+ const pcm = await decodeAudioBlob(blob, { targetSampleRate: "source" });
+ return await this.separatePcm(pcm, options);
+ }
+
+ /**
+ * Separate already-decoded stereo PCM. Input channel data are copied before
+ * transfer, so callers retain ownership of their original arrays.
+ */
+ async separatePcm(
+ pcm: StereoPcm,
+ options: DiCoSeSeparateOptions = {},
+ ): Promise {
+ this.requireAlive();
+ validatePcm(pcm);
+ await this.initialize();
+ const left = pcm.left.slice();
+ const right = pcm.right.slice();
+ const raw = await this.request(
+ "separate",
+ {
+ pcm: {
+ sampleRate: pcm.sampleRate,
+ length: pcm.length,
+ left: left.buffer,
+ right: right.buffer,
+ },
+ options: {
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
+ ...(options.outputMode === undefined ? {} : { outputMode: options.outputMode }),
+ },
+ },
+ [left.buffer, right.buffer],
+ options.onProgress ?? this.options.onProgress,
+ );
+ return materializeResult(raw);
+ }
+
+ /** Release the worker's GPU buffers and terminate its isolated thread. */
+ async dispose(): Promise {
+ if (this.disposed) return;
+ this.disposed = true;
+ try {
+ await this.request("dispose", {}, [], undefined, true);
+ } finally {
+ this.worker.removeEventListener("message", this.handleMessage);
+ this.worker.removeEventListener("messageerror", this.handleMessageError);
+ this.worker.removeEventListener("error", this.handleWorkerError);
+ this.worker.terminate();
+ this.rejectAll(new Error("DiCoSe worker was disposed"));
+ }
+ }
+
+ private request(
+ type: "initialize" | "separate" | "dispose",
+ payload: object,
+ transfer: Transferable[],
+ onProgress: ((progress: DiCoSeProgress) => void) | undefined,
+ allowDisposed = false,
+ ): Promise {
+ if (!allowDisposed) this.requireAlive();
+ const id = this.nextId;
+ this.nextId += 1;
+ return new Promise((resolve, reject) => {
+ this.pending.set(id, {
+ resolve: resolve as (value: unknown) => void,
+ reject,
+ ...(onProgress === undefined ? {} : { onProgress }),
+ });
+ try {
+ this.worker.postMessage({ type, id, ...payload }, transfer);
+ } catch (error) {
+ this.pending.delete(id);
+ reject(error instanceof Error ? error : new Error(String(error)));
+ }
+ });
+ }
+
+ private readonly handleMessage = (event: MessageEvent): void => {
+ if (!isDiCoSeWorkerEvent(event.data)) return;
+ const message = event.data;
+ const pending = this.pending.get(message.id);
+ if (pending === undefined) return;
+ if (message.type === "progress") {
+ pending.onProgress?.(message.progress);
+ return;
+ }
+ this.pending.delete(message.id);
+ switch (message.type) {
+ case "initialized":
+ case "disposed":
+ pending.resolve(undefined);
+ break;
+ case "result":
+ pending.resolve(message.result);
+ break;
+ case "error":
+ pending.reject(workerError(message.error));
+ break;
+ default:
+ assertNever(message);
+ }
+ };
+
+ private readonly handleMessageError = (): void => {
+ this.rejectAll(new Error("DiCoSe worker sent an unreadable message"));
+ };
+
+ private readonly handleWorkerError = (event: ErrorEvent): void => {
+ this.rejectAll(new Error(event.message || "DiCoSe worker failed"));
+ };
+
+ private rejectAll(error: Error): void {
+ for (const pending of this.pending.values()) pending.reject(error);
+ this.pending.clear();
+ }
+
+ private requireAlive(): void {
+ if (this.disposed) throw new Error("DiCoSe worker client has been disposed");
+ }
+}
+
+function resolveOptionalUrl(value: string | undefined): string | undefined {
+ return value === undefined ? undefined : new URL(value, globalThis.location.href).href;
+}
+
+function materializeResult(result: DiCoSeWorkerResult): DiCoSeSeparation {
+ const stems = {} as Record;
+ for (const name of DICOSE_STEM_NAMES) {
+ const stem = result.stems[name];
+ if (stem === undefined) throw new Error(`DiCoSe worker omitted the ${name} stem`);
+ const left = new Float32Array(stem.left);
+ const right = new Float32Array(stem.right);
+ if (left.length !== stem.length || right.length !== stem.length) {
+ throw new Error(`DiCoSe worker returned malformed ${name} PCM`);
+ }
+ stems[name] = makeStereoPcm(stem.sampleRate, left, right);
+ }
+ const instrumental = materializePcm(result.instrumental, "instrumental");
+ if (
+ instrumental.sampleRate !== stems.vocals.sampleRate ||
+ instrumental.length !== stems.vocals.length
+ ) {
+ throw new Error("DiCoSe worker returned instrumental PCM on a different timeline");
+ }
+ return {
+ outputMode: result.outputMode,
+ stems,
+ instrumental,
+ timing: Object.freeze({ ...result.timing }),
+ diagnostics: result.diagnostics,
+ };
+}
+
+function materializePcm(
+ transfer: DiCoSeWorkerResult["instrumental"],
+ label: string,
+): StereoPcm {
+ const left = new Float32Array(transfer.left);
+ const right = new Float32Array(transfer.right);
+ if (left.length !== transfer.length || right.length !== transfer.length) {
+ throw new Error(`DiCoSe worker returned malformed ${label} PCM`);
+ }
+ return makeStereoPcm(transfer.sampleRate, left, right);
+}
+
+function makeStereoPcm(sampleRate: number, left: Float32Array, right: Float32Array): StereoPcm {
+ if (!Number.isFinite(sampleRate) || sampleRate <= 0 || !Number.isInteger(sampleRate)) {
+ throw new Error("DiCoSe worker returned an invalid PCM sample rate");
+ }
+ if (left.length !== right.length) throw new Error("DiCoSe worker returned unequal stereo channels");
+ return Object.freeze({
+ sampleRate,
+ length: left.length,
+ left,
+ right,
+ channels: [left, right] as const,
+ });
+}
+
+function validatePcm(pcm: StereoPcm): void {
+ if (pcm.left.length !== pcm.length || pcm.right.length !== pcm.length) {
+ throw new RangeError("Stereo PCM channel lengths must match the declared length");
+ }
+ if (!Number.isInteger(pcm.sampleRate) || pcm.sampleRate <= 0) {
+ throw new RangeError("Stereo PCM sample rate must be a positive integer");
+ }
+}
+
+function workerError(error: DiCoSeWorkerError): Error {
+ const result = new Error(error.message);
+ result.name = error.name;
+ if (error.stack !== undefined) result.stack = error.stack;
+ return result;
+}
+
+function assertNever(value: never): never {
+ throw new Error(`Unhandled DiCoSe worker event: ${JSON.stringify(value)}`);
+}
diff --git a/packages/dicose/src/demo.css b/packages/dicose/src/demo.css
new file mode 100644
index 0000000..9248bb3
--- /dev/null
+++ b/packages/dicose/src/demo.css
@@ -0,0 +1,940 @@
+:root {
+ color-scheme: dark;
+ font-family:
+ Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ --background: #090a0d;
+ --surface: #111217;
+ --surface-raised: #17191f;
+ --surface-soft: #1c1e25;
+ --line: rgba(255, 255, 255, 0.1);
+ --line-strong: rgba(255, 255, 255, 0.18);
+ --text: #f6f5f2;
+ --muted: #a8a8b1;
+ --dim: #737580;
+ --accent: #b8f451;
+ --accent-bright: #d0ff77;
+ --accent-ink: #111509;
+ --danger: #ff7a73;
+ --danger-surface: rgba(255, 84, 78, 0.1);
+ --shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ min-width: 320px;
+ scroll-behavior: smooth;
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ color: var(--text);
+ background:
+ radial-gradient(circle at 50% -12%, rgba(184, 244, 81, 0.11), transparent 31rem),
+ radial-gradient(circle at 7% 42%, rgba(92, 82, 255, 0.06), transparent 30rem),
+ var(--background);
+}
+
+body::before {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ pointer-events: none;
+ content: "";
+ opacity: 0.2;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
+ background-size: 64px 64px;
+ mask-image: linear-gradient(to bottom, black, transparent 75%);
+}
+
+button,
+input {
+ font: inherit;
+}
+
+button {
+ color: inherit;
+}
+
+a {
+ color: inherit;
+}
+
+.skip-link {
+ position: fixed;
+ top: 0.75rem;
+ left: 0.75rem;
+ z-index: 100;
+ padding: 0.65rem 0.9rem;
+ color: var(--accent-ink);
+ font-size: 0.85rem;
+ font-weight: 750;
+ text-decoration: none;
+ background: var(--accent);
+ border-radius: 0.55rem;
+ transform: translateY(-150%);
+ transition: transform 160ms ease;
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+.site-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: min(1120px, calc(100% - 3rem));
+ margin: 0 auto;
+ padding: 1.6rem 0;
+}
+
+.brand {
+ display: inline-flex;
+ gap: 0.7rem;
+ align-items: center;
+ font-size: 0.96rem;
+ font-weight: 760;
+ letter-spacing: -0.02em;
+ text-decoration: none;
+}
+
+.brand-mark {
+ display: flex;
+ gap: 2px;
+ align-items: center;
+ justify-content: center;
+ width: 1.75rem;
+ height: 1.75rem;
+ background: var(--accent);
+ border-radius: 0.48rem;
+ box-shadow: 0 0 22px rgba(184, 244, 81, 0.18);
+}
+
+.brand-mark span {
+ width: 2px;
+ background: var(--accent-ink);
+ border-radius: 999px;
+}
+
+.brand-mark span:nth-child(1) {
+ height: 6px;
+}
+
+.brand-mark span:nth-child(2),
+.brand-mark span:nth-child(4) {
+ height: 12px;
+}
+
+.brand-mark span:nth-child(3) {
+ height: 9px;
+}
+
+.runtime-badge {
+ display: inline-flex;
+ gap: 0.5rem;
+ align-items: center;
+ padding: 0.48rem 0.75rem;
+ color: #c8c8ce;
+ font-size: 0.72rem;
+ font-weight: 680;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--line);
+ border-radius: 999px;
+}
+
+.runtime-badge__dot {
+ width: 0.4rem;
+ height: 0.4rem;
+ background: var(--accent);
+ border-radius: 50%;
+ box-shadow: 0 0 0 4px rgba(184, 244, 81, 0.1);
+}
+
+main {
+ width: min(980px, calc(100% - 3rem));
+ margin: 0 auto;
+ padding: 5.4rem 0 3rem;
+}
+
+.hero {
+ max-width: 820px;
+ margin: 0 auto 4.25rem;
+ text-align: center;
+}
+
+.eyebrow,
+.section-kicker {
+ margin: 0 0 0.75rem;
+ color: var(--accent);
+ font-size: 0.7rem;
+ font-weight: 780;
+ letter-spacing: 0.13em;
+ text-transform: uppercase;
+}
+
+.hero h1 {
+ margin: 0;
+ font-size: clamp(3.35rem, 8vw, 6.8rem);
+ font-weight: 780;
+ letter-spacing: -0.075em;
+ line-height: 0.91;
+}
+
+.hero h1 span {
+ color: #8a8b93;
+}
+
+.hero__summary {
+ max-width: 600px;
+ margin: 1.9rem auto 0;
+ color: var(--muted);
+ font-size: clamp(1rem, 2.2vw, 1.16rem);
+ line-height: 1.7;
+}
+
+.hero__facts {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6rem 1.25rem;
+ justify-content: center;
+ margin: 1.6rem 0 0;
+ padding: 0;
+ color: #c3c3c9;
+ font-size: 0.76rem;
+ font-weight: 610;
+ list-style: none;
+}
+
+.hero__facts li {
+ display: inline-flex;
+ gap: 0.45rem;
+ align-items: center;
+}
+
+.hero__facts span {
+ color: var(--accent);
+ font-size: 0.5rem;
+}
+
+.workspace,
+.results {
+ position: relative;
+ padding: clamp(1.25rem, 4vw, 2.25rem);
+ overflow: hidden;
+ background: linear-gradient(145deg, rgba(24, 25, 31, 0.98), rgba(15, 16, 20, 0.98));
+ border: 1px solid var(--line);
+ border-radius: 1.35rem;
+ box-shadow: var(--shadow);
+}
+
+.workspace::before,
+.results::before {
+ position: absolute;
+ top: 0;
+ right: 8%;
+ left: 8%;
+ height: 1px;
+ content: "";
+ background: linear-gradient(90deg, transparent, rgba(184, 244, 81, 0.42), transparent);
+}
+
+.workspace__heading,
+.results__header {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-end;
+ justify-content: space-between;
+ margin-bottom: 1.45rem;
+}
+
+.workspace__heading h2,
+.results__header h2 {
+ margin: 0;
+ font-size: clamp(1.45rem, 3vw, 1.85rem);
+ letter-spacing: -0.045em;
+}
+
+.workspace__heading > p,
+.results__header > p {
+ margin: 0 0 0.15rem;
+ color: var(--dim);
+ font-size: 0.78rem;
+}
+
+.upload-picker {
+ position: relative;
+}
+
+.file-input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+}
+
+.drop-zone {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 1rem;
+ align-items: center;
+ min-height: 8.4rem;
+ padding: 1.3rem 1.45rem;
+ cursor: pointer;
+ background: rgba(255, 255, 255, 0.022);
+ border: 1px dashed rgba(255, 255, 255, 0.2);
+ border-radius: 1rem;
+ transition:
+ border-color 160ms ease,
+ background-color 160ms ease,
+ transform 160ms ease;
+}
+
+.drop-zone:hover,
+.drop-zone.is-dragging {
+ background: rgba(184, 244, 81, 0.045);
+ border-color: rgba(184, 244, 81, 0.65);
+}
+
+.drop-zone[aria-disabled="true"] {
+ cursor: wait;
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.drop-zone.is-dragging {
+ transform: scale(1.005);
+}
+
+.file-input:focus-visible + .drop-zone {
+ outline: 2px solid var(--accent);
+ outline-offset: 4px;
+}
+
+.upload-glyph {
+ display: flex;
+ gap: 3px;
+ align-items: center;
+ justify-content: center;
+ width: 3.2rem;
+ height: 3.2rem;
+ background: rgba(184, 244, 81, 0.1);
+ border: 1px solid rgba(184, 244, 81, 0.22);
+ border-radius: 0.85rem;
+}
+
+.upload-glyph span {
+ width: 3px;
+ background: var(--accent);
+ border-radius: 999px;
+}
+
+.upload-glyph span:nth-child(1),
+.upload-glyph span:nth-child(5) {
+ height: 0.6rem;
+}
+
+.upload-glyph span:nth-child(2),
+.upload-glyph span:nth-child(4) {
+ height: 1.25rem;
+}
+
+.upload-glyph span:nth-child(3) {
+ height: 1.8rem;
+}
+
+.drop-zone__copy,
+.selected-file {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+}
+
+.drop-zone__copy strong,
+.selected-file strong {
+ font-size: 0.95rem;
+ letter-spacing: -0.015em;
+}
+
+.drop-zone__copy > span,
+.selected-file > span {
+ color: var(--dim);
+ font-size: 0.8rem;
+}
+
+.drop-zone__action {
+ color: var(--accent);
+ text-decoration: underline;
+ text-decoration-color: rgba(184, 244, 81, 0.4);
+ text-underline-offset: 0.2em;
+}
+
+.selected-file {
+ min-width: 12rem;
+ max-width: 18rem;
+ padding-left: 1.1rem;
+ text-align: right;
+ border-left: 1px solid var(--line);
+}
+
+.selected-file strong {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.drop-zone:not(.has-file) .selected-file {
+ opacity: 0.52;
+}
+
+.help-text {
+ margin: 0.65rem 0 0;
+ color: var(--dim);
+ font-size: 0.72rem;
+}
+
+.configuration-picker {
+ min-width: 0;
+ margin: 2.25rem 0 0;
+ padding: 0;
+ border: 0;
+}
+
+.configuration-picker legend {
+ display: block;
+ width: 100%;
+ margin: 0 0 1rem;
+ padding: 0;
+}
+
+.configuration-picker .section-kicker {
+ display: block;
+ margin-bottom: 0.4rem;
+}
+
+.legend-title {
+ color: #d7d7dc;
+ font-size: 0.92rem;
+ font-weight: 650;
+}
+
+.configuration-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+}
+
+.configuration-option {
+ position: relative;
+ min-width: 0;
+ cursor: pointer;
+}
+
+.configuration-option > input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+}
+
+.configuration-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 12rem;
+ padding: 1.05rem;
+ background: rgba(255, 255, 255, 0.025);
+ border: 1px solid var(--line);
+ border-radius: 0.9rem;
+ transition:
+ border-color 160ms ease,
+ background-color 160ms ease,
+ transform 160ms ease;
+}
+
+.configuration-option:hover .configuration-card {
+ background: rgba(255, 255, 255, 0.04);
+ border-color: var(--line-strong);
+ transform: translateY(-1px);
+}
+
+.configuration-option > input:focus-visible + .configuration-card {
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
+}
+
+.configuration-option > input:checked + .configuration-card {
+ background: linear-gradient(150deg, rgba(184, 244, 81, 0.095), rgba(184, 244, 81, 0.025));
+ border-color: rgba(184, 244, 81, 0.55);
+}
+
+.configuration-card__header {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.configuration-card__name {
+ font-size: 1.03rem;
+ font-weight: 730;
+ letter-spacing: -0.02em;
+}
+
+.configuration-card__tag {
+ padding: 0.3rem 0.45rem;
+ color: #c5c5cb;
+ font-size: 0.57rem;
+ font-weight: 720;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ background: rgba(255, 255, 255, 0.055);
+ border-radius: 999px;
+}
+
+.configuration-option > input:checked + .configuration-card .configuration-card__tag {
+ color: var(--accent-ink);
+ background: var(--accent);
+}
+
+.configuration-card__description {
+ margin-top: 1rem;
+ color: var(--muted);
+ font-size: 0.79rem;
+ line-height: 1.55;
+}
+
+.configuration-card__detail {
+ margin-top: auto;
+ padding-top: 1rem;
+ color: var(--dim);
+ font-size: 0.66rem;
+}
+
+.configuration-card__check {
+ position: absolute;
+ right: 0.8rem;
+ bottom: 0.8rem;
+ width: 0.78rem;
+ height: 0.78rem;
+ border: 1px solid var(--line-strong);
+ border-radius: 50%;
+}
+
+.configuration-option > input:checked + .configuration-card .configuration-card__check {
+ background: var(--accent);
+ border: 3px solid #24291c;
+ box-shadow: 0 0 0 1px var(--accent);
+}
+
+.run-panel {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 2rem;
+ padding-top: 1.4rem;
+ border-top: 1px solid var(--line);
+}
+
+.run-panel__status {
+ display: flex;
+ gap: 0.65rem;
+ align-items: center;
+ min-width: 0;
+}
+
+.run-panel__status p {
+ margin: 0;
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 0.78rem;
+ text-overflow: ellipsis;
+}
+
+.status-indicator {
+ flex: 0 0 auto;
+ width: 0.42rem;
+ height: 0.42rem;
+ background: var(--dim);
+ border-radius: 50%;
+}
+
+.workspace.is-running .status-indicator {
+ background: var(--accent);
+ box-shadow: 0 0 0 5px rgba(184, 244, 81, 0.1);
+ animation: status-pulse 1.5s ease-in-out infinite;
+}
+
+.workspace[data-status-tone="success"] .status-indicator {
+ background: var(--accent);
+ box-shadow: 0 0 0 5px rgba(184, 244, 81, 0.1);
+}
+
+.workspace[data-status-tone="danger"] .status-indicator {
+ background: var(--danger);
+ box-shadow: 0 0 0 5px rgba(255, 122, 115, 0.1);
+}
+
+#status[data-tone="success"] {
+ color: #d6f4a4;
+}
+
+#status[data-tone="danger"] {
+ color: #ffb0ac;
+}
+
+.run-button {
+ display: inline-flex;
+ flex: 0 0 auto;
+ gap: 1.5rem;
+ align-items: center;
+ justify-content: center;
+ min-width: 12.5rem;
+ min-height: 3.25rem;
+ padding: 0.75rem 1rem 0.75rem 1.25rem;
+ color: var(--accent-ink);
+ font-size: 0.84rem;
+ font-weight: 760;
+ cursor: pointer;
+ background: var(--accent);
+ border: 0;
+ border-radius: 0.75rem;
+ box-shadow: 0 12px 30px rgba(184, 244, 81, 0.13);
+ transition:
+ background-color 160ms ease,
+ box-shadow 160ms ease,
+ transform 160ms ease;
+}
+
+.run-button:hover:not(:disabled) {
+ background: var(--accent-bright);
+ box-shadow: 0 15px 38px rgba(184, 244, 81, 0.2);
+ transform: translateY(-1px);
+}
+
+.run-button:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.run-button:focus-visible {
+ outline: 2px solid white;
+ outline-offset: 3px;
+}
+
+.run-button:disabled {
+ color: #686b5c;
+ cursor: not-allowed;
+ background: #252821;
+ box-shadow: none;
+}
+
+.run-button__arrow {
+ display: grid;
+ width: 1.75rem;
+ height: 1.75rem;
+ place-items: center;
+ font-size: 1rem;
+ background: rgba(17, 21, 9, 0.12);
+ border-radius: 50%;
+}
+
+.progress {
+ position: relative;
+ height: 0.3rem;
+ margin-top: 1rem;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.07);
+ border-radius: 999px;
+}
+
+.progress__fill {
+ display: block;
+ width: 0;
+ height: 100%;
+ background: linear-gradient(90deg, #78bd34, var(--accent));
+ border-radius: inherit;
+ box-shadow: 0 0 14px rgba(184, 244, 81, 0.45);
+ transition: width 220ms ease;
+}
+
+.progress.is-indeterminate .progress__fill {
+ width: 38%;
+ animation: progress-slide 1.2s ease-in-out infinite;
+}
+
+.error-message {
+ margin: 1rem 0 0;
+ padding: 0.85rem 1rem;
+ color: #ffb0ac;
+ font-size: 0.8rem;
+ line-height: 1.5;
+ background: var(--danger-surface);
+ border: 1px solid rgba(255, 122, 115, 0.25);
+ border-radius: 0.7rem;
+}
+
+.results {
+ margin-top: 1.25rem;
+}
+
+.timing-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 0.6rem;
+ margin-bottom: 1rem;
+}
+
+.timing-card {
+ padding: 0.85rem 0.95rem;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid var(--line);
+ border-radius: 0.72rem;
+}
+
+.timing-card__label {
+ display: block;
+ margin-bottom: 0.35rem;
+ color: var(--dim);
+ font-size: 0.61rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.timing-card__value {
+ font-size: 0.98rem;
+ font-weight: 680;
+}
+
+.stems-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+}
+
+.stem-card {
+ min-width: 0;
+ padding: 1.05rem;
+ background: rgba(255, 255, 255, 0.028);
+ border: 1px solid var(--line);
+ border-radius: 0.9rem;
+}
+
+.stem-card--instrumental {
+ grid-column: 1 / -1;
+}
+
+.stem-card__header {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.9rem;
+}
+
+.stem-card__name {
+ margin: 0;
+ font-size: 0.92rem;
+ letter-spacing: -0.015em;
+ text-transform: capitalize;
+}
+
+.stem-card__meta {
+ color: var(--dim);
+ font-size: 0.66rem;
+}
+
+.stem-card audio {
+ width: 100%;
+ height: 2.3rem;
+}
+
+.stem-card__download {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 2.25rem;
+ margin-top: 0.8rem;
+ padding: 0.5rem 0.75rem;
+ color: #dfe0d7;
+ font-size: 0.7rem;
+ font-weight: 670;
+ text-decoration: none;
+ border: 1px solid var(--line-strong);
+ border-radius: 0.55rem;
+ transition:
+ color 160ms ease,
+ border-color 160ms ease,
+ background-color 160ms ease;
+}
+
+.stem-card__download:hover {
+ color: var(--accent);
+ background: rgba(184, 244, 81, 0.05);
+ border-color: rgba(184, 244, 81, 0.4);
+}
+
+.browser-note {
+ margin: 1.25rem 0 0;
+ color: #5f6068;
+ font-size: 0.68rem;
+ text-align: center;
+}
+
+.automation-output {
+ position: fixed !important;
+ width: 1px !important;
+ height: 1px !important;
+ margin: -1px !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+ clip: rect(0 0 0 0) !important;
+ clip-path: inset(50%) !important;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+@keyframes status-pulse {
+ 0%,
+ 100% {
+ opacity: 0.55;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes progress-slide {
+ from {
+ transform: translateX(-110%);
+ }
+ to {
+ transform: translateX(265%);
+ }
+}
+
+@media (max-width: 760px) {
+ .site-header,
+ main {
+ width: min(100% - 1.5rem, 980px);
+ }
+
+ main {
+ padding-top: 3.8rem;
+ }
+
+ .hero {
+ margin-bottom: 3rem;
+ }
+
+ .hero h1 {
+ font-size: clamp(3rem, 16vw, 5.2rem);
+ }
+
+ .drop-zone {
+ grid-template-columns: auto 1fr;
+ }
+
+ .selected-file {
+ grid-column: 1 / -1;
+ min-width: 0;
+ max-width: none;
+ padding: 0.85rem 0 0;
+ text-align: left;
+ border-top: 1px solid var(--line);
+ border-left: 0;
+ }
+
+ .configuration-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .configuration-card {
+ min-height: 9.5rem;
+ }
+
+ .timing-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 520px) {
+ .site-header {
+ padding-top: 1rem;
+ }
+
+ .runtime-badge {
+ font-size: 0.61rem;
+ }
+
+ main {
+ padding-top: 2.7rem;
+ }
+
+ .hero h1 {
+ letter-spacing: -0.065em;
+ }
+
+ .hero__facts {
+ gap: 0.6rem 0.9rem;
+ }
+
+ .workspace__heading,
+ .results__header {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .drop-zone {
+ align-items: start;
+ padding: 1.1rem;
+ }
+
+ .upload-glyph {
+ width: 2.7rem;
+ height: 2.7rem;
+ }
+
+ .run-panel {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .run-button {
+ width: 100%;
+ }
+
+ .stems-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/packages/dicose/src/demo.ts b/packages/dicose/src/demo.ts
new file mode 100644
index 0000000..a2c8657
--- /dev/null
+++ b/packages/dicose/src/demo.ts
@@ -0,0 +1,863 @@
+import "./demo.css";
+
+import {
+ DICOSE_STEM_NAMES,
+ DiCoSeWorkerClient,
+ type DiCoSeOutputMode,
+ type DiCoSeProgress,
+ type DiCoSeSeparation,
+ type DiCoSeStemName,
+ type StereoPcm,
+} from "./index.js";
+import { encodeStereoWav } from "./runtime/audio.js";
+
+export interface DiCoSeBrowserRunOptions {
+ /** Defaults to the bundled audio fixture served from Vite's public root. */
+ readonly audioUrl?: string;
+ /** Defaults to `/model/manifest.json`. */
+ readonly manifestUrl?: string;
+ /** Fixed CD-noise seed used for reproducible browser runs. */
+ readonly seed?: number;
+ /** Full one-step refinement remains the default. */
+ readonly outputMode?: DiCoSeOutputMode;
+}
+
+export interface DiCoSeStemSummary {
+ readonly sampleRate: number;
+ readonly samples: number;
+ readonly durationSeconds: number;
+ readonly peak: number;
+ readonly rms: number;
+ readonly finiteSamples: number;
+}
+
+export interface DiCoSeBrowserMetrics {
+ readonly audioUrl: string;
+ readonly inputBytes: number;
+ readonly inputDurationSeconds: number;
+ readonly outputDurationSeconds: number;
+ readonly outputMode: DiCoSeOutputMode;
+ readonly elapsedMs: number;
+ readonly timing: Readonly>;
+}
+
+export interface DiCoSeBrowserOutput {
+ readonly stems: Readonly>;
+ /** Decoded input mixture minus the restored vocal estimate. */
+ readonly instrumental: DiCoSeStemSummary;
+ readonly diagnostics: DiCoSeSeparation["diagnostics"];
+}
+
+export interface DiCoSeBrowserBenchmarkMetrics {
+ readonly mode: "benchmark";
+ readonly warmupRuns: number;
+ readonly measuredRuns: number;
+ /** End-to-end samples after warmups, in milliseconds. */
+ readonly samplesMs: readonly number[];
+ readonly aggregate: Readonly<{
+ readonly minMs: number;
+ readonly maxMs: number;
+ readonly meanMs: number;
+ readonly medianMs: number;
+ }>;
+ /** Per-stage model timing from the corresponding measured runs. */
+ readonly timingSamples: readonly Readonly>[];
+}
+
+export type DiCoSeBrowserRunResult =
+ | {
+ readonly ok: true;
+ readonly metrics: DiCoSeBrowserMetrics;
+ readonly output: DiCoSeBrowserOutput;
+ }
+ | {
+ readonly ok: false;
+ readonly metrics: Readonly>;
+ readonly error: Readonly<{ name: string; message: string; stack?: string }>;
+ };
+
+export type DiCoSeBrowserBenchmarkReport =
+ | {
+ readonly ok: true;
+ readonly metrics: DiCoSeBrowserBenchmarkMetrics;
+ }
+ | {
+ readonly ok: false;
+ readonly metrics: Readonly>;
+ readonly error: Readonly<{ name: string; message: string; stack?: string }>;
+ };
+
+export type DiCoSeBrowserReport = DiCoSeBrowserRunResult | DiCoSeBrowserBenchmarkReport;
+
+export interface DiCoSeBrowserHarness {
+ /**
+ * Run the bundled end-to-end workflow without UI input or file downloads.
+ * It always resolves to a serializable success/failure object so automation
+ * can inspect `#result` or await this promise directly.
+ */
+ run(options?: DiCoSeBrowserRunOptions): Promise;
+ /** Filled automatically by `?mode=benchmark`; safe for automation to poll. */
+ report?: DiCoSeBrowserReport;
+ /** Alias retained for simple browser harness polling. */
+ lastReport?: DiCoSeBrowserReport;
+}
+
+declare global {
+ interface Window {
+ __DICOSE_BROWSER__?: DiCoSeBrowserHarness;
+ __DICOSE_BROWSER_REPORT__?: DiCoSeBrowserReport;
+ }
+}
+
+const DEFAULT_AUDIO_URL = "/Mixture_audio_1.wav";
+const DEFAULT_MANIFEST_URL = "/model/manifest.json";
+const DEMO_OUTPUT_NAMES = [...DICOSE_STEM_NAMES, "instrumental"] as const;
+type DemoOutputName = (typeof DEMO_OUTPUT_NAMES)[number];
+
+type DemoConfiguration = "full" | "fast";
+
+interface DemoConfigurationSpec {
+ readonly label: string;
+ readonly outputMode: DiCoSeOutputMode;
+ readonly fileSuffix: string;
+}
+
+const DEMO_CONFIGURATIONS: Readonly> = {
+ full: { label: "Full", outputMode: "refined", fileSuffix: "full" },
+ fast: { label: "Fast", outputMode: "deterministic", fileSuffix: "fast" },
+};
+
+const statusNode = document.querySelector("#status");
+const resultNode = document.querySelector("#result");
+const fileInput = requiredElement("#file-input");
+const dropZone = requiredElement("#drop-zone");
+const fileNameNode = requiredElement("#file-name");
+const fileMetaNode = requiredElement("#file-meta");
+const workspaceNode = requiredElement("#workspace");
+const runButton = requiredElement("#run-button");
+const runButtonLabel = requiredElement("#run-button-label");
+const progressNode = requiredElement("#progress");
+const progressFillNode = requiredElement("#progress-fill");
+const resultsNode = requiredElement("#results");
+const timingGridNode = requiredElement("#timing-grid");
+const stemsGridNode = requiredElement("#stems-grid");
+const errorNode = requiredElement("#error");
+
+let client: DiCoSeWorkerClient | undefined;
+let clientConfiguration: string | undefined;
+let activeRun: Promise | undefined;
+let selectedFile: File | undefined;
+let interactiveBusy = false;
+let outputUrls: string[] = [];
+
+const harness: DiCoSeBrowserHarness = {
+ async run(options: DiCoSeBrowserRunOptions = {}): Promise {
+ return await executeRun(options, true);
+ },
+};
+
+window.__DICOSE_BROWSER__ = harness;
+
+const query = new URLSearchParams(window.location.search);
+const automated = query.get("autorun") === "1" || query.get("mode") === "benchmark";
+document.documentElement.dataset.runMode = automated ? "automation" : "interactive";
+if (automated) {
+ setStatus("Starting automated run…");
+ const options = autorunOptions(query);
+ if (query.get("mode") === "benchmark") {
+ void runConfiguredBenchmark(options, query);
+ } else {
+ void harness.run(options);
+ }
+} else {
+ initializeInteractiveDemo();
+}
+
+async function executeRun(
+ options: DiCoSeBrowserRunOptions,
+ publish: boolean,
+): Promise {
+ if (activeRun !== undefined) return await activeRun;
+ const run = performRun(options, publish);
+ activeRun = run;
+ try {
+ return await run;
+ } finally {
+ activeRun = undefined;
+ }
+}
+
+async function performRun(
+ options: DiCoSeBrowserRunOptions,
+ publish: boolean,
+): Promise {
+ const started = performance.now();
+ try {
+ const audioUrl = new URL(options.audioUrl ?? DEFAULT_AUDIO_URL, window.location.href).href;
+ setStatus("Fetching input audio…");
+ const response = await fetch(audioUrl, { cache: "no-store" });
+ if (!response.ok) throw new Error(`Could not fetch test audio: HTTP ${response.status}`);
+ const blob = await response.blob();
+ const separation = await separateBlob(blob, options);
+ const elapsedMs = performance.now() - started;
+ const result: DiCoSeBrowserRunResult = {
+ ok: true,
+ metrics: makeMetrics(audioUrl, blob.size, elapsedMs, separation),
+ output: {
+ stems: summarizeStems(separation),
+ instrumental: summarizeStem(separation.instrumental),
+ diagnostics: separation.diagnostics,
+ },
+ };
+ setStatus(`Completed in ${result.metrics.elapsedMs.toFixed(1)} ms.`, "success");
+ if (publish) publishResult(result);
+ return result;
+ } catch (error) {
+ await discardClient();
+ const result: DiCoSeBrowserRunResult = {
+ ok: false,
+ metrics: {},
+ error: serializeError(error),
+ };
+ setStatus(`Failed: ${result.error.message}`, "danger");
+ if (publish) publishResult(result);
+ return result;
+ }
+}
+
+async function separateBlob(
+ blob: Blob,
+ options: DiCoSeBrowserRunOptions,
+): Promise {
+ const manifestUrl = new URL(options.manifestUrl ?? DEFAULT_MANIFEST_URL, window.location.href).href;
+ const configuration = manifestUrl;
+ if (client !== undefined && clientConfiguration !== configuration) {
+ setStatus("Switching model configuration…");
+ await client.dispose();
+ client = undefined;
+ clientConfiguration = undefined;
+ }
+ if (client === undefined) {
+ client = new DiCoSeWorkerClient({
+ manifestUrl,
+ onProgress: reportProgress,
+ });
+ clientConfiguration = configuration;
+ }
+ setStatus("Decoding and resampling audio…");
+ return await client.separateAudio(blob, {
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
+ ...(options.outputMode === undefined ? {} : { outputMode: options.outputMode }),
+ });
+}
+
+function initializeInteractiveDemo(): void {
+ setStatus("Choose a WAV file to begin.");
+ setProgress(0);
+ updateRunButton();
+
+ fileInput.addEventListener("click", () => {
+ fileInput.value = "";
+ });
+ fileInput.addEventListener("change", () => {
+ const file = fileInput.files?.[0];
+ if (file !== undefined) selectLocalFile(file);
+ });
+
+ let dragDepth = 0;
+ dropZone.addEventListener("dragenter", (event) => {
+ event.preventDefault();
+ dragDepth += 1;
+ dropZone.classList.add("is-dragging");
+ });
+ dropZone.addEventListener("dragover", (event) => {
+ event.preventDefault();
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "copy";
+ });
+ dropZone.addEventListener("dragleave", () => {
+ dragDepth = Math.max(0, dragDepth - 1);
+ if (dragDepth === 0) dropZone.classList.remove("is-dragging");
+ });
+ dropZone.addEventListener("drop", (event) => {
+ event.preventDefault();
+ dragDepth = 0;
+ dropZone.classList.remove("is-dragging");
+ const file = event.dataTransfer?.files[0];
+ if (file !== undefined) selectLocalFile(file);
+ });
+
+ for (const input of document.querySelectorAll('input[name="configuration"]')) {
+ input.addEventListener("change", updateRunButton);
+ }
+ runButton.addEventListener("click", () => void runInteractiveSeparation());
+ window.addEventListener("pagehide", (event) => {
+ clearInteractiveResults();
+ if (event.persisted) return;
+ void discardClient();
+ });
+
+ if (navigator.gpu === undefined) {
+ showError("WebGPU is unavailable. Open this page in a current WebGPU-capable browser.");
+ runButton.disabled = true;
+ }
+}
+
+function selectLocalFile(file: File): void {
+ if (interactiveBusy) return;
+ clearError();
+ if (!isWavFile(file)) {
+ selectedFile = undefined;
+ fileInput.value = "";
+ fileNameNode.textContent = "No WAV selected";
+ fileMetaNode.textContent = "Choose a .wav file or drop it here.";
+ dropZone.classList.remove("has-file");
+ clearInteractiveResults();
+ setProgress(0);
+ setStatus("Choose a valid WAV file.", "danger");
+ showError("Please choose a WAV file. Other audio containers are not enabled in this demo.");
+ updateRunButton();
+ return;
+ }
+ if (file.size === 0) {
+ selectedFile = undefined;
+ fileInput.value = "";
+ dropZone.classList.remove("has-file");
+ fileNameNode.textContent = "No WAV selected";
+ fileMetaNode.textContent = "Choose a non-empty stereo or mono WAV file.";
+ clearInteractiveResults();
+ setProgress(0);
+ setStatus("Choose a non-empty WAV file.", "danger");
+ showError("The selected WAV file is empty.");
+ updateRunButton();
+ return;
+ }
+ selectedFile = file;
+ dropZone.classList.add("has-file");
+ fileNameNode.textContent = file.name;
+ fileMetaNode.textContent = `${formatBytes(file.size)} · Local file stays in this browser tab`;
+ setStatus("Ready to separate.");
+ setProgress(0);
+ clearInteractiveResults();
+ updateRunButton();
+}
+
+async function runInteractiveSeparation(): Promise {
+ const file = selectedFile;
+ if (file === undefined || interactiveBusy) return;
+ const configuration = selectedConfiguration();
+ const spec = DEMO_CONFIGURATIONS[configuration];
+ interactiveBusy = true;
+ clearError();
+ clearInteractiveResults();
+ setBusy(true);
+ setProgress(0.01);
+ setStatus(`Preparing ${spec.label.toLowerCase()} separation…`);
+ const started = performance.now();
+ try {
+ const separation = await separateBlob(file, {
+ outputMode: spec.outputMode,
+ });
+ const elapsedMs = performance.now() - started;
+ const result: Extract = {
+ ok: true,
+ metrics: makeMetrics(
+ `local:${file.name}`,
+ file.size,
+ elapsedMs,
+ separation,
+ ),
+ output: {
+ stems: summarizeStems(separation),
+ instrumental: summarizeStem(separation.instrumental),
+ diagnostics: separation.diagnostics,
+ },
+ };
+ publishResult(result);
+ renderInteractiveResults(file, configuration, separation, elapsedMs);
+ setProgress(1);
+ setStatus(`${spec.label} separation completed in ${formatElapsed(elapsedMs)}.`, "success");
+ } catch (error) {
+ clearInteractiveResults();
+ await discardClient();
+ const failure: DiCoSeBrowserRunResult = {
+ ok: false,
+ metrics: {},
+ error: serializeError(error),
+ };
+ publishResult(failure);
+ setProgress(0);
+ setStatus("Separation failed.", "danger");
+ showError(friendlyError(error));
+ } finally {
+ interactiveBusy = false;
+ setBusy(false);
+ }
+}
+
+function renderInteractiveResults(
+ file: File,
+ configuration: DemoConfiguration,
+ separation: DiCoSeSeparation,
+ elapsedMs: number,
+): void {
+ revokeOutputUrls();
+ timingGridNode.replaceChildren();
+ stemsGridNode.replaceChildren();
+ const durationSeconds = separation.stems.vocals.length / separation.stems.vocals.sampleRate;
+ const throughput = durationSeconds / (elapsedMs / 1_000);
+ const timingCards: readonly [string, string][] = [
+ ["Mode", DEMO_CONFIGURATIONS[configuration].label],
+ ["Wall time", formatElapsed(elapsedMs)],
+ ["Runtime total", formatElapsed(requireTiming(separation, "totalMs"))],
+ ["Audio", formatClock(durationSeconds)],
+ ["Throughput", `${throughput.toFixed(2)}× realtime`],
+ ["Prepare", formatElapsed(requireTiming(separation, "prepareMs"))],
+ ["Deterministic", formatElapsed(requireTiming(separation, "deterministicMs"))],
+ ["Mapping", formatElapsed(requireTiming(separation, "mappingMs"))],
+ ["Refinement", formatElapsed(requireTiming(separation, "refinementMs"))],
+ ["ISTFT", formatElapsed(requireTiming(separation, "istftMs"))],
+ ];
+ for (const [label, value] of timingCards) timingGridNode.append(createMetricCard(label, value));
+
+ const players: HTMLAudioElement[] = [];
+ const baseName = safeFileBase(file.name);
+ for (const name of DEMO_OUTPUT_NAMES) {
+ const pcm = outputPcm(separation, name);
+ const wav = encodeStereoWav(pcm);
+ const url = URL.createObjectURL(wav);
+ outputUrls.push(url);
+
+ const card = document.createElement("article");
+ card.className = name === "instrumental"
+ ? "stem-card stem-card--instrumental"
+ : "stem-card";
+ const header = document.createElement("div");
+ header.className = "stem-card__header";
+ const heading = document.createElement("h3");
+ heading.className = "stem-card__name";
+ heading.textContent = capitalize(name);
+ const stats = summarizeStem(pcm);
+ const detail = document.createElement("p");
+ detail.className = "stem-card__meta";
+ const derivation = name === "instrumental" ? " · mix − vocals" : "";
+ detail.textContent = `${formatClock(stats.durationSeconds)} · peak ${stats.peak.toFixed(4)} · RMS ${stats.rms.toFixed(4)}${derivation}`;
+ header.append(heading, detail);
+ const audio = document.createElement("audio");
+ audio.controls = true;
+ audio.preload = "metadata";
+ audio.src = url;
+ audio.setAttribute("aria-label", `Play ${name} output`);
+ audio.addEventListener("play", () => {
+ for (const player of players) {
+ if (player !== audio) player.pause();
+ }
+ });
+ players.push(audio);
+ const download = document.createElement("a");
+ download.className = "stem-card__download";
+ download.href = url;
+ download.download = `${baseName}-${name}-${DEMO_CONFIGURATIONS[configuration].fileSuffix}.wav`;
+ download.textContent = "Download WAV";
+ card.append(header, audio, download);
+ stemsGridNode.append(card);
+ }
+ resultsNode.hidden = false;
+ resultsNode.scrollIntoView({ behavior: "smooth", block: "start" });
+}
+
+function createMetricCard(label: string, value: string): HTMLElement {
+ const card = document.createElement("div");
+ card.className = "timing-card";
+ const valueNode = document.createElement("strong");
+ valueNode.className = "timing-card__value";
+ valueNode.textContent = value;
+ const labelNode = document.createElement("span");
+ labelNode.className = "timing-card__label";
+ labelNode.textContent = label;
+ card.append(labelNode, valueNode);
+ return card;
+}
+
+function clearInteractiveResults(): void {
+ timingGridNode.replaceChildren();
+ stemsGridNode.replaceChildren();
+ revokeOutputUrls();
+ resultsNode.hidden = true;
+}
+
+function revokeOutputUrls(): void {
+ for (const url of outputUrls) URL.revokeObjectURL(url);
+ outputUrls = [];
+}
+
+async function discardClient(): Promise {
+ const staleClient = client;
+ client = undefined;
+ clientConfiguration = undefined;
+ await staleClient?.dispose().catch(() => {});
+}
+
+function selectedConfiguration(): DemoConfiguration {
+ const selected = document.querySelector('input[name="configuration"]:checked');
+ const value = selected?.value;
+ if (value === "full" || value === "fast") return value;
+ throw new Error("Choose an inference configuration");
+}
+
+function setBusy(busy: boolean): void {
+ fileInput.disabled = busy;
+ workspaceNode.classList.toggle("is-running", busy);
+ dropZone.setAttribute("aria-disabled", String(busy));
+ dropZone.dataset.busy = String(busy);
+ for (const input of document.querySelectorAll('input[name="configuration"]')) {
+ input.disabled = busy;
+ }
+ updateRunButton();
+}
+
+function updateRunButton(): void {
+ const hasWebGpu = navigator.gpu !== undefined;
+ runButton.disabled = selectedFile === undefined || interactiveBusy || !hasWebGpu;
+ const spec = DEMO_CONFIGURATIONS[selectedConfiguration()];
+ runButtonLabel.textContent = interactiveBusy ? "Separating…" : `Separate with ${spec.label}`;
+}
+
+function reportProgress(progress: DiCoSeProgress): void {
+ setStatus(describeProgress(progress.phase, progress.detail));
+ const fraction = progressFraction(progress);
+ if (fraction !== undefined) setProgress(fraction);
+}
+
+function progressFraction(progress: DiCoSeProgress): number | undefined {
+ const completion = progress.completed !== undefined && progress.total !== undefined && progress.total > 0
+ ? Math.min(1, Math.max(0, progress.completed / progress.total))
+ : 0;
+ switch (progress.phase) {
+ case "initializing": return 0.02;
+ case "device": return 0.03 + completion * 0.04;
+ case "weights": return 0.07 + completion * 0.23;
+ case "separating": return 0.31;
+ case "chunk": return 0.32 + completion * 0.67;
+ case "stft": return 0.32 + completion * 0.04;
+ case "deterministic": return 0.36 + completion * 0.2;
+ case "mapping": return 0.57 + completion * 0.02;
+ case "refinement": return 0.59 + completion * 0.34;
+ case "istft": return 0.94 + completion * 0.05;
+ default: return undefined;
+ }
+}
+
+function setProgress(value: number): void {
+ const percent = Math.round(Math.min(1, Math.max(0, value)) * 100);
+ progressNode.hidden = percent === 0;
+ progressFillNode.style.width = `${percent}%`;
+ progressNode.setAttribute("aria-valuenow", String(percent));
+}
+
+function showError(message: string): void {
+ errorNode.textContent = message;
+ errorNode.hidden = false;
+}
+
+function clearError(): void {
+ errorNode.textContent = "";
+ errorNode.hidden = true;
+}
+
+function isWavFile(file: File): boolean {
+ const mime = file.type.toLowerCase();
+ return file.name.toLowerCase().endsWith(".wav") || [
+ "audio/wav",
+ "audio/x-wav",
+ "audio/wave",
+ "audio/vnd.wave",
+ ].includes(mime);
+}
+
+function safeFileBase(name: string): string {
+ const withoutExtension = name.replace(/\.wav$/i, "");
+ const safe = withoutExtension.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "");
+ return safe.slice(0, 80) || "dicose-output";
+}
+
+function formatBytes(bytes: number): string {
+ if (bytes < 1_024) return `${bytes} B`;
+ if (bytes < 1_048_576) return `${(bytes / 1_024).toFixed(1)} KB`;
+ if (bytes < 1_073_741_824) return `${(bytes / 1_048_576).toFixed(1)} MB`;
+ return `${(bytes / 1_073_741_824).toFixed(2)} GB`;
+}
+
+function formatElapsed(milliseconds: number): string {
+ return milliseconds < 1_000
+ ? `${milliseconds.toFixed(0)} ms`
+ : `${(milliseconds / 1_000).toFixed(2)} s`;
+}
+
+function formatClock(seconds: number): string {
+ const minutes = Math.floor(seconds / 60);
+ const remainder = seconds - minutes * 60;
+ return minutes === 0
+ ? `${remainder.toFixed(1)} s`
+ : `${minutes}:${remainder.toFixed(1).padStart(4, "0")}`;
+}
+
+function capitalize(value: string): string {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+function friendlyError(error: unknown): string {
+ const message = error instanceof Error ? error.message : String(error);
+ if (/out of memory|allocation|device lost/i.test(message)) {
+ return `${message} The browser GPU could not complete a fixed-size DiCoSe model chunk.`;
+ }
+ return message;
+}
+
+function requireTiming(separation: DiCoSeSeparation, name: string): number {
+ const value = separation.timing[name];
+ return value !== undefined && Number.isFinite(value) ? value : 0;
+}
+
+function makeMetrics(
+ audioUrl: string,
+ inputBytes: number,
+ elapsedMs: number,
+ separation: DiCoSeSeparation,
+): DiCoSeBrowserMetrics {
+ const vocals = separation.stems.vocals;
+ return {
+ audioUrl,
+ inputBytes,
+ inputDurationSeconds: vocals.length / vocals.sampleRate,
+ outputDurationSeconds: vocals.length / vocals.sampleRate,
+ outputMode: separation.outputMode,
+ elapsedMs,
+ timing: separation.timing,
+ };
+}
+
+function summarizeStems(separation: DiCoSeSeparation): Readonly> {
+ const summaries = {} as Record;
+ for (const name of DICOSE_STEM_NAMES) summaries[name] = summarizeStem(separation.stems[name]);
+ return summaries;
+}
+
+function outputPcm(separation: DiCoSeSeparation, name: DemoOutputName): StereoPcm {
+ return name === "instrumental" ? separation.instrumental : separation.stems[name];
+}
+
+function summarizeStem(pcm: StereoPcm): DiCoSeStemSummary {
+ let peak = 0;
+ let sumSquares = 0;
+ let finiteSamples = 0;
+ const channels = [pcm.left, pcm.right] as const;
+ for (const channel of channels) {
+ for (let index = 0; index < channel.length; index += 1) {
+ const value = channel[index]!;
+ if (!Number.isFinite(value)) continue;
+ finiteSamples += 1;
+ const magnitude = Math.abs(value);
+ if (magnitude > peak) peak = magnitude;
+ sumSquares += value * value;
+ }
+ }
+ return {
+ sampleRate: pcm.sampleRate,
+ samples: pcm.length,
+ durationSeconds: pcm.length / pcm.sampleRate,
+ peak,
+ rms: finiteSamples === 0 ? Number.NaN : Math.sqrt(sumSquares / finiteSamples),
+ finiteSamples,
+ };
+}
+
+function describeProgress(phase: string, detail: string | undefined): string {
+ return detail === undefined ? phase : `${phase}: ${detail}`;
+}
+
+function setStatus(value: string, tone: "neutral" | "success" | "danger" = "neutral"): void {
+ if (statusNode !== null) {
+ statusNode.textContent = value;
+ statusNode.dataset.tone = tone;
+ workspaceNode.dataset.statusTone = tone;
+ }
+}
+
+function setResult(value: DiCoSeBrowserRunResult | DiCoSeBrowserBenchmarkReport): void {
+ if (resultNode !== null) resultNode.textContent = JSON.stringify(value, null, 2);
+}
+
+function serializeError(error: unknown): { readonly name: string; readonly message: string; readonly stack?: string } {
+ if (error instanceof Error) {
+ return {
+ name: error.name,
+ message: error.message,
+ ...(error.stack === undefined ? {} : { stack: error.stack }),
+ };
+ }
+ return { name: "Error", message: String(error) };
+}
+
+function autorunSeed(): number | undefined {
+ const value = new URLSearchParams(window.location.search).get("seed");
+ if (value === null) return undefined;
+ const parsed = Number(value);
+ if (!Number.isSafeInteger(parsed)) {
+ setStatus("Ignoring invalid ?seed value; using the runtime default.");
+ return undefined;
+ }
+ return parsed;
+}
+
+function autorunOptions(query: URLSearchParams): DiCoSeBrowserRunOptions {
+ const seed = autorunSeed();
+ const audioUrl = query.get("source") ?? query.get("audioUrl") ?? undefined;
+ const manifestUrl = query.get("manifestUrl") ?? undefined;
+ const outputMode = queryChoice(query.get("outputMode"), ["refined", "deterministic"] as const, "outputMode");
+ return {
+ ...(audioUrl === undefined ? {} : { audioUrl }),
+ ...(manifestUrl === undefined ? {} : { manifestUrl }),
+ ...(seed === undefined ? {} : { seed }),
+ ...(outputMode === undefined ? {} : { outputMode }),
+ };
+}
+
+function queryChoice(
+ value: string | null,
+ choices: T,
+ name: string,
+): T[number] | undefined {
+ if (value === null) return undefined;
+ if ((choices as readonly string[]).includes(value)) return value as T[number];
+ throw new RangeError(`${name} must be one of: ${choices.join(", ")}`);
+}
+
+async function runConfiguredBenchmark(
+ options: DiCoSeBrowserRunOptions,
+ query: URLSearchParams,
+): Promise {
+ try {
+ return await runBenchmark(
+ options,
+ benchmarkCount(query.get("warmupRuns"), 1, 0),
+ benchmarkCount(query.get("measuredRuns"), 1, 1),
+ );
+ } catch (error) {
+ const report: DiCoSeBrowserBenchmarkReport = {
+ ok: false,
+ metrics: {},
+ error: serializeError(error),
+ };
+ publishReport(report);
+ setStatus(`Benchmark failed: ${report.error.message}`);
+ setResult(report);
+ return report;
+ }
+}
+
+async function runBenchmark(
+ options: DiCoSeBrowserRunOptions,
+ warmupRuns: number,
+ measuredRuns: number,
+): Promise {
+ delete harness.report;
+ delete harness.lastReport;
+ delete window.__DICOSE_BROWSER_REPORT__;
+ if (resultNode !== null) resultNode.textContent = "";
+ try {
+ for (let index = 0; index < warmupRuns; index += 1) {
+ setStatus(`Warmup ${index + 1}/${warmupRuns}…`);
+ const result = await executeRun(options, false);
+ if (!result.ok) return publishBenchmarkFailure(result);
+ }
+ const measurements: DiCoSeBrowserMetrics[] = [];
+ for (let index = 0; index < measuredRuns; index += 1) {
+ setStatus(`Benchmark ${index + 1}/${measuredRuns}…`);
+ const result = await executeRun(options, false);
+ if (!result.ok) return publishBenchmarkFailure(result);
+ measurements.push(result.metrics);
+ }
+ const samplesMs = measurements.map((sample) => sample.elapsedMs);
+ const report: DiCoSeBrowserBenchmarkReport = {
+ ok: true,
+ metrics: {
+ mode: "benchmark",
+ warmupRuns,
+ measuredRuns,
+ samplesMs,
+ aggregate: summarizeSamples(samplesMs),
+ timingSamples: measurements.map((sample) => sample.timing),
+ },
+ };
+ publishReport(report);
+ setStatus(`Benchmark completed: median ${report.metrics.aggregate.medianMs.toFixed(1)} ms.`);
+ setResult(report);
+ return report;
+ } catch (error) {
+ const report: DiCoSeBrowserBenchmarkReport = {
+ ok: false,
+ metrics: {},
+ error: serializeError(error),
+ };
+ publishReport(report);
+ setStatus(`Benchmark failed: ${report.error.message}`);
+ setResult(report);
+ return report;
+ }
+}
+
+function publishBenchmarkFailure(result: Exclude): DiCoSeBrowserBenchmarkReport {
+ const report: DiCoSeBrowserBenchmarkReport = {
+ ok: false,
+ metrics: {},
+ error: result.error,
+ };
+ publishReport(report);
+ setStatus(`Benchmark failed: ${result.error.message}`);
+ setResult(report);
+ return report;
+}
+
+function publishResult(result: DiCoSeBrowserRunResult): void {
+ publishReport(result);
+ setResult(result);
+}
+
+function publishReport(report: DiCoSeBrowserReport): void {
+ harness.report = report;
+ harness.lastReport = report;
+ window.__DICOSE_BROWSER_REPORT__ = report;
+}
+
+function benchmarkCount(value: string | null, fallback: number, minimum: number): number {
+ if (value === null) return fallback;
+ const count = Number(value);
+ if (!Number.isSafeInteger(count) || count < minimum || count > 20) {
+ throw new RangeError(`Benchmark counts must be whole numbers between ${minimum} and 20`);
+ }
+ return count;
+}
+
+function summarizeSamples(samples: readonly number[]): DiCoSeBrowserBenchmarkMetrics["aggregate"] {
+ if (samples.length === 0) {
+ return { minMs: Number.NaN, maxMs: Number.NaN, meanMs: Number.NaN, medianMs: Number.NaN };
+ }
+ const sorted = [...samples].sort((left, right) => left - right);
+ const middle = Math.floor(sorted.length / 2);
+ const medianMs = sorted.length % 2 === 0
+ ? (sorted[middle - 1]! + sorted[middle]!) / 2
+ : sorted[middle]!;
+ const meanMs = sorted.reduce((sum, sample) => sum + sample, 0) / sorted.length;
+ return {
+ minMs: sorted[0]!,
+ maxMs: sorted[sorted.length - 1]!,
+ meanMs,
+ medianMs,
+ };
+}
+
+function requiredElement(selector: string): T {
+ const element = document.querySelector(selector);
+ if (element === null) throw new Error(`Demo page is missing ${selector}`);
+ return element;
+}
diff --git a/packages/dicose/src/index.ts b/packages/dicose/src/index.ts
new file mode 100644
index 0000000..9bf6db6
--- /dev/null
+++ b/packages/dicose/src/index.ts
@@ -0,0 +1,18 @@
+export {
+ DiCoSeWorkerClient,
+ type DiCoSeClientOptions,
+ type DiCoSeSeparation,
+ type DiCoSeSeparateOptions,
+} from "./api.js";
+export {
+ DICOSE_STEM_NAMES,
+ type DiCoSeOutputMode,
+ type DiCoSeProgress,
+ type DiCoSeStemName,
+} from "./worker-protocol.js";
+export {
+ DICOSE_SAMPLE_RATE,
+ decodeAudioBlob,
+ type StereoPcm,
+} from "./runtime/audio.js";
+export { checkSupport, type DiCoSeSupport } from "./webgpu/capabilities.js";
diff --git a/packages/dicose/src/model/manifest.ts b/packages/dicose/src/model/manifest.ts
new file mode 100644
index 0000000..c6a185f
--- /dev/null
+++ b/packages/dicose/src/model/manifest.ts
@@ -0,0 +1,111 @@
+/** Browser package metadata emitted by model/convert.py. */
+export interface DiCoSeTensorManifest {
+ readonly name: string;
+ readonly shape: readonly number[];
+ readonly offset: number;
+ readonly byteLength: number;
+ /** All model tensors are native IEEE 754 binary16 values. */
+ readonly dtype: "f16";
+ /** Linear weights keep logical [in_features, out_features] shape. */
+ readonly layout:
+ | "row-major"
+ | "linear-in-out"
+ | "linear-tile-n128-k32"
+ | "linear-tile-n256-k32"
+ | "conv-oihw";
+}
+
+export interface DiCoSeModelConfig {
+ readonly sampleRate: 44100;
+ readonly nFft: 2048;
+ readonly hopLength: 441;
+ readonly winLength: 2048;
+ readonly stereo: true;
+ readonly stems: readonly ["drums", "bass", "other", "vocals"];
+ readonly dim: 384;
+ readonly depth: 8;
+ readonly heads: 8;
+ readonly dimHead: 64;
+ readonly freqsPerBands: readonly number[];
+}
+
+export interface DiCoSeModelManifest {
+ readonly schema: "dicose-wgsl-package-v1";
+ readonly source: Readonly<{
+ readonly upstreamRevision: string;
+ readonly deterministicCheckpointSha256: string;
+ readonly cdCheckpointSha256: string;
+ }>;
+ readonly config: DiCoSeModelConfig;
+ readonly weights: Readonly<{
+ readonly file: string;
+ readonly byteLength: number;
+ readonly sha256: string;
+ }>;
+ readonly tensors: readonly DiCoSeTensorManifest[];
+}
+
+export function parseModelManifest(value: unknown): DiCoSeModelManifest {
+ if (value === null || typeof value !== "object") {
+ throw new TypeError("DiCoSe model manifest must be an object");
+ }
+ const manifest = value as Partial;
+ if (manifest.schema !== "dicose-wgsl-package-v1") {
+ throw new TypeError("Unsupported DiCoSe model package schema");
+ }
+ if (manifest.config === undefined || manifest.weights === undefined) {
+ throw new TypeError("DiCoSe model manifest omits config or weights");
+ }
+ if (
+ typeof manifest.weights.file !== "string" || manifest.weights.file.length === 0 ||
+ !Number.isSafeInteger(manifest.weights.byteLength) || manifest.weights.byteLength <= 0 ||
+ !/^[0-9a-f]{64}$/.test(manifest.weights.sha256)
+ ) {
+ throw new TypeError("DiCoSe model manifest has invalid weight metadata");
+ }
+ if (!Array.isArray(manifest.tensors) || manifest.tensors.length === 0) {
+ throw new TypeError("DiCoSe model manifest has no tensors");
+ }
+ const config = manifest.config;
+ if (
+ config.dim !== 384 || config.depth !== 8 || config.nFft !== 2048 ||
+ config.hopLength !== 441 || !Array.isArray(config.freqsPerBands)
+ ) {
+ throw new TypeError("DiCoSe model config is not the published BS-RoFormer profile");
+ }
+ const seen = new Set();
+ for (const tensor of manifest.tensors) validateTensor(tensor, seen);
+ return manifest as DiCoSeModelManifest;
+}
+
+function validateTensor(
+ tensor: unknown,
+ seen: Set,
+): asserts tensor is DiCoSeTensorManifest {
+ if (tensor === null || typeof tensor !== "object") {
+ throw new TypeError("DiCoSe tensor metadata must be an object");
+ }
+ const value = tensor as Partial;
+ if (typeof value.name !== "string" || value.name.length === 0) {
+ throw new TypeError("DiCoSe tensor is missing a name");
+ }
+ if (seen.has(value.name)) throw new TypeError(`Duplicate tensor ${value.name}`);
+ seen.add(value.name);
+ if (
+ value.dtype !== "f16" ||
+ (value.layout !== "row-major" && value.layout !== "linear-in-out" &&
+ value.layout !== "linear-tile-n128-k32" &&
+ value.layout !== "linear-tile-n256-k32" &&
+ value.layout !== "conv-oihw") ||
+ !Array.isArray(value.shape) || value.shape.length === 0 ||
+ value.shape.some((dimension) => !Number.isSafeInteger(dimension) || dimension <= 0) ||
+ typeof value.offset !== "number" || !Number.isSafeInteger(value.offset) || value.offset < 0 || value.offset % 256 !== 0 ||
+ typeof value.byteLength !== "number" || !Number.isSafeInteger(value.byteLength) || value.byteLength <= 0 || value.byteLength % 2 !== 0
+ ) {
+ throw new TypeError(`Invalid DiCoSe tensor metadata for ${value.name}`);
+ }
+ const elements = value.shape.reduce((product, dimension) => product * dimension, 1);
+ if (elements * 2 !== value.byteLength) {
+ throw new TypeError(`Byte length mismatch for DiCoSe tensor ${value.name}`);
+ }
+}
diff --git a/packages/dicose/src/model/package.ts b/packages/dicose/src/model/package.ts
new file mode 100644
index 0000000..081fcf2
--- /dev/null
+++ b/packages/dicose/src/model/package.ts
@@ -0,0 +1,137 @@
+import {
+ parseModelManifest,
+ type DiCoSeModelManifest,
+ type DiCoSeTensorManifest,
+} from "./manifest.js";
+
+export interface GpuWeightPackage {
+ readonly manifest: DiCoSeModelManifest;
+ readonly buffer: GPUBuffer;
+ tensor(name: string): GpuWeightTensor;
+ destroy(): void;
+}
+
+export interface GpuWeightTensor extends DiCoSeTensorManifest {
+ readonly buffer: GPUBuffer;
+}
+
+export interface LoadModelProgress {
+ readonly phase: "manifest" | "weights";
+ readonly loadedBytes: number;
+ readonly totalBytes: number;
+}
+
+/**
+ * Stream package bytes straight to GPU memory. This deliberately never holds
+ * the full ~625 MB inference package in JavaScript memory.
+ */
+export async function loadGpuWeightPackage(
+ device: GPUDevice,
+ manifestUrl: string | URL,
+ onProgress?: (event: LoadModelProgress) => void,
+): Promise {
+ const url = new URL(manifestUrl, globalThis.location?.href);
+ const response = await fetch(url, { cache: "no-store" });
+ if (!response.ok) throw new Error(`Could not load model manifest: HTTP ${response.status}`);
+ const manifest = parseModelManifest(await response.json());
+ onProgress?.({ phase: "manifest", loadedBytes: 1, totalBytes: 1 });
+ // The converter intentionally keeps a stable filename. Bind the browser
+ // cache key to the manifest digest so a freshly fetched manifest can never
+ // be paired with an older same-name package from HTTP cache.
+ const weightsUrl = new URL(manifest.weights.file, url);
+ weightsUrl.searchParams.set("sha256", manifest.weights.sha256);
+ const weightResponse = await fetch(weightsUrl, { cache: "force-cache" });
+ if (!weightResponse.ok || weightResponse.body === null) {
+ throw new Error(`Could not load DiCoSe weights: HTTP ${weightResponse.status}`);
+ }
+ const reportedLengthHeader = weightResponse.headers.get("content-length");
+ if (reportedLengthHeader !== null) {
+ const reportedLength = Number(reportedLengthHeader);
+ if (!Number.isFinite(reportedLength) || reportedLength !== manifest.weights.byteLength) {
+ throw new Error("DiCoSe weight file length differs from its manifest");
+ }
+ }
+ const buffer = device.createBuffer({
+ label: "dicose-f16-weights",
+ size: align4(manifest.weights.byteLength),
+ // CD mapping copies four stem-embedding rows into transient tensors before
+ // adding the time embedding. Without COPY_SRC Chrome invalidates the whole
+ // mapping encoder and the refiner continues with unusable FiLM vectors.
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ let loaded = 0;
+ let written = 0;
+ let tail = new Uint8Array(0);
+ const reader = weightResponse.body.getReader();
+ try {
+ for (;;) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ if (chunk.value === undefined) continue;
+ const bytes = chunk.value;
+ loaded += bytes.byteLength;
+ const joined = join(tail, bytes);
+ const writableLength = joined.byteLength - (joined.byteLength % 4);
+ if (writableLength > 0) {
+ const upload = new Uint8Array(writableLength);
+ upload.set(joined.subarray(0, writableLength));
+ device.queue.writeBuffer(buffer, written, upload.buffer);
+ written += writableLength;
+ }
+ tail = joined.slice(writableLength);
+ onProgress?.({
+ phase: "weights",
+ loadedBytes: loaded,
+ totalBytes: manifest.weights.byteLength,
+ });
+ }
+ } catch (error) {
+ buffer.destroy();
+ throw error;
+ }
+ if (loaded !== manifest.weights.byteLength) {
+ buffer.destroy();
+ throw new Error(`DiCoSe weight stream ended at ${loaded}, expected ${manifest.weights.byteLength}`);
+ }
+ if (tail.byteLength > 0) {
+ const padded = padToFour(tail);
+ device.queue.writeBuffer(buffer, written, padded.buffer);
+ }
+ const tensors = new Map(manifest.tensors.map((tensor) => [tensor.name, {
+ ...tensor,
+ buffer,
+ }] as const));
+ let destroyed = false;
+ return Object.freeze({
+ manifest,
+ buffer,
+ tensor(name: string): GpuWeightTensor {
+ const tensor = tensors.get(name);
+ if (tensor === undefined) throw new Error(`Model package does not contain ${name}`);
+ return tensor;
+ },
+ destroy(): void {
+ if (destroyed) return;
+ destroyed = true;
+ buffer.destroy();
+ },
+ });
+}
+
+function padToFour(bytes: Uint8Array): Uint8Array {
+ const padded = new Uint8Array(align4(bytes.byteLength));
+ padded.set(bytes);
+ return padded;
+}
+
+function join(left: Uint8Array, right: Uint8Array): Uint8Array {
+ if (left.byteLength === 0) return right;
+ const joined = new Uint8Array(left.byteLength + right.byteLength);
+ joined.set(left);
+ joined.set(right, left.byteLength);
+ return joined;
+}
+
+function align4(value: number): number {
+ return Math.ceil(value / 4) * 4;
+}
diff --git a/packages/dicose/src/runtime/audio.ts b/packages/dicose/src/runtime/audio.ts
new file mode 100644
index 0000000..7b69a78
--- /dev/null
+++ b/packages/dicose/src/runtime/audio.ts
@@ -0,0 +1,1096 @@
+/**
+ * Browser-side audio primitives used by the DiCoSe runtime.
+ *
+ * The model's front end is deliberately kept on the CPU: Web Audio handles
+ * container decoding, while the deterministic conversion/STFT code below
+ * gives WebGPU a stable, explicit tensor layout. Spectra are frame-major:
+ * `value[frame * binCount + frequencyBin]`.
+ */
+
+export const DICOSE_SAMPLE_RATE = 44_100 as const;
+export const DICOSE_STFT_N_FFT = 2_048 as const;
+export const DICOSE_STFT_HOP_LENGTH = 441 as const;
+
+const TWO_PI = Math.PI * 2;
+const UINT32_UNIT = 1 / 0x1_0000_0000;
+const SINC_LOWPASS_FILTER_WIDTH = 6;
+const SINC_ROLLOFF = 0.99;
+const MAX_SINC_KERNEL_COEFFICIENTS = 8_000_000;
+
+interface SincResampleKernel {
+ readonly sourceStride: number;
+ readonly targetPhases: number;
+ readonly width: number;
+ readonly tapCount: number;
+ readonly coefficients: Float32Array;
+ readonly firstNonzeroTaps: Uint32Array;
+ readonly lastNonzeroTaps: Uint32Array;
+}
+
+const sincResampleKernelCache = new Map();
+
+/** Structural subset of AudioBuffer, which also makes this easy to test in Node. */
+export interface AudioBufferLike {
+ readonly sampleRate: number;
+ readonly length: number;
+ readonly numberOfChannels: number;
+ getChannelData(channel: number): Float32Array;
+}
+
+/** Two independent, planar channels at one sample rate. */
+export interface StereoPcm {
+ readonly sampleRate: number;
+ readonly length: number;
+ readonly left: Float32Array;
+ readonly right: Float32Array;
+ readonly channels: readonly [Float32Array, Float32Array];
+}
+
+export interface DecodeAudioOptions {
+ /** Defaults to the DiCoSe model's 44.1 kHz source timeline. */
+ readonly targetSampleRate?: number | "source";
+ /** Linear exists only to replay the frozen pre-resampler model oracle. */
+ readonly resampler?: "sinc" | "linear";
+}
+
+/**
+ * Decode any browser-supported container (including WAV) and return a fresh,
+ * planar stereo PCM buffer. The temporary AudioContext is never started, so
+ * this does not require a user gesture or playback permission.
+ */
+export async function decodeAudioBlob(
+ blob: Blob,
+ options: DecodeAudioOptions = {},
+): Promise {
+ const AudioContextConstructor = globalThis.AudioContext;
+ if (AudioContextConstructor === undefined) {
+ throw new Error("Web Audio AudioContext is unavailable in this environment");
+ }
+
+ const encoded = await blob.arrayBuffer();
+ // decodeAudioData resamples to its context rate. For WAV we can retain the
+ // file's rate (including the bundled 22.05 kHz fixture) by inspecting its
+ // container header, then perform the canonical bandlimited conversion
+ // ourselves.
+ const encodedSampleRate = wavSampleRateFromRiff(encoded);
+ const context = encodedSampleRate === undefined
+ ? new AudioContextConstructor()
+ : new AudioContextConstructor({ sampleRate: encodedSampleRate });
+ try {
+ const decoded = await context.decodeAudioData(encoded);
+ return audioBufferToStereoPcm(decoded, options.targetSampleRate, options.resampler);
+ } finally {
+ // close() does not require an activated context and releases the decoder's
+ // resources promptly in a long-running browser session.
+ await context.close();
+ }
+}
+
+/** Alias that makes the canonical WAV input path self-documenting. */
+export const decodeWavBlob = decodeAudioBlob;
+
+/**
+ * Convert an AudioBuffer into independent stereo channels, duplicating mono
+ * inputs and using the first two channels of multichannel sources. Resampling
+ * matches torchaudio 2.0.2's default Hann-windowed sinc path rather than using
+ * a device-dependent Web Audio rendering pass.
+ */
+export function audioBufferToStereoPcm(
+ source: AudioBufferLike,
+ targetSampleRate: number | "source" = DICOSE_SAMPLE_RATE,
+ resampler: "sinc" | "linear" = "sinc",
+): StereoPcm {
+ validateSampleRate(source.sampleRate, "source.sampleRate");
+ const outputSampleRate = targetSampleRate === "source"
+ ? source.sampleRate
+ : targetSampleRate;
+ validateSampleRate(outputSampleRate, "targetSampleRate");
+ if (!Number.isSafeInteger(source.length) || source.length < 0) {
+ throw new RangeError("AudioBuffer length must be a non-negative safe integer");
+ }
+ if (!Number.isSafeInteger(source.numberOfChannels) || source.numberOfChannels < 1) {
+ throw new RangeError("AudioBuffer must contain at least one channel");
+ }
+
+ const sourceLeft = source.getChannelData(0);
+ if (sourceLeft.length !== source.length) {
+ throw new RangeError("AudioBuffer left-channel length does not match length");
+ }
+ const resample = resampler === "sinc"
+ ? resampleSinc
+ : resampler === "linear"
+ ? resampleLinear
+ : undefined;
+ if (resample === undefined) throw new RangeError(`Unsupported audio resampler: ${String(resampler)}`);
+ const left = resample(
+ sourceLeft,
+ source.sampleRate,
+ outputSampleRate,
+ );
+
+ let right: Float32Array;
+ if (source.numberOfChannels === 1) {
+ // Do not alias the two channels: the model pipeline may process or replace
+ // one channel independently of the other.
+ right = left.slice();
+ } else {
+ const sourceRight = source.getChannelData(1);
+ if (sourceRight.length !== source.length) {
+ throw new RangeError("AudioBuffer right-channel length does not match length");
+ }
+ right = resample(
+ sourceRight,
+ source.sampleRate,
+ outputSampleRate,
+ );
+ }
+
+ return makeStereoPcm(outputSampleRate, left, right);
+}
+
+/**
+ * Resample one planar channel with torchaudio 2.0.2's default sinc kernel:
+ * lowpass_filter_width=6, rolloff=0.99, and a Hann window. The convolution is
+ * zero-padded and trimmed to `ceil(inputLength * targetRate / sourceRate)`, as
+ * in `torchaudio.functional.resample`.
+ */
+export function resampleSinc(
+ source: Float32Array,
+ sourceSampleRate: number,
+ targetSampleRate: number,
+): Float32Array {
+ validateSampleRate(sourceSampleRate, "sourceSampleRate");
+ validateSampleRate(targetSampleRate, "targetSampleRate");
+ if (source.length === 0) return new Float32Array(0);
+ if (sourceSampleRate === targetSampleRate) return source.slice();
+
+ const commonDivisor = greatestCommonDivisor(sourceSampleRate, targetSampleRate);
+ const sourceStride = sourceSampleRate / commonDivisor;
+ const targetPhases = targetSampleRate / commonDivisor;
+ const targetLengthNumerator = source.length * targetPhases;
+ if (!Number.isSafeInteger(targetLengthNumerator)) {
+ throw new RangeError("resampled audio length exceeds the safe integer range");
+ }
+ const targetLength = Math.ceil(targetLengthNumerator / sourceStride);
+ const kernel = getSincResampleKernel(sourceStride, targetPhases);
+ const output = new Float32Array(targetLength);
+
+ for (let targetIndex = 0; targetIndex < targetLength; targetIndex += 1) {
+ const sourceFrame = Math.floor(targetIndex / kernel.targetPhases);
+ const phase = targetIndex - sourceFrame * kernel.targetPhases;
+ const firstSourceIndex = sourceFrame * kernel.sourceStride - kernel.width;
+ const firstTap = Math.max(
+ kernel.firstNonzeroTaps[phase]!,
+ -firstSourceIndex,
+ );
+ const lastTap = Math.min(
+ kernel.lastNonzeroTaps[phase]!,
+ source.length - firstSourceIndex,
+ );
+ const coefficientOffset = phase * kernel.tapCount;
+ let sum = 0;
+ for (let tap = firstTap; tap < lastTap; tap += 1) {
+ sum += source[firstSourceIndex + tap]!
+ * kernel.coefficients[coefficientOffset + tap]!;
+ }
+ output[targetIndex] = Math.fround(sum);
+ }
+ return output;
+}
+
+/** Resample both planar channels without changing their stereo ordering. */
+export function resampleStereoSinc(
+ source: StereoPcm,
+ targetSampleRate: number = DICOSE_SAMPLE_RATE,
+): StereoPcm {
+ validateStereoPcm(source);
+ validateSampleRate(targetSampleRate, "targetSampleRate");
+ return makeStereoPcm(
+ targetSampleRate,
+ resampleSinc(source.left, source.sampleRate, targetSampleRate),
+ resampleSinc(source.right, source.sampleRate, targetSampleRate),
+ );
+}
+
+/** Resample stereo PCM and then enforce an externally declared frame count. */
+export function resampleStereoSincToLength(
+ source: StereoPcm,
+ targetSampleRate: number,
+ targetLength: number,
+): StereoPcm {
+ if (!Number.isSafeInteger(targetLength) || targetLength < 0) {
+ throw new RangeError("targetLength must be a non-negative safe integer");
+ }
+ const resampled = resampleStereoSinc(source, targetSampleRate);
+ if (resampled.length === targetLength) return resampled;
+ const left = new Float32Array(targetLength);
+ const right = new Float32Array(targetLength);
+ left.set(resampled.left.subarray(0, targetLength));
+ right.set(resampled.right.subarray(0, targetLength));
+ return makeStereoPcm(targetSampleRate, left, right);
+}
+
+/**
+ * Derive the complementary instrumental waveform from an input mixture and
+ * its estimated vocals. Both operands must already share the same timeline.
+ * The result is deliberately not clamped: valid subtraction can exceed unit
+ * peak and `encodeStereoWav` will preserve it in an IEEE-float WAV.
+ */
+export function subtractStereoPcm(
+ mixture: StereoPcm,
+ vocals: StereoPcm,
+): StereoPcm {
+ validateStereoPcm(mixture);
+ validateStereoPcm(vocals);
+ if (mixture.sampleRate !== vocals.sampleRate || mixture.length !== vocals.length) {
+ throw new RangeError("Stereo PCM subtraction requires matching timelines");
+ }
+ const left = new Float32Array(mixture.length);
+ const right = new Float32Array(mixture.length);
+ for (let index = 0; index < mixture.length; index += 1) {
+ left[index] = Math.fround(mixture.left[index]! - vocals.left[index]!);
+ right[index] = Math.fround(mixture.right[index]! - vocals.right[index]!);
+ }
+ return makeStereoPcm(mixture.sampleRate, left, right);
+}
+
+/**
+ * Legacy deterministic-reference helper. Production audio paths use
+ * `resampleSinc`. Positions beyond the final sample hold that final value.
+ */
+export function resampleLinear(
+ source: Float32Array,
+ sourceSampleRate: number,
+ targetSampleRate: number,
+): Float32Array {
+ validateSampleRate(sourceSampleRate, "sourceSampleRate");
+ validateSampleRate(targetSampleRate, "targetSampleRate");
+ if (source.length === 0) return new Float32Array(0);
+ if (sourceSampleRate === targetSampleRate) return source.slice();
+
+ const targetLength = Math.max(
+ 1,
+ Math.round((source.length * targetSampleRate) / sourceSampleRate),
+ );
+ if (!Number.isSafeInteger(targetLength)) {
+ throw new RangeError("resampled audio length exceeds the safe integer range");
+ }
+
+ const output = new Float32Array(targetLength);
+ const sourceLastIndex = source.length - 1;
+ const sourceStep = sourceSampleRate / targetSampleRate;
+ for (let targetIndex = 0; targetIndex < targetLength; targetIndex += 1) {
+ const sourcePosition = targetIndex * sourceStep;
+ const beforeIndex = Math.min(
+ sourceLastIndex,
+ Math.floor(sourcePosition),
+ );
+ const afterIndex = Math.min(sourceLastIndex, beforeIndex + 1);
+ const fraction = sourcePosition - beforeIndex;
+ const before = source[beforeIndex]!;
+ const after = source[afterIndex]!;
+ output[targetIndex] = Math.fround(before + (after - before) * fraction);
+ }
+ return output;
+}
+
+function getSincResampleKernel(
+ sourceStride: number,
+ targetPhases: number,
+): SincResampleKernel {
+ const cacheKey = `${sourceStride}:${targetPhases}`;
+ const cached = sincResampleKernelCache.get(cacheKey);
+ if (cached !== undefined) return cached;
+
+ const baseFrequency = Math.min(sourceStride, targetPhases) * SINC_ROLLOFF;
+ const width = Math.ceil(
+ SINC_LOWPASS_FILTER_WIDTH * sourceStride / baseFrequency,
+ );
+ const tapCount = sourceStride + width * 2;
+ const coefficientCount = targetPhases * tapCount;
+ if (
+ !Number.isSafeInteger(coefficientCount) ||
+ coefficientCount > MAX_SINC_KERNEL_COEFFICIENTS
+ ) {
+ throw new RangeError(
+ `resampling ratio ${sourceStride}:${targetPhases} requires an impractically large sinc kernel`,
+ );
+ }
+
+ // This is the phase-major kernel constructed by torchaudio 2.0.2's
+ // _get_sinc_resample_kernel. Build in float64 and cast each coefficient to
+ // float32, matching its default dtype path.
+ const coefficients = new Float32Array(coefficientCount);
+ const firstNonzeroTaps = new Uint32Array(targetPhases);
+ const lastNonzeroTaps = new Uint32Array(targetPhases);
+ const scale = baseFrequency / sourceStride;
+ for (let phase = 0; phase < targetPhases; phase += 1) {
+ const phaseOffset = -phase / targetPhases;
+ const coefficientOffset = phase * tapCount;
+ let firstNonzeroTap = tapCount;
+ let lastNonzeroTap = 0;
+ for (let tap = 0; tap < tapCount; tap += 1) {
+ const kernelIndex = tap - width;
+ const unclamped = (
+ phaseOffset + kernelIndex / sourceStride
+ ) * baseFrequency;
+ const time = Math.max(
+ -SINC_LOWPASS_FILTER_WIDTH,
+ Math.min(SINC_LOWPASS_FILTER_WIDTH, unclamped),
+ );
+ const window = Math.cos(
+ time * Math.PI / SINC_LOWPASS_FILTER_WIDTH / 2,
+ ) ** 2;
+ const angle = time * Math.PI;
+ const sinc = angle === 0 ? 1 : Math.sin(angle) / angle;
+ const coefficient = Math.fround(
+ sinc * window * scale,
+ );
+ coefficients[coefficientOffset + tap] = coefficient;
+ if (coefficient !== 0) {
+ firstNonzeroTap = Math.min(firstNonzeroTap, tap);
+ lastNonzeroTap = tap + 1;
+ }
+ }
+ firstNonzeroTaps[phase] = firstNonzeroTap;
+ lastNonzeroTaps[phase] = lastNonzeroTap;
+ }
+
+ const kernel: SincResampleKernel = {
+ sourceStride,
+ targetPhases,
+ width,
+ tapCount,
+ coefficients,
+ firstNonzeroTaps,
+ lastNonzeroTaps,
+ };
+ sincResampleKernelCache.set(cacheKey, kernel);
+ return kernel;
+}
+
+function greatestCommonDivisor(left: number, right: number): number {
+ while (right !== 0) {
+ const remainder = left % right;
+ left = right;
+ right = remainder;
+ }
+ return left;
+}
+
+/** Interleave planar stereo as `[left0, right0, left1, right1, ...]`. */
+export function interleaveStereo(source: StereoPcm): Float32Array {
+ validateStereoPcm(source);
+ const output = new Float32Array(source.length * 2);
+ for (let sample = 0; sample < source.length; sample += 1) {
+ const offset = sample * 2;
+ output[offset] = source.left[sample]!;
+ output[offset + 1] = source.right[sample]!;
+ }
+ return output;
+}
+
+export interface StftOptions {
+ readonly nFft?: number;
+ readonly hopLength?: number;
+}
+
+/**
+ * One-sided complex spectrum produced by `torch.stft(..., return_complex=True,
+ * center=True, pad_mode="reflect", window=torch.hann_window(nFft))`.
+ */
+export interface CenteredHannStft {
+ readonly layout: "frame-frequency";
+ readonly window: "hann-periodic";
+ readonly center: true;
+ readonly nFft: number;
+ readonly hopLength: number;
+ readonly binCount: number;
+ readonly frameCount: number;
+ /** Original, unpadded source length. Used as ISTFT's exact default length. */
+ readonly sourceLength: number;
+ readonly real: Float32Array;
+ readonly imag: Float32Array;
+}
+
+export interface IstftOptions {
+ /** Mirrors torch.istft's `length`; defaults to the STFT source length. */
+ readonly length?: number;
+}
+
+interface FftPlan {
+ readonly size: number;
+ readonly bitReversed: Uint32Array;
+ readonly twiddleReal: Float32Array;
+ readonly twiddleForwardImaginary: Float32Array;
+}
+
+const FFT_PLANS = new Map();
+const HANN_WINDOWS = new Map();
+
+/** Return a caller-owned periodic Hann window matching torch.hann_window's default. */
+export function createPeriodicHannWindow(
+ nFft: number = DICOSE_STFT_N_FFT,
+): Float32Array {
+ validateFftSize(nFft);
+ return new Float32Array(periodicHannWindow(nFft));
+}
+
+/**
+ * Compute a centered, reflection-padded, one-sided Hann STFT. The output is
+ * contiguous frame-major complex data, which is convenient for one upload per
+ * channel before the WebGPU model begins.
+ */
+export function centeredHannStft(
+ samples: Float32Array,
+ options: StftOptions = {},
+): CenteredHannStft {
+ const { nFft, hopLength } = resolveStftOptions(options);
+ const padding = nFft / 2;
+ if (samples.length <= padding) {
+ throw new RangeError(
+ `torch-compatible reflection padding requires more than ${padding} samples`,
+ );
+ }
+
+ const frameCount = Math.floor(samples.length / hopLength) + 1;
+ const binCount = padding + 1;
+ const valueCount = frameCount * binCount;
+ if (!Number.isSafeInteger(valueCount)) {
+ throw new RangeError("STFT output length exceeds the safe integer range");
+ }
+
+ const real = new Float32Array(valueCount);
+ const imag = new Float32Array(valueCount);
+ const fftReal = new Float32Array(nFft);
+ const fftImag = new Float32Array(nFft);
+ const window = periodicHannWindow(nFft);
+ const plan = fftPlan(nFft);
+
+ for (let frame = 0; frame < frameCount; frame += 1) {
+ const frameStart = frame * hopLength - padding;
+ for (let sample = 0; sample < nFft; sample += 1) {
+ const sourceIndex = reflectIndex(frameStart + sample, samples.length);
+ fftReal[sample] = Math.fround(
+ samples[sourceIndex]! * window[sample]!,
+ );
+ fftImag[sample] = 0;
+ }
+ fftInPlace(fftReal, fftImag, plan, false);
+
+ const outputOffset = frame * binCount;
+ for (let bin = 0; bin < binCount; bin += 1) {
+ real[outputOffset + bin] = fftReal[bin]!;
+ imag[outputOffset + bin] = fftImag[bin]!;
+ }
+ }
+
+ return {
+ layout: "frame-frequency",
+ window: "hann-periodic",
+ center: true,
+ nFft,
+ hopLength,
+ binCount,
+ frameCount,
+ sourceLength: samples.length,
+ real,
+ imag,
+ };
+}
+
+/**
+ * Invert a centered Hann STFT with window-squared overlap-add normalization.
+ * Passing an explicit length exactly follows `torch.istft(..., length=...)`:
+ * it removes the left center padding and then takes that many output samples.
+ */
+export function centeredHannIstft(
+ spectrum: CenteredHannStft,
+ options: IstftOptions = {},
+): Float32Array {
+ validateSpectrum(spectrum);
+ const requestedLength = options.length ?? spectrum.sourceLength;
+ if (!Number.isSafeInteger(requestedLength) || requestedLength < 0) {
+ throw new RangeError("ISTFT length must be a non-negative safe integer");
+ }
+
+ const { nFft, hopLength, frameCount, binCount } = spectrum;
+ const padding = nFft / 2;
+ const overlapLength = nFft + (frameCount - 1) * hopLength;
+ if (!Number.isSafeInteger(overlapLength)) {
+ throw new RangeError("ISTFT overlap-add length exceeds the safe integer range");
+ }
+
+ // Float64 accumulation keeps normalization stable even for long source audio;
+ // values are rounded once at the public Float32 output boundary.
+ const overlapAdd = new Float64Array(overlapLength);
+ const envelope = new Float64Array(overlapLength);
+ const fftReal = new Float32Array(nFft);
+ const fftImag = new Float32Array(nFft);
+ const window = periodicHannWindow(nFft);
+ const plan = fftPlan(nFft);
+ const nyquist = nFft / 2;
+
+ for (let frame = 0; frame < frameCount; frame += 1) {
+ const inputOffset = frame * binCount;
+ for (let bin = 0; bin < binCount; bin += 1) {
+ fftReal[bin] = spectrum.real[inputOffset + bin]!;
+ // rFFT endpoint imaginary components do not represent a real signal and
+ // are ignored by torch.fft.irfft as well.
+ fftImag[bin] = bin === 0 || bin === nyquist
+ ? 0
+ : spectrum.imag[inputOffset + bin]!;
+ }
+ for (let bin = binCount; bin < nFft; bin += 1) {
+ const mirroredBin = nFft - bin;
+ fftReal[bin] = fftReal[mirroredBin]!;
+ fftImag[bin] = -fftImag[mirroredBin]!;
+ }
+
+ fftInPlace(fftReal, fftImag, plan, true);
+ const outputOffset = frame * hopLength;
+ for (let sample = 0; sample < nFft; sample += 1) {
+ const weighted = fftReal[sample]! * window[sample]!;
+ const targetIndex = outputOffset + sample;
+ overlapAdd[targetIndex] = overlapAdd[targetIndex]! + weighted;
+ const windowValue = window[sample]!;
+ envelope[targetIndex] = envelope[targetIndex]! + windowValue * windowValue;
+ }
+ }
+
+ const output = new Float32Array(requestedLength);
+ for (let sample = 0; sample < requestedLength; sample += 1) {
+ const overlapIndex = padding + sample;
+ if (overlapIndex >= overlapLength) continue;
+ const normalization = envelope[overlapIndex]!;
+ if (normalization > 1e-11) {
+ output[sample] = Math.fround(overlapAdd[overlapIndex]! / normalization);
+ }
+ }
+ return output;
+}
+
+/** Convert one IEEE-754 binary32 number to round-to-nearest-even binary16 bits. */
+export function float32ToFloat16Bits(value: number): number {
+ FLOAT32_CONVERSION_VIEW[0] = value;
+ const bits = UINT32_CONVERSION_VIEW[0]!;
+ const sign = (bits >>> 16) & 0x8000;
+ const exponent = (bits >>> 23) & 0xff;
+ const fraction = bits & 0x7f_ffff;
+
+ if (exponent === 0xff) {
+ if (fraction === 0) return sign | 0x7c00;
+ const payload = fraction >>> 13;
+ return sign | 0x7c00 | (payload === 0 ? 1 : payload);
+ }
+
+ let halfExponent = exponent - 127 + 15;
+ if (halfExponent >= 31) return sign | 0x7c00;
+ if (halfExponent <= 0) {
+ // All binary32 subnormals are below binary16's representable subnormal
+ // range. The leading bit is present for every normal binary32 value here.
+ if (halfExponent < -10) return sign;
+ const normalizedFraction = fraction | 0x80_0000;
+ const shift = 14 - halfExponent;
+ let halfFraction = normalizedFraction >>> shift;
+ const remainder = normalizedFraction & ((1 << shift) - 1);
+ const halfway = 1 << (shift - 1);
+ if (
+ remainder > halfway ||
+ (remainder === halfway && (halfFraction & 1) !== 0)
+ ) {
+ halfFraction += 1;
+ }
+ return sign | halfFraction;
+ }
+
+ let halfFraction = fraction >>> 13;
+ const remainder = fraction & 0x1fff;
+ if (remainder > 0x1000 || (remainder === 0x1000 && (halfFraction & 1) !== 0)) {
+ halfFraction += 1;
+ if (halfFraction === 0x400) {
+ halfFraction = 0;
+ halfExponent += 1;
+ if (halfExponent >= 31) return sign | 0x7c00;
+ }
+ }
+ return sign | (halfExponent << 10) | halfFraction;
+}
+
+/** Decode IEEE-754 binary16 storage bits into the exactly representable binary32 value. */
+export function float16BitsToFloat32(bits: number): number {
+ if (!Number.isInteger(bits) || bits < 0 || bits > 0xffff) {
+ throw new RangeError("float16 bits must be an unsigned 16-bit integer");
+ }
+ const sign = (bits & 0x8000) === 0 ? 1 : -1;
+ const exponent = (bits >>> 10) & 0x1f;
+ const fraction = bits & 0x03ff;
+ if (exponent === 0) {
+ if (fraction === 0) return sign < 0 ? -0 : 0;
+ return Math.fround(sign * fraction * 2 ** -24);
+ }
+ if (exponent === 0x1f) {
+ return fraction === 0 ? sign * Infinity : Number.NaN;
+ }
+ return Math.fround(sign * (1 + fraction / 1024) * 2 ** (exponent - 15));
+}
+
+/** Pack binary32 tensor values for `array` / `u32` WebGPU uploads. */
+export function packFloat16(values: Float32Array): Uint16Array {
+ const packed = new Uint16Array(values.length);
+ for (let index = 0; index < values.length; index += 1) {
+ packed[index] = float32ToFloat16Bits(values[index]!);
+ }
+ return packed;
+}
+
+/** Expand binary16 storage into a binary32 tensor. */
+export function unpackFloat16(values: Uint16Array): Float32Array {
+ const unpacked = new Float32Array(values.length);
+ for (let index = 0; index < values.length; index += 1) {
+ unpacked[index] = float16BitsToFloat32(values[index]!);
+ }
+ return unpacked;
+}
+
+export type GaussianSeed = number | bigint;
+
+/**
+ * Tiny deterministic random stream for initial CD noise. It intentionally has
+ * no dependency on browser crypto or Math.random, so worker, main-thread, and
+ * Node test results are reproducible for a given seed.
+ */
+export class SeededGaussian {
+ private state: number;
+ private spare: number | undefined;
+
+ constructor(seed: GaussianSeed) {
+ this.state = normalizeGaussianSeed(seed);
+ }
+
+ next(): number {
+ if (this.spare !== undefined) {
+ const output = this.spare;
+ this.spare = undefined;
+ return output;
+ }
+
+ const radius = Math.sqrt(-2 * Math.log(this.nextOpenUnit()));
+ const angle = TWO_PI * this.nextOpenUnit();
+ this.spare = Math.fround(radius * Math.sin(angle));
+ return Math.fround(radius * Math.cos(angle));
+ }
+
+ private nextOpenUnit(): number {
+ return (this.nextUint32() + 0.5) * UINT32_UNIT;
+ }
+
+ private nextUint32(): number {
+ let value = this.state;
+ value ^= value << 13;
+ value ^= value >>> 17;
+ value ^= value << 5;
+ this.state = value >>> 0;
+ return this.state;
+ }
+}
+
+/** Fill a caller-owned Float32 tensor in contiguous row-major order. */
+export function fillSeededGaussian(
+ output: Float32Array,
+ seed: GaussianSeed,
+): void {
+ const random = new SeededGaussian(seed);
+ for (let index = 0; index < output.length; index += 1) {
+ output[index] = random.next();
+ }
+}
+
+/** Allocate deterministic standard-normal noise for a model tensor. */
+export function seededGaussianNoise(
+ length: number,
+ seed: GaussianSeed,
+): Float32Array {
+ if (!Number.isSafeInteger(length) || length < 0) {
+ throw new RangeError("Gaussian noise length must be a non-negative safe integer");
+ }
+ const output = new Float32Array(length);
+ fillSeededGaussian(output, seed);
+ return output;
+}
+
+/** Encode one or more equal-length planar channels as little-endian PCM16 WAV. */
+export function encodePcm16Wav(
+ channels: readonly Float32Array[],
+ sampleRate: number = DICOSE_SAMPLE_RATE,
+): ArrayBuffer {
+ return encodeWav(channels, sampleRate, "pcm16");
+}
+
+/** Encode one or more equal-length planar channels as IEEE-float WAV. */
+export function encodeFloat32Wav(
+ channels: readonly Float32Array[],
+ sampleRate: number = DICOSE_SAMPLE_RATE,
+): ArrayBuffer {
+ return encodeWav(channels, sampleRate, "float32");
+}
+
+function encodeWav(
+ channels: readonly Float32Array[],
+ sampleRate: number,
+ encoding: "pcm16" | "float32",
+): ArrayBuffer {
+ validateSampleRate(sampleRate, "sampleRate");
+ if (channels.length < 1 || channels.length > 0xffff) {
+ throw new RangeError("WAV must contain between one and 65,535 channels");
+ }
+ const frameCount = channels[0]?.length;
+ if (frameCount === undefined) throw new Error("WAV channels are unexpectedly empty");
+ for (let channel = 0; channel < channels.length; channel += 1) {
+ const values = channels[channel]!;
+ if (values.length !== frameCount) {
+ throw new RangeError("all WAV channels must have the same frame count");
+ }
+ }
+
+ const bytesPerSample = encoding === "pcm16" ? 2 : 4;
+ const blockAlign = channels.length * bytesPerSample;
+ const dataBytes = frameCount * blockAlign;
+ const byteRate = sampleRate * blockAlign;
+ if (
+ !Number.isSafeInteger(dataBytes) ||
+ dataBytes > 0xffff_ffff ||
+ !Number.isSafeInteger(byteRate) ||
+ byteRate > 0xffff_ffff
+ ) {
+ throw new RangeError("WAV is too large for the RIFF container");
+ }
+
+ const bytes = new ArrayBuffer(44 + dataBytes);
+ const view = new DataView(bytes);
+ writeAscii(view, 0, "RIFF");
+ view.setUint32(4, 36 + dataBytes, true);
+ writeAscii(view, 8, "WAVE");
+ writeAscii(view, 12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, encoding === "pcm16" ? 1 : 3, true);
+ view.setUint16(22, channels.length, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, byteRate, true);
+ view.setUint16(32, blockAlign, true);
+ view.setUint16(34, bytesPerSample * 8, true);
+ writeAscii(view, 36, "data");
+ view.setUint32(40, dataBytes, true);
+
+ let byteOffset = 44;
+ for (let frame = 0; frame < frameCount; frame += 1) {
+ for (let channel = 0; channel < channels.length; channel += 1) {
+ const value = channels[channel]![frame]!;
+ if (!Number.isFinite(value)) {
+ throw new RangeError("WAV samples must be finite");
+ }
+ if (encoding === "float32") {
+ view.setFloat32(byteOffset, value, true);
+ } else {
+ const clamped = Math.min(1, Math.max(-1, value));
+ const integer = clamped < 0
+ ? Math.round(clamped * 32_768)
+ : Math.round(clamped * 32_767);
+ view.setInt16(byteOffset, integer, true);
+ }
+ byteOffset += bytesPerSample;
+ }
+ }
+ return bytes;
+}
+
+/** Encode a model-ready stereo PCM object as an audio/wav Blob. */
+export function encodeStereoPcm16Wav(source: StereoPcm): Blob {
+ validateStereoPcm(source);
+ return new Blob([encodePcm16Wav(source.channels, source.sampleRate)], {
+ type: "audio/wav",
+ });
+}
+
+/** Preserve over-range model output using the same peak rule as upstream. */
+export function encodeStereoWav(source: StereoPcm): Blob {
+ validateStereoPcm(source);
+ let requiresFloat = false;
+ for (const channel of source.channels) {
+ for (let index = 0; index < channel.length; index += 1) {
+ const value = channel[index]!;
+ if (!Number.isFinite(value)) throw new RangeError("WAV samples must be finite");
+ requiresFloat ||= Math.abs(value) > 1;
+ }
+ }
+ const encoded = requiresFloat
+ ? encodeFloat32Wav(source.channels, source.sampleRate)
+ : encodePcm16Wav(source.channels, source.sampleRate);
+ return new Blob([encoded], { type: "audio/wav" });
+}
+
+const FLOAT32_CONVERSION_BUFFER = new ArrayBuffer(4);
+const FLOAT32_CONVERSION_VIEW = new Float32Array(FLOAT32_CONVERSION_BUFFER);
+const UINT32_CONVERSION_VIEW = new Uint32Array(FLOAT32_CONVERSION_BUFFER);
+
+function makeStereoPcm(
+ sampleRate: number,
+ left: Float32Array,
+ right: Float32Array,
+): StereoPcm {
+ if (left.length !== right.length) {
+ throw new RangeError("stereo channels must have equal lengths");
+ }
+ return {
+ sampleRate,
+ length: left.length,
+ left,
+ right,
+ channels: [left, right],
+ };
+}
+
+function validateStereoPcm(source: StereoPcm): void {
+ validateSampleRate(source.sampleRate, "source.sampleRate");
+ if (!Number.isSafeInteger(source.length) || source.length < 0) {
+ throw new RangeError("stereo PCM length must be a non-negative safe integer");
+ }
+ if (
+ source.left.length !== source.length ||
+ source.right.length !== source.length ||
+ source.channels[0] !== source.left ||
+ source.channels[1] !== source.right
+ ) {
+ throw new RangeError("stereo PCM channel metadata is inconsistent");
+ }
+}
+
+function validateSampleRate(value: number, name: string): void {
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 0xffff_ffff) {
+ throw new RangeError(`${name} must be a positive integer sample rate`);
+ }
+}
+
+function resolveStftOptions(options: StftOptions): {
+ readonly nFft: number;
+ readonly hopLength: number;
+} {
+ const nFft = options.nFft ?? DICOSE_STFT_N_FFT;
+ const hopLength = options.hopLength ?? DICOSE_STFT_HOP_LENGTH;
+ validateFftSize(nFft);
+ if (!Number.isSafeInteger(hopLength) || hopLength < 1) {
+ throw new RangeError("hopLength must be a positive safe integer");
+ }
+ return { nFft, hopLength };
+}
+
+function validateFftSize(nFft: number): void {
+ if (
+ !Number.isSafeInteger(nFft) ||
+ nFft < 2 ||
+ nFft > 1 << 20 ||
+ nFft % 2 !== 0 ||
+ (nFft & (nFft - 1)) !== 0
+ ) {
+ throw new RangeError("nFft must be an even power of two no greater than 1,048,576");
+ }
+}
+
+function validateSpectrum(spectrum: CenteredHannStft): void {
+ if (
+ spectrum.layout !== "frame-frequency" ||
+ spectrum.window !== "hann-periodic" ||
+ spectrum.center !== true
+ ) {
+ throw new TypeError("ISTFT requires a centered periodic-Hann STFT");
+ }
+ validateFftSize(spectrum.nFft);
+ if (!Number.isSafeInteger(spectrum.hopLength) || spectrum.hopLength < 1) {
+ throw new RangeError("STFT hopLength must be a positive safe integer");
+ }
+ if (!Number.isSafeInteger(spectrum.frameCount) || spectrum.frameCount < 1) {
+ throw new RangeError("STFT frameCount must be a positive safe integer");
+ }
+ if (!Number.isSafeInteger(spectrum.sourceLength) || spectrum.sourceLength < 0) {
+ throw new RangeError("STFT sourceLength must be a non-negative safe integer");
+ }
+ const expectedBinCount = spectrum.nFft / 2 + 1;
+ const expectedValueCount = spectrum.frameCount * expectedBinCount;
+ if (
+ spectrum.binCount !== expectedBinCount ||
+ !Number.isSafeInteger(expectedValueCount) ||
+ spectrum.real.length !== expectedValueCount ||
+ spectrum.imag.length !== expectedValueCount
+ ) {
+ throw new RangeError("STFT complex storage shape is inconsistent");
+ }
+}
+
+function periodicHannWindow(nFft: number): Float32Array {
+ let cached = HANN_WINDOWS.get(nFft);
+ if (cached !== undefined) return cached;
+ cached = new Float32Array(nFft);
+ for (let index = 0; index < nFft; index += 1) {
+ cached[index] = Math.fround(
+ 0.5 - 0.5 * Math.cos((TWO_PI * index) / nFft),
+ );
+ }
+ HANN_WINDOWS.set(nFft, cached);
+ return cached;
+}
+
+function reflectIndex(index: number, length: number): number {
+ if (index >= 0 && index < length) return index;
+ const period = length * 2 - 2;
+ let reflected = index % period;
+ if (reflected < 0) reflected += period;
+ return reflected < length ? reflected : period - reflected;
+}
+
+function fftPlan(size: number): FftPlan {
+ let plan = FFT_PLANS.get(size);
+ if (plan !== undefined) return plan;
+
+ const bitCount = Math.round(Math.log2(size));
+ const bitReversed = new Uint32Array(size);
+ for (let value = 0; value < size; value += 1) {
+ let remainder = value;
+ let reversed = 0;
+ for (let bit = 0; bit < bitCount; bit += 1) {
+ reversed = (reversed << 1) | (remainder & 1);
+ remainder >>>= 1;
+ }
+ bitReversed[value] = reversed;
+ }
+
+ const twiddleReal = new Float32Array(size / 2);
+ const twiddleForwardImaginary = new Float32Array(size / 2);
+ for (let index = 0; index < size / 2; index += 1) {
+ const phase = (TWO_PI * index) / size;
+ twiddleReal[index] = Math.fround(Math.cos(phase));
+ twiddleForwardImaginary[index] = Math.fround(-Math.sin(phase));
+ }
+ plan = { size, bitReversed, twiddleReal, twiddleForwardImaginary };
+ FFT_PLANS.set(size, plan);
+ return plan;
+}
+
+function fftInPlace(
+ real: Float32Array,
+ imag: Float32Array,
+ plan: FftPlan,
+ inverse: boolean,
+): void {
+ const { size, bitReversed, twiddleReal, twiddleForwardImaginary } = plan;
+ for (let index = 0; index < size; index += 1) {
+ const mirrored = bitReversed[index]!;
+ if (index >= mirrored) continue;
+ const realValue = real[index]!;
+ real[index] = real[mirrored]!;
+ real[mirrored] = realValue;
+ const imagValue = imag[index]!;
+ imag[index] = imag[mirrored]!;
+ imag[mirrored] = imagValue;
+ }
+
+ for (let butterflySize = 2; butterflySize <= size; butterflySize *= 2) {
+ const halfSize = butterflySize / 2;
+ const twiddleStride = size / butterflySize;
+ for (let start = 0; start < size; start += butterflySize) {
+ for (let offset = 0; offset < halfSize; offset += 1) {
+ const twiddleIndex = offset * twiddleStride;
+ const twiddleR = twiddleReal[twiddleIndex]!;
+ const forwardImaginary = twiddleForwardImaginary[twiddleIndex]!;
+ const twiddleI = inverse ? -forwardImaginary : forwardImaginary;
+ const upperIndex = start + offset;
+ const lowerIndex = upperIndex + halfSize;
+ const lowerR = real[lowerIndex]!;
+ const lowerI = imag[lowerIndex]!;
+ const transformedR = twiddleR * lowerR - twiddleI * lowerI;
+ const transformedI = twiddleR * lowerI + twiddleI * lowerR;
+ const upperR = real[upperIndex]!;
+ const upperI = imag[upperIndex]!;
+ real[upperIndex] = upperR + transformedR;
+ imag[upperIndex] = upperI + transformedI;
+ real[lowerIndex] = upperR - transformedR;
+ imag[lowerIndex] = upperI - transformedI;
+ }
+ }
+ }
+
+ if (inverse) {
+ for (let index = 0; index < size; index += 1) {
+ real[index] = real[index]! / size;
+ imag[index] = imag[index]! / size;
+ }
+ }
+}
+
+function normalizeGaussianSeed(seed: GaussianSeed): number {
+ let low: number;
+ let high: number;
+ if (typeof seed === "bigint") {
+ if (seed < 0n || seed > 0xffff_ffff_ffff_ffffn) {
+ throw new RangeError("Gaussian bigint seed must fit in unsigned 64 bits");
+ }
+ low = Number(seed & 0xffff_ffffn);
+ high = Number(seed >> 32n);
+ } else {
+ if (!Number.isSafeInteger(seed) || seed < 0) {
+ throw new RangeError("Gaussian numeric seed must be a non-negative safe integer");
+ }
+ low = seed >>> 0;
+ high = Math.floor(seed / 0x1_0000_0000) >>> 0;
+ }
+
+ let state = (low ^ Math.imul(high, 0x9e37_79b9)) >>> 0;
+ // xorshift32 has a forbidden all-zero state.
+ if (state === 0) state = 0x6d2b_79f5;
+ return state;
+}
+
+function writeAscii(view: DataView, offset: number, value: string): void {
+ for (let index = 0; index < value.length; index += 1) {
+ view.setUint8(offset + index, value.charCodeAt(index));
+ }
+}
+
+/** Return a native RIFF/WAVE sample rate without decoding PCM payload bytes. */
+function wavSampleRateFromRiff(encoded: ArrayBuffer): number | undefined {
+ if (encoded.byteLength < 12) return undefined;
+ const view = new DataView(encoded);
+ if (
+ readAscii(view, 0, 4) !== "RIFF" ||
+ readAscii(view, 8, 4) !== "WAVE"
+ ) {
+ return undefined;
+ }
+
+ let chunkOffset = 12;
+ while (chunkOffset + 8 <= view.byteLength) {
+ const chunkId = readAscii(view, chunkOffset, 4);
+ const chunkLength = view.getUint32(chunkOffset + 4, true);
+ const dataOffset = chunkOffset + 8;
+ if (dataOffset + chunkLength > view.byteLength) return undefined;
+ if (chunkId === "fmt " && chunkLength >= 16) {
+ const sampleRate = view.getUint32(dataOffset + 4, true);
+ return Number.isSafeInteger(sampleRate) && sampleRate > 0
+ ? sampleRate
+ : undefined;
+ }
+ chunkOffset = dataOffset + chunkLength + (chunkLength & 1);
+ }
+ return undefined;
+}
+
+function readAscii(
+ view: DataView,
+ offset: number,
+ length: number,
+): string {
+ let value = "";
+ for (let index = 0; index < length; index += 1) {
+ value += String.fromCharCode(view.getUint8(offset + index));
+ }
+ return value;
+}
diff --git a/packages/dicose/src/runtime/bs-roformer.ts b/packages/dicose/src/runtime/bs-roformer.ts
new file mode 100644
index 0000000..36202ed
--- /dev/null
+++ b/packages/dicose/src/runtime/bs-roformer.ts
@@ -0,0 +1,940 @@
+import type { GpuWeightPackage, GpuWeightTensor } from "../model/package.js";
+import { GpuOps, type AttentionKernel } from "../webgpu/ops.js";
+import {
+ createF16Tensor,
+ destroyTensors,
+ readF16Tensor,
+ writeF16Tensor,
+ type GpuTensor,
+} from "../webgpu/tensor.js";
+import { packFloat16 } from "./audio.js";
+
+export const DICOSE_BANDS = Object.freeze([
+ ...Array(24).fill(2),
+ ...Array(12).fill(4),
+ ...Array(8).fill(12),
+ ...Array(8).fill(24),
+ ...Array(8).fill(48),
+ 128,
+ 129,
+]);
+
+const DIM = 384;
+const BANDS = 62;
+const SPECTRAL_WIDTH = 4_100;
+const FREQUENCIES = 1_025;
+const STEMS = 4;
+
+type WorkspaceName = "det" | "cd";
+
+interface DeterministicPassBase {
+ readonly spectra: readonly Uint16Array[];
+ readonly elapsedMs: number;
+ /** Evenly sampled f16 activations, populated only by the reference audit harness. */
+ readonly trace?: DeterministicTrace;
+}
+
+export interface DeterministicTraceTensor {
+ readonly elements: number;
+ readonly values: Uint16Array;
+}
+
+export type DeterministicTrace = Readonly>;
+
+export interface CdTraceTensor {
+ readonly elementsPerCall: readonly number[];
+ readonly values: Uint16Array;
+}
+
+export type CdTrace = Readonly>;
+
+export interface CdStemPass {
+ readonly spectrum: Uint16Array;
+ readonly trace?: CdTrace;
+}
+
+export interface ConditionedDeterministicPass extends DeterministicPassBase {
+ readonly conditions: DiCoSeConditions;
+}
+
+export interface StandaloneDeterministicPass extends DeterministicPassBase {
+ readonly conditions?: undefined;
+}
+
+/** GPU-resident mixture conditioning, valid only for one input length. */
+export class DiCoSeConditions {
+ private destroyed = false;
+
+ constructor(
+ readonly frames: number,
+ readonly stft: GpuTensor,
+ readonly band: GpuTensor,
+ readonly time: readonly GpuTensor[],
+ readonly frequency: readonly GpuTensor[],
+ ) {}
+
+ destroy(): void {
+ if (this.destroyed) return;
+ this.destroyed = true;
+ destroyTensors([this.stft, this.band, ...this.time, ...this.frequency]);
+ }
+}
+
+export class DiCoSeMappingContexts {
+ private destroyed = false;
+
+ constructor(
+ readonly perStem: readonly StemMappingContext[],
+ readonly trace?: CdTrace,
+ ) {}
+
+ destroy(): void {
+ if (this.destroyed) return;
+ this.destroyed = true;
+ for (const context of this.perStem) destroyTensors(context.scaleShifts);
+ }
+}
+
+interface StemMappingContext {
+ readonly scaleShifts: readonly GpuTensor[];
+}
+
+interface Workspace {
+ readonly frames: number;
+ readonly rows: number;
+ readonly x: GpuTensor;
+ readonly alternate: GpuTensor;
+ readonly norm: GpuTensor;
+ readonly attention: GpuTensor;
+ readonly wide: GpuTensor;
+ readonly gates: GpuTensor;
+ readonly adapter: GpuTensor;
+ readonly bandInput: GpuTensor;
+ readonly bandNorm: GpuTensor;
+ readonly bandFeature: GpuTensor;
+ readonly maskMid: GpuTensor;
+ readonly maskWide: GpuTensor;
+ readonly bandMask: GpuTensor;
+ readonly globalMask: GpuTensor;
+ readonly masked: GpuTensor;
+}
+
+interface PendingTraceTensor {
+ readonly name: string;
+ readonly elements: number;
+ readonly samples: number;
+ readonly tensor: GpuTensor;
+ readonly repeatPeriod?: number;
+}
+
+/**
+ * Exact graph ordering for the released BS-RoFormer separator and one-step
+ * consistency-distilled refiner. DSP and sampling remain outside this class;
+ * this owner only consumes and produces centered-STFT f16 tensors.
+ */
+export class DiCoSeBsrRoFormer {
+ private readonly ops: GpuOps;
+ private readonly workspaces = new Map();
+ /** The worker may serve many input durations; retain at most one CD arena. */
+ private cdWorkspaceFrames: number | undefined;
+ private destroyed = false;
+
+ constructor(
+ private readonly device: GPUDevice,
+ private readonly weights: GpuWeightPackage,
+ attentionKernel: AttentionKernel = "flash",
+ ) {
+ this.ops = new GpuOps(device, attentionKernel);
+ const bands = weights.manifest.config.freqsPerBands;
+ if (bands.length !== BANDS || bands.some((band, index) => band !== DICOSE_BANDS[index])) {
+ throw new Error("This runtime only supports the released 62-band DiCoSe BS-RoFormer profile");
+ }
+ }
+
+ /** Run the deterministic separator and construct CD conditioning once. */
+ async runDeterministic(
+ spectral: Uint16Array,
+ frames: number,
+ options: { readonly captureConditions: false; readonly traceSamples?: number },
+ ): Promise;
+ async runDeterministic(
+ spectral: Uint16Array,
+ frames: number,
+ options?: { readonly captureConditions?: true; readonly traceSamples?: number },
+ ): Promise;
+ async runDeterministic(
+ spectral: Uint16Array,
+ frames: number,
+ options: { readonly captureConditions?: boolean; readonly traceSamples?: number } = {},
+ ): Promise {
+ this.requireAlive();
+ requireSpectral(spectral, frames);
+ const captureConditions = options.captureConditions ?? true;
+ const started = performance.now();
+ let input: GpuTensor | undefined;
+ let conditions: DiCoSeConditions | undefined;
+ let convIntermediates: readonly [GpuTensor, GpuTensor, GpuTensor] | undefined;
+ const outputs: GpuTensor[] = [];
+ const traceOutputs: PendingTraceTensor[] = [];
+ const traceSamples = options.traceSamples;
+ if (traceSamples !== undefined && (!Number.isSafeInteger(traceSamples) || traceSamples <= 0)) {
+ throw new RangeError("Deterministic trace sample count must be a positive integer");
+ }
+ const captureTrace = (
+ pass: GPUComputePassEncoder,
+ name: string,
+ source: GpuTensor,
+ elements: number,
+ ): void => {
+ if (traceSamples === undefined) return;
+ const samples = Math.min(traceSamples, elements);
+ const tensor = createF16Tensor(this.device, samples, `dicose-trace-${name}`);
+ traceOutputs.push({ name, elements, samples, tensor });
+ this.ops.sampleEven(pass, source, tensor, elements, samples);
+ };
+ try {
+ input = createF16Tensor(this.device, frames * SPECTRAL_WIDTH, "dicose-mixture-stft");
+ writeF16Tensor(this.device, input, spectral);
+ const workspace = this.workspace("det", frames);
+ if (captureConditions) {
+ conditions = this.createConditions(frames);
+ convIntermediates = this.createStftAdapterIntermediates(frames);
+ }
+ for (let stem = 0; stem < STEMS; stem += 1) {
+ outputs.push(createF16Tensor(this.device, frames * SPECTRAL_WIDTH, `dicose-det-output-${stem}`));
+ }
+ this.ops.beginGraph();
+ const encoder = this.device.createCommandEncoder({ label: "dicose-deterministic-graph" });
+ const pass = encoder.beginComputePass({ label: "dicose-deterministic" });
+ if (conditions !== undefined && convIntermediates !== undefined) {
+ this.runStftAdapter(pass, input, conditions.stft, frames, convIntermediates);
+ captureTrace(pass, "cd.stftAdapter", conditions.stft, frames * SPECTRAL_WIDTH);
+ }
+ captureTrace(pass, "spectrum.input", input, frames * SPECTRAL_WIDTH);
+ this.bandSplit(pass, "det", input, workspace);
+ if (conditions !== undefined) {
+ captureTrace(pass, "cd.bandConditionInput", workspace.x, workspace.rows * DIM);
+ this.runAdapter(
+ pass,
+ "cd.band_split_feature_adapter",
+ workspace.x,
+ conditions.band,
+ workspace.rows,
+ (stage, source) => captureTrace(
+ pass,
+ `cd.bandCondition${stage}`,
+ source,
+ workspace.rows * DIM,
+ ),
+ );
+ captureTrace(pass, "cd.bandCondition", conditions.band, workspace.rows * DIM);
+ }
+ captureTrace(pass, "band", workspace.x, workspace.rows * DIM);
+ for (let layer = 0; layer < 8; layer += 1) {
+ this.transformer(
+ pass,
+ "det",
+ layer,
+ 0,
+ workspace.x,
+ workspace.alternate,
+ BANDS,
+ workspace.frames,
+ undefined,
+ 0,
+ layer === 0
+ ? (name, source, elements) => captureTrace(
+ pass,
+ `layer0.time.${name}`,
+ source,
+ elements,
+ )
+ : undefined,
+ );
+ captureTrace(pass, `layer${layer}.time`, workspace.x, workspace.rows * DIM);
+ if (conditions !== undefined) {
+ this.runAdapter(
+ pass,
+ `cd.transformer_feature_adapters.${layer * 2}`,
+ workspace.x,
+ conditions.time[layer]!,
+ workspace.rows,
+ );
+ }
+ this.transformer(
+ pass,
+ "det",
+ layer,
+ 1,
+ workspace.x,
+ workspace.alternate,
+ workspace.frames,
+ BANDS,
+ );
+ captureTrace(pass, `layer${layer}.frequency`, workspace.x, workspace.rows * DIM);
+ if (conditions !== undefined) {
+ this.runAdapter(
+ pass,
+ `cd.transformer_feature_adapters.${layer * 2 + 1}`,
+ workspace.x,
+ conditions.frequency[layer]!,
+ workspace.rows,
+ );
+ }
+ }
+ this.ops.rmsNorm(
+ pass,
+ workspace.x,
+ this.weight("det.final_norm.gamma"),
+ workspace.norm,
+ workspace.rows,
+ DIM,
+ );
+ captureTrace(pass, "finalNorm", workspace.norm, workspace.frames * BANDS * DIM);
+ for (let stem = 0; stem < STEMS; stem += 1) {
+ this.maskEstimator(pass, "det", stem, input, workspace.norm, workspace);
+ captureTrace(pass, `mask.${stem}`, workspace.globalMask, frames * SPECTRAL_WIDTH);
+ captureTrace(pass, `spectrum.${stem}`, workspace.masked, frames * SPECTRAL_WIDTH);
+ this.ops.copy(pass, workspace.masked, outputs[stem]!, frames * SPECTRAL_WIDTH);
+ }
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+ await this.device.queue.onSubmittedWorkDone();
+ const spectra = await Promise.all(outputs.map(async (output) => await readF16Tensor(this.device, output)));
+ const traceEntries = await Promise.all(traceOutputs.map(async ({ name, elements, tensor }) => [
+ name,
+ { elements, values: await readF16Tensor(this.device, tensor) },
+ ] as const));
+ const trace = traceEntries.length === 0
+ ? undefined
+ : Object.freeze(Object.fromEntries(traceEntries)) as DeterministicTrace;
+ const elapsedMs = performance.now() - started;
+ if (conditions === undefined) return { spectra, elapsedMs, ...(trace === undefined ? {} : { trace }) };
+ const resultConditions = conditions;
+ conditions = undefined;
+ return {
+ spectra,
+ conditions: resultConditions,
+ elapsedMs,
+ ...(trace === undefined ? {} : { trace }),
+ };
+ } finally {
+ input?.buffer.destroy();
+ destroyTensors(convIntermediates ?? []);
+ destroyTensors(outputs);
+ destroyTensors(traceOutputs.map(({ tensor }) => tensor));
+ conditions?.destroy();
+ // Conditioning survives a successful return, but this transient arena
+ // must not outlive a failed deterministic graph.
+ this.releaseWorkspace("det", frames);
+ }
+ }
+
+ /** Precompute every stem/layer FiLM vector for a CD sampler sigma. */
+ async createMappings(
+ sigma: number,
+ options: { readonly traceSamples?: number } = {},
+ ): Promise {
+ this.requireAlive();
+ if (!Number.isFinite(sigma) || sigma <= 0) throw new RangeError("CD sigma must be positive");
+ const temporaries: GpuTensor[] = [];
+ const traceOutputs: PendingTraceTensor[] = [];
+ let contexts: StemMappingContext[] = [];
+ const traceSamples = options.traceSamples;
+ requireTraceSamples(traceSamples, "CD mapping");
+ const temporary = (elements: number, label: string): GpuTensor => {
+ const tensor = createF16Tensor(this.device, elements, label);
+ temporaries.push(tensor);
+ return tensor;
+ };
+ const captureTrace = (
+ pass: GPUComputePassEncoder,
+ name: string,
+ source: GpuTensor,
+ elements: number,
+ ): void => {
+ if (traceSamples === undefined) return;
+ const samples = Math.min(traceSamples, elements);
+ const tensor = createF16Tensor(this.device, samples, `dicose-trace-${name}-${traceOutputs.length}`);
+ traceOutputs.push({ name, elements, samples, tensor });
+ this.ops.sampleEven(pass, source, tensor, elements, samples);
+ };
+ try {
+ const timeInput = temporary(DIM, "dicose-cd-time-input");
+ // KarrasDenoiser first transports sigma as 250*log(sigma), but the CD
+ // wrapper unscales it and passes log(sigma)/4 into the BS-RoFormer.
+ writeF16Tensor(this.device, timeInput, positionalEmbedding(Math.log(sigma) / 4, DIM));
+ for (let stem = 0; stem < STEMS; stem += 1) contexts.push(this.createMappingContext());
+ const embedded = Array.from({ length: STEMS }, (_, stem) =>
+ temporary(1_536, `dicose-cd-stem-embedding-${stem}`),
+ );
+ const time = Array.from({ length: STEMS }, (_, stem) =>
+ temporary(1_536, `dicose-cd-time-${stem}`),
+ );
+ const mappingA = Array.from({ length: STEMS }, (_, stem) =>
+ temporary(1_536, `dicose-cd-mapping-a-${stem}`),
+ );
+ const mappingB = Array.from({ length: STEMS }, (_, stem) =>
+ temporary(1_536, `dicose-cd-mapping-b-${stem}`),
+ );
+ const mappingGelu = Array.from({ length: STEMS }, (_, stem) =>
+ temporary(1_536, `dicose-cd-mapping-gelu-${stem}`),
+ );
+ this.ops.beginGraph();
+ const encoder = this.device.createCommandEncoder({ label: "dicose-cd-mapping" });
+ const embedding = this.weight("cd.stem_embedding.weight");
+ for (let stem = 0; stem < STEMS; stem += 1) {
+ encoder.copyBufferToBuffer(
+ embedding.buffer,
+ embedding.offset + stem * 1_536 * 2,
+ embedded[stem]!.buffer,
+ 0,
+ 1_536 * 2,
+ );
+ }
+ const pass = encoder.beginComputePass({ label: "dicose-cd-mapping-compute" });
+ for (let stem = 0; stem < STEMS; stem += 1) {
+ this.ops.linear(pass, timeInput, this.weight("cd.to_time.0.1.weight"), this.weight("cd.to_time.0.1.bias"), time[stem]!, {
+ rows: 1, inner: DIM, columns: 1_536, activation: "gelu",
+ });
+ if (stem === 0) captureTrace(pass, "cd.timeEmbedding", time[stem]!, 1_536);
+ this.ops.add(pass, embedded[stem]!, time[stem]!, 1_536);
+ captureTrace(pass, "cd.mappingInput", time[stem]!, 1_536);
+ this.ops.linear(pass, time[stem]!, this.weight("cd.to_mapping.0.weight"), this.weight("cd.to_mapping.0.bias"), mappingA[stem]!, {
+ rows: 1, inner: 1_536, columns: 1_536, activation: "gelu",
+ });
+ this.ops.linear(pass, mappingA[stem]!, this.weight("cd.to_mapping.2.weight"), this.weight("cd.to_mapping.2.bias"), mappingB[stem]!, {
+ rows: 1, inner: 1_536, columns: 1_536, activation: "gelu",
+ });
+ captureTrace(pass, "cd.mappingOutput", mappingB[stem]!, 1_536);
+ // MappingToScaleShift is nn.Sequential(GELU, Linear), so each FiLM
+ // projection receives one additional GELU after the mapping network.
+ this.ops.copy(pass, mappingB[stem]!, mappingGelu[stem]!, 1_536);
+ this.ops.geluInPlace(pass, mappingGelu[stem]!, 1_536);
+ let mappingIndex = 0;
+ for (let layer = 0; layer < 8; layer += 1) {
+ for (const axis of [0, 1] as const) {
+ for (const block of [0, 1] as const) {
+ const base = `cd.layers.${layer}.${axis}.layers.0.${block}.to_scale_shift.to_scale_shift.1`;
+ this.ops.linear(pass, mappingGelu[stem]!, this.weight(`${base}.weight`), this.weight(`${base}.bias`), contexts[stem]!.scaleShifts[mappingIndex]!, {
+ rows: 1, inner: 1_536, columns: 768,
+ });
+ if (mappingIndex === 0) {
+ if (traceSamples !== undefined) {
+ const semanticElements = BANDS * 768;
+ const samples = Math.min(traceSamples, semanticElements);
+ const tensor = createF16Tensor(
+ this.device,
+ 768,
+ `dicose-trace-cd-film-layer0-time-attention-${stem}`,
+ );
+ traceOutputs.push({
+ name: "cd.film.layer0.time.attention",
+ elements: semanticElements,
+ samples,
+ tensor,
+ repeatPeriod: 768,
+ });
+ this.ops.copy(
+ pass,
+ contexts[stem]!.scaleShifts[mappingIndex]!,
+ tensor,
+ 768,
+ );
+ }
+ }
+ mappingIndex += 1;
+ }
+ }
+ }
+ }
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+ await this.device.queue.onSubmittedWorkDone();
+ const trace = await readCdTrace(this.device, traceOutputs);
+ const result = new DiCoSeMappingContexts(contexts, trace);
+ contexts = [];
+ return result;
+ } finally {
+ destroyTensors(temporaries);
+ destroyTensors(traceOutputs.map(({ tensor }) => tensor));
+ for (const context of contexts) destroyTensors(context.scaleShifts);
+ }
+ }
+
+ /** One CD network evaluation for one stem, returning masked STFT f16. */
+ async runCdStem(
+ noisySpectral: Uint16Array,
+ stem: number,
+ conditions: DiCoSeConditions,
+ mappings: DiCoSeMappingContexts,
+ options: { readonly traceSamples?: number } = {},
+ ): Promise {
+ this.requireAlive();
+ if (!Number.isInteger(stem) || stem < 0 || stem >= STEMS) throw new RangeError("Invalid stem index");
+ requireSpectral(noisySpectral, conditions.frames);
+ const context = mappings.perStem[stem];
+ if (context === undefined) throw new Error("CD mapping context is missing a stem");
+ const traceOutputs: PendingTraceTensor[] = [];
+ const traceSamples = options.traceSamples;
+ requireTraceSamples(traceSamples, "CD stem");
+ const captureTrace = (
+ pass: GPUComputePassEncoder,
+ name: string,
+ source: GpuTensor,
+ elements: number,
+ ): void => {
+ if (traceSamples === undefined) return;
+ const samples = Math.min(traceSamples, elements);
+ const tensor = createF16Tensor(this.device, samples, `dicose-trace-${name}-${stem}`);
+ traceOutputs.push({ name, elements, samples, tensor });
+ this.ops.sampleEven(pass, source, tensor, elements, samples);
+ };
+ let input: GpuTensor | undefined;
+ try {
+ input = createF16Tensor(this.device, noisySpectral.length, `dicose-cd-input-${stem}`);
+ writeF16Tensor(this.device, input, noisySpectral);
+ const workspace = this.cdWorkspace(conditions.frames);
+ this.ops.beginGraph();
+ const encoder = this.device.createCommandEncoder({ label: `dicose-cd-stem-${stem}` });
+ const pass = encoder.beginComputePass({ label: `dicose-cd-stem-${stem}-compute` });
+ this.ops.add(pass, conditions.stft, input, noisySpectral.length);
+ captureTrace(pass, "cd.stftCombined", input, noisySpectral.length);
+ this.bandSplit(pass, "cd", input, workspace);
+ captureTrace(pass, "cd.bandRaw", workspace.x, workspace.rows * DIM);
+ this.ops.add(pass, conditions.band, workspace.x, workspace.rows * DIM);
+ captureTrace(pass, "cd.bandConditioned", workspace.x, workspace.rows * DIM);
+ for (let layer = 0; layer < 8; layer += 1) {
+ this.transformer(
+ pass,
+ "cd",
+ layer,
+ 0,
+ workspace.x,
+ workspace.alternate,
+ BANDS,
+ workspace.frames,
+ context,
+ layer * 4,
+ );
+ if (layer === 0) {
+ captureTrace(pass, "cd.layer0.time", workspace.x, workspace.rows * DIM);
+ }
+ this.ops.add(pass, conditions.time[layer]!, workspace.x, workspace.rows * DIM);
+ this.transformer(
+ pass,
+ "cd",
+ layer,
+ 1,
+ workspace.x,
+ workspace.alternate,
+ workspace.frames,
+ BANDS,
+ context,
+ layer * 4 + 2,
+ );
+ if (layer === 0) {
+ captureTrace(pass, "cd.layer0.frequency", workspace.x, workspace.rows * DIM);
+ } else if (layer === 7) {
+ captureTrace(pass, "cd.layer7.frequency", workspace.x, workspace.rows * DIM);
+ }
+ this.ops.add(pass, conditions.frequency[layer]!, workspace.x, workspace.rows * DIM);
+ }
+ this.ops.rmsNorm(
+ pass,
+ workspace.x,
+ this.weight("cd.final_norm.gamma"),
+ workspace.norm,
+ workspace.rows,
+ DIM,
+ );
+ captureTrace(pass, "cd.finalNorm", workspace.norm, workspace.frames * BANDS * DIM);
+ this.maskEstimator(pass, "cd", stem, input, workspace.norm, workspace);
+ captureTrace(pass, "cd.mask", workspace.globalMask, conditions.frames * SPECTRAL_WIDTH);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+ await this.device.queue.onSubmittedWorkDone();
+ const [spectrum, trace] = await Promise.all([
+ readF16Tensor(this.device, workspace.masked),
+ readCdTrace(this.device, traceOutputs),
+ ]);
+ return { spectrum, ...(trace === undefined ? {} : { trace }) };
+ } finally {
+ input?.buffer.destroy();
+ destroyTensors(traceOutputs.map(({ tensor }) => tensor));
+ }
+ }
+
+ destroy(): void {
+ if (this.destroyed) return;
+ this.destroyed = true;
+ for (const workspace of this.workspaces.values()) destroyWorkspace(workspace);
+ this.workspaces.clear();
+ this.ops.destroy();
+ }
+
+ private runStftAdapter(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ output: GpuTensor,
+ frames: number,
+ intermediates: readonly [GpuTensor, GpuTensor, GpuTensor],
+ ): void {
+ const [pixels, first, second] = intermediates;
+ this.ops.spectralToPixels(pass, source, pixels, frames, FREQUENCIES, 4);
+ this.ops.conv2d(pass, pixels, this.weight("cd.stft_feature_adapter.1.weight"), this.weight("cd.stft_feature_adapter.1.bias"), first, FREQUENCIES, frames, 4, 128, 3);
+ this.ops.geluInPlace(pass, first, FREQUENCIES * frames * 128);
+ this.ops.conv2d(pass, first, this.weight("cd.stft_feature_adapter.3.weight"), this.weight("cd.stft_feature_adapter.3.bias"), second, FREQUENCIES, frames, 128, 128, 1);
+ this.ops.geluInPlace(pass, second, FREQUENCIES * frames * 128);
+ this.ops.conv2d(pass, second, this.weight("cd.stft_feature_adapter.5.weight"), this.weight("cd.stft_feature_adapter.5.bias"), first, FREQUENCIES, frames, 128, 128, 1);
+ this.ops.geluInPlace(pass, first, FREQUENCIES * frames * 128);
+ this.ops.conv2d(pass, first, this.weight("cd.stft_feature_adapter.7.weight"), this.weight("cd.stft_feature_adapter.7.bias"), pixels, FREQUENCIES, frames, 128, 4, 3);
+ this.ops.pixelsToSpectral(pass, pixels, output, frames, FREQUENCIES, 4);
+ }
+
+ private bandSplit(
+ pass: GPUComputePassEncoder,
+ prefix: "det" | "cd",
+ spectral: GpuTensor,
+ workspace: Workspace,
+ ): void {
+ const frames = workspace.frames;
+ let offset = 0;
+ for (let band = 0; band < BANDS; band += 1) {
+ const width = DICOSE_BANDS[band]! * 4;
+ this.ops.gatherSlice(pass, spectral, workspace.bandInput, frames, SPECTRAL_WIDTH, offset, width);
+ this.ops.rmsNorm(pass, workspace.bandInput, this.weight(`${prefix}.band_split.to_features.${band}.0.gamma`), workspace.bandNorm, frames, width);
+ this.ops.linear(pass, workspace.bandNorm, this.weight(`${prefix}.band_split.to_features.${band}.1.weight`), this.weight(`${prefix}.band_split.to_features.${band}.1.bias`), workspace.bandFeature, {
+ rows: frames, inner: width, columns: DIM,
+ });
+ this.ops.scatterSlice(pass, workspace.bandFeature, workspace.x, frames, BANDS * DIM, band * DIM, DIM);
+ offset += width;
+ }
+ }
+
+ private transformer(
+ pass: GPUComputePassEncoder,
+ prefix: "det" | "cd",
+ layer: number,
+ axis: 0 | 1,
+ input: GpuTensor,
+ alternate: GpuTensor,
+ sequences: number,
+ tokens: number,
+ mapping?: StemMappingContext,
+ mappingIndex = 0,
+ trace?: (name: string, source: GpuTensor, elements: number) => void,
+ ): void {
+ const rows = sequences * tokens;
+ const base = `${prefix}.layers.${layer}.${axis}.layers.0`;
+ const attentionMapping = mapping?.scaleShifts[mappingIndex];
+ const ffMapping = mapping?.scaleShifts[mappingIndex + 1];
+ const attentionGeometry = {
+ sequences,
+ tokens,
+ strided: axis === 0,
+ } as const;
+ this.ops.rmsNorm(pass, input, this.weight(`${base}.0.norm.gamma`), this.workspaceFor(input).norm, rows, DIM, attentionMapping);
+ const workspace = this.workspaceFor(input);
+ trace?.("norm", workspace.norm, rows * DIM);
+ this.ops.linear(pass, workspace.norm, this.weight(`${base}.0.to_qkv.weight`), undefined, workspace.wide, {
+ rows,
+ inner: DIM,
+ columns: DIM * 4,
+ rotaryKeys: attentionGeometry,
+ });
+ trace?.("qkv", workspace.wide, rows * DIM * 4);
+ this.ops.linear(pass, workspace.norm, this.weight(`${base}.0.to_gates.weight`), this.weight(`${base}.0.to_gates.bias`), workspace.gates, {
+ rows, inner: DIM, columns: 8,
+ });
+ trace?.("gates", workspace.gates, rows * 8);
+ this.ops.attention(
+ pass,
+ workspace.wide,
+ workspace.attention,
+ { ...attentionGeometry, gates: workspace.gates, rotatedKeys: true },
+ );
+ this.ops.linear(pass, workspace.attention, this.weight(`${base}.0.to_out.0.weight`), undefined, alternate, {
+ rows, inner: 512, columns: DIM, residual: input,
+ });
+ trace?.("postAttentionResidual", alternate, rows * DIM);
+ this.ops.rmsNorm(pass, alternate, this.weight(`${base}.1.net.0.gamma`), workspace.norm, rows, DIM, ffMapping);
+ trace?.("feedForwardNorm", workspace.norm, rows * DIM);
+ this.ops.linear(pass, workspace.norm, this.weight(`${base}.1.net.1.weight`), this.weight(`${base}.1.net.1.bias`), workspace.wide, {
+ rows, inner: DIM, columns: 1_536, activation: "gelu",
+ });
+ trace?.("feedForwardGelu", workspace.wide, rows * 1_536);
+ this.ops.linear(pass, workspace.wide, this.weight(`${base}.1.net.4.weight`), this.weight(`${base}.1.net.4.bias`), input, {
+ rows, inner: 1_536, columns: DIM, residual: alternate,
+ });
+ trace?.("output", input, rows * DIM);
+ }
+
+ private maskEstimator(
+ pass: GPUComputePassEncoder,
+ prefix: "det" | "cd",
+ stem: number,
+ spectral: GpuTensor,
+ features: GpuTensor,
+ workspace: Workspace,
+ ): void {
+ let maskOffset = 0;
+ const frames = workspace.frames;
+ for (let band = 0; band < BANDS; band += 1) {
+ const width = DICOSE_BANDS[band]! * 4;
+ // MaskEstimator.to_freqs[band] is Sequential(MLP(...), GLU), and the
+ // MLP itself is Sequential(Linear, Tanh, Linear):
+ // `to_freqs.{band}.0.0` / `.0.2` in the checkpoint.
+ const base = `${prefix}.mask_estimators.${stem}.to_freqs.${band}.0`;
+ this.ops.gatherSlice(pass, features, workspace.bandFeature, frames, BANDS * DIM, band * DIM, DIM);
+ this.ops.linear(pass, workspace.bandFeature, this.weight(`${base}.0.weight`), this.weight(`${base}.0.bias`), workspace.maskMid, {
+ rows: frames, inner: DIM, columns: 768, activation: "tanh",
+ });
+ this.ops.linear(pass, workspace.maskMid, this.weight(`${base}.2.weight`), this.weight(`${base}.2.bias`), workspace.maskWide, {
+ rows: frames, inner: 768, columns: width * 2,
+ });
+ this.ops.gluInPlace(pass, workspace.maskWide, workspace.bandMask, frames, width);
+ this.ops.scatterSlice(pass, workspace.bandMask, workspace.globalMask, frames, SPECTRAL_WIDTH, maskOffset, width);
+ maskOffset += width;
+ }
+ this.ops.complexMultiply(pass, spectral, workspace.globalMask, workspace.masked, frames * SPECTRAL_WIDTH / 2);
+ }
+
+ private runAdapter(
+ pass: GPUComputePassEncoder,
+ base: string,
+ input: GpuTensor,
+ output: GpuTensor,
+ rows: number,
+ trace?: (stage: "Linear" | "Gelu", source: GpuTensor) => void,
+ ): void {
+ const workspace = this.workspaceFor(input);
+ this.ops.linear(pass, input, this.weight(`${base}.0.weight`), this.weight(`${base}.0.bias`), workspace.adapter, {
+ rows, inner: DIM, columns: DIM,
+ });
+ trace?.("Linear", workspace.adapter);
+ this.ops.geluInPlace(pass, workspace.adapter, rows * DIM);
+ trace?.("Gelu", workspace.adapter);
+ this.ops.linear(pass, workspace.adapter, this.weight(`${base}.2.weight`), this.weight(`${base}.2.bias`), output, {
+ rows, inner: DIM, columns: DIM,
+ });
+ }
+
+ private createConditions(frames: number): DiCoSeConditions {
+ const rows = frames * BANDS;
+ const tensors: GpuTensor[] = [];
+ const temporary = (elements: number, label: string): GpuTensor => {
+ const tensor = createF16Tensor(this.device, elements, label);
+ tensors.push(tensor);
+ return tensor;
+ };
+ try {
+ const stft = temporary(frames * SPECTRAL_WIDTH, "dicose-condition-stft");
+ const band = temporary(rows * DIM, "dicose-condition-band");
+ const time = Array.from({ length: 8 }, (_, index) =>
+ temporary(rows * DIM, `dicose-condition-time-${index}`),
+ );
+ const frequency = Array.from({ length: 8 }, (_, index) =>
+ temporary(rows * DIM, `dicose-condition-frequency-${index}`),
+ );
+ return new DiCoSeConditions(frames, stft, band, time, frequency);
+ } catch (error) {
+ destroyTensors(tensors);
+ throw error;
+ }
+ }
+
+ private createStftAdapterIntermediates(frames: number): readonly [GpuTensor, GpuTensor, GpuTensor] {
+ const pixels = FREQUENCIES * frames;
+ const tensors: GpuTensor[] = [];
+ const temporary = (elements: number, label: string): GpuTensor => {
+ const tensor = createF16Tensor(this.device, elements, label);
+ tensors.push(tensor);
+ return tensor;
+ };
+ try {
+ return [
+ temporary(pixels * 4, "dicose-stft-adapter-pixels"),
+ temporary(pixels * 128, "dicose-stft-adapter-first"),
+ temporary(pixels * 128, "dicose-stft-adapter-second"),
+ ];
+ } catch (error) {
+ destroyTensors(tensors);
+ throw error;
+ }
+ }
+
+ private createMappingContext(): StemMappingContext {
+ const scaleShifts: GpuTensor[] = [];
+ try {
+ for (let index = 0; index < 32; index += 1) {
+ scaleShifts.push(createF16Tensor(this.device, 768, `dicose-cd-scale-shift-${index}`));
+ }
+ return { scaleShifts };
+ } catch (error) {
+ destroyTensors(scaleShifts);
+ throw error;
+ }
+ }
+
+ private workspace(name: WorkspaceName, frames: number): Workspace {
+ const key = `${name}:${frames}`;
+ const existing = this.workspaces.get(key);
+ if (existing !== undefined) return existing;
+ const rows = frames * BANDS;
+ const tensors: GpuTensor[] = [];
+ const temporary = (elements: number, label: string): GpuTensor => {
+ const tensor = createF16Tensor(this.device, elements, label);
+ tensors.push(tensor);
+ return tensor;
+ };
+ try {
+ const workspace: Workspace = {
+ frames,
+ rows,
+ x: temporary(rows * DIM, `${key}:x`),
+ alternate: temporary(rows * DIM, `${key}:alternate`),
+ norm: temporary(rows * DIM, `${key}:norm`),
+ // Multi-head attention has 8 × 64 = 512 inner features. It is projected
+ // back to the model's 384 features only by `to_out`.
+ attention: temporary(rows * 512, `${key}:attention`),
+ wide: temporary(rows * 1_536, `${key}:wide`),
+ gates: temporary(rows * 8, `${key}:gates`),
+ adapter: temporary(rows * DIM, `${key}:adapter`),
+ bandInput: temporary(frames * 516, `${key}:band-input`),
+ bandNorm: temporary(frames * 516, `${key}:band-norm`),
+ bandFeature: temporary(frames * DIM, `${key}:band-feature`),
+ maskMid: temporary(frames * 768, `${key}:mask-mid`),
+ maskWide: temporary(frames * 1_032, `${key}:mask-wide`),
+ bandMask: temporary(frames * 516, `${key}:band-mask`),
+ globalMask: temporary(frames * SPECTRAL_WIDTH, `${key}:global-mask`),
+ masked: temporary(frames * SPECTRAL_WIDTH, `${key}:masked`),
+ };
+ this.workspaces.set(key, workspace);
+ return workspace;
+ } catch (error) {
+ destroyTensors(tensors);
+ throw error;
+ }
+ }
+
+ /**
+ * A CD arena is deliberately reused across stems and same-size requests, but
+ * a long-lived browser worker must not retain one ~hundreds-of-MiB arena for
+ * every historical input duration.
+ */
+ private cdWorkspace(frames: number): Workspace {
+ if (this.cdWorkspaceFrames !== undefined && this.cdWorkspaceFrames !== frames) {
+ this.releaseWorkspace("cd", this.cdWorkspaceFrames);
+ }
+ const workspace = this.workspace("cd", frames);
+ this.cdWorkspaceFrames = frames;
+ return workspace;
+ }
+
+ private releaseWorkspace(name: WorkspaceName, frames: number): void {
+ const key = `${name}:${frames}`;
+ const workspace = this.workspaces.get(key);
+ if (workspace === undefined) return;
+ destroyWorkspace(workspace);
+ this.workspaces.delete(key);
+ if (name === "cd" && this.cdWorkspaceFrames === frames) this.cdWorkspaceFrames = undefined;
+ }
+
+ private workspaceFor(tensor: GpuTensor): Workspace {
+ for (const workspace of this.workspaces.values()) {
+ if (workspace.x === tensor || workspace.alternate === tensor) return workspace;
+ }
+ throw new Error("DiCoSe transformer tensor is not owned by a workspace");
+ }
+
+ private weight(name: string): GpuWeightTensor {
+ return this.weights.tensor(name);
+ }
+
+ private requireAlive(): void {
+ if (this.destroyed) throw new Error("DiCoSe separator was destroyed");
+ }
+}
+
+async function readCdTrace(
+ device: GPUDevice,
+ pending: readonly PendingTraceTensor[],
+): Promise {
+ if (pending.length === 0) return undefined;
+ const captured = await Promise.all(pending.map(async (entry) => {
+ const physical = await readF16Tensor(device, entry.tensor);
+ if (entry.repeatPeriod === undefined) {
+ if (physical.length !== entry.samples) {
+ throw new Error(`CD trace ${entry.name} returned the wrong sample count`);
+ }
+ return { name: entry.name, elements: entry.elements, values: physical };
+ }
+ if (physical.length !== entry.repeatPeriod) {
+ throw new Error(`CD trace ${entry.name} returned the wrong repeat period`);
+ }
+ const values = new Uint16Array(entry.samples);
+ for (let index = 0; index < values.length; index += 1) {
+ const semanticIndex = Math.floor(index * entry.elements / entry.samples);
+ values[index] = physical[semanticIndex % entry.repeatPeriod]!;
+ }
+ return { name: entry.name, elements: entry.elements, values };
+ }));
+ const grouped = new Map();
+ for (const entry of captured) {
+ let group = grouped.get(entry.name);
+ if (group === undefined) {
+ group = { elements: [], values: [] };
+ grouped.set(entry.name, group);
+ }
+ group.elements.push(entry.elements);
+ group.values.push(entry.values);
+ }
+ const trace: Record = {};
+ for (const [name, group] of grouped) {
+ const length = group.values.reduce((total, values) => total + values.length, 0);
+ const values = new Uint16Array(length);
+ let offset = 0;
+ for (const call of group.values) {
+ values.set(call, offset);
+ offset += call.length;
+ }
+ trace[name] = Object.freeze({
+ elementsPerCall: Object.freeze(group.elements.slice()),
+ values,
+ });
+ }
+ return Object.freeze(trace);
+}
+
+function requireTraceSamples(value: number | undefined, label: string): void {
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
+ throw new RangeError(`${label} trace sample count must be a positive integer`);
+ }
+}
+
+function destroyWorkspace(workspace: Workspace): void {
+ destroyTensors(Object.values(workspace).filter((value): value is GpuTensor =>
+ typeof value === "object" && value !== null && "buffer" in value,
+ ));
+}
+
+function positionalEmbedding(time: number, dim: number): Uint16Array {
+ const values = new Float32Array(dim);
+ const half = dim / 2;
+ for (let index = 0; index < half; index += 1) {
+ const frequency = Math.pow(1 / 10_000, index / (half - 1));
+ // PositionalEmbedding emits [cos(all), sin(all)], then reshapes and
+ // flips its two halves, so the final representation is [sin(all),
+ // cos(all)] rather than interleaved pairs.
+ values[index] = Math.sin(time * frequency);
+ values[half + index] = Math.cos(time * frequency);
+ }
+ return packFloat16(values);
+}
+
+function requireSpectral(spectral: Uint16Array, frames: number): void {
+ if (spectral.length !== frames * SPECTRAL_WIDTH) {
+ throw new RangeError(`Expected ${frames * SPECTRAL_WIDTH} f16 STFT values, received ${spectral.length}`);
+ }
+}
diff --git a/packages/dicose/src/runtime/chunking.ts b/packages/dicose/src/runtime/chunking.ts
new file mode 100644
index 0000000..8dfc342
--- /dev/null
+++ b/packages/dicose/src/runtime/chunking.ts
@@ -0,0 +1,355 @@
+import {
+ DICOSE_SAMPLE_RATE,
+ DICOSE_STFT_HOP_LENGTH,
+ type GaussianSeed,
+ type StereoPcm,
+} from "./audio.js";
+
+const UINT32_UNIT = 1 / 0x1_0000_0000;
+
+/** Released DiCoSe train/eval item geometry: exactly 11 seconds at 44.1 kHz. */
+export const DICOSE_CHUNK_SAMPLES = 485_100;
+/** The upstream overlap-add window linearly fades over 10% of a chunk. */
+export const DICOSE_CHUNK_FADE_SAMPLES = 48_510;
+/** Released Full whole-track inference retains the upstream 50% overlap. */
+export const DICOSE_FULL_CHUNK_STEP_SAMPLES = 242_550;
+/** Fast overlaps only the existing 10% fade region. */
+export const DICOSE_FAST_CHUNK_STEP_SAMPLES = 436_590;
+/** Preserve the included 11.89-second oracle as one graph; chunk above 12 seconds. */
+export const DICOSE_SINGLE_PASS_SAMPLES = 12 * DICOSE_SAMPLE_RATE;
+
+export interface DiCoSeChunkGeometry {
+ readonly stepSamples: number;
+ readonly fadeSamples: number;
+}
+
+export const DICOSE_FULL_CHUNK_GEOMETRY: DiCoSeChunkGeometry = Object.freeze({
+ stepSamples: DICOSE_FULL_CHUNK_STEP_SAMPLES,
+ fadeSamples: DICOSE_CHUNK_FADE_SAMPLES,
+});
+
+export const DICOSE_FAST_CHUNK_GEOMETRY: DiCoSeChunkGeometry = Object.freeze({
+ stepSamples: DICOSE_FAST_CHUNK_STEP_SAMPLES,
+ fadeSamples: DICOSE_CHUNK_FADE_SAMPLES,
+});
+
+export interface DiCoSeChunkSpan {
+ readonly index: number;
+ readonly paddedStart: number;
+ readonly validSamples: number;
+ readonly tailPadding: "zero" | "reflect";
+ readonly chunkReadStart: number;
+ readonly outputStart: number;
+ readonly outputSamples: number;
+}
+
+export interface DiCoSeChunkPlan {
+ readonly sourceSamples: number;
+ readonly paddedSamples: number;
+ readonly borderSamples: number;
+ readonly geometry: DiCoSeChunkGeometry;
+ readonly spans: readonly DiCoSeChunkSpan[];
+}
+
+export interface StereoPcmAccumulator {
+ readonly left: Float32Array;
+ readonly right: Float32Array;
+}
+
+/**
+ * Plan the generic MSST whole-track policy without materializing its reflected
+ * outer padding. Spans that can only affect padding later cropped away are
+ * omitted, unlike the upstream helper's redundant final call.
+ */
+export function makeDiCoSeChunkPlan(
+ sourceSamples: number,
+ geometry: DiCoSeChunkGeometry = DICOSE_FULL_CHUNK_GEOMETRY,
+): DiCoSeChunkPlan {
+ if (!Number.isSafeInteger(sourceSamples) || sourceSamples <= DICOSE_CHUNK_SAMPLES) {
+ throw new RangeError(`Chunked DiCoSe inference requires more than ${DICOSE_CHUNK_SAMPLES} samples`);
+ }
+ validateChunkGeometry(geometry);
+ const frozenGeometry = Object.freeze({
+ stepSamples: geometry.stepSamples,
+ fadeSamples: geometry.fadeSamples,
+ });
+ const borderSamples = DICOSE_CHUNK_SAMPLES - frozenGeometry.stepSamples;
+ const paddedSamples = sourceSamples + borderSamples * 2;
+ const cropStart = borderSamples;
+ const cropEnd = cropStart + sourceSamples;
+ const spans: DiCoSeChunkSpan[] = [];
+ let rawIndex = 0;
+ for (
+ let paddedStart = 0;
+ paddedStart < paddedSamples;
+ paddedStart += frozenGeometry.stepSamples
+ ) {
+ const validSamples = Math.min(DICOSE_CHUNK_SAMPLES, paddedSamples - paddedStart);
+ const intersectionStart = Math.max(paddedStart, cropStart);
+ const intersectionEnd = Math.min(paddedStart + validSamples, cropEnd);
+ const outputSamples = Math.max(0, intersectionEnd - intersectionStart);
+ if (outputSamples > 0) {
+ spans.push(Object.freeze({
+ index: rawIndex,
+ paddedStart,
+ validSamples,
+ tailPadding: validSamples > DICOSE_CHUNK_SAMPLES / 2 ? "reflect" : "zero",
+ chunkReadStart: intersectionStart - paddedStart,
+ outputStart: intersectionStart - cropStart,
+ outputSamples,
+ }));
+ }
+ rawIndex += 1;
+ }
+ return Object.freeze({
+ sourceSamples,
+ paddedSamples,
+ borderSamples,
+ geometry: frozenGeometry,
+ spans: Object.freeze(spans),
+ });
+}
+
+export function makeDiCoSeChunkWindow(
+ chunkSamples = DICOSE_CHUNK_SAMPLES,
+ fadeSamples = DICOSE_CHUNK_FADE_SAMPLES,
+): Float32Array {
+ if (
+ !Number.isSafeInteger(chunkSamples) || chunkSamples <= 0 ||
+ !Number.isSafeInteger(fadeSamples) || fadeSamples <= 1 || fadeSamples * 2 > chunkSamples
+ ) {
+ throw new RangeError("Invalid DiCoSe chunk window geometry");
+ }
+ const window = new Float32Array(chunkSamples);
+ window.fill(1);
+ for (let index = 0; index < fadeSamples; index += 1) {
+ const value = index / (fadeSamples - 1);
+ window[index] = Math.fround(value);
+ window[chunkSamples - 1 - index] = Math.fround(value);
+ }
+ return window;
+}
+
+function validateChunkGeometry(geometry: DiCoSeChunkGeometry): void {
+ const { stepSamples, fadeSamples } = geometry;
+ const borderSamples = DICOSE_CHUNK_SAMPLES - stepSamples;
+ if (
+ !Number.isSafeInteger(stepSamples) || stepSamples <= 0 ||
+ !Number.isSafeInteger(fadeSamples) || fadeSamples <= 1 ||
+ borderSamples < fadeSamples ||
+ stepSamples % DICOSE_STFT_HOP_LENGTH !== 0 ||
+ fadeSamples % DICOSE_STFT_HOP_LENGTH !== 0 ||
+ borderSamples % DICOSE_STFT_HOP_LENGTH !== 0
+ ) {
+ throw new RangeError("Invalid DiCoSe chunk geometry");
+ }
+}
+
+/** Materialize one fixed-size model item from virtual reflected outer padding. */
+export function materializeDiCoSeChunk(
+ source: StereoPcm,
+ plan: DiCoSeChunkPlan,
+ span: DiCoSeChunkSpan,
+): StereoPcm {
+ if (
+ source.sampleRate !== DICOSE_SAMPLE_RATE || source.length !== plan.sourceSamples ||
+ source.left.length !== source.length || source.right.length !== source.length
+ ) {
+ throw new RangeError("DiCoSe chunk source does not match its plan");
+ }
+ const left = new Float32Array(DICOSE_CHUNK_SAMPLES);
+ const right = new Float32Array(DICOSE_CHUNK_SAMPLES);
+ for (let local = 0; local < span.validSamples; local += 1) {
+ const paddedIndex = span.paddedStart + local;
+ const sourceIndex = reflectIndex(paddedIndex - plan.borderSamples, source.length);
+ left[local] = source.left[sourceIndex]!;
+ right[local] = source.right[sourceIndex]!;
+ }
+ if (span.tailPadding === "reflect") {
+ for (let local = span.validSamples; local < DICOSE_CHUNK_SAMPLES; local += 1) {
+ const reflected = reflectIndex(local, span.validSamples);
+ left[local] = left[reflected]!;
+ right[local] = right[reflected]!;
+ }
+ }
+ return Object.freeze({
+ sampleRate: DICOSE_SAMPLE_RATE,
+ length: DICOSE_CHUNK_SAMPLES,
+ left,
+ right,
+ channels: [left, right] as const,
+ });
+}
+
+/** Accumulate every stem from one model item using the shared OLA denominator. */
+export function overlapAddDiCoSeChunk(
+ accumulators: readonly StereoPcmAccumulator[],
+ denominator: Float32Array,
+ chunks: readonly StereoPcm[],
+ span: DiCoSeChunkSpan,
+ window: Float32Array,
+): void {
+ if (accumulators.length === 0 || accumulators.length !== chunks.length) {
+ throw new RangeError("DiCoSe overlap-add requires matching output and chunk stems");
+ }
+ if (window.length !== DICOSE_CHUNK_SAMPLES) {
+ throw new RangeError("DiCoSe overlap-add window has the wrong model length");
+ }
+ const outputEnd = span.outputStart + span.outputSamples;
+ const chunkEnd = span.chunkReadStart + span.outputSamples;
+ if (
+ span.outputStart < 0 || outputEnd > denominator.length ||
+ span.chunkReadStart < 0 || chunkEnd > DICOSE_CHUNK_SAMPLES
+ ) {
+ throw new RangeError("DiCoSe overlap-add span exceeds its buffers");
+ }
+ for (const accumulator of accumulators) {
+ if (accumulator.left.length !== denominator.length || accumulator.right.length !== denominator.length) {
+ throw new RangeError("DiCoSe overlap-add accumulator has the wrong output length");
+ }
+ }
+ for (const chunk of chunks) {
+ if (
+ chunk.sampleRate !== DICOSE_SAMPLE_RATE || chunk.length !== DICOSE_CHUNK_SAMPLES ||
+ chunk.left.length !== chunk.length || chunk.right.length !== chunk.length
+ ) {
+ throw new RangeError("DiCoSe overlap-add chunk has the wrong model geometry");
+ }
+ }
+
+ for (let offset = 0; offset < span.outputSamples; offset += 1) {
+ const sourceIndex = span.chunkReadStart + offset;
+ const outputIndex = span.outputStart + offset;
+ const weight = window[sourceIndex]!;
+ denominator[outputIndex] = denominator[outputIndex]! + weight;
+ for (let stem = 0; stem < chunks.length; stem += 1) {
+ const chunk = chunks[stem]!;
+ const accumulator = accumulators[stem]!;
+ accumulator.left[outputIndex] = accumulator.left[outputIndex]! + chunk.left[sourceIndex]! * weight;
+ accumulator.right[outputIndex] = accumulator.right[outputIndex]! + chunk.right[sourceIndex]! * weight;
+ }
+ }
+}
+
+/** Divide a completed overlap-add sum in place, rejecting gaps instead of emitting NaNs/noise. */
+export function normalizeDiCoSeOverlapAdd(
+ accumulators: readonly StereoPcmAccumulator[],
+ denominator: Float32Array,
+): void {
+ if (accumulators.length === 0) throw new RangeError("DiCoSe overlap-add has no output stems");
+ for (const accumulator of accumulators) {
+ if (accumulator.left.length !== denominator.length || accumulator.right.length !== denominator.length) {
+ throw new RangeError("DiCoSe overlap-add accumulator has the wrong output length");
+ }
+ }
+ for (let index = 0; index < denominator.length; index += 1) {
+ const weight = denominator[index]!;
+ if (!Number.isFinite(weight) || weight <= 0) {
+ throw new Error(`DiCoSe overlap-add left output sample ${index} uncovered`);
+ }
+ for (const accumulator of accumulators) {
+ const left = accumulator.left[index]! / weight;
+ const right = accumulator.right[index]! / weight;
+ if (!Number.isFinite(left) || !Number.isFinite(right)) {
+ throw new Error(`DiCoSe overlap-add produced a non-finite sample at ${index}`);
+ }
+ accumulator.left[index] = Math.fround(left);
+ accumulator.right[index] = Math.fround(right);
+ }
+ }
+}
+
+/**
+ * Add CD noise keyed by padded-track coordinate. Overlapping chunks therefore
+ * see identical noise at the same sample instead of crossfading independent
+ * noise fields and creating a periodic seam in the final consistency affine.
+ */
+export function addDiCoSeCoordinateNoise(
+ source: StereoPcm,
+ span: DiCoSeChunkSpan,
+ stem: number,
+ seed: GaussianSeed,
+ sigma: number,
+): StereoPcm {
+ if (
+ source.sampleRate !== DICOSE_SAMPLE_RATE || source.length !== DICOSE_CHUNK_SAMPLES ||
+ source.left.length !== source.length || source.right.length !== source.length
+ ) {
+ throw new RangeError("DiCoSe coordinate noise requires one fixed-size model item");
+ }
+ if (!Number.isSafeInteger(span.paddedStart) || span.paddedStart < 0) {
+ throw new RangeError("DiCoSe coordinate noise requires a non-negative padded offset");
+ }
+ if (!Number.isSafeInteger(stem) || stem < 0 || !Number.isFinite(sigma) || sigma < 0) {
+ throw new RangeError("Invalid DiCoSe coordinate-noise stream");
+ }
+ const seedWords = gaussianSeedWords(seed);
+ const left = new Float32Array(source.length);
+ const right = new Float32Array(source.length);
+ for (let local = 0; local < source.length; local += 1) {
+ const coordinate = span.paddedStart + local;
+ left[local] = Math.fround(
+ source.left[local]! + sigma * coordinateGaussian(seedWords, stem, 0, coordinate),
+ );
+ right[local] = Math.fround(
+ source.right[local]! + sigma * coordinateGaussian(seedWords, stem, 1, coordinate),
+ );
+ }
+ return Object.freeze({
+ sampleRate: DICOSE_SAMPLE_RATE,
+ length: source.length,
+ left,
+ right,
+ channels: [left, right] as const,
+ });
+}
+
+function reflectIndex(index: number, length: number): number {
+ if (length <= 1) return 0;
+ const period = 2 * length - 2;
+ const wrapped = ((index % period) + period) % period;
+ return wrapped < length ? wrapped : period - wrapped;
+}
+
+function gaussianSeedWords(seed: GaussianSeed): readonly [number, number] {
+ if (typeof seed === "bigint") {
+ if (seed < 0n || seed > 0xffff_ffff_ffff_ffffn) {
+ throw new RangeError("Gaussian bigint seed must fit in unsigned 64 bits");
+ }
+ return [Number(seed & 0xffff_ffffn), Number(seed >> 32n)];
+ }
+ if (!Number.isSafeInteger(seed) || seed < 0) {
+ throw new RangeError("Gaussian numeric seed must be a non-negative safe integer");
+ }
+ return [seed >>> 0, Math.floor(seed / 0x1_0000_0000) >>> 0];
+}
+
+function coordinateGaussian(
+ seed: readonly [number, number],
+ stem: number,
+ channel: number,
+ coordinate: number,
+): number {
+ const pair = Math.floor(coordinate / 2);
+ const low = pair >>> 0;
+ const high = Math.floor(pair / 0x1_0000_0000) >>> 0;
+ const stream = mix32(
+ seed[0] ^ Math.imul(seed[1], 0x9e37_79b9) ^
+ Math.imul(stem + 1, 0x85eb_ca6b) ^ Math.imul(channel + 1, 0xc2b2_ae35),
+ );
+ const first = mix32(stream ^ low ^ Math.imul(high, 0x27d4_eb2d));
+ const second = mix32(first ^ 0xa511_e9b3);
+ const radius = Math.sqrt(-2 * Math.log((first + 0.5) * UINT32_UNIT));
+ const angle = Math.PI * 2 * ((second + 0.5) * UINT32_UNIT);
+ return Math.fround(radius * (coordinate % 2 === 0 ? Math.cos(angle) : Math.sin(angle)));
+}
+
+function mix32(value: number): number {
+ let mixed = value >>> 0;
+ mixed ^= mixed >>> 16;
+ mixed = Math.imul(mixed, 0x7feb_352d);
+ mixed ^= mixed >>> 15;
+ mixed = Math.imul(mixed, 0x846c_a68b);
+ mixed ^= mixed >>> 16;
+ return mixed >>> 0;
+}
diff --git a/packages/dicose/src/runtime/separator.ts b/packages/dicose/src/runtime/separator.ts
new file mode 100644
index 0000000..503ec75
--- /dev/null
+++ b/packages/dicose/src/runtime/separator.ts
@@ -0,0 +1,836 @@
+import { loadGpuWeightPackage, type LoadModelProgress } from "../model/package.js";
+import { requestDiCoSeDevice } from "../webgpu/capabilities.js";
+import {
+ DICOSE_SAMPLE_RATE,
+ DICOSE_STFT_HOP_LENGTH,
+ DICOSE_STFT_N_FFT,
+ SeededGaussian,
+ centeredHannIstft,
+ centeredHannStft,
+ float16BitsToFloat32,
+ float32ToFloat16Bits,
+ resampleStereoSinc,
+ resampleStereoSincToLength,
+ type CenteredHannStft,
+ type GaussianSeed,
+ type StereoPcm,
+} from "./audio.js";
+import {
+ DiCoSeBsrRoFormer,
+ type CdTrace,
+ type DeterministicTrace,
+ type DiCoSeMappingContexts,
+} from "./bs-roformer.js";
+import {
+ addDiCoSeCoordinateNoise,
+ DICOSE_FAST_CHUNK_GEOMETRY,
+ DICOSE_FULL_CHUNK_GEOMETRY,
+ DICOSE_SINGLE_PASS_SAMPLES,
+ makeDiCoSeChunkPlan,
+ makeDiCoSeChunkWindow,
+ materializeDiCoSeChunk,
+ normalizeDiCoSeOverlapAdd,
+ overlapAddDiCoSeChunk,
+ type StereoPcmAccumulator,
+} from "./chunking.js";
+
+export const DICOSE_STEMS = ["drums", "bass", "other", "vocals"] as const;
+export type DiCoSeStem = typeof DICOSE_STEMS[number];
+
+const SIGMA_MAX = 0.003_934;
+const SIGMA_MIN = 0.000_1;
+const SIGMA_DATA = 0.06;
+const DEFAULT_NOISE_SEED = 0xd1c05e;
+const SPECTRAL_COMPONENTS = 4;
+
+export interface SeparatorProgress {
+ readonly phase: "device" | "weights" | "chunk" | "stft" | "deterministic" | "mapping" | "refinement" | "istft";
+ readonly completed: number;
+ readonly total: number;
+ readonly detail?: string;
+}
+
+export interface DiCoSeSeparatorOptions {
+ readonly manifestUrl?: string | URL;
+ /** Test seam for exact-q64 versus blockwise-Flash quality comparisons. */
+ readonly attentionKernel?: "q64" | "flash";
+ readonly onProgress?: (progress: SeparatorProgress) => void;
+}
+
+export interface SeparatePcmOptions {
+ /** Fixed by default so identical input produces identical CD noise. */
+ readonly seed?: GaussianSeed;
+ /** Return the released deterministic separator directly, or apply one-step CD refinement. */
+ readonly outputMode?: "refined" | "deterministic";
+ /** Reference-audit seam; omitted in normal inference, so it has zero runtime cost. */
+ readonly traceSamples?: number;
+}
+
+export interface SeparatorTiming {
+ readonly prepareMs: number;
+ readonly deterministicMs: number;
+ readonly mappingMs: number;
+ readonly refinementMs: number;
+ readonly istftMs: number;
+ readonly totalMs: number;
+}
+
+export interface StemSignalStats {
+ readonly peak: number;
+ readonly rms: number;
+}
+
+export interface SeparatorResult {
+ readonly outputMode: "refined" | "deterministic";
+ readonly stems: Readonly>;
+ readonly timing: SeparatorTiming;
+ readonly trace?: DeterministicTrace;
+ readonly cdTrace?: CdTrace;
+ /** Compact execution checkpoints for unattended correctness monitoring. */
+ readonly diagnostics: Readonly<{
+ readonly deterministic: Readonly>;
+ readonly cdModelOutput?: Readonly>;
+ }>;
+}
+
+/**
+ * Raw-WebGPU DiCoSe inference owner. The BS-RoFormer execution, including
+ * all dense layers, attention, FiLM, masks, and feature adapters, stays on
+ * the GPU. The CPU only performs the published centered STFT/ISTFT boundary
+ * and the one-step CM waveform affine.
+ */
+export class DiCoSeSeparator {
+ private disposed = false;
+
+ private constructor(
+ private readonly device: GPUDevice,
+ private readonly model: DiCoSeBsrRoFormer,
+ private readonly weights: Awaited>,
+ private readonly onProgress?: (progress: SeparatorProgress) => void,
+ ) {}
+
+ static async create(options: DiCoSeSeparatorOptions = {}): Promise {
+ options.onProgress?.({ phase: "device", completed: 0, total: 1, detail: "Requesting WebGPU device" });
+ const device = await requestDiCoSeDevice();
+ options.onProgress?.({ phase: "device", completed: 1, total: 1, detail: "WebGPU device ready" });
+ let weights: Awaited> | undefined;
+ try {
+ weights = await loadGpuWeightPackage(
+ device,
+ options.manifestUrl ?? new URL("/model/manifest.json", globalThis.location.href),
+ (event) => reportWeightProgress(options.onProgress, event),
+ );
+ return new DiCoSeSeparator(
+ device,
+ new DiCoSeBsrRoFormer(
+ device,
+ weights,
+ options.attentionKernel ?? "flash",
+ ),
+ weights,
+ options.onProgress,
+ );
+ } catch (error) {
+ weights?.destroy();
+ device.destroy();
+ throw error;
+ }
+ }
+
+ async separatePcm(
+ input: StereoPcm,
+ options: SeparatePcmOptions = {},
+ ): Promise {
+ this.requireLive();
+ const started = performance.now();
+ const resampleStarted = performance.now();
+ const source = input.sampleRate === DICOSE_SAMPLE_RATE
+ ? input
+ : resampleStereoSinc(input, DICOSE_SAMPLE_RATE);
+ const resampleMs = performance.now() - resampleStarted;
+ if (source.length <= DICOSE_STFT_N_FFT / 2) {
+ throw new RangeError(`DiCoSe requires more than ${DICOSE_STFT_N_FFT / 2} samples`);
+ }
+
+ const outputMode = requireOutputMode(options.outputMode);
+ if (source.length <= DICOSE_SINGLE_PASS_SAMPLES) {
+ const result = await this.withGpuErrorScopes(
+ "single-pass inference",
+ () => this.separateSinglePcm(source, { ...options, outputMode }),
+ );
+ const withInputTiming = {
+ ...result,
+ timing: {
+ ...result.timing,
+ prepareMs: result.timing.prepareMs + resampleMs,
+ totalMs: performance.now() - started,
+ },
+ };
+ return restoreInputTimeline(withInputTiming, input, started);
+ }
+ if (options.traceSamples !== undefined) {
+ throw new RangeError("Deterministic tracing is only supported for a single DiCoSe model chunk");
+ }
+ const result = await this.separateChunkedPcm(source, { ...options, outputMode }, started, resampleMs);
+ return restoreInputTimeline(result, input, started);
+ }
+
+ private async separateSinglePcm(
+ source: StereoPcm,
+ options: SeparatePcmOptions,
+ shared: {
+ readonly mappings?: DiCoSeMappingContexts;
+ readonly random?: SeededGaussian;
+ readonly noise?: (source: StereoPcm, stem: number) => StereoPcm;
+ readonly reportStages?: boolean;
+ } = {},
+ ): Promise {
+ const started = performance.now();
+ const report = shared.reportStages === false
+ ? (_progress: SeparatorProgress): void => {}
+ : (progress: SeparatorProgress): void => this.report(progress);
+
+ report({ phase: "stft", completed: 0, total: 1, detail: "Computing mixture STFT" });
+ const prepareStarted = performance.now();
+ const mixture = stereoStft(source);
+ const mixturePacked = packModelSpectrum(mixture.left, mixture.right);
+ const prepareMs = performance.now() - prepareStarted;
+ report({ phase: "stft", completed: 1, total: 1, detail: `${mixture.left.frameCount} STFT frames` });
+
+ const outputMode = requireOutputMode(options.outputMode);
+ if (outputMode === "deterministic") {
+ report({ phase: "deterministic", completed: 0, total: 1, detail: "Running deterministic BS-RoFormer" });
+ const deterministic = await this.model.runDeterministic(
+ mixturePacked,
+ mixture.left.frameCount,
+ {
+ captureConditions: false,
+ ...(options.traceSamples === undefined ? {} : { traceSamples: options.traceSamples }),
+ },
+ );
+ const deterministicMs = deterministic.elapsedMs;
+ report({ phase: "deterministic", completed: 1, total: 1, detail: "Deterministic stems ready" });
+
+ const istftStarted = performance.now();
+ const deterministicPcm = deterministic.spectra.map((spectrum) => istftStereo(
+ modelSpectrumToStereo(spectrum, mixture.left.frameCount, source.length),
+ ));
+ const stems = namedStems(deterministicPcm);
+ const deterministicStats = {} as Record;
+ for (const name of DICOSE_STEMS) deterministicStats[name] = signalStats(stems[name]);
+ const istftMs = performance.now() - istftStarted;
+ const totalMs = performance.now() - started;
+ report({ phase: "istft", completed: 1, total: 1, detail: "Final deterministic stem waveforms ready" });
+ return {
+ outputMode,
+ stems,
+ timing: {
+ prepareMs,
+ deterministicMs,
+ mappingMs: 0,
+ refinementMs: 0,
+ istftMs,
+ totalMs,
+ },
+ diagnostics: { deterministic: deterministicStats },
+ ...(deterministic.trace === undefined ? {} : { trace: deterministic.trace }),
+ };
+ }
+
+ report({ phase: "deterministic", completed: 0, total: 1, detail: "Running deterministic BS-RoFormer" });
+ const deterministic = await this.model.runDeterministic(
+ mixturePacked,
+ mixture.left.frameCount,
+ options.traceSamples === undefined ? {} : { traceSamples: options.traceSamples },
+ );
+ const deterministicMs = deterministic.elapsedMs;
+ report({ phase: "deterministic", completed: 1, total: 1, detail: "Deterministic stems ready" });
+
+ // `runDeterministic` owns a large condition arena. Establish cleanup as
+ // soon as it has succeeded, so a later mapping/ISTFT failure cannot strand
+ // GPU memory in the reusable browser worker.
+ const conditions = deterministic.conditions;
+ let ownedMappings: DiCoSeMappingContexts | undefined;
+ try {
+ const mappingStarted = performance.now();
+ if (shared.mappings === undefined) {
+ report({ phase: "mapping", completed: 0, total: 1, detail: "Preparing CD FiLM mappings" });
+ ownedMappings = await this.model.createMappings(
+ SIGMA_MAX,
+ options.traceSamples === undefined ? {} : { traceSamples: options.traceSamples },
+ );
+ }
+ const mappings = shared.mappings ?? ownedMappings;
+ if (mappings === undefined) throw new Error("DiCoSe CD mappings were not created");
+ const mappingMs = shared.mappings === undefined ? performance.now() - mappingStarted : 0;
+ if (shared.mappings === undefined) {
+ report({ phase: "mapping", completed: 1, total: 1, detail: "CD FiLM mappings ready" });
+ }
+
+ const istftStarted = performance.now();
+ const initialStems = deterministic.spectra.map((spectrum) =>
+ modelSpectrumToStereo(spectrum, mixture.left.frameCount, source.length),
+ );
+ const deterministicPcm = initialStems.map((spectrum) => istftStereo(spectrum));
+ const deterministicStats = {} as Record;
+ for (let stem = 0; stem < DICOSE_STEMS.length; stem += 1) {
+ deterministicStats[DICOSE_STEMS[stem]!] = signalStats(deterministicPcm[stem]!);
+ }
+ const istftAfterDeterministicMs = performance.now() - istftStarted;
+
+ const random = shared.noise === undefined
+ ? shared.random ?? new SeededGaussian(options.seed ?? DEFAULT_NOISE_SEED)
+ : undefined;
+ const scales = consistencyScales(SIGMA_MAX);
+ const refined = {} as Record;
+ const cdModelOutputStats = {} as Record;
+ const cdTraces: CdTrace[] = [];
+ for (const name of [
+ "cd.stftAdapter",
+ "cd.bandConditionInput",
+ "cd.bandConditionLinear",
+ "cd.bandConditionGelu",
+ "cd.bandCondition",
+ ] as const) {
+ const tensor = deterministic.trace?.[name];
+ if (tensor !== undefined) cdTraces.push(singleCallCdTrace(name, tensor));
+ }
+ if (mappings.trace !== undefined) cdTraces.push(mappings.trace);
+ let refinementMs = 0;
+ let outputIstftMs = 0;
+
+ for (let stem = 0; stem < DICOSE_STEMS.length; stem += 1) {
+ const stemName = DICOSE_STEMS[stem]!;
+ report({
+ phase: "refinement",
+ completed: stem,
+ total: DICOSE_STEMS.length,
+ detail: `Refining ${stemName}`,
+ });
+ const noisy = shared.noise?.(deterministicPcm[stem]!, stem) ?? addNoise(
+ deterministicPcm[stem]!,
+ requireRandom(random),
+ SIGMA_MAX,
+ );
+ const cdInput = scaleStereo(noisy, scales.cIn);
+ const cdStft = stereoStft(cdInput);
+ const refinementStarted = performance.now();
+ const cdPass = await this.model.runCdStem(
+ packModelSpectrum(cdStft.left, cdStft.right),
+ stem,
+ conditions,
+ mappings,
+ options.traceSamples === undefined ? {} : { traceSamples: options.traceSamples },
+ );
+ if (cdPass.trace !== undefined) cdTraces.push(cdPass.trace);
+ refinementMs += performance.now() - refinementStarted;
+ const outputStarted = performance.now();
+ const outputSpectra = modelSpectrumToStereo(cdPass.spectrum, mixture.left.frameCount, source.length);
+ const modelPcm = istftStereo(outputSpectra);
+ cdModelOutputStats[stemName] = signalStats(modelPcm);
+ refined[stemName] = combineConsistency(modelPcm, noisy, scales.cOut, scales.cSkip);
+ outputIstftMs += performance.now() - outputStarted;
+ report({
+ phase: "refinement",
+ completed: stem + 1,
+ total: DICOSE_STEMS.length,
+ detail: `Refined ${stemName}`,
+ });
+ }
+
+ const totalMs = performance.now() - started;
+ report({ phase: "istft", completed: 1, total: 1, detail: "Final stem waveforms ready" });
+ return {
+ outputMode,
+ stems: refined,
+ timing: {
+ prepareMs,
+ deterministicMs,
+ mappingMs,
+ refinementMs,
+ istftMs: istftAfterDeterministicMs + outputIstftMs,
+ totalMs,
+ },
+ diagnostics: {
+ deterministic: deterministicStats,
+ cdModelOutput: cdModelOutputStats,
+ },
+ ...(deterministic.trace === undefined ? {} : { trace: deterministic.trace }),
+ ...(cdTraces.length === 0 ? {} : { cdTrace: mergeCdTraces(cdTraces) }),
+ };
+ } finally {
+ conditions.destroy();
+ ownedMappings?.destroy();
+ }
+ }
+
+ private async separateChunkedPcm(
+ source: StereoPcm,
+ options: SeparatePcmOptions,
+ started: number,
+ resampleMs: number,
+ ): Promise {
+ const outputMode = requireOutputMode(options.outputMode);
+ const geometry = outputMode === "deterministic"
+ ? DICOSE_FAST_CHUNK_GEOMETRY
+ : DICOSE_FULL_CHUNK_GEOMETRY;
+ const plan = makeDiCoSeChunkPlan(source.length, geometry);
+ const window = makeDiCoSeChunkWindow(undefined, geometry.fadeSamples);
+ const denominator = new Float32Array(source.length);
+ const accumulators = DICOSE_STEMS.map(() => ({
+ left: new Float32Array(source.length),
+ right: new Float32Array(source.length),
+ }));
+ const timing = {
+ prepareMs: resampleMs,
+ deterministicMs: 0,
+ mappingMs: 0,
+ refinementMs: 0,
+ istftMs: 0,
+ };
+ const deterministicStats = makeStatsAccumulators();
+ const cdModelOutputStats = outputMode === "refined" ? makeStatsAccumulators() : undefined;
+ const noiseSeed = options.seed ?? DEFAULT_NOISE_SEED;
+ let mappings: DiCoSeMappingContexts | undefined;
+
+ try {
+ if (outputMode === "refined") {
+ this.report({ phase: "mapping", completed: 0, total: 1, detail: "Preparing shared CD FiLM mappings" });
+ const mappingStarted = performance.now();
+ mappings = await this.withGpuErrorScopes(
+ "shared CD mapping",
+ () => this.model.createMappings(SIGMA_MAX),
+ );
+ timing.mappingMs = performance.now() - mappingStarted;
+ this.report({ phase: "mapping", completed: 1, total: 1, detail: "Shared CD FiLM mappings ready" });
+ }
+
+ for (let index = 0; index < plan.spans.length; index += 1) {
+ const span = plan.spans[index]!;
+ this.report({
+ phase: "chunk",
+ completed: index,
+ total: plan.spans.length,
+ detail: `Running model chunk ${index + 1} of ${plan.spans.length}`,
+ });
+ const chunk = materializeDiCoSeChunk(source, plan, span);
+ const result = await this.withGpuErrorScopes(
+ `model chunk ${index + 1}`,
+ () => this.separateSinglePcm(
+ chunk,
+ options,
+ {
+ reportStages: false,
+ ...(mappings === undefined ? {} : { mappings }),
+ ...(outputMode === "deterministic" ? {} : {
+ noise: (deterministic: StereoPcm, stem: number) =>
+ addDiCoSeCoordinateNoise(deterministic, span, stem, noiseSeed, SIGMA_MAX),
+ }),
+ },
+ ),
+ );
+ overlapAddDiCoSeChunk(
+ accumulators,
+ denominator,
+ DICOSE_STEMS.map((name) => result.stems[name]),
+ span,
+ window,
+ );
+ timing.prepareMs += result.timing.prepareMs;
+ timing.deterministicMs += result.timing.deterministicMs;
+ timing.mappingMs += result.timing.mappingMs;
+ timing.refinementMs += result.timing.refinementMs;
+ timing.istftMs += result.timing.istftMs;
+ accumulateStats(deterministicStats, result.diagnostics.deterministic, span.outputSamples);
+ if (cdModelOutputStats !== undefined && result.diagnostics.cdModelOutput !== undefined) {
+ accumulateStats(cdModelOutputStats, result.diagnostics.cdModelOutput, span.outputSamples);
+ }
+ }
+
+ normalizeDiCoSeOverlapAdd(accumulators, denominator);
+ const stems = {} as Record;
+ for (let stem = 0; stem < DICOSE_STEMS.length; stem += 1) {
+ const name = DICOSE_STEMS[stem]!;
+ const accumulator = accumulators[stem]!;
+ stems[name] = stereoPcm(accumulator.left, accumulator.right, DICOSE_SAMPLE_RATE);
+ }
+ const diagnostics = outputMode === "deterministic"
+ ? { deterministic: stemSignalStats(stems) }
+ : {
+ deterministic: finishStats(deterministicStats),
+ cdModelOutput: finishStats(requireStats(cdModelOutputStats)),
+ };
+ this.report({
+ phase: "chunk",
+ completed: plan.spans.length,
+ total: plan.spans.length,
+ detail: `Reassembled ${plan.spans.length} model chunks`,
+ });
+ return {
+ outputMode,
+ stems,
+ timing: {
+ ...timing,
+ totalMs: performance.now() - started,
+ },
+ diagnostics,
+ };
+ } finally {
+ mappings?.destroy();
+ }
+ }
+
+ async dispose(): Promise {
+ if (this.disposed) return;
+ this.disposed = true;
+ this.model.destroy();
+ this.weights.destroy();
+ await this.device.queue.onSubmittedWorkDone();
+ this.device.destroy();
+ }
+
+ private report(progress: SeparatorProgress): void {
+ this.onProgress?.(progress);
+ }
+
+ private async withGpuErrorScopes(label: string, operation: () => Promise): Promise {
+ const filters = ["validation", "out-of-memory", "internal"] as const;
+ for (const filter of filters) this.device.pushErrorScope(filter);
+ let outcome: { readonly value: T } | undefined;
+ let failed = false;
+ let failure: unknown;
+ try {
+ outcome = { value: await operation() };
+ } catch (error) {
+ failed = true;
+ failure = error;
+ }
+
+ const gpuErrors: string[] = [];
+ for (let index = filters.length - 1; index >= 0; index -= 1) {
+ try {
+ const error = await this.device.popErrorScope();
+ if (error !== null) gpuErrors.push(error.message);
+ } catch (error) {
+ gpuErrors.push(error instanceof Error ? error.message : String(error));
+ }
+ }
+ if (failed) throw failure;
+ if (gpuErrors.length > 0) {
+ throw new Error(`DiCoSe ${label} failed WebGPU validation: ${gpuErrors.join("; ")}`);
+ }
+ if (outcome === undefined) throw new Error(`DiCoSe ${label} returned no result`);
+ return outcome.value;
+ }
+
+ private requireLive(): void {
+ if (this.disposed) throw new Error("DiCoSe separator was disposed");
+ }
+}
+
+interface StereoSpectrum {
+ readonly left: CenteredHannStft;
+ readonly right: CenteredHannStft;
+}
+
+function stereoStft(source: StereoPcm): StereoSpectrum {
+ return { left: centeredHannStft(source.left), right: centeredHannStft(source.right) };
+}
+
+function istftStereo(spectrum: StereoSpectrum): StereoPcm {
+ const left = centeredHannIstft(spectrum.left, { length: spectrum.left.sourceLength });
+ const right = centeredHannIstft(spectrum.right, { length: spectrum.right.sourceLength });
+ return stereoPcm(left, right, DICOSE_SAMPLE_RATE);
+}
+
+/** `[time][frequency][left re, left im, right re, right im]` for BS-RoFormer. */
+function packModelSpectrum(left: CenteredHannStft, right: CenteredHannStft): Uint16Array {
+ if (left.frameCount !== right.frameCount || left.binCount !== right.binCount) {
+ throw new Error("Stereo STFT dimensions differ");
+ }
+ const packed = new Uint16Array(left.frameCount * left.binCount * SPECTRAL_COMPONENTS);
+ for (let index = 0; index < left.real.length; index += 1) {
+ const target = index * SPECTRAL_COMPONENTS;
+ packed[target] = float32ToFloat16Bits(left.real[index]!);
+ packed[target + 1] = float32ToFloat16Bits(left.imag[index]!);
+ packed[target + 2] = float32ToFloat16Bits(right.real[index]!);
+ packed[target + 3] = float32ToFloat16Bits(right.imag[index]!);
+ }
+ return packed;
+}
+
+function modelSpectrumToStereo(
+ packed: Uint16Array,
+ frameCount: number,
+ sourceLength: number,
+): StereoSpectrum {
+ const binCount = DICOSE_STFT_N_FFT / 2 + 1;
+ const values = frameCount * binCount;
+ if (packed.length !== values * SPECTRAL_COMPONENTS) {
+ throw new RangeError("DiCoSe model returned a spectrum with an unexpected shape");
+ }
+ const leftReal = new Float32Array(values);
+ const leftImag = new Float32Array(values);
+ const rightReal = new Float32Array(values);
+ const rightImag = new Float32Array(values);
+ for (let index = 0; index < values; index += 1) {
+ const source = index * SPECTRAL_COMPONENTS;
+ // The released Python implementation explicitly zeroes DC before ISTFT.
+ if (index % binCount === 0) continue;
+ leftReal[index] = float16BitsToFloat32(packed[source]!);
+ leftImag[index] = float16BitsToFloat32(packed[source + 1]!);
+ rightReal[index] = float16BitsToFloat32(packed[source + 2]!);
+ rightImag[index] = float16BitsToFloat32(packed[source + 3]!);
+ }
+ return {
+ left: spectrum(leftReal, leftImag, frameCount, sourceLength),
+ right: spectrum(rightReal, rightImag, frameCount, sourceLength),
+ };
+}
+
+function spectrum(
+ real: Float32Array,
+ imag: Float32Array,
+ frameCount: number,
+ sourceLength: number,
+): CenteredHannStft {
+ return {
+ layout: "frame-frequency",
+ window: "hann-periodic",
+ center: true,
+ nFft: DICOSE_STFT_N_FFT,
+ hopLength: DICOSE_STFT_HOP_LENGTH,
+ binCount: DICOSE_STFT_N_FFT / 2 + 1,
+ frameCount,
+ sourceLength,
+ real,
+ imag,
+ };
+}
+
+function addNoise(source: StereoPcm, random: SeededGaussian, sigma: number): StereoPcm {
+ const left = new Float32Array(source.length);
+ const right = new Float32Array(source.length);
+ for (let index = 0; index < source.length; index += 1) left[index] = Math.fround(source.left[index]! + sigma * random.next());
+ for (let index = 0; index < source.length; index += 1) right[index] = Math.fround(source.right[index]! + sigma * random.next());
+ return stereoPcm(left, right, source.sampleRate);
+}
+
+function scaleStereo(source: StereoPcm, scale: number): StereoPcm {
+ const left = new Float32Array(source.length);
+ const right = new Float32Array(source.length);
+ for (let index = 0; index < source.length; index += 1) {
+ left[index] = Math.fround(source.left[index]! * scale);
+ right[index] = Math.fround(source.right[index]! * scale);
+ }
+ return stereoPcm(left, right, source.sampleRate);
+}
+
+function combineConsistency(
+ modelOutput: StereoPcm,
+ noisyInput: StereoPcm,
+ cOut: number,
+ cSkip: number,
+): StereoPcm {
+ const left = new Float32Array(modelOutput.length);
+ const right = new Float32Array(modelOutput.length);
+ for (let index = 0; index < modelOutput.length; index += 1) {
+ left[index] = clampUnit(cOut * modelOutput.left[index]! + cSkip * noisyInput.left[index]!);
+ right[index] = clampUnit(cOut * modelOutput.right[index]! + cSkip * noisyInput.right[index]!);
+ }
+ return stereoPcm(left, right, modelOutput.sampleRate);
+}
+
+function consistencyScales(sigma: number): { readonly cIn: number; readonly cOut: number; readonly cSkip: number } {
+ const denominator = Math.sqrt(sigma * sigma + SIGMA_DATA * SIGMA_DATA);
+ return {
+ cIn: 1 / denominator,
+ cSkip: (SIGMA_DATA * SIGMA_DATA) / ((sigma - SIGMA_MIN) ** 2 + SIGMA_DATA * SIGMA_DATA),
+ cOut: ((sigma - SIGMA_MIN) * SIGMA_DATA) / denominator,
+ };
+}
+
+function namedStems(stems: readonly StereoPcm[]): Readonly> {
+ if (stems.length !== DICOSE_STEMS.length) {
+ throw new Error(`DiCoSe deterministic graph returned ${stems.length} stems`);
+ }
+ const output = {} as Record;
+ for (let index = 0; index < DICOSE_STEMS.length; index += 1) {
+ output[DICOSE_STEMS[index]!] = stems[index]!;
+ }
+ return output;
+}
+
+function stereoPcm(left: Float32Array, right: Float32Array, sampleRate: number): StereoPcm {
+ return { sampleRate, length: left.length, left, right, channels: [left, right] };
+}
+
+function restoreInputTimeline(
+ result: SeparatorResult,
+ input: StereoPcm,
+ started: number,
+): SeparatorResult {
+ const firstStem = result.stems[DICOSE_STEMS[0]];
+ if (firstStem.sampleRate === input.sampleRate && firstStem.length === input.length) return result;
+ const restoreStarted = performance.now();
+ const stems = {} as Record;
+ for (const name of DICOSE_STEMS) {
+ stems[name] = resampleStereoSincToLength(
+ result.stems[name],
+ input.sampleRate,
+ input.length,
+ );
+ }
+ const restoreMs = performance.now() - restoreStarted;
+ return {
+ ...result,
+ stems,
+ timing: {
+ ...result.timing,
+ istftMs: result.timing.istftMs + restoreMs,
+ totalMs: performance.now() - started,
+ },
+ };
+}
+
+function clampUnit(value: number): number {
+ return Math.fround(Math.min(1, Math.max(-1, value)));
+}
+
+function signalStats(source: StereoPcm): StemSignalStats {
+ let peak = 0;
+ let sumSquares = 0;
+ const count = source.length * 2;
+ for (const channel of source.channels) {
+ for (let index = 0; index < channel.length; index += 1) {
+ const sample = channel[index]!;
+ peak = Math.max(peak, Math.abs(sample));
+ sumSquares += sample * sample;
+ }
+ }
+ return { peak, rms: Math.sqrt(sumSquares / count) };
+}
+
+interface SignalStatsAccumulator {
+ peak: number;
+ sumSquares: number;
+ samples: number;
+}
+
+function makeStatsAccumulators(): Record {
+ const output = {} as Record;
+ for (const name of DICOSE_STEMS) output[name] = { peak: 0, sumSquares: 0, samples: 0 };
+ return output;
+}
+
+function accumulateStats(
+ accumulators: Record,
+ stats: Readonly>,
+ outputSamples: number,
+): void {
+ const scalarSamples = outputSamples * 2;
+ for (const name of DICOSE_STEMS) {
+ const accumulator = accumulators[name];
+ const chunk = stats[name];
+ accumulator.peak = Math.max(accumulator.peak, chunk.peak);
+ accumulator.sumSquares += chunk.rms * chunk.rms * scalarSamples;
+ accumulator.samples += scalarSamples;
+ }
+}
+
+function finishStats(
+ accumulators: Record,
+): Readonly> {
+ const output = {} as Record;
+ for (const name of DICOSE_STEMS) {
+ const accumulator = accumulators[name];
+ if (accumulator.samples <= 0) throw new Error(`DiCoSe omitted ${name} diagnostics`);
+ output[name] = {
+ peak: accumulator.peak,
+ rms: Math.sqrt(accumulator.sumSquares / accumulator.samples),
+ };
+ }
+ return output;
+}
+
+function stemSignalStats(
+ stems: Readonly>,
+): Readonly> {
+ const output = {} as Record;
+ for (const name of DICOSE_STEMS) output[name] = signalStats(stems[name]);
+ return output;
+}
+
+function requireStats(
+ value: Record | undefined,
+): Record {
+ if (value === undefined) throw new Error("DiCoSe CD diagnostics accumulator is missing");
+ return value;
+}
+
+function requireRandom(value: SeededGaussian | undefined): SeededGaussian {
+ if (value === undefined) throw new Error("DiCoSe CD noise generator is missing");
+ return value;
+}
+
+function singleCallCdTrace(
+ name: string,
+ tensor: { readonly elements: number; readonly values: Uint16Array },
+): CdTrace {
+ return Object.freeze({
+ [name]: Object.freeze({
+ elementsPerCall: Object.freeze([tensor.elements]),
+ values: tensor.values,
+ }),
+ });
+}
+
+function mergeCdTraces(traces: readonly CdTrace[]): CdTrace {
+ const groups = new Map();
+ for (const trace of traces) {
+ for (const [name, tensor] of Object.entries(trace)) {
+ let group = groups.get(name);
+ if (group === undefined) {
+ group = { elements: [], values: [] };
+ groups.set(name, group);
+ }
+ group.elements.push(...tensor.elementsPerCall);
+ group.values.push(tensor.values);
+ }
+ }
+ const merged: Record = {};
+ for (const [name, group] of groups) {
+ const length = group.values.reduce((total, values) => total + values.length, 0);
+ const values = new Uint16Array(length);
+ let offset = 0;
+ for (const call of group.values) {
+ values.set(call, offset);
+ offset += call.length;
+ }
+ merged[name] = Object.freeze({
+ elementsPerCall: Object.freeze(group.elements.slice()),
+ values,
+ });
+ }
+ return Object.freeze(merged);
+}
+
+function requireOutputMode(value: SeparatePcmOptions["outputMode"]): "refined" | "deterministic" {
+ const outputMode = value ?? "refined";
+ if (outputMode !== "refined" && outputMode !== "deterministic") {
+ throw new RangeError(`Unsupported DiCoSe output mode: ${String(outputMode)}`);
+ }
+ return outputMode;
+}
+
+function reportWeightProgress(
+ progress: ((progress: SeparatorProgress) => void) | undefined,
+ event: LoadModelProgress,
+): void {
+ progress?.({
+ phase: "weights",
+ completed: event.loadedBytes,
+ total: event.totalBytes,
+ detail: event.phase === "manifest" ? "Model manifest ready" : "Streaming f16 weights to GPU",
+ });
+}
diff --git a/packages/dicose/src/webgpu/capabilities.ts b/packages/dicose/src/webgpu/capabilities.ts
new file mode 100644
index 0000000..f7a1238
--- /dev/null
+++ b/packages/dicose/src/webgpu/capabilities.ts
@@ -0,0 +1,77 @@
+export interface DiCoSeSupport {
+ readonly supported: boolean;
+ readonly errors: readonly string[];
+ readonly adapter: Readonly<{
+ readonly vendor: string;
+ readonly architecture: string;
+ readonly device: string;
+ readonly description: string;
+ }> | null;
+}
+
+const REQUIRED_FEATURES = ["shader-f16", "subgroups"] as const;
+const REQUIRED_BUFFER_BYTES = 1_024 * 1024 * 1024;
+const REQUIRED_WORKGROUP_STORAGE_BYTES = 25_344;
+
+export async function checkSupport(): Promise {
+ if (!globalThis.isSecureContext) return unsupported("WebGPU requires HTTPS or localhost");
+ if (navigator.gpu === undefined) return unsupported("This browser does not expose WebGPU");
+ const adapter = await navigator.gpu.requestAdapter({
+ powerPreference: "high-performance",
+ forceFallbackAdapter: false,
+ });
+ if (adapter === null) return unsupported("No high-performance WebGPU adapter is available");
+ const errors: string[] = [];
+ for (const feature of REQUIRED_FEATURES) {
+ if (!adapter.features.has(feature)) errors.push(`DiCoSe requires ${feature}`);
+ }
+ // The raw attention and RMSNorm kernels map one logical 64-wide head over
+ // two lanes per member of a fixed eight-subgroup, 256-lane workgroup.
+ // Accepting variable subgroup widths would silently produce incorrect audio.
+ if (adapter.info.subgroupMinSize !== 32 || adapter.info.subgroupMaxSize !== 32) {
+ errors.push("DiCoSe requires fixed 32-wide WebGPU subgroups");
+ }
+ if (adapter.limits.maxBufferSize < REQUIRED_BUFFER_BYTES) {
+ errors.push(`DiCoSe requires a ${REQUIRED_BUFFER_BYTES}-byte GPU buffer`);
+ }
+ if (adapter.limits.maxStorageBufferBindingSize < REQUIRED_BUFFER_BYTES) {
+ errors.push(`DiCoSe requires a ${REQUIRED_BUFFER_BYTES}-byte storage binding`);
+ }
+ if (adapter.limits.maxComputeWorkgroupStorageSize < REQUIRED_WORKGROUP_STORAGE_BYTES) {
+ errors.push(`DiCoSe requires ${REQUIRED_WORKGROUP_STORAGE_BYTES} bytes of workgroup storage`);
+ }
+ const info = adapter.info;
+ return {
+ supported: errors.length === 0,
+ errors,
+ adapter: {
+ vendor: info.vendor,
+ architecture: info.architecture,
+ device: info.device,
+ description: info.description,
+ },
+ };
+}
+
+export async function requestDiCoSeDevice(): Promise {
+ const support = await checkSupport();
+ if (!support.supported) throw new Error(support.errors.join("; "));
+ const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
+ if (adapter === null) throw new Error("WebGPU adapter disappeared");
+ // Device limits default to conservative WebGPU minima even when an adapter
+ // advertises much larger storage. Request the ceiling explicitly: the f16
+ // package itself is ~623 MB and several activation buffers are hundreds of
+ // megabytes on the supplied audio.
+ return await adapter.requestDevice({
+ requiredFeatures: [...REQUIRED_FEATURES],
+ requiredLimits: {
+ maxBufferSize: REQUIRED_BUFFER_BYTES,
+ maxStorageBufferBindingSize: REQUIRED_BUFFER_BYTES,
+ maxComputeWorkgroupStorageSize: REQUIRED_WORKGROUP_STORAGE_BYTES,
+ },
+ });
+}
+
+function unsupported(error: string): DiCoSeSupport {
+ return { supported: false, errors: [error], adapter: null };
+}
diff --git a/packages/dicose/src/webgpu/ops.ts b/packages/dicose/src/webgpu/ops.ts
new file mode 100644
index 0000000..c45c078
--- /dev/null
+++ b/packages/dicose/src/webgpu/ops.ts
@@ -0,0 +1,1919 @@
+import type { GpuWeightTensor } from "../model/package.js";
+import { createF16Tensor, destroyTensors, type GpuTensor } from "./tensor.js";
+
+const PARAMETER_BYTES = 256;
+const PARAMETER_SLOTS = 32_768;
+
+export type Activation = "none" | "gelu" | "tanh";
+export type AttentionKernel = "query8" | "q64" | "flash";
+export type PackedAccumulation = "exact" | "k2" | "k4";
+
+export interface AttentionGeometry {
+ readonly sequences: number;
+ readonly tokens: number;
+ readonly strided?: boolean;
+}
+
+export interface AttentionDescriptor extends AttentionGeometry {
+ readonly kernel?: AttentionKernel;
+ readonly gates?: GpuTensor;
+ readonly rotatedKeys?: boolean;
+}
+
+export interface LinearDescriptor {
+ readonly rows: number;
+ readonly inner: number;
+ readonly columns: number;
+ readonly activation?: Activation;
+ /** Output ownership width; may split converter-native N256 tiles in half. */
+ readonly outputTileColumns?: 128 | 256;
+ /** Load and source-unroll four adjacent K operands while retaining FP32 FMA order. */
+ readonly vectorizeK?: boolean;
+ /** Packed-owner reduction arithmetic; approximate modes retain FP32 running state. */
+ readonly accumulation?: PackedAccumulation;
+ /** Add after the projection's f16 rounding, preserving the former add pass. */
+ readonly residual?: GpuTensor;
+ /** Rotate the K slice of a packed 3×512 QKV projection after f16 rounding. */
+ readonly rotaryKeys?: AttentionGeometry;
+}
+
+interface DynamicBindGroup {
+ readonly bindGroup: GPUBindGroup;
+ readonly parameterOffset: number;
+}
+
+/**
+ * The small primitive set used by both BS-RoFormer graphs. All activation and
+ * weight storage is f16; reductions and attention softmax use f32.
+ */
+export class GpuOps {
+ private readonly parameters: GPUBuffer;
+ private parameterCursor = 0;
+ private readonly zero: GPUBuffer;
+ private readonly pipelines = new Map();
+ private readonly layouts = new Map();
+ private readonly rotaryTables: GpuTensor[] = [];
+ private rotaryTable: GpuTensor | undefined;
+ private rotaryTableTokens = 0;
+ private destroyed = false;
+
+ constructor(
+ readonly device: GPUDevice,
+ private readonly defaultAttentionKernel: AttentionKernel = "flash",
+ private readonly defaultPackedAccumulation: PackedAccumulation = "exact",
+ ) {
+ this.parameters = device.createBuffer({
+ label: "dicose-dynamic-parameters",
+ size: PARAMETER_BYTES * PARAMETER_SLOTS,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ this.zero = device.createBuffer({
+ label: "dicose-zero-bias",
+ size: 16_384,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+ }
+
+ beginGraph(): void {
+ this.requireAlive();
+ this.parameterCursor = 0;
+ }
+
+ linear(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ weight: GpuWeightTensor,
+ bias: GpuWeightTensor | undefined,
+ output: GpuTensor,
+ descriptor: LinearDescriptor,
+ ): void {
+ const activation = descriptor.activation ?? "none";
+ const packedTileColumns = packedLinearTileColumns(weight.layout);
+ if (packedTileColumns !== undefined) {
+ this.packedLinear(
+ pass,
+ input,
+ weight,
+ bias,
+ output,
+ descriptor,
+ activation,
+ packedTileColumns,
+ );
+ return;
+ }
+ if (descriptor.outputTileColumns !== undefined) {
+ throw new Error(`Packed output ownership requires a packed weight: ${weight.name}`);
+ }
+ if (descriptor.accumulation !== undefined && descriptor.accumulation !== "exact") {
+ throw new Error(`Approximate accumulation requires a packed weight: ${weight.name}`);
+ }
+ if (descriptor.residual !== undefined || descriptor.rotaryKeys !== undefined) {
+ throw new Error(`Fused packed-linear post-op requires a packed weight: ${weight.name}`);
+ }
+ const pipeline = this.pipeline("linear", LINEAR_WGSL);
+ const params = this.parametersFor([
+ descriptor.rows,
+ descriptor.inner,
+ descriptor.columns,
+ activationCode(activation),
+ ]);
+ const bindings = this.bind("linear", [
+ storage(input.buffer, input.byteLength),
+ storage(weight.buffer, weight.byteLength, weight.offset),
+ storage(bias?.buffer ?? this.zero, bias?.byteLength ?? 16_384, bias?.offset ?? 0),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [params]);
+ pass.dispatchWorkgroups(
+ Math.ceil(descriptor.columns / 16),
+ Math.ceil(descriptor.rows / 16),
+ );
+ }
+
+ private packedLinear(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ weight: GpuWeightTensor,
+ bias: GpuWeightTensor | undefined,
+ output: GpuTensor,
+ descriptor: LinearDescriptor,
+ activation: Activation,
+ tileColumns: 128 | 256,
+ ): void {
+ if (
+ descriptor.inner % 32 !== 0 || descriptor.columns % tileColumns !== 0 ||
+ weight.shape.length !== 2 || weight.shape[0] !== descriptor.inner ||
+ weight.shape[1] !== descriptor.columns
+ ) {
+ throw new RangeError(
+ `Invalid packed linear ${descriptor.rows}x${descriptor.inner}x${descriptor.columns} for ${weight.name}`,
+ );
+ }
+ const rotaryKeys = descriptor.rotaryKeys;
+ const optimizeFullRowTiles = descriptor.rows >= 32;
+ const outputTileColumns = descriptor.outputTileColumns ?? (
+ optimizeFullRowTiles ? 128 : tileColumns
+ );
+ const vectorizeK = descriptor.vectorizeK ?? (
+ optimizeFullRowTiles && outputTileColumns === 128
+ );
+ if (outputTileColumns > tileColumns || tileColumns % outputTileColumns !== 0) {
+ throw new RangeError(`Invalid packed output ownership for ${weight.name}`);
+ }
+ const requestedAccumulation = descriptor.accumulation ?? this.defaultPackedAccumulation;
+ const approximateEligible = outputTileColumns === 128 && vectorizeK;
+ if (
+ descriptor.accumulation !== undefined && requestedAccumulation !== "exact" &&
+ !approximateEligible
+ ) {
+ throw new RangeError(
+ `Packed ${requestedAccumulation} accumulation requires owner128/K4 loads for ${weight.name}`,
+ );
+ }
+ // A graph-wide approximate default deliberately leaves incompatible small-row
+ // and N256 owners exact. Explicit incompatible requests remain programmer errors.
+ const accumulation = approximateEligible ? requestedAccumulation : "exact";
+ if (rotaryKeys !== undefined) {
+ if (
+ descriptor.residual !== undefined || activation !== "none" ||
+ descriptor.columns !== 1_536 || tileColumns !== 256 ||
+ !Number.isSafeInteger(rotaryKeys.sequences) || rotaryKeys.sequences <= 0 ||
+ !Number.isSafeInteger(rotaryKeys.tokens) || rotaryKeys.tokens <= 0 ||
+ rotaryKeys.sequences * rotaryKeys.tokens !== descriptor.rows
+ ) {
+ throw new RangeError(`Invalid fused K rotation for ${weight.name}`);
+ }
+ }
+ const pipelineKey = `linear-${tileColumns}x32-owner${outputTileColumns}-${vectorizeK ? "loadk4" : "loadk1"}-${accumulation}-${descriptor.inner}x${descriptor.columns}-${activation}`;
+ const params = this.parametersFor([
+ descriptor.rows,
+ rotaryKeys?.sequences ?? 0,
+ rotaryKeys?.tokens ?? 0,
+ rotaryKeys?.strided === true ? 1 : 0,
+ ]);
+ const commonBindings = [
+ storage(input.buffer, input.byteLength),
+ storage(weight.buffer, weight.byteLength, weight.offset),
+ storage(bias?.buffer ?? this.zero, bias?.byteLength ?? 16_384, bias?.offset ?? 0),
+ storage(output.buffer, output.byteLength),
+ ];
+ if (rotaryKeys !== undefined) {
+ const rotaryTable = this.prepareRotaryTable(pass, rotaryKeys.tokens);
+ const normalBindings = this.bind("linear-packed", [
+ ...commonBindings,
+ dynamicUniform(this.parameters),
+ ]);
+ const rotaryBindings = this.bind("linear-packed-rotary-keys", [
+ ...commonBindings,
+ storage(rotaryTable.buffer, rotaryTable.byteLength),
+ dynamicUniformAt(this.parameters, 5),
+ ], 5);
+ const rowWorkgroups = Math.ceil(descriptor.rows / 32);
+ const sliceWorkgroups = 512 / outputTileColumns;
+ const dispatch = (
+ pipeline: GPUComputePipeline,
+ bindings: DynamicBindGroup,
+ ): void => {
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [params]);
+ pass.dispatchWorkgroups(sliceWorkgroups, rowWorkgroups);
+ };
+ dispatch(this.pipeline(
+ pipelineKey,
+ packedLinearWgsl(descriptor.inner, descriptor.columns, tileColumns, outputTileColumns, vectorizeK, accumulation, activation, false, false),
+ "linear-packed",
+ ), normalBindings);
+ dispatch(this.pipeline(
+ `${pipelineKey}-rotary-keys-offset-2`,
+ packedLinearWgsl(descriptor.inner, descriptor.columns, tileColumns, outputTileColumns, vectorizeK, accumulation, activation, false, true, sliceWorkgroups),
+ "linear-packed-rotary-keys",
+ ), rotaryBindings);
+ dispatch(this.pipeline(
+ `${pipelineKey}-offset-4`,
+ packedLinearWgsl(descriptor.inner, descriptor.columns, tileColumns, outputTileColumns, vectorizeK, accumulation, activation, false, false, sliceWorkgroups * 2),
+ "linear-packed",
+ ), normalBindings);
+ return;
+ }
+ const hasResidual = descriptor.residual !== undefined;
+ const packedLayout = hasResidual ? "linear-packed-residual" : "linear-packed";
+ const pipeline = this.pipeline(
+ `${pipelineKey}${hasResidual ? "-residual" : ""}`,
+ packedLinearWgsl(
+ descriptor.inner,
+ descriptor.columns,
+ tileColumns,
+ outputTileColumns,
+ vectorizeK,
+ accumulation,
+ activation,
+ hasResidual,
+ false,
+ ),
+ packedLayout,
+ );
+ const bindings = this.bind(packedLayout, [
+ ...commonBindings,
+ ...(descriptor.residual === undefined ? [] : [
+ storage(descriptor.residual.buffer, descriptor.residual.byteLength),
+ ]),
+ hasResidual ? dynamicUniformAt(this.parameters, 5) : dynamicUniform(this.parameters),
+ ], hasResidual ? 5 : 4);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [params]);
+ pass.dispatchWorkgroups(
+ descriptor.columns / outputTileColumns,
+ Math.ceil(descriptor.rows / 32),
+ );
+ }
+
+ rmsNorm(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ gamma: GpuWeightTensor,
+ output: GpuTensor,
+ rows: number,
+ columns: number,
+ scaleShift?: GpuTensor,
+ owner: "auto" | "row1" = "auto",
+ workgroupWidthLimit = this.device.limits.maxComputeWorkgroupsPerDimension,
+ ): void {
+ if (
+ !Number.isSafeInteger(workgroupWidthLimit) ||
+ workgroupWidthLimit <= 0 ||
+ workgroupWidthLimit > this.device.limits.maxComputeWorkgroupsPerDimension
+ ) {
+ throw new RangeError(`Invalid RMSNorm workgroup-width limit ${workgroupWidthLimit}`);
+ }
+ const rowsPerWorkgroup = owner === "auto" ? 8 : 1;
+ const pipeline = this.pipeline(
+ owner === "auto" ? "rmsnorm-rows8" : "rmsnorm-row1",
+ owner === "auto" ? RMSNORM_ROWS8_WGSL : RMSNORM_WGSL,
+ "rmsnorm",
+ );
+ const workgroups = Math.ceil(rows / rowsPerWorkgroup);
+ const workgroupWidth = Math.min(workgroups, workgroupWidthLimit);
+ const workgroupHeight = Math.ceil(workgroups / workgroupWidth);
+ if (workgroupHeight > this.device.limits.maxComputeWorkgroupsPerDimension) {
+ throw new RangeError(`DiCoSe RMSNorm dispatch exceeds the device workgroup grid for ${rows} rows`);
+ }
+ const params = this.parametersFor([rows, columns, scaleShift === undefined ? 0 : 1, workgroupWidth]);
+ const bindings = this.bind("rmsnorm", [
+ storage(input.buffer, input.byteLength),
+ storage(gamma.buffer, gamma.byteLength, gamma.offset),
+ storage(scaleShift?.buffer ?? this.zero, scaleShift?.byteLength ?? 16_384),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [params]);
+ pass.dispatchWorkgroups(workgroupWidth, workgroupHeight);
+ }
+
+ add(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ destination: GpuTensor,
+ elements: number,
+ ): void {
+ this.elementwise(pass, "add", ADD_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(destination.buffer, destination.byteLength),
+ dynamicUniform(this.parameters),
+ ], [elements, 0, 0, 0]);
+ }
+
+ copy(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ destination: GpuTensor,
+ elements: number,
+ ): void {
+ this.elementwise(pass, "copy", COPY_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(destination.buffer, destination.byteLength),
+ dynamicUniform(this.parameters),
+ ], [elements, 0, 0, 0]);
+ }
+
+ /** Capture evenly spaced f16 words without reading a production tensor back in full. */
+ sampleEven(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ output: GpuTensor,
+ elements: number,
+ samples: number,
+ ): void {
+ if (
+ !Number.isSafeInteger(elements) || elements <= 0 ||
+ !Number.isSafeInteger(samples) || samples <= 0 || samples > elements ||
+ output.byteLength !== samples * 2
+ ) {
+ throw new RangeError("Invalid evenly sampled tensor geometry");
+ }
+ this.elementwise(pass, "sample-even", SAMPLE_EVEN_WGSL, [
+ storage(input.buffer, input.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [elements, samples, 0, 0], Math.ceil(samples / 256));
+ }
+
+ transposeTB(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ output: GpuTensor,
+ time: number,
+ bands: number,
+ dim: number,
+ ): void {
+ this.elementwise(pass, "transpose-tb", TRANSPOSE_TB_WGSL, [
+ storage(input.buffer, input.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [time, bands, dim, 0], Math.ceil(time * bands * dim / 256));
+ }
+
+ gatherSlice(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ output: GpuTensor,
+ rows: number,
+ sourceWidth: number,
+ offset: number,
+ width: number,
+ ): void {
+ this.elementwise(pass, "gather-slice", GATHER_SLICE_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [rows, sourceWidth, offset, width], Math.ceil(rows * width / 256));
+ }
+
+ scatterSlice(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ output: GpuTensor,
+ rows: number,
+ destinationWidth: number,
+ offset: number,
+ width: number,
+ ): void {
+ this.elementwise(pass, "scatter-slice", SCATTER_SLICE_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [rows, destinationWidth, offset, width], Math.ceil(rows * width / 256));
+ }
+
+ spectralToPixels(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ output: GpuTensor,
+ time: number,
+ frequencies: number,
+ channels: number,
+ ): void {
+ this.elementwise(pass, "spectral-to-pixels", SPECTRAL_TO_PIXELS_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [time, frequencies, channels, 0], Math.ceil(time * frequencies * channels / 256));
+ }
+
+ pixelsToSpectral(
+ pass: GPUComputePassEncoder,
+ source: GpuTensor,
+ output: GpuTensor,
+ time: number,
+ frequencies: number,
+ channels: number,
+ ): void {
+ this.elementwise(pass, "pixels-to-spectral", PIXELS_TO_SPECTRAL_WGSL, [
+ storage(source.buffer, source.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [time, frequencies, channels, 0], Math.ceil(time * frequencies * channels / 256));
+ }
+
+ attention(
+ pass: GPUComputePassEncoder,
+ qkv: GpuTensor,
+ output: GpuTensor,
+ descriptor: AttentionDescriptor,
+ ): void {
+ const {
+ sequences,
+ tokens,
+ kernel = this.defaultAttentionKernel,
+ gates,
+ strided = false,
+ rotatedKeys = false,
+ } = descriptor;
+ if (
+ !Number.isSafeInteger(sequences) || sequences <= 0 ||
+ !Number.isSafeInteger(tokens) || tokens <= 0
+ ) {
+ throw new RangeError("Invalid attention geometry");
+ }
+ const grouped = kernel !== "query8";
+ if (!grouped && (strided || rotatedKeys)) {
+ throw new Error("Strided or pre-rotated-key attention requires a grouped kernel");
+ }
+ const pipelineName = grouped
+ ? `${kernel}${rotatedKeys ? "-rotated-keys" : ""}`
+ : "attention";
+ const pipeline = this.pipeline(
+ pipelineName,
+ kernel === "flash"
+ ? attentionFlashWgsl(rotatedKeys)
+ : kernel === "q64" ? attentionQ64Wgsl(rotatedKeys) : ATTENTION_WGSL,
+ grouped ? "attention-grouped" : "attention",
+ );
+ const parameterOffset = this.parametersFor([
+ sequences,
+ tokens,
+ gates === undefined ? 0 : 1,
+ strided ? 1 : 0,
+ ]);
+ const rotaryTable = grouped ? this.prepareRotaryTable(pass, tokens) : undefined;
+ const bindings = this.bind(grouped ? "attention-grouped" : "attention", [
+ storage(qkv.buffer, qkv.byteLength),
+ storage(output.buffer, output.byteLength),
+ ...(rotaryTable === undefined ? [] : [storage(rotaryTable.buffer, rotaryTable.byteLength)]),
+ ...(grouped ? [storage(gates?.buffer ?? this.zero, gates?.byteLength ?? 16_384)] : []),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ const queriesPerWorkgroup = grouped ? 64 : 8;
+ pass.dispatchWorkgroups(Math.ceil(tokens / queriesPerWorkgroup), 8, sequences);
+ }
+
+ applyGates(
+ pass: GPUComputePassEncoder,
+ context: GpuTensor,
+ gates: GpuTensor,
+ rows: number,
+ ): void {
+ this.elementwise(pass, "gates", GATES_WGSL, [
+ storage(context.buffer, context.byteLength),
+ storage(gates.buffer, gates.byteLength),
+ dynamicUniform(this.parameters),
+ ], [rows, 0, 0, 0], Math.ceil(rows * 512 / 256));
+ }
+
+ complexMultiply(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ mask: GpuTensor,
+ output: GpuTensor,
+ complexes: number,
+ ): void {
+ this.elementwise(pass, "complex-multiply", COMPLEX_MULTIPLY_WGSL, [
+ storage(input.buffer, input.byteLength),
+ storage(mask.buffer, mask.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [complexes, 0, 0, 0]);
+ }
+
+ affine(
+ pass: GPUComputePassEncoder,
+ modelOutput: GpuTensor,
+ noisyInput: GpuTensor,
+ output: GpuTensor,
+ elements: number,
+ cOut: number,
+ cSkip: number,
+ ): void {
+ const params = new ArrayBuffer(16);
+ const u32 = new Uint32Array(params);
+ const f32 = new Float32Array(params);
+ u32[0] = elements;
+ f32[1] = cOut;
+ f32[2] = cSkip;
+ this.elementwise(pass, "affine", AFFINE_WGSL, [
+ storage(modelOutput.buffer, modelOutput.byteLength),
+ storage(noisyInput.buffer, noisyInput.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], new Uint32Array(params));
+ }
+
+ conv2d(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ weight: GpuWeightTensor,
+ bias: GpuWeightTensor | undefined,
+ output: GpuTensor,
+ height: number,
+ width: number,
+ inChannels: number,
+ outChannels: number,
+ kernel: 1 | 3,
+ owner: "auto" | "generic" = "auto",
+ ): void {
+ if (owner === "auto" && kernel === 3 && inChannels === 4 && outChannels === 128) {
+ const pipeline = this.pipeline("conv3x3-4x128", CONV3X3_4X128_WGSL, "conv2d");
+ const rows = height * width;
+ const parameterOffset = this.parametersFor([rows, width, height, 0]);
+ const bindings = this.bind("conv2d", [
+ storage(input.buffer, input.byteLength),
+ storage(weight.buffer, weight.byteLength, weight.offset),
+ storage(bias?.buffer ?? this.zero, bias?.byteLength ?? 16_384, bias?.offset ?? 0),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ pass.dispatchWorkgroups(1, Math.ceil(rows / 32));
+ return;
+ }
+ if (owner === "auto" && kernel === 1 && inChannels === 128 && outChannels === 128) {
+ const pipeline = this.pipeline("conv1x1-128", CONV1X1_128_WGSL, "conv2d");
+ const rows = height * width;
+ const parameterOffset = this.parametersFor([rows, 0, 0, 0]);
+ const bindings = this.bind("conv2d", [
+ storage(input.buffer, input.byteLength),
+ storage(weight.buffer, weight.byteLength, weight.offset),
+ storage(bias?.buffer ?? this.zero, bias?.byteLength ?? 16_384, bias?.offset ?? 0),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ pass.dispatchWorkgroups(1, Math.ceil(rows / 32));
+ return;
+ }
+ const pipeline = this.pipeline("conv2d", CONV2D_WGSL);
+ const parameterOffset = this.parametersFor([
+ height,
+ width,
+ inChannels,
+ outChannels,
+ kernel,
+ 0,
+ 0,
+ 0,
+ ]);
+ const bindings = this.bind("conv2d", [
+ storage(input.buffer, input.byteLength),
+ storage(weight.buffer, weight.byteLength, weight.offset),
+ storage(bias?.buffer ?? this.zero, bias?.byteLength ?? 16_384, bias?.offset ?? 0),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ pass.dispatchWorkgroups(Math.ceil(width / 8), Math.ceil(height / 8), outChannels);
+ }
+
+ geluInPlace(pass: GPUComputePassEncoder, tensor: GpuTensor, elements: number): void {
+ this.elementwise(pass, "gelu", GELU_WGSL, [
+ storage(tensor.buffer, tensor.byteLength),
+ dynamicUniform(this.parameters),
+ ], [elements, 0, 0, 0]);
+ }
+
+ tanhInPlace(pass: GPUComputePassEncoder, tensor: GpuTensor, elements: number): void {
+ this.elementwise(pass, "tanh", TANH_WGSL, [
+ storage(tensor.buffer, tensor.byteLength),
+ dynamicUniform(this.parameters),
+ ], [elements, 0, 0, 0]);
+ }
+
+ gluInPlace(
+ pass: GPUComputePassEncoder,
+ input: GpuTensor,
+ output: GpuTensor,
+ rows: number,
+ columns: number,
+ ): void {
+ this.elementwise(pass, "glu", GLU_WGSL, [
+ storage(input.buffer, input.byteLength),
+ storage(output.buffer, output.byteLength),
+ dynamicUniform(this.parameters),
+ ], [rows, columns, 0, 0], Math.ceil(rows * columns / 256));
+ }
+
+ createF16(elements: number, label: string): GpuTensor {
+ return createF16Tensor(this.device, elements, label);
+ }
+
+ destroy(): void {
+ if (this.destroyed) return;
+ this.destroyed = true;
+ this.parameters.destroy();
+ this.zero.destroy();
+ destroyTensors(this.rotaryTables);
+ this.pipelines.clear();
+ this.layouts.clear();
+ }
+
+ /**
+ * Materialize the fixed RoPE sin/cos table once on the GPU. The attention
+ * graph previously re-evaluated pow/sin/cos for every key and every
+ * 32-query block; the longest table for the supplied WAV is only ~304 KiB.
+ */
+ private prepareRotaryTable(pass: GPUComputePassEncoder, tokens: number): GpuTensor {
+ if (this.rotaryTable !== undefined && this.rotaryTableTokens >= tokens) {
+ return this.rotaryTable;
+ }
+ const table = createF16Tensor(this.device, tokens * 32 * 4, `dicose-rope-table-${tokens}`);
+ // createF16Tensor sizes in two-byte elements; four such elements reserve
+ // one vec2 record per (position, rotary pair).
+ this.rotaryTables.push(table);
+ this.rotaryTable = table;
+ this.rotaryTableTokens = tokens;
+ const pipeline = this.pipeline("rotary-table", ROTARY_TABLE_WGSL);
+ const parameterOffset = this.parametersFor([tokens, 0, 0, 0]);
+ const bindings = this.bind("rotary-table", [
+ storage(table.buffer, table.byteLength),
+ dynamicUniform(this.parameters),
+ ]);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ pass.dispatchWorkgroups(Math.ceil(tokens * 32 / 256));
+ return table;
+ }
+
+ private elementwise(
+ pass: GPUComputePassEncoder,
+ name: string,
+ code: string,
+ entries: readonly GPUBindGroupEntry[],
+ values: readonly number[] | Uint32Array,
+ workgroups?: number,
+ ): void {
+ const pipeline = this.pipeline(name, code);
+ const parameterOffset = this.parametersFor(values);
+ const bindings = this.bind(name, entries);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindings.bindGroup, [parameterOffset]);
+ this.dispatchElementwise(pass, workgroups ?? Math.ceil(Number(values[0]) / 256));
+ }
+
+ /**
+ * Chrome/Metal caps a single dispatch dimension at 65,535 workgroups. The
+ * frequency-axis feature tensor for the supplied WAV is already larger than
+ * that when processed by a 256-lane elementwise kernel, so flatten over X/Y
+ * rather than relying on an invalid one-dimensional dispatch.
+ */
+ private dispatchElementwise(pass: GPUComputePassEncoder, workgroups: number): void {
+ const limit = this.device.limits.maxComputeWorkgroupsPerDimension;
+ const width = Math.min(workgroups, limit);
+ const height = Math.ceil(workgroups / width);
+ if (height > limit) {
+ throw new RangeError(`DiCoSe elementwise dispatch exceeds the device workgroup grid for ${workgroups} workgroups`);
+ }
+ pass.dispatchWorkgroups(width, height);
+ }
+
+ private pipeline(name: string, code: string, layoutName = name): GPUComputePipeline {
+ const existing = this.pipelines.get(name);
+ if (existing !== undefined) return existing;
+ const layout = this.layout(layoutName, bindingsFor(layoutName));
+ const pipeline = this.device.createComputePipeline({
+ label: `dicose-${name}`,
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [layout] }),
+ compute: { module: this.device.createShaderModule({ label: `dicose-${name}-wgsl`, code }), entryPoint: "main" },
+ });
+ this.pipelines.set(name, pipeline);
+ return pipeline;
+ }
+
+ private layout(name: string, entries: readonly GPUBindGroupLayoutEntry[]): GPUBindGroupLayout {
+ const existing = this.layouts.get(name);
+ if (existing !== undefined) return existing;
+ const layout = this.device.createBindGroupLayout({ label: `dicose-${name}-layout`, entries });
+ this.layouts.set(name, layout);
+ return layout;
+ }
+
+ private bind(
+ name: string,
+ entries: readonly GPUBindGroupEntry[],
+ uniformBinding = 4,
+ ): DynamicBindGroup {
+ let nextStorageBinding = 0;
+ const normalized = entries.map((entry) => {
+ if (entry.binding === uniformBinding) return entry;
+ const binding = nextStorageBinding;
+ nextStorageBinding += 1;
+ return { ...entry, binding };
+ });
+ return {
+ bindGroup: this.device.createBindGroup({
+ label: `dicose-${name}-bindings`,
+ layout: this.layout(name, bindingsFor(name)),
+ entries: normalized,
+ }),
+ parameterOffset: 0,
+ };
+ }
+
+ private parametersFor(values: readonly number[] | Uint32Array): number {
+ this.requireAlive();
+ if (this.parameterCursor >= PARAMETER_SLOTS) throw new Error("DiCoSe uniform pool exhausted");
+ const bytes = new ArrayBuffer(PARAMETER_BYTES);
+ const u32 = new Uint32Array(bytes);
+ for (let index = 0; index < values.length; index += 1) u32[index] = values[index] ?? 0;
+ const offset = this.parameterCursor * PARAMETER_BYTES;
+ this.device.queue.writeBuffer(this.parameters, offset, bytes);
+ this.parameterCursor += 1;
+ return offset;
+ }
+
+ private requireAlive(): void {
+ if (this.destroyed) throw new Error("DiCoSe GPU ops were destroyed");
+ }
+}
+
+function storage(buffer: GPUBuffer, size: number, offset = 0): GPUBindGroupEntry {
+ return { binding: 0, resource: { buffer, offset, size: align4(size) } };
+}
+
+function dynamicUniform(buffer: GPUBuffer): GPUBindGroupEntry {
+ return { binding: 4, resource: { buffer, size: PARAMETER_BYTES } };
+}
+
+function dynamicUniformAt(buffer: GPUBuffer, binding: number): GPUBindGroupEntry {
+ return { binding, resource: { buffer, size: PARAMETER_BYTES } };
+}
+
+function bindingsFor(name: string): readonly GPUBindGroupLayoutEntry[] {
+ const storageRead = (binding: number): GPUBindGroupLayoutEntry => ({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } });
+ const storageWrite = (binding: number): GPUBindGroupLayoutEntry => ({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } });
+ const uniform = (binding: number): GPUBindGroupLayoutEntry => ({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: PARAMETER_BYTES } });
+ switch (name) {
+ case "linear": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), uniform(4)];
+ case "linear-packed": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), uniform(4)];
+ case "linear-packed-residual": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), storageRead(4), uniform(5)];
+ case "linear-packed-rotary-keys": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), storageRead(4), uniform(5)];
+ case "rmsnorm": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), uniform(4)];
+ case "add": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "copy": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "sample-even": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "transpose-tb": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "gather-slice": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "scatter-slice": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "spectral-to-pixels": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "pixels-to-spectral": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "attention": return [storageRead(0), storageWrite(1), uniform(4)];
+ case "attention-grouped": return [storageRead(0), storageWrite(1), storageRead(2), storageRead(3), uniform(4)];
+ case "rotary-table": return [storageWrite(0), uniform(4)];
+ case "gates": return [storageWrite(0), storageRead(1), uniform(4)];
+ case "complex-multiply": return [storageRead(0), storageRead(1), storageWrite(2), uniform(4)];
+ case "affine": return [storageRead(0), storageRead(1), storageWrite(2), uniform(4)];
+ case "conv2d": return [storageRead(0), storageRead(1), storageRead(2), storageWrite(3), uniform(4)];
+ case "gelu": return [storageWrite(0), uniform(4)];
+ case "tanh": return [storageWrite(0), uniform(4)];
+ case "glu": return [storageRead(0), storageWrite(1), uniform(4)];
+ default: throw new Error(`Unknown DiCoSe pipeline ${name}`);
+ }
+}
+
+function activationCode(value: Activation): number {
+ if (value === "gelu") return 1;
+ if (value === "tanh") return 2;
+ return 0;
+}
+
+function align4(value: number): number {
+ return Math.ceil(value / 4) * 4;
+}
+
+function packedLinearTileColumns(layout: GpuWeightTensor["layout"]): 128 | 256 | undefined {
+ if (layout === "linear-tile-n128-k32") return 128;
+ if (layout === "linear-tile-n256-k32") return 256;
+ return undefined;
+}
+
+/**
+ * Emit the two production dense owners. Four fixed-32 subgroups each own
+ * eight rows; every lane owns one N128 or two N256 vec4 columns. Converter-
+ * native [N-tile, K-tile, K32, N] weights let each loaded vector serve eight
+ * rows without workgroup staging or barriers. The exact arm visits K in source
+ * order with FP32 FMA. Owner128/K4-load experiments may instead form bounded
+ * native-f16 K2 or K4 dot partials and immediately widen into FP32 state.
+ */
+function packedLinearWgsl(
+ inner: number,
+ columns: number,
+ storageTileColumns: 128 | 256,
+ outputTileColumns: 128 | 256,
+ vectorizeK: boolean,
+ accumulation: PackedAccumulation,
+ activation: Activation,
+ hasResidual: boolean,
+ hasRotaryKeys: boolean,
+ tileOffset = 0,
+): string {
+ if (accumulation !== "exact" && (outputTileColumns !== 128 || !vectorizeK)) {
+ throw new RangeError(`${accumulation} accumulation requires owner128/K4 loads`);
+ }
+ const rowsPerSubgroup = 8;
+ const vectorsPerLane = outputTileColumns / 128;
+ const weightVectorsPerInner = storageTileColumns / 4;
+ const ownerTilesPerStorageTile = storageTileColumns / outputTileColumns;
+ const declarations = Array.from({ length: rowsPerSubgroup }, (_, row) =>
+ Array.from({ length: vectorsPerLane }, (_, vector) =>
+ ` var acc${row}_${vector} = vec4(0.0);`,
+ ).join("\n"),
+ ).join("\n");
+ const broadcasts = (suffix: string, laneValue: string): string =>
+ Array.from({ length: rowsPerSubgroup }, (_, row) =>
+ ` let a${row}${suffix} = subgroupBroadcast(${laneValue}, ${row}u);`,
+ ).join("\n");
+ const weightLoads = (suffix: string, weightBase: string): string =>
+ Array.from({ length: vectorsPerLane }, (_, vector) =>
+ ` let b${vector}${suffix} = vec4(weight[
+ ${weightBase} + subgroup_lane * ${vectorsPerLane}u + ${vector}u
+ ]);`,
+ ).join("\n");
+ const contractions = (suffix: string): string =>
+ Array.from({ length: rowsPerSubgroup }, (_, row) =>
+ Array.from({ length: vectorsPerLane }, (_, vector) =>
+ ` acc${row}_${vector} = fma(vec4(f32(a${row}${suffix})), b${vector}${suffix}, acc${row}_${vector});`,
+ ).join("\n"),
+ ).join("\n");
+ const approximateContractions = Array.from({ length: rowsPerSubgroup }, (_, row) => {
+ const broadcast = ` let a${row} = subgroupBroadcast(lane_a, ${row}u);`;
+ if (accumulation === "k2") {
+ return `${broadcast}
+ let partial${row}_01 = vec4(
+ dot(a${row}.xy, vec2(b0.x, b1.x)),
+ dot(a${row}.xy, vec2(b0.y, b1.y)),
+ dot(a${row}.xy, vec2(b0.z, b1.z)),
+ dot(a${row}.xy, vec2(b0.w, b1.w))
+ );
+ acc${row}_0 = acc${row}_0 + vec4(partial${row}_01);
+ let partial${row}_23 = vec4(
+ dot(a${row}.zw, vec2(b2.x, b3.x)),
+ dot(a${row}.zw, vec2(b2.y, b3.y)),
+ dot(a${row}.zw, vec2(b2.z, b3.z)),
+ dot(a${row}.zw, vec2(b2.w, b3.w))
+ );
+ acc${row}_0 = acc${row}_0 + vec4(partial${row}_23);`;
+ }
+ return `${broadcast}
+ let partial${row} = vec4(
+ dot(a${row}, vec4(b0.x, b1.x, b2.x, b3.x)),
+ dot(a${row}, vec4(b0.y, b1.y, b2.y, b3.y)),
+ dot(a${row}, vec4(b0.z, b1.z, b2.z, b3.z)),
+ dot(a${row}, vec4(b0.w, b1.w, b2.w, b3.w))
+ );
+ acc${row}_0 = acc${row}_0 + vec4(partial${row});`;
+ }).join("\n");
+ const approximateTraversal = ` for (var inner_in_tile = 0u; inner_in_tile < 32u; inner_in_tile += 4u) {
+ let inner_index = inner_tile * 32u + inner_in_tile;
+ var lane_a = vec4(0.0h);
+ let lane_row = row_base + subgroup_lane;
+ if (subgroup_lane < 8u && lane_row < params.rows) {
+ lane_a = input[(lane_row * INNER + inner_index) / 4u];
+ }
+ let weight_base0 = tile_base + inner_in_tile * WEIGHT_VECTORS_PER_INNER + storage_column_offset;
+ let weight_base1 = weight_base0 + WEIGHT_VECTORS_PER_INNER;
+ let weight_base2 = weight_base1 + WEIGHT_VECTORS_PER_INNER;
+ let weight_base3 = weight_base2 + WEIGHT_VECTORS_PER_INNER;
+ let b0 = weight[weight_base0 + subgroup_lane];
+ let b1 = weight[weight_base1 + subgroup_lane];
+ let b2 = weight[weight_base2 + subgroup_lane];
+ let b3 = weight[weight_base3 + subgroup_lane];
+${approximateContractions}
+ }`;
+ const exactInnerTraversal = vectorizeK
+ ? ` for (var inner_in_tile = 0u; inner_in_tile < 32u; inner_in_tile += 4u) {
+ let inner_index = inner_tile * 32u + inner_in_tile;
+ var lane_a = vec4(0.0h);
+ let lane_row = row_base + subgroup_lane;
+ if (subgroup_lane < 8u && lane_row < params.rows) {
+ lane_a = input[(lane_row * INNER + inner_index) / 4u];
+ }
+${["x", "y", "z", "w"].map((component, index) => {
+ const suffix = `_${index}`;
+ const weightBase = `weight_base${suffix}`;
+ return ` let ${weightBase} = tile_base + (inner_in_tile + ${index}u) * WEIGHT_VECTORS_PER_INNER + storage_column_offset;
+${weightLoads(suffix, weightBase)}
+${broadcasts(suffix, `lane_a.${component}`)}
+${contractions(suffix)}`;
+ }).join("\n")}
+ }`
+ : ` for (var inner_in_tile = 0u; inner_in_tile < 32u; inner_in_tile += 1u) {
+ let inner_index = inner_tile * 32u + inner_in_tile;
+ var lane_a = 0.0h;
+ let lane_row = row_base + subgroup_lane;
+ if (subgroup_lane < 8u && lane_row < params.rows) {
+ lane_a = input[lane_row * INNER + inner_index];
+ }
+ let weight_base = tile_base + inner_in_tile * WEIGHT_VECTORS_PER_INNER + storage_column_offset;
+${weightLoads("", "weight_base")}
+${broadcasts("", "lane_a")}
+${contractions("")}
+ }`;
+ const innerTraversal = accumulation === "exact"
+ ? exactInnerTraversal
+ : approximateTraversal;
+ const applyActivation = (value: string): string => {
+ if (activation === "gelu") {
+ return ` ${value} = vec4(gelu(${value}.x), gelu(${value}.y), gelu(${value}.z), gelu(${value}.w));`;
+ }
+ if (activation === "tanh") {
+ return ` ${value} = vec4(tanh(${value}.x), tanh(${value}.y), tanh(${value}.z), tanh(${value}.w));`;
+ }
+ return "";
+ };
+ const stores = Array.from({ length: rowsPerSubgroup }, (_, row) =>
+ Array.from({ length: vectorsPerLane }, (_, vector) => {
+ const value = `value${row}_${vector}`;
+ const store = hasResidual
+ ? `let rounded = vec4(${value});
+ output[row * COLUMN_VECTORS + column_vector] = vec4(
+ vec4(rounded) + vec4(residual[row * COLUMN_VECTORS + column_vector])
+ );`
+ : hasRotaryKeys
+ ? `output[row * COLUMN_VECTORS + column_vector] = rotate_key_vector(${value}, row, column_vector);`
+ : `output[row * COLUMN_VECTORS + column_vector] = vec4(${value});`;
+ return `
+ {
+ let row = row_base + ${row}u;
+ if (row < params.rows) {
+ let column_vector = column_vector_base + ${vector}u;
+ var ${value} = acc${row}_${vector} + vec4(bias[column_vector]);
+${applyActivation(value)}
+ ${store}
+ }
+ }`;
+ }).join("\n"),
+ ).join("\n");
+ return `${COMMON_WGSL}
+struct Params {
+ rows: u32,
+ sequences: u32,
+ tokens: u32,
+ strided: u32,
+}
+const INNER: u32 = ${inner}u;
+const INNER_TILES: u32 = ${inner / 32}u;
+const COLUMN_VECTORS: u32 = ${columns / 4}u;
+const TILE_VECTORS: u32 = ${outputTileColumns / 4}u;
+const WEIGHT_VECTORS_PER_INNER: u32 = ${weightVectorsPerInner}u;
+@group(0) @binding(0) var input: array<${vectorizeK ? "vec4" : "f16"}>;
+@group(0) @binding(1) var weight: array>;
+@group(0) @binding(2) var bias: array>;
+@group(0) @binding(3) var output: array>;
+${hasResidual ? "@group(0) @binding(4) var residual: array>;" : hasRotaryKeys ? "@group(0) @binding(4) var rotary_table: array>;" : ""}
+@group(0) @binding(${hasResidual || hasRotaryKeys ? 5 : 4}) var params: Params;
+
+${hasRotaryKeys ? `fn rotate_key_vector(value: vec4, row: u32, column_vector: u32) -> vec4 {
+ let rounded = vec4(value);
+ let dimension = ((column_vector - 128u) * 4u) % 64u;
+ let position = select(row % params.tokens, row / params.sequences, params.strided != 0u);
+ let rotation0 = rotary_table[position * 32u + dimension / 2u];
+ let rotation1 = rotary_table[position * 32u + dimension / 2u + 1u];
+ return vec4(
+ f16(f32(rounded.x) * rotation0.x - f32(rounded.y) * rotation0.y),
+ f16(f32(rounded.x) * rotation0.y + f32(rounded.y) * rotation0.x),
+ f16(f32(rounded.z) * rotation1.x - f32(rounded.w) * rotation1.y),
+ f16(f32(rounded.z) * rotation1.y + f32(rounded.w) * rotation1.x)
+ );
+}` : ""}
+
+@compute @workgroup_size(128, 1, 1)
+fn main(
+ @builtin(subgroup_invocation_id) subgroup_lane: u32,
+ @builtin(subgroup_id) subgroup: u32,
+ @builtin(subgroup_size) subgroup_size: u32,
+ @builtin(workgroup_id) group: vec3,
+) {
+ if (subgroup_size != 32u) { return; }
+ let row_base = group.y * 32u + subgroup * 8u;
+ let column_tile = group.x + ${tileOffset}u;
+ let column_vector_base = column_tile * TILE_VECTORS + subgroup_lane * ${vectorsPerLane}u;
+${declarations}
+ for (var inner_tile = 0u; inner_tile < INNER_TILES; inner_tile += 1u) {
+ let storage_column_tile = column_tile / ${ownerTilesPerStorageTile}u;
+ let storage_column_offset = (column_tile % ${ownerTilesPerStorageTile}u) * TILE_VECTORS;
+ let tile_base =
+ (storage_column_tile * INNER_TILES + inner_tile) * 32u * WEIGHT_VECTORS_PER_INNER;
+${innerTraversal}
+ }
+${stores}
+}`;
+}
+
+const COMMON_WGSL = /* wgsl */ `
+enable f16;
+enable subgroups;
+
+fn flat_element_index(id: vec3, workgroups: vec3) -> u32 {
+ return id.x + id.y * workgroups.x * 256u + id.z * workgroups.x * workgroups.y * 256u;
+}
+
+fn gelu(x: f32) -> f32 {
+ let sign = select(-1.0, 1.0, x >= 0.0);
+ // GELU(x) = 0.5 * x * (1 + erf(x / sqrt(2))). The previous kernel fed
+ // x directly to erf, making every deterministic and CD GELU too steep.
+ let ax = abs(x) * 0.7071067811865476;
+ let t = 1.0 / (1.0 + 0.3275911 * ax);
+ let erf = sign * (1.0 - (((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t) * exp(-ax * ax));
+ return 0.5 * x * (1.0 + erf);
+}
+`;
+
+const LINEAR_WGSL = `${COMMON_WGSL}
+struct Params { rows: u32, inner: u32, columns: u32, activation: u32, }
+@group(0) @binding(0) var input: array;
+@group(0) @binding(1) var weight: array;
+@group(0) @binding(2) var bias: array;
+@group(0) @binding(3) var output: array;
+@group(0) @binding(4) var params: Params;
+var a_tile: array;
+var b_tile: array;
+@compute @workgroup_size(16, 16, 1)
+fn main(@builtin(local_invocation_id) local: vec3, @builtin(workgroup_id) group: vec3) {
+ let row = group.y * 16u + local.y;
+ let column = group.x * 16u + local.x;
+ var sum = 0.0;
+ for (var base = 0u; base < params.inner; base += 16u) {
+ let a_column = base + local.x;
+ let b_row = base + local.y;
+ let local_index = local.y * 16u + local.x;
+ a_tile[local_index] = select(0.0h, input[row * params.inner + a_column], row < params.rows && a_column < params.inner);
+ b_tile[local_index] = select(0.0h, weight[b_row * params.columns + column], b_row < params.inner && column < params.columns);
+ workgroupBarrier();
+ for (var k = 0u; k < 16u; k += 1u) {
+ sum = fma(f32(a_tile[local.y * 16u + k]), f32(b_tile[k * 16u + local.x]), sum);
+ }
+ workgroupBarrier();
+ }
+ if (row < params.rows && column < params.columns) {
+ var value = sum + f32(bias[column]);
+ if (params.activation == 1u) { value = gelu(value); }
+ if (params.activation == 2u) { value = tanh(value); }
+ output[row * params.columns + column] = f16(value);
+ }
+}`;
+
+const RMSNORM_WGSL = `${COMMON_WGSL}
+struct Params { rows: u32, columns: u32, has_mapping: u32, workgroup_width: u32, }
+@group(0) @binding(0) var input: array;
+@group(0) @binding(1) var gamma: array;
+@group(0) @binding(2) var mapping: array;
+@group(0) @binding(3) var output: array;
+@group(0) @binding(4) var params: Params;
+var partials: array;
+@compute @workgroup_size(256)
+fn main(@builtin(local_invocation_index) lane: u32, @builtin(subgroup_invocation_id) subgroup_lane: u32, @builtin(subgroup_id) subgroup: u32, @builtin(workgroup_id) group: vec3) {
+ let row = group.x + group.y * params.workgroup_width;
+ if (row >= params.rows) { return; }
+ var sum = 0.0;
+ for (var c = lane; c < params.columns; c += 256u) { let x = f32(input[row * params.columns + c]); sum = fma(x, x, sum); }
+ let sub = subgroupAdd(sum);
+ if (subgroup_lane == 0u) { partials[subgroup] = sub; }
+ workgroupBarrier();
+ if (lane == 0u) { var total = 0.0; for (var i = 0u; i < 8u; i += 1u) { total += partials[i]; } partials[0] = inverseSqrt(max(total, 1e-24)) * sqrt(f32(params.columns)); }
+ workgroupBarrier();
+ let factor = partials[0];
+ for (var c = lane; c < params.columns; c += 256u) {
+ var value = f32(input[row * params.columns + c]) * factor * f32(gamma[c]);
+ if (params.has_mapping != 0u) { value = value * (f32(mapping[c]) + 1.0) + f32(mapping[params.columns + c]); }
+ output[row * params.columns + c] = f16(value);
+ }
+}`;
+
+const RMSNORM_ROWS8_WGSL = `${COMMON_WGSL}
+struct Params { rows: u32, columns: u32, has_mapping: u32, workgroup_width: u32, }
+@group(0) @binding(0) var input: array;
+@group(0) @binding(1) var gamma: array;
+@group(0) @binding(2) var mapping: array;
+@group(0) @binding(3) var output: array;
+@group(0) @binding(4) var params: Params;
+@compute @workgroup_size(256)
+fn main(
+ @builtin(subgroup_invocation_id) subgroup_lane: u32,
+ @builtin(subgroup_id) subgroup: u32,
+ @builtin(subgroup_size) subgroup_size: u32,
+ @builtin(workgroup_id) group: vec3,
+) {
+ if (subgroup_size != 32u) { return; }
+ let workgroup = group.x + group.y * params.workgroup_width;
+ let row = workgroup * 8u + subgroup;
+ if (row >= params.rows) { return; }
+ var total = 0.0;
+ for (var original_subgroup = 0u; original_subgroup < 8u; original_subgroup += 1u) {
+ let original_lane = original_subgroup * 32u + subgroup_lane;
+ var partial = 0.0;
+ for (var column = original_lane; column < params.columns; column += 256u) {
+ let value = f32(input[row * params.columns + column]);
+ partial = fma(value, value, partial);
+ }
+ total += subgroupAdd(partial);
+ }
+ let factor = inverseSqrt(max(total, 1e-24)) * sqrt(f32(params.columns));
+ for (var column = subgroup_lane; column < params.columns; column += 32u) {
+ var value = f32(input[row * params.columns + column]) * factor * f32(gamma[column]);
+ if (params.has_mapping != 0u) {
+ value = value * (f32(mapping[column]) + 1.0) + f32(mapping[params.columns + column]);
+ }
+ output[row * params.columns + column] = f16(value);
+ }
+}`;
+
+const ADD_WGSL = `${COMMON_WGSL}
+struct Params { elements: u32, _a: u32, _b: u32, _c: u32, }
+@group(0) @binding(0) var source: array;
+@group(0) @binding(1) var destination: array;
+@group(0) @binding(4) var params: Params;
+@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3, @builtin(num_workgroups) workgroups: vec3) { let index = flat_element_index(id, workgroups); if (index < params.elements) { destination[index] = f16(f32(destination[index]) + f32(source[index])); } }`;
+
+const COPY_WGSL = `${COMMON_WGSL}
+struct Params { elements: u32, _a: u32, _b: u32, _c: u32, }
+@group(0) @binding(0) var source: array;
+@group(0) @binding(1) var destination: array;
+@group(0) @binding(4) var params: Params;
+@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3, @builtin(num_workgroups) workgroups: vec3) { let index = flat_element_index(id, workgroups); if (index < params.elements) { destination[index] = source[index]; } }`;
+
+const SAMPLE_EVEN_WGSL = `${COMMON_WGSL}
+struct Params { elements: u32, samples: u32, _a: u32, _b: u32, }
+@group(0) @binding(0) var input: array;
+@group(0) @binding(1) var output: array;
+@group(0) @binding(4) var params: Params;
+@compute @workgroup_size(256)
+fn main(@builtin(global_invocation_id) id: vec3, @builtin(num_workgroups) workgroups: vec3) {
+ let index = flat_element_index(id, workgroups);
+ if (index >= params.samples) { return; }
+ let quotient = params.elements / params.samples;
+ let remainder = params.elements % params.samples;
+ let source_index = index * quotient + (index * remainder) / params.samples;
+ output[index] = input[source_index];
+}`;
+
+const TRANSPOSE_TB_WGSL = `${COMMON_WGSL}
+struct Params { time: u32, bands: u32, dim: u32, _pad: u32, }
+@group(0) @binding(0) var input: array;
+@group(0) @binding(1) var output: array;
+@group(0) @binding(4) var params: Params;
+@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3