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

contact_sheet.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Dense contact sheets — LOOK at the footage before planning anything.

One PNG grid per clip, frames sampled evenly across the FULL duration.
This is how you catch what probe metadata can't: talking heads, burned-in
captions/lyrics (often intermittent — mid-clip only), screen recordings,
misnamed files, shaky/unusable sections, and where the actual action lives.

Frames are laid out row-major. No timestamps are burned in (Homebrew ffmpeg
lacks drawtext), so use the printed "frame N ≈ N x interval" mapping — the
script prints the sampling interval per clip.

Usage:
  contact_sheet.py work/clips.json --outdir work/sheets
  contact_sheet.py work/clips.json --outdir work/sheets --interval 1.0  # denser
"""
import argparse, json, math, subprocess, sys
from pathlib import Path

COLS = 6
MIN_FRAMES, MAX_FRAMES = 12, 60


def sheet_for(clip, outdir, interval):
    src = Path(clip["file"])
    dur = clip["duration"]
    n = max(MIN_FRAMES, min(MAX_FRAMES, math.ceil(dur / interval)))
    step = dur / n
    rows = math.ceil(n / COLS)
    dst = outdir / f"{src.stem}_sheet.png"
    tmp = outdir / f"{src.stem}_sheet.tmp.png"
    vf = f"fps=1/{step:.4f},scale=320:-2,tile={COLS}x{rows}:padding=2"
    r = subprocess.run(
        ["ffmpeg", "-y", "-v", "error", "-i", str(src),
         "-vf", vf, "-frames:v", "1", str(tmp)],
        capture_output=True, text=True)
    if r.returncode != 0 or not tmp.exists():
        tmp.unlink(missing_ok=True)
        print(f"  FAILED {src.name}: {r.stderr.strip()[-300:]}", file=sys.stderr)
        return None
    tmp.rename(dst)
    print(f"  {dst.name}: {n} frames, 1 every {step:.1f}s "
          f"(frame N is at ~N x {step:.1f}s, row-major)")
    return dst


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("clips_json")
    ap.add_argument("--outdir", default="work/sheets")
    ap.add_argument("--interval", type=float, default=2.0,
                    help="Target seconds between sampled frames (default 2.0; "
                         "capped at %d frames for very long clips — outliers "
                         "get coarser sampling, extract a segment first)" % MAX_FRAMES)
    args = ap.parse_args()

    clips = [c for c in json.loads(Path(args.clips_json).read_text())["clips"]
             if "error" not in c]
    outdir = Path(args.outdir)
    outdir.mkdir(parents=True, exist_ok=True)

    print(f"Contact sheets -> {outdir}/")
    made = [sheet_for(c, outdir, args.interval) for c in clips]
    ok = [m for m in made if m]
    print(f"\n{len(ok)}/{len(clips)} sheets written. VIEW EVERY SHEET before "
          f"building a cut plan: reject clips with burned-in text, talking "
          f"heads, or screen content; note where the best action moments are "
          f"(for in-points, hook, and closer).")
    if len(ok) < len(clips):
        sys.exit(1)


if __name__ == "__main__":
    main()

tile.json