Write talking-head scripts and produce Instagram reels and YouTube shorts
94
97%
Does it follow best practices?
Impact
85%
2.36xAverage score across 3 eval scenarios
Low
Low-risk findings worth noting
#!/usr/bin/env python3
"""Find where b-roll CAN go in a talking-head take, and how much fits.
This script does not decide what is worth illustrating. It cannot: judging
which claim in a script deserves a picture is exactly the kind of call that
depends on knowing what the reel is arguing, and a keyword heuristic dressed up
as one would be confidently wrong. See rules/script-delegation.md.
What it does is the part that IS deterministic, and that a human or an agent
gets wrong by eye:
- group the take into sentences from word-level timestamps
- compute the b-roll budget (talking-head reels cap b-roll around 40%, since
the face is what carries an opinion piece)
- exclude the opening and closing, where cutting away from the face costs
most — the hook has to land on a person, and so does the CTA
- drop sentences too short to cover, and cap each beat so no single still
outstays its welcome
- emit the surviving slots with their timings and what is said over them
The agent then reads what each slot SAYS and writes the prompt. Slots left with
an empty prompt are simply not illustrated, which is the common case — most
sentences do not want a picture.
The output doubles as a gen_images.py spec, so a filled-in file renders with no
conversion step.
Usage:
plan_broll.py words.json --out plan/broll.json
plan_broll.py words.json --out plan/broll.json --ceiling 0.3 --max-beat 2.5
"""
import argparse, json, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from gen_captions import load_words # noqa: E402
from trim_words import sentence_groups # noqa: E402
CEILING = 0.40 # share of runtime b-roll may occupy (talking-head)
MAX_BEAT = 3.0 # seconds any single still may hold before it reads as dead
MIN_BEAT = 1.5 # shorter than this and a cutaway is a flicker, not a beat
HEAD_GUARD = 0.15 # opening share reserved for the face — the hook
TAIL_GUARD = 0.10 # closing share reserved for the face — the CTA
def slots_for(words, ceiling=CEILING, max_beat=MAX_BEAT, min_beat=MIN_BEAT,
head=HEAD_GUARD, tail=TAIL_GUARD):
"""Coverable sentences, in order, with the budget they would consume.
Returns (slots, meta). Every slot is eligible on TIMING alone — whether it
earns a picture is not something this can know.
"""
if not words:
return [], {"runtime": 0.0, "ceiling_seconds": 0.0}
runtime = float(words[-1]["end"])
guard_lo, guard_hi = runtime * head, runtime * (1.0 - tail)
budget = runtime * ceiling
slots = []
for group in sentence_groups(words):
start = float(words[group[0]]["start"])
end = float(words[group[-1]]["end"])
text = " ".join(words[i]["text"] for i in group).strip()
if end - start < min_beat:
continue
if start < guard_lo or end > guard_hi:
continue
duration = round(min(end - start, max_beat), 3)
slots.append({
"name": f"still_{len(slots) + 1:02d}",
"at": round(start, 3),
"until": round(start + duration, 3),
"duration": duration,
"says": text,
"prompt": "",
})
# Every eligible slot is offered, and the budget is reported alongside.
# Spending it greedily front-to-back would silently hide the back half of
# the take from whoever is choosing — and choosing is the judgment call
# this script exists NOT to make.
return slots, {
"runtime": round(runtime, 2),
"ceiling_seconds": round(budget, 2),
"offered_seconds": round(sum(s["duration"] for s in slots), 2),
"face_only_before": round(guard_lo, 2),
"face_only_after": round(guard_hi, 2),
}
def budget_check(slots, ceiling_seconds):
"""Seconds the FILLED slots consume, and whether that clears the ceiling."""
used = round(sum(s["duration"] for s in slots
if (s.get("prompt") or "").strip()), 2)
return used, used <= ceiling_seconds
def main():
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("words_json", help="Word-level transcript (as gen_captions takes)")
ap.add_argument("--out", default="plan/broll.json")
ap.add_argument("--ceiling", type=float, default=CEILING,
help=f"Share of runtime b-roll may occupy (default {CEILING})")
ap.add_argument("--max-beat", type=float, default=MAX_BEAT,
help=f"Seconds any one still may hold (default {MAX_BEAT})")
ap.add_argument("--min-beat", type=float, default=MIN_BEAT,
help=f"Below this a cutaway flickers (default {MIN_BEAT})")
a = ap.parse_args()
words = load_words(a.words_json)
slots, meta = slots_for(words, a.ceiling, a.max_beat, a.min_beat)
if not slots:
sys.exit("No slot survives the guards. On a short take that is normal "
"— the face carries it. Lower --min-beat only if you mean it.")
out = Path(a.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps({
"style_anchor": "",
"model": "gpt-image-2",
"budget": meta,
"images": slots,
}, indent=2))
print(f"{len(slots)} coverable slot(s) offered "
f"({meta['offered_seconds']}s); budget is {meta['ceiling_seconds']}s, "
f"so pick a subset -> {out}\n")
for s in slots:
print(f" {s['name']} {s['at']:6.2f}-{s['until']:<6.2f} {s['says'][:64]}")
print(f"\nFace only before {meta['face_only_before']}s and after "
f"{meta['face_only_after']}s — the hook and the CTA land on a person.")
print("Now READ what each slot says and write a prompt for the few that "
"earn a picture. Leave the rest empty; most sentences do not want "
"one. Then fill style_anchor and run gen_images.py.", file=sys.stderr)
if __name__ == "__main__":
main().tessl-plugin
evals
skills
reel-builder
assets
references
scripts
yap-writer