<?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: Shantanav Kapse</title>
    <description>The latest articles on DEV Community by Shantanav Kapse (@shantanavkapse73).</description>
    <link>https://dev.to/shantanavkapse73</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%2F4070002%2F602d9a11-7cb4-4adc-881a-c0982e1ee50e.png</url>
      <title>DEV Community: Shantanav Kapse</title>
      <link>https://dev.to/shantanavkapse73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shantanavkapse73"/>
    <language>en</language>
    <item>
      <title>Building Real-Time Dictation from Scratch: Escaping the Offline Trap</title>
      <dc:creator>Shantanav Kapse</dc:creator>
      <pubDate>Sun, 09 Aug 2026 19:14:16 +0000</pubDate>
      <link>https://dev.to/shantanavkapse73/building-real-time-dictation-from-scratch-escaping-the-offline-trap-l14</link>
      <guid>https://dev.to/shantanavkapse73/building-real-time-dictation-from-scratch-escaping-the-offline-trap-l14</guid>
      <description>&lt;p&gt;When the requirement first landed on my desk, it sounded simple enough: Build a real-time, cursor-based dictation feature. The user speaks into their browser, and the words appear live at the text cursor so they can draft long documents.&lt;/p&gt;

&lt;p&gt;But there was a catch. The domain used highly specialized vocabulary, and the audio was strictly regulated. Sending the data to a slick cloud API was out of the question-audio could never leave our servers.&lt;/p&gt;

&lt;p&gt;This is the story of my first deep dive into the trenches of speech-to-text (STT) engineering. Here is what I explored, the brutal obstacles I hit, and the architecture I ultimately shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Choice: Faking It vs. Native Streaming
&lt;/h2&gt;

&lt;p&gt;My first architectural crossroad was choosing the model family. I initially looked at offline attention encoder-decoder models. They are famous for their high accuracy. However, they are trained to transcribe a complete audio file.&lt;/p&gt;

&lt;p&gt;To use an offline model for "live" dictation, you have to fake it. You maintain a rolling window of audio (say, the last few seconds), and every 0.5 seconds, you re-transcribe the entire window. To decide what text to actually keep, you wait until two consecutive transcriptions agree on a word (local agreement) and then commit it.&lt;/p&gt;

&lt;p&gt;I quickly realized this was a trap. It re-transcribes overlapping audio constantly, melting CPU cycles. Worse, when fed absolute silence, these offline models hallucinated wildly, inventing stock phrases or sign-offs. Because the hallucination was stable across passes, the system would permanently commit cascades of repeated ghost words.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pivot: I ditched the offline approach and moved to streaming neural transducers (the RNN-T family).
&lt;/h3&gt;

&lt;p&gt;Transducers are built differently. They emit tokens as the audio arrives and have a built-in notion of "endpointing" (detecting a pause to mark the end of an utterance). They are natively real-time, mathematically stable, and most importantly, they don't hallucinate text out of thin air.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Key Takeaway: If you are building live dictation, force-fitting an offline model fights the use case. A streaming transducer is the right tool for the job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Nailing the UX: The Two-State Contract
&lt;/h2&gt;

&lt;p&gt;To make the live cursor feel magical rather than jarring, I needed a rock-solid UI contract. A jumping, flickering text cursor is incredibly frustrating for users drafting long documents.&lt;/p&gt;

&lt;p&gt;I implemented a two-state output contract where every WebSocket update carries two fields: {committed, buffer}.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Committed: Stable text the client appends permanently (rendered in a normal font weight).&lt;/li&gt;
&lt;li&gt;Buffer: The model's current provisional guess (rendered in grey or italics, replacing itself on every update).
Because the streaming model handles endpointing naturally, it fires off the final text when the user pauses. The client promotes the buffer to committed, and the stream resets. The stable text never jumps backward, and the provisional tail visibly settles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Obstacle 1: The Silent WebSocket Killer
&lt;/h2&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%2Flf4ufalenf4l9zeo4fvo.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%2Flf4ufalenf4l9zeo4fvo.png" alt="The key feature is the DECOUPLING of the " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting the browser to capture audio was straightforward. The browser records microphone audio as Opus-compressed chunks and streams them over a WebSocket every ~250 milliseconds. The server then decodes this into the raw 16 kHz mono PCM the model requires.&lt;/p&gt;

