open-standards/wai/wai-web/demo/int_hyper.js
Transaction Science 89e0da789e wai: compact binary model + in-browser conditional-codec demo
Makes the in-browser decode of the full conditional codec practical: the model
shrinks from ~52MB JSON to ~23.5MB binary (~15.5MB gzipped on the wire), and a
real HTML page decodes a SOTA-class learned codec in the browser, verifying its
hash against the native reference.

- typed IntModel: refactored run_net/decode/decode_conditional off serde_json
  Value onto an IntModel { afrac, gf, shift, layers: Vec<IntLayer> }, built by
  either IntModel::from_json (existing schema) or IntModel::from_bytes (compact
  binary: magic WHM1, int12 weights as i16, IGDN gamma as i32, the rest i64).
  Decode is byte-identical whichever the source.
- export tool writes *_model.bin alongside the JSON (model_to_bin).
- conformance: the binary model decodes byte-identically to JSON (new assert in
  tests/int_hyper_synth.rs); all 3 tests green in release (~11s, the 100s was
  debug — release wasm decode is ~10s).
- wasm: decodeIntHyperBin (binary path) beside decodeIntHyper (JSON); shared
  cond_decode helper. demo/verify_int_hyper_wasm.mjs checks both paths.
- demo/int_hyper.html + .js: fetch the binary models + 1.4KB bitstream, decode
  with pure-i64 wasm (no ML runtime), paint the canvas, FNV-1a the RGB against
  cond.json. Verified in headless Chrome: 256x256, 42.61 dB, decoded hash
  adeb12d347d0bf7e == native, ~10s decode, zero console errors.

The PTQ model files are large and NOT committed (gitignored demo/int_hyper/);
regenerate with tools/wai_sota_int_synth_export.py. pkg/ stays a build artifact.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 03:14:46 -04:00

83 lines
3.7 KiB
JavaScript
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.

// WAI integer-exact CONDITIONAL codec sink — decode a wai.neural.int_hyper
// bitstream (factorized z + Gaussian-conditional y) to RGB with the SAME pure-i64
// Rust that decodes natively, compiled to wasm32, with NO ML runtime (no
// onnxruntime-web, no WebGPU, no float). Then verify the decoded image's FNV-1a
// against the native reference hash, in-page — the integer-entropy contract: a
// SOTA-class learned codec whose reconstruction hash is portable.
//
// The model files (g_s/h_s, PTQ weights) are large and NOT committed; place the
// export tool's output under demo/int_hyper/ (the .bin models are ~½ the JSON).
import init, { decodeIntHyperBin } from './pkg/wai_web.js';
const $ = (id) => document.getElementById(id);
const ui = { badge: $('badge'), dims: $('dims'), bits: $('bits'), psnr: $('psnr'), got: $('got'), want: $('want') };
const canvas = $('c'), g = canvas.getContext('2d');
const base = 'int_hyper';
// FNV-1a 64-bit — the same dependency-free checksum the native Rust decode and
// the export tool share (raw RGB bytes, no domain separation).
function fnv1a(buf) {
const mask = (1n << 64n) - 1n;
let h = 14695981039346656037n;
for (let i = 0; i < buf.length; i++) h = ((h ^ BigInt(buf[i])) * 1099511628211n) & mask;
return h.toString(16).padStart(16, '0');
}
async function bytes(path) {
const r = await fetch(path);
if (!r.ok) throw new Error(`${path}: ${r.status}`);
return new Uint8Array(await r.arrayBuffer());
}
(async () => {
await init();
let conf, gS, hS, ent, bs;
try {
conf = await (await fetch(`${base}/cond.json`)).json();
[gS, hS, ent, bs] = await Promise.all([
bytes(`${base}/int_synth_sota_model.bin`), // g_s (compact binary)
bytes(`${base}/int_hs_model.bin`), // h_s (compact binary)
bytes(`${base}/cond_entropy.json`), // y Gaussian-conditional + z factorized tables
bytes(`${base}/cond_bitstream.json`), // z_rans + y_rans
]);
} catch (e) {
ui.badge.className = 'bad';
ui.badge.textContent = `model not staged — run tools/wai_sota_int_synth_export.py into demo/${base}/ (${e.message})`;
return;
}
ui.badge.textContent = 'decoding (pure-i64 wasm, no ML runtime)…';
await new Promise((r) => requestAnimationFrame(r));
const t0 = performance.now();
const img = decodeIntHyperBin(gS, hS, ent, bs); // THE decode — pure-i64 Rust in wasm
const ms = Math.round(performance.now() - t0);
const rgb = img.rgb;
const got = fnv1a(rgb);
const ok = got === conf.rgb_hash && img.width === conf.out_w && img.height === conf.out_h;
// Paint RGB → RGBA, nearest-neighbour upscaled to fill the canvas.
const id = g.createImageData(img.width, img.height);
for (let i = 0, j = 0; i < rgb.length; i += 3, j += 4) {
id.data[j] = rgb[i]; id.data[j + 1] = rgb[i + 1]; id.data[j + 2] = rgb[i + 2]; id.data[j + 3] = 255;
}
const off = new OffscreenCanvas(img.width, img.height);
off.getContext('2d').putImageData(id, 0, 0);
g.imageSmoothingEnabled = false;
g.clearRect(0, 0, canvas.width, canvas.height);
const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
const dw = img.width * scale, dh = img.height * scale;
g.drawImage(off, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
const bsj = JSON.parse(new TextDecoder().decode(bs));
const nbytes = bsj.z_rans.length + bsj.rans.length;
ui.dims.textContent = `${img.width}×${img.height}`;
ui.bits.textContent = `${nbytes} B (${ms} ms decode)`;
ui.psnr.textContent = conf.psnr_cond ? `${conf.psnr_cond.toFixed(2)} dB` : '—';
ui.got.textContent = got;
ui.want.textContent = conf.rgb_hash;
ui.badge.className = ok ? 'ok' : 'bad';
ui.badge.textContent = ok ? '✓ hash matches the native reference' : '✗ HASH MISMATCH';
})();