<?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>Why gpu-unlock Speeds Up RTX 3090 Prefill but Barely Changes Decode</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:03:59 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-gpu-unlock-speeds-up-rtx-3090-prefill-but-barely-changes-decode-4572</link>
      <guid>https://dev.to/orca_forge/why-gpu-unlock-speeds-up-rtx-3090-prefill-but-barely-changes-decode-4572</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/rtx3000-fp16-bf16-unlock-gpu-unlock/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=rtx3000-fp16-bf16-unlock-gpu-unlock" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR gpu-unlock changes FP16/BF16 instruction throughput on some RTX 3000-series GeForce cards. An RTX 3090 report measured 33% faster vLLM prefill and 24% less time to the first token. Decode changed by only 1.2%, because it is limited by memory bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Being Held Back
&lt;/h2&gt;

&lt;p&gt;According to the README, the affected cards are RTX 3000 series GPUs based on GA102, GA103, GA104, and GA106. It says the unlock doubles the instruction issue rate for FP16/BF16 multiply-accumulate operations using FMA, where the results are accumulated in FP32. In the README’s words, this makes them “as fast as their professional equivalents.” It does not speed up the whole GPU; only this type of operation gets faster. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;According to the explanation of how it works, the signed firmware that runs during Ampere startup—the Booter running on SEC2—has a buffer overflow vulnerability. This can be used to execute code via ROP. Through that path, the module changes write permissions for registers that drivers normally cannot write to, then changes the value that controls the instruction issue rate. The rate is selected using a 4-bit value, with options such as full speed and half speed. &lt;a href="https://github.com/Zagitta/gpu-unlock/blob/main/docs/How%20Ampere%20was%20unlocked.md" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The module is loaded before the NVIDIA driver connects to the card. It changes the setting, performs an FLR (a per-card reset), and then hands control to the unmodified driver. It is neither a UEFI app nor a modified driver. For that reason, the README says there is no need to reapply patches every time a new driver comes out. The module only runs when the target card is specified on the kernel command line; removing that setting restores the original behavior. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Faster—and What Doesn’t
&lt;/h2&gt;

