Every registered string now lives in one table (codecs/registry.rs) with its kind, status, media class, registering SPEC section, edition and registration date. `wai registry export` writes registry/capabilities.json, its JSON Schema, the generated table in REGISTRY.md and vocab v2; --check fails on drift. vocab v1 stays stable. A candidate capability (a registered string whose payload format no revision has pinned) must not be emitted. The C packers, the browser writers and `wai wrap` refuse one, and `codecs::is_dispatchable` is the single dispatch and selection predicate (SPEC §4): candidates, companion-only capabilities, records and derivations are not dispatchable, and an unregistered string is decoded only where the sink advertises it. The codec glossary is linked to the registry through a reviewed table and held to its schema by a library test; `org` becomes `body`, from a closed allowlist. The SPEC gains §5 sections on kinds and candidates, a §8 edition rule, corrected citations and rewritten appendices. The quantum-ops vectors are regenerated with neutral device ids. CI declares the feature-gated test targets with required-features, checks the registry export, builds wai-web for wasm32, and runs two public-text checks (comparative prose, and private name patterns) with a self-test. The specification, extensions, research notes, READMEs, glossary and source comments are rewritten to name no company or product and to make no comparisons. Co-Authored-By: Claude Opus 5.5 <[email protected]>
115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
"""Export the Mimi decoder to ONNX for the WAI browser runtime.
|
|
|
|
Mimi takes `audio_codes [B, n_codebooks, T]` directly (like DAC) and
|
|
returns `audio_values [B, 1, samples]`. Same WAI neural-audio ABI as
|
|
EnCodec/DAC/WavTokenizer: `[1, 1, q, t]` int64 in + `[1]` float32 in
|
|
(scales unused) → `[1, 1, samples]` float out.
|
|
|
|
Usage:
|
|
python tools/wai_mimi_export_onnx.py \
|
|
--out wai-web/demo/models/mimi/decoder.onnx
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from transformers import MimiModel
|
|
|
|
|
|
def _patch_packed_sequence_detection() -> None:
|
|
"""Mimi's decoder calls into transformers/masking_utils.py to detect
|
|
packed sequences in `position_ids`. That path uses `torch.diff` plus
|
|
a bool->cumsum chain — neither of which the torch.onnx tracer handles
|
|
cleanly. For a single-sequence inference graph (which is what we
|
|
export) the result is always None, so short-circuit it.
|
|
"""
|
|
import transformers.masking_utils as mu
|
|
|
|
def _const_none(*args, **kwargs):
|
|
return None
|
|
# The real fn is find_packed_sequence_indices (masking_utils.py:708).
|
|
# masking_utils itself calls it via a local name inside other helpers,
|
|
# so we patch the canonical attribute the rest of the module imports.
|
|
mu.find_packed_sequence_indices = _const_none
|
|
|
|
|
|
class WaiMimiDecoder(torch.nn.Module):
|
|
"""Adapts MimiModel.decode to the WAI neural-audio ONNX contract."""
|
|
def __init__(self, model: MimiModel):
|
|
super().__init__()
|
|
self.m = model
|
|
|
|
def forward(self, audio_codes: torch.Tensor,
|
|
audio_scales: torch.Tensor) -> torch.Tensor:
|
|
codes = audio_codes.squeeze(1) # [B, 1, q, t] → [B, q, t]
|
|
out = self.m.decode(audio_codes=codes, return_dict=True)
|
|
wav = out.audio_values
|
|
if wav.dim() == 2:
|
|
wav = wav.unsqueeze(1) # → [B, 1, samples]
|
|
keep_scales = audio_scales.sum() / audio_scales.sum().clamp(min=1e-9)
|
|
return wav * keep_scales
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--model-id", default="kyutai/mimi", # allow-name: model-hub id the tool defaults to
|
|
help="HF model id or local path.")
|
|
ap.add_argument("--out", required=True)
|
|
ap.add_argument("--opset", type=int, default=17)
|
|
args = ap.parse_args()
|
|
out = Path(args.out); out.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
_patch_packed_sequence_detection()
|
|
print(f"loading Mimi from {args.model_id}…")
|
|
m = MimiModel.from_pretrained(args.model_id).eval()
|
|
n_q = getattr(m.config, "num_quantizers", None) or \
|
|
(getattr(m.config, "num_semantic_quantizers", 0)
|
|
+ getattr(m.config, "num_acoustic_quantizers", 8))
|
|
print(f" num_quantizers≈{n_q} sampling_rate={m.config.sampling_rate} "
|
|
f"frame_rate={m.config.frame_rate}")
|
|
wrap = WaiMimiDecoder(m).eval()
|
|
|
|
# Build dummy with the same q the encoder produces (use a runtime probe
|
|
# via a tiny dummy forward pass to discover the true number).
|
|
with torch.no_grad():
|
|
probe = m.encode(torch.zeros(1, 1, 24000), padding_mask=None)
|
|
q = probe.audio_codes.shape[1]
|
|
print(f" probed q={q}")
|
|
T = 64
|
|
dummy_codes = torch.zeros(1, 1, q, T, dtype=torch.long)
|
|
dummy_scales = torch.ones (1, dtype=torch.float32)
|
|
|
|
with torch.no_grad():
|
|
ref = wrap(dummy_codes, dummy_scales)
|
|
print(f"reference forward pass OK: {tuple(ref.shape)}")
|
|
|
|
print(f"exporting → {out} (opset {args.opset})…")
|
|
torch.onnx.export(
|
|
wrap, (dummy_codes, dummy_scales), out.as_posix(),
|
|
input_names=["audio_codes", "audio_scales"],
|
|
output_names=["audio_values"],
|
|
dynamic_axes={"audio_codes": {3: "T"}, "audio_values": {2: "samples"}},
|
|
opset_version=args.opset, do_constant_folding=True, dynamo=False,
|
|
)
|
|
sz = out.stat().st_size
|
|
print(f"wrote {sz:,} B")
|
|
|
|
import onnxruntime as ort, numpy as np
|
|
sess = ort.InferenceSession(out.as_posix(), providers=["CPUExecutionProvider"])
|
|
ort_out = sess.run(None, {
|
|
"audio_codes": dummy_codes.numpy(),
|
|
"audio_scales": dummy_scales.numpy(),
|
|
})
|
|
diff = float(np.max(np.abs(ort_out[0] - ref.numpy())))
|
|
print(f"onnxruntime sanity: shape={ort_out[0].shape} "
|
|
f"max |onnx-torch| = {diff:.3e}")
|
|
if diff > 1e-3:
|
|
print("WARNING: numerical mismatch > 1e-3", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|