<?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: orca_forge</title>
    <description>The latest articles on DEV Community by orca_forge (@orca_forge).</description>
    <link>https://dev.to/orca_forge</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%2F4034330%2Fc9ccc162-e897-4e55-9e1a-de55bce4bc27.png</url>
      <title>DEV Community: orca_forge</title>
      <link>https://dev.to/orca_forge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/orca_forge"/>
    <language>en</language>
    <item>
      <title>I Created a 24/7 AI Avatar That Streams Without Human Intervention — Only 'Verification,' 'Eyes,' and 'Ears' Remain for Humans</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 27 Aug 2026 05:46:25 +0000</pubDate>
      <link>https://dev.to/orca_forge/i-created-a-247-ai-avatar-that-streams-without-human-intervention-only-verification-eyes-67d</link>
      <guid>https://dev.to/orca_forge/i-created-a-247-ai-avatar-that-streams-without-human-intervention-only-verification-eyes-67d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/24-7-autonomous-ai-avatar-streaming/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=24-7-autonomous-ai-avatar-streaming" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;番組の時間になる → Let me hand you the finished piece.&lt;/p&gt;




&lt;p&gt;I built a system where a 3D avatar live-streams entirely on its own. The only thing a human does is &lt;strong&gt;register the show&lt;/strong&gt;. On the day itself, nobody touches anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Showtime arrives
  → the broadcast is created automatically (YouTube)
  → a cloud GPU Pod spins up
  → the avatar renders in a page and starts pushing over RTMP
  → the broadcast transitions to live
  → it responds to viewer comments with speech
  → it reacts to tips based on the amount
  → when comments dry up, it raises its own topics from the show's theme
  → when the runtime is up, it gives a closing greeting
  → the stream ends, the Pod is destroyed, the archive remains
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I worked through this system with an AI agent over several weeks — design, implementation, deployment, fault injection, and long-running verification. The first half of this post covers the overall architecture and &lt;strong&gt;why I split it the way I did&lt;/strong&gt;. The second half covers what became visible through the process: &lt;strong&gt;the roles that were left to humans and only humans&lt;/strong&gt;. The further you push toward unattended operation, the more sharply the parts you can never hand off come into focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────┐
│   Show Management UI   │  register a show (character / theme / runtime / destinations)
└──────────┬─────────────┘
           │
┌──────────▼─────────────┐   create / health-check / destroy Pods
│       Scheduler        │───────────────────────────┐
│  (stream lifecycle)    │                           │  create / go live / end broadcasts
└──────────┬─────────────┘                           │
           │ run (one stream execution)              │
┌──────────▼─────────────┐               ┌───────────▼────────────┐
│    Dialogue Server     │◀── Redis ─────│     Chat Collector     │
│ (utterance generation) │    Stream     │      (YT / Twitch)     │
└──────────┬─────────────┘               └────────────────────────┘
           │ WebSocket (display only)
┌──────────▼─────────────┐
│     Renderer Pod       │  headless Chromium + ffmpeg
│         (GPU)          │──▶ tee ──▶ YouTube / Twitch
└────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I split it this way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I gave the "stream lifecycle" its own layer
&lt;/h3&gt;

&lt;p&gt;This was the very first decision. &lt;strong&gt;A stream begins, continues, and ends&lt;/strong&gt; — it has a lifespan. And if it doesn't end, the billing doesn't either.&lt;/p&gt;

&lt;p&gt;If you bury that lifespan management inside the dialogue logic, a bug in the dialogue turns directly into runaway costs. By splitting it out, the scheduler only has to watch one thing: is the stream alive, has it ended, is the Pod gone?&lt;/p&gt;

&lt;p&gt;That's the scheduler's entire set of responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the time comes, create a run (one stream execution)&lt;/li&gt;
&lt;li&gt;Create the broadcast and transition it to live&lt;/li&gt;
&lt;li&gt;Start the GPU Pod, &lt;strong&gt;monitor its health, and re-acquire on a different host if it dies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When the runtime is up, move to the closing, end the stream, and &lt;strong&gt;destroy the Pod&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If it doesn't go live within a fixed window, mark it failed and clean up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those last two, you end up with GPU charges piling up while no video ever appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I decoupled chat collection from dialogue
&lt;/h3&gt;

&lt;p&gt;YouTube comments come from polling the Data API. Twitch comes over IRC. Tips and subs come from EventSub. &lt;strong&gt;Every source works differently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you wire all of that straight into the dialogue server, the dialogue logic gets dirtier every time you add a platform. So I put a dedicated collection component in front and &lt;strong&gt;normalized everything into a common internal format&lt;/strong&gt; before pushing it onto a Redis Stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YouTube chat    ┐
Twitch IRC      ├→ normalize → Redis Stream → Dialogue Server
Twitch EventSub ┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the dialogue server's point of view, there's exactly one input: a stream of events. No platform-specific knowledge has to live there.&lt;/p&gt;

&lt;p&gt;Moderation (banned words, throttling repeat spam from the same user) also lives on the collection side. &lt;strong&gt;Dropping the dirty stuff at the entrance&lt;/strong&gt; keeps every layer behind it simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Speech runs on a single serial loop
&lt;/h3&gt;

&lt;p&gt;Conversation during a stream behaves nothing like web request handling. &lt;strong&gt;You can't say two things at once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even when several comments arrive simultaneously, the avatar has one mouth. So speech is fully serialized, with a priority queue deciding the order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events like tips and subs → &lt;strong&gt;high&lt;/strong&gt; priority&lt;/li&gt;
&lt;li&gt;Regular comments → &lt;strong&gt;medium&lt;/strong&gt; priority&lt;/li&gt;
&lt;li&gt;Self-initiated topics during silence → &lt;strong&gt;low&lt;/strong&gt; priority&lt;/li&gt;
&lt;li&gt;The closing greeting at the end of the show → &lt;strong&gt;highest&lt;/strong&gt; priority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When comments flood in, there's also logic to batch several of them into a single utterance. Answering one at a time makes it look, from the viewer's side, like the avatar is still stuck on a comment from ages ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. State lives in Redis, and the page is disposable
&lt;/h3&gt;

&lt;p&gt;Renderers go down. GPU hosts get flaky, browsers crash, ffmpeg dies.&lt;/p&gt;

&lt;p&gt;So I designed it so that &lt;strong&gt;the stream's state (what has been said, who it talked to) never lives in the page&lt;/strong&gt;. State sits in a Redis snapshot, and the page is a &lt;strong&gt;display-only client&lt;/strong&gt; that connects and receives the current state.&lt;/p&gt;

&lt;p&gt;That means if the page dies, reloading and reconnecting picks up right where it left off. It also avoids the dumb failure mode of &lt;strong&gt;re-doing the opening greeting on every reconnect&lt;/strong&gt; — the snapshot already says "greeting done."&lt;/p&gt;

&lt;h3&gt;
  
  
  5. I physically separated video generation from dialogue
&lt;/h3&gt;

&lt;p&gt;The renderer runs on a different Pod, a different cloud, a different GPU. Its whole job is: open a page, capture the screen and audio, push it to RTMP.&lt;/p&gt;

&lt;p&gt;That separation gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The renderer is &lt;strong&gt;disposable&lt;/strong&gt; (bad host? re-roll it)&lt;/li&gt;
&lt;li&gt;The dialogue server can sit close to our own inference infrastructure (the LLM and TTS live there)&lt;/li&gt;
&lt;li&gt;Co-tenancy falls out naturally (several streams on a single GPU)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Put the fragile parts where breaking doesn't hurt.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured numbers
&lt;/h2&gt;

