<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VoxRT</title>
    <description>The latest articles on DEV Community by VoxRT (@voxrtio).</description>
    <link>https://dev.to/voxrtio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072597%2F9b966283-95b9-478d-9c79-2242f32c080c.png</url>
      <title>DEV Community: VoxRT</title>
      <link>https://dev.to/voxrtio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voxrtio"/>
    <language>en</language>
    <item>
      <title>Four on-device TTS models on one CPU core: LinnetTTS, Piper, Kokoro, pocket-tts</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 09:40:44 +0000</pubDate>
      <link>https://dev.to/voxrtio/four-on-device-tts-models-on-one-cpu-core-linnettts-piper-kokoro-pocket-tts-4lmc</link>
      <guid>https://dev.to/voxrtio/four-on-device-tts-models-on-one-cpu-core-linnettts-piper-kokoro-pocket-tts-4lmc</guid>
      <description>&lt;p&gt;Most CPU TTS writeups benchmark one model in isolation. When you are building a voice product, that is the wrong question. The real one is which small model earns its slot on your CPU budget, and what you give up compared to its neighbours.&lt;/p&gt;

&lt;p&gt;Four open-source TTS models fit under 100 M parameters and target consumer CPUs: &lt;strong&gt;&lt;a href="https://github.com/VoxRT/LinnetTTS" rel="noopener noreferrer"&gt;LinnetTTS 41 M&lt;/a&gt;&lt;/strong&gt; (VoxRT), &lt;strong&gt;&lt;a href="https://github.com/OHF-Voice/piper1-gpl" rel="noopener noreferrer"&gt;Piper medium 16 M&lt;/a&gt;&lt;/strong&gt; (OHF-Voice, originally Rhasspy), &lt;strong&gt;&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro-82M&lt;/a&gt;&lt;/strong&gt; (hexgrad), and &lt;strong&gt;&lt;a href="https://github.com/kyutai-labs/pocket-tts" rel="noopener noreferrer"&gt;pocket-tts 100 M&lt;/a&gt;&lt;/strong&gt; (Kyutai Labs). Each has a different stance on voice cloning, streaming, and multilingual coverage. Here is how they land on the same desktop CPU with the same text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four models
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LinnetTTS 41 M&lt;/strong&gt; (VoxRT, shipped 2026-11-04) is an English-only autoregressive flow LM over a 25 Hz continuous-latent neural codec with a Vocos-style decoder. It clones a voice from a short audio prompt and emits 80 ms of audio per step, so playback can start before the full sentence is generated. Runtime is in Rust, no PyTorch at inference. LinnetTTS is an open-source on-device TTS runtime (Apache-2.0) with openly available model weights under a free license that permits commercial use; voice cloning and model training are licensed separately. Model card on Hugging Face: &lt;a href="https://huggingface.co/VoxRT/linnet-en-35m" rel="noopener noreferrer"&gt;VoxRT/linnet-en-35m&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Piper medium 16 M&lt;/strong&gt; is a VITS-style single-speaker English TTS, one voice per checkpoint, no voice cloning. It processes a sentence as one unit with no in-sentence streaming. GPL-3.0 (the original &lt;code&gt;rhasspy/piper&lt;/code&gt; under MIT is now archived, active development is at &lt;a href="https://github.com/OHF-Voice/piper1-gpl" rel="noopener noreferrer"&gt;OHF-Voice/piper1-gpl&lt;/a&gt; and &lt;code&gt;pip install piper-tts&lt;/code&gt; resolves there). Voices and model files on Hugging Face: &lt;a href="https://huggingface.co/rhasspy/piper-voices" rel="noopener noreferrer"&gt;rhasspy/piper-voices&lt;/a&gt;. Primary audience is offline voice assistants and Home Assistant setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kokoro-82M&lt;/strong&gt; (hexgrad, published early 2025) is a multilingual TTS covering 8 languages with a bank of preset voices. Audio is generated per full utterance with no streaming. Apache-2.0 license. The release has strong quality on short English utterances and has become a common on-device peer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pocket-tts 100 M&lt;/strong&gt; (version 4.0.1 tested) targets real-time on 2-core CPUs with voice prompts and 80 ms streaming. It is similar in shape to Linnet but with a wider parameter budget and a more aggressive multi-core assumption. MIT license. The pip package is &lt;code&gt;pocket-tts&lt;/code&gt;, project homepage at &lt;a href="https://kyutai.org/pocket-tts" rel="noopener noreferrer"&gt;kyutai.org/pocket-tts&lt;/a&gt;. An English model is used here, with 7 languages available as separate models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;Same hardware for all four: a desktop Core i9-13900K running Linux (WSL2) with processes pinned to performance cores via &lt;code&gt;taskset&lt;/code&gt;. One-thread runs use one core, four-thread runs use four physical cores. The same three English test texts go through each model via its own Python package (&lt;code&gt;pip install linnet-tts&lt;/code&gt;, &lt;code&gt;piper-tts&lt;/code&gt;, &lt;code&gt;kokoro-onnx&lt;/code&gt;, &lt;code&gt;pocket-tts&lt;/code&gt;). Peak RSS is &lt;code&gt;ru_maxrss&lt;/code&gt; of the Python process, interpreter and framework included, so these are process-level memory numbers, not raw model-weight sizes on disk.&lt;/p&gt;

&lt;p&gt;One caveat the numbers hide: pocket-tts is designed to run across two cores and reports RTF on two cores. Linnet, Piper, and Kokoro are reported on one core. I kept both configurations because squashing pocket-tts to one core underreports the design intent.&lt;/p&gt;

