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

capture_screencast.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Real-time screencast capture for app b-roll, via OpenScreen's headless CLI.

This is the sibling of capture_window.py, not its replacement. The two split
on WHO DRIVES THE UI:

  capture_window.py  an agent steps the UI. Frames are pulled one per step, so
                     a frame can never be stale relative to what produced it.
                     This is the ONLY thing that works on a browser tab driven
                     over the CDP/debug bridge, where macOS and Chrome throttle
                     repaints — measured at 8 frozen seconds out of 13. No
                     realtime capture API can fix that, because the compositor
                     never painted the frames in the first place.

  capture_screencast.py  a human drives, live. screencapture manages roughly
                     3-5 fps, which reads as a slideshow; ScreenCaptureKit
                     records real motion (measured 55 fps on a window capture).
                     Use it for scrolling, video playback, smooth animation.

Reaching for this one on agent-driven UI brings the frozen-frame bug back.

The skill owns the edit. This lands a raw capture in raw/ and stops — probe,
normalize, plan, grade and export it like any other clip. Do not use
OpenScreen's own editor or exporter for pipeline work; two edits competing over
one timeline is how the approval gates stop meaning anything. The exception
worth breaking that for is `openscreen export --auto-zoom`, whose cursor-driven
zoom and path smoothing ffmpeg genuinely cannot do — a per-beat decision, not a
pipeline default.

Requires OpenScreen.app (MIT, github.com/getopenscreen/openscreen) and Screen
Recording permission. Verify what you installed before trusting it — a
look-alike distributing an unsigned build under this name exists:

  codesign -dv /Applications/Openscreen.app     # expect TeamIdentifier=M4LK7C6S84
  spctl -a -t exec -vv /Applications/Openscreen.app   # expect: accepted

Usage:
  capture_screencast.py sources
  capture_screencast.py record --window "Iron Trainer" --duration 20 --into raw/
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

APP = Path("/Applications/Openscreen.app/Contents/MacOS/OpenScreen")
TEAM_ID = "M4LK7C6S84"        # Etienne Lescot — the real project's signing identity


def cli_path():
    """The OpenScreen binary, or exit naming what is lost without it."""
    if APP.exists():
        return APP
    found = shutil.which("openscreen")
    if found:
        return Path(found)
    sys.exit("OpenScreen not found. Install the MIT build from\n"
             "  https://github.com/getopenscreen/openscreen/releases\n"
             "Without it, use capture_window.py — it captures stepped UI at "
             "3-5 fps, which is fine for agent-driven screens and wrong for "
             "live motion.")


def verify_signature(path):
    """True when the binary carries the real project's Developer ID.

    An unsigned look-alike ships under this name from a different publisher.
    Checking the team identifier costs nothing and is the whole difference
    between a notarized build and an ad-hoc one.
    """
    app = path.parents[2] if path.name == "OpenScreen" else path
    r = subprocess.run(["codesign", "-dv", str(app)],
                       capture_output=True, text=True)
    return f"TeamIdentifier={TEAM_ID}" in (r.stderr + r.stdout)


def run_ndjson(args):
    """Run the CLI and return its NDJSON events as dicts, in order."""
    r = subprocess.run([str(cli_path()), *args, "--json"],
                       capture_output=True, text=True)
    events = parse_events(r.stdout)
    if not events and r.returncode != 0:
        sys.exit(f"openscreen {' '.join(args)} failed:\n{r.stderr.strip()[-500:]}")
    return events


def parse_events(stdout):
    """NDJSON lines to dicts, skipping the CLI's plain-text log chatter.

    The helper interleaves bracketed native log lines with the event stream, so
    a strict json.loads over every line would die on the first one.
    """
    out = []
    for line in stdout.splitlines():
        line = line.strip()
        if not line.startswith("{"):
            continue
        try:
            out.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return out


def done_event(events):
    """The terminal `done` event, or None if the run never reached one."""
    for e in reversed(events):
        if e.get("event") == "done":
            return e
    return None


def link_into(src, dest_dir, name=None):
    """Symlink a capture into raw/, matching how footage is staged there.

    Symlink rather than copy: the recording already sits in OpenScreen's own
    recordings directory, and the pipeline never modifies raw/.
    """
    src = Path(src)
    if not src.exists():
        sys.exit(f"OpenScreen reported {src}, but it is not there.")
    dest_dir = Path(dest_dir)
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest = dest_dir / (name or src.name)
    if dest.is_symlink() or dest.exists():
        dest.unlink()
    dest.symlink_to(src.resolve())
    return dest


def apply_spec(project, spec):
    """Add zoom and annotation regions to a .openscreen project, in place.

    The spec talks in SECONDS because that is what a cut plan and a human talk
    in; the project file wants milliseconds. Doing the conversion here is the
    whole reason this is a function and not a shell one-liner.

    zooms: at/until/depth, plus cx/cy for where to look (0-1 of the frame)
    notes: at/until/text, plus x/y as a percentage of the frame
    """
    editor = project.setdefault("editor", {})
    zooms = editor.setdefault("zoomRegions", [])
    notes = editor.setdefault("annotationRegions", [])

    for i, z in enumerate(spec.get("zooms", [])):
        zooms.append({
            "id": f"z{i + 1}",
            "startMs": int(round(z["at"] * 1000)),
            "endMs": int(round(z["until"] * 1000)),
            "depth": z.get("depth", 2),
            "focus": {"cx": z.get("cx", 0.5), "cy": z.get("cy", 0.5)},
            "focusMode": "manual",
            "source": "manual",
        })
    for i, n in enumerate(spec.get("notes", [])):
        text = n["text"]
        notes.append({
            "id": f"a{i + 1}",
            "startMs": int(round(n["at"] * 1000)),
            "endMs": int(round(n["until"] * 1000)),
            "type": "text",
            "content": text,
            "textContent": text,
            "position": {"x": n.get("x", 8), "y": n.get("y", 6)},
            "size": {"width": n.get("width", 40), "height": n.get("height", 12)},
            "style": {"fontSize": n.get("font_size", 24),
                      "color": n.get("color", "#fff")},
            "zIndex": i + 1,
        })
    if "auto_zoom" in spec:
        editor["autoZoomEnabled"] = bool(spec["auto_zoom"])
    return project


def run_demo(a):
    """record -> edit the project -> export -> land the rendered clip in raw/.

    This is the one place OpenScreen's exporter is allowed to render, because
    cursor-driven auto-zoom needs the .cursor.json telemetry and there is no
    ffmpeg equivalent. The result is a single b-roll CLIP, not a reel: it goes
    into raw/ and the pipeline still owns the cut, the grade and both gates.
    Keep it inside the ~40% b-roll ceiling — roughly 3s per app beat.
    """
    import tempfile
    if not a.window and a.display is None:
        sys.exit("Pass --window TITLE or --display N (see: sources).")

    spec = {}
    if a.spec:
        spec = json.loads(Path(a.spec).read_text())

    tmp = Path(tempfile.mkdtemp(prefix="screencast_"))
    project = Path(a.keep_project) if a.keep_project else tmp / "demo.openscreen"
    project.parent.mkdir(parents=True, exist_ok=True)

    args = ["record", "--duration", str(a.duration), "--project", str(project)]
    args += ["--window", a.window] if a.window else ["--display", str(a.display)]
    done = done_event(run_ndjson(args))
    if not done or not done.get("success"):
        sys.exit(f"Recording failed: "
                 f"{(done or {}).get('error', 'no result event')}")

    if spec:
        data = json.loads(project.read_text())
        apply_spec(data, spec)
        project.write_text(json.dumps(data, indent=2))

    # Render straight into the destination. Exporting to a temp dir and
    # linking would leave raw/ pointing at /var/folders, which the OS clears
    # out from under it — a dangling clip discovered at render time.
    into = Path(a.into)
    into.mkdir(parents=True, exist_ok=True)
    out = into / (a.name or "demo.mp4")
    ex = ["export", str(project), "-o", str(out)]
    if a.auto_zoom:
        ex.append("--auto-zoom")
    if a.audio:
        ex += ["--audio", a.audio, "--audio-mode", a.audio_mode]
    done = done_event(run_ndjson(ex))
    if not done or not done.get("success"):
        sys.exit(f"Export failed: {(done or {}).get('error', 'no result event')}")

    rendered = Path(done.get("outputPath") or out)
    if not rendered.exists():
        sys.exit(f"Export reported success but {rendered} is not there.")
    print(json.dumps({
        "rendered": str(rendered),
        "size": [done.get("width"), done.get("height")],
        "zooms": len(spec.get("zooms", [])),
        "notes": len(spec.get("notes", [])),
        "project": str(project) if a.keep_project else None,
    }, indent=2))
    print("\nThis is one b-roll clip, not a reel — probe and normalize it like "
          "any other footage. The pipeline still owns the cut and the grade.",
          file=sys.stderr)


