From db7b8a22c41ee0185583da8d11ced213c8220e89 Mon Sep 17 00:00:00 2001
From: Lucas Winther
Date: Fri, 10 Jul 2026 11:18:03 +0200
Subject: [PATCH] WebNN support initial
---
src/App.tsx | 8 +++++---
src/components/ModelPicker.tsx | 8 +++++++-
src/lib/models.ts | 23 +++++++++++++++--------
src/worker/inference.worker.ts | 15 ++++++++++++---
src/worker/protocol.ts | 4 ++--
5 files changed, 41 insertions(+), 17 deletions(-)
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 {