speaker_helper.lexicon module
Pronunciation lexicon: per-word overrides applied before synthesis.
Module summary
Some words a document contains are not words the engine’s own grapheme-to-phoneme stage gets right: proper nouns (“Grasse” the town, not “grasse” the adjective), acronyms (“ONU”), anglicisms in French text, product names. This module lets whoever owns the text state, once, how such a word is pronounced — and applies that statement automatically on every synthesis.
The statement can take two forms, because the engines take two forms:
IPA (
ipa:) — exact phonemes. Deterministic, but only an engine with a phoneme input can honour it. Thekokorobackend does: it already phonemises text itself (espeak-ng → IPA) and hands the model phonemes, so a lexicon entry is spliced into that stream verbatim (seephonemise_with_lexicon()).Respelling (
respelling:) — an orthographic rewrite (“GitHub” → “guite-heube”) that an engine without phoneme access reads correctly. Fragile by nature but universal: it works by rewriting the text itself, so Chatterbox, Kyutai, and any future end-to-end model benefit.
When an entry states neither, the respelling is derived once by a local
LLM — resolved through best-engine-ai-helper (the suite rule: no
hardcoded model names) and served by Ollama at temperature 0 — then persisted
to a sidecar cache next to the lexicon file. After that first derivation the
whole feature is deterministic and fully offline.
Who applies which tier is decided by the backend’s LEXICON_TIER class
attribute: "ipa" means “the backend splices phonemes itself, leave the
text alone”; anything else (the default) means
Speaker rewrites the text with
Lexicon.respell() before calling the engine.
Usage example
>>> lex = Lexicon.from_mapping({"entries": [
... {"word": "GitHub", "language": "*", "respelling": "guite-heube"},
... ]})
>>> lex.respell("Va sur GitHub ce soir.", "fr")
'Va sur guite-heube ce soir.'
- speaker_helper.lexicon.ANY_LANGUAGE = '*'
an entry carrying it applies to every language. Useful for brand names that are mangled the same way everywhere.
- Type:
The wildcard language
- speaker_helper.lexicon.CACHE_SUFFIX = '.llm-cache.yaml'
Sidecar suffix for LLM-derived respellings. A separate file (not a rewrite of the user’s lexicon) because the lexicon is hand-edited and commented — regenerating it through a YAML dumper would destroy those comments.
- class speaker_helper.lexicon.Lexicon(entries, *, path=None, llm_enabled=True)[source]
Bases:
objectA set of pronunciation overrides, applied by word-boundary matching.
- Parameters:
entries (list of LexiconEntry) – The overrides, in file order (later entries win on duplicate words).
path (Path or None) – Where the lexicon was loaded from — used to place the LLM sidecar cache.
Nonekeeps everything in memory (tests, ad-hoc use).llm_enabled (bool) – Allow deriving missing respellings with the local LLM. Off means a word without a respelling is simply left as written.
- ensure_respelling(entry, language)[source]
Return the entry’s respelling, deriving it with the LLM if needed.
- Parameters:
entry (LexiconEntry) – The entry to complete. Mutated in place when a derivation succeeds (and persisted to the sidecar cache).
language (str) – Language of the text being synthesised — used when the entry is a wildcard, because a respelling is written in a language’s orthography even when the word applies to all of them.
- Returns:
The respelling, or
""when none exists and none could be derived.- Return type:
- entry_for(word, language)[source]
Return the entry matching
wordinlanguage, if any.- Parameters:
- Returns:
The exact-language entry when one exists, else the
"*"entry, elseNone.- Return type:
LexiconEntry or None
- classmethod from_mapping(mapping, *, path=None)[source]
Build a lexicon from a plain mapping (the YAML document shape).
- classmethod load(path)[source]
Load a lexicon from a YAML file.
- Parameters:
path (str or Path) – The lexicon file (see
lexicon.yaml.exampleat the repo root).- Returns:
The populated lexicon.
- Return type:
- Raises:
FileNotFoundError – When
pathdoes not exist — a configured-but-missing lexicon is a mistake worth hearing about, unlike an unconfigured one.
- respell(text, language)[source]
Rewrite
textso an engine without phoneme input says it right.Every lexicon word found in
textis replaced by its respelling — the hand-written one when the entry has it, otherwise one derived once by the local LLM and cached. A word whose respelling cannot be obtained (no LLM available) is left exactly as written.
- split(text, language)[source]
Cut
textinto plain chunks and lexicon matches, in order.This is the primitive the IPA tier builds on: the phonemiser runs on the plain chunks, and each matched entry contributes its phonemes.
- Parameters:
- Returns:
Pairs in reading order.
(chunk, None)is ordinary text;(matched_word, entry)is a lexicon hit, with the word as it appeared in the text.- Return type:
list of (str, LexiconEntry or None)
Examples
>>> lex = Lexicon.from_mapping( ... {"entries": [{"word": "ONU", "language": "fr", "ipa": "oɛny"}]}) >>> lex.split("Vive l'ONU !", "fr") [("Vive l'", None), ('ONU', LexiconEntry(word='ONU', ...)), (' !', None)]
- class speaker_helper.lexicon.LexiconEntry(word, language='*', ipa='', respelling='', source='user')[source]
Bases:
objectOne pronunciation override: a word and how to say it.
- Parameters:
word (str) – The written form to match (matched case-insensitively, on word boundaries).
language (str) – ISO-639-1 code the entry applies to, or
"*"for every language.ipa (str) – IPA phonemes for the word (no surrounding slashes). Honoured verbatim by phoneme-input backends; used as a hint when deriving a respelling.
respelling (str) – Orthographic rewrite for engines without phoneme access. Empty means “derive one with the local LLM on first use”.
source (str) –
"user"for hand-written entries,"llm"for a derived respelling — kept so a cached derivation is distinguishable from a human decision.
- speaker_helper.lexicon.builtin_entries()[source]
The packaged per-language entries, language stamped from the filename.
- Returns:
Every entry of every
lexicons/<code>.yaml, withlanguageset to<code>(the files do not repeat it) andsourceset to"builtin"so a merged lexicon stays auditable.- Return type:
list of LexiconEntry
- speaker_helper.lexicon.load_lexicon(settings)[source]
Resolve and load the lexicon the settings point at, if any.
Resolution order for the file path:
settings.lexicon["path"], then theSPEAKER_HELPER_LEXICONenvironment variable (already folded into settings bySettings), then alexicon.yamlsitting in the current directory — mirroring howsettings.yamlitself is found.
- speaker_helper.lexicon.phonemise_with_lexicon(text, language, lexicon, phonemise)[source]
Phonemise
text, splicing lexicon IPA in place of matched words.The deterministic tier: the engine’s own phonemiser runs on everything the lexicon does not claim, and each claimed word contributes, in order of preference, its
ipaverbatim, the phonemisation of itsrespelling, or the phonemisation of the word itself (a no-op override).- Parameters:
text (str) – The text to phonemise.
language (str) – Language of the text (decides which lexicon entries apply).
lexicon (Lexicon or None) – The active lexicon;
Nonedegrades to a plainphonemise(text).phonemise (callable) –
str -> str, the backend’s own phonemiser for this language — passed in so this module depends on no backend.
- Returns:
One phoneme string for the whole text.
- Return type:
Examples
>>> lex = Lexicon.from_mapping( ... {"entries": [{"word": "Grasse", "language": "fr", "ipa": "ɡʁas"}]}) >>> phonemise_with_lexicon("Grasse", "fr", lex, lambda s: f"<{s}>") 'ɡʁas'