broll-composite
Applies the cutaways chosen by broll-pick. For each pick [t0,t1] the whole
1080×1920 frame is replaced by the B-roll (hard cut, no transition). The 16:9
source is scaled to COVER and SUBJECT-cropped to 1080×1920 — full-bleed, never
letterboxed (same philosophy as fill-vertical). The crop offset on the slack
axis follows the cutaway's subject (a face if present, else a person, else the
thresholded multi-frame saliency mass), so the subject reads centered instead of
sliced off to one side. The podcast audio stays
continuous for the whole clip; B-roll audio is dropped. No transition SFX by
default — audio stays pure stream-copy. BROLL_SFX=1 re-enables a 0.3s
synthesized whoosh (make_whoosh.py, ~-15dBFS under speech) that sweeps up into
each cutaway and down out of it. Runs BEFORE
burn-subtitles so karaoke captions render on top of the cutaways.
Invoke
.claude/skills/broll-composite/broll-composite.sh <in_clip.mp4> <broll_plan.json> <out.mp4>
in_clip: the vertical 1080×1920 clip (from fill-vertical)broll_plan.json: from broll-pickout: composited clip
Behavior
- Zero picks, or every
clip_pathmissing → copy-passthrough, exit 0. - A pick whose
clip_pathno longer exists is skipped; remaining picks apply. - ffmpeg failure → falls back to passthrough (never blocks the pipeline).
Idempotency
Caches on in_clip+plan mtimes + BROLL_SFX via <out>.compmeta.
Notes
- Subject-cover crop reuses fill-vertical's framing engine (
build_filter.pyimportsfill_verticalfor face/pose detection): per cutaway it samples 5 frames and picks the slack-axis offset face -> person -> thresholded multi-frame saliency. Still COVER (no extra punch-in) — only WHERE the crop sits changes. Degrades to a centered crop when MediaPipe/cv2 are unavailable. - Whole video is re-encoded (overlay can't partial stream-copy); audio is
amix'd with the whoosh bed to AAC 192k (or
-c:a copywhen BROLL_SFX=0 / no picks). Uses_lib/encode.sh(VideoToolbox/x264) + thread caps.