CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/speaker-toolkit

Eight-skill presentation system: ingest talks into a rhetoric vault, run interactive clarification, generate a speaker profile, create presentations that match your documented patterns, produce the deck illustrations + thumbnail visual layer, create and publish talk-content Agent Skills with talk pages to a Jekyll shownotes site, verify a recorded screencast against its storyboard, and edit a Camtasia screencast into a speaker-first video with corrected captions and chapters, all informed by a 113-entry Presentation Patterns taxonomy (83 observable: 64 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns) for scoring, brainstorming, and go-live preparation.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

audit-framing.pyskills/screencast-editor/scripts/

#!/usr/bin/env python3
"""Check that the pointer stays inside the framed view of every screen shot.

A zoom that moves the row the presenter is pointing at out of frame looks fine
in every still, because the pointer is not in the screen pixels. This replays
the recorded pointer path against the shot plan's framing.

Usage:
    audit-framing.py <shot-plan.json> <pointer.json> [--step 0.25]

A sample outside the view is `pointing` when the pointer has come to rest there
after moving during this shot: the presenter put it on something the framing
cuts off. A pointer still where an earlier shot left it is `resting`, and one
travelling into frame is neither; neither is a framing error. Stdout: JSON per screen shot. Exit 0 when no shot has a
`pointing` miss, 1 when one does, 2 on usage error.
"""

from __future__ import annotations

import argparse
import bisect
import json
import math
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
# The sibling module resolves only after the sys.path insert above.
import camtasia_model as model  # noqa: E402


def audit(plan: dict, samples: list[list[float]], step: float = 0.25) -> list[dict]:
    canvas = model.plan_canvas(plan)
    times = [s[0] for s in samples]

    def pointer_at(t: float) -> tuple[float, float]:
        i = max(0, bisect.bisect_right(times, t) - 1)
        return samples[i][1], samples[i][2]

    def moved_between(a: float, b: float) -> bool:
        lo = max(0, bisect.bisect_right(times, a) - 1)
        hi = bisect.bisect_right(times, b)
        return len({(s[1], s[2]) for s in samples[lo:hi]}) > 1

    report = []
    for shot in plan["shots"]:
        if shot["kind"] != "screen":
            continue
        cues = shot["cues"]
        misses = []
        t = shot["start"]
        while t < shot["end"]:
            if any(0 < c[0] - t < model.MOVE_SECONDS for c in cues[1:]):
                t += step
                continue
            cue = [c for c in cues if c[0] <= t][-1]
            x0, y0, x1, y1 = model.visible_rect(cue[1], cue[2], cue[3], canvas)
            px, py = pointer_at(t)
            if not (x0 <= px <= x1 and y0 <= py <= y1):
                if moved_between(t - step, t):
                    kind = "travelling"
                elif moved_between(shot["start"], t):
                    kind = "pointing"
                else:
                    kind = "resting"
                misses.append({"t": round(t, 2), "pointer": [px, py], "kind": kind})
            t += step
        report.append(
            {
                "label": shot.get("label", ""),
                "start": shot["start"],
                "pointing": [m for m in misses if m["kind"] == "pointing"],
                "resting_samples": sum(m["kind"] == "resting" for m in misses),
            }
        )
    return report


def load_samples(path: Path) -> list[list[float]]:
    """Pointer samples from trec-pointer.py; refuses an empty or malformed path."""
    try:
        samples = json.loads(path.read_text(encoding="utf-8")).get("samples")
    except (OSError, json.JSONDecodeError, AttributeError) as e:
        raise ValueError(f"cannot read pointer samples {path}: {e}") from e
    if not isinstance(samples, list) or not samples:
        raise ValueError(f"{path} holds no pointer samples; rerun trec-pointer.py")
    for row in samples:
        if not (
            isinstance(row, list)
            and len(row) == 3
            and all(
                isinstance(v, (int, float))
                and not isinstance(v, bool)
                and math.isfinite(v)
                for v in row
            )
        ):
            raise ValueError(
                f"{path}: every sample must be [seconds, x, y] finite numbers, got {row!r}"
            )
    if any(b[0] < a[0] for a, b in zip(samples, samples[1:])):
        raise ValueError(
            f"{path}: samples must be in time order; regenerate it with trec-pointer.py"
        )
    return samples


def positive_seconds(value: str) -> float:
    step = float(value)
    if not math.isfinite(step) or step <= 0:
        raise argparse.ArgumentTypeError("--step must be a positive number of seconds")
    return step


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=(__doc__ or "").split("\n")[0])
    parser.add_argument("plan", type=Path)
    parser.add_argument("pointer", type=Path)
    parser.add_argument("--step", type=positive_seconds, default=0.25)
    args = parser.parse_args(argv)
    try:
        plan = model.load_plan(args.plan)
        samples = load_samples(args.pointer)
    except (ValueError, OSError, KeyError) as e:
        print(f"audit-framing: {e}", file=sys.stderr)
        return 2
    report = audit(plan, samples, args.step)
    print(json.dumps(report, indent=1))
    failed = [r for r in report if r["pointing"]]
    for r in failed:
        first = r["pointing"][0]
        print(
            f"audit-framing: '{r['label']}' frames out the pointer at {first['t']}s "
            f"(pointer at {first['pointer']}); hold a framing that contains it",
            file=sys.stderr,
        )
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())

skills

README.md

tile.json