<?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: Leo Huang</title>
    <description>The latest articles on DEV Community by Leo Huang (@huangchihhungleo).</description>
    <link>https://dev.to/huangchihhungleo</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%2F4014142%2Ff98d8693-2550-4f9e-9aa6-8c516ba04b84.jpg</url>
      <title>DEV Community: Leo Huang</title>
      <link>https://dev.to/huangchihhungleo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huangchihhungleo"/>
    <language>en</language>
    <item>
      <title>1,377 frames in, 60 out, and none of them knew what time it was</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Sat, 25 Jul 2026 19:12:34 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/1377-frames-in-60-out-and-none-of-them-knew-what-time-it-was-29a5</link>
      <guid>https://dev.to/huangchihhungleo/1377-frames-in-60-out-and-none-of-them-knew-what-time-it-was-29a5</guid>
      <description>&lt;p&gt;A user opened an issue on my open-source video tool last week that named a gap I had been shipping around for months.&lt;/p&gt;

&lt;p&gt;He runs lectures through crv so an LLM can read them. One 22-minute lecture: 1,377 candidate frames extracted, 60 kept after dedup and &lt;code&gt;--max-frames&lt;/code&gt; thinning. The 60 frames come out in the right order. That is all they come out with.&lt;/p&gt;

&lt;p&gt;His complaint, in one line: the LLM can describe the slide, but it cannot tell you when the slide was on screen.&lt;/p&gt;

&lt;p&gt;That breaks more than it sounds like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you cannot cite visual evidence with a timestamp&lt;/li&gt;
&lt;li&gt;you cannot line a chart up against the nearby &lt;code&gt;transcript.json&lt;/code&gt; segments&lt;/li&gt;
&lt;li&gt;you cannot jump from a keyframe back to that moment in the video&lt;/li&gt;
&lt;li&gt;you cannot verify the claim afterwards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The transcript had timestamps the whole time. The frames did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the timestamps died
&lt;/h2&gt;

&lt;p&gt;The pipeline goes: extract with ffmpeg, drop near-identical frames, thin down to &lt;code&gt;--max-frames&lt;/code&gt;, rename everything to &lt;code&gt;frame_001.jpg&lt;/code&gt;, &lt;code&gt;frame_002.jpg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every one of those steps is lossy for position. Extraction writes files, dedup deletes some, thinning deletes more, renaming closes the gaps. By the time you are holding &lt;code&gt;frame_012.jpg&lt;/code&gt;, the only fact left in the filename is "twelfth surviving frame", and twelfth of what is no longer recoverable from the output directory.&lt;/p&gt;

&lt;p&gt;The tempting fix is arithmetic: &lt;code&gt;timestamp = frame_number / fps&lt;/code&gt;. That is wrong on any variable frame rate source, which covers most screen recordings and a lot of phone video. It gives you a number that looks right and drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;ffmpeg already knows. The &lt;code&gt;showinfo&lt;/code&gt; filter prints the real PTS of every frame it passes, on the same &lt;code&gt;select&lt;/code&gt; pass you are already running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"select=...,showinfo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parse that log and you get true presentation timestamps with no second decode pass. Then you carry them: attach the PTS at extraction, keep it attached through dedup, through thinning, through the rename, and write it out next to the images as &lt;code&gt;frames.json&lt;/code&gt;:&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;"frames"&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;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frame_001.jpg"&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_sec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;18.42&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:00:18.420"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"selection_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scene"&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;&lt;code&gt;selection_reason&lt;/code&gt; records which dedup channel kept the frame. That one is worth adding early: it is what you read when a frame you wanted is missing and you need to know which stage ate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I nearly skipped
&lt;/h2&gt;

&lt;p&gt;If the &lt;code&gt;showinfo&lt;/code&gt; log and the extracted files ever disagree on count, the tool writes no timestamps at all rather than approximate ones.&lt;/p&gt;

&lt;p&gt;That felt overly strict while I was writing it. It is the opposite. A missing timestamp makes the model say "I don't know when". A wrong timestamp makes it cite 00:03:41 with total confidence, and nothing downstream can catch it. In a pipeline whose entire job is handing a model verifiable evidence, a plausible wrong number is the worst thing you can emit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it hold up
&lt;/h2&gt;

&lt;p&gt;The person who filed the issue re-ran his 22:12 lecture on the new build and checked it himself: 1,377 candidates down to 60 final frames, 60 entries in the mapping, all monotonic, no missing or extra image files. He replayed the full extraction pass against the original source and matched every final image back to its recorded timestamp. 60 out of 60.&lt;/p&gt;

&lt;p&gt;I did not ask him to do that. It is the most useful thing anyone has done for this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you build any extract, filter and rename pipeline that feeds an LLM, decide early where position lives. Threading an identifier through four stages is much cheaper than reconstructing it from a directory listing afterwards. And when the identifier is uncertain, emit nothing instead of something.&lt;/p&gt;

&lt;p&gt;crv is MIT and on PyPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; claude-real-video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a paid Pro build if you need camera motion, audio and speaker labels on top of frames and transcript: &lt;a href="https://capafy.ai/agent/llm-real-video-pro-let-any-llm-watch-videos/5451082151?ct=devto" rel="noopener noreferrer"&gt;https://capafy.ai/agent/llm-real-video-pro-let-any-llm-watch-videos/5451082151?ct=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>ffmpeg</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A 2,181-video field report made my open-source video tool better in one day</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:02:22 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/a-2181-video-field-report-made-my-open-source-video-tool-better-in-one-day-3d3d</link>
      <guid>https://dev.to/huangchihhungleo/a-2181-video-field-report-made-my-open-source-video-tool-better-in-one-day-3d3d</guid>
      <description>&lt;p&gt;Last week a user emailed me a field report. He had run &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;claude-real-video&lt;/a&gt; — my open-source tool that turns a video into something an LLM can actually read — over his entire photo library: &lt;strong&gt;2,181 videos in about four days&lt;/strong&gt;. Then he sent me the bug list, worst first.&lt;/p&gt;

&lt;p&gt;The worst one was a design flaw I had been shipping for months without noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot
&lt;/h2&gt;

&lt;p&gt;Frame dedup compares downscaled frames and drops a frame when too few pixels changed. Sounds reasonable — until the thing that matters is small in the frame.&lt;/p&gt;

&lt;p&gt;A person filmed at phone-camera distance covers roughly &lt;strong&gt;0.5% of the picture&lt;/strong&gt;. Whatever they do, they can never change 8% of the pixels. So percentage-based dedup calls the crucial second "a duplicate" and deletes it. On his repro clip — a static shot where a vehicle knocks someone down in about one second — extraction produced 69 frames and dedup kept 4. The analysis described "a vehicle passes close to the camera" and missed the incident entirely. Setting the threshold to zero did not help. The math is structurally blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Percentages cannot see small subjects, so 0.7.16 adds a third check that ignores percentages: &lt;strong&gt;if a handful of cells change hard, the frame stays.&lt;/strong&gt; That is the whole idea. On the same repro, the action now survives 10/10 frames and a vision model narrates the event correctly. On normal footage the kept-count barely moves, so you do not pay extra for it.&lt;/p&gt;

&lt;p&gt;Everything else he reported — a crash on non-UTF-8 metadata, a flag name that means the opposite of what it says, 68 GB of intermediates piling up silently — shipped the same day, also in 0.7.16.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code (Codex, Cursor and Gemini CLI work too):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add HUANGCHIHHUNGLeo/claude-real-video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add HUANGCHIHHUNGLeo/claude-real-video
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;claude-real-video@claude-real-video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;claude-real-video
crv &lt;span class="s2"&gt;"your video URL or file"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a 60-second demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Htory2jlr-0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  日本のユーザーの方へ
&lt;/h2&gt;

