open-standards/wai/tools/wai_validate_synthetic.py
Transaction Science 46c5688b86 wai: the WAI open standard — full cargo-class surface + provenance
Commit the WAI standard source: a content + capability-dispatch standard
whose thesis is "carries whatever it can reconstruct, verifies whatever
it carries." Source only — large vendored/model assets and build output
are gitignored.

Reconstruct classes (each: spec + conformance corpus + in-tab browser
sink that re-derives the corpus hash, one wai.det.fixed64 fixed-point
floor, one *-equivalence apiece):
  - interactive worlds  (replay-equivalence)   + 3 sim registrants,
    multiplayer over a relay, session receipts, glTF + real SPZ assets
  - spatial audio        (mixdown-equivalence)
  - haptics              (sample-equivalence)
  - state-feeds          (replay-equivalence, non-physics sim)
  - avatars / digital humans (pose-equivalence) + telepresence over the relay
  - volumetric / 4D splats   (frame-state-equivalence) + WebGPU EWA render,
    real-SPZ-derived clip

Verify half (lever 4):
  - asset provenance: a signed, energy-accounted lineage for opaque
    captured content WAI does not reconstruct; tamper-evident, in-tab.

Cross-cutting: JWP receipt profiles per class; a native energy meter
(IOReport via macmon, no sudo) emitting receipts with REAL measured
joules; the learned-image/neural codec family and JPEG AI entropy port
from earlier work.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 22:28:51 -04:00

