open-standards/wai/wai-web/demo/int_hyper.js
Transaction Science 648f407f09 wai: the headline decode tests fail on a missing input, and CI supplies their inputs
The tests behind WAI's whole-decode claims read inputs from outside the
repository and returned early — reporting a pass — when those inputs were
absent. No workflow supplied them, so the claims were not checked
continuously.

- WAI_REQUIRE_SOTA_VECTORS=1 turns a missing input into a failure. A path
  variable naming a missing or incomplete input fails with or without the
  switch. Local runs without either still skip, and every skip prints a
  banner saying the claim was not verified.
- wai-sota-vectors.yml regenerates the int_hyper vectors from the pretrained
  mbt2018_mean checkpoint (pinned by SHA-256, with a pinned Python stack) and
  runs int_hyper_synth and int_video_sota_keyframe with the switch set. The
  regenerated vectors identify one run, not one machine: the encoder side is
  float.
- wai-jpegai-e2e.yml fetches the pinned JPEG AI inputs from the public models
  root, checks each against a SHA-256 manifest, and runs the whole-decode and
  reconstruction-binding tests with the switch set, requiring each test by
  name and the pinned pixel hash.
- Both jobs fail on any skip banner, and trigger on the kernels and float
  helpers the decode imports.
- The int_hyper claims are stated as the checks CI runs, with its quality
  measured on the pinned image; JPEG AI cross-machine identity is stated as a
  measurement, not a registry-class claim. determinism-tiers §5–§6 record the
  jobs.

Neither workflow has yet run through the forge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:40:59 -04:00

84 lines
3.8 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
// pretrained learned codec (mbt2018_mean, exported to integers) 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';
})();