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'
- 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:
objectThe 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) –
voicenames 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:
name (str)
samples (list[VoiceSample])
language (str | None)
- Return type:
- 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:
- 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_VOICESotherwise. 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”.
- async synthesize(text, *, language=None, voice=None, speed=1.0)[source]
Synthesise
textinto oneAudioResult.- 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.
Nonefalls 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:
- 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.
fris 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 sayslanguage="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
langthat 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:
- speaker_helper.backends.kokoro.detect_language(text, fallback='en')[source]
Guess what
textis written in, or returnfallback.Uses
langdetectwhen 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.
- speaker_helper.backends.kokoro.espeak_config()[source]
Where this machine’s working espeak-ng lives.
Prefers a system install over the
espeakng-loaderwheel. The wheel’s copy was built on a CI runner and looks for itsphontabin 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-dataand falls back to<path>itself, and with the wheel’s layout neither candidate holds a readablephontab. The failure is also not catchable — it is reported from C, followed byexit(1), so the calling process dies without a traceback.- Return type:
- speaker_helper.backends.kokoro.language_for(voice)[source]
The language a voice name implies — see
VOICE_PREFIX_LANG.
- speaker_helper.backends.kokoro.model_paths()[source]
The ONNX graph and voice bank, or
Nonewhen they are not fetched.
- 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") ''
- 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
waverather than soundfile: this is the hot path of every synthesis call, and it has no reason to depend on anything.
- 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_backbeing 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.