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 beat

Then 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 playsComes from
The spoken linenarration, else caption, else an auto-generated line from the action.
The on-screen cardcaption, else an auto-generated one.
Write the long version in 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#

ProviderNotes
kokoroKokoro-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).
sayThe 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#

For 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.wav

No 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: -19

sfx 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 --watch

Edit narration, save, listen. Synthesized lines are cached by content, so only the lines you actually changed are re-spoken.