// Face-detection worker — UltraFace (version-RFB-320) via onnxruntime-web, CPU // WASM backend, self-hosted. Kept off the main thread so model load + inference // never freeze the UI. Pre/post-processing mirrors the Phase 0 face spike. // // Protocol: // main -> worker: { type:'init' } // { type:'detect', rgba:ArrayBuffer(320*240*4), origW, origH, thr } // worker -> main: { type:'ready' } | { type:'error', message } // { type:'result', faces:[{x,y,w,h,score}] } (original-image px) import * as ort from '../vendor/ort/ort.wasm.bundle.min.mjs'; const IN_W = 320, IN_H = 240; const MODEL_URL = new URL('../vendor/models/version-RFB-320.onnx', import.meta.url); // All WASM served from our own origin; single-threaded so no cross-origin // isolation / SharedArrayBuffer is required on the static host. ort.env.wasm.wasmPaths = new URL('../vendor/ort/', import.meta.url).href; ort.env.wasm.numThreads = 1; ort.env.wasm.proxy = false; let session = null; let inputName = null; async function init() { if (session) return; const buf = await (await fetch(MODEL_URL)).arrayBuffer(); session = await ort.InferenceSession.create(new Uint8Array(buf), { executionProviders: ['wasm'], }); inputName = session.inputNames[0]; } function toTensor(rgba) { const chw = new Float32Array(3 * IN_W * IN_H); const plane = IN_W * IN_H; for (let y = 0; y < IN_H; y++) { for (let x = 0; x < IN_W; x++) { const i = (y * IN_W + x) * 4; const o = y * IN_W + x; chw[o] = (rgba[i] - 127) / 128; // R chw[plane + o] = (rgba[i + 1] - 127) / 128; // G chw[2 * plane + o] = (rgba[i + 2] - 127) / 128; // B } } return new ort.Tensor('float32', chw, [1, 3, IN_H, IN_W]); } function iou(a, b) { const ix = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)); const iy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)); const inter = ix * iy; const uni = a.w * a.h + b.w * b.h - inter; return uni <= 0 ? 0 : inter / uni; } function postprocess(scores, boxes, N, origW, origH, thr) { const dets = []; for (let i = 0; i < N; i++) { const p = scores[i * 2 + 1]; if (p < thr) continue; const x1 = boxes[i * 4] * origW, y1 = boxes[i * 4 + 1] * origH; const x2 = boxes[i * 4 + 2] * origW, y2 = boxes[i * 4 + 3] * origH; dets.push({ score: +p.toFixed(3), x: Math.round(x1), y: Math.round(y1), w: Math.round(x2 - x1), h: Math.round(y2 - y1) }); } dets.sort((a, b) => b.score - a.score); const keep = []; for (const d of dets) { if (d.w <= 1 || d.h <= 1) continue; if (!keep.some((k) => iou(k, d) > 0.3)) keep.push(d); } return keep; } async function detect(rgbaBuf, origW, origH, thr) { await init(); const input = toTensor(new Uint8ClampedArray(rgbaBuf)); const out = await session.run({ [inputName]: input }); const scoresT = out[session.outputNames.find((n) => /score/i.test(n)) || session.outputNames[0]]; const boxesT = out[session.outputNames.find((n) => /box/i.test(n)) || session.outputNames[1]]; const N = scoresT.dims[1]; return postprocess(scoresT.data, boxesT.data, N, origW, origH, thr); } self.onmessage = async (e) => { const msg = e.data || {}; try { if (msg.type === 'init') { await init(); self.postMessage({ type: 'ready' }); return; } if (msg.type === 'detect') { const faces = await detect(msg.rgba, msg.origW, msg.origH, msg.thr ?? 0.7); self.postMessage({ type: 'result', faces }); } } catch (err) { self.postMessage({ type: 'error', message: String(err && err.message || err) }); } };