Broll overlay skill

Illustration · not a resultFrom the novoads/agent-skills README — shows the whole collection, not only this skill.source ↗
Overlays silent b-roll cutaway clips onto a finished base video — the base audio keeps running underneath while the picture cuts away and returns, and the final duration always equals the base duration.
by novoads·MIT license·★ 23 Stars on the repo·GitHub ↗
npx degit novoads/agent-skills/shared/skills/broll-overlay#main ~/.claude/skills/broll-overlayChecked ·commit main

Illustration · not a resultFrom the novoads/agent-skills README — shows the whole collection, not only this skill.view on GitHub
Files of Broll overlay
Show the full text164 lines
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.
| 1 | |
| 2 | name broll-overlay |
| 3 | metadata {packVersion: 1.3.0} |
| 4 | description >- |
| 5 | Overlays silent b-roll cutaway clips onto a finished base video — the base |
| 6 | audio keeps running underneath while the picture cuts away and returns, and |
| 7 | the final duration always equals the base duration. Use when the user asks to |
| 8 | add b-roll, add cutaways, or overlay clips on a video: "add b-roll to this", |
| 9 | "cut away to a product shot here", "overlay these clips on my UGC ad". Entered |
| 10 | from an approved base video plus its transcript, which comes from |
| 11 | POST /v1/transcripts and needs no local install; it never generates the base |
| 12 | itself and it never extends a video. |
| 13 | |
| 14 | |
| 15 | # B-roll overlay |
| 16 | |
| 17 | Lays silent cutaway clips **over** a finished base video: the base audio runs |
| 18 | untouched underneath, the picture cuts away and returns, and the final duration |
| 19 | equals the base duration exactly. That is the whole contract — overlay, never |
| 20 | concatenation. A previous run concatenated: 24s for a 15s base, voice dead 12s. |
| 21 | |
| 22 | Assembly mechanics live entirely in `scripts/broll_overlay.py`; this file is |
| 23 | judgment only — when to enter, where the windows go, what the user sees first. |
| 24 | |
| 25 | ## Entry condition |
| 26 | |
| 27 | Two things must already exist. This skill never generates the base — if there |
| 28 | isn't one yet, say so and stop. |
| 29 | |
| 30 | **A finished, approved base video with exactly one audio stream.** Approved |
| 31 | means the user has watched it and signed off — b-roll is a polish pass over a |
| 32 | locked cut. The script stream-copies one voice track under the picture, so a |
| 33 | two-stream base is rejected: `ffmpeg -i base -map 0:v -map 0:a:0 -c copy`. |
| 34 | **Its transcript, with timings.** One call to **`POST /v1/transcripts`** with |
| 35 | the base's `jobId` (or its `assetId` if it was uploaded). It returns `text`, |
| 36 | word-level `words[]` and `segments[]` — **timings in SECONDS**, which is what |
| 37 | the EDL below takes — plus an `srt` you can ignore here. **Price it with the |
| 38 | transcript arm of `POST /v1/estimates` and quote that number**, never one from |
| 39 | this file. The meter is per minute of source, rounded up from a one-minute |
| 40 | minimum, and **a repeat of the same source is free**, so asking twice costs |
| 41 | once. No local install. |
| 42 | |
| 43 | *(The **captions** endpoint still cannot supply this — it burns subtitles into |
| 44 | a new MP4 and returns no text or timings, verified 2026-08-04 and re-checked |
| 45 | when transcripts shipped. That finding is correct and is kept here so nobody |
| 46 | re-discovers it and files a bug against captions.)* |
| 47 | |
| 48 | **Offline fallback:** a local whisper pass (`whisper-cli` or `openai-whisper`) |
| 49 | still works and is the path when there is no API key or no network. Note |
| 50 | whisper reports timings in **milliseconds** where the API reports seconds, and |
| 51 | `whisper-cli` with no model downloaded returns an EMPTY transcript rather than |
| 52 | an error — which reads exactly like a bad render. See README for the model |
| 53 | download. |
| 54 | |
| 55 | Placement is read from the transcript, not eyeballed. |
| 56 | |
| 57 | > **REST key required. A Novoads MCP connector is not a substitute.** If |
| 58 | > `NOVOADS_API_KEY` is missing or still the placeholder, stop before any |
| 59 | > generation work and tell the user: "Before continuing, create an API key at |
| 60 | > <https://novoads.ai/dashboard/settings?tab=api> and paste it into `.env`." |
| 61 | > That holds even when `mcp__novoads__*` tools are connected and authenticated in |
| 62 | > the session. Never call `mcp__novoads__*` tools from this repo's workflows: they |
| 63 | > are a different surface with different behavior, including the units they quote |
| 64 | > costs in. Repo installs verify with `./scripts/check-novoads-env.sh`; a solo |
| 65 | > install checks `NOVOADS_API_KEY` in the environment. |
| 66 | |
| 67 | **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. |
| 68 | |
| 69 | ## Workflow |
| 70 | |
| 71 | **Read the transcript.** Note what is said and when — the words drive every |
| 72 | decision below. |
| 73 | **Generate the b-roll clips, only after base approval.** One clip per moment |
| 74 | you intend to cover, each rendered silent (`audioEnabled: false`). Overlay |
| 75 | audio is ignored by the script regardless, and generating before approval |
| 76 | spends credits on a cut that may still change. |
| 77 | **Price the set live first — `POST /v1/estimates` this session, the total |
| 78 | shown against the user's balance, an explicit yes.** Four cutaways is four |
| 79 | charges, and a remembered price is not a price. |
| 80 | **Cast the base actor in every cutaway that shows a person:** pass the same |
| 81 | `referenceAssetIds` the base used — Seedance re-casts every render, and a |
| 82 | stranger's hands or skin breaks the ad (observed live 2026-08-04: Black base |
| 83 | actor, five white-cast cutaways). With no identity ref, keep people out of |
| 84 | frame entirely — objects and environments, not "face out of frame". |
| 85 | **Propose an EDL.** JSON with `base`, `output`, and `overlays` (`file`, |
| 86 | `start`, `end`, `covers`). `covers` is mandatory and **quotes the spoken line |
| 87 | the window illustrates** — a window you can't quote is one you're guessing at. |
| 88 | **Show the EDL to the user and wait for approval.** Rendering before the user |
| 89 | has seen the plan is precisely the failure this step prevents. |
| 90 | **Render:** `python3 scripts/broll_overlay.py edl.json` |
| 91 | **Report the script's verification output verbatim** — the measured durations, |
| 92 | the audio check and the window count, not a paraphrase. Then offer 2–3 cut |
| 93 | variations (different windows, fewer cutaways, a tighter opener) to react to. |
| 94 | |
| 95 | ## Placement judgment |
| 96 | |
| 97 | Default cadence, measured 2026-08-04 frame by frame from the reference edit this |
| 98 | pack reproduces — ~15.5s, 10–11 shots, a cut every ~1.4s: |
| 99 | |
| 100 | **Alternate A-B-A-B: the face returns between every cutaway.** Never two |
| 101 | overlay windows back to back; the talking head is the spine. |
| 102 | **4–6 windows per 15s, ~1–1.5s each, ~40–45% coverage.** Short and frequent. |
| 103 | This retires our older "fewer, longer windows" rule — taste, reversed by |
| 104 | measurement; our own two renders ran 2 windows of 2.5s/2.0s, ~30% coverage. |
| 105 | **The b-roll may travel; the base never does.** The reference visits 4 |
| 106 | distinct b-roll settings across 2–3 rooms; the talking head never moves. |
| 107 | **Never cover the opening hook beat.** The first beat is a face making a |
| 108 | claim; cutting away there spends the retention the hook just bought. |
| 109 | **End on the person, not on b-roll.** The closing beat is the verdict, and it |
| 110 | only lands if the viewer is looking at whoever delivers it. |
| 111 | **Put product shots where the product is being spoken about.** B-roll that |
| 112 | illustrates the current sentence reads as evidence; anything else is filler. |
| 113 | |
| 114 | **Default shot plan.** The reference's five cutaways tell one arc: problem → |
| 115 | stress → product macro → dose/usage → relief — 2 product shots to 3 emotional |
| 116 | beats. Adapt the imagery to the script, keep the ratio. |
| 117 | |
| 118 | Evidence: one measured edit (n=1) — a default, not a law; depart deliberately and |
| 119 | say why (evals.md OV3/OV6). Every run prints its own cadence against this |
| 120 | envelope; `--stats` prints it without rendering. |
| 121 | |
| 122 | ## Hard rule: assembly always goes through the script |
| 123 | |
| 124 | Never hand-write overlay or concat ffmpeg for this task. `broll_overlay.py` owns |
| 125 | validation, rendering and verification, and it fails loudly on exactly what |
| 126 | fails silently by hand: duration drift, re-encoded audio, overlapping or |
| 127 | zero-frame windows, a clip shorter than its window, a rotated phone base |
| 128 | composited at the wrong geometry, a window that quietly composited nothing. |
| 129 | Improvised ffmpeg is how the founding failure happened. |
| 130 | |
| 131 | **It needs ffmpeg 7.1 or newer.** The script scales each overlay against the base |
| 132 | branch with `scale=w=rw:h=rh`, and those reference constants arrived in 7.1. On an |
| 133 | older build — Ubuntu 24.04's apt ships 6.1 — the graph does not parse and the |
| 134 | error names `rw`, not a version. Check with `ffmpeg -version` before blaming the |
| 135 | EDL; on Debian/Ubuntu the fix is a static build, not `apt install ffmpeg`. |
| 136 | |
| 137 | **Escape hatch:** if a request genuinely doesn't fit the EDL model — a speed |
| 138 | ramp, a picture-in-picture inset, audio that actually needs editing — say so and |
| 139 | ask the user how to proceed. Do not improvise around the script. |
| 140 | |
| 141 | ## Other modes |
| 142 | |
| 143 | `python3 scripts/broll_overlay.py edl.json --dry-run` — validate the EDL and |
| 144 | print the plan, rendering nothing. Cheap way to check window geometry. |
| 145 | `python3 scripts/broll_overlay.py edl.json --stats` — window count, lengths, |
| 146 | coverage, base-return gaps, each marked against the envelope. Never an error. |
| 147 | `python3 scripts/broll_overlay.py --verify-only FINAL --base BASE [--edl edl.json]` |
| 148 | — re-check any output against its base, including one from an old session. |
| 149 | **Pass `--edl` whenever you have it:** without it the check is duration + |
| 150 | audio only, which a plain copy of the base also passes; with it each window's |
| 151 | midpoint frame is compared against the base, so a window that composited |
| 152 | nothing fails. A run without `--edl` says so out loud. |
| 153 | |
| 154 | Exit codes: `2` validation, `3` render, `4` verification. A nonzero exit is a |
| 155 | real failure — surface it, don't retry blindly. Renders go to a hidden temp file |
| 156 | beside the output, renamed only after verification passes: a failed run never |
| 157 | leaves a half-written or unverified file at the output path. |
| 158 | |
| 159 | ## Evals |
| 160 | |
| 161 | `evals.md` defines the six scenarios this skill is held to (OV1–OV6). |
| 162 | `scripts/test_broll_overlay.py` implements the mechanical ones against synthetic |
| 163 | fixtures — run it after touching the script. |
| 164 |
Discussion
Alternatives
Browse more free Claude skills or everything in Content creator.


