<?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: WgeorgeAssistantIA</title>
    <description>The latest articles on DEV Community by WgeorgeAssistantIA (@wgeorgeassistantia).</description>
    <link>https://dev.to/wgeorgeassistantia</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%2F3988867%2Fb500a859-a1d4-4ff7-a1d4-f187a1632580.png</url>
      <title>DEV Community: WgeorgeAssistantIA</title>
      <link>https://dev.to/wgeorgeassistantia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wgeorgeassistantia"/>
    <language>en</language>
    <item>
      <title>How to Reduce AI Hallucinations When Extracting Information from Videos</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Thu, 05 Nov 2026 12:17:09 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/how-to-reduce-ai-hallucinations-when-extracting-information-from-videos-23kc</link>
      <guid>https://dev.to/wgeorgeassistantia/how-to-reduce-ai-hallucinations-when-extracting-information-from-videos-23kc</guid>
      <description>&lt;p&gt;When a language model reads a transcript and returns a list of tools, two kinds of mistakes appear. The first is invention: a tool that was never mentioned. The second is subtler: a real tool paired with the wrong link or the wrong meaning. Both erode trust. This post lists the techniques that helped us reduce them, and the numbers we saw, with the caveat that our samples are small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: tools that were never said
&lt;/h2&gt;

&lt;p&gt;The cure is to make the model show its evidence and to check it with code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require a &lt;strong&gt;verbatim quote&lt;/strong&gt; for every extracted item&lt;/li&gt;
&lt;li&gt;Check that the tool's name appears in the transcript, and drop the item if it does not&lt;/li&gt;
&lt;li&gt;Check the quote and the timestamp against the transcript too, and remove whichever cannot be found, so nothing unverified is shown as evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A caution: a naive check of the form "the tool name must appear in the transcript" can reject real tools when captions split or respell them (a product written as FastAPI may appear as "fast API"). Match names with some tolerance, and log rejections so you can see whether you are throwing away good results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: real tools, wrong meaning
&lt;/h2&gt;

&lt;p&gt;This one surprised us. In one test run on twenty technical videos, a large share of the official links our resolver returned were wrong. A database called Neon led to a cinema studio. An assistant called Serena led to a tennis player's website. Codex led to a food-safety standard. The model had named the tools correctly; the failure was in resolving ambiguous names with a web search.&lt;/p&gt;

&lt;p&gt;What fixed most of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add context to the query&lt;/strong&gt;: include the category and a short description of the tool, not just its name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept canonical repositories&lt;/strong&gt;: many CLI and open-source tools have no website, only a GitHub repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block unreliable sources&lt;/strong&gt; as official: aggregators, course platforms, forums, social networks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score and threshold&lt;/strong&gt;: compute a confidence for each match, and reject anything below a threshold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return nothing when unsure&lt;/strong&gt;: an empty field is honest, a wrong link is not&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Improve the prompt, then measure
&lt;/h2&gt;

&lt;p&gt;Prompts matter. We rewrote ours with an explicit definition of what counts as a tool, a list of exclusions and an instruction to quote verbatim. On four videos of very different lengths, from 47 minutes to over 7 hours, and labelling the results by hand, precision moved from about 89 to about 96 percent and recall from about 76 to about 93 percent. Those are indicative figures: four videos, one run each, a model with some randomness. Treat them as direction, not proof.&lt;/p&gt;

&lt;p&gt;The point is the method: label a small set by hand once, then re-run it after every prompt change, so you notice regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't hide partial results
&lt;/h2&gt;

&lt;p&gt;A different kind of hallucination is silent partial coverage. If the transcript is too long and you truncate it, the output looks complete but only reflects the beginning. We hit this: a seven-hour video was analysed on its first twelve minutes. The fix was to process the whole transcript in chunks and to report coverage explicitly, with a warning when an analysis is partial.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Name found in the transcript, quote required, quote verified&lt;/li&gt;
&lt;li&gt;Context in link resolution&lt;/li&gt;
&lt;li&gt;Confidence threshold, with &lt;code&gt;None&lt;/code&gt; as a valid answer&lt;/li&gt;
&lt;li&gt;A small labelled set to catch regressions&lt;/li&gt;
&lt;li&gt;Honest coverage reporting&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;We applied these rules in &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;VidScope&lt;/a&gt;, which lists the tools mentioned in a YouTube video with timestamps and official links, and prefers no link to an uncertain one.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can hallucinations be eliminated?&lt;/strong&gt;&lt;br&gt;
No, but requiring and verifying evidence reduces them a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why return nothing instead of a best guess?&lt;/strong&gt;&lt;br&gt;
A wrong link looks authoritative; an empty field tells the user to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I measure accuracy on a small project?&lt;/strong&gt;&lt;br&gt;
Label a few videos by hand and re-run them after every change, while remembering the sample is small.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Audio Crossfading in Python: Eliminating Pops and Clicks at Edit Points</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Mon, 02 Nov 2026 12:15:45 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/audio-crossfading-in-python-eliminating-pops-and-clicks-at-edit-points-2b33</link>
      <guid>https://dev.to/wgeorgeassistantia/audio-crossfading-in-python-eliminating-pops-and-clicks-at-edit-points-2b33</guid>
      <description>&lt;p&gt;When you automatically remove silences from speech or podcast recordings, you cut hundreds of audio slices and stitch them back together.&lt;/p&gt;

&lt;p&gt;If you do this naively by simply concatenating raw audio arrays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Naive concatenation: guaranteed audio artifacts
&lt;/span&gt;&lt;span class="n"&gt;clean_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;segment_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment_b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your output will be riddled with audible &lt;strong&gt;clicks, pops, and ticks&lt;/strong&gt;. Every single edit point sounds like a tiny vinyl scratch.&lt;/p&gt;

