narrate
narrate is the narration half of spitfire-storybook — a privacy-first,
local-only, open-source tool. It doesn't just help write your book, it reads it
to you. Every byte of synthesis happens on your own hardware via a bundled local
Kokoro server. There is no hosted version and nothing is ever uploaded.
The script lives at cli/narrate.py (relative to the repo root). Run it with
your system Python:
python3 cli/narrate.py <mode> [args] [flags]
Prerequisites
- A local Kokoro server. The CLI talks to it over HTTP. Start one from
cli/kokoro/ (./start.sh, or python3 cli/kokoro/server.py). First run
downloads the ~330 MB Kokoro-82M model and caches it locally. See
cli/kokoro/README.md.
- python3 + ffmpeg on your PATH. That's it — synthesis is dependency-light;
all audio decode/concat/encode goes through ffmpeg (no numpy/soundfile).
PIL
is optional and only used to auto-generate a book cover.
- The CLI auto-detects the server at
http://localhost:8081 (and
http://127.0.0.1:8081). Override the endpoint with the KOKORO_URL
environment variable or the --server flag. KOKORO_VOICE overrides the
default voice. If no server is reachable, the CLI exits with a message telling
you to start one.
Three modes
1. say — speak a string
python3 cli/narrate.py say "Welcome to the first chapter." -o intro.mp3 --play
Speaks the given text. Defaults to narration.mp3 when -o is omitted.
2. file — narrate a .txt/.md file
python3 cli/narrate.py file notes.md -o notes.mp3
Reads the file, applies Markdown cleaning, and synthesizes it. The default
output name is the input filename with an .mp3 extension.
3. book — build a chaptered .m4b audiobook
python3 cli/narrate.py book ./my-book \
--title "The Fictional Field Guide" \
--subtitle "An Imaginary Manual" \
--author "A. N. Author" \
--year 2026
The input is either:
- a directory of
.md/.txt files (sorted recursively). Each file becomes a
chapter; its title is the first # H1 heading, or the filename if there's none.
- a manifest
manifest.json: a list of [{title, text|src|text_inline, ...}]
entries, giving explicit chapter order, titles, and content.
It synthesizes each chapter, then muxes them into a single .m4b with embedded
chapter markers, metadata, and (by default) an auto-generated cover. Output
defaults to a slugified title with the .m4b extension.
Book mode is resumable. Chapter WAVs are cached in a .narrate_build/
directory next to the output; if a chapter file already exists, it's skipped on
the next run. Delete that directory (or a single chapter WAV) to force a
re-synthesis.
Flags
Common to all modes:
| Flag |
Meaning |
-o, --out PATH |
Output file (.mp3 / .wav / .m4a / .m4b). Sensible default per mode. |
--voice NAME |
Kokoro voice (default af_heart = "Heart"). |
--gap SECONDS |
Pause inserted between chunks (default 0.3). |
--play |
Play the result when done (macOS afplay). |
--server URL |
Override the Kokoro endpoint (else auto-detect / KOKORO_URL). |
file and book also accept:
| Flag |
Meaning |
--clean md|plain|none |
Markdown cleaning mode (default md). |
Book-only:
| Flag |
Meaning |
--title |
Book / album title. |
--subtitle |
Subtitle (stored as comment). |
--author |
Author. |
--narrator |
Narrator credit (default Heart · Kokoro TTS). |
--year |
Publication year. |
--cover auto|none|PATH |
auto generates a cover (needs PIL); default auto. |
--bitrate |
AAC bitrate for the .m4b (default 96k). |
Markdown → spoken cleaning
With --clean md (the default), the text is converted from Markdown into
something that reads naturally aloud before synthesis:
- strips YAML frontmatter, code fences, inline code, HTML, images, and bare URLs
- turns links into their visible text
- linearizes tables into spoken rows and flattens list markers / blockquotes into
sentences
- converts headings into spoken lines with a terminal period
- removes emphasis markers (
**, *, _)
- expands symbols Kokoro otherwise mispronounces:
% → "percent",
$1,500 → "1500 dollars", $5k → "5 thousand dollars", 3x → "3 times",
& → "and", →/-> → "leads to", 24/7 → "twenty-four seven", plus common
abbreviations (e.g., i.e., etc., vs., approx., w/).
Use --clean plain to just collapse whitespace (keeps the raw text), or
--clean none to feed the text through untouched. In book mode, .txt chapters
are always treated as plain regardless of --clean.
Long text is automatically split into ~1500-character chunks at paragraph and
sentence boundaries, synthesized chunk-by-chunk, and stitched together with the
configured gap.
Importing the .m4b into Apple Books
- Open the Books app on macOS.
- Drag the generated
.m4b onto the Books window, or use File → Add to
Library… and select it.
- It appears under Audiobooks with chapter navigation, the embedded cover,
and resume-where-you-left-off playback. It syncs to your iPhone/iPad via the
same Apple ID.
(.m4b is a standard AAC audiobook container, so the same file also imports into
Podcasts and most third-party audiobook players.)
1---2name: narrate3description: Turns any text, Markdown, or book into spoken audio entirely on your own machine using the local Kokoro text-to-speech model (voice "Heart" / af_heart) — no cloud, no API keys, nothing uploaded, free. Three modes: speak a short string, narrate a .txt/.md file to an mp3, or compile a folder of chapters into a chaptered .m4b audiobook ready for Apple Books. Trigger this skill whenever someone says "narrate this", "read this aloud", "make an audiobook", "turn this into an mp3", "turn this into audio", "make a voiceover", "say this with Heart", "read me this file", or otherwise wants text spoken locally. Handles Markdown-to-spoken cleanup (strips syntax, linearizes tables and lists, expands symbols Kokoro mispronounces) and chunked, resumable synthesis.4---56# narrate78`narrate` is the narration half of **spitfire-storybook** — a privacy-first,9local-only, open-source tool. It doesn't just help write your book, it reads it10to you. Every byte of synthesis happens on your own hardware via a bundled local11Kokoro server. There is no hosted version and nothing is ever uploaded.1213The script lives at `cli/narrate.py` (relative to the repo root). Run it with14your system Python:1516```bash17python3 cli/narrate.py <mode> [args] [flags]18```1920## Prerequisites2122- **A local Kokoro server.** The CLI talks to it over HTTP. Start one from23 `cli/kokoro/` (`./start.sh`, or `python3 cli/kokoro/server.py`). First run24 downloads the ~330 MB Kokoro-82M model and caches it locally. See25 `cli/kokoro/README.md`.26- **python3 + ffmpeg** on your PATH. That's it — synthesis is dependency-light;27 all audio decode/concat/encode goes through ffmpeg (no numpy/soundfile). `PIL`28 is optional and only used to auto-generate a book cover.29- The CLI **auto-detects** the server at `http://localhost:8081` (and30 `http://127.0.0.1:8081`). Override the endpoint with the `KOKORO_URL`31 environment variable or the `--server` flag. `KOKORO_VOICE` overrides the32 default voice. If no server is reachable, the CLI exits with a message telling33 you to start one.3435## Three modes3637### 1. `say` — speak a string3839```bash40python3 cli/narrate.py say "Welcome to the first chapter." -o intro.mp3 --play41```4243Speaks the given text. Defaults to `narration.mp3` when `-o` is omitted.4445### 2. `file` — narrate a .txt/.md file4647```bash48python3 cli/narrate.py file notes.md -o notes.mp349```5051Reads the file, applies Markdown cleaning, and synthesizes it. The default52output name is the input filename with an `.mp3` extension.5354### 3. `book` — build a chaptered .m4b audiobook5556```bash57python3 cli/narrate.py book ./my-book \58 --title "The Fictional Field Guide" \59 --subtitle "An Imaginary Manual" \60 --author "A. N. Author" \61 --year 202662```6364The input is either:6566- **a directory** of `.md`/`.txt` files (sorted recursively). Each file becomes a67 chapter; its title is the first `# H1` heading, or the filename if there's none.68- **a manifest** `manifest.json`: a list of `[{title, text|src|text_inline, ...}]`69 entries, giving explicit chapter order, titles, and content.7071It synthesizes each chapter, then muxes them into a single `.m4b` with embedded72chapter markers, metadata, and (by default) an auto-generated cover. Output73defaults to a slugified title with the `.m4b` extension.7475**Book mode is resumable.** Chapter WAVs are cached in a `.narrate_build/`76directory next to the output; if a chapter file already exists, it's skipped on77the next run. Delete that directory (or a single chapter WAV) to force a78re-synthesis.7980## Flags8182Common to all modes:8384| Flag | Meaning |85| --- | --- |86| `-o`, `--out PATH` | Output file (`.mp3` / `.wav` / `.m4a` / `.m4b`). Sensible default per mode. |87| `--voice NAME` | Kokoro voice (default `af_heart` = "Heart"). |88| `--gap SECONDS` | Pause inserted between chunks (default `0.3`). |89| `--play` | Play the result when done (macOS `afplay`). |90| `--server URL` | Override the Kokoro endpoint (else auto-detect / `KOKORO_URL`). |9192`file` and `book` also accept:9394| Flag | Meaning |95| --- | --- |96| `--clean md\|plain\|none` | Markdown cleaning mode (default `md`). |9798Book-only:99100| Flag | Meaning |101| --- | --- |102| `--title` | Book / album title. |103| `--subtitle` | Subtitle (stored as comment). |104| `--author` | Author. |105| `--narrator` | Narrator credit (default `Heart · Kokoro TTS`). |106| `--year` | Publication year. |107| `--cover auto\|none\|PATH` | `auto` generates a cover (needs PIL); default `auto`. |108| `--bitrate` | AAC bitrate for the `.m4b` (default `96k`). |109110## Markdown → spoken cleaning111112With `--clean md` (the default), the text is converted from Markdown into113something that reads naturally aloud before synthesis:114115- strips YAML frontmatter, code fences, inline code, HTML, images, and bare URLs116- turns links into their visible text117- linearizes tables into spoken rows and flattens list markers / blockquotes into118 sentences119- converts headings into spoken lines with a terminal period120- removes emphasis markers (`**`, `*`, `_`)121- **expands symbols Kokoro otherwise mispronounces**: `%` → "percent",122 `$1,500` → "1500 dollars", `$5k` → "5 thousand dollars", `3x` → "3 times",123 `&` → "and", `→`/`->` → "leads to", `24/7` → "twenty-four seven", plus common124 abbreviations (`e.g.`, `i.e.`, `etc.`, `vs.`, `approx.`, `w/`).125126Use `--clean plain` to just collapse whitespace (keeps the raw text), or127`--clean none` to feed the text through untouched. In `book` mode, `.txt` chapters128are always treated as `plain` regardless of `--clean`.129130Long text is automatically split into ~1500-character chunks at paragraph and131sentence boundaries, synthesized chunk-by-chunk, and stitched together with the132configured gap.133134## Importing the .m4b into Apple Books1351361. Open the **Books** app on macOS.1372. Drag the generated `.m4b` onto the Books window, or use **File → Add to138 Library…** and select it.1393. It appears under **Audiobooks** with chapter navigation, the embedded cover,140 and resume-where-you-left-off playback. It syncs to your iPhone/iPad via the141 same Apple ID.142143(`.m4b` is a standard AAC audiobook container, so the same file also imports into144Podcasts and most third-party audiobook players.)