Narration & voices
Narration is synthesized at compose time, offline, from the lines in your spec. Which means rewriting the script costs a re-compose, not a re-record — and no audio ever leaves your machine.
Turning it on#
output:
audio:
narration: tts
provider: kokoro # or 'say' on macOS
voice: heart # heart (default) or michael
rate: 105 # kokoro: playback speed ×100
sfx: true # soft click at each click beatThen write the line on the step:
- click: 'role=button[name="Create user"]'
caption: Create the account
narration: One click, and the account is live — no invitation email to chase.Spoken line versus caption#
These are separate fields on purpose. A caption has to fit a card on screen; a spoken line does not. Resolution order:
| What plays | Comes from |
|---|---|
| The spoken line | narration, else caption, else an auto-generated line from the action. |
| The on-screen card | caption, else an auto-generated one. |
narration. Keeping caption short is what stops the captions check from reporting an overflow, and a viewer reads a short card far more easily than a paragraph.Providers#
| Provider | Notes |
|---|---|
kokoro | Kokoro-82M, a natural neural voice that runs offline on any platform. Needs the optional kokoro-js package, which npx playhead init installs along with the model (skip it with --no-tts). |
say | The macOS built-in. Zero dependencies, Mac-only, and audibly robotic. It is the schema default because it is always available there — for anything shareable, use Kokoro. |
Voices#
Two curated voices, heart (default) and michael. The aliases af_heart and am_michael are also accepted. The set is deliberately small: two voices that sound good in a product video beats forty that mostly do not.
rate means two different things#
say, rate is words per minute (around 178 is normal). For kokoro it is playback speed ×100, where 100 is normal and 80–140 is the sensible range. A say-tuned spec switched to Kokoro would render at 1.78× — so the schema rejects it at parse time rather than at listen time.Audition before you commit#
playhead say "One click, and the account is live." \
--provider kokoro --voice michael --rate 105 -o line.wavNo spec, no render — it just speaks. This is much the fastest way to settle on a voice and a rate before writing twenty lines in the wrong one.
Pacing grows to fit the voice#
A step never ends mid-sentence. With narration on, compose lengthens steps so each line finishes inside its own window, and the audio check proves it afterwards by confirming every line is audible in its window.
The practical consequence is worth planning for: narration makes videos considerably longer than the capture. The walkthrough on the examples page is 47 seconds of captured source and 116 seconds of finished video, entirely because twenty spoken lines needed room.
Sound effects and music#
output:
audio:
narration: tts
provider: kokoro
sfx: true
music:
file: assets/bed.mp3
gainDb: -19sfx adds a soft synthesized click at each click and select beat — it does a surprising amount of work in making a synthetic cursor feel real. A music bed is your own licensed file (nothing is bundled) and is ducked beneath narration automatically.
Loudness#
Any audio work finishes with EBU R128 normalization to I = −16 LUFS, so videos from different specs sit at the same level and nobody reaches for the volume between two clips in the same docs page.
Iterating on a script#
playhead capture specs/tour.yaml -o out # once
playhead compose out/capture --spec tour.yaml --watchEdit narration, save, listen. Synthesized lines are cached by content, so only the lines you actually changed are re-spoken.