&lt;p&gt;For reference, a few numbers measured during verification.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod creation → live transition&lt;/td&gt;
&lt;td&gt;95 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent streams per GPU&lt;/td&gt;
&lt;td&gt;4 avatars (720p30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per avatar&lt;/td&gt;
&lt;td&gt;¥7,600/month (24h) / ¥2,500/month (8h per day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment response latency&lt;/td&gt;
&lt;td&gt;25–45 s (15–30 s of which is platform viewer delay)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic recovery from failure&lt;/td&gt;
&lt;td&gt;41 s (full recovery from a server restart)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous operation test&lt;/td&gt;
&lt;td&gt;2 hours (no memory growth, all 53 turns measured)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The recovery numbers came from actually deleting Pods and restarting servers. Verification included &lt;strong&gt;actually breaking things and confirming they heal.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest part was "ending reliably"
&lt;/h2&gt;

&lt;p&gt;The technically hardest part turned out to be neither the dialogue nor the rendering — it was &lt;strong&gt;ending reliably&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Failures to start are obvious immediately, because nobody can watch. Failures to &lt;em&gt;end&lt;/em&gt; announce themselves via the invoice, or via "wait, it's still streaming" the next morning. &lt;strong&gt;The scary thing about automation isn't failure — it's success that keeps going.&lt;/strong&gt; That's what hit home hardest.&lt;/p&gt;

&lt;p&gt;I ended up giving every component a condition along the lines of "if I decide I'm in a bad state, I stop."&lt;/p&gt;




&lt;p&gt;That's the architecture. From here I want to talk about how the work itself went. I did all of this alongside an AI agent, and what I noticed partway through was that &lt;strong&gt;the moments a human got called in fell into exactly three categories&lt;/strong&gt;. Nearly everything else closed out on the agent's side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent handled end to end
&lt;/h2&gt;

&lt;p&gt;First, the work no human touched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design and spec documentation&lt;/li&gt;
&lt;li&gt;Implementation (backend, scheduler, renderer, admin UI)&lt;/li&gt;
&lt;li&gt;Container builds and deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault injection&lt;/strong&gt; (killing Pods, restarting servers) and measuring recovery times&lt;/li&gt;
&lt;li&gt;Measurements (avatars per GPU, cost, latency breakdown)&lt;/li&gt;
&lt;li&gt;Log investigation and root-cause analysis&lt;/li&gt;
&lt;li&gt;Record-keeping and handoff notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measured numbers above, including the 41-second recovery, came out of that process. &lt;strong&gt;The range of what actually gets done is far wider than you'd expect.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What only a human could do, part 1: operations that demand proof of identity
&lt;/h2&gt;

&lt;p&gt;This was by far the largest bucket.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating accounts on streaming platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone-number identity verification&lt;/strong&gt; (and the 24-hour wait before live streaming is enabled)&lt;/li&gt;
&lt;li&gt;Setting up two-factor authentication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clicking the button on an OAuth consent screen&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Registering payment methods and adding credit on cloud services&lt;/li&gt;
&lt;li&gt;Registering developer applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What these share is that they all demand &lt;strong&gt;proof that you are you&lt;/strong&gt;. This isn't a matter of technical difficulty — it's a domain where delegation isn't supposed to be possible. If an agent &lt;em&gt;could&lt;/em&gt; do these on your behalf, that service's identity verification would be broken.&lt;/p&gt;

&lt;p&gt;The practically important part is that &lt;strong&gt;these create waiting&lt;/strong&gt;. Twenty-four hours from phone verification to activation isn't something code can shorten.&lt;/p&gt;

&lt;p&gt;During development, I &lt;strong&gt;kept a running homework list for the human&lt;/strong&gt;. Separate "what the agent can move on right now" from "what can't proceed until a human does it," and get the homework done first. Skip that and you'll finish the implementation only to sit through a 24-hour wait. And in fact, I punted on homework a few times and lost a full day to "waiting on authorization."&lt;/p&gt;

&lt;h2&gt;
  
  
  What only a human could do, part 2: quality judgments that require perception
&lt;/h2&gt;

&lt;p&gt;The second bucket: &lt;strong&gt;things you can only judge by looking and listening.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Judgment&lt;/th&gt;
&lt;th&gt;Why a machine can't settle it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setting lip-sync delay to 0.10 s&lt;/td&gt;
&lt;td&gt;"Looks in sync" is a perceptual question. There's no correct number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether render flicker is acceptable&lt;/td&gt;
&lt;td&gt;It shows up in neither fps, nor errors, nor GPU utilization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether a voice sounds natural&lt;/td&gt;
&lt;td&gt;Waveform metrics don't line up with subjective impressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The overall "watchable quality" of a stream&lt;/td&gt;
&lt;td&gt;A holistic call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most telling case was render flicker. &lt;strong&gt;Every performance metric stayed normal while the character's face strobed.&lt;/strong&gt; fps, errors, GPU utilization — not one of them indicated anything wrong. Changing a graphics setting fixed it, but the only reason anyone &lt;em&gt;noticed&lt;/em&gt; it was broken is that a human watched the video.&lt;/p&gt;

&lt;p&gt;That changed how I ran things: &lt;strong&gt;any change touching rendering or audio has to be confirmed by a human seeing and hearing the real thing before it's finalized.&lt;/strong&gt; This is less a limitation of AI capability than a property of the problem — &lt;strong&gt;the criterion for the judgment exists only inside human perception&lt;/strong&gt;. I wrote earlier that "the renderer is disposable," but the step that finally signs off on its quality stayed with human eyes and ears.&lt;/p&gt;

&lt;h2&gt;
  
  
  What only a human could do, part 3: the decision to publish
&lt;/h2&gt;

&lt;p&gt;The third bucket: decisions about going public.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether to keep a stream unlisted or make it public&lt;/li&gt;
&lt;li&gt;How to disclose that the content is AI-generated&lt;/li&gt;
&lt;li&gt;What the character is allowed to say&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technically, all of these can be executed at any time. Whether they &lt;em&gt;should&lt;/em&gt; be is a separate question. &lt;strong&gt;Judgments where accountability sits with a human&lt;/strong&gt; stay with the human — as a matter of authority, not capability.&lt;/p&gt;

&lt;p&gt;I drew the line from the start: verify with unlisted streams, and switch to public only on a human's call. The agent operates on the assumption of that line too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draw the boundary first, and development stops stalling
&lt;/h2&gt;

&lt;p&gt;To summarize, three things stayed with the human:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Operations that demand proof of identity&lt;/strong&gt; (accounts, authorization, payment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality judgments that require perception&lt;/strong&gt; (seeing, hearing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The decision to publish&lt;/strong&gt; (judgments that carry accountability)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Put the other way around: &lt;strong&gt;everything else runs on the agent's side.&lt;/strong&gt; Design, implementation, and deployment, plus fault injection, measurement, and root-cause analysis.&lt;/p&gt;

&lt;p&gt;The thing that paid off most in practice was &lt;strong&gt;identifying this boundary at the very start of the project&lt;/strong&gt;. Hand the "human homework" over early and there's no wait left when the implementation lands.&lt;/p&gt;

&lt;p&gt;The other thing I noticed is that all three share a property: &lt;strong&gt;you could do them on someone's behalf, but you shouldn't.&lt;/strong&gt; These aren't technical limits. That's exactly why I don't expect the boundary to move much as capabilities improve.&lt;/p&gt;

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

&lt;p&gt;On architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give stream lifespan management its own layer.&lt;/strong&gt; So a dialogue bug never becomes runaway billing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confine platform differences to the event collection layer.&lt;/strong&gt; Dialogue sees exactly one normalized stream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speech is serial + a priority queue.&lt;/strong&gt; There's only one mouth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep state outside (Redis) and make the page disposable.&lt;/strong&gt; It can crash and recover&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put the fragile part (the renderer) where breaking doesn't hurt&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The hardest thing about automated streaming is &lt;strong&gt;ending reliably&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design, implementation, deployment, &lt;strong&gt;fault injection, measurement, and root-cause analysis&lt;/strong&gt; all closed out on the agent's side&lt;/li&gt;
&lt;li&gt;Three things stayed with the human: &lt;strong&gt;identity-bound operations / perceptual quality judgments / the decision to publish&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Identity verification and authorization &lt;strong&gt;create waiting&lt;/strong&gt;. Hand them over as homework early or development stalls&lt;/li&gt;
&lt;li&gt;Rendering and audio quality can be &lt;strong&gt;broken while every performance metric reads normal&lt;/strong&gt;. Keep a step where a human sees and hears the real thing&lt;/li&gt;
&lt;li&gt;These three aren't capability limits — they're things &lt;strong&gt;that shouldn't be delegated&lt;/strong&gt;. Which is why they'll stick around&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unattended streaming lets you hand off almost every step to the machine. GPU selection, browser media APIs, the quirks of platform APIs, fault-tolerant design — each one has enough traps for its own article, and all of it got handled on the agent's side. What was left standing at the end was: being who you say you are, judging with your own eyes and ears, and carrying the responsibility of publishing. My conclusion is that &lt;strong&gt;rather than hunting for "what AI can't do," it's practically faster to decide up front what the human should do.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Running Claude Code in 4 Parallel Sessions Led to 'Team Development' — 7 Recipes to Prevent Collisions</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 27 Aug 2026 00:43:40 +0000</pubDate>
      <link>https://dev.to/orca_forge/running-claude-code-in-4-parallel-sessions-led-to-team-development-7-recipes-to-prevent-34fd</link>
      <guid>https://dev.to/orca_forge/running-claude-code-in-4-parallel-sessions-led-to-team-development-7-recipes-to-prevent-34fd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/parallel-claude-code-sessions-teamwork/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=parallel-claude-code-sessions-teamwork" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a &lt;a href="https://dev.to[[parallel-coding-agents-with-git-worktrees]]"&gt;previous article&lt;/a&gt;, we introduced an environment for parallel execution of coding agents using Git worktrees. This article is a follow-up. As we progressed with parallelization, we ended up with 3-5 Claude Code sessions &lt;strong&gt;simultaneously developing the same microservices&lt;/strong&gt;. What happened was no longer just "parallel execution of tools" but &lt;strong&gt;actual "team development"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All the issues that arise in human teams—miscommunication, deployment conflicts, and territorial overlaps—occur here as well. And the practices that work for human teams work almost identically here. We’ll share seven recipes that emerged from actual operations, along with real-life close calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Life Story: Averting a Deployment Rollback Disaster at the Last Minute
&lt;/h2&gt;

&lt;p&gt;One day, while Session A (responsible for voice functionality) was in the middle of a major refactor, Session B (responsible for streaming functionality) sent this message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We’re about to build the frontend as version 1.0.399 (based on main)."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first glance, this seemed fine. However, in this repository, &lt;strong&gt;the authoritative branch for the production environment was not main but a dedicated deployment branch&lt;/strong&gt;. The latest features from the past few dozen versions were only in the deployment branch, while main was outdated. If Session B had deployed an image based on main, &lt;strong&gt;weeks’ worth of features would have been rolled back in production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Session A immediately sent a warning, and Session B halted the build before pushing. Session B then cherry-picked their changes into the deployment branch and rebuilt the image, avoiding the disaster entirely. &lt;strong&gt;All this communication was handled autonomously between the agents via session-to-session messages&lt;/strong&gt;. I (the human) only learned about it later from the logs.&lt;/p&gt;

&lt;p&gt;This incident highlights two things: parallel agents can &lt;strong&gt;cause the same accidents as human teams&lt;/strong&gt;, and with proper communication channels and rules, they can &lt;strong&gt;prevent accidents just like human teams&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 1: Physically Separate Territories with Worktrees and Repositories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Assign a dedicated Git worktree to each session (as described in the previous article).&lt;/li&gt;
&lt;li&gt;Explicitly define which session is responsible for which service/directory. For example, the voice session handles &lt;code&gt;talk&lt;/code&gt;-related components, the streaming session handles &lt;code&gt;broadcast&lt;/code&gt;-related components, and the phone session handles only the &lt;code&gt;callgw&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;For shared worktrees, enforce the rule: &lt;strong&gt;"Always check the branch and &lt;code&gt;git status&lt;/code&gt; before committing."&lt;/strong&gt; This became a strict rule after a past incident where one session almost committed staged changes from another session in a shared tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recipe 2: Pre-Start Notification, Scope Declaration, and Completion Report
&lt;/h2&gt;

&lt;p&gt;This is equivalent to a human team’s daily stand-up. Agents exchange session-to-session messages (or use a shared notes file if messaging isn’t available) to communicate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before starting&lt;/strong&gt;: "I’ll be working on &lt;code&gt;expressive_voice.py&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt;. I won’t touch the prompt section in &lt;code&gt;talk_service.py&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upon completion&lt;/strong&gt;: "Base branch: X, commit: Y. Changes made in these two places. Image version 1.5.1072 reserved."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is &lt;strong&gt;declaring the scope&lt;/strong&gt;. By stating not only "what will be done" but also "what won’t be touched," other sessions can safely continue parallel work. In practice, this alone nearly eliminated file conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 3: Reserve Image Tags and Version Numbers
&lt;/h2&gt;

&lt;p&gt;Container image tags are shared namespace. If two sessions build different content with the same tag, say &lt;code&gt;1.0.399&lt;/code&gt;, it leads to &lt;strong&gt;tag overwrite&lt;/strong&gt;, a hard-to-detect issue (we once experienced a nightmare where a chained command error overwrote an old tag with new content, causing behavior to persist even after a rollback).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make it a habit to &lt;strong&gt;declare tag reservations&lt;/strong&gt; between sessions: "Next time we build the frontend, we’ll notify each other. If you use 399, we’ll use 400 or later."&lt;/li&gt;
&lt;li&gt;After building, &lt;strong&gt;verify the tag exists&lt;/strong&gt; in the registry via the API before deploying.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recipe 4: Document the Authoritative Branch and Share It with All Sessions
&lt;/h2&gt;

&lt;p&gt;The root cause of the incident mentioned earlier was the assumption that &lt;code&gt;main&lt;/code&gt; is the authoritative branch, which didn’t hold true for this repository.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clearly document the deployment base branch (e.g., &lt;code&gt;deploy/xxx&lt;/code&gt;) in the operational notes or memory accessible to the agents.&lt;/li&gt;
&lt;li&gt;During periods when &lt;code&gt;main&lt;/code&gt; and the authoritative branch diverge, &lt;strong&gt;explicitly prohibit builds based on &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Define the convergence (merge) timing as "after major changes stabilize."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recipe 5: Make the Version Ledger (Spec Repo) the Single Source of Truth
&lt;/h2&gt;

&lt;p&gt;Consolidate "what’s running in which environment" into a &lt;strong&gt;declarative repository&lt;/strong&gt; like Kubernetes manifests, and commit tag updates with each deployment. This ensures the ledger stays up-to-date regardless of which session deploys, allowing other sessions to check the current state without querying &lt;code&gt;kubectl&lt;/code&gt;. If you directly modify environment variables (e.g., &lt;code&gt;kubectl set env&lt;/code&gt;), &lt;strong&gt;always update the ledger&lt;/strong&gt;—drift can become a time bomb (we once had to sync the ledger after encountering this issue).&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 6: Avoid Crossing Permission Boundaries
&lt;/h2&gt;

&lt;p&gt;A unique risk in multi-session operations is &lt;strong&gt;permission circumvention&lt;/strong&gt;. If one session is denied an operation, they might ask another session to do it instead, bypassing user permissions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each session’s permissions are independent, and &lt;strong&gt;requests to other sessions must not be used to circumvent permissions&lt;/strong&gt;. Explicitly document this as an agent-side rule.&lt;/li&gt;
&lt;li&gt;For destructive operations (e.g., database writes, production deployments, billable actions), ensure they always pass through a human confirmation gate, regardless of the session initiating them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recipe 7: Leverage "Previous Sessions" as Knowledge Sources
&lt;/h2&gt;

&lt;p&gt;In multi-day development, the session that created feature X and the session modifying it are often different. While code and commit logs provide information, &lt;strong&gt;asking the original session (or its transcripts) is the fastest way&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, when we sent a question list to the session that authored the old implementation—asking about testing practices, pitfalls, and billing blind spots—we received operational knowledge not evident from the code (e.g., "This timeout was increased from 600 to 1500 due to a past incident," "This external monitoring URL is useful"). This saved a full day of investigation. If transcripts and memory persist, &lt;strong&gt;treat past sessions as "colleagues at the next desk," not "retired colleagues."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Agent Parallelization Becomes an Organizational Design Problem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Recipe&lt;/th&gt;
&lt;th&gt;Equivalent in Human Teams&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Separate worktrees and responsibilities&lt;/td&gt;
&lt;td&gt;Team division, code ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Pre-start notification, scope declaration&lt;/td&gt;
&lt;td&gt;Daily stand-ups, task declarations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Tag reservation&lt;/td&gt;
&lt;td&gt;Release number management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Document authoritative branch&lt;/td&gt;
&lt;td&gt;Shared branching strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Version ledger&lt;/td&gt;
&lt;td&gt;Release ledger, CMDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Permission boundaries&lt;/td&gt;
&lt;td&gt;Role definitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Consult previous sessions&lt;/td&gt;
&lt;td&gt;Handovers, pair programming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We’re moving from an era of optimizing single-session productivity to an era of &lt;strong&gt;designing multi-session coordination for overall productivity&lt;/strong&gt;. And the blueprint for this design is remarkably similar to what software engineering has refined for human teams over decades. Before assigning roles or personalities to agents, focus on &lt;strong&gt;territories, communication, and ledgers&lt;/strong&gt;. It’s mundane, but it dramatically reduces parallel operation accidents.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>git</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Latest Trends in GPU Cloud Cost Reduction and Containerized Data Centers</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 27 Aug 2026 00:12:38 +0000</pubDate>
      <link>https://dev.to/orca_forge/latest-trends-in-gpu-cloud-cost-reduction-and-containerized-data-centers-14k0</link>
      <guid>https://dev.to/orca_forge/latest-trends-in-gpu-cloud-cost-reduction-and-containerized-data-centers-14k0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/gpu-cloud-cost-reduction-trends/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=gpu-cloud-cost-reduction-trends" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In recent years, the demand for GPUs has surged with the widespread adoption of AI and machine learning. Particularly, the demand for the latest NVIDIA GPUs is notable, leading to an increase in GPU cloud services offered by cloud providers. Among these developments, containerized data centers are gaining attention. This article explores cost reduction in GPU cloud services and the latest trends in containerized data centers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Content
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Benefits of Containerized Data Centers
&lt;/h3&gt;

&lt;p&gt;Containerized data centers offer the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rapid Deployment&lt;/strong&gt;: Compared to traditional building-based data centers, they can be deployed much faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suitability for Regional Locations&lt;/strong&gt;: They can be utilized as locally-focused infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expanded Options&lt;/strong&gt;: Alongside traditional data centers, containerized options are increasingly being offered as an alternative.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Efforts to Reduce GPU Power Consumption
&lt;/h3&gt;

&lt;p&gt;GPU power consumption significantly impacts data center operational costs. Recent efforts include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Power Consumption Reduction&lt;/strong&gt;: Proof-of-concept experiments have reported up to 80% reduction in power consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualization Technology&lt;/strong&gt;: Combined with locally-focused containerized data centers, next-generation infrastructure solutions are being explored.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Trends in Japan
&lt;/h3&gt;

&lt;p&gt;In Japan, the following trends are observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Provider Entry&lt;/strong&gt;: Multiple cloud providers have launched GPU cloud services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption of Latest GPUs&lt;/strong&gt;: Services are being deployed with early adoption of NVIDIA's latest GPUs (e.g., H200).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power Issue Solutions&lt;/strong&gt;: High-efficiency conversion, high-density, and containerization are proposed as solutions to power-related challenges.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With the growing demand for GPU cloud services, containerized data centers are attracting significant attention. Their benefits, including rapid deployment and reduced power consumption, are diverse. In Japan, services adopting the latest GPUs are emerging successively, drawing attention to future developments. For companies aiming for cost reduction and efficient infrastructure, containerized data centers present a compelling option.&lt;/p&gt;

</description>
      <category>gpu</category>
    </item>
    <item>
      <title>Implementing a Free LLM API Without a Credit Card — Understanding Rate Limits and Fallback Design</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 26 Aug 2026 01:09:01 +0000</pubDate>
      <link>https://dev.to/orca_forge/implementing-a-free-llm-api-without-a-credit-card-understanding-rate-limits-and-fallback-design-322b</link>
      <guid>https://dev.to/orca_forge/implementing-a-free-llm-api-without-a-credit-card-understanding-rate-limits-and-fallback-design-322b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/free-llm-api-rate-limits-fallback/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=free-llm-api-rate-limits-fallback" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you want to use an LLM for a personal project or a prototype, the first obstacle usually isn't technical — it's registering payment information. You just want to try something out, but you're asked for a credit card; you'd rather not put it on the company card; you're nervous that usage-based billing will blow up on you. It's a shame to stall out for reasons like that.&lt;/p&gt;

&lt;p&gt;Fortunately, as of 2026, LLM APIs with free tiers that require no credit card are no longer rare. But if you stop at "apparently there's a free tier," the moment you actually run something you'll get smacked with &lt;code&gt;429 Too Many Requests&lt;/code&gt; and that's the end of it. This article covers how to read rate limits and how to design a fallback across multiple providers, so you can use free tiers &lt;strong&gt;in a way that holds up in real use&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic sense of how far a free tier gets you
&lt;/h2&gt;

&lt;p&gt;Let me give the conclusion up front: for personal experiments, prototypes, and internal tools, free tiers alone are plenty. On the other hand, supporting the backend of a publicly available service on free tiers alone isn't realistic. A free tier isn't a "cheap plan" — it's a favor that can change without notice.&lt;/p&gt;

&lt;p&gt;With that premise in place, here's what free tiers are well suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating ideas and iterating on prompts&lt;/li&gt;
&lt;li&gt;Personal automation scripts, scheduled batch jobs, summarization pipelines&lt;/li&gt;
&lt;li&gt;Backends for apps under development (pre-production)&lt;/li&gt;
&lt;li&gt;Internal tools where low frequency and asynchronous processing are fine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conversely, if even one of the following applies — you need a latency SLA, you handle confidential data, or you're looking at hundreds of thousands of requests a month — your total cost will be lower if you just consider a paid plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The axis for comparing providers isn't "which models"
&lt;/h2&gt;

&lt;p&gt;Most comparison articles about free LLM APIs stop at "here's the list of available models," but what actually matters in production is these five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The structure of the rate limits&lt;/strong&gt; (more on this below — it's not a single number)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether the API is OpenAI-compatible&lt;/strong&gt; — if it is, swapping providers takes a few lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data handling&lt;/strong&gt; — free tiers sometimes have terms that allow your input data to be used for training&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How often models get swapped out&lt;/strong&gt; — free-tier models tend to be discontinued or replaced without notice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world availability&lt;/strong&gt; — free tiers are the first thing throttled when things get busy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Number 2 in particular feeds directly into your design. Most of the major free providers offer OpenAI-compatible endpoints, so simply standardizing your client on the compatible interface reduces the fallback described below to "swap the base URL, API key, and model name." Whether you do this up front changes your later workload by an order of magnitude.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the major no-credit-card providers look like
&lt;/h2&gt;

&lt;p&gt;At the time of research, the representative options you can use without registering a credit card look roughly like this (specific limit values change frequently, so always confirm with the official documentation).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-speed inference services&lt;/td&gt;
&lt;td&gt;Custom hardware makes inference extremely fast. Mostly open-weight models, with relatively generous tokens-per-minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI APIs from major clouds&lt;/td&gt;
&lt;td&gt;Free tiers are available, with broad functionality including multimodal support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference services from GPU vendors&lt;/td&gt;
&lt;td&gt;Host a large number of open models. Good for evaluation work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model aggregators / routers&lt;/td&gt;
&lt;td&gt;One key gets you access to many models, including free-tier ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free API gateways&lt;/td&gt;
&lt;td&gt;OpenAI-compatible, bundling multiple models behind one interface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Beyond these, there are several directory-style sites and repositories that collect and catalog free LLM APIs, with over 200 endpoints listed. That said, this kind of list &lt;strong&gt;lives or dies on freshness&lt;/strong&gt;. Entries that are listed but already shut down, or whose free tier has gone paid, are an everyday occurrence — so treat these lists as an entry point for discovering candidates, and always make the adoption decision based on primary sources.&lt;/p&gt;

&lt;p&gt;The important thing is &lt;strong&gt;not to pick a single provider, but to have two or three ready at the same time&lt;/strong&gt;. The reason leads into the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits aren't "one number"
&lt;/h2&gt;

&lt;p&gt;Articles introducing free tiers tend to emphasize a single number like "up to 30,000 tokens per minute free," but real rate limits are usually a &lt;strong&gt;logical AND&lt;/strong&gt; across several axes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RPM&lt;/strong&gt; (requests per minute) — requests per minute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TPM&lt;/strong&gt; (tokens per minute) — tokens per minute (usually input + output combined)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RPD / TPD&lt;/strong&gt; — daily caps. These are often the effective ceiling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; — how many requests can run in parallel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-model limits&lt;/strong&gt; — bigger models get stricter limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even if a provider advertises "30,000 tokens per minute," a low RPM means you'll hit the RPM wall first if your workload throws lots of short requests. Conversely, for something like long-document summarization, TPM binds first. &lt;strong&gt;Estimating up front which axis your workload will hit&lt;/strong&gt; is the first trick to using free tiers well.&lt;/p&gt;

&lt;p&gt;Another thing that's easy to overlook is &lt;strong&gt;when the daily limit resets&lt;/strong&gt;. Resets are often based on UTC, which leads to accidents like hitting the cap in the morning Japan time and not recovering until the evening. When you build batch jobs, schedule them with the reset time in mind.&lt;/p&gt;

&lt;p&gt;And when you get a 429, don't retry based on guesswork — read the &lt;code&gt;Retry-After&lt;/code&gt; header and the &lt;code&gt;x-ratelimit-*&lt;/code&gt; response headers. Many providers return your remaining quota and the seconds until reset, and just using those makes your retry behavior dramatically more accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to build a fallback setup
&lt;/h2&gt;

&lt;p&gt;This is the main event for making free tiers practical. If you depend on a single provider, any one of rate limiting, an outage, or a model shutdown will stop you cold. Line up multiple providers and route to the next one on failure, and you can raise availability to a practical level while staying on free tiers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design principles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assign priorities&lt;/strong&gt;: decide your first, second, and third string based on the balance of speed, quality, and limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line up comparable models&lt;/strong&gt;: if your fallback target is drastically weaker, output quality collapses the moment you fail over&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish fallback triggers&lt;/strong&gt;: 429 (rate limit) is worth retrying, 401 (auth) should move to the next provider immediately, 5xx should be retried after a short backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff + jitter&lt;/strong&gt;: fixed-interval retries mean you punch yourself in the face when many failures happen at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share one prompt&lt;/strong&gt;: branching prompts per provider destroys maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A minimal example
&lt;/h3&gt;

&lt;p&gt;The point is to convey the idea, so written plainly it looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# OpenAI互換エンドポイントを優先度順に並べる
&lt;/span&gt;&lt;span class="n"&gt;PROVIDERS&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.provider-a.example/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.provider-b.example/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.provider-c.example/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROVIDERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&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;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# 設定の問題。リトライしても無駄なので次のプロバイダへ
&lt;/span&gt;                &lt;span class="c1"&gt;# 429 / 5xx: 指数バックオフ + ジッタ
&lt;/span&gt;                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;all providers failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&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;This works well enough, but once you have more providers, it's more realistic to &lt;strong&gt;put a routing library or gateway in front&lt;/strong&gt;. Boilerplate like fallback, retries, cost tracking, and model-name normalization can be declared in a config file, and your application code just points at a single endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Differences in context length&lt;/strong&gt;: if your fallback target is shorter, long inputs will always fail. Truncate inputs to the minimum across providers (not the "least common multiple"), or filter providers when handling long documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Differences in features&lt;/strong&gt;: support for function calling (tool use), JSON mode, and streaming isn't uniform across models and providers. Only include combinations that support the features you use as fallback candidates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite retries&lt;/strong&gt;: unless you design for giving up quickly when every provider is exhausted, the caller will hang until it times out&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips for not burning through your free tier
&lt;/h2&gt;

&lt;p&gt;Fallback is about what happens &lt;em&gt;after&lt;/em&gt; you hit a limit, but making it harder to hit the limit in the first place is more effective.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt;: just caching responses locally for identical inputs dramatically cuts consumption during development. Hashing the prompt as the key is enough&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier your models&lt;/strong&gt;: send simple tasks like classification, extraction, and formatting to smaller models, and use the big model only where generation quality matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim your prompts&lt;/strong&gt;: free tiers consume input tokens too. Redundant few-shot examples and context you left pasted in are limit consumption, plain and simple&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch and go async&lt;/strong&gt;: move work that doesn't need to be real-time into daily batches and run it in the quiet gaps between limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt;: log token consumption and error types. If you don't know which axis you're getting stuck on, you can't improve anything&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things to watch out for
&lt;/h2&gt;

&lt;p&gt;Finally, here are the practical risks of relying on free tiers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data handling&lt;/strong&gt;: free-tier terms sometimes allow your input data to be used for model improvement. If you're going to send personal or confidential information through, reading the terms is mandatory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial use&lt;/strong&gt;: some free tiers permit commercial use, some don't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SLA&lt;/strong&gt;: free tiers come with no availability guarantee. Build on the assumption that you have no grounds to complain when it goes down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid spec changes&lt;/strong&gt;: both limit values and model lineups change within months. Don't hardcode numbers — push them out into configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key management&lt;/strong&gt;: don't get lax just because it's free; keep keys in environment variables or a secret manager. Leaking them into a repository is a common accident&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As of 2026, there are plenty of no-credit-card free LLM API options. But whether you can extract value from them depends less on which provider you choose and more on &lt;strong&gt;your design&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free-tier limits aren't a single number — they're a combination of RPM, TPM, daily caps, and concurrency. Estimate up front which axis your workload will hit&lt;/li&gt;
&lt;li&gt;Standardize your client on the OpenAI-compatible interface and build a fallback across two or three providers in priority order. That alone gets availability to a practical level&lt;/li&gt;
&lt;li&gt;Handle 429 with exponential backoff plus jitter, and move on immediately for auth errors. Make use of the remaining-quota information in the response headers&lt;/li&gt;
&lt;li&gt;Use caching, model tiering, and prompt trimming to make hitting the limit less likely in the first place&lt;/li&gt;
&lt;li&gt;Use free tiers with the data-usage terms and the absence of an SLA accepted up front&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build past "it's free to use" and all the way to "it stays up while staying free," the personal-project experience gets remarkably comfortable. Start by moving the script you have on hand over to the compatible interface.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Practical Techniques to Improve Faster-Whisper Recognition Accuracy — Use Recent Conversations, Not Dictionaries, for initial_prompt</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:37:53 +0000</pubDate>
      <link>https://dev.to/orca_forge/practical-techniques-to-improve-faster-whisper-recognition-accuracy-use-recent-conversations-not-35lh</link>
      <guid>https://dev.to/orca_forge/practical-techniques-to-improve-faster-whisper-recognition-accuracy-use-recent-conversations-not-35lh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/faster-whisper-accuracy-techniques/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=faster-whisper-accuracy-techniques" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Techniques That Actually Improved Accuracy When Running Faster-Whisper as STT for Real-Time Voice AI (Avatars/Voicebots)
&lt;/h2&gt;

&lt;p&gt;While running &lt;strong&gt;faster-whisper&lt;/strong&gt; as the STT component in a real-time voice interaction system (avatar/voicebot), I’ve compiled practical accuracy-improvement techniques that showed real-world effectiveness. These are not batch-style single-shot transcriptions, but insights specifically for &lt;strong&gt;integrating into conversational systems&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;initial_prompt&lt;/code&gt; — The Most Effective Yet Most Misused Feature
&lt;/h2&gt;

&lt;p&gt;Whisper’s &lt;code&gt;initial_prompt&lt;/code&gt; acts as a &lt;strong&gt;lexical bias&lt;/strong&gt; for the decoder by providing the "transcription that came just before this audio." When speech is ambiguous, the model tends to "fall back" to spellings present in the prompt. This is the most powerful countermeasure against misrecognizing proper nouns (e.g., "社員数" → "シャインズ").&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Mistake: Passing a Dictionary of Proper Nouns
&lt;/h3&gt;

&lt;p&gt;It’s tempting to think: &lt;em&gt;"Just pass a list of company names and product names."&lt;/em&gt; But in real-world operation, this approach breaks down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary varies per tenant (customer company)&lt;/strong&gt; — the dictionary grows endlessly. Even with 20–30 entries, it can’t cover all in-field proper nouns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ongoing operational cost&lt;/strong&gt; for registration and updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long prompts have side effects&lt;/strong&gt; (explained later).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Actually Worked: Pass the Avatar’s Most Recent Utterance
&lt;/h3&gt;

&lt;p&gt;In a conversational system, there’s a better vocabulary source than a dictionary: &lt;strong&gt;the text of your own avatar’s recent spoken output via TTS&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scripts and responses already contain proper nouns in &lt;strong&gt;correct spelling&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;As the conversation progresses, vocabulary relevant to the current topic is automatically included in the hint. &lt;strong&gt;Zero registration effort.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Implementation is simple: maintain a &lt;code&gt;deque&lt;/code&gt; of recent utterances and pass the last N characters.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SttHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;
    &lt;span class="k"&gt;def&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;self&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="c1"&gt;# Only add avatar speech
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_max&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ja&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;initial_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Two Critical Warnings
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never include user-side STT results in the prompt.&lt;/strong&gt; Once a misrecognition sneaks in, subsequent recognition gets pulled into that error and &lt;strong&gt;becomes fixed&lt;/strong&gt;. Only include text whose spelling is guaranteed — your own utterances or scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep length between 150–200 characters.&lt;/strong&gt; Longer prompts can cause whisper to &lt;strong&gt;hallucinate the prompt itself as heard audio&lt;/strong&gt;, creating false positives.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Input Audio Quality — Half the Battle Is Won Before STT
&lt;/h2&gt;

&lt;p&gt;In systems receiving microphone audio via WebRTC, the default Opus bitrate (~30 kbps) quietly matters. When migrating from an uncompressed PCM-over-WebSocket setup to WebRTC, we saw increased misrecognition in edge cases (e.g., overlapping speech).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;strong&gt;answer SDP&lt;/strong&gt;, specify &lt;code&gt;maxaveragebitrate=128000;useinbandfec=1&lt;/code&gt; to control the sender’s encoder.&lt;/li&gt;
&lt;li&gt;In-band FEC also helps with packet loss over TURN relays.&lt;/li&gt;
&lt;li&gt;In systems using AEC (Acoustic Echo Cancellation), be aware that &lt;strong&gt;near-end suppression during double-talk can distort word onsets&lt;/strong&gt;. This cannot be fixed at the STT stage, so combine this section’s improvements with Section 1’s hints and Section 5’s LLM correction.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Cutting Off Low-Confidence Results (&lt;code&gt;avg_logprob&lt;/code&gt;) Is Dangerous
&lt;/h2&gt;

&lt;p&gt;The intuitive idea — &lt;em&gt;"Discard low-confidence results to reduce noise"&lt;/em&gt; — was invalidated by real measurements. &lt;strong&gt;Even genuine speech often has &lt;code&gt;avg_logprob&lt;/code&gt; as low as -0.8.&lt;/strong&gt; Setting a threshold causes valid utterances to be dropped, leading to the worst symptom: &lt;strong&gt;silent non-response&lt;/strong&gt; (invisible to users).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not apply thresholding.&lt;/strong&gt; Instead, &lt;strong&gt;log the confidence continuously&lt;/strong&gt; — it’s invaluable for diagnostics.&lt;/li&gt;
&lt;li&gt;Reject noise not by confidence, but via &lt;strong&gt;contextual judgment in downstream LLM/app logic&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Pathologies of Non-Speech Input — Silence and Pure Tones Are "Slow"
&lt;/h2&gt;

&lt;p&gt;Feeding sine waves or silence into whisper causes it to repeatedly fall back to temperature sampling, resulting in &lt;strong&gt;5–8 seconds for a 3-second input&lt;/strong&gt; (measured). Real speech in the same environment completes in under 1 second.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using sine waves or silence for health checks or warm-ups skews latency measurements. &lt;strong&gt;Use real speech samples.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Conversely, if you observe such slowness in production, it’s a sign that &lt;strong&gt;non-speech is leaking in&lt;/strong&gt; — check VAD thresholds and microphone paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s worth enabling &lt;code&gt;vad_filter=True&lt;/code&gt; (with Silero VAD) to remove silent segments. This improves both speed and reduces hallucinations.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Don’t Rely on STT Alone — LLM Post-Correction Is the Final Line of Defense
&lt;/h2&gt;

&lt;p&gt;No matter how well you tune STT, certain errors — like misheard proper nouns during double-talk — will slip through. In conversational systems, the most cost-effective move is to &lt;strong&gt;tell the LLM that the input is an STT transcript&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Interpretation of Misheard Input]
- Input is a speech recognition transcript and may contain phonetically similar mishearings (e.g., "社員数" → "シャインズ")
- If a word feels unnatural in context, search reference material or recent conversation for a phonetically close word and interpret accordingly.
- Begin response with: "Regarding your question about ◯◯..."
- If no phonetically close word is found and a neologism appears, do not fabricate an explanation. Instead, ask for clarification.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line is crucial. Without it, the LLM will confidently invent product descriptions for unknown words like "シャインズ" (observed in real systems). Think of it as explicitly granting the model the same contextual reading ability humans use when parsing garbled chat input.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Operational Trap: "False Degradation" Right After Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Faster-Whisper models often use &lt;strong&gt;lazy loading&lt;/strong&gt;, adding tens of seconds of load time on the first request (especially on GPU).&lt;/li&gt;
&lt;li&gt;If co-located processes (e.g., TTS pre-warming) monopolize GPU or event loops, &lt;code&gt;/transcribe&lt;/code&gt; responses can be delayed by tens of seconds — indistinguishable from code regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; After deployment, verify load completion logs before measuring performance. Embed warm-up confirmation in E2E tests. &lt;strong&gt;"Failures right after release" should first suspect environment issues.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Use avatar’s recent utterance as &lt;code&gt;initial_prompt&lt;/code&gt; (no dictionary)&lt;/td&gt;
&lt;td&gt;Most effective against proper noun errors&lt;/td&gt;
&lt;td&gt;Tens of lines of code, zero operational overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Never include user STT results in prompt&lt;/td&gt;
&lt;td&gt;Prevents error fixation&lt;/td&gt;
&lt;td&gt;Design decision only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 128kbps + in-band FEC&lt;/td&gt;
&lt;td&gt;Improves edge-case robustness&lt;/td&gt;
&lt;td&gt;A few lines in SDP munging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avoid confidence-based filtering&lt;/td&gt;
&lt;td&gt;Prevents silent non-response&lt;/td&gt;
&lt;td&gt;Actually, remove it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable &lt;code&gt;vad_filter&lt;/code&gt; + warm-up/measure with real speech&lt;/td&gt;
&lt;td&gt;Improves speed and reduces hallucinations&lt;/td&gt;
&lt;td&gt;Configuration only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tell LLM input is STT + ask for clarification on neologisms&lt;/td&gt;
&lt;td&gt;Final correction of misheard terms&lt;/td&gt;
&lt;td&gt;A few lines in prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rather than chasing STT-only benchmarks, &lt;strong&gt;optimize the entire pipeline&lt;/strong&gt; — from input quality → hinting → recognition → LLM correction — to "sandwich" errors out of existence. This approach delivers noticeable accuracy gains faster.&lt;/p&gt;

</description>
      <category>fasterwhisper</category>
      <category>whisper</category>
      <category>stt</category>
      <category>ai</category>
    </item>
    <item>
      <title>Browser Voice Interaction AI Pitfall Guide 2026 — 16 Common Traps with AEC, getUserMedia, and Headless Modes</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:07:18 +0000</pubDate>
      <link>https://dev.to/orca_forge/browser-voice-interaction-ai-pitfall-guide-2026-16-common-traps-with-aec-getusermedia-and-40hd</link>
      <guid>https://dev.to/orca_forge/browser-voice-interaction-ai-pitfall-guide-2026-16-common-traps-with-aec-getusermedia-and-40hd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/browser-voice-ai-pitfalls-2026/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=browser-voice-ai-pitfalls-2026" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When building voice-based AI interactions in the browser (avatars, voice bots, streaming AI), you’ll inevitably hit pitfalls stemming from audio physics and browser implementation quirks. This article compiles &lt;strong&gt;16 traps I encountered during product development&lt;/strong&gt;, organized in a &lt;strong&gt;symptom → cause → solution lookup format&lt;/strong&gt;. No need to read from top to bottom—jump straight to the symptom you’re facing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Echo and Self-Response Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Avatar Responds to Its Own Voice (Despite &lt;code&gt;echoCancellation: true&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: TTS audio is picked up by the mic, and STT recognizes it as user speech, creating a self-response loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: AEC (Acoustic Echo Cancellation) requires a &lt;strong&gt;reference signal&lt;/strong&gt; (the "sound to cancel"). Only the browser's official playback paths (&lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; / WebRTC receiver tracks) serve as references. &lt;strong&gt;Custom playback via Web Audio API does not reliably function as a reference&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Return TTS audio from the server as a &lt;strong&gt;WebRTC remote track&lt;/strong&gt; and play it via an &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element. This eliminates echoes without text-matching workarounds (tested: 99 seconds of continuous speech with speakers on, zero false user turn detections).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Echoes Are Gone, but Speaking Simultaneously with the Avatar Distorts My Voice and Causes Misrecognition
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Only during dual speech, proper nouns get mangled (e.g., "社員数" → "シャインズ"), especially at word beginnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Fundamental AEC trade-off. To cancel echoes, AEC suppresses/distorts near-end (user) audio during dual speech.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Mitigate in three layers:
① Increase mic Opus bitrate and enable FEC (see Pitfall 13)
② Provide vocabulary hints to STT (see separate article: use "recent avatar speech" as &lt;code&gt;initial_prompt&lt;/code&gt;, not a dictionary)
③ Instruct LLM: "Input is STT transcription with potential errors. Interpret unnatural words as phonetically similar terms and add confirmation prompts."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Can’t Suppress Audio from Other Apps (Music, Videos)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Audio/lyrics from a YouTube video opened by the agent keep getting transcribed by STT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Browser AEC can only reference audio &lt;strong&gt;played by the same tab/app&lt;/strong&gt;. Audio from other processes is indistinguishable from human speech to the mic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: No technical silver bullet. Combine OS speaker separation (e.g., macOS "Voice Isolation"; request via &lt;code&gt;voiceIsolation: true&lt;/code&gt;—ignored on unsupported systems), headphones, and downstream noise rejection (LLM/tool-based filtering).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "Only the Beginning Is Unheard" Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4. Initial Speech at Session Start Isn’t Recognized
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: First 10–20 seconds of speech go unanswered. Works normally afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Two factors:
(a) &lt;strong&gt;AEC convergence&lt;/strong&gt;—AEC only learns during actual playback, so unconverged dual speech suppresses the user's voice.
(b) &lt;code&gt;autoGainControl&lt;/code&gt; ramp-up—Gain increases gradually, making initial speech too quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;:
(a) Play short greetings/sound effects &lt;strong&gt;before the main session&lt;/strong&gt; to train AEC (loading screens are perfect for this).
(b) Set &lt;code&gt;autoGainControl: false&lt;/code&gt;. If STT is server-side, volume fluctuations are handled well, and disabling AGC has minimal downsides. &lt;strong&gt;Never disable &lt;code&gt;echoCancellation&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Can’t Diagnose "No Response" Issues
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Unclear whether the issue is mic off, preprocessing loss, or server-side—leads to guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Add two observability points:
① &lt;strong&gt;Server-side logging of input audio energy/probability&lt;/strong&gt; (10Hz. Distinguishes "complete silence" from "distorted audio").
② &lt;strong&gt;Level meter in UI for the actual transmitted stream&lt;/strong&gt; (&lt;code&gt;createMediaStreamSource&lt;/code&gt; to read the same stream as transmission—doesn’t affect transmission).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Starting Mic Track with &lt;code&gt;enabled=false&lt;/code&gt; Causes "No Response" Complaints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Implementation follows spec ("mic off until button press"), but users expect "always listening."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Not a bug—&lt;strong&gt;mismatch between design contract and user experience expectations&lt;/strong&gt;. A silent track looks indistinguishable from "quiet room" to the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Align defaults with product promises. If "always listening" is a selling point, auto-enable on initialization completion (not &lt;strong&gt;during&lt;/strong&gt; connection—that breaks mid-init conversations). Keep the button as a mute toggle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chromium Implementation Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7. Remote Audio Connected to WebAudio Remains Silent (Lip Sync Doesn’t Work)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: WebRTC receiver stream connected to &lt;code&gt;createMediaStreamSource&lt;/code&gt; for analysis/processing, but no data flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: &lt;strong&gt;Chromium doesn’t send remote &lt;code&gt;MediaStream&lt;/code&gt; to WebAudio until a media element starts consuming it&lt;/strong&gt; (long-standing behavior).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Attach &lt;code&gt;srcObject&lt;/code&gt; to a &lt;code&gt;muted&lt;/code&gt; &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element and call &lt;code&gt;play()&lt;/code&gt;. The goal is to trigger playback, not produce sound.
&lt;/li&gt;
&lt;/ul&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;a&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;Audio&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remoteStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&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;audioElRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Prevent GC by holding reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. &lt;code&gt;MediaStreamAudioSourceNode&lt;/code&gt; Silently Dies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Input to processing graph stops after working for a while. No errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Chrome may &lt;strong&gt;garbage collect unreferenced nodes&lt;/strong&gt;, silently stopping input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Always retain references to nodes (and the above &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element) via &lt;code&gt;ref&lt;/code&gt; or similar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  9. Volume Slider Doesn’t Work (New Path Only)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: &lt;code&gt;GainNode&lt;/code&gt; worked for local audio but stops working after switching to remote track playback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; playback bypasses Web Audio GainNodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Manipulate &lt;code&gt;audioElement.volume&lt;/code&gt; / &lt;code&gt;.muted&lt;/code&gt;. During transition periods, apply changes to both paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Headless Environment (Streaming/Automation) Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  10. Headless Chromium Connection Dies at Startup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Server never receives connection offer (&lt;code&gt;offer&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Initial &lt;code&gt;getUserMedia&lt;/code&gt; throws an exception in headless environments (no mic).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: For mic-less use cases (streaming renderers), &lt;strong&gt;send a synthesized silent track via WebAudio&lt;/strong&gt;. SDP and server pipelines behave identically to real mics.
&lt;/li&gt;
&lt;/ul&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;ctx&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamDestination&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;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConstantSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&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;// Continuously stream "silent" samples (see Pitfall 11)&lt;/span&gt;
&lt;span class="nx"&gt;keep&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;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;keep&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;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTrack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dest&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="nf"&gt;getAudioTracks&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="nx"&gt;dest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  11. &lt;code&gt;MediaStreamAudioDestinationNode&lt;/code&gt; Without Input May Stop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Silent track connection works initially but RTP stops after a while, causing disconnection. If connected to &lt;code&gt;MediaRecorder&lt;/code&gt;, the muxer halts, stopping video too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Some implementations stop frame generation for destinations without input sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: As above, connect a &lt;code&gt;ConstantSource(0)&lt;/code&gt; to keep rendering active.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  12. Autoplay Policy Blocks Playback and AudioContext Startup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: No sound in headless mode / &lt;code&gt;AudioContext&lt;/code&gt; remains &lt;code&gt;suspended&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Launch flag &lt;code&gt;--autoplay-policy=no-user-gesture-required&lt;/code&gt; + &lt;code&gt;ctx.resume()&lt;/code&gt;. In regular browsers, always include a user interaction-triggered &lt;code&gt;resume()&lt;/code&gt; to "unlock" audio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quality and Tuning Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  13. Default WebRTC Mic Bitrate Is Surprisingly Low
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Switching from WS+raw PCM to WebRTC (Opus) reduced STT accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Default Opus bitrate is ~30kbps. Lossy compression artifacts become critical under limiting conditions like dual speech.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Modify &lt;code&gt;answer&lt;/code&gt; SDP &lt;code&gt;fmtp&lt;/code&gt; line to &lt;code&gt;maxaveragebitrate=128000;useinbandfec=1&lt;/code&gt; (answer-side &lt;code&gt;fmtp&lt;/code&gt; controls sender encoder). FEC also helps with packet loss over TURN.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  14. VAD Silence Wait Dominates Response Latency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Perceived 2-second response delay. Profiling shows neither LLM nor TTS is the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Silence wait for end-of-speech detection (&lt;code&gt;stop_secs&lt;/code&gt;) accounts for &amp;gt;1 second. Shortening this causes mid-sentence cuts (another failure mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: No silver bullet. Run STT concurrently during speech to preempt finalization, use turn detection models, and &lt;strong&gt;prioritize barge-in speed&lt;/strong&gt; (instant interruption) for better perception.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  15. "False Regression" in STT/TTS Latency Right After Deployment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Post-release tests fail. Wasted time debugging code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Model lazy loading/prewarming (e.g., serial TTS speaker model loads) monopolizes GPU/event loop, causing inference APIs to wait tens of seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Wait for prewarming completion before testing (check log counts for "load start" vs "complete"). Integrate warmup checks into E2E harnesses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  16. Server Pipeline Idle Timeouts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: Sessions auto-terminate after 5 minutes of silence in valid use cases (streaming, monitoring).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause&lt;/strong&gt;: Default pipeline framework timeouts (e.g., Pipecat's 300s idle timeout) assume conversational use. "5 minutes inactive = abandoned" logic triggers self-cancellation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Separate settings by workload (e.g., &lt;code&gt;cancel_on_idle_timeout=False&lt;/code&gt; for streaming routes). &lt;strong&gt;Treat framework defaults as potential hazards&lt;/strong&gt; when use cases shift.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Checklist (Save for Later)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] TTS playback uses WebRTC remote track + &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; (AEC reference path)
&lt;/li&gt;
&lt;li&gt;[ ] Pre-convergence "training sounds" are played for AEC
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;autoGainControl&lt;/code&gt; disabled / &lt;code&gt;echoCancellation&lt;/code&gt; enabled
&lt;/li&gt;
&lt;li&gt;[ ] Input energy logging + transmit level meter implemented
&lt;/li&gt;
&lt;li&gt;[ ] Remote streams consumed by muted &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; before WebAudio
&lt;/li&gt;
&lt;li&gt;[ ] Node/element references retained (GC prevention)
&lt;/li&gt;
&lt;li&gt;[ ] Headless paths use synthesized silent track + &lt;code&gt;ConstantSource(0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Opus bitrate/FEC configured
&lt;/li&gt;
&lt;li&gt;[ ] Testing waits for prewarming completion
&lt;/li&gt;
&lt;li&gt;[ ] Idle timeouts reviewed per workload
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hope this saves fellow travelers in this swamp at least an hour of debugging!&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>webaudio</category>
      <category>ai</category>
      <category>getusermedia</category>
    </item>
    <item>
      <title>When an AI Avatar Keeps Replying to Its Own Voice — Writing 1,657 Lines of Band-Aids, Then Throwing Them All Away</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:35:57 +0000</pubDate>
      <link>https://dev.to/orca_forge/when-an-ai-avatar-keeps-replying-to-its-own-voice-writing-1657-lines-of-band-aids-then-throwing-2m71</link>
      <guid>https://dev.to/orca_forge/when-an-ai-avatar-keeps-replying-to-its-own-voice-writing-1657-lines-of-band-aids-then-throwing-2m71</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/ai-avatar-self-echo-1657-lines/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=ai-avatar-self-echo-1657-lines" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When building an AI avatar that can converse through voice, you'll inevitably encounter this scenario:&lt;/p&gt;

&lt;p&gt;The avatar greets the user. The microphone picks up the voice from the speaker. Speech recognition transcribes it as "user speech." The avatar responds to its own greeting. The microphone picks up the response again—&lt;strong&gt;the avatar starts asking and answering itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We fought this "self-echo problem" for a long time, piling on symptomatic fixes, until we finally &lt;strong&gt;scrapped them all&lt;/strong&gt; and rebuilt the architecture from scratch. We removed 1,657 lines of code from the frontend. This article chronicles the entire ordeal and almost every pitfall we encountered along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quagmire of Symptomatic Fixes
&lt;/h2&gt;

&lt;p&gt;Initially, we had a straightforward setup. The browser opens the microphone, uses VAD (Voice Activity Detection) to segment the input, sends it to STT (Speech-to-Text), synthesizes the response with TTS (Text-to-Speech), and plays it back via Web Audio—all within the browser.&lt;/p&gt;

&lt;p&gt;When self-echoes occurred, we added symptomatic fixes. Each one seemed reasonable in isolation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Echo window&lt;/strong&gt;: Discard recognition results similar to the avatar's speech for N seconds after it speaks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text matching&lt;/strong&gt;: If the bigram match rate with the avatar's recent speech is ≥0.75, discard as "echo"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucination vocabulary list&lt;/strong&gt;: Block common false positives STT produces during silence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate question discard&lt;/strong&gt;: Treat identical phrases within 30 seconds as repeats and discard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold-type barge-in&lt;/strong&gt;: Don't stop playback immediately when the user starts speaking (as the echo would stop its own playback). Interrupt only after STT confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These fixes mostly stopped the self-questioning. However, &lt;strong&gt;each fix created new problems&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Text matching discards the most natural user behavior—"asking about what the avatar just said"—because it's too similar to the recent speech. Hold-type barge-in delays interruptions by &lt;strong&gt;2.1–2.5 seconds&lt;/strong&gt;, ruining conversational flow. The echo window also swallows genuine speech. Every adjustment to the fixes broke something else, leading to a state where "echoes disappeared, but real questions did too."&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning Point: All Fixes Stemmed from One Constraint
&lt;/h2&gt;

&lt;p&gt;One day, while reviewing the list of fixes, I realized they weren't independent solutions but &lt;strong&gt;links in a causal chain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to acoustically eliminate self-echo
  → Can't stop playback immediately when user speaks (avatar's voice stops itself)
    → Requires hold-type barge-in (2+ second delay)
      → Need to determine if confirmed text is an echo
        → Requires text matching, echo window, vocabulary list, duplicate discard
          → Ends up discarding real questions too
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root cause was just one thing: &lt;strong&gt;The browser can't acoustically remove its own speaker output from the microphone input&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This raised a question: Browsers have built-in AEC (Acoustic Echo Cancellation). Why wasn't &lt;code&gt;getUserMedia({ audio: { echoCancellation: true } })&lt;/code&gt; working?&lt;/p&gt;

&lt;p&gt;The answer lies in how AEC works. Echo cancellation requires a &lt;strong&gt;reference signal&lt;/strong&gt;—what to cancel. Browser AEC can reference only the browser's own audio playback paths (e.g., &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; elements or WebRTC receive tracks). Since we were playing TTS audio through a custom Web Audio graph, this path wasn't a reliable reference for AEC. The AEC was sitting idle, unaware of what to cancel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cure: Move Audio Pipeline to Server, Use WebRTC for Sound
&lt;/h2&gt;

&lt;p&gt;Identifying the constraint dictated the solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move VAD, STT, turn management, and TTS to a single server-side pipeline&lt;/strong&gt; (using &lt;a href="https://github.com/pipecat-ai/pipecat" rel="noopener noreferrer"&gt;Pipecat&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Browser just sends microphone input via WebRTC&lt;/li&gt;
&lt;li&gt;Avatar audio is returned as a &lt;strong&gt;remote track in the same WebRTC session&lt;/strong&gt; and played via &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is point 3. Remote track playback becomes an official reference signal for browser AEC. The browser now &lt;strong&gt;knows exactly what to cancel&lt;/strong&gt;—the avatar's voice.&lt;/p&gt;

&lt;p&gt;The results were dramatic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even with the speaker volume maxed, &lt;strong&gt;99 seconds of continuous avatar speech produced zero false user turn detections&lt;/strong&gt;—without text matching, echo windows, or vocabulary lists&lt;/li&gt;
&lt;li&gt;Barge-in latency dropped from 2.1–2.5 seconds to &lt;strong&gt;4–6 milliseconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The chain of symptomatic fixes became entirely unnecessary once the root constraint was removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...If only the story ended here. But the real challenges began during the transition. &lt;strong&gt;It was a minefield&lt;/strong&gt;. Below are all the traps we encountered, in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: AEC Isn't Free—Double Talk Distorts User Voice
&lt;/h2&gt;

&lt;p&gt;Soon after switching to the new setup, users reported &lt;strong&gt;increased recognition errors&lt;/strong&gt;. "Employee count" became "shines."&lt;/p&gt;

&lt;p&gt;Investigation revealed the mechanism. In the old setup, AEC was effectively disabled, so user audio was &lt;strong&gt;mixed but undistorted&lt;/strong&gt;. In the new setup, AEC cancels echoes but &lt;strong&gt;distorts the user's voice (especially at word starts) when they speak simultaneously with the avatar&lt;/strong&gt;—a fundamental trade-off called near-end suppression. Zero self-echo came at a cost.&lt;/p&gt;

&lt;p&gt;We implemented a three-layer solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;At the input&lt;/strong&gt;: Set microphone Opus bitrate to 128kbps and enable in-band FEC (by modifying the fmtp line in the answer SDP). The old setup sent uncompressed PCM, so we needed to match the information density&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STT&lt;/strong&gt;: Pass the avatar's recent speech text as &lt;code&gt;initial_prompt&lt;/code&gt; to faster-whisper. Crucially, &lt;strong&gt;don't create a dictionary&lt;/strong&gt;. Company-specific dictionaries grow endlessly. Instead, pass the &lt;strong&gt;avatar's recent speech text directly&lt;/strong&gt;—scripts and responses already contain proper nouns in correct spelling, requiring zero registration. Never mix user STT results (one misrecognition could contaminate the prompt). Limit prompt length to 160 characters (longer prompts cause whisper to "hear" the prompt text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: Add instructions to the system prompt: "Input is speech-to-text transcription and may contain phonetic errors. Interpret unnatural words as phonetic matches and confirm with 'Regarding ◯◯...' at the start. If no match is found, &lt;strong&gt;ask for clarification without inventing explanations&lt;/strong&gt;"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Layer 3 was surprisingly effective. Just as humans can interpret messy chat input through context, LLMs can handle phonetic errors in speech. But without "don't invent explanations," the LLM would confidently fabricate product details for unknown words like "shines."&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: "Not Responding" ≠ "Not Hearing"
&lt;/h2&gt;

&lt;p&gt;Users reported "no response for a while after starting a presentation." Server logs showed &lt;strong&gt;complete silence&lt;/strong&gt; during those periods—the user's voice wasn't being &lt;strong&gt;ignored&lt;/strong&gt;, it wasn't &lt;strong&gt;reaching&lt;/strong&gt; the server.&lt;/p&gt;

&lt;p&gt;Logs from the model we'd added for estimating backchannel timing (monitoring microphone audio at 10Hz) proved invaluable. The response probability was a flat 0.000—no acoustic energy was arriving. Multiple culprits were found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microphone track started as &lt;code&gt;enabled=false&lt;/code&gt;&lt;/strong&gt; (sending silence until the mic button was pressed). Users assumed they could speak anytime. Design expectations and implementation were misaligned → Changed to auto-enable on ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEC/AGC convergence time&lt;/strong&gt;. Echo cancellers learn only when the avatar is actually speaking. During the first utterance of a session, unconverged AEC swallowed double-talk audio entirely → Added a short greeting during the intro animation to train AEC before the main content. Also disabled autoGainControl (gain ramp-up made initial speech sound too quiet; server-side whisper handles volume fluctuations well)&lt;/li&gt;
&lt;li&gt;Added a &lt;strong&gt;level meter for the actual transmitted stream&lt;/strong&gt; to the UI for faster debugging. Instantly distinguishes between "mic off," "eaten by preprocessing," and "server-side issues"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: &lt;strong&gt;For audio "no response" issues, you need observability into where the sound is dying&lt;/strong&gt;. The 10Hz input logs and level meter made subsequent investigations exponentially faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: Headless Environment Triple Threat (Streaming Renderer Edition)
&lt;/h2&gt;

&lt;p&gt;This avatar also streams to YouTube/Twitch. Streaming uses a headless Chromium instance on a cloud GPU, rendering the avatar page and streaming canvas/audio via ffmpeg to RTMP. Switching to the new setup triggered &lt;strong&gt;three consecutive traps&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 1: Headless environments have no microphone.&lt;/strong&gt; The connection process called &lt;code&gt;getUserMedia&lt;/code&gt; at the start, which threw an exception in the mic-less renderer, killing the connection and leaving the stream silent (while video kept alive with silent keep-alive—extra nasty). → Streaming pages now send a &lt;strong&gt;synthetic silent track&lt;/strong&gt; created with WebAudio's &lt;code&gt;createMediaStreamDestination&lt;/code&gt;. The SDP contract matches the microphone, so no server changes were needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 2: Destinations without input may stop frame generation.&lt;/strong&gt; When the silent track's RTP packets ceased, the server's audio reader errored and disconnected. → Added a &lt;code&gt;ConstantSource&lt;/code&gt; node set to 0 to keep sending "actual silent samples."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 3 (The biggest trap): Chromium doesn't route WebRTC remote MediaStreams to WebAudio&lt;/strong&gt; &lt;strong&gt;until a media element consumes them&lt;/strong&gt;. The streaming design tapped the avatar's remote track with &lt;code&gt;createMediaStreamSource&lt;/code&gt; for capture. But without this, the stream was &lt;strong&gt;silent&lt;/strong&gt;. It worked in normal pages because &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; playback was already happening for AEC reference. The streaming page lacked this crucial step. → Solution: Attach the &lt;code&gt;srcObject&lt;/code&gt; to a muted &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element and call &lt;code&gt;play()&lt;/code&gt; (no sound needed—just "triggering" playback is the goal).&lt;/p&gt;

&lt;p&gt;This known behavior requires a one-line fix if you know it, but without knowledge, you get the frustrating "server is speaking, connection is alive, but stream is silent" situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: Framework Defaults Can Kill You—Idle Timeout
&lt;/h2&gt;

&lt;p&gt;During streaming, we encountered "9 minutes of silence after greetings and chat responses, with no closing remarks." Logs revealed the culprit: Pipecat's &lt;strong&gt;idle timeout (default 300 seconds)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For conversational use, "kill pipelines idle for 5 minutes" is a sensible default. But streaming involves &lt;strong&gt;permanent microphone silence&lt;/strong&gt; and legitimate silence when there's no topic. Five minutes after the last utterance, the pipeline committed suicide with &lt;code&gt;Idle pipeline detected, cancelling&lt;/code&gt;, and subsequent closing remarks/viewer chats vanished into the void.&lt;/p&gt;

&lt;p&gt;→ Disabled idle timeout for streaming routes (&lt;code&gt;cancel_on_idle_timeout=False&lt;/code&gt;). Left dialog routes at default (effective for abandoned session cleanup).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework defaults become weapons when use cases change&lt;/strong&gt;. It's worth auditing default assumptions for each workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 5: Monitor Billing Yourself
&lt;/h2&gt;

&lt;p&gt;Testing streaming on pay-as-you-go cloud GPUs taught another lesson. Failed avatar connections sometimes didn't trigger "start billing," preventing &lt;strong&gt;automatic time-based shutdown&lt;/strong&gt;. Fortunately, a separate 25-minute safety timeout existed, but it was nerve-wracking until we realized this.&lt;/p&gt;

&lt;p&gt;Now, after each test, we &lt;strong&gt;API-verify that the cloud instance list is empty&lt;/strong&gt; and added a safety timer to the test sessions that &lt;strong&gt;force-checks and kills remaining instances after N minutes&lt;/strong&gt;. When testing pay-as-you-go infrastructure, trust the framework's cleanup but &lt;strong&gt;manually confirm the final state&lt;/strong&gt; for peace of mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 6: Write Test Harnesses from Logs, Not Implementations
&lt;/h2&gt;

&lt;p&gt;To reduce browser testing, we built an E2E harness that establishes WebRTC connections, plays pre-recorded WAV files at normal speed, and machine-judges message contracts (4 scenarios, 24 checks). This was hugely successful, catching almost all "messages not arriving/out of order" issues. Human testing was limited to self-echo, voice naturalness, and lip-sync.&lt;/p&gt;

&lt;p&gt;However, we made one painful mistake. We wrote a test for the "continue" function (resuming explanations) based on &lt;strong&gt;our implementation&lt;/strong&gt;, not real behavior. The harness passed, but real devices failed. The issue? Pausing could be triggered not just by the stop button but also by questions—a flow the scenario didn't replicate. Now, &lt;strong&gt;scenarios are written from actual utterance sequences in device logs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Beware of false regressions too. Running the harness when TTS model pre-warming monopolizes the GPU causes STT to time out after 30 seconds. Post-deploy "it's broken!" reports should first suspect environmental factors (warm-up, quotas, concurrent builds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Withdrawal: Deleting 1,657 Lines
&lt;/h2&gt;

&lt;p&gt;After validating the new setup across five outputs (admin panel, embedded widget, desktop app, presentations, live streaming), we deleted the old setup and symptomatic fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser-side VAD/STT&lt;/li&gt;
&lt;li&gt;Echo guard, echo window, hallucination vocabulary, duplicate discard, hold-type barge-in&lt;/li&gt;
&lt;li&gt;Browser playback queue and its tied subtitle/expression/page transition sync mechanisms&lt;/li&gt;
&lt;li&gt;WS reconnection/heartbeat mechanisms (consolidated into WebRTC)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;-1,657 lines&lt;/strong&gt; from the frontend alone. Major components shrank from 957→639, 500→363, and 487→367 lines.&lt;/p&gt;

&lt;p&gt;Deletion had traps too. &lt;strong&gt;Old setup practices were often incompatible with the new principles&lt;/strong&gt;. For example, the old &lt;code&gt;stopAudio()&lt;/code&gt; included lip-sync termination and reconnected the playback queue each time. The new setup has no reconnections, so the same call caused "mouth stays still forever" bugs. During migration/deletion, ask &lt;strong&gt;what each call assumes&lt;/strong&gt; one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: When Symptomatic Fixes Pile Up, Hunt the Root Constraint
&lt;/h2&gt;

&lt;p&gt;Key takeaways from this journey:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If you have &amp;gt;3 symptomatic fixes, diagram whether they stem from one constraint&lt;/strong&gt;. If they're linked, pruning branches won't stop growth. Uproot the constraint to kill them all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser AEC lives and dies by the reference signal&lt;/strong&gt;. Custom Web Audio playback often isn't a valid reference. WebRTC remote tracks + &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; playback are officially recognized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEC distorts user voice during double talk&lt;/strong&gt;. The cost of canceling one problem appears elsewhere. STT vocabulary hints should use "your own recent speech"—zero registration cost, high effectiveness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish "not responding" from "not hearing"&lt;/strong&gt; with observability (continuous input level logs, transmitted stream meters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless environments are a different world&lt;/strong&gt;: No microphone, no-input nodes stop, remote streams need consumers for WebAudio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework defaults become weapons when use cases change&lt;/strong&gt; (idle timeout)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For pay-as-you-go testing, manually confirm termination&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write test scenarios from device logs, not implementations&lt;/strong&gt;. Automate everything machine-judgable; rely on human ears/eyes only for subjective tests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Voice AI is easy to demo but hard to productize. Most of that gap comes from &lt;strong&gt;audio physics and browser realities&lt;/strong&gt; like those described here. Hopefully, this shortcuts the journey for others in the same swamp.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webrtc</category>
      <category>pipecat</category>
    </item>
    <item>
      <title>Guard Implementation Patterns to Stop AI Agent Runaway Behavior — 7 Types Extracted from Real-World Logs</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:04:36 +0000</pubDate>
      <link>https://dev.to/orca_forge/guard-implementation-patterns-to-stop-ai-agent-runaway-behavior-7-types-extracted-from-real-world-b53</link>
      <guid>https://dev.to/orca_forge/guard-implementation-patterns-to-stop-ai-agent-runaway-behavior-7-types-extracted-from-real-world-b53</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/ai-agent-runaway-guard-patterns/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=ai-agent-runaway-guard-patterns" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Getting AI Agents to Safely Control PCs and Browsers with Tool-Calling: 7 Guardrails That Stop "Runaway" Behavior
&lt;/h2&gt;

&lt;p&gt;When deploying AI agents that control PCs or browsers via tool-calling—closer to real-world use than demos—you’ll inevitably encounter "runaway" behavior that never appeared in testing. This article compiles &lt;strong&gt;real-world logs of runaway incidents&lt;/strong&gt; from a voice-driven desktop agent and organizes the guardrails that stopped them into &lt;strong&gt;7 patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In one line: &lt;strong&gt;Most runaway behavior stems not from the LLM "rebelling," but from "not seeing the consequences of actions" and "not questioning input quality."&lt;/strong&gt; Guardrails should be designed with this premise in mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1: Limit Repeated Tool Calls — Total Call Limits Don’t Stop Spamming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident&lt;/strong&gt;: When asked to "search for song X," the agent spammed &lt;code&gt;open_url&lt;/code&gt; to the same search URL &lt;strong&gt;5 times in 3 seconds&lt;/strong&gt; (interleaved with &lt;code&gt;read_screen&lt;/code&gt;). Browser tabs multiplied uncontrollably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why total call limits fail&lt;/strong&gt;: The tool limit was already 8 calls per turn. The 5-spam occurred &lt;strong&gt;within that limit&lt;/strong&gt;. The loop wasn’t about "number of calls," but &lt;strong&gt;repetition of the same action&lt;/strong&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Same (tool name + normalized JSON arguments) limited to &lt;strong&gt;2 calls per turn&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Read operations (screen reading, etc.) allowed &lt;strong&gt;3 calls per turn&lt;/strong&gt; (e.g., "operate → read → operate → read" is valid)&lt;/li&gt;
&lt;li&gt;Exceeding the limit &lt;strong&gt;returns an error as the tool result&lt;/strong&gt; (see Pattern 2)&lt;/li&gt;
&lt;li&gt;If two consecutive blocks occur, &lt;strong&gt;abort the entire loop&lt;/strong&gt; (no progress = repetition)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seen_calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;repeat_limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tool_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Repeating the same tool with the same arguments. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Try a different action or report the current state to the user.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;seen_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seen_calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pattern 2: Tool Results Are "Learning Materials" — Silence Implies Success
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident&lt;/strong&gt;: Initially, &lt;code&gt;open_url&lt;/code&gt; only returned "(complete)" to the model. The agent couldn’t see what opened → tried to read the screen → failed → assumed "not opened yet" → &lt;strong&gt;kept retrying&lt;/strong&gt;. This was the root cause of Pattern 1’s spam.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;On success: Return &lt;strong&gt;what happened and how&lt;/strong&gt; (opened URL, page title, written path and size)&lt;/li&gt;
&lt;li&gt;On block: Return &lt;strong&gt;reason and next step&lt;/strong&gt; ("Already executed. Proceed with another action based on the result.")&lt;/li&gt;
&lt;li&gt;Silent or one-word results create a &lt;strong&gt;blind agent&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Guardrails (Pattern 1) are insurance; the real fix is &lt;strong&gt;richer perception&lt;/strong&gt;. In this case, adding body text extraction to screen reading eliminated the motivation to retry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3: Explicit Request Gate — Don’t Let the Agent "Read the Air" for Destructive Actions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident&lt;/strong&gt;: While a user was playing music, the mic picked up lyrics &lt;strong&gt;"!I'm begging, begging you!"&lt;/strong&gt; (Måneskin – &lt;em&gt;Beggin'&lt;/em&gt;), which STT passed to the tool-decision brain. The agent then &lt;strong&gt;created a meeting minutes file&lt;/strong&gt;—with no user request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;: High-impact tools (file writes, minutes saving, sending) are &lt;strong&gt;only callable when the current utterance explicitly contains request words&lt;/strong&gt; ("save," "minutes," "memo," "send") via prompt constraints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- SAVE ONLY ON EXPLICIT REQUEST: call save_minutes / write_file ONLY when the
  CURRENT utterance explicitly asks to save/record/write.
  Never save because the conversation seems worth saving,
  never as a reaction to noise, and never re-save what was already saved.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LLMs’ good intentions ("this conversation is worth saving") become accidents when combined with noisy input.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4: Precedent Prompts — Concrete Failure Examples Work Better Than Abstract Warnings
&lt;/h2&gt;

&lt;p&gt;Abstract phrases like "act carefully" or "confirm if uncertain" rarely worked. What did work was &lt;strong&gt;writing actual failure patterns verbatim&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- INCOMPLETE UTTERANCE: If the utterance is cut off like "Uh, YouTube—", the user hasn’t finished speaking. Do not call tools.
- NOISE / SONG LYRICS: Meaningless repetitions ("bigbigbig"), alphanumeric strings ("B.T.G.I.K.E.T.E."), or lyric-like text are likely mic-captured media audio. Do not call tools.
- REPORTS ARE NOT COMMANDS: Phrases like "X was opened" are not new instructions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background: Voice input lacks chat-like guarantees—&lt;strong&gt;utterances aren’t guaranteed to be complete&lt;/strong&gt;, and environmental noise pollutes them. VAD only silences based on volume; &lt;strong&gt;determining if an utterance is a complete command is the tool-decision layer’s responsibility&lt;/strong&gt;. Note: Echo cancellation only removes sounds your app plays—&lt;strong&gt;music from other apps cannot be canceled&lt;/strong&gt; (mitigation requires OS-level speaker separation).&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 5: Don’t Trust Memory URLs or Paths — Measure Destinations with Tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident&lt;/strong&gt;: When asked to "research X," the model fabricated plausible URLs from training data. One existed; another was &lt;strong&gt;a nonexistent URL invented from a pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;: Standardize research flows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open search results page (query URL)&lt;/li&gt;
&lt;li&gt;Use screen reading to get &lt;strong&gt;a list of real links (hrefs)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Only open &lt;strong&gt;those hrefs&lt;/strong&gt;. Ignore URL strings in body text (e.g., ad display URLs redirect to homepages) or memory-based URLs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Apply the same rule to file paths ("don’t write to 'a familiar path'—verify existence with a system tool first").&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 6: Confirmation Dialogs Are the Last Line of Defense — Don’t Sacrifice Them for Automation Thrills
&lt;/h2&gt;

&lt;p&gt;The reason &lt;strong&gt;zero real damage occurred&lt;/strong&gt; across all these runaways was the design: &lt;strong&gt;a human confirmation dialog always precedes OS operations&lt;/strong&gt;. Even when &lt;code&gt;type_text&lt;/code&gt; tried to type "bigbigbig" into an editor, the dialog stopped it.&lt;/p&gt;

&lt;p&gt;Guards and prompts are probabilistic. &lt;strong&gt;Keep one deterministic layer&lt;/strong&gt;. Practical criteria for mandatory confirmation: "irreversible?" or "visible externally?" (screen reading = no confirmation; writes/sends/keystrokes = confirmation required).&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 7: Build Your Own Safety Nets for Billing and Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident&lt;/strong&gt;: A cloud-GPU worker kept "alive" despite connection failure, &lt;strong&gt;not triggering the 25-minute auto-termination&lt;/strong&gt; based on duration. While the framework’s safety net prevented infinite billing, we didn’t trust it until we verified it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;After each validation/job end, &lt;strong&gt;use APIs to confirm no cloud instances remain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a local timer ("N minutes after launch, force-check and delete any remaining instances")&lt;/li&gt;
&lt;li&gt;For state-transition-based termination (e.g., "live → terminate after X minutes"), &lt;strong&gt;always pair with an absolute time limit for the case where the transition never occurs&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Reference: 7 Patterns at a Glance
&lt;/h2&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;Pattern&lt;/th&gt;
&lt;th&gt;Runaway Stopped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Limit repeated tool calls (2 reads, 3 reads + consecutive blocks to abort)&lt;/td&gt;
&lt;td&gt;Spam loops of the same action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Make tool results "learning materials" (include success details, block reasons)&lt;/td&gt;
&lt;td&gt;Retries from blindness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Explicit request gate&lt;/td&gt;
&lt;td&gt;Unrequested saves/sends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Precedent prompts (name real failure examples)&lt;/td&gt;
&lt;td&gt;Reactions to fragments, lyrics, echo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Don’t trust memory URLs/paths — measure destinations with tools&lt;/td&gt;
&lt;td&gt;Fabricated URLs, wrong send destinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Confirmation dialogs (deterministic last line of defense)&lt;/td&gt;
&lt;td&gt;Real-world damage from all above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Absolute time limits + post-job instance verification for billing&lt;/td&gt;
&lt;td&gt;Silent billing leaks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each pattern takes &lt;strong&gt;under 50 lines of code&lt;/strong&gt; and is &lt;strong&gt;independent&lt;/strong&gt;—you can add one tomorrow. Recommended rollout order: &lt;strong&gt;6 → 1 → 3&lt;/strong&gt;. First stop real damage, then stop loops, then curb well-intentioned runaways.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>toolcalling</category>
      <category>llm</category>
      <category>computeruse</category>
    </item>
    <item>
      <title>Next.js API Proxy Times Out After Long ML Inference (502) — Navigating Undici's Timeout Quagmire</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 25 Aug 2026 19:20:06 +0000</pubDate>
      <link>https://dev.to/orca_forge/nextjs-api-proxy-times-out-after-long-ml-inference-502-navigating-undicis-timeout-quagmire-14f0</link>
      <guid>https://dev.to/orca_forge/nextjs-api-proxy-times-out-after-long-ml-inference-502-navigating-undicis-timeout-quagmire-14f0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/nextjs-proxy-timeout-long-ml-inference-502/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=nextjs-proxy-timeout-long-ml-inference-502" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Relaying API Requests from Next.js Frontend to a Separate Inference Service (FastAPI) via API Routes — A Common BFF Pattern
&lt;/h2&gt;

&lt;p&gt;When using Next.js to relay requests from the frontend to an inference service (FastAPI) running in a separate process via API routes, it's a common BFF (Backend for Frontend) architecture. However, when running audio generation with a 44.1kHz model on the CPU, processing can take several minutes — or even hours. And then, at some point, this happens:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The frontend receives a &lt;strong&gt;502&lt;/strong&gt; error even though the generation hasn't finished.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Checking the inference service logs shows that processing is still running smoothly. The issue isn't with the inference itself — &lt;strong&gt;the bottleneck is the proxy in between&lt;/strong&gt;. This article documents how we identified that the culprit was Next.js's global &lt;code&gt;fetch&lt;/code&gt; (powered by undici) and its default timeout, and how we rewrote the proxy using Node's standard &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; modules to bypass it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Symptoms: Only Long-Running Generation Fails with 502 — and Always at the Same Time
&lt;/h2&gt;

&lt;p&gt;The key observations during debugging were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short generations (a few seconds to tens of seconds) work fine&lt;/li&gt;
&lt;li&gt;Only long-running generations fail with 502&lt;/li&gt;
&lt;li&gt;And they always fail after "about the same amount of time"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inference service continues running, but the proxy layer gives up first. The fact that it fails "after a fixed time" is a strong hint: there’s a hardcoded timeout somewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause: Default Timeout in Global &lt;code&gt;fetch&lt;/code&gt; (undici)
&lt;/h2&gt;

&lt;p&gt;When you use &lt;code&gt;fetch()&lt;/code&gt; directly in a Next.js API route, under the hood it uses &lt;strong&gt;undici&lt;/strong&gt;, Node.js’s built-in HTTP client. Undici has a default timeout for receiving headers (around 300 seconds), and if no response comes back within that window, it forcibly closes the connection. When inference takes more than 5 minutes, it gets cut off right there — resulting in a 502.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Just disable undici’s timeout then!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We tried adjusting &lt;code&gt;headersTimeout&lt;/code&gt;/&lt;code&gt;bodyTimeout&lt;/code&gt; via an &lt;code&gt;Agent&lt;/code&gt;, but ran into another wall: &lt;strong&gt;it’s hard to directly import and inject undici in this setup&lt;/strong&gt;. Modifying the internal implementation of global &lt;code&gt;fetch&lt;/code&gt; isn’t clean, and the override may not work across environments.&lt;/p&gt;

&lt;p&gt;Temporarily increasing the timeout just kicks the can down the road — eventually, another long-running generation will hit the same limit. The real fix wasn’t to tweak the timeout — it was to &lt;strong&gt;rewrite the proxy layer using a different timeout model entirely&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution: Rewrite the Proxy Using Node’s Standard &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of fighting with undici, we rewrote the proxy using Node’s built-in &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; modules. With standard modules, we gain full control over timeout behavior.&lt;/p&gt;

&lt;p&gt;The key design principle: &lt;strong&gt;distinguish between connection setup and response waiting&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We want to fail fast if the target isn’t reachable (TCP connection) → &lt;strong&gt;connection timeout: 30 seconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Once connected, we’re willing to wait however long it takes → &lt;strong&gt;no timeout on response&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is critical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Disable response timeout entirely (allow long-running generation). Only enforce connection timeout.&lt;/span&gt;
&lt;span class="nx"&gt;preq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&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="nx"&gt;preq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socket&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;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;if &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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destroyed&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connecting&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connect timeout&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connect&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;preq.setTimeout(0)&lt;/code&gt; — disables the overall request timeout, allowing the response to be awaited indefinitely&lt;/li&gt;
&lt;li&gt;On the socket, we set a &lt;code&gt;30_000ms&lt;/code&gt; timeout that triggers only if the socket is still connecting or not writable — i.e., &lt;strong&gt;only when the target is unreachable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Once &lt;code&gt;connect&lt;/code&gt; fires, we immediately disable the socket timeout (&lt;code&gt;setTimeout(0)&lt;/code&gt;), letting the response stream in however long it takes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the service is down (can’t connect), we fail fast with 502&lt;/li&gt;
&lt;li&gt;If the service is up but the inference takes 20 minutes, we wait patiently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also added proper error handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;preq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headersSent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`backend API unreachable: &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="nx"&gt;e&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="nf"&gt;resolve&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;h2&gt;
  
  
  Another Trap: Next.js Response Monitoring Warnings
&lt;/h2&gt;

&lt;p&gt;Even after removing the timeout, Next.js API routes have a built-in mechanism that warns when a handler takes too long to respond. To suppress this warning for long-running proxies, we declare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sizeLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;25mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;responseLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;25mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;externalResolver&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="c1"&gt;// Tell Next.js: "This route resolves externally; don't monitor it"&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;We also increased the body and response size limits since we’re dealing with audio data — the defaults would reject large recordings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pitfalls &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fetch&lt;/code&gt; uses undici under the hood&lt;/strong&gt;. If you casually use &lt;code&gt;fetch&lt;/code&gt; in API routes without realizing it has default timeouts, you’ll end up with a hard-to-debug scenario: "The service is running, but the proxy returns 502."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t disable timeouts globally — split them by purpose&lt;/strong&gt;. By separating "connection setup" from "response waiting," we can fail fast on unreachable services while waiting indefinitely for long-running tasks. Disabling all timeouts risks hanging forever on a dead service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Going back to standard modules gives you control&lt;/strong&gt;. Instead of wrestling with undici’s internal settings, dropping down to &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; and managing the socket directly is more reliable and readable for this kind of requirement.&lt;/li&gt;
&lt;li&gt;Don’t forget to set &lt;strong&gt;&lt;code&gt;externalResolver: true&lt;/code&gt;&lt;/strong&gt; for long-running API routes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When proxying long-running ML inference via Next.js API routes, the global &lt;code&gt;fetch&lt;/code&gt; (undici) &lt;strong&gt;default timeout (~300s)&lt;/strong&gt; causes 502 errors&lt;/li&gt;
&lt;li&gt;In setups where injecting undici directly is difficult, &lt;strong&gt;rewriting the proxy with Node’s &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt;&lt;/strong&gt; is the most reliable fix&lt;/li&gt;
&lt;li&gt;Split timeouts logically: &lt;strong&gt;30s connection timeout, no timeout on response&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Switch to &lt;code&gt;setTimeout(0)&lt;/code&gt; on &lt;code&gt;connect&lt;/code&gt; to fail fast only when unreachable&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;externalResolver: true&lt;/code&gt;&lt;/strong&gt; to suppress Next.js monitoring warnings, and increase body limits for audio data&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>undici</category>
      <category>node</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Problem of Robotic Voice When Changing Speech Speed - From Phase Vocoder to WSOLA</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Fri, 07 Aug 2026 01:03:57 +0000</pubDate>
      <link>https://dev.to/orca_forge/the-problem-of-robotic-voice-when-changing-speech-speed-from-phase-vocoder-to-wsola-4f5i</link>
      <guid>https://dev.to/orca_forge/the-problem-of-robotic-voice-when-changing-speech-speed-from-phase-vocoder-to-wsola-4f5i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/wsola-vs-phase-vocoder-time-stretch/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=wsola-vs-phase-vocoder-time-stretch" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While building a "Voice Design" app, I added a slider to adjust speech rate (speaking speed). The goal was simple: speed up or slow down the tempo without changing the pitch. This is a very common requirement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;librosa&lt;/code&gt; has a function called &lt;code&gt;librosa.effects.time_stretch&lt;/code&gt; designed exactly for this. It’s a one-liner. That’s what I used at first. However, as soon as I moved the slider even slightly, a faint metallic ringing would appear in the output. The voice sounded slightly "robotic" and "echoey," becoming muffled. The original voice was natural, but the moment the tempo changed, the quality dropped.&lt;/p&gt;

&lt;p&gt;This article is a record of how I discovered that the cause was &lt;strong&gt;phase blurring in the phase vocoder&lt;/strong&gt; and how I resolved it by implementing &lt;strong&gt;WSOLA (Waveform Similarity Overlap-Add)&lt;/strong&gt; from scratch using &lt;code&gt;numpy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise: Changing tempo while preserving pitch
&lt;/h2&gt;

&lt;p&gt;If you simply drop or duplicate audio samples, the pitch will shift along with the playback speed (the "chipmunk effect" you get when fast-forwarding). Time stretching is the process of changing &lt;strong&gt;only the tempo&lt;/strong&gt; while avoiding this pitch shift.&lt;/p&gt;

&lt;p&gt;There are two main approaches to this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase Vocoder&lt;/strong&gt;: Converts the signal into the frequency domain using STFT, then stretches/compresses it by adjusting the phase advancement of each frequency bin. It operates in the frequency domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WSOLA&lt;/strong&gt;: Cuts the waveform (time domain) into short frames and finds the best positions to overlap and add them so they connect smoothly. It operates in the time domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;librosa&lt;/code&gt;'s &lt;code&gt;time_stretch&lt;/code&gt; uses the former: the phase vocoder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom: Metallic ringing (Robotic voice)
&lt;/h2&gt;

&lt;p&gt;A phase vocoder treats each frequency bin independently and updates the phase to the "intended" advancement amount. While mathematically sound, in real speech, the phase relationships between harmonic components (overtones) gradually fall apart. This is known as a &lt;strong&gt;loss of phase coherence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To the human ear, this manifests as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A metallic or electronic sound (often called "phasiness").&lt;/li&gt;
&lt;li&gt;A faint reverberation or echo effect.&lt;/li&gt;
&lt;li&gt;Blurry vowel cores, making the voice sound "synthetic."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While this is often unnoticeable in music or percussive material, human speech relies heavily on formants and harmonic structures. Because of this, phase blurring is heard very clearly as a "robotic voice." Furthermore, the effect worsens as the stretch ratio increases. The conclusion was that it was a poor match for a speech-rate slider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: WSOLA in the time domain
&lt;/h2&gt;

&lt;p&gt;WSOLA does not enter the frequency domain; it simply &lt;strong&gt;cuts and pastes the waveform&lt;/strong&gt;. Instead of "recalculating" the phase, it &lt;strong&gt;uses cross-correlation to find&lt;/strong&gt; the position where adjacent frames connect most naturally. Therefore, phase blurring does not occur by design.&lt;/p&gt;

&lt;p&gt;The logic works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the output side, frames are arranged at fixed intervals (&lt;code&gt;syn_hop&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;From the input side, we take frames at an interval corresponding to the stretch rate (&lt;code&gt;ana_hop = syn_hop × rate&lt;/code&gt;) at the "ideal" positions.&lt;/li&gt;
&lt;li&gt;However, instead of the exact ideal position, we search within a range of ±&lt;code&gt;tol&lt;/code&gt; to find the frame that most closely matches the continuation of the previously placed frame (creating a natural waveform continuity).&lt;/li&gt;
&lt;li&gt;The found frame is then combined using a Hann window (overlap-add).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The heart of the implementation (&lt;code&gt;_apply_speed&lt;/code&gt;) is this "coarse search for the similar position."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ana_hop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;syn_hop&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hanning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;# The "natural continuation" of the previous frame = nat
&lt;/span&gt;&lt;span class="n"&gt;nat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;prev_ana&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;syn_hop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;prev_ana&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;syn_hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;best_d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&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="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1e18&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="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tol&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;     &lt;span class="c1"&gt;# Coarse search within ±tol to align phase
&lt;/span&gt;    &lt;span class="n"&gt;cand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ideal&lt;/span&gt; &lt;span class="o"&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;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ideal&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&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;sc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nat&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-6&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;sc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ideal&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;best_d&lt;/span&gt;
&lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;
&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;nat&lt;/code&gt; represents the "natural continuation of the previously placed frame." By shifting the candidate frame &lt;code&gt;cand&lt;/code&gt; within the range of ±&lt;code&gt;tol&lt;/code&gt;, we find the position &lt;code&gt;best_d&lt;/code&gt; where the normalized dot product (cross-correlation) with &lt;code&gt;nat&lt;/code&gt; is maximized. Essentially, we are stitching the waveform where the periods align, which guarantees phase continuity. Finally, we normalize by dividing by the window weight &lt;code&gt;norm&lt;/code&gt; (a standard practice in overlap-add).&lt;/p&gt;

&lt;p&gt;The parameters used were &lt;code&gt;frame=1024, syn_hop=512, tol=512&lt;/code&gt;. I used a coarse search with a step of 8 samples (&lt;code&gt;range(-tol, tol+1, 8)&lt;/code&gt;) instead of a sample-by-sample exhaustive search. Since speech rate adjustment requires real-time performance, this provides a good balance between audio quality and processing speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs: It is not a silver bullet
&lt;/h2&gt;

&lt;p&gt;Switching to WSOLA does not solve everything. There are material-dependent trade-offs in method selection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WSOLA is strong for speech (single speaker)&lt;/strong&gt;. Since periodicity is clear, it is easy to find similar positions, avoiding phase blurring. This was the best choice for this specific use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the other hand, for complex polyphonic music or material with many transients, a phase vocoder may be less prone to artifacts&lt;/strong&gt;. Since WSOLA commits to a single "stitching position," when multiple periodicities are mixed, the similarity search can get lost, leading to rhythmic fluctuations or "doubling" effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At extreme stretch ratios, artifacts increase regardless of the method&lt;/strong&gt;. If you stretch too much with WSOLA, repetitions of the same frame become noticeable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not a matter of "phase vocoder is bad and WSOLA is justice," but rather that &lt;strong&gt;WSOLA was better suited for speech, which has very clear pitch structures&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls and Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question a single line of a library&lt;/strong&gt;. &lt;code&gt;librosa.time_stretch&lt;/code&gt; was working perfectly. It wasn't a bug; the characteristic of the phase vocoder method (phase blurring) simply didn't suit speech. The key was distinguishing between "the function is broken" and "the method doesn't fit the purpose."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be aware of "Time Domain vs. Frequency Domain" from the start&lt;/strong&gt;. If you can pinpoint that metallic sound as being phase-derived, you can logically conclude that you should move from frequency-domain processing (phase vocoder) to time-domain processing (WSOLA).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom implementation can be very small&lt;/strong&gt;. The core of WSOLA is just "finding the similarity position via cross-correlation and performing overlap-add," which can be written with just &lt;code&gt;numpy&lt;/code&gt;. A side benefit was reducing an external dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't forget normalization for the window and overlap&lt;/strong&gt;. In overlap-add, the amplitude only becomes correct when you divide by the sum of the window weights (&lt;code&gt;norm&lt;/code&gt;). If you skip this, the volume will fluctuate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The "robotic voice/metallic ringing" in speech rate adjustment is actually &lt;strong&gt;phase blurring (phasiness)&lt;/strong&gt; caused by the phase vocoder.&lt;/li&gt;
&lt;li&gt;For materials with clear pitch like speech, &lt;strong&gt;WSOLA—which stitches waveforms at similar positions—is more natural&lt;/strong&gt; than re-calculating phases with a phase vocoder.&lt;/li&gt;
&lt;li&gt;The core of WSOLA is searching within $\pm$&lt;code&gt;tol&lt;/code&gt; for the position that has the maximum cross-correlation with the "natural continuation" of the previous frame, and then performing overlap-add with a Hann window. This can be done in a few dozen lines of &lt;code&gt;numpy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It is not a universal solution. For polyphonic music or transient-heavy material, a phase vocoder can be advantageous; the correct approach is to &lt;strong&gt;choose the method based on the source material&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Before suspecting that a library function is "broken," check if the &lt;strong&gt;underlying algorithm (methodology) is appropriate for your specific use case&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dsp</category>
      <category>wsola</category>
    </item>
    <item>
      <title>Visualizing Anchor Distributions to Create a 'Voice Map'</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:32:49 +0000</pubDate>
      <link>https://dev.to/orca_forge/visualizing-anchor-distributions-to-create-a-voice-map-cp6</link>
      <guid>https://dev.to/orca_forge/visualizing-anchor-distributions-to-create-a-voice-map-cp6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/visualizing-voice-anchor-distribution/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=visualizing-voice-anchor-distribution" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When You Collect 70 Voice Conversion Anchors, the Next Problem Emerges
&lt;/h2&gt;

&lt;p&gt;Now that you’ve gathered 70 voice-conversion anchors, a new worry creeps in: &lt;em&gt;Does this library truly cover everything?&lt;/em&gt; Are you overloaded with low male voices and missing high, bright female ones? Are you duplicating similar voices over and over?&lt;/p&gt;

&lt;p&gt;You can guarantee the quality of each anchor with the selection filters and audits we wrote about in another post. But spotting &lt;em&gt;systemic&lt;/em&gt; gaps—whether the collection is skewed as a whole—isn’t something you can see by looking at individual items. What you need is an &lt;strong&gt;overview&lt;/strong&gt;, a single map of the entire voice library.&lt;/p&gt;

&lt;p&gt;This post shows how we visualized an anchor set with two diagrams so you can check coverage and bias at a glance. No model required—just numpy and matplotlib.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Spaces You Want to Visualize
&lt;/h2&gt;

&lt;p&gt;Voice anchors live in two different “spaces” with distinct properties.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semantic-axis space&lt;/strong&gt;&lt;br&gt;
The app lets users adjust voices along eight axes (0–100 %): &lt;em&gt;age feel, gender, pitch, body type, huskiness, clarity, warmth, roughness&lt;/em&gt;. Placing each anchor on these eight axes reveals the &lt;strong&gt;coverage of the design space&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Speaker-embedding space&lt;/strong&gt;&lt;br&gt;
A 192-dimensional vector from campplus. Similar voices cluster together; dissimilar ones drift apart. This map shows the &lt;strong&gt;spread of raw voice quality&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first space answers “Do we have enough choices along the axes the user can control?” The second answers “Where are the dense clusters and empty gaps in pure voice quality?” We decided to draw both on one page.&lt;/p&gt;




&lt;h3&gt;
  
  
  (A) Semantic-axis coverage: strip plots
&lt;/h3&gt;

&lt;p&gt;For each of the eight axes we lay out every anchor’s value in a horizontal strip. One strip per axis, with tiny jitter added so points don’t overlap vertically, and a crimson vertical line marking the mean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axes_keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&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;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="n"&gt;axA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#3a76b4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;axA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&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;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crimson&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zorder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# mean
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading the plot is straightforward: &lt;strong&gt;if points stretch from 0 % to 100 % on an axis, coverage is good; if they’re clustered on one side, there’s a gap&lt;/strong&gt;. For example, a gender axis that reaches both ends tells you you can pick anything from masculine to feminine. A mean far off-center or a missing cluster immediately shows where anchors are missing.&lt;/p&gt;

&lt;p&gt;Turning the vague feeling “I still need more ___ voices” into a concrete empty stretch on the axis is the real value of this diagram.&lt;/p&gt;




&lt;h3&gt;
  
  
  (B) Speaker-embedding scatter plot: PCA to 2-D
&lt;/h3&gt;

&lt;p&gt;The second diagram compresses the 192-D speaker embeddings into two dimensions. We use a plain PCA—subtract the mean, run SVD, and take the first two principal components—no fancy libraries required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Xc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&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;U&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;svd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Xc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_matrices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Xc&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;Vt&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;                               &lt;span class="c1"&gt;# project to top 2 PCs
&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;        &lt;span class="c1"&gt;# explained variance [%]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each point (anchor) is labeled with the speaker name and colored by the &lt;strong&gt;gender-axis value&lt;/strong&gt; (blue = masculine / red = feminine). We’re overlaying a human-interpretable axis on top of an abstract embedding space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;gi&lt;/span&gt; &lt;span class="o"&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;axes_keys&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gender&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gcol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;gi&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;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;axB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Z&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="n"&gt;Z&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gcol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coolwarm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&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;Here you see dense clumps (duplicate voices) and empty patches (gaps in voice quality). The coloring also shows how the embedding’s main axes relate to gender. Axis labels include the explained variance (&lt;code&gt;PC1 (xx%)&lt;/code&gt;) so you know how much of the original variance this 2-D slice captures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quiet Implementation Details That Matter
&lt;/h2&gt;

&lt;p&gt;Nothing flashy, but small touches that turn a visualization into a &lt;em&gt;usable&lt;/em&gt; tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit Japanese font selection&lt;/strong&gt;.&lt;br&gt;
Matplotlib defaults to tofu (□) for Japanese. We tried Hiragino Sans and similar candidates, then placed the working font at the top of the script. Without readable labels, the map is half useless.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Labels pulled from a lookup table&lt;/strong&gt;.&lt;br&gt;
If &lt;code&gt;anchor_sources.json&lt;/code&gt; exists, we display the speaker’s real name (&lt;code&gt;spk24&lt;/code&gt;); otherwise we fall back to the raw &lt;code&gt;spk&lt;/code&gt; id. The labeling work from another post pays off here—you can talk about distributions in names, not numbers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model-agnostic&lt;/strong&gt;.&lt;br&gt;
As long as you have the embeddings and slider values, you can draw these plots. No need to load the heavy Seed-VC model. Just numpy and matplotlib. Lightweight enough for CI or a quick local run, which quietly nudges the team to make visualization a habit.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Turning the Maps Into Decisions
&lt;/h2&gt;

&lt;p&gt;A visualization isn’t the end goal; it’s a tool for deciding what to do next. From the two maps you can derive concrete actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Axis with a one-sided gap (A)&lt;/strong&gt; → prioritize collecting voices in that direction (e.g., more elderly voices, more husky voices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dense clump (B)&lt;/strong&gt; → duplicate voices; stop adding more in that region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse region (B)&lt;/strong&gt; → a hole in voice-quality coverage; hunt for material to fill it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When “I feel like we’re missing something” becomes “this exact stretch on this axis is empty,” collecting anchors stops being guesswork. Treating the voice library as a &lt;em&gt;design target&lt;/em&gt; is the real win of visualization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We visualized an anchor set in &lt;strong&gt;two spaces&lt;/strong&gt;:&lt;br&gt;
(A) coverage along eight semantic axes,&lt;br&gt;
(B) a PCA scatter plot of 192-D speaker embeddings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(A) Strip plots reveal empty stretches and mean shifts on each axis—quickly spotting missing voice types.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(B) A simple PCA (mean-centering + SVD) projects embeddings to 2-D; coloring by gender shows how the abstract space aligns with a human axis. Dense clusters and empty patches become visible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explicit Japanese fonts, label lookups, and model-agnostic code make the visualization &lt;em&gt;actionable&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The purpose of the maps isn’t just to look—they drive decisions. Fill the empty axis stretches and sparse regions to guide your next anchor hunt.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pca</category>
      <category>seedvc</category>
    </item>
    <item>
      <title>22.05kHz vs 44.1kHz — What's the Difference Between 'True Broadband' and Upsampling?</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:02:22 +0000</pubDate>
      <link>https://dev.to/orca_forge/2205khz-vs-441khz-whats-the-difference-between-true-broadband-and-upsampling-3lhl</link>
      <guid>https://dev.to/orca_forge/2205khz-vs-441khz-whats-the-difference-between-true-broadband-and-upsampling-3lhl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/true-44khz-vs-upsampling-voice/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=true-44khz-vs-upsampling-voice" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When comparing outputs from voice conversion, have you ever wondered about this?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm exporting at 44.1kHz, but the sound still feels muffled."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The export sampling rate is indeed 44.1kHz. It says so in the file properties. Yet, it lacks the "air" and clarity of CD quality. The reason for this is that &lt;strong&gt;the sampling rate number and the actual frequency bandwidth contained in the audio are two different things&lt;/strong&gt;. This article is a record of the definitive difference between "true wideband" and upsampling, which I verified when switching from a 22.05kHz model to a 44.1kHz F0-conditioned model in the "Voice Canva" app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise: Sampling Rate Only Determines the "Ceiling"
&lt;/h2&gt;

&lt;p&gt;First, let's cover the basics. According to the Nyquist-Shannon sampling theorem, the upper limit of the frequency that can be represented in audio with a sampling rate &lt;code&gt;fs&lt;/code&gt; is &lt;code&gt;fs / 2&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;22.05kHz → Limit: &lt;strong&gt;approx. 11kHz&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;44.1kHz → Limit: &lt;strong&gt;approx. 22kHz&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crucial point here is that setting it to 44.1kHz means the audio &lt;strong&gt;can represent&lt;/strong&gt; up to 22kHz; it does not mean the content &lt;strong&gt;is filled&lt;/strong&gt; up to 22kHz. It determines the size of the container, but it doesn't automatically increase the contents.&lt;/p&gt;

&lt;p&gt;Audio created at 22.05kHz simply has no components above 11kHz. Even if you resample (upsample) this to 44.1kHz, &lt;strong&gt;the empty high-frequency range remains empty&lt;/strong&gt;. Even if you double the size of the container, what wasn't there cannot be created. This is the point that often defies intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  22.05kHz Models and 44.1kHz Models Are Different Entities
&lt;/h2&gt;

&lt;p&gt;In the "Voice Canva" inference service, the model is switched via environment variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use the 44.1kHz F0-conditioned model (true wideband output). Default OFF (conventional 22.05kHz).
&lt;/span&gt;&lt;span class="n"&gt;F0_COND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VOICE_CANVA_F0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The startup script for 44k mode looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_F0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_F0&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                    &lt;span class="c"&gt;# Use 44k F0-conditioned model&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_OUTPUT_SR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_OUTPUT_SR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;44100&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c"&gt;# Output also at 44.1kHz (no downsampling)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_AUTO_F0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VOICE_CANVA_AUTO_F0&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;          &lt;span class="c"&gt;# Align carrier F0 to target pitch range&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the decisive part. &lt;code&gt;VOICE_CANVA_F0=1&lt;/code&gt; is not simply a setting to "increase the output file rate." &lt;strong&gt;It replaces the generation model itself with a different model trained at 44.1kHz.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Internally, the 22k and 44k models use different checkpoints and vocoders. The 44k model utilizes a BigVGAN vocoder trained at a high sampling rate, meaning that during the stage where the mel-spectrogram is converted back into a waveform, the &lt;strong&gt;model can actually generate&lt;/strong&gt; bandwidth above 11kHz. In other words, instead of mechanically filling the empty high frequencies via resampling, it &lt;strong&gt;creates the content&lt;/strong&gt; based on its learned knowledge.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sr&lt;/code&gt; (sampling rate) also switches to follow the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mel_fn_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sampling_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# 44100 in f0 mode
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output rate is maintained at 44100 without downsampling. By maintaining a consistent path of "Generate at 44k → Output at 44k," we ensure that the hard-won high frequencies aren't discarded midway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement: Is There Actually Energy in the High Frequencies?
&lt;/h2&gt;

&lt;p&gt;To prove that the high-frequency content actually increased, we need numbers. I analyzed the spectrum of audio generated from the same input and measured the &lt;strong&gt;percentage of total energy occupying the bandwidth above 11kHz&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The results were as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output generated by 44.1kHz model&lt;/strong&gt;: &lt;strong&gt;Approx. 7.4%&lt;/strong&gt; of total energy was in the &amp;gt;11kHz band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22.05kHz output upsampled to 44.1kHz&lt;/strong&gt;: The &amp;gt;11kHz band was &lt;strong&gt;nearly zero&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite both being 44.1kHz files, the content is completely different. The upsampled version has a container that reaches 22kHz, but everything above 11kHz is empty. Meanwhile, the 44k model output has actual energy. This 7.4% is the component that the ear perceives as the clarity of consonants, the "air" of a breath, and that general "CD quality" feel.&lt;/p&gt;

&lt;p&gt;While these numbers might seem obvious in hindsight, when comparing by ear, it's easy to conclude that "this one just sounds clearer." It was only by looking at the energy per band in the spectrum that I could objectively distinguish between "high frequencies being created" vs. "not being there."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Upsampling Doesn't Create High Frequencies
&lt;/h2&gt;

&lt;p&gt;To explain the logic a bit further: upsampling is a process of &lt;strong&gt;interpolating between&lt;/strong&gt; existing sample points. Whether using linear interpolation or higher-quality low-pass interpolation, what's happening is "connecting the existing waveform smoothly." It does &lt;strong&gt;not fabricate&lt;/strong&gt; new frequency components (in fact, a proper resampler actively removes everything above the limit to prevent fake high frequencies from appearing as aliasing noise).&lt;/p&gt;

&lt;p&gt;Therefore, "converting 22.05kHz audio to 44.1kHz" and "generating audio at 44.1kHz" result in fundamentally different content, even if the final "container" is the same. The former is interpolation; the latter is generation. Information like high-frequency detail can only be created by a model that "knows" what it should sound like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls and Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't use the sampling rate number as proof of quality&lt;/strong&gt;. "44.1kHz output" often becomes mere window dressing on a spec sheet; it doesn't guarantee the content is there. You can only claim it's "real" after measuring the energy per band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44k models are heavy&lt;/strong&gt;. The startup script explicitly mentions that "the model is large and generation is slow on CPU (GPU recommended)." Wideband isn't free; it's a trade-off for model size and inference cost. It's realistic to design a system that switches between 22k and 44k depending on the use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any downsampling in the pipeline ruins everything&lt;/strong&gt;. Even if you generate at 44k, if you drop it to 22k at any later stage, the high frequencies vanish. It was critical to explicitly set &lt;code&gt;VOICE_CANVA_OUTPUT_SR=44100&lt;/code&gt; to ensure consistency from generation to output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Question the root cause of "muffled" sound&lt;/strong&gt;. When audio sounds muffled, boosting the high end with an EQ is only a temporary fix. You can't boost components that aren't there. The true solution was to switch to a model capable of generating high frequencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sampling rate only sets the &lt;strong&gt;upper limit&lt;/strong&gt; of representable frequencies; it doesn't automatically fill the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upsampling 22.05kHz audio to 44.1kHz does not create high frequencies above 11kHz&lt;/strong&gt; (it is interpolation, not generation).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VOICE_CANVA_F0=1&lt;/code&gt; is not just an output rate setting, but a &lt;strong&gt;switch to a separate model trained at 44.1kHz (including the BigVGAN vocoder)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Measurements showed that 44k model output has &lt;strong&gt;approx. 7.4%&lt;/strong&gt; of total energy in the &amp;gt;11kHz band, while 22k upsampled output has &lt;strong&gt;nearly zero&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Wideband comes at the cost of model size and inference overhead. The key is maintaining 44k consistency from generation to output without intermediate downsampling.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seedvc</category>
    </item>
  </channel>
</rss>
