Skip to content

fix(skills): caption burn works on FFmpeg 9 and silent takes - #556

Open
TobiOnabolu wants to merge 1 commit into
superdesigndev:mainfrom
TobiOnabolu:fix/ugc-caption-ffmpeg9-and-silent-input
Open

TobiOnabolu wants to merge 1 commit into
superdesigndev:mainfrom
TobiOnabolu:fix/ugc-caption-ffmpeg9-and-silent-input

Conversation

@TobiOnabolu

@TobiOnabolu TobiOnabolu commented Sep 17, 2026

Copy link
Copy Markdown

What this does

Fixes two independent faults in caption_burn.py (the ugc-talking-head-video skill's caption step),
each of which aborted a render outright on a current machine. Both are now covered by tests.

FFmpeg 9 removed -filter_complex_script. Every burn died with Unrecognized option 'filter_complex_script' before a frame was encoded. Worth recording why this is not a one-line flag
bump: the replacement the release points at, -/filter_complex, does not appear in
ffmpeg -h full, so no capability probe finds it by inspection — and a naive substring probe
matches -filter_complex inside -/filter_complex and silently returns the wrong flag (I hit
exactly this while fixing it). Passing the graph inline sidesteps the version question entirely:
every version accepts it, the argv list reaches exec directly so there is no escaping step, and a
caption graph is far under ARG_MAX.

A silent take has no audio stream, but the render mapped 0:a unconditionally. ffmpeg aborted
with Stream map '' matches no streams. The sting is that the skill's own silent mode emits exactly
that video-only file, so captions could not be burned onto the output that mode produces. Audio is
now copied only when a stream is actually present.

How it was tested

tests/test_ugc_caption_burn.py — 5 passed:

tests/test_ugc_caption_burn.py::test_graph_is_passed_inline_not_via_a_script_file PASSED
tests/test_ugc_caption_burn.py::test_audio_is_not_mapped_for_a_silent_take PASSED
tests/test_ugc_caption_burn.py::test_audio_is_copied_when_the_take_has_a_stream PASSED
tests/test_ugc_caption_burn.py::test_output_maps_the_final_chained_overlay PASSED
tests/test_ugc_caption_burn.py::test_has_audio_reads_ffprobe_output PASSED

To make the command checkable without ffmpeg, the render argv moves into ffmpeg_command() and the
audio probe into has_audio(); the assertions read the argv directly. Pillow is imported lazily —
it is only needed to draw, and the module-level import made the script unimportable anywhere
without it.

Also rendered a real 12 s 9:16 take end to end and inspected it:

  • output duration 12.041667 s, matching the source
  • ffprobe confirms a silent input still yields a captioned output (0 audio streams in, 0 out)
  • frame at t=3.0s visually checked: header at 12% height, caption at 78%, white Helvetica Bold with
    dark outline, source video otherwise untouched

Not run: the full suite. PyPI's CDN (files.pythonhosted.org) is unreachable from this machine,
so uv sync cannot resolve the dev dependencies and the repo's conftest.py (which imports FastAPI)
does not load. The 5 tests above were run under real pytest 9.0.3 against an isolated copy of the
test file. Treat CI as the authority on the whole suite; if the run is blocked pending approval,
that is the sandbox, not a failure.

Checklist

  • uv run --with pytest-xdist pytest -n auto -q passes locally — could not run in full, see above
  • Added or updated tests for the change
  • Updated the relevant docs/context/ fragment — not applicable; no subsystem changed, the
    script is a standalone skill asset and no fragment documents it
  • No secrets in the diff

`caption_burn.py` from the `ugc-talking-head-video` skill could not complete a
single render on a current machine, for two independent reasons. Both are fixed,
and both are pinned by tests so they cannot regress silently.

FFmpeg 9 removed `-filter_complex_script`, so every burn died with "Unrecognized
option" before a frame was encoded. The release's replacement, `-/filter_complex`,
does not appear in `ffmpeg -h full` — a capability probe has to execute ffmpeg to
find it, and a naive substring probe matches `-filter_complex` inside it and picks
the wrong flag. The graph is passed inline instead, which every version accepts,
needs no escaping (the argv list goes straight to exec), and is far under ARG_MAX
at caption-graph sizes.

The render also mapped `0:a` unconditionally. A silent take has no audio stream,
and the skill's own silent mode emits exactly that video-only file — so captions
could not be burned onto the output that mode produces: ffmpeg aborted with
`Stream map '' matches no streams`. Audio is now copied only when a stream exists.

The render argv moves into `ffmpeg_command()` and the audio check into
`has_audio()`, so both are verifiable without ffmpeg. Pillow is imported lazily:
it is only needed to draw, and requiring it at import time made the module
unimportable in an environment without it.

Verified by rendering a real 12 s 9:16 take end to end — header at 12% height,
captions at 78%, source audio untouched — and by `ffprobe` on the output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant