Write talking-head scripts and produce Instagram reels and YouTube shorts
94
97%
Does it follow best practices?
Impact
85%
2.36xAverage score across 3 eval scenarios
Low
Low-risk findings worth noting
#!/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().tessl-plugin
evals
skills
reel-builder
assets
references
scripts
yap-writer