Skip to content

Getting started

This page takes you from installing Kenkui to rendering a book with a full cast. Each step matches a runnable script in examples/.

1. Install

You need CPython 3.11, 3.12, or 3.13, and FFmpeg 5 or newer.

pip install kenkui

brew install ffmpeg                  # macOS
sudo apt-get install -y ffmpeg       # Debian / Ubuntu

Pocket-TTS brings in PyTorch, so expect a download of more than a gigabyte. See installation for details.

2. Look at your book

Before anything is downloaded or rendered, check that Kenkui reads your book the way you expect:

import kenkui as kk

book = kk.book("book.epub")
inspection = book.inspect()
print(inspection.metadata.title, inspection.metadata.author)
for chapter in inspection.chapters:
    print(chapter.id, chapter.title, chapter.speech_characters)

Chapter IDs such as ch-v1-2cc5ecca00df5ea4a766ea67 are stable for a given book. They are how you select chapters and address corrections later on. Image-only pages, such as covers, are skipped.

01_inspect.py also lists the voices you can choose from.

3. Render with one narrator

import kenkui as kk

if __name__ == "__main__":
    kk.load_voice("eponine")
    result = kk.magic_run("book.epub", narrator="eponine")
    print(result.output)

load_voice() downloads the English engine and the voice the first time it is called, and does nothing on later calls. Rendering itself never downloads.

Keep rendering under if __name__ == "__main__":

Rendering runs in worker processes, and each worker imports your script again. Without the guard, every worker would start the whole render over.

To control the details, build the pipeline yourself. magic_run() is shorthand for this:

import kenkui as kk

if __name__ == "__main__":
    kk.load_voice("eponine")
    result = (
        kk.book("book.epub")
        .pronounce(numbers="standard")  # read "1984" and "£5" aloud properly
        .pauses(chapter_ms=1500, paragraph_ms=300)
        .assign_voice("eponine")
        .tts()
        .metadata(title="My Book", author="An Author", cover="source")
        .write("book.m4b", workers="auto", overwrite=True)
    )

Every method returns a new pipeline and leaves the original unchanged. See 03_explicit_pipeline.py for chapter selection, progress events, cancellation, and error handling.

4. Give each character a voice

A cast takes three steps: find the characters, work out who speaks each line of dialogue, and assign voices.

pip install "kenkui[spacy]"
python -m spacy download en_core_web_lg
export OPENROUTER_API_KEY=...        # or any provider LiteLLM supports
import kenkui as kk

MODEL = "openrouter/deepseek/deepseek-v4-flash"

if __name__ == "__main__":
    for voice in ("eponine", "paul", "anna", "charles", "jane", "george"):
        kk.load_voice(voice)  # casting draws only on loaded voices

    resolved = (
        kk.book("book.epub")
        .infer_characters("spacy")  # local; a LiteLLM model also works
        .attribute_quotes(MODEL)  # the step that calls a model
        .assign_voices(narrator="eponine", unknown="paul")
        .resolve()  # optional: see the cast before rendering
    )
    print(dict(resolved.inspect().casting.assignments))
    resolved.tts().write("book.m4b")

Casting is deterministic: the same book with the same loaded voices always gets the same cast. Attribution is stored, so running this again, or casting the book differently, makes no new model calls. Use cast={"alice": "anna"} to pin a character to a voice.

To keep characters' voices across volumes, add .series("my-series", book=1). Before attribution, you can also review and correct the character list.

5. Fix what sounds wrong

script() shows what each line will be spoken as, and why. Corrections are rules addressed by position, and a preview renders only the lines they touch:

import kenkui as kk

if __name__ == "__main__":
    book = kk.book("book.epub").annotations().assign_voice("eponine")
    where = {"chapter": "*", "paragraph": 1}

    for row in book.script().at(where):
        print(row.path, row.character, row.provenance, row.text[:60])

    book = (
        book.attribute("irulan", where=where)
        .silence(900, where=where)
        .pronounce({"Atreides": "Ah-tray-deez"})
    )
    book.select(where).preview("probe.wav")
    book.write_annotations()  # saved as book.kenkui.json beside the EPUB

The next render that calls .annotations() picks these corrections up. Audio from the preview is cached and reused by the full render.

Where to next