openwakeword-web
v0.1.0
Published
Native browser (JavaScript) port of openWakeWord. Runs wake word detection fully client-side using ONNX Runtime Web.
Downloads
165
Maintainers
Readme
openWakeWord — native browser port
A native JavaScript / browser port of openWakeWord. Wake word detection runs fully client-side in the browser using ONNX Runtime Web — no Python server, no websocket streaming.
It loads the exact same ONNX models as the Python package (melspectrogram,
speech embedding, and the pre-trained wake word models) and faithfully
reimplements the streaming feature pipeline (AudioFeatures) and prediction
logic (Model.predict) in JavaScript.
Looking for the server-based approach instead? See
[examples/web](../examples/web)for streaming microphone audio over a websocket into openWakeWord running in a Python backend.
Pipeline
mic (16 kHz, 16-bit PCM)
└─ melspectrogram.onnx → mel features (32 bins), transformed x/10 + 2
└─ embedding_model.onnx → 96-dim speech embeddings (76-frame windows, step 8)
└─ <wakeword>.onnx → detection score 0..1Quick start
cd web
npm install # installs onnxruntime-web
npm run download-models # fetches the ONNX models into ./models
npm run serve # or any static file server
# open http://localhost:8080/demo/Then open the page, click Start Listening, and say "hey jarvis", "alexa", "hey mycroft", or "hey rhasspy".
The demo loads
onnxruntime-webfrom a CDN via an import map, so it works without a bundler. A static server is still required because ES modules and the AudioWorklet cannot be loaded fromfile://.
Two hard requirements (browser rules)
- Must be served from
http://localhostorhttps://. ES modules,AudioWorklet, and microphone access all require a real, secure origin, so opening the file directly viafile://will not work. - Model files must be same-origin (or served with CORS headers). The
upstream GitHub release assets do not send
Access-Control-Allow-Origin, so you cannot pointbaseUrldirectly at the GitHub release — host the.onnxfiles yourself (which is whatnpm run download-modelssets up).
Library usage
npm install openwakeword-webimport { OpenWakeWord } from "openwakeword-web";
import { Microphone } from "openwakeword-web/microphone";
const oww = await OpenWakeWord.create({
baseUrl: "./models/",
wakewordModels: ["hey_jarvis", "alexa"], // or omit for all pre-trained models
threshold: 0.5,
onDetection: ({ label, score }) => {
console.log(`Wake word detected: ${label} (score ${score.toFixed(2)})`);
},
});
const mic = new Microphone(async (frame) => {
// frame is an Int16Array of 1280 samples (80 ms @ 16 kHz)
await oww.predict(frame); // onDetection fires automatically when threshold is met
});
await mic.start();predict() accepts any Int16Array of 16 kHz PCM audio (ideally multiples of
1280 samples) and returns a { label: score } map, matching the Python API.
The onDetection callback fires inside predict() for every label whose score
meets threshold — so you don't have to poll the returned scores yourself.
Both threshold and onDetection can be updated at runtime:
oww.threshold = 0.75;
oww.onDetection = ({ label }) => triggerAssistant(label);If you prefer the polling style, simply omit onDetection and inspect the
scores returned by predict() directly:
const scores = await oww.predict(frame);
if (scores["hey_jarvis"] >= 0.5) console.log("detected hey jarvis!");Utterance capture (wake word + command)
Pass onUtterance to automatically record audio from the moment a wake word is
detected until the user stops speaking (via Silero VAD), then receive the full
utterance as a single Int16Array. Feed it to any speech-to-text API.
const oww = await OpenWakeWord.create({
baseUrl: "./models/", // must also contain silero_vad.onnx
wakewordModels: ["hey_jarvis"],
onUtterance: async ({ label, audio }) => {
// `audio` is Int16Array: wake word + user command, 16 kHz PCM
console.log(`Captured ${audio.length / 16000}s after "${label}"`);
await sendToSpeechToText(audio);
},
// optional tuning:
vadStopThreshold: 0.5, // VAD score below this = silence (default 0.5)
vadStopFrames: 6, // consecutive silent frames before firing (~480 ms)
maxCaptureDuration: 10, // hard cap in seconds (default 10)
});
const mic = new Microphone(async (frame) => {
await oww.predict(frame);
});
await mic.start();onDetection and onUtterance can be used together — onDetection fires
immediately when the threshold is crossed; onUtterance fires later once
speech ends.
Model file:
onUtterancerequiressilero_vad.onnxto be served frombaseUrl(or override withvadUrl). The file is included when you runnpm run download-models.
Bundlers:
Microphoneloads its AudioWorklet vianew URL("./mic-worklet.js", import.meta.url), which Vite and webpack 5 handle automatically. If your bundler does not emit the worklet asset, copymic-worklet.jssomewhere same-origin and pass it explicitly:new Microphone(onFrame, { workletUrl: "/mic-worklet.js" }).
TypeScript type definitions (
.d.ts) ship with the package, so the API is fully typed out of the box.
Usage in React (Vite)
Place the .onnx model files in public/assets/models/ so Vite serves them
as static assets, then use a useEffect hook to own the lifecycle:
// src/hooks/useWakeWord.ts
import { useEffect, useRef } from "react";
import { OpenWakeWord, configureOrt } from "openwakeword-web";
import type { DetectionEvent, UtteranceEvent } from "openwakeword-web";
import { Microphone } from "openwakeword-web/microphone";
interface UseWakeWordOptions {
wakewordModels?: string[];
threshold?: number;
onDetection?: (e: DetectionEvent) => void;
/** Receives the full utterance (wake word + command) once speech ends. */
onUtterance?: (e: UtteranceEvent) => void;
vadStopThreshold?: number;
vadStopFrames?: number;
}
export function useWakeWord({
wakewordModels = ["hey_jarvis", "alexa"],
threshold = 0.5,
onDetection,
onUtterance,
vadStopThreshold = 0.5,
vadStopFrames = 6,
}: UseWakeWordOptions = {}) {
// Stable refs so callbacks never cause re-initialisation.
const onDetectionRef = useRef(onDetection);
onDetectionRef.current = onDetection;
const onUtteranceRef = useRef(onUtterance);
onUtteranceRef.current = onUtterance;
useEffect(() => {
let oww: OpenWakeWord | null = null;
let mic: Microphone | null = null;
let cancelled = false;
configureOrt({ numThreads: 1 }); // avoid COOP/COEP requirement
OpenWakeWord.create({
baseUrl: "/assets/models/",
wakewordModels,
threshold,
onDetection: onDetectionRef.current
? (e) => onDetectionRef.current!(e)
: undefined,
onUtterance: onUtteranceRef.current
? (e) => onUtteranceRef.current!(e)
: undefined,
vadStopThreshold,
vadStopFrames,
}).then((instance) => {
if (cancelled) return;
oww = instance;
mic = new Microphone(async (frame) => {
await oww!.predict(frame);
});
mic.start();
});
return () => {
cancelled = true;
mic?.stop();
oww?.reset();
};
}, [threshold, vadStopThreshold, vadStopFrames, ...wakewordModels]);
}// src/App.tsx
import { useWakeWord } from "./hooks/useWakeWord";
export default function App() {
useWakeWord({
wakewordModels: ["hey_jarvis", "alexa"],
onDetection: ({ label }) => console.log(`Detected: ${label}`),
onUtterance: async ({ label, audio }) => {
// audio is Int16Array (16 kHz PCM): wake word + user command
console.log(`Utterance after "${label}": ${audio.length / 16000}s`);
// e.g. send to Whisper or any STT API
},
});
return <div>Listening for wake words…</div>;
}Model files — copy the .onnx files into public/assets/models/:
public/
assets/
models/
melspectrogram.onnx
embedding_model.onnx
silero_vad.onnx ← required for onUtterance
hey_jarvis_v0.1.onnx
alexa_v0.1.onnxYou can copy them from the web/models/ directory after running
npm run download-models in this package, or point baseUrl at any
same-origin path that serves the files with correct CORS headers.
Custom models
const oww = await OpenWakeWord.create({
wakewordModels: [
"alexa", // pre-trained, by name
{ name: "my_word", url: "/models/my_word.onnx" }, // your own trained model
],
});Using your own ONNX runtime / wasm hosting
import { configureOrt } from "./src/openwakeword.js";
configureOrt({ wasmPaths: "/ort/", numThreads: 1 });Multi-threaded wasm requires the page to be cross-origin isolated
(COOP/COEP headers). The demo uses numThreads: 1 to avoid that
requirement.
Layout
| Path | Purpose |
| ----------------------------- | ------------------------------------------------------- |
| src/openwakeword.js | Main OpenWakeWord class — model loading + predict() |
| src/audio-features.js | Streaming melspectrogram + embedding pipeline |
| src/models.js | Pre-trained model registry + class mappings |
| src/microphone.js | Mic capture helper (16 kHz AudioContext + worklet) |
| src/mic-worklet.js | AudioWorklet: float → 16-bit PCM framing |
| demo/index.html | Live detection demo UI (uses the src/ modules) |
| scripts/download-models.mjs | Downloads the ONNX models from the GitHub release |
| test/verify.mjs | Node script that runs the port on the repo's test clips |
Verification
test/verify.mjs streams the repository's test clips
(tests/data/alexa_test.wav, tests/data/hey_mycroft_test.wav) through the
port under Node and checks the correct wake word fires:
cd web
npm test
# alexa_test.wav → alexa = 1.000 PASS
# hey_mycroft_test.wav → hey_mycroft = 1.000 PASSBrowser support
Requires AudioWorklet, ES modules, and AudioContext({ sampleRate: 16000 })
— supported by current Chrome, Edge, Firefox, and Safari. A secure context
(https:// or localhost) is required for microphone access.
License
Apache-2.0, same as openWakeWord. The bundled model files are downloaded from the upstream openWakeWord releases and retain their original licenses.
