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

compose-thumbnail.pyskills/screencast-editor/scripts/

#!/usr/bin/env python3
"""Compose a screencast thumbnail and emit a JSON receipt.

Runs the illustrations skill's generate-thumbnail.py with the same interpreter,
routes its human progress output to stderr, and confirms the image it wrote.

Usage:
    compose-thumbnail.py --slide-image <png> --speaker-photo <path> --title "<TITLE>"
        --aesthetic <photo|comic_book> --vault <vault_root> --output thumbnail.png

Stdout: {"thumbnail": path, "format": "png" | "jpg", "width", "height", "bytes"}.
Over YouTube's 2 MB limit the generator writes a JPEG; the file is renamed to
match. The title may have at most five words. Exit 0 on success, 1 when the
generator fails or writes no image, 2 on usage error.
"""

from __future__ import annotations

import argparse
import json
import struct
import subprocess
import sys
from pathlib import Path

GENERATOR = (
    Path(__file__).resolve().parents[2]
    / "illustrations"
    / "scripts"
    / "generate-thumbnail.py"
)
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
MAX_TITLE_WORDS = 5  # thumbnail-generation-rules: a hook, not the full title
JPEG_FRAME_MARKERS = {
    0xC0,
    0xC1,
    0xC2,
    0xC3,
    0xC5,
    0xC6,
    0xC7,
    0xC9,
    0xCA,
    0xCB,
    0xCD,
    0xCE,
    0xCF,
}


def image_info(path: Path) -> tuple[str, int, int]:
    """(format, width, height) of a PNG or JPEG, read from its header."""
    data = path.read_bytes()
    if data[:8] == PNG_SIGNATURE and data[12:16] == b"IHDR":
        width, height = struct.unpack(">II", data[16:24])
        return "png", width, height
    if data[:2] == b"\xff\xd8":
        offset = 2
        while offset + 9 <= len(data) and data[offset] == 0xFF:
            marker = data[offset + 1]
            length = struct.unpack(">H", data[offset + 2 : offset + 4])[0]
            if marker in JPEG_FRAME_MARKERS:
                height, width = struct.unpack(">HH", data[offset + 5 : offset + 9])
                return "jpg", width, height
            offset += 2 + length
    raise ValueError(f"{path} is neither a PNG nor a JPEG")


def title_words(value: str) -> str:
    if not value.split() or len(value.split()) > MAX_TITLE_WORDS:
        raise argparse.ArgumentTypeError(
            f"--title must be 1 to {MAX_TITLE_WORDS} words"
        )
    return value


def compose(generator: Path, forwarded: list[str], output: Path) -> dict:
    try:
        result = subprocess.run(
            [sys.executable, str(generator), *forwarded, "--output", str(output)],
            capture_output=True,
            text=True,
            check=False,
        )
    except OSError as e:
        raise ValueError(f"cannot run {generator}: {e}") from e
    sys.stderr.write(result.stdout)
    sys.stderr.write(result.stderr)
    if result.returncode != 0:
        raise ValueError(
            f"generate-thumbnail.py exited {result.returncode}; see the messages above"
        )
    if not output.is_file():
        raise ValueError(
            f"generate-thumbnail.py reported success but wrote no {output}"
        )
    kind, width, height = image_info(output)
    # Over 2 MB the generator falls back to JPEG; keep the name true to the bytes.
    if output.suffix.lower().lstrip(".").replace("jpeg", "jpg") != kind:
        renamed = output.with_suffix(f".{kind}")
        output.replace(renamed)
        output = renamed
    return {
        "thumbnail": str(output),
        "format": kind,
        "width": width,
        "height": height,
        "bytes": output.stat().st_size,
    }


def main(argv: list[str] | None = None, generator: Path = GENERATOR) -> int:
    parser = argparse.ArgumentParser(description=(__doc__ or "").split("\n")[0])
    parser.add_argument("--slide-image", required=True)
    parser.add_argument("--speaker-photo", required=True)
    parser.add_argument("--title", type=title_words, required=True)
    parser.add_argument("--aesthetic", choices=["photo", "comic_book"], required=True)
    parser.add_argument("--vault", required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args(argv)
    forwarded = [
        "--slide-image",
        args.slide_image,
        "--speaker-photo",
        args.speaker_photo,
        "--title",
        args.title,
        "--aesthetic",
        args.aesthetic,
        "--vault",
        args.vault,
    ]
    try:
        receipt = compose(generator, forwarded, args.output)
    except (ValueError, OSError) as e:
        print(f"compose-thumbnail: {e}", file=sys.stderr)
        return 1
    print(json.dumps(receipt))
    return 0


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

skills

README.md

tile.json