Drop in one talking-head video and get it finished — transcribe it, search the web for supporting b-roll (article screenshots, memes, images, video clips), present the asset list for approval, then hand off to reel-builder to normalize audio, burn subtitles, cut the b-roll in, and export for Instagram and LinkedIn. Use when the user drops a video and wants illustrations/screenshots/memes added, asks to "edit this like CapCut", wants b-roll found for them, or wants a talking-head take prepped for Instagram/LinkedIn. Sourcing and approval only — all rendering belongs to the reel-builder skill.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/usr/bin/env python3
"""Put a transition hit on every cut where the picture changes source.
render_reel.py concatenates segments with `-c copy` — hard cuts, no
transitions. This is a post-process on the graded master, run BEFORE captions
are burned.
WHY NOT xfade. A crossfade consumes time: each one shortens the picture by its
duration, and a talking-head reel's audio is a continuous VO laid over the
whole timeline (--vo-overlay), so the first dissolve desyncs everything after
it. Every style here is DURATION-PRESERVING — the effect is keyed to a time
window with ffmpeg's `enable`, so not one frame moves and verify_cut.py still
passes. If you truly want dissolves, they belong in the cut plan with segment
durations padded to pay for them, not here.
Styles (all ~3 frames at 30fps by default):
flash dip to white — the default; reads as energy, survives IG's recompress
dip dip to black — heavier, for a topic change
whip horizontal smear — mimics a whip pan into a cutaway
Cuts are read from the plan: only where the SOURCE CLIP CHANGES. A word trim
inside one take is meant to be invisible, and a flash there announces the edit.
Usage:
transitions.py work/master.mp4 plan/cut_plan.json --out work/master_fx.mp4
transitions.py work/master.mp4 plan/cut_plan.json --style whip --duration 0.13
transitions.py work/master.mp4 --at 3.4 8.1 --out work/master_fx.mp4
transitions.py --selftest
"""
import argparse, json, subprocess, sys
from pathlib import Path
STYLES = {
"flash": "eq=brightness=0.6:contrast=1.15",
"dip": "eq=brightness=-0.9",
"whip": "avgblur=sizeX=48:sizeY=1",
}
def source_change_cuts(segs):
"""Times where the picture changes clip. Same-clip joins stay invisible."""
cuts, t = [], 0.0
for a, b in zip(segs, segs[1:]):
t += float(a["duration"])
if a.get("clip") != b.get("clip"):
cuts.append(round(t, 3))
return cuts
def filter_expr(cuts, style, duration, total=None):
"""One timeline-enabled filter covering every cut. '' if nothing to do."""
half = duration / 2.0
windows = []
for c in cuts:
lo = max(0.0, c - half)
hi = c + half if total is None else min(total, c + half)
if hi > lo:
# commas escaped: this lands inside a -vf filtergraph
windows.append(f"between(t\\,{lo:.3f}\\,{hi:.3f})")
if not windows:
return ""
return f"{STYLES[style]}:enable='{'+'.join(windows)}'"
def selftest():
segs = [{"clip": "a.mp4", "duration": 2.0}, {"clip": "a.mp4", "duration": 1.0},
{"clip": "b.mp4", "duration": 3.0}, {"clip": "a.mp4", "duration": 1.5}]
# 2.0 is a same-clip join (invisible); 3.0 and 6.0 change source.
assert source_change_cuts(segs) == [3.0, 6.0], source_change_cuts(segs)
assert source_change_cuts([{"clip": "a.mp4", "duration": 5.0}]) == []
f = filter_expr([3.0], "flash", 0.1)
assert f == "eq=brightness=0.6:contrast=1.15:enable='between(t\\,2.950\\,3.050)'", f
# A cut at 0 clamps to the frame start rather than going negative.
assert "between(t\\,0.000\\," in filter_expr([0.02], "dip", 0.1)
# Clamped to total, and a window past the end is dropped entirely.
assert filter_expr([9.99], "whip", 0.1, total=10.0).endswith("9.940\\,10.000)'")
# A hit entirely past the end is dropped rather than emitting an
# inverted window, which ffmpeg accepts and silently never fires.
assert filter_expr([10.5], "whip", 0.1, total=10.0) == ""
assert filter_expr([], "flash", 0.1) == ""
# Multiple windows OR together with '+' — verified against ffmpeg, but
# nothing here would catch a regression in the join itself.
assert filter_expr([1.0, 2.0, 3.0], "flash", 0.1).count("between") == 3
print("ok")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("video", nargs="?")
ap.add_argument("plan", nargs="?", help="cut_plan.json; omit if using --at")
ap.add_argument("--out")
ap.add_argument("--style", choices=sorted(STYLES), default="flash")
ap.add_argument("--duration", type=float, default=0.10,
help="Seconds per hit (default 0.10 = 3 frames at 30fps)")
ap.add_argument("--at", nargs="*", type=float, default=None,
help="Explicit cut times, overriding the plan")
ap.add_argument("--crf", type=int, default=16, help="Match the master's CRF")
ap.add_argument("--selftest", action="store_true")
a = ap.parse_args()
if a.selftest:
return selftest()
if not a.video or not a.out:
ap.error("need a video and --out (or --selftest)")
if a.duration <= 0:
ap.error("--duration must be positive")
if a.at is not None and not a.at:
ap.error("--at needs at least one time")
if a.at is not None:
cuts = a.at
elif a.plan:
plan = json.loads(Path(a.plan).read_text())
cuts = source_change_cuts(plan["segments"])
else:
ap.error("pass a cut_plan.json or --at")
total = float(subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", a.video], capture_output=True, text=True
).stdout.strip() or 0) or None
vf = filter_expr(cuts, a.style, a.duration, total)
if not vf:
sys.exit("No source-change cuts found — nothing to do. "
"Use --at to place hits by hand.")
print(f"{len(cuts)} {a.style} hits at: "
f"{', '.join(f'{c:.2f}s' for c in cuts)}", file=sys.stderr)
out = Path(a.out)
out.parent.mkdir(parents=True, exist_ok=True)
tmp = out.with_name(out.stem + ".tmp" + out.suffix)
# +faststart matches render_reel.py; without it this pass moves the moov
# atom to the end of the file and kills progressive playback on the web.
subprocess.run(["ffmpeg", "-y", "-i", a.video, "-vf", vf,
"-c:v", "libx264", "-preset", "medium", "-crf", str(a.crf),
"-pix_fmt", "yuv420p", "-movflags", "+faststart",
"-c:a", "copy", str(tmp)], check=True)
tmp.rename(out)
print(f"wrote {out}", file=sys.stderr)
if __name__ == "__main__":
main()