<?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: Pawpi team</title>
    <description>The latest articles on DEV Community by Pawpi team (@pawpiteam).</description>
    <link>https://dev.to/pawpiteam</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%2F4236740%2F39d8c842-60a5-4107-b566-0d9145f1153b.png</url>
      <title>DEV Community: Pawpi team</title>
      <link>https://dev.to/pawpiteam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawpiteam"/>
    <language>en</language>
    <item>
      <title>Checking a word-reveal caption pipeline without mistaking hashes for review</title>
      <dc:creator>Pawpi team</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:49:00 +0000</pubDate>
      <link>https://dev.to/pawpiteam/checking-a-word-reveal-caption-pipeline-without-mistaking-hashes-for-review-4cp0</link>
      <guid>https://dev.to/pawpiteam/checking-a-word-reveal-caption-pipeline-without-mistaking-hashes-for-review-4cp0</guid>
      <description>&lt;p&gt;A short video can decode correctly while its captions are wrong. The useful engineering split is to check file identity, timeline boundaries, audio preservation and visual behavior separately. Here is one concrete 24 fps caption pipeline, including the limits of its checks.&lt;/p&gt;

&lt;p&gt;Generated with AI from Pawpi’s caption code and production records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep frame coordinates explicit
&lt;/h2&gt;

&lt;p&gt;The examined vertical clip retains source frames [94, 475), using an exclusive end. Its length is 475 − 94 = 381 frames, or 15.875 seconds. The first retained story frame becomes output frame zero. An opening of 94 frames at 24 fps is 3.916666… seconds; rounding that to 3.9 before rebasing introduces avoidable error.&lt;/p&gt;

&lt;p&gt;For a cue wholly inside this single continuous retained interval:&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;source_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;94&lt;/span&gt;
&lt;span class="n"&gt;source_end_exclusive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;475&lt;/span&gt;
&lt;span class="n"&gt;short_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_end_exclusive&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;source_start&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;short_frames&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;381&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rebase_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;source_start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;source_frame&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;source_end_exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Frame is outside the retained range"&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;source_frame&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;source_start&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;rebase_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;124&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a small illustrative calculation, not the full production editor. Cues crossing a cut need explicit trimming or resegmentation. Multiple retained ranges need a piecewise map. Do not apply this one subtraction indiscriminately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve measured word positions
&lt;/h2&gt;

&lt;p&gt;The first cue in the actual timing file has an output interval [30, 59). Its four word onsets are 30, 35, 42 and 47. The source metadata retains alignment methods and source timestamps. The renderer requires nonempty word timing, matching displayed tokens, ordered onsets and speech ends within each cue. It also refuses to silently shrink a phrase beyond its two-line layout limit.&lt;/p&gt;

&lt;p&gt;The existing implementation uses Python assertions for these guards. That is a limitation: Python optimization can disable assertions. A production invocation must not enable optimization; a reusable validator should use explicit exceptions for input validation. These records and guards do not prove that the alignment sounds correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare audio separately from the MP4
&lt;/h2&gt;

&lt;p&gt;The clean and captioned video necessarily have different whole-file hashes. To check that adding captions preserved the compressed audio payload, the existing renderer copies the audio stream and hashes the selected audio stream with FFmpeg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg -v error -i clean.mp4 -map 0:a:0 -c copy -f &lt;span class="nb"&gt;hash&lt;/span&gt; -hash sha256 -
ffmpeg -v error -i captioned.mp4 -map 0:a:0 -c copy -f &lt;span class="nb"&gt;hash&lt;/span&gt; -hash sha256 -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;clean.mp4&lt;/code&gt; and &lt;code&gt;captioned.mp4&lt;/code&gt; are descriptive placeholders for the two actual files. Rerunning these commands on the project files produced the same audio hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SHA256=c800875ad8cd4e355a2b1c11fba320cc36f0faa88009a20d1c9a87242e4a8399
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That supports unchanged selected audio bytes across these two files. It does not prove perfect audiovisual synchronization, source-to-master editing accuracy, or identical container timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode and inspect the resulting stream
&lt;/h2&gt;

&lt;p&gt;The captioned file also passed a complete decode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg -v error -i captioned.mp4 -f null -
ffprobe -v error -select_streams v:0   -show_entries &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;width,height,nb_frames,avg_frame_rate,duration   -of json captioned.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The observed video was 1080×1920 at 24/1 fps, with 381 frames and a 15.875-second duration. The timing file contained eight cues and 28 words. File hashes for the clean clip, captioned clip, timing JSON and SRT matched their existing manifest entries.&lt;/p&gt;

&lt;p&gt;The renderer uses a transparent caption strip, reveals each word at its onset, and positions the whole phrase consistently. It quantizes overlay time to the nearest frame before choosing a cue-specific vertical position; this avoids a decimal-time comparison choosing the default position at a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the remaining review visible
&lt;/h2&gt;

&lt;p&gt;A successful decode and matching hashes cannot tell you whether text covers an eye or whether a word appears when it is heard. Frame samples were inspected for this note; a new complete listening review was not performed. The checks support reproducibility and audio preservation, while motion, placement and listening remain distinct review tasks. No performance improvement or production-time saving is claimed.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://ffmpeg.org/ffmpeg.html" rel="noopener noreferrer"&gt;FFmpeg stream selection and stream copy&lt;/a&gt;, &lt;a href="https://ffmpeg.org/ffmpeg-formats.html#hash-1" rel="noopener noreferrer"&gt;FFmpeg hash muxer&lt;/a&gt;, &lt;a href="https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement" rel="noopener noreferrer"&gt;Python assert statement&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
