CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/reels-producer-skill

Write talking-head scripts and produce Instagram reels and YouTube shorts

94

2.36x
Quality

97%

Does it follow best practices?

Impact

85%

2.36x

Average score across 3 eval scenarios

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

plan_broll.pyskills/reel-builder/scripts/

#!/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()

.mcp.json

tile.json