open-standards/wai/tools/wai_neural_audio_vectors.py
Transaction Science 32b148cd75
Some checks failed
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 14s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 18s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m43s
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 2m33s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 4m6s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 4m38s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m17s
wai-key-proxy / does this change touch it (push) Has been cancelled
wai-key-proxy / proxy · linux (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Has been cancelled
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Has been cancelled
wai: EnCodec 24/48 kHz and DAC 16/24 kHz registered with a pinned payload; SoundStream a candidate
The neural audio models WAI did not yet name, each at the operating points of its own
publication or release, royalty-free (EnCodec's repository and DAC's weights are MIT),
neural-float, one ONNX file each:

- wai.neural.encodec24: mono, 2/4/8/16/32 codebooks of 1024 at 75 Hz (1.5 to 24 kbps)
- wai.neural.encodec48: stereo, one-second chunks overlapping by 1% with a scale each,
  2/4/8/16 codebooks at 150 Hz (3 to 24 kbps)
- wai.neural.dac16: 1 to 12 codebooks at 50 Hz; wai.neural.dac24: 1 to 32 at 75 Hz
- wai.neural.soundstream: a candidate. The publication's models were not released, and
  the open speech codec built on its architecture ships its own non-ONNX models and
  packet, so no payload format or parameter set is pinned; never dispatched.

SPEC §5 gains *Payloads of the neural audio codecs*: the payload (the layout the route
already read, and a chunked form for the 48 kHz model), the checks made before the
decoder runs, and the decoder contract: the model's full codebook count always, a row
of 1024 marking a codebook not used, so one file decodes every bitrate. DAC's 16/24 kHz
decoders give t*320 - 8 samples, and the bound says so.

neural/audio.rs checks the payload, fills the unused rows, decodes chunk by chunk and
joins the 48 kHz chunks by the model's linear overlap-add; DecodedAudio gains
`channels`, two interleaved. The C ABI has no two-channel form, so encodec48 is not
advertised through it and is refused before decoding.

tools/wai_neural_audio_export_onnx.py exports each decoder from the released weights
and checks it against the model's own decode with the first q codebooks: on Thor all
four agreed within 1.5e-5 at every operating point tested. Vectors in
wai-rs/tests/vectors/neural_audio/ (stdlib generator, its own Python decode as the
second implementation); tests/neural_audio_vectors.rs decodes each through the dispatch
within 1e-5 of it and refuses a payload outside its model, an unpinned decoder, the
candidate and two channels through the C ABI; the neural gates workflow runs it.
Registry exports, wai.h, the glossary links and pin_verify.py's one-file set follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 09:40:50 -05:00

326 lines
14 KiB
Python

#!/usr/bin/env python3
"""Write, or check, the vectors of the neural audio route: wai-rs/tests/vectors/neural_audio/.
One vector for each capability whose payload SPEC §5 pins (*Payloads of the neural audio
codecs*): `wai.neural.encodec24`, `wai.neural.encodec48`, `wai.neural.dac16` and
`wai.neural.dac24`. Each directory holds
decoder.onnx a tiny decoder with the model's geometry and its decoder contract: the model's
codebook count Q, codebooks of 1024, a hop of 320, its channels; inputs
audio_codes int64 [1, 1, Q, T] and audio_scales float [1], output audio_values
float [1, C, T*320 - d] (d = 8 for DAC 16 and 24 kHz, whose decoders give 8
samples fewer, else 0). Its network is not the model's: a code c of codebook i adds
c*A[i] + B[i] to the frame's latent z (a row holding 1024 adds nothing), and
sample k of the frame on channel ch is tanh(z * W[ch][k]) * scale, the last d
samples dropped. A, B and W
are dyadic, so every value before the tanh is exact in float32.
vector.wai a WAI1 envelope of the capability, pinned to decoder.onnx, whose payload is
seeded codes in the pinned layout (some codebooks only, so the route must mark
the others unused; for encodec48, two chunks with their own scales).
expected.f32 the decode at the samples meta.json selects, every channel of each, as
little-endian float32, computed here in Python from the payload and the
decoder's parameters: the second implementation of the route, which reads the
payload, fills the codebooks, runs the network and joins the chunks by the
model's linear overlap-add on its own.
meta.json what the vector is, and which samples expected.f32 holds: index i where
i % every == 0 or i lies in an overlap region.
The ONNX files are written here, protobuf field by field, so the generator needs nothing but
the standard library and its bytes are the same on every machine.
python3 -I tools/wai_neural_audio_vectors.py # write the vectors
python3 -I tools/wai_neural_audio_vectors.py --check # rebuild them and compare
--check exits 0 when every committed file is what the generator writes (expected.f32 within
1e-6, since it holds a libm tanh), 1 otherwise.
"""
import hashlib
import json
import math
import os
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "..", "wai-rs", "tests", "vectors", "neural_audio")
K = 1024 # entries per codebook; the value that marks a codebook not used
HOP = 320
# name: capability, sample rate, channels, model codebooks Q, codebooks carried q, frames of each
# chunk, samples per channel n, chunk scales (None: no chunks), chunk stride, the samples the
# decoder gives fewer than frames*320 (DAC 16 and 24 kHz: 8), seed
VECTORS = {
"encodec24": ("wai.neural.encodec24", 24000, 1, 32, 8, [5], 1523, None, 0, 0, 1),
"encodec48": ("wai.neural.encodec48", 48000, 2, 16, 4, [150, 6], 49340, [0.75, 0.5], 47520, 0, 4),
"dac16": ("wai.neural.dac16", 16000, 1, 12, 5, [4], 1201, None, 0, 8, 2),
"dac24": ("wai.neural.dac24", 24000, 1, 32, 32, [3], 950, None, 0, 8, 3),
}
# ---- the decoder's parameters -------------------------------------------------------------
def params(Q, C):
"""A[i] = a/2^16, B[i] = b/2^8, W[ch][k] = w/2^4: |z| < 4 in steps of 2^-16, and z*W
takes at most 22 significant bits, so every value before the tanh is exact in float32."""
A = [[1, -2, 3, -4, 4, -3, 2, -1][i % 8] / 65536.0 for i in range(Q)]
B = [(((i * 5) % 17) - 8) / 256.0 for i in range(Q)]
W = [[(((k * 3 + ch * 5) % 31) - 15) / 16.0 for k in range(HOP)] for ch in range(C)]
return A, B, W
# ---- protobuf, as much of onnx.proto as the decoder needs ---------------------------------
def varint(v):
out = bytearray()
v &= (1 << 64) - 1
while True:
b = v & 0x7F
v >>= 7
if v:
out.append(b | 0x80)
else:
out.append(b)
return bytes(out)
def fv(field, v):
return varint(field << 3) + varint(v)
def fb(field, b):
if isinstance(b, str):
b = b.encode()
return varint((field << 3) | 2) + varint(len(b)) + b
FLOAT, INT64, BOOL = 1, 7, 9
def tensor(name, dtype, dims, raw):
# TensorProto: dims = 1, data_type = 2, name = 8, raw_data = 9.
return b"".join(fv(1, d) for d in dims) + fv(2, dtype) + fb(8, name) + fb(9, raw)
def f32s(xs):
return b"".join(struct.pack("<f", x) for x in xs)
def value_info(name, dtype, dims):
# ValueInfoProto: name = 1, type = 2; TypeProto.tensor_type = 1 {elem_type = 1, shape = 2};
# TensorShapeProto.dim = 1 {dim_value = 1 | dim_param = 2}.
shape = b"".join(fb(1, fb(2, d) if isinstance(d, str) else fv(1, d)) for d in dims)
return fb(1, name) + fb(2, fb(1, fv(1, dtype) + fb(2, shape)))
def attr_int(name, v):
# AttributeProto: name = 1, i = 3, type = 20 (INT = 2).
return fb(1, name) + fv(3, v) + fv(20, 2)
def attr_ints(name, vs):
# ints = 8 (INTS = 7).
return fb(1, name) + b"".join(fv(8, v) for v in vs) + fv(20, 7)
def node(op, inputs, outputs, attrs=()):
# NodeProto: input = 1, output = 2, name = 3, op_type = 4, attribute = 5.
return (b"".join(fb(1, i) for i in inputs) + b"".join(fb(2, o) for o in outputs)
+ fb(3, outputs[0]) + fb(4, op) + b"".join(fb(5, a) for a in attrs))
def decoder_onnx(Q, C, shrink):
A, B, W = params(Q, C)
nodes = [
node("Less", ["audio_codes", "K"], ["present"]),
node("Cast", ["present"], ["mask"], [attr_int("to", FLOAT)]),
node("Cast", ["audio_codes"], ["codes_f"], [attr_int("to", FLOAT)]),
node("Mul", ["codes_f", "A"], ["scaled"]),
node("Add", ["scaled", "B"], ["shifted"]),
node("Mul", ["shifted", "mask"], ["used"]),
node("ReduceSum", ["used", "axes"], ["z"], [attr_int("keepdims", 0)]),
node("ConvTranspose", ["z", "W"], ["y"], [attr_ints("strides", [HOP]), attr_ints("pads", [0, shrink])]),
node("Tanh", ["y"], ["bounded"]),
node("Mul", ["bounded", "audio_scales"], ["audio_values"]),
]
inits = [
tensor("K", INT64, [], struct.pack("<q", K)),
tensor("A", FLOAT, [1, 1, Q, 1], f32s(A)),
tensor("B", FLOAT, [1, 1, Q, 1], f32s(B)),
tensor("axes", INT64, [1], struct.pack("<q", 2)),
tensor("W", FLOAT, [1, C, HOP], f32s([w for row in W for w in row])),
]
# GraphProto: node = 1, name = 2, initializer = 5, input = 11, output = 12.
graph = (b"".join(fb(1, n) for n in nodes) + fb(2, "neural_audio_test_decoder")
+ b"".join(fb(5, t) for t in inits)
+ fb(11, value_info("audio_codes", INT64, [1, 1, Q, "T"]))
+ fb(11, value_info("audio_scales", FLOAT, [1]))
+ fb(12, value_info("audio_values", FLOAT, [1, C, "samples"])))
# ModelProto: ir_version = 1, producer_name = 2, graph = 7, opset_import = 8 {version = 2}.
return fv(1, 8) + fb(2, "wai_neural_audio_vectors") + fb(7, graph) + fb(8, fv(2, 17))
# ---- the payload ----------------------------------------------------------------------------
def splitmix64(seed):
s = seed & ((1 << 64) - 1)
while True:
s = (s + 0x9E3779B97F4A7C15) & ((1 << 64) - 1)
z = s
z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & ((1 << 64) - 1)
z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & ((1 << 64) - 1)
yield z ^ (z >> 31)
def chunks_of(q, frames, seed):
"""Each chunk's codes: q rows of its frame count, codebook-major."""
g = splitmix64(seed)
return [[[next(g) % K for _ in range(t)] for _ in range(q)] for t in frames]
def payload(q, frames, n, scales, chunks):
if scales is None:
head = struct.pack("<III", q, frames[0], n)
else:
head = struct.pack("<IIII", q, frames[-1], n, len(frames)) + f32s(scales)
return head + b"".join(struct.pack("<H", c) for ch in chunks for row in ch for c in row)
def envelope(cap, kind, sr, C, n, body, digest):
man = {
"wai": "1.0", "media": "audio", "intent": "create",
"model_requirement": {"capability": cap},
"conditioning": {"kind": kind, "prior": {
"id": f"neural-audio test decoder {kind}", "sha256": digest,
"determinism": "none: float inference through ONNX Runtime", "tier": "none"}},
"target": {"sr": sr, "channels": C, "n_samples": n},
}
mb = json.dumps(man, separators=(",", ":")).encode()
return b"WAI1" + struct.pack("<I", len(mb)) + mb + struct.pack("<I", len(body)) + body
# ---- the second implementation of the route -------------------------------------------------
def parse(cap, Q, C, body, chunked):
"""Read the payload as SPEC §5 lays it out, independently of the generator's writer."""
if chunked:
q, t_last, n, count = struct.unpack_from("<IIII", body, 0)
scales = list(struct.unpack_from(f"<{count}f", body, 16))
frames = [150] * (count - 1) + [t_last]
at = 16 + 4 * count
else:
q, t, n = struct.unpack_from("<III", body, 0)
scales, frames, at = [1.0], [t], 12
chunks = []
for t in frames:
rows = []
for _ in range(q):
rows.append(list(struct.unpack_from(f"<{t}H", body, at)))
at += 2 * t
chunks.append(rows)
assert at == len(body), f"{cap}: {len(body) - at} bytes after the codes"
return q, n, scales, chunks
def decode_chunk(Q, C, rows, scale, shrink):
"""The tiny decoder on one chunk: rows past the payload's hold K and add nothing, and the
last `shrink` samples are not given."""
A, B, W = params(Q, C)
t = len(rows[0])
full = rows + [[K] * t for _ in range(Q - len(rows))]
z = [0.0] * t
for i in range(Q):
for j in range(t):
c = full[i][j]
if c < K:
z[j] += c * A[i] + B[i]
out = [[math.tanh(z[j] * W[ch][k]) * scale for j in range(t) for k in range(HOP)] for ch in range(C)]
return [o[:t * HOP - shrink] for o in out]
def overlap_add(decoded, stride):
"""The model's linear overlap-add: a triangle over the first chunk's length."""
C = len(decoded[0])
len0 = len(decoded[0][0])
total = stride * (len(decoded) - 1) + len(decoded[-1][0])
out = [[0.0] * total for _ in range(C)]
wsum = [0.0] * total
for k, ch in enumerate(decoded):
off = k * stride
for j in range(len(ch[0])):
w = 0.5 - abs((j + 1) / (len0 + 1) - 0.5)
wsum[off + j] += w
for c in range(C):
out[c][off + j] += w * ch[c][j]
return [[v / s for v, s in zip(o, wsum)] for o in out]
def selected(n, every, overlaps):
return [i for i in range(n) if i % every == 0 or any(a <= i < b for a, b in overlaps)]
def build(name):
cap, sr, C, Q, q, frames, n, scales, stride, shrink, seed = VECTORS[name]
onnx = decoder_onnx(Q, C, shrink)
digest = hashlib.sha256(onnx).hexdigest()
body = payload(q, frames, n, scales, chunks_of(q, frames, seed))
wai = envelope(cap, name, sr, C, n, body, digest)
# The second implementation reads the payload back and decodes it.
q2, n2, sc, chunks = parse(cap, Q, C, body, scales is not None)
decoded = [decode_chunk(Q, C, rows, s, shrink) for rows, s in zip(chunks, sc)]
joined = overlap_add(decoded, stride) if scales is not None else decoded[0]
every = 16 if scales is not None else 1
overlaps = [[k * stride, (k - 1) * stride + frames[0] * HOP] for k in range(1, len(frames))]
at = selected(n2, every, overlaps)
expected = [joined[c][i] for i in at for c in range(C)]
meta = {
"capability": cap, "sample_rate": sr, "channels": C, "model_codebooks": Q,
"codebooks": q2, "frames": frames, "n_samples": n2, "scales": scales, "shrink": shrink,
"every": every, "overlaps": overlaps, "expected_values": len(expected),
"decoder_sha256": digest, "seed": seed,
}
return {
"decoder.onnx": onnx,
"vector.wai": wai,
"expected.f32": f32s(expected),
"meta.json": (json.dumps(meta, indent=2) + "\n").encode(),
}
def main(argv):
check = argv[1:] == ["--check"]
if argv[1:] and not check:
print(__doc__)
return 2
bad = 0
for name in sorted(VECTORS):
d = os.path.join(OUT, name)
for fname, want in build(name).items():
path = os.path.join(d, fname)
if not check:
os.makedirs(d, exist_ok=True)
with open(path, "wb") as fh:
fh.write(want)
continue
try:
with open(path, "rb") as fh:
got = fh.read()
except OSError as e:
print(f"{name}/{fname}: {e}")
bad += 1
continue
if fname == "expected.f32":
g = struct.unpack(f"<{len(got) // 4}f", got)
w = struct.unpack(f"<{len(want) // 4}f", want)
worst = max((abs(a - b) for a, b in zip(g, w)), default=0.0)
if len(g) != len(w) or worst > 1e-6:
print(f"{name}/{fname}: {len(g)} values, max |diff| {worst:.2e}; want {len(w)} within 1e-6")
bad += 1
elif got != want:
print(f"{name}/{fname}: differs from what the generator writes")
bad += 1
print(f"{name}: {'checked' if check else 'written'}")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))