<?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>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>
    <item>
      <title>Recording Spec-Compliant WAV Files (16-bit/Mono/Uncompressed) Using Only Web Audio</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:20:45 +0000</pubDate>
      <link>https://dev.to/orca_forge/recording-spec-compliant-wav-files-16-bitmonouncompressed-using-only-web-audio-40n8</link>
      <guid>https://dev.to/orca_forge/recording-spec-compliant-wav-files-16-bitmonouncompressed-using-only-web-audio-40n8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/spec-compliant-wav-recording-web-audio/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spec-compliant-wav-recording-web-audio" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction to Clean Audio Recording
&lt;/h2&gt;

&lt;p&gt;When it comes to recording audio in the browser, the first thing that comes to mind is likely the &lt;code&gt;MediaRecorder&lt;/code&gt; API. It's easy to use and only requires a few lines of code to start recording. However, when it comes to using the recorded audio for machine learning preprocessing, such as voice conversion or feature extraction, &lt;code&gt;MediaRecorder&lt;/code&gt; becomes inconvenient.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MediaRecorder&lt;/code&gt; typically outputs &lt;code&gt;webm/opus&lt;/code&gt;, which is a non-reversible compression format. While it's sufficient for human ears, the fine details of the audio are already lost during the preprocessing stage. Moreover, many downstream pipelines require a strictly specified WAV format, which is &lt;strong&gt;16-bit PCM, mono, and uncompressed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we'll introduce an implementation that uses Web Audio to record raw PCM and encodes it into a WAV file manually. Although it's a tedious task to write binary data one byte at a time, once it's done, you'll have a high-quality recording with zero degradation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: Why Raw PCM?
&lt;/h2&gt;

&lt;p&gt;The Web Audio API allows us to redirect the microphone input to an &lt;code&gt;AudioContext&lt;/code&gt; graph and extract the raw samples (Float32, -1 to 1) at any point. Since we're not passing the audio through any compression codec, we can capture the original signal from the microphone. By encoding these Float32 samples into a 16-bit WAV file, we can create a file that meets the specifications without any degradation.&lt;/p&gt;

&lt;p&gt;Our approach consists of two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recording&lt;/strong&gt;: Collecting Float32 raw samples from the microphone (using Web Audio)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding&lt;/strong&gt;: Converting the collected Float32 samples into a 16-bit PCM WAV byte array&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Opening the Microphone with Clean Settings
&lt;/h2&gt;

&lt;p&gt;We use &lt;code&gt;getUserMedia&lt;/code&gt; to access the microphone, but it's essential to &lt;strong&gt;disable all browser audio processing&lt;/strong&gt;. Echo cancellation, noise suppression, and automatic gain control are useful for voice calls, but they're unnecessary for recording and can even degrade the audio quality.&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;channelCount&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="na"&gt;echoCancellation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="na"&gt;noiseSuppression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;autoGainControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set &lt;code&gt;channelCount&lt;/code&gt; to 1 to require mono audio. To achieve "clean recording" as specified, it's crucial to explicitly set these flags to false.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Collecting Raw Samples with ScriptProcessor
&lt;/h2&gt;

&lt;p&gt;We create a &lt;code&gt;MediaStreamSource&lt;/code&gt; from the microphone and capture the samples flowing from it. Here, we use a &lt;code&gt;ScriptProcessorNode&lt;/code&gt;.&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;this&lt;/span&gt;&lt;span class="p"&gt;.&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// usually 48000 (&amp;gt;= 44.1kHz)&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScriptProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// buffer, in=1, out=1&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onaudioprocess&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// getChannelData uses an internal buffer, so we must copy and escape it&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;Float32Array&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="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// required for some browsers to trigger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few key points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Copy the samples&lt;/strong&gt;: The Float32Array returned by &lt;code&gt;getChannelData(0)&lt;/code&gt; is an internal buffer that will be overwritten on the next callback. If we don't copy it using &lt;code&gt;new Float32Array(...)&lt;/code&gt;, the entire recording will be overwritten with the last frame.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Connect to destination&lt;/strong&gt;: Some browsers require the &lt;code&gt;ScriptProcessorNode&lt;/code&gt; to be connected to the &lt;code&gt;ctx.destination&lt;/code&gt; to trigger the &lt;code&gt;onaudioprocess&lt;/code&gt; event. Even if we don't need to play the audio, we connect it to ensure the event is triggered.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sample rate is device-dependent&lt;/strong&gt;: &lt;code&gt;ctx.sampleRate&lt;/code&gt; is dependent on the environment, and most devices use 48000 (which is greater than or equal to 44.1kHz). We record the actual sample rate here and use it to write the correct header later. &lt;strong&gt;Using the actual recorded sample rate in the header&lt;/strong&gt; is crucial for creating a compliant file without any resampling.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;ScriptProcessorNode&lt;/code&gt; is deprecated, and its successor is &lt;code&gt;AudioWorklet&lt;/code&gt;. However, for simple use cases like collecting samples, &lt;code&gt;ScriptProcessorNode&lt;/code&gt; is still sufficient. If you need low latency or heavy DSP processing, consider migrating to &lt;code&gt;AudioWorklet&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When stopping the recording, we concatenate the collected chunks into a single Float32Array, release the microphone and &lt;code&gt;AudioContext&lt;/code&gt;, and then proceed to the next encoding step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Manually Building the 16-bit PCM WAV
&lt;/h2&gt;

&lt;p&gt;Now, let's create the WAV byte array. The WAV format consists of a &lt;strong&gt;44-byte header followed by the PCM data&lt;/strong&gt;. We use an &lt;code&gt;ArrayBuffer&lt;/code&gt; and &lt;code&gt;DataView&lt;/code&gt; to write the fields one by one.&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;function&lt;/span&gt; &lt;span class="nf"&gt;encodeWav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Blob&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;buffer&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;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 16bit = 2byte/sample&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&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;DataView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&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;writeStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kr"&gt;string&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;writeStr&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RIFF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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;// file size - 8&lt;/span&gt;
  &lt;span class="nf"&gt;writeStr&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WAVE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;writeStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fmt &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&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;// fmt chunk size&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// format = 1 (PCM uncompressed)&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;22&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// channel count = mono&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// sample rate&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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;// bytes per second = sample rate * block align&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// block align = mono * 16bit/8&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&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;// bit depth = 16&lt;/span&gt;
  &lt;span class="nf"&gt;writeStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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;// data length&lt;/span&gt;
  &lt;span class="c1"&gt;// ... body ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key points in the header are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Format number 1 means PCM (uncompressed)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Multi-byte values are little-endian&lt;/strong&gt;, so we write them with &lt;code&gt;true&lt;/code&gt; as the third argument to &lt;code&gt;DataView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Block align and byte rate are calculated from the channel count and bit depth&lt;/strong&gt; (for mono 16-bit, block align = 2, byte rate = sample rate * 2). By writing the actual sample rate here, we ensure that the WAV file plays at the correct speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The body of the WAV file is where we quantize the Float32 samples into 16-bit integers. Note that the scale is asymmetric:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;         &lt;span class="c1"&gt;// clamp to range&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInt16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mh"&gt;0x7fff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// asymmetric scale&lt;/span&gt;
  &lt;span class="nx"&gt;off&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="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio/wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 16-bit signed range is &lt;strong&gt;asymmetric&lt;/strong&gt;: -32768 to +32767. For negative values, we multiply by 0x8000 (32768), and for positive values, we multiply by 0x7fff (32767) to utilize the full scale correctly. Some implementations may multiply both by 0x7fff, but this would lose some dynamic range on the negative side. Although it's a minor detail, it's essential for creating a compliant file. Clamping the input to the -1 to 1 range before quantization also prevents overflow.&lt;/p&gt;

&lt;p&gt;Finally, we create a &lt;code&gt;Blob&lt;/code&gt; (with &lt;code&gt;type: 'audio/wav'&lt;/code&gt;) that can be used as needed, such as downloading it using &lt;code&gt;URL.createObjectURL&lt;/code&gt; or sending it to a server via &lt;code&gt;FormData&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;To achieve clean audio recording for preprocessing, we must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Use Web Audio to record raw PCM instead of &lt;code&gt;MediaRecorder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Disable all browser audio processing using &lt;code&gt;getUserMedia&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Collect Float32 samples using &lt;code&gt;ScriptProcessorNode&lt;/code&gt; and copy them to avoid overwriting.&lt;/li&gt;
&lt;li&gt;  Connect the &lt;code&gt;ScriptProcessorNode&lt;/code&gt; to the &lt;code&gt;destination&lt;/code&gt; to ensure the &lt;code&gt;onaudioprocess&lt;/code&gt; event is triggered.&lt;/li&gt;
&lt;li&gt;  Use the actual recorded sample rate in the WAV header.&lt;/li&gt;
&lt;li&gt;  Build the WAV file manually using &lt;code&gt;ArrayBuffer&lt;/code&gt; and &lt;code&gt;DataView&lt;/code&gt;, considering little-endian and asymmetric quantization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following these steps, you'll be able to create high-quality, degradation-free audio recordings that meet the required specifications for machine learning preprocessing.&lt;/p&gt;

</description>
      <category>webaudio</category>
      <category>typescript</category>
      <category>wav</category>
    </item>
    <item>
      <title>Identifying Speakers by Voice Quality Alone — Labeling Unknown Audio Sources</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:50:25 +0000</pubDate>
      <link>https://dev.to/orca_forge/identifying-speakers-by-voice-quality-alone-labeling-unknown-audio-sources-3hbi</link>
      <guid>https://dev.to/orca_forge/identifying-speakers-by-voice-quality-alone-labeling-unknown-audio-sources-3hbi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/speaker-identification-by-voice/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=speaker-identification-by-voice" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you are growing an audio dataset, you will inevitably run into the "Who's voice is this again?" problem. The anchors (target speakers for voice conversion) in this app were no exception. As I collected more material, I found anchors that had lost their labels—they were just numbered, like &lt;code&gt;spk7&lt;/code&gt; through &lt;code&gt;spk18&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Files had missing names, some were renamed halfway through, and the mapping between the raw source directories and the extracted anchors became disconnected. For various reasons, the result is the same: you end up with "voice data that exists, but whose identity is unknown."&lt;/p&gt;

