<?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 angle between the head and chest was 0.000 in every frame: The time my motion tracking was broken</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 17 Sep 2026 00:53:41 +0000</pubDate>
      <link>https://dev.to/orca_forge/the-angle-between-the-head-and-chest-was-0000deg-in-every-frame-the-time-my-motion-tracking-was-f82</link>
      <guid>https://dev.to/orca_forge/the-angle-between-the-head-and-chest-was-0000deg-in-every-frame-the-time-my-motion-tracking-was-f82</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/head-chest-angle-zero-degrees/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=head-chest-angle-zero-degrees" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The character's head would tilt along with the body whenever it twisted. When I measured the angle difference between the head and chest, it was 0.000° for every frame. I ended up redoing the head orientation logic downstream three times before realizing the cause.&lt;/p&gt;

&lt;p&gt;The issue was that the nose and ear landmarks used for measurement were synthesized from the torso's base vector. I thought I was measuring the head and chest separately, but I was actually comparing the same orientation.&lt;/p&gt;

&lt;p&gt;This is the second installment in a series about creating VRM animations from fixed-camera live-action videos. This time, I'll organize the lessons learned from actual mistakes regarding measurement targets, coordinate systems, and verification methods.&lt;/p&gt;

&lt;p&gt;In the previous article, I covered the entire pipeline and design decisions. Links to each installment in the series are provided at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are We Measuring?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  0.000° Means "Measuring the Same Thing Twice," Not "Fixed"
&lt;/h3&gt;

&lt;p&gt;In this material, the live actor's head moved about ±10° relative to the chest, so I was suspicious of the 0.000° result for every frame. Upon investigation, I found that the head and chest measurements were not independent.&lt;/p&gt;

&lt;p&gt;Here's the dependency diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old: Shoulder line ──→ Torso base ──┬──→ Nose/Ear (synthesized)──→ Head orientation
                             └───────────────────→ Chest orientation
                             (Two outputs from the same input)

New: SMPL mesh actual vertices (nose/ear)─────────────→ Face orientation
    Torso base ──────────────────────────────→ Chest orientation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By definition, the ear line matches the shoulder line. This means the head orientation can never be anything other than the chest orientation in the calculation. Subtracting them always results in 0. So 0.000° didn't mean "the head is fixed to the chest," but rather "I was subtracting two values created from the same input."&lt;/p&gt;

&lt;p&gt;I ended up redoing the downstream logic three times, thinking the issue was with how the head orientation was applied in the FK (forward kinematics) stage.&lt;/p&gt;

&lt;p&gt;The fix wasn't in the estimator itself, but in the adapter (&lt;code&gt;pipeline/estimate_pose_gvhmr.py&lt;/code&gt;) that converts GVHMR outputs to landmarks for this pipeline. It reads the actual nose and ear vertices from the SMPL mesh and outputs a unit vector connecting their midpoint every frame. The implementation name is &lt;code&gt;gaze&lt;/code&gt;, but it's actually an approximation of face orientation, not the eyeball's line of sight.&lt;/p&gt;

&lt;p&gt;I also added two checks to the QA process: if the standard deviation of the head-chest angle difference is less than 2°, or if the average face elevation angle exceeds 12°, a confirmation prompt is triggered. These thresholds are designed to catch anomalies in this pipeline. They don't flag intentionally fixed head movements or constant upward gazes as defects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capsule Approximation Said "4mm Clearance"
&lt;/h3&gt;

&lt;p&gt;This happened when creating a video of two characters fighting. To the human eye, the kicking leg was penetrating the opponent's head. The comparison script reported "4mm clearance."&lt;/p&gt;

&lt;p&gt;The comparison used a model approximating the torso as a cylinder with a 0.16m radius and the head as a sphere with a 0.11m radius. This model is lightweight and fast, useful for estimating distances and reach. However, it doesn't represent the character's silhouette. The hood, shoulders, gloves, and boots are all outside this approximation.&lt;/p&gt;

