<?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: Guo Xiufeng</title>
    <description>The latest articles on DEV Community by Guo Xiufeng (@guoxiufeng).</description>
    <link>https://dev.to/guoxiufeng</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%2F4239645%2Fd5cd4f2f-d376-4664-adf3-1c783f0c9d2f.png</url>
      <title>DEV Community: Guo Xiufeng</title>
      <link>https://dev.to/guoxiufeng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guoxiufeng"/>
    <language>en</language>
    <item>
      <title>Shipping a real-time speech translation layer into WhatsApp, Zoom and OBS</title>
      <dc:creator>Guo Xiufeng</dc:creator>
      <pubDate>Wed, 11 Nov 2026 18:15:27 +0000</pubDate>
      <link>https://dev.to/guoxiufeng/shipping-a-real-time-speech-translation-layer-into-whatsapp-zoom-and-obs-28ln</link>
      <guid>https://dev.to/guoxiufeng/shipping-a-real-time-speech-translation-layer-into-whatsapp-zoom-and-obs-28ln</guid>
      <description>&lt;p&gt;Every B2B sales conversation across a language barrier has the same shape: one side writes in English, the other reads with a translation tool, and somewhere in the middle about four minutes get lost. Not on the words — on the round trip.&lt;/p&gt;

&lt;p&gt;We wanted to remove the round trip. Here is what actually broke, in the order it broke, and what we would do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;A procurement manager in Mombasa asks a question over WhatsApp voice. The exporter in Shandong does not read English comfortably. So the exporter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;plays the clip&lt;/li&gt;
&lt;li&gt;opens a translation app&lt;/li&gt;
&lt;li&gt;copies the text in&lt;/li&gt;
&lt;li&gt;reads the output&lt;/li&gt;
&lt;li&gt;types an answer in Chinese&lt;/li&gt;
&lt;li&gt;translates it back&lt;/li&gt;
&lt;li&gt;pastes it into WhatsApp&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Seven steps, roughly four minutes, for one question. Meanwhile the buyer is asking the same question to five other suppliers. Whoever answers first usually wins.&lt;/p&gt;

&lt;p&gt;This is not a language proficiency problem. It is a latency problem, and latency problems are engineering problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture, briefly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mic (16k PCM) -&amp;gt; WebSocket -&amp;gt; ASR + MT (streaming) -&amp;gt; TTS -&amp;gt; two output sinks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use a streaming speech-to-speech model over a persistent WebSocket, chunking audio at a small frame size so the model starts emitting before the sentence finishes. Measured end to end, the delay sits a bit above two seconds — which turns out to be the threshold where the conversation stops feeling like a walkie-talkie and starts feeling like an interpreter sitting next to you.&lt;/p&gt;

&lt;p&gt;Two seconds is not a number we chose. It is the point where people stop pausing to wait for the translation and start talking normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: you need two output paths, not one
&lt;/h2&gt;

&lt;p&gt;This one cost us the most time.&lt;/p&gt;

&lt;p&gt;The naive design routes the translated stream to the call. Then the speaker hears their own translated voice come back through the same device, and you get the classic public-address feedback loop — the rising hum when a microphone gets too close to a speaker.&lt;/p&gt;

&lt;p&gt;The fix is two separate sinks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sink A&lt;/strong&gt; -&amp;gt; the remote participant. Translated audio only. They never hear the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sink B&lt;/strong&gt; -&amp;gt; the local operator. Original plus translation. They need to hear what they said to know if it landed right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing these into one stream is the single most common mistake in this class of product, and it is not a tuning problem — it is a topology problem. No amount of gain staging fixes a wrong topology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: process priority does not buy you latency
&lt;/h2&gt;

&lt;p&gt;When the first prototype felt sluggish, the obvious move was to raise the process priority and pin threads. It changed nothing measurable.&lt;/p&gt;

&lt;p&gt;It is worth internalizing early: the delay lives in the network hop and the model's first-token time. Scheduling on the client is a rounding error next to those. If a user reports lag and you reach for &lt;code&gt;SetPriorityClass&lt;/code&gt; before you reach for a network trace, you are optimizing the wrong layer.&lt;/p&gt;

&lt;p&gt;We keep a small telemetry breakdown — capture, upload, model first token, synthesis — and it is obvious from that table where the time goes. Any one of the four can dominate, and none of them is the CPU scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: do not write an audio driver
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to write your own virtual audio device so the translation layer can sit transparently inside any app. On Windows that means a signed kernel-mode driver, WHQL submission, and an ongoing compatibility surface across every host application. This is months of work with zero product differentiation.&lt;/p&gt;

&lt;p&gt;Use an existing virtual cable. It already works, it is already signed, and users who have ever set up a stream already know what it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: you probably do not need to build a mixer
&lt;/h2&gt;

&lt;p&gt;Same reasoning. OBS, Streamlabs and the native live tools on each platform already ship mature routing and mixing. Exposing one more device is a day of work. Building your own mixer is a quarter, and it will be worse than the one users already have open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with the telemetry breakdown before writing the pipeline.&lt;/strong&gt; Four numbers in a table would have saved us the priority-tuning detour entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the two sinks as a first-class architectural constraint&lt;/strong&gt;, not a settings toggle. Every code path that assumes a single audio output has to be found and fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide the transport story early.&lt;/strong&gt; Where the model runs relative to the user sets your floor on latency; a client-side or single-hop-region topology will not save a bad routing decision.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;We shipped it inside a larger cross-border sales tool — ad creation, lead intake, and the translation layer above. The translation piece is free to try for 15 days, mostly because we want people to test the two-seconds-plus claim on their own hardware and their own network, not on ours.&lt;/p&gt;

&lt;p&gt;The engine is not the interesting part. The topology is. Two sinks, one transport, and honest numbers in a telemetry table — that is the whole trick.&lt;/p&gt;

&lt;p&gt;If you want to poke at it: &lt;a href="https://cgwd.wang" rel="noopener noreferrer"&gt;cgwd.wang&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>websocket</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
