DEV Community

mage0535
mage0535

Posted on

How I Fixed a Silent Content-Truncation Bug in My Video Pipeline

How I Fixed a Silent Content-Truncation Bug in My Video Pipeline

Last week I shipped several short videos across 9 platforms. Everything passed quality gates. The videos looked fine, the audio was mixed correctly, the subtitles were burned in.

Then a user asked: "Why does every video have the same topic?" — and while re-auditing, I found something much worse hiding underneath: my pipeline was silently dropping ~60% of the script content in every video it rendered.

This is the story of that bug — why it was invisible, how I found it, and the guard I built so it can't happen again.

The symptom

I write scripts as 8 paragraphs, one per video card. A typical script looks like:

Paragraph 1: Monthly spreadsheet cleanup is a nightmare...
Paragraph 2: Here's what AI actually handles well...
Paragraph 3: The 3-step method...
... (8 paragraphs total, ~600-700 chars)
Enter fullscreen mode Exit fullscreen mode

After rendering, I check the output video. It's 50 seconds, 8 cards, audio mixed, subtitles burned, all quality gates pass. Looks done.

But when I measured actual content coverage — the sum of the text that made it into the voiceover vs. the script — I got 35%. The renderer was only using the first 8 sentences of a 20-sentence script.

The root cause

The pipeline splits a script into "beats" (one beat per video card) with this regex:

parts = re.split(r"\n+|[。.!?;;]", script)
beats = [part for part in parts if len(part) >= 8][:10]
Enter fullscreen mode Exit fullscreen mode

It splits on sentence punctuation — periods, question marks, Chinese full stops. My 8 paragraphs average 2-3 sentences each, so the script became ~20 short fragments, and the code took the first 10. Everything after sentence 10 — the lessons, the traps, the call-to-action — silently vanished.

Why didn't the quality gate catch it? Because the gate checks output artifacts (audio present, subtitles burned, duration in range). It never checks whether the content is complete. A 50-second video with 8 cards passes every gate even if it's missing half its message.

The fix

The correct way to split a script into beats is by paragraph (empty-line separated), not by sentence:

paragraphs = [p.strip() for p in re.split(r"\n\s*\n", script) if p.strip()]
if len(paragraphs) >= 2:
    beats = paragraphs[:10]   # 8 paragraphs → 8 beats, one per card
Enter fullscreen mode Exit fullscreen mode

Now an 8-paragraph script produces 8 complete beats. Content coverage went from 35% to 97%.

I applied the same fix to the landscape renderer, which had a similar line-based split that would truncate any paragraph containing internal newlines.

The guard: content-coverage check

The most important part isn't the fix — it's preventing regression. I added a content-coverage assertion to the pipeline:

import json
cards = json.load(open('render/cards.json'))
tts_chars = sum(len(str(c.get('tts', ''))) for c in cards)
script_chars = len(open('script.md').read())
coverage = tts_chars / script_chars
assert coverage > 0.9, f"content truncated: only {coverage:.0%} of script reached voiceover"
Enter fullscreen mode Exit fullscreen mode

Any future change that truncates content fails CI immediately.

Lessons

  1. Gates that check artifacts don't check semantics. A pipeline can pass every quality check while losing meaning. Measure the thing that matters — content coverage, not just file presence.
  2. Splitting text is a correctness decision, not a formatting one. Choose the split unit (paragraph vs sentence) based on what the consumer needs, and verify the full text survives.
  3. A good regression guard is a coverage ratio, not a spot check. content_chars / source_chars > 0.9 is a cheap assertion that catches whole classes of truncation bugs.

If you build any text-to-media pipeline — video, slides, podcast — add a content-coverage check today. It takes five minutes and will save you from shipping silent, half-finished content.

Top comments (0)