&lt;p&gt;The top of the README lists numbers for an RTX 3090 Ti. They are the median of five runs of a dense 8192³ GEMM at 450 W, with results accumulated in FP32.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Numbers in the README (reported in issue #1)&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP16&lt;/td&gt;
&lt;td&gt;87.46 TFLOPS&lt;/td&gt;
&lt;td&gt;118.39 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;87.50 TFLOPS&lt;/td&gt;
&lt;td&gt;157.41 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TF32&lt;/td&gt;
&lt;td&gt;42.58 TFLOPS&lt;/td&gt;
&lt;td&gt;82.61 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers weren’t measured by the repository’s author. They came from someone who posted in &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;issue #1&lt;/a&gt; and tested an experimental fork with RTX 3090 Ti support. The setup included a “modified NVIDIA 610.43.02 driver” and a custom startup script. The same issue also reports tests with the unmodified package and a standard driver. In those tests, BF16 performance rose from 63.0 to 105.9 TFLOPS. However, the power limit also changed from 300 W to 380 W, so the conditions weren’t the same before and after.&lt;/p&gt;

&lt;p&gt;A GEMM speedup doesn’t translate directly into an application speedup. The README says workloads with a lot of matrix computation get faster. It lists LLM prefill, image generation (diffusion), and FlashAttention. LLM decode is limited by memory bandwidth, so it barely changes. INT8 computation is also unchanged. One report comparing outputs found them identical down to the bit. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reports from users show a more nuanced picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A vLLM report showed prefill getting 33–36% faster, with similar results at 8K, 32K, and 128K context lengths. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;On the same card, a GGUF engine showed no change at 8K and a 12–13% speedup at 32K and 128K. The reporter noted that gains depend on the engine and kernels, and that short prompts may see no change at all. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Two reports on an RTX 3090 showed 14–16% faster prefill at 16K, with no change at 4K. One MoE model showed no change with a very long prompt of around 32K, because loading experts from RAM took longer. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;issue #9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Decode was within the margin of error in every report. INT8 GEMM also showed no change in reports that measured it for comparison. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another factor affecting the results is the power limit. During GEMM, the card stays at its power limit, so the clock drops when it does more work per clock cycle. In one report with the limit set to 220 W, the SM clock fell from about 1,720 MHz to about 1,330–1,365 MHz. Even with roughly 1.9× more work per clock cycle, the real-world speedup doesn’t reach that level. The README also notes that its smallest gain, +25%, came from a card capped at 225 W. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Cards Have Been Reported
&lt;/h2&gt;

&lt;p&gt;The compatibility table in the README summarizes reports from users. The reported BF16 GEMM gains come from different benchmarks, so they can’t be compared side by side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;BF16 GEMM gain (reported)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3090 Ti&lt;/td&gt;
&lt;td&gt;+68–80%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3090&lt;/td&gt;
&lt;td&gt;+25–67%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;#9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;#25&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;#33&lt;/a&gt; and others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 Ti 20 GB (OEM)&lt;/td&gt;
&lt;td&gt;Some reports say it remains at half speed; others report about 2×&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/28" rel="noopener noreferrer"&gt;#28&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 10 GB&lt;/td&gt;
&lt;td&gt;+70–73%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/14" rel="noopener noreferrer"&gt;#14&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 20 GB (memory-modified)&lt;/td&gt;
&lt;td&gt;+56–59%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/10" rel="noopener noreferrer"&gt;#10&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;#15&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3070&lt;/td&gt;
&lt;td&gt;+73–81%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/10" rel="noopener noreferrer"&gt;#10&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/12" rel="noopener noreferrer"&gt;#12&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12 GB&lt;/td&gt;
&lt;td&gt;About +80%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/pull/13" rel="noopener noreferrer"&gt;#13&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/31" rel="noopener noreferrer"&gt;#31&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX 3070 Ti has not been tested with this module. There are no reports yet for GA103 cards.&lt;/p&gt;

&lt;p&gt;The results are mixed for the OEM RTX 3080 Ti 20 GB. In the first report, the unlock appeared to succeed and the setting’s value remained changed, but BF16 was still running at half speed. Downgrading the driver to 610.43.03 raised performance to about 65% of full speed, but it still didn’t reach full speed. Another user with two cards of the same model reported about 2× performance with driver 610.43.03. The other card, which was not unlocked, remained at its original speed. The two users’ cards have different board IDs. As of October 26, the cause is still unknown. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/28" rel="noopener noreferrer"&gt;issue #28&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Consider Before Installing
&lt;/h2&gt;

&lt;p&gt;This article doesn’t cover the installation steps. Please read the README. Instead, here are the points that stood out to me after reading the primary sources.&lt;/p&gt;

&lt;p&gt;The primary sources say nothing about warranty coverage. Check with your card’s manufacturer or retailer.&lt;/p&gt;

&lt;p&gt;The boot process also changes. In the reports I read, everyone had Secure Boot disabled. Because this loads an unsigned kernel module, the kernel is marked as tainted. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;issue #9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; Whether you can unlock the card again after a warm reboot depends on the board. Some users report success, while others only use it after a cold boot. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;issue #1&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; You can also use it with a card that’s driving a display. However, the screen goes blank between the reset and when &lt;code&gt;nvidia-drm&lt;/code&gt; takes over. You’ll need to unlock the card again after resuming from suspend, and hibernation isn’t supported. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its relationship to NVIDIA’s terms is unclear. The firmware used by the module is extracted from the NVIDIA driver package and is subject to the NVIDIA driver license agreement. The NOTICE states that permission to redistribute it publicly has not been obtained. The primary sources I read contain no record of NVIDIA commenting on this module. The mechanism itself exploits a vulnerability in NVIDIA-signed firmware. &lt;a href="https://github.com/Zagitta/gpu-unlock/blob/main/firmware/NOTICE.md" rel="noopener noreferrer"&gt;firmware/NOTICE.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Power consumption and stability are also worth considering. As noted above, gains are small when the power limit is low. Reported figures with a higher limit vary widely in their testing conditions. The longest stability report describes more than eight hours of mixed workloads without errors. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; Another user wrote, “a few hours, not weeks.” &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt; We still don’t know what happens with long-term use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measure whether prefill or decode is the source of your wait time.&lt;/li&gt;
&lt;li&gt;Compare reports with similar cards, power limits, drivers, and engines.&lt;/li&gt;
&lt;li&gt;Do not expect decode token/s to improve from this module.&lt;/li&gt;
&lt;li&gt;The README lists image generation as a workload, but the issues reviewed here provide no before and after performance numbers.&lt;/li&gt;
&lt;li&gt;Check Secure Boot requirements, kernel tainting, and the limited long-term stability reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/h100-only-3b-for-conversation/" rel="noopener noreferrer"&gt;We spent ¥5 million on a GPU, but only 3B parameters were usable for conversation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/compute-vs-graphics-gpu-h100-cant-render/" rel="noopener noreferrer"&gt;We rented an H100, but it couldn’t render a single frame — Compute GPUs and graphics GPUs are different&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/ollama-v1-context-4096/" rel="noopener noreferrer"&gt;Why Ollama wouldn’t call tools: a context length of 4,096&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gpu</category>
      <category>llm</category>
      <category>linux</category>
    </item>
    <item>
      <title>How to Catch Japanese Text in Automated dev.to Crossposts</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:01:18 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-catch-japanese-text-in-automated-devto-crossposts-3imo</link>
      <guid>https://dev.to/orca_forge/how-to-catch-japanese-text-in-automated-devto-crossposts-3imo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/devto-translation-shipped-in-japanese/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=devto-translation-shipped-in-japanese" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR A Japanese blog was translated and automatically posted to dev.to, but its title and description remained in Japanese. The English body and generated &lt;code&gt;devto.md&lt;/code&gt; made the post look ready, so the check for total translation failure did not stop it. A pre-post check now inspects the title, description, body, and tags, while people still review meaning and numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Body Is in English, It Slips Through
&lt;/h2&gt;

&lt;p&gt;If translation failed, no file for posting was created. That safeguard existed. But it only stopped the process when the entire translation failed and no file was produced.&lt;/p&gt;

&lt;p&gt;In a case like this one, the body was in English while only the title and description remained in Japanese. The English body had been generated, and so had &lt;code&gt;devto.md&lt;/code&gt;. From the perspective of the downstream &lt;code&gt;crosspost.mjs&lt;/code&gt;, it looked like a post-ready article was available.&lt;/p&gt;

&lt;p&gt;The rule “don’t create the file if English translation fails” assumed that failure would affect the article as a whole. In reality, each field could succeed or fail independently. The body could be translated while the title and description were left behind. There was no check to catch that partial success.&lt;/p&gt;

&lt;p&gt;It wasn’t just the title. The article also had a Japanese table and timeline inside a code block. For example, this section with the numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YouTube Shorts
  Impressions        Approx. 2,900
  Views                       279   (continued viewing: 9.5%)
  Average view duration      0:13   (about 85% of a 15.3-second video)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when the body paragraphs had been translated into English, blocks like these weren’t necessarily in English too. Code blocks aren’t only for programs; they can also contain logs, tables of numbers, and processing timelines. Reading just some of the visible paragraphs wasn’t enough to know the state of the article as a whole.&lt;/p&gt;

&lt;h2&gt;
  
  
  The English Translation Had Other Errors, Too
&lt;/h2&gt;

&lt;p&gt;The articles I reviewed around the same time also contained mistranslations in the body text.&lt;/p&gt;

&lt;p&gt;The output token count, “134万” (1.34 million), had become &lt;code&gt;134,000&lt;/code&gt; in English. “二次関数的に膨らむ” (grow quadratically) had been translated as &lt;code&gt;exponentially&lt;/code&gt;, and “制限にはかかっていません” (it is not subject to the limit) as &lt;code&gt;it is not being used&lt;/code&gt;. Even when the meanings seem close, they do not describe the same state or value.&lt;/p&gt;

&lt;p&gt;The code block language tags were wrong, too. Every log or text block was tagged &lt;code&gt;python&lt;/code&gt;. Checking only whether the text had become English could not tell us whether the numbers were preserved, the meanings were accurate, or the block types were correct.&lt;/p&gt;

&lt;p&gt;There were limits to checking translation success with a single indicator. The fact that a file had been created did not mean that every part of the article had been translated correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural-Sounding English Errors Can’t Be Found by Checking the Characters Alone
&lt;/h2&gt;

&lt;p&gt;On October 2, 2026, a person spotted and corrected another article’s title before it was published.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Comparing Japanese &lt;strong&gt;Speech Recognition&lt;/strong&gt; with 1,000 Utterances&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The article was about classifying utterances, not speech recognition. The English phrasing wasn’t unnatural. That’s exactly why checking only for leftover Japanese characters can’t catch this kind of error. The tags also contained the same value twice: &lt;code&gt;"llm, llm"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A person caught the title before it went live. That made one thing clear: if we rely on pre-publication review, we could miss another error. We need to catch leftover Japanese, but we also need to block anything we can validate mechanically at the entry point to the publishing process.&lt;/p&gt;

&lt;p&gt;Still, character checks can’t catch every mistranslation. &lt;code&gt;Speech Recognition&lt;/code&gt; is natural English. The number &lt;code&gt;134,000&lt;/code&gt; also looks like a natural English number. Checking only the character types won’t reveal a mismatch with the original meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the Article Itself Right Before Posting
&lt;/h2&gt;

&lt;p&gt;On October 3, 2026, I added a pre-post check to &lt;code&gt;crosspost.mjs&lt;/code&gt;. It checks the title, description, body, and tags in the actual &lt;code&gt;devto.md&lt;/code&gt; being sent—not whether the translation process succeeded.&lt;/p&gt;

&lt;p&gt;If the title or description contains hiragana or katakana, the post is held. If any Japanese remains at the point of posting, it is not sent as is. For the body, the check excludes code blocks and inline code, then holds the post if three or more lines contain kana or katakana.&lt;/p&gt;

&lt;p&gt;The body threshold is not zero. It allows one or two lines of Japanese, such as in data examples or quotations. Code blocks and inline code are also excluded from the body check. That way, Japanese characters or values there alone are not treated as a translation failure across the entire body.&lt;/p&gt;

&lt;p&gt;The check also holds the post if tags are duplicated. When a post is held, the reason is logged and a notification is sent to the Mac. If the content has been reviewed, it can still be posted with &lt;code&gt;--force&lt;/code&gt;. After automatically stopping a post, there’s still a way to review the content and let it through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results from Checking 94 Articles
&lt;/h2&gt;

&lt;p&gt;After adding the checks, I reviewed 94 existing articles. None had Japanese text left in their titles or descriptions.&lt;/p&gt;

&lt;p&gt;This means no Japanese text was found in the titles and descriptions of the 94 articles reviewed. It does not mean there were no mistranslations in the body text or that all the numbers were correct. We need to distinguish what the checks can verify from what still requires a person to read and review.&lt;/p&gt;

&lt;p&gt;This setup can catch hiragana or katakana in titles and descriptions, substantial amounts of hiragana or katakana left in the body text, and duplicate tags. It cannot identify mistranslations that read naturally in English or numbers that have been transcribed incorrectly. A person still needs to read the articles to check for those issues.&lt;/p&gt;

&lt;p&gt;This check also cannot detect text embedded in PNG diagrams, so the text in those diagrams remains in Japanese. If we create diagrams in SVG or HTML, we can replace the text and export English versions. We have decided to take that approach, but the process for creating English diagrams has not yet been implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the title, description, body, and tags in the output immediately before posting.&lt;/li&gt;
&lt;li&gt;Hold posts when the title or description contains hiragana or katakana, or when three or more body lines contain them outside code.&lt;/li&gt;
&lt;li&gt;Hold duplicate tags, log the reason, and send a Mac notification; reviewed posts can be sent with &lt;code&gt;--force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Human review is still needed for mistranslations and number changes that look natural in English.&lt;/li&gt;
&lt;li&gt;The check does not inspect text embedded in PNG diagrams; the English diagram workflow is not implemented yet.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devto</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>How to Recover 13 Deleted DNS Records in 11 Minutes</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-recover-13-deleted-dns-records-in-11-minutes-12o2</link>
      <guid>https://dev.to/orca_forge/how-to-recover-13-deleted-dns-records-in-11-minutes-12o2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/deleted-wrong-domain-dns-restored-11min/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=deleted-wrong-domain-dns-restored-11min" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR We selected a live domain beside the intended domain in the DNS management UI and deleted 13 records. AXFR was refused, but the pre-deletion list and authoritative DNS data were still available. We enumerated names, queried them with &lt;code&gt;dig&lt;/code&gt;, cross-checked public DNS caches, and manually restored the records in about 11 minutes.&lt;/p&gt;

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

&lt;p&gt;If you accidentally delete DNS records, the last known list and values retrieved from the authoritative DNS server (the server that returns the domain’s official DNS information) can help you restore them. AXFR was refused in this case, but by enumerating names, querying them with &lt;code&gt;dig&lt;/code&gt;, and cross-checking public DNS caches, I manually restored 13 records in about 11 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the DNS Records Were Before Deletion
&lt;/h2&gt;

&lt;p&gt;Thirteen records were deleted: 11 A records, one CNAME record, and one MX record. The recovery source was a complete list that another operator had copied from the management console just before deletion. They were working in a separate AI session.&lt;/p&gt;

&lt;p&gt;After deletion, it becomes difficult to remember the original values one by one. In this case, the list had been saved, so there was enough information to re-create all 13 records manually.&lt;/p&gt;

&lt;p&gt;However, the list alone could not confirm the state of the authoritative DNS. Immediately after the deletion request was submitted, the update to the authoritative DNS was delayed. Running &lt;code&gt;dig&lt;/code&gt; still returned every record that existed before deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Verified Every Record After AXFR Was Denied
&lt;/h2&gt;

&lt;p&gt;The AXFR request to transfer the entire DNS zone was denied. So I enumerated the names and queried them with &lt;code&gt;dig&lt;/code&gt;, then checked the results against a recently copied list and the records retrieved from the authoritative DNS server.&lt;/p&gt;

&lt;p&gt;I also compared them with public DNS caches—temporarily stored DNS information. I used results from 8.8.8.8 and 1.1.1.1 to double-check, and verified what was visible through those separate caches instead of relying only on the list retrieved from the authoritative DNS server.&lt;/p&gt;

&lt;p&gt;I had three sources to guide the manual restoration: the surviving list, the full set of records retrieved from the authoritative DNS server right after deletion, and the public DNS cache results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying Recovery Time Using TTL and SOA Serial
&lt;/h2&gt;

&lt;p&gt;The A record’s TTL (the length of time DNS information is kept in cache) was 3,600 seconds, and the negative cache TTL (how long a “does not exist” response is cached) was 300 seconds. The SOA serial (the zone’s update number) showed that the zone became empty at 10:06 and was restored by re-registration at 10:17. According to the records, recovery took about 11 minutes.&lt;/p&gt;

&lt;p&gt;Recovery depended on more than the TTL values. Even after deletion, we could retrieve all records from the authoritative DNS server and compare them with the previous list and the results from public DNS caches. This let us verify what to enter manually.&lt;/p&gt;

&lt;p&gt;We didn’t undo the deletion in the admin console. Instead, we gathered information to verify the records and re-registered them manually. Because the original values were still available, we could proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Avoid Accidentally Deleting DNS Records
&lt;/h2&gt;

&lt;p&gt;This mistake happened while I was working with domains that had similar names on the same screen. I meant to delete an unused domain, but selected the adjacent one that was still active. The lesson: don’t manage similarly named domains on the same screen, and export the entire zone before deleting anything.&lt;/p&gt;

&lt;p&gt;Even when you try to retrieve every record in a zone with AXFR, the request may be denied. That happened here, so I enumerated the names and queried them with &lt;code&gt;dig&lt;/code&gt;. Keeping a copy of the list before deleting records means you won’t have to start from scratch searching for their values afterward.&lt;/p&gt;

&lt;p&gt;I restored everything in 11 minutes, not because I found a special recovery feature, but because I had a recently copied list, could retrieve every record with &lt;code&gt;dig&lt;/code&gt; while the changes were still propagating to the authoritative DNS servers, and could cross-check the results against public DNS caches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check for a pre-deletion record list in the management UI.&lt;/li&gt;
&lt;li&gt;If authoritative DNS propagation is delayed, enumerate names and use &lt;code&gt;dig&lt;/code&gt; to see whether all records can be retrieved.&lt;/li&gt;
&lt;li&gt;Cross-check results against the public DNS caches at 8.8.8.8 and 1.1.1.1.&lt;/li&gt;
&lt;li&gt;Manually re-register the verified values and check the time with the SOA serial.&lt;/li&gt;
&lt;li&gt;Before the next deletion, identify the target domain and export the full zone.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dns</category>
    </item>
    <item>
      <title>Why MediaRecorder Turns a 190-Second Recording Into 92 Seconds</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:19:57 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-mediarecorder-turns-a-190-second-recording-into-92-seconds-4jpf</link>
      <guid>https://dev.to/orca_forge/why-mediarecorder-turns-a-190-second-recording-into-92-seconds-4jpf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/mediarecorder-recording-half-lost/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=mediarecorder-recording-half-lost" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR MediaRecorder delivers recording data in chunks, and processing them is asynchronous. If stopping closes the file before pending chunk sends finish, the final data is lost. Silent audio and a stop check that misses the studio avatar's voice can also cause empty files or cut speech.&lt;/p&gt;

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

&lt;p&gt;The reason a roughly 190-second recording with MediaRecorder lasted only 92 seconds was that the system didn't wait for the final chunk processing upon stopping, no audio data was sent during silence, and the stop logic did not account for the end of the studio-side audio. I implemented fixes to wait for the chunk transmission to complete, stream silence using a &lt;code&gt;ConstantSource&lt;/code&gt;, and ensure the recording continues until the studio-side audio track has actually finished. After these fixes, a 60-second recording of silence was saved correctly with 60.49 seconds of video and 60.48 seconds of audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the last data was missing after stopping the recording
&lt;/h2&gt;

&lt;p&gt;The reason for the missing last data was that the main side was notified of the recording stop before finishing processing the chunks.&lt;/p&gt;

&lt;p&gt;MediaRecorder divides the recorded data into chunks and passes them on. Even after receiving a chunk, the process of converting its contents to a byte array is asynchronous (a method that proceeds to the next process without waiting for completion). Right before stopping, the last chunk had arrived, but I didn't wait for the processing to finish and notified the main side to stop the recording.&lt;/p&gt;

&lt;p&gt;The main side receives the stop instruction and closes the file. Even if the chunk processing is completed afterwards, the destination for writing is already closed. Although it seems like the recording was working until the end, the last data is not saved.&lt;/p&gt;

&lt;p&gt;In the fix, I processed the chunks in the order they were received and sent the next one after the previous chunk's transmission was completed. When stopping, I wait until all pending transmissions are finished. What was necessary was not the fact that the stop was called, but that the processing started before the stop was completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why My 60-Second Voiceless Recording Resulted in a 0-Byte File
&lt;/h2&gt;

&lt;p&gt;The reason the voiceless recording came out as a 0-byte file was that no data was flowing through the audio channel.&lt;/p&gt;

&lt;p&gt;When I examined the case where I recorded for 60 seconds without any voice, I found that no audio data was being transmitted through the audio track used for the recording until a voice actually entered. With the audio input empty, the &lt;code&gt;MediaRecorder&lt;/code&gt; did not write anything, resulting in a 0-byte file even after stopping the recording.&lt;/p&gt;

&lt;p&gt;I had assumed that only the video was being captured. In reality, the recording input included an audio track (the audio data intended to be included in the recording), and this audio channel remained empty during periods of silence.&lt;/p&gt;

&lt;p&gt;I connected a &lt;code&gt;ConstantSource&lt;/code&gt; (an audio source that continuously outputs a constant value) to the audio channel, outputting a value of 0. This ensures that silent audio data is always flowing. The purpose is to continue passing the audio track to the recording even during moments when no voice is present. I discussed this mechanism in a previous article. What I discovered this time is that waiting for 60 seconds without any voice resulted in a 0-byte file, and that the root cause was the lack of incoming data on the audio channel. Even if the intent is to record video only, when using a recording setup that combines an audio track, the state of the audio input can significantly affect the final output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the last voice of the studio-side avatar was cut off
&lt;/h2&gt;

&lt;p&gt;The reason why the last word was cut off in the middle is that the recording stop judgment was not looking at the voice of the studio-side avatar.&lt;/p&gt;

&lt;p&gt;The stop judgment was looking at the voice sounding in the avatar window on the desktop. However, the voice of the avatar that appears only on the broadcast screen is on the studio side. The window being looked at by the stop judgment does not have the end time of that voice.&lt;/p&gt;

&lt;p&gt;I thought that if the side that determines the stop of the recording judges that "it's quiet now", it would also think that the voice on the studio side has ended. However, that premise was incorrect. Even just before stopping the recording, there were cases where the voice on the stage was still sounding. In fact, in a scene where the last word lasted for about 34 seconds, it was cut off after about 2 seconds.&lt;/p&gt;

&lt;p&gt;So, I made the studio side have the end time of the voice for each avatar. When stopping the recording, it waits until the time when the last voice ends plus 1.5 seconds. However, this waiting time has an upper limit of 120 seconds. I also extended the main side's save wait time from 15 seconds to 150 seconds. This is to prevent the main side's wait from being cut off first while the studio is waiting for the end of the voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Recording Time and Audio Length Changed After Fixes
&lt;/h2&gt;

&lt;p&gt;After the fixes, the video and audio lengths were almost aligned, and the final subtitles fit within the video.&lt;/p&gt;

&lt;p&gt;Recording 60 seconds without speaking resulted in a video length of 60.49 seconds and an audio length of 60.48 seconds. A recording with six dialogue exchanges resulted in a video length of 171.42 seconds and an audio length of 171.37 seconds.&lt;/p&gt;

&lt;p&gt;The alignment was achieved by revisiting the processing at three points before stopping the recording: waiting for chunk transmission, streaming silence in the audio path during silent periods, and waiting for the avatar's voice to end in the studio before deciding to stop. Fixing only one of these would still result in gaps in the remaining paths.&lt;/p&gt;

&lt;p&gt;The recording time and the saved video length are not always the same. It was necessary to track not only the time when the stop was called but also the ongoing processes before and after, as well as which audio the termination judgment was monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Process chunks in order and wait for all pending sends before stopping.&lt;/li&gt;
&lt;li&gt;Feed silent data into the audio path with ConstantSource when no one is speaking.&lt;/li&gt;
&lt;li&gt;Use the studio avatar's voice end time in the stop check.&lt;/li&gt;
&lt;li&gt;Cap the studio wait at 120 seconds and extend the main save wait from 15 to 150 seconds.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mediarecorder</category>
      <category>webaudioapi</category>
    </item>
    <item>
      <title>Why a VRM Right Hand Disappeared: Empty Cuff Ring and NaN</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:04:39 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-a-vrm-right-hand-disappeared-empty-cuff-ring-and-nan-2f47</link>
      <guid>https://dev.to/orca_forge/why-a-vrm-right-hand-disappeared-empty-cuff-ring-and-nan-2f47</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/vrm-right-hand-vanished-nan/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=vrm-right-hand-vanished-nan" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR A VRM export made only the right hand disappear, even across edits and poses. The shared cuff position found no right-hand vertices, leaving an empty ring. Its mean produced NaN values that spread to the pivot and finger bones.&lt;/p&gt;

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

&lt;p&gt;The disappearance of the right hand in the VRM was caused by the right hand's vertices failing to reach the common cuff position for both arms, which allowed NaN (Not a Number) values to propagate from an empty loop. We fixed this by ensuring that scaling transforms are not baked into the vertices, and by adding a safeguard that halts the process if NaN values are detected in empty loops or bones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was the Disappearing Right Hand a Display or Pose Issue?
&lt;/h2&gt;

&lt;p&gt;On October 5 at 17:01 UTC, a user reported an issue: only the right hand was missing on screen, both in an unedited state and regardless of the pose. My first thought was that it was a visibility toggle or posing issue.&lt;/p&gt;

&lt;p&gt;When I inspected the VRM exported at 17:11 UTC, the Hands mesh had 1,462 vertices. Of those, 711 vertices—accounting for the entire right hand—had their positions set to NaN. The renderer simply doesn't draw vertices with NaN coordinates. The invisible hand wasn't a rendering bug after all.&lt;/p&gt;

&lt;p&gt;Initially, I checked component visibility toggles, edit operations, weight sources, poses, and culling. (Weights define how much a bone influences vertices.) The hand's scale was also set to 1. None of these were the culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  At which stage was NaN born?
&lt;/h2&gt;

&lt;p&gt;I investigated the pipeline—the processing flow up until export—stage by stage. The files were normal up until the manual adjustment stage. Starting from the files in the synchronization stage, the rest matrix (a matrix representing the reference pose of the bones) and lengths for the 15 bones of the right finger were NaN.&lt;/p&gt;

&lt;p&gt;NaN did not suddenly appear during export. Even before the synchronization stage, the input for the coordinate generation process was already corrupted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Ring at the Cuff's Vertex Became Empty
&lt;/h2&gt;

&lt;p&gt;The data before modification was normal. The change began with the influence of the 3D editing done afterwards.&lt;/p&gt;

&lt;p&gt;In the modification, a shrink transformation was attached to the Hands object. In world coordinates (coordinates based on the entire scene), it changed from -0.586 to +0.662, and the right hand was shifted about 7cm inward.&lt;/p&gt;

&lt;p&gt;After that, step 1 of replacing the head bakes the transformation of all objects into the vertices. Baking refers to the process of reflecting the transformation attached to an object into the vertex coordinates. Although step 2 of synchronization also has a process called &lt;code&gt;matrix_world = Identity&lt;/code&gt;, it is after baking, so the transformation cannot be restored here.&lt;/p&gt;

&lt;p&gt;The problematic calculation was in step 6. The cuff position was determined by a common value for both left and right sides.&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;cuff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="c1"&gt;# approximately 0.63
&lt;/span&gt;&lt;span class="n"&gt;x_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;approximately&lt;/span&gt; &lt;span class="mf"&gt;0.61&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right hand only reaches up to -0.586. Even if we search for vertices on the cuff side from approximately 0.61, there are no corresponding vertices for the right hand. The ring (a collection of vertices along the cuff) on the right became empty.&lt;/p&gt;

&lt;p&gt;Taking the average of an empty ring results in NaN. The pivot (the reference point for transformation) becomes NaN from that value, and NaN spreads to the 15 bones of the right fingers as well. The direct cause of the destruction of 711 vertices was handling different ranges on the left and right with a common cuff value.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Stop the Empty Loop and NaN in Bones
&lt;/h2&gt;

&lt;p&gt;The correction was done in two stages.&lt;/p&gt;

&lt;p&gt;In the stage of replacing the head, the hands are excluded from the conversion target. The reduction that was attached to the hands was prevented from being baked into the vertices.&lt;/p&gt;

&lt;p&gt;In the synchronization stage, a process was added to raise a &lt;code&gt;RuntimeError&lt;/code&gt; if the loop is empty. Furthermore, if there is an NaN in the bones, it is stopped by an assert (a check that confirms the condition and stops the process if it is not met). This is to detect abnormal values before proceeding and exporting VRM.&lt;/p&gt;

&lt;p&gt;Re-execution took real 0m21.657s and exited with 0. The log output included "hands ×0.924 (from sleeve to fingertips 0.134 m)". In the confirmation output, the number of NaNs in the 1462 vertices of the Hands mesh was 0.&lt;/p&gt;

&lt;p&gt;It was confirmed that the hands were restored. However, this alone did not reveal other problems in the processing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Python Errors Don’t Stop Execution in Our Pipeline
&lt;/h2&gt;

&lt;p&gt;In a related log from another day, a 3D editing application was returning exit code 0 despite encountering Python errors. Relying solely on the exit code meant the calling process couldn’t detect the anomaly.&lt;/p&gt;

&lt;p&gt;On 10-06, I added &lt;code&gt;--python-exit-code 1&lt;/code&gt; to some steps. By 10-07, all steps were routed through a common function, using both &lt;code&gt;--python-exit-code 1&lt;/code&gt; and &lt;code&gt;set -e -o pipefail&lt;/code&gt;. The &lt;code&gt;grep&lt;/code&gt; command for log verification was wrapped in &lt;code&gt;{ grep … || true; }&lt;/code&gt; to prevent the entire process from halting if no results were found.&lt;/p&gt;

&lt;p&gt;Additionally, I introduced a &lt;code&gt;FAIL_AT=&amp;lt;step&amp;gt;&lt;/code&gt; option for testing. Setting &lt;code&gt;FAIL_AT=ring&lt;/code&gt; intentionally triggered a failure, confirming that the process stopped with exit code 1 and subsequent steps were skipped.&lt;/p&gt;

&lt;p&gt;This exit code issue is separate from the problem of the missing right hand. However, if a pipeline detects an anomaly but continues processing, corrupted values will propagate downstream. It’s essential not only to log errors but also to ensure the process halts at the correct point when anomalies occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify Values Are Created at the Right Stage
&lt;/h2&gt;

&lt;p&gt;Simply chasing NaN vertices didn't lead to the root cause of the disappearing right hand. It was only after comparing each stage in order—applying transforms, baking transforms, and defining the cuffs—that I finally tracked it down to an empty loop.&lt;/p&gt;

&lt;p&gt;When processing values shared between the left and right sides, you can't assume both sides will have the same range of data. To fix this, I made sure the calculation aborts if the loop is empty and added validation for the bone values as well.&lt;/p&gt;

&lt;p&gt;Finally, I intentionally induced failures to confirm whether the pipeline truly stops when a step returns an error. The fix wasn't complete just by bringing the right hand back; it was about ensuring we never miss an anomaly the next time one occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&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;Making a VRM Dance from a Single Live-Action Video&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;The Cause of Wrist Skin Creasing Was Skinning, Not the Pose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check whether the issue affects only the right hand and reproduces across edits and poses.&lt;/li&gt;
&lt;li&gt;Check the exported VRM Hands mesh for NaN values.&lt;/li&gt;
&lt;li&gt;Compare files before hand editing, after transform baking, and at the sync stage.&lt;/li&gt;
&lt;li&gt;Stop when the cuff ring is empty, and check bones for NaN values.&lt;/li&gt;
&lt;li&gt;Make Python errors return exit code 1 and confirm later pipeline steps do not run.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vrm</category>
      <category>blender</category>
      <category>python</category>
      <category>3d</category>
    </item>
    <item>
      <title>Why uvx MCP servers fail with Pydantic 2.14</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:07:17 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-uvx-mcp-servers-fail-with-pydantic-214-i77</link>
      <guid>https://dev.to/orca_forge/why-uvx-mcp-servers-fail-with-pydantic-214-i77</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/pydantic-214-breaks-old-mcp-servers/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=pydantic-214-breaks-old-mcp-servers" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR: Older &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; servers load Pydantic's internal &lt;code&gt;eval_type_backport&lt;/code&gt; function.&lt;br&gt;
Pydantic 2.14 no longer has that function, so startup fails with an &lt;code&gt;ImportError&lt;/code&gt;.&lt;br&gt;
Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; to the applicable &lt;code&gt;uvx&lt;/code&gt; configuration, then check each server's &lt;code&gt;initialize&lt;/code&gt; response after reconnecting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;Old &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; MCP servers started with &lt;code&gt;uvx&lt;/code&gt; fail to launch due to &lt;code&gt;ImportError: cannot import name 'eval_type_backport'&lt;/code&gt; with Pydantic 2.14. Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; to pin Pydantic below 2.14, then reconnect and verify the per-server &lt;code&gt;initialize&lt;/code&gt; response.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Pydantic 2.14 Causes an ImportError
&lt;/h2&gt;

&lt;p&gt;The target server was pinned to &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;, which belongs to an older &lt;code&gt;mcp&lt;/code&gt; release line. The constraint &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; specifies using a version lower than 1.10. This version line imports an internal function called &lt;code&gt;eval_type_backport&lt;/code&gt; from Pydantic. However, that function was removed in Pydantic 2.14.&lt;/p&gt;

&lt;p&gt;As a result, the server fails with an &lt;code&gt;ImportError&lt;/code&gt; when attempting to import the missing function. Both the SQLite and Hacker News configurations where this error occurred were being launched via &lt;code&gt;uvx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The workaround is to pin Pydantic to below 2.14 in addition to the older &lt;code&gt;mcp&lt;/code&gt; version. Add the following flags to your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--with "mcp&amp;lt;1.10" --with "pydantic&amp;lt;2.14"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new addition here is &lt;code&gt;pydantic&amp;lt;2.14&lt;/code&gt;. The &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; constraint was already in place to maintain compatibility with older servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Pydantic &amp;lt;2.14 Specification to uvx Arguments
&lt;/h2&gt;

&lt;p&gt;If you're separating commands and arguments in configuration screens, add &lt;code&gt;--with&lt;/code&gt; and the package specification to the &lt;code&gt;uvx&lt;/code&gt; arguments. For a SQLite server, for example, it would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command: uvx
args: ["--with", "mcp&amp;lt;1.10", "--with", "pydantic&amp;lt;2.14", "mcp-server-sqlite", "--db-path", "&amp;lt;SQLite DB path&amp;gt;"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;SQLite DB path&amp;gt;&lt;/code&gt; with the location of your existing database. In the startup arguments, pass this value after &lt;code&gt;--db-path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Apply the same fix to the Hacker News server. The package name to start is &lt;code&gt;mcp-hn&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command: uvx
args: ["--with", "mcp&amp;lt;1.10", "--with", "pydantic&amp;lt;2.14", "mcp-hn"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;--with "mcp&amp;lt;1.10"&lt;/code&gt; is already present, leave that specification as is and add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt;. Review the entire configuration to ensure there are no duplicate Pydantic version pins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify MCP Server &lt;code&gt;initialize&lt;/code&gt; Response After Reconnection
&lt;/h2&gt;

&lt;p&gt;After saving your configuration, reconnect to the target MCP server. A simple "connected" status doesn't confirm that the initialization request (&lt;code&gt;initialize&lt;/code&gt;) was successful. You'll need to verify the response on the client side.&lt;/p&gt;

&lt;p&gt;Check SQLite and Hacker News separately. Even if one initializes successfully, it doesn't guarantee the other has as well. If errors disappear and both respond to initialization, you've avoided the startup &lt;code&gt;ImportError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to verify using only locally available dependencies, you can use &lt;code&gt;uvx --offline&lt;/code&gt;. Our records show that both SQLite and Hacker News respond to MCP initialization offline. However, this method may not work in environments without locally installed packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Existing Configurations to Add Pydantic Specifications To
&lt;/h2&gt;

&lt;p&gt;Some older configurations only pinned &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;. In those cases, add the Pydantic specification. However, for configurations where users have already specified their own Pydantic version, do not alter that value.&lt;/p&gt;

&lt;p&gt;The target for modification is configurations that start with &lt;code&gt;uvx&lt;/code&gt;, include &lt;code&gt;--with mcp&amp;lt;1.10&lt;/code&gt;, and do not yet have a Pydantic specification. Only add it to servers that meet these conditions. Do not mechanically apply the same change to configurations with different startup commands or those that already specify Pydantic.&lt;/p&gt;

&lt;p&gt;This error was related to compatibility between dependency packages rather than the server's startup command itself. When &lt;code&gt;eval_type_backport&lt;/code&gt; is not found, check if an older version of &lt;code&gt;mcp&lt;/code&gt; is being used. If so, pin Pydantic to below 2.14 and verify the &lt;code&gt;initialize&lt;/code&gt; response for each server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check that the affected server starts with &lt;code&gt;uvx&lt;/code&gt; and specifies &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; only when the configuration has no Pydantic version specified.&lt;/li&gt;
&lt;li&gt;Reconnect and check the &lt;code&gt;initialize&lt;/code&gt; response for SQLite and Hacker News separately.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;uvx --offline&lt;/code&gt; to check with local dependencies when available.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>uvx</category>
      <category>pydantic</category>
      <category>python</category>
    </item>
    <item>
      <title>Why Ollama Tool Calls Fail at a 4,096 Context Length</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:07:46 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-ollama-tool-calls-fail-at-a-4096-context-length-pe9</link>
      <guid>https://dev.to/orca_forge/why-ollama-tool-calls-fail-at-a-4096-context-length-pe9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/ollama-v1-context-4096/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=ollama-v1-context-4096" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR The app passed 29 tools and expected a context window of 16,384, but the model ran at 4,096. The OpenAI-compatible API path did not apply the expected window during model loading, and oversized input was truncated without a 400 response. Routing tool calls through &lt;code&gt;/api/chat&lt;/code&gt; and setting &lt;code&gt;options.num_ctx&lt;/code&gt; to 16,384 aligned the request path with the model context setting.&lt;/p&gt;

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

&lt;p&gt;In Ollama, the tool wasn't invoked, and the response was cut off mid-way. However, when using the OpenAI-compatible API (a connection method that sends requests in OpenAI's API format), the input was truncated from the beginning without returning a 400 error. The cause was that the window, which was expected to be 16,384, wasn't reflected when loading the model, and it was actually running with the default value of 4,096. I also sent the tool invocation to &lt;code&gt;/api/chat&lt;/code&gt; and verified by specifying &lt;code&gt;options.num_ctx&lt;/code&gt; (the context window setting value passed when loading the model) as 16,384.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Didn’t I Get a 400 Error and Couldn’t Pinpoint the Cause?
&lt;/h2&gt;

&lt;p&gt;Initially, I thought the tool descriptions were too long and assumed reducing their number would solve the issue. Previously, I fixed a case where a 400 error was returned when the context window was exceeded. From the error message, I remembered the window size and reduced the number of tools in the next request. Additionally, if the window was exceeded, I truncated the old results and resent them.&lt;/p&gt;

&lt;p&gt;With this mechanism, a 400 error could serve as a clue. However, in this case, no 400 error was returned, and even reducing the tools didn’t clarify what was happening. I couldn’t determine whether the model wasn’t calling the tools, the tool definitions weren’t reaching it, or the conversation history was being cut off midway—the response alone didn’t provide enough information.&lt;/p&gt;

&lt;p&gt;So, I suspected that the estimated window size might be too small and investigated how the app determined this value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the limit changed from 16,384 to 4,096
&lt;/h2&gt;

&lt;p&gt;In our initial window estimation, we compared the value obtained from the model information with 16,384 and used the smaller of the two. Since the model information specified 262,144, the app was expected to use 16,384. Here, tokens refer to the units the model uses to process text.&lt;/p&gt;

&lt;p&gt;However, when the model was actually loaded, the &lt;code&gt;context_length&lt;/code&gt; was 4,096.&lt;/p&gt;

&lt;p&gt;This wasn't just a simple estimation error. In the OpenAI-compatible API &lt;code&gt;/v1/chat/completions&lt;/code&gt;, the expected window value was not being reflected in the model loading. The model was running at the default of 4,096.&lt;/p&gt;

&lt;p&gt;Furthermore, when the window limit was exceeded, Ollama did not return a 400 error; instead, it silently truncated the input from the beginning. In other words, the signal the system relied on—"seeing a 400 to remember the window size"—never arrived, and the process to filter down the tools never began.&lt;/p&gt;

&lt;p&gt;The mere fact that we passed 29 tools wasn't enough to realize the cause. The number of tools passed during transmission and what the model was actually able to read were two different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed the tool call destination
&lt;/h2&gt;

&lt;p&gt;Simply changing the window size on the application side wasn't enough; I had to rethink the entire path through which tool calls are sent.&lt;/p&gt;

&lt;p&gt;In this fix, tool calls are sent to &lt;code&gt;/api/chat&lt;/code&gt;. Along this route, I specify &lt;code&gt;options.num_ctx&lt;/code&gt; as 16,384 and pass the window value when the model is loaded.&lt;/p&gt;

&lt;p&gt;If I had only toggled the regular conversation and left the tool usage logic on the old route, the same issue would persist. Therefore, I moved the tool calls through &lt;code&gt;/api/chat&lt;/code&gt; as well, converting the arguments and history to match the expected format of the destination. I avoided specifying &lt;code&gt;tool_choice: required&lt;/code&gt;, as Ollama does not support it.&lt;/p&gt;

&lt;p&gt;With this, I synchronized the window estimation with the actual loading settings using the same path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it reach the end after specifying 16,384?
&lt;/h2&gt;

&lt;p&gt;In the post-fix verification, I was able to specify 16,384 for the context window, and the 400 errors were zero. I was also able to pass all 29 tools.&lt;/p&gt;

&lt;p&gt;The logs show that the process progressed from tool calling to searching and retrieval, through to the final response. Unlike before, I did not need to wait for errors before reducing the number of tools.&lt;/p&gt;

&lt;p&gt;What I confirmed here is that the window specification and tool transmission are working as intended. However, simply fixing the window size does not guarantee the model's response content or speed. Differences in responses will remain depending on the specific model.&lt;/p&gt;

&lt;p&gt;Initially, I was looking for the presence of the 400 error. However, in this path, a 400 is not generated even if the input is truncated. The absence of an error was not evidence that the entire input had arrived.&lt;/p&gt;

&lt;p&gt;When investigating issues with tool calls, in addition to estimating the window size, I verify the actual transmission path and the specified values. The value obtained from model information and the value used during loading are not necessarily the same. If the ability to specify the window varies by the destination, I align the path, including the number of tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the window value from model information.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;context_length&lt;/code&gt; when the model loads.&lt;/li&gt;
&lt;li&gt;Verify the tool call route and whether it sets the context window.&lt;/li&gt;
&lt;li&gt;Route tool calls through &lt;code&gt;/api/chat&lt;/code&gt; and set &lt;code&gt;options.num_ctx&lt;/code&gt; to 16,384.&lt;/li&gt;
&lt;li&gt;After the change, check the 400 count and the record from tool call through final response.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ollama</category>
      <category>openaiapi</category>
    </item>
    <item>
      <title>How to Check Your Mac for the LiteLLM 1.107.0 Compromise</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Wed, 21 Oct 2026 18:47:46 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-check-your-mac-for-the-litellm-11070-compromise-7if</link>
      <guid>https://dev.to/orca_forge/how-to-check-your-mac-for-the-litellm-11070-compromise-7if</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/litellm-1-107-0-compromise-check-mac/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=litellm-1-107-0-compromise-check-mac" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR: LiteLLM 1.107.0 distributed through PyPI was tampered with. On a Mac, check the installed version in each Python environment, then search for three files and indicators of infection. The article also explains why Spotlight can miss files and how to distinguish a malicious &lt;code&gt;sitecustomize.py&lt;/code&gt; from a legitimate one.&lt;/p&gt;

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

&lt;p&gt;The affected environments are those where the tampered version &lt;code&gt;1.107.0&lt;/code&gt; was installed. To verify this, check the installed version, three specific files, and the infection markers. If the compromised version was present, simply deleting the files and processes is not enough; you must also rotate any secret keys or tokens that were readable from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Version is Dangerous?
&lt;/h2&gt;

&lt;p&gt;The affected package is &lt;code&gt;litellm==1.107.0&lt;/code&gt; on PyPI. Both the sdist and wheels for Linux, macOS, and Windows were compromised. It was published around 20:39 JST on October 21 and removed from PyPI around 22:40 JST.&lt;/p&gt;

&lt;p&gt;After learning about the news, I separately checked my own gateway and an AI tool that depends on LiteLLM. First, I verified the version in each Python environment. Assuming the virtual environment path is &lt;code&gt;&amp;lt;venv&amp;gt;&lt;/code&gt;, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;venv&amp;gt;/bin/pip show litellm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the line starting with &lt;code&gt;Version:&lt;/code&gt;. If &lt;code&gt;pip show&lt;/code&gt; isn't available, the name of the &lt;code&gt;litellm-*.dist-info&lt;/code&gt; file in the environment's &lt;code&gt;site-packages&lt;/code&gt; directory can also provide a clue.&lt;/p&gt;

&lt;p&gt;In my gateway, I found &lt;code&gt;litellm-1.92.0.dist-info&lt;/code&gt;. I also checked another tool and uv's cache, both of which were at version 1.92.0. Once the version is confirmed, ensure that the startup script doesn't automatically update the package. My startup script only included logic to run LiteLLM in the virtual environment, with no installation or update processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Find the Three Files
&lt;/h2&gt;

&lt;p&gt;Malicious files are installed in Python's &lt;code&gt;site-packages&lt;/code&gt;. Their names are &lt;code&gt;cook.pth&lt;/code&gt;, &lt;code&gt;cook.py&lt;/code&gt;, and &lt;code&gt;sitecustomize.py&lt;/code&gt;. Since &lt;code&gt;.pth&lt;/code&gt; and &lt;code&gt;sitecustomize.py&lt;/code&gt; are loaded when Python starts, they may work even in processes that do not import LiteLLM.&lt;/p&gt;

&lt;p&gt;On a Mac, you can also try searching with Spotlight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdfind &lt;span class="nt"&gt;-name&lt;/span&gt; cook.pth
mdfind &lt;span class="nt"&gt;-name&lt;/span&gt; sitecustomize.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, even if nothing is displayed, it does not necessarily mean that the files are not present. This is because Spotlight (&lt;code&gt;mdfind&lt;/code&gt;) does not search hidden folders like &lt;code&gt;.venv&lt;/code&gt;, &lt;code&gt;~/.cache&lt;/code&gt;, or &lt;code&gt;~/.local&lt;/code&gt;. I also did not get any results with Spotlight. Next, I used &lt;code&gt;find&lt;/code&gt; to directly search my home directory, Python installation location, Homebrew location, and other places.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~ /opt/homebrew /usr/local /Library/Frameworks/Python.framework &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"cook.pth"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"cook.py"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"sitecustomize.py"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"litellm-*.dist-info"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-print&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searching a wide range of targets can take a long time. Excluding large folders like photos, videos, and iCloud using &lt;code&gt;-prune&lt;/code&gt; can speed up the search. Be sure to include the locations where you installed Python and the cache used by tools in your search targets. The same approach applies to Linux; just replace the Mac-specific search locations with the locations where you installed Python and tools in your Linux environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Look for If You Find sitecustomize.py
&lt;/h2&gt;

&lt;p&gt;Just seeing the filename &lt;code&gt;sitecustomize.py&lt;/code&gt; isn't enough to determine whether a machine is infected. I found five instances of it on my system, but they turned out to be part of legitimate tools and standard Python environments. Instead of relying on the filename alone, you need to check the file's hash and contents.&lt;/p&gt;

&lt;p&gt;Calculate the SHA-256 hash for each file using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 /path/to/sitecustomize.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the resulting value against the malicious file hashes published in SafeDep's analysis report. I won't list the hashes in this post, but the analysis report contains the full list of malicious hashes along with more details.&lt;/p&gt;

&lt;p&gt;Next, search the file's contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"remote_code_loader|PRIVCOLLECT|litellm&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;dev"&lt;/span&gt; /path/to/sitecustomize.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the five files I found matched the malicious hashes in the report, nor did they contain any of the indicator strings searched above. When inspecting these files, don't jump to conclusions based on the filename alone—always verify it alongside the hash and contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking for Marker Files and Processes
&lt;/h2&gt;

&lt;p&gt;Next, we confirm the signs of sent stolen data and the operation of resident processes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do not finish our judgment just because neither of them exists, and we also investigate the processes. The &lt;code&gt;sitecustomize.py&lt;/code&gt; at the time of infection creates a resident process that is separated from the parent process. One of the conditions for the marker is "the parent is a Python process with PID 1".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-axo&lt;/span&gt; pid,ppid,lstart,command | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$2==1'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we would like to note that even if the parent has PID 1, it does not necessarily mean it is suspicious. A process that is resident with launchd also has a parent with PID 1. Please check the command line in the output and confirm whether it is a process you started. In my case, the Python process with PID 1 as its parent was a gateway started by launchd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify Environment Variables and Communication as Well
&lt;/h2&gt;

&lt;p&gt;We also look for the environment variable &lt;code&gt;PRIVCOLLECT_ACTIVE=1&lt;/code&gt; as a landmark. Since macOS does not have &lt;code&gt;/proc&lt;/code&gt;, we check the environment variables of processes using &lt;code&gt;ps eww -ax&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps eww &lt;span class="nt"&gt;-ax&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"[P]RIVCOLLECT_ACTIVE=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I ran &lt;code&gt;ps eww -ax | grep -c PRIVCOLLECT_ACTIVE&lt;/code&gt; and was surprised to find the result was 2. This was because &lt;code&gt;grep&lt;/code&gt; itself and the line of the shell command that included the search command were being counted. When I re-counted, excluding the line of the search command, the result was 0. By writing the first character as &lt;code&gt;[P]&lt;/code&gt; as shown above, &lt;code&gt;grep&lt;/code&gt; itself does not match. Please check not only the count but also the contents of the displayed lines.&lt;/p&gt;

&lt;p&gt;If you want to investigate the communication destination, you can do so as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I confirmed that the only listener was &lt;code&gt;127.0.0.1:4000&lt;/code&gt;. We make a judgment based on the combined results of these verifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if you had version 1.107.0
&lt;/h2&gt;

&lt;p&gt;The malicious code steals SSH keys, cloud credentials, AI tool tokens, passwords and history stored in browsers, environment variables, and API keys located in the LiteLLM proxy database, sending them to the attacker's server. The domain mentioned in the analysis report is &lt;code&gt;www.litellm[.]dev&lt;/code&gt;. This is not the official LiteLLM domain.&lt;/p&gt;

&lt;p&gt;If you had version 1.107.0 installed, simply deleting the three files is insufficient. You must stop any resident processes, delete the marker file, and treat all secrets readable from that terminal as compromised. You will then need to regenerate your SSH keys, cloud keys, AI tool tokens, saved passwords, and API keys for the various services. For a detailed analysis and the hashes of the malicious files, please refer to the &lt;a href="https://safedep.io/litellm-pypi-compromise-credential-stealer-backdoor/" rel="noopener noreferrer"&gt;SafeDep analysis article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I did not find version 1.107.0 in the environments I investigated, nor could I confirm the presence of the three files or the marker. Since LiteLLM was installed across multiple environments, I did not stop after looking at just one virtual environment; I also investigated the tool's environments and caches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check each environment with &lt;code&gt;pip show litellm&lt;/code&gt; or look for &lt;code&gt;litellm-*.dist-info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;find&lt;/code&gt; to search for &lt;code&gt;cook.pth&lt;/code&gt;, &lt;code&gt;cook.py&lt;/code&gt;, and &lt;code&gt;sitecustomize.py&lt;/code&gt;; continue if Spotlight finds nothing.&lt;/li&gt;
&lt;li&gt;Compare &lt;code&gt;sitecustomize.py&lt;/code&gt; hashes and contents with the analysis article; do not judge by filename alone.&lt;/li&gt;
&lt;li&gt;Check the two indicator files, &lt;code&gt;PRIVCOLLECT_ACTIVE=1&lt;/code&gt;, and Python processes with parent PID 1; inspect the command line when PID 1 is the parent.&lt;/li&gt;
&lt;li&gt;If 1.107.0 was installed, stop the process and rotate keys or tokens for secrets the machine could read.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>litellm</category>
      <category>mac</category>
      <category>python</category>
    </item>
    <item>
      <title>Why codesign Fails After Copying a Mac App from a DMG</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:01:15 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-codesign-fails-after-copying-a-mac-app-from-a-dmg-2cgg</link>
      <guid>https://dev.to/orca_forge/why-codesign-fails-after-copying-a-mac-app-from-a-dmg-2cgg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/japanese-filename-breaks-mac-signature/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=japanese-filename-breaks-mac-signature" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR The app showed “is damaged and can’t be opened” after distribution. Four Japanese filenames containing dakuten changed from NFC to NFD after copying from the DMG. That made the copied names differ from the names sealed by the signature, so codesign verification failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this Mac app, after copying it from the DMG, four Japanese filenames containing voiced sound marks (&lt;em&gt;dakuten&lt;/em&gt;) were converted from NFC to NFD normalization, causing &lt;code&gt;codesign --verify --deep --strict&lt;/code&gt; to fail with &lt;code&gt;a sealed resource is missing or invalid&lt;/code&gt;. To resolve this, I bundled the Japanese-named files into a single ASCII-named file and updated the app to extract them using NFC filenames on the first launch. After this fix, I confirmed that code signature verification passes after copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I checked when I encountered a signature error
&lt;/h2&gt;

&lt;p&gt;First, I verified whether the signature was actually corrupted. I asked the user to run &lt;code&gt;codesign --verify --deep --strict&lt;/code&gt; on their device, which returned the error: &lt;code&gt;a sealed resource is missing or invalid&lt;/code&gt;. This indicated there was a discrepancy between the app contents and the signature, going beyond a simple UI warning.&lt;/p&gt;

&lt;p&gt;Checking with &lt;code&gt;xattr&lt;/code&gt;, I found that the app had the quarantine attribute—essentially, it was marked as being downloaded. In the signature verification results, the same filename appeared as both &lt;code&gt;file added&lt;/code&gt; and &lt;code&gt;file missing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At this point, I began to doubt my initial hypothesis. Previously, I assumed the app was being flagged as "broken" because of adhoc signing. However, that alone didn't explain why the same name was being reported as both added and missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Japanese Filenames Changed After Copying from a DMG
&lt;/h2&gt;

&lt;p&gt;I tested copying the app via a DMG on my local machine. That's when I noticed that the filenames' internal representation—the way the computer stores the characters—had changed.&lt;/p&gt;

&lt;p&gt;In Japanese, characters with &lt;em&gt;dakuten&lt;/em&gt; (voiced sound marks) look identical on screen, but have two different internal representations: NFC and NFD. Right when the app was built, the filenames were still in NFC, so &lt;code&gt;codesign --verify&lt;/code&gt; running immediately after the build succeeded.&lt;/p&gt;

&lt;p&gt;However, once the app was copied via the DMG, four filenames containing &lt;em&gt;dakuten&lt;/em&gt; were converted to NFD. Names without &lt;em&gt;dakuten&lt;/em&gt; remained in NFC. Even though they looked identical visually, the filenames sealed during signing no longer matched the filenames after copying, causing the signature verification to fail.&lt;/p&gt;

&lt;p&gt;The issue didn't surface during the build itself, but only after passing through the DMG. Because I was only checking right after the build, I had missed inspecting the exact step where the failure actually occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modified to Avoid Including Japanese Names in Distributables
&lt;/h2&gt;

&lt;p&gt;In this update, I've changed the approach to avoid including files with Japanese names directly in the app. Multiple files, along with their contents, are now consolidated into a single file with an ASCII name (alphanumeric characters). Upon the app's first launch, necessary files are extracted from this consolidated file.&lt;/p&gt;

&lt;p&gt;The extracted file names are normalized to NFC (Unicode Normalization Form C). Since file names created in the user's folder are not part of the signing process, Japanese names can be used. The goal is to avoid including Japanese names within the distributable package.&lt;/p&gt;

&lt;p&gt;When modifying the original files, the consolidated data is updated using a generation command. Tests are in place to verify that the consolidated content matches the original files, that names are NFC-compliant, and that the original file set is not included in the distribution configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  After Fixing, Did the Copied App Pass Signature Verification?
&lt;/h2&gt;

&lt;p&gt;I recreated the DMG for the fixed version, mounted it (opened and displayed its contents), and then copied the app. After that, running &lt;code&gt;codesign --verify --deep --strict&lt;/code&gt; passed this time. It was necessary to verify the signature after copying the app, not while it was still inside the DMG.&lt;/p&gt;

&lt;p&gt;On the other hand, the &lt;code&gt;spctl&lt;/code&gt; assessment remained rejected. Officially, this is due to the result of ad-hoc signing. Additionally, I haven’t yet confirmed how the warning displayed on the actual device changes after the fix.&lt;/p&gt;

&lt;p&gt;What I learned from this is that correctly signing the source and ensuring the distributed app remains in the same state are two different things. Even if the file names appear correct, the internal representation may not be the same. After creating the DMG, it’s essential to verify the signature of the copied app as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check quarantine attributes with &lt;code&gt;xattr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify the signature with &lt;code&gt;codesign --verify --deep --strict&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;a sealed resource is missing or invalid&lt;/code&gt; appears, compare the filenames reported as added and missing.&lt;/li&gt;
&lt;li&gt;Verify the app again after copying it from the DMG.&lt;/li&gt;
&lt;li&gt;For Japanese filenames, check the difference between NFC and NFD.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electron</category>
      <category>macos</category>
    </item>
    <item>
      <title>How to Fix WSL2 Connections to Windows VOICEVOX</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:05:31 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-fix-wsl2-connections-to-windows-voicevox-5dgm</link>
      <guid>https://dev.to/orca_forge/how-to-fix-wsl2-connections-to-windows-voicevox-5dgm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/wsl2-cannot-reach-windows-voicevox/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=wsl2-cannot-reach-windows-voicevox" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR WSL2's default NAT networking makes &lt;code&gt;127.0.0.1&lt;/code&gt; inside WSL point to WSL itself, not Windows. A Windows VOICEVOX engine listening on Windows &lt;code&gt;127.0.0.1:50021&lt;/code&gt; is therefore unreachable from a web app running in WSL. Try mirrored networking, expose the engine with &lt;code&gt;--host 0.0.0.0&lt;/code&gt; and set &lt;code&gt;VOICEVOX_URL&lt;/code&gt; to the Windows IP, or run the engine inside WSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasons why localhost is unreachable
&lt;/h2&gt;

&lt;p&gt;The connection destination of Voice Canvas is specified by the environment variable &lt;code&gt;VOICEVOX_URL&lt;/code&gt;. The default value is &lt;code&gt;http://127.0.0.1:50021&lt;/code&gt;. When running with Docker, &lt;code&gt;http://host.docker.internal:50021&lt;/code&gt; is used.&lt;/p&gt;

&lt;p&gt;Here, it's important to note that &lt;code&gt;127.0.0.1&lt;/code&gt; refers to the connecting side itself. The Windows version of VOICEVOX only listens on Windows' &lt;code&gt;127.0.0.1:50021&lt;/code&gt;. On the other hand, the app is running inside WSL.&lt;/p&gt;

&lt;p&gt;WSL's default network is NAT. Therefore, even if a web server inside WSL connects to &lt;code&gt;127.0.0.1:50021&lt;/code&gt;, the connection destination is WSL itself, not Windows. Even if VOICEVOX is started on Windows, it will not reach the listening destination.&lt;/p&gt;

&lt;p&gt;On Windows, Voice Canvas is run inside WSL2's Ubuntu. This is because the startup script is written in bash and the location of venv assumes a Linux/Mac format. The README notes that operation has been confirmed on Windows 11 and WSL2 (Ubuntu 22.04).&lt;/p&gt;

&lt;p&gt;There are mainly three ways to fix the connection destination issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 1: Setting WSL to Mirror Mode
&lt;/h2&gt;

&lt;p&gt;This is a method that can be tried without changing the code. Write the following settings to &lt;code&gt;%USERPROFILE%\.wslconfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[wsl2]&lt;/span&gt;
&lt;span class="py"&gt;networkingMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mirrored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, run the following in PowerShell and restart WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this method, you can continue to use the default connection destination of Voice Canvas, &lt;code&gt;http://127.0.0.1:50021&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a precaution to note. &lt;code&gt;wsl --shutdown&lt;/code&gt; will stop all work inside WSL. Please save any work you are doing in servers or terminals before running it.&lt;/p&gt;

&lt;p&gt;This method is suitable when you do not want to change the connection destination of the app. Since the procedure to stop and restart WSL is necessary, choose the timing carefully when you have ongoing work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 2: Start VOICEVOX in a Connectable State from WSL
&lt;/h2&gt;

&lt;p&gt;Start the Windows version of the VOICEVOX engine with &lt;code&gt;--host 0.0.0.0&lt;/code&gt;. Then, specify the Windows IP address in &lt;code&gt;VOICEVOX_URL&lt;/code&gt;. If using WSL's default gateway, you can find the IP address within WSL using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/default/ {print $3}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if the command returns &lt;code&gt;172.19.128.1&lt;/code&gt;, set the connection target as follows:&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;VOICEVOX_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://172.19.128.1:50021
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This IP is just an example. Be sure to verify the actual value by running the command within WSL. You may also need to allow the connection through the Windows firewall.&lt;/p&gt;

&lt;p&gt;This method is suitable when you want to explicitly specify the connection target without using mirror mode. You'll need to configure both the engine's startup method and the app's connection target, as well as verify the Windows IP address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 3: Run the engine inside WSL
&lt;/h2&gt;

&lt;p&gt;This method involves running the VOICEVOX engine inside WSL instead of Windows. You can either use the Linux version or run &lt;code&gt;voicevox/voicevox_engine&lt;/code&gt; using Docker.&lt;/p&gt;

&lt;p&gt;If both the app and the engine are within the same WSL environment, you can configure the connection to &lt;code&gt;127.0.0.1&lt;/code&gt;. If you are running Voice Canvas via Docker, use &lt;code&gt;http://host.docker.internal:50021&lt;/code&gt; as the connection address.&lt;/p&gt;

&lt;p&gt;If you prefer to use the Windows version of VOICEVOX as-is, please choose a different solution. However, if you are able to run the engine on the WSL side as well, this is a viable option to consider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providing WSL Guidance from the App
&lt;/h2&gt;

&lt;p&gt;When the connection cannot be established, displaying only "VOICEVOX not found" to the user makes it difficult to identify the cause. In Voice Canvas, the app determines whether it is running on WSL or not and provides guidance tailored to the environment.&lt;/p&gt;

&lt;p&gt;You can determine if it's WSL by checking if &lt;code&gt;microsoft&lt;/code&gt; is included in the Linux environment's &lt;code&gt;/proc/version&lt;/code&gt;. When the connection fails, a &lt;code&gt;503&lt;/code&gt; response with a WSL indicator is attached, and WSL-specific guidance is displayed on the screen.&lt;/p&gt;

&lt;p&gt;The type of failure is also distinguished. The location to check differs between when there is no response and when the connection is refused. By conveying the difference in errors to the user, it becomes easier to verify the connection destination or engine waiting settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another Pitfall: Node.js on the Windows Side
&lt;/h2&gt;

&lt;p&gt;Sometimes, the Windows-side Node.js can end up mixed into the WSL PATH. In environments where you use tools like nvm4w, running &lt;code&gt;node&lt;/code&gt; inside WSL might accidentally invoke the Windows version instead of the Linux one.&lt;/p&gt;

&lt;p&gt;If you're using Node.js 20 or later, install the Linux version inside WSL. Use the following command to check which executable is being used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the displayed path points to a location within WSL. If issues persist at app startup even after correcting VOICEVOX's connection target, also verify the Node.js execution environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing an Approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use mirror mode if you want to test without changing the connection target.&lt;/li&gt;
&lt;li&gt;If you continue using the Windows version of the engine and want to explicitly specify the connection target, set &lt;code&gt;--host 0.0.0.0&lt;/code&gt; and the IP address on the Windows side.&lt;/li&gt;
&lt;li&gt;If you can run the engine within WSL, use the Linux version or the Docker version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless of the method, first verify where the engine is listening for connections. The Voice Canvas README also lists the required packages for launching on WSL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git build-essential python3.10-venv python3.10-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python3.10-venv&lt;/code&gt; is used for virtual environments. &lt;code&gt;build-essential&lt;/code&gt; and &lt;code&gt;python3.10-dev&lt;/code&gt; are required to build &lt;code&gt;pyworld&lt;/code&gt;. Pre-built packages for &lt;code&gt;pyworld&lt;/code&gt; are not available for Python 3.10, so it must be built from source.&lt;/p&gt;

&lt;p&gt;If you plan to use a GPU, configuration is not limited to the WSL side. The GPU in WSL runs using the NVIDIA driver on the Windows side. Since torch 2.4.0 is built for CUDA 12.1, you need a driver version that supports CUDA 12.1 or higher. With an older driver, such as 522.06 (which supports up to CUDA 11.8), you will see the message "NVIDIA driver on your system is too old" and the system will fall back to CPU processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use mirrored networking when you want to keep the default &lt;code&gt;VOICEVOX_URL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For the Windows engine, start it with &lt;code&gt;--host 0.0.0.0&lt;/code&gt; and set &lt;code&gt;VOICEVOX_URL&lt;/code&gt; to the Windows IP found from WSL.&lt;/li&gt;
&lt;li&gt;If the engine can run in WSL, use the Linux or Docker version; the app and engine can then share &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;First check where the engine is listening. If startup problems remain, check which Node.js executable &lt;code&gt;which node&lt;/code&gt; selects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/voice-canvas-oss-release/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/voice-canvas-oss-release/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/designing-voice-with-semantic-axes/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/designing-voice-with-semantic-axes/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/maccotaro/voice-canvas" rel="noopener noreferrer"&gt;Voice Canvas&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wsl2</category>
      <category>windows</category>
      <category>voicevox</category>
    </item>
    <item>
      <title>Why reasoning_effort behaves differently across providers</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:03:04 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-reasoningeffort-behaves-differently-across-providers-29f4</link>
      <guid>https://dev.to/orca_forge/why-reasoningeffort-behaves-differently-across-providers-29f4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/openai-compatible-reasoning-effort/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=openai-compatible-reasoning-effort" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR The app uses OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoints across cloud, self-hosted vLLM, and a relay. Browser actions never ran because an error was classified as lack of tool support. The cause was provider-specific handling of &lt;code&gt;reasoning_effort&lt;/code&gt; and a classifier that ignored the error details.&lt;/p&gt;

&lt;h2&gt;
  
  
  ① The Cloud Rejected the Combination of Tools and Configuration
&lt;/h2&gt;

&lt;p&gt;The first rejection occurred when we sent the tools and &lt;code&gt;reasoning_effort&lt;/code&gt; simultaneously. The target endpoint was &lt;code&gt;gpt-5.6-luna&lt;/code&gt;. We received a 400 status code with the following message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Function tools with reasoning_effort are not supported for gpt-5.6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Upon seeing &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;not supported&lt;/code&gt; in the error body, the application concluded that "this model does not support tools." From then on, it excluded tools from requests entirely.&lt;/p&gt;

&lt;p&gt;This was the erroneous misjudgment introduced at the beginning. Even when asked to perform browser operations, no tools were provided, so not a single operation was executed. Although it might appear that the model was avoiding calling tools, there was simply no opportunity to call them in the first place.&lt;/p&gt;

&lt;p&gt;The error message explicitly provided a solution. To use tools, one should either use &lt;code&gt;/v1/responses&lt;/code&gt; or set &lt;code&gt;reasoning_effort&lt;/code&gt; to &lt;code&gt;'none'&lt;/code&gt;. The application, however, skipped reading that part and immediately made the decision to discard the tools.&lt;/p&gt;

&lt;p&gt;The fix was to read the resolution before treating the rejection as a permanent tool incompatibility. We reissued the request with &lt;code&gt;reasoning_effort: 'none'&lt;/code&gt; while keeping the tools attached. Essentially, for this model, using tools meant stopping the reasoning process.&lt;/p&gt;

&lt;p&gt;Speed-wise, this change had its appeal. Setting &lt;code&gt;reasoning_effort: 'none'&lt;/code&gt; reduced the latency from input to the start of the output from 4–5 seconds to just 2 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  ② Our in-house vLLM flat-out rejected the value
&lt;/h2&gt;

&lt;p&gt;When I applied the same configuration to the tool-calling path, our own vLLM instance returned a 400 error.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Input should be 'low', 'medium' or 'high'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The error type was &lt;code&gt;literal_error&lt;/code&gt;. It wasn't that the field was missing; rather, the endpoint simply did not accept the value &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you only test with plain conversation, you might stop right here. Since conversations speed up, it looks like things are working. It was only after testing the tool-calling path that we realized the endpoint would handle values differently.&lt;/p&gt;

&lt;p&gt;For this endpoint, we detect the value rejection and stop sending &lt;code&gt;reasoning_effort&lt;/code&gt; altogether. To disable thinking, we instead send &lt;code&gt;chat_template_kwargs: { enable_thinking: false }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are the measured results for comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method of disabling thinking&lt;/th&gt;
&lt;th&gt;Time taken&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do nothing&lt;/td&gt;
&lt;td&gt;87 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/no_think&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;154 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enable_thinking=false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;65 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reasoning_effort none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures were measured using a model on-device. The differences between methods were significant, meaning that just because configuration names are similar does not imply the behavior will be aligned.&lt;/p&gt;

&lt;p&gt;On top of that, the output fields for thinking don't match up either. The name used varies among three possibilities: &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;reasoning_content&lt;/code&gt;, and &lt;code&gt;thinking&lt;/code&gt;. The writing side was aware of all three, but the reading side was only looking for &lt;code&gt;reasoning&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;vLLM writes to &lt;code&gt;reasoning_content&lt;/code&gt;. The moment we switched to our own vLLM, the classifier flagged "empty response" five times in a row. The model was returning the thinking, but because the reader wasn't looking at the correct field, we couldn't even count the amount of thinking generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  ③ The relay server returned empty only when streaming
&lt;/h2&gt;

&lt;p&gt;The third point concerns the handling of sequential responses.&lt;/p&gt;

&lt;p&gt;There were some endpoints that ignored the &lt;code&gt;stream: true&lt;/code&gt; flag and returned standard JSON. The app discards any line that does not start with &lt;code&gt;data:&lt;/code&gt;. As a result, zero fragments were captured. Since &lt;code&gt;response.ok&lt;/code&gt; was true, no exception was thrown, and nothing appeared on the screen.&lt;/p&gt;

&lt;p&gt;I fixed this by checking the &lt;code&gt;content-type&lt;/code&gt; to determine whether the response is a streaming response or standard JSON.&lt;/p&gt;

&lt;p&gt;But that wasn't the end of it. One relay server endpoint would return a 200 OK in 94ms only when streaming was enabled, but the body was empty. It would reject non-streaming calls with a 400 error. In empty responses, there are no clues like an error message body.&lt;/p&gt;

&lt;p&gt;For this endpoint, I decided to treat an empty response as an exception. I configured it to retry once after disabling the "thinking" specification, and to stop sending requests after that. When I retried without the specification, the correct JSON was returned in 327ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  A table of measured behavior per endpoint
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint behavior&lt;/th&gt;
&lt;th&gt;Data sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard (OpenAI, etc.)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reasoning_effort: 'none'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;none&lt;/code&gt; not in the value list (vLLM, rejects with 400)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chat_template_kwargs: { enable_thinking: false }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Returns empty when streaming (relay server)&lt;/td&gt;
&lt;td&gt;Send nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of these were decided based on actual measurements. I don't ask the users. The app learns based on the response from the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Inflicted Regressions
&lt;/h2&gt;

&lt;p&gt;After implementing a fix to account for differences between endpoints, I introduced a couple of new issues myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regression 1: Overly Broad Handling of 400 Error Bodies
&lt;/h3&gt;

&lt;p&gt;The logic for detecting rejected values was also checking for the presence of the word &lt;code&gt;invalid&lt;/code&gt;. If a 400 error body contained &lt;code&gt;invalid_request_error&lt;/code&gt;, all errors were treated the same.&lt;/p&gt;

&lt;p&gt;As a result, the system incorrectly learned that &lt;code&gt;gpt-5.6-luna&lt;/code&gt; was an endpoint "missing the &lt;code&gt;reasoning_effort&lt;/code&gt; field". It stopped at the &lt;code&gt;none&lt;/code&gt; designation required in ①, causing the entire browser operation to fail.&lt;/p&gt;

&lt;p&gt;The root cause was that I had created custom error messages for testing. Since these differed slightly from actual rejections, the discrepancy in detection went unnoticed. I adjusted the tests to use recorded error bodies directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regression 2: Applying Local Settings to Cloud Endpoints
&lt;/h3&gt;

&lt;p&gt;In voice conversations, I had enabled a setting to automatically truncate thoughts. This setting was inadvertently applied to the official cloud endpoints as well, causing &lt;code&gt;none&lt;/code&gt; to be sent to OpenAI.&lt;/p&gt;

&lt;p&gt;Every response generation request timed out after 60 seconds, resulting in a 500 error. This occurred consistently across four attempts, despite OpenAI's status being "normal".&lt;/p&gt;

&lt;p&gt;The code originally noted that "cloud providers use the same keys for different purposes". I failed to consider endpoint-specific meanings and applied the setting globally. I corrected this by limiting the setting to local providers only.&lt;/p&gt;

&lt;p&gt;Additionally, there was a flaw in the retry mechanism for rejected fields. Each utterance allowed only one retry. If both &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;frequency_penalty&lt;/code&gt; were rejected, the first utterance would fail silently.&lt;/p&gt;

&lt;p&gt;I updated the logic to remove and retry rejected fields upon each rejection, up to 8 attempts. If a response with at least one character is received before a failure, no retry is attempted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read the full 400 response body before classifying the error.&lt;/li&gt;
&lt;li&gt;Match &lt;code&gt;reasoning_effort&lt;/code&gt; handling to each endpoint's observed behavior.&lt;/li&gt;
&lt;li&gt;For vLLM, use &lt;code&gt;chat_template_kwargs: { enable_thinking: false }&lt;/code&gt; when &lt;code&gt;none&lt;/code&gt; is rejected.&lt;/li&gt;
&lt;li&gt;If a relay returns an empty streaming response, retry without the reasoning setting and stop sending it to that endpoint.&lt;/li&gt;
&lt;li&gt;Use observed endpoint behavior in tests and provider-specific configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/jev-vs-local-models-routing-1000/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/jev-vs-local-models-routing-1000/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/h100-only-3b-for-conversation/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/h100-only-3b-for-conversation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/free-llm-api-rate-limits-fallback/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/free-llm-api-rate-limits-fallback/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>openai</category>
      <category>vllm</category>
      <category>api</category>
    </item>
    <item>
      <title>Why Electron Screen Recording Used 492% CPU</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sat, 17 Oct 2026 13:01:33 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-electron-screen-recording-used-492-cpu-m67</link>
      <guid>https://dev.to/orca_forge/why-electron-screen-recording-used-492-cpu-m67</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/screen-recording-cpu-492-percent/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=screen-recording-cpu-492-percent" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR The Electron streaming window reached 492% CPU while ffmpeg used 28%. The recording size was unset, so capture used the Retina display resolution, and MediaRecorder plus ffmpeg encoded the video twice. Passing size and frame rate limits and copying the recorded format reduced recording-side CPU usage to about one-thirtieth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause: Recording Size and Double Encoding
&lt;/h2&gt;

&lt;p&gt;There were two main causes.&lt;/p&gt;

&lt;p&gt;The first was that I hadn't specified the size for the screen recording. The code was &lt;code&gt;getDisplayMedia({ video: true })&lt;/code&gt;. Although the settings screen displayed 1280×720 at 30fps, those values were not actually being reflected in the recording.&lt;/p&gt;

&lt;p&gt;Without a specification, it was recording at the native Retina resolution. The screen size was something like 3456×2234. For someone like me who was estimating streaming load based on the settings values, this was a major blind spot. Even though I intended to record a small window, I was actually capturing a massive display.&lt;/p&gt;

&lt;p&gt;The second cause was that the video was being encoded twice. &lt;code&gt;MediaRecorder&lt;/code&gt; encoded the video to VP8 using the CPU, and then &lt;code&gt;ffmpeg&lt;/code&gt; converted that video into H.264. The device's VideoToolbox encoder was only being used on the ffmpeg side. The VP8 encoding for the recording remained stuck on the CPU.&lt;/p&gt;

&lt;p&gt;Even the recording phase, which happens before the streaming, involves heavy encoding work. By measuring the processes separately, I was able to identify exactly where the load was occurring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding the Recording Format First
&lt;/h2&gt;

&lt;p&gt;In the revision, we first passed the size and frame rate to &lt;code&gt;getDisplayMedia&lt;/code&gt; with &lt;code&gt;max&lt;/code&gt;. This informs the recording side of the upper limit.&lt;/p&gt;

&lt;p&gt;Next, when using MediaRecorder, we try H.264 first. If the device's encoder can record in a usable format, we don't need to re-create the video with ffmpeg. The actual recorded format was &lt;code&gt;video/mp4;codecs=avc1,mp4a.40.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In that case, ffmpeg only needs to re-wrap the video with &lt;code&gt;-c:v copy&lt;/code&gt;. Here, specifying &lt;code&gt;-b:v&lt;/code&gt; or &lt;code&gt;-r&lt;/code&gt; would result in re-creating the video after all. If we're just copying, there's no need for extra specifications.&lt;/p&gt;

&lt;p&gt;However, we can't know what format MediaRecorder can record in until we check the window side. So, we've changed it to launch ffmpeg after receiving the format notification from the window.&lt;/p&gt;

&lt;p&gt;When changing the order, we first investigate the reason why it can be stopped. If it's due to ffmpeg being unavailable or a key being missing, we can inform the user before opening the window. If we open the window first and then display "cannot send", it will appear to the user as if the stream is being transmitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recording side is about 1/30
&lt;/h2&gt;

&lt;p&gt;After modification, I measured it 4 times at 20-second intervals on the same Mac. The CPU usage rate on the recording side was 14.8%, 16.3%, 19.6%, and 19.2%. ffmpeg was at 0.1%. The load average was 3.28, 3.48, 3.27, and 4.16.&lt;/p&gt;

&lt;p&gt;The load on the recording side has become about 1/30. The load average of 3 to 4 includes the load of the application itself, such as LLM and embedding. The load of the distribution itself is almost negligible.&lt;/p&gt;

&lt;p&gt;Let me note one thing. The modified episode was after the distribution slot of the receiver ended, so I haven't reconfirmed the reach to YouTube. The reach was confirmed in the episode before the modification. At that time, I distributed for 9 minutes and 30 seconds, the image quality was "very good" (非常に良い), and the bitrate was 3064 kbps. The fact that the load decreased after modification and the fact that the reach after modification was confirmed are separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Load is Too High, the App Automatically Stops Streaming
&lt;/h2&gt;

&lt;p&gt;Even if the load is reduced, operational concerns remain. The biggest issue this time was that users couldn't stop the stream. I was able to stop it through another method, but users didn't have the same option.&lt;/p&gt;

&lt;p&gt;To address this, I implemented a mechanism where the app itself stops streaming when the load is too high. During streaming, the app checks the average load over the past minute every 10 seconds. If the load exceeds the number of CPUs × 2.5 for three consecutive checks (approximately 30 seconds), the app automatically stops the stream.&lt;/p&gt;

&lt;p&gt;It won't stop if the load spikes momentarily. If the load drops even once, the counter resets. When stopping, the app also displays the reason and how to fix it. For example, it might suggest reducing the recording resolution.&lt;/p&gt;

&lt;p&gt;By the time the app becomes too slow for users to operate, asking them to stop it manually is already too late. Detecting anomalies and stopping the stream should be part of the streaming feature's design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set size and frame rate limits in &lt;code&gt;getDisplayMedia&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the format actually produced by MediaRecorder before starting ffmpeg.&lt;/li&gt;
&lt;li&gt;When the format can be copied, use &lt;code&gt;-c:v copy&lt;/code&gt; and avoid settings that re-encode the video.&lt;/li&gt;
&lt;li&gt;Monitor load during streaming and stop automatically if high load persists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/av-sync-and-mediarecorder-pitfalls/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/av-sync-and-mediarecorder-pitfalls/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/automate-youtube-twitch-live-streaming/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/automate-youtube-twitch-live-streaming/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/headless-chromium-real-gpu-rendering/" rel="noopener noreferrer"&gt;https://forge.workstyle.tech/blog/headless-chromium-real-gpu-rendering/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electron</category>
      <category>mediarecorder</category>
      <category>ffmpeg</category>
    </item>
  </channel>
</rss>
