CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/reels-producer-skill

help quicky produce instagram reels and youtube shorts

80

1.95x
Quality

93%

Does it follow best practices?

Impact

47%

1.95x

Average score across 1 eval scenario

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

pipeline.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Pipeline state machine for a reel project — status, next step, gate log.

Run from the project folder. Stage completion is detected from artifacts on
disk (never from memory), so status can't drift from reality; only things
disk can't know live in state.json: gate decisions, target duration, and the
waiting-on-NLE flag. Staleness chains through file mtimes — edit
plan/cut_plan.json and the preview, master, and exports all flip to STALE
automatically.

Commands:
  pipeline.py init --target-duration 30   scaffold folders + project.md + state.json
  pipeline.py status                      full stage table, gates, warnings
  pipeline.py next                        the single next action (command or gate)
  pipeline.py gate 1 presented            log that plan+preview went to the human
  pipeline.py gate 1 approved             refuses if the preview is stale
  pipeline.py gate 1 nle-wait             pipeline paused: human editing in NLE —
                                          cut_plan.json must NOT be touched
  pipeline.py gate 1 reopen               back to pending (also: after nle import)
  pipeline.py gate 2 presented|approved|reopen

The agent ritual: `status` at session start (with project.md), `next` when
unsure, `gate` at every human decision. Creative judgment (what to reject,
hook picks, pacing feedback) is yours, not this script's.
"""
import argparse, json, shutil, sys, time
from pathlib import Path

VIDEO_EXT = {".mp4", ".mov", ".m4v", ".mts", ".avi", ".mkv"}
AUDIO_EXT = {".mp3", ".m4a", ".wav", ".aac", ".flac", ".ogg"}
STATE = Path("state.json")


# ---------- state ----------

def load_state():
    if STATE.exists():
        return json.loads(STATE.read_text())
    return {"target_duration": 30, "gates": {}}

def save_state(st):
    tmp = STATE.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(st, indent=2))
    tmp.rename(STATE)


# ---------- artifact probes ----------

def mtime(p):
    return p.stat().st_mtime

def newest(pattern):
    files = [p for p in Path().glob(pattern) if p.is_file()]
    return max(files, key=mtime) if files else None

def raw_clips():
    raw = Path("raw")
    if not raw.exists():
        return []
    return sorted(p for p in raw.rglob("*") if p.suffix.lower() in VIDEO_EXT)

def probed_clips():
    cj = Path("work/clips.json")
    if not cj.exists():
        return None
    try:
        clips = json.loads(cj.read_text())["clips"]
    except (json.JSONDecodeError, KeyError):
        return None
    return [c for c in clips if "error" not in c]

def music_file():
    m = Path("music")
    if not m.exists():
        return None
    files = sorted(p for p in m.iterdir() if p.suffix.lower() in AUDIO_EXT)
    return files[0] if files else None


# ---------- stages ----------
# each check returns (status, detail, action)
# status: "done" | "stale" | "ready" | "blocked"

def check_probe(st):
    raws = raw_clips()
    cj = Path("work/clips.json")
    cmd = (f"python3 scripts/probe_clips.py raw/ --target-duration "
           f"{st['target_duration']:g} --out work/clips.json")
    if not raws:
        return ("blocked", "no clips in raw/ — get footage from the user "
                "(or symlink: ln -s /Volumes/… raw)", None)
    if not cj.exists() or probed_clips() is None:
        return ("ready", f"{len(raws)} raw clips await probing", cmd)
    if max(mtime(p) for p in raws) > mtime(cj):
        return ("stale", "raw/ changed after last probe — re-probe", cmd)
    clips = probed_clips()
    warn = sum(1 for c in clips if "warning" in c)
    d = f"{len(clips)} usable clips"
    if warn:
        d += f", {warn} duration outlier(s) — extract segments before normalizing"
    return ("done", d, None)

def check_inspect(st):
    clips = probed_clips()
    cmd = "python3 scripts/contact_sheet.py work/clips.json --outdir work/sheets"
    if not clips:
        return ("blocked", "needs probe", None)
    missing = [c for c in clips
               if not Path(f"work/sheets/{Path(c['file']).stem}_sheet.png").exists()]
    if missing:
        return ("ready", f"{len(missing)}/{len(clips)} sheets missing — "
                "generate, then VIEW every sheet before planning", cmd)
    return ("done", f"{len(clips)}/{len(clips)} sheets (did you look at them?)", None)

def check_normalize(st):
    clips = probed_clips()
    cmd = "python3 scripts/normalize_clips.py work/clips.json --outdir work/mezz"
    if not clips:
        return ("blocked", "needs probe", None)
    missing, stale = [], []
    for c in clips:
        mz = Path(f"work/mezz/{Path(c['file']).stem}.mp4")
        if not mz.exists():
            missing.append(c)
        elif Path(c["file"]).exists() and mtime(Path(c["file"])) > mtime(mz):
            stale.append(c)
    if missing:
        return ("ready", f"{len(missing)}/{len(clips)} mezzanine files to render "
                "(wait for process exit, not files)", cmd)
    if stale:
        return ("stale", f"{len(stale)} source clip(s) newer than mezzanine", cmd)
    return ("done", f"{len(clips)} mezzanine files", None)

def check_beats(st):
    m = music_file()
    bj = Path("work/beats.json")
    if not m:
        return ("blocked", "no music in music/ — ASK THE USER: drop a file, "
                "YouTube via scripts/fetch_music.py, or --bpm fixed grid", None)
    cmd = f"python3 scripts/detect_beats.py '{m}' --out work/beats.json"
    if not bj.exists():
        return ("ready", f"track: {m.name}", cmd)
    if mtime(m) > mtime(bj):
        return ("stale", "music changed after beat detection", cmd)
    return ("done", f"{m.name}", None)

def check_plan(st):
    plan = Path("plan/cut_plan.json")
    cmd = (f"python3 scripts/build_cut_plan.py work/clips.json work/beats.json "
           f"--target-duration {st['target_duration']:g} --out plan/cut_plan.json  "
           "# add --hook/--close from contact-sheet picks")
    deps = [Path("work/clips.json"), Path("work/beats.json")]
    if not all(d.exists() for d in deps):
        return ("blocked", "needs probe + beats", None)
    if not plan.exists():
        return ("ready", "propose the cut plan (use --hook/--close)", cmd)
    return ("done", "plan/cut_plan.json (human-editable)", None)

def check_preview(st):
    plan = Path("plan/cut_plan.json")
    pv = newest("preview/draft_v*.mp4")
    n = int(pv.stem.split("_v")[-1]) + 1 if pv else 1
    cmd = (f"python3 scripts/render_reel.py plan/cut_plan.json --preview "
           f"--out preview/draft_v{n}.mp4 && python3 scripts/check_cuts.py "
           f"plan/cut_plan.json preview/draft_v{n}.mp4 --out work/qc_v{n}")
    if not plan.exists():
        return ("blocked", "needs cut plan", None)
    if not pv:
        return ("ready", "render draft + QC before showing the human", cmd)
    if mtime(plan) > mtime(pv):
        return ("stale", f"cut plan edited after {pv.name} — re-render + QC", cmd)
    qc = newest("work/qc_v*.json")
    d = pv.name
    if qc and mtime(qc) >= mtime(pv):
        problems = json.loads(qc.read_text()).get("problems", [])
        d += f", QC: {len(problems)} problem(s)" if problems else ", QC clean"
    else:
        d += ", QC MISSING — run check_cuts.py"
    return ("done", d, None)

def check_master(st):
    plan = Path("plan/cut_plan.json")
    mst = Path("work/master.mp4")
    cmd = ("python3 scripts/render_reel.py plan/cut_plan.json --grade gritty "
           "--out work/master.mp4 && python3 scripts/check_cuts.py "
           "plan/cut_plan.json work/master.mp4 --out work/qc_master")
    if gate_state(st, 1) != "approved":
        return ("blocked", "needs Gate 1 approval", None)
    if not mst.exists():
        return ("ready", "final render with grade + QC", cmd)
    if plan.exists() and mtime(plan) > mtime(mst):
        return ("stale", "cut plan edited after master render", cmd)
    return ("done", "work/master.mp4", None)

def check_export(st):
    mst = Path("work/master.mp4")
    cmd = ("python3 scripts/export_variants.py work/master.mp4 "
           "--outdir exports/ --thumb-at 1.5")
    if gate_state(st, 2) != "approved":
        return ("blocked", "needs Gate 2 approval", None)
    outs = [Path(f"exports/{n}.mp4") for n in ("tiktok", "ig_reels", "yt_shorts")]
    if not all(p.exists() for p in outs):
        return ("ready", "export platform variants", cmd)
    if mst.exists() and any(mtime(mst) > mtime(p) for p in outs):
        return ("stale", "master newer than exports", cmd)
    styled = len(list(Path("exports").glob("thumbnail-*.jpg")))
    d = "tiktok / ig_reels / yt_shorts + thumbnail"
    if styled:
        d += f" (+{styled} styled — human picks the cover)"
    return ("done", d, None)

STAGES = [("probe", check_probe), ("inspect", check_inspect),
          ("normalize", check_normalize), ("beats", check_beats),
          ("plan", check_plan), ("preview", check_preview),
          ("master", check_master), ("export", check_export)]


# ---------- gates ----------

def gate_state(st, n):
    return st.get("gates", {}).get(str(n), {}).get("state", "pending")

def gate_ready(st, n):
    """Is the gate's input fresh enough to present/approve?"""
    if n == 1:
        s, _, _ = check_preview(st)
        return s == "done", "preview missing or stale — re-render + QC first"
    s, _, _ = check_master(st)
    return s == "done", "master missing or stale — re-render + QC first"

