Voice-over
A video can carry a voice-over: lines of narration written in the same
Markdown file, spoken by Voxwerk
(Qwen3-TTS in pure C#) in a voice you describe or pick from a catalog, and
mixed into the MP4 by neko render.
The narration is a track of its own. The scenes keep their animation on their own clocks, exactly as they would without a voice; the voice-over is laid on the video's clock at anchors you choose, and its lines never overlap. Before anything is synthesized, Neko estimates every sentence, so you can line the two up in the preview and only then spend time on the voice.
---
title: Launch
video:
narration:
voice: "A warm, clear female narrator in her thirties, calm and friendly."
---
--- {id="opening" layout="title"}
# Say it *in Markdown*
--- {id="track"}
## The narration is *its own track*
- A time
- A scene
- An element {#element}
--- {narration}
{at="opening+0.6"}
Every Neko video can carry a voice-over, written in the same Markdown file.
{at="track"}
The narration is a track of its own. It never moves the animation.
{at="#element"}
Each line starts at an anchor: a time, a scene, or an element on screen.
The workflow
- Write the scenes as usual, and the narration in one or more
--- {narration}parts. - Preview the timings:
neko voiceover launch.md --previewprints every scene, animation and sentence on one clock, and the player's Timing panel draws them as lanes. Sentences not yet spoken are estimated. - Align: move anchors, change a scene's
duration, split or shorten a line, until no line is pushed and nothing runs into the wrong scene. - Synthesize:
neko voiceover launch.mdspeaks every sentence without a take and prints the sheet again, now with measured times. - Render:
neko render launch.mdmixes the takes into the MP4 (it synthesizes anything still missing first).
Choosing a voice
The narration: key of the video: front matter sets the voice. A bare string
is the voice; a mapping takes the options below.
video:
narration: "An elderly man with a slow, raspy, gentle voice, like a grandfather telling a story."
video:
narration:
voice: host # a name from `voices:`, a catalog id, or a description
language: english
seed: 7
voices:
host: "A bright, energetic young woman, upbeat and clear."
guest: en-gb-rupert-vale # a voice from the Voxwerk catalog
A description is the most direct: Voxwerk's VoiceDesign model creates the
voice from it — timbre, gender, age, accent, emotion, pace. A catalog id
(<language>-<country>-<name>) uses a ready-made voice from the
Voxwerk.Voices.English catalog: its design prompt and language. List and
search the catalog with neko voices:
neko voices --search "calm narration" --gender female --country gb
neko voices --style Narration --age old --json
| Option | Default | Meaning |
|---|---|---|
voice |
a calm, friendly narrator | A name from voices, a catalog id, or a description. |
voices |
— | Named voices a line picks with {voice="name"}. |
language |
the voice's, else auto |
english, german, french, spanish, italian, portuguese, russian, japanese, korean, chinese. |
seed |
7 |
Sampling seed. The same voice, language, seed and text always give the same take. |
start |
0.5 |
When the first line starts, when it has no anchor. |
gap |
0.5 |
Seconds between two lines that follow each other. |
pause |
0.25 |
Seconds between the sentences of a line. |
volume |
1 |
Gain of the voice-over, 0 to 2. |
wpm |
150 |
Words per minute assumed for a sentence that has not been synthesized. |
model |
Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign |
The TTS checkpoint: a Hugging Face repo id or a local folder. |
folder |
assets/voiceover/<page> |
Where the takes are kept, relative to the page. |
Writing the narration
A part whose rule carries the narration flag — --- {narration} (or
{voiceover}) — is the voice-over track, not a scene. It can sit anywhere in the
file: at the end, after each scene, or both. Its parts are read in file order.
- Every paragraph or list item is a line. A line is split into sentences: the unit that is synthesized, timed and shown as a subtitle.
- A
{…}block at the end of a line, or on its own line just before it, sets the line's anchor and voice. - Headings label the script and are not spoken; HTML comments are ignored.
- Emphasis, links and code marks are taken out of what is said.
| Attribute | Meaning |
|---|---|
at |
The line's anchor (below). Without one, the line follows the line before. |
voice |
A name from voices, a catalog id, or a description. |
language / seed |
Per-line versions of the options. |
pause |
Seconds between this line's sentences. |
gap |
Seconds after the line before, when this line follows it. |
volume |
This line's gain. |
The part's own rule takes the same attributes as defaults for its lines:
--- {narration voice="guest" at="scene-4"} speaks every line of the part in the
guest's voice and starts the first one at scene 4.
Anchors
at |
Starts the line… |
|---|---|
12.5, 1:04.2 |
At that time on the video's clock. |
+0.8 |
0.8 s after the line before has finished. |
scene-3, opening |
When scene 3 (or the scene with that id) has settled: its transition in is over. |
#chart, #element |
When the element with that id enters ({#chart} on any block, or a heading's own id). |
scene-3+1.2, #chart-0.3 |
An anchor plus or minus seconds. |
No overlaps
Lines are placed in order. A line whose anchor falls while the line before is still speaking waits for it — it starts 0.15 s after — and Neko warns: the timing sheet marks it shifted, and the panel outlines it. Fix it by moving the anchor, lengthening the scene before, or shortening the line.
The animation never moves for the voice. The one exception is the end: when the voice-over runs past the last scene, the last scene holds until it is done.
Previewing the timing
neko voiceover videos/launch.md --preview # the sheet, no model needed
neko voiceover videos/launch.md -p --json timing.json
neko render videos/launch.md --preview # the same sheet
videos/launch.md: 0:20.46, 3 scenes, 4 voice-over lines in 5 sentences
time end sc animation voice-over
──────────────────────────────────────────────────────────────────────────────────
0:00.00 0:03.12 01 ▶ intro · title (start)
0:00.30 0:01.15 01 Write Markdown. Get video. words #write-…
0:00.50 0:03.79 01 ● Every video starts as one Markdown file. [line 1.1 · runs into scene 2]
0:02.52 0:05.83 02 ▶ scene-2 · default (push 0.6s)
0:03.49 0:04.09 02 Paragraphs and quotes rise rise #bullets
0:06.10 0:11.78 03 ● Nothing here needs a time… [line 2 @ scene-2 ⚠ shifted]
● is a synthesized take, ~ an estimate. runs into scene N flags a sentence
that is still being spoken when the next scene arrives.
In the browser preview, Timing in the control bar (or T, or
#timing in the URL) opens the timing panel: the scenes, every animation and
every sentence as lanes on the video's clock, with the playhead. Click any item
to jump to it. The sentence being spoken shows as a subtitle under the canvas
(C hides it); estimated sentences show in italics, and synthesized ones
play their takes. Neither the panel nor the subtitle is ever filmed.
Synthesizing
neko voiceover videos/launch.md # speak every sentence that has no take
neko voiceover videos/ --threads 4 # every video page in the folder
neko voiceover videos/launch.md --force # speak them all again
The first run downloads the VoiceDesign checkpoint (about 4.5 GB) to
~/.cache/voxwerk. Synthesis runs on the CPU, about five times slower than
real time on four cores, so a minute of narration takes a few minutes. Each
take is trimmed of the silence around it and kept as Ogg Opus in
assets/voiceover/<page>/, named by a hash of the model, voice, language, seed
and text: editing one sentence re-speaks only that sentence, and takes no
sentence uses any more are removed (--keep-unused keeps them).
Commit the takes with the page. The build copies them with the site's assets, so the preview plays them, and a render in CI needs no model at all.
| Option | Meaning |
|---|---|
-p, --preview |
Only compute and print the timings. |
--json <file> |
Also write the sheet as JSON: scenes, animation, lines and sentences. |
-f, --force |
Synthesize every sentence again. |
--keep-unused |
Keep takes no sentence uses any more. |
--threads <n> |
Worker threads for synthesis (0 = all cores). |
--int8 |
int8 talker weights: faster, approximate. |
Rendering
neko render synthesizes any sentence still without a take, then mixes every
take into one voice-over track at its sentence's time, under the music bed and
any scene voice files. --no-voiceover skips the synthesis (sentences without a
take stay silent), and --no-audio leaves the whole soundtrack out.