&lt;p&gt;Speed and memory only. Audio quality is not measured here. You will need to listen to decide that for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;RTF 1-core&lt;/th&gt;
&lt;th&gt;RTF 4-core&lt;/th&gt;
&lt;th&gt;Peak RSS&lt;/th&gt;
&lt;th&gt;First audio&lt;/th&gt;
&lt;th&gt;Streaming&lt;/th&gt;
&lt;th&gt;Voice cloning&lt;/th&gt;
&lt;th&gt;Languages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Piper medium 16 M&lt;/td&gt;
&lt;td&gt;5.8%&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;td&gt;503 MB&lt;/td&gt;
&lt;td&gt;180 ms&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinnetTTS 41 M&lt;/td&gt;
&lt;td&gt;10.6%&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;td&gt;187 MB&lt;/td&gt;
&lt;td&gt;87 ms&lt;/td&gt;
&lt;td&gt;80 ms steps&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pocket-tts 4.0.1 (100 M)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;24.1%&lt;/td&gt;
&lt;td&gt;968 MB&lt;/td&gt;
&lt;td&gt;71 ms&lt;/td&gt;
&lt;td&gt;80 ms steps&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1 (7 separate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kokoro-82M&lt;/td&gt;
&lt;td&gt;51.7%&lt;/td&gt;
&lt;td&gt;17.2%&lt;/td&gt;
&lt;td&gt;1246 MB&lt;/td&gt;
&lt;td&gt;1.5 s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Piper is the fastest here, partly because it is also the smallest and has the simplest decoding path (no voice prompts, no streaming). LinnetTTS is second fastest and the smallest in RSS by a wide margin. pocket-tts hits its design point on 2 cores at 23.8% RTF, and adding more cores barely moves the number (24.1% on four), so plan the budget around 2 cores. It also has the lowest first-audio latency at 71 ms, 16 ms ahead of LinnetTTS at 87 ms. Kokoro is the slowest in this group with 1.2 GB resident, which is the price its multilingual bank of voices pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to pick and when
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Piper&lt;/strong&gt; if you need a single English voice, no cloning, no in-sentence streaming, and the smallest install on disk (&lt;code&gt;.onnx&lt;/code&gt; 64 MB for the medium voice). This is the right call for a static TTS backend on constrained devices, including small Raspberry Pis, where extras are overhead you do not need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick LinnetTTS&lt;/strong&gt; if you need voice cloning from a short prompt, 80 ms streaming so playback starts before the sentence ends, and a smaller resident footprint (187 MB vs 968 MB for pocket-tts, mostly a Rust-vs-PyTorch runtime difference as noted in Known limits). The trade is English only and a hybrid license (Apache code, Linnet Weights License for weights).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick pocket-tts&lt;/strong&gt; if your target is a laptop or desktop with at least 2 performance cores available, you want voice prompts, and you accept around 1 GB of process memory. Fully permissive under MIT if license matters to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Kokoro&lt;/strong&gt; if you need more than one language. It is the only one here with 8-language coverage. The cost is the highest memory and the slowest speed, and no in-sentence streaming.&lt;/p&gt;

&lt;p&gt;Rule of thumb for single-English on CPU: Piper if no cloning, Linnet if cloning. For multilingual CPU: Kokoro, with pocket-tts as an alternate when English-only is enough and memory is not tight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits of this benchmark
&lt;/h2&gt;

&lt;p&gt;Three things this does not tell you. First, audio quality is not scored. Mean opinion scores on a shared test set would need a listening study, which is a different write-up. Second, this is one desktop CPU. Phones and Raspberry Pis will reorder the ranking because the models respond differently to thermal and memory pressure. Third, each model was run through its own Python package, so the numbers include whatever overhead each library imposes. For Piper and Kokoro that is ONNX Runtime, for pocket-tts it is PyTorch, for Linnet it is a thin Rust-backed wrapper. Much of the RSS gap between models is the runtime itself, not the model weights. Raw model-weight files on disk tell a different story: Piper 64 MB, Linnet 83 MB fp16, pocket-tts 236 MB, Kokoro 326 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;The code, Rust runtime, and benchmark script are at &lt;a href="https://github.com/VoxRT/LinnetTTS" rel="noopener noreferrer"&gt;github.com/VoxRT/LinnetTTS&lt;/a&gt;. If you want to hear Linnet alongside the other three on the same text, the &lt;a href="https://huggingface.co/spaces/VoxRT/linnet-tts" rel="noopener noreferrer"&gt;Compare tab on the demo Space&lt;/a&gt; runs all four side by side. Model card and weights are at &lt;a href="https://huggingface.co/VoxRT/linnet-en-35m" rel="noopener noreferrer"&gt;huggingface.co/VoxRT/linnet-en-35m&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Which of these four is sitting in your product right now, and did the switch to a smaller model buy you what you expected, or did you trade quality for speed in a way that bit you later?&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>discuss</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Wake-word vs KWS vs streaming ASR: when to pick what for voice interfaces</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Tue, 03 Nov 2026 07:36:50 +0000</pubDate>
      <link>https://dev.to/voxrtio/wake-word-vs-kws-vs-streaming-asr-when-to-pick-what-for-voice-interfaces-2p5n</link>
      <guid>https://dev.to/voxrtio/wake-word-vs-kws-vs-streaming-asr-when-to-pick-what-for-voice-interfaces-2p5n</guid>
      <description>&lt;h2&gt;
  
  
  Why architecture choice matters
&lt;/h2&gt;

&lt;p&gt;Three different voice ML architectures solve three different problems, but they get mixed up constantly. Picking the wrong one means either overpaying on compute and storage (using full ASR where KWS would do) or underwhelming the UX (trying to scale a wake-word into a task that needed KWS).&lt;/p&gt;

&lt;p&gt;This article is a short decision guide. Three architectures (wake-word, KWS, streaming ASR), a profile of each with real numbers, hybrid flows, a decision tree by use case, and common mistakes we see in projects.&lt;/p&gt;

&lt;p&gt;Numbers below are VoxRT reference measurements (our shipping SDKs). Competitor profiles vary. The point of this article is architectural fit, not vendor benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three architectures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Wake-word (always-on trigger detection)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; 100 KB (verified voxrt-wake-word-linux)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; 5.3% RTF on Raspberry Pi Zero 2 W (one A53 core at 1 GHz)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; under 100 ms detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary:&lt;/strong&gt; 1 fixed trigger phrase ("Hey Assistant" or similar)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case:&lt;/strong&gt; entry trigger for voice interaction (24/7 listening at minimal load)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; cannot do anything beyond detecting the one phrase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wake-word model is the smallest and cheapest of the three. Its job is to listen 24/7 on a minimal CPU budget, decide whether the trigger phrase was said, and hand off to another model or wake up the app. Not used standalone for actual commands.&lt;/p&gt;

&lt;p&gt;Real projects sometimes ship 2 or 3 wake-word models in parallel (an entry trigger plus a barge-in stop phrase, for example). Each adds its own CPU share, but at 5% per model this is affordable on all but the most constrained hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. KWS (keyword spotting)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; 1.3 MB (verified voxrt-kws-linux)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; 32.2% RTF on Pi Zero 2 W&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; 120 ms sustained detection (3 consecutive detection frames at 25 fps) plus output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary:&lt;/strong&gt; 14 fixed commands (&lt;code&gt;yes/no/cancel/play/pause/next/previous/up/down/back/on/off&lt;/code&gt; plus 2 wake-like)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case:&lt;/strong&gt; closed-set voice control (media controls, UI navigation, binary switches, confirmation flows)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; vocabulary is fixed. Custom words require retraining, not runtime config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KWS sits in the middle ground between wake-word and ASR. It knows a limited command set (14 in our case), runs real-time on cheap hardware like Pi Zero 2 W, and is more accurate for its vocabulary than open-vocab ASR because the confusion space is smaller. Underrated for DIY voice-control projects. Most projects reach for ASR when KWS would do the job.&lt;/p&gt;

&lt;p&gt;Beyond raw detection, real KWS deployments spend time tuning per-command thresholds, FAR (false accept rate) vs FRR (false reject rate), and sometimes applying downstream confidence filtering. Default threshold 0.9 is a starting point, not a final answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Streaming ASR (open-vocabulary transcription)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; 60.4 MB (verified voxrt-asr-models v0.1.2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; does not fit Pi Zero 2 W (overload). Works on Pi 4 and up, plus mobile class hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; 200 to 500 ms first partial transcript, more to end of utterance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary:&lt;/strong&gt; open English speech (any word)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case:&lt;/strong&gt; dictation, voice assistant queries, free-form user input, transcription&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; large model, requires meaningful hardware (mobile or Pi 4 minimum)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streaming ASR is the "say anything" architecture. It accepts arbitrary speech and emits text transcripts incrementally. Expensive on compute and storage compared to WW and KWS, but the only path when you need to handle arbitrary user speech.&lt;/p&gt;

&lt;p&gt;Many ASR engines also support hot-word biasing: a runtime list of words to weight higher in decoding (useful for custom product names, people names, domain vocabulary). Sherpa-onnx and whisper.cpp both support this. It is a middle path between the fully open vocabulary and the fully fixed KWS set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid flows
&lt;/h2&gt;

&lt;p&gt;Real production voice systems rarely use just one architecture. Common combinations:&lt;/p&gt;

&lt;h3&gt;
  
  
  Wake-word plus ASR
&lt;/h3&gt;

&lt;p&gt;The voice assistant classic (Alexa, Siri, Google Assistant pattern). Wake-word listens 24/7 cheaply and triggers streaming ASR for the complex query. Battery efficient because ASR only runs when needed, not as a sustained drain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wake-word plus KWS
&lt;/h3&gt;

&lt;p&gt;For constrained UIs: smart display, kiosk, embedded controls, hands-free lab setup. Wake-word gates attention and KWS handles the limited command set. Faster than ASR for known actions because you do not need to load a large model. A good fit for Pi Zero or Pi 4 with media or IoT controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wake-word plus KWS plus ASR (full hybrid)
&lt;/h3&gt;

&lt;p&gt;Pi 4 class or mobile hardware. Wake-word triggers, KWS tries a fast path for common commands, ASR falls back for anything else. Best latency for predictable commands with graceful fallback for complex phrases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge-cloud hybrid
&lt;/h3&gt;

&lt;p&gt;Dominant pattern in shipping consumer products: on-device wake-word triggers cloud ASR. Cheap always-on listening with heavyweight transcription offloaded. Trade-offs on privacy, latency, and ongoing cost. Not an on-device-only architecture, but worth naming because readers often pick it without realizing they have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration note:&lt;/strong&gt; most SDKs do not ship a turnkey multi-stage orchestrator with VAD + audio buffer carryover + state machine for free. Picovoice ships Porcupine + Rhino + Cheetah with documented reference flows. Sherpa-onnx has &lt;code&gt;KeywordSpotter&lt;/code&gt; plus streaming ASR with sample orchestration code in the repo. Everyone else leaves the state machine ("waiting for trigger", "listening for command", "transcribing") to the integrator. Expect to spend meaningful time on orchestration code even if the model integration is simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision tree by use case
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need a single trigger phrase ("Hey X"):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Wake-word only.&lt;/strong&gt; 100 KB, lowest CPU, works on everything down to microcontrollers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need 5 to 14 fixed commands (UI control, media, IoT):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;KWS.&lt;/strong&gt; Faster and more accurate than ASR for this scope. Works on Pi Zero 2 W, Pi 4, mobile. 1.3 MB model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need open vocabulary (dictation, user queries, free-form input):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Streaming ASR.&lt;/strong&gt; Mobile class hardware minimum (Pi 4 or above). 60.4 MB model. No way around it for truly open vocabulary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need both commands and dictation:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Wake-word plus KWS plus ASR hybrid.&lt;/strong&gt; Pi 4 or modern mobile minimum. Plan to write orchestration code yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need custom commands not in the 14-list:&lt;/strong&gt;&lt;br&gt;
→ Three options: &lt;strong&gt;ASR with hot-word biasing&lt;/strong&gt; for a few extra terms, &lt;strong&gt;custom KWS training&lt;/strong&gt; for a few dozen fixed commands, or &lt;strong&gt;open-vocabulary ASR&lt;/strong&gt; for anything beyond that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need multi-language support:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Streaming ASR with multilingual models.&lt;/strong&gt; Whisper multilingual, sherpa-onnx multilingual Zipformer, Vosk 20+ language packs. Wake-word and KWS vocabularies are language-locked to the model you ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target is a noisy environment (room-wide, car cabin, 2+ meters):&lt;/strong&gt;&lt;br&gt;
→ Architecture choice is secondary. &lt;strong&gt;Microphone and audio frontend&lt;/strong&gt; matter more (beamforming array, AEC, noise suppression) than WW vs KWS vs ASR. Expect to spend most engineering time on the acoustic pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware is a microcontroller (ESP32 class):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Wake-word only.&lt;/strong&gt; KWS and ASR do not fit in MCU budgets today. Picovoice Porcupine runs on Cortex-M. TensorFlow Lite Micro has reference keyword-spotting examples. Expect to build the acoustic frontend yourself (PDM mic to MFCC), budget around 10 mW total, and plan for per-device calibration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser (WASM):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Wake-word via WASM&lt;/strong&gt; works (see our &lt;a href="https://dev.to/voxrtio/wake-word-voice-commands-in-the-browser-a-full-offline-pipeline-40eo"&gt;browser wake-word article&lt;/a&gt;). KWS has limited WASM support across vendors. Streaming ASR is possible: sherpa-onnx has WASM builds, whisper.cpp has a WASM port (non-streaming). VoxRT itself does not ship a browser streaming ASR today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always-on considerations
&lt;/h2&gt;

&lt;p&gt;A reader on our recent KWS tutorial raised an important point: benchmark RTF may be measured over a short run, but always-on behavior can differ. Sustained 24/7 inference stacks heat, especially in small cases on Pi Zero 2 W (no stock heatsink, PCB with limited copper heatspreading, RAM PoP-stacked on top of the SoC).&lt;/p&gt;

&lt;p&gt;Rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wake-word at 5% RTF: comfortable 24/7 on the cheapest hardware&lt;/li&gt;
&lt;li&gt;KWS at 32% RTF: works 24/7 but worth monitoring &lt;code&gt;vcgencmd get_throttled&lt;/code&gt; and &lt;code&gt;measure_temp&lt;/code&gt; in production, plus watching for thermal throttling that kicks in mid-inference and drops frames&lt;/li&gt;
&lt;li&gt;Streaming ASR: usually NOT run always-on (triggered by WW). If sustained, hardware choice is critical (Pi 4 with heatsink minimum)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Duty-cycling (sleep between inference bursts), CPU governor settings, and memory fragmentation over multi-day runs are the next layer of concern once the model fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Using ASR when KWS would do.&lt;/strong&gt; Overpaying 60.4 MB and mobile hardware for a 10-command UI. KWS at 1.3 MB does the same job with equal or better accuracy on target vocabulary. We see this regularly. "We just want voice control for 10 things" often ends up in ASR territory when it should not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Using only wake-word when multiple commands are needed.&lt;/strong&gt; Single trigger plus "did it fire?" does not scale beyond entry. Do not bolt separate WW models per command. Use KWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Not budgeting always-on CPU and thermal.&lt;/strong&gt; Wake-word at 5% sustained sounds free. 24/7 means real battery drain and heat buildup. Measure, do not assume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No VAD pre-gate before KWS or ASR.&lt;/strong&gt; Running inference on silence or non-speech wastes compute and makes false-positive rates look worse than they are. A cheap VAD (Silero, WebRTC VAD) in front of KWS and ASR is the single biggest wasted-compute fix we see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Audio buffer sizing and format mismatches.&lt;/strong&gt; Ring buffer too small drops audio under load. Device-native 48 kHz float versus the 16 kHz mono int16 that voice models want is the number-one integration bug on fresh projects. Capture format mismatches silently produce garbage inference. Test the audio path end-to-end before trusting any accuracy number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right fit
&lt;/h2&gt;

&lt;p&gt;A short honest check. What is the actual voice input need?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specific closed command set → &lt;strong&gt;KWS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open phrases or dictation → &lt;strong&gt;streaming ASR&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Just an entry trigger → &lt;strong&gt;wake-word&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mix of commands plus dictation → &lt;strong&gt;hybrid&lt;/strong&gt; (orchestration cost is real)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat on "pick the smallest architecture that handles the use case". If the product roadmap is likely to grow from fixed commands to dictation within a year, shipping KWS first and then re-integrating ASR later means two SDK integrations, two sets of user retraining expectations, and sometimes two license agreements. Future-proofing has a real cost, but so does over-engineering for a hypothetical. Judgment call.&lt;/p&gt;

&lt;p&gt;All three architectures ship as production SDKs in 2026: VoxRT (hybrid license), Picovoice (commercial), sherpa-onnx (Apache-2.0 open), Vosk (Apache-2.0), whisper.cpp (streaming via sliding window, MIT). A prior comparison article &lt;a href="https://dev.to/voxrtio/on-device-streaming-asr-in-2026-five-approaches-and-when-to-pick-what-5h1n"&gt;On-device streaming ASR in 2026&lt;/a&gt; goes deeper on per-vendor trade-offs for ASR specifically.&lt;/p&gt;

&lt;p&gt;If a project does not fit cleanly into one category, you probably need a hybrid. If it does fit, pick the smallest architecture that handles the use case, not the biggest.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>iot</category>
      <category>raspberrypi</category>
      <category>showdev</category>
    </item>
    <item>
      <title>14 voice commands on a $15 Raspberry Pi Zero 2 W (no cloud, no ASR)</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:36:43 +0000</pubDate>
      <link>https://dev.to/voxrtio/14-voice-commands-on-a-15-raspberry-pi-zero-2-w-no-cloud-no-asr-iaj</link>
      <guid>https://dev.to/voxrtio/14-voice-commands-on-a-15-raspberry-pi-zero-2-w-no-cloud-no-asr-iaj</guid>
      <description>&lt;h2&gt;
  
  
  Why KWS instead of full ASR
&lt;/h2&gt;

&lt;p&gt;For most DIY voice-control projects, a full open-vocabulary ASR model is overkill. If you want to control a lamp, a slideshow, a media player, or a simple UI by voice, you do not need a model that can transcribe arbitrary speech. You need to reliably recognize 10 to 20 specific commands.&lt;/p&gt;

&lt;p&gt;That is the job of &lt;strong&gt;KWS (keyword spotting)&lt;/strong&gt;, a separate class of models trained on a fixed closed set of commands. Trade-offs versus full ASR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size.&lt;/strong&gt; Around 1 MB versus 60 MB or more for streaming ASR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU footprint.&lt;/strong&gt; One third of a core on a Pi Zero 2 W versus full-load or overload for streaming ASR on that hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy on target vocabulary.&lt;/strong&gt; Higher than ASR on the same words because the confusion space is smaller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off.&lt;/strong&gt; Vocabulary is fixed. Custom words require retraining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Tens of milliseconds versus hundreds for streaming ASR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For voice-control projects where the command set is small and known in advance, KWS wins on every axis except vocabulary flexibility.&lt;/p&gt;

&lt;p&gt;This article is a tutorial for KWS on a Pi Zero 2 W ($15 hardware, 1 GHz A53). We cover the 14 supported commands, basic detection code, two practical integration examples (GPIO LED and HTTP webhook dispatcher), and honest KWS limits.&lt;/p&gt;

&lt;p&gt;All numbers are verified against the &lt;a href="https://github.com/VoxRT/voxrt-kws-linux" rel="noopener noreferrer"&gt;voxrt-kws-linux&lt;/a&gt; release artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 14 commands
&lt;/h2&gt;

&lt;p&gt;The KWS model supports 14 phrases, grouped by purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmation and control (3):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yes&lt;/code&gt;: confirm&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no&lt;/code&gt;: deny&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cancel&lt;/code&gt;: cancel action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Media playback (5):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;play&lt;/code&gt;: start playback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pause&lt;/code&gt;: pause&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next&lt;/code&gt;: next track or slide&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;previous&lt;/code&gt;: previous&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;back&lt;/code&gt;: back navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Direction (2):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;up&lt;/code&gt;: up (volume, scroll)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;down&lt;/code&gt;: down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switch (2):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on&lt;/code&gt;: turn on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;off&lt;/code&gt;: turn off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wake-like (2):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;voxrt&lt;/code&gt;: brand trigger phrase&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hey_vox&lt;/code&gt;: alternative trigger phrase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twelve functional commands cover most DIY use cases: media control, UI navigation, binary switches, confirmation flows. The two wake-like commands can serve as an entry trigger for multi-step flows (first &lt;code&gt;hey_vox&lt;/code&gt;, then a command).&lt;/p&gt;

&lt;p&gt;The modelled vocabulary is fixed. If you need the word "lights", it is not in the vocab and will not be recognized. Workaround: map semantically (&lt;code&gt;on&lt;/code&gt; and &lt;code&gt;off&lt;/code&gt; to a lamp), or wait for custom training as a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raspberry Pi Zero 2 W ($15, 1 GHz quad-core A53, 512 MB RAM, see &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/" rel="noopener noreferrer"&gt;raspberrypi.com/products/raspberry-pi-zero-2-w&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;USB microphone (see mic quality note below)&lt;/li&gt;
&lt;li&gt;MicroSD card 8 GB or more with Raspberry Pi OS Lite (32-bit)&lt;/li&gt;
&lt;li&gt;Power supply 5V 2.5A&lt;/li&gt;
&lt;li&gt;Optional: GPIO breadboard, LED, and 220 Ohm resistor for the hardware integration example&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mic quality matters more than you'd expect.&lt;/strong&gt; A $5 USB lavalier works if the speaker is 0.5 to 1 meter away in a quiet room. For reliable far-field detection (2+ meters) or noisy rooms, consider a ReSpeaker 4-mic array or Matrix Voice ($35 to $80 extra). Cheap single-element USB mics have poor AGC and no beamforming, which measurably hurts KWS recall in realistic conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Pi Zero 2 W:&lt;/strong&gt;&lt;br&gt;
It is the cheapest Pi that handles the KWS workload in realtime. The original Pi Zero is too weak (single-core 1 GHz, 32-bit ARM11, no NEON SIMD). Pi 3, 4, and 5 run faster but cost $35 to $80. Pi Zero 2 W is the sweet spot for always-on voice-control projects where price and power efficiency matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OS setup:&lt;/strong&gt;&lt;br&gt;
Flash the standard Pi image through Raspberry Pi Imager. After boot, configure Wi-Fi via &lt;code&gt;sudo raspi-config&lt;/code&gt; and enable SSH. For audio, &lt;code&gt;arecord -l&lt;/code&gt; should show the USB mic as a card and the device index (card number). Note that index for the next step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install the SDK and hello world
&lt;/h2&gt;

&lt;p&gt;System dependencies first (fresh Bookworm Lite does not have portaudio headers):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; portaudio19-dev python3-dev python3-venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Python package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create venv (required on Bookworm 2023+ per PEP 668)&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv ~/kws-venv
&lt;span class="nb"&gt;source&lt;/span&gt; ~/kws-venv/bin/activate

&lt;span class="c"&gt;# Install the SDK (Python 3.9 to 3.13 supported, auto-selects arch wheel)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;voxrt-kws
pip &lt;span class="nb"&gt;install &lt;/span&gt;pyaudio

&lt;span class="c"&gt;# Download the model file from the voxrt-kws-models repo (~1.3 MB)&lt;/span&gt;
wget https://github.com/VoxRT/voxrt-kws-models/releases/latest/download/voxrt_kws.vxrt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hello world: detect and print:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_kws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;

&lt;span class="c1"&gt;# Load model (single argument)
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_kws.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configure as attributes (mutable at any time, change safely between chunks)
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;             &lt;span class="c1"&gt;# 0.0-1.0, higher = stricter
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consecutive_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;      &lt;span class="c1"&gt;# 120 ms sustained before fire
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;               &lt;span class="c1"&gt;# ~1 s between repeated emits
&lt;/span&gt;
&lt;span class="c1"&gt;# Audio capture setup: 16 kHz mono PCM int16, 512-sample chunks
# Replace MIC_INDEX with the card number from `arecord -l`
&lt;/span&gt;&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PyAudio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paInt16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input_device_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frames_per_buffer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Listening... say one of the 14 commands&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception_on_overflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# push_pcm_i16 is a generator. Iterate to consume detections.
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heard: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (score &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;python hello_kws.py&lt;/code&gt;. Say &lt;code&gt;yes&lt;/code&gt; and you see &lt;code&gt;heard: yes (score 0.95)&lt;/code&gt; or similar in the terminal. Latency on my Pi Zero 2 W is around 150 ms end-to-end from end of pronunciation to the output line (120 ms consecutive-frame buffer plus inference and output).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MIC_INDEX.&lt;/strong&gt; PyAudio defaults to the system &lt;code&gt;default&lt;/code&gt; device, which on Pi is often the onboard audio (broken for mic input). Always set &lt;code&gt;input_device_index&lt;/code&gt; explicitly from &lt;code&gt;arecord -l&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio format is strict.&lt;/strong&gt; 16 kHz mono, PCM int16 signed little-endian, 512-sample chunks (32 ms). Other formats require resampling via SoX, libsndfile, or similar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exception_on_overflow=False&lt;/code&gt;&lt;/strong&gt; silently drops audio on overrun. On Pi Zero 2 W under system load, this can fire occasionally. For debugging, change to &lt;code&gt;True&lt;/code&gt; to see when.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integration example 1: GPIO LED control
&lt;/h2&gt;

&lt;p&gt;The most maker-friendly example is turning an LED on and off by voice through GPIO. For real use, swap the LED for a relay module and the relay drives a lamp, fan, or anything else on mains power.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wiring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPIO 17 to a 220 Ohm resistor to the LED anode&lt;/li&gt;
&lt;li&gt;LED cathode to GND&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install gpiozero:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;gpiozero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gpiozero&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LED&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_kws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;

&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGE THIS to card number from `arecord -l`
&lt;/span&gt;
&lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_kws.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PyAudio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paInt16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_device_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames_per_buffer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception_on_overflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LED on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LED off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# try/finally guarantees cleanup even on unexpected exceptions
&lt;/span&gt;    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Say &lt;code&gt;on&lt;/code&gt; and the LED lights up. Say &lt;code&gt;off&lt;/code&gt; and it goes dark. The other 12 commands are ignored (not mapped in the handler).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling to real hardware:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;LED&lt;/code&gt; with &lt;code&gt;OutputDevice&lt;/code&gt; for a 5V relay module&lt;/li&gt;
&lt;li&gt;Multiple devices with a dict mapping like &lt;code&gt;{"on": [lamp_relay.on, speaker.on], "off": [...]}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scenes by combining commands (&lt;code&gt;play&lt;/code&gt; turns on lights and starts music via a sequence of actions)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integration example 2: HTTP webhook dispatcher
&lt;/h2&gt;

&lt;p&gt;More flexible than hardware control is a dispatcher that fires HTTP requests to your home automation server. This works on Pi OS Lite (headless, no X11 needed) and adapts to Home Assistant, OpenHAB, Node-RED, or any custom webhook endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install requests:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_kws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;

&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGE THIS to card number from `arecord -l`
&lt;/span&gt;
&lt;span class="c1"&gt;# Point to your home automation webhook (adapt URL/payload to your system)
&lt;/span&gt;&lt;span class="n"&gt;WEBHOOK_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://your-automation-server:8080/voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# non-blocking, next command still fires
&lt;/span&gt;
&lt;span class="n"&gt;ACTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lights_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lights_off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;play&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;music_play&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;music_pause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;music_next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;music_prev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume_up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume_down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_kws.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PyAudio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pyaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paInt16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_device_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MIC_INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames_per_buffer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception_on_overflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ACTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triggered: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern works with any HTTP-reachable automation layer. For Home Assistant you POST to &lt;code&gt;/api/services/light/turn_on&lt;/code&gt; with a Bearer token. For MQTT instead of HTTP, swap &lt;code&gt;requests.post&lt;/code&gt; for &lt;code&gt;paho.mqtt.publish.single&lt;/code&gt;. The dispatcher dict keeps the mapping readable and easy to extend from YAML or JSON config.&lt;/p&gt;

&lt;p&gt;Two things to know. First, only 8 of the 14 commands are mapped above (&lt;code&gt;cancel&lt;/code&gt;, &lt;code&gt;back&lt;/code&gt;, &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;voxrt&lt;/code&gt;, &lt;code&gt;hey_vox&lt;/code&gt; fall through silently). Add them to &lt;code&gt;ACTIONS&lt;/code&gt; to handle more. Second, &lt;code&gt;requests.post&lt;/code&gt; blocks the loop for up to the timeout. If your webhook endpoint is slow, wrap it in a thread or queue to avoid audio buffer drops while waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks on Pi Zero 2 W
&lt;/h2&gt;

&lt;p&gt;Measurements on my Pi Zero 2 W (1 GHz A53, 32-bit Raspberry Pi OS Bookworm):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTF (CPU usage for realtime)&lt;/td&gt;
&lt;td&gt;32.2% of one A53 core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference latency per chunk&lt;/td&gt;
&lt;td&gt;around 10 ms (p99 13 ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end detection latency&lt;/td&gt;
&lt;td&gt;120 ms (3 consecutive frames at 32 ms) plus output time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model file on disk&lt;/td&gt;
&lt;td&gt;1.3 MB (fp16, encrypted .vxrt)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power under KWS load&lt;/td&gt;
&lt;td&gt;approximately 1 W (community-measured, varies with mic and setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RTF 32.2% on one core means you can run KWS 24/7 on a Pi Zero 2 W and still have most of the quad-core available for your app logic (relay control, HTTP server, display rendering). For $15 of hardware and roughly 1 W of power, you get honest realtime always-on voice control.&lt;/p&gt;

&lt;p&gt;For comparison: full streaming ASR on the same hardware does not fit (CPU overload). Wake-word only (covered in a &lt;a href="https://dev.to/voxrtio/wake-word-on-a-15-raspberry-pi-zero-2-w-53-rtf-always-on-4f5m"&gt;previous article about Pi Zero&lt;/a&gt;) takes 5.3% on the same core but is limited to a single trigger. KWS is the middle ground.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Battery estimate:&lt;/strong&gt; at roughly 1 W total draw, a 10000 mAh USB power bank (around 37 Wh usable after conversion losses) runs the setup for approximately 30 hours. Enough for multi-day field deployments if you add solar charging or battery swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest KWS limits
&lt;/h2&gt;

&lt;p&gt;Where KWS fails and when to pick a different architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed vocabulary.&lt;/strong&gt; 14 commands is the ceiling. If a project needs "turn on the bedroom light", the word "bedroom" is not in the vocab and will not be recognized. Workaround: a two-step flow (&lt;code&gt;hey_vox&lt;/code&gt; as a trigger for an ASR path that handles complex phrases). Note that the handoff between KWS and ASR requires custom orchestration (audio buffer carryover, ASR instantiation timing, VAD gating). It is not a prebuilt SDK feature today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Far-field performance.&lt;/strong&gt; KWS works best at close range (0.5 to 1 meter from the mic). Beyond 2 meters accuracy drops noticeably. For room-wide voice control, use a better mic array (ReSpeaker 4-mic at ~$35 or Matrix Voice at ~$70) or place the mic closer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Background noise.&lt;/strong&gt; The default threshold of 0.9 is tuned for quiet environments. In a room with TV or music in the background you have to raise the threshold (fewer false positives, more missed detections) or add a VAD pre-gate to filter non-speech sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Accent variability.&lt;/strong&gt; The model was trained primarily on American English pronunciations. Non-native English speakers and strong regional accents can see noticeably lower recall. Pronunciation tweaks (saying &lt;code&gt;pause&lt;/code&gt; more like "pawz") sometimes help marginally but are not a real fix. Realistic options: raise or lower the threshold per-speaker, accept degraded recall, or wait for custom training support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. No custom commands currently.&lt;/strong&gt; If a project needs specific words outside the 14-list, custom training is required and that is not shipping as a public SDK feature today. Options: stay inside the 14-list, use &lt;code&gt;yes&lt;/code&gt; and &lt;code&gt;no&lt;/code&gt; and &lt;code&gt;cancel&lt;/code&gt; plus an ASR path for complex interactions, or look at vendor options like Picovoice Rhino intents, Snips, or Rhasspy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When KWS versus alternatives
&lt;/h2&gt;

&lt;p&gt;A short decision guide:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick KWS if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project needs up to 14 fixed commands&lt;/li&gt;
&lt;li&gt;Pi Zero or similar low-power hardware&lt;/li&gt;
&lt;li&gt;Always-on operation matters (CPU budget is tight)&lt;/li&gt;
&lt;li&gt;Latency under 150 ms is a requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick wake-word (not KWS) if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only need 1 trigger phrase (not 14)&lt;/li&gt;
&lt;li&gt;Hardware is even more constrained (microcontroller or original Pi Zero)&lt;/li&gt;
&lt;li&gt;A 100 KB model is preferred over 1.3 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick streaming ASR if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vocabulary is open or over 20 words&lt;/li&gt;
&lt;li&gt;Arbitrary user phrases&lt;/li&gt;
&lt;li&gt;Platform is at least Pi 4 or mobile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick a KWS plus ASR hybrid if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need both simple commands (fast path via KWS) and complex phrases (fallback via ASR)&lt;/li&gt;
&lt;li&gt;Hardware allows both (Pi 4 and up)&lt;/li&gt;
&lt;li&gt;You are willing to build the orchestration code yourself (not a prebuilt feature)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to build next
&lt;/h2&gt;

&lt;p&gt;This tutorial is a minimal setup. From here you can extend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-room setup:&lt;/strong&gt; several Pi Zero units publishing commands via MQTT to a central controller&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice-controlled dashboard:&lt;/strong&gt; display state plus voice navigation for kiosk mode (requires Pi OS Desktop)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility tool:&lt;/strong&gt; voice shortcuts for users with mobility limitations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hands-free lab setup:&lt;/strong&gt; voice commands for photo capture (3D scanning rigs), timer start/stop (chemistry experiments)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery-powered portable device:&lt;/strong&gt; Pi Zero 2 W plus a USB power bank plus a USB mic gives around 30 hours on a 10000 mAh pack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All code from this tutorial is available in the examples folder of &lt;a href="https://github.com/VoxRT/voxrt-kws-linux" rel="noopener noreferrer"&gt;voxrt-kws-linux&lt;/a&gt;. If you build something interesting, drop a comment with what you made.&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>tutorial</category>
      <category>iot</category>
      <category>python</category>
    </item>
    <item>
      <title>On-device streaming ASR in 2026: five approaches and when to pick what</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:35:11 +0000</pubDate>
      <link>https://dev.to/voxrtio/on-device-streaming-asr-in-2026-five-approaches-and-when-to-pick-what-5h1n</link>
      <guid>https://dev.to/voxrtio/on-device-streaming-asr-in-2026-five-approaches-and-when-to-pick-what-5h1n</guid>
      <description>&lt;h2&gt;
  
  
  Why this topic right now
&lt;/h2&gt;

&lt;p&gt;In 2023 there were two options. Take OpenAI Whisper and live with offline batch inference (3+ GB large-v3, not streaming), or wait for cloud ASR with its privacy, cost, and latency trade-offs. On-device streaming ASR as a production option was fringe.&lt;/p&gt;

&lt;p&gt;In 2026 there are at least five viable approaches on the table, each optimized for a different use case and different constraints. This post is an ecosystem overview and decision guide, not a head-to-head benchmark with a single winner.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is being compared, and what is not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In scope:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming. The model emits partial transcripts incrementally, not waiting for end of utterance. First-partial latency measured in hundreds of milliseconds.&lt;/li&gt;
&lt;li&gt;On-device. Inference runs locally, no network round-trip, no cloud dependency.&lt;/li&gt;
&lt;li&gt;Production-ready. Shipping in real mobile and edge apps today, not research prototypes.&lt;/li&gt;
&lt;li&gt;Five options that meet all of these criteria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Out of scope:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud ASR APIs (Deepgram, Whisper API, Google USM via API). Different trade-off entirely.&lt;/li&gt;
&lt;li&gt;Batch-only on-device models (Whisper offline without a streaming wrapper). Not streaming.&lt;/li&gt;
&lt;li&gt;Wake-word and keyword spotting. Narrower task scope, different SDKs.&lt;/li&gt;
&lt;li&gt;ASR at research stage without a production SDK.&lt;/li&gt;
&lt;li&gt;A few worth naming but excluded for specific reasons: NVIDIA Riva (Jetson-tier and server, not mobile-first), Moonshine from Useful Sensors (recent 2024 release, short production track record), SeamlessStreaming from Meta (speech translation primary, not pure ASR). All legitimate for adjacent use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Methodology for the numbers:&lt;/strong&gt;&lt;br&gt;
All size, platform, and license claims come from each vendor's own published sources (model cards, release artifacts, official docs). There are no cross-benchmarks of our test subset against someone else's full set. Apples-to-apples comparison is only honest when the same test methodology is applied to every cell. Where specific accuracy numbers are needed, the post names the test set and whose claim it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. VoxRT
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; 60.4 MB (verified against the &lt;a href="https://github.com/VoxRT/voxrt-asr-models/releases" rel="noopener noreferrer"&gt;voxrt-asr-models&lt;/a&gt; v0.1.2 release artifact)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; NeMo FastConformer family, RNN-T decoding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms:&lt;/strong&gt; iOS, Android, Linux (aarch64, tested on Raspberry Pi)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; hybrid. Apache-2.0 on the SDK wrapper, proprietary runtime binary, CC-BY-4.0 for model weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; English, open-vocabulary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; native, 200 ms chunk size (3200 samples at 16 kHz)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; closed runtime, open SDK wrappers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repos:&lt;/strong&gt; &lt;a href="https://github.com/VoxRT/voxrt-asr-linux" rel="noopener noreferrer"&gt;voxrt-asr-linux&lt;/a&gt;, &lt;a href="https://github.com/VoxRT/voxrt-asr-ios" rel="noopener noreferrer"&gt;voxrt-asr-ios&lt;/a&gt;, &lt;a href="https://github.com/VoxRT/voxrt-asr-android" rel="noopener noreferrer"&gt;voxrt-asr-android&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shipping since 2026, targeting iOS, Android, and Linux (no desktop or browser builds today). The hybrid license covers three layers. Apache-2.0 on the wrapper SDK means you can fork and modify integration code. The proprietary runtime binary cannot be audited, forked, patched, or deployed outside VoxRT's distribution. CC-BY-4.0 model weights can be redistributed with attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Picovoice Cheetah
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; compact (vendor does not publish an exact figure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; end-to-end neural network (closed-source, specifics not published)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms:&lt;/strong&gt; iOS, Android, Linux, macOS, Windows, Raspberry Pi, WebAssembly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; commercial. Free tier for personal and non-commercial use, paid tier for production commercial use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; 8 languages per &lt;a href="https://picovoice.ai/docs/cheetah/" rel="noopener noreferrer"&gt;vendor docs&lt;/a&gt; (English, French, German, Italian, Japanese, Korean, Spanish, Portuguese)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; closed-source, commercial product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://picovoice.ai/platform/cheetah" rel="noopener noreferrer"&gt;picovoice.ai/platform/cheetah&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Picovoice is a commercial on-device voice AI vendor with wide platform coverage. The company separately offers microcontroller support for wake-word and keyword spotting (Porcupine and Rhino SDKs), but Cheetah streaming ASR itself targets mobile, desktop, Raspberry Pi, and web. Business model is SaaS-style licensing with a free tier for exploration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. sherpa-onnx (k2-fsa project)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; roughly 40 MB (20M-param &lt;a href="https://k2-fsa.github.io/sherpa/onnx/pretrained_models/online-transducer/zipformer-transducer-models.html" rel="noopener noreferrer"&gt;streaming Zipformer int8&lt;/a&gt;) to 340 MB (full fp32), depending on model configuration and quantization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; streaming Zipformer, deployed via ONNX runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms:&lt;/strong&gt; Linux, macOS, Windows, Android, iOS, HarmonyOS, embedded (very wide)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0 (code and most models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; multi-language. English, Chinese, and many others depending on the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; native. Zipformer was designed specifically for streaming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; open-source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/k2-fsa/sherpa-onnx" rel="noopener noreferrer"&gt;github.com/k2-fsa/sherpa-onnx&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Part of the broader k2-fsa project (Next-generation Kaldi). Very active open-source community under the k2-fsa umbrella. The model zoo keeps growing, with streaming Zipformer models for different languages and different sizes. Using ONNX runtime for inference keeps deployment portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Vosk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; small models around 40-50 MB (vosk-model-small-en-us-0.15 = 40 MB), standard models up to ~4.4 GB (largest is vosk-model-de-tuda-0.6-900k) per the &lt;a href="https://alphacephei.com/vosk/models" rel="noopener noreferrer"&gt;Vosk models page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; Kaldi-based (TDNN acoustic model plus n-gram or RNNLM language model)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms:&lt;/strong&gt; Python, Node.js, Java, Android, iOS, C#, Go, C++ (wide binding support)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; 20+ languages with pretrained models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; open-source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://alphacephei.com/vosk" rel="noopener noreferrer"&gt;alphacephei.com/vosk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Long-running open-source project (Alpha Cephei). Kaldi at the core is proven technology, with pre-conformer architecture (TDNN plus HMM-style decoding). One of the smallest production-ready options in this list through the 40 MB small model.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. whisper.cpp (with the streaming example)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size:&lt;/strong&gt; tiny 75 MB, base 142 MB, small 466 MB, medium 1.5 GB, large 2.9+ GB (per the &lt;a href="https://github.com/openai/whisper#available-models-and-languages" rel="noopener noreferrer"&gt;OpenAI Whisper README&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; OpenAI Whisper (encoder-decoder transformer), ported to C/C++&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms:&lt;/strong&gt; Linux, macOS, Windows, iOS, Android, Web (via WASM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT (whisper.cpp code), MIT (Whisper weights from OpenAI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; &lt;a href="https://github.com/openai/whisper#available-models-and-languages" rel="noopener noreferrer"&gt;99 languages&lt;/a&gt; (multilingual Whisper models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; &lt;strong&gt;not native&lt;/strong&gt;. Whisper is a batch model, and whisper.cpp has a &lt;code&gt;stream&lt;/code&gt; example built on a sliding-window pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; open-source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ggml-org/whisper.cpp" rel="noopener noreferrer"&gt;github.com/ggml-org/whisper.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An important caveat. Whisper is architecturally a batch model that processes 30-second windows end-to-end. The streaming example in whisper.cpp creates streaming-like behavior through overlapping sliding windows, but first-partial latency is higher than for models designed around streaming, and accuracy is reported to degrade on short chunks. For use cases where accuracy is critical and latency is secondary, this is a valid approach. For real-time dictation with a &amp;lt;500 ms first-partial requirement, the streaming-first models in items 1-4 above are a closer fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;VoxRT&lt;/th&gt;
&lt;th&gt;Picovoice Cheetah&lt;/th&gt;
&lt;th&gt;sherpa-onnx&lt;/th&gt;
&lt;th&gt;Vosk&lt;/th&gt;
&lt;th&gt;whisper.cpp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60 MB&lt;/td&gt;
&lt;td&gt;Compact (not published)&lt;/td&gt;
&lt;td&gt;~40 MB int8 lightweight to ~340 MB fp32&lt;/td&gt;
&lt;td&gt;40 MB small, up to 4.4 GB&lt;/td&gt;
&lt;td&gt;75 MB tiny, 466 MB small, 1.5 GB med&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Sliding-window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platforms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS, Android, Linux&lt;/td&gt;
&lt;td&gt;iOS, Android, Linux, Mac, Win, RPi, WASM&lt;/td&gt;
&lt;td&gt;Linux, Mac, Win, iOS, Android, HarmonyOS, embedded&lt;/td&gt;
&lt;td&gt;Py, Node, Java, Android, iOS, C#, Go, C++&lt;/td&gt;
&lt;td&gt;Linux, Mac, Win, iOS, Android, Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hybrid (Apache SDK, proprietary runtime, CC-BY model)&lt;/td&gt;
&lt;td&gt;Commercial (free tier plus paid)&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Languages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;8 languages&lt;/td&gt;
&lt;td&gt;Multi-language&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runtime closed&lt;/td&gt;
&lt;td&gt;Closed&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First-partial latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native streaming&lt;/td&gt;
&lt;td&gt;Native streaming&lt;/td&gt;
&lt;td&gt;Native streaming&lt;/td&gt;
&lt;td&gt;Native streaming&lt;/td&gt;
&lt;td&gt;Higher (sliding window)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Platform notes: Picovoice separately offers MCU support (Cortex-M) for wake-word and keyword spotting (Porcupine, Rhino), not for Cheetah streaming ASR. sherpa-onnx supports a wide embedded range including RISC-V.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision tree by use case
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Smallest model size matters above all other considerations:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;Vosk small&lt;/strong&gt; (40 MB) or &lt;strong&gt;Picovoice Cheetah&lt;/strong&gt; (compact). Trade-off: Vosk is pre-conformer accuracy, Picovoice is commercial license.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-source plus production-ready streaming plus platform maturity:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;sherpa-onnx&lt;/strong&gt; (Zipformer well-supported, large community) or &lt;strong&gt;Vosk&lt;/strong&gt; (older but stable).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-language from day one:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;whisper.cpp&lt;/strong&gt; (99 languages) or &lt;strong&gt;Vosk&lt;/strong&gt; (20+). VoxRT and Picovoice are English-focused today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target is embedded or microcontroller:&lt;/strong&gt;&lt;br&gt;
→ For embedded Linux (ARM, RISC-V), &lt;strong&gt;sherpa-onnx&lt;/strong&gt; has the widest support. For microcontrollers (Cortex-M), full ASR is not realistic today. Picovoice's Porcupine/Rhino offer wake-word and keyword spotting at that tier, which is a different task than streaming ASR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile voice input, English, want a simple SDK integration and streaming-first behavior:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;VoxRT&lt;/strong&gt; or &lt;strong&gt;Picovoice Cheetah&lt;/strong&gt;. Trade-off: VoxRT has a hybrid license (proprietary runtime), Picovoice uses a commercial subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whisper quality required, latency secondary:&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;whisper.cpp&lt;/strong&gt; (streaming is a sliding-window pattern but accuracy is preserved).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser inference (WASM):&lt;/strong&gt;&lt;br&gt;
→ &lt;strong&gt;whisper.cpp&lt;/strong&gt; (WASM supported) or &lt;strong&gt;sherpa-onnx&lt;/strong&gt; (if the WASM path works for your model configuration, verify before integrating). VoxRT does not ship ASR for the browser today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing fits cleanly:&lt;/strong&gt;&lt;br&gt;
→ Download your top two candidates based on size, license, and platform fit. Run them on your actual audio data. Measure RTF and accuracy. Decide on measured numbers, not predicted ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-offs. Where someone else beats us.
&lt;/h2&gt;

&lt;p&gt;I work on VoxRT, but a fair comparison needs an honest picture of where we do not win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vosk small is smaller than our model&lt;/strong&gt; (around 40 MB versus our 60 MB). If bytes-on-disk is the primary constraint, Vosk small wins on that axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sherpa-onnx has wider platform coverage for embedded Linux targets&lt;/strong&gt; (ARM, RISC-V). Browser (WASM) is also not something we ship. For those targets we are not an option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sherpa-onnx has significantly wider language coverage.&lt;/strong&gt; Chinese, Japanese, European languages with active model development. We are English-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;whisper.cpp handles 99 languages&lt;/strong&gt; through OpenAI multilingual models. For truly multilingual products, the Whisper family is still the baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sherpa-onnx and Vosk are fully open-source&lt;/strong&gt; (Apache-2.0). If your organization will not accept a proprietary runtime binary, our hybrid license does not fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picovoice has a longer track record&lt;/strong&gt; as an established commercial vendor. For teams that want a paid support contract and SLA, Picovoice fits traditional procurement better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VoxRT is less than a year old.&lt;/strong&gt; Picovoice has multi-year production use, Vosk and whisper.cpp are each years into shipping. If track record is a procurement gate, we lack it today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We do not publish WER numbers currently.&lt;/strong&gt; Picovoice, Vosk, and sherpa-onnx all publish WER evaluations on standard benchmarks. If your decision gates on specific accuracy numbers from the vendor, that absence is a real gap for us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closed runtime means you cannot patch bugs, fork for custom modifications, or audit the inference path.&lt;/strong&gt; For teams that require full-source transparency, our hybrid license disqualifies us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VoxRT was built around a specific trade-off: streaming-first behavior, mobile focus, hybrid license acceptable, English voice input. For that slice we think the engineering is sound. For other slices, the options above are a better fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ecosystem is healthier than it was before 2024
&lt;/h2&gt;

&lt;p&gt;Three years ago this would have been a much shorter post. Whisper had just shipped (2022), on-device streaming ASR was at research stage, Vosk was the best open-source option but had a visible accuracy gap versus cloud, and Picovoice was the commercial choice but with narrower vocabulary coverage.&lt;/p&gt;

&lt;p&gt;In 2026 there are five production-ready options, with different architectural choices, different license models, and different target hardware. The one-size-fits-all approach does not work anymore because use cases are not one size.&lt;/p&gt;

&lt;p&gt;Pick based on actual constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;License tolerance (open-only? hybrid acceptable? commercial subscription fine?)&lt;/li&gt;
&lt;li&gt;Platform requirements (mobile only? MCU too? browser?)&lt;/li&gt;
&lt;li&gt;Language requirements (English only? multi-language needed?)&lt;/li&gt;
&lt;li&gt;Model size budget (bytes on disk? runtime memory?)&lt;/li&gt;
&lt;li&gt;Latency requirements (real-time dictation? post-utterance OK?)&lt;/li&gt;
&lt;li&gt;Support expectations (self-service open-source? paid vendor contract?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All five options in this post are production-ready for shipping in 2026. If you cannot pick between options, download your top two candidates by size, license, and platform fit, and run them on your real data. Measure. Decide on the numbers, not the marketing (including mine).&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Why we built voice AI for the device, not the cloud</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:23:06 +0000</pubDate>
      <link>https://dev.to/voxrtio/why-we-built-voice-ai-for-the-device-not-the-cloud-pec</link>
      <guid>https://dev.to/voxrtio/why-we-built-voice-ai-for-the-device-not-the-cloud-pec</guid>
      <description>&lt;h2&gt;
  
  
  The default is to send voice to the cloud
&lt;/h2&gt;

&lt;p&gt;If you are shipping a voice feature in your app today, odds are you are sending audio to a server. That is how most existing solutions work. The mobile app captures sound from the microphone, packages it as WAV or opus, streams it to an API endpoint, and gets back JSON with the recognized text. Your business logic then operates on text.&lt;/p&gt;

&lt;p&gt;This pattern dominates not because it is the best approach, but because historically it was the only one that worked at real production model sizes. Open ASR models were too big. On-device model quality was noticeably worse than the cloud equivalents. Mobile hardware could not sustain ML inference in the background without destroying battery life inside an hour.&lt;/p&gt;

&lt;p&gt;The trade-off did not satisfy us for very long. We built the VoxRT stack for on-device from day one. We did not migrate from the cloud. We picked a different path from the start. This article covers why that decision keeps paying off, what trade-offs we accepted, and where cloud still legitimately wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics that changed our thinking
&lt;/h2&gt;

&lt;p&gt;Picture a hypothetical voice-notes app with 100,000 monthly active users. The average user dictates 20 minutes a month. That is 2 million minutes of audio to transcribe.&lt;/p&gt;

&lt;p&gt;Take a typical cloud ASR provider rate of roughly $0.004 per minute, which is on the lower end of mainstream pricing today. The arithmetic is simple. 2,000,000 x $0.004 = &lt;strong&gt;$8,000 a month&lt;/strong&gt;. $96,000 a year. For transcription alone. No wake-word, no keyword spotting, no TTS.&lt;/p&gt;

&lt;p&gt;Now consider on-device. The engineering work is upfront, not per-user. Integrating an on-device SDK is usually 2 to 6 weeks for a straightforward single-platform start, longer when you ship across multiple platforms or need custom optimization. There is ongoing cost too: SDK upgrades when model versions improve, device-specific regressions to debug, new OS releases to validate against. None of it scales linearly with user minutes. The per-unit variable cost drops to zero.&lt;/p&gt;

&lt;p&gt;One factor we do not want to hide: on-device SDKs are not universally free. Our stack ships under a hybrid license (open wrappers, proprietary runtime), and other on-device voice vendors use per-seat, per-deployment, or revenue-share pricing. For most teams the engineering cost plus any license fee combined still beats per-minute cloud pricing at scale, but the full picture requires factoring both sides, not just the per-minute delta.&lt;/p&gt;

&lt;p&gt;The breakeven point depends on the user base. For our hypothetical app, a few thousand paying users are enough for on-device to beat cloud economics. If you expect your app to reach hundreds of thousands or millions of users, on-device wins over the long run almost without exception.&lt;/p&gt;

&lt;p&gt;The calculation also ignores second-order effects. No cold-start latency from the network. No rate limits during peak load. No scaling cost when you grow. No backend team to monitor the uptime of your ASR pipeline. Those effects compound differently per product. For a voice-notes app running at scale, the first two matter most. For an occasional-use feature in a bigger app, the engineering commitment to a mobile SDK may not pay off even if the per-minute math eventually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cases where on-device wins cleanly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privacy is a legal requirement.&lt;/strong&gt; Healthcare apps with voice notes. Legal tools recording client meetings. Personal journals. For these use cases, sending audio to a third-party server is not a performance question. It is a compliance question. HIPAA, GDPR, and the EU AI Act are all simpler to operate under when the audio never physically leaves the device. It is easier to prove to a lawyer that user voice was never sent to a server when the stack is architecturally incapable of making network calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app has to work offline.&lt;/strong&gt; Field-work tools for inspectors, construction workers, and surveyors. Travel apps on airplanes or in countries without data roaming. Workflows for medics and first responders in remote areas. If a user needs voice input in these contexts, cloud-only is not an option. You can build an offline fallback through a cache, but if you are already building the on-device part, why keep the cloud path running in parallel?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency has to be below 100 ms.&lt;/strong&gt; For wake-word and hot-word detection, the cycle "user said it, app responded" must feel instant. Any network round-trip breaks the UX. Even on perfect 5G, you lose 50 to 150 ms to RTT alone, plus server-side inference. On-device wake-word inference typically lands between 50 and 200 ms end-to-end. For a wake-detection flow, that is the difference between "this works" and "this is annoying."&lt;/p&gt;

&lt;p&gt;These three cases are not edge cases. They are a substantial slice of real voice use cases: privacy-sensitive apps, offline-first tools, and always-listening trigger layers inside a larger voice stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is genuinely hard about on-device
&lt;/h2&gt;

&lt;p&gt;This article would not be honest without a section on the trade-offs we accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model size versus quality.&lt;/strong&gt; To fit a model into 100 KB (our wake-word), you cut the architecture deep. For our wake-word we accepted a fixed trigger phrase ("Hey Assistant") and ship one proven model, not a custom-trained one per user. That worked for our scenario, but it means we cannot offer custom trigger phrases without a training pipeline that we have not released publicly. The streaming ASR model is larger (around 60 MB of weights for the medium model), still well under what cloud providers run server-side on their multi-gigabyte models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware fragmentation is real.&lt;/strong&gt; We ship across iOS, Android, browser (via WASM), and Linux (Raspberry Pi, Jetson, servers). The shared Rust runtime compiles to each target, but every target still needs its own benchmark run and build tuning for local SIMD extensions, memory layout, and audio API conventions. Publishing a single "RTF of 0.05" number across all platforms is not honest. The reality is 5 to 10 benchmark points on different devices, each with its own caveats. That is more work than cloud providers face, because their infrastructure is uniform while ours is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updates ship through the SDK release cycle.&lt;/strong&gt; If you have a bug in a cloud ASR, you push a fix to the server and everything is current tomorrow. For us, a bug means a new SDK version, npm/pip/CocoaPods updates, and users have to upgrade. Mobile developers know this pain from any native library. A stable C ABI between the runtime and the SDK bindings reduces the breaking-change frequency, but it does not remove the user-upgrade step. That is a real friction we have not solved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmarking requires discipline.&lt;/strong&gt; It is easy to cherry-pick one device and publish a pretty number. It is hard to publish a multi-device matrix with measured RTF, different CPU budgets on different cores, and p99 chunk latency. We make this harder on ourselves than we have to because we think the alternative misleads integrators who then build on expectations that do not match their hardware. The problem compounds every time a new flagship phone ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-device debugging in production is thinner.&lt;/strong&gt; When a cloud ASR regresses on a specific user, the server team has logs, metrics, and the ability to replay the audio. On-device, your telemetry surface is whatever you can get back from the user's device without violating the privacy premise that made you pick on-device in the first place. We ship RTF counters and detection scores back via opt-in telemetry only, and even that cuts off at what a user is comfortable sharing. Diagnosing a regression on one specific chipset family often means buying that device and reproducing the issue locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this trade-off actually lands
&lt;/h2&gt;

&lt;p&gt;We share the numbers below not as a pitch but as evidence that the on-device path has a real engineering shape in 2026, not just an aspirational one. Each of these came out of an actual measurement that we had to defend internally before publishing.&lt;/p&gt;

&lt;p&gt;A wake-word model that fits in 100 KB and sustains 5.3% of one A53 core on a $15 Raspberry Pi Zero 2 W is on the extreme end of what on-device looks like today. On a Pi 4 B or Pi 5 it drops to 1 to 2% CPU. The constraints that forced the 100 KB target (single trigger phrase, fixed, no custom training in-SDK) are real limitations, not features. We shipped them because the alternative was not shipping at all on hardware this cheap.&lt;/p&gt;

&lt;p&gt;A voice activity detector running at 3% CPU on a mid-range Android (Snapdragon 662 class) is the ceiling that lets us run always-on microphone capture without destroying battery. We only ship VAD on mobile, not in browser, because the WASM profile for continuous audio capture is different enough that we have not justified the engineering investment yet.&lt;/p&gt;

&lt;p&gt;Keyword spotting across 14 fixed English commands at 0.97% RTF in Chrome WASM and 16% RTF on Snapdragon 662 is what makes a browser voice-UX feel instant. The full browser stack (runtime included) bundles to about 1.47 MB. That is small enough to not matter for most apps and large enough to matter for landing pages optimized for mobile 3G.&lt;/p&gt;

&lt;p&gt;Streaming ASR (NeMo FastConformer, 60 MB weights) runs real-time on mobile. Partial transcripts arrive while the user is still speaking, with finalization after a silence timeout. This is the model that would have been impossible to ship on-device three years ago. Today it is a shipping choice.&lt;/p&gt;

&lt;p&gt;These numbers live in the READMEs at github.com/VoxRT. We publish them with the test set, the hardware, the methodology, and the caveats. We do not publish subset measurements next to another vendor's full-set measurements. If our wake-word gets compared with another approach, both numbers need to be on the same test set, or it is not a comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where cloud still wins honestly
&lt;/h2&gt;

&lt;p&gt;Cloud ASR is the right call for several scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multilingual coverage across 50-plus languages.&lt;/strong&gt; Open ASR models for rarer languages are often dramatically worse than cloud equivalents. If you need voice in Swahili, Thai, or Tagalog, cloud providers are still ahead. On-device multilingual coverage is catching up, but slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast iteration on models.&lt;/strong&gt; A cloud team can push an improved model today, and tomorrow every user gets better accuracy without any change to their app. On-device apps have to re-ship. For products that iterate fast on model quality, that is a meaningful velocity difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise compliance tooling around the API.&lt;/strong&gt; Major cloud ASR providers come with SOC2, HIPAA, and PCI-DSS compliance infrastructure, audit logs, and enterprise SLA agreements. If you sell into the Fortune 500, buying compliance through a vendor is sometimes easier than proving it yourself for on-device deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-ended complex workflows.&lt;/strong&gt; If your voice UX is "ask AI a complex question via voice, run it through ASR, pipe it to an LLM, get a voice response," the entire pipeline already lives on a server. Adding transcription on the same server costs you very little extra latency. The on-device win on the first step is small when every other step already runs in the cloud.&lt;/p&gt;

&lt;p&gt;These cases are real. On-device is not a universal answer. It is the right answer for a specific set of product scenarios that are becoming more common.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;To understand why we think on-device picks up more market share, it helps to see how much ground has already shifted. Capabilities that were impractical or research-only three years ago are standard engineering choices today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;2022-2023 state&lt;/th&gt;
&lt;th&gt;2026 state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streaming ASR on mobile&lt;/td&gt;
&lt;td&gt;Whisper-class models (1-3 GB, batch architecture) impractical for real-time mobile streaming&lt;/td&gt;
&lt;td&gt;FastConformer-class models (~60 MB weights) run real-time on commodity mobile hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser voice inference&lt;/td&gt;
&lt;td&gt;Pre-SIMD128 WASM, 2-4x realtime typical, Safari lacked SIMD128 until March 2023&lt;/td&gt;
&lt;td&gt;SIMD128 across all major browsers, sub-1% RTF achievable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wake-word on cheap hardware&lt;/td&gt;
&lt;td&gt;Reproducible open benchmarks rare on Pi Zero class, mostly proprietary paid tiers&lt;/td&gt;
&lt;td&gt;Open benchmarks published, 5.3% RTF measured on $15 Pi Zero 2 W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice-cloning TTS on-device&lt;/td&gt;
&lt;td&gt;On-device voice-cloning was research-stage, production voice-cloning was cloud-only&lt;/td&gt;
&lt;td&gt;Shipping products exist, single-step flow sampling made fast synthesis realistic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three trends we see on the two-to-three year horizon:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile silicon for ML becomes standard.&lt;/strong&gt; Apple Neural Engine in iPhones, Google Tensor TPU, Qualcomm Hexagon NPU in Snapdragon. Every flagship now has dedicated silicon for ML inference. What was a feat in 2022 (running a transformer on a phone) is a standard capability in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser as a first-class target.&lt;/strong&gt; WASM SIMD128 (available in Chrome 91+, Firefox 89+, and Safari 16.4+) opened the door to serious ML inference in the browser. Our 100 KB wake-word runs at 0.16% RTF in Chrome M4. A few years ago that was not possible without a native wrapper. Fully-in-browser voice UX is now a real option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy becomes a compliance requirement.&lt;/strong&gt; The EU AI Act for GPAI models started applying with enforcement teeth. Regulators in the US and Asia are following. For voice-app companies, proving to a regulator that user voice is never sent to the cloud is easier when the architecture makes that impossible by construction. On-device moves from nice-to-have to a compliance posture.&lt;/p&gt;

&lt;p&gt;Our prediction is that cloud ASR stays the default for broad applications over the next 2-3 years, but on-device picks up market share in privacy-sensitive, offline-first, and latency-critical segments. This is not "on-device will kill cloud." It is a reasonable coexistence of two architectural approaches for different use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repos and resources
&lt;/h2&gt;

&lt;p&gt;Our runtime and SDKs are on &lt;a href="https://github.com/VoxRT" rel="noopener noreferrer"&gt;github.com/VoxRT&lt;/a&gt;. Platform-specific bindings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux (Pi, Jetson, servers): &lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux" rel="noopener noreferrer"&gt;voxrt-wake-word-linux&lt;/a&gt;, &lt;a href="https://github.com/VoxRT/voxrt-kws-linux" rel="noopener noreferrer"&gt;voxrt-kws-linux&lt;/a&gt;, &lt;a href="https://github.com/VoxRT/voxrt-asr-linux" rel="noopener noreferrer"&gt;voxrt-asr-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Browser (WASM): &lt;a href="https://github.com/VoxRT/voxrt-wake-word-browser" rel="noopener noreferrer"&gt;voxrt-wake-word-browser&lt;/a&gt;, &lt;a href="https://github.com/VoxRT/voxrt-kws-browser" rel="noopener noreferrer"&gt;voxrt-kws-browser&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mobile: iOS and Android bindings in the respective repos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More technical articles from our team on on-device voice are on &lt;a href="https://dev.to/voxrtio"&gt;dev.to/voxrtio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you looked at on-device voice recently and chose cloud instead, what was the deciding factor? The honest answers are the ones worth hearing.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>architecture</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Build a voice-controlled music player on Raspberry Pi 4 (fully offline)</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:15:52 +0000</pubDate>
      <link>https://dev.to/voxrtio/build-a-voice-controlled-music-player-on-raspberry-pi-4-fully-offline-2mgp</link>
      <guid>https://dev.to/voxrtio/build-a-voice-controlled-music-player-on-raspberry-pi-4-fully-offline-2mgp</guid>
      <description>&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A media player that listens for voice. The user says "Hey Assistant", then one of six commands (&lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;previous&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;), and the track plays, pauses, or the volume moves. All on a Raspberry Pi 4. No cloud, no API keys, no external services.&lt;/p&gt;

&lt;p&gt;Three components make it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wake-word (VoxRT)&lt;/strong&gt; listens to the microphone 24/7. About 2% of one CPU core on Pi 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword spotter (VoxRT)&lt;/strong&gt; recognizes a command in a 2-second window after the wake-word triggers. About 15% CPU during the burst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MPD (Music Player Daemon)&lt;/strong&gt; plays the actual music. Commands go through &lt;code&gt;mpc&lt;/code&gt;, the standard CLI client. Our script runs the right subprocess for each recognized command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing sets up in about 30 minutes on a Pi 4 with a USB microphone and a pair of speakers. Six of our built-in KWS commands cover music control cleanly, so no custom model training is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WW plus KWS fits music control
&lt;/h2&gt;

&lt;p&gt;The VoxRT KWS model recognizes 14 fixed English commands out of the box:&lt;br&gt;
&lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;back&lt;/code&gt;, &lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;previous&lt;/code&gt;, &lt;code&gt;cancel&lt;/code&gt;, &lt;code&gt;voxrt&lt;/code&gt;, &lt;code&gt;hey_vox&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Six of those map to music control:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Voice&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;play&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;start playback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;next track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;previous&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;previous track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;volume +5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;volume -5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No custom model training, no Whisper-style open-vocabulary ASR, no cloud service. All you need is a mapping from six voice labels to six shell commands.&lt;/p&gt;

&lt;p&gt;The triage pattern is the same as our &lt;a href="https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m"&gt;full Pi 4 voice assistant tutorial&lt;/a&gt;, only simpler. The ASR fallback is not needed here because KWS covers every command in this scope.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hardware
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi 4 (2GB or more)&lt;/strong&gt; running 64-bit Raspberry Pi OS (aarch64)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USB microphone&lt;/strong&gt; (any class-compliant device, around $10 to $15) or an I2S HAT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speakers or headphones&lt;/strong&gt; via the 3.5mm jack or USB audio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some music files&lt;/strong&gt; in &lt;code&gt;.mp3&lt;/code&gt; / &lt;code&gt;.flac&lt;/code&gt; / &lt;code&gt;.ogg&lt;/code&gt;. Your own collection or a few downloaded tracks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.9+&lt;/strong&gt; on the Pi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tutorial assumes you are comfortable with basic Linux commands (&lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, editing a config file). No previous MPD experience is required. We cover everything you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Three packages via apt (MPD, mpc, and PortAudio for the mic):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;mpd mpc libportaudio2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three Python libraries. On current Raspberry Pi OS (Bookworm and later) bare &lt;code&gt;pip install&lt;/code&gt; refuses to touch the system Python because of PEP 668. Use a venv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv ~/voxrt-env
&lt;span class="nb"&gt;source&lt;/span&gt; ~/voxrt-env/bin/activate
pip &lt;span class="nb"&gt;install &lt;/span&gt;voxrt-wake-word voxrt-kws sounddevice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything from here on runs inside that venv. If you prefer &lt;code&gt;pipx&lt;/code&gt;, or want to install globally with &lt;code&gt;--break-system-packages&lt;/code&gt;, either works. The rest of this tutorial uses the venv path.&lt;/p&gt;

&lt;p&gt;VoxRT model files. Download them to the directory you plan to run the script from (or use absolute paths in &lt;code&gt;WakeWordEngine.from_path()&lt;/code&gt; and &lt;code&gt;KwsEngine.from_path()&lt;/code&gt; further down).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/VoxRT/voxrt-wake-word-models/releases/download/v0.1.0/voxrt_wake_word.vxrt
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/VoxRT/voxrt-kws-models/releases/download/v0.1.0/voxrt_kws.vxrt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the exact versions in each repo README. They update periodically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure MPD
&lt;/h2&gt;

&lt;p&gt;The default MPD config on Raspberry Pi OS almost works. You need to point it at your music library and (optionally) adjust the audio output.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;/etc/mpd.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/mpd.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check or edit these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;music_directory&lt;/span&gt;     &lt;span class="s2"&gt;"/home/YOUR_USER/Music"&lt;/span&gt;
&lt;span class="n"&gt;playlist_directory&lt;/span&gt;  &lt;span class="s2"&gt;"/var/lib/mpd/playlists"&lt;/span&gt;
&lt;span class="n"&gt;db_file&lt;/span&gt;             &lt;span class="s2"&gt;"/var/lib/mpd/tag_cache"&lt;/span&gt;

&lt;span class="n"&gt;audio_output&lt;/span&gt; {
    &lt;span class="n"&gt;type&lt;/span&gt;            &lt;span class="s2"&gt;"alsa"&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;            &lt;span class="s2"&gt;"Default ALSA"&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;          &lt;span class="s2"&gt;"hw:0,0"&lt;/span&gt;   &lt;span class="c"&gt;# verify via `aplay -l`
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_USER&lt;/code&gt; with your actual Pi username. Recent Raspberry Pi OS (Bookworm and later) does not ship a default &lt;code&gt;pi&lt;/code&gt; user, so hardcoding &lt;code&gt;/home/pi/Music&lt;/code&gt; will not work unless you specifically created that account. Alternative is &lt;code&gt;/var/lib/mpd/music&lt;/code&gt;, which the packaged &lt;code&gt;mpd&lt;/code&gt; user already owns.&lt;/p&gt;

&lt;p&gt;MPD runs as its own &lt;code&gt;mpd&lt;/code&gt; system user. If you point it at a folder inside your home directory, &lt;code&gt;mpd&lt;/code&gt; needs read access. Common first-run trap is an empty &lt;code&gt;mpc listall&lt;/code&gt; because MPD cannot see the files. Fix with &lt;code&gt;sudo usermod -a -G $(whoami) mpd&lt;/code&gt; (add mpd to your group) and make sure the Music folder is group-readable.&lt;/p&gt;

&lt;p&gt;Drop your music into that directory. Restart MPD and reindex the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart mpd
mpc update
mpc listall | &lt;span class="nb"&gt;head&lt;/span&gt;    &lt;span class="c"&gt;# should print track paths&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify playback works manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mpc add /
mpc play
mpc pause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If music plays, you are ready. If there is no sound, run &lt;code&gt;aplay -l&lt;/code&gt; to find your audio device and update the &lt;code&gt;device "hw:X,Y"&lt;/code&gt; line in &lt;code&gt;mpd.conf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pi-side pipeline
&lt;/h2&gt;

&lt;p&gt;Around 70 lines of Python. Wake-word listens all the time, KWS activates after the trigger, matched commands run through &lt;code&gt;mpc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sounddevice&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_kws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;

&lt;span class="c1"&gt;# --- Config
&lt;/span&gt;&lt;span class="n"&gt;KWS_WINDOW_SEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
&lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;
&lt;span class="n"&gt;CHUNK_FRAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;   &lt;span class="c1"&gt;# 32 ms
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Command mapping
&lt;/span&gt;&lt;span class="n"&gt;COMMANDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;play&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;play&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Engines
&lt;/span&gt;&lt;span class="n"&gt;ww&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="n"&gt;kws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_kws.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;consecutive_frames_required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cooldown_frames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- State
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;IDLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;KWS_CHECK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
&lt;span class="n"&gt;kws_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="c1"&gt;# --- Audio capture
&lt;/span&gt;&lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queue[np.ndarray]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;samplerate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;blocksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CHUNK_FRAMES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# --- Main loop
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WW] wake score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KWS_CHECK&lt;/span&gt;
                &lt;span class="n"&gt;kws_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KWS_WINDOW_SEC&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KWS_CHECK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COMMANDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[KWS] command=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMMANDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
                    &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kws_deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[KWS] no matching command, back to idle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 voice_music.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say "Hey Assistant", then one of the six commands. The player should react.&lt;/p&gt;

&lt;p&gt;A few details worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;COMMANDS&lt;/code&gt; dict is the single place where you change the mapping. Want to add &lt;code&gt;stop&lt;/code&gt;? Add &lt;code&gt;"cancel": ["mpc", "stop"]&lt;/code&gt; and it will work immediately (the KWS vocabulary already contains &lt;code&gt;cancel&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Commands from the KWS vocabulary that are not in the dict (&lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;back&lt;/code&gt;, &lt;code&gt;voxrt&lt;/code&gt;, &lt;code&gt;hey_vox&lt;/code&gt;) are silently ignored during the 2-second window. If one gets recognized, the script does nothing and returns to idle after the timeout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subprocess.run(..., check=False)&lt;/code&gt; does not raise on a non-zero exit code. If MPD is not running or the library is empty, the script keeps working instead of crashing.&lt;/li&gt;
&lt;li&gt;Wake-word &lt;code&gt;cooldown_frames = 100&lt;/code&gt; means about 3 seconds of silence after a detection before the next trigger. Useful to prevent multiple triggers if the phrase "Hey Assistant" lands twice in the buffer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance on Pi 4
&lt;/h2&gt;

&lt;p&gt;Numbers from the official VoxRT READMEs, extrapolated to Pi 4 from a measured Pi Zero 2 W baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WW always-on.&lt;/strong&gt; RTF 0.018 to 0.024, which works out to about &lt;strong&gt;2% of one A72 core&lt;/strong&gt; on Pi 4 B / 400 at 1.5 to 1.8 GHz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS in the 2-second burst.&lt;/strong&gt; RTF &lt;strong&gt;13% to 16%&lt;/strong&gt;, roughly one-sixth of one core for a short window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mpc&lt;/code&gt; command exec.&lt;/strong&gt; Sub-millisecond. Negligible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Voice-to-music latency.&lt;/strong&gt; You say "Hey Assistant, play", the wake-word fires about 100 to 200 ms after the phrase ends, KWS recognizes the command in the following 500 to 1500 ms window, &lt;code&gt;mpc play&lt;/code&gt; runs in under 50 ms, MPD starts playback. Typical end-to-end is 700 to 1700 ms. It feels instant in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAM.&lt;/strong&gt; Roughly 50 to 70 MB for both VoxRT engines combined, plus another 20 to 40 MB for MPD. Small fraction of a 2GB Pi 4. Your exact numbers depend on your Python version, MPD version, and how much of your music library MPD has indexed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power draw&lt;/strong&gt; on a Pi 4 B running this stack is around 3.5W total (bare Pi 4 idle is closer to 2.7W, add ~0.1 to 0.2W for VoxRT plus the USB mic overhead). Over a month of 24/7 operation that is around 2.5 to 2.6 kWh. Under a dollar in most regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending
&lt;/h2&gt;

&lt;p&gt;A few directions past the first working version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More commands.&lt;/strong&gt; MPD has plenty of useful operations via &lt;code&gt;mpc&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mpc stop&lt;/code&gt; stops playback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mpc shuffle&lt;/code&gt; shuffles the queue&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mpc repeat on/off&lt;/code&gt; toggles loop mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mpc clear &amp;amp;&amp;amp; mpc add /&lt;/code&gt; clears and reloads the full library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extend the &lt;code&gt;COMMANDS&lt;/code&gt; dict with more mappings from the KWS vocabulary. For example, &lt;code&gt;"cancel": ["mpc", "stop"]&lt;/code&gt;, &lt;code&gt;"off": ["mpc", "clear"]&lt;/code&gt;, &lt;code&gt;"on": ["mpc", "shuffle"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playlists.&lt;/strong&gt; MPD supports named playlists as &lt;code&gt;.m3u&lt;/code&gt; files in &lt;code&gt;/var/lib/mpd/playlists/&lt;/code&gt;. Load one with &lt;code&gt;mpc load workout&lt;/code&gt; (loads &lt;code&gt;workout.m3u&lt;/code&gt;). Adding voice commands for specific playlists is possible, but the 14-word vocabulary does not include custom names. A workaround is to bind one voice command to "cycle through the next playlist" and rotate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-room.&lt;/strong&gt; If you have several Pis around the house, each with its own MPD and voice script, you get independent voice zones by default. Synchronised multi-room playback is a separate topic. See MPD &lt;a href="https://mpd.readthedocs.io/en/stable/user.html#satellite-setup" rel="noopener noreferrer"&gt;satellite mode&lt;/a&gt; or Snapcast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternative players.&lt;/strong&gt; Not a fan of MPD? The same mapping pattern works with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mpv:&lt;/strong&gt; &lt;code&gt;mpv --input-ipc-server=/tmp/mpv.sock&lt;/code&gt; plus &lt;code&gt;echo cycle pause | socat - /tmp/mpv.sock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLC:&lt;/strong&gt; &lt;code&gt;cvlc --extraintf rc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spotifyd&lt;/strong&gt; (Spotify Connect): &lt;code&gt;dbus-send&lt;/code&gt; commands to the MediaPlayer2 interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You only change the contents of &lt;code&gt;COMMANDS&lt;/code&gt;. The rest of the code stays the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wake-word threshold tuning.&lt;/strong&gt; The default &lt;code&gt;threshold = 0.9&lt;/code&gt; is conservative. If music is playing loud and the wake-word gets missed, drop it to 0.7 or 0.8. If a TV or a conversation triggers false wakes, raise it to 0.95.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;You now have a fully local voice-driven MPD music player. No audio, no recognition, no metadata leaves the Pi. It runs without an internet connection, which makes it viable for a cabin, a garage, or anywhere WiFi is unreliable.&lt;/p&gt;

&lt;p&gt;Three things this article does not cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTS feedback&lt;/strong&gt; ("playing next track"). The VoxRT TTS SDK is in development. That will get its own article when it ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom wake-word phrase.&lt;/strong&gt; Only "Hey Assistant" ships as a ready-to-use model today. Training a custom trigger is not yet a public feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice search&lt;/strong&gt; ("play something jazz"). That needs open-vocabulary ASR. We have a separate SDK for it, and the pattern is covered in our &lt;a href="https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m"&gt;full Pi 4 voice assistant tutorial&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repos:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WW Linux SDK: &lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;KWS Linux SDK: &lt;a href="https://github.com/VoxRT/voxrt-kws-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-kws-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;All SDKs: &lt;a href="https://github.com/VoxRT" rel="noopener noreferrer"&gt;github.com/VoxRT&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More on-device voice tutorials from our team on &lt;a href="https://dev.to/voxrtio"&gt;dev.to/voxrtio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Curious to hear what you would build with a setup like this. Which player do you use on your Pi (MPD, mpv, Volumio, something else)? And which voice commands beyond play, pause, next would you add first?&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>tutorial</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Wake word + voice commands in the browser: a full offline pipeline</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:20:28 +0000</pubDate>
      <link>https://dev.to/voxrtio/wake-word-voice-commands-in-the-browser-a-full-offline-pipeline-40eo</link>
      <guid>https://dev.to/voxrtio/wake-word-voice-commands-in-the-browser-a-full-offline-pipeline-40eo</guid>
      <description>&lt;p&gt;Voice interfaces in the browser have always had the same friction: Web Speech API is patchy across browsers, ships your audio to a vendor, and often needs network to respond. If you want a private, offline pipeline that works the same everywhere, you have had to wire it yourself. That got easier when WebAssembly SIMD128 shipped in every modern browser.&lt;/p&gt;

&lt;p&gt;A few weeks ago I wrote a piece on adding a wake word to a web app in five minutes. This is the natural next step. A wake word alone tells you when the user wants attention. To do anything useful, you also need to know what they said. This post wires up the second half: a 14-command keyword spotter that activates on wake and dispatches commands to your app. Everything runs on-device, in about 1.75 MB of assets total, at roughly 1% of one CPU core on a modern laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A modern browser: Chrome/Edge 91+, Firefox 89+, Safari 16.4+. WASM SIMD128 is required.&lt;/li&gt;
&lt;li&gt;A microphone.&lt;/li&gt;
&lt;li&gt;Any static file server on HTTPS or localhost. Browsers block &lt;code&gt;getUserMedia&lt;/code&gt; on plain HTTP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern: cheap wake, activated commands
&lt;/h2&gt;

&lt;p&gt;You could run keyword spotting continuously and skip the wake word. Don't. Two reasons.&lt;/p&gt;

&lt;p&gt;First, wake-word models are trained for one job: sit in a hot loop for hours, fire on a single phrase, ignore everything else. They are tiny (100 KB of weights, ~275 KB total bundle) and hardened against false triggers from radio, TV, and background conversation. Command spotters are heavier (~1.47 MB with a 14-word vocabulary) and, running always-on, will occasionally fire when someone in the room says "play" or "next" without meaning to talk to your app.&lt;/p&gt;

&lt;p&gt;Second, the split matches how users actually interact. A wake word says "I am about to talk to you." A command says what to do. Gating the command recognizer on the wake cuts false triggers and CPU when the user is not addressing your app.&lt;/p&gt;

&lt;p&gt;The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Microphone]
     ↓
[Wake word engine] ── always on, ~275 KB, ~0.16% RTF
     ↓ (fires on "Hey Assistant")
[Listening window ~3 seconds]
     ↓
[KWS engine] ── activated during window, ~1.47 MB, ~0.97% RTF
     ↓
[Command: play / pause / next / ...]
     ↓
[Your app]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both engines read the same 16 kHz mono PCM stream from the microphone. The wake engine sees every frame. The KWS engine only sees frames inside a listening window opened by the wake trigger. When a command fires (or the window times out), listening closes and the pipeline goes quiet again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: install the SDKs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @voxrt/wake-word-browser @voxrt/kws-browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via CDN if you skip the bundler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;initWake&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt-wake-word-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;initKws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KwsEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/kws-browser@0.1.0/voxrt-kws-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both packages ship two things: a WebAssembly runtime (&lt;code&gt;.js&lt;/code&gt; + &lt;code&gt;.wasm&lt;/code&gt;) and a &lt;code&gt;.vxrt&lt;/code&gt; model file. The model files download separately at runtime and get memory-mapped by the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: HTML shell
&lt;/h2&gt;

&lt;p&gt;Minimum needed to show what is happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;Voice control demo&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start listening&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Idle&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Last command: &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"last"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve over HTTPS or from localhost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: initialize both engines
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;initWake&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt-wake-word-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;initKws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KwsEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/kws-browser@0.1.0/voxrt-kws-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;initWake&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;initKws&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wakeBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kwsBytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/kws-browser@0.1.0/voxrt_kws.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wakeBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kwsBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two engines, two model files, both loaded before we touch the microphone. Downloads happen in parallel. On a warm cache the whole init is under 100 ms.&lt;/p&gt;

&lt;p&gt;Default thresholds are 0.9 in sigmoid space for both. Lower them to increase sensitivity (more false accepts). Raise them to be pickier. We will tune later if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: capture microphone audio
&lt;/h2&gt;

&lt;p&gt;Both engines want 16 kHz mono. We ask &lt;code&gt;AudioContext&lt;/code&gt; for it and verify, because Firefox and Safari sometimes ignore the hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Hz. Need 16000. Resample or reject.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScriptProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// detection loop goes here (Step 5)&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening for wake word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas from the wake-word post still apply here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ScriptProcessorNode&lt;/code&gt; is deprecated. Fine for a demo, swap for &lt;code&gt;AudioWorkletNode&lt;/code&gt; in production.&lt;/li&gt;
&lt;li&gt;The muted &lt;code&gt;GainNode&lt;/code&gt; between processor and destination is a Web Audio quirk. Without connecting the processor to the graph, &lt;code&gt;audioprocess&lt;/code&gt; never fires. Muting the gain prevents the microphone from being routed straight to the speakers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: detection loop with a listening window
&lt;/h2&gt;

&lt;p&gt;Here is the actual logic. The wake engine sees every frame. On a wake hit, we open a 3-second window during which frames also go to the KWS engine. First command detection closes the window early.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LISTEN_WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int16Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;listeningUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// performance.now() timestamp; 0 means closed&lt;/span&gt;

&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audioprocess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Float32 [-1, 1] to Int16&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x7fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Wake word always runs&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Wake at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestampSec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s, score &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;listeningUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;LISTEN_WINDOW_MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening for command...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// KWS only runs while the listening window is open&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;listeningUntil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;kws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="err"&gt;=== "&lt;/span&gt;&lt;span class="nc"&gt;hey_vox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; || command.class === &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;voxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;) {
        // Also in KWS vocab. Treat as a second wake: re-arm the window, do not dispatch.
        listeningUntil = performance.now() + LISTEN_WINDOW_MS;
        continue;
      }
      console.log(`Command &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; score ${command.score.toFixed(3)}`);
      document.getElementById(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;).textContent = command.class;
      dispatch(command.class);
      listeningUntil = 0; // close window after first command
      document.getElementById(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;).textContent = &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;Listening&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;wake&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;;
    }
  } else if (listeningUntil !== 0) {
    // window expired without a command
    listeningUntil = 0;
    document.getElementById(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;).textContent = &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;Listening&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;wake&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;;
  }
});

function dispatch(command) {
  switch (command) {
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;play&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:  /* start media playback */  break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;pause&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;: /* pause media playback */  break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:  /* skip to next item */     break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;: /* previous item */      break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:    /* volume up */             break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:  /* volume down */           break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:    /* enable something */      break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:   /* disable something */     break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:   /* confirm */               break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:    /* dismiss */               break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;: /* cancel current action */ break;
    case &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nx"&gt;back&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:  /* go back / navigate up */ break;
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note on the vocabulary overlap: &lt;code&gt;voxrt&lt;/code&gt; and &lt;code&gt;hey_vox&lt;/code&gt; are both in KWS's 14-word set. Once the listening window is open, the user can say them and KWS will fire. The filter at the top of the loop treats those two as a "second wake" (re-arm the window) rather than a command. Without that filter, saying "hey vox" mid-listening would silently close the window on a no-op dispatch. Either behaviour is defensible, but the reader should see the choice.&lt;/p&gt;

&lt;p&gt;That is the whole runtime. Say "Hey Assistant", then within 3 seconds say one of the 14 supported commands, and &lt;code&gt;dispatch&lt;/code&gt; fires. Open DevTools (F12, Console) to see the timing and scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;Measured on a MacBook Pro M4 running Chrome, single-threaded WASM with SIMD128:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wake word:&lt;/strong&gt; RTF 0.16% (625× real time), always on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS:&lt;/strong&gt; RTF 0.97% (103× real time), only during listening windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combined footprint:&lt;/strong&gt; ~1% of one CPU core when actively listening, closer to 0.2% between windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model quality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wake word ("Hey Assistant"):&lt;/strong&gt; ROC AUC 0.9966, precision 0.993 and recall 0.982 at threshold 0.9 on the reference test split (11,656 utterances)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS (14 commands):&lt;/strong&gt; F1 macro 0.9671 at threshold 0.9 on a speaker-disjoint test set. Per-class ROC AUC is ≥0.99 for every command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Assets shipped to the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wake-word runtime + model: ~275 KB&lt;/li&gt;
&lt;li&gt;KWS runtime + model: ~1.47 MB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total pipeline:&lt;/strong&gt; ~1.75 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is smaller than the average site's hero image.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 14-word vocabulary
&lt;/h2&gt;

&lt;p&gt;Fixed in v0.1.0: &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;cancel&lt;/code&gt;, &lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;previous&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, &lt;code&gt;back&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;voxrt&lt;/code&gt;, &lt;code&gt;hey_vox&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It covers three classes of interaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirmation and cancellation:&lt;/strong&gt; yes, no, cancel, back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media control:&lt;/strong&gt; play, pause, next, previous&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State toggles and navigation:&lt;/strong&gt; up, down, on, off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those 12 cover most "wake, then dispatch" flows: a recipe app in the kitchen, an accessibility layer for hands-free navigation, a dashboard that toggles panels by voice, a car UI. The remaining two (&lt;code&gt;voxrt&lt;/code&gt;, &lt;code&gt;hey_vox&lt;/code&gt;) exist as alternate wake-style phrases if you want a second entry point.&lt;/p&gt;

&lt;p&gt;If you need product-specific words (custom brand names, unique verbs, domain jargon), the vocabulary is currently fixed at these 14. Custom command sets are a paid tier: &lt;code&gt;help@voxrt.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Two-stage voice control fits a specific shape of use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hands are busy or dirty.&lt;/strong&gt; Kitchen recipe apps, workshop tool controllers, medical stations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility.&lt;/strong&gt; Users with motor limitations get an entry-level voice control layer that does not depend on Apple or Google account state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-car dashboards.&lt;/strong&gt; Voice shortcuts on top of a touch UI, without shipping cabin audio to a cloud provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiosks and public terminals.&lt;/strong&gt; No microphone stream leaves the machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need continuous transcription (arbitrary sentences, not fixed commands), you want streaming ASR instead. That is not in browser yet: our streaming ASR runtime is Android, iOS, and Linux only. In-browser ASR is a real question we get and the honest answer today is "not shipped."&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Full pipeline, on-device, in ~1.75 MB. Two SDKs, one shared &lt;code&gt;.vxrt&lt;/code&gt; runtime, no cloud, no per-request cost. Wake word optimized for always-on, command spotter activated on demand.&lt;/p&gt;

&lt;p&gt;Repos and packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wake word: &lt;a href="https://github.com/VoxRT/voxrt-wake-word-browser" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-browser&lt;/a&gt;. npm: &lt;code&gt;@voxrt/wake-word-browser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;KWS: &lt;a href="https://github.com/VoxRT/voxrt-kws-browser" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-kws-browser&lt;/a&gt;. npm: &lt;code&gt;@voxrt/kws-browser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Model files: served from unpkg alongside the JS, or from GitHub releases&lt;/li&gt;
&lt;li&gt;VoxRT overview: &lt;a href="https://voxrt.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=browser-voice-commands-pipeline" rel="noopener noreferrer"&gt;voxrt.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with this, the vocabulary limits (14 words, English) will be the first thing that pinches. That is next on our roadmap.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>tutorial</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a fully offline voice assistant on Raspberry Pi 4</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:16:09 +0000</pubDate>
      <link>https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m</link>
      <guid>https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m</guid>
      <description>&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A voice assistant that runs entirely on a Raspberry Pi 4. No cloud, no API keys, no per-minute pricing. The user says "Hey Assistant", the device wakes up, and then handles whatever was said next.&lt;/p&gt;

&lt;p&gt;Two interaction paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user says one of 14 fixed commands (&lt;code&gt;turn on lights&lt;/code&gt;, &lt;code&gt;play music&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt;, and so on). The device recognises it instantly and runs the action.&lt;/li&gt;
&lt;li&gt;The user says something outside that set (&lt;code&gt;read me the news&lt;/code&gt;, &lt;code&gt;what time does the store open&lt;/code&gt;). The device transcribes the speech into text and processes the open-ended query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three models work together. A &lt;strong&gt;wake-word detector&lt;/strong&gt; listens to the microphone 24/7. It is cheap and always on. A &lt;strong&gt;keyword spotter&lt;/strong&gt; takes over for the next couple of seconds and tries to match a fixed vocabulary. And a &lt;strong&gt;streaming ASR&lt;/strong&gt; transcribes open-ended speech if the keyword spotter finds no match. It is heavier but only runs when actually needed.&lt;/p&gt;

&lt;p&gt;All three install with three &lt;code&gt;pip install&lt;/code&gt; calls and run on a $35 Raspberry Pi 4 without an internet connection. This article walks through the full Python code, shows equivalent snippets in Node.js, Go and C, and explains why splitting this into three SDKs on one runtime keeps the always-on cost low without giving up open-ended speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the triage pattern (WW then KWS then ASR)
&lt;/h2&gt;

&lt;p&gt;The core idea is to avoid running the heaviest model until it is actually needed.&lt;/p&gt;

&lt;p&gt;The wake-word detector is the lightest piece of the pipeline. Our model is about 100 KB and uses about 5.3% of one A53 core on a $15 Raspberry Pi Zero 2 W (24/7 sustained, per our earlier benchmarks, link below). On a Pi 4 the cost drops further because the Cortex-A72 cores are faster and run in 64-bit mode. That means the wake-word can listen to the environment continuously without stealing meaningful CPU from anything else on the device.&lt;/p&gt;

&lt;p&gt;But the wake-word only knows one thing. Did the trigger phrase (&lt;code&gt;"Hey Assistant"&lt;/code&gt; in our case) get said. To understand what the user actually wants, you need another model.&lt;/p&gt;

&lt;p&gt;The obvious option is to fire up ASR as soon as the wake-word triggers. That works but it is overkill for most real requests. In a voice assistant, users usually say one of a small set of commands. Turn on lights. Set a timer. Play music. Running full open-vocabulary ASR on that is like using a regex when &lt;code&gt;strcmp&lt;/code&gt; would do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KWS (keyword spotter)&lt;/strong&gt; solves this cheaply. Our model processes 16 kHz mono audio in 32 ms frames and classifies each into one of 14 fixed command classes. It does not transcribe arbitrary speech, but it does quickly answer "yes, this is command X" or "no match".&lt;/p&gt;

&lt;p&gt;The triage logic looks like this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;WW listening state.&lt;/strong&gt; Wake-word runs continuously, waiting for &lt;code&gt;"Hey Assistant"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WW triggered.&lt;/strong&gt; Switch to KWS. Give the user about 2 seconds to say a command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS matched.&lt;/strong&gt; Run the command action (turn on lights, stop, and so on). Go back to state 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS did not match.&lt;/strong&gt; Fall back to ASR. Transcribe the open-ended speech until silence or a timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASR finished.&lt;/strong&gt; Process the text (route to a local LLM, a parser, or send it to your backend). Go back to state 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In compute terms this is a &lt;code&gt;switch&lt;/code&gt; with branches for known cases and a &lt;code&gt;default&lt;/code&gt; for everything else. WW costs almost nothing all the time. KWS runs only after a wake-word (typically 1 to 2 seconds). ASR runs only when the command missed the KWS vocabulary. In practice, most assistant requests fall inside a small command vocabulary, though the exact fraction depends heavily on your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware
&lt;/h2&gt;

&lt;p&gt;Minimum setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi 4 (4GB).&lt;/strong&gt; Four gigabytes of RAM give comfortable headroom for the ASR model, which keeps its weights in memory. The 2GB variant will boot the whole pipeline but leaves little room for anything else running on the device. Pi 5 and Pi 3B+ also work, but we tested on Pi 4 since that is the most common Pi model developers reach for right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A USB microphone&lt;/strong&gt; ($10 to $15, any class-compliant device) or an I2S mic HAT such as the ReSpeaker 2-Mics Pi HAT (about $10) for cleaner audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 32GB microSD card&lt;/strong&gt; with 64-bit Raspberry Pi OS (aarch64). The 64-bit build is required. Our Linux SDKs ship aarch64 wheels only, and the 32-bit armhf variant is not supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional:&lt;/strong&gt; a speaker or headphones if you want to add a TTS response. TTS is in development. This tutorial does not cover it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wake-word alone (without the rest of the pipeline) works even on a Pi Zero 2 W. We tested that in an earlier article, &lt;a href="https://dev.to/voxrtio/wake-word-on-a-15-raspberry-pi-zero-2-w-53-rtf-always-on-4f5m"&gt;wake-word on a $15 Pi Zero 2 W&lt;/a&gt;. Adding KWS and ASR bumps the compute budget, so this tutorial uses a Pi 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the three SDKs
&lt;/h2&gt;

&lt;p&gt;Each model ships as a separate package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;voxrt-wake-word voxrt-kws voxrt-asr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three libraries share the same VoxRT runtime under the hood. That runtime is a Rust inference engine with a C ABI. We do not ship a single &lt;code&gt;voxrt-all&lt;/code&gt; package, because many integrators only need part of the pipeline (wake-word only for a kiosk device, ASR only for a dictation app) and carrying weights they never load makes no sense.&lt;/p&gt;

&lt;p&gt;Model weights download separately as files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wake-word "Hey Assistant"&lt;/span&gt;
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/VoxRT/voxrt-wake-word-models/releases/download/v0.1.0/voxrt_wake_word.vxrt

&lt;span class="c"&gt;# Keyword spotter (14 commands)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/VoxRT/voxrt-kws-models/releases/download/v0.1.0/voxrt_kws.vxrt

&lt;span class="c"&gt;# Streaming ASR (medium PC model)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/VoxRT/voxrt-asr-models/releases/download/v0.1.2/streaming_medium_pc.vxrt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Check the exact versions in each repo README. They update periodically.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Node.js, Go and C install options are covered in the "Same pipeline in other languages" section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: the triage pipeline in Python
&lt;/h2&gt;

&lt;p&gt;The full example breaks into three parts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Microphone capture.&lt;/strong&gt; Shared across all three SDKs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State machine.&lt;/strong&gt; Controls which engine gets audio at any given moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiring.&lt;/strong&gt; The event loop that puts it all together.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Audio capture
&lt;/h3&gt;

&lt;p&gt;We use &lt;code&gt;sounddevice&lt;/code&gt; (a PortAudio wrapper), which is the standard way to read a microphone in Python on Linux. It provides a callback-based API. The PortAudio audio thread calls our function with each new chunk of samples. We push them into a queue and the main thread drains it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sounddevice&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;

&lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;   &lt;span class="c1"&gt;# all three models expect 16 kHz mono
&lt;/span&gt;&lt;span class="n"&gt;CHUNK_FRAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;    &lt;span class="c1"&gt;# 32 ms, matches WW and KWS internal chunk size
&lt;/span&gt;
&lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queue[np.ndarray]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# indata is (frames, 1) int16. Copy the buffer since sounddevice
&lt;/span&gt;    &lt;span class="c1"&gt;# reuses the same backing storage between callbacks.
&lt;/span&gt;    &lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;samplerate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;blocksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CHUNK_FRAMES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this call, 32 ms blocks of int16 samples land in &lt;code&gt;audio_queue&lt;/code&gt; steadily. The next question is which engine handles them at any given moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  State machine
&lt;/h3&gt;

&lt;p&gt;We keep one state enum and a main loop that pulls chunks off the queue and routes them into the right engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;IDLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="c1"&gt;# only WW is listening
&lt;/span&gt;    &lt;span class="n"&gt;KWS_CHECK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;       &lt;span class="c1"&gt;# WW fired, KWS is checking for 2 seconds
&lt;/span&gt;    &lt;span class="n"&gt;ASR_TRANSCRIBE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;# KWS missed, ASR is transcribing
&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initialise the three engines
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_kws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_asr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsrStreamingEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DecodeMode&lt;/span&gt;

&lt;span class="n"&gt;ww&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# ignore 100 frames (~3.2 s) after a detection
&lt;/span&gt;
&lt;span class="n"&gt;kws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KwsEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_kws.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;consecutive_frames_required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cooldown_frames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KWS commands: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_names&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AsrStreamingEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streaming_medium_pc.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DecodeMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RNNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The main event loop
&lt;/h3&gt;

&lt;p&gt;This is where the triage happens. Broken down by state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;

&lt;span class="n"&gt;KWS_WINDOW_SEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;    &lt;span class="c1"&gt;# window after WW for a KWS attempt
&lt;/span&gt;&lt;span class="n"&gt;ASR_SILENCE_SEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;   &lt;span class="c1"&gt;# silence at the end of an ASR utterance
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running fixed command: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# wire this into GPIO, home automation API, whatever you need
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_open_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open-ended query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# send this to a local LLM, a parser, wherever
&lt;/span&gt;

&lt;span class="n"&gt;kws_check_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;asr_last_speech_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunk_i16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;samples_i16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk_i16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# WW listens all the time.
&lt;/span&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples_i16&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WW] wake detected, score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KWS_CHECK&lt;/span&gt;
                &lt;span class="n"&gt;kws_check_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KWS_WINDOW_SEC&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KWS_CHECK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Try to match a fixed command in the next 2 seconds.
&lt;/span&gt;            &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples_i16&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[KWS] command: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;handle_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
                &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kws_check_deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[KWS] no match, falling back to ASR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASR_TRANSCRIBE&lt;/span&gt;
                &lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
                &lt;span class="n"&gt;asr_last_speech_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASR_TRANSCRIBE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# ASR expects f32 samples. Convert from int16.
&lt;/span&gt;            &lt;span class="n"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;32768.0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples_i16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# ASR wants 200 ms chunks (3200 samples).
&lt;/span&gt;            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;chunk_200ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asr_buffer_f32&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3200&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
                &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_200ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[ASR partial] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;asr_last_speech_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;# If silence lasts more than 1.5 seconds, finalise.
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;asr_last_speech_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ASR_SILENCE_SEC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[ASR final] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;handle_open_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;# asr.reset() keeps the loaded weights and resets the streaming
&lt;/span&gt;                &lt;span class="c1"&gt;# state. Cheaper than re-loading ~60 MB via from_path().
&lt;/span&gt;                &lt;span class="c1"&gt;# If your voxrt-asr version does not expose reset(), fall back
&lt;/span&gt;                &lt;span class="c1"&gt;# to AsrStreamingEngine.from_path(...) here, but expect a
&lt;/span&gt;                &lt;span class="c1"&gt;# multi-second stall while weights re-load.
&lt;/span&gt;                &lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One key detail. WW and KWS want &lt;strong&gt;int16&lt;/strong&gt; samples. ASR wants &lt;strong&gt;float32&lt;/strong&gt; normalised to &lt;code&gt;[-1.0, 1.0]&lt;/code&gt;. The conversion is one line (&lt;code&gt;s / 32768.0&lt;/code&gt;) but it is easy to forget. Chunk sizes also differ. 32 ms for WW and KWS. 200 ms for ASR (matching the ASR model's internal attention window).&lt;/p&gt;

&lt;p&gt;This is not "one API for everything". It is "consistent integration pattern (from_path then push_something then read the result), with input format matched to each model's requirements". Wake-word and KWS have the same shape internally (both are event detectors on short chunks), so they share a signature. ASR works differently (it is a streaming decoder), so it has a different signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same pipeline in other languages
&lt;/h2&gt;

&lt;p&gt;Full quickstart examples for Node.js, Go and C are in the repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;github.com/VoxRT/voxrt-wake-word-linux/tree/main/examples/{nodejs,go,c}/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github.com/VoxRT/voxrt-kws-linux/tree/main/examples/{nodejs,go,c}/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github.com/VoxRT/voxrt-asr-linux/tree/main/examples/{nodejs,go,c}/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below are the critical snippets. How &lt;code&gt;push_pcm_i16&lt;/code&gt; and &lt;code&gt;push_audio&lt;/code&gt; look in each language. Follow the repo links for the full runnable examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@voxrt/wake-word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KwsEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@voxrt/kws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsrStreamingEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DecodeMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@voxrt/asr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ww&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// mic capture via any ALSA/PortAudio Node binding available on Pi.&lt;/span&gt;
&lt;span class="nx"&gt;mic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Buffer is a slice of a larger ArrayBuffer. Use byteOffset and&lt;/span&gt;
  &lt;span class="c1"&gt;// byteLength to build a view over just the samples we received.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int16Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;det&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ww&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[WW] score=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// switch state to KWS_CHECK here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;

    &lt;span class="n"&gt;wakeword&lt;/span&gt; &lt;span class="s"&gt;"github.com/VoxRT/voxrt-wake-word-linux/go"&lt;/span&gt;
    &lt;span class="c"&gt;// kws     "github.com/VoxRT/voxrt-kws-linux/go"&lt;/span&gt;
    &lt;span class="c"&gt;// asr     "github.com/VoxRT/voxrt-asr-linux/go"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;wakeword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"voxrt_wake_word.vxrt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetThreshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetCooldownFrames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// mic capture via any ALSA/PortAudio Go binding available on Pi.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;micChan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// chunk is []int16 of length 512&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[WW] score=%.3f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// switch state here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stddef.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;voxrt_wake_word.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// load_vxrt_model() mmaps the model file and returns (bytes, len).&lt;/span&gt;
&lt;span class="c1"&gt;// See examples/c/alsa-mic-quickstart/main.c in the repo for the exact&lt;/span&gt;
&lt;span class="c1"&gt;// implementation, about 20 lines around open() + fstat() + mmap().&lt;/span&gt;
&lt;span class="c1"&gt;// alsa_read_chunk() is a thin wrapper over snd_pcm_readi().&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;model_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;load_vxrt_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"voxrt_wake_word.vxrt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to mmap the wake-word model file&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;voxrt_wake_word_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;voxrt_status_t&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;VOXRT_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"voxrt_wake_word_create failed: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;voxrt_wake_word_set_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;voxrt_wake_word_set_cooldown_frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ALSA capture, 16 kHz mono int16, 512 samples per chunk.&lt;/span&gt;
&lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;voxrt_wake_word_detection_t&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alsa_read_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word_push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// VOXRT_ERR_BUFFER_TOO_SMALL just means more detections would fit;&lt;/span&gt;
    &lt;span class="c1"&gt;// the ones we got are still valid.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;VOXRT_OK&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;VOXRT_ERR_BUFFER_TOO_SMALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"push failed: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[WW] score=%.3f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// switch state here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;voxrt_wake_word_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// remember to munmap(model_bytes, model_len)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each SDK's quickstart file already does this work. It opens the mic, feeds the engine, and prints detections. Our triage loop is the state machine on top of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected performance on Pi 4
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Important disclaimer.&lt;/strong&gt; The numbers below extrapolate from benchmarks on other hardware. We have not run the full triage loop on our Pi 4 yet. Your numbers should land in this range but you will only know exactly after you measure on your own board.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IDLE state (WW only).&lt;/strong&gt; About 1 to 2% of one CPU core. Extrapolated from 5.3% on Pi Zero 2 W (A53 at 1 GHz running the default 32-bit Raspberry Pi OS). Pi 4 uses a 64-bit A72 at 1.5 GHz with better IPC and more efficient NEON when the OS runs in aarch64 mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS_CHECK state (2 seconds after a wake).&lt;/strong&gt; About 15 to 25% CPU during the burst. Extrapolated from 16% RTF on Snapdragon 662, but Pi 4's A72 is a generation older than the Kryo 260 Gold cores in SD662, so expect somewhat higher RTF. KWS only runs in short windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASR_TRANSCRIBE state.&lt;/strong&gt; Roughly 40 to 70% CPU during active transcription. ASR is the heaviest of the three, and NEON-specialised int8 kernels tuned for mobile Snapdragons will not extract the same efficiency on A72. Real-time streaming (RTF under 1.0) should still be achievable on Pi 4 aarch64, but the headroom is the tightest of the three. Measure on your device before assuming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RAM footprint&lt;/strong&gt; with all three engines initialised: around 200 to 300 MB. (model weights plus Python runtime, audio buffers, and engine overhead). ASR is the biggest single component at roughly 60 MB of weights. That is why we recommend Pi 4 with 4 GB. The 2 GB variant also works but leaves little room for the rest of your software.&lt;/p&gt;

&lt;p&gt;If we run real Pi 4 numbers after publish, we will do a follow-up article with actual measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this article does not cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTS (speaking back).&lt;/strong&gt; In development. A separate on-device voice-cloning model. Will get its own article when it ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom wake-word phrase.&lt;/strong&gt; We currently only ship &lt;code&gt;"Hey Assistant"&lt;/code&gt; as a pre-trained model. Training your own trigger phrase requires a separate training pipeline that we have not released publicly yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speaker verification / voice biometrics.&lt;/strong&gt; Not included. The models do not tell you who is speaking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-language support.&lt;/strong&gt; Wake-word and KWS are English-only at the moment. The ASR model is multilingual in general, but we have only tested it on English.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything above is on the VoxRT roadmap. If any of it is critical for your use case, mention it in the comments so we can prioritise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repos and resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wake-word Linux SDK:&lt;/strong&gt; &lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KWS Linux SDK:&lt;/strong&gt; &lt;a href="https://github.com/VoxRT/voxrt-kws-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-kws-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASR Linux SDK:&lt;/strong&gt; &lt;a href="https://github.com/VoxRT/voxrt-asr-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-asr-linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All SDKs:&lt;/strong&gt; &lt;a href="https://github.com/VoxRT" rel="noopener noreferrer"&gt;github.com/VoxRT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our earlier on-device voice articles:&lt;/strong&gt; &lt;a href="https://dev.to/voxrtio"&gt;dev.to/voxrtio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building anything voice-driven and want the audio to stay on the device, all three SDKs are on GitHub with the documentation in the README. Full quickstart examples in Python, Node.js, Go and C are in every repo.&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>python</category>
      <category>tutorial</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How we prepared voice cloning TTS to run on-device</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:05:00 +0000</pubDate>
      <link>https://dev.to/voxrtio/how-we-prepared-voice-cloning-tts-to-run-on-device-184</link>
      <guid>https://dev.to/voxrtio/how-we-prepared-voice-cloning-tts-to-run-on-device-184</guid>
      <description>&lt;p&gt;We trained a text-to-speech model with voice cloning that fits into 100 MB (fp16) and runs on-device for synthesis, from smartphones to a Raspberry Pi. The model is in a file, generation happens on the device. No API, no cloud backend at inference time.&lt;/p&gt;

&lt;p&gt;Early tests on a budget Android phone hit faster-than-realtime on a single core. Full benchmarks with numbers and different devices are coming in a separate post. Here is how we got there.&lt;/p&gt;

&lt;p&gt;The model is about 45M parameters (a language model plus an audio decoder). For comparison, typical server-side TTS systems are hundreds of MB to several GB of weights. We trained both stages from scratch and packaged them into a single binary.&lt;/p&gt;

&lt;p&gt;The road here was not smooth. This post is a short architecture overview (without it, it is hard to explain what broke) and two concrete stories of what broke and how we fixed it. If you end up training something similar, this should save you a few surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture in two paragraphs
&lt;/h2&gt;

&lt;p&gt;We use the classic two-stage scheme. The first stage is a language model (LM) that, given text and a short voice sample, generates audio latents autoregressively, roughly the way an LLM generates text, except the outputs are continuous audio representations instead of words. The second stage is the decoder of our own neural codec (24 kHz, compact continuous latent space), which turns those latents back into a waveform.&lt;/p&gt;

&lt;p&gt;The key design decision is a single-step flow sampler instead of iterative diffusion. Classical diffusion TTS often needs many forward passes per frame to sound right. That is fine on a server. On mobile or a Raspberry Pi, that does not work. One step per frame gives you speed on weak hardware. But you pay for it in training complexity, described below.&lt;/p&gt;

&lt;p&gt;Two more things for context. The architecture is causal, so it streams by construction. You do not have to wait for the end of the input, and audio comes out with sub-second time-to-first-audio. Voice cloning works from a short sample of around 5 seconds. The cloning part runs off-device, the synthesis is on-device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke #1: single-step sampler collapses speech to averaged sound
&lt;/h2&gt;

&lt;p&gt;Diffusion models work well partly because stochasticity accumulates over many steps. Each step adds a bit of noise and the model learns to remove it. By the end of the process you get a quality sample.&lt;/p&gt;

&lt;p&gt;When you do this in a single step, that stochasticity disappears. A naively trained one-step flow sampler tends to converge to an "averaged" sound. The model finds an output that minimizes loss on average across the training set, rather than a diverse, characteristic sound.&lt;/p&gt;

&lt;p&gt;We saw this on early iterations as a lifeless "muddled" voice, identical in intonation regardless of input. Test recordings sounded like a robotic monotone from the nineties.&lt;/p&gt;

&lt;p&gt;What helped: a special sampler training scheme. I will skip the architectural details (that part of the recipe stays internal), but the general idea is to force the sampler to account for the target variance of the distribution, not just its mean. Verification was through a separate synthetic test set specifically for this failure mode, where voice diversity is expected. The model passed, it started to sound like real speech.&lt;/p&gt;

&lt;p&gt;The takeaway: single-step sampling is not free. You save on inference and pay in training complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke #2: mixed precision, one poisoned step costs 5000 iterations
&lt;/h2&gt;

&lt;p&gt;We trained in mixed precision, some operations in fp16 for speed, some in fp32 for stability. Standard practice for large neural nets on GPUs.&lt;/p&gt;

&lt;p&gt;The problem is that fp16 has a narrow range of representable numbers. Sometimes gradients overflow and you get a NaN. The usual protection (loss scaling and gradient clipping) works most of the time. But sometimes a rare combination of input data plus current weight state causes one operation to produce a NaN. That NaN gradient then poisons every parameter it touches at the next optimizer step.&lt;/p&gt;

&lt;p&gt;One such "poisoned" step can invalidate 5000 previous training iterations. The model starts emitting noise, loss shoots to infinity, and everything since the last checkpoint is gone.&lt;/p&gt;

&lt;p&gt;The fix was multi-layer protection. Per-batch NaN check, per-epoch sanity check, running-loss anomaly detection, then automatic rollback to the last good checkpoint plus a human alert. Not free compute-wise, but cheaper than losing a week of training. Mixed precision is a real engineering discipline, not a flag you enable and forget, especially on long runs where accumulated risk grows linearly with time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we optimized for (and what we did not)
&lt;/h2&gt;

&lt;p&gt;The design constraints were tight.&lt;/p&gt;

&lt;p&gt;Streaming by construction, with sub-second time-to-first-audio on weak hardware. No "wait a moment while the model thinks".&lt;/p&gt;

&lt;p&gt;Numbers, dates, and currency without special-case hacks. The model reads "March 21st, 2026" and "$19.99" correctly. To get there, we added synthetic speech to the training mix with quality control specifically on those edge cases.&lt;/p&gt;

&lt;p&gt;Around 45M parameters, because 100 MB is what you can bundle into an app without a noticeable impact on bundle size. Any bigger and it gets uncomfortable for mobile apps.&lt;/p&gt;

&lt;p&gt;What we did not prioritize: language diversity (English only for now, the training corpus was around 3000 hours for the codec and 1700 hours for the LM), and extreme quality benchmarks for professional use. We aim for natural-sounding output on default use cases, not to beat top commercial TTS in blind studio tests.&lt;/p&gt;

&lt;p&gt;Quality is judged via blind listening tests on a fixed text set: numbers, dates, tongue twisters, long narratives. Not perfect, but reproducible, and raters agree with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we go next
&lt;/h2&gt;

&lt;p&gt;The model is going through final polishing (we are fine-tuning the decoder for the production generation mode) and packaging into the VoxRT runtime. The delivery format will be the same as our other SDKs: a single binary model file plus native SDKs for iOS, Android, and Raspberry Pi.&lt;/p&gt;

&lt;p&gt;Release in September 2026. Code, model, and release notes will land on GitHub. Star the repo if you want a ping when it drops.&lt;/p&gt;




&lt;p&gt;More about our other on-device SDKs (VAD, wake-word, KWS, streaming ASR) at &lt;a href="https://voxrt.com?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=tts-on-device" rel="noopener noreferrer"&gt;voxrt.com&lt;/a&gt;. Models and SDKs on &lt;a href="https://github.com/VoxRT" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>showdev</category>
      <category>python</category>
    </item>
    <item>
      <title>Add a wake-word to your web app in 5 minutes</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:00:03 +0000</pubDate>
      <link>https://dev.to/voxrtio/add-a-wake-word-to-your-web-app-in-5-minutes-3781</link>
      <guid>https://dev.to/voxrtio/add-a-wake-word-to-your-web-app-in-5-minutes-3781</guid>
      <description>&lt;p&gt;Voice interfaces are getting easier to add to the web, but wake-words have been the hard part. Cloud APIs mean latency, per-request billing, and audio leaving the user's device. On-device wake-words in the browser were awkward until WebAssembly SIMD128 shipped in every major browser.&lt;/p&gt;

&lt;p&gt;You will have a working "Hey Assistant" detector in about five minutes. Everything runs on-device, no cloud, no per-request cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A modern browser (Chrome/Edge 91+, Firefox 89+, or Safari 16.4+). WASM SIMD128 is a hard requirement&lt;/li&gt;
&lt;li&gt;A microphone&lt;/li&gt;
&lt;li&gt;Any static file server on HTTPS or localhost. Browsers block &lt;code&gt;getUserMedia&lt;/code&gt; on plain HTTP&lt;/li&gt;
&lt;li&gt;Node.js if you want npm install. Otherwise CDN works fine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full download is ~275 KB (170 KB WASM runtime + ~100 KB model). That is smaller than most icon fonts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: install the SDK
&lt;/h2&gt;

&lt;p&gt;Two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;via npm:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @voxrt/wake-word-browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;via CDN (no build step):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt-wake-word-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use CDN to skip the bundler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: HTML shell
&lt;/h2&gt;

&lt;p&gt;Nothing fancy. A single button and a status line will do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;Wake-word demo&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start listening&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Idle&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve this over HTTPS or localhost. Microphone access does not work on plain &lt;code&gt;http://&lt;/code&gt; except for localhost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: initialize the engine and load the model
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt-wake-word-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole "load the model" step. The &lt;code&gt;.vxrt&lt;/code&gt; file is the pre-trained "Hey Assistant" model at ~100 KB. &lt;code&gt;init()&lt;/code&gt; bootstraps the WebAssembly runtime.&lt;/p&gt;

&lt;p&gt;The threshold is in sigmoid space [0, 1]. Default is 0.9. Lower it and you get more sensitive detection (more false accepts). Raise it and you get fewer false accepts but might miss quieter or accented pronunciations. We will tune this later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: capture microphone audio
&lt;/h2&gt;

&lt;p&gt;Wake-word models need 16 kHz mono audio. Modern browsers give you higher sample rates by default, so we ask &lt;code&gt;AudioContext&lt;/code&gt; for 16 kHz explicitly. Not all browsers honor the request, so we also check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Hz instead of 16000. Detection quality will drop. Resample or check the SDK docs.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScriptProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// detection loop goes here (Step 5)&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on the code above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ScriptProcessorNode&lt;/code&gt; is deprecated in favor of &lt;code&gt;AudioWorkletNode&lt;/code&gt; for production. For a 5-minute demo, &lt;code&gt;ScriptProcessor&lt;/code&gt; is simpler. Swap it out when you productionize&lt;/li&gt;
&lt;li&gt;The muted &lt;code&gt;GainNode&lt;/code&gt; between processor and destination is a Web Audio quirk. Without connecting the processor to something, &lt;code&gt;audioprocess&lt;/code&gt; never fires. Setting gain to 0 avoids sending your microphone straight to the speakers (which would cause feedback)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: push audio frames and detect
&lt;/h2&gt;

&lt;p&gt;The engine wants Int16 PCM samples in fixed-size chunks. Here is the detection loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int16Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audioprocess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// convert Float32 [-1, 1] to Int16&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x7fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// push to engine, iterate detections&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detection&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Wake detected at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestampSec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s, score &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wake-word detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. Open DevTools (F12, then Console), click the button, say "Hey Assistant", and you should see a log line. The &lt;code&gt;detection.score&lt;/code&gt; is the sigmoid probability at the moment of detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What just happened
&lt;/h2&gt;

&lt;p&gt;You loaded a ~100 KB model into WebAssembly, streamed 16 kHz mono audio through it in 512-sample frames (32 ms each), and got detections when the score crossed threshold.&lt;/p&gt;

&lt;p&gt;The engine handles its own cooldown so you do not get spam detections on the same utterance. Default cooldown is 100 frames (about 1 second at 10 ms hop). You can tune with &lt;code&gt;engine.cooldownFrames&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two quick customizations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prefer Float32 over Int16?&lt;/strong&gt; Skip the conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audioprocess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detection&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmF32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Detected: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tuning threshold:&lt;/strong&gt; Watch &lt;code&gt;engine.currentScore()&lt;/code&gt; to see live confidence. If your users often speak quietly, lower the threshold to 0.85. If you get false triggers from radio or TV, raise it to 0.93.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Live score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentScore&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the default threshold of 0.9, the model has precision 0.993 and recall 0.982 on the reference test split (11,656 utterances, ROC AUC 0.9966).&lt;/p&gt;

&lt;h2&gt;
  
  
  Full code (copy-paste starter)
&lt;/h2&gt;

&lt;p&gt;Here is everything above, glued together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt-wake-word-browser.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unpkg.com/@voxrt/wake-word-browser@0.1.1/voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Hz instead of 16000. Detection quality will drop.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScriptProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int16Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audioprocess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputFloat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
      &lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x7fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detection&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmBuffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Wake at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestampSec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s score &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wake-word detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve this alongside the HTML from Step 2 and you have a working wake-word demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom phrase or another language:&lt;/strong&gt; the free tier ships with "Hey Assistant" only. Custom phrases and additional languages are available on a paid tier. We train the model for you and swap the &lt;code&gt;.vxrt&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native mobile:&lt;/strong&gt; same SDK exists for iOS (Swift) and Android (Kotlin). Wake-word runs at 1.5% RTF on iPhone A15, 2.1% on Snapdragon 662&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux and edge:&lt;/strong&gt; available for Linux aarch64 with bindings for Python, Node.js, Go, and C. Holds at 5.3% RTF sustained on a $15 Raspberry Pi Zero 2 W&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom on-device pipeline:&lt;/strong&gt; wake-word into ASR into your app logic works well for privacy-critical voice UIs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One note on licensing
&lt;/h2&gt;

&lt;p&gt;The wrapper (Rust crate, wasm-bindgen bindings, examples) is Apache-2.0 so you can freely integrate it. The compiled WebAssembly runtime and the model weights are proprietary. Redistribution is allowed only as an unmodified part of the SDK package. In practical terms: install and use it in your product, no additional legal setup beyond reading the license.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://voxrt.com/wake-word-demo?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=wakeword-browser-tutorial" rel="noopener noreferrer"&gt;voxrt.com/wake-word-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/VoxRT/voxrt-wake-word-browser" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-browser&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Wake-word on a $15 Raspberry Pi Zero 2 W: 5.3% RTF always-on</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:15:02 +0000</pubDate>
      <link>https://dev.to/voxrtio/wake-word-on-a-15-raspberry-pi-zero-2-w-53-rtf-always-on-4f5m</link>
      <guid>https://dev.to/voxrtio/wake-word-on-a-15-raspberry-pi-zero-2-w-53-rtf-always-on-4f5m</guid>
      <description>&lt;h2&gt;
  
  
  The problem: always-on wake-word is hard and expensive
&lt;/h2&gt;

&lt;p&gt;A wake-word detector is the part of the voice stack that listens to the mic 24/7 and triggers the rest of the pipeline when it hears a specific phrase like "Alexa", "Hey Siri", or "Hey Google". Because it is always-on, wake-word dominates the energy and compute budget of any voice-first device.&lt;/p&gt;

&lt;p&gt;Ready-made options for hobbyists are limited, and all have trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alexa/Google Nest/Apple HomePod&lt;/strong&gt; work well but are locked to their vendor platforms, require cloud connectivity in some scenarios, and use closed hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picovoice / Porcupine&lt;/strong&gt; is an on-device SDK that works offline after activation. The model is proprietary, and the free tier was deprecated as of 30 June 2026 (now a 7-day trial followed by paid).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;openWakeWord&lt;/strong&gt; is open source (Apache-2.0) with community models, but training a custom wake phrase is non-trivial. You need to gather data, train the model, and iterate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Python + TF Lite from scratch&lt;/strong&gt; requires ML expertise and hours of work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem: hobbyists want a simple recipe. Grab a $15 Pi, install a package with one line, hear a custom wake word, do not pay per activation.&lt;/p&gt;

&lt;p&gt;This post covers our solution (proprietary, but with a free tier "Hey Assistant" by default). The headline number: &lt;strong&gt;5.3% CPU&lt;/strong&gt; on one A53 core of a $15 Raspberry Pi Zero 2 W, 24/7 sustained.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a wake-word detector is: quick technical background
&lt;/h2&gt;

&lt;p&gt;Three components usually connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VAD (Voice Activity Detection)&lt;/strong&gt; is the lightest. It answers "is there any speech in this audio?" (regardless of what kind). Runs at ~1% CPU. Used as a pre-filter for heavier stages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wake-word detector&lt;/strong&gt; answers "is there a &lt;em&gt;specific&lt;/em&gt; phrase?". Heavier than VAD (needs actual pattern matching) but still light (typically 10-100 KB model). Runs 1-10% CPU in always-on mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASR (Automatic Speech Recognition)&lt;/strong&gt; does full transcription: "what exactly was said?". Heavy (~60 MB model, 30-40% CPU on mid-range Android). Only fires after wake-word success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wake-word sits in the sweet spot. Not as light as VAD (VAD does not distinguish "Hey Assistant" from "hello world"), not as heavy as ASR (no full vocabulary needed). Cache-friendly architecture, sub-100 ms decisions, runs on any ARM SoC from 2020+ without a DSP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Pi Zero 2 W as target
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Raspberry Pi Zero 2 W&lt;/strong&gt; is the most popular "minimal" Pi (~$15). Specs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ARM Cortex-A53 quad-core @ 1 GHz&lt;/strong&gt;: same class as midrange smartphones from 2016-2018&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;512 MB RAM&lt;/strong&gt;: enough for small ML models, not for heavy Whisper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No GPU&lt;/strong&gt;: CPU-only inference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~$15 retail&lt;/strong&gt;: cheap enough for DIY smart speakers, doorbell notifications, custom voice UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logic: if it works on Pi Zero 2 W, it works everywhere. More powerful Pi 4/5 and Jetson Nano are 4-10x faster, embedded ARM SoCs (Rockchip, Allwinner) are comparable class. So Pi Zero 2 W sets the lower bound: if the numbers hold there, they hold everywhere else.&lt;/p&gt;

&lt;p&gt;Wake-word across the full Pi ladder:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;RTF (Real-Time Factor)&lt;/th&gt;
&lt;th&gt;CPU budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pi Zero 2 W (Cortex-A53)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.053&lt;/strong&gt; (5.3%)&lt;/td&gt;
&lt;td&gt;one core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi 3 A+/B/B+ (Cortex-A53)&lt;/td&gt;
&lt;td&gt;~0.038-0.044 (3.8-4.4%)&lt;/td&gt;
&lt;td&gt;one core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi 4 B / 400 (Cortex-A72)&lt;/td&gt;
&lt;td&gt;~0.018-0.024 (1.8-2.4%)&lt;/td&gt;
&lt;td&gt;one core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi 5 (Cortex-A76)&lt;/td&gt;
&lt;td&gt;~0.008-0.012 (0.8-1.2%)&lt;/td&gt;
&lt;td&gt;one core&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pi Zero 2 W number is measured (60-second sustained live-mic push). Pi 3/4/5 numbers are estimates scaled by clock speed and architecture generation, not independently benchmarked.&lt;/p&gt;

&lt;p&gt;RTF 0.053 means the processor spends 53 milliseconds per 1 second of audio. Wake-word takes ~5% of one core on Pi Zero 2 W, leaving 95% plus the other 3 cores for the application (UI, LEDs, LLM inference, storage). On Pi 5, wake-word is essentially free (&amp;lt;1%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Model architecture
&lt;/h2&gt;

&lt;p&gt;Key characteristics of our wake-word model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~48 thousand parameters&lt;/strong&gt;: very small neural network by modern standards (for comparison, Whisper base.en has ~74 million parameters, roughly 1500x larger).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size: ~100 KB&lt;/strong&gt; (encrypted &lt;code&gt;.vxrt&lt;/code&gt; format).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling rate: 16 kHz mono, 32 ms frames&lt;/strong&gt;: standard for speech recognition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trained on 100% synthetic data with augmentation&lt;/strong&gt;: no human voice samples in the training set, which addresses licensing concerns for commercial deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier phrase: "Hey Assistant"&lt;/strong&gt;: works out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom phrases: paid tier&lt;/strong&gt;: training pipeline available on request (&lt;a href="mailto:help@voxrt.com"&gt;help@voxrt.com&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accuracy metrics (measured on a test set of 5,240 positive + 6,416 negative utterances):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ROC AUC: 0.9966&lt;/strong&gt;: very clean separation between positive and negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR AUC (Average Precision): 0.9899&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold 0.90:&lt;/strong&gt; precision 0.993 / recall 0.982.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice: at threshold 0.90 the model produces ~1 false positive per 100+ triggers and misses ~2% of real wake-words. Acceptable for an always-on hot loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark: 5.3% RTF sustained on Pi Zero 2 W
&lt;/h2&gt;

&lt;p&gt;RTF (Real-Time Factor) is CPU time divided by audio duration. RTF 5.3% means the processor spends 53 milliseconds of CPU time per 1 second of audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How we measured:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pi Zero 2 W, Cortex-A53 @ 1 GHz, aarch64 Linux&lt;/li&gt;
&lt;li&gt;60-second continuous loop with microphone input&lt;/li&gt;
&lt;li&gt;Used one core out of four&lt;/li&gt;
&lt;li&gt;Sustained RTF (not peak) because sustained better reflects real-world thermal throttling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why sustained matters more than peak:&lt;/strong&gt; Pi Zero 2 W without a heat sink can throttle CPU after 2-3 minutes of sustained load. Peak RTF (first 5 seconds) is typically 10-20% better than sustained. We publish the sustained number, 5.3%, because that is what a hobbyist actually gets in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with the remaining 95% CPU plus 3 cores:&lt;/strong&gt; UI (LEDs / display), Bluetooth / WiFi for sending triggers to home assistant, local logic (dimmer, thermostat), or on a mid-tier board like Pi 4, running a smaller LLM for command interpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why NEON matters: 8.7x speedup
&lt;/h2&gt;

&lt;p&gt;The wake-word runtime is written in Rust. But "pure Rust" is not enough for these RTF numbers. The key ingredient is &lt;strong&gt;ARM NEON SIMD intrinsics&lt;/strong&gt; in the inner loops (matrix multiply, activation functions).&lt;/p&gt;

&lt;p&gt;Measured on Snapdragon 662, Cortex-A73 big cluster pinned via HIGH_PERF affinity (SD662 is a big.LITTLE SoC with 4x A73 + 4x A53, and the perf cluster is used for stable clock):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rust scalar (no SIMD)&lt;/td&gt;
&lt;td&gt;0.182 (18.2%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust + NEON SIMD&lt;/td&gt;
&lt;td&gt;0.021 (2.1%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Speedup: 8.7x.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What this means for the reader: even a small neural network (48K params) on CPU-only inference depends critically on SIMD acceleration. Writing your own solution in pure Python + NumPy gives you an RTF that is 10-20x higher than an optimized SIMD implementation.&lt;/p&gt;

&lt;p&gt;On Pi Zero 2 W (A53, one generation older than A73), the NEON speedup ratio is similar, but A53 has fewer cycles per second per core. That is why we see 5.3% RTF on A53 vs 2.1% on A73 with the same code.&lt;/p&gt;

&lt;p&gt;Practical takeaway: if you plan to write a custom wake-word, targeting ARM NEON is mandatory. Otherwise it will be too slow for always-on mode on any embedded ARM.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run: setup + code (4 languages)
&lt;/h2&gt;

&lt;p&gt;The SDK is available for Python, Node.js, Go, and C with an identical API pattern: load a &lt;code&gt;.vxrt&lt;/code&gt; file, set &lt;code&gt;threshold&lt;/code&gt; and &lt;code&gt;cooldown_frames&lt;/code&gt;, push PCM int16 chunks (16 kHz mono), receive detection events with &lt;code&gt;frame_index&lt;/code&gt;, &lt;code&gt;timestamp_sec&lt;/code&gt;, and &lt;code&gt;score&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;voxrt_wake_word.vxrt&lt;/code&gt; model (~100 KB, free tier "Hey Assistant") for all examples below can be downloaded from &lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux/releases" rel="noopener noreferrer"&gt;the releases page&lt;/a&gt; or &lt;a href="https://huggingface.co/VoxRT/wake-word-hey-assistant-vxrt" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All examples below are simplified versions of the quickstarts in &lt;code&gt;examples/{python,nodejs,go,c}/&lt;/code&gt; in the repository. Full versions (with WAV parsing, ALSA live-mic, performance timing) live there too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;voxrt-wake-word
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;mic_iter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# int16 mono @ 16 kHz, 512-sample chunks
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wake! t=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp_sec&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For live mic, install &lt;code&gt;sounddevice&lt;/code&gt; (&lt;code&gt;pip install sounddevice&lt;/code&gt;) and feed &lt;code&gt;sd.InputStream(samplerate=16000, channels=1, dtype='int16', blocksize=512, callback=...)&lt;/code&gt; chunks into &lt;code&gt;engine.push_pcm_i16&lt;/code&gt; inside the callback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @voxrt/wake-word
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@voxrt/wake-word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WakeWordEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voxrt_wake_word.vxrt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cooldownFrames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;micIter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// Int16Array, 512 samples, 16 kHz mono&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`wake! t=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestampSec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s score=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For live mic, use the &lt;code&gt;mic&lt;/code&gt; or &lt;code&gt;node-record-lpcm16&lt;/code&gt; npm packages. Both return Node &lt;code&gt;Buffer&lt;/code&gt; objects, so convert to &lt;code&gt;Int16Array&lt;/code&gt; before pushing: &lt;code&gt;new Int16Array(buf.buffer, buf.byteOffset, buf.length / 2)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/VoxRT/voxrt-wake-word-linux/go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="n"&gt;wakeword&lt;/span&gt; &lt;span class="s"&gt;"github.com/VoxRT/voxrt-wake-word-linux/go"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;wakeword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"voxrt_wake_word.vxrt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetThreshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetCooldownFrames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// micIter returns &amp;lt;-chan []int16 (512-sample chunks, 16 kHz mono)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;micIter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PushPcmI16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wake! t=%.3fs score=%.4f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimestampSec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For live mic on Linux, use &lt;code&gt;github.com/gordonklaus/portaudio&lt;/code&gt; or pipe from &lt;code&gt;arecord&lt;/code&gt; (simpler for a quickstart). Have your mic reader emit &lt;code&gt;&amp;lt;-chan []int16&lt;/code&gt; for the &lt;code&gt;range&lt;/code&gt; loop above.&lt;/p&gt;

&lt;h3&gt;
  
  
  C
&lt;/h3&gt;

&lt;p&gt;Install headers and shared library from the tarball release, then build with pkg-config for flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc main.c &lt;span class="si"&gt;$(&lt;/span&gt;pkg-config &lt;span class="nt"&gt;--cflags&lt;/span&gt; &lt;span class="nt"&gt;--libs&lt;/span&gt; voxrt-wake-word&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-lasound&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; wake-word-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;voxrt_wake_word.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Load model bytes via mmap. See full example in repo for the ~30-line&lt;/span&gt;
    &lt;span class="c1"&gt;// helper (open + fstat + mmap). Placeholders here for brevity:&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// TODO: mmap voxrt_wake_word.vxrt&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;model_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// TODO: file size&lt;/span&gt;

    &lt;span class="n"&gt;voxrt_wake_word_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;voxrt_status_t&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;voxrt_wake_word_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;VOXRT_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"voxrt_wake_word_create failed: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;voxrt_wake_word_set_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;voxrt_wake_word_set_cooldown_frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;voxrt_wake_word_detection_t&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Read 512 int16 samples from ALSA / mic into buf.&lt;/span&gt;
        &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;voxrt_wake_word_push_pcm_i16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wake! t=%.3fs score=%.4f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;timestamp_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;voxrt_wake_word_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full C example with all ALSA setup (~150 lines) is at &lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux/tree/main/examples/c/alsa-mic-quickstart" rel="noopener noreferrer"&gt;examples/c/alsa-mic-quickstart/main.c&lt;/a&gt;. There is also a C++ variant (&lt;code&gt;alsa-mic-quickstart-cpp&lt;/code&gt;) and a CMake consumer example for integrating into existing projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters and what is not shown
&lt;/h3&gt;

&lt;p&gt;All four languages use identical parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;threshold&lt;/code&gt; (default 0.5): confidence threshold for firing. 0.9 is strict (fewer false positives). For always-on in a noisy environment, 0.85 to 0.90 is a reasonable baseline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cooldown_frames&lt;/code&gt; (default 50, ~1.6 sec): how many silence frames after a detection before the next trigger. Prevents double-fire.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not shown in the snippets (standard boilerplate): microphone init, error handling, action on detection (starting ASR, HTTP webhook to Home Assistant, turning on an LED).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wake-word alternatives: honest comparison
&lt;/h2&gt;

&lt;p&gt;Three main options for a hobbyist in 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Porcupine (Picovoice)&lt;/th&gt;
&lt;th&gt;openWakeWord&lt;/th&gt;
&lt;th&gt;VoxRT wake-word&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model size&lt;/td&gt;
&lt;td&gt;~200 KB - 1 MB (varies by tier)&lt;/td&gt;
&lt;td&gt;~50-400 KB per model + ~3 MB shared runtime&lt;/td&gt;
&lt;td&gt;~100 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTF on Pi Zero 2 W&lt;/td&gt;
&lt;td&gt;~3.8% on Pi 3 per Picovoice docs, no Pi Zero 2 W numbers published (likely 4-8% scaled by clock)&lt;/td&gt;
&lt;td&gt;reported CPU-heavy on Pi Zero 2 W, no public RTF, on Pi 4 &amp;lt;5 ms per 80 ms chunk&lt;/td&gt;
&lt;td&gt;5.3% (measured)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;proprietary C/C++ SDK&lt;/td&gt;
&lt;td&gt;Python + ONNX Runtime&lt;/td&gt;
&lt;td&gt;proprietary Rust SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;commercial (7-day trial then paid, no free tier since 30 June 2026)&lt;/td&gt;
&lt;td&gt;Apache-2.0 (fully open)&lt;/td&gt;
&lt;td&gt;proprietary (LICENSE-BINARY), free default phrase, paid custom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom wake phrase&lt;/td&gt;
&lt;td&gt;commercial console (paid)&lt;/td&gt;
&lt;td&gt;DIY training pipeline (Python, data collection, hours of work)&lt;/td&gt;
&lt;td&gt;paid tier (email us)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity (out-of-the-box "Hey X")&lt;/td&gt;
&lt;td&gt;medium (auth flow)&lt;/td&gt;
&lt;td&gt;high (need to train or download a community model)&lt;/td&gt;
&lt;td&gt;low (pip install + download)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Each tool has its fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Porcupine&lt;/strong&gt;: if you need a large catalog of pre-trained wake phrases and you are fine with commercial licensing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;openWakeWord&lt;/strong&gt;: if 100% open source is critical and you have time for DIY training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VoxRT wake-word&lt;/strong&gt;: if you need the smallest footprint and simplest deployment ("Hey Assistant" out of the box, custom phrase via paid tier).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No tool is universally "better" than the others. It depends on requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing model
&lt;/h2&gt;

&lt;p&gt;Important disclosure: &lt;strong&gt;VoxRT wake-word is a proprietary tool&lt;/strong&gt; (unlike our Silero VAD packaging, which is MIT).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime + model:&lt;/strong&gt; LICENSE-BINARY (Elephant Enterprises LLC). Distributable as part of an unmodified SDK, no forking, no rebuilding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier phrase:&lt;/strong&gt; "Hey Assistant" works out of the box without activation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom wake phrases:&lt;/strong&gt; paid tier. We train a custom model on your phrase (typical 2-3 weeks turnaround). Contact: &lt;code&gt;help@voxrt.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-activation billing:&lt;/strong&gt; unlike Porcupine, no ongoing fees. A one-time custom-phrase fee covers redistribution in your product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a trade-off: &lt;strong&gt;you get the smallest footprint and simplest deployment&lt;/strong&gt;, but you cannot modify the runtime or train your own model. For a 100% open-source requirement, use openWakeWord (Apache-2.0).&lt;/p&gt;

&lt;h2&gt;
  
  
  WASM bonus: 0.16% RTF in Chrome
&lt;/h2&gt;

&lt;p&gt;The same ~100 KB &lt;code&gt;.vxrt&lt;/code&gt; file runs via WASM SIMD128 in the browser:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome / MacBook Pro M4 (WASM SIMD128)&lt;/td&gt;
&lt;td&gt;0.16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari / iPhone A15 (WASM)&lt;/td&gt;
&lt;td&gt;0.23%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Essentially free. The full npm bundle &lt;code&gt;@voxrt/wake-word-browser&lt;/code&gt; is ~275 KB (model + WASM runtime).&lt;/p&gt;

&lt;p&gt;Not the primary story of this post, but worth mentioning because it is unusual. Voice AI in the browser without a backend, integrated into a web app via npm install. The same model runs on Pi, iPhone, Android, and in a user's Chrome tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Wake-word detection on a $15 Raspberry Pi Zero 2 W, always on, 5.3% CPU is not an abstract idea. One-line install with &lt;code&gt;pip install voxrt-wake-word&lt;/code&gt;, add a microphone reader loop, done.&lt;/p&gt;

&lt;p&gt;Three things worth remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTF sustained (60+ sec) matters more than peak.&lt;/strong&gt; Thermal throttling on a Pi Zero 2 W without a heat sink makes peak numbers misleading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM NEON SIMD is critical for CPU inference.&lt;/strong&gt; 8.7x speedup vs scalar Rust. Custom implementations without SIMD will be too slow for always-on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wake-word is not VAD, not ASR.&lt;/strong&gt; Three tiers with different CPU budgets. Wake-word is the middle tier: heavier than VAD, light enough for always-on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools from this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/VoxRT/voxrt-wake-word-linux" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-linux&lt;/a&gt;: Linux SDK (aarch64)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/VoxRT/voxrt-wake-word-android" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-android&lt;/a&gt;: Android SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/VoxRT/voxrt-wake-word-ios" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-ios&lt;/a&gt;: iOS SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/VoxRT/voxrt-wake-word-browser" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-wake-word-browser&lt;/a&gt;: WASM browser SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/VoxRT/wake-word-hey-assistant-vxrt" rel="noopener noreferrer"&gt;huggingface.co/VoxRT/wake-word-hey-assistant-vxrt&lt;/a&gt;: free tier model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previous posts in this voice AI series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/voxrtio/building-private-voice-notes-for-ios-and-android-without-the-cloud-54a8"&gt;Building Private Voice Notes for iOS and Android Without the Cloud&lt;/a&gt;: full tutorial for a VAD + ASR pipeline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/voxrtio/streaming-asr-vs-whisper-on-mobile-when-to-switch-5cm7"&gt;Streaming ASR vs Whisper on mobile: when to switch&lt;/a&gt;: architectural comparison for live voice apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learn more about the runtime and product family: &lt;a href="https://voxrt.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=wake-word-pi-zero" rel="noopener noreferrer"&gt;voxrt.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Custom wake phrase for your product: &lt;code&gt;help@voxrt.com&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>machinelearning</category>
      <category>iot</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Streaming ASR vs Whisper on mobile: when to switch</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:45:10 +0000</pubDate>
      <link>https://dev.to/voxrtio/streaming-asr-vs-whisper-on-mobile-when-to-switch-5cm7</link>
      <guid>https://dev.to/voxrtio/streaming-asr-vs-whisper-on-mobile-when-to-switch-5cm7</guid>
      <description>&lt;h2&gt;
  
  
  The problem: Whisper feels laggy for live apps
&lt;/h2&gt;

&lt;p&gt;Voice-first apps became mainstream in 2024-2026. Voice agents, real-time captions, live meeting transcription, voice notes with instant feedback. Developers building this new generation of apps typically look at Whisper first (whisper.cpp, faster-whisper). It is the de facto default for on-device speech-to-text.&lt;/p&gt;

&lt;p&gt;And for batch transcription (upload audio file, wait, get transcript), Whisper is genuinely great. Interview transcription, meeting notes, lecture recordings on MacBook or in an Android app. Whisper handles all these accurately and reliably.&lt;/p&gt;

&lt;p&gt;But as soon as you try Whisper for &lt;strong&gt;live&lt;/strong&gt; scenarios (user speaks, expects instant response), something feels off. A 300-500 ms delay minimum shows up, often up to 1-2 seconds. Words get "sliced" at chunk boundaries. Transcript comes out in jerks. For a voice agent that should respond like a human in conversation, this is a frustrating UX.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;not a Whisper bug&lt;/strong&gt;. It is a design mismatch: Whisper is a batch model retrofitted for streaming, while a real-time scenario fundamentally needs a different architecture.&lt;/p&gt;

&lt;p&gt;This post is about what native streaming ASR is, how it differs from Whisper's approach, what alternatives exist, and when you should switch (or stay).&lt;/p&gt;

&lt;h2&gt;
  
  
  What "streaming" really means in ASR
&lt;/h2&gt;

&lt;p&gt;Before comparing options, terminology first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch ASR&lt;/strong&gt; processes audio as a whole: the model receives the entire file (or a fixed-length chunk), runs inference in one pass, returns the full transcript. Classic architectures (Whisper, original Wav2Vec2, seq2seq encoder-decoder) are batch by nature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming ASR&lt;/strong&gt; processes audio continuously: the model takes a stream of samples, emits text incrementally as speech arrives. Key streaming parameters: &lt;strong&gt;lookahead&lt;/strong&gt; (how many milliseconds of future audio the model "sees" before emitting text) and &lt;strong&gt;chunk latency&lt;/strong&gt; (how many milliseconds the model buffers between emissions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification by lookahead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infinite lookahead&lt;/strong&gt;: batch (the whole file is known upfront). Whisper, original NeMo Conformer non-streaming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large lookahead (1000ms+)&lt;/strong&gt;: pseudo-streaming batch with large chunks. Whisper.cpp streaming mode, faster-whisper VAD-based.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small lookahead (80-320 ms)&lt;/strong&gt;: native streaming. NeMo FastConformer streaming, streaming Conformer variants, Parakeet streaming, some Kaldi TDNN variants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero lookahead&lt;/strong&gt;: causal streaming. Fastest response but usually strictly worse accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference between "pseudo-streaming" and "native streaming" is fundamental but often confused. Both technically "stream." But native streaming is trained with a constraint to see only limited future audio, while pseudo-streaming is a hack on top of a batch model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whisper's approach: chunked batch retrofit
&lt;/h2&gt;

&lt;p&gt;Whisper is trained on 30-second audio chunks. To get streaming behavior, developers use one of two workarounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whisper.cpp streaming mode:&lt;/strong&gt; rolling window of 500 ms to 3 s, each chunk is processed as a mini-batch, outputs are concatenated. It works, but has several side effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chunk boundary artifacts: words at chunk borders can be cut, repeated, or misrecognized.&lt;/li&gt;
&lt;li&gt;Latency equals chunk size plus inference time. On SD662 with Whisper base.en at 500 ms chunks, perceived latency is 700-1500 ms.&lt;/li&gt;
&lt;li&gt;VAD and endpointing need separate tuning (Whisper has no native VAD).&lt;/li&gt;
&lt;li&gt;Punctuation can be inconsistent between chunks (the model "loses context" at borders).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;VAD-based streaming wrappers&lt;/strong&gt; (e.g., Whisper-Streaming or WhisperLive built on faster-whisper): VAD triggers, the accumulated segment gets transcribed, output emits. Cleaner output (no chunk boundary artifacts), but latency is even higher (you have to wait for end of speech for VAD trigger).&lt;/p&gt;

&lt;p&gt;Neither of these approaches delivers what native streaming does: continuous text stream with sub-100 ms lookahead. This is an architectural limitation, not a bug.&lt;/p&gt;

&lt;p&gt;Whisper is not bad. Whisper is not for live. These are two different task categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native streaming ASR: how it works
&lt;/h2&gt;

&lt;p&gt;Native streaming architectures (NeMo FastConformer streaming, streaming Conformer, Parakeet streaming variants) work differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache-aware inference.&lt;/strong&gt; The model is trained knowing it will see only limited future audio (typically 80-320 ms lookahead). Internal hidden states cache between chunks. No need to re-process previous audio for each new segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous emission.&lt;/strong&gt; The model emits tokens incrementally as speech arrives. No "chunk boundaries" in perceived output. Text simply prints in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off exists, but minimal.&lt;/strong&gt; Native streaming models are usually slightly less accurate than their batch counterparts (the limited future audio constraint reduces context understanding). But the gap is small, typically 0.2-0.5% WER on LibriSpeech test-clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example architecture:&lt;/strong&gt; NeMo FastConformer streaming (&lt;code&gt;stt_en_fastconformer_hybrid_medium_streaming_80ms_pc&lt;/code&gt; by NVIDIA). Roughly 32 million parameters, hybrid decoder (RNN-T + CTC), 80 ms cache-aware lookahead, WER 3.267% on LibriSpeech test-clean with the RNN-T decoder (our WER-500 measurement, see footnote in the comparison table).&lt;/p&gt;

&lt;p&gt;Similar variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parakeet streaming&lt;/strong&gt;: same NeMo family, different size variants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming Conformer&lt;/strong&gt;: general architectural family, different implementations from Google, Microsoft, and NVIDIA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kaldi TDNN-F&lt;/strong&gt;: old-school, but streaming, still used in Vosk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the task is live, these models deliver a qualitatively different experience than chunked Whisper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our implementation: VoxRT, a Rust runtime around NeMo streaming
&lt;/h2&gt;

&lt;p&gt;We (&lt;a href="https://voxrt.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=streaming-asr-vs-whisper-mobile&amp;amp;utm_content=implementation" rel="noopener noreferrer"&gt;VoxRT&lt;/a&gt;) built a Rust runtime that packages NeMo FastConformer streaming (the same cache-aware architecture described above) into ready-made &lt;a href="https://github.com/VoxRT/voxrt-asr-ios" rel="noopener noreferrer"&gt;SPM (iOS)&lt;/a&gt; and &lt;a href="https://github.com/VoxRT/voxrt-asr-android" rel="noopener noreferrer"&gt;Gradle(Android)&lt;/a&gt; packages. The model is &lt;code&gt;stt_en_fastconformer_hybrid_medium_streaming_80ms_pc&lt;/code&gt; from NVIDIA, ported into our &lt;code&gt;.vxrt&lt;/code&gt; format without architectural changes (fp16 quantization). Our &lt;code&gt;.vxrt&lt;/code&gt; format supports AES-256-GCM encryption for proprietary models, but the NeMo streaming ASR model here remains under CC-BY-4.0, not encrypted.&lt;/p&gt;

&lt;p&gt;What this gives a developer compared to the "write your own wrapper" approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-line integration&lt;/strong&gt; in Gradle/SPM. No custom Python + ONNX bridge to write for JNI or Objective-C interop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symmetric API surface&lt;/strong&gt; between iOS and Android. Kotlin and Swift code is nearly identical, same lifecycle pattern (&lt;code&gt;init&lt;/code&gt; -&amp;gt; &lt;code&gt;processPcm&lt;/code&gt; -&amp;gt; &lt;code&gt;stop&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native inference via Rust runtime&lt;/strong&gt;. No PyTorch, no ONNX runtime dependency, minimal APK/IPA footprint (native binary ~424 KB stripped on Android, ~500 KB framework compressed on iOS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundled Silero VAD&lt;/strong&gt; for pre-filtering. No separate VAD integration to write, the 1.2 MB model works out of the box paired with ASR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honest trade-off: our runtime is proprietary (LICENSE-BINARY, Elephant Enterprises LLC), though the wrapper is Apache-2.0. The model is under CC-BY-4.0 (NVIDIA), which requires attribution in your app's credits. For commercial closed-source apps, no compliance issues. Pure GPL/AGPL projects will hit a license conflict with our proprietary runtime. In that case, writing your own wrapper around the open-source NeMo runtime is a better path.&lt;/p&gt;

&lt;p&gt;From here on, all "streaming ASR" numbers in the post come from our implementation of this architecture. Whisper and Vosk numbers come from their upstream benchmarks (source cited).&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;p&gt;Extended comparison versus the first post: added axes for streaming approach, chunk latency, and ready SDK availability. All numbers cited.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;whisper.cpp (base.en)&lt;/th&gt;
&lt;th&gt;whisper.cpp (small.en)&lt;/th&gt;
&lt;th&gt;Vosk (small-en-0.15)&lt;/th&gt;
&lt;th&gt;VoxRT NeMo FastConformer streaming&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model type&lt;/td&gt;
&lt;td&gt;Batch (encoder-decoder Transformer)&lt;/td&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;Streaming (Kaldi TDNN-F)&lt;/td&gt;
&lt;td&gt;Native streaming (Conformer + cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model size&lt;/td&gt;
&lt;td&gt;~149 MB ¹&lt;/td&gt;
&lt;td&gt;~489 MB ¹&lt;/td&gt;
&lt;td&gt;40 MB (+ 1.6 GB for punctuation) ²&lt;/td&gt;
&lt;td&gt;60.4 MB ³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WER (LibriSpeech test-clean)&lt;/td&gt;
&lt;td&gt;4.27% ⁴&lt;/td&gt;
&lt;td&gt;3.05% ⁴&lt;/td&gt;
&lt;td&gt;9.85% ²&lt;/td&gt;
&lt;td&gt;3.267% (RNN-T) / 4.895% (CTC) ⁵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming approach&lt;/td&gt;
&lt;td&gt;Chunked batch (~500 ms blocks)&lt;/td&gt;
&lt;td&gt;Chunked batch&lt;/td&gt;
&lt;td&gt;Native streaming&lt;/td&gt;
&lt;td&gt;Native cache-aware (80 ms lookahead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency pattern&lt;/td&gt;
&lt;td&gt;500-1500 ms per chunk (persistent)&lt;/td&gt;
&lt;td&gt;500-1500 ms per chunk&lt;/td&gt;
&lt;td&gt;300-500 ms per chunk&lt;/td&gt;
&lt;td&gt;1.12 s initial buffer, then continuous ~100-200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Punctuation by default&lt;/td&gt;
&lt;td&gt;✓ (in-model)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗ (needs +1.6 GB model)&lt;/td&gt;
&lt;td&gt;✓ (in-model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready iOS SPM package&lt;/td&gt;
&lt;td&gt;✗ (only community &lt;code&gt;ggml-org/whisper.spm&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready Android Gradle package&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT (code + weights)&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Apache-2.0 (code + weights)&lt;/td&gt;
&lt;td&gt;Apache-2.0 wrapper / proprietary runtime / CC-BY-4.0 model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;¹ whisper.cpp README, &lt;code&gt;ggml&lt;/code&gt; format disk size (source lists 142 MiB and 466 MiB, converted to MB).&lt;br&gt;
² alphacephei.com/vosk/models, evaluation table.&lt;br&gt;
³ voxrt-asr-models v0.1.2 downloads table.&lt;br&gt;
⁴ HuggingFace model cards &lt;code&gt;openai/whisper-base.en&lt;/code&gt; and &lt;code&gt;openai/whisper-small.en&lt;/code&gt;, evaluation section.&lt;br&gt;
⁵ Our WER-500 measurements on LibriSpeech test-clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important latency nuance:&lt;/strong&gt; whisper.cpp has persistent per-chunk latency (each chunk 500-1500 ms). VoxRT NeMo streaming has an initial buffer of 1.12 s (for cache warmup), then sustained latency of ~100-200 ms perceived. Meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For short queries (under 3 s)&lt;/strong&gt;: whisper.cpp can feel faster (VoxRT's initial buffer is just overhead).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For longer live sessions (over 3 s)&lt;/strong&gt;: VoxRT streaming feels smoother (continuous), whisper.cpp remains "jerky."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real numbers on mobile
&lt;/h2&gt;

&lt;p&gt;Everything above is architectural comparison. But any dev reading this will ask one question: &lt;strong&gt;how heavy is this on the device?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VoxRT NeMo FastConformer streaming:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapdragon 662 (mid-range Android from 2020): RTF &lt;strong&gt;0.302&lt;/strong&gt; (file replay) / &lt;strong&gt;0.353&lt;/strong&gt; (live-mic). That is 300-350 milliseconds of CPU time per 1 second of audio.&lt;/li&gt;
&lt;li&gt;iPhone 13 Pro Max (Apple A15): RTF &lt;strong&gt;0.08-0.10&lt;/strong&gt;. Processor is roughly 10× faster than real time.&lt;/li&gt;
&lt;li&gt;Runtime memory: &lt;strong&gt;~150 MB&lt;/strong&gt; steady-state.&lt;/li&gt;
&lt;li&gt;APK/IPA impact: 60.4 MB (model) + ~500 KB (native framework) = &lt;strong&gt;~61 MB&lt;/strong&gt; added to app size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;VoxRT Silero VAD (used as pre-filter):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SD662: RTF &lt;strong&gt;3.05%&lt;/strong&gt;, latency ~1 ms per 32-ms frame.&lt;/li&gt;
&lt;li&gt;iPhone A15: RTF &lt;strong&gt;1.85%&lt;/strong&gt;, latency ~0.6 ms.&lt;/li&gt;
&lt;li&gt;Model size: &lt;strong&gt;1.2 MB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Whisper.cpp benchmarks on mobile:&lt;/strong&gt; no public RTF numbers for Snapdragon or iOS in the whisper.cpp README, only Apple Silicon benchmarks. Community measurements (posts in r/androiddev, r/iOSProgramming) usually put Whisper base.en RTF in the 40-70% range on mid-range Android SoCs from 2020-2022. That is roughly 2× heavier than VoxRT NeMo for comparable accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical conclusion:&lt;/strong&gt; for always-on live transcription on mid-range Android, Whisper base.en often burns enough CPU to noticeably heat up the device and drain battery. Streaming ASR under 40% RTF is sustainable without visible performance impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision framework: when to switch, when to stay
&lt;/h2&gt;

&lt;p&gt;Practical rules for the reader.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay with Whisper if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch transcription of files (audio, wait, transcript). Live UX not required. Whisper accuracy is great, community is huge, plenty of ready tools.&lt;/li&gt;
&lt;li&gt;Short one-shot voice notes (user hits record, speaks 10 seconds, releases, gets transcript). Streaming ASR initial buffer is not justified here.&lt;/li&gt;
&lt;li&gt;You do not care about battery or CPU load (e.g., transcription runs only on-demand, not always-on).&lt;/li&gt;
&lt;li&gt;You want maximum community support, example code, and integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switch to streaming ASR if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live voice agents (chatbot that responds while user speaks). Every 500-1500 ms delay equals broken conversation.&lt;/li&gt;
&lt;li&gt;Real-time captions for accessibility (hearing-impaired users). Here latency directly impacts UX.&lt;/li&gt;
&lt;li&gt;Live meeting transcription with on-screen text visible as people speak.&lt;/li&gt;
&lt;li&gt;Always-on background listening (voice notes 24/7, wake-word context). CPU load matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Go with a hosted API (Deepgram, AssemblyAI, etc.) if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Concurrency: thousands of parallel sessions. Self-hosted infrastructure becomes overhead.&lt;/li&gt;
&lt;li&gt;Diarization required out of the box (who is speaking when). Native streaming ASR usually has no built-in diarization, you need a separate model (pyannote and similar).&lt;/li&gt;
&lt;li&gt;Multi-language mixed audio. Streaming models are usually English-focused, hosted APIs have multilingual support.&lt;/li&gt;
&lt;li&gt;Budget allows and privacy is not a hard requirement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Go with self-hosted streaming ASR (native streaming architecture) if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Privacy is required (data cannot leave device or your infrastructure).&lt;/li&gt;
&lt;li&gt;Mobile deployment: apps must work offline.&lt;/li&gt;
&lt;li&gt;Scale exists, but budget matters. Hosted APIs get expensive at high volume.&lt;/li&gt;
&lt;li&gt;You want low sustained latency without a hosted API dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Streaming vs batch in ASR is not a runtime tweak, it is an architectural choice. Whisper for batch is a valid default. For live it is a mismatch. Native streaming ASR (NeMo FastConformer, streaming Conformer, Parakeet variants) is a different tool for a different job.&lt;/p&gt;

&lt;p&gt;Three things worth remembering when picking a stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency pattern matters more than absolute latency.&lt;/strong&gt; Persistent 500 ms per chunk (Whisper chunked) subjectively feels worse than 1 s initial buffer + continuous ~100 ms (native streaming).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punctuation by default&lt;/strong&gt; is critical for UX. Whisper and native streaming provide it. Vosk requires a separate 1.6 GB model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready-made mobile SDK.&lt;/strong&gt; The only project that ships symmetric SPM + Gradle packages for streaming ASR is us (VoxRT). Whisper.cpp has a community SPM (&lt;code&gt;ggml-org/whisper.spm&lt;/code&gt;) but no Gradle. Vosk offers only bindings, wrap yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We build a Rust runtime for on-device voice models. Our ASR repos: &lt;a href="https://github.com/orgs/VoxRT/repositories?q=asr+sort%3Astars" rel="noopener noreferrer"&gt;github.com/orgs/VoxRT/repositories?q=asr+sort:stars&lt;/a&gt;. If you are building a live voice app for iOS, Android, or Linux, this stack plus a native streaming model give you everything needed.&lt;/p&gt;

&lt;p&gt;Our first post covered building a full on-device voice notes pipeline (VAD + ASR + local storage) with working Kotlin and Swift snippets: &lt;a href="https://dev.to/voxrtio/building-private-voice-notes-for-ios-and-android-without-the-cloud-54a8"&gt;dev.to/voxrtio/building-private-voice-notes-for-ios-and-android-without-the-cloud-54a8&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Learn more about the runtime and product family: &lt;a href="https://voxrt.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=streaming-asr-vs-whisper-mobile" rel="noopener noreferrer"&gt;voxrt.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Attribution: the streaming ASR model is a derivative of NVIDIA NeMo &lt;code&gt;stt_en_fastconformer_hybrid_medium_streaming_80ms_pc&lt;/code&gt;, released under CC-BY-4.0.&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>rust</category>
      <category>machinelearning</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
