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]>
64 lines
3.2 KiB
JavaScript
64 lines
3.2 KiB
JavaScript
// Reproducible proof: the wasm32 FULL CONDITIONAL codec decode is byte-identical
|
||
// to the native reference — a SOTA-class learned image codec (PTQ of mbt2018_mean)
|
||
// whose whole bitstream→image decode (factorized z, integer h_s, integer-σ
|
||
// entropy, integer g_s) runs in the browser engine with NO ML runtime, and whose
|
||
// RGB verifies against the native FNV-1a hash. The integer-entropy contract,
|
||
// in-browser.
|
||
//
|
||
// Run via demo/verify_int_hyper.sh (builds the nodejs-target pkg first), or:
|
||
// node verify_int_hyper_wasm.mjs [path/to/wai_web.js] [model-dir]
|
||
// The model dir (default $WAI_SOTA_INT_DIR or /tmp/sota_int) holds the artifacts
|
||
// tools/wai_sota_int_synth_export.py emits (the g_s/h_s model JSONs are large, so
|
||
// they are NOT committed — regenerate them with the export tool).
|
||
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
const pkg = resolve(here, process.argv[2] ?? './pkg-node/wai_web.js');
|
||
const dir = resolve(process.argv[3] ?? process.env.WAI_SOTA_INT_DIR ?? '/tmp/sota_int');
|
||
|
||
if (!existsSync(`${dir}/cond.json`)) {
|
||
console.log(`skip: artifacts not in ${dir} (run tools/wai_sota_int_synth_export.py)`);
|
||
process.exit(0);
|
||
}
|
||
const wai = await import(pathToFileURL(pkg).href);
|
||
const bytes = (name) => new Uint8Array(readFileSync(`${dir}/${name}`));
|
||
|
||
// FNV-1a 64-bit — the same dependency-free checksum the export tool and the
|
||
// native Rust decode share (no domain separation; raw RGB bytes).
|
||
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');
|
||
}
|
||
|
||
const conf = JSON.parse(readFileSync(`${dir}/cond.json`, 'utf8'));
|
||
const ent = bytes('cond_entropy.json'); // y Gaussian-conditional + z factorized tables
|
||
const bs = bytes('cond_bitstream.json'); // z_rans + y_rans
|
||
|
||
function check(label, img) {
|
||
const got = fnv1a(img.rgb);
|
||
const ok = got === conf.rgb_hash
|
||
&& img.width === conf.out_w && img.height === conf.out_h
|
||
&& img.rgb.length === conf.out_w * conf.out_h * conf.out_c;
|
||
console.log(`${ok ? 'PASS' : 'FAIL'} ${label.padEnd(12)} ${img.width}x${img.height} ${img.rgb.length}B `
|
||
+ `${got.slice(0, 16)}… ${got === conf.rgb_hash ? '== native hash' : `MISMATCH (want ${conf.rgb_hash})`} `
|
||
+ `(${conf.psnr_cond?.toFixed?.(2) ?? '?'} dB)`);
|
||
return ok;
|
||
}
|
||
|
||
// JSON model path and the COMPACT BINARY model path must both reproduce the
|
||
// native hash (the binary is what the browser demo downloads — ~½ the size).
|
||
const json = check('json model', wai.decodeIntHyper(
|
||
bytes('int_synth_sota_model.json'), bytes('int_hs_model.json'), ent, bs));
|
||
const bin = check('binary model', wai.decodeIntHyperBin(
|
||
bytes('int_synth_sota_model.bin'), bytes('int_hs_model.bin'), ent, bs));
|
||
|
||
const pass = json && bin;
|
||
console.log(pass
|
||
? '\n✓ wasm32 conditional-codec decode (JSON + compact-binary model) is byte-identical to the native reference — a SOTA learned codec decoded in-browser, zero JS ML runtime.'
|
||
: '\n✗ mismatch');
|
||
process.exit(pass ? 0 : 1);
|