&lt;p&gt;When checking for overlaps with the actual skinned mesh, I found intersections in 4 frames, with a maximum of 230 overlapping face pairs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capsule approximation: 4mm clearance
Actual mesh: 230 face intersections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on, I used Blender's BVH to check for face intersections between actual meshes for penetration confirmation in this scene. BVH checks for "face intersections," not all contacts or enclosures. There's a distinction between approximations for corrections and final visual confirmation (pushing with approximate shapes is covered in &lt;a href="https://forge.workstyle.tech/blog/arm-clearance-flipping-front-to-back/" rel="noopener noreferrer"&gt;Part 4&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The lesson here, in one sentence, is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When visuals and numbers conflict, first verify if the numbers are measuring the same thing as the visuals.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Same Point Name Doesn't Mean Same Location
&lt;/h3&gt;

&lt;p&gt;Measurement breakdowns often occur when two compared points aren't actually the same point. I encountered three such cases in the same session.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison/Judgment&lt;/th&gt;
&lt;th&gt;Mismatch&lt;/th&gt;
&lt;th&gt;Observed Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Actor's nose vs character's &lt;code&gt;Head&lt;/code&gt; bone&lt;/td&gt;
&lt;td&gt;Face position vs skull base&lt;/td&gt;
&lt;td&gt;Compared points were ~10cm apart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shoulder line vs &lt;code&gt;Neck&lt;/code&gt; bone&lt;/td&gt;
&lt;td&gt;Shoulder vs neck height&lt;/td&gt;
&lt;td&gt;Compared points were 5-10cm apart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact judgment using only leg endpoints&lt;/td&gt;
&lt;td&gt;Endpoints vs line segment&lt;/td&gt;
&lt;td&gt;Missed shin penetration when knee and ankle were outside&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third case is particularly problematic. Relying only on endpoints means you can't detect "shin penetrating the head" if both the knee and ankle are outside the target.&lt;/p&gt;

&lt;p&gt;Another issue is mistaking body proportions for pose errors. In one clip flagged for "hand position too high relative to face," 4.6cm of the 14.8cm difference was due to the character's head being lower relative to the shoulders compared to the actor. The comparison script now separates body proportion ratios from pose errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Coordinate System Are We Measuring In?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Camera "Up" Isn't Gravity
&lt;/h3&gt;

&lt;p&gt;I received feedback that a kick "lacked height." Measurements confirmed it was low, but the issue wasn't the kick itself - I was reading the height in the camera coordinate system without correcting for the camera's tilt.&lt;/p&gt;

&lt;p&gt;The camera was tilted 4.2° downward. Without adjusting for this tilt, there was a 0.14m error in the kick's peak height relative to the head. Before judging the kick as insufficient, I needed to align the comparison coordinate systems.&lt;/p&gt;

&lt;p&gt;In this material, the camera tilt wasn't noticeable just by watching the video. Even when mounted on a tripod, this level of tilt is common. When measuring height, establish the gravity direction first - leading to the next point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare Two People in a Shared Coordinate System
&lt;/h3&gt;

&lt;p&gt;This is from a record of handling a two-person fight video (found in the forked repository's documentation).&lt;/p&gt;

&lt;p&gt;Measuring the distance between the kicking foot and the opponent's head gave 0.18m. However, both the video and independently measured image quantities showed 0.29m. The difference between 0.18m and 0.29m is significant - "barely missing" versus "clearly missing."&lt;/p&gt;

&lt;p&gt;The cause was that the estimator's world coordinate system normalized each person so "frame 0 faced forward." With two people, there are two separate coordinate systems. Any difference in their starting orientations creates a misalignment. Arranging their limbs in a single space produces results that seem plausible but are subtly incorrect.&lt;/p&gt;

&lt;p&gt;For this comparison, I aligned the estimation results of both individuals to a common camera coordinate system. Note that the scale conversion from image quantities to meters and the assumptions used are not recorded in the original documentation. To reuse the 0.29m value, that conversion would need to be redone.&lt;/p&gt;

&lt;h3&gt;
  
  
  World and In-Camera Are Correct for Different Things
&lt;/h3&gt;

&lt;p&gt;The pose estimator (GVHMR) produces two types of outputs for the same body:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;World&lt;/strong&gt;: Estimation results in a gravity-aligned coordinate system. In this material, position and height drift were problematic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-camera&lt;/strong&gt;: Estimation results in the camera coordinate system. Easier to compare with images, but depth estimation errors and camera tilt need separate handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are estimation results. It's not a contrast between "world as prediction and in-camera as measurement." Mixing these two can lead to bugs with unclear failure modes. Here are some real-world examples (different materials and comparisons in each row, so don't compare numbers directly):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Material&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;What Was Compared&lt;/th&gt;
&lt;th&gt;Numbers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plaza&lt;/td&gt;
&lt;td&gt;Ground rises over time&lt;/td&gt;
&lt;td&gt;World height time change&lt;/td&gt;
&lt;td&gt;0.25m over 3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fountain&lt;/td&gt;
&lt;td&gt;Jump height not captured&lt;/td&gt;
&lt;td&gt;World upward movement vs image upward movement&lt;/td&gt;
&lt;td&gt;0.02-0.07m vs 0.12-0.26m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plaza&lt;/td&gt;
&lt;td&gt;Backwards-starting material flips entirely&lt;/td&gt;
&lt;td&gt;Normalized orientation vs video orientation&lt;/td&gt;
&lt;td&gt;180°&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Material with approach (241 frames)&lt;/td&gt;
&lt;td&gt;Insufficient step-in distance&lt;/td&gt;
&lt;td&gt;World movement vs actual step-in&lt;/td&gt;
&lt;td&gt;Reported 0.68m for 0.92m, final 0.16m marker mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is a clear example. The world output seems physically consistent and "correct" intuitively, but it drifts due to model-based estimation. Meanwhile, the in-camera output matched independently measured image quantities (waist pixel count ÷ body pixel count) within 2cm for all 241 frames. Again, the conversion from pixel ratios to meters isn't recorded, so the match accuracy needs retesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suspect Common Causes When Applying Multiple Fixes
&lt;/h3&gt;

&lt;p&gt;At one point, I was applying separate corrections for orientation flipping, lateral movement disappearance, and jump disappearance. All three issues stemmed from using the estimator's world output for position, height, and orientation.&lt;/p&gt;

&lt;p&gt;I discarded all the fixes and unified the coordinate system. A constant rotation is calculated from the in-camera and world joint configurations to align with gravity. This rotation transforms the camera coordinate system estimation results, while translation uses the camera fit values.&lt;/p&gt;

&lt;p&gt;The rotation is solved using the Kabsch method. The key is subtracting the center of mass each frame, which eliminates translation and drift, leaving only orientation.&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;# pipeline/frames.py:37-44
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;camera_to_world_rotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;joints_incam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;joints_world&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;joints_incam&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;joints_incam&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdims&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="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;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;joints_world&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;joints_world&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdims&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="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;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;kabsch&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;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the camera is fixed, I assume constant rotation for the entire clip here, treating all frames as one least-squares problem. Finally, I add a yaw rotation to make the lens point along +z, completing the output coordinate system with y as up and y=0 as the floor.&lt;/p&gt;

&lt;p&gt;In this coordinate system, downstream processes like "re-estimating orientation" and "restoring movement" become unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Discarded Alternatives
&lt;/h3&gt;

&lt;p&gt;I tested two other approaches that seemed better but ultimately discarded them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Position from image plane&lt;/strong&gt;: When the camera is tilted, actual depth movement leaks into height. In dance_full's approach, it was 0.16m lower&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth from lowest foot ray-floor intersection&lt;/strong&gt;: Performs poorly when the pelvis ray is nearly horizontal. For chest-height shots (most of this material), depth fluctuated ±17% (fit itself was ±4%)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In conclusion, the fit's own depth was the least bad option. Remaining errors are "depth error × sin(camera tilt)," which is a few centimeters in these materials. Compared to the world's 0.25-0.35m drift, it's sufficiently small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify with Quantities Not Used in Fitting
&lt;/h2&gt;

&lt;p&gt;Here's an effective verification pattern.&lt;/p&gt;

&lt;p&gt;I wrote logic to transform hand estimation results (camera coordinates) to the rig's coordinate system. The transformation rotation is calculated by matching four torso points (shoulders and hips) between the two coordinate systems.&lt;/p&gt;

&lt;p&gt;The challenge is verifying if this rotation is truly correct. You can't confirm its applicability to other parts using only the fitted points. Naturally, the rotation matches well when verified with the torso used for fitting. While not an exact match due to shape differences between the rig and actor, this verification is weak.&lt;/p&gt;

&lt;p&gt;So I verified with the forearms, which weren't used in fitting. The camera-coordinate forearm orientation is transformed using this rotation and compared to the rig's forearm. In the desk material, the directional difference was 3.6°. Matching corresponding points and consistency across different parts are checked separately.&lt;/p&gt;

&lt;p&gt;The implementation calculates rotation using SVD and adds a matrix sign to prevent mirroring. Without this, SVD fits can happily return flipped rotations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before Removing Based on Old Measurements, Retest if Reproducible Now
&lt;/h2&gt;

&lt;p&gt;There's another temporal pitfall in measurement stories: your own past measurements.&lt;/p&gt;

&lt;p&gt;Depth errors in fitting scale the entire translation, meaning depth mistakes also raise/lower foot height and produce tilted floors relative to depth. Actual measurements showed dance_full at +4° and plaza at -13°.&lt;/p&gt;

&lt;p&gt;I didn't measure the actual floor tilt. The tilt appearing in estimation results is corrected to align foot heights. Floor plane fitting uses iterative reweighted least squares on the lower envelope, with 3 rounds, using only the lower 20% of residuals each time, and a ridge term (walking along one line makes x and z collinear).&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;# pipeline/frames.py:126-134
&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&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;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;z&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones_like&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="n"&gt;axis&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;sel&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;ones&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;low&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;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;lam&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;diag&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# ridge: a walk along one line leaves x and z collinear
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;As&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ys&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;sel&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;coef&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;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;As&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;As&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lam&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;ys&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;As&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;ys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;low&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;coef&lt;/span&gt;
    &lt;span class="n"&gt;sel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At one point, based on an old measurement claiming "floor tilt estimation was counterproductive in all three materials," I decided to remove this processing and actually deleted it.&lt;/p&gt;

&lt;p&gt;Later, when retesting with 12 clips containing tilts, the old measurement wasn't reproducible. The metric was the 5th-95th percentile width of the lowest foot height (ankle or toe, whichever is lower). Smaller values mean more consistent foot heights. 9 of 12 clips worsened without correction, 2 (desk1, desk1b) improved 5-8%, and the remaining clip's result wasn't recorded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Material&lt;/th&gt;
&lt;th&gt;With correction: Lowest foot height p5-p95 width (m)&lt;/th&gt;
&lt;th&gt;Without correction: Same (m)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dance_full&lt;/td&gt;
&lt;td&gt;0.112&lt;/td&gt;
&lt;td&gt;0.144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clip showing 29° tilt&lt;/td&gt;
&lt;td&gt;0.195&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.98&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;desk1b&lt;/td&gt;
&lt;td&gt;0.092&lt;/td&gt;
&lt;td&gt;0.085&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Based on these results, I restored the processing and recorded the retest conditions and process in the code. When making changes based on old measurements, first verify if they're reproducible with the current code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: 2D Detection Consistency and Smoothing Side Effects
&lt;/h2&gt;

&lt;p&gt;Here are two implementation points that come into play after aligning measurements, summarized briefly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2D Detection Matches Image Positions Better
&lt;/h3&gt;

&lt;p&gt;3D body fitting can be off by centimeters for hand and foot tips. In dance_full, wrist reprojection error was 20px (p50) to 40px (p90) - 3-7cm for a ~1000px actor. Hands that touch in the video appear 5cm apart.&lt;/p&gt;

&lt;p&gt;In this material, ViTPose's 2D detection (already run by GVHMR as preprocessing) matched image joint positions better than reprojecting 3D fit points. However, 2D detection alone doesn't determine depth.&lt;/p&gt;

&lt;p&gt;So I keep the 3D fit's depth but use 2D detection ray points as correction targets. In practice, correction amounts toward these targets are interpolated and smoothed. The code below uses camera coordinates (x right, y down, z depth), with &lt;code&gt;p[:, 2]&lt;/code&gt; as depth.&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;# pipeline/frames.py:194-196
&lt;/span&gt;&lt;span class="n"&gt;target&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;stack&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="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="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uv&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="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="n"&gt;f&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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_fill_and_smooth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="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;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[:,&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="n"&gt;delta&lt;/span&gt;      &lt;span class="c1"&gt;# z (depth) untouched
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;delta&lt;/code&gt; is smoothed before addition, corrected points aren't strictly on the ray each frame. Two subtle but effective tweaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip frames with confidence &amp;lt;0.7&lt;/strong&gt;. Missing frames are interpolated from neighbors and median-smoothed over 5 frames. Frame-level 2D detection jitter shouldn't affect the character&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip waist and shoulders&lt;/strong&gt;. Different "waist" and "shoulder" definitions in SMPL vs COCO. Elbows and knees are corrected after subtracting clip-wide median offsets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is detailed work checking "which joints, which definitions, which coordinate systems" one by one. Being vague here leads back to the failures in the first section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smoothing Halved Deceleration
&lt;/h3&gt;

&lt;p&gt;I was applying a 7-frame Savitzky-Golay filter as preprocessing - a noise reduction standard. Testing on dance_full showed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;With filter (7 frames)&lt;/th&gt;
&lt;th&gt;Without&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wrist deceleration: |acceleration| p99 (m/s²)&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Stop" event count (times)&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noise median&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;10.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "noise" in row 3 isn't defined in the records - treat the ratio as indicative.&lt;/p&gt;

&lt;p&gt;In this material, deceleration dropped from 64m/s² to 32m/s², and "stop" events from 51 to 29. While reducing noise, it may also weaken desired sharp movements. The event reduction rate doesn't directly equal "lost sharpness," but strike deceleration is clearly dulled.&lt;/p&gt;

&lt;p&gt;I also measured kick peaks. For a roundhouse kick peaking in ~6 frames, a 7-frame window reduced shin angle by 5° and height by 0.04m. A 5-frame window reduced angle by 1.4°.&lt;/p&gt;

&lt;p&gt;It's now off by default. If the estimator is noisy, re-enable via spec, but it wasn't a default processing step. The same topic appears in &lt;a href="https://forge.workstyle.tech/blog/thumb-rests-on-the-index-not-pushed-away/" rel="noopener noreferrer"&gt;Part 5&lt;/a&gt; (hand smoothing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When numbers align suspiciously, check measurement independence and rounding&lt;/li&gt;
&lt;li&gt;Distinguish between approximate shapes being sufficient and needing actual mesh verification&lt;/li&gt;
&lt;li&gt;Align not just point names but position definitions and coordinate systems when comparing&lt;/li&gt;
&lt;li&gt;When applying multiple fixes, investigate common upstream causes&lt;/li&gt;
&lt;li&gt;Before removing processing based on old measurements, retest if reproducible now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checklist for questioning numbers has settled on these five:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Comparing the same points (by position definition, not just name)&lt;/li&gt;
&lt;li&gt;Same coordinate system and scale (gravity direction established? conversion assumptions documented?)&lt;/li&gt;
&lt;li&gt;Measurements independent (two values not created from the same source?)&lt;/li&gt;
&lt;li&gt;Looking at current output (not relying on old logs or measurements?)&lt;/li&gt;
&lt;li&gt;Verified with quantities not used in fitting?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Preview
&lt;/h2&gt;

&lt;p&gt;Next time, with measurements aligned, we'll place feet on the floor. Starting from an estimation where the swinging leg penetrates the floor by 148mm, we'll cover how to detect ground contact and align feet and waist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Installments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/video-to-vrm-pipeline-overview/" rel="noopener noreferrer"&gt;Part 1: Animating VRM from a Single Live Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 2 (this article): Head-Chest Angle Difference of 0.000° in Every Frame - Broken Motion Measurement&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/one-percent-shorter-leg-16-degree-knee/" rel="noopener noreferrer"&gt;Part 3: 1% Shorter Legs Resulted in 16° Bent Knees&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/arm-clearance-flipping-front-to-back/" rel="noopener noreferrer"&gt;Part 4: Arm Penetration Correction Jumps Front to Back&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/thumb-rests-on-the-index-not-pushed-away/" rel="noopener noreferrer"&gt;Part 5: Thumb "Placed" on Index Finger, Not Pushed Away&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/wrist-crease-is-skinning-not-pose/" rel="noopener noreferrer"&gt;Part 6: Wrist Crease Caused by Skinning, Not Pose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation &amp;amp; Sources
&lt;/h2&gt;

&lt;p&gt;The materials and code in this article are based on &lt;a href="https://github.com/squall01337/mixamo-llm-mocap" rel="noopener noreferrer"&gt;squall01337/mixamo-llm-mocap&lt;/a&gt; (MIT). The coordinate system discussion for two-person comparisons comes from the forked repo's documentation. Coordinate unification, floor leveling, and 2D consistency were added later.&lt;/p&gt;

&lt;p&gt;Code quoted in this article that was added after forking isn't publicly available. Assume retesting isn't possible when reading (quotes include file names and line numbers).&lt;/p&gt;

&lt;p&gt;Code excerpts are for explanation - see the referenced files (&lt;code&gt;pipeline/frames.py&lt;/code&gt;, &lt;code&gt;pipeline/wrist_heading.py&lt;/code&gt;, &lt;code&gt;pipeline/estimate_pose_gvhmr.py&lt;/code&gt;) for full implementations including initialization and helper functions.&lt;/p&gt;

</description>
      <category>python</category>
      <category>3dcg</category>
    </item>
    <item>
      <title>Making a VRM Character Dance from a Single Live-Action Video: Fixing Leg, Arm, and Finger Clipping After Estimation</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Thu, 17 Sep 2026 00:22:04 +0000</pubDate>
      <link>https://dev.to/orca_forge/making-a-vrm-character-dance-from-a-single-live-action-video-fixing-leg-arm-and-finger-clipping-2eo6</link>
      <guid>https://dev.to/orca_forge/making-a-vrm-character-dance-from-a-single-live-action-video-fixing-leg-arm-and-finger-clipping-2eo6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/video-to-vrm-pipeline-overview/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=video-to-vrm-pipeline-overview" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Creating a Pipeline to Make VRM Characters Dance from Live-Action Videos
&lt;/h2&gt;

&lt;p&gt;We are building a pipeline to make VRM characters dance from live-action videos. However, when we applied the estimated motion to the character, the feet penetrated the floor, the arms intersected with the body, and the thumb passed through the index finger.&lt;/p&gt;

&lt;p&gt;In our dance material, we were able to reduce the maximum penetration into the floor from 148mm on the left and 94mm on the right to 0.4mm on the left and 1.5mm on the right. While correcting the grounding, we also confirmed that the original motion was not overly trimmed.&lt;/p&gt;

&lt;p&gt;The input is a video shot with a fixed camera, and the output is a VRM animation (VRMA) and a browser viewer. This series will introduce the process of measuring and correcting the defects that remain after estimation, divided into six parts. The first part will cover the overall pipeline and design decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing Before and After Correction with Numbers
&lt;/h2&gt;

&lt;p&gt;We compared the before and after correction using dance material (1,789 frames, 60fps). The slip is the total of the entire clip, and the penetration is the maximum value. p90 is the 90th percentile when the values are arranged in order from smallest, and f represents frames. The numbers are not the direct output of the solver, but were measured by writing the correction back to the Blender rig and re-observing it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Before Correction&lt;/th&gt;
&lt;th&gt;After Correction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foot Slip (Total, Left/Right)&lt;/td&gt;
&lt;td&gt;9366 / 8084 mm&lt;/td&gt;
&lt;td&gt;1362 / 1537 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Penetration into the Floor (Maximum, Left/Right)&lt;/td&gt;
&lt;td&gt;148 / 94 mm&lt;/td&gt;
&lt;td&gt;0.4 / 1.5 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foot Acceleration p90 (Left/Right)&lt;/td&gt;
&lt;td&gt;6.8 / 9.7 mm/f²&lt;/td&gt;
&lt;td&gt;7.2 / 9.6 mm/f²&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The foot acceleration p90 was 6.8→7.2 on the left and 9.7→9.6 mm/f² on the right. While reducing slip and penetration, this indicator maintained a close value. However, with p90 alone, it cannot be determined whether individual steps or kicks were preserved. The slip did not become zero, and some correction remained at the boundary of the grounding interval.&lt;/p&gt;

&lt;p&gt;The slip indicator selects the target vertex based on height conditions, so it cannot be determined whether the number of evaluation targets changed based on the total value alone. This point will be discussed in &lt;a href="https://forge.workstyle.tech/blog/one-percent-shorter-leg-16-degree-knee/" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's easy to "eliminate slip" by just stopping the feet, but that would also make the motion flat. The goodness or badness of the correction should be judged not only by whether the desired error is reduced but also by whether the quantity that should not be eliminated remains. The numbers that appear in this series are mostly in sets of these two.&lt;/p&gt;

&lt;p&gt;What we originally wanted to do was simple: "make the VRM avatar dance with the same motion as the video we took." If we use commercial motion capture products, it would be faster, but we wanted to create it from a single video shot with a handheld camera without a suit or markers. Monocular pose estimation (a model that restores 3D humans from a single video) has become quite practical in recent years.&lt;/p&gt;

&lt;p&gt;The estimation itself is surprisingly accurate. The defects that occur are after that, and what we actually observed were symptoms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feet penetrating or floating from the floor&lt;/li&gt;
&lt;li&gt;Knees bending even when standing&lt;/li&gt;
&lt;li&gt;Arms intersecting with the body&lt;/li&gt;
&lt;li&gt;Thumbs passing through index fingers&lt;/li&gt;
&lt;li&gt;Wrist skin being crushed like candy wrapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This series is a record of measuring and correcting each of these defects one by one.&lt;/p&gt;

&lt;p&gt;Note that the process of applying the estimated motion to a character with a different physique is called "retargeting." In this article, we refer to the process of reassembling the bone direction to match the character's bone length as "lifting," and the process of creating a pose from bone rotation as "FK application."&lt;/p&gt;

&lt;h2&gt;
  
  
  Flow from Estimation to Correction and Verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="mf"&gt;0.&lt;/span&gt; &lt;span class="n"&gt;Material&lt;/span&gt; &lt;span class="nf"&gt;video &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixed&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;1.&lt;/span&gt; &lt;span class="n"&gt;Estimation&lt;/span&gt;        &lt;span class="n"&gt;Convert&lt;/span&gt; &lt;span class="n"&gt;GVHMR&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;joint&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pelvis&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;sight&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Coordinate&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;  &lt;span class="n"&gt;Unify&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;camera coordinate system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;aligned&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gravity&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt; &lt;span class="n"&gt;integration&lt;/span&gt;  &lt;span class="n"&gt;Reload&lt;/span&gt; &lt;span class="n"&gt;wrist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ankle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elbow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;knee&lt;/span&gt; &lt;span class="n"&gt;onto&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt; &lt;span class="n"&gt;ray&lt;/span&gt;
&lt;span class="mf"&gt;2.&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt;            &lt;span class="n"&gt;Detect&lt;/span&gt; &lt;span class="n"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;takeoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;impact&lt;/span&gt; &lt;span class="n"&gt;numerically&lt;/span&gt;
&lt;span class="mf"&gt;3.&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;            &lt;span class="n"&gt;Support&lt;/span&gt; &lt;span class="n"&gt;foot&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;correction&lt;/span&gt; &lt;span class="nf"&gt;specification &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;4.&lt;/span&gt; &lt;span class="n"&gt;Lifting&lt;/span&gt;      &lt;span class="n"&gt;Reassemble&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="n"&gt;rig&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;
&lt;span class="mf"&gt;5.&lt;/span&gt; &lt;span class="n"&gt;FK&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;  &lt;span class="n"&gt;Orient&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt; &lt;span class="n"&gt;bones&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;Blender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;explore&lt;/span&gt; &lt;span class="n"&gt;waist&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;
&lt;span class="mf"&gt;5.1&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;motion&lt;/span&gt;  &lt;span class="n"&gt;Keep&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt; &lt;span class="n"&gt;joints&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="n"&gt;anatomical&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;
&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;Grounding&lt;/span&gt;      &lt;span class="n"&gt;Detect&lt;/span&gt; &lt;span class="n"&gt;foot&lt;/span&gt; &lt;span class="n"&gt;grounding&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;solve&lt;/span&gt; &lt;span class="n"&gt;foot&lt;/span&gt; &lt;span class="n"&gt;trajectory&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;waist&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;once&lt;/span&gt;
&lt;span class="mf"&gt;5.2&lt;/span&gt; &lt;span class="n"&gt;Interference&lt;/span&gt;      &lt;span class="n"&gt;Push&lt;/span&gt; &lt;span class="n"&gt;arms&lt;/span&gt; &lt;span class="n"&gt;away&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="mf"&gt;5.3&lt;/span&gt; &lt;span class="n"&gt;Hand&lt;/span&gt;            &lt;span class="n"&gt;Finger&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt; &lt;span class="n"&gt;thumb&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="n"&gt;finger&lt;/span&gt;
&lt;span class="mf"&gt;6.&lt;/span&gt; &lt;span class="n"&gt;Comparison&lt;/span&gt;      &lt;span class="n"&gt;Measure&lt;/span&gt; &lt;span class="n"&gt;difference&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;eye&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comparing&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;
&lt;span class="mf"&gt;7.&lt;/span&gt; &lt;span class="n"&gt;QA&lt;/span&gt;          &lt;span class="n"&gt;Numerical&lt;/span&gt; &lt;span class="nf"&gt;gate &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We output the corrected animation to VRMA and verify it with a viewer.&lt;/p&gt;

&lt;p&gt;The joint points in step 1 are not in the format directly output by the estimation model. An adapter in this repository composes MediaPipe-style 33 points from SMPL joints and facial mesh vertices. The downstream process only looks at these 33 points, so even if the estimator is replaced, the same code can be run from there.&lt;/p&gt;

&lt;p&gt;Among these, steps 1b, 1c, and 5.1 onwards are layers that were added after actually moving and finding them to be insufficient. It wasn't in this form from the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferring Bone "Direction"
&lt;/h2&gt;

&lt;p&gt;If we apply the estimated joint positions to the character as is, defects will occur because the physique is different. On the other hand, a method that adds and subtracts differences also breaks down when the correction amount becomes large. The original documentation has a record that with a 20cm correction, the hand interval shrunk from 0.25m to 0.08m, and the wrist angle changed from 7° to 123°.&lt;/p&gt;

&lt;p&gt;In this pipeline, we use the estimated bone direction and reassemble it using the actual measured value of the rig length.&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;# pipeline/lift_to_mixamo.py:441-449
&lt;/span&gt;&lt;span class="n"&gt;l_elbow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_arm&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left_shoulder&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;left_elbow&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="n"&gt;LEN&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_arm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;l_wrist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_elbow&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left_elbow&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;left_wrist&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="n"&gt;LEN&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_fore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;l_hand&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_wrist&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;hand_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&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="n"&gt;LEN&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_hand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;l_knee&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_upleg&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left_hip&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;left_knee&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="n"&gt;LEN&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_upleg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;l_ankle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_knee&lt;/span&gt;  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left_knee&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;left_ankle&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="n"&gt;LEN&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_leg&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;&lt;code&gt;seg()&lt;/code&gt; is a unit vector between two points, and &lt;code&gt;LEN&lt;/code&gt; is the actual measured length of the rig. The position of the shoulders and hip joints is also not taken from the estimation, but the offset from the rest pose is applied to the estimated base of the waist.&lt;/p&gt;

&lt;p&gt;This method ensures that the bone length does not shift by definition. However, the problem of the character's hands and feet being thicker than the performer remains. The bone length is summarized in &lt;code&gt;rig_profile.json&lt;/code&gt;, and when the character is replaced, only this one file is replaced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding Blender's IK Constraint and Writing to Rotation Key
&lt;/h3&gt;

&lt;p&gt;With the rig and settings used this time, applying Blender's IK constraint caused the feet to fly away to -81m. Therefore, we configured it to write the corrected pose as the rotation of each bone. We also use our own analytical 2-bone IK (intersection of two circles) for calculations that solve leg and arm positions. Putting the feet on the floor is also done by "exploring the waist height to find the height at which the support foot touches the ground," rather than using a constraint.&lt;/p&gt;

&lt;p&gt;There are other premises that should be noted regarding the rig.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The armature is &lt;strong&gt;rotated 90° around the X-axis and scaled to 0.01&lt;/strong&gt;. If we create a pose matrix in world coordinates and enter it into the pose bone, the hands will fly away &lt;strong&gt;100 times&lt;/strong&gt;. The target is to decide in armature space (centimeter system).&lt;/li&gt;
&lt;li&gt;The only moving bone is &lt;strong&gt;Hips&lt;/strong&gt;. All others are quaternions, and location is reset to 0 every frame (old keys will remain and cause defects).&lt;/li&gt;
&lt;li&gt;World positions are read after &lt;code&gt;view_layer.update()&lt;/code&gt; using &lt;code&gt;(arm.matrix_world @ pose_bone.matrix).to_translation()&lt;/code&gt;. When comparing positions, the coordinate space and evaluation timing must be aligned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These points will be discussed in &lt;a href="https://forge.workstyle.tech/blog/wrist-crease-is-skinning-not-pose/" rel="noopener noreferrer"&gt;Part 6&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching Estimation Results and Repeating Correction on CPU
&lt;/h2&gt;

&lt;p&gt;GVHMR took 52 seconds to estimate 300 frames on an RTX 4080 Laptop (VRAM 12GB), with a VRAM peak of 4.2GB. We cache the estimation results and repeat the correction using NumPy and Blender.&lt;/p&gt;

&lt;p&gt;The only part that uses the GPU is the pose estimation. There are two backends for hand finger estimation: MediaPipe, which runs on CPU, and WiLoR, which runs on GPU. If WiLoR is chosen, the GPU is also used for this part.&lt;/p&gt;

&lt;p&gt;This division has practical benefits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Estimation only needs to be done once, and the results can be cached. Subsequent trial and error do not compete for the GPU.&lt;/li&gt;
&lt;li&gt;Grounding processing is written only with NumPy, so &lt;strong&gt;changing a constant and re-measuring takes only a few seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it takes several minutes to make one round trip with Blender, this difference becomes the difference in the number of trial iterations. In fact, the grounding processing is divided into three segments: "Blender observation → NumPy solution → Blender writing," and only the middle part was repeated dozens of times to decide on the constants.&lt;/p&gt;

&lt;p&gt;For reference, the actual time for each segment when passing a 4-minute material (14,373 frames, 60fps) is as follows. Hand and facial expression estimation are not included in this table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pose estimation (GPU)&lt;/td&gt;
&lt;td&gt;Several minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifting + FK application&lt;/td&gt;
&lt;td&gt;About 10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grounding processing (observation → solution → writing → re-observation)&lt;/td&gt;
&lt;td&gt;About 35 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arm interference (NumPy part is 3 minutes)&lt;/td&gt;
&lt;td&gt;About 25 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thumb&lt;/td&gt;
&lt;td&gt;About 1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VRMA output&lt;/td&gt;
&lt;td&gt;About 30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The total is about 2.5 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  spec Includes Material Information and Correction Specifications
&lt;/h2&gt;

&lt;p&gt;Each clip has one JSON spec that summarizes material information, support foot schedule, standing pose adjustment, and override of estimated intervals that went wrong. Automatically generated content is also corrected as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fountain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landmarks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plates/fountain/landmarks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"src_fps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dst_fps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;// Support foot. Intervals are all written in the frame number of the original video.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"support"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"left"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"support"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"both"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;129&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"support"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// Floating&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;145&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"support"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"left"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;// Standing pose adjustment (knee in/out, stride, elbow)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"knee_in_pct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"toe_in_deg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;// Override estimated intervals that went wrong, with human reading&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arm_overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"side"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;163&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ramp_src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"wrist_local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;-0.17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.33&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"elbow_local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;-0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are aware of the following three design considerations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unify frame numbers to the original video's frame number.&lt;/strong&gt; When a person looks at the video and says "the right arm is strange at frame 27," it can be written directly into the spec. If the frame number after conversion to 60fps is inserted, it becomes easier to mix up the frame numbers. In fact, we made a mistake here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correction must always have an interval and a ramp.&lt;/strong&gt; Corrections such as tightening the elbow only for a certain interval or overriding the arm position only for a certain interval will have a step difference at the boundary if switched abruptly. We use &lt;code&gt;ramp_src&lt;/code&gt; to smooth it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are scenes where human reading wins over estimation.&lt;/strong&gt; In intervals where the arm is hidden behind the body, the estimation error becomes large. In such cases, it is faster for a person to look at the video and write it. However, &lt;strong&gt;only for that interval&lt;/strong&gt;, and without touching other joints.&lt;/p&gt;

&lt;p&gt;The support foot schedule is troublesome to write by hand, so we automatically generate it from the pelvis acceleration and correct only the necessary parts. When processing 41 materials in bulk, we did not make individual adjustments, and everything was automatic generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared Three Estimators and Returned to the First One
&lt;/h2&gt;

&lt;p&gt;We set up two other estimators (PromptHMR-Vid, Human3R) for comparison. All of them can write &lt;code&gt;landmarks.json&lt;/code&gt; in the same format, and the downstream process runs unchanged.&lt;/p&gt;

&lt;p&gt;In this comparison, the slip and penetration after grounding processing were at the same level for the three estimators.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Estimator/Setting&lt;/th&gt;
&lt;th&gt;Slip p90 (mm/f, Left/Right)&lt;/th&gt;
&lt;th&gt;Penetration Maximum (mm, Left/Right)&lt;/th&gt;
&lt;th&gt;Apply Error p50 (mm, Left/Right)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GVHMR&lt;/td&gt;
&lt;td&gt;3.8 / 5.4&lt;/td&gt;
&lt;td&gt;6.1 / 8.1&lt;/td&gt;
&lt;td&gt;11 / 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PromptHMR&lt;/td&gt;
&lt;td&gt;4.5 / 4.7&lt;/td&gt;
&lt;td&gt;5.7 / 2.1&lt;/td&gt;
&lt;td&gt;5 / 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PromptHMR raw&lt;/td&gt;
&lt;td&gt;2.6 / 5.7&lt;/td&gt;
&lt;td&gt;4.5 / 5.6&lt;/td&gt;
&lt;td&gt;7 / 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human3R&lt;/td&gt;
&lt;td&gt;4.1 / 2.9&lt;/td&gt;
&lt;td&gt;2.2 / 1.9&lt;/td&gt;
&lt;td&gt;5 / 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We note the following three conditions. This comparison is for the same dance material passed at 30fps, and the fps and evaluation process are different from the table at the beginning. Both cannot be read as the same numerical value. &lt;code&gt;PromptHMR raw&lt;/code&gt; is the estimation result without PromptHMR's own post-processing optimization (translation and camera re-fitting).&lt;/p&gt;

&lt;p&gt;The remaining difference is the nature of the estimator itself. With this material and setting, PromptHMR-Vid had 1.3-1.7 times more jerky foot and waist acceleration, and the jump disappeared (no floating intervals remained) when the post-processing optimization was applied. Human3R estimated that the camera moved 3.5°/0.29m even though it was a fixed camera clip.&lt;/p&gt;

&lt;p&gt;So, we stuck with the default GVHMR. Another thing we found out from this comparison is that &lt;strong&gt;none of the estimators can make the feet touch the ground&lt;/strong&gt;. The lowest vertex of the shoe sole was floating from the ground by 45-85mm on average, and the frames where it touched the ground were less than 1%.&lt;/p&gt;

&lt;p&gt;By the way, the initial version of this comparison table had an error. The other three columns were processed without "walk-away correction," leaving a difference of 0.4-1.1m, which appeared as &lt;strong&gt;85-143mm of fake penetration&lt;/strong&gt;. If the comparison conditions are not aligned, apparent differences will be generated in this form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output: VRMA and Viewer
&lt;/h2&gt;

&lt;p&gt;The final output is VRMA (VRM Animation), and we are using a three.js viewer to play it back. We set up a local server and made it possible to switch materials with a pull-down.&lt;/p&gt;

&lt;p&gt;What comes into effect here is the problem of &lt;strong&gt;which correction reaches which route&lt;/strong&gt;. Bone pose corrections are written as animation keys to VRMA. Skinning weight corrections, which will be discussed in &lt;a href="https://forge.workstyle.tech/blog/wrist-crease-is-skinning-not-pose/" rel="noopener noreferrer"&gt;Part 6&lt;/a&gt;, belong to the avatar's mesh and are used in the corrected VRM in the viewer. Whether the effect of Blender's modifiers or constraints reaches depends on the output route and whether baking is performed.&lt;/p&gt;

&lt;p&gt;We also had an accident due to the difference in routes. The processing of finger range of motion and line of sight was only in the rendering segment and &lt;strong&gt;did not run once in the VRMA creation route&lt;/strong&gt;. The viewer had a character with "eyes that don't follow and fingers that bend more than 90°." We solved this by moving the processing to right after the arm interference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Series Will Cover
&lt;/h2&gt;

&lt;p&gt;Each part will be written to be readable independently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Part 1 (this article)&lt;/td&gt;
&lt;td&gt;Making VRM characters dance from live-action videos ── Correcting defects in feet, arms, and fingers that remain after estimation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://forge.workstyle.tech/blog/head-chest-angle-zero-degrees/" rel="noopener noreferrer"&gt;The difference in head and chest angles was 0.000° for all frames ── A story about how motion measurement was broken&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://forge.workstyle.tech/blog/one-percent-shorter-leg-16-degree-knee/" rel="noopener noreferrer"&gt;Shortening the leg by 1% made the knee bend by 16° ── A pitfall in VRM's grounding correction&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://forge.workstyle.tech/blog/arm-clearance-flipping-front-to-back/" rel="noopener noreferrer"&gt;Arm penetration correction flips front and back ── Deciding the direction to escape using dynamic planning&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://forge.workstyle.tech/blog/thumb-rests-on-the-index-not-pushed-away/" rel="noopener noreferrer"&gt;The thumb rests on the index finger, not pushed away ── Correction to maintain grasping and smoothing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://forge.workstyle.tech/blog/wrist-crease-is-skinning-not-pose/" rel="noopener noreferrer"&gt;The cause of the wrist skin being crushed was skinning, not pose&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The next part will start with the abnormality that "the difference in head and chest angles was 0.000° for all frames." It's a story about doubting measurement before motion when appearance and numbers contradict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supplement: The Foundation is Someone Else's Implementation
&lt;/h2&gt;

&lt;p&gt;The skeleton of this pipeline (estimation → score → spec → lifting → FK application → QA, and comparison with the original video) is an implementation of &lt;a href="https://github.com/squall01337/mixamo-llm-mocap" rel="noopener noreferrer"&gt;squall01337/mixamo-llm-mocap&lt;/a&gt; (MIT). The part referred to as "fork origin" in this article refers to this repository. We explicitly state the reference to avoid confusion with repositories of the same name.&lt;/p&gt;

&lt;p&gt;The parts that we added after forking, which are quoted in this article, are not published. Please note that it is not possible to reproduce and verify the implementation by looking at the repository (the quotes include file names and line numbers).&lt;/p&gt;

&lt;p&gt;The unification of coordinate systems, range of motion, grounding, arm interference, hand processing, and migration to VRM are all parts that we added afterwards. The code quotes are necessary for explanation and are excerpted from the implementation. They are not code that can be pasted and run as is, but rather calculations that were taken out to illustrate the point.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>python</category>
      <category>3dcg</category>
      <category>vrm</category>
    </item>
    <item>
      <title>Apology Messages Were Tainting the Next Search — A Loop of Failure Strengthening Itself</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 16 Sep 2026 01:11:47 +0000</pubDate>
      <link>https://dev.to/orca_forge/apology-messages-were-tainting-the-next-search-a-loop-of-failure-strengthening-itself-4oe</link>
      <guid>https://dev.to/orca_forge/apology-messages-were-tainting-the-next-search-a-loop-of-failure-strengthening-itself-4oe</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/apology-that-poisoned-the-search/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=apology-that-poisoned-the-search" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Log of Voice Dialogue Avatar Had This Interval
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s content is explained      🤖 The corresponding description was not found.
👤 Can you hear me?                     🤖 (Same apology)
👤 Can you hear me again?               🤖 (Same apology)
👤 I can hear you.                      🤖 (Same apology)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4 consecutive turns returned almost the same apology.&lt;/strong&gt; The more the user tries to confirm, the more the same answer is returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause Was the Improvement I Made Earlier
&lt;/h2&gt;

&lt;p&gt;A little while ago, I made the following improvement: "Detailed" phrases like "Tell me more about that" cannot be searched alone, so &lt;strong&gt;add the previous conversation (user's speech + avatar's answer) to the search&lt;/strong&gt;. The measurement was also effective (0.602 → 0.741).&lt;/p&gt;

&lt;p&gt;Looking at the log, it was 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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;retrieval&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Add&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Can you hear me? I apologize, but the corresponding description was not found.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The apology sentence without grounds was also included in the search query.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The apology sentence has no topic. Not only that, but words like "corresponding description" and "another way of saying" pull the search in a completely unrelated direction. That's why the next one is also wrong. And the failed sentence is included in the next query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure was reinforcing itself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Judge by String
&lt;/h2&gt;

&lt;p&gt;At first, I thought of "detecting and excluding apology sentences". I stopped soon.&lt;/p&gt;

&lt;p&gt;Apology phrases &lt;strong&gt;differ depending on the job profile&lt;/strong&gt;. They can be set differently for window, support, and delivery, and can change during operation. Judging by string would create holes every time a new phrase is added.&lt;/p&gt;

&lt;p&gt;Instead, I decided based on &lt;strong&gt;facts&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_last_turn_grounded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;     &lt;span class="c1"&gt;# Don't add answers from turns without grounds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Whether there was a ground for that turn" is already judged by the system. It doesn't depend on phrases.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;I shouldn't have written this update in the recording process&lt;/strong&gt;. Since conversation log recording is best-effort (it doesn't stop the conversation even if it fails), if an exception occurs, the update will also fail. Even if the recording fails, the presence or absence of grounds affects the search quality of the next turn. I moved it to the end of the turn.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The apology path also needed to be marked&lt;/strong&gt;. If I drop it, the apology sentence will be included in the next query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another Thing That Went Against the Facts
&lt;/h2&gt;

&lt;p&gt;While investigating the same 4 turns, I noticed another problem. &lt;strong&gt;The page text was not held even once during that time&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;  &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;didn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t appear even once during the 4 turns
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was answering "This page does not contain the corresponding description" without holding the page text. &lt;strong&gt;This is against the facts&lt;/strong&gt;. Whether the visitor can't read it or it's not there after reading is completely different for the visitor.&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;No&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m currently reading the page content. Please wait a moment.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The corresponding description was not found on the page I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m currently viewing.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ I almost made another mistake. &lt;strong&gt;The synthesis audio reuse key didn't distinguish between phrases&lt;/strong&gt;. Since the standard response reuses audio by caching, if left as is, the audio for "reading" would play when intending to say "not found". I split the key by phrase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather like today?                → 🤖 (Try to answer the page content)
👤 Tell me about the weather.                   → 🤖 This page does not contain weather information, so I apologize.
👤 (Next question)                             → 🤖 (Answer normally)  ← Apology doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generalizable Things
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Designs that return output to input amplify when they fail&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Using conversation history as context is a natural design. However, there are two types of "avatar answers":&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answers with grounds     … Contain topics. Useful as context
Apology answers without grounds     … Contain no topics. Rather, distort the search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I should have used only the former. The system already had the material to distinguish (whether there was a ground for that turn), but I was treating answers as one thing.&lt;/p&gt;

&lt;p&gt;Similar structures exist elsewhere. Using summaries as input for the next summary, mixing generated data into learning data, using search results as the next search query — &lt;strong&gt;all of them "get better when good, worse when bad"&lt;/strong&gt;. A mechanism to stop when bad is needed.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;the judgment to stop should be based on facts, not strings&lt;/strong&gt;. Phrases change, but "whether there was a ground" doesn't change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Until the Voice Dialogue Avatar Answers Correctly
&lt;/h2&gt;

&lt;p&gt;This article is the last part of &lt;strong&gt;Part 2: Understanding Language&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: &lt;a href="https://forge.workstyle.tech/blog/one-line-in-a-huge-prompt/" rel="noopener noreferrer"&gt;The one line at the end of a huge prompt was ignored four times&lt;/a&gt;&lt;br&gt;
→ Next: Not all utterances are questions&lt;/p&gt;

&lt;p&gt;Series of 8 articles&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Stopping Sound&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There were two types of events with the same name&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The self-echo countermeasure never fired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/a-finger-on-the-speaker/" rel="noopener noreferrer"&gt;A finger on the speaker was breaking the echo canceller&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Understanding Language&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;The three "its" were different things&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/one-line-in-a-huge-prompt/" rel="noopener noreferrer"&gt;The one line at the end of a huge prompt was ignored four times&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forge.workstyle.tech/blog/apology-that-poisoned-the-search/" rel="noopener noreferrer"&gt;The apology words were poisoning the search&lt;/a&gt;&lt;/strong&gt; ← Now here&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Judging&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not all utterances are questions&lt;/li&gt;
&lt;li&gt;I thought I was measuring, but I was measuring something else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notes that led to this insight are summarized in &lt;a href="https://forge.workstyle.tech/blog/improving-voice-dialogue-avatars/" rel="noopener noreferrer"&gt;Improving the Response Quality of Voice Dialogue Avatars&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
    </item>
    <item>
      <title>My smartphone fingers were breaking the echo cancellation — speech cutting out due to non-verbal sounds</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:40:52 +0000</pubDate>
      <link>https://dev.to/orca_forge/sumahonozhi-gaekokiyanserawohuai-siteita-yin-denaimonodefa-hua-gaqie-reru-2lo2</link>
      <guid>https://dev.to/orca_forge/sumahonozhi-gaekokiyanserawohuai-siteita-yin-denaimonodefa-hua-gaqie-reru-2lo2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/a-finger-on-the-speaker/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=a-finger-on-the-speaker" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Avatar that Stopped Working on iPhone Safari
&lt;/h2&gt;

&lt;p&gt;The voice dialogue avatar that worked fine on PC had an issue on iPhone Safari.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The avatar seems to be reacting to its own speech.&lt;br&gt;
It also seems to be reacting when the speaker's volume changes while operating the smartphone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the Logs Showed
&lt;/h2&gt;

&lt;p&gt;Looking at the judgment records, a pattern emerged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;52.843&lt;/span&gt;  &lt;span class="nx"&gt;Speech&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="nf"&gt;detected &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VAD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;52.848&lt;/span&gt;  &lt;span class="nx"&gt;Bot&lt;/span&gt; &lt;span class="nx"&gt;stopped&lt;/span&gt; &lt;span class="nx"&gt;speaking&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="nx"&gt;was&lt;/span&gt; &lt;span class="nx"&gt;stopped&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="nx"&gt;milliseconds&lt;/span&gt; &lt;span class="nx"&gt;later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five instances showed the same pattern. The decisive factor was that &lt;strong&gt;no transcription was output&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The VAD judged that "speech has started" and stopped the avatar's speech as an interrupt. However, the sound was not recognized as a word, and no turn occurred. There was also a period where the avatar &lt;strong&gt;started speaking and was stopped repeatedly at 2-second intervals&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism
&lt;/h2&gt;

&lt;p&gt;The user's guess was " sudden decrease in volume". To clarify further, when you cover the speaker with your finger, &lt;strong&gt;the sound's loudness and resonance change&lt;/strong&gt;. The browser's echo canceller learns the relationship between the "sound sent to the speaker" and the "sound returned to the microphone" and cancels it out. If this relationship changes suddenly, &lt;strong&gt;it cannot be completely cancelled out, and the residual sound leaks out&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The VAD sees this as human speech. The reason there was no transcription output was that &lt;strong&gt;it was not a word&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason it didn't happen on PC was not because the countermeasures were effective, but because the browser's AEC was canceling out the echo during playback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gate I Created is Not Effective for This Symptom
&lt;/h2&gt;

&lt;p&gt;I had already implemented a gate as a countermeasure against self-echo, which drops speech immediately after the sound finishes. However, this is not effective. By design, &lt;strong&gt;speech during playback is allowed to pass through&lt;/strong&gt;. This is done to prevent interrupts from being killed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Self-echo    … After the sound finishes (AEC tail) → Gate's responsibility
Incorrect interrupt    … During playback                   → Outside gate's responsibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Echo and incorrect interrupts occur at different times. A different countermeasure is needed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Industry Handles This
&lt;/h2&gt;

&lt;p&gt;I investigated how web services that keep the microphone open for conversation handle this issue.&lt;/p&gt;

&lt;p&gt;LiveKit's publicly available numbers were specific. They don't trigger an interrupt immediately, but instead &lt;strong&gt;look at the first few hundred milliseconds of speech&lt;/strong&gt; (the rise, sustain, and rhythm of the waveform) before making a judgment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Listen to 216ms of audio on average&lt;/strong&gt; before interrupting&lt;/li&gt;
&lt;li&gt;This &lt;strong&gt;rejects 51% of VAD-based interrupts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The judgment model's inference itself is less than 30ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, &lt;strong&gt;delaying the judgment by 200-300ms to examine the audio's characteristics&lt;/strong&gt; was the answer.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;strong&gt;no one has a silver bullet for echo itself&lt;/strong&gt;. Deepgram's official documentation only says "leave it to the browser's standard", and there is no mention of mobile or speakerphones. Depending on the client-side AEC is a common premise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Make VAD Less Sensitive During Playback
&lt;/h2&gt;

&lt;p&gt;Since all observed misfires were &lt;strong&gt;short sounds&lt;/strong&gt; (no transcription output), we can filter by duration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;During&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s speech, increase the continuous time required to recognize speech from 0.2 seconds to 0.5 seconds
Do not change the normal time ＝ The response time of the turn remains unchanged. Only the interrupt becomes less sensitive
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework we were using had a mechanism for interrupt strategy, but &lt;strong&gt;waiting for the transcription to be ready&lt;/strong&gt; would delay the interrupt by about 1.6 seconds (1.2 seconds of silence waiting + speech recognition). This contradicts the requirements for responsiveness, so we didn't adopt it.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The VAD parameter setting API could not be used&lt;/strong&gt;. Because it initializes the internal state, &lt;strong&gt;calling it while the user is interrupting and speaking will cut off the speech&lt;/strong&gt;. Moreover, the timing you want to call it (when the avatar's speech stops) is precisely that moment. We changed it to directly overwrite the threshold frame count.&lt;/p&gt;

&lt;p&gt;⚠️ This attribute &lt;strong&gt;does not exist immediately after creating the object&lt;/strong&gt;. It is born when the sampling rate is decided (at the time the pipeline starts). We were testing with a fake object, so we didn't notice it, and &lt;strong&gt;it crashed the moment we added the item to test with the real thing&lt;/strong&gt;. Until then, we were suppressing exceptions and doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Before countermeasures    After countermeasures
Interrupts during playback    5 instances      0 instances
Self-echo disposal      0 instances      1 instance (speech 7ms after playback ended)
Real speech           ―        3 instances all passed (5.0 seconds / 10.5 seconds / 17.2 seconds later)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also left evidence of the countermeasures being effective in the logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Countermeasure&lt;/span&gt; &lt;span class="nx"&gt;against&lt;/span&gt; &lt;span class="nx"&gt;incorrect&lt;/span&gt; &lt;span class="nx"&gt;interrupts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;increase&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;speech&lt;/span&gt; &lt;span class="nx"&gt;judgment&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="nx"&gt;during&lt;/span&gt; &lt;span class="nx"&gt;playback&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This line not being output means the countermeasures are not working&lt;/strong&gt;. Since we were once deceived by this, we made sure to output both the evidence that it worked and the fact that it didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizable Points
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Audio input" and "speech" are different events, and VAD can only judge the former&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;VAD cuts speech intervals based on audio energy and features. It cannot determine whether a person intentionally spoke. Therefore, coughs, noise, machine contact sounds, and echo residuals all become "speech".&lt;/p&gt;

&lt;p&gt;There are two directions for countermeasures.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cut by time&lt;/strong&gt; (ignore sounds that don't continue for a certain time) — deterministic, light, and delayed by several hundred milliseconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut by characteristics&lt;/strong&gt; (look at waveforms or rhythms) — high accuracy, requires a model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The difficulty level also changes depending on the environment. &lt;strong&gt;Desktop PCs with headsets are easy, while smartphones with speakers are difficult&lt;/strong&gt;. The device moves, the way it's held changes, and the speaker is covered with a finger — the echo canceller's premise that "the sound path is constant" does not hold for mobile devices.&lt;/p&gt;

&lt;p&gt;"Since it worked on PC, it's fine" is not applicable to voice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Making the Voice Dialogue Avatar Answer Correctly
&lt;/h2&gt;

&lt;p&gt;This article is the last part of &lt;strong&gt;Part 1: Stopping the Sound&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: &lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The gate that never fired&lt;/a&gt;&lt;br&gt;
→ Next: &lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;The three kinds of "it"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Series of 8 articles&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Stopping the Sound&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There were two types of events with the same name&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The gate that never fired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forge.workstyle.tech/blog/a-finger-on-the-speaker/" rel="noopener noreferrer"&gt;A finger on the speaker was breaking the echo canceller&lt;/a&gt;&lt;/strong&gt; ← Now here&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Understanding Words&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;The three kinds of "it"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/one-line-in-a-huge-prompt/" rel="noopener noreferrer"&gt;One line at the end of a huge prompt was ignored four times&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/apology-that-poisoned-the-search/" rel="noopener noreferrer"&gt;Apology words were poisoning the search&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Judging&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not all speech is a question&lt;/li&gt;
&lt;li&gt;I thought I was measuring something, but it was actually something else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notes that led to this insight are summarized in &lt;a href="https://forge.workstyle.tech/blog/" rel="noopener noreferrer"&gt;The Quality of the Voice Dialogue Avatar's Response&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vad</category>
    </item>
    <item>
      <title>The single line at the end of the massive prompt was ignored all four times</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:09:18 +0000</pubDate>
      <link>https://dev.to/orca_forge/the-single-line-at-the-end-of-the-massive-prompt-was-ignored-all-four-times-afb</link>
      <guid>https://dev.to/orca_forge/the-single-line-at-the-end-of-the-massive-prompt-was-ignored-all-four-times-afb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/one-line-in-a-huge-prompt/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=one-line-in-a-huge-prompt" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conversation with the Voice Dialogue Avatar
&lt;/h2&gt;

&lt;p&gt;I had the following conversation with the 24-hour avatar.&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="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;working&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;mechanism&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Explain&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;whole&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;
&lt;span class="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;What&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Pod&lt;/span&gt; &lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;mentioned&lt;/span&gt; &lt;span class="n"&gt;earlier&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;                &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Explain&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;whole&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="n"&gt;again&lt;/span&gt;
&lt;span class="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;Tell&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;                                         &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Explain&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;whole&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="n"&gt;again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When I tried to dig deeper, I got the same summary back.&lt;/strong&gt; The conversation log had a repetition flag set for 5 out of 13 turns.&lt;/p&gt;

&lt;p&gt;The anaphora judgment was working correctly (recognizing "earlier" as a remote context and adding the corresponding past conversation to the search query). What was off was &lt;strong&gt;the response&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Countermeasure
&lt;/h2&gt;

&lt;p&gt;I added instructions right after the latest user utterance — the most effective position in the prompt.&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;visitor&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;listening&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Without&lt;/span&gt; &lt;span class="n"&gt;repeating&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;overall&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;stated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;please&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;specific&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;pointing&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;following&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;corresponding&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
&lt;span class="n"&gt;Do&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;again&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose this position for a reason. In the same project, I had placed speech-linked pointing instructions and tool call reminders in this position, and they were effective. As the history grows, the model starts to omit tags by simulating the "past self" that didn't output tags, so I reminded it to output tags at the end of each turn — a countermeasure that actually worked.&lt;/p&gt;

&lt;p&gt;I deployed and tried it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing Changed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll tell you more about it.    → Explain the whole thing
👤 Can you tell me more about it?      → Explain the whole thing again (repetition flag)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, I checked if the instructions were actually being sent. &lt;strong&gt;They were sent. All four times.&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;Question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Tell&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;For&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;visitor&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;listening&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Without&lt;/span&gt; &lt;span class="n"&gt;repeating&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;overall&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;stated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;please&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;specific&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;pointing&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;following&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;corresponding&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
&lt;span class="n"&gt;Do&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;again&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt included &lt;strong&gt;the full text of the page, 8,207 characters&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Answer Was Written in the Same File
&lt;/h2&gt;

&lt;p&gt;While searching for the cause and reading the code, I came across a comment that I didn't write.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Micro-call separation for extraction responses: For turns where the corresponding sentence can be determined deterministically, &lt;br&gt;
switch to a dedicated call for the minimum prompt that excludes persona, catalog, history, and tag conventions.&lt;br&gt;
&lt;strong&gt;A single line in a huge prompt is ignored&lt;/strong&gt; (actual measurement: even if the correct sentence is placed at the beginning, it flows into a summary), &lt;br&gt;
&lt;strong&gt;but a weak model will follow a small context and a single task&lt;/strong&gt; (separation of generation responsibilities).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The same phenomenon was recorded in the same file.&lt;/strong&gt; And the existing code was escaping to a dedicated small call for that reason.&lt;/p&gt;

&lt;p&gt;I fell into the same trap, in the same place, after reporting that I had placed it in the "most effective position".&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating Responsibilities
&lt;/h2&gt;

&lt;p&gt;I made the digging deeper the same shape. I stopped passing the full text and instead passed &lt;strong&gt;only the relevant section&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="err"&gt;【&lt;/span&gt;&lt;span class="n"&gt;Already&lt;/span&gt; &lt;span class="n"&gt;mentioned&lt;/span&gt;&lt;span class="err"&gt;】&lt;/span&gt;&lt;span class="n"&gt;Previous&lt;/span&gt; &lt;span class="nf"&gt;conversation &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;【&lt;/span&gt;&lt;span class="n"&gt;Main&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="err"&gt;】&lt;/span&gt;&lt;span class="n"&gt;Relevant&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="nf"&gt;only &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;【&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="err"&gt;】&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="n"&gt;utterance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instruction is "Without repeating what has already been mentioned, describe the specific content that has not been touched yet in 2-4 sentences".&lt;br&gt;
I made it a single task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number of calls does not increase.&lt;/strong&gt; Since it replaces the existing call, it's actually faster because the input is smaller. &lt;br&gt;
For turns where there is no relevant section or previous conversation, it follows the same route as before, so it doesn't get worse.&lt;/p&gt;
&lt;h2&gt;
  
  
  It Was Still 0
&lt;/h2&gt;

&lt;p&gt;I deployed and tried it, and &lt;strong&gt;the digging deeper call didn't fire at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There were &lt;strong&gt;two routes to get the material&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Full text route      … Turns that use the full text of the page. I had implemented it here
Search route      … Turns where the page search hits. **This was missing**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Ironically, &lt;strong&gt;the turns where the search hits are the ones where the material for digging deeper is most available&lt;/strong&gt;. &lt;br&gt;
I wasn't picking it up. The previous time it happened to fire was only when it went through the full text route.&lt;/p&gt;

&lt;p&gt;This wouldn't be found in testing. I only found out when I saw the 0 fires in the actual machine log.&lt;/p&gt;
&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="mi"&gt;15&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="mi"&gt;28&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain this page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="err"&gt;🤖&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;continues&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;broadcast&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;AI&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;its&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt; &lt;span class="n"&gt;assurance&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tell me more about it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;things that only come out after a long time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="n"&gt;Already&lt;/span&gt; &lt;span class="n"&gt;mentioned&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;322&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt; &lt;span class="n"&gt;Main&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1519&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt;
      &lt;span class="err"&gt;🤖&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bugs that only come out after a long time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="n"&gt;like&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="n"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;periodic&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
         &lt;span class="n"&gt;which&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;appear&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="n"&gt;passes&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It entered a specific part instead of repeating the summary. The repetition flag was not set either.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generalizable Things
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's not the position of the instruction, but what you pass with it that determines its effectiveness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"It's most effective to put it at the end of the prompt" is conditionally correct. If the surroundings are short, it's effective. &lt;br&gt;
If you put it with an 8,000-character main text, that one line becomes less than 1% of the context. &lt;br&gt;
The model is pulled into the dominant task of "summarizing the given data".&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;the more ambiguous the input, the more the model escapes into a summary&lt;/strong&gt;. The same thing happened when the state was the same.&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="err"&gt;👤&lt;/span&gt; &lt;span class="n"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather like today?          → 🤖 This page is about the system that continues to broadcast with an AI avatar... (summary)
👤 Yes, please wait.  → 🤖 This page is about the system that continues to broadcast with an AI avatar... (summary)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even for utterances that aren't questions, the same summary is returned. The latter was &lt;strong&gt;a state where the same answer was returned no matter what&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The direction of the countermeasure was not "to make the instruction stronger", but &lt;strong&gt;"to make the task single and narrow down the material"&lt;/strong&gt;. &lt;br&gt;
The less you pass, the relatively larger the instruction becomes.&lt;/p&gt;

&lt;p&gt;Another thing. &lt;strong&gt;The answer to the same problem may already be in the codebase&lt;/strong&gt;. In my case, it was a comment that I found after implementing the countermeasure and deploying it, and it didn't work.&lt;br&gt;
Searching comments in the codebase for "has this symptom occurred before" is more cost-effective than I thought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Until the Voice Dialogue Avatar Answers Correctly
&lt;/h2&gt;

&lt;p&gt;This article is part of &lt;strong&gt;Part 2: Understanding Language&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: &lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;""It", "this page", and "earlier" were different things&lt;/a&gt;&lt;br&gt;
→ Next: Apology words were contaminating the next search&lt;/p&gt;

&lt;p&gt;Series of 8 articles&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Stopping the Sound&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There were two types of events with the same name&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The self-echo countermeasure had never fired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The smartphone's finger was breaking the echo canceller&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Understanding Language&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;""It", "this page", and "earlier" were different things&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forge.workstyle.tech/blog/one-line-in-a-huge-prompt/" rel="noopener noreferrer"&gt;A single line in a huge prompt was ignored four times&lt;/a&gt;&lt;/strong&gt; ← Now here&lt;/li&gt;
&lt;li&gt;Apology words were contaminating the next search&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Seeing Through&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not all utterances are questions&lt;/li&gt;
&lt;li&gt;I thought I was measuring, but I was measuring something else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notes that became the basis for this knowledge are summarized in &lt;a href="https://forge.workstyle.tech/blog/voice-dialogue-avatars/" rel="noopener noreferrer"&gt;The Response Quality of Voice Dialogue Avatars&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Mic Doesn't Return Even After Reconnecting — It Was the Old Microphone Marked for Disposal That Was Enabled</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:36:14 +0000</pubDate>
      <link>https://dev.to/orca_forge/mic-doesnt-return-even-after-reconnecting-it-was-the-old-microphone-marked-for-disposal-that-was-27ge</link>
      <guid>https://dev.to/orca_forge/mic-doesnt-return-even-after-reconnecting-it-was-the-old-microphone-marked-for-disposal-that-was-27ge</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/reconnected-but-the-mic-stayed-off/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=reconnected-but-the-mic-stayed-off" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Operating a Voice Dialogue Avatar Embedded in a Website
&lt;/h2&gt;

&lt;p&gt;The avatar connects when the page is opened and responds when spoken to. Behind the scenes, WebRTC connects the browser and voice pipeline.&lt;/p&gt;

&lt;p&gt;However, after restarting the server, the avatar stopped responding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;👤 (speaking)
🤖 (no response)
👤 (speaking again)
🤖 (no response)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually, everything seems fine. The avatar is standing, and the microphone button is still ON. &lt;strong&gt;Visitors won't notice that it's "broken" until they reload the page.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Initially, There Was No Reconnection Mechanism
&lt;/h2&gt;

&lt;p&gt;The first thing I checked was that there was no code to reconnect after a disconnection.&lt;/p&gt;

&lt;p&gt;The connection process is only called once when the page is opened, and there is no path to reconnect after a disconnection. This means that not only server restarts but also events like subway rides, Wi-Fi switching, or waking up from sleep can cause the same issue. With each deployment, visitors who happen to be speaking to the avatar at that moment will be met with silence.&lt;/p&gt;

&lt;p&gt;I added a reconnection process with exponential backoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&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;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;waits&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="nx"&gt;retryRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;waits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="nx"&gt;retryRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;reconnectRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will &lt;strong&gt;not timeout after the last 30 seconds, but repeat indefinitely&lt;/strong&gt;. I intentionally did this because visitors are likely to leave the page open, rather than coming back later. Since the cost of reconnection is almost zero, there's no reason to give up.&lt;/p&gt;

&lt;p&gt;After deploying and restarting the server, I got this log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[widget] Connection lost — reconnection in 1 second (1st attempt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...but it stopped there. The first log appeared, but the second one didn't. It didn't reconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-Idempotent Function Was Called as If It Were Idempotent
&lt;/h2&gt;

&lt;p&gt;The connection process started like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// ← here&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If an old connection object remains, it will return without doing anything.&lt;/strong&gt; As a function called only once when the page is opened, this guard is correct to prevent double connections.&lt;/p&gt;

&lt;p&gt;However, when reconnecting, it's precisely the "old connection" that remains. The &lt;code&gt;connect()&lt;/code&gt; function was quietly returning without doing anything every time. No error, no log.&lt;/p&gt;

&lt;p&gt;I changed it to call &lt;code&gt;disconnect()&lt;/code&gt; before reconnecting. This time, it connected successfully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15:09:43  Server restart
15:10:16  Voice pipeline detected disconnection
15:10:28  Reconnection (12 seconds later)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conversation history remained intact. Since the conversation ID is stored in the browser's &lt;code&gt;sessionStorage&lt;/code&gt;, reconnection allows the conversation to &lt;strong&gt;"continue from where it left off."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Microphone Alone Didn't Come Back
&lt;/h2&gt;

&lt;p&gt;Even though it reconnected, speaking to the avatar yielded no response. The microphone button was still ON.&lt;/p&gt;

&lt;p&gt;I confirmed this on the server-side log, where I had implemented a diagnostic that outputs the volume and "human voice-like" detection every 30 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[vad] Last 30 seconds: 1500 judgments, 0 speeches, 0 near-misses (average voice 0.00, volume 0.00)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The volume remained 0.00.&lt;/strong&gt; No sound was reaching the server. The microphone track is obtained when connecting and &lt;strong&gt;disabled by default&lt;/strong&gt;. The button is enabled when pressed. So, after reconnection, if the microphone was originally ON, it needs to be re-enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Read the Source and Missed It Three Times
&lt;/h2&gt;

&lt;p&gt;This is where the embarrassing part begins. I read the code, formed a hypothesis, fixed it, and missed it three times.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suspected cause&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Limit on usage&lt;/td&gt;
&lt;td&gt;There's a process that mutes the microphone when the limit is reached&lt;/td&gt;
&lt;td&gt;The limit wasn't being applied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-second status check misjudging&lt;/td&gt;
&lt;td&gt;I had added a process to verify the reconnection later&lt;/td&gt;
&lt;td&gt;It was unrelated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnection process's internal order being swapped&lt;/td&gt;
&lt;td&gt;Asynchronous processes were running in parallel&lt;/td&gt;
&lt;td&gt;This was also incorrect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three times, I &lt;strong&gt;"read the source and chose a plausible explanation."&lt;/strong&gt; Each one seemed to make sense when reading the code, but none of them were actually happening.&lt;/p&gt;

&lt;p&gt;On the fourth attempt, I gave up and added diagnostic logs to see &lt;strong&gt;who was muting the microphone and when&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Log Line Solved It
&lt;/h2&gt;

&lt;p&gt;The returned log was this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[widget] After microphone recovery: {called: true, enabled: [true]}
[voicepipe] Obtaining new microphone track (default is disabled)    ← ★this is later
[widget] 2 seconds after microphone recovery: {enabled: [true], returned: true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The microphone was being enabled before obtaining the new track.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In other words, the track I was enabling was the &lt;strong&gt;old track that was about to be discarded&lt;/strong&gt;. After that, &lt;code&gt;connect()&lt;/code&gt; created a new track, which started disabled by default.&lt;/p&gt;

&lt;p&gt;It wasn't that someone was muting it later; &lt;strong&gt;the order was simply reversed&lt;/strong&gt;. My three hypotheses were all about "who muted it," but no one was muting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Premature Reason Was a Lying State Variable
&lt;/h2&gt;

&lt;p&gt;Then, why did the enablement run before the new track was obtained? The condition was written like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voicepipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// enable when connected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The update of the connection status was like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oniceconnectionstatechange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iceConnectionState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disconnected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// ← only when failed&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The status remains 'connected' even when 'disconnected' or 'closed'.&lt;/strong&gt; The status is only updated to 'failed' when it actually fails.&lt;/p&gt;

&lt;p&gt;So, the condition "enable when connected" &lt;strong&gt;was already met immediately after disconnection&lt;/strong&gt;. I was waiting, but I wasn't actually waiting.&lt;/p&gt;

&lt;p&gt;This state variable isn't wrong. It's reasonable not to immediately set the status to 'failed' when disconnected, as it might recover. &lt;strong&gt;The problem is that I used this variable, which has a specific meaning, to judge "is it currently connected?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was simple. Instead of looking at the state variable, I waited for the connection process to complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;voicepipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;voicepipe&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="c1"&gt;// wait for completion&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;micOnRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;voicepipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMicEnabled&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;// then enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;connect()&lt;/code&gt; function returns after obtaining a new track and completing the offer/answer process, so at this point, I can be sure to get the new track. &lt;strong&gt;I changed from "looking at the status and judging" to "executing in order."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It Worked
&lt;/h2&gt;

&lt;p&gt;I confirmed it by restarting the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15:10:28  Reconnection (45 seconds after server restart / 12 seconds after disconnection detection)
15:17:42  [vad] Volume 0.51, 0 speeches      ← microphone is alive and waiting
15:18:12  [vad] 31 speeches, voice 0.03        ← responded to speech
15:17:49  [Talk TTS] 'Hello. What brings you here today?'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a byproduct, &lt;strong&gt;I can now determine the microphone's status from the server-side log alone.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Volume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Microphone OFF&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microphone ON&lt;/td&gt;
&lt;td&gt;around 0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After realizing this, I no longer need to ask visitors to open their browser's console. I can verify the issue from the server-side log when they report that the microphone isn't responding despite being turned on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Be careful not to call non-idempotent functions as if they were idempotent.&lt;/strong&gt; The &lt;code&gt;if (already exists) return;&lt;/code&gt; guard is correct for initialization, but it becomes a &lt;strong&gt;"quietly do nothing" function&lt;/strong&gt; when called from the "retry" path. Since it doesn't produce an error, it takes time to discover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State variables might only be looking at a part of the failures.&lt;/strong&gt; The &lt;code&gt;status&lt;/code&gt; variable this time was meant to indicate "&lt;code&gt;failed&lt;/code&gt; or not," not "is it currently connected?" The name looks like the latter, but &lt;strong&gt;the processes that use this variable as a condition will all be wrong.&lt;/strong&gt; If you want to wait for something, it's more reliable to &lt;code&gt;await&lt;/code&gt; the completion rather than looking at the state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypotheses formed by reading the source can be wrong.&lt;/strong&gt; I was wrong three times. Diagnostic logs solved it in one attempt. Code only teaches you "&lt;strong&gt;what can happen,"&lt;/strong&gt; not "&lt;strong&gt;what happened."&lt;/strong&gt; If you come up with three plausible hypotheses, it's likely that &lt;strong&gt;you can't figure it out just by reading the code.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnection is not just about server restarts.&lt;/strong&gt; Before implementing reconnection, I organized the causes of disconnections.

&lt;ul&gt;
&lt;li&gt;Deployment, pod restart, node replacement&lt;/li&gt;
&lt;li&gt;Wi-Fi ↔ mobile network switching, subway, elevator&lt;/li&gt;
&lt;li&gt;Smartphone sleep and wake (background connections may be terminated)&lt;/li&gt;
&lt;li&gt;Intermediate NAT or TURN timeouts (may drop after prolonged silence)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While server-side issues can be reduced with operation, the last three cannot be reduced. &lt;strong&gt;Investing in "reconnect even if disconnected" rather than "don't disconnect" is the right approach.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>react</category>
    </item>
    <item>
      <title>'That,' 'this page,' and 'that one' are different: 1 win and 3 losses by treating pronouns uniformly</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:04:26 +0000</pubDate>
      <link>https://dev.to/orca_forge/that-this-page-and-that-one-are-different-1-win-and-3-losses-by-treating-pronouns-uniformly-4d5l</link>
      <guid>https://dev.to/orca_forge/that-this-page-and-that-one-are-different-1-win-and-3-losses-by-treating-pronouns-uniformly-4d5l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=three-kinds-of-it" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An avatar that interacts through voice receives the following utterances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;👤 Are there any articles about streaming on YouTube?   → Article identified
👤 Can you explain that in more detail?               → Only responds with "Yes, I'll explain in detail."
👤 Wait, you're not explaining in detail?           → Only responds with "Sorry, just a little more..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Despite identifying the subject in the previous turn, the context is lost in the next turn.&lt;/strong&gt; While this is the most natural way to continue the conversation, it's the weakest for search. The phrase "Can you explain that in more detail?" doesn't contain any searchable terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naive Solution and Results
&lt;/h2&gt;

&lt;p&gt;I modified the system to include the user's previous utterance in the search query. The utterance itself, which is passed to the response, remains unchanged (rewriting it causes the model to respond to things the user didn't say).&lt;/p&gt;

&lt;p&gt;After running this for a while, I re-evaluated it on a real site (38 pages).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utterance&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;With Previous&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can you explain that in more detail?&lt;/td&gt;
&lt;td&gt;0.435 ❌&lt;/td&gt;
&lt;td&gt;0.602 ❌&lt;/td&gt;
&lt;td&gt;Correct answer was 2nd. Retrieved the homepage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What about the avatar discussion earlier?&lt;/td&gt;
&lt;td&gt;0.597 ❌&lt;/td&gt;
&lt;td&gt;0.594 ❌&lt;/td&gt;
&lt;td&gt;Retrieved a different avatar article&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What other articles are on this site?&lt;/td&gt;
&lt;td&gt;0.489 ✅&lt;/td&gt;
&lt;td&gt;0.429 ⚠️&lt;/td&gt;
&lt;td&gt;Score decreased when added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why is silent streaming necessary?&lt;/td&gt;
&lt;td&gt;0.627 ✅&lt;/td&gt;
&lt;td&gt;0.579 ❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Correct answer dropped when added&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The result was 1 win and 3 losses.&lt;/strong&gt; The intended fix worsened the results in 3 out of 4 cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Types of Deictic Expressions Mixed
&lt;/h2&gt;

&lt;p&gt;Upon examining actual conversation logs, it became clear that "this" and "that" &lt;strong&gt;refer to completely different things&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;👤 What's on this page?        ← Current screen
👤 What other articles are on this site?        ← Current site
👤 Can you explain that in more detail?                   ← Previous interaction
👤 What about the avatar discussion earlier?               ← Interaction from a few turns ago
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Japanese grammar, these are distinguished as &lt;strong&gt;situational deixis&lt;/strong&gt; (referring to something present in the speech situation) and &lt;strong&gt;textual deixis&lt;/strong&gt; (referring to a previously mentioned linguistic expression). In a voice + screen UI, &lt;strong&gt;the situation = the screen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Textual deixis further splits into two types: referring to the immediately preceding interaction and referring to an interaction from several turns ago.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Context Needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Situational Deixis&lt;/td&gt;
&lt;td&gt;This page / This site&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Don't add&lt;/strong&gt;. The answer is on the current page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immediate Textual&lt;/td&gt;
&lt;td&gt;That / It / In more detail / Continue&lt;/td&gt;
&lt;td&gt;Add the &lt;strong&gt;full previous exchange&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distant Textual&lt;/td&gt;
&lt;td&gt;Earlier about X / The X example&lt;/td&gt;
&lt;td&gt;Search history for X and add &lt;strong&gt;only the relevant exchange&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(Interrogatives)&lt;/td&gt;
&lt;td&gt;Why / How / What&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not deictic&lt;/strong&gt;. Adding drops the correct answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Including interrogatives in the list caused the deterioration in the 4th example. "Why is silent streaming necessary?" is an independent question with no carryover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Be Added Isn't the "User's Utterance" but the "Full Exchange"
&lt;/h2&gt;

&lt;p&gt;In "Can you explain that in more detail?", "that" typically refers to &lt;strong&gt;what the avatar just explained&lt;/strong&gt;, not the previous question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query Construction&lt;/th&gt;
&lt;th&gt;1st&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline utterance only&lt;/td&gt;
&lt;td&gt;❌ Unrelated article&lt;/td&gt;
&lt;td&gt;0.435&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous user utterance + current&lt;/td&gt;
&lt;td&gt;❌ Homepage&lt;/td&gt;
&lt;td&gt;0.602&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full previous exchange + current&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Correct&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.741&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous avatar response + current&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;0.729&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only when the full exchange (user's utterance + avatar's response) was added did the correct answer rank first.&lt;/p&gt;

&lt;h2&gt;
  
  
  For Distant Textual Deixis, "Sliding Window" Performs Worse Than Not Adding
&lt;/h2&gt;

&lt;p&gt;For "What about the avatar discussion earlier?", where the topic was 3 turns prior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;1st&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline utterance only&lt;/td&gt;
&lt;td&gt;❌ Different avatar article&lt;/td&gt;
&lt;td&gt;0.597&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immediate 1 exchange + current&lt;/td&gt;
&lt;td&gt;❌ Same as above&lt;/td&gt;
&lt;td&gt;0.587&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Immediate 2 exchanges + current&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌❌ &lt;strong&gt;Unrelated streaming article&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0.564&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immediate 3 exchanges + current (includes correct)&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;0.732&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Only relevant exchange + current&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.813&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things became clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Earlier" doesn't reach with just the immediate exchange.&lt;/strong&gt; The uncertainty of how far back to go is inherent in this expression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial backtracking performs worse than not adding.&lt;/strong&gt; It gets dragged down by intervening topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding only the relevant exchange is better than adding everything&lt;/strong&gt; (0.813 vs 0.732). Extra context dilutes the query.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fortunately, this utterance &lt;strong&gt;contains its own clue&lt;/strong&gt;: "Earlier about &lt;strong&gt;avatar&lt;/strong&gt;". Searching the conversation history for exchanges containing "avatar" suffices. Matching with 2-gram character overlap was enough; no morphological analysis or additional embeddings were needed.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Failed attempt to embed extracted terms in the prompt.&lt;/strong&gt; The implementation dropped function words at the character level, turning "それをもうちょっと詳しく" into &lt;strong&gt;"うちょ詳"&lt;/strong&gt;. While usable as a search key, it's unreadable as Japanese. Since the question itself is already in the prompt, there was no need to reinsert it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "In Detail" Was Being Truncated by Character Count
&lt;/h2&gt;

&lt;p&gt;Another finding from real data came from user feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When "in detail" appears, it should always be accompanied by either a request or an intention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verified with 22 real data points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contains "in detail" / "specifically"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22 (all)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accompanied by request (explain / tell)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accompanied by intention (want to know / want to ask)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neither&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8 (36%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While the meaning aligns with the feedback, &lt;strong&gt;one-third were omitted on the surface&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;About creating a stream, in detail.
Specific steps are.
Specific content, pipeline about.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent is clearly a request, yet the verb is dropped (nominalization). This is common in Japanese, similar to zero anaphora. Making "accompanied by a request or intention" mandatory would drop one-third of cases.&lt;/p&gt;

&lt;p&gt;Meanwhile, &lt;strong&gt;"in detail" / "specifically" appeared in all 22 cases&lt;/strong&gt;, making the terms themselves the most reliable indicators.&lt;/p&gt;

&lt;p&gt;This verification also uncovered a larger flaw: &lt;strong&gt;4 out of 22 cases exceeded 25 characters&lt;/strong&gt; (longest was 70 characters).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No, not that, but about how maintaining 30FPS with set intervals gradually desynchronizes lips and audio, I want to know more about that.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision to add context was tied to the decision for follow-up. This included a 25-character cutoff to catch "short utterances that can't be searched alone." Thus, &lt;strong&gt;these weren't treated as follow-up requests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Adding context" and "indicating follow-up" are separate dimensions.&lt;/strong&gt; Long utterances can be searched independently but still require follow-up instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utterance&lt;/th&gt;
&lt;th&gt;Before Fix&lt;/th&gt;
&lt;th&gt;After Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can you explain that in more detail?&lt;/td&gt;
&lt;td&gt;0.602 (incorrect)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.741 (correct)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What about the avatar discussion earlier?&lt;/td&gt;
&lt;td&gt;0.594 (incorrect)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832 (correct)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What other articles are on this site?&lt;/td&gt;
&lt;td&gt;0.502 (correct)&lt;/td&gt;
&lt;td&gt;0.483 (correct)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why is silent streaming necessary?&lt;/td&gt;
&lt;td&gt;0.624 (correct)&lt;/td&gt;
&lt;td&gt;0.627 (correct)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Zero LLM round trips&lt;/strong&gt;, with no change in response time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizable Insights
&lt;/h2&gt;

&lt;p&gt;In conversational search, this process is called &lt;strong&gt;Conversational Query Rewriting&lt;/strong&gt;. It handles two phenomena: &lt;strong&gt;anaphora&lt;/strong&gt; ("that" referring back) and &lt;strong&gt;ellipsis&lt;/strong&gt; (omitted terms), with the latter being a specialized research area in Japanese as "zero anaphora."&lt;/p&gt;

&lt;p&gt;Industry reports suggest &lt;strong&gt;over 60% of follow-up utterances contain unresolved anaphora or implicit context&lt;/strong&gt;. Rewriting can be handled by smaller models, with p95 under 100ms.&lt;/p&gt;

&lt;p&gt;However, research also shows &lt;strong&gt;"Not All Queries Need Rewriting."&lt;/strong&gt; Rewriting queries that already contain sufficient information &lt;strong&gt;reduces dense retrieval performance&lt;/strong&gt;. The recommendation is to default to no rewriting in stable domains and use search confidence as a proxy when selectively rewriting.&lt;/p&gt;

&lt;p&gt;My implementation decided based on "utterance appearance" (character count and word list), causing deterioration in 3 out of 4 cases.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;voice + screen UIs, one research assumption changes&lt;/strong&gt;: Unlike text-only conversational search, &lt;strong&gt;"this" can refer to the screen&lt;/strong&gt;. In real data, many deictic expressions pointed to the screen, not the conversation. This distinction directly corresponded to Japanese grammar's situational/textual deixis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Making Voice Interaction Avatars Respond Properly
&lt;/h2&gt;

&lt;p&gt;This article is &lt;strong&gt;Part 2: Decoding Language&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: Smartphone Fingers Were Breaking Echo Cancellers&lt;br&gt;&lt;br&gt;
→ Next: The Line Added to the End of a Huge Prompt Was Ignored All 4 Times&lt;/p&gt;

&lt;p&gt;Full Series (8 Articles)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Stopping Sound&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There Were Two Events with the Same Name&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The Echo Countermeasure Never Activated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Smartphone Fingers Were Breaking Echo Cancellers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Decoding Language&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forge.workstyle.tech/blog/three-kinds-of-it/" rel="noopener noreferrer"&gt;“That,” “This Page,” and “Earlier” Were All Different&lt;/a&gt;&lt;/strong&gt; ← You are here&lt;/li&gt;
&lt;li&gt;The Line Added to the End of a Huge Prompt Was Ignored All 4 Times&lt;/li&gt;
&lt;li&gt;Apology Words Were Contaminating the Next Search&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Discerning&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not All Utterances Are Questions&lt;/li&gt;
&lt;li&gt;What I Thought I Was Measuring, I Wasn’t&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The insights are summarized in the notes on &lt;strong&gt;Voice Interaction Avatar Response Quality&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>rag</category>
    </item>
    <item>
      <title>Echo Issue Went Unnoticed as It Never Triggered</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:32:36 +0000</pubDate>
      <link>https://dev.to/orca_forge/echo-issue-went-unnoticed-as-it-never-triggered-57ln</link>
      <guid>https://dev.to/orca_forge/echo-issue-went-unnoticed-as-it-never-triggered-57ln</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=the-gate-that-never-fired" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Voice Dialogue Avatar Was Responding to Its Own Utterances
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;🤖&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yes, I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll check, please wait a moment.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;STT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll check, please wait a moment.] Confidence -0.306
🤖 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;please&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;moment&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It's not fabrication, but correct transcription&lt;/strong&gt;, so it can't be dropped due to confidence. The confidence threshold is intentionally disabled (the true identity of low confidence is surrounding environmental noise, which can lower the confidence of real utterances to -0.826).&lt;/p&gt;

&lt;p&gt;There was no defense on the server side, and only the browser's echo canceller was relied upon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Made According to Convention
&lt;/h2&gt;

&lt;p&gt;Investigation revealed that this issue has a established solution: &lt;strong&gt;discard input for a certain period of time after utterance playback ends&lt;/strong&gt;.&lt;br&gt;
300ms was insufficient, and 500-800ms was required (AEC tail).&lt;/p&gt;

&lt;p&gt;Input during playback is not discarded. Discarding it would prevent interruption, which is the responsibility of the echo canceller.&lt;/p&gt;

&lt;p&gt;I implemented this and wrote tests.&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;OK&lt;/span&gt; &lt;span class="n"&gt;Utterance&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="n"&gt;immediately&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="n"&gt;playback&lt;/span&gt; &lt;span class="nf"&gt;ends &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Discarded&lt;/span&gt;
&lt;span class="n"&gt;OK&lt;/span&gt; &lt;span class="n"&gt;Utterance&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="n"&gt;playback&lt;/span&gt; &lt;span class="nf"&gt;ends &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="n"&gt;AEC&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Discarded&lt;/span&gt;
&lt;span class="n"&gt;OK&lt;/span&gt; &lt;span class="n"&gt;Utterance&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="n"&gt;outside&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nf"&gt;window &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Passed&lt;/span&gt;
&lt;span class="n"&gt;OK&lt;/span&gt; &lt;span class="n"&gt;Utterance&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="n"&gt;during&lt;/span&gt; &lt;span class="nf"&gt;playback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;barge&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;Passed&lt;/span&gt;
&lt;span class="n"&gt;OK&lt;/span&gt; &lt;span class="n"&gt;All&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;discarded&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="n"&gt;Passed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also ran it in a container with the real library. 8/8. Deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero Incidents in Actual Machine
&lt;/h2&gt;

&lt;p&gt;A user verified it on their smartphone and collected judgment logs.&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;echo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="n"&gt;breakdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Discarded&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conversation was successful. The avatar uttered 18 times, and the user started speaking 17 times. Yet, &lt;br&gt;
&lt;strong&gt;the gate judgment was never reached&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The setting was enabled. The pipeline was entered. The file contents matched the source hash.&lt;/p&gt;

&lt;p&gt;The cause was that &lt;strong&gt;the frame type being observed was different&lt;/strong&gt;. The frame type that VAD outputs for utterance start and the frame type that the user aggregator outputs were different, and I was looking at the latter. The aggregator is downstream of this gate. &lt;strong&gt;Things created downstream do not flow upstream&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The gate was operating as if it didn't exist.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the Tests Were Green
&lt;/h2&gt;

&lt;p&gt;It was only natural. &lt;strong&gt;The tests were using frames I created and passed through&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserStartedSpeakingFrame&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;DOWNSTREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's only natural that what I created and passed through would be processed, and &lt;strong&gt;the fact that the actual frame type is different&lt;/strong&gt; wouldn't appear in this test.&lt;/p&gt;

&lt;p&gt;I rewrote the test and added the following to the beginning:&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;_transport_emits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Whether the input transport actually outputs this frame.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipecat.transports&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base_input&lt;/span&gt;
    &lt;span class="k"&gt;return&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;frame_cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getsource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;check&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;UserStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is the type that transport.input() actually outputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;_transport_emits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserStart&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the library source code. This became the first item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stepped on the Same Issue Again on the Same Day
&lt;/h2&gt;

&lt;p&gt;I added another countermeasure (raising the VAD threshold during playback) to the revised version. The tests passed. This time, I was using a &lt;strong&gt;fake VAD object&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_FakeVAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Minimum VAD with only `_vad_start_frames` (same attribute name as the real one).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_vad_start_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment I added an item to verify that "it works with the real VAD", it failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: '_vad_start_frames'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The real VAD doesn't have that attribute initially&lt;/strong&gt;. It's created when the sampling rate is decided (at pipeline startup). My code was swallowing the exception, so &lt;strong&gt;it was "fixed" without actually being fixed&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="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: BLE001
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;          &lt;span class="c1"&gt;# ← This creates "it worked without actually working"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made sure to log when swallowing exceptions. Both the evidence of it working (info) and the fact that it didn't work (warning, only once).&lt;/p&gt;

&lt;h2&gt;
  
  
  Effect of the Countermeasure
&lt;/h2&gt;

&lt;p&gt;The revised version with the correct type finally made the gate work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo-gap 7ms within window → Discarded as self-echo
echo-gap 5037ms outside window → Passed (real utterance)
echo-gap 10454ms outside window → Passed
echo-gap 17231ms outside window → Passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Utterance that started 7 milliseconds after playback ended. Clearly its own voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizable Points
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Implemented" and "working" are different, and the difference can't be covered by tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's only natural that tests pass with the input I prepared, but if the actual input is different, the tests will all be green while the actual machine does nothing. I stepped on the same issue 4 times that day.&lt;/p&gt;

&lt;p&gt;I was able to detect it because &lt;strong&gt;I was counting how many times it fired in the actual machine&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of judgments entered (all, including passed ones)&lt;/li&gt;
&lt;li&gt;Number of discards&lt;/li&gt;
&lt;li&gt;Evidence of countermeasures being effective&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Don't just count the number of discards&lt;/strong&gt;. That would make it impossible to judge whether the window width is reasonable, as the "leaked" part outside the window would be invisible in principle. &lt;strong&gt;Also record the passed ones&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And when swallowing exceptions, always log them. &lt;code&gt;except: pass&lt;/code&gt; is a setup that makes it look like it's working when it's not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Until the Voice Dialogue Avatar Responds Correctly
&lt;/h2&gt;

&lt;p&gt;This article is part of &lt;strong&gt;Part 1: Stopping the Sound&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: There were two types of events with the same name&lt;br&gt;
→ Next: The smartphone's finger was breaking the echo canceller&lt;/p&gt;

&lt;p&gt;Series of 8 articles&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Stopping the Sound&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There were two types of events with the same name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forge.workstyle.tech/blog/the-gate-that-never-fired/" rel="noopener noreferrer"&gt;The self-echo countermeasure was never working&lt;/a&gt;&lt;/strong&gt; ← Now here&lt;/li&gt;
&lt;li&gt;The smartphone's finger was breaking the echo canceller&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Understanding Language&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"It", "this page", and "earlier" were different things&lt;/li&gt;
&lt;li&gt;The one line at the end of the huge prompt was ignored all 4 times&lt;/li&gt;
&lt;li&gt;Apology words were contaminating the next search&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Judging&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not all utterances are questions&lt;/li&gt;
&lt;li&gt;I was measuring something different from what I thought&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notes that led to this insight are summarized in &lt;a href="https://forge.workstyle.tech/blog/improving-voice-dialogue-avatars/" rel="noopener noreferrer"&gt;Improving the Response Quality of Voice Dialogue Avatars&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pipecat</category>
    </item>
    <item>
      <title>Half of Free LLM APIs Were Dead After 2 Months</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:12:49 +0000</pubDate>
      <link>https://dev.to/orca_forge/half-of-free-llm-apis-were-dead-after-2-months-o14</link>
      <guid>https://dev.to/orca_forge/half-of-free-llm-apis-were-dead-after-2-months-o14</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/free-llm-api-2-months-later-2026-09/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=free-llm-api-2-months-later-2026-09" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In July, I wrote an article titled "Summary of Free LLM API Providers That Don't Require a Credit Card." I listed eight providers and explained that by bundling multiple providers and implementing fallbacks, you could achieve real-world operation using only the free tiers.&lt;/p&gt;

&lt;p&gt;Two months later, I inspected the pipeline running on this setup and found that the &lt;strong&gt;success rate had dropped to 60%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Upon investigating the cause, I discovered that several of the eight providers I had listed were no longer functioning for various reasons, and &lt;strong&gt;none of the fallbacks were working&lt;/strong&gt;. This article documents the full investigation. All figures are based on actual API calls made on 2026-09-07.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue: A 6-Provider Setup Running on Essentially One Provider
&lt;/h2&gt;

&lt;p&gt;I sent the same request to the router (LiteLLM) ten times, and here's what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success: 6 / Failure: 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the details of the failures:&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;litellm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CerebrasException&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;access&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;
  &lt;span class="n"&gt;Available&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="n"&gt;Group&lt;/span&gt; &lt;span class="n"&gt;Fallbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="n"&gt;litellm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GeminiException&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;flash&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;longer&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line &lt;code&gt;Available Model Group Fallbacks=None&lt;/code&gt; says it all. &lt;strong&gt;Despite bundling six providers, no fallbacks were occurring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I then individually tested each provider's API to check their status.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;✗ 401 Invalid API Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cerebras&lt;/td&gt;
&lt;td&gt;✗ 402 Payment required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;✗ 401 API key expired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;✗ 404 model no longer available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;✗ 429 Rate limit exceeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;○ 200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only one provider was still functioning&lt;/strong&gt;. The reason for the 6 out of 10 successes was that LiteLLM's &lt;code&gt;num_retries: 5&lt;/code&gt; setting kept retrying until it landed on Cohere. While it appeared to have redundancy, in reality, all the load was on a single provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decay Wasn't Uniform
&lt;/h2&gt;

&lt;p&gt;This is where things get interesting. I thought it would be a simple case of "free tiers ending," but &lt;strong&gt;each provider had a different reason&lt;/strong&gt; for failing. Here’s the classification after testing 13 candidates:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Free Tier Itself Became Paid — Cerebras
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;402 Payment required to access this resource. Visit your billing tab.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is valid, authentication passes, but payment is required. In July, there was a "1M tokens/day" free tier. &lt;strong&gt;This is the only case where the information in my article was clearly incorrect.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Service Terminated — GitHub Models
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://models.github.ai/catalog/models
→ HTTP 410 Gone
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 410 status code means "permanently gone." While I had considered it a candidate, it’s no longer an option.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Models Completely Replaced — Groq
&lt;/h3&gt;

&lt;p&gt;After reissuing the key, the error changed from 401 to 404.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;404 The model `llama-3.3-70b-versatile` does not exist or you do not have access to it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The free tier is still available, but the models are gone.&lt;/strong&gt; The current catalog includes 9 models, none of which are Llama-based.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allam-2-7b / canopylabs/orpheus-* / groq/compound / groq/compound-mini
openai/gpt-oss-120b / openai/gpt-oss-20b / qwen/qwen3.6-27b / qwen/qwen3.8-27b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Only Model Names Expired — Gemini
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;404 NOT_FOUND: This model models/gemini-2.0-flash is no longer available.
    Please update your code to use models/gemini-3.6-flash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free tier is still available here too. &lt;strong&gt;It was simply a matter of not keeping up with the model name changes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a fix, I switched to using the &lt;code&gt;gemini-flash-latest&lt;/code&gt; alias. This works in practice and will automatically adapt to future name changes. It’s a measure to avoid repeating the same mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keys Had Expiration Dates — OpenRouter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;401 API key expired.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking the dashboard, it shows &lt;code&gt;Expires: Expired / Last Used: Never / Key limit: unlimited&lt;/code&gt;. &lt;strong&gt;The key expired due to its expiration date, not usage.&lt;/strong&gt; When creating keys, you can choose "No expiration," which should always be selected for persistent use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. "No Card Required" but Balance Needed — Z.ai
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 Insufficient balance or no resource package. Please recharge.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While no card is required to issue a key, &lt;strong&gt;you can't make a single request without recharging&lt;/strong&gt;. It’s often introduced as a "no-card-required free tier," but the reality is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Catalog Doesn't Reflect Reality — NVIDIA NIM
&lt;/h3&gt;

&lt;p&gt;The model list API returns 81 entries. &lt;strong&gt;I tested all of them.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success: 13
HTTP 404: 55   ← Models listed in the catalog but don't exist
Timeout/Exceptions: 7
503 / 500 / 400: 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the 13 functioning models, 3 were for content safety classification, 2 were dedicated to translation, and 1 required image input. &lt;strong&gt;Only 5 were suitable for general chat use.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you take the explanation "81 models available for free" at face value, you’ll only realize the issue after implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Almost Defunct — SambaNova
&lt;/h3&gt;

&lt;p&gt;Here are the results after testing all 7 models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DeepSeek-V3.1 / V3.2 / Meta-Llama-3.3-70B / gpt-oss-120b → 402 A payment method is required
MiniMax-M2.7 / M3                                        → 429 high demand
gemma-4-31B-it                                           → ○ 200 (but took 12,021ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Only one model remains free, and it takes 12 seconds.&lt;/strong&gt; Compared to Groq's 220ms, it’s 55 times slower. While you could say "free tier available," its practicality is questionable.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Card Required — Nebius / Scaleway
&lt;/h3&gt;

&lt;p&gt;Nebius's registration page requires billing details, including name, address, and &lt;strong&gt;card information (with $0 authorization for card verification)&lt;/strong&gt;. Scaleway's official documentation states that "a card and KYC are required to receive normal rate limits."&lt;/p&gt;

&lt;p&gt;Neither meets the "no credit card required" condition.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Token Permission Pitfalls — HuggingFace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;403 This authentication method does not have sufficient permissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking the token, it only had &lt;code&gt;repo.content.read&lt;/code&gt; permissions. For inference, &lt;strong&gt;&lt;code&gt;inference.serverless.write&lt;/code&gt;&lt;/strong&gt; (displayed as "Make calls to Inference Providers" in the UI) is required.&lt;/p&gt;

&lt;p&gt;After re-enabling the permission, it worked. &lt;strong&gt;Issuing a key and the key being usable are two different things&lt;/strong&gt;, but I didn’t realize this until seeing the error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Reason Fallbacks Didn't Work
&lt;/h2&gt;

&lt;p&gt;This was the biggest lesson learned.&lt;/p&gt;

&lt;p&gt;Here’s how LiteLLM was configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;router_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;routing_strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple-shuffle&lt;/span&gt;
  &lt;span class="na"&gt;num_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;allowed_fails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;cooldown_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it seems robust with 5 retries. However, in practice, it failed immediately upon hitting Cerebras.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;402 (payment required) and 404 (model not found) are not subject to retries.&lt;/strong&gt; Retries and fallbacks are designed for issues like 429 (rate limits) or timeouts, which can be resolved by waiting. Since 402 and 404 are permanent failures, they are returned immediately.&lt;/p&gt;

&lt;p&gt;In other words,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Leaving dead providers in the pool will continue to generate failures at the expected rate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If one out of six providers is permanently dead, simple math tells us that 1/6 of requests will fail. The assumption that "having many providers ensures safety" &lt;strong&gt;only holds if all providers are functioning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In fact, just removing Cerebras from the pool improved the success rate as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Just fixing Groq and Gemini model names: 6/12 (50%) ← All failures were Cerebras' 402
Excluding Cerebras: 15/15 (100%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Removing the dead provider was more effective than fixing model names.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Types of Inference Model Traps
&lt;/h2&gt;

&lt;p&gt;In my July article, I mentioned that inference models should be avoided for agent use cases. This time, I discovered an even more troublesome issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  (a) Multi-Turn Conversations Break (Known)
&lt;/h3&gt;

&lt;p&gt;Inference model responses include &lt;code&gt;reasoning_content&lt;/code&gt;. Agents accumulate conversation history and send it to the next turn, inadvertently sending this unfamiliar field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'messages.*.assistant.reasoning_content'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is a problem specific to agents (multi-turn conversations)&lt;/strong&gt;. Single-turn generation doesn’t send history, so this issue doesn’t occur. This was already mentioned in July.&lt;/p&gt;

&lt;h3&gt;
  
  
  (b) Even Single-Turn Generation Returns Empty Content (New Discovery)
&lt;/h3&gt;

&lt;p&gt;The real problem is this. Here are the results of asking several inference models, "What is 2+2? Answer with just the number."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen/qwen3.8-27b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"4"&lt;/code&gt; ← Non-inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen/qwen3.6-27b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;""&lt;/code&gt; ← &lt;strong&gt;Empty&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-oss-20b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;""&lt;/code&gt; ← &lt;strong&gt;Empty&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;liquid/lfm-2.5-2.6b:free&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;""&lt;/code&gt; ← &lt;strong&gt;Empty&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvidia/nemotron-3-super-120b-a12b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"User asks 2+2..."&lt;/code&gt; ← &lt;strong&gt;Exposes reasoning&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The answer ends up in &lt;code&gt;reasoning_content&lt;/code&gt;, leaving &lt;code&gt;content&lt;/code&gt; empty.&lt;/strong&gt; Code that only reads &lt;code&gt;choices[0].message.content&lt;/code&gt; receives an empty string without error.&lt;/p&gt;

&lt;p&gt;This happens even in single-turn generation. So, it’s not enough to "just be careful with agents when using inference models." &lt;strong&gt;Without defenses like empty response retries, it will silently break.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Note that even within the same Qwen series, &lt;strong&gt;3.6 outputs reasoning while 3.8 does not&lt;/strong&gt;. You can’t judge based on model name alone, so &lt;strong&gt;it’s best to test each model once before adding it to the pool to check for the presence of &lt;code&gt;reasoning_content&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison with July Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;July Description&lt;/th&gt;
&lt;th&gt;September Actual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;td&gt;○ Available ( &lt;strong&gt;Model name update required&lt;/strong&gt; )&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;td&gt;○ Available ( &lt;strong&gt;All Llama models gone → Replaced with Qwen, etc.&lt;/strong&gt; )&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cerebras&lt;/td&gt;
&lt;td&gt;1M tokens/day&lt;/td&gt;
&lt;td&gt;✗ &lt;strong&gt;402 Payment required&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;:free 20RPM&lt;/td&gt;
&lt;td&gt;○ Available ( &lt;strong&gt;Key expiration, all models are inference type&lt;/strong&gt; )&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA NIM&lt;/td&gt;
&lt;td&gt;Numerous models&lt;/td&gt;
&lt;td&gt;△ &lt;strong&gt;Only 13 out of 81 responded&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;Trial tier&lt;/td&gt;
&lt;td&gt;○ Available (The only one functioning consistently this time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SambaNova&lt;/td&gt;
&lt;td&gt;Permanent free tier&lt;/td&gt;
&lt;td&gt;✗ &lt;strong&gt;6 out of 7 models returned 402&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;Experiment tier&lt;/td&gt;
&lt;td&gt;△ Frequent 429 errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;(Not mentioned)&lt;/td&gt;
&lt;td&gt;○ &lt;strong&gt;Cloudflare Workers AI&lt;/strong&gt; emerged as a strong contender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;(Not mentioned)&lt;/td&gt;
&lt;td&gt;○ &lt;strong&gt;HuggingFace&lt;/strong&gt; works if permission settings are correct&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only 3 out of the original 8 providers remained valid.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  New Addition: Cloudflare Workers AI
&lt;/h2&gt;

&lt;p&gt;The most straightforward provider to get working was Cloudflare Workers AI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10,000 Neurons/day, no card required&lt;/li&gt;
&lt;li&gt;API token requires only &lt;strong&gt;Account &amp;gt; Workers AI &amp;gt; Read&lt;/strong&gt; permission for inference ( &lt;code&gt;Edit&lt;/code&gt; not needed)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@cf/meta/llama-3.3-70b-instruct-fp8-fast&lt;/code&gt; responds in 570ms, non-inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caution: the permission names are confusing. &lt;strong&gt;&lt;code&gt;AI Gateway&lt;/code&gt; permissions do not work.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A token with only AI Gateway permission returns 401 with error code 10000.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap left by Groq removing Llama models was filled by this provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Recovery Setup
&lt;/h2&gt;

&lt;p&gt;After removing dead providers, updating model names, and adding three new providers, here’s the result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Measured Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen/qwen3.8-27b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;220ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@cf/meta/llama-3.3-70b-instruct-fp8-fast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;570ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;&lt;code&gt;command-a-03-2025&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;748ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HuggingFace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta-llama/Llama-3.3-70B-Instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;937ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-26b-a4b-it:free&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;957ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA NIM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/diffusiongemma-26b-a4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,070ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-flash-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mistral-small-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Frequent 429 errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success rate trend: 6/10 (60%) → 15/15 → 20/20 → 24/24 → 28/28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: Summary Articles Go Stale in Two Months
&lt;/h2&gt;

&lt;p&gt;Since half of my own article became unusable in two months, this is also a reminder to myself. Here are three practical conclusions for readers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Implement Health Checks First.&lt;/strong&gt;&lt;br&gt;
It’s more effective to &lt;strong&gt;detect and remove dead providers&lt;/strong&gt; than to add more providers. In this case, removing one dead provider contributed more to success rate improvement than fixing model names. A simple script sending one request to each provider is sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fallbacks Are Not a Panacea.&lt;/strong&gt;&lt;br&gt;
They work for 429 errors and timeouts. &lt;strong&gt;They don’t work for 402 (payment required), 404 (model gone), or 401 (key expired).&lt;/strong&gt; These are permanent failures, so they’ll keep generating errors until manually removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Don’t Fix Model Names; Verify Before Adding.&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;latest&lt;/code&gt; aliases if available. If not, test each model once before adding it to the pool to ensure &lt;strong&gt;a response is returned&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;reasoning_content&lt;/code&gt; is not included&lt;/strong&gt;. Behavior can vary even within the same series.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: All figures in this article are based on actual measurements as of 2026-09-07. Free tier conditions, available models, and rate limits can change within months, as seen here. Always check each provider’s official documentation for the latest information before implementation. This article, too, will likely be half incorrect in two months.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>api</category>
      <category>litellm</category>
      <category>ai</category>
    </item>
    <item>
      <title>18 Insights from Mass-Producing Voice Models — From Diffusion TTS Voice Design to Training Corpus Creation and Quality Gate Pitfalls</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:16:22 +0000</pubDate>
      <link>https://dev.to/orca_forge/18-insights-from-mass-producing-voice-models-from-diffusion-tts-voice-design-to-training-corpus-164e</link>
      <guid>https://dev.to/orca_forge/18-insights-from-mass-producing-voice-models-from-diffusion-tts-voice-design-to-training-corpus-164e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/voice-model-production-pipeline-series/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=voice-model-production-pipeline-series" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a record of designing voices from single-line captions, automatically creating a learning corpus, and passing all 12 role-specific voices (narrator/counselor/sales/presenter/operator/MC for both men and women) through full inspection. I wrote about the failures I encountered during approximately one month of actual work, divided into 18 articles. This article is the table of contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Conclusion Upfront
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Voice design, voice manufacturing, and voice operation are different technologies with different failures.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design&lt;/strong&gt; uses diffusion TTS. The voice is determined by the caption and random seed, making it fully reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manufacturing&lt;/strong&gt; is primarily about corpus generation. &lt;strong&gt;The design of the quality gate directly determines the voice quality.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation&lt;/strong&gt; relies on lightweight pre-trained models. Diffusion TTS is too slow for conversation (2.5 times slower on the same GPU).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson boils down to one point: &lt;strong&gt;Having a quality gate and it being effective are two different things.&lt;/strong&gt; Six of these 18 articles are about gates that existed but weren’t effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Order
&lt;/h2&gt;

&lt;p&gt;The articles are arranged in the order of design → manufacturing → inspection → operation. Reading from the top will take you through the journey of a single voice being created and deployed into production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 1: Design — How to Determine the Voice
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/diffusion-tts-too-slow-for-conversation/" rel="noopener noreferrer"&gt;The TTS Chosen for Sound Quality Was Too Slow for Conversation&lt;/a&gt;
A 2.5x real-time factor (RTF) difference. How we settled on a two-stage approach: designing voices with diffusion TTS and using pre-trained models for speech.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/deterministic-voice-gacha-and-design-ledger/" rel="noopener noreferrer"&gt;Drawing Voices Like a Gacha&lt;/a&gt;
Voices are determined by captions and random seeds. By keeping a ledger of design values, voices can be recreated even if the model is lost.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/screening-voices-by-metrics-not-ears/" rel="noopener noreferrer"&gt;Letting a Machine Choose "Narrator-like Voices" from 24 Candidates&lt;/a&gt;
Listening to all candidates is unsustainable. Automatically measure speech rate, intonation, and stability to only listen to the top candidates. Also, the story of how all male candidates were eliminated due to metric limitations.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Chapter 2: Manufacturing — Corpus Quality Directly Becomes the Voice
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/quality-gate-selection-bias-flat-takes/" rel="noopener noreferrer"&gt;The Stricter the Quality Gate, the More Monotonous the Takes Survive&lt;/a&gt;
The reason all emotion-infused corpora ended up monotonous was the quality gate itself. &lt;strong&gt;This is the central story of this series.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/speaking-style-is-baked-into-the-corpus/" rel="noopener noreferrer"&gt;Speech Rate Cannot Be Changed After Training&lt;/a&gt;
Endings and speech rate are baked into the corpus. They cannot be adjusted with synthesis parameters.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/tts-changes-recording-room-every-time/" rel="noopener noreferrer"&gt;TTS That Changes "Recording Location" Every Time&lt;/a&gt;
Even with the same model and speaker, frequency characteristics differ for each clip. If not standardized, style switching results in inconsistent sound quality.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/one-rough-clip-ruins-the-whole-style/" rel="noopener noreferrer"&gt;One Rough Clip Ruins the Entire Style&lt;/a&gt;
If one out of five clips is rough, the entire style becomes hoarse. Dilution into the average doesn’t help.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Chapter 3: Inspection — Having a Gate and It Being Effective Are Different
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/where-did-the-elongated-ending-come-from/" rel="noopener noreferrer"&gt;Where Did the AI’s Habit of Stretching "Hello" Come From?&lt;/a&gt;
Verification discarded long vowels due to kana normalization, making the defect &lt;strong&gt;fundamentally undetectable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/the-character-that-broke-the-tts-input/" rel="noopener noreferrer"&gt;"A Little" Becomes "Shomo"&lt;/a&gt;
The model and parameters were fine, but &lt;strong&gt;the input text was broken&lt;/strong&gt;. The symbol removal list was dropping characters like "々", "〆", and "髙".&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/hallucination-guard-that-never-fired/" rel="noopener noreferrer"&gt;The Hallucination Guard Code Only Failed During Hallucinations&lt;/a&gt;
A single-line bug made the guard ineffective only in necessary situations.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/three-chars-became-a-verbal-tic/" rel="noopener noreferrer"&gt;The "Three Characters" Allowed by the Quality Gate Became a Verbal Tic&lt;/a&gt;
The size passing through the gate matched the size of the sound reproduced by the model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/measuring-factory-defects-as-product-traits/" rel="noopener noreferrer"&gt;Rejecting Candidates for Fixable Defects&lt;/a&gt;
Is the metric measuring the product’s characteristics or the process’s state?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/defects-invisible-to-transcription/" rel="noopener noreferrer"&gt;Defects Unseen in Transcription&lt;/a&gt;
STT-only inspection misses 0.1-second additive sounds with silence in between. Capture them with waveform envelopes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Chapter 4: Operation — Running as a Factory
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/70-minutes-lost-to-a-network-blink/" rel="noopener noreferrer"&gt;70 Minutes of Training Material Lost to a Network Blink&lt;/a&gt;
One disconnection led to 205 retries. Finding areas without retries and designing idempotent resumable processes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/ja-vs-jp-babbling-model/" rel="noopener noreferrer"&gt;From "ja" to "JP": Creating a Babbling Model&lt;/a&gt;
Language code case sensitivity led to a completed babbling model. A catalog of pitfalls encountered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/four-registration-paths-one-exit/" rel="noopener noreferrer"&gt;Four Registration Paths, Zero Management Screens&lt;/a&gt;
Asset catalogs became unmanageable. Only after creating an inventory API could we see the full picture.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/who-is-rolling-back-whom/" rel="noopener noreferrer"&gt;Each Deployment Overwrote the Other’s Work&lt;/a&gt;
Two parallel workflows alternately deleted each other’s changes from production. Branch checks didn’t prevent it; only matching sha256 with running Pods stopped it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/chasing-unmeasured-targets-with-thresholds/" rel="noopener noreferrer"&gt;Chasing Unmeasured Targets with Thresholds Always Leads to Failure&lt;/a&gt;
Interpreting zero metrics as "no issues" and tightening thresholds. &lt;strong&gt;Not realizing what wasn’t being observed&lt;/strong&gt; until the end.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three Articles for Those Short on Time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interested in quality gate design?&lt;/strong&gt; → Article 4 (selection bias) → Article 11 (three characters) → Article 13 (defects unseen by STT). These three cover all patterns of "gates existing but not being effective."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning to train a speech model?&lt;/strong&gt; → Article 15 (pitfall catalog) → Article 9 (broken input) → Article 5 (style baked into the corpus).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading as an MLOps/manufacturing story?&lt;/strong&gt; → Article 12 (process defects vs. product traits) → Article 14 (idempotent resume) → Article 18 (not chasing unmeasured targets).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Thread Across These 18 Articles
&lt;/h2&gt;

&lt;p&gt;Most failures occurred not in models or GPUs, but in &lt;strong&gt;inspection design&lt;/strong&gt;. It wasn’t that gates were too loose; it was that gates &lt;strong&gt;weren’t measuring what mattered&lt;/strong&gt;, and those unmeasured aspects got baked into the product. Unlike text, with audio, defects that slip through aren’t noticed until heard.&lt;/p&gt;

&lt;p&gt;What ultimately worked wasn’t finding smarter models, but &lt;strong&gt;systematically addressing what wasn’t being measured&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The work notes that formed the basis of these insights are compiled in &lt;a href="https://forge.workstyle.tech/blog/manufacturing-pipeline-for-practical-voices/" rel="noopener noreferrer"&gt;Manufacturing Pipeline for Practical Voices from Diffusion TTS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tts</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Chasing Unmeasured Metrics with Thresholds Always Leads to Failure — A Tale of 10 Mistakes in One Day</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Sat, 05 Sep 2026 01:12:35 +0000</pubDate>
      <link>https://dev.to/orca_forge/chasing-unmeasured-metrics-with-thresholds-always-leads-to-failure-a-tale-of-10-mistakes-in-one-2m3j</link>
      <guid>https://dev.to/orca_forge/chasing-unmeasured-metrics-with-thresholds-always-leads-to-failure-a-tale-of-10-mistakes-in-one-2m3j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/chasing-unmeasured-targets-with-thresholds/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=chasing-unmeasured-targets-with-thresholds" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a single day, I encountered the same type of failure ten times. I made four of these mistakes myself, while my colleague, working in parallel, made seven (with one overlapping case). The issues spanned various domains, including speech synthesis, search, audit logs, text preprocessing, and frame processing, with no overlap. Yet, the underlying structure of the failures was identical.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The metric returned "0 items" or "no match," which I interpreted as "the target does not exist."&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;I continued to adjust thresholds and narrow down lists in response.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The target was outside the observable system from the beginning. No matter how I adjusted the thresholds, it was impossible to reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Six Cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;① Additional sounds at the end of speech model outputs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I assumed this was due to hallucinations in the training corpus and tightened the script alignment insertion tolerance from &lt;code&gt;3→1→0&lt;/code&gt;. After three re-trainings, the additional sounds went from &lt;code&gt;3/6 → 2/6 → 3/6&lt;/code&gt;, which was within the margin of error.&lt;/p&gt;

&lt;p&gt;Scanning 198 audio clips, I found that the four clips with additional sounds all had &lt;strong&gt;insertion 0&lt;/strong&gt;. Since the speech-to-text (STT) system didn’t recognize these sounds as text, they weren’t included in the threshold denominator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② Dialogue act detection never triggered in real conversations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used a vocabulary list to determine if a statement was a question. Since it never triggered, I added more words, narrowed the list, and adjusted conditions. In reality, it &lt;strong&gt;failed to detect compound utterances&lt;/strong&gt; (e.g., "What is this, and also the previous one") and only passed on simple test sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ "Evidence present" metric always returned True&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I measured whether answers included evidence as a binary value. Since the results were too good, I tightened the threshold. In reality, when automatic page navigation was enabled, something was always attached as evidence, so &lt;strong&gt;the metric itself carried no meaningful information&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;④ Audit log showed 0 navigate events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user reported "the page transitioned automatically," I checked the audit table and found 0 entries, concluding it hadn’t occurred. &lt;strong&gt;The implementation didn’t log audit events at issuance&lt;/strong&gt;, so it couldn’t have been recorded. I &lt;strong&gt;mistook the absence of records as evidence that the event didn’t happen&lt;/strong&gt;. I misdiagnosed the first report and only questioned the logging system after the second report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑤ Unable to answer based on page content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I added &lt;strong&gt;four guards&lt;/strong&gt;: search thresholds, exclusion conditions, and handover judgments. When a user suggested, "Shouldn’t you grasp the entire page text first?" I realized I was &lt;strong&gt;applying vector search to a 231-character page&lt;/strong&gt;. The text was too short for meaningful segmentation, and preprocessing was dropping characters, causing searches to fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑥ Pronunciation breakdown&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The phrase "少々お待ちください" (Please wait a moment) was heard as "しょもおまちください." I checked the model, synthesis parameters, cache, and routing in sequence. Finally, I printed the preprocessing output and found 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="nf"&gt;_clean_tts_text&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="err"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allowed character whitelist included kanji in the range &lt;code&gt;一-鿿&lt;/code&gt;, but &lt;strong&gt;「々」 (CJK Symbols block) was excluded&lt;/strong&gt;. Synthesizing "少お待ちください" resulted in "ショーをお待ちください" (Show o omachi kudasai).&lt;/p&gt;

&lt;h2&gt;
  
  
  Four More Cases (Later That Day)
&lt;/h2&gt;

&lt;p&gt;While writing this article, four more cases arrived from my colleague, all following the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Misidentified frame type, causing the gate to never trigger&lt;/strong&gt; (discovered via 0 real-world logs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fake VAD passed, but the real one lacked attributes and did nothing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instructions placed at the end of a large prompt were ignored four times&lt;/strong&gt; (the same issue was documented in the same file)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;elapsed_ms&lt;/code&gt; was misinterpreted, leading to unnecessary fixes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The colleague summarized it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The common thread is &lt;strong&gt;"tests pass, but real-world logs reveal the issue."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first case was particularly harsh. &lt;strong&gt;I built the gate, wrote tests, passed 8/8, deployed, and it never triggered.&lt;/strong&gt; Tests are written based on my assumptions about the frame, so if the actual frame type differs, both the test and the implementation are based on the same misunderstanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Pattern
&lt;/h2&gt;

&lt;p&gt;There are three layers to these failures:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Interpreting "0 items" as evidence
&lt;/h3&gt;

&lt;p&gt;In ①, "0 STT detections meant no additional sounds," and in ④, "0 audit logs meant no transitions." Both assumed &lt;strong&gt;a functioning observation system&lt;/strong&gt;, even though a broken system would also return 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"0 items" is not evidence of absence.&lt;/strong&gt; It could mean either "the observation system is working, and the target is absent" or "the observation system is broken." The value 0 doesn’t distinguish between these two scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Continuously adding guards to symptoms
&lt;/h3&gt;

&lt;p&gt;In ②, I added vocabulary; in ③, I tightened thresholds; in ⑤, I added four guards. &lt;strong&gt;All adjustments were made near the symptoms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This seems logical—symptoms are likely close to the cause. But when dealing with &lt;strong&gt;unobservable targets&lt;/strong&gt;, tightening around symptoms is ineffective. In ①, adjusting thresholds three times had no meaningful impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Not questioning my own observation systems
&lt;/h3&gt;

&lt;p&gt;This is the deepest layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In ①, I wrote the quality gate, so I assumed "if it passed, the material was normal."&lt;/li&gt;
&lt;li&gt;In ④, the audit logs were implemented by the other party, so I assumed "if they implemented it, it should be visible."&lt;/li&gt;
&lt;li&gt;In ③ and ⑤, the creators didn’t question their own metrics or searches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;We’re less likely to doubt systems we’ve built ourselves, especially if we remember them working.&lt;/strong&gt; And without doubt, we don’t look beyond them.&lt;/p&gt;

&lt;p&gt;The colleague who encountered ②–④ also failed to notice that &lt;strong&gt;the pipeline failure visualization panel they built was 404ing the next day&lt;/strong&gt;. They built it, saw it working, and moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Only Effective Solution
&lt;/h2&gt;

&lt;p&gt;The cases I resolved myself shared a common approach: &lt;strong&gt;I changed the axis of observation and measured the target again.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Original Axis&lt;/th&gt;
&lt;th&gt;New Axis&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① Additional sounds&lt;/td&gt;
&lt;td&gt;STT transcription&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Waveform envelope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discovered 4 additional sounds in insertion 0 clips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑤ Page content&lt;/td&gt;
&lt;td&gt;Search score&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Character count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Found 231 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑥ Pronunciation&lt;/td&gt;
&lt;td&gt;Listening to synthesized audio&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Printing preprocessing output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Found "々" was removed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each took less than 5 minutes. Scanning waveforms in ① took 30 minutes, still less than a tenth of the time spent adjusting thresholds and retraining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-validation of metrics is inherently difficult.&lt;/strong&gt; You can’t verify a broken metric using the same metric. Instead of asking, "Is this metric correct?" it’s faster to &lt;strong&gt;apply a different measure once.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  User Reports as Independent Observations
&lt;/h2&gt;

&lt;p&gt;Another effective approach across all cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When metrics and user reports conflict, doubt the metrics first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In ①, even after a user reported hearing "こちらですって" (It’s over here), I prioritized my measurement: "STT detected 0/6, so it wasn’t passed." &lt;strong&gt;Another observation system (human ears) had already provided results.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Similarly, in ④, I dismissed the first user report based on 0 audit logs.&lt;/p&gt;

&lt;p&gt;User reports are often treated as weaker evidence than machine metrics because they’re subjective and ambiguous. But as &lt;strong&gt;independent observation systems&lt;/strong&gt;, their value is high when conflicts arise. If two independent measurements of the same target disagree, and one says "anomaly," doubt your own instruments first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaways
&lt;/h2&gt;

&lt;p&gt;Here’s the distilled procedure—nothing special:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before adjusting thresholds, ask once if the target is observable with that threshold.&lt;/strong&gt; For ①, ask, "Can additional sounds exist in insertion 0 clips?" This question leads to checking waveforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When seeing "0 items," verify the logging system separately.&lt;/strong&gt; For ④, intentionally trigger an audit event to confirm it’s recorded. Takes 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before adding a second guard to symptoms, look upstream once.&lt;/strong&gt; For ⑤, count page characters. If the first guard didn’t fix it, &lt;strong&gt;it likely wasn’t capturing the target.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After building a detector, test it with non-target cases first.&lt;/strong&gt; The waveform detector in ① initially flagged all 12 models as "anomalous," mistaking pauses for additional sounds. &lt;strong&gt;If detection rates are too high, doubt the detector, not the target.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For systems you’ve built, verify them with a different axis at least once.&lt;/strong&gt; This is the most effective step. Doing it immediately after building would have saved the three retraining attempts in ①.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming the Issues
&lt;/h2&gt;

&lt;p&gt;Finally, the most frustrating realization:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Almost all were known, standard problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Additional sounds in speech are known as &lt;strong&gt;trailing artifacts / babbling&lt;/strong&gt;. The phenomenon of saying "I’ll move" but not moving is called &lt;strong&gt;procedural hallucination&lt;/strong&gt;, with dedicated benchmarks. The need for a 500–800ms microphone gate after speech ends (not 300ms) to prevent self-echo is well-documented.&lt;/p&gt;

&lt;p&gt;We treated all issues as environment-specific and dug internally. &lt;strong&gt;If we’d first checked if the symptoms had names&lt;/strong&gt;, we could have quickly found known properties like "STT can’t detect them."&lt;/p&gt;

&lt;p&gt;I wrote "10 failures in one day," but accurately, &lt;strong&gt;all 10 had precedents outside our environment.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Mass-Producing Practical Voices from Diffusion TTS
&lt;/h2&gt;

&lt;p&gt;This series documents designing voices from single-line captions, manufacturing training corpora, and mass-producing role-specific practical voices. This article is &lt;strong&gt;Part 5: Summary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: &lt;a href="https://forge.workstyle.tech/blog/who-is-rolling-back-whom/" rel="noopener noreferrer"&gt;Deploying Overwrote Each Other’s Work&lt;/a&gt;&lt;br&gt;&lt;br&gt;
(End of series)&lt;/p&gt;

&lt;p&gt;All 18 Parts&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/diffusion-tts-too-slow-for-conversation/" rel="noopener noreferrer"&gt;High-Quality TTS Was Too Slow for Conversation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/deterministic-voice-gacha-and-design-ledger/" rel="noopener noreferrer"&gt;Voice Gacha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/screening-voices-by-metrics-not-ears/" rel="noopener noreferrer"&gt;Machine Screening 24 Narrator-Like Voices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/quality-gate-selection-bias-flat-takes/" rel="noopener noreferrer"&gt;Stricter Quality Gates Kept Flat Takes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/speaking-style-is-baked-into-the-corpus/" rel="noopener noreferrer"&gt;Speaking Style Is Baked into the Corpus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/tts-changes-recording-room-every-time/" rel="noopener noreferrer"&gt;TTS Changes Recording Room Every Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/one-rough-clip-ruins-the-whole-style/" rel="noopener noreferrer"&gt;One Rough Clip Ruins the Whole Style&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/where-did-the-elongated-ending-come-from/" rel="noopener noreferrer"&gt;Where Did the Elongated Ending Come From?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-character-that-broke-the-tts-input/" rel="noopener noreferrer"&gt;Allowed Characters List Broke Japanese TTS Input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/hallucination-guard-that-never-fired/" rel="noopener noreferrer"&gt;Hallucination Guard Never Fired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/three-chars-became-a-verbal-tic/" rel="noopener noreferrer"&gt;Three Characters Became a Verbal Tic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/measuring-factory-defects-as-product-traits/" rel="noopener noreferrer"&gt;Measuring Factory Defects as Product Traits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/defects-invisible-to-transcription/" rel="noopener noreferrer"&gt;Defects Invisible to Transcription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/70-minutes-lost-to-a-network-blink/" rel="noopener noreferrer"&gt;70 Minutes Lost to a Network Blink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/ja-vs-jp-babbling-model/" rel="noopener noreferrer"&gt;JA vs JP Babbling Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/four-registration-paths-one-exit/" rel="noopener noreferrer"&gt;Four Registration Paths, One Exit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/who-is-rolling-back-whom/" rel="noopener noreferrer"&gt;Deploying Overwrote Each Other’s Work&lt;/a&gt;
&lt;strong&gt;18. &lt;a href="https://forge.workstyle.tech/blog/chasing-unmeasured-targets-with-thresholds/" rel="noopener noreferrer"&gt;Chasing Unmeasured Targets with Thresholds&lt;/a&gt;&lt;/strong&gt; ← You are here&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The insights are compiled in the notes on &lt;strong&gt;Mass-Producing Practical Voices from Diffusion TTS Manufacturing Pipeline&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Overwriting Each Other’s Work with Every Deployment — 3 Rollback Incidents in 2 Sessions</title>
      <dc:creator>orca_forge</dc:creator>
      <pubDate>Sat, 05 Sep 2026 00:41:30 +0000</pubDate>
      <link>https://dev.to/orca_forge/overwriting-each-others-work-with-every-deployment-3-rollback-incidents-in-2-sessions-59hm</link>
      <guid>https://dev.to/orca_forge/overwriting-each-others-work-with-every-deployment-3-rollback-incidents-in-2-sessions-59hm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/who-is-rolling-back-whom/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=who-is-rolling-back-whom" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two teams were working on the same set of microservices in parallel: one on the audio pipeline and the other on conversation quality. Their responsibilities were separate, and the files they worked on barely overlapped.&lt;/p&gt;

&lt;p&gt;Yet, &lt;strong&gt;three times, they overwrote each other's work in production&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1st Incident: 5 Frontend Commits Disappeared
&lt;/h2&gt;

&lt;p&gt;To apply a frontend fix, I created a branch from &lt;code&gt;main&lt;/code&gt;, built, and deployed it.&lt;/p&gt;

&lt;p&gt;Then, a user reported:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The record is gone, and a screen I thought was removed has reappeared.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A screen that was supposed to be removed was back, and a feature I added was missing. Upon investigation, &lt;strong&gt;the running image wasn't built from &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;.&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"virtualpv:1.0.407"&lt;/span&gt; ~/kaniko-builds/&lt;span class="k"&gt;*&lt;/span&gt;.yaml | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--branch&lt;/span&gt;
  &lt;span class="nt"&gt;--branch&lt;/span&gt; feat/character-list-preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That branch was 5 commits ahead of &lt;code&gt;main&lt;/code&gt;. By building from &lt;code&gt;main&lt;/code&gt;, those 5 changes were removed from production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this repository, &lt;code&gt;main&lt;/code&gt; wasn't the source of truth for production.&lt;/strong&gt; Unmerged branches piled up: 54, 12, 6, and so on. The common assumption that "main is the latest" didn't hold here.&lt;/p&gt;

&lt;p&gt;I restored the previous tag using &lt;code&gt;kubectl set image&lt;/code&gt;, cherry-picked the changes to the correct base, and rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  2nd Incident: 17 Backend Versions Disappeared
&lt;/h2&gt;

&lt;p&gt;After the first incident, we established this procedure:&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;# Before building, check if you've missed any commits from the production base branch&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; HEAD..&amp;lt;production-base&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite &lt;strong&gt;following this procedure&lt;/strong&gt;, I caused another incident during the next build.&lt;/p&gt;

&lt;p&gt;Before building the backend, I checked the running Pod to ensure my changes were included.&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="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;judge_transcript&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_inserted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;# My value. The running version included my changes.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feeling confident, I built from &lt;code&gt;main&lt;/code&gt;. As a result, &lt;strong&gt;the other team's 17 versions disappeared&lt;/strong&gt;, halting conversation logging and losing 60 minutes of data.&lt;/p&gt;

&lt;p&gt;I checked if &lt;strong&gt;the running image included my changes&lt;/strong&gt;, but I should have checked if &lt;strong&gt;my build included the running image's changes&lt;/strong&gt;. These are two different things, yet I treated them as the same.&lt;/p&gt;

&lt;p&gt;A simple diff would have revealed the issue:&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="nv"&gt;$ &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &amp;lt;my-HEAD&amp;gt;..&amp;lt;production-base&amp;gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this once before building would have prevented the incident. Although we had a procedure, &lt;strong&gt;I didn't follow it when switching repositories&lt;/strong&gt;. For the frontend, I even checked the manifest's &lt;code&gt;--branch&lt;/code&gt;, but for the backend, I only verified a single value inside the Pod.&lt;/p&gt;

&lt;h2&gt;
  
  
  3rd Incident: The Team Following the Procedure Was Affected
&lt;/h2&gt;

&lt;p&gt;Later, the opposite happened.&lt;/p&gt;

&lt;p&gt;I fixed the backend and deployed it. Then, &lt;strong&gt;I switched to monitoring the rebuild but forgot to push to &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The other team followed the correct procedure, comparing their branch to the production base and ensuring no commits were missed. Since &lt;strong&gt;my changes weren't in &lt;code&gt;main&lt;/code&gt; or their branch at that time&lt;/strong&gt;, their check passed. As a result, my changes were overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No matter how correctly one team checks, if the other doesn't push, incidents can't be prevented.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch Checks Weren't Enough
&lt;/h2&gt;

&lt;p&gt;After the third incident, the other team nearly caused a &lt;strong&gt;fourth incident in another repository&lt;/strong&gt;, which they caught just in time. They compared the SHA256 hashes of files in the running Pod with their branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipeline.py     Match
brain_talk.py   ⚠️ Mismatch  Running=1a02…  Local=341e…
app.py          ⚠️ Mismatch  Running=8a41…  Local=4110…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mismatches were three delivery recovery fixes I had added. If they had done a full build, all would have been lost.&lt;/p&gt;

&lt;p&gt;At that time, &lt;strong&gt;they had mistaken the base branch&lt;/strong&gt; (they thought it was a feature branch, but it was actually built from &lt;code&gt;main&lt;/code&gt;). The &lt;code&gt;git log HEAD..&amp;lt;base&amp;gt;&lt;/code&gt; procedure assumes the base is known, so &lt;strong&gt;it fails if there's a misunderstanding&lt;/strong&gt;. SHA256 comparison works without knowing the branch name.&lt;/p&gt;

&lt;p&gt;Another lesson was that &lt;strong&gt;checking for file existence isn't enough&lt;/strong&gt;. After the second incident, I started checking if the other team's &lt;code&gt;conversation_log.py&lt;/code&gt; existed, but in this case, &lt;strong&gt;the files existed but had different contents&lt;/strong&gt;. Existence checks wouldn't catch this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Two Steps Are Enough
&lt;/h2&gt;

&lt;p&gt;From the three incidents, two essential steps emerged:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Push to &lt;code&gt;main&lt;/code&gt; immediately after deployment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the core. If both teams follow this, &lt;code&gt;main&lt;/code&gt; will always match production, and step 2 becomes automatic. The third incident occurred because I failed to do this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Before building, compare the running Pod and your branch using SHA256.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the safety net. Even if the other team doesn't follow step 1, this will catch issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;ns&amp;gt; deploy/&amp;lt;dep&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'cd /app &amp;amp;&amp;amp; find apps/services -name "*.py" | sort | xargs sha256sum'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/pod.txt
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;repo&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; find apps/services &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | xargs &lt;span class="nb"&gt;sha256sum&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/br.txt
diff /tmp/pod.txt /tmp/br.txt   &lt;span class="c"&gt;# Ensure differences are only your changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 alone couldn't prevent the third incident, and step 1 alone couldn't prevent the first. Both are necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Pitfalls Encountered
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubectl rollout status&lt;/code&gt; completion can't be trusted.&lt;/strong&gt; "Successfully rolled out" appeared immediately, but two Pods were running simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exista-voicepipe-&amp;lt;rs-old&amp;gt;-lpqtx   Running  ...:0.7.14
exista-voicepipe-&amp;lt;rs-new&amp;gt;-cv92x  Running  ...:0.7.15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They converged after a few seconds, but during that time, testing would have resulted in inconsistent behavior. &lt;strong&gt;After deployment, count Pods directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A similar incident occurred in a batch job generating 200 clips. The Pod was replaced mid-generation, resulting in &lt;strong&gt;half the clips using different settings&lt;/strong&gt;. I only noticed when logs showed the new gate hadn't triggered once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't reuse build definitions.&lt;/strong&gt; I once modified an old manifest with &lt;code&gt;sed&lt;/code&gt; but forgot to update the &lt;code&gt;--branch&lt;/code&gt;, causing the first incident. It's better to keep separate definitions for each purpose. The other team now includes a note to use the running tag as the base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beware of tag overwrites.&lt;/strong&gt; Once, a chain of &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; broke, skipping a tag bump and &lt;strong&gt;pushing new content to an old tag&lt;/strong&gt;. Rolling back to that tag no longer restores the original behavior. Without knowing this history, troubleshooting becomes impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizable Lessons
&lt;/h2&gt;

&lt;p&gt;These incidents seem specific to parallel work, but &lt;strong&gt;they can happen even solo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The first incident's root cause was &lt;strong&gt;the common assumption that &lt;code&gt;main&lt;/code&gt; is the source of truth didn't apply here&lt;/strong&gt;. The deployment branch was the source of truth, and &lt;code&gt;main&lt;/code&gt; was outdated. This is common in environments with unstructured release flows.&lt;/p&gt;

&lt;p&gt;Even solo, &lt;strong&gt;if past-you built from a different branch&lt;/strong&gt;, the same issue arises. Your past self is as unpredictable as another team member.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key is to check if your build includes what's currently running, not what you added.&lt;/strong&gt; You remember what you added but not what's running. That's why mechanical comparison is essential.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: Mass-Producing Practical Voices from Diffusion TTS
&lt;/h2&gt;

&lt;p&gt;This series documents designing voices from a single caption, creating training corpora, and mass-producing role-specific practical voices. This article is &lt;strong&gt;Part 4: Operations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;← Previous: &lt;a href="https://forge.workstyle.tech/blog/four-registration-paths-one-exit/" rel="noopener noreferrer"&gt;Four Registration Paths, Zero Management Screens&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ Next: Failing When Chasing Unmeasured Metrics with Thresholds&lt;/p&gt;

&lt;p&gt;All 18 Articles in the Series&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/diffusion-tts-too-slow-for-conversation/" rel="noopener noreferrer"&gt;The High-Quality TTS Was Too Slow for Conversations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/deterministic-voice-gacha-and-design-ledger/" rel="noopener noreferrer"&gt;Voice Gacha: Designing Voices Deterministically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/screening-voices-by-metrics-not-ears/" rel="noopener noreferrer"&gt;Selecting "Narrator-Like" Voices from 24 Candidates Using Metrics, Not Ears&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/quality-gate-selection-bias-flat-takes/" rel="noopener noreferrer"&gt;Stricter Quality Gates Let More Monotone Voices Through&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/speaking-style-is-baked-into-the-corpus/" rel="noopener noreferrer"&gt;Speaking Speed Can't Be Changed After Training&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/tts-changes-recording-room-every-time/" rel="noopener noreferrer"&gt;TTS That Changes "Recording Room" Every Generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/one-rough-clip-ruins-the-whole-style/" rel="noopener noreferrer"&gt;One Rough Clip Ruins the Entire Style&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/where-did-the-elongated-ending-come-from/" rel="noopener noreferrer"&gt;Where Did the AI's Habit of Stretching "Hello" Come From?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/the-character-that-broke-the-tts-input/" rel="noopener noreferrer"&gt;The Character That Broke the TTS Input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/hallucination-guard-that-never-fired/" rel="noopener noreferrer"&gt;Hallucination Guard That Never Fired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/three-chars-became-a-verbal-tic/" rel="noopener noreferrer"&gt;Three Characters Became a Verbal Tic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/measuring-factory-defects-as-product-traits/" rel="noopener noreferrer"&gt;Measuring Factory Defects as Product Traits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/defects-invisible-to-transcription/" rel="noopener noreferrer"&gt;Defects Invisible to Transcription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/70-minutes-lost-to-a-network-blink/" rel="noopener noreferrer"&gt;70 Minutes Lost to a Network Blink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/ja-vs-jp-babbling-model/" rel="noopener noreferrer"&gt;"ja" vs "JP": The Babbling Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forge.workstyle.tech/blog/four-registration-paths-one-exit/" rel="noopener noreferrer"&gt;Four Registration Paths, Zero Management Screens&lt;/a&gt;
&lt;strong&gt;17. &lt;a href="https://forge.workstyle.tech/blog/who-is-rolling-back-whom/" rel="noopener noreferrer"&gt;Overwriting Each Other's Work with Every Deployment&lt;/a&gt;&lt;/strong&gt; ← You are here
&lt;/li&gt;
&lt;li&gt;Failing When Chasing Unmeasured Metrics with Thresholds&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The insights are compiled in the &lt;a href="https://forge.workstyle.tech/blog/diffusion-tts-practical-voice-pipeline/" rel="noopener noreferrer"&gt;Practical Voice Mass Production Pipeline from Diffusion TTS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cicd</category>
      <category>git</category>
    </item>
  </channel>
</rss>