def cmd_gate(st, n, action):
    gates = st.setdefault("gates", {})
    g = gates.setdefault(str(n), {})
    cur = g.get("state", "pending")
    if action in ("presented", "approved"):
        ok, why = gate_ready(st, n)
        if not ok:
            sys.exit(f"Refusing gate {n} '{action}': {why}")
    if action == "nle-wait":
        if n != 1:
            sys.exit("NLE round trip is a Gate 1 flow")
        plan = Path("plan/cut_plan.json")
        g["plan_mtime_at_wait"] = mtime(plan) if plan.exists() else 0
    if action == "reopen":
        action = "pending"
    g["state"] = action if action != "presented" else "presented"
    g["at"] = time.strftime("%Y-%m-%d %H:%M:%S")
    save_state(st)
    print(f"Gate {n}: {cur} -> {g['state']}")
    if g["state"] == "nle-wait":
        print("PIPELINE PAUSED. Do not render, re-plan, or touch "
              "plan/cut_plan.json until the edited fcpxml returns "
              "(then: import_timeline.py + `gate 1 reopen`).")
    if g["state"] == "approved" and n == 1:
        print("Gate 1 approved — next: final render (see `pipeline.py next`).")
    if g["state"] == "approved" and n == 2:
        print("Gate 2 approved — next: export variants.")


