Better tests and more user feedback while doing transcription; WIP.

This commit is contained in:
Lucas Winther
2026-07-14 14:44:01 +02:00
parent 18acead43c
commit be82ecfe57
19 changed files with 447 additions and 138 deletions
+33 -21
View File
@@ -2,9 +2,10 @@
* Inference Web Worker (Decisions.md D-003 — inference never runs on the main
* thread). Hosts the Transformers.js `automatic-speech-recognition` pipeline.
*
* M2: WebGPU detection with automatic WASM fallback (D-004), catalog-driven
* dtype defaults (D-006), model switching with pipeline disposal, and
* language/task options for multilingual models.
* M2: hardware backend detection with automatic fallback — "auto" resolves
* WebNN → WebGPU → WASM (D-004, D-017) — catalog-driven dtype defaults
* (D-006), model switching with pipeline disposal, and language/task options
* for multilingual models.
*/
import {
@@ -16,7 +17,9 @@ import {
} from "@huggingface/transformers";
import { resolveDefaultDtype } from "../lib/models";
import {
deviceAttempts,
isUiToWorkerMessage,
type DeviceCapabilities,
type DevicePreference,
type InferenceDevice,
type LoadMessage,
@@ -56,7 +59,7 @@ function errorMessage(error: unknown): string {
* Forward Transformers.js progress events to the UI as load-progress messages.
* The library's own "ready" event is swallowed — the worker posts its own
* `ready` (with the selected device) once the pipeline actually resolved,
* which also covers the WebGPU → WASM fallback retry (D-004).
* which also covers the device fallback retries (D-004, D-017).
*/
function forwardProgress(info: ProgressInfo): void {
switch (info.status) {
@@ -83,7 +86,9 @@ function forwardProgress(info: ProgressInfo): void {
}
// ---------------------------------------------------------------------------
// Device selection (D-004): WebGPU when available, automatic WASM fallback
// Device selection (D-004, D-017): "auto" resolves WebNN → WebGPU → WASM.
// The probe only answers "does the backend exist?"; the attempt ordering and
// fallback policy live in the pure `deviceAttempts` (protocol.ts, unit-tested).
// ---------------------------------------------------------------------------
interface NavigatorWithHardware {
@@ -91,21 +96,26 @@ interface NavigatorWithHardware {
ml?: unknown;
}
async function detectDevice(preference: DevicePreference): Promise<InferenceDevice> {
if (preference === "wasm") return "wasm";
if (preference === "webnn") {
async function probeCapabilities(
preference: DevicePreference,
): Promise<DeviceCapabilities> {
const capabilities: DeviceCapabilities = { webnn: false, webgpu: false };
const hardware = navigator as NavigatorWithHardware;
if (preference === "auto" || preference === "webnn") {
try {
return (navigator as NavigatorWithHardware).ml ? "webnn" : "wasm";
capabilities.webnn = Boolean(hardware.ml);
} catch {
return "wasm";
// Probing threw — treat WebNN as unavailable.
}
}
try {
const adapter = await (navigator as NavigatorWithHardware).gpu?.requestAdapter();
return adapter ? "webgpu" : "wasm";
} catch {
return "wasm";
if (preference === "auto" || preference === "webgpu") {
try {
capabilities.webgpu = Boolean(await hardware.gpu?.requestAdapter());
} catch {
// Probing threw — treat WebGPU as unavailable.
}
}
return capabilities;
}
// ---------------------------------------------------------------------------
@@ -131,12 +141,14 @@ function loadKey(message: LoadMessage): string {
async function createPipeline(
message: LoadMessage,
): Promise<AutomaticSpeechRecognitionPipeline> {
const detected = await detectDevice(message.device);
// If WebGPU was selected but pipeline creation throws (driver quirks,
// unsupported ops, out-of-memory), fall back to WASM automatically (D-004).
const attempts: InferenceDevice[] =
detected === "webgpu" ? ["webgpu", "wasm"] :
detected === "webnn" ? ["webnn", "wasm"] : ["wasm"];
// Ordered devices to try (D-004/D-017): the first is the selected device;
// if pipeline creation throws there (driver quirks, unsupported ops,
// out-of-memory), the next one is tried automatically. For "auto" the chain
// is WebNN → WebGPU → WASM (filtered by the probe); WASM always terminates it.
const attempts: InferenceDevice[] = deviceAttempts(
message.device,
await probeCapabilities(message.device),
);
let lastError: unknown;
for (const device of attempts) {
+52 -3
View File
@@ -10,8 +10,10 @@
export type InferenceDevice = "webgpu" | "wasm" | "webnn";
/**
* What the UI may request. "auto" (the default) lets the worker probe
* `navigator.gpu` and pick WebGPU with automatic WASM fallback (D-004).
* What the UI may request. "auto" (the default) lets the worker probe the
* available backends and pick in priority order WebNN → WebGPU → WASM
* (D-004/D-017), with automatic fallback down the same chain if pipeline
* creation fails on the picked device.
*/
export type DevicePreference = InferenceDevice | "auto";
@@ -79,7 +81,7 @@ export type UiToWorkerMessage = LoadMessage | TranscribeMessage;
* Model download / initialization progress. Mirrors the Transformers.js
* `progress_callback` events we care about; `status: "ready"` marks the end
* of a successful load and reports the backend that actually got selected
* (after WebGPU detection / fallback, D-004).
* (after device detection / fallback, D-004, D-017).
*/
export type LoadProgressMessage =
| { type: "load-progress"; status: "initiate"; file: string }
@@ -135,6 +137,53 @@ export type WorkerToUiMessage =
| ResultMessage
| ErrorMessage;
// ---------------------------------------------------------------------------
// Device selection policy (D-004, D-017) — pure so it is unit-testable; the
// worker probes the browser capabilities and this decides the attempt order.
// ---------------------------------------------------------------------------
/** What the worker's capability probe found (see inference.worker.ts). */
export interface DeviceCapabilities {
/** `navigator.ml` exists (WebNN). */
webnn: boolean;
/** `navigator.gpu.requestAdapter()` returned an adapter (WebGPU). */
webgpu: boolean;
}
/**
* Resolve a device preference against probed capabilities into the ordered
* list of devices to attempt pipeline creation on. The first entry is the
* selected device; later entries are automatic fallbacks for when creation
* throws (driver quirks, unsupported ops, out-of-memory). Never empty — WASM
* is always the last resort.
*
* - "auto": WebNN → WebGPU → WASM (D-017), skipping backends the probe ruled
* out, so an auto-detected WebNN that fails still tries WebGPU before WASM.
* - Explicit "webnn"/"webgpu": that device (if available) → WASM. An explicit
* choice deliberately does not detour through the other accelerator.
* - "wasm": WASM only.
*/
export function deviceAttempts(
preference: DevicePreference,
capabilities: DeviceCapabilities,
): InferenceDevice[] {
switch (preference) {
case "wasm":
return ["wasm"];
case "webgpu":
return capabilities.webgpu ? ["webgpu", "wasm"] : ["wasm"];
case "webnn":
return capabilities.webnn ? ["webnn", "wasm"] : ["wasm"];
case "auto": {
const attempts: InferenceDevice[] = [];
if (capabilities.webnn) attempts.push("webnn");
if (capabilities.webgpu) attempts.push("webgpu");
attempts.push("wasm");
return attempts;
}
}
}
// ---------------------------------------------------------------------------
// Runtime guards (postMessage data arrives untyped)
// ---------------------------------------------------------------------------