From 123b4365e251341e972ec457c1a9a08294f51e9c Mon Sep 17 00:00:00 2001 From: DecodedXR Date: Wed, 8 Jul 2026 17:23:10 -0500 Subject: [PATCH 1/3] feat: M8 model-download progress in #status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transformers.js 4.2.0 aggregate progress_total events (confirmed from installed source: DefaultProgressCallback in utils/core.js) drive a live "Downloading depth model… NN%" readout. WebGPU path passes the callback straight to pipeline(); the WASM worker relays raw events over the bridge and depth.js filters in one place. Test seam __app.__emitProgress. Autopilot: STATUS.md Milestone 8 Co-Authored-By: Claude Fable 5 --- src/depth-worker.js | 8 ++++++- src/depth.js | 31 +++++++++++++++++++++++-- src/main.js | 22 +++++++++++++++++- tests/smoke.spec.js | 56 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/depth-worker.js b/src/depth-worker.js index 455e926..6ae81e0 100644 --- a/src/depth-worker.js +++ b/src/depth-worker.js @@ -11,6 +11,9 @@ // // Protocol (all messages are plain objects): // main → worker { type: 'init', model } +// worker → main { type: 'progress', event } (raw transformers.js +// progress events during the weight download — plain +// clonable objects; the bridge filters for progress_total) // worker → main { type: 'ready' } | { type: 'init-error', error } // main → worker { type: 'infer', id, data, width, height } // — RGBA ImageData pixels; the buffer arrives transferred. @@ -39,7 +42,10 @@ self.onmessage = async (e) => { try { const { pipeline, RawImage } = await import('@huggingface/transformers'); RawImageCtor = RawImage; - estimator = await pipeline('depth-estimation', msg.model, { device: 'wasm' }); + estimator = await pipeline('depth-estimation', msg.model, { + device: 'wasm', + progress_callback: (event) => self.postMessage({ type: 'progress', event }), + }); self.postMessage({ type: 'ready' }); } catch (err) { self.postMessage({ type: 'init-error', error: errMessage(err) }); diff --git a/src/depth.js b/src/depth.js index 3dd58cc..af35867 100644 --- a/src/depth.js +++ b/src/depth.js @@ -30,6 +30,31 @@ export function getSelectedDevice() { return selectedDevice; } +// Milestone 8: model-download progress. pipeline() wraps any progress_callback +// in DefaultProgressCallback (utils/core.js, confirmed from the installed +// 4.2.0 source), which emits aggregate `progress_total` events — progress is +// 0–100 across ALL weight files — alongside the raw per-file events. We +// filter for the aggregate here, in one place for both device paths: the +// WebGPU (main-thread) pipeline gets emitProgress directly; the WASM worker +// relays its events over the bridge (see createWorkerEstimator). +let onProgress = null; + +export function setProgressHandler(fn) { + onProgress = fn; +} + +function emitProgress(event) { + if (event && event.status === 'progress_total' && onProgress) { + onProgress(event.progress); + } +} + +// Test seam: CI can't download the real model, so tests feed fake events +// through the same filter the real callbacks use. +export function _emitProgressForTests(event) { + emitProgress(event); +} + // Returns (and caches) the depth-estimation pipeline. A failed load clears // the cache so the next upload can retry (e.g. after a network blip during // the weight download). @@ -77,7 +102,7 @@ async function loadEstimator(resetCache) { if (device === 'webgpu') { // WebGPU already computes off the main thread — keep the direct path. const { pipeline } = await import('@huggingface/transformers'); - return pipeline('depth-estimation', MODEL, { device }); + return pipeline('depth-estimation', MODEL, { device, progress_callback: emitProgress }); } // Milestone 6: on the WASM fallback the ONNX session blocks whatever thread // runs it (~4–11s/pass, M4 finding) — run it in a dedicated module worker so @@ -126,7 +151,9 @@ function createWorkerEstimator(resetCache) { }); worker.addEventListener('message', (e) => { const msg = e.data; - if (msg.type === 'ready') { + if (msg.type === 'progress') { + emitProgress(msg.event); + } else if (msg.type === 'ready') { resolve(estimatorFn); } else if (msg.type === 'init-error') { fail(new Error(msg.error)); diff --git a/src/main.js b/src/main.js index 2de869e..6537745 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,12 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; -import { getDepthEstimator, getSelectedDevice, _setEstimatorForTests } from './depth.js'; +import { + getDepthEstimator, + getSelectedDevice, + setProgressHandler, + _setEstimatorForTests, + _emitProgressForTests, +} from './depth.js'; import { initHud } from './hud.js'; // --------------------------------------------------------------------------- @@ -237,6 +243,16 @@ statusEl.id = 'status'; statusEl.textContent = 'Load a photo to see its depth field.'; controlPanel.appendChild(statusEl); +// M8: live model-download progress. depth.js emits the aggregate 0–100 +// percentage (transformers.js `progress_total`) on both device paths; each +// call site's next status write ('Estimating depth…' / 'Starting webcam…') +// naturally ends the readout once the load resolves. Weights are cached +// after the first load, so later loads emit nothing (or an instant flash +// from the browser cache). +setProgressHandler((pct) => { + statusEl.textContent = `Downloading depth model… ${Math.round(pct)}%`; +}); + // =========================================================================== // MILESTONE 5: polish readouts — FPS, per-pass inference time, and a // user-visible warning when depth runs on the WASM fallback. All @@ -517,6 +533,10 @@ window.__app = { // hidden