# ---------- commands ----------

def warnings(st):
    w = []
    g1 = st.get("gates", {}).get("1", {})
    if g1.get("state") == "nle-wait":
        plan = Path("plan/cut_plan.json")
        if plan.exists() and mtime(plan) > g1.get("plan_mtime_at_wait", 0) + 1:
            w.append("cut_plan.json CHANGED while waiting on the NLE edit — "
                     "the human's edit may be clobbered on import. Reconcile "
                     "before proceeding.")
        else:
            w.append("waiting on NLE edit — hands off plan/cut_plan.json "
                     "(import_timeline.py when the fcpxml comes back, "
                     "then `gate 1 reopen`)")
    for n in (1, 2):
        if gate_state(st, n) == "approved":
            ok, _ = gate_ready(st, n)
            if not ok:
                w.append(f"Gate {n} was approved but its input went stale — "
                         f"re-render and re-confirm (`gate {n} reopen`)")
    return w

def next_action(st):
    if st.get("gates", {}).get("1", {}).get("state") == "nle-wait":
        return ("WAITING on the human's NLE edit. When the fcpxml returns: "
                "python3 scripts/import_timeline.py <edited.fcpxml> --out "
                "plan/cut_plan.json --base-plan plan/cut_plan.json, "
                "then `pipeline.py gate 1 reopen`")
    for name, check in STAGES:
        s, detail, cmd = check(st)
        if s in ("ready", "stale"):
            return f"[{name}] {detail}\n  {cmd}" if cmd else f"[{name}] {detail}"
        if s == "blocked" and name in ("probe", "beats"):
            return f"[{name}] {detail}"   # human-input blockers surface immediately
        if name == "preview" and s == "done":
            gs = gate_state(st, 1)
            if gs == "pending":
                return ("GATE 1: present plan + preview together to the human "
                        "(then `pipeline.py gate 1 presented`)")
            if gs == "presented":
                return ("GATE 1: awaiting the human — approve / edits / NLE "
                        "(`gate 1 approved` | edit plan | `gate 1 nle-wait`)")
        if name == "master" and s == "done" and gate_state(st, 2) in ("pending", "presented"):
            gs = gate_state(st, 2)
            return ("GATE 2: show the master to the human "
                    "(`pipeline.py gate 2 presented`)" if gs == "pending" else
                    "GATE 2: awaiting the human (`gate 2 approved` or loop back)")
    return "All stages done — deliver exports/ to the user and update project.md."

