Skip to content

Repository files navigation

presentation-coach

Audit a rehearsal recording against the slide deck it was delivered with, then track whether the next run actually got better.

Give it a video of yourself practising and your deck. It transcribes locally, measures how you delivered it, cross-checks every number you said against every number on your slides, and hands an agent a pack of measured facts to review.

Nothing is uploaded. Transcription runs on your machine, and the tool never calls a model itself.

The thing it catches

From a real rehearsal: the speaker said "180 ms" while the slide behind him read "120.0 ms". Nothing in the audio reveals that — but the room sees and hears both at once.

16:05  spoke '180 ms'   conf=0.52
       nearest: 120 ms on slides 33, 41, 42, 44

It survives into the transcript only because the recogniser is deliberately never primed with numbers. Prime it with the correct figure and it "hears" the correct figure — the same audio transcribed as 120 at confidence 0.49 instead of 180 at 0.91, silently deleting the finding.

Why not just ask an LLM?

Ask a language model to "review this transcript" and it produces a confident, well-formatted review containing invented numbers. A widely used tool, given a real 28:59 rehearsal, reported:

It said Reality
Runtime 19:52 28:59
A 12.0 s pause Longest real gap 4.50 s
16 slides 45 slides

Its judgements were reasonable. Its measurements were fiction — and the runtime error alone inverted the headline finding, turning "five minutes under budget" into what was really four minutes over.

So the work is split, and the split is enforced:

Python measures. The agent judges.

Everything countable is computed from timestamps in Python. The reviewer prompt forbids the agent from recomputing any of it.

Install

As a Claude Code plugin — recommended, and the way it is designed to be used:

/plugin marketplace add robbiegal/plugin-marketplace
/plugin install presentation-coach@robbiegal-tools
/presentation-coach:coach-doctor

coach-doctor builds a Python environment in the plugin's data directory, which survives plugin updates. Nothing installs at plugin-install time — the transcription extra is hundreds of megabytes before a ~3 GB model, and no session should block on that.

As a plain CLI:

git clone https://github.com/robbiegal/presentation-coach
cd presentation-coach
pip install -e ".[transcribe,prosody]"

Needs Python 3.10+ and ffmpeg. Core install is stdlib-only and takes seconds; the heavy pieces are optional extras (transcribe, prosody, align, pdf, ocr).

Use it

mkdir my-talk && cd my-talk
presentation-coach init                       # talk.yaml + ledger.md

presentation-coach audit \
    --video "dry-run-1.mp4" \
    --deck  "deck.pptx" \
    --label "first run"

presentation-coach prompt review              # give this + evidence.json to an agent
presentation-coach validate --run run-01      # refuses a malformed findings file
presentation-coach trend                      # once you have two runs

The audit writes audit/runs/run-01/:

File What it is
evidence.json Every measured number. The reviewer's only source of facts
transcript.timestamped.md Verbatim, uncertain words marked [?]
transcript.txt · .srt · .vtt Reading copy and subtitles
transcript.words.json Every word with timestamp and confidence

The agent writes findings.json back. Validate it — a malformed findings file that got silently skipped would make the next run report every finding as resolved. It is refused loudly instead. See examples/ for the exact shape.

What it measures

Dimension Notes
measured Timing vs budget Talk is timed first word to last; setup and teardown silence are not delivery
Pace Speaking-only wpm. Wall-clock is depressed by embedded video and belongs to timing
Filler discipline um/uh scored against a 2% cap. so/right/basically counted, never scored
Slide/speech consistency Every spoken figure against every figure on the deck
Pitch variation How much of the talk went flat, in semitones
judged Technical accuracy · claim discipline · structure · audience fit · demo narration · Q&A exposure · disclosure The agent's, from the evidence

Composite is a weighted mean on 0–5, not a total, so it stays comparable between runs that scored different dimensions. Readiness is a separate gate: NOT READY while any BLOCKER is open, however good the average.

A check that could not run is reported, never scored as zero. Deck-dependent checks degrade to nothing on decks without the right properties, and evidence.capabilities says which and why — a silent zero reads as a clean result.

Tracking across runs

Decks change between rehearsals, so decks are versioned separately from runs:

presentation-coach audit --video dry-run-2.mp4 --deck deck.pptx      # same deck
presentation-coach audit --video dry-run-3.mp4 --deck deck-v2.pptx   # revised
presentation-coach status
presentation-coach deck-diff deck-01 deck-02
run      deck       duration    wpm  fillers  fig?  label
run-01   deck-01       28:59  125.5      131     4  first run
run-02   deck-01       26:10  131.0       88     2  second run
run-03   deck-02       25:05  134.2       71     1  after deck revision

The trap it exists to avoid: you can "fix" a wrong-number finding by editing the slide to match what you said. The metric improves and the talk gets worse. So every resolved finding is labelled fixed-by-speaker, fixed-by-deck or removed, and only the first is progress.

Where a delta would be meaningless the tool refuses it and says why — a changed deck, a changed alignment method, a moved pitch bracket, or two runs covering very different spans of speech.

Configuration

talk.yaml — all optional; defaults describe a generic 25-minute talk.

talk:
  budget_minutes: 25
delivery:
  wpm_target: [130, 160]   # compared against speaking-only wpm
  filler_rate_max: 0.02    # tier 1 (um/uh) only

ledger.md holds domain truths your deck cannot express — the claims a reviewer should check hardest. Optional, but it is what lifts a review from "you said um a lot" to "that contradicts your own result."


Reference

How delivery is measured — and why two of its numbers are not scored

