Imported from novoads/agent-skills (
shared/skills/broll-overlay/SKILL.md). Install upstream withnpx skills add novoads/agent-skills --skill broll-overlay. Copyright stays with the author.
B-roll overlay
Lays silent cutaway clips over a finished base video: the base audio runs untouched underneath, the picture cuts away and returns, and the final duration equals the base duration exactly. That is the whole contract — overlay, never concatenation. A previous run concatenated: 24s for a 15s base, voice dead 12s.
Assembly mechanics live entirely in scripts/broll_overlay.py; this file is
judgment only — when to enter, where the windows go, what the user sees first.
Entry condition
Two things must already exist. This skill never generates the base — if there isn't one yet, say so and stop.
-
A finished, approved base video with exactly one audio stream. Approved means the user has watched it and signed off — b-roll is a polish pass over a locked cut. The script stream-copies one voice track under the picture, so a two-stream base is rejected:
ffmpeg -i base -map 0:v -map 0:a:0 -c copy. -
Its transcript, with timings. One call to
POST /v1/transcriptswith the base'sjobId(or itsassetIdif it was uploaded). It returnstext, word-levelwords[]andsegments[]— timings in SECONDS, which is what the EDL below takes — plus ansrtyou can ignore here. Price it with the transcript arm ofPOST /v1/estimatesand quote that number, never one from this file. The meter is per minute of source, rounded up from a one-minute minimum, and a repeat of the same source is free, so asking twice costs once. No local install.(The captions endpoint still cannot supply this — it burns subtitles into a new MP4 and returns no text or timings, verified 2026-08-04 and re-checked when transcripts shipped. That finding is correct and is kept here so nobody re-discovers it and files a bug against captions.)
Offline fallback: a local whisper pass (
whisper-clioropenai-whisper) still works and is the path when there is no API key or no network. Note whisper reports timings in milliseconds where the API reports seconds, andwhisper-cliwith no model downloaded returns an EMPTY transcript rather than an error — which reads exactly like a bad render. See README for the model download.Placement is read from the transcript, not eyeballed.
REST key required. A Novoads MCP connector is not a substitute. If
NOVOADS_API_KEYis missing or still the placeholder, stop before any generation work and tell the user: "Before continuing, create an API key at https://novoads.ai/dashboard/settings?tab=api and paste it into.env." That holds even whenmcp__novoads__*tools are connected and authenticated in the session. Never callmcp__novoads__*tools from this repo's workflows: they are a different surface with different behavior, including the units they quote costs in. Repo installs verify with./scripts/check-novoads-env.sh; a solo install checksNOVOADS_API_KEYin the environment.
Pack version. Every /v1 response carries X-Novoads-Pack-Version; mention a newer pack at https://github.com/novoads/agent-skills only when that header names a version NEWER than this file's metadata.packVersion — equal or older is nothing to say, and it is never a reason to stop.
Workflow
- Read the transcript. Note what is said and when — the words drive every decision below.
- Generate the b-roll clips, only after base approval. One clip per moment
you intend to cover, each rendered silent (
audioEnabled: false). Overlay audio is ignored by the script regardless, and generating before approval spends credits on a cut that may still change. Price the set live first —POST /v1/estimatesthis session, the total shown against the user's balance, an explicit yes. Four cutaways is four charges, and a remembered price is not a price. Cast the base actor in every cutaway that shows a person: pass the samereferenceAssetIdsthe base used — Seedance re-casts every render, and a stranger's hands or skin breaks the ad (observed live 2026-08-04: Black base actor, five white-cast cutaways). With no identity ref, keep people out of frame entirely — objects and environments, not "face out of frame". - Propose an EDL. JSON with
base,output, andoverlays(file,start,end,covers).coversis mandatory and quotes the spoken line the window illustrates — a window you can't quote is one you're guessing at. - Show the EDL to the user and wait for approval. Rendering before the user has seen the plan is precisely the failure this step prevents.
- Render:
python3 scripts/broll_overlay.py edl.json - Report the script's verification output verbatim — the measured durations, the audio check and the window count, not a paraphrase. Then offer 2–3 cut variations (different windows, fewer cutaways, a tighter opener) to react to.
Placement judgment
Default cadence, measured 2026-08-04 frame by frame from the reference edit this pack reproduces — ~15.5s, 10–11 shots, a cut every ~1.4s:
- Alternate A-B-A-B: the face returns between every cutaway. Never two overlay windows back to back; the talking head is the spine.
- 4–6 windows per 15s, ~1–1.5s each, ~40–45% coverage. Short and frequent. This retires our older "fewer, longer windows" rule — taste, reversed by measurement; our own two renders ran 2 windows of 2.5s/2.0s, ~30% coverage.
- The b-roll may travel; the base never does. The reference visits 4 distinct b-roll settings across 2–3 rooms; the talking head never moves.
- Never cover the opening hook beat. The first beat is a face making a claim; cutting away there spends the retention the hook just bought.
- End on the person, not on b-roll. The closing beat is the verdict, and it only lands if the viewer is looking at whoever delivers it.
- Put product shots where the product is being spoken about. B-roll that illustrates the current sentence reads as evidence; anything else is filler.
Default shot plan. The reference's five cutaways tell one arc: problem → stress → product macro → dose/usage → relief — 2 product shots to 3 emotional beats. Adapt the imagery to the script, keep the ratio.
Evidence: one measured edit (n=1) — a default, not a law; depart deliberately and
say why (evals.md OV3/OV6). Every run prints its own cadence against this
envelope; --stats prints it without rendering.
Hard rule: assembly always goes through the script
Never hand-write overlay or concat ffmpeg for this task. broll_overlay.py owns
validation, rendering and verification, and it fails loudly on exactly what
fails silently by hand: duration drift, re-encoded audio, overlapping or
zero-frame windows, a clip shorter than its window, a rotated phone base
composited at the wrong geometry, a window that quietly composited nothing.
Improvised ffmpeg is how the founding failure happened.
It needs ffmpeg 7.1 or newer. The script scales each overlay against the base
branch with scale=w=rw:h=rh, and those reference constants arrived in 7.1. On an
older build — Ubuntu 24.04's apt ships 6.1 — the graph does not parse and the
error names rw, not a version. Check with ffmpeg -version before blaming the
EDL; on Debian/Ubuntu the fix is a static build, not apt install ffmpeg.
Escape hatch: if a request genuinely doesn't fit the EDL model — a speed ramp, a picture-in-picture inset, audio that actually needs editing — say so and ask the user how to proceed. Do not improvise around the script.
Other modes
python3 scripts/broll_overlay.py edl.json --dry-run— validate the EDL and print the plan, rendering nothing. Cheap way to check window geometry.python3 scripts/broll_overlay.py edl.json --stats— window count, lengths, coverage, base-return gaps, each marked against the envelope. Never an error.python3 scripts/broll_overlay.py --verify-only FINAL --base BASE [--edl edl.json]— re-check any output against its base, including one from an old session. Pass--edlwhenever you have it: without it the check is duration + audio only, which a plain copy of the base also passes; with it each window's midpoint frame is compared against the base, so a window that composited nothing fails. A run without--edlsays so out loud.
Exit codes: 2 validation, 3 render, 4 verification. A nonzero exit is a
real failure — surface it, don't retry blindly. Renders go to a hidden temp file
beside the output, renamed only after verification passes: a failed run never
leaves a half-written or unverified file at the output path.
Evals
evals.md defines the six scenarios this skill is held to (OV1–OV6).
scripts/test_broll_overlay.py implements the mechanical ones against synthetic
fixtures — run it after touching the script.