int_synth + int_entropy are pure i64 (no ort, no float), so the SAME Rust that decodes natively compiles to wasm32 and runs in the browser with no ML runtime at all — and the bytes are identical to native on every machine. This is the integer-entropy contract's payoff made visible: a learned-codec reconstruction whose hash is portable. - int_synth: factor the decode into `decode_from_spec(payload, spec_bytes)` (no std::fs); `decode(payload, path)` now reads the spec and delegates. WASM/no-FS sinks call the bytes entry point with host-supplied weights. Behavior- identical (4 int_synth tests green). - wai-web: add the `neural_int` feature + a `neural_int` wasm-bindgen module: decodeIntSynth(payload, specJson) -> RGB, the int_entropy primitives (fixedScaleEdges / quantizeScale / detBuildIndex), and blake3Hex for the in-page receipt check. Compiles to wasm32 (web + nodejs targets). - demo: int_synth.html/.js — decode a vector to a canvas and verify its BLAKE3 against the frozen native hash in-page. verify_int_synth.sh + verify_int_synth_wasm.mjs — reproducible CLI proof. Verified: all four conformance vectors (incl. the real trained 128×128 photo model, 21.6 dB) decode in wasm32 byte-identical to the frozen NATIVE corpus hashes. Deterministic learned-codec decode in-browser, zero JS ML runtime. Co-Authored-By: Claude Opus 4.8 <[email protected]>
67 lines
2.9 KiB
JavaScript
67 lines
2.9 KiB
JavaScript
// WAI integer-exact neural image sink — decode a wai.neural.int_synth payload
|
||
// 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 BLAKE3 against the frozen native conformance hash, in-page —
|
||
// the integer-entropy contract: a learned-codec reconstruction whose hash is
|
||
// portable, so it verifies client-side with no server round-trip.
|
||
|
||
import init, { decodeIntSynth, blake3Hex } from './pkg/wai_web.js';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const ui = {
|
||
vec: $('vec'), badge: $('badge'),
|
||
dims: $('dims'), inch: $('inch'), layers: $('layers'),
|
||
got: $('got'), want: $('want'),
|
||
};
|
||
const canvas = $('c'), g = canvas.getContext('2d');
|
||
|
||
// The conformance hash is domain-separated: blake3("wai:int-synth-rgb"||0x01||rgb).
|
||
const DOMAIN = new Uint8Array([...new TextEncoder().encode('wai:int-synth-rgb'), 0x01]);
|
||
|
||
await init();
|
||
const expected = await (await fetch('int_synth/expected_hashes.json')).json();
|
||
|
||
async function bytes(path) {
|
||
return new Uint8Array(await (await fetch(path)).arrayBuffer());
|
||
}
|
||
|
||
async function load(name) {
|
||
const exp = expected[name];
|
||
const payload = await bytes(`int_synth/${name}/latent.bin`);
|
||
const spec = await bytes(`int_synth/${name}/int_synth.json`);
|
||
|
||
// THE decode — pure-i64 Rust in wasm, no ML runtime.
|
||
const img = decodeIntSynth(payload, spec);
|
||
const rgb = img.rgb; // width*height*3
|
||
|
||
// In-page receipt check: domain-separated BLAKE3, compared to the frozen hash
|
||
// the native reference froze into expected_hashes.json.
|
||
const buf = new Uint8Array(DOMAIN.length + rgb.length);
|
||
buf.set(DOMAIN, 0); buf.set(rgb, DOMAIN.length);
|
||
const got = blake3Hex(buf);
|
||
const ok = got === exp.rgb_hash && img.width === exp.width && img.height === exp.height;
|
||
|
||
// Paint RGB → RGBA on the canvas, nearest-neighbour upscaled to fill.
|
||
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);
|
||
|
||
ui.dims.textContent = `${img.width}×${img.height}`;
|
||
ui.inch.textContent = exp.in_channels;
|
||
ui.layers.textContent = exp.layers;
|
||
ui.got.textContent = got.slice(0, 16) + '…';
|
||
ui.want.textContent = exp.rgb_hash.slice(0, 16) + '…';
|
||
ui.badge.className = ok ? 'ok' : 'bad';
|
||
ui.badge.textContent = ok ? '✓ hash matches native conformance' : '✗ HASH MISMATCH';
|
||
}
|
||
|
||
ui.vec.addEventListener('change', () => load(ui.vec.value));
|
||
load(ui.vec.value);
|