Six-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, and publish talk pages to a Jekyll shownotes site. Includes a 111-entry Presentation Patterns taxonomy (81 observable: 62 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns) for scoring, brainstorming, and go-live preparation.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/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: the first integer in `talk_duration_estimate` is the value;
an "hour"/"hr" unit multiplies it by 60 (so "1 hour" -> 60). 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
import sys
TREND_EPSILON = 0.10
MIN_TALKS_FOR_TREND = 4
DEFAULT_WORST_N = 5
def parse_minutes(estimate: object) -> int | None:
"""First integer in the string, ×60 when an hour unit is present."""
if not isinstance(estimate, str):
return None
match = re.search(r"\d+", estimate)
if match is None:
return None
value = int(match.group())
if re.search(r"\b(hour|hr)s?\b", estimate, re.IGNORECASE):
value *= 60
return value 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
shownotes-publisher
vault-clarification
vault-ingress
references
scripts
vault-profile