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]>
84 lines
3.8 KiB
JavaScript
84 lines
3.8 KiB
JavaScript
// 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';
|
||
})();
|