&lt;p&gt;Here is why naive concatenation fails, what a linear crossfade fixes (and what it doesn't), how an &lt;strong&gt;equal-power crossfade&lt;/strong&gt; works in NumPy, and what we actually ship in &lt;a href="https://voxcutpro.com" rel="noopener noreferrer"&gt;VoxCut&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Edit Points Pop: Amplitude Discontinuities
&lt;/h2&gt;

&lt;p&gt;Digital audio is a sequence of samples (e.g. 44,100 per second), each one an amplitude between -1.0 and +1.0.&lt;/p&gt;

&lt;p&gt;When you cut audio, you almost never cut at an exact &lt;strong&gt;zero-crossing&lt;/strong&gt; (a sample where the amplitude is 0.0):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Segment A&lt;/strong&gt; ends at an amplitude of &lt;code&gt;+0.42&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment B&lt;/strong&gt; begins at an amplitude of &lt;code&gt;-0.31&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Concatenating them creates a jump of &lt;code&gt;0.73&lt;/code&gt; between two consecutive samples (1/44100th of a second apart).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A step like that has energy spread across the whole audible spectrum. The speaker cone is asked to move almost instantly, and you hear it as a &lt;strong&gt;click&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Linear Crossfades: The Midpoint Dip
&lt;/h2&gt;

&lt;p&gt;The first instinct is to overlap the two segments for a few milliseconds and apply a linear crossfade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Segment A fades out linearly from 1.0 to 0.0&lt;/li&gt;
&lt;li&gt;Segment B fades in linearly from 0.0 to 1.0
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Linear crossfade weights
&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fade_samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fade_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="n"&gt;fade_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This kills the click: there is no longer a step anywhere. But for two &lt;strong&gt;uncorrelated&lt;/strong&gt; signals (two different moments of speech), what adds up is power, not amplitude:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Power&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;At the exact midpoint of a linear crossfade (t = 0.5):&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Power&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0.5&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0.5&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.50&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mord"&gt;3&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;dB&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;On long fades between two loud signals, that dip can be heard as a small "breathing" in the volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Theory Fix: Constant-Power (Sine/Cosine) Crossfade
&lt;/h2&gt;

&lt;p&gt;To keep the energy constant across uncorrelated material, use trigonometric curves:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Fade&amp;nbsp;Out&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size2"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;π&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size2"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Fade&amp;nbsp;In&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;sin&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size2"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;π&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size2"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Because 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;&lt;span class="mop"&gt;sin&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
, the summed power stays at 1.0 for every sample of the transition.&lt;/p&gt;


&lt;h2&gt;
  
  
  A NumPy Implementation
&lt;/h2&gt;

&lt;p&gt;Here is a self-contained function that splices two buffers with an equal-power crossfade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;equal_power_crossfade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg_b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fade_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Crossfades the end of seg_a into the beginning of seg_b using constant-power curves.
    Supports 1D (mono) and 2D (stereo: samples, channels) arrays.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;fade_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;fade_ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# No room for a fade (or fade disabled): plain splice.
&lt;/span&gt;    &lt;span class="c1"&gt;# Note: seg_a[:-0] would return an EMPTY array, hence the fade_len &amp;lt;= 0 check.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fade_len&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;fade_len&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg_b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Normalized time vector [0.0, 1.0]
&lt;/span&gt;    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Constant-power weights
&lt;/span&gt;    &lt;span class="n"&gt;w_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndim&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Broadcast over stereo channels
&lt;/span&gt;        &lt;span class="n"&gt;w_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w_out&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newaxis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;w_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w_in&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newaxis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Overlapping region
&lt;/span&gt;    &lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;w_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg_b&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;w_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Assemble: A (prefix) + overlap + B (suffix)
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="n"&gt;seg_a&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;seg_b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fade_len&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Swap the two weight lines for &lt;code&gt;w_out = 1.0 - t&lt;/code&gt; and &lt;code&gt;w_in = t&lt;/code&gt; and you have the linear version.&lt;/p&gt;


&lt;h2&gt;
  
  
  What VoxCut Actually Ships (and Why It's Linear)
&lt;/h2&gt;

&lt;p&gt;Honest disclosure: VoxCut's desktop export does &lt;strong&gt;not&lt;/strong&gt; use the equal-power curve above. It uses a &lt;strong&gt;40 ms linear crossfade&lt;/strong&gt; (the equivalent of FFmpeg's &lt;code&gt;acrossfade=c1=tri:c2=tri&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Why that's good enough for silence removal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The edges we join are the &lt;strong&gt;quiet tails&lt;/strong&gt; of speech around a removed pause, not two loud musical passages. A -3 dB dip at the midpoint of a 40 ms fade between two near-silent edges is inaudible in practice.&lt;/li&gt;
&lt;li&gt;What matters is removing the step, and a linear ramp does that just as well as a sine curve.&lt;/li&gt;
&lt;li&gt;If any kept segment is shorter than two fade lengths (80 ms), the export falls back to plain cuts rather than fading into a sliver of audio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you crossfade &lt;strong&gt;music&lt;/strong&gt;, or long fades between two loud signals, equal-power is the safer default.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Performance Lesson
&lt;/h3&gt;

&lt;p&gt;Our first version built an FFmpeg &lt;code&gt;filter_complex&lt;/code&gt; graph: one &lt;code&gt;atrim&lt;/code&gt; per segment plus a chain of N &lt;code&gt;acrossfade&lt;/code&gt; filters. On a dense podcast (~870 cuts in 2 hours), that graph became the bottleneck: FFmpeg re-read the input for every segment.&lt;/p&gt;

&lt;p&gt;The current version does it in a single pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One FFmpeg process decodes the file to raw &lt;code&gt;f32le&lt;/code&gt; PCM on &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Python reads it in 1-second chunks, skips the silent ranges, and applies the fade ramps in NumPy.&lt;/li&gt;
&lt;li&gt;A second FFmpeg process encodes from &lt;code&gt;stdin&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a 30-minute podcast, this was about &lt;strong&gt;50x faster&lt;/strong&gt; than the filter graph in our benchmark. The old graph is still there as a fallback if the streaming path fails.&lt;/p&gt;


&lt;h2&gt;
  
  
  Critical Gotcha: The Cumulative Length Shrink
&lt;/h2&gt;

&lt;p&gt;Each overlap crossfade consumes &lt;code&gt;fade_len&lt;/code&gt; samples, because the two segments overlap in time.&lt;/p&gt;

&lt;p&gt;If your silence detector finds &lt;strong&gt;600 cuts&lt;/strong&gt; in a 45-minute episode and you apply a 40 ms crossfade at each join:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;40&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;ms&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;seconds&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;For an audio-only podcast, nobody cares. But if that audio has to stay in sync with &lt;strong&gt;video&lt;/strong&gt;, you are 24 seconds out of sync by the end. (That's why VoxCut skips the overlap crossfade entirely on video exports.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Video-Safe Rule
&lt;/h3&gt;

&lt;p&gt;When timeline sync matters, apply micro-fades at the boundaries without overlapping:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fade out the last ~5 ms of segment A to zero.&lt;/li&gt;
&lt;li&gt;Fade in the first ~5 ms of segment B from zero.&lt;/li&gt;
&lt;li&gt;Concatenate without overlapping time indices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The step disappears (both sides meet at zero), and in speech a 5 ms dip is very hard to notice.&lt;/p&gt;




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

&lt;p&gt;Clicks at edit points are not caused by bad microphones. They come from cutting waveforms that are not at zero.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never splice raw arrays at arbitrary indices.&lt;/li&gt;
&lt;li&gt;A short crossfade removes the click; linear is fine for speech edges, equal-power is better for music and long fades.&lt;/li&gt;
&lt;li&gt;Use non-overlapping micro-fades when you must keep the timeline in sync with video.&lt;/li&gt;
&lt;li&gt;If you have hundreds of cuts, avoid giant FFmpeg filter graphs: stream PCM through Python instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the pipeline behind &lt;a href="https://voxcutpro.com" rel="noopener noreferrer"&gt;VoxCut&lt;/a&gt;'s "smooth transitions" option. If you're building your own silence remover, I'd love to hear how you handle edit points.&lt;/p&gt;

</description>
      <category>audio</category>
      <category>python</category>
      <category>ffmpeg</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From YouTube Transcript to Structured Technical Knowledge</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:56:02 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/from-youtube-transcript-to-structured-technical-knowledge-12aj</link>
      <guid>https://dev.to/wgeorgeassistantia/from-youtube-transcript-to-structured-technical-knowledge-12aj</guid>
      <description>&lt;p&gt;A transcript is a wall of text. A knowledge base is a set of records you can query. This post is about the part in between: designing the data model that turns one into the other, so the result is searchable, checkable and mergeable across videos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the questions you want to answer
&lt;/h2&gt;

&lt;p&gt;Before choosing a schema, list the questions a user will ask later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tools were mentioned in this video?&lt;/li&gt;
&lt;li&gt;Where exactly, so I can listen to it?&lt;/li&gt;
&lt;li&gt;Was it recommended, compared or criticised?&lt;/li&gt;
&lt;li&gt;What is the official source?&lt;/li&gt;
&lt;li&gt;Which videos mention the same tool?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each question maps to a field. If a field has no question behind it, leave it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal record
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"supabase"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backend and authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mention"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"used"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"we use Supabase for auth and the database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;761&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"official_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://supabase.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design choices are worth explaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store seconds, not strings.&lt;/strong&gt; Keep the timestamp as a number of seconds. Format it for display later (&lt;code&gt;00:12:41&lt;/code&gt;) and build deep links from it (&lt;code&gt;?t=761&lt;/code&gt;). Strings like "12:41" break as soon as a video passes one hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the quote.&lt;/strong&gt; The quote is your audit trail. It lets a human confirm the claim in five seconds and lets your code reject records whose quote is not actually in the transcript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;aliases&lt;/code&gt;.&lt;/strong&gt; Videos say "Postgres", "PostgreSQL" and "Postgres DB". Normalise to one canonical name and keep the variants for matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging chunks without duplicating
&lt;/h2&gt;

&lt;p&gt;Long transcripts are processed in chunks, so the same tool appears several times. Merge on a normalised key: lower-case the name, strip punctuation and common suffixes, then group. When merging, keep all evidence entries and the earliest start time, and take the most specific category. Rare conflicts, such as two categories for the same name, are worth logging instead of resolving silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating before you trust
&lt;/h2&gt;

&lt;p&gt;Run a few cheap checks on every record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The timestamp falls inside the video's duration&lt;/li&gt;
&lt;li&gt;The quote appears in the transcript near that timestamp&lt;/li&gt;
&lt;li&gt;The name is not on a block list of generic words ("API", "cloud", a channel's own name)&lt;/li&gt;
&lt;li&gt;The URL, if present, passes the same validation as the name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Records that fail go to a rejected list with the reason. Looking at the rejected list is the fastest way to improve your prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it usable
&lt;/h2&gt;

&lt;p&gt;Once records are clean, the value comes from presentation. Sort by first appearance so the list follows the video. Make every timestamp a link. Group by category. Offer an export, because people will paste the result into their own notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where VidScope fits
&lt;/h2&gt;

&lt;p&gt;Everything above is what we ended up building in &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;VidScope&lt;/a&gt;, an experiment in turning technical YouTube videos into structured, timestamped lists of tools. The schema above is simplified, but the principles are the same: seconds not strings, quotes as evidence, and validation before display.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why store timestamps as seconds?&lt;/strong&gt;&lt;br&gt;
They are unambiguous, sortable and easy to turn into deep links and display formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I deduplicate tools across chunks?&lt;/strong&gt;&lt;br&gt;
Normalise names (case, punctuation, suffixes) and merge records while keeping all evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why keep the quote?&lt;/strong&gt;&lt;br&gt;
It is the proof: it lets a human check the claim quickly and lets code reject records not present in the transcript.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Committed Memory Above RAM Doesn't Mean Your PC Is Swapping</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:37:47 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/committed-memory-above-ram-doesnt-mean-your-pc-is-swapping-1opm</link>
      <guid>https://dev.to/wgeorgeassistantia/committed-memory-above-ram-doesnt-mean-your-pc-is-swapping-1opm</guid>
      <description>&lt;p&gt;One rule in my Windows diagnostic tool said: &lt;em&gt;"Your PC is compensating for missing RAM by using the disk."&lt;/em&gt; The condition behind it was one line: committed memory is greater than physical RAM.&lt;/p&gt;

&lt;p&gt;On my own machine, that line was true. And my PC was fine.&lt;/p&gt;

&lt;p&gt;This post explains why "commit &amp;gt; RAM" proves nothing, and what I measure instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first version did
&lt;/h2&gt;

&lt;p&gt;The rule read two numbers and compared them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;committed memory (what Windows has promised to processes)&lt;/li&gt;
&lt;li&gt;total physical RAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the first was larger than the second, it flagged "swap in use" and suggested more RAM or fewer programs.&lt;/p&gt;

&lt;p&gt;It sounded reasonable. It was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why commit is the wrong signal
&lt;/h2&gt;

&lt;p&gt;Committed memory is memory that has been &lt;strong&gt;reserved&lt;/strong&gt;, not memory that is being &lt;strong&gt;used&lt;/strong&gt;. A process can reserve a lot and touch very little of it. Browsers are the typical example: they reserve large regions up front.&lt;/p&gt;

&lt;p&gt;So commit can exceed RAM on a perfectly healthy machine, with no page ever read back from the disk.&lt;/p&gt;

&lt;p&gt;There is a second reason. Commit is charged against the &lt;strong&gt;commit limit&lt;/strong&gt;, which is RAM plus the pagefile. A machine with a large pagefile can commit far more than its RAM by design, so &lt;code&gt;commit &amp;gt; RAM&lt;/code&gt; was never a pressure signal on its own. (Thanks to Kai for pointing this out in the comments.) In the rule below it is only a precondition: the other two conditions do the real work.&lt;/p&gt;

&lt;p&gt;Here is what my own PC showed on September 24:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Committed memory: &lt;strong&gt;22.5 GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Physical RAM: &lt;strong&gt;15.9 GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Available RAM: &lt;strong&gt;24 %&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Pages read from disk to serve memory: &lt;strong&gt;0.4 per second&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commit was well above RAM. The disk was almost idle. The rule fired anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shows a PC is compensating on disk
&lt;/h2&gt;

&lt;p&gt;The real signal is the &lt;strong&gt;hard page fault rate&lt;/strong&gt;: pages that Windows has to read from disk because the data it needs is no longer in RAM. The counter is &lt;code&gt;Pages Input/sec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I read it twice, at the start and at the end of my measurement window, and divide the difference by the elapsed time. It is a raw cumulative counter, so one reading alone tells you nothing:&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="nv"&gt;$m1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_PerfRawData_PerfOS_Memory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$m2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_PerfRawData_PerfOS_Memory&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timestamp_Sys100NS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$m1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timestamp_Sys100NS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1e7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$pagesPerSec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PagesInputPersec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$m1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PagesInputPersec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail that cost me time: I use the WMI class, not &lt;code&gt;Get-Counter&lt;/code&gt;. Counter names are translated according to the language of Windows, so a script that works on an English install can fail silently on a French one. The WMI class property names stay the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two thresholds, and why there are two
&lt;/h2&gt;

&lt;p&gt;The new condition needs three things together:&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="nv"&gt;$SEUIL_PAGES_DISQUE&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# pages/s read from disk for memory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SEUIL_RAM_DISPO_SWAP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# % of RAM available&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$commitGb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ramTotalGb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$pctRamAvailable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SEUIL_RAM_DISPO_SWAP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pagesPerSec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pagesPerSec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SEUIL_PAGES_DISQUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# flag it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100 pages per second is roughly 400 KB/s of reads, at 4 KB per page. That is a level where the disk is clearly doing the memory's job.&lt;/p&gt;

&lt;p&gt;Why not use the page counter alone? Because &lt;code&gt;Pages Input/sec&lt;/code&gt; also counts programs being &lt;strong&gt;loaded&lt;/strong&gt; from disk. If the user opens an application during the measurement, the counter jumps even though RAM is fine. Requiring low available RAM as well keeps those reads from being blamed on missing memory.&lt;/p&gt;

&lt;p&gt;If the counter cannot be read, the rule says nothing. A false "your PC is compensating" is worse than silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;I ran the real analysis on my machine: the only thing still flagged was a full disk, in 15 seconds. I also have a simulated test bench, where heavy paging triggers the rule. I did not build a separate case for "high commit, idle disk": my own PC on September 24 was that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not proved
&lt;/h2&gt;

&lt;p&gt;The thresholds come from one machine, mine, and from reasoning about the counters. I do not know yet how 100 pages/s behaves across very different hardware. That rate is about 400 KB/s: close to free on an NVMe SSD, where a 4 KB random read takes tens of microseconds, but potentially dominant on a spinning disk, where each fault can cost a seek. A raw count only means something once it is weighted by device latency. The next version should read the disk's read latency (or read queue length) next to the fault rate, and flag a problem when the added time is visible, not when a count crosses a fixed number. A field test on about ten PCs is still the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;This is part of &lt;a href="https://nyctale.fr" rel="noopener noreferrer"&gt;Nyctale&lt;/a&gt;, a Windows PC diagnostic tool that tries to name the real cause of a slow PC and says so when no software will fix it. This correction shipped in version 1.0.3. The quick diagnostic is free.&lt;/p&gt;

&lt;p&gt;If you monitor memory on Windows: which counter do you trust to tell a slow PC from a PC that just reserves a lot?&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>windows</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Four Bugs I Only Found by Driving the Real Window of a PySide6 App</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:28:12 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/four-bugs-i-only-found-by-driving-the-real-window-of-a-pyside6-app-138m</link>
      <guid>https://dev.to/wgeorgeassistantia/four-bugs-i-only-found-by-driving-the-real-window-of-a-pyside6-app-138m</guid>
      <description>&lt;p&gt;My app is a desktop tool that traces images into SVG. The tracing engine is vtracer, a Rust extension, and the UI is PySide6. The engine had good unit tests. Then I used the app for real, and four bugs showed up that none of those tests could see.&lt;/p&gt;

&lt;p&gt;All four come from the same place: the interaction between Qt, threads and a native extension. If you build a PySide6 or PyQt app that runs work in a &lt;code&gt;QThread&lt;/code&gt;, you may hit the same ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the app died with no exception
&lt;/h2&gt;

&lt;p&gt;While a trace was running, the user could paint another stroke. The handler for the mask-changed signal put the selection back with &lt;code&gt;canvas.set_mask(...)&lt;/code&gt;. But &lt;code&gt;set_mask&lt;/code&gt; emits the same signal again, so the handler called itself.&lt;/p&gt;

&lt;p&gt;That is infinite recursion, but not the kind Python complains about. It went through Qt's C++ signal machinery, so Python's recursion limit never triggered. The process simply died with a native stack overflow (&lt;code&gt;0xC00000FD&lt;/code&gt; on Windows). No traceback, no message.&lt;/p&gt;

&lt;p&gt;The rule I took from it: &lt;strong&gt;never react to a Qt signal by modifying the thing that emitted it.&lt;/strong&gt; A guard flag fixes it:&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_suspend_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear_mask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# emits mask_changed
&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_suspend_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_mask_changed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_suspend_mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug 2: the UI froze forever after a Rust panic
&lt;/h2&gt;

&lt;p&gt;The worker looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QThread&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_fn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a corrupt image, vtracer panicked. pyo3 turns a Rust panic into a Python &lt;code&gt;PanicException&lt;/code&gt;, and that class derives from &lt;code&gt;BaseException&lt;/code&gt;, not &lt;code&gt;Exception&lt;/code&gt;. My &lt;code&gt;except&lt;/code&gt; never caught it. &lt;code&gt;failed&lt;/code&gt; was never emitted, so the code that re-enables the buttons never ran. Every button stayed greyed out, with no message.&lt;/p&gt;

&lt;p&gt;The fix is to catch &lt;code&gt;BaseException&lt;/code&gt; in the worker (re-raising &lt;code&gt;KeyboardInterrupt&lt;/code&gt; and &lt;code&gt;SystemExit&lt;/code&gt;), and to add a safety net: &lt;code&gt;finished&lt;/code&gt; is emitted even when &lt;code&gt;run()&lt;/code&gt; ends on an uncaught exception, so a handler on it can reset the "busy" state.&lt;/p&gt;

&lt;p&gt;If you wrap any native extension, check what its panics or errors derive from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: a second worker silently refused to start
&lt;/h2&gt;

&lt;p&gt;After a click, the app ran one worker, and when it finished it should start a second one. I called the next &lt;code&gt;_run()&lt;/code&gt; directly from the &lt;code&gt;done&lt;/code&gt; handler. Sometimes nothing happened.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_run()&lt;/code&gt; starts with a guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem: &lt;code&gt;done&lt;/code&gt; is emitted from inside &lt;code&gt;run()&lt;/code&gt;, that is, from the worker's own thread, before the thread has finished stopping. At that exact moment &lt;code&gt;isRunning()&lt;/code&gt; can still return &lt;code&gt;True&lt;/code&gt;. The new call was rejected by its own guard, depending on timing. It never showed up when I tested one case alone, only inside the full test suite.&lt;/p&gt;

&lt;p&gt;The fix is to let the event loop turn once before chaining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_selection_done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleShot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_apply_selection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My rule now: &lt;strong&gt;chaining one worker from another's &lt;code&gt;done&lt;/code&gt; always goes through &lt;code&gt;QTimer.singleShot(0, ...)&lt;/code&gt;&lt;/strong&gt;, never a direct call.&lt;/p&gt;

&lt;p&gt;A related trap sits in the same area. Each worker removes itself from &lt;code&gt;self._worker&lt;/code&gt; on &lt;code&gt;finished&lt;/code&gt;. A chain started from &lt;code&gt;done&lt;/code&gt; had already installed the next worker, and the previous worker's &lt;code&gt;finished&lt;/code&gt; then wiped it out. Nothing blocked two jobs from running on the same session. The fix is to forget a worker only if it is still the current one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_forget_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug 4: a keyboard shortcut bypassed the greyed-out buttons
&lt;/h2&gt;

&lt;p&gt;I disabled the buttons while a trace was running. But a shortcut like Ctrl+O does not go through a button. Pressing it during a computation opened a new image, which closed the session and deleted its temporary folder under the running thread. The thread then died with "Cannot create output file".&lt;/p&gt;

&lt;p&gt;The fix is a single predicate used by every entry point, shortcuts included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_idle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_idle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I test this now
&lt;/h2&gt;

&lt;p&gt;Engine tests cannot catch these. The failures only exist when the real window, the real event loop and the real thread run together. So I keep a separate file of regression tests that drives the actual &lt;code&gt;MainWindow&lt;/code&gt; in Qt's offscreen mode, with modal dialogs disabled (otherwise a test blocks silently on a message box).&lt;/p&gt;

&lt;p&gt;Two practical details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To test "Ctrl+O during a computation", force a slow job (&lt;code&gt;time.sleep(1.5)&lt;/code&gt; in the worker). On a small test image a real trace ends in tens of milliseconds, and the test would check nothing.&lt;/li&gt;
&lt;li&gt;To test the panic case, raise a custom &lt;code&gt;BaseException&lt;/code&gt; subclass in the worker and assert that the buttons are enabled again afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Qt signals cross threads and C++ code. Python's safety nets do not follow them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;done&lt;/code&gt; is not "the thread has stopped".&lt;/li&gt;
&lt;li&gt;Every way to start work needs the same guard, not just the buttons.&lt;/li&gt;
&lt;li&gt;Test the glue, not just the engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;This is part of &lt;a href="https://www.vectorpop.fr/vectofix" rel="noopener noreferrer"&gt;VectoFix&lt;/a&gt;, a local Windows tool that traces an image into SVG, measures what the tracing lost compared with the source, and lets you fix a zone with a brush. Everything runs on the user's machine.&lt;/p&gt;

&lt;p&gt;Have you hit a thread or signal bug that only appeared with the real UI? I am curious what your regression tests look like.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pyside6</category>
      <category>qt</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Windows Event ID 37 Doesn't Mean Your CPU Is Overheating</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:07:57 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/windows-event-id-37-doesnt-mean-your-cpu-is-overheating-4f9i</link>
      <guid>https://dev.to/wgeorgeassistantia/windows-event-id-37-doesnt-mean-your-cpu-is-overheating-4f9i</guid>
      <description>&lt;p&gt;I build a small Windows diagnostic tool. One of its rules looked at the System event log, found &lt;code&gt;Kernel-Processor-Power&lt;/code&gt; Event ID 37, and told the user: &lt;em&gt;"your cooling system is probably the problem."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I ran it on my own PC, which has no cooling problem. It told me my cooling was the problem.&lt;/p&gt;

&lt;p&gt;This post is about why Event 37 is a terrible proxy for overheating, and the three filters that fixed the false positive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Event 37 actually says
&lt;/h2&gt;

&lt;p&gt;Event ID 37 from &lt;code&gt;Microsoft-Windows-Kernel-Processor-Power&lt;/code&gt; reads roughly: &lt;em&gt;the speed of processor N is being limited by system firmware.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Heat is one reason firmware does that. It is not the only one. A power limit, a weak charger, a battery state or an OEM power profile produce the same event. The event tells you a limit was applied. It does not tell you why.&lt;/p&gt;

&lt;p&gt;My first version of the rule was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;count Event 37 over the last 14 days&lt;/li&gt;
&lt;li&gt;ignore the ones right after the latest boot&lt;/li&gt;
&lt;li&gt;if three or more remain, flag "physical cooling problem"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was also compared with the CPU load measured during an 8-second scan, taken while the user is idle by definition. Two things measured at two different times, used as one argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers on my machine
&lt;/h2&gt;

&lt;p&gt;Over 14 days my log had 12 Event 37 entries. The old rule kept 11 of them as "not at boot" and concluded the worst.&lt;/p&gt;

&lt;p&gt;Looking at them one by one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 of the 12&lt;/strong&gt; happened within 10 minutes after a boot. Not just the latest boot: any boot in the period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The other 4&lt;/strong&gt; were not heat spikes at all. Each reported a cap lasting 33 to 48 hours. That is a permanent power ceiling, not a thermal event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real overheating episodes: zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: ignore every boot, not just the last one
&lt;/h2&gt;

&lt;p&gt;Firmware often applies a limit while the system starts. Collect every boot in the window with Event 12 from &lt;code&gt;Kernel-General&lt;/code&gt;, then drop any Event 37 that lands in the 10 minutes after one.&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="nv"&gt;$boots&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'System'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProviderName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Microsoft-Windows-Kernel-General'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;StartTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;AddDays&lt;/span&gt;&lt;span class="err"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeCreated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ev37&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$atBoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$boots&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeCreated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalMinutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-le&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$atBoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# ...next filters&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 2: only count events on mains power
&lt;/h2&gt;

&lt;p&gt;On battery, limiting the CPU is intended behaviour. &lt;code&gt;Kernel-Power&lt;/code&gt; Event 105 records power source changes, and its &lt;code&gt;AcOnline&lt;/code&gt; field says whether the machine was plugged in. For each Event 37, take the latest Event 105 before it. If the machine was on battery, it is not evidence of anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: use the duration, not just the count
&lt;/h2&gt;

&lt;p&gt;Event 37 carries a &lt;code&gt;CapDurationInSeconds&lt;/code&gt; field. A thermal throttle comes and goes in minutes. A cap that lasts a day or more is a configuration, not a symptom. I treat anything of an hour or longer as a permanent cap and drop it.&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="nv"&gt;$xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToXml&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dur&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$xml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EventData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'CapDurationInSeconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'#text'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dur&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only what survives all three filters counts as an episode. On my machine: 0. The verdict also changed from a statement ("your cooling is the problem") to a hedged one ("very probably") with a simple test the user can do themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PowerShell trap I hit on the way
&lt;/h2&gt;

&lt;p&gt;In the same session I broke the translation table. My loop used a variable named &lt;code&gt;$t&lt;/code&gt; for the event time. The table of translated strings was called &lt;code&gt;$T&lt;/code&gt;. PowerShell variable names are case-insensitive, so &lt;code&gt;$t&lt;/code&gt; silently overwrote &lt;code&gt;$T&lt;/code&gt; and every string came back empty. If you use single-letter variables in a script that already has one in capitals, rename one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not proved
&lt;/h2&gt;

&lt;p&gt;I recalibrated this on one machine, mine. The three filters remove the false positive I could see, but I do not yet know the false-positive rate across other laptops and desktops. A 10-PC field test is next. If you have Event 37 data from machines with a real cooling fault, I would like to see how the filters behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;This is part of &lt;a href="https://nyctale.fr" rel="noopener noreferrer"&gt;Nyctale&lt;/a&gt;, a Windows PC diagnostic tool that tries to name the actual cause of a slow or hot PC, and says "no software will fix this" when that is the honest answer. The fixed rule shipped in version 1.0.3. The quick diagnostic is free.&lt;/p&gt;

&lt;p&gt;Have you used Event 37 for anything? Did you find other signals that separate real thermal throttling from firmware power limits?&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>windows</category>
      <category>debugging</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to Identify Technologies Mentioned in a YouTube Video</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:41:54 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/how-to-identify-technologies-mentioned-in-a-youtube-video-e91</link>
      <guid>https://dev.to/wgeorgeassistantia/how-to-identify-technologies-mentioned-in-a-youtube-video-e91</guid>
      <description>&lt;p&gt;"Which technologies does this video talk about?" sounds like a named-entity task, and it is, with a twist: the entities are product names that are short, ambiguous, constantly new and often misheard. This post covers the three problems you meet, and a practical approach to each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: what counts as a technology?
&lt;/h2&gt;

&lt;p&gt;If you do not decide, the model will. Without a definition you get everything from "JavaScript" to "the cloud" to the presenter's company. Write the definition into the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Include: named products, frameworks, libraries, languages, services, CLIs and protocols&lt;/li&gt;
&lt;li&gt;Exclude: generic words ("API", "database", "framework"), company names that are not products, channel names, people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then maintain a block list of the noise you actually observe. In our runs, recurring false positives included generic words such as "API" and "Google", and the names of channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: the same tool, many spellings
&lt;/h2&gt;

&lt;p&gt;Speakers say "Postgres", "PostgreSQL", "Postgres DB". Captions write "Next JS", "next.js" or "Nexjs". Two things help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalise&lt;/strong&gt;: lower-case, remove spaces and punctuation, strip common suffixes (&lt;code&gt;js&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;) to create a matching key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an alias list&lt;/strong&gt; for the display name and for deduplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch for duplicates that differ by one letter. We saw a tool appear twice because one occurrence was misspelt; a fuzzy match on names within the same video catches it.&lt;/p&gt;

&lt;p&gt;Translation can also distort names. If your pipeline summarises in another language, instruct the model to keep product names exactly as spoken. A product name turned into a translated phrase is no longer searchable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: mentioned does not mean recommended
&lt;/h2&gt;

&lt;p&gt;A technology can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Used&lt;/strong&gt; in the demo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommended&lt;/strong&gt; as an alternative&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compared&lt;/strong&gt; against another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Criticised&lt;/strong&gt; or dismissed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classify the mention type, or at least keep the quote so a reader can judge. This is the difference between a useful tool list and a misleading one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ambiguous names need context
&lt;/h2&gt;

&lt;p&gt;"Neon", "Serena" and "Phantom" are all valid product names, and all share their name with something unrelated. Give the model the surrounding sentence, and when you look up the official site, pass the category as well. A good result for an ambiguous name needs context at both extraction and resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it on real videos
&lt;/h2&gt;

&lt;p&gt;Pick ten videos in your field, run the pipeline and read the list yourself. You will quickly see what kind of noise appears and where the prompt needs a rule. Update the block list and the prompt, run again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our implementation
&lt;/h2&gt;

&lt;p&gt;We built these rules into &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;VidScope&lt;/a&gt;, which returns the tools mentioned in a YouTube video with a timestamp and an official link, and avoids returning a link when the match is unclear.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this a named-entity recognition problem?&lt;/strong&gt;&lt;br&gt;
Yes, with the twist that names are short, ambiguous and often misheard in captions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop generic words being returned?&lt;/strong&gt;&lt;br&gt;
Define what counts as a tool in the prompt and keep a block list of observed noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle different spellings of the same tool?&lt;/strong&gt;&lt;br&gt;
Normalise names to a matching key and keep an alias list.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Extract Tools and Technologies from YouTube Videos Automatically</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:48:12 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/how-to-extract-tools-and-technologies-from-youtube-videos-automatically-32ie</link>
      <guid>https://dev.to/wgeorgeassistantia/how-to-extract-tools-and-technologies-from-youtube-videos-automatically-32ie</guid>
      <description>&lt;p&gt;If you watch technical YouTube videos for work, you know the problem: a 40-minute walkthrough mentions a dozen tools, a few commands and a couple of versions, and an hour later you cannot remember which ones. This post shows a practical pipeline to extract the tools and technologies mentioned in a video automatically, step by step, and the pitfalls that make the naive version unreliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline in four steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get the transcript&lt;/li&gt;
&lt;li&gt;Split it into chunks the model can handle&lt;/li&gt;
&lt;li&gt;Extract candidate tools with their timestamps&lt;/li&gt;
&lt;li&gt;Verify each candidate and attach an official link&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step has a failure mode. We will go through them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: get the transcript
&lt;/h2&gt;

&lt;p&gt;Most videos have captions, either written by the author or generated automatically. Libraries such as &lt;a href="https://github.com/yt-dlp/yt-dlp" rel="noopener noreferrer"&gt;yt-dlp&lt;/a&gt; can fetch them along with timing data. Keep the timing: every segment of text has a start time, and that is what lets you link each tool back to the exact moment of the video later.&lt;/p&gt;

&lt;p&gt;Pitfall: automatic captions mangle product names. Words like "Supabase", "Vercel" or "Kubernetes" come out phonetically. Do not clean them yet; the model will need context to recover them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: split the transcript into chunks
&lt;/h2&gt;

&lt;p&gt;Long videos do not fit in one prompt, and even when they do, extraction quality drops as the input grows. A simple approach is to cut the transcript into chunks of fixed size, run them in parallel and merge the results.&lt;/p&gt;

&lt;p&gt;We found chunk size matters more than expected. In our tests on a transcript of roughly 475,000 characters, small chunks found more tools than large ones: very large chunks returned fewer distinct tools, because the model summarises instead of listing. Smaller chunks cost more calls but give better recall. A middle size of about 30,000 characters gave a good balance between the number of calls and the tools found.&lt;/p&gt;

&lt;p&gt;The mistake to avoid is silent truncation. An early version of our pipeline cut the text at a fixed length and analysed only the first minutes of a seven-hour video without telling anyone. If you cap the input, say so in the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: extract candidates with timestamps
&lt;/h2&gt;

&lt;p&gt;Ask the model for structured output, not prose. For each candidate tool, request the name, a short description, a category and a verbatim quote from the transcript with its timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"we are going to use Supabase for the backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00:12:41"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules improve the results a lot. First, define what counts as a tool in the prompt, and list exclusions (generic words, channel names, company names that are not products). Second, require a verbatim quote. A candidate whose quote does not appear in the transcript is rejected.&lt;/p&gt;

&lt;p&gt;After merging the chunks, deduplicate by normalised name and drop candidates without a valid timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: verify and link
&lt;/h2&gt;

&lt;p&gt;This is the step most tutorials skip, and the one that matters most. A name alone is ambiguous: "Neon", "Serena" or "Codex" each match several unrelated things on the web. If you resolve the official website with a naive search, you will get cinema studios and food standards instead of software.&lt;/p&gt;

&lt;p&gt;What worked for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the search query from the tool's category and description, not only its name&lt;/li&gt;
&lt;li&gt;Accept canonical open-source repositories when a tool has no website of its own&lt;/li&gt;
&lt;li&gt;Block aggregator sites, course platforms, forums and social networks as official sources&lt;/li&gt;
&lt;li&gt;Score the match, and return nothing when the confidence is low&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point is the hardest culture shift: an empty link is better than a wrong one. Users forgive a missing link; they do not forgive a confident wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;A good extraction pipeline is mostly about discipline: keep the timestamps, never truncate silently, require quotes, and prefer no link to a bad link. Models are the easy part. The rest is verification.&lt;/p&gt;

&lt;p&gt;If you want to see this applied rather than build it yourself, we built &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;VidScope&lt;/a&gt; to explore this problem: paste a YouTube link and it returns the tools mentioned with timestamps and official links.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I extract tools from any YouTube video?&lt;/strong&gt;&lt;br&gt;
From any video that has a transcript, though automatic captions can misspell product names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why keep timestamps?&lt;/strong&gt;&lt;br&gt;
They let you check each tool at the moment it was mentioned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just ask an AI to summarise the video?&lt;/strong&gt;&lt;br&gt;
A summary tells you the idea; it rarely gives you the exact tools, versions and the evidence for each.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built an AI Tool That Extracts Tech Stacks from YouTube Videos</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Sat, 12 Sep 2026 18:57:29 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/i-built-an-ai-tool-that-extracts-tech-stacks-from-youtube-videos-1520</link>
      <guid>https://dev.to/wgeorgeassistantia/i-built-an-ai-tool-that-extracts-tech-stacks-from-youtube-videos-1520</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhxgf2rh5ym35iv9isgt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhxgf2rh5ym35iv9isgt.jpeg" alt=" " width="799" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every developer has done this: you open a 45-minute "Build a SaaS in a weekend" video, and by minute 12 you're already pausing every 30 seconds to screenshot a terminal command or squint at a VS Code sidebar to figure out which ORM the creator just installed. You rewind three times because you missed the name of the vector database. By the time the video ends, you've spent an hour to extract maybe eight lines of useful information: a framework name, two library names, one hosting provider. I built VidScope because I got tired of doing this manually, and I wanted to share how the extraction pipeline actually works under the hood — and why "AI video summarizer" was the wrong way to think about the problem from the start.&lt;/p&gt;

&lt;p&gt;The first mistake I made was treating this as a summarization problem. Early prototypes fed the full YouTube transcript into an LLM and asked for "a summary of the tools used." The results looked plausible and were frequently wrong — the model would infer a database was PostgreSQL because the creator said "SQL," or assume a deployment target was Vercel because the video mentioned Next.js, even when the actual clip named Railway. Summarization models are optimized to sound coherent, not to be strictly grounded in the source. For a tool whose entire value proposition is "tell me exactly what was mentioned," a confidently wrong answer is worse than no answer. That's the core design constraint that shaped everything else: extraction, not interpretation.&lt;/p&gt;

&lt;p&gt;The pipeline itself is simpler than people expect. VidScope pulls the video's transcript (falling back to audio transcription when no captions exist), chunks it to stay inside context limits for long-form content — some tutorials and conference talks run past two hours — and runs extraction passes that are explicitly instructed to only output tool, framework, or library names that appear verbatim or near-verbatim in the transcript text. No inference from context, no "this is probably React because they mentioned JSX." If a name isn't in the transcript, it doesn't make it into the report. This is the "0% hallucination" constraint, and it's enforced at the prompt level with strict extraction rules, not just requested politely.&lt;/p&gt;

&lt;p&gt;The part that took the most iteration wasn't the extraction — it was verification. A raw list of tool names is only marginally more useful than pausing the video yourself. What makes the report actually save time is that every extracted name gets resolved to its official documentation or homepage link automatically, so a developer can go from "watched half a tutorial" to "have the actual docs open in five tabs" in under 30 seconds. Getting this resolution step accurate meant handling ambiguous names (there are at least four different open-source projects called "Flow"), filtering out generic terms that get mentioned but aren't tools (the model initially flagged "the cloud" and "database" as extracted entities before I tightened the extraction schema), and deduplicating variants of the same tool referenced multiple times under slightly different names across a single video.&lt;/p&gt;

&lt;p&gt;Long videos exposed a separate scaling problem: a two-hour conference talk produces a transcript that's tens of thousands of tokens, and naively extracting per-chunk without merging state across chunks produces duplicate or fragmented tool lists — the same framework mentioned in minute 5 and minute 90 shows up twice with slightly different context. The fix was maintaining an extraction state across chunks rather than treating each chunk independently, which also meant the final report generation step needed to be idempotent regardless of how many chunks a video's transcript happened to split into. This is the unglamorous part of building an "AI feature" that nobody talks about: the actual model call is a small fraction of the engineering effort compared to chunking, state management, and making outputs consistent at different video lengths.&lt;/p&gt;

&lt;p&gt;None of this matters if the tool doesn't fit into how developers actually work. The workflow I optimized for is: paste a YouTube link, wait 30 seconds, get a clean list of every tool and framework mentioned with verified links, export to Markdown or PDF, move on. No account required to try it, no video re-upload, no watching required at all. It's built for the moment when you find a promising tutorial at 11pm and want to know in thirty seconds whether it's using a stack you actually want to learn, before committing 45 minutes to watching it.&lt;/p&gt;

&lt;p&gt;If you've ever kept a messy notes file of "tools mentioned in videos I watched," or rewound a stream three times to catch a package name, that's exactly the problem VidScope solves. You can try it free on two videos, no credit card required, at &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;vidscope.fr&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a Local Silence Remover That Processes a 5GB Audio File in Under 6 Minutes</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:44:45 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/how-i-built-a-local-silence-remover-that-processes-a-5gb-audio-file-in-under-6-minutes-1ebf</link>
      <guid>https://dev.to/wgeorgeassistantia/how-i-built-a-local-silence-remover-that-processes-a-5gb-audio-file-in-under-6-minutes-1ebf</guid>
      <description>&lt;p&gt;VoxCut is a dedicated silence remover for audio and video: it automatically detects and cuts silent pauses — dead air, long breaths, gaps between takes — from podcasts, interviews and recordings. Unlike full editors that bundle silence removal as one feature among many, it does this one job, entirely on your device.&lt;/p&gt;

&lt;p&gt;I want to walk through the actual engineering problem behind it, because "remove silence from audio" sounds trivial until someone hands you a 72-hour, 5GB recording and expects it back in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach falls over first
&lt;/h2&gt;

&lt;p&gt;The obvious way to build this: load the whole file into memory, scan the waveform, cut the quiet parts, re-encode. That's roughly what most desktop audio editors do — and it's exactly why they choke on anything past a couple of GB. Once you're loading multi-hour recordings, you hit a RAM ceiling that has nothing to do with your CPU.&lt;/p&gt;

&lt;p&gt;VoxCut instead runs a local, FFmpeg-based streaming pass over the file rather than materializing the whole thing in memory. That single architectural choice is what lets it handle files "beyond 5GB" without a hard ceiling — the real limit becomes your disk space, not your RAM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdsmyzqw1pq7ulsw1ktty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdsmyzqw1pq7ulsw1ktty.png" alt="VoxCut waveform before/after" width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection: comparing amplitude against a threshold
&lt;/h2&gt;

&lt;p&gt;The detection logic itself is simple in concept and easy to get wrong in practice: compare each segment's amplitude against a decibel threshold and a minimum pause duration. Too aggressive, and you cut a natural breath mid-sentence. Too conservative, and you leave dead air in.&lt;/p&gt;

&lt;p&gt;VoxCut previews exactly what will be cut before anything is removed, so the sensitivity setting is something you tune by ear rather than trust blindly — nothing is deleted until you confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turbo Mode trade-off (V2)
&lt;/h2&gt;

&lt;p&gt;The 2.0 release added a Pro-only Turbo Mode that's a good example of an honest trade-off rather than a free performance win. The standard export stitches kept segments together with crossfades, which sounds smoother but costs time on files with hundreds of detected cuts. Turbo mode removes silence in a single pass instead — no crossfades — which is roughly 2x faster on realistic content (podcasts, interviews, meetings).&lt;/p&gt;

&lt;p&gt;The cost: sharper cuts instead of smooth transitions. For most spoken-word content that's barely audible; for content that leans on the crossfades to hide the cut points, standard mode is still the default.&lt;/p&gt;

&lt;p&gt;On realistic content (not the "silence-only" edge case that inflates marketing numbers), expect roughly 35-50x real-time in standard mode and 70-100x in Turbo, depending on the machine. A 72-hour, 5GB file was processed in under 6 minutes on ordinary consumer hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going cross-platform: Windows/Linux desktop, then Android
&lt;/h2&gt;

&lt;p&gt;VoxCut started as a Windows/Linux desktop app. Porting the same core logic to Android (shipped as v2.0.2, same week as the 2.0 desktop release) meant a separate app with its own constraints: mobile storage, a different free-tier limit (5 minutes per file vs. 10 minutes on desktop), and Pro features gated differently — AI noise reduction and loudness normalization are Android-Pro-only, priced independently (€14.99 lifetime) from the desktop license.&lt;/p&gt;

&lt;p&gt;Keeping "100% local, no upload" true on both platforms was the one non-negotiable constraint across the port — it's the core promise, not a checkbox feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;If I started over, I'd build the streaming-pass architecture from day one instead of retrofitting it — the in-memory prototype worked fine on my own test files and only broke once real users started throwing multi-hour recordings at it. Test with files bigger than you think anyone will use.&lt;/p&gt;




&lt;p&gt;VoxCut is free to try (Windows, Linux, Android) — &lt;a href="https://voxcutpro.com" rel="noopener noreferrer"&gt;voxcutpro.com&lt;/a&gt;. Feedback and questions welcome in the comments, this is my first shipped piece of software and I'm still learning in public.&lt;/p&gt;

&lt;h3&gt;
  
  
  About VoxCut
&lt;/h3&gt;

&lt;p&gt;VoxCut is a dedicated silence remover for audio and video. It automatically removes silence, pauses and dead air while processing your files locally — with no upload and no subscription.&lt;/p&gt;

&lt;p&gt;VoxCut — Silence Remover for Audio &amp;amp; Video · &lt;a href="https://voxcutpro.com" rel="noopener noreferrer"&gt;https://voxcutpro.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>audio</category>
      <category>ffmpeg</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why I built a local-first PNG-to-SVG converter instead of using an online tool</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:58:26 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/why-i-built-a-local-first-png-to-svg-converter-instead-of-using-an-online-tool-4415</link>
      <guid>https://dev.to/wgeorgeassistantia/why-i-built-a-local-first-png-to-svg-converter-instead-of-using-an-online-tool-4415</guid>
      <description>&lt;p&gt;Most "convert your image to SVG" tools online are the same story: upload your file to a server you don't control, wait, download a result that's either bloated with thousands of tiny paths or stripped of the details that made the image worth vectorizing in the first place. I hit this wall enough times doing client logo work that I built my own converter — VectorPop — and kept it entirely local, no upload, no subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;Vectorizing a raster image well isn't really about tracing pixels — any tool can call a tracing library and spit out an SVG. The actual work is everything that happens &lt;em&gt;before&lt;/em&gt; tracing: merging near-identical colors so you don't get 40 shades of "almost the same blue," rebuilding gradients as real &lt;code&gt;&amp;lt;linearGradient&amp;gt;&lt;/code&gt; elements instead of dozens of flat color bands, and cleanly separating a subject from its background. Skip that preprocessing and you get technically-valid SVGs that are unusable in a real design workflow — too many paths, wrong colors, gradients turned into stripes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical approach
&lt;/h2&gt;

&lt;p&gt;VectorPop wraps &lt;code&gt;vtracer&lt;/code&gt; (the Rust tracing engine) with a Pillow-based preprocessing layer I built specifically to solve those problems: color quantization and merging before tracing, gradient reconstruction that samples the original pixels to estimate a real gradient rather than approximating it geometrically, and an "Optimize" button that tries ~12 setting combinations and keeps whichever one is closest to the source image — no deep learning, just systematic comparison. Background removal works two ways: flat-color removal with a tolerance slider for logos on solid backgrounds, and optional AI segmentation (rembg) for photos — the AI path is opt-in and the app degrades gracefully if the dependency isn't present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local, and why this pricing model
&lt;/h2&gt;

&lt;p&gt;Everything runs on-device: no image ever leaves your machine, which matters if you're vectorizing client logos or anything under NDA, and it also means no per-conversion cost to pass on to the user. That's why VectorPop is free for unlimited vectorization with 3 SVG exports/day, and a one-time €39 for the Pro tier (unlimited exports, PDF/high-res PNG, AI cutout, batch processing) — no subscription, because a desktop tool that runs locally shouldn't need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;VectorPop is a Windows app, live on the Microsoft Store, built by a solo dev after getting tired of redoing SVGs by hand in Illustrator for logo and print work. If you've fought with online converters that either paywall basic exports or upload your files to a server, it's worth a look: &lt;a href="https://vectorpop.fr" rel="noopener noreferrer"&gt;vectorpop.fr&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Curious what this community thinks of the gradient-reconstruction approach vs. just tracing flat color bands — happy to go into more technical detail in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>svg</category>
      <category>windows</category>
    </item>
    <item>
      <title>Stop Copy-Pasting Your Invoices: The Complete Guide to Local PDF Mail Merge</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:01:59 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/stop-copy-pasting-your-invoices-the-complete-guide-to-local-pdf-mail-merge-3pn3</link>
      <guid>https://dev.to/wgeorgeassistantia/stop-copy-pasting-your-invoices-the-complete-guide-to-local-pdf-mail-merge-3pn3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk08gy8vqpseyg5xpucd6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk08gy8vqpseyg5xpucd6.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run a small business, a freelance practice, or a local club, you probably spend more time than you'd like doing administrative chores. One of the worst offenders? Copying and pasting data from a spreadsheet to create individual documents.&lt;/p&gt;

&lt;p&gt;Whether it's monthly invoices, employee training certificates, or personalized event badges, doing this manually is a guaranteed way to waste hours and introduce typos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter the PDF Mail Merge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mail merge isn't just for sending letters anymore. By using your spreadsheet as a simple database and connecting it to a PDF template, you can automate this entire workflow. But there is a catch.&lt;/p&gt;

&lt;p&gt;Many people default to online web tools. While convenient, this means uploading sensitive financial data, client names, and addresses to a third-party server. If you care about data privacy (or GDPR compliance), this is a red flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Local-First Solution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The safest and most efficient way to handle this is locally on your own computer. We developed &lt;a href="https://www.inoneshot.fr/" rel="noopener noreferrer"&gt;InOneShot&lt;/a&gt; exactly for this purpose.&lt;/p&gt;

&lt;p&gt;Here is how a &lt;strong&gt;local PDF mail merge&lt;/strong&gt; works in 3 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepare your Data&lt;/strong&gt;: Keep a clean Excel or CSV file. One row per invoice or person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design your Template&lt;/strong&gt;: Create a beautiful PDF design once. Leave blank spaces for the variable data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge and Generate&lt;/strong&gt;: Use InOneShot to map your spreadsheet columns to your PDF. As an offline &lt;strong&gt;bulk PDF generator&lt;/strong&gt;, the app runs locally on your machine, generating hundreds of unique, named PDFs in seconds.
You get a clean ZIP file of documents. No monthly subscriptions, no data privacy concerns, just hours of your life back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automating your admin work doesn't require complex coding or expensive cloud tools. Sometimes, a smart local app is all you need.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Ready to get your time back? Download &lt;a href="https://www.inoneshot.fr/" rel="noopener noreferrer"&gt;InOneShot&lt;/a&gt;, the ultimate local PDF creator, and start automating your paperwork today!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>automation</category>
      <category>softwaredevelopment</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
