diff --git a/src/App.tsx b/src/App.tsx index 342c08b..ec852f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -444,9 +444,11 @@ export function App() { Model: {modelId} ยท Engine:{" "} {activeDevice === "webgpu" ? "WebGPU (on-device)" - : activeDevice === "wasm" - ? "WASM (on-device)" - : "on-device โ€” backend selected on first load"} + : activeDevice === "webnn" + ? "WebNN (on-device)" + : activeDevice === "wasm" + ? "WASM (on-device)" + : "on-device โ€” backend selected on first load"}

diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx index e6082cb..73d060f 100644 --- a/src/components/ModelPicker.tsx +++ b/src/components/ModelPicker.tsx @@ -24,6 +24,7 @@ interface ModelPickerProps { const DEVICE_OPTIONS: readonly { value: DevicePreference; label: string }[] = [ { value: "auto", label: "Auto (WebGPU if available)" }, { value: "webgpu", label: "WebGPU" }, + { value: "webnn", label: "WebNN" }, { value: "wasm", label: "WASM (CPU)" }, ]; @@ -47,7 +48,12 @@ export function ModelPicker({ // the device. Until the worker has reported one, assume WASM unless the // user explicitly asked for WebGPU. const estimateDevice: InferenceDevice = - activeDevice ?? (devicePreference === "webgpu" ? "webgpu" : "wasm"); + activeDevice ?? + (devicePreference === "webgpu" + ? "webgpu" + : devicePreference === "webnn" + ? "webnn" + : "wasm"); const groups: { title: string; items: WhisperModel[] }[] = [ { diff --git a/src/lib/models.ts b/src/lib/models.ts index f8731b5..749eb7b 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -122,6 +122,12 @@ const WEBGPU_FP16_Q4: DtypeDefault = { note: "fp16 encoder + q4 merged decoder โ€” the common working WebGPU setup.", }; +const WEBNN_FP16: DtypeDefault = { + dtype: "fp16", + verified: false, + note: "fp16 is commonly used with WebNN.", +}; + // --------------------------------------------------------------------------- // The catalog (D-006: curated, not exhaustive) // --------------------------------------------------------------------------- @@ -138,6 +144,7 @@ export const MODELS: readonly WhisperModel[] = [ // Verified in-browser during M1 (WASM, Transformers.js default q8). wasm: { ...WASM_Q8, verified: true }, webgpu: WEBGPU_FP32, + webnn: WEBNN_FP16, }, }, { @@ -147,7 +154,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: true, encoderParamsM: 8, decoderParamsM: 31, - defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32 }, + defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-base.en", @@ -156,7 +163,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: false, encoderParamsM: 20, decoderParamsM: 54, - defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32 }, + defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-base", @@ -165,7 +172,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: true, encoderParamsM: 20, decoderParamsM: 54, - defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32 }, + defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP32, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-small.en", @@ -174,7 +181,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: false, encoderParamsM: 88, decoderParamsM: 156, - defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP16_Q4 }, + defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-small", @@ -183,7 +190,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: true, encoderParamsM: 88, decoderParamsM: 156, - defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP16_Q4 }, + defaults: { wasm: WASM_Q8, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-medium.en", @@ -192,7 +199,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: false, encoderParamsM: 306, decoderParamsM: 463, - defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4 }, + defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-medium", @@ -201,7 +208,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: true, encoderParamsM: 306, decoderParamsM: 463, - defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4 }, + defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 }, }, { id: "onnx-community/whisper-large-v3-turbo", @@ -210,7 +217,7 @@ export const MODELS: readonly WhisperModel[] = [ multilingual: true, encoderParamsM: 635, decoderParamsM: 174, - defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4 }, + defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 }, }, ]; diff --git a/src/worker/inference.worker.ts b/src/worker/inference.worker.ts index f6bb2c2..0037aa6 100644 --- a/src/worker/inference.worker.ts +++ b/src/worker/inference.worker.ts @@ -86,14 +86,22 @@ function forwardProgress(info: ProgressInfo): void { // Device selection (D-004): WebGPU when available, automatic WASM fallback // --------------------------------------------------------------------------- -interface NavigatorWithGpu { +interface NavigatorWithHardware { gpu?: { requestAdapter(): Promise }; + ml?: unknown; } async function detectDevice(preference: DevicePreference): Promise { if (preference === "wasm") return "wasm"; + if (preference === "webnn") { + try { + return (navigator as NavigatorWithHardware).ml ? "webnn" : "wasm"; + } catch { + return "wasm"; + } + } try { - const adapter = await (navigator as NavigatorWithGpu).gpu?.requestAdapter(); + const adapter = await (navigator as NavigatorWithHardware).gpu?.requestAdapter(); return adapter ? "webgpu" : "wasm"; } catch { return "wasm"; @@ -127,7 +135,8 @@ async function createPipeline( // 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"] : ["wasm"]; + detected === "webgpu" ? ["webgpu", "wasm"] : + detected === "webnn" ? ["webnn", "wasm"] : ["wasm"]; let lastError: unknown; for (const device of attempts) { diff --git a/src/worker/protocol.ts b/src/worker/protocol.ts index 3c48644..d577646 100644 --- a/src/worker/protocol.ts +++ b/src/worker/protocol.ts @@ -7,7 +7,7 @@ */ /** Devices the worker can actually run on (D-004). */ -export type InferenceDevice = "webgpu" | "wasm"; +export type InferenceDevice = "webgpu" | "wasm" | "webnn"; /** * What the UI may request. "auto" (the default) lets the worker probe @@ -147,7 +147,7 @@ function isOptionalString(value: unknown): value is string | undefined { return value === undefined || typeof value === "string"; } -const DEVICES: readonly string[] = ["webgpu", "wasm"]; +const DEVICES: readonly string[] = ["webgpu", "wasm", "webnn"]; const DTYPES: readonly string[] = ["fp32", "fp16", "q8", "q4"]; export function isInferenceDevice(value: unknown): value is InferenceDevice {