CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/reels-producer-skill

help quicky produce instagram reels and youtube shorts

94

2.12x
Quality

96%

Does it follow best practices?

Impact

87%

2.12x

Average score across 3 eval scenarios

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

make_cards.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Render headline / logo cards for talking-head b-roll, via Remotion.

Talking-head b-roll is usually cards and screen recordings rather than footage,
and a static card held for three seconds reads as dead air. These render as
real motion clips at mezzanine spec, so the cut plan treats them like any other
segment.

Requires Node 18+. The first run installs Remotion into the project (a few
hundred MB including a headless Chrome), so it is slow once and fast after.
The template is copied out of the plugin into <outdir>/_project first —
node_modules never lands inside the installed skill.

Input JSON (a list, or {"cards": [...]}):
  [
    {"name": "acquisition", "title": "Garmin bought TrainingPeaks",
     "kicker": "July 2026", "subtitle": "The neutral ground changed hands",
     "duration": 3.0, "bg": "#0F1D2B", "accent": "#F26B21", "logo": "garmin.png"}
  ]

Only "title" is required. "logo" is a filename inside <outdir>/_project/public.

Usage:
  make_cards.py cards.json --outdir work/cards
  make_cards.py cards.json --outdir work/cards --resolution 4k
"""
import argparse, json, shutil, subprocess, sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from render_reel import RESOLUTIONS

TEMPLATE = Path(__file__).resolve().parent.parent / "assets/remotion-cards"
FPS = 30
DEFAULTS = {"kicker": "", "subtitle": "", "duration": 3.0,
            "bg": "#0F1D2B", "accent": "#F26B21", "fg": "#FFFFFF", "logo": ""}


def require_node():
    if not shutil.which("node"):
        sys.exit("Node not found — install Node 18+ (brew install node) or "
                 "build cards as stills and let normalize_clips.py apply Ken "
                 "Burns motion instead.")


def ensure_project(outdir):
    """Copy the template out of the plugin and install deps once."""
    proj = outdir / "_project"
    if not proj.exists():
        shutil.copytree(TEMPLATE, proj)
        (proj / "public").mkdir(exist_ok=True)
    if not (proj / "node_modules").exists():
        print("Installing Remotion (first run only, several minutes)...",
              flush=True)
        r = subprocess.run(["npm", "install", "--no-audit", "--no-fund"],
                           cwd=proj, capture_output=True, text=True)
        if r.returncode != 0:
            sys.exit(f"npm install failed:\n{r.stderr[-1200:]}")
    return proj


def render_card(proj, card, dims, outdir):
    name = card.get("name") or card["title"][:40].strip().replace(" ", "_")
    dst = outdir / f"{name}.mp4"
    props = {**DEFAULTS, **{k: v for k, v in card.items() if k != "name"}}
    duration = float(props.pop("duration"))
    frames = max(1, round(duration * FPS))
    w, h = dims
    cmd = ["npx", "remotion", "render", "Card", str(dst.resolve()),
           "--props", json.dumps(props),
           "--width", str(w), "--height", str(h),
           "--frames", f"0-{frames - 1}",
           "--log", "error"]
    r = subprocess.run(cmd, cwd=proj, capture_output=True, text=True)
    if r.returncode != 0:
        print(f"  FAILED {name}: {r.stderr.strip()[-500:]}", file=sys.stderr)
        return None
    return dst


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("cards_json")
    ap.add_argument("--outdir", default="work/cards")
    ap.add_argument("--resolution", choices=sorted(RESOLUTIONS), default="1080",
                    help="Match the mezzanine the project was normalized at.")
    args = ap.parse_args()

    require_node()
    spec = json.loads(Path(args.cards_json).read_text())
    cards = spec["cards"] if isinstance(spec, dict) else spec
    if not cards:
        sys.exit("No cards in the spec — nothing to render.")
    for c in cards:
        if not c.get("title"):
            sys.exit(f"Every card needs a 'title': {json.dumps(c)[:120]}")

    outdir = Path(args.outdir)
    outdir.mkdir(parents=True, exist_ok=True)
    proj = ensure_project(outdir)
    dims = RESOLUTIONS[args.resolution]

    done, failed = [], []
    for c in cards:
        print(f"Rendering card: {c['title'][:60]}...", flush=True)
        out = render_card(proj, c, dims, outdir)
        (done if out else failed).append(c.get("name") or c["title"])

    print(json.dumps({"rendered": len(done), "failed": failed,
                      "outdir": str(outdir),
                      "resolution": f"{dims[0]}x{dims[1]}"}, indent=2))
    if failed:
        sys.exit(1)
    print(f"\nAdd these to raw/ (or reference directly) and probe them — they "
          f"are ordinary {dims[0]}x{dims[1]} clips from here on.",
          file=sys.stderr)


if __name__ == "__main__":
    main()

.mcp.json

tile.json