def main():
    ap = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)

    p = sub.add_parser("sources", help="list displays and windows")
    p.add_argument("--filter", help="substring match on the window title")

    p = sub.add_parser("record", help="record real-time motion into raw/")
    p.add_argument("--window", help="record the first window whose title contains this")
    p.add_argument("--display", type=int, help="screen index instead of a window")
    p.add_argument("--duration", type=float, required=True,
                   help="seconds; b-roll beats run about 3s, so keep takes short")
    p.add_argument("--into", default="raw",
                   help="directory to link the capture into (default raw/)")
    p.add_argument("--name", help="filename inside --into (default: OpenScreen's)")
    p.add_argument("--system-audio", action="store_true")
    p.add_argument("--mic", action="store_true")
    p.add_argument("--skip-verify", action="store_true",
                   help="skip the Developer ID check (do not use routinely)")

    p = sub.add_parser("demo", help="record, annotate, render a polished demo clip")
    p.add_argument("--window", help="record the first window whose title contains this")
    p.add_argument("--display", type=int)
    p.add_argument("--duration", type=float, required=True)
    p.add_argument("--spec", help="JSON of zooms/notes in SECONDS (see --help)")
    p.add_argument("--auto-zoom", action="store_true",
                   help="add cursor-driven zooms — the one thing ffmpeg cannot "
                        "reproduce, since it needs the .cursor.json telemetry")
    p.add_argument("--audio", help="voiceover to mix in (mp3/wav/m4a)")
    p.add_argument("--audio-mode", choices=("mix", "replace"), default="mix",
                   help="mix ducks the recording's own audio to 40%%; replace "
                        "drops it. For talking-head reels the VO comes from the "
                        "pipeline, so app clips usually want neither")
    p.add_argument("--into", default="raw")
    p.add_argument("--name", help="filename inside --into")
    p.add_argument("--keep-project", metavar="PATH",
                   help="also keep the .openscreen project for re-export")
    p.add_argument("--skip-verify", action="store_true")

    a = ap.parse_args()
    path = cli_path()

    if not a.__dict__.get("skip_verify", False) and not verify_signature(path):
        sys.exit(f"{path} is not signed by the OpenScreen project "
                 f"(expected TeamIdentifier={TEAM_ID}).\n"
                 f"An unsigned look-alike distributes under this name. Check:\n"
                 f"  spctl -a -t exec -vv /Applications/Openscreen.app\n"
                 f"Re-install from https://github.com/getopenscreen/openscreen/releases")

    if a.cmd == "sources":
        done = done_event(run_ndjson(["sources"]))
        if not done or not done.get("success"):
            sys.exit("Could not list sources. Grant Screen Recording permission "
                     "to the terminal app running this.")
        src = done.get("sources", {})
        for d in src.get("displays", []):
            print(f"  display {d.get('index')}  {d.get('name')}")
        for w in src.get("windows", []):
            line = f"  {w.get('id')}  {w.get('name')}"
            if not a.filter or a.filter.lower() in line.lower():
                print(line)
        return

    if a.cmd == "demo":
        return run_demo(a)

    if not a.window and a.display is None:
        sys.exit("Pass --window TITLE or --display N (see: sources).")

    args = ["record", "--duration", str(a.duration)]
    if a.window:
        args += ["--window", a.window]
    else:
        args += ["--display", str(a.display)]
    if a.system_audio:
        args.append("--system-audio")
    if a.mic:
        args.append("--mic")

    done = done_event(run_ndjson(args))
    if not done:
        sys.exit("Recording produced no result event — it may have been "
                 "interrupted, or Screen Recording permission is missing.")
    if not done.get("success"):
        sys.exit(f"Recording failed: {done.get('error', 'no reason given')}")

    dest = link_into(done["screenVideoPath"], a.into, a.name)
    print(json.dumps({
        "source": done["screenVideoPath"],
        "linked": str(dest),
        "duration": round(done.get("durationMs", 0) / 1000.0, 2),
        "cursor_data": done.get("cursorDataPath"),
    }, indent=2))
    print(f"\nProbe it before planning against it — ScreenCaptureKit writes "
          f"variable frame rate, and normalize_clips.py is what puts it on the "
          f"mezzanine's CFR grid.", file=sys.stderr)


if __name__ == "__main__":
    main()

.mcp.json

tile.json