speaker_helper.backends.kokoro module

Kokoro: the default voice, running in this process.

Module summary

Kokoro is an 82M-parameter text-to-speech model run through ONNX Runtime, Apache-2.0 in code and weights, with 54 voices across nine languages. On a laptop CPU it synthesises at roughly a fifth of real time — fast enough that streaming a conversation is a matter of chunking, not of hardware.

It is the default here for a reason that is architectural rather than aesthetic: it needs no server. The engine this package shipped before spoke REST to a desktop application the user had to install, launch and keep running on a fixed port; the first thing anybody met was a connection error. pip install 'speaker-helper[kokoro]' and one weights fetch is the whole setup, and the time-to-first-audio no longer includes an HTTP round trip.

Two things pip cannot give you

espeak-ng. Kokoro phonemises through it, and the copy bundled in the espeakng-loader wheel does not find its own data on macOS — it was built on a CI runner and resolves to that machine’s home directory. The failure is not catchable: espeak reports it from C and calls exit(1), so there is no traceback, only a path belonging to somebody else’s computer and a dead process. espeak_config() therefore prefers a system install, which is built against its own prefix and initialises first try.

The weights. ~340 MB, fetched once. The package being importable says nothing about whether they are on disk, and discovering that at the first sentence of a long narration is the wrong moment — so check_install() asks both questions separately.

Usage example

>>> from speaker_helper.backends.kokoro import KokoroEngine, language_for
>>> # Voices are named for the language they speak: the first letter says which.
>>> language_for("ff_siwis")
'fr-fr'

Author

Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui

speaker_helper.backends.kokoro.BUILTIN_VOICES: tuple[str, ...] = ('af_alloy', 'af_aoede', 'af_bella', 'af_heart', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova', 'af_river', 'af_sarah', 'af_sky', 'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam', 'am_michael', 'am_onyx', 'am_puck', 'am_santa', 'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily', 'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis', 'ef_dora', 'em_alex', 'em_santa', 'ff_siwis', 'hf_alpha', 'hf_beta', 'hm_omega', 'hm_psi', 'if_sara', 'im_nicola', 'jf_alpha', 'jf_gongitsune', 'jf_nezumi', 'jf_tebukuro', 'jm_kumo', 'pf_dora', 'pm_alex', 'pm_santa', 'zf_xiaobei', 'zf_xiaoni', 'zf_xiaoxiao', 'zf_xiaoyi', 'zm_yunjian', 'zm_yunxi', 'zm_yunxia', 'zm_yunyang')

All 54, grouped by the language the first letter encodes (see VOICE_PREFIX_LANG); the second letter is the speaker’s gender.

Written out rather than read from the voice bank, because the question “which voices are there” is asked before anybody has 340 MB on disk — by someone deciding whether to download them at all. list_voices() prefers the loaded model when there is one and falls back to this, so the answer is never “load the weights and ask again”.

speaker_helper.backends.kokoro.CACHE_CANDIDATES: tuple[Path, ...] = (PosixPath('/Users/warithharchaoui/.cache/speaker-helper/kokoro'), PosixPath('/Users/warithharchaoui/.cache/sprezzature-skill/kokoro'))

Where the weights are looked for, in order. This package’s own cache first, then sprezzature’s — that suite downloaded the same two files before this engine lived here, and making somebody fetch 340 MB they already have is a poor welcome.

speaker_helper.backends.kokoro.CACHE_ROOT: Path = PosixPath('/Users/warithharchaoui/.cache/speaker-helper/kokoro')

Where a fresh download goes.

speaker_helper.backends.kokoro.DEFAULT_VOICE_BY_LANG: dict[str, str] = {'en': 'af_heart', 'es': 'ef_dora', 'fr': 'ff_siwis', 'hi': 'hf_alpha', 'it': 'if_sara', 'ja': 'jf_alpha', 'pt': 'pf_dora', 'zh': 'zf_xiaoxiao'}

Detected language → the voice that reads it. Within a language the pick is the model card’s highest-graded voice.

Nothing here hard-codes a single default language, and a narrator is the last place to start: an American voice reading French does not produce an accent, it produces nonsense, because the phonemiser is told the wrong language and spells the words out as if they were English.

speaker_helper.backends.kokoro.FALLBACK_VOICE: str = 'af_heart'

Used when the text is in a language Kokoro has no voice for. Reported as a fallback rather than passed off as a choice.

class speaker_helper.backends.kokoro.KokoroEngine(settings)[source]

Bases: object

The in-process Kokoro backend.

The model is loaded lazily and once: the first call pays for it, every later call does not. That matters more here than it looks — a conversation loop calls this per sentence, and reloading an ONNX graph between sentences would dominate the latency budget it is trying to protect.

Parameters:

settings (Settings) – voice names a built-in voice; empty means “read the text, detect the language, pick the voice that speaks it”.

async aclose()[source]

Release the model.

There is no handle to close — ONNX Runtime frees its session when the object is collected — so this drops the reference and lets that happen, which is what an async with Speaker(...) block expects.

Return type:

None

async clone_voice(name, samples, *, language=None)[source]

Refuse, and name the engines that do clone.

Kokoro reads with one of its built-in voices; it has no speaker encoder. Saying so is more useful than a generic NotImplementedError, because the caller’s next question is always “then which one”.

Parameters:
Return type:

str

async health()[source]

Whether this engine could synthesise right now, and why not.

Answers without loading the model: a health check that pays for an ONNX session is one nobody puts in a readiness probe.

Return type:

dict

async list_voices(engine=None)[source]

Every built-in voice, with the language its name implies.

Answered from the loaded model when there is one, and from BUILTIN_VOICES otherwise. The fallback is the point: somebody choosing a narrator is usually deciding whether to download 340 MB at all, and “fetch the weights, then ask again” is a poor answer to “which voices are there”.

Parameters:

engine (str | None)

Return type:

VoiceList

async synthesize(text, *, language=None, voice=None, speed=1.0)[source]

Synthesise text into one AudioResult.

Parameters:
  • text (str) – What to say.

  • language (str or None) – Overrides the language the voice implies. Normally None — a voice is a language here, and disagreeing with it produces an accent nobody asked for.

  • voice (str or None) – A built-in voice. None falls back to the configured one, and an unconfigured one means “detect the language of the text”.

  • speed (float) – Multiplier. Past about ±20 % the vocoder sounds wound-up or drugged rather than paced.

Return type:

AudioResult

speaker_helper.backends.kokoro.LANG_TO_ESPEAK: dict[str, str] = {'en': 'en-us', 'es': 'es', 'fr': 'fr-fr', 'hi': 'hi', 'it': 'it', 'ja': 'ja', 'pt': 'pt-br', 'zh': 'cmn'}

ISO-639-1 → the spelling espeak-ng wants, derived from the two tables above rather than written out a third time. fr is a perfectly good language code and not a language espeak knows: it answers language “fr” is not supported by the espeak backend and the synthesis fails. A caller who says language="fr" is right, and the translation is this module’s job.

speaker_helper.backends.kokoro.MODEL_FILENAME: str = 'kokoro-v1.0.onnx'

The ONNX graph and the voice bank, by the names their release uses.

speaker_helper.backends.kokoro.SAMPLE_RATE_HZ: int = 24000

Kokoro’s native sample rate (24 kHz mono). Fixed by the model, asserted at synthesis time: a different rate would silently misplace every cue after the first.

speaker_helper.backends.kokoro.VOICE_PREFIX_LANG: dict[str, str] = {'a': 'en-us', 'b': 'en-gb', 'e': 'es', 'f': 'fr-fr', 'h': 'hi', 'i': 'it', 'j': 'ja', 'p': 'pt-br', 'z': 'cmn'}

A voice is a language here — the name’s first letter says which, and a lang that disagrees with the voice produces an accent nobody asked for.

speaker_helper.backends.kokoro.check_install()[source]

Return 0 when this engine could actually synthesise, 1 otherwise.

Both halves are checked, because they fail at different times and only one of them is a pip install.

Return type:

int

speaker_helper.backends.kokoro.detect_language(text, fallback='en')[source]

Guess what text is written in, or return fallback.

Uses langdetect when it is installed and says nothing when it is not: guessing the language wrong is worse than not guessing, so a caller without the extra gets the configured voice rather than a coin flip.

Parameters:
Return type:

str

speaker_helper.backends.kokoro.espeak_config()[source]

Where this machine’s working espeak-ng lives.

Prefers a system install over the espeakng-loader wheel. The wheel’s copy was built on a CI runner and looks for its phontab in that machine’s home directory:

Error processing file '/Users/runner/work/espeakng-loader/.../phontab'

Handing it the data directory explicitly does not rescue it: espeak looks for <path>/espeak-ng-data and falls back to <path> itself, and with the wheel’s layout neither candidate holds a readable phontab. The failure is also not catchable — it is reported from C, followed by exit(1), so the calling process dies without a traceback.

Return type:

Any

speaker_helper.backends.kokoro.language_for(voice)[source]

The language a voice name implies — see VOICE_PREFIX_LANG.

Parameters:

voice (str)

Return type:

str

speaker_helper.backends.kokoro.model_paths()[source]

The ONNX graph and voice bank, or None when they are not fetched.

Return type:

tuple[Path, Path] | None

speaker_helper.backends.kokoro.normalise_language(code)[source]

Turn a language code into the spelling espeak-ng accepts.

Returns "" for anything unrecognised, so the caller can fall back to what the voice implies rather than pass a string the engine will refuse.

Examples

>>> normalise_language("fr")
'fr-fr'
>>> normalise_language("fr-fr")
'fr-fr'
>>> normalise_language("klingon")
''
Parameters:

code (str)

Return type:

str

speaker_helper.backends.kokoro.pcm_to_wav_bytes(samples, sample_rate=24000)[source]

Wrap int16 PCM in a RIFF header, mono.

Written with the standard library’s wave rather than soundfile: this is the hot path of every synthesis call, and it has no reason to depend on anything.

Parameters:
  • samples (Any)

  • sample_rate (int)

Return type:

bytes

speaker_helper.backends.kokoro.voice_for_language(code)[source]

The voice that reads code, and whether this is a fallback.

Returns (voice, fell_back). fell_back being true means the text is in a language this engine has no voice for, and is reported rather than hidden: the audio alone will not make it obvious to whoever generated it and does not speak the language either.

Parameters:

code (str)

Return type:

tuple[str, bool]