Pitch dispersion is reported as a p10–p90 range in semitones, not as Hincks' pitch variation quotient, and that choice was forced by measurement rather than taste.

PVQ is F0 standard deviation over F0 mean, validated against listener ratings of liveliness at r = 0.83 — better pedigree than anything else here. But standard deviation is not robust. On one real recording PVQ read 0.146, 0.194 or 0.491 depending only on where Praat's pitch ceiling sat, because a handful of octave-up frames dominate a variance. The semitone range over those same three settings moved 5.89 → 6.04 → 7.47. Semitones are also the unit the published comparison is in: English TED speakers show ~7.05 st of 80-percentile range.

So PVQ is reported and never compared against its own published threshold — that threshold belongs to pitch settings the paper does not record.

Measured Scored Why
Pitch range (semitones) yes Robust to the settings that swing PVQ 3.4×
Flat windows yes Share of 10 s windows spanning under 4 st. Calibrated on four consecutive rehearsals: 16 / 9 / 7 / 7 %
Pace variation no Seven talks all fell between 0.148 and 0.265 — any threshold sits inside the whole observed spread
Sentence endings no The rising fraction moved 38–50% on one recording from the tail length alone
Level dynamics no Spread ran 5–24 dB across four talks on recording chain alone

The useful output is not the score but the located stretch:

Flattest stretches: 23:00 for 130s (3.4 st, slides 38-43), 25:20 for 80s (3.6 st)

"Pitch variation 3/5" is not something anyone can rehearse. "You were inside a major third for 130 seconds across the mitigation slides" is.

Slide timing, and matching video frames against the deck

Each slide is mapped to the stretch of time it was on screen, so section timing is measured rather than guessed. The default needs only ffmpeg.

Two signals combine: the transcript and the deck are two orderings of the same material, so aligning them says which slide is up; ffmpeg scene detection says when the picture changed.

   00:04    06:36  Background
   06:40    05:30  Acquisition & Operation
   12:10    02:25  Demo
   14:35    10:23  Measurement
   24:57    03:56  Detection / Mitigations

Sections come from the deck's own divider slides, so they survive insertions and reordering — defining them by slide ranges breaks the moment you add a slide.

--refine renders the deck with LibreOffice and matches sampled video frames against it, an independent signal that does not care whether the speaker read the slide, paraphrased it, or talked past it.

The hard part is not matching, it is cropping. A recording composites the deck with a camera tile, event branding and letterbox, and hashing the whole frame compares furniture. So the slide rectangle is located first, by searching for the crop that best correlates with the rendered deck. Measured across four real talks, boundary error is 0.02–0.06 of the frame, including two broadcast layouts where inferring the region from motion alone failed.

Method Slides matched Mean error Needs
Content (default) 28 / 45 ~64 s ffmpeg only
--refine 41 / 45 ~23 s LibreOffice, .pptx, a few extra minutes

--refine is .pptx only and falls back to content alignment, with a warning, if the toolchain is missing. Slides nobody spoke to get no span at all — and are reported as unmatched, not as skipped, because at a low match score those are as likely to be alignment misses.

Deck formats, and decks that are only images
Format Text Rendering for --refine
.pptx stdlib (OOXML) LibreOffice
.html stdlib not supported
.pdf [pdf] extra built in — no LibreOffice needed

Hidden slides are skipped and the remainder renumbered, so slide numbers match what the room saw.

Image-only PDFs — a deck exported as page images — are common and carry no extractable text at all. The tool says so rather than reporting an empty deck as a deck with nothing on it:

NOTE: talk.pdf contains no extractable text (81 slides scanned).
      It is almost certainly a deck exported as page images.
      Without --ocr there is no vocabulary, no figure cross-check and no
      section detection. Slide/time alignment still works with --refine.

--ocr recovers it (~3 s/slide, cached on the deck's bytes). On a real 81-slide conference deck it recovered text on 77 slides and produced a usable domain vocabulary from a deck that had looked completely empty.

Keep the deck format consistent across runs. Deck versions are identified by content, and the .pptx and .html exports of the same deck differ slightly. Switching format registers as a slide revision that never happened.

Reading the numbers
  • Two wpm figures. Speaking-only is the delivery number; wall-clock is depressed by embedded video and silence and belongs to timing.
  • Fillers are two tiers. um/uh are scored. so/right/basically are ordinary English — counted for tic detection, never scored. Blending them gives a number nobody can act on.
  • [?] means the recogniser was unsure, not that you misspoke. Check the timestamp before believing it.
  • Overrun is a content-volume problem. The tool quotes a word count to cut, never "speak faster" — pace is close to a personal constant and asking someone to add 30 wpm under stage pressure fails on the day.
  • Speaker notes are ignored by design — the audience cannot see them.
Updating the plugin
/plugin update presentation-coach@robbiegal-tools

The scoped id is required; the bare plugin name reports "not found". A plugin update does not rebuild the Python environment — the environment lives in a data directory that deliberately outlives every plugin version. The SessionStart check says when the two have drifted, and coach-doctor refreshes it.


Status

Everything described above works: transcription, deck parsing (.pptx / .html / .pdf), vocabulary derivation, figure cross-check, delivery and prosody measurement, slide↔time alignment including render-based refinement, section timing, deck versioning and diffing, findings lifecycle with resolution attribution, trend reporting, and the Claude Code plugin.

See CLAUDE.md for the design invariants before changing anything.

Licence

MIT.

About

Audit a rehearsal recording against its slide deck - timing, pace, fillers, figures you got wrong, and vocal delivery - and track progress across dry runs. Python measures, an agent judges.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages