Use when working on kesha TTS internals — voice routing and which engine serves which voice-id prefix, Kokoro/Vosk ONNX I/O shapes, the CharsiuG2P vs FluidAudio G2P split, SSML handling, multilingual behaviour (es/fr/it/pt on ONNX, hi/ja/zh on darwin-arm64), or the KESHA_* TTS environment variables. Explains why darwin-arm64 routes differently from every other build.
Install
npx skillscat add drakulavich/kesha-voice-kit/tts-internals Install via the SkillsCat registry.
TTS internals
Engines
Text-to-speech via three engines selected by voice id prefix:
en-*→ Kokoro-82M. Separate model + per-voice style embedding. Output 24 kHz.ru-*→ Vosk-TTS (alphacep/vosk-tts). Multi-speaker model, 5 baked-in speakers. Output 22.05 kHz.macos-*→ AVSpeechSynthesizer Swift sidecar (#141). Zero model download, notification-grade quality, darwin-arm64 release feature setcoreml,tts,system_tts;kesha installplacessay-avspeech-darwin-arm64next to the engine and runtime lookup is sibling-first (rust/src/tts/avspeech.rs::helper_path).
Install Kokoro + Vosk-TTS explicitly with kesha install --tts (~990 MB). macos-* voices use installed macOS voices and need no model install.
Behavior, G2P, and SSML
- TTS models are never auto-downloaded —
kesha sayfails loudly with akesha install --ttshint when models are missing. kesha saywrites WAV mono f32 to stdout unless--outis given. Stderr is progress/errors only.- G2P split: English (
en/en-us/en-gb) uses embeddedmisaki-rs(Kokoro-trained inventory, no system deps, OOV letter-spell); Russian uses Vosk-TTS internals (BERT prosody + dictionary, no system deps);es/fr/it/ptuse CharsiuG2P on ONNX builds and FluidAudio's own G2P on darwin-arm64system_kokoro(see below); every other language bails out ofg2p::text_to_ipa_cachedwith a pointer to #212 — darwin-arm64hi/ja/zhnever reach that function at all (#492, below). espeak-ng (#210) was retired in #214, which also dropped CharsiuG2P (#123) once it had no callers left; CharsiuG2P came back for the Romance languages in #509, closing #212. - Auto-routing: omitted
--voicecalls TSNLLanguageRecognizerand picksen-am_michael,macos-com.apple.voice.compact.ru-RU.Milenaon darwin Russian, orru-vosk-m02elsewhere. Confidence < 0.5 or unmapped language falls to engine default. Routing table:src/voice-routing.ts::pickVoiceForLang. - SSML (
--ssml):ssml-parser; supports required<speak>root and<break time="...">; rejects<!DOCTYPE>; unknown tags (<emphasis>,<prosody>,<phoneme>,<say-as>) warn once and strip tags while synthesizing contained text.tts::ssml::parsereturnsVec<Segment>;tts::say()loads the engine once, concatenates text/silence f32 samples, then callswav::encode_wav. Scope/future tags: #122.
ONNX I/O shapes
- Kokoro ONNX (post-#207 official
kokoro-onnxv1.0): inputstokensint64[1,N],stylef32[1,256]rank-2,speedf32[1]; output"audio"; voice file 510x256. The earlier HF onnx-community variant usedinput_ids/waveformand brokeaf_heart. - Vosk-TTS ONNX (post-#214): one
Synth+Modelper call (Vosk::load:model.onnx,bert/model.onnx, dictionary, ~1-2s cold).Model::newtakesOption<&str>dir;Synth::synth_audioreturns i16 PCM at model sample rate (22050 Hz forvosk-model-tts-ru-0.9-multi);rust/src/tts/vosk.rsconverts to f32 / 32768.0. Speakers 0..4 map toru-vosk-{f01,f02,f03,m01,m02}invoices::resolve_vosk_ru; multi-call perf tracked in #213. - AVSpeech (#141,
system_tts, default darwin-arm64): engine spawnssay-avspeech; path resolution tries sibling-of-exe (~/.cache/kesha/bin/say-avspeech) then build-time$OUT_DIR/say-avspeech. stdin UTF-8, argv[1] voice id,--list-voicesemitsidentifier|language|name, Rust prefixesmacos-and merges intosay --list-voices. Output: complete mono f32 IEEE_FLOAT WAV @ 22050 Hz. Must pumpCFRunLoopRun()because callbacks dispatch on main queue;DispatchSemaphorehangs.--ratemapping TBD; SSML + AVSpeech rejected in v1.
Environment variables
KESHA_ENGINE_BIN— override the engine-binary path (useful when iterating onrust/target/release/kesha-engine).KESHA_CACHE_DIR— isolated test cache.KESHA_MODEL_MIRROR— redirect HF downloads to an internal mirror (#121), preserving/<owner>/<repo>/resolve/<ref>/<file>forwget --mirror; empty/unset = no-op. Rustmodels.rs::apply_mirrorand TSstatus.ts::activeModelMirrorboth trim trailing slashes.KESHA_KOKORO_COMPUTE_UNITS—default(FluidAudio's tuned per-stage mapping; the RNN-bearing Albert/PostAlbert/Alignment/Prosody/Vocoder on the ANE, the all-fp32 Noise and Tail iSTFT on the GPU) ·cpu-and-gpu·all-ane·cpu-only. Diagnostic only, and darwin-arm64system_kokoroonly: FluidAudio'sKokoroAne.mdrecommends the CPU baseline when deciding whether an artefact is the model or the accelerator. Unset isdefault; blank is treated as unset (a conditional GHAenv:exports an empty string); an unknown value fails before model init rather than falling back to the ANE the caller was avoiding. Nothing in CI sets it — a non-ANE preset does not rescue themacos-14image, which fails on the vocoder's shape contract regardless (#678).defaultis what makes M5 work out of the box (FluidAudio #667/#671, Noise-on-GPU #677): it is the only routing that keeps the prosody RNN off the GPU, where M5 aborts inGPURNNOps, while keeping the tail iSTFT offlibBNNS.cpu-and-gpudeliberately breaks that invariant and must not be set on M5 (#717).- macOS dev runtime:
DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib. Release binaries fix up viainstall_name_tool. - macOS build env:
LIBCLANG_PATH=/Library/Developer/CommandLineTools/usr/lib,RUSTFLAGS="-L /opt/homebrew/lib".
CharsiuG2P engine (es/fr/it/pt on ONNX builds)
Romance-language G2P uses the klebster 3-file KV-cache ONNX export of CharsiuG2P
(Zhu et al. 2022). Three ort sessions implement an autoregressive byte-level seq2seq decode:encoder_model.onnx (run once), decoder_model.onnx (step 0, seeds all 16 KV presents),decoder_with_past_model.onnx (steps 1..N, 8 rolling decoder KV + 8 constant encoder KV).
License: CC-BY 4.0 (attribution in NOTICES).
Tokenizer: ByT5 byte-level — input format "<tag>: word" where tag is one of<spa> (es), <fra> (fr), <ita> (it), <por-bz> (pt). Each byte maps tobyte_value + 3 (special-token offset), followed by EOS id 1.
OOV remap: Charsiu can emit IPA symbols outside Kokoro's phoneme vocabulary
(tie-bar affricates t͡s/t͡ʃ/d͡ʒ, Latin g U+0067, pre-composed nasals õ/ũ/ẽ).tts::charsiu::remap normalizes these to Kokoro-vocab equivalents (ʦ/ʧ/ʤ,
script-g U+0261, NFD base+combining-tilde); locked by a zero-residual-OOV regression test.
Normalize pass: numbers and acronyms are expanded before G2P (512 → quinientos doce
in es, etc.) via tts::normalize::{numbers,acronyms}. CharsiuG2P collapses raw digits;
the normalizer runs first so digit sentences produce longer, correctly-paced audio.
IO contract (PR #185, verified against the pinned export). encoder_model: ininput_ids int64 [B,S] + attention_mask int64 [B,S], out last_hidden_state
f32 [B,S,256]. decoder_model (step 0): in input_ids + encoder_attention_mask
encoder_hidden_states, outlogitsf32[B,S,384]+ 16present.{0..3}.{decoder,encoder}.{key,value}
f32[B,6,S,64].decoder_with_past_model(steps 1..N): ininput_ids[B,1]+encoder_attention_mask+ the 16past_key_values.*, outlogits[B,1,384]+ only
the 8 decoder presents — the encoder K/V are seeded once at step 0 and re-fed
verbatim every step. Decode is greedy, stops on EOS, capped at 128 steps.
Model is ByT5-tiny:vocab_size384,d_model256, 12 encoder / 4 decoder layers,
6 heads. Measured ~36 ms/word single-thread on M2, byte-identical to the Python
reference for es/fr/it/pt.
Multilingual G2P (#511)
--lang es-ES selects Castilian Spanish via charsiu::is_castilian_region / base_lang
resolution. Because the upstream CharsiuG2P klebster export contains no Castilian θ tag
(confirmed in the #511 Phase-0 spike), the CASTILIAN decision constant is set toDegrade: the synthesizer falls back to Latin-American phonology (<spa> tag) and emits
a one-time stderr note. es / es-419 / es-MX continue to use Latin-American directly
with no warning. Per-language acronym stop-lists (ES/FR/IT/PT_STOP_LIST inrust/src/tts/normalize/acronyms.rs) are curated seeds that prevent word-acronyms
(OTAN, OVNI, FIFA…) from being letter-spelled; they are not exhaustive.
FluidAudio KokoroAne variants — macOS Chinese (#492)
On system_kokoro (darwin/ANE), tts::fluid_kokoro::with_kokoro resolves the voice's
language (lang_for_fluid_id) and passes it to init_kokoro(voice, lang). The fork's
Swift bridge (fluidaudio-rs, FluidAudio 0.14.8) maps it to a KokoroAneVariant:zh → .mandarin (tone-aware G2P: jieba + g2pw + bopomofo + tone sandhi), everything
else → .english (en plus Latin-script es/fr/it/pt, which the English G2P handles
acceptably). The .mandarin variant fetches its own ANE-zh/ bundle (nestedvoices/<id>.bin) on first synth — zh voices are therefore not staged inmodels.rs::ANE_KOKORO_VOICES and are exempt from the staging-coverage test (likeaf_heart). Default zh voice: zh-zm_050 (male). Native-script hi/ja still fail fast
(E_SCRIPT_UNSUPPORTED) — no FluidAudio KokoroAne variant for them yet. The-Wl,-rpath,/usr/lib/swift link arg in build.rs is emitted undercoreml/system_kokoro/system_diarize so the Swift runtime loads withoutMACOSX_DEPLOYMENT_TARGET=14.0 locally.
History
Original spec assumed Silero TTS; pivoted to Piper during the M3 spike (Silero ships PyTorch-only, no public ONNX), and Piper was later dropped for the current Kokoro/Vosk/AVSpeech split. The lesson that stuck is CLAUDE.md's "verify third-party model formats with a spike".