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
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
#!/usr/bin/env python3
"""Compute the speaker-profile `pacing.adherence` object from scored talks.
Deterministic helper for vault-profile Step 4 (per `rules/script-delegation.md`):
parses each talk's duration, derives slides-per-minute, classifies it against the
speaker's slide budgets, and emits the corpus-level adherence summary. The skill
orchestrator pipes in the scored talks plus the budget table and copies the result
into `pacing.adherence`.
Contract
--------
Stdin (JSON):
{
"talks": [
{"filename": "...", "date": "YYYY-MM-DD",
"slide_count": <int>, "talk_duration_estimate": "35 min"}
],
"slide_budgets": [
{"duration_min": <int>, "max_slides": <int>, "slides_per_min": <float>}
],
"worst_n": 5 # optional, default 5
}
Stdout (JSON):
{
"talks_over_budget": <int>,
"talks_scored": <int>,
"over_budget_rate": <float>, # 0.0 when nothing scored
"trend": "improving|stable|worsening",
"worst_offenders": [
{"filename": "...", "slides_per_minute": <float>,
"budget_slides_per_minute": <float>, "over_by": "40%"}
]
}
Duration parsing: an hour quantity in `talk_duration_estimate` ("h", "hr",
"hour", decimals allowed) converts to minutes and adds a directly following
minute quantity ("2 h 01 min" -> 121, "2h01m" -> 121, "1.5 hours" -> 90,
".5 hours" -> 30). A quantity longer than six digits is malformed input and
exits 1 with a diagnostic. Otherwise the first
integer is the value in minutes ("~45-50 min" -> 45). Talks with no
parseable minutes, missing/zero slide_count, or zero minutes are skipped (not
scored). Budget band: the entry with the largest duration <= the talk's minutes
(band duration read from `duration_min` or `duration_minutes`), or the smallest
band when the talk is shorter than every band.
Trend compares the over-budget rate of the older half vs. the newer half of the
date-sorted scored talks; fewer than 4 scored talks yields "stable".
Exit codes:
0 success
1 malformed stdin JSON or wrong shape. Diagnostic goes to stderr.
"""
from __future__ import annotations
import json
import re
from fractions import Fraction
import sys
TREND_EPSILON = 0.10
MIN_TALKS_FOR_TREND = 4
DEFAULT_WORST_N = 5
# A unit ends where letters end, so compact "2h01m" matches while "5 happy" does not.
# The quantity starts only at a number's first character and has one reading, so
# a long digit run is scanned linearly instead of backtracking.
_HOURS_MINUTES = re.compile(
r"(?<![\d.])(\d+(?:\.\d+)?|\.\d+)\s*(?:hours?|hrs?|h)(?![a-z])"
r"(?:\s*(\d+)\s*(?:minutes?|mins?|m)(?![a-z]))?",
re.IGNORECASE,
)
# No talk runs a million minutes; a longer quantity is malformed input, not a duration.
_MAX_QUANTITY_DIGITS = 6
def _quantity(text: str, estimate: str) -> str:
if len(text.replace(".", "")) > _MAX_QUANTITY_DIGITS:
raise ValueError(
f"talk_duration_estimate quantity {text[:12]}... is longer than "
f"{_MAX_QUANTITY_DIGITS} digits; write minutes like '45 min' or "
f"'2 h 01 min' (estimate starts {estimate[:40]!r})"
)
return text
def parse_minutes(estimate: object) -> int | None:
"""Minutes in a duration estimate.
An hour quantity ("2 h", "1.5 hours", "2 h 01 min") converts to minutes and
adds a directly following minute quantity. Otherwise the first integer is
read as minutes ("35 min", "45-50 min" -> 45).
"""
if not isinstance(estimate, str):
return None
hours = _HOURS_MINUTES.search(estimate)
if hours is not None:
# Fraction keeps decimal hours exact; no float can overflow.
hour_text = _quantity(hours.group(1), estimate)
minute_text = _quantity(hours.group(2) or "0", estimate)
value = round(Fraction(hour_text) * 60) + int(minute_text)
return value or None
match = re.search(r"\d+", estimate)
if match is None:
return None
return int(_quantity(match.group(), estimate)) or None
def _band_minutes(band: dict) -> int:
"""Band duration, tolerating `duration_min` or `duration_minutes`.
Matches the both-key tolerance in presentation-creator/guardrail-check.py.
"""
value = band.get("duration_min")
if value is None:
value = band.get("duration_minutes")
if value is None:
raise KeyError("budget band missing duration_min/duration_minutes")
return value
def budget_for(minutes: int, budgets: list[dict]) -> float | None:
"""slides_per_min of the applicable band (largest duration <= minutes)."""
if not budgets:
return None
ordered = sorted(budgets, key=_band_minutes)
applicable = [b for b in ordered if _band_minutes(b) <= minutes]
band = applicable[-1] if applicable else ordered[0]
value = float(band["slides_per_min"])
if value <= 0:
raise ValueError(f"slide budget slides_per_min must be positive, got {value}")
return value
def _over_budget_rate(talks: list[dict]) -> float:
if not talks:
return 0.0
return sum(1 for t in talks if t["over_budget"]) / len(talks)
def compute(payload: dict) -> dict:
budgets = payload.get("slide_budgets", [])
worst_n = payload.get("worst_n", DEFAULT_WORST_N)
scored: list[dict] = []
for talk in payload.get("talks", []):
slide_count = talk.get("slide_count")
minutes = parse_minutes(talk.get("talk_duration_estimate", ""))
if not slide_count or not minutes:
continue
budget = budget_for(minutes, budgets)
if budget is None:
continue
spm = slide_count / minutes
scored.append(
{
"filename": talk.get("filename"),
"date": talk.get("date", ""),
"slides_per_minute": round(spm, 2),
"budget_slides_per_minute": budget,
"over_budget": spm > budget,
"over_by_ratio": (spm / budget) - 1.0,
}
)
over = [t for t in scored if t["over_budget"]]
worst = sorted(over, key=lambda t: t["over_by_ratio"], reverse=True)[:worst_n]
trend = "stable"
if len(scored) >= MIN_TALKS_FOR_TREND:
chrono = sorted(scored, key=lambda t: t["date"])
half = len(chrono) // 2
delta = _over_budget_rate(chrono[half:]) - _over_budget_rate(chrono[:half])
if delta > TREND_EPSILON:
trend = "worsening"
elif delta < -TREND_EPSILON:
trend = "improving"
return {
"talks_over_budget": len(over),
"talks_scored": len(scored),
"over_budget_rate": round(_over_budget_rate(scored), 2),
"trend": trend,
"worst_offenders": [
{
"filename": t["filename"],
"slides_per_minute": t["slides_per_minute"],
"budget_slides_per_minute": t["budget_slides_per_minute"],
"over_by": f"{round(t['over_by_ratio'] * 100)}%",
}
for t in worst
],
}
def main() -> int:
try:
payload = json.loads(sys.stdin.read())
except json.JSONDecodeError as exc:
print(f"ERROR: stdin is not valid JSON: {exc}", file=sys.stderr)
return 1
if not isinstance(payload, dict):
print(
"ERROR: stdin JSON must be an object with 'talks' and 'slide_budgets'.",
file=sys.stderr,
)
return 1
if not isinstance(payload.get("talks", []), list) or not isinstance(
payload.get("slide_budgets", []), list
):
print(
"ERROR: 'talks' and 'slide_budgets' must be JSON arrays.", file=sys.stderr
)
return 1
try:
result = compute(payload)
except (AttributeError, KeyError, TypeError, ValueError) as exc:
print(f"ERROR: malformed payload shape: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main()).tessl-plugin
rules
skills
illustrations
presentation-creator
references
patterns
build
deliver
prepare
scripts
screencast-editor
screencast-recorder
shownotes-publisher
vault-clarification
vault-ingress
references
scripts
vault-profile