<?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>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>voxrt</category>
      <category>rust</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Building Private Voice Notes for iOS and Android Without the Cloud</title>
      <dc:creator>VoxRT</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:01:00 +0000</pubDate>
      <link>https://dev.to/voxrtio/building-private-voice-notes-for-ios-and-android-without-the-cloud-54a8</link>
      <guid>https://dev.to/voxrtio/building-private-voice-notes-for-ios-and-android-without-the-cloud-54a8</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Voice-notes apps became one of the main tools for lawyers, doctors, journalists, and anyone who needs to capture a thought by speaking and find it later as text. Otter, Rev, Descript, Notion AI, Fireflies, and dozens of others all work the same way: the user hits "record," audio goes to the company's servers, ASR (automatic speech recognition) runs there, transcript comes back.&lt;/p&gt;

&lt;p&gt;The problem with this setup is simple but fundamental: &lt;strong&gt;your recordings leave the device&lt;/strong&gt;. This is not a hypothetical concern. Over the past two years, several major transcription services had incidents where user recordings leaked. Some ended up in publicly accessible cloud buckets. Some got exposed through compromised employee accounts that had training-data access.&lt;/p&gt;

&lt;p&gt;For a casual user, that is annoying. For a lawyer discussing case strategy with a client, it is an attorney-client privilege violation. For a doctor, it is a HIPAA problem. For a journalist, it can burn a source. And even without a leak, for many use cases the mere fact of audio being sent to a third-party server is already unacceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is an alternative, but it needs a different architecture&lt;/strong&gt;: transcription has to happen on the device itself. Not a single byte of audio leaves the phone. No API keys. No network calls. No dependency on whether your provider's backend is up.&lt;/p&gt;

&lt;p&gt;This post walks through how to do it on iOS and Android in roughly one evening of wiring. Preview: the code fits in about 50 lines per platform, the SDK is under 500 KB, and the transcription model is 60 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "on-device" actually means
&lt;/h2&gt;

&lt;p&gt;Before jumping into code, let's break down what a voice-notes pipeline actually contains. Understanding the architecture makes it easier to pick the right SDK.&lt;/p&gt;

&lt;p&gt;Three stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Microphone.&lt;/strong&gt; Provides a continuous stream of PCM (uncompressed audio samples): 16 kHz mono, 16-bit signed integers. This is the standard format for speech recognition. The sampling rate covers the human voice range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. VAD (Voice Activity Detection).&lt;/strong&gt; Determines whether the current audio chunk contains speech. A light model (~1 MB), but critical. Without VAD, you would run silence, air-conditioner hum, and chair squeaks through your ASR, which either produces garbage text or just burns CPU and battery for no reason. VAD cuts the input stream into speech segments and drops everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. ASR (Automatic Speech Recognition).&lt;/strong&gt; The actual speech-to-text step. A heavier model (~60 MB) that takes an audio segment and returns text. Modern streaming models emit text incrementally as audio comes in, without waiting for the recording to end. That is important for UX: users see the transcript "typing" in real time.&lt;/p&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Microphone] → [PCM: 16 kHz mono] → [VAD] → speech present?
    → yes → [segment buffer] → [ASR streaming] → [text]
    → no  → discard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this runs locally. The final text goes into a local database (SQLite, CoreData, whatever you use), optionally encrypted. Zero network traffic during transcription.&lt;/p&gt;

&lt;p&gt;The key metric for on-device work is &lt;strong&gt;RTF (Real-Time Factor)&lt;/strong&gt;: the ratio of processing time to audio length. RTF 5% means the processor spends 50 milliseconds on one second of audio. Lower RTF, less CPU and battery burn. For always-on voice notes, RTF above 50% starts being a problem: the phone heats up and the battery drains. A decent solution keeps RTF under 40% for transcription and under 5% for VAD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the alternatives
&lt;/h2&gt;

