CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/reels-producer-skill

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

90

1.77x
Quality

97%

Does it follow best practices?

Impact

64%

1.77x

Average score across 3 eval scenarios

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

check_deps.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Preflight: verify every tool this pipeline needs, before touching media.

Run this FIRST, every session. A missing dependency found at step 6 has already
cost a normalize pass; found here it costs nothing.

Exits non-zero when a REQUIRED tool is missing, so it can gate a workflow.
Optional tools are reported with the exact capability that is lost — never
silently substituted, because a fallback the user did not hear about is
indistinguishable from a bug.

Usage:
  check_deps.py                       # music-video requirements (default)
  check_deps.py --mode talking-head   # VO-spine requirements
  check_deps.py --json                # machine-readable, for gating
"""
import argparse, json, os, shutil, subprocess, sys
from pathlib import Path

VENV_PY = Path(".venv/bin/python3")


def which_version(exe, args, first_line=True):
    path = shutil.which(exe)
    if not path:
        return None, None
    try:
        r = subprocess.run([path] + args, capture_output=True, text=True, timeout=20)
        out = (r.stdout or r.stderr).strip()
        return path, (out.splitlines()[0][:60] if first_line and out else "")
    except (subprocess.SubprocessError, OSError):
        return path, ""


def python_module(mod):
    """Check a module inside the project venv, falling back to this python.

    Reports which interpreter satisfied it — a module present only in system
    python is NOT usable by the pipeline, which invokes .venv/bin/python3.
    """
    interp = str(VENV_PY) if VENV_PY.exists() else sys.executable
    r = subprocess.run(
        [interp, "-c", f"import {mod}; print(getattr({mod}, '__version__', ''))"],
        capture_output=True, text=True)
    if r.returncode != 0:
        return None, interp
    return (r.stdout.strip() or "present"), interp


# ffmpeg is one row, but the pipeline leans on specific FILTERS and ENCODERS
# inside it, and a build can ship without them (Homebrew bottles and CI
# runners drop libass, libzimg, libfreetype). Ask the build, once, and read
# every listing three ways: available, missing, or unknown when the listing
# itself could not be parsed. Unknown is never folded into missing (a filter
# that exists is not reported absent) nor into available (a failed detection
# is not a pass). Adapted from kajisho5/ffmpeg-skill's doctor (MIT).
FILTERS_REQUIRED = {
    "scale": "every normalize and render", "crop": "aspect crops and pans",
    "fps": "CFR conform", "format": "yuv420p output", "eq": "all grades",
    "colorbalance": "gritty/rapha grades", "curves": "cine grade",
    "lut3d": "LUT looks and D-Log conversion", "tile": "contact sheets and audits",
    "select": "tighten_vo.py cuts", "aselect": "tighten_vo.py cuts",
    "setpts": "tighten_vo.py cuts", "asetpts": "tighten_vo.py cuts",
    "loudnorm": "export loudness", "volume": "music/VO mix",
    "afade": "segment audio joins", "amix": "VO over b-roll",
    "silencedetect": "tighten_vo.py pause detection",
    "zoompan": "Ken Burns on stills", "overlay": "styled captions",
    "noise": "gritty grade grain",
}
FILTERS_OPTIONAL = {
    "subtitles": "render_reel.py --srt burns nothing — plain captions unavailable, styled ones (gen_captions.py) still work",
    "zscale": "HDR (HLG/PQ) footage cannot be tone-mapped — normalize_clips.py leaves it washed out",
    "tonemap": "HDR (HLG/PQ) footage cannot be tone-mapped — normalize_clips.py leaves it washed out",
    "afftdn": "clean_audio.py has no default denoiser",
    "arnndn": "clean_audio.py --engine arnndn unavailable",
}
ENCODERS_REQUIRED = {"libx264": "no H.264 output — nothing exports",
                     "aac": "no AAC audio — nothing exports"}


def parse_listing(text):
    """Names from `ffmpeg -filters` / `-encoders` output, or an unknown status.

    Both listings print a legend, a ` ------` separator, then one row per
    entry: flags, name, then (filters only) an io spec like `V->V`. The name is
    keyed off that shape, not the legend, which has changed across releases.
    Anything without the separator did not come from ffmpeg and is `unknown`.
    """
    lines = (text or "").splitlines()
    try:
        start = next(i for i, l in enumerate(lines) if l.strip().startswith("------")) + 1
    except StopIteration:
        return {"status": "unknown", "names": set()}
    names = set()
    for line in lines[start:]:
        parts = line.split()
        if len(parts) >= 2 and not parts[0].startswith("-"):
            names.add(parts[1])
    return {"status": "parsed" if names else "unknown", "names": names}


def ffmpeg_listing(kind):
    """Parsed `ffmpeg -<kind>` output, or status missing/unknown."""
    exe = shutil.which("ffmpeg")
    if not exe:
        return {"status": "missing", "names": set()}
    try:
        r = subprocess.run([exe, "-hide_banner", f"-{kind}"], capture_output=True,
                           text=True, timeout=20)
    except (subprocess.TimeoutExpired, OSError):
        return {"status": "unknown", "names": set()}
    if r.returncode != 0:
        return {"status": "unknown", "names": set()}
    return parse_listing(r.stdout)


def capability_state(listing, name):
    """available | missing | unknown for one filter or encoder."""
    if listing["status"] == "parsed":
        return "available" if name in listing["names"] else "missing"
    return listing["status"] if listing["status"] == "missing" else "unknown"


def check(mode):
    rows = []

    def add(name, required, found, detail, lost):
        rows.append({"name": name, "required": required, "present": bool(found),
                     "detail": detail or "", "without_it": lost})

    p, v = which_version("ffmpeg", ["-version"])
    add("ffmpeg", True, p, v, "nothing runs — every stage shells out to it")
    p, v = which_version("ffprobe", ["-version"])
    add("ffprobe", True, p, v, "no probing, no duration validation")

    filters, encoders = ffmpeg_listing("filters"), ffmpeg_listing("encoders")

    def add_cap(kind, listing, name, required, lost):
        state = capability_state(listing, name)
        detail = "" if state == "available" else (
            "listing unreadable" if state == "unknown" else "not in this build")
        add(f"{kind}:{name}", required, state == "available", detail,
            lost if state != "unknown" else
            f"could not read ffmpeg -{kind}s — {lost}")

    for name, use in FILTERS_REQUIRED.items():
        add_cap("filter", filters, name, True, f"{use} fails")
    for name, lost in FILTERS_OPTIONAL.items():
        add_cap("filter", filters, name, False, lost)
    for name, lost in ENCODERS_REQUIRED.items():
        add_cap("encoder", encoders, name, True, lost)

    venv_ok = VENV_PY.exists()
    add(".venv", mode == "music-video", venv_ok,
        str(VENV_PY) if venv_ok else "",
        "macOS refuses system pip (PEP 668) — python deps cannot be installed")

    for mod, needed_for in (("librosa", "music-video"), ("soundfile", "music-video")):
        ver, interp = python_module(mod)
        add(mod, mode == needed_for, ver,
            f"{ver} via {interp}" if ver else "",
            "no beat detection — cuts cannot be synced to the track")

    # Optional: each names precisely the capability lost, so the agent can
    # tell the user what degrades instead of quietly choosing a lesser path.
    p, v = which_version("auto-editor", ["--version"])
    add("auto-editor", False, p, v,
        "tighten_vo.py uses its ffmpeg silencedetect path (measured equivalent)")
    p, v = which_version("deepFilter", ["--help"])
    add("deepFilter", False, p, "",
        "clean_audio.py uses ffmpeg afftdn — audibly weaker on noisy rooms")
    p, v = which_version("node", ["--version"])
    add("node", False, p, v,
        "make_cards.py cannot run — build cards as stills and use Ken Burns")
    ver, interp = python_module("moviepy")
    add("moviepy", False, ver, ver or "",
        "render_moviepy.py unavailable — no engine A/B (ffmpeg still renders)")
    p, v = which_version("yt-dlp", ["--version"])
    add("yt-dlp", False, p, v, "fetch_music.py cannot pull audio from a link")
    ver, interp = python_module("mlx_whisper")
    add("mlx-whisper", False, ver, ver or "",
        "no local transcription — captions must come from elsewhere, and "
        "talking-head reels are watched muted" if mode == "talking-head"
        else "no local transcription — captions must come from elsewhere")
    ver, interp = python_module("Vision")
    add("apple vision", False, ver, "" if not ver else "pyobjc",
        "no subject detection — build_cut_plan --score-in-points and "
        "detect_subjects.py are unavailable, in-points stay positional")
    ver, interp = python_module("Quartz")
    add("pyobjc quartz", False, ver, "" if not ver else "pyobjc",
        "capture_window.py cannot enumerate windows — app b-roll must be "
        "screen-recorded by hand")
    osapp = "/Applications/Openscreen.app/Contents/MacOS/OpenScreen"
    add("openscreen", False, osapp if os.path.exists(osapp) else "", "",
        "no real-time screencasts — capture_window.py still records stepped UI "
        "at 3-5 fps, which is right for agent-driven screens and a slideshow "
        "for live motion")
    p, v = which_version("magick", ["-version"])
    add("imagemagick", False, p, v,
        "gen_captions.py cannot render styled cards — only plain "
        "render_reel.py --srt captions")
    key = next((k for k in ("GEMINI_API_KEY", "GOOGLE_AI_API_KEY",
                            "GOOGLE_API_KEY") if os.environ.get(k)), None)
    add("gemini api key", False, key, key or "",
        "no hook-copy drafts or stylized thumbnails")
    return rows


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--mode", choices=("music-video", "talking-head"),
                    default="music-video")
    ap.add_argument("--json", action="store_true")
    args = ap.parse_args()

    rows = check(args.mode)
    missing_required = [r for r in rows if r["required"] and not r["present"]]
    missing_optional = [r for r in rows if not r["required"] and not r["present"]]

    if args.json:
        print(json.dumps({"mode": args.mode, "ok": not missing_required,
                          "missing_required": [r["name"] for r in missing_required],
                          "missing_optional": [r["name"] for r in missing_optional],
                          "checks": rows}, indent=2))
    else:
        print(f"Dependency check — {args.mode}\n")
        caps_ok = [r["name"].split(":", 1)[1] for r in rows
                   if ":" in r["name"] and r["present"]]
        if caps_ok:
            print(f"  OK   ffmpeg build carries {len(caps_ok)} needed filters/encoders")
        for r in rows:
            if ":" in r["name"] and r["present"]:
                continue
            mark = "OK  " if r["present"] else ("MISS" if r["required"] else "--  ")
            tag = "required" if r["required"] else "optional"
            print(f"  {mark} {r['name']:<16} {tag:<9} {r['detail']}")
        if missing_optional:
            print("\nUnavailable, and what each costs:")
            for r in missing_optional:
                print(f"  - {r['name']}: {r['without_it']}")
        if missing_required:
            print("\nMISSING REQUIRED — do not start the pipeline:")
            for r in missing_required:
                print(f"  - {r['name']}: {r['without_it']}")
            print("\n  ffmpeg/ffprobe:  brew install ffmpeg")
            print("  .venv + deps:    python3 -m venv .venv && "
                  ".venv/bin/pip install librosa soundfile")

    if missing_required:
        sys.exit(1)


if __name__ == "__main__":
    main()

.mcp.json

tile.json