395 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Reproducible end-to-end validation harness for the elic + mlicpp decoders.
elic (`wai.neural.elic`) and mlicpp (`wai.neural.mlicpp`) have no pretrained
model in the CompressAI zoo, so we can't cross-check them against a reference
codec the way bmshj2018 / mbt2018_mean are checked. Instead we build *minimal
CompressAI-compatible* models (real EntropyBottleneck + GaussianConditional,
random-but-non-trivial weights), run them through the SAME export + encode logic
the real tools use, and emit the `.wai` envelope + a reference reconstruction.
The Rust tests `elic_matches_reference` / `mlicpp_matches_reference` then decode
the envelope and must match the reference within ORT-version g_s rounding.
This makes those two validations reproducible from the repo (the artifacts the
Rust tests consume otherwise live only in /tmp). Run before the gated tests:
python tools/wai_validate_synthetic.py # writes /tmp artifacts
cargo test --features neural --test neural_native_decode _matches_reference
Needs torch + compressai + onnxruntime + numpy + Pillow.
"""
from __future__ import annotations
import argparse
import json
import math
import struct
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
import onnxruntime as ort
from PIL import Image
from compressai.models import CompressionModel
from compressai.entropy_models import GaussianConditional, EntropyBottleneck
from compressai.ans import BufferedRansEncoder
def scale_table():
return torch.exp(torch.linspace(math.log(0.11), math.log(256), 64))
def _convs(M, N):
dn = lambda ci, co: nn.Conv2d(ci, co, 5, 2, 2)
up = lambda ci, co: nn.ConvTranspose2d(ci, co, 5, 2, 2, output_padding=1)
g_a = nn.Sequential(dn(3, M), nn.ReLU(), dn(M, M), nn.ReLU(), dn(M, M), nn.ReLU(), dn(M, M))
g_s = nn.Sequential(up(M, M), nn.ReLU(), up(M, M), nn.ReLU(), up(M, M), nn.ReLU(), up(M, 3))
h_a = nn.Sequential(dn(M, N), nn.ReLU(), dn(N, N))
h_s = nn.Sequential(up(N, M), nn.ReLU(), up(M, 2 * M))
return g_a, g_s, h_a, h_s
class MiniElic(CompressionModel):
def __init__(self, M=48, N=48):
super().__init__()
self.entropy_bottleneck = EntropyBottleneck(N)
self.gaussian_conditional = GaussianConditional(None)
self.g_a, self.g_s, self.h_a, self.h_s = _convs(M, N)
self.context = nn.Conv2d(M, 2 * M, 5, 1, 2)
class MiniMlic(CompressionModel):
def __init__(self, M=48, N=48):
super().__init__()
self.entropy_bottleneck = EntropyBottleneck(N)
self.gaussian_conditional = GaussianConditional(None)
self.g_a, self.g_s, self.h_a, self.h_s = _convs(M, N)
self.entropy = nn.Conv2d(3 * M, 2 * M, 5, 1, 2)
def build(model_cls, seed):
torch.manual_seed(seed)
net = model_cls().eval()
for p in net.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p, gain=3.0) # gain 3 → real latent entropy
if hasattr(net, "gaussian_conditional"):
net.gaussian_conditional.update_scale_table(scale_table())
net.update(force=True)
return net
def fblob(eb):
cl = eb._cdf_length.int().tolist()
return {"channels": len(cl), "cdf_length": cl, "offset": eb._offset.int().tolist(),
"medians": eb._get_medians().detach().reshape(-1).tolist(),
"cdf": [r[:cl[i]] for i, r in enumerate(eb._quantized_cdf.int().tolist())]}
def gblob(g):
cl = g._cdf_length.int().tolist()
return {"scale_table": g.scale_table.reshape(-1).tolist(), "cdf_length": cl,
"offset": g._offset.int().tolist(),
"cdf": [r[:cl[i]] for i, r in enumerate(g._quantized_cdf.int().tolist())]}
def export_onnx(mod, dummy, path):
torch.onnx.export(mod, (dummy,), str(path), input_names=["i"], output_names=["o"],
dynamic_axes={"i": {2: "h", 3: "w"}, "o": {2: "h", 3: "w"}},
opset_version=17, dynamo=False)
def test_image(path, size):
if path.exists():
return
x, y = np.meshgrid(np.linspace(0, 1, size), np.linspace(0, 1, size))
r = (x * 255).astype(np.uint8); g = (y * 255).astype(np.uint8)
b = (((np.sin(x * 12) * np.cos(y * 12)) * 0.5 + 0.5) * 255).astype(np.uint8)
img = np.stack([r, g, b], -1)
yy, xx = np.ogrid[:size, :size]
img[(yy - size // 2) ** 2 + (xx - size // 2) ** 2 < (size // 5) ** 2] = [200, 40, 120]
Image.fromarray(img).save(path)
def write_wai(path, cap, kind, header, z, y, H, W):
manifest = {"wai": "1.0", "media": "image", "intent": "replicate",
"model_requirement": {"capability": cap}, "conditioning": {"kind": kind},
"target": {"w": W, "h": H}}
mb = json.dumps(manifest, separators=(",", ":")).encode()
payload = header + z + y
path.write_bytes(b"WAI1" + struct.pack("<I", len(mb)) + mb + struct.pack("<I", len(payload)) + payload)
def onx(sess, t):
return torch.from_numpy(sess.run(None, {sess.get_inputs()[0].name: t.numpy().astype(np.float32)})[0])
def roundtrip_elic(net, x, mdir, size):
mdir.mkdir(parents=True, exist_ok=True)
gc = net.gaussian_conditional
export_onnx(net.h_s, torch.zeros(1, 48, 4, 4), mdir / "hyper_synthesis.onnx")
export_onnx(net.context, torch.zeros(1, 48, 16, 16), mdir / "context.onnx")
export_onnx(net.g_s, torch.zeros(1, 48, 16, 16), mdir / "synthesis.onnx")
(mdir / "hyper_cdfs_q3.json").write_text(json.dumps(fblob(net.entropy_bottleneck), separators=(",", ":")))
(mdir / "gauss_cdfs_q3.json").write_text(json.dumps(gblob(gc), separators=(",", ":")))
hs = ort.InferenceSession(str(mdir / "hyper_synthesis.onnx"))
cx = ort.InferenceSession(str(mdir / "context.onnx"))
gs = ort.InferenceSession(str(mdir / "synthesis.onnx"))
with torch.no_grad():
y = net.g_a(x); z = net.h_a(y)
zs = net.entropy_bottleneck.compress(z); z_hat = net.entropy_bottleneck.decompress(zs, z.size()[-2:])
base = onx(hs, z_hat); sb, mb = base.chunk(2, 1)
_, Cy, Sy, _ = y.shape
yy, xx = torch.meshgrid(torch.arange(Sy), torch.arange(Sy), indexing="ij")
anc = ((yy + xx) % 2 == 0).numpy()
syb = torch.round(y - mb).to(torch.int32)
ya = ((syb.float() + mb) * torch.from_numpy(anc)).to(y.dtype)
ctx = onx(cx, ya); sc, mc = ctx.chunk(2, 1); syc = torch.round(y - mc).to(torch.int32)
ib = gc.build_indexes(sb)[0]; ic = gc.build_indexes(sc)[0]; syb, syc = syb[0], syc[0]
syms, idxs = [], []
for isa in (True, False):
st, it = (syb, ib) if isa else (syc, ic)
for ch in range(Cy):
for r in range(Sy):
for c in range(Sy):
if bool(anc[r, c]) == isa:
syms.append(int(st[ch, r, c])); idxs.append(int(it[ch, r, c]))
enc = BufferedRansEncoder()
enc.encode_with_indexes(syms, idxs, gc._quantized_cdf.tolist(), gc._cdf_length.tolist(), gc._offset.tolist())
yb = enc.flush()
yhat = (syb.float() + mb) * torch.from_numpy(anc) + (syc.float() + mc) * torch.from_numpy(~anc)
img = onx(gs, yhat if yhat.dim() == 4 else yhat.unsqueeze(0))
_, Cz, Sz, _ = z.shape
hdr = struct.pack("<IIIIHHBBH", size, size, len(zs[0]), len(yb), Cz, Cy, 2, 4, 3)
write_wai(Path("/tmp/test.elic.wai"), "wai.neural.elic", "elic", hdr, zs[0], yb, size, size)
(np.clip(img[0].numpy(), 0, 1).transpose(1, 2, 0) * 255).round().astype(np.uint8).tofile("/tmp/ref_elic.rgb")
return len(yb), len(syms)
def roundtrip_mlicpp(net, x, mdir, size, K=4):
mdir.mkdir(parents=True, exist_ok=True)
gc = net.gaussian_conditional
export_onnx(net.h_s, torch.zeros(1, 48, 4, 4), mdir / "hyper_synthesis.onnx")
export_onnx(net.entropy, torch.zeros(1, 144, 16, 16), mdir / "entropy.onnx")
export_onnx(net.g_s, torch.zeros(1, 48, 16, 16), mdir / "synthesis.onnx")
(mdir / "hyper_cdfs_q3.json").write_text(json.dumps(fblob(net.entropy_bottleneck), separators=(",", ":")))
(mdir / "gauss_cdfs_q3.json").write_text(json.dumps(gblob(gc), separators=(",", ":")))
hs = ort.InferenceSession(str(mdir / "hyper_synthesis.onnx"))
en = ort.InferenceSession(str(mdir / "entropy.onnx"))
gs = ort.InferenceSession(str(mdir / "synthesis.onnx"))
with torch.no_grad():
y = net.g_a(x); z = net.h_a(y)
zs = net.entropy_bottleneck.compress(z); z_hat = net.entropy_bottleneck.decompress(zs, z.size()[-2:])
hyper = onx(hs, z_hat); _, Cy, Sy, _ = y.shape; sw = Cy // K
yy, xx = torch.meshgrid(torch.arange(Sy), torch.arange(Sy), indexing="ij")
anc = ((yy + xx) % 2 == 0).numpy()
y_dec = torch.zeros(1, Cy, Sy, Sy); y0 = y[0]; syms, idxs = [], []
for k in range(K):
c0, c1 = k * sw, (k + 1) * sw
for isa in (True, False):
params = onx(en, torch.cat([y_dec, hyper], 1)); sc, mc = params.chunk(2, 1)
idx = gc.build_indexes(sc)[0]; m0 = mc[0]
for ch in range(c0, c1):
for r in range(Sy):
for c in range(Sy):
if bool(anc[r, c]) == isa:
sym = int(round(float(y0[ch, r, c] - m0[ch, r, c])))
syms.append(sym); idxs.append(int(idx[ch, r, c]))
y_dec[0, ch, r, c] = sym + m0[ch, r, c]
enc = BufferedRansEncoder()
enc.encode_with_indexes(syms, idxs, gc._quantized_cdf.tolist(), gc._cdf_length.tolist(), gc._offset.tolist())
yb = enc.flush(); img = onx(gs, y_dec)
_, Cz, Sz, _ = z.shape
hdr = struct.pack("<IIIIHHBBHH", size, size, len(zs[0]), len(yb), Cz, Cy, 2, 4, 3, K)
write_wai(Path("/tmp/test.mlic.wai"), "wai.neural.mlicpp", "mlicpp", hdr, zs[0], yb, size, size)
(np.clip(img[0].numpy(), 0, 1).transpose(1, 2, 0) * 255).round().astype(np.uint8).tofile("/tmp/ref_mlic.rgb")
return len(yb), len(syms)
class SynthSnac(nn.Module):
"""SNAC's multi-rate-sum decode interface (3 levels, ×4/×2/×1 → finest),
with clean ONNX ops only — the real SNAC decoder's noise/attention blocks
don't export (a sink-side concern). Validates snac.rs's multi-level wire
parse + dynamic codes_0..codes_{N-1} dispatch."""
HOP = 512
def forward(self, c0, c1, c2):
f0 = c0.float().repeat_interleave(4, dim=1)
f1 = c1.float().repeat_interleave(2, dim=1)
z = (f0 + f1 + c2.float()) / 12000.0
return z.unsqueeze(1).repeat_interleave(self.HOP, dim=2)
def roundtrip_snac(mdir: Path):
mdir.mkdir(parents=True, exist_ok=True)
sr, lengths = 24_000, [12, 24, 48]
nsamp = lengths[-1] * SynthSnac.HOP
rng = np.random.RandomState(7)
codes = [torch.from_numpy(rng.randint(0, 4096, size=(1, L)).astype(np.int64)) for L in lengths]
names = [f"codes_{i}" for i in range(3)]
torch.onnx.export(SynthSnac().eval(), tuple(codes), str(mdir / "decoder.onnx"),
input_names=names, output_names=["audio_values"],
dynamic_axes={**{f"codes_{i}": {1: f"t{i}"} for i in range(3)},
"audio_values": {2: "s"}}, opset_version=17, dynamo=False)
sess = ort.InferenceSession(str(mdir / "decoder.onnx"))
ref = sess.run(None, {f"codes_{i}": codes[i].numpy() for i in range(3)})[0]
hdr = struct.pack("<III", sr, nsamp, 3) + b"".join(struct.pack("<I", L) for L in lengths)
body = b"".join(codes[i][0].numpy().astype(np.uint16).tobytes() for i in range(3))
man = (b'{"wai":"1.0","media":"audio","intent":"replicate","model_requirement":'
b'{"capability":"wai.neural.snac"},"conditioning":{"kind":"snac"}}')
payload = hdr + body
Path("/tmp/test.snac.wai").write_bytes(
b"WAI1" + struct.pack("<I", len(man)) + man + struct.pack("<I", len(payload)) + payload)
ref.astype(np.float32).tofile("/tmp/ref_snac.f32")
return nsamp
class MiniDCVC(CompressionModel):
"""Hyperprior entropy + a temporal decoder (latents, ref_feature) →
(image, feature). Validates the inter-frame frame-loop orchestration."""
def __init__(self, M=48, N=48, Cf=48):
super().__init__()
self.entropy_bottleneck = EntropyBottleneck(N)
self.gaussian_conditional = GaussianConditional(None)
self.g_a, self.g_s, self.h_a, self.h_s = _convs(M, N)
self.t_feat = nn.Conv2d(M + Cf, Cf, 3, 1, 1)
up = lambda ci, co: nn.ConvTranspose2d(ci, co, 5, 2, 2, output_padding=1)
self.t_img = nn.Sequential(up(M + Cf, M), nn.ReLU(), up(M, M), nn.ReLU(),
up(M, M), nn.ReLU(), up(M, 3))
class _TDec(nn.Module):
def __init__(s, m): super().__init__(); s.m = m
def forward(s, latents, ref_feature):
x = torch.cat([latents, ref_feature], 1)
return s.m.t_img(x), torch.relu(s.m.t_feat(x))
def roundtrip_dcvc(net, x, mdir: Path, size, Cf=48):
mdir.mkdir(parents=True, exist_ok=True)
gc = net.gaussian_conditional
export_onnx(net.h_s, torch.zeros(1, 48, 4, 4), mdir / "hyper_synthesis.onnx")
torch.onnx.export(_TDec(net).eval(), (torch.zeros(1, 48, 16, 16), torch.zeros(1, Cf, 16, 16)),
str(mdir / "temporal_decoder.onnx"), input_names=["latents", "ref_feature"],
output_names=["image", "feature"],
dynamic_axes={"latents": {2: "h", 3: "w"}, "ref_feature": {2: "h", 3: "w"},
"image": {2: "h", 3: "w"}, "feature": {2: "h", 3: "w"}},
opset_version=17, dynamo=False)
(mdir / "hyper_cdfs_q3.json").write_text(json.dumps(fblob(net.entropy_bottleneck), separators=(",", ":")))
(mdir / "gauss_cdfs_q3.json").write_text(json.dumps(gblob(gc), separators=(",", ":")))
hs = ort.InferenceSession(str(mdir / "hyper_synthesis.onnx"))
gs = ort.InferenceSession(str(mdir / "temporal_decoder.onnx"))
frames = [np.roll(np.asarray(x[0].permute(1, 2, 0)), shift=8 * k, axis=1) for k in range(3)]
F = torch.zeros(1, Cf, 16, 16); fcodes = []; ref = []
for fr in frames:
xi = torch.from_numpy(np.ascontiguousarray(fr)).permute(2, 0, 1).unsqueeze(0)
with torch.no_grad():
y = net.g_a(xi); z = net.h_a(y); zs = net.entropy_bottleneck.compress(z)
z_hat = net.entropy_bottleneck.decompress(zs, z.size()[-2:])
params = onx(hs, z_hat); sc, mn = params.chunk(2, 1)
ys = gc.compress(y, gc.build_indexes(sc), means=mn); y_hat = gc.decompress(ys, gc.build_indexes(sc), means=mn)
img, F = onx(gs, [y_hat, F]) if False else [torch.from_numpy(t) for t in gs.run(None, {"latents": y_hat.numpy(), "ref_feature": F.numpy()})]
fcodes.append((zs[0], ys[0])); ref.append((np.clip(img[0].numpy(), 0, 1).transpose(1, 2, 0) * 255).round().astype(np.uint8))
_, Cz, Sz, _ = z.shape; _, Cy, Sy, _ = y.shape
hdr = struct.pack("<IIIIHHBBHH", size, size, 3, 30000, Cy, Cz, 4, 2, 3, Cf)
table = b"".join(struct.pack("<II", len(zb), len(yb)) for zb, yb in fcodes)
body = b"".join(zb + yb for zb, yb in fcodes)
write_wai(Path("/tmp/test.dcvc.wai"), "wai.neural.dcvc_rt", "dcvc_rt", hdr, b"", table + body, size, size)
np.concatenate([r.reshape(-1) for r in ref]).astype(np.uint8).tofile("/tmp/ref_dcvc.rgb")
return 3
from compressai.entropy_models.entropy_models import pmf_to_quantized_cdf # noqa: E402
_RL = 1 << 31
def _rans_cat_encode(symbols, cdfs):
"""Bypass-free categorical rANS matching wai-rs decode_categorical."""
state, out = _RL, []
for sym, cdf in zip(reversed(symbols), reversed(cdfs)):
start, freq = cdf[sym], cdf[sym + 1] - cdf[sym]
x_max = ((_RL >> 16) << 32) * freq
while state >= x_max:
out.append(state & 0xFFFFFFFF); state >>= 32
state = ((state // freq) << 16) + (state % freq) + start
words = [state & 0xFFFFFFFF, (state >> 32) & 0xFFFFFFFF] + list(reversed(out))
return b"".join(struct.pack("<I", w) for w in words)
class MiniGLC(CompressionModel):
"""Categorical/VQ entropy: hyper + checkerboard context predict per-position
logits over a K-codebook; synthesis maps the index grid → image. Logits are
ROUNDED in-graph so encoder/decoder ORT builds agree (categorical CDFs are
razor-sensitive — the determinism contract, sharper than Gaussian)."""
def __init__(self, K=16, Cz=32):
super().__init__()
self.entropy_bottleneck = EntropyBottleneck(Cz)
up = lambda ci, co: nn.ConvTranspose2d(ci, co, 5, 2, 2, output_padding=1)
self.hyper_entropy = nn.Sequential(up(Cz, 64), nn.ReLU(), up(64, K))
self.context_entropy = nn.Conv2d(1, K, 5, 1, 2)
self.synthesis = nn.Sequential(up(1, 32), nn.ReLU(), up(32, 32), nn.ReLU(),
up(32, 32), nn.ReLU(), up(32, 3))
class _Round(nn.Module):
def __init__(s, m): super().__init__(); s.m = m
def forward(s, x): return torch.round(s.m(x))
def roundtrip_glc(net, mdir: Path, size, K=16, Cz=32, Sz=4, Sy=16):
mdir.mkdir(parents=True, exist_ok=True)
torch.onnx.export(_Round(net.hyper_entropy), (torch.zeros(1, Cz, Sz, Sz),), str(mdir / "hyper_entropy.onnx"),
input_names=["i"], output_names=["o"], dynamic_axes={"i": {2: "h", 3: "w"}, "o": {2: "h", 3: "w"}}, opset_version=17, dynamo=False)
torch.onnx.export(_Round(net.context_entropy), (torch.zeros(1, 1, Sy, Sy),), str(mdir / "context_entropy.onnx"),
input_names=["i"], output_names=["o"], dynamic_axes={"i": {2: "h", 3: "w"}, "o": {2: "h", 3: "w"}}, opset_version=17, dynamo=False)
export_onnx(net.synthesis, torch.zeros(1, 1, Sy, Sy), mdir / "synthesis.onnx")
(mdir / "hyper_cdfs_q3.json").write_text(json.dumps(fblob(net.entropy_bottleneck), separators=(",", ":")))
he = ort.InferenceSession(str(mdir / "hyper_entropy.onnx")); ce = ort.InferenceSession(str(mdir / "context_entropy.onnx"))
gs = ort.InferenceSession(str(mdir / "synthesis.onnx"))
rng = np.random.RandomState(7)
sm = lambda l: (lambda e: e / e.sum())(np.exp(l - l.max()))
cdf_of = lambda l: pmf_to_quantized_cdf(torch.tensor(sm(l), dtype=torch.float32), 16).tolist()
z = torch.randn(1, Cz, Sz, Sz); zs = net.entropy_bottleneck.compress(z); z_hat = net.entropy_bottleneck.decompress(zs, (Sz, Sz))
base = he.run(None, {"i": z_hat.numpy()})[0][0]
idx = rng.randint(0, K, size=(Sy, Sy)).astype(np.int64)
anc = np.array([[(y + x) % 2 == 0 for x in range(Sy)] for y in range(Sy)])
ctx = ce.run(None, {"i": np.where(anc, idx, 0).astype(np.float32)[None, None]})[0][0]
syms, cdfs = [], []
for isa in (True, False):
for y in range(Sy):
for x in range(Sy):
if anc[y, x] == isa:
lg = base[:, y, x] if isa else base[:, y, x] + ctx[:, y, x]
syms.append(int(idx[y, x])); cdfs.append(cdf_of(lg))
yb = _rans_cat_encode(syms, cdfs)
ref = gs.run(None, {"i": idx.astype(np.float32)[None, None]})[0]
(np.clip(ref[0], 0, 1).transpose(1, 2, 0) * 255).round().astype(np.uint8).tofile("/tmp/ref_glc.rgb")
hdr = struct.pack("<IIIIHHBBH", size, size, len(zs[0]), len(yb), Cz, K, 2, 4, 3)
write_wai(Path("/tmp/test.glc.wai"), "wai.neural.glc", "glc", hdr, zs[0], yb, size, size)
return len(syms)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--size", type=int, default=256)
args = ap.parse_args()
img_path = Path("/tmp/wai_test.png")
test_image(img_path, args.size)
img = Image.open(img_path).convert("RGB").resize((args.size, args.size), Image.LANCZOS)
x = torch.from_numpy(np.asarray(img, np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0)
ny, ns = roundtrip_elic(build(MiniElic, 0), x, Path("/tmp/wai_elic"), args.size)
print(f"elic → /tmp/test.elic.wai (y={ny}B, {ns} symbols)")
ny, ns = roundtrip_mlicpp(build(MiniMlic, 1), x, Path("/tmp/wai_mlic"), args.size)
print(f"mlicpp → /tmp/test.mlic.wai (y={ny}B, {ns} symbols)")
nsamp = roundtrip_snac(Path("/tmp/wai_snac"))
print(f"snac → /tmp/test.snac.wai ({nsamp} samples, 3 levels)")
roundtrip_dcvc(build(MiniDCVC, 2), x, Path("/tmp/wai_dcvc"), args.size)
print("dcvc → /tmp/test.dcvc.wai (3 inter-frames, feature propagation)")
n = roundtrip_glc(build(MiniGLC, 3), Path("/tmp/wai_glc"), args.size)
print(f"glc → /tmp/test.glc.wai ({n} categorical indices)")
print("artifacts written; now: cargo test --features neural --test neural_native_decode _matches_reference")
return 0
if __name__ == "__main__":
raise SystemExit(main())