&lt;p&gt;If left alone, this kind of material tends to become "dead data"—ignored because the source is unclear. But the clue actually lies within the sound itself. &lt;strong&gt;Since we know the vocal characteristics, we can just match them by voice quality.&lt;/strong&gt; This article is a record of how I re-labeled unknown anchors by relying solely on speaker embedding similarity and rebuilt the &lt;code&gt;anchor_sources.json&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea: Matching by Voice, Not by Label
&lt;/h2&gt;

&lt;p&gt;Here is the breakdown of what I wanted to achieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a large amount of raw source material with labels (wav files where the speaker's name is in the filename).&lt;/li&gt;
&lt;li&gt;On the other hand, I have unlabeled anchors (&lt;code&gt;spk7&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;I want to match the two using &lt;strong&gt;voice quality&lt;/strong&gt; to restore the correct names to the anchors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is essentially classical speaker verification. Fortunately, this app already uses &lt;code&gt;campplus&lt;/code&gt; speaker embeddings for its anchor definitions. By measuring the cosine similarity within the same embedding space, I can determine "which raw source file is most similar to this anchor."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Source Side: Creating Robust Speaker Vectors
&lt;/h2&gt;

&lt;p&gt;First, I create a "gallery" to act as the comparison target. I extract one speaker vector from each raw source file. However, taking just a single 4-second window is unstable; if the window hits silence, a breath, or a stutter, the resulting vector won't represent that person's voice accurately.&lt;/p&gt;

&lt;p&gt;Instead, I extract embeddings from &lt;strong&gt;12 distributed windows&lt;/strong&gt; across the entire file, L2-normalize them, and then average them. I discard silent chunks based on their amplitude.&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;def&lt;/span&gt; &lt;span class="nf"&gt;_robust_embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;campplus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y16&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WIN_S&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SR16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;guard&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="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SR16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&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;y16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;guard&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;embs&lt;/span&gt; &lt;span class="o"&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&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;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N_WIN&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="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# N_WIN=12 windows
&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;y16&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&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;max&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="c1"&gt;# Discard silence
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;campplus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;embs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;/&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;e&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-9&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="c1"&gt;# Normalize each window
&lt;/span&gt;    &lt;span class="n"&gt;v&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;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embs&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&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;v&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-9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# Re-normalize the mean
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is the sequence: "Normalize, then average, then re-normalize the average." If you average without normalizing first, the result is pulled too heavily toward windows that happen to have a large norm. By converting them to unit vectors before averaging, you create a robust speaker vector that treats each window equally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anchor Side: Reusing Existing Embeddings
&lt;/h2&gt;

&lt;p&gt;For the anchors being compared, there is no need to calculate new embeddings. Since this project already saves all anchor embeddings in &lt;code&gt;anchor_embeddings.npz&lt;/code&gt; via &lt;code&gt;precompute_anchors.py&lt;/code&gt;, I just need to load and normalize them.&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;d&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;load&lt;/span&gt;&lt;span class="p"&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;path&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;DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchor_embeddings.npz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;allow_pickle&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="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;embs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&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;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;emap&lt;/span&gt; &lt;span class="o"&gt;=&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;embs&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="o"&gt;/&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;embs&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="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-9&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&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;names&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using the same vectors used for generation to perform the estimation, I maintain consistency: "Selection, definition, and estimation all using the same ruler."&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching: Top 3 Candidates and Confidence Levels
&lt;/h2&gt;

&lt;p&gt;Finally, I calculate the dot product (which equals cosine similarity since they are normalized) between the target anchor vector and the entire gallery, then look at the top results. If the gallery is stored as a matrix &lt;code&gt;G&lt;/code&gt;, a single &lt;code&gt;G @ v&lt;/code&gt; operation gives the similarity scores for all source files.&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;sims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;emap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;order&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;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;)[::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;cand&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="nf"&gt;join&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;glabels&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="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;sims&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="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;conf&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&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="mf"&gt;0.55&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&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="mf"&gt;0.45&lt;/span&gt; &lt;span class="k"&gt;else&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&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;conf&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;cand&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;The important thing is not to declare the result based on "only the top result." I list the top 3 candidates and use the best score to categorize the confidence into three levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;○ (≥0.55)&lt;/strong&gt;: Same level as the single-speaker threshold. Can be considered certain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;△ (0.45〜0.55)&lt;/strong&gt;: A candidate, but a gray zone where a human should decide by looking at the top 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;× (&amp;lt;0.45)&lt;/strong&gt;: No match in the gallery. Either a different person or the source material is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This threshold of &lt;code&gt;0.55&lt;/code&gt; is the same as &lt;code&gt;homo-thresh&lt;/code&gt; used to identify single speakers during anchor selection. I standardized the criteria for "closeness that can be called the same speaker" across both selection and labeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Pick "Number 1"?
&lt;/h2&gt;

&lt;p&gt;When people hear "automation," they often want to immediately adopt the top result, but in speaker verification, that leads to errors. There are real people with very similar voice qualities (e.g., deep-voiced men, high-pitched bright women), and source material recorded under similar conditions can look similar. If the scores for 1st and 2nd place are close, it is a signal that "the machine cannot decide."&lt;/p&gt;

&lt;p&gt;Therefore, I output the top 3 candidates with their scores and decided that anything marked as △ or below must be reviewed by a human. This isn't a stopgap; it is a &lt;strong&gt;division of labor&lt;/strong&gt;. The machine handles the vast number of obvious ○ cases, and the human only looks at the small number of truly ambiguous ones. This is orders of magnitude more efficient than listening to every single clip.&lt;/p&gt;

&lt;p&gt;Once an estimation is confirmed, I write it back to &lt;code&gt;anchor_sources.json&lt;/code&gt; (the mapping table of Anchor Name $\to$ Raw Source Filename). Having this mapping makes everything downstream much easier. Quality audits (&lt;code&gt;audit_anchor_quality.py&lt;/code&gt;) and distribution visualizations (&lt;code&gt;plot_anchor_distribution.py&lt;/code&gt;) can now use this table to display &lt;code&gt;spk24&lt;/code&gt; by its actual speaker name. Data that could only be referred to by numbers has been transformed into an asset that can be discussed by name.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust a single-window embedding.&lt;/strong&gt; A speaker vector should be a normalized average of multiple windows. If you are skewed by a single window containing silence or a stutter, the same person will look like someone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicitly show confidence and hand gray zones to humans.&lt;/strong&gt; Automatically adopting the #1 result will definitely cause issues with speakers of similar voice qualities. I created a division of labor between machine and human using "Top 3 + 3 levels of confidence."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the same embeddings and the same thresholds for estimation and definition.&lt;/strong&gt; By using the same &lt;code&gt;campplus&lt;/code&gt; and &lt;code&gt;0.55&lt;/code&gt; threshold for both, I prevent the discrepancy where "the machine says they are similar, but they don't sound similar in production."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels can be recovered from the voice.&lt;/strong&gt; Even if metadata is lost, the voice itself is primary information. Before discarding data because the source is unknown, it is worth questioning if you can match them by voice quality.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;I restored labels for unknown anchors using nothing but &lt;strong&gt;cosine similarity of speaker embeddings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;I created noise-resistant speaker vectors by calculating the normalized average of 12 windows per source file.&lt;/li&gt;
&lt;li&gt;I reused the existing &lt;code&gt;anchor_embeddings.npz&lt;/code&gt; for the anchors, ensuring estimation and definition use the same "ruler."&lt;/li&gt;
&lt;li&gt;I implemented a division of labor: Obvious cases are handled by the machine (○ ≥0.55), while ambiguous cases are sent to humans (△ ≥0.45).&lt;/li&gt;
&lt;li&gt;By writing the restored labels back to &lt;code&gt;anchor_sources.json&lt;/code&gt;, audits and visualizations can use names instead of numbers, turning dead data into a valuable asset.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seedvc</category>
    </item>
    <item>
      <title>In-Depth Explanation of the Seed-VC Architecture — Decomposing Voice into 'Who, What, and How' in a 4-Stage Structure</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:16:14 +0000</pubDate>
      <link>https://dev.to/orca_forge/in-depth-explanation-of-the-seed-vc-architecture-decomposing-voice-into-who-what-and-how-in-a-3hjh</link>
      <guid>https://dev.to/orca_forge/in-depth-explanation-of-the-seed-vc-architecture-decomposing-voice-into-who-what-and-how-in-a-3hjh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/seed-vc-architecture-voice-conversion/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=seed-vc-architecture-voice-conversion" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Understanding "Voice" — Breaking Down the Components
&lt;/h2&gt;

&lt;p&gt;When trying to convert your recorded voice into someone else's, the first question that comes to mind is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly &lt;em&gt;is&lt;/em&gt; a "voice"?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even if the same words are spoken, different speakers produce different sounds. Even the same speaker produces different sounds depending on what they're saying. Adding intonation changes it further. In other words, speech is a signal composed of multiple independent pieces of information mixed together — at least: &lt;strong&gt;who is speaking (speaker identity)&lt;/strong&gt;, &lt;strong&gt;what is being said (content)&lt;/strong&gt;, and &lt;strong&gt;how it's being said (prosody)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The core challenge of &lt;strong&gt;voice conversion (VC)&lt;/strong&gt; lies here. If you naively process speech to replace only the speaker identity, the content and intonation often get altered as well. In this article, we'll explain how &lt;strong&gt;Seed-VC&lt;/strong&gt;, which we adopted for the backend of our "voice design" app, solves this problem — by &lt;strong&gt;separating and handling information using four modules&lt;/strong&gt;, while walking through the actual model loading and inference code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Philosophy: Delegate Separable Information to Dedicated Modules
&lt;/h2&gt;

&lt;p&gt;At the heart of Seed-VC is the idea: &lt;em&gt;"Don't make a monolithic model do everything."&lt;/em&gt; Instead, decompose the components of speech by type and assign each to a dedicated module, then recombine them at the end. The model loading process at startup 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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;semantic_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f0_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vocoder_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;campplus_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mel_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mel_fn_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_models&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While seven components are returned, they can be grouped into four functional layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;whisper (&lt;code&gt;semantic_fn&lt;/code&gt;)&lt;/strong&gt; — Extracts &lt;strong&gt;content (what is being said)&lt;/strong&gt; as semantic features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;campplus (&lt;code&gt;campplus_model&lt;/code&gt;)&lt;/strong&gt; — Encodes &lt;strong&gt;speaker identity (who is speaking)&lt;/strong&gt; into a single vector embedding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CFM/DiT (&lt;code&gt;model.cfm&lt;/code&gt;)&lt;/strong&gt; — A diffusion model that generates a mel-spectrogram conditioned on the above two&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigVGAN (&lt;code&gt;vocoder_fn&lt;/code&gt;)&lt;/strong&gt; — Converts the mel-spectrogram back into an audible waveform&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each role — "extract meaning", "extract speaker", "draw the spectrogram", "convert to sound" — is cleanly separated. This division of labor is why we can replace only the speaker identity while keeping everything else intact. Let's go through each step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: whisper — Extract Only "What Is Being Said"
&lt;/h2&gt;

&lt;p&gt;For content extraction, we use the &lt;strong&gt;encoder&lt;/strong&gt; of the speech recognition model Whisper. We don't use the decoder (transcription). Instead, we take the intermediate representation (a sequence of semantic features) directly as a feature vector.&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;s_alt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_t&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A key point: the input is resampled to &lt;strong&gt;16kHz&lt;/strong&gt; before processing. Since Whisper is designed for 16kHz audio, even if the main conversion model operates at 44.1kHz, we always downsample to 16kHz before semantic extraction. The resulting sequence contains little to no information about pitch or timbre — it represents &lt;strong&gt;a representation aligned with the spoken content&lt;/strong&gt;. This is the prerequisite that allows us to freely replace speaker identity later.&lt;/p&gt;

&lt;p&gt;Note: Whisper has a limitation — it can only process up to 30 seconds of audio at a time. For longer audio, chunking is required. This constraint itself was a major pitfall (see our other article: &lt;a href="https://dev.to/your-link"&gt;"The Culprit Behind the 'Slow Speech' Bug in Voice Conversion Was Whisper's 30-Second Limit"&lt;/a&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: campplus — Condense "Who Is Speaking" into a 192-Dimensional Vector
&lt;/h2&gt;

&lt;p&gt;Speaker identity extraction is handled by &lt;strong&gt;CAMPPlus&lt;/strong&gt;, a speaker embedding model. It takes audio, computes fbank features, and outputs a &lt;strong&gt;single 192-dimensional vector&lt;/strong&gt;.&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;feat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kaldi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fbank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_mel_bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dither&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;sample_frequency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;feat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feat&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;feat&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;dim&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;keepdim&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_S&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campplus_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;feat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&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="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fact that "speaker identity = a single vector" is crucial to our "voice design" app. Because vectors can be &lt;strong&gt;added, blended, or interpolated&lt;/strong&gt;, we precompute embeddings from clean recordings of 18 speakers as "anchors", then blend them using weighted averages based on slider inputs.&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;design_weights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slider_values&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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="o"&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="nf"&gt;sum&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We define sliders for interpretable axes like "age", "pitch", and "huskiness", compute weights &lt;code&gt;w&lt;/code&gt;, and take a weighted average of anchor embeddings. This "design by blending voices" operation is only possible because speaker identity is modularized as a standalone vector. If speaker identity were entangled with content, blending would corrupt the spoken words.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: CFM/DiT — Use Diffusion to "Draw" the Mel-Spectrogram
&lt;/h2&gt;

&lt;p&gt;Once we have the semantic features (content) and speaker embedding (identity), we proceed to synthesis. This is where the diffusion model (CFM: Conditional Flow Matching; implemented as DiT) comes in. It starts from random noise and gradually generates a mel-spectrogram conditioned on the inputs.&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;vt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_S&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;cfm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LongTensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&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="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                               &lt;span class="n"&gt;mel2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&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="n"&gt;STEPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inference_cfg_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CFG&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;vt&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;mel2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the arguments reveals how this stage integrates outputs from previous modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cat&lt;/code&gt; — Semantic features (content). Concatenation of prompt audio (&lt;code&gt;pc&lt;/code&gt;) and target condition (&lt;code&gt;cond&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;style&lt;/code&gt; — Speaker embedding from Step 2 (= the blended design embedding)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mel2&lt;/code&gt; — Mel-spectrogram of the prompt audio. After generation, we trim the first &lt;code&gt;mel2.size(-1)&lt;/code&gt; frames to keep only the new content&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STEPS&lt;/code&gt; / &lt;code&gt;inference_cfg_rate&lt;/code&gt; — Number of diffusion steps and strength of classifier-free guidance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model receives &lt;strong&gt;content (&lt;code&gt;cat&lt;/code&gt;)&lt;/strong&gt; and &lt;strong&gt;speaker identity (&lt;code&gt;style&lt;/code&gt;)&lt;/strong&gt; as &lt;strong&gt;separate arguments&lt;/strong&gt;. It interprets them as instructions: &lt;em&gt;"Draw this content in this speaker's voice."&lt;/em&gt; If you fix one and change the other, only the intended attribute changes. The separation design is reflected directly in the inference interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Prosody: &lt;code&gt;length_regulator&lt;/code&gt; and F0
&lt;/h2&gt;

&lt;p&gt;"How it's being said" — prosody (tempo and pitch movement) is a bit different. It cannot be fully disentangled from content, so instead of a dedicated module, it's handled as a &lt;strong&gt;connection point&lt;/strong&gt; between content and speaker identity.&lt;/p&gt;

&lt;p&gt;First, tempo (duration). The semantic sequence from Whisper needs to be stretched to match the desired number of mel frames. This is handled by &lt;code&gt;length_regulator&lt;/code&gt;.&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;cond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_S&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="nf"&gt;length_regulator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_alt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ylens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LongTensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;mel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&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="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                        &lt;span class="n"&gt;n_quantizers&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="n"&gt;f0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By passing the target frame length (&lt;code&gt;ylens&lt;/code&gt;), the semantic sequence is adjusted to the desired duration. Next, pitch (F0). In a 44.1kHz F0-conditional model, &lt;code&gt;length_regulator&lt;/code&gt; can accept an &lt;code&gt;f0&lt;/code&gt; argument, enabling prosody control such as shifting the carrier voice's pitch range toward the target speaker's range.&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;lf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;F0_alt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;F0_alt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_alt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m_ori&lt;/span&gt;   &lt;span class="c1"&gt;# Shift carrier F0 to target median
&lt;/span&gt;&lt;span class="n"&gt;shifted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_S&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="nf"&gt;length_regulator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S_alt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ylens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tgt_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_quantizers&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="n"&gt;f0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shifted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple operation: shifting in the log domain by the difference in medians. Prosody isn't treated as a separate module but as an &lt;strong&gt;injected parameter&lt;/strong&gt;. This reflects a pragmatic compromise — attempting full disentanglement can lead to unnatural results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: BigVGAN — Convert Mel Back to Audible Sound
&lt;/h2&gt;

&lt;p&gt;The diffusion model generates a mel-spectrogram — a kind of "blueprint" of sound. The final step, converting it into a waveform humans can hear, is handled by the vocoder: BigVGAN.&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;wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_S&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vocoder_fn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;vt&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="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;cpu&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="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Though it's just one line, this step greatly impacts audio quality. Even with the same mel-spectrogram, different vocoders produce different sounds. Our 44.1kHz model can output "true broadband" because BigVGAN is trained at high sampling rates (see our other article: &lt;a href="https://dev.to/your-link"&gt;"22.05kHz vs 44.1kHz"&lt;/a&gt;).&lt;/p&gt;




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

&lt;p&gt;Here are some key lessons from implementing and operating this four-layer architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sampling rates differ per layer&lt;/strong&gt;. Semantic extraction is fixed at 16kHz, the conversion model operates at 22.05kHz or 44.1kHz, and speaker embeddings use 16kHz fbank. Misunderstanding which layer expects which rate silently degrades quality. Explicitly resampling at each stage is safest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speaker identity as a single vector unlocks applications&lt;/strong&gt;. If speaker identity were embedded inside the diffusion model, we couldn't implement "voice blending". Modular separation isn't just about quality — it directly enhances &lt;strong&gt;product expressiveness&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prosody cannot be fully separated — design accordingly&lt;/strong&gt;. Tempo and F0 are tied to content, so treating them as &lt;strong&gt;conditional parameters&lt;/strong&gt; injected via &lt;code&gt;length_regulator&lt;/code&gt; is the pragmatic solution. Forcing full disentanglement often leads to unnatural artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each layer is replaceable&lt;/strong&gt;. The vocoder can be swapped between BigVGAN, HiFiGAN, or Vocos. The semantic extractor can be swapped between Whisper and CNHuBERT. The modular design grants extensibility — each stage can be upgraded independently.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;Seed-VC's voice conversion uses a &lt;strong&gt;four-layer architecture&lt;/strong&gt;: &lt;code&gt;whisper (semantic) + campplus (speaker) + CFM/DiT (diffusion mel generation) + BigVGAN (vocoder)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The design philosophy is to &lt;strong&gt;decompose speech into "who, what, and how", assigning each to a dedicated module&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;By representing speaker identity as a &lt;strong&gt;single 192-dimensional vector&lt;/strong&gt;, we enable blending multiple speakers — the core of our "voice design" app&lt;/li&gt;
&lt;li&gt;The diffusion model receives content (&lt;code&gt;cat&lt;/code&gt;) and speaker identity (&lt;code&gt;style&lt;/code&gt;) as &lt;strong&gt;separate arguments&lt;/strong&gt; — the separation design is reflected in the inference interface&lt;/li&gt;
&lt;li&gt;Prosody (tempo and F0) is not fully disentangled; instead, it's injected as a &lt;strong&gt;conditional parameter&lt;/strong&gt; via &lt;code&gt;length_regulator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each layer operates at different sampling rates and roles, and is &lt;strong&gt;replaceable&lt;/strong&gt;. This modularity improves quality, expressiveness, and extensibility.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seedvc</category>
    </item>
    <item>
      <title>Focus on Root Cause Resolution Rather Than Quick Fixes: A Collection of Bug Investigation Case Studies</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 05 Aug 2026 01:14:15 +0000</pubDate>
      <link>https://dev.to/orca_forge/focus-on-root-cause-resolution-rather-than-quick-fixes-a-collection-of-bug-investigation-case-h8p</link>
      <guid>https://dev.to/orca_forge/focus-on-root-cause-resolution-rather-than-quick-fixes-a-collection-of-bug-investigation-case-h8p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/root-cause-over-band-aid-debugging/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=root-cause-over-band-aid-debugging" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you encounter a bug, the quickest fix is to "eliminate the symptoms." If an error occurs, wrap it in a &lt;code&gt;try-catch&lt;/code&gt; and swallow it. If it breaks only with a specific value, avoid that value via hardcoding. If the precision is off, boost it with a heuristic keyword to fake the result. All of these seem to work temporarily.&lt;/p&gt;

&lt;p&gt;However, these quick fixes will inevitably come back to bite you. Because the root cause remains alive, the same problem will resurface through a different entry point. Swallowed errors leak downstream in much more cryptic forms. Hardcoded conditions become landmines for the next developer making a change.&lt;/p&gt;

&lt;p&gt;When working with AI coding agents (like Claude Code), this temptation actually intensifies. Agents can suggest "fixes that work for now" at high speed. This is precisely why it is effective to &lt;strong&gt;explicitly impose a principle on the agent: "Ban quick fixes; always strive for the root cause resolution."&lt;/strong&gt; In this article, I will introduce a pattern for investigation—reaching the root cause without hiding the symptoms—using three bugs I actually encountered while developing a voice conversion app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Grand Principle: Eliminate the Root Cause, Not the Symptom
&lt;/h2&gt;

&lt;p&gt;First, let me establish the decision-making criteria that run through this article. When a proposed fix is presented, ask yourself the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you eliminating the &lt;strong&gt;symptom&lt;/strong&gt; or the &lt;strong&gt;root cause&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Will this fix also eliminate &lt;strong&gt;other symptoms&lt;/strong&gt; derived from the same root cause?&lt;/li&gt;
&lt;li&gt;Can you explain &lt;strong&gt;why the fix works&lt;/strong&gt; in a single sentence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third point is particularly crucial. A fix that you cannot explain is usually just hiding a symptom. Saying "If we avoid this value, it won't crash" is not an explanation. Saying "It crashes because the assumption of [X] breaks when this value is provided; therefore, I made it so the assumption is always met" &lt;em&gt;is&lt;/em&gt; an explanation.&lt;/p&gt;

&lt;p&gt;Let's look at three real-world examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 1: Converted Audio "Speaks Slowly" — Proportionality Points to the Root Cause
&lt;/h2&gt;

&lt;p&gt;The first symptom was that only the voice-converted audio would play back with an unnaturally stretched cadence. The input recording was at a normal speed, but the output sounded like a slow, drunken speech.&lt;/p&gt;

&lt;p&gt;One could think of endless quick fixes. For example, applying time-stretching to the output to force it back to normal speed. However, that explains nothing about &lt;em&gt;why&lt;/em&gt; it was slow in the first place.&lt;/p&gt;

&lt;p&gt;What worked here was the &lt;strong&gt;observation of proportionality&lt;/strong&gt;. The issue didn't occur with short audio files, only with long recordings. Moreover, the longer the input, the slower the output became. A 131-second recording resulted in playback over 4 times slower than normal—this clue, that the "issue worsens in proportion to length," pointed me directly to the location of the root cause.&lt;/p&gt;

&lt;p&gt;If it were a sampling rate mismatch, the audio would be consistently slow by a fixed ratio, regardless of length. The same applies to a time-stretch bug. A proportional relationship where "the issue scales with length" only exists when &lt;strong&gt;a fixed-length segment is being stretched to fit the total duration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The root cause was the "30-second limit" of the Whisper encoder used for feature extraction. When I passed a 131-second recording, it only retrieved the content for the first 30 seconds. Since that 30-second chunk was being stretched to 131 seconds, it became 131 ÷ 30 ≒ 4.4x slower. This matched my "over 4x slower" perception perfectly.&lt;/p&gt;

&lt;p&gt;The solution was to split the audio into overlapping 30-second chunks, run each through Whisper, and concatenate the results. This wasn't a symptomatic time-stretch; it was a fix at the source—&lt;strong&gt;ensuring correct information is obtained during the feature extraction stage.&lt;/strong&gt; I documented the technical details of this investigation in a separate article: "The culprit behind the 'low speech' bug in voice conversion was Whisper's 30-second limit."&lt;/p&gt;

&lt;p&gt;The lesson here is simple: &lt;strong&gt;Proportionality is an arrow to the root cause.&lt;/strong&gt; If you measure what the symptom scales with, you can mechanically narrow down the suspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 2: 502 Errors During Long ML Inference — Don't "Extend" Timeouts, "Change the Mechanism"
&lt;/h2&gt;

&lt;p&gt;Next was a bug where attempting to generate long audio with a 44.1kHz wideband model resulted in a 502 error at the frontend. Short audio worked fine, but if generation took too long, it inevitably resulted in a 502.&lt;/p&gt;

&lt;p&gt;The easiest quick fix is to set the timeout value to a massive number. However, this is a classic symptomatic treatment: tinkering with numbers without understanding &lt;em&gt;why&lt;/em&gt; the connection is dropping. Even if you increase the number, it will just crash again once an input exceeds that new limit. You've just postponed the landmine.&lt;/p&gt;

&lt;p&gt;By chasing the root cause, I discovered that when the Next.js server relayed requests to the inference backend, the internal &lt;code&gt;fetch&lt;/code&gt; implementation (undici) had a default timeout. It was closing the connection because it couldn't wait for the long-running response. The 502 was the result of the proxy layer giving up while the upstream server was still alive.&lt;/p&gt;

&lt;p&gt;This is where the decision path diverges. "Disabling the undici timeout" would technically work, but the more robust solution was to &lt;strong&gt;replace the proxy relay with Node's standard http/https and allow unlimited waiting for a response.&lt;/strong&gt; Given the nature of long-running inference, the very premise of "cutting off after a certain time" was incompatible with this endpoint. Therefore, the fix was to change the implementation so that this assumption was removed—treating the root cause.&lt;/p&gt;

&lt;p&gt;The lesson here is: &lt;strong&gt;When you feel the urge to tinker with "numbers" like timeouts or retry counts, stop and ask if this is just symptomatic treatment.&lt;/strong&gt; In many cases, you shouldn't be adjusting the number; you should be questioning "why is the architecture designed such that this limit exists?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 3: Massive Model Download Stalls — Don't "Ignore and Proceed," "Ensure Placement"
&lt;/h2&gt;

&lt;p&gt;The third issue involved the process of fetching multi-gigabyte model weights from HuggingFace stalling halfway through. In environments with unstable networks, the download would simply stop silently and hang.&lt;/p&gt;

&lt;p&gt;The temptation for a quick fix here was to "swallow the download failure and attempt to continue starting the app." However, if you attempt inference with incomplete model weights, you'll just encounter much more confusing errors later in the pipeline. Swallowing the error merely hides the problem; it doesn't solve it.&lt;/p&gt;

&lt;p&gt;The root cause was that the standard downloader &lt;strong&gt;could not detect "stalling" (silently stopping); once it got stuck, it couldn't recover on its own.&lt;/strong&gt; It didn't crash, and it didn't return an error; it just sat there silently. This meant there was nothing to "swallow"—no exception was being thrown in the first place.&lt;/p&gt;

&lt;p&gt;The solution was to &lt;strong&gt;use a downloader that supports stall detection and resumption (using specific &lt;code&gt;curl&lt;/code&gt; options) and ensure the artifacts are reliably placed in the HuggingFace cache directory.&lt;/strong&gt; If no data flows for a certain period, the process treats it as a failure, interrupts, and resumes from where it left off. This guarantees that a complete file eventually lands in the cache, even on unstable networks.&lt;/p&gt;

&lt;p&gt;The lesson here is: &lt;strong&gt;"Silent stalling" is more troublesome than "crashing with an error."&lt;/strong&gt; You cannot swallow an exception that is never thrown. The correct approach is to provide a reliable acquisition mechanism and define "completion" as the moment the artifact is successfully and accurately placed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Pattern Found in These 3 Cases
&lt;/h2&gt;

&lt;p&gt;While these are three different bugs, the pattern used to reach the root cause is the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the "Effectiveness" of the Symptom&lt;/strong&gt; — As in Case 1 ("proportional to length"), observe what the symptom scales with. Proportionality, boundaries, and reproduction conditions are direct arrows to the root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate the "Likely Suspects" First&lt;/strong&gt; — Like testing sampling rates or time-stretching, eliminate suspicious candidates based on observed facts. What remains points to the root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use "Can you explain why it works in one sentence?" as a Gatekeeper&lt;/strong&gt; — If a fix cannot be explained, suspect it is merely hiding a symptom. In Case 2, "increasing the timeout" was not an explanation, so it failed the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be Wary of Tinkering with Numbers and Swallowing Errors&lt;/strong&gt; — Increasing timeout values (Case 2) or swallowing errors (Case 3) are classic signals of symptomatic treatment. If you reach for them, stop and think.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practicality When Working with AI Agents
&lt;/h2&gt;

&lt;p&gt;This principle is worth institutionalizing specifically when working with AI coding agents. Because agents can rapidly mass-produce quick fixes, if left unchecked, you will end up with a mountain of code that "works, but only hides the symptoms."&lt;/p&gt;

&lt;p&gt;What is effective is to define a permanent instruction for the agent: "&lt;strong&gt;Prohibit quick fixes. Follow this sequence: Identify root cause $\rightarrow$ Appropriate technology selection $\rightarrow$ Propose design-level solution $\rightarrow$ Implementation.&lt;/strong&gt;" Then, when a proposal is made, the human must put it through the gate: "Is this the symptom or the root cause?" and "Can you explain why it works in one sentence?" Agent productivity and the discipline of root-cause resolution can coexist.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Quick fixes (swallowing errors with &lt;code&gt;try-catch&lt;/code&gt; / escaping via hardcoding / faking with heuristics) preserve the root cause and will inevitably recur.&lt;/li&gt;
&lt;li&gt;Evaluate fix proposals using two gates: "Is this eliminating the symptom or the root cause?" and "Can you explain why it works in one sentence?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 1:&lt;/strong&gt; The &lt;strong&gt;proportionality&lt;/strong&gt; of the symptom pointed to the root cause (Whisper's 30-second limit). Proportionality is an arrow to the root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 2:&lt;/strong&gt; The 502 during long inference was solved not by &lt;strong&gt;increasing timeouts&lt;/strong&gt;, but by changing to a relay mechanism that doesn't cut off by time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 3:&lt;/strong&gt; The stall in massive downloads was solved not by &lt;strong&gt;swallowing the error&lt;/strong&gt;, but by using stall detection + resumption to ensure the artifact is placed reliably.&lt;/li&gt;
&lt;li&gt;AI agents are prone to mass-producing quick fixes. Make "&lt;strong&gt;strive for root-cause resolution&lt;/strong&gt;" a permanent instruction, and use humans to act as the gatekeepers.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Creating Perceivable Vibrato/Pitch Fluctuations</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:40:56 +0000</pubDate>
      <link>https://dev.to/orca_forge/creating-perceivable-vibratopitch-fluctuations-4jcn</link>
      <guid>https://dev.to/orca_forge/creating-perceivable-vibratopitch-fluctuations-4jcn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/perceptible-vibrato-delay-modulation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=perceptible-vibrato-delay-modulation" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Adding a Vibrato Slider to the Voice Design App
&lt;/h2&gt;

&lt;p&gt;We added a "pitch variation" slider to the "voice design" app to reduce the robotic feeling of the voice and add a natural vibrato. The implementation was straightforward, and with a sine wave, we could verify that the pitch was indeed fluctuating periodically. However, when we applied it to an actual voice, even with the slider set to maximum, the difference was barely audible to the ear. This is a typical problem in DSP, where the parameters are correct, but the effect is not perceivable.&lt;/p&gt;

&lt;p&gt;This article discusses the mechanism of creating vibrato using delay modulation and the design of modulation depth to achieve a perceivable effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise: Why Delay Modulation Makes Pitch Fluctuate
&lt;/h2&gt;

&lt;p&gt;There are several ways to fluctuate pitch, but here, we used a time-varying delay (fractional delay). This is the same principle as chorus and flanger effects.&lt;/p&gt;

&lt;p&gt;The key point is that it's not the delay time itself, but the rate of change of the delay time that fluctuates the pitch. As the delay quantity increases over time, the playback position slowly moves backward, causing the pitch to decrease due to the Doppler effect. Conversely, if the delay quantity decreases, the pitch increases. By making the delay quantity oscillate with a sine wave, the pitch fluctuates up and down, creating a vibrato.&lt;/p&gt;

&lt;p&gt;Mathematically, the instantaneous pitch change is roughly proportional to &lt;code&gt;1 - d(delay)/dt&lt;/code&gt;. Therefore, both the speed of the fluctuation (modulation frequency) and the depth of the fluctuation (maximum delay amount) determine the strength of the vibrato.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Oscillating Delay Quantity with a Sine Wave
&lt;/h2&gt;

&lt;p&gt;The implementation (&lt;code&gt;_apply_pitch_variation&lt;/code&gt;) is as follows:&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;n&lt;/span&gt; &lt;span class="o"&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;wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t&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;arange&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="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;
&lt;span class="n"&gt;rate_hz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;
&lt;span class="n"&gt;max_delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0030&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;           &lt;span class="c1"&gt;# maximum ~3.0ms (100% for ± approximately 65 cents of vibrato)
&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;cos&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate_hz&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;idx&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;clip&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;arange&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;delay&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;n&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="n"&gt;i0&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&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="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i0&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="n"&gt;i1&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;minimum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i0&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="n"&gt;n&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rate_hz = 4.5&lt;/code&gt; is the &lt;strong&gt;vibrato speed&lt;/strong&gt;. Human natural vibrato is around 4-7Hz, so we placed it near the lower end. If it's too fast, it sounds like a tremolo, and if it's too slow, it sounds out of tune.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delay&lt;/code&gt; oscillates between 0 and &lt;code&gt;max_delay&lt;/code&gt; with a sine wave. The delay quantity oscillates, so its derivative, the pitch, fluctuates up and down.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;idx&lt;/code&gt; is the "current sample position". It shifts backward by the delay amount. Since the position is not an integer, it becomes a &lt;strong&gt;fractional delay&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The last three lines are &lt;strong&gt;linear interpolation&lt;/strong&gt;. We interpolate between &lt;code&gt;i0&lt;/code&gt; (integer part) and &lt;code&gt;i1&lt;/code&gt; (next sample) with &lt;code&gt;frac&lt;/code&gt; (fractional part). This interpolation is necessary to achieve fractional delay; without it, the sound becomes jittery and noisy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;amt&lt;/code&gt; (0-1) linearly affects &lt;code&gt;max_delay&lt;/code&gt;. Currently, the setting is a maximum delay of ~3.0ms at 100% slider value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Topic: Solving the "Inaudible" Problem with Depth
&lt;/h2&gt;

&lt;p&gt;In the initial implementation, &lt;code&gt;max_delay&lt;/code&gt; was &lt;strong&gt;1.0ms&lt;/strong&gt;. Verification with a sine wave was normal, but the effect was not perceivable with an actual voice.&lt;/p&gt;

&lt;p&gt;The reason was simple: &lt;strong&gt;the modulation was too shallow&lt;/strong&gt;. From the above relationship, the modulation depth was approximately ±24 cents. Since one half-tone equals 100 cents, 24 cents is less than a quarter of a half-tone. While it's noticeable with a sine wave, it's not perceivable with an actual voice that has overtones and formants. The cause of the "existent but not perceivable" problem was this shallow modulation.&lt;/p&gt;

&lt;p&gt;We increased the &lt;strong&gt;maximum delay from 1.0ms to 3.0ms&lt;/strong&gt;. This resulted in a vibrato depth of ± approximately 65 cents at 100% slider value, which was clearly perceivable. This change is also reflected in the git history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fix: Pitch fluctuation is not perceivable. Increased modulation depth from 1.0ms to 3.0ms (100% for ± approximately 65 cents)
Sine wave verification showed the effect was present, but ±24 cents was too subtle and not perceivable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interestingly, &lt;strong&gt;even with increased depth, it doesn't sound out of tune&lt;/strong&gt;. As long as the speed (4.5Hz) remains within the natural vibrato range, increasing the depth doesn't make it sound out of tune; instead, it's perceived as a "fluctuating expression." The roles are divided: depth (max_delay) makes the effect stronger, and speed (rate_hz) ensures it sounds natural.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sine wave verification only shows if it's working&lt;/strong&gt;, but it doesn't guarantee the effect is perceivable with actual audio material. DSP requires separate evaluations for "operation correctness" and "perceivable effect".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perception is non-linear&lt;/strong&gt;. In terms of the perceived pitch scale (cents), ±24 cents and ±65 cents differ by a factor of roughly 2.7, but the perceived difference is qualitative, changing from "imperceptible" to "clearly audible". Parameters should be designed based on &lt;strong&gt;perceived quantities, not physical quantities&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate the roles of depth and speed&lt;/strong&gt;. Effect strength is determined by depth (max_delay), while naturalness is ensured by speed (rate_hz). Adjusting both simultaneously makes it difficult to distinguish between their effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fractional delay requires linear interpolation&lt;/strong&gt;. Rounding to the nearest sample introduces jitter and aliasing, making the sound noisy and ruining the vibrato effect.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Vibrato, or pitch fluctuation, can be created using &lt;strong&gt;time-varying delay (fractional delay)&lt;/strong&gt;. The pitch is fluctuated not by the delay itself, but by the &lt;strong&gt;rate of change of the delay&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The speed (&lt;code&gt;rate_hz&lt;/code&gt;) should be within the natural vibrato range (4-7Hz), and the delay quantity should oscillate with a sine wave. Reading is done using &lt;strong&gt;linear interpolation&lt;/strong&gt; for fractional delay.&lt;/li&gt;
&lt;li&gt;The main cause of the "existent but not perceivable" problem was &lt;strong&gt;insufficient modulation depth&lt;/strong&gt;. ±24 cents was too subtle, while ±65 cents was clearly perceivable.&lt;/li&gt;
&lt;li&gt;Increasing depth doesn't make it sound out of tune if the speed remains within the natural range. &lt;strong&gt;Depth makes the effect stronger, and speed ensures naturalness&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Verifying operation with a sine wave and verifying perception with actual audio material are distinct. Parameters should be designed based on &lt;strong&gt;perceived quantities (cents)&lt;/strong&gt;, not physical quantities.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dsp</category>
    </item>
    <item>
      <title>Using Multiple AI Agents to Review UI Fidelity to Custom Designs</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:10:34 +0000</pubDate>
      <link>https://dev.to/orca_forge/using-multiple-ai-agents-to-review-ui-fidelity-to-custom-designs-496d</link>
      <guid>https://dev.to/orca_forge/using-multiple-ai-agents-to-review-ui-fidelity-to-custom-designs-496d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/multi-agent-ui-design-review/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=multi-agent-ui-design-review" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Have you ever been hit by a wave of anxiety after implementing a UI based on a mockup? You might think, "I tried to build it as faithfully as possible, but is it &lt;em&gt;really&lt;/em&gt; matching the design?" As the person doing the implementation, it is easy to overlook small compromises made during coding or details missed in the mockup. This is the inherent limitation of self-reviewing your own code.&lt;/p&gt;

&lt;p&gt;When humans work together, having multiple people involved increases the number of perspectives. My approach—which I introduce in this article—is to &lt;strong&gt;replicate this by running multiple AI agents&lt;/strong&gt;. By having several independent review agents evaluate how faithfully your implementation matches the mockup (in terms of layout structure, positioning, and component mapping), you can reinforce the single review with multi-angled verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "One" is Not Enough
&lt;/h2&gt;

&lt;p&gt;Self-reviewing by the original implementer has structural weaknesses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirmation Bias&lt;/strong&gt; — Since you already have the memory of "how you built it," you tend to view the mockup in a way that justifies your own implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduction of Oversights&lt;/strong&gt; — If you missed a specific element in the mockup during implementation, you are likely to miss it again during review for the same reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed Perspectives&lt;/strong&gt; — Being alone can lead to cognitive bias; for example, you might focus too much on "are the colors correct?" while overlooking the "hierarchical structure of the layout."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if you ask a single AI agent to "compare the mockup and the implementation," you will encounter similar issues, albeit to a lesser degree. A single response tends to lean toward one perspective; if you ask it to look at too many things at once, each individual check becomes shallow.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;split the perspectives and assign them to multiple agents&lt;/strong&gt;. Instead of asking one agent to look at everything, assign each agent a specialized role and have them run independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow: Parallel Review via Divided Perspectives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Define the Review "Axes"
&lt;/h3&gt;

&lt;p&gt;First, break down the criteria for evaluating mockup fidelity. For UI design adherence, you might divide it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout Structure&lt;/strong&gt; — Does the overall grid, column configuration, and area division match? (e.g., the skeleton: a 2-column layout where the right column is further split vertically).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element Placement and Hierarchy&lt;/strong&gt; — Which elements are placed in which area, and in what order/nesting?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component Mapping&lt;/strong&gt; — Are all the parts from the mockup (sliders, charts, previews, lists, etc.) present in the implementation without excess or deficiency?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Tone&lt;/strong&gt; — The visual quality, such as color, spacing, and typography.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is to explicitly separate these axes. Because each agent focuses solely on its assigned axis, it can perform a deep dive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Assign Independent Agents to Each Axis
&lt;/h3&gt;

&lt;p&gt;Launch one specialized review agent for each axis. While you provide the same materials to everyone—the mockup (image or specs) and the implementation (the relevant component code or the actual rendered screen)—the &lt;strong&gt;instructions (prompts) must be specialized for the assigned axis&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, for the Layout Structure agent, you might instruct: "Ignore colors and text. Only compare the structural division of areas between the mockup and the implementation and list any discrepancies." This forces the agent to focus strictly on the skeleton without getting distracted by other details.&lt;/p&gt;

&lt;p&gt;Crucially, you must run the agents &lt;strong&gt;independently&lt;/strong&gt;. If they can see each other's conclusions, they will be influenced by one another, defeating the purpose of having independent perspectives. This is the same logic used in human code reviews: writing down your own comments before looking at what others have said.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reconcile and Integrate Findings
&lt;/h3&gt;

&lt;p&gt;A human (or an integration agent) then reconciles the feedback provided by each agent. You should look for three main types of findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Findings reported by multiple agents independently&lt;/strong&gt; — High reliability. These should be prioritized for fixing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Findings reported by only one agent&lt;/strong&gt; — This is either a caught oversight or a deep insight unique to that specific axis. Evaluate these carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflicting findings between agents&lt;/strong&gt; — This indicates a discrepancy in how the mockup is being interpreted. This is where a human makes the final decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a single reviewer, you can never get the powerful signal of "multiple independent sources reporting the same issue." This is the greatest value of running multiple agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fix and Re-review
&lt;/h3&gt;

&lt;p&gt;Once the issues are addressed, run the same group of agents again. This ensures that the fixes haven't introduced new discrepancies. In practice, for large changes—like restructuring a layout from a single column to a two-column setup—it rarely becomes perfect on the first try, making this iterative process essential.&lt;/p&gt;

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

&lt;p&gt;Here are some observations from running this workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dividing the axes is essential&lt;/strong&gt; — If you give every agent the same prompt ("Check if this matches the mockup"), you'll just get multiple superficial, similar responses. Dividing the perspectives is the essence of the method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep materials consistent&lt;/strong&gt; — Ensure the mockup and implementation snapshots are identical for every agent. Otherwise, you won't be able to reconcile their feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect agent independence&lt;/strong&gt; — Do not let them share conclusions. The moment they do, you move from "independent perspectives" to "majority rule/conformity."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans hold final authority&lt;/strong&gt; — Discrepancies between agents often visualize "ambiguity in the mockup interpretation." Instead of automating this away, let humans decide the intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Providing the actual screen is powerful&lt;/strong&gt; — Simply reading static code might miss discrepancies in the actual rendering. Whenever possible, include the rendered screen as a material (I will cover the method of showing real screens via browser automation in another article).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Self-reviewing is prone to confirmation bias and narrow perspectives, making it easy to miss mockup discrepancies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divide the evaluation into axes&lt;/strong&gt; (Layout Structure / Placement &amp;amp; Hierarchy / Component Mapping / Visual Tone) and run &lt;strong&gt;independent review agents&lt;/strong&gt; for each.&lt;/li&gt;
&lt;li&gt;By using the same materials but &lt;strong&gt;specialized prompts&lt;/strong&gt;, ensure agents evaluate &lt;strong&gt;without seeing each other's conclusions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Findings reported by multiple independent agents are highly reliable. Conflicts highlight mockup ambiguity for human judgment.&lt;/li&gt;
&lt;li&gt;After fixing, re-review with the same agents to finalize the implementation.&lt;/li&gt;
&lt;li&gt;The goal of multi-agent review is to reinforce a single review with multiple, independent perspectives.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>ui</category>
    </item>
    <item>
      <title>Tips for Running Stable Background ML Inference on macOS</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 04 Aug 2026 01:08:21 +0000</pubDate>
      <link>https://dev.to/orca_forge/tips-for-running-stable-background-ml-inference-on-macos-26dc</link>
      <guid>https://dev.to/orca_forge/tips-for-running-stable-background-ml-inference-on-macos-26dc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/macos-background-ml-inference-ops-tips/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=macos-background-ml-inference-ops-tips" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running an inference service as a background process on macOS, with a Linux server mindset, can lead to subtle issues. Things like "a one-liner that works on Linux doesn't work on Mac" or "grepping logs results in garbled text errors and crashes" — these are minor but time-consuming problems.&lt;/p&gt;

&lt;p&gt;This article compiles a collection of short tips gathered from running a Seed-VC based voice conversion service (FastAPI + uvicorn, local &lt;code&gt;127.0.0.1:8770&lt;/code&gt;) as a background process on macOS. It focuses on macOS-specific pitfalls not covered in Linux-centric articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 1: &lt;code&gt;setsid&lt;/code&gt; / &lt;code&gt;timeout&lt;/code&gt; are not available on macOS
&lt;/h2&gt;

&lt;p&gt;First, it's important to note that &lt;strong&gt;macOS (BSD-based) does not include GNU coreutils' &lt;code&gt;setsid&lt;/code&gt; or &lt;code&gt;timeout&lt;/code&gt; by default&lt;/strong&gt;. If you use these commands, which are used for backgrounding and timed execution on Linux, directly in a Mac script, you'll get a &lt;code&gt;command not found&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;There are two solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;coreutils&lt;/code&gt; via Homebrew and use &lt;code&gt;gsetsid&lt;/code&gt;/&lt;code&gt;gtimeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use standard tools as alternatives (next TIP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For background scripts that avoid external dependencies, using standard tools as alternatives is a safer option.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 2: Use &lt;code&gt;nohup&lt;/code&gt; + &lt;code&gt;disown&lt;/code&gt; for background processes
&lt;/h2&gt;

&lt;p&gt;In environments without &lt;code&gt;setsid&lt;/code&gt;, the combination of &lt;code&gt;nohup&lt;/code&gt; and &lt;code&gt;disown&lt;/code&gt; is reliable for keeping processes alive even after closing the shell.&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="c"&gt;# Run the inference service as a background process&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;bash scripts/start-backend.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backend.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nb"&gt;disown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nohup&lt;/code&gt; ... Ignores hangup signals (SIGHUP), keeping the process alive even after the terminal is closed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; backend.log 2&amp;gt;&amp;amp;1&lt;/code&gt; ... Redirects standard output and standard error to a log file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&lt;/code&gt; ... Runs the process in the background&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disown&lt;/code&gt; ... Removes the job from the shell's job table, preventing it from being terminated when the shell is closed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While &lt;code&gt;nohup&lt;/code&gt; alone usually keeps the process alive, adding &lt;code&gt;disown&lt;/code&gt; ensures that closing the terminal won't accidentally terminate the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 3: &lt;code&gt;tr&lt;/code&gt; / &lt;code&gt;grep&lt;/code&gt; fail with binary data in logs → use &lt;code&gt;LC_ALL=C&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This was the most problematic issue on Mac. Inference logs may contain progress bar control characters or, occasionally, garbled multibyte sequences. When processed by macOS's &lt;code&gt;tr&lt;/code&gt; or &lt;code&gt;grep&lt;/code&gt;, you'll see:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;tr: Illegal byte sequence&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This happens because the locale is set to UTF-8, causing invalid byte sequences to be treated as "invalid characters" and throwing an exception.&lt;/p&gt;

&lt;p&gt;The solution is to set the locale to &lt;strong&gt;C (pass-through as byte sequences)&lt;/strong&gt; for those commands.&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="c"&gt;# Remove unwanted control characters from logs (avoids Illegal byte sequence)&lt;/span&gt;
&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt; &amp;lt; backend.log &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backend.clean.log
&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"ERROR"&lt;/span&gt; backend.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;LC_ALL=C&lt;/code&gt; treats text as "bytes" rather than "characters," preventing crashes due to invalid sequences. This is safer for pipelines that process or search logs programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 4: Wait for startup completion using the health endpoint
&lt;/h2&gt;

&lt;p&gt;Loading models takes time, so sending requests immediately after starting with &lt;code&gt;nohup&lt;/code&gt; will fail because the service isn't ready. Using &lt;code&gt;sleep 10&lt;/code&gt; as a workaround is unreliable—too short for slow machines and too long for fast ones.&lt;/p&gt;

&lt;p&gt;The proper approach is to &lt;strong&gt;poll the service's health endpoint until it returns a 200 status&lt;/strong&gt;. In this setup, the health endpoint is &lt;code&gt;http://127.0.0.1:8770/health&lt;/code&gt;, so we poll it.&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="c"&gt;# Wait for the health endpoint to be ready before proceeding&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; http://127.0.0.1:8770/health &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"backend ready"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;curl -sf&lt;/code&gt; exits with a non-zero status on failure, so combining it with &lt;code&gt;until&lt;/code&gt; allows you to wait until the service is ready. Waiting based on &lt;strong&gt;state&lt;/strong&gt;, not a fixed delay, significantly improves the reliability of startup scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 5: Stop processes with &lt;code&gt;pkill&lt;/code&gt; using pattern matching
&lt;/h2&gt;

&lt;p&gt;For background processes without a saved PID, &lt;code&gt;pkill&lt;/code&gt; with a command-line pattern is convenient for stopping them.&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="c"&gt;# Stop the inference service started with uvicorn&lt;/span&gt;
pkill &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"uvicorn server:app"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; option matches the entire command line, so including specific details like the port or app name in the pattern prevents unrelated processes from being affected. For more precision, save the PID during startup and target it directly.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;macOS &lt;strong&gt;does not have &lt;code&gt;setsid&lt;/code&gt;/&lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt;. Either install Homebrew's &lt;code&gt;coreutils&lt;/code&gt; (&lt;code&gt;gsetsid&lt;/code&gt;/&lt;code&gt;gtimeout&lt;/code&gt;) or use standard tool alternatives&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;nohup ... &amp;amp; disown&lt;/code&gt;&lt;/strong&gt; for background processes. Ignore SIGHUP and remove the job from the job table to prevent termination when the terminal is closed&lt;/li&gt;
&lt;li&gt;Binary data in logs causes &lt;code&gt;tr&lt;/code&gt;/&lt;code&gt;grep&lt;/code&gt; to fail with &lt;strong&gt;&lt;code&gt;Illegal byte sequence&lt;/code&gt;&lt;/strong&gt; → Temporarily set &lt;strong&gt;&lt;code&gt;LC_ALL=C&lt;/code&gt;&lt;/strong&gt; to treat data as byte sequences&lt;/li&gt;
&lt;li&gt;Instead of fixed &lt;code&gt;sleep&lt;/code&gt; delays, &lt;strong&gt;poll the health endpoint with &lt;code&gt;until curl -sf&lt;/code&gt;&lt;/strong&gt; to wait based on state&lt;/li&gt;
&lt;li&gt;Stop processes with &lt;strong&gt;&lt;code&gt;pkill -f "specific pattern"&lt;/code&gt;&lt;/strong&gt;. Use a detailed pattern to avoid affecting unrelated processes&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>machinelearning</category>
      <category>devops</category>
    </item>
    <item>
      <title>HuggingFace's Large File Downloads Keep Stopping — Resuming with curl for Reliable Retrieval</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:37:44 +0000</pubDate>
      <link>https://dev.to/orca_forge/huggingfaces-large-file-downloads-keep-stopping-resuming-with-curl-for-reliable-retrieval-43ni</link>
      <guid>https://dev.to/orca_forge/huggingfaces-large-file-downloads-keep-stopping-resuming-with-curl-for-reliable-retrieval-43ni</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/huggingface-large-file-download-curl-resume/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=huggingface-large-file-download-curl-resume" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Development of a Voice Conversion App: Overcoming Seed-VC Model Download Issues
&lt;/h2&gt;

&lt;p&gt;When developing a voice conversion app, I encountered an issue while trying to download the 44.1kHz Seed-VC model set (DiT weights, rmvpe, and BigVGAN vocoder) using &lt;code&gt;huggingface_hub&lt;/code&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When attempting to download models using &lt;code&gt;huggingface_hub&lt;/code&gt;, the transfer would stop halfway through large files (hundreds of MB) and never progress.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While small &lt;code&gt;config.json&lt;/code&gt; files would download quickly, larger &lt;code&gt;.pth&lt;/code&gt; or &lt;code&gt;.pt&lt;/code&gt; files would get stuck at a certain point and freeze. This article describes how to use &lt;strong&gt;curl's stagnation detection and automatic resumption&lt;/strong&gt; to ensure successful downloads, and create a &lt;strong&gt;custom Hugging Face cache structure&lt;/strong&gt; to enable model reading even when &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms: Only Large Files Freeze Silently
&lt;/h2&gt;

&lt;p&gt;On my machine, large file transfers using &lt;code&gt;hf_hub_download&lt;/code&gt; would stall halfway through. The suspected causes include IPv6 route unavailability and connection drops over extended periods. The issues are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It doesn't fail with an exception (and retry doesn't work)&lt;/li&gt;
&lt;li&gt;The progress bar freezes&lt;/li&gt;
&lt;li&gt;Small files don't reproduce the issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a matter of a slow network, but rather large transfers dying under specific conditions, so simply increasing the retry count won't solve the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach: Use curl for Transfers and Create a Custom HF-Compatible Structure
&lt;/h2&gt;

&lt;p&gt;The appeal of &lt;code&gt;huggingface_hub&lt;/code&gt; lies in its &lt;strong&gt;cache management&lt;/strong&gt;, not just as a downloader. &lt;code&gt;load_custom_model_from_hf&lt;/code&gt; and &lt;code&gt;from_pretrained&lt;/code&gt; assume a specific directory structure (blob entities + symbolic links + refs) and will re-download the model if this structure is broken.&lt;/p&gt;

&lt;p&gt;To address this, I adopted a two-step approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use curl to handle transfers&lt;/strong&gt; (with stagnation detection and automatic resumption)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manually configure the downloaded entities in an HF cache-compatible structure&lt;/strong&gt; (enabling subsequent references by &lt;code&gt;hf_hub&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Metadata (etag, commit hash, size, and entity URL) can be retrieved from the &lt;code&gt;huggingface_hub&lt;/code&gt; API to construct the cache structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using curl: Detecting Stagnation and Automatic Resumption
&lt;/h2&gt;

&lt;p&gt;The key is combining curl options:&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;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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;curl&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;-L&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;--fail&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;-C&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;-&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;--retry&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;50&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;--retry-delay&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;3&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;--retry-all-errors&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;--speed-time&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;20&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;--speed-limit&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;2000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# 20 seconds at 2KB/s or lower, consider transfer failed and retry
&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;check&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The roles of each option are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--speed-time 20 --speed-limit 2000&lt;/code&gt; — &lt;strong&gt;Consider transfer failed and retry if 20 seconds pass at a speed of 2KB/s or lower&lt;/strong&gt;. This proactively cuts off silent stagnation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-C -&lt;/code&gt; — &lt;strong&gt;Resume download from where it left off&lt;/strong&gt; (Range request). This avoids restarting from scratch each time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--retry 50 --retry-delay 3 --retry-all-errors&lt;/code&gt; — Retry after 3 seconds, up to 50 times. Transfers cut off by &lt;code&gt;--speed-time&lt;/code&gt; are also retried.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-L&lt;/code&gt; (follow redirects) and &lt;code&gt;--fail&lt;/code&gt; (non-zero exit on HTTP errors).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination automatically loops through "stagnation → cut → resume" and eventually completes the transfer. This was the most reliable method in environments where large files would freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Custom HF Cache Structure
&lt;/h2&gt;

&lt;p&gt;The core of the Hugging Face cache (e.g., &lt;code&gt;~/.cache/huggingface/hub&lt;/code&gt;) has the following structure per repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;models--{org}--{repo}/
├── blobs/
│   └── {etag}                      # File entity, named by etag (content hash)
├── snapshots/
│   └── {commit_hash}/
│       └── {filename}              # Symbolic link to blob
└── refs/
    └── main                        # Branch name → commit hash text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entities are stored in &lt;code&gt;blobs/&lt;/code&gt; with etag names&lt;/strong&gt;. This design allows a single entity to serve multiple revisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;snapshots/{commit}/&lt;/code&gt; contains symbolic links&lt;/strong&gt; to blobs. User code and libraries access these human-readable file names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;refs/main&lt;/code&gt; contains the commit hash&lt;/strong&gt;. This enables resolution for &lt;code&gt;revision="main"&lt;/code&gt; and allows &lt;code&gt;hf_hub&lt;/code&gt; to find the correct snapshot even when &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Metadata can be obtained from the &lt;code&gt;huggingface_hub&lt;/code&gt; API:&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;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hf_hub_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_hf_file_metadata&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_hf_file_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Becomes the blob file name
&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_hash&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="c1"&gt;# Becomes the snapshots/ directory name
&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;                       &lt;span class="c1"&gt;# Used for download completion detection
&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;             &lt;span class="c1"&gt;# Entity URL (redirected)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest involves using curl to download the entity to &lt;code&gt;blobs/{etag}&lt;/code&gt;, creating a &lt;strong&gt;relative symbolic link&lt;/strong&gt; from &lt;code&gt;snapshots/{commit}/{filename}&lt;/code&gt;, and writing the commit to &lt;code&gt;refs/main&lt;/code&gt;. The relative link ensures the cache remains intact even when moved to another machine.&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symlink&lt;/span&gt;&lt;span class="p"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&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;path&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;refsdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&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;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the size is known, checking it against the existing blob size and skipping if complete can make re-runs idempotent.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Download Only Necessary Files
&lt;/h3&gt;

&lt;p&gt;Some repositories, like BigVGAN, contain large unnecessary files for inference (e.g., discriminator or optimizer states). Downloading the entire repository can result in getting stuck with the largest file. &lt;strong&gt;Specify the necessary files explicitly&lt;/strong&gt; to avoid this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create refs for Offline Use
&lt;/h3&gt;

&lt;p&gt;Failing to create &lt;code&gt;refs/main&lt;/code&gt; alongside &lt;code&gt;snapshots&lt;/code&gt; and &lt;code&gt;blobs&lt;/code&gt; will prevent &lt;code&gt;hf_hub&lt;/code&gt; from resolving the correct commit when &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;, even if the cache exists. Always create the &lt;strong&gt;three-part set (blob, snapshot link, and refs)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HEAD Verification May Require Online Access
&lt;/h3&gt;

&lt;p&gt;Even with a cache, &lt;code&gt;hf_hub&lt;/code&gt; performs a HEAD request at startup to check for updates (without re-downloading large files). To ensure complete offline functionality, set &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt; or &lt;code&gt;TRANSFORMERS_OFFLINE=1&lt;/code&gt;. Conversely, if you want to see updates but prevent downloads, you can leave it online and just allow HEAD requests.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;In environments where large &lt;code&gt;hf_hub&lt;/code&gt; transfers &lt;strong&gt;freeze silently&lt;/strong&gt;, use curl's &lt;code&gt;--speed-time&lt;/code&gt; and &lt;code&gt;--speed-limit&lt;/code&gt; to &lt;strong&gt;proactively cut off stagnation&lt;/strong&gt;, and &lt;code&gt;-C -&lt;/code&gt; with &lt;code&gt;--retry&lt;/code&gt; for resumption.&lt;/li&gt;
&lt;li&gt;Configure downloaded entities in an &lt;strong&gt;HF cache-compatible structure&lt;/strong&gt; (&lt;code&gt;blobs/{etag}&lt;/code&gt; + &lt;code&gt;snapshots/{commit}/&lt;/code&gt; with relative symbolic links + &lt;code&gt;refs/main&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Metadata (etag, commit, size, location) can be retrieved using &lt;code&gt;get_hf_file_metadata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create relative symbolic links to ensure the cache remains valid even when moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicitly exclude unnecessary large files&lt;/strong&gt; (like discriminators or optimizers) from downloads.&lt;/li&gt;
&lt;li&gt;For complete offline functionality, ensure &lt;code&gt;refs/main&lt;/code&gt; is created to enable &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>huggingface</category>
      <category>curl</category>
      <category>machinelearning</category>
      <category>devops</category>
    </item>
    <item>
      <title>Fixing Visual Discrepancies with Claude Code + Chrome Extension</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:07:22 +0000</pubDate>
      <link>https://dev.to/orca_forge/fixing-visual-discrepancies-with-claude-code-chrome-extension-3h2</link>
      <guid>https://dev.to/orca_forge/fixing-visual-discrepancies-with-claude-code-chrome-extension-3h2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/fixing-ui-with-claude-code-chrome/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=fixing-ui-with-claude-code-chrome" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've got a code that looks correct when read, but when you open it in the browser, it's slightly different from the mockup - this "visual discrepancy" is the most troublesome part of UI development. A slight CSS specification, nesting of elements, and flex wrapping. Discrepancies that cannot be noticed by statically reading the code together will only appear when actually rendered.&lt;/p&gt;

&lt;p&gt;Until now, it was necessary for a human to open the screen in a browser, compare it with the mockup image, and verbally communicate the differences to the AI. This workflow replaces the process of "humans visually seeing and verbalizing" by &lt;strong&gt;showing the screen to the AI agent itself via the browser&lt;/strong&gt;. By combining Claude Code and browser automation extensions (Chrome extensions), we will "see" the screen actually rendered on localhost, compare it with the mockup, identify layout discrepancies, and fix them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it necessary to "show the actual screen"?
&lt;/h2&gt;

&lt;p&gt;There are limitations to just handing over the code for UI review. It's difficult for both humans and AI to completely reproduce the final rendering result in their minds from the code. In particular, these discrepancies are difficult to detect just by looking at the code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout skeleton discrepancies&lt;/strong&gt; - One area is crushed when it's supposed to be a 2-column layout, or the vertical split ratio is different from the mockup, resulting in structural-level discrepancies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element placement errors&lt;/strong&gt; - A preview that should be in the upper right column is wrapped around to the bottom&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unexpected wrapping and overflow&lt;/strong&gt; - The component wraps due to insufficient width, changing the impression from the mockup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These discrepancies cannot be determined without seeing the "rendering result" as a fact. That's why we show the actual screen to the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow: Show, Compare, and Fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Provide the mockup as a baseline
&lt;/h3&gt;

&lt;p&gt;First, provide the target mockup image to the AI and share the baseline that "this is the correct appearance". It's particularly helpful to verbalize the &lt;strong&gt;layout skeleton&lt;/strong&gt; to make the comparison more accurate later. For example, describe the structure as "The whole is a 2-column layout. The left column is the adjustment UI. The right column is divided into upper and lower parts, with a radar chart and preview at the top, and voice samples and advanced adjustments at the bottom".&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Launch the actual screen locally and "show" it to the AI
&lt;/h3&gt;

&lt;p&gt;Start the development server on localhost and have Claude Code open that screen through the browser automation extension. The role of the extension here is to &lt;strong&gt;deliver the actually rendered DOM and screenshot to the AI's eyes&lt;/strong&gt;. The AI no longer imagines the code but judges based on the rendered actual object.&lt;/p&gt;

&lt;p&gt;If necessary, operate the screen to a specific state (after recording audio, after moving a slider, etc.) before taking a screenshot. This is because dynamically changing UIs cannot capture all discrepancies just by looking at the static initial screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compare the mockup with the actual screen to identify discrepancies
&lt;/h3&gt;

&lt;p&gt;With the baseline (mockup) and the actual object (screenshot) in hand, have the AI list the differences. What works here is that the skeleton was verbalized in step 1. Have the AI specifically point out &lt;strong&gt;structural-level discrepancies&lt;/strong&gt;, such as "The preview that should be in the upper right column is at the bottom" or "The right column is not divided into upper and lower parts and is in one column".&lt;/p&gt;

&lt;p&gt;The key is to break down the abstract "something is different" into specifics, such as "which area, which element, where it is in the mockup, and where it is in the actual screen". This becomes the correction instruction as is.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fix and show again
&lt;/h3&gt;

&lt;p&gt;Based on the feedback, have Claude Code fix the code and &lt;strong&gt;open it in the browser again for comparison&lt;/strong&gt;. Check if the fix is actually reflected in the rendering result and if it hasn't created new discrepancies. In reality, layout reconstructions such as 2-column + right column vertical splits often don't work on the first try, and this "show, fix, and show again" loop is essential. If you skip the confirmation and just fix the code, discrepancies that you thought were fixed may remain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls and Learnings
&lt;/h2&gt;

&lt;p&gt;Here are some cautionary notes that have emerged from running this loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Take a new screenshot with each fix&lt;/strong&gt; - Reusing a previously taken screen won't allow you to evaluate the fixed state. Take a new screenshot of the actual object every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the state before showing&lt;/strong&gt; - Not just the initial display, but also operate to the state assumed by the mockup, such as after recording or slider operation, before taking a screenshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the viewport width&lt;/strong&gt; - Wrapping changes with different display widths. Compare after matching the browser to the width assumed by the mockup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbalize the skeleton first&lt;/strong&gt; - Having a structural description like "2-column / right column vertical split" keeps the AI's feedback from becoming abstract and makes it a fixable granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code review and screen review are different&lt;/strong&gt; - Consistency in the code and consistency in the rendering result do not match. Always make the final judgment on the actual screen.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Discrepancies in appearance, such as layout skeleton discrepancies and wrapping/overflow, cannot be detected by statically reading the code together&lt;/li&gt;
&lt;li&gt;Use browser automation extensions to "show" the actual screen on localhost to Claude Code, compare it with the mockup image, and identify discrepancies&lt;/li&gt;
&lt;li&gt;Specify discrepancies as specifically as "which area, which element, and how it differs between the mockup and actual screen", and use this as the correction instruction&lt;/li&gt;
&lt;li&gt;After fixing, &lt;strong&gt;open it in the browser again for comparison&lt;/strong&gt;. The loop of showing, fixing, and showing again is essential&lt;/li&gt;
&lt;li&gt;Take a new screenshot every time, match the state and viewport width, and then compare&lt;/li&gt;
&lt;li&gt;Make the final judgment on the actual screen, not the code - the new practice is to show the screen to the AI and have it fix it&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