&lt;p&gt;Then came the hardest engineering bug of the project.&lt;/p&gt;

&lt;p&gt;The dictation would work perfectly for a sentence or two, and then the WebSocket would randomly disconnect. No errors, just a dropped connection.&lt;/p&gt;

&lt;p&gt;I traced it to a concurrency and back-pressure failure. Initially, a single task was both reading the decoded audio and running the STT model inline. While the model was busy crunching numbers, nothing was draining the audio decoder's output pipe. The pipe filled up, the decoder stopped pulling input, the upstream socket blocked, and the WebSocket receive loop stalled. The keepalive ping/pong stopped, and the connection died.&lt;/p&gt;

&lt;p&gt;The Fix: I decoupled the pipeline into two isolated tasks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Reader: Continuously drains the decoder into an in-memory buffer so the pipe never blocks.&lt;/li&gt;
&lt;li&gt;A Processor: Consumes that buffer and runs the model at its own pace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once decoupled, back-pressure never reached the socket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obstacle 2: Formatting and Domain Jargon
&lt;/h2&gt;

&lt;p&gt;Documents need capital letters and periods. Many streaming models emit normalized text (lowercase, no punctuation).&lt;/p&gt;

&lt;p&gt;I had two choices: run a secondary punctuation-restoration model on the committed text, or find a model that handles casing natively. I opted for the latter. By checking the model's token vocabulary for mixed-case pieces and punctuation tokens, I bypassed the need for a secondary formatting layer entirely.&lt;/p&gt;

&lt;p&gt;Handling specialized vocabulary was trickier. General models stumble on niche jargon. While contextual biasing (word boosting) is great, the lightweight runtime for my streaming model only supported greedy decoding, meaning acoustic biasing was unavailable.&lt;/p&gt;

&lt;p&gt;I solved this pragmatically with a Post-ASR Correction Map-a simple dictionary of {misheard -&amp;gt; correct} terms applied to the committed text. It isn't as elegant as acoustic biasing, but it reliably catches repeatable mistakes based on actual user data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deployment Reality: CPU over GPU
&lt;/h2&gt;

&lt;p&gt;You might assume a real-time AI model requires massive GPU power. In reality, the streaming transducer is incredibly efficient.&lt;/p&gt;

&lt;p&gt;The Real-Time Factor (RTF) is processing time divided by audio duration. For a ~0.6 billion parameter streaming model, I measured an RTF of roughly 0.09 on a standard laptop CPU. That is 11x real-time headroom.&lt;/p&gt;

&lt;p&gt;This led to a counter-intuitive deployment strategy:&lt;/p&gt;

&lt;p&gt;Keep it off the GPU: Co-locating this STT model with a large LLM on a single GPU is a bad idea (I already had an LLM deployed on the server, but the CPU was not utilized). The bursty generation of the LLM will stall the latency-sensitive audio stream, causing audible stuttering.&lt;br&gt;
Run on CPU: The ASR runs comfortably on spare CPU cores. I wrapped it in a container with hard CPU and memory limits so it couldn't physically exceed its lane or trigger an Out-Of-Memory (OOM) crash on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building this from scratch taught me that the hardest parts of applied AI often aren't the neural networks themselves-they are the plumbing, the back-pressure, and the UX contracts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;th&gt;Cloud API&lt;/th&gt;
&lt;th&gt;Local Streaming Transducer (My Build)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Audio leaves premises&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Stays entirely on-prem&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-minute billing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free / Compute only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accuracy / Drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor-managed, highly tuned&lt;/td&gt;
&lt;td&gt;Requires manual correction maps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network dependent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Near-instant (RTF &amp;lt; 0.1 on CPU)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choosing to self-host a streaming model meant giving up the automatic updates and easy jargon-boosting of a cloud API. But for a privacy-regulated environment, it was the only honest trade. We achieved true real-time, zero-cost, private dictation-and the cursor feels just right.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