&lt;p&gt;日本からの利用者が増えてきて、うれしいです。インストールは &lt;code&gt;pip install claude-real-video&lt;/code&gt;、そのあと &lt;code&gt;crv "動画のURLまたはファイル"&lt;/code&gt; を実行するだけです。ご質問は日本語でも大丈夫です。&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A bug list this long only comes from mileage. If someone runs your tool 2,181 times and writes down everything that broke, that is not criticism — that is the roadmap. Treasure those users.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your LLM can't actually watch video. Here's the smallest fix (MIT)</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Sun, 19 Jul 2026 03:01:16 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/your-llm-cant-actually-watch-video-heres-the-smallest-fix-mit-2igp</link>
      <guid>https://dev.to/huangchihhungleo/your-llm-cant-actually-watch-video-heres-the-smallest-fix-mit-2igp</guid>
      <description>&lt;p&gt;Every model card says "multimodal". Then you hand the model a real video file and discover what that means in practice: ChatGPT reads the subtitle track, Claude doesn't accept video files at all. The model narrates a video it mostly never saw.&lt;/p&gt;

&lt;p&gt;I unpack viral videos daily for my own content work, so I couldn't route around this. I built a small tool instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mechanism
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;claude-real-video&lt;/code&gt; converts a video into three things an LLM can genuinely read:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scene-aware sampled frames&lt;/strong&gt; — ffmpeg scene scores decide where to sample, so you get a frame when the picture changes, not every N seconds. An &lt;code&gt;--adaptive&lt;/code&gt; flag handles slow deformations (a real user bug report: fixed thresholds missed squash/stretch morphs entirely).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timestamped transcript&lt;/strong&gt; — whisper by default; if &lt;code&gt;faster-whisper&lt;/code&gt; is installed it runs in-process and several times faster, with automatic fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One MANIFEST timeline&lt;/strong&gt; — frames and transcript merged into a single file, so the model follows the video in order instead of guessing from fragments. A &lt;code&gt;--text-anchors&lt;/code&gt; flag force-samples frames at subtitle cues so on-screen text never falls between frames.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then you point any LLM at the output folder — Claude, GPT, Gemini, or a local model. No API of mine in the middle, everything runs on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;claude-real-video
crv &lt;span class="s2"&gt;"video.mp4"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Honest limitations
&lt;/h3&gt;

&lt;p&gt;Not real-time — a 90-second video takes about 1–2 minutes all-in on an M-series Mac. Frame sampling can still miss motion between frames; the flags above patch the worst cases, both born from real GitHub issues.&lt;/p&gt;

&lt;p&gt;It's MIT, currently at 1,731 stars with ~8k installs last month, which taught me the problem was never just mine:&lt;br&gt;
&lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The eye corrects the ear: fixing my LLM's video hallucinations with OCR and a VAD gate</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Fri, 17 Jul 2026 09:14:29 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/the-eye-corrects-the-ear-fixing-my-llms-video-hallucinations-with-ocr-and-a-vad-gate-1kea</link>
      <guid>https://dev.to/huangchihhungleo/the-eye-corrects-the-ear-fixing-my-llms-video-hallucinations-with-ocr-and-a-vad-gate-1kea</guid>
      <description>&lt;p&gt;&lt;em&gt;Sequel to &lt;a href="https://dev.to/huangchihhungleo/my-llm-could-not-tell-a-timelapse-from-real-time-so-i-taught-it-physics-5g4c"&gt;My LLM could not tell a timelapse from real time — so I taught it physics&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;crv&lt;/a&gt;, an open-source tool that turns videos into something an LLM can actually read: scene-aware keyframes, a timestamped transcript, and a fused timeline. This week two of its senses started lying to it, and fixing that taught me one lesson worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ear lies: whisper invents captions over music
&lt;/h2&gt;

&lt;p&gt;An 8-second, music-only clip came back with the caption &lt;strong&gt;"I'll see you next time."&lt;/strong&gt; Nobody says anything in the clip. Whisper's decoder has seen too many outros — music at the end of a video "should" have that line, so it writes it.&lt;/p&gt;

