<?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: Yankel Perelman</title>
    <description>The latest articles on DEV Community by Yankel Perelman (@yankel_perelman_96a1f31a6).</description>
    <link>https://dev.to/yankel_perelman_96a1f31a6</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%2F4181188%2F23528aad-7f13-4e33-8bb5-c6b77716e1c8.png</url>
      <title>DEV Community: Yankel Perelman</title>
      <link>https://dev.to/yankel_perelman_96a1f31a6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yankel_perelman_96a1f31a6"/>
    <language>en</language>
    <item>
      <title>Streaming microphone audio to Gemini Live from a browser: what I learned building a small voice assistant</title>
      <dc:creator>Yankel Perelman</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:35:26 +0000</pubDate>
      <link>https://dev.to/yankel_perelman_96a1f31a6/streaming-microphone-audio-to-gemini-live-from-a-browser-what-i-learned-building-a-small-voice-16m7</link>
      <guid>https://dev.to/yankel_perelman_96a1f31a6/streaming-microphone-audio-to-gemini-live-from-a-browser-what-i-learned-building-a-small-voice-16m7</guid>
      <description>&lt;p&gt;I wanted to ask my server how it was doing by voice: “what’s the load?”, “how do I free some memory?”. So I built a small FastAPI app that relays microphone audio from a browser to Gemini Live and plays the answer back. The code is open source: &lt;a href="https://github.com/yankelperelman/anna-dev" rel="noopener noreferrer"&gt;https://github.com/yankelperelman/anna-dev&lt;/a&gt;. Here is what turned out to matter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp87509xbsd5wf5lqmkr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp87509xbsd5wf5lqmkr.png" alt="Anna web interface with model and voice selection, a start button and a live transcript of the conversation" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Browser ↔ WebSocket ↔ FastAPI ↔ Gemini Live. The server in the middle keeps the API key out of the page and runs the tools (server stats, web search, Telegram) itself. Each connection runs two asyncio tasks, browser→Gemini and Gemini→browser. When either one ends, the other is cancelled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing audio
&lt;/h2&gt;

&lt;p&gt;The Live API expects 16 kHz 16-bit PCM, while browsers give you 44.1 or 48 kHz floats. I downsample in JavaScript with linear interpolation, convert to int16 and send binary WebSocket frames:&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;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputRate&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="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;outLength&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;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&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;right&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;output&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;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&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="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;left&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;This runs in a &lt;code&gt;ScriptProcessorNode&lt;/code&gt;, which is deprecated; &lt;code&gt;AudioWorklet&lt;/code&gt; is the right tool and is on my to-do list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playback
&lt;/h2&gt;

&lt;p&gt;Replies arrive as 24 kHz PCM chunks. I decode each chunk into an &lt;code&gt;AudioBuffer&lt;/code&gt; and schedule it right after the previous one:&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;startAt&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextPlayTime&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;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;nextPlayTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a good connection this is smooth. On a mobile connection I got stuttering, because late chunks leave gaps. On one carrier it disappeared when I forced 3G instead of 4G. I haven’t found the root cause. A larger lead time trades latency for smoothness, and I experimented with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session lifecycle
&lt;/h2&gt;

&lt;p&gt;The receive iterator finishes at the end of every turn, so the loop has to go around it. An iterator that ends without delivering anything means the session is gone, so I treat that as an error instead of looping forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool calls
&lt;/h2&gt;

&lt;p&gt;When the model calls a declared function, the server runs it and sends back a &lt;code&gt;FunctionResponse&lt;/code&gt;. The system prompt says explicitly never to claim an action was done until the tool returned success. Voice agents are tempted to say “done” first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the model on topic
&lt;/h2&gt;

&lt;p&gt;The system prompt limits Anna to server administration and tells her to decline everything else. This is a soft guard: a persistent user can talk around it. If you need a hard limit, remove tools instead of relying on the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A security detail worth knowing
&lt;/h2&gt;

&lt;p&gt;A local WebSocket server can be reached from any web page open in your browser. I added an Origin check, so other sites can’t connect or change the saved API key, and an optional access-token cookie for servers exposed on a domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s rough
&lt;/h2&gt;

&lt;p&gt;Deprecated audio API, no anti-aliasing filter in the resampler, no authentication unless you configure a token, and the monitoring skill only works on Linux. Feedback and issues are welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