&lt;p&gt;Before showing VoxRT code, let's take an honest look at the alternatives. A mobile developer in 2026 has three real options for on-device speech: whisper.cpp (a C++ port of OpenAI Whisper), Vosk (Kaldi-based, Apache-2.0), and VoxRT (our Rust runtime on top of Silero VAD and NeMo FastConformer).&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;Vosk (small-en-0.15)&lt;/th&gt;
&lt;th&gt;VoxRT ASR (streaming-medium-pc)&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;142 MB&lt;/td&gt;
&lt;td&gt;40 MB (+ 1.6 GB for punctuation = 1.64 GB total)&lt;/td&gt;
&lt;td&gt;60.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WER on LibriSpeech test-clean&lt;/td&gt;
&lt;td&gt;4.27% ¹&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&lt;/td&gt;
&lt;td&gt;✓ (chunked via &lt;code&gt;whisper-stream&lt;/code&gt;, ~500 ms blocks)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (native cache-aware, 80 ms lookahead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready-made iOS SPM package&lt;/td&gt;
&lt;td&gt;✓ (community: &lt;code&gt;ggml-org/whisper.spm&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓ (from the main org)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready-made Android Gradle package&lt;/td&gt;
&lt;td&gt;✗ (only &lt;code&gt;.android&lt;/code&gt; example)&lt;/td&gt;
&lt;td&gt;✗ (bindings only)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Punctuation by default&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;License&lt;/td&gt;
&lt;td&gt;MIT (code + weights)&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;¹ Source: HuggingFace model card &lt;code&gt;openai/whisper-base.en&lt;/code&gt;, evaluation section.&lt;br&gt;
² Source: alphacephei.com/vosk/models, evaluation table.&lt;br&gt;
³ Our WER-500 measurements on LibriSpeech test-clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What matters in the table:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accuracy (WER on LibriSpeech test-clean).&lt;/strong&gt; VoxRT (3.267% with RNN-T) beats Whisper base.en (4.27%) at a slightly smaller size (60.4 MB vs 142 MB). Vosk small (9.85%) loses to both. That is a mobile-friendly tradeoff. The "full" Vosk (&lt;code&gt;vosk-model-en-us-0.22&lt;/code&gt;) reaches 5.69% WER but weighs 1.8 GB. Whisper also has a more accurate tier: &lt;code&gt;whisper.cpp small.en&lt;/code&gt; gives &lt;strong&gt;3.05%&lt;/strong&gt; WER but weighs &lt;strong&gt;466 MB&lt;/strong&gt;, roughly 7.7× larger than ours for comparable accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size.&lt;/strong&gt; Vosk small is the smallest base package (40 MB), but to get punctuated transcripts you need an extra &lt;code&gt;vosk-recasepunc-en-0.22&lt;/code&gt; model at 1.6 GB. Full-featured Vosk ends up ~27× heavier than VoxRT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDK readiness.&lt;/strong&gt; Only VoxRT ships both SPM (iOS) and Gradle (Android) packages from the same org with symmetric APIs. whisper.cpp has a community SPM package (&lt;code&gt;ggml-org/whisper.spm&lt;/code&gt;) but no Gradle package, so on Android you still write your own wrapper around the C++ example. Vosk has neither. Bindings exist for many languages, but no ready-made SPM or Gradle package. If you want a stable, symmetric integration on both mobile platforms without writing your own native wrapper, VoxRT is the only option today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licensing.&lt;/strong&gt; VoxRT SDK is ready for use in commercial iOS/Android apps. The Kotlin/Swift wrapper is Apache-2.0, a standard permissive license, safe to embed in closed-source products. The native Rust runtime is proprietary (LICENSE-BINARY, Elephant Enterprises LLC), shipped as part of the SDK. The model is under NVIDIA's CC-BY-4.0, which needs an attribution line in your app's credits/about section. For comparison: whisper.cpp is MIT, Vosk is Apache-2.0, both fully open-source (including native code).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On our WER.&lt;/strong&gt; Both decoders live in the same model file, switched via an API constant. RNN-T is more accurate (3.267%), CTC is roughly 15% faster at inference (4.895%). Pick based on whether quality or speed matters more in your scenario.&lt;/p&gt;

&lt;p&gt;For the rest of this post I'll use VoxRT as the example, because a one-line dependency setup lets us fit a working tutorial in a reasonable length. The pipeline logic (VAD → ASR → text) is the same for any of the three options.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing the SDK
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Android.&lt;/strong&gt; Add JitPack as a repository source in &lt;code&gt;settings.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencyResolutionManagement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;maven&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://jitpack.io"&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;Two dependencies in your app module's &lt;code&gt;build.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.VoxRT:voxrt-silero-android:v0.1.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.VoxRT:voxrt-asr-android:v0.1.1"&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;Drop the model files (&lt;code&gt;.vxrt&lt;/code&gt;) into your app's &lt;code&gt;assets/&lt;/code&gt;: &lt;code&gt;silero_vad.vxrt&lt;/code&gt; (~1.2 MB) for VAD and &lt;code&gt;streaming_medium_pc.vxrt&lt;/code&gt; (60.4 MB) for ASR. Download them from the releases of the corresponding repos at github.com/VoxRT.&lt;/p&gt;

&lt;p&gt;One more thing for Android: tell Gradle not to compress the &lt;code&gt;.vxrt&lt;/code&gt; assets, so the SDK can memory-map them directly from the APK. Add this to the &lt;code&gt;android { ... }&lt;/code&gt; block in your app's &lt;code&gt;build.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;androidResources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;noCompress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vxrt"&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;Skipping this leads to slower init and, on some setups, runtime errors when opening the model file descriptor.&lt;/p&gt;

&lt;p&gt;Minimum supported Android is API 26 (Android 8.0). Architecture: arm64-v8a (which covers 99% of modern devices).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS.&lt;/strong&gt; Add two packages to &lt;code&gt;Package.swift&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/VoxRT/voxrt-silero-ios.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.1.3"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/VoxRT/voxrt-asr-ios.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.1.2"&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;Or through the Xcode UI: &lt;code&gt;File → Add Packages&lt;/code&gt; → paste URL. Model files go into the app bundle (Copy Bundle Resources).&lt;/p&gt;

&lt;p&gt;Minimum iOS is 16.0, arm64 devices (iPhone 5s and newer, plus Apple Silicon simulators).&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic workflow: code
&lt;/h2&gt;

&lt;p&gt;Both platforms below. The logic is fully symmetric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin (Android):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.voxrt.silero.VoxrtSileroVadEngine&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.voxrt.silero.VadEvent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.voxrt.asr.VoxrtAsrStreamingEngine&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VoiceNotesRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;vad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"silero_vad.vxrt"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;VoxrtSileroVadEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAssetFd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;asr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"streaming_medium_pc.vxrt"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;VoxrtAsrStreamingEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAssetFd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;speechBuffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableListOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;inSpeech&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;processAudioFrame&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="nc"&gt;ShortArray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processPcm&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;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;VadEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpeechOnset&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inSpeech&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
                &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;VadEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpeechOffset&lt;/span&gt; &lt;span class="p"&gt;-&amp;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="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;delta&lt;/span&gt; &lt;span class="p"&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;processPcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFloatArray&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;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nf"&gt;onTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;inSpeech&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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="c1"&gt;// Convert samples only when we are actually accumulating speech.&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;inSpeech&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="n"&gt;sample&lt;/span&gt; &lt;span class="k"&gt;in&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;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;32768f&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="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;tail&lt;/span&gt; &lt;span class="p"&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="n"&gt;asr&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;vad&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onTranscript&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Save to your local DB: SQLite, Room, whatever&lt;/span&gt;
        &lt;span class="c1"&gt;// Nothing goes over the network&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;All you have to do from the outside: feed &lt;code&gt;processAudioFrame&lt;/code&gt; with PCM frames from the microphone (16 kHz mono, Int16). The standard &lt;code&gt;AudioRecord&lt;/code&gt; API on Android delivers exactly that format. &lt;code&gt;onTranscript&lt;/code&gt; fires whenever a transcript chunk is ready. You can write it straight to the UI or to a database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swift (iOS):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;VoxrtSilero&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;VoxrtAsr&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;VoiceNotesRecorder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;vad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VoxrtSileroVadEngine&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VoxrtAsrStreamingEngine&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;inSpeech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;vadURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"silero_vad"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"vxrt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;asrURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"streaming_medium_pc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"vxrt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;fatalError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".vxrt files not found in bundle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;vad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;VoxrtSileroVadEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;modelURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vadURL&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="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;VoxrtAsrStreamingEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;modelURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asrURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;processAudioFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Int16&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processPcm&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;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speechOnset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;inSpeech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speechOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processPcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;inSpeech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Convert samples only when we are actually accumulating speech.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inSpeech&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;speechBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentsOf&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;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;32768.0&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="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="o"&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;vad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;onTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Save locally: CoreData / SwiftData / SQLite&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;Audio on iOS comes from &lt;code&gt;AVAudioEngine&lt;/code&gt; via installTap. Converting to 16 kHz Int16 mono is a standard task via &lt;code&gt;AVAudioConverter&lt;/code&gt;, documented by Apple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this code does:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audio from the microphone goes into VAD frame by frame.&lt;/li&gt;
&lt;li&gt;When VAD signals "speech started," we start accumulating samples in a buffer.&lt;/li&gt;
&lt;li&gt;When VAD signals "speech ended," we send the accumulated buffer to ASR, get the text back, and clear the buffer.&lt;/li&gt;
&lt;li&gt;On stop (&lt;code&gt;stop()&lt;/code&gt;), we drain any remaining buffered text from ASR and release resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipped for brevity: error handling, microphone permissions (declare in &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and &lt;code&gt;Info.plist&lt;/code&gt;), background execution, and converting mic audio to the right format. Standard mobile plumbing, not specific to voice recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real numbers: device performance
&lt;/h2&gt;

&lt;p&gt;Any developer reading this and thinking about integration will ask: how heavy is this on the device? Numbers below come from real measurements, hardware specified for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VAD (Silero via the VoxRT runtime):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapdragon 662 (mid-range Android from 2020): RTF &lt;strong&gt;3.05%&lt;/strong&gt;, latency &lt;strong&gt;~1 ms&lt;/strong&gt; per 32-ms frame.&lt;/li&gt;
&lt;li&gt;iPhone 13 Pro Max (Apple A15): RTF &lt;strong&gt;1.85%&lt;/strong&gt;, latency &lt;strong&gt;~0.6 ms&lt;/strong&gt; per 32-ms frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both numbers mean the same thing: &lt;strong&gt;VAD is essentially free&lt;/strong&gt;. Even on a mid-range Android from five years ago, it takes ~3% of one CPU core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ASR (streaming FastConformer via the VoxRT runtime):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapdragon 662: RTF &lt;strong&gt;0.302&lt;/strong&gt; (file replay) / &lt;strong&gt;0.353&lt;/strong&gt; (live-mic). In practice, the processor handles one second of audio in about 300-350 milliseconds.&lt;/li&gt;
&lt;li&gt;iPhone 13 Pro Max: RTF &lt;strong&gt;0.08-0.10&lt;/strong&gt;. On A15 it processes roughly 10× faster than real time.&lt;/li&gt;
&lt;li&gt;Chunk latency: &lt;strong&gt;~1.12 s&lt;/strong&gt; of buffering before the first text output. That is an architectural property of the model: cache-aware streaming with 80 ms lookahead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Accuracy (WER on LibriSpeech test-clean):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RNN-T decoder (default): &lt;strong&gt;3.267%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CTC decoder (~15% faster inference): &lt;strong&gt;4.895%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Punctuation and capitalization come from the model directly, no post-processing. The output reads like normal English, not one long lowercase stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File sizes (what ends up in your APK/IPA):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VAD model: &lt;strong&gt;1.2 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;ASR model: &lt;strong&gt;60.4 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VAD native binary (Android, stripped): &lt;strong&gt;~424 KB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VAD framework (iOS, compressed): &lt;strong&gt;~500 KB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total impact on app size: &lt;strong&gt;~62 MB&lt;/strong&gt; (mostly the ASR model). Comparable to what an average App Store game takes, and smaller than the full whisper.cpp base.en (142 MB) or Vosk with punctuation (1.64 GB).&lt;/p&gt;

&lt;h2&gt;
  
  
  What else a production app needs
&lt;/h2&gt;

&lt;p&gt;The tutorial above is the minimum working setup. A production app needs a few more things I won't walk through line by line but should mention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions.&lt;/strong&gt; Android: &lt;code&gt;RECORD_AUDIO&lt;/code&gt; in the manifest plus a runtime permission request. iOS: &lt;code&gt;NSMicrophoneUsageDescription&lt;/code&gt; in Info.plist. Standard procedure, documented by both Apple and Google.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background recording.&lt;/strong&gt; iOS: &lt;code&gt;background modes → audio&lt;/code&gt;. Android: foreground service with a notification. Without these, iOS kills your recorder when the app goes to background, and Android does the same after 5-10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local encryption for transcripts.&lt;/strong&gt; Since we promised the user privacy, it is worth encrypting saved notes. iOS: Keychain for the key plus &lt;code&gt;CryptoKit&lt;/code&gt; for AES-GCM. Android: &lt;code&gt;EncryptedSharedPreferences&lt;/code&gt;, or Keystore plus &lt;code&gt;Cipher&lt;/code&gt;. Simple plumbing, but must-have for privacy-first positioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microphone interruption handling.&lt;/strong&gt; An incoming call arrives, you have to stop the recording, release the audio session, then restore it. iOS: &lt;code&gt;AVAudioSession.interruptionNotification&lt;/code&gt;. Android: &lt;code&gt;AudioManager.OnAudioFocusChangeListener&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model lifecycle.&lt;/strong&gt; ASR models take ~150 MB RAM in the loaded state. If your app goes to background for a long time, call &lt;code&gt;close()&lt;/code&gt; on the engine and reinitialize on return. VAD is light enough to keep loaded all the time.&lt;/p&gt;

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

&lt;p&gt;Private voice notes without the cloud are not an abstract idea or a heavy engineering effort. In one evening you can integrate a full pipeline (VAD → ASR → local storage) into an existing iOS/Android app. Result: the user gets an Otter-like experience, but not a single byte of audio leaves the device.&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;RTF&lt;/strong&gt; is the target metric for on-device work. A good solution keeps transcription under 40% RTF on a mid-range Android and under 10% on iPhone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punctuation by default&lt;/strong&gt; saves you 1.6 GB (Vosk's punctuation model) or the need to write custom post-processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready-made SPM (iOS) and Gradle (Android) packages from a single project&lt;/strong&gt; save several days of boilerplate compared to wrapping a C++ library yourself, especially on Android where whisper.cpp and Vosk still require your own wrapper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools used in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silero VAD wrapper: &lt;a href="https://github.com/VoxRT/voxrt-silero-android" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-silero-android&lt;/a&gt; / &lt;a href="https://github.com/VoxRT/voxrt-silero-ios" rel="noopener noreferrer"&gt;voxrt-silero-ios&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Streaming ASR wrapper: &lt;a href="https://github.com/VoxRT/voxrt-asr-android" rel="noopener noreferrer"&gt;github.com/VoxRT/voxrt-asr-android&lt;/a&gt; / &lt;a href="https://github.com/VoxRT/voxrt-asr-ios" rel="noopener noreferrer"&gt;voxrt-asr-ios&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Models: releases in the corresponding &lt;code&gt;-models&lt;/code&gt; repos in the &lt;a href="https://github.com/VoxRT" rel="noopener noreferrer"&gt;github.com/VoxRT&lt;/a&gt; organization.&lt;/li&gt;
&lt;li&gt;Main VoxRT site with a full stack overview: &lt;a href="https://voxrt.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=private-voice-notes" rel="noopener noreferrer"&gt;voxrt.com&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full example code, including the wiring around &lt;code&gt;AudioRecord&lt;/code&gt; / &lt;code&gt;AVAudioEngine&lt;/code&gt;, will land as a small demo app in a follow-up post.&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>android</category>
      <category>ios</category>
      <category>whisper</category>
      <category>voxrt</category>
    </item>
  </channel>
</rss>