MARK = {"done": ".", "ready": ">", "stale": "!", "blocked": "x"}

def cmd_status(st):
    print(f"Reel pipeline — {Path.cwd().name}  "
          f"(target {st['target_duration']:g}s)\n")
    for name, check in STAGES:
        s, detail, _ = check(st)
        print(f"  {MARK[s]} {name:<10} {s:<8} {detail}")
    print(f"\n  Gate 1 (plan+preview): {gate_state(st, 1)}    "
          f"Gate 2 (master): {gate_state(st, 2)}")
    for w in warnings(st):
        print(f"\n  WARNING: {w}")
    print(f"\nNext: {next_action(st)}")

def cmd_init(args):
    if STATE.exists():
        sys.exit("state.json already exists — this is already a reel project "
                 "(use `pipeline.py status`)")
    for d in ("raw", "music", "work", "plan", "preview", "exports"):
        Path(d).mkdir(exist_ok=True)
    tmpl = Path(__file__).resolve().parent.parent / "assets/project_template.md"
    if not Path("project.md").exists() and tmpl.exists():
        shutil.copy(tmpl, "project.md")
    save_state({"target_duration": args.target_duration, "gates": {},
                "created": time.strftime("%Y-%m-%d %H:%M:%S")})
    print(f"Project scaffolded (target {args.target_duration:g}s). "
          "Fill project.md, drop footage in raw/ (or symlink), music in music/.")


def main():
    ap = argparse.ArgumentParser()
    sub = ap.add_subparsers(dest="cmd", required=True)
    p_init = sub.add_parser("init")
    p_init.add_argument("--target-duration", type=float, default=30)
    sub.add_parser("status")
    sub.add_parser("next")
    p_gate = sub.add_parser("gate")
    p_gate.add_argument("n", type=int, choices=(1, 2))
    p_gate.add_argument("action", choices=("presented", "approved",
                                           "nle-wait", "reopen"))
    args = ap.parse_args()

    if args.cmd == "init":
        return cmd_init(args)
    st = load_state()
    if args.cmd == "status":
        cmd_status(st)
    elif args.cmd == "next":
        print(next_action(st))
    elif args.cmd == "gate":
        cmd_gate(st, args.n, args.action)


if __name__ == "__main__":
    main()

tile.json