Voice: narration, word timings, pronunciation
Read this when a video needs a voice-over. That covers picking a voice or language (Spanish included), writing narration, sizing a script to a length, fixing how a word is said, timing scenes or captions to the voice, and aligning a recorded take to its script.
See it done: 02 How a heat pump heats a home · 13 A Wikipedia article, explained · 26 Nobel Chemistry, in English and Spanish
Everything runs locally. Kokoro is the default engine. It gives exact word timings and 54 voices across 9 language variants (8 languages, English counted once). The text never leaves the machine.
Essentials#
- Defaults:
af_heart(English),ef_dora(Spanish),bf_emma(British English),ff_siwis(French). Never swap a voice the user named; otherwise use the default and say which voice you used (§ Choosing a voice) - Narration:
showtime voice script narration.md -o voice/ --fit 15 --estimatefirst (it prints the estimate and speaks nothing), then the same command without--estimate; one## <scene id>per scene,--fit 15lands it on 15 s; on "cut about N words", cut them and rerun (§ Script → timeline → scenes) - Outputs:
vo.wav(−16 LUFS, thevoicetrack inaudio/mix.json),timeline.json(slots, word times),vo.words.json(forshowtime captions),vo.srt,lines/NN-id.wav(§ Script → timeline → scenes) - The voice sets scene lengths: DOM projects run
showtime retime <project> --from-voice voice/timeline.json; canvas films loadshowtime voice cues voice/timeline.json -o voice/cues.js. Never hand-edit durations; trigger a reveal at thestartof the word that names it (§ Script → timeline → scenes) - To change a line, edit it and rerun: only that line re-synthesizes; later
startvalues move, so rereadtimeline.json(§ Script → timeline → scenes) - Budget about 2.8–3.3 words/s in English at speed 1.0, 2.9 in Spanish, 2.4 for a calm read; leave 10–20 % of the video without speech (§ Word budgets)
- One idea per sentence, 8–16 words; end on the payoff word with 0.5–1 s of tail. Directions go in
<!-- comments -->or> quotelines, never in the spoken text (§ Writing for the ear) - Speed: 0.9–0.95 for explainers, 1.0 natural, 1.05–1.15 for hooks; above 1.2 sounds rushed. When a fit leaves a line faster than x1.1, cut words rather than accept it (§ Choosing a voice, § Script → timeline → scenes)
- Wrong word:
showtime voice ipa "Word" [--lang es], then alexicon.jsonentry (ipaKokoro only,sayevery engine) or inline[SQL](sequel)(§ Pronunciation fixes) - Read-back:
voice scriptandvoice saytranscribe what they wrote and WARN when a name, acronym or number is heard differently ("OpenAI" heard as "OpenI"). Fix it and rerun until it passes (§ Pronunciation fixes) - Spanish voices read English names with Spanish rules: add the product name to the lexicon in every Spanish
video.
--lang esis Castilian,--lang es-419Latin American (§ Pronunciation fixes, § Choosing a voice) - Supertonic is not reproducible: keep its WAVs and
timeline.json. You cannot listen: compare candidates withshowtime transcriberound-trips andvoice ipa(§ Choosing a voice) - Narration masters to −16 LUFS / −1.5 dBTP; keep it there and let
audio mixduck the music (§ Mastering) - Never imitate a real person; say the voice is synthetic where the platform expects it; CC-BY Piper voices need their credit line (§ Engines and licensing)
- Synthesize the voice before a long browser render; "Kokoro is not ready":
showtime setup(§ Performance, § Platform notes and troubleshooting)
| Section | Lines |
|---|---|
| Commands | 58-70 |
| Choosing a voice | 72-111 |
| Writing for the ear | 113-124 |
| Word budgets | 126-137 |
| Script → timeline → scenes (timing-driven editing) | 139-194 |
| Captions from TTS timings | 196-202 |
| Pronunciation fixes | 204-247 |
| Aligning a recorded voice (or another engine's output) | 249-264 |
| Mastering | 266-282 |
| Engines and licensing | 284-294 |
| Bring your own voice engine | 296-317 |
| Performance (measured on a 6-core Intel i5-8500, CPU only, load ~3) | 319-334 |
| Platform notes and troubleshooting | 336-349 |
Commands#
| Command | What it does |
|---|---|
showtime voice list [--lang es] [--json] |
Lists voices with language, gender, quality grade, notes and whether each is installed. |
showtime voice say "text" -v af_heart -o vo.wav |
Writes vo.wav (48 kHz, −16 LUFS) and vo.words.json (start and end time of every word). |
showtime voice script narration.md -o voice/ |
Writes per-line clips, vo.wav, timeline.json, vo.words.json, vo.srt and readback.json (the read-back, § Pronunciation fixes). --fit 15 lands the whole narration on 15 s (--fit 15 --estimate first says if it can). |
showtime voice align take.wav -f script.txt -o take.words.json |
Gives word timings for a recording and its known text. |
showtime voice master raw.wav -o vo.wav [--preset podcast] |
Runs high-pass, de-ess and gentle compression, then two-pass loudness normalization. |
showtime voice ipa "Kubernetes" [--lang es] |
Shows the phonemes espeak-ng produces, so you can write a pronunciation fix. |
showtime voice bench [-v a,b] |
Measures speed and speaking rate on this machine. |
Every command has --help with examples, and --json for machine output. A say or script line whose text, voice, speed and lexicon are unchanged comes back from the cache instantly. Editing one line re-synthesizes only that line.
Choosing a voice#
The defaults are af_heart for English, ef_dora for Spanish, bf_emma for British English and ff_siwis for French. Grades are the model author's published ratings (A is best). Words per second (w/s) was measured here at speed 1.0 on one paragraph and includes normal sentence pauses.
| Tone / use | Voice | Grade | w/s | Notes |
|---|---|---|---|---|
| Warm, natural, all-round (default) | af_heart |
A | 3.06 | Safest choice for launches and explainers |
| Bright, energetic (promo, social) | af_bella |
A- | 2.96 | |
| Calm, intimate, slow | af_nicole |
B- | 1.96 | Soft and close to the mic; for calm pieces, not fast cuts. Plan about 1.8 w/s of script for a nicole explainer (a 70 s fit took 124 words) |
| Clear and neutral (docs, UI walkthroughs) | af_sarah |
C+ | 3.15 | |
| Steady male, trustworthy (default male) | am_michael |
C+ | 2.80 | |
| Deep, confident (trailers, big reveals) | am_fenrir |
C+ | 3.07 | Pair with --style trailer |
| Lively male (upbeat promos) | am_puck |
C+ | 3.10 | |
| British, polished (tutorials, docs) | bf_emma |
B- | 3.34 | Fast: consider speed 0.92 |
| British, authoritative (documentary) | bm_george |
C | 2.84 | |
| British storyteller | bm_fable |
C | 3.43 | |
| Spanish, female (default) | ef_dora |
– | 2.90 | Exact timings; use the lexicon for English brand names |
| Spanish, male | em_alex |
– | 2.87 | |
| Spanish, most natural (optional) | supertonic:F1 / supertonic:M1 + --lang es |
– | 2.54 / 2.51 | Needs showtime setup --with supertonic; timings are aligned (about 20–30 ms) |
Which Spanish voice: ef_dora is the default (installed, exact word timings, the localize workflow
uses it); Supertonic sounds more natural when it is installed. Accent: --lang es gives Castilian
pronunciation for Kokoro and Piper voices ("cero" with θ); --lang es-419 gives Latin American seseo
(also in voice ipa, front matter lang: es-419, or a line's lang). Supertonic uses its own model and
ignores the region code. Supertonic is not reproducible: synthesizing the same line again moves its
timing by 30-260 ms (Kokoro stays within ~50 ms), so a project whose cues come from its timeline must
keep its WAVs and timeline.json (or re-run voice cues/retime --from-voice after a rebuild). You cannot listen: compare
candidates with showtime transcribe round-trips and voice ipa, and state the choice to the user.
| Use | Voice | Grade | Words/s | Notes |
|---|---|---|---|---|
| Spanish or English quick drafts | piper:es_MX-claude-high, piper:en_US-ljspeech-high |
– | 2.66 / 3.21 | Fast but robotic; downloads 65–115 MB on first use |
| French / Italian / Portuguese (BR) / Hindi | ff_siwis / if_sara, im_nicola / pf_dora, pm_alex / hf_alpha, hm_omega |
B- to C | ||
| Japanese / Mandarin | jf_alpha / zf_xiaobei |
C+ / D | Phonemes come from espeak-ng, so the accent is approximate. Avoid these for published work. |
- Blends:
-v af_heart:60+am_michael:40mixes voice styles (the weights are normalized). Use a blend to make a brand's own voice. - Styles:
--stylesets speed and pause lengths together. The styles areneutral,calm(0.92x, long pauses),warm,upbeat(1.08x),energetic(1.15x),tutorial,documentaryandtrailer(0.9x, 0.6 s sentence pauses). - Speed: 0.9–0.95 suits explainers and anything the viewer must follow on screen. 1.0 is natural. 1.05–1.15 suits hooks and promos. Above 1.2 sounds rushed. Kokoro accepts 0.5–2.0.
- Explicit choices: a voice the user names is never swapped silently. An unknown voice is an error that lists close matches.
- Asking the user: ask about gender or tone only when it changes the video. Otherwise use the default and say which voice you used.
Writing for the ear#
- One idea per sentence, 8–16 words. Long sentences lose the listener and make captions wrap badly.
- Write numbers the way they should be said when it matters: "twenty twenty-six", "three and a half". Common forms are handled already. "3.5" becomes "3 point 5". "in 2026" becomes "twenty twenty-six". "Dr." becomes "doctor". "e.g." becomes "for example". "showtime.dev" becomes "showtime dot dev". "v2.0" becomes "version 2 point 0". A bare year after "the" is not covered ("the 1973 Nobel" is read "nineteen hundred seventy three"): write it inline,
the [1973](nineteen seventy-three) Nobel. - Spanish (and French, Portuguese, Italian, German) numbers are read as written in those languages: "60 000" and "60.000" are one number ("sesenta mil", not digit by digit), "13,7" is "trece coma siete". The Hawaiian ʻokina (U+02BB, or ‘ between letters) is silent instead of spelled out ("Hawaiʻi").
- Math and sequences by ear. A listener hears no punctuation: "One. One plus three is four." is heard as "one one plus three". Never end a sentence and start the next on the same word; say a running sum as steps ("Start with one. Add three, and you have four. Add five: nine.") and symbols as words ("n squared", "two n minus one").
voice scriptwarns when a line repeats a word across a sentence break. - Front-load the subject: "Showtime renders on your machine", not "On your machine, rendering is done by Showtime".
- Name what is on screen at the moment it appears. The voice sets the timing, so a reveal happens when its word is spoken, and not before.
- Use contractions and plain words. "It's", "you'll", "use". Avoid "utilize". Read the script aloud once in your head.
- Punctuation is timing. A period gives about a 0.3 s pause (0.45 s with
calm). A comma gives about 0.12 s.[pause 0.6]gives an exact silence. Use a dash for a quick turn. - Never put directions in the spoken text. Directions go in
<!-- comments -->or> quotelines in a Markdown script, and are never spoken. - End on the payoff word: "...rendered entirely on your own machine." Leave 0.5–1 s of tail before the end card.
Word budgets#
At speed 1.0 the Kokoro English voices speak about 2.8–3.3 words per second (170–200 wpm) including sentence pauses. Spanish runs about 2.9 w/s. A calm read (speed 0.92 with longer pauses) drops to about 2.4 w/s. Plan with these numbers, then trust the real durations in timeline.json.
| Spoken length | Brisk (1.0, ~3.0 w/s) | Calm (~2.4 w/s) |
|---|---|---|
| 6 s hook | 15–18 words | 12–14 words |
| 15 s | 40–45 words | 32–36 words |
| 30 s | 80–90 words | 65–72 words |
| 60 s | 160–180 words | 130–145 words |
Leave 10–20 % of a video's length without speech: the open, pauses before reveals, and the end card. Run showtime voice bench -v <voice> to measure a specific voice on this machine.
Script → timeline → scenes (timing-driven editing)#
The voice decides how long each scene lasts. Write the script as one line per scene, build it, then set each scene's duration from its slot.
---
voice: af_heart
gap: 0.4 # default silence after each line (s)
tail: 0.8 # silence after the last line
---
## hook
Meet Showtime. [pause 0.3] The video studio that lives in your terminal.
> Director's note: warm, not salesy (quotes and <!-- comments --> are never spoken)
## demo {voice=am_michael speed=1.05 pause_after=0.7}
Point it at any project, and get a polished launch film in minutes.
## outro {style=calm fit=3.2}
Rendered entirely on your own machine.
Front matter (and the top level of a JSON script) takes voice, speed, style, lang, engine,
gap, lead_in, tail, lufs, lexicon and fit; each line can override voice, speed, style,
lang, engine, pause_after, fit and at.
JSON works the same way: {"voice": ..., "gap": ..., "lines": [{"id", "text", "voice", "speed", "style", "lang", "engine", "pause_after", "fit", "at"}]}. A bare list of strings also works. Without headings, each paragraph of a Markdown or text file is one line.
fit: 3.2adjusts that line's speed (within 0.8–1.25x of its own speed) so it lasts about 3.2 s. Use it when a scene has a fixed length, such as a beat-locked cut.at: 12.0pins a line to start at 12 s, for example on a music drop. Later lines follow on from it. If the pinned line would overlap the one before, you get a warning.
Fitting the whole script to a length: showtime voice script narration.md -o voice --fit 15 (run it with --estimate first to see if it fits). The target can also be a top-level "fit": 15 in the JSON or fit: 15 in the front matter. It works in three steps and stops as soon as the narration is on target (within 1 %, at least 0.1 s; then a small overshoot trims the pauses and a short result is padded, so vo.wav lasts the target):
- Every line without its own
fitchanges speed together, within 0.85–1.15x. A new speed means every line re-synthesizes (up to four tries; it says so first), so a 13-line script can take a few minutes. Each try's lines are cached. - Still long: the pauses between lines shrink (never below 0.2 s) and so does the tail (never below 0.3 s).
- Still long: it prints
still Xs over ... cut about N words (of M; the voice speaks W words/s). Cut them from the script and run it again; nothing is dropped for you.
When the fit leaves any line faster than x1.1 (its own speed times the fit factor), it warns that the voice sounds rushed and says how many words to cut to fit at x1.0: explainers and documentaries read best at x0.9-1.0, so cut words rather than accept x1.15. A script that ends short is slowed (down to 0.85x), then padded with silence at the end so vo.wav lasts exactly the target; the output estimates how many words would fill the gap. It prints fit T: before -> result (speed xF, pauses -Xs, tail +Ys of silence), and timeline.json gets a fit block (target, before, result, speed_factor, pauses_trimmed, tail_padded, cut_words, add_words, wps, tolerance); line pause_after values and tail show the trimmed times.
Know before it speaks: with --fit, the run first estimates the length with the same arithmetic as the fit (speed 0.85–1.15x, then pauses and tail down to their floors) and prints fit T, estimated before any speech: .... Each line's length is its letters and digits at the voice's own rate, measured from lines that voice spoke earlier in this install (~/.showtime/cache/voice/rates.json, after 20 s of speech), else an assumed 11.8 characters/s that errs on the side of cutting more. A script that cannot fit says cut about N words right there, as a warning, before the first line is synthesized, and the same "x1.15 sounds rushed, cut about N words instead" warning as the real run; stop and cut instead of waiting for the passes. showtime voice script narration.md --fit 57 --estimate prints only that estimate (also as JSON under estimate) and synthesizes nothing. It is a count, not a measure: against recorded narrations it lands within 10 % of the real length, and the real run still reports the exact cut.
showtime voice script narration.md writes voice/ next to the script:
| File | Contents |
|---|---|
vo.wav |
All lines placed with their pauses, mastered to −16 LUFS, 48 kHz. Use it as the voice track in audio/mix.json. |
timeline.json |
duration, loudness and lines[]. Each line has id, text, voice, start, end, speech_start, speech_end, slot {start, end, duration}, pause_after, at (when pinned), file, timing, wps and words[] (absolute seconds). A flat words[] has a line id on each word. |
vo.words.json |
Every word in the shared transcript format (text, start, end, type: "word"), ready for showtime captions. |
vo.srt |
Captions of at most 32 characters per line (fits vertical video too), balanced, no one-word orphans, readable at 20 characters/s or slower. |
lines/NN-id.wav |
Each line alone, plus .words.json with line-relative times. Use these when scenes are rendered separately. |
Scene durations: make each scene lines[i].slot.duration long. A slot runs from the line's start to the next line's start, so it includes the pause. The last slot includes the tail. In a DOM project, showtime retime <project> --from-voice voice/timeline.json does it all at once: name each line after its scene (## demo narrates <section id="demo">), and it sets the scene lengths (0.3 s of picture, --pad, plus the slots), places each line as its own voice track, ducks the music and moves sections, effects, the poster and the caption words (render.md). Canvas films load showtime voice cues voice/timeline.json -o voice/cues.js (a VO table of line and word times) and read their cue times from it, so re-running voice script + voice cues re-times the film.
Reveal cues: trigger an element at the start of the word that names it. Take it from timeline.json words (add the line's scene offset when scenes are rendered separately).
Changing one line: edit it and run the script again. Only that line is synthesized again. Later start values move, so reread timeline.json and never hand-edit durations.
Captions from TTS timings#
Kokoro's word times come straight from the model's phoneme durations. The waveform runs about 50 ms behind that duration grid, so the times are shifted earlier by that amount (measured over 6 voices and 3 speeds). Each word start then snaps to a clear onset in the audio when there is one within 80 ms, and silence is trimmed from word ends. Measured on test sentences, starts land within about 10 ms of clear acoustic onsets, and within 15–40 ms of an independent CTC aligner.
- For styled or karaoke captions, feed
vo.words.jsontoshowtime captions(footage module). You can also loadtimeline.jsonin the page and highlightwords[i]whiletis betweenstartandend. vo.srtis a quick fallback for players and platforms.- The caption text is the script's own spelling ("2026", "Showtime,"), even where the speech says "twenty twenty-six".
Pronunciation fixes#
-
Run
showtime voice ipa "Kubernetes kubectl" [--lang es]to see what espeak-ng says for each word. It reads thelexicon.jsonin the current folder, or its one sub-folder with alexicon.json, or--project <dir>(voice scripthas no--project: it reads thelexicon.jsonnext to its script or one folder up, plus any--lexicon FILE), shows which lexicon it used, and itsphrase:line is what the voice reads with the lexicon and inline fixes applied. -
Add an entry to
lexicon.jsonnext to the script or project, or under"pronunciations"inbrand.json:{ "Showtime": "ʃˈoʊtaɪm", "kubectl": {"ipa": "kjˈuːb kəntɹˈoʊl"}, "SQL": {"say": "sequel"}, "GIF": {"ipa": {"en": "ɡˈɪf", "es": "ɡˈif"}}, "CLI": {"ipa": {"en": "sˌiːˌɛlˈaɪ"}, "case": true} }ipauses the symbols espeak-ng prints:ˈprimary stress before the stressed syllable,ːlong vowel. It works with Kokoro only.sayis a respelling that works with every engine (Supertonic and Piper read text, not phonemes).- Values can be per language (
"en","es","*"for any). Matching ignores case unless"case": true. Possessives ("Showtime's") reuse the entry.
-
For a one-off, fix the word inline:
[Showtime](/ʃˈoʊtaɪm/)for IPA or[SQL](sequel)for a respelling.
The lexicons merge in this order, and later ones win: the built-in list (lib/st/voice/lexicon.json, tech words espeak gets wrong such as CLI, JSON, kubectl, Redis, and English brand names for Spanish voices such as GitHub, YouTube, Claude), then $SHOWTIME_LEXICON, then the project's lexicon.json or brand.json, then --lexicon FILE.
For Spanish voices, English brand names need a lexicon entry; otherwise espeak reads them with Spanish rules ("Showtime" comes out as "show-TEE-meh"). The built-in list covers common ones. Add the product name for every Spanish video.
Read-back. The script can spell a name right and the voice still say it wrong. So voice script and voice say transcribe every line they wrote with the local recognizer (the one showtime transcribe uses, cached by the line's audio: an unchanged line costs nothing, a 60 s narration adds a few seconds). The heard words are lined up with the script sound by sound. Every word that matters and was heard differently is a WARN with the fix to try. Words that matter are names (capitalised inside a sentence, camelCase, acronyms), numbers, and words with a lexicon entry or an inline fix. Case, punctuation, other spellings of the same sound (colour, color) and digits against number words ("68", "sixty-eight") pass.
read-back: 1 word heard differently in 7 lines ("Open A I" as "OpenI")
WARN 26.66s shot-6: "Open A I" heard as "OpenI"
fix: the voice reads a lone "A" as the article ("uh") and swallows it: write the name as one word (OpenAI) ...
The recognizer can also respell a rare name the voice said right (Parakeet writes "JSO" for a good "JSON"). So a line with a flagged word is heard once more by a second recognizer when one is installed (Whisper small.en for English; it never downloads one). The note after the word says what it heard: Whisper small.en hears it as written (the voice is likely right) or also hears "JSO".
What to do with a WARN:
- Do not ship a line it flags until someone listened to it. The clip is
lines/NN-id.wav. - If it sounds wrong, run
showtime voice ipa "Word"to see what the voice is told. Fix the word with a lexiconsayoripaentry, or write it the way the voice reads it right, then rerunvoice scriptuntil the read-back passes. - If it sounds right, clear the word in the project's
showtime.json:"readback": {"ok": ["JSON"]}.voice script,voice sayand qa then list it as cleared, not as a WARN. Clear only words someone listened to. - If you listened and the voice is right, and
voice ipashows the sound it is told, but the recognizer keeps spelling the word another way ("QN" for "Qwen"), list it in"readback": {"note": ["Qwen"]}instead ofok: it stays in the read-back as a NOTE (NOTE ... "Qwen" heard as "QN" [listed in readback.note by the project: ...], INFO in qa) and never fails qa.voice ipaalone proves nothing, since the voice is always told its lexicon entry:noteis for after a person has listened, the same asok, and the tag says it is the project's claim, not a check.okandnotealso cover a name inside a longer phrase ("Qwen" for "Qwen Image") when the phrase's other words were heard as written;okwins when a word is in both. - Do not spell names letter by letter with a lone "A" ("Open A I"): espeak reads it as the article and the voice swallows it. Write the name as one word ("OpenAI"), or give it a lexicon entry.
readback.json(next tovo.wav) has each line's script and heard text, and for each flagged word what the voice was told (said), what was heard (heard_sounds) and the second recognizer's verdict (confirmed,second), andnotedwhen the word is listed inreadback.note.- qa checks the same thing on the project's
voice/vo.wav(thereadbackitem, § Hearing in qa.md), and review-pack puts the table into the critic'saudio.txt. qa FAILs a word only when it is a name in the title, the brand or the project lexicon and the second recognizer also heard it differently; otherwise it is a WARN.
It runs for the 25 languages of the local recognizer (Parakeet v3, English and Spanish included) when that recognizer is installed: it arrives with the first transcription, or showtime setup --fetch parakeet-tdt-0.6b-v3-int8 (about 490 MB; the read-back never downloads it on its own). Without it the read-back is skipped with a note. --no-readback or SHOWTIME_READBACK=0 turns it off, for example when a flag is wrong and you cannot clear the word in showtime.json.
Aligning a recorded voice (or another engine's output)#
showtime voice align take.wav -f script.txt [--lang es] [--method auto|ctc|tts|whisper|even]
The output keeps the script's exact words and punctuation. The table below compares each method's word starts with Kokoro's exact times on Kokoro speech.
| Method | Languages | Start error (mean) | Cost |
|---|---|---|---|
ctc: a wav2vec2 letter model (95 MB, downloaded on first use) plus forced alignment |
English | 5–40 ms (typical ~20) | ~1–3 s per minute of audio |
tts: the text is spoken with Kokoro, which has exact times, then warped onto the audio (MFCC + DTW) |
every Kokoro language | 20–35 ms with a different voice as reference | one Kokoro synthesis |
whisper: faster-whisper word times mapped onto the script |
99 | 100–125 ms | slow (large model for non-English) |
even: words spread over the voiced parts |
any | ~110–140 ms | instant |
auto tries ctc, then tts, then whisper, then even for English. For other languages it tries tts, then whisper, then even. Every method finishes by snapping word edges to the audio energy. --no-snap turns that off.
For real human takes the numbers are larger, because the speaker's pace and pauses differ from the reference. Check the result against the energy on your own footage.
Mastering#
voice say and voice script master by default (−16 LUFS, −1.5 dBTP, mono 48 kHz, 24-bit). --raw or --no-master skips it.
The chain is: high-pass at 70 Hz, de-esser, 3:1 compressor, then two-pass linear loudnorm. If a peaky or very short clip cannot reach the target, gain is added under a limiter. The chain adds no delay, so word times stay valid.
Presets for voice master:
| Preset | Target |
|---|---|
voice |
−16 |
podcast |
−16, stronger compression |
youtube |
−14 |
broadcast |
−23 |
gentle |
no compression |
The final video mix is normalized again (−14 by default), so keep narration at −16 and let audio mix duck the music under it.
Engines and licensing#
| Engine | Weights license | Notes |
|---|---|---|
| Kokoro-82M v1.0 (default) | Apache-2.0 | kokoro-onnx (MIT). Needs espeak-ng (GPL-3.0), which is loaded at run time from the installed wheel or the system, never copied into this repo. |
| Supertonic 3 (optional) | OpenRAIL-M: commercial use is allowed with use-based restrictions (no impersonation, deception or harm) | 31 languages. Code MIT. Upstream development has stopped, so the version is pinned. |
| Piper voices (optional, sherpa-onnx, Apache-2.0) | per voice | Only commercially usable voices are listed. Public domain: en_US-ljspeech-high, en_US-john/kristin/norman-medium, en_GB-cori-high. Apache-2.0: es_MX-claude-high. CC0: es_ES-davefx-medium. Unlicense: es_MX-ald-medium. CC-BY, credit required: en_US-libritts_r-medium, en_GB-alba-medium, es_ES-sharvard-medium. The credit line is in voice list --json → attribution; put it in the video's credits. |
| English aligner | Apache-2.0 | wav2vec2-base-960h, ONNX export |
- No cloning: no engine here clones a real person's voice. Do not imitate a real person.
- Disclosure: say that the voice is synthetic where the platform or audience expects it. Platforms increasingly ask for an "AI voice" label.
Bring your own voice engine#
Kokoro stays the default. To narrate with a local engine you already run, write a Python module that sets
ENGINE to an instance of st.voice.engines.base.Engine and save it as <name>.py, where the name matches
^[a-z][a-z0-9_]{0,31}$. A copy of st/voice/engines/piper.py is a good start: the module is loaded as
st.voice.engines.<name>, so its relative imports (from .base import Engine, from ...common import ShowtimeError) work from any folder. showtime looks for it in the folders
on $SHOWTIME_ENGINE_PATH (separated like PATH), then ~/.showtime/engines/ (it survives an update), then
next to piper.py. The names kokoro, supertonic and piper always mean the built-in engines.
synth()returns mono float32 audio. Withhas_timings = False(the usual case) the words are aligned afterwards like Piper's:ctcfor English, otherwisetts,whisperoreven(§ Aligning a recorded voice).available()is a cheap readiness check;default_voice,default_langandvoices()are optional.- Use it with
-v <name>:<voice>,--engine <name>, orSHOWTIME_VOICE=<name>:<voice>(the plugin's voice setting) as the default.showtime voice list --engine <name>lists whatvoices()returns. showtime doctorlists each engine it finds and whether it imports. When the voice setting names another engine, missing Kokoro files are a SKIP, not a FAIL; Kokoro is still needed for Kokoro voices and thettsaligner.
It is your code on your machines. If your engine talks to a process on localhost or a box you own on your network, that is your call, and showtime just loads it. showtime itself never calls a hosted service and never asks for a key, and it ships no network engine.
Performance (measured on a 6-core Intel i5-8500, CPU only, load ~3)#
| Step | Speed | Notes |
|---|---|---|
| Kokoro | RTF 0.34–0.42 | A 60 s narration takes about 25 s. |
| Supertonic | engine RTF 0.19–0.20 | Plus alignment. |
Piper es_MX-claude-high |
engine RTF 0.06 | |
| Model load | Kokoro about 1 s, aligner 0.2 s | |
| First espeak self-test | a few seconds, once | Cached in ~/.showtime/cache/voice/espeak.json. |
RTF means synthesis time divided by audio length; below 1 is faster than real time. Apple Silicon and recent x86 CPUs are faster.
Tips:
- Synthesize the voice before starting a long browser render: both use every core.
SHOWTIME_THREADS=Nlimits ONNX threads.
Platform notes and troubleshooting#
- espeak-ng. showtime uses a system espeak-ng when one is found (Homebrew or MacPorts on macOS,
apt install espeak-ngon Linux, the.msion Windows). Otherwise it uses the copy inside theespeakng-loaderwheel, which exists for macOS arm64/x64, Linux x64/arm64 and Windows x64/arm64. Each candidate is tested once in a child process, because a broken espeak kills its process.- A data path over ~140 bytes, or a non-ASCII path on Windows, is copied once to a short folder:
<home>/cache,%ProgramData%on Windows,$TMPDIR, then/tmp/showtime-espeak-<uid>(your own 0700 folder) on macOS and Linux. This avoids a known espeak-ng path-buffer bug.showtime setupruns the same self-test asshowtime doctor, so it fails when no candidate works. - To force a choice, set
SHOWTIME_ESPEAK=bundled|system, or give exact paths withSHOWTIME_ESPEAK_LIBandSHOWTIME_ESPEAK_DATA.
- A data path over ~140 bytes, or a non-ASCII path on Windows, is copied once to a short folder:
| Symptom | Fix |
|---|---|
| "Kokoro is not ready" | Run showtime setup (the minimal tier includes Kokoro). |
| A word is read wrong | Run voice ipa, then add a lexicon entry. |
| Spanish reads an English name oddly | Add a lexicon entry with an "es" or "*" value. |
| Timings are "estimated" on a few words | The word had no pronounceable letters (a symbol, an emoji) and was interpolated. Rewrite it as words. |
| Offline machine | Set SHOWTIME_OFFLINE=1. Alignment then skips ctc and uses tts; Piper voices must already be downloaded. |
| Cache | Stored in ~/.showtime/cache/voice/tts/ and capped at 1 GB (SHOWTIME_TTS_CACHE_MB); the least recently used lines go first. showtime voice cache shows its size, --clear empties it. SHOWTIME_NO_CACHE=1 or --no-cache bypasses it. |