&lt;p&gt;The standard fix works: switch to faster-whisper and enable its Silero VAD (voice-activity detection) gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vad_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vad_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_silence_duration_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;condition_on_previous_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segments with no detected speech never reach the model. Nothing goes in, nothing gets invented.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that was mine, not whisper's
&lt;/h2&gt;

&lt;p&gt;Here is the part I have not seen written down anywhere. My pipeline had a fallback: if the fast engine returns nothing, fall back to the whisper CLI. Sounds harmless — until the VAD gate &lt;strong&gt;correctly&lt;/strong&gt; hears no speech and returns an empty segment list. My code read "empty" as "engine failed", fell back to the ungated CLI, and the phantom caption walked right back in through the back door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An empty result is an answer, not an error.&lt;/strong&gt; If you put a gated path in front of a fallback path, make sure the gate's verdict can't be overruled by your own plumbing. My manifest now says &lt;em&gt;"the voice-activity gate heard no speech; music/ambient-only audio"&lt;/em&gt; — an honest sentence instead of a fake caption.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eye corrects the ear: OCR as ground truth
&lt;/h2&gt;

&lt;p&gt;Short-form video is wall-to-wall burned-in captions, and those captions are the video's own script. So crv Pro now OCRs every kept frame and places on-screen text on the same timeline as the ASR transcript.&lt;/p&gt;

&lt;p&gt;Real example from a Chinese video I processed yesterday: whisper heard &lt;strong&gt;猴狼&lt;/strong&gt; ("monkey-wolf" — not a word), while the burned-in caption at the same second clearly said &lt;strong&gt;后浪&lt;/strong&gt; ("the rising generation", the video's whole point). Same timestamp, two readings. The manifest tells the reading LLM: &lt;em&gt;prefer on-screen wording over the ASR transcript for names, numbers and terms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The ear mishears; the eye reads the script. Cross-modal redundancy beats either sense alone.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;VAD-gate your ASR. Hallucinated captions poison everything downstream.&lt;/li&gt;
&lt;li&gt;Audit your fallback paths — a correct empty result must not trigger a fallback to the thing you were protecting against.&lt;/li&gt;
&lt;li&gt;If the video carries its own text, treat it as ground truth and let it correct the transcript.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything above ships in &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;claude-real-video 0.7.13&lt;/a&gt; (free, MIT) and crv Pro 0.8.12. All local — nothing leaves your machine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>whisper</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My LLM could not tell a timelapse from real time — so I taught it physics</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Thu, 16 Jul 2026 14:33:52 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/my-llm-could-not-tell-a-timelapse-from-real-time-so-i-taught-it-physics-5g4c</link>
      <guid>https://dev.to/huangchihhungleo/my-llm-could-not-tell-a-timelapse-from-real-time-so-i-taught-it-physics-5g4c</guid>
      <description>&lt;p&gt;Yesterday I got asked a simple question: "can your tool tell this reel is a timelapse?"&lt;/p&gt;

&lt;p&gt;It could not. The tool (&lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;claude-real-video&lt;/a&gt;, open source, MIT) turns any video into keyframes + a timestamped transcript so an LLM can actually read it. But keyframes alone don't carry playback speed. My model watched a hyperlapse of a guy typing and described it as "a man typing."&lt;/p&gt;

&lt;p&gt;Five hours later there was a working prototype. Here's what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The physics is simple
&lt;/h2&gt;

&lt;p&gt;A video is a &lt;em&gt;sampling of time&lt;/em&gt;. Detecting speed manipulation reduces to one question: does the motion between frames match the time the container claims passed between them?&lt;/p&gt;

&lt;p&gt;Three measurable signals fall out of that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trajectory continuity.&lt;/strong&gt; Post speed-up drops frames from continuously captured footage — motion is fast but trackable. Interval capture (timelapse) never recorded the in-between frames — subjects teleport, optical-flow tracking collapses. Dense frame extraction can recover the former, never the latter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate-frame patterns.&lt;/strong&gt; Slow motion by frame duplication leaves a periodic fingerprint: hold-hold-hold-move. Frame-rate conversion (24→30fps) leaves a different one: one duplicate every five frames. A still slide leaves one long run. Same "duplicate ratio", three different verdicts — run-length structure is the tell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera vs. subject motion.&lt;/strong&gt; Estimate the global affine transform per frame pair (RANSAC over LK tracks), subtract it, and classify what's left. Skip this and a stabilized sped-up walking tour reads as normal — the "speed" was all in the camera channel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the benchmark taught me
&lt;/h2&gt;

&lt;p&gt;I built a labeled corpus the cheap way: took clean YouTube footage, generated known transforms with ffmpeg (2x, 4x, 8x/30x interval sampling, duplicated slow-mo), and ran the classifier against ground truth.&lt;/p&gt;

&lt;p&gt;Results after five iterations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero false positives on clean footage&lt;/strong&gt; — the one metric I refuse to trade away. A forensics tool that cries wolf is worse than no tool.&lt;/li&gt;
&lt;li&gt;Heavy manipulation (30x lapse, padded slow-mo, 4x on visible subjects): caught, with per-segment verdicts.&lt;/li&gt;
&lt;li&gt;Subtle 2x on slow scenes: &lt;strong&gt;missed, and honestly unfixable with displacement statistics alone.&lt;/strong&gt; A slow camera sped up 2x still moves within normal-camera range. You need a reference clock — something in the frame with a known real-world rate. Human gait (~2 steps/s) is the obvious next channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two bugs were more instructive than the wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My own corpus generation manufactured evidence: normalizing 23.976fps film to 30fps created a perfect pulldown pattern that the tool flagged as slow motion. The fix wasn't a threshold — it was teaching the classifier to &lt;em&gt;recognize frame-rate conversion as its own category&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Median motion statistics erased the flagship case. In a reel where typing hands occupy 10% of the frame, the median over 400 tracked corners is the static wall. The manipulation lives in the tail (p90) and in the moving cluster — aggregate stats hide exactly what you're looking for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Never say "normal speed"
&lt;/h2&gt;

&lt;p&gt;The design rule I'm most attached to came from an adversarial review: the tool never outputs "this video is normal speed." It outputs "no reliable evidence of manipulation." Cleanly re-encoded speed-up is theoretically indistinguishable from native low-frame-rate capture — a tool that pretends otherwise is lying. Evidence tiers (strong/moderate/weak/insufficient), never fake certainty.&lt;/p&gt;

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

&lt;p&gt;The prototype (387 lines, OpenCV + ffmpeg, no GPU, ~1s per 10s of video) ships as an opt-in &lt;code&gt;--speed-check&lt;/code&gt; flag in &lt;a href="https://leoaido.com/crv-pro/" rel="noopener noreferrer"&gt;crv Pro&lt;/a&gt; once it passes a benchmark built from real reels and Shorts — because that's what people actually feed these tools, and the current corpus is too polite.&lt;/p&gt;

&lt;p&gt;The free base — scene-aware keyframes + timestamped transcript, 100% local — is here: &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've worked on temporal forensics (SpeedNet, the recent Cornell "Seeing Fast and Slow" work) I'd genuinely like to hear where this naive-physics approach breaks.&lt;/p&gt;

</description>
      <category>aipythonopencvvideo</category>
    </item>
    <item>
      <title>Your LLM isn't watching that video — it's reading the subtitles</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:55:47 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/your-llm-isnt-watching-that-video-its-reading-the-subtitles-1fib</link>
      <guid>https://dev.to/huangchihhungleo/your-llm-isnt-watching-that-video-its-reading-the-subtitles-1fib</guid>
      <description>&lt;p&gt;A few months ago I pasted a YouTube link into an AI chat and asked "what happens in this video?"&lt;/p&gt;

&lt;p&gt;It answered instantly. Confidently. And completely from the &lt;em&gt;transcript&lt;/em&gt;. The video had a sight gag in the middle — the whole point of the clip — and the model had no idea, because nobody ever showed it a single frame.&lt;/p&gt;

&lt;p&gt;That bugged me enough to build &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;claude-real-video&lt;/a&gt; (crv), a small open-source CLI that turns any video into something an LLM can actually read. It hit the Hacker News front page and just passed 1.6k GitHub stars, so I figured it's time to write up how it works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach fails on tokens
&lt;/h2&gt;

&lt;p&gt;The obvious fix is "extract frames, paste them in." But at a fixed 1 fps, a 58-second clip becomes 58 images. Most are near-duplicates of their neighbours, and vision tokens are expensive — you're paying to show the model the same talking head 40 times.&lt;/p&gt;

&lt;p&gt;Fixed-interval sampling has the opposite failure too: a fast cut between two samples just disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  What crv does instead
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"claude-real-video[whisper]"&lt;/span&gt;
crv &lt;span class="s2"&gt;"https://www.youtube.com/watch?v=..."&lt;/span&gt;
&lt;span class="c"&gt;# → crv-out/frames/*.jpg + frames.json + transcript.txt/.json + MANIFEST.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs locally. No ML models to download for the core path — it's ffmpeg doing the heavy lifting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Scene-change detection, not a fixed quota.&lt;/strong&gt; One ffmpeg metadata pass computes a scene score for every frame. Frames are kept where the content actually changes, so that same 58-second clip yields 26 frames instead of 58 — and no cut slips through, because cuts are exactly what scene scores spike on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sliding-window dedup.&lt;/strong&gt; Near-duplicates that survive the threshold get compared against a rolling window and dropped. What's left is the minimal set of frames that differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Contact sheets.&lt;/strong&gt; &lt;code&gt;--grid&lt;/code&gt; packs the survivors into a few labeled grid images. 26 frames become 3 contact sheets. Fewer images, same information, and the timestamps are printed on each cell so the model can reference "at 0:41" correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Timestamped transcript.&lt;/strong&gt; Subtitles when the platform provides them, Whisper when it doesn't — written both as plain text and as &lt;code&gt;transcript.json&lt;/code&gt; with per-segment timestamps, so the frames and the words line up on one timeline.&lt;/p&gt;

&lt;p&gt;The output is one folder with a &lt;code&gt;MANIFEST.txt&lt;/code&gt; on top. Drop it into Claude, ChatGPT or Gemini and ask away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two failure modes that took real users to find
&lt;/h2&gt;

&lt;p&gt;The fixed scene-score threshold turned out to have blind spots, and both fixes came from GitHub issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow morphs never spike.&lt;/strong&gt; An animator reported that a 2-3 second squash-and-stretch never triggered a single frame — no individual frame differs enough from the previous one. &lt;code&gt;--adaptive&lt;/code&gt; fixes this by scoring each frame against its rolling 2-second neighbourhood mean instead of a global constant. Slow change accumulates against the local baseline and gets caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slides don't change when the speaker does.&lt;/strong&gt; In lectures and screen recordings, the picture can sit still for a minute while the audio moves through three ideas. &lt;code&gt;--text-anchors&lt;/code&gt; forces one extra frame at each subtitle-cue timestamp (capped at one per second), so every spoken segment has a matching visual even when scene detection sees nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local matters here
&lt;/h2&gt;

&lt;p&gt;The model never needs the video file — it needs the &lt;em&gt;residue&lt;/em&gt;: which frames changed, what was said, when. That residue is small enough to compute on any laptop with ffmpeg, which means the video itself never has to leave your machine. What goes to a cloud LLM afterwards is only whatever you choose to paste.&lt;/p&gt;

&lt;p&gt;If you're on Claude Code, the repo ships a skill folder — install it and the agent watches videos on its own when you paste a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest footnote
&lt;/h2&gt;

&lt;p&gt;crv is MIT and stays free. I fund the work with a paid add-on (crv Pro) that adds camera-motion and emotion-timeline analysis for creators — the free core is the complete watching pipeline, not a demo.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;br&gt;
PyPI: &lt;a href="https://pypi.org/project/claude-real-video/" rel="noopener noreferrer"&gt;https://pypi.org/project/claude-real-video/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;— Leo Huang (黃志弘, LeoAido), building a one-person company with an AI team.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Your LLM isn't watching the video. It's reading subtitles.</title>
      <dc:creator>Leo Huang</dc:creator>
      <pubDate>Fri, 03 Jul 2026 21:57:26 +0000</pubDate>
      <link>https://dev.to/huangchihhungleo/your-llm-isnt-watching-the-video-its-reading-subtitles-2jjl</link>
      <guid>https://dev.to/huangchihhungleo/your-llm-isnt-watching-the-video-its-reading-subtitles-2jjl</guid>
      <description>&lt;p&gt;Paste a YouTube link into ChatGPT and ask "what's this video about?" — you'll get an answer. But here's the thing: it read the transcript. The slides, the live demo, the thing the presenter actually showed on screen? All thrown away.&lt;/p&gt;

&lt;p&gt;I found this out the hard way, and it bugged me enough to build a tool for it. Last week it hit the front page of Hacker News and just passed 500 GitHub stars, so I figured I'd write down how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of "AI watching video" today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; won't accept a video file at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT&lt;/strong&gt; takes a YouTube link, reads the subtitles, and answers from those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; genuinely reads video — but it samples at a fixed interval (1 fps by default), so fast cuts slip between samples while a 10-minute static slide burns 600 near-identical frames. And your footage goes to the cloud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For talks, tutorials, and demos — where most of the value is on screen, not in the audio — none of these actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude-real-video&lt;/code&gt; takes a URL or a local file and produces a folder any LLM can read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;claude-real-video
crv &lt;span class="s2"&gt;"https://www.youtube.com/watch?v=..."&lt;/span&gt; &lt;span class="nt"&gt;--grid&lt;/span&gt;
&lt;span class="c"&gt;# → crv-out/frames/  +  transcript.txt  +  MANIFEST.txt  +  grids/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ideas, all boring on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grab a frame only when the picture actually changes.&lt;/strong&gt; Scene-change detection instead of a fixed sampling interval — a 10-minute static slide collapses to one frame, a rapid-fire edit keeps every cut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop what the model already saw.&lt;/strong&gt; A sliding-window dedup compares each new frame against the last few kept ones, so an A-B-A cutaway doesn't send shot A twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the model what it's looking at.&lt;/strong&gt; One MANIFEST.txt lists every frame with its timestamp, aligned with the Whisper transcript.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real numbers from a 58-second clip: fixed 1 fps sampling gives you 58 frames; this keeps the 26 that actually differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Keyframes are not video"
&lt;/h2&gt;

&lt;p&gt;Fairest criticism I got on HN. A stack of stills loses motion and order. v0.4.0's answer is &lt;code&gt;--grid&lt;/code&gt;: it packs consecutive keyframes into 3x3 contact sheets, so the model reads a chronological sequence instead of scattered images — and you send 9x fewer images while you're at it.&lt;/p&gt;

&lt;p&gt;It still won't recover true motion or object permanence — I'd rather say that plainly than oversell it. (I'm exploring measured motion data — camera moves, cut rhythm — as a paid add-on called crv Pro, but the free tool stands on its own.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything runs locally
&lt;/h2&gt;

&lt;p&gt;ffmpeg + faster-whisper on your machine. Nothing is uploaded by the tool — what reaches an LLM is only what you choose to paste into one afterwards. MIT licensed.&lt;/p&gt;

&lt;p&gt;If you use Claude Code, there's a ready-made skill in the repo — drop it into &lt;code&gt;~/.claude/skills&lt;/code&gt; and Claude will run the whole pipeline itself when you paste a video link.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/HUANGCHIHHUNGLeo/claude-real-video" rel="noopener noreferrer"&gt;https://github.com/HUANGCHIHHUNGLeo/claude-real-video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm Leo — a liberal-arts founder running a one-person company with an AI team. Happy to answer anything about the approach.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
