From 4a5604364976575ee7b5f5c3b1a23bb1b526aa34 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Tue, 15 Sep 2026 14:35:26 -0600 Subject: [PATCH] kokoro_tts: skip phonemes the vocab has no id for, as KModel does eSpeak-ng glottalises /t/ before a syllabic nasal, so the built-in G2P produces a U+0329 syllabic mark for ordinary words -- "button", "kitten", "written", "forgotten" -- and encode_input_ids_and_count then threw on the very symbol it had just produced, losing the whole request. The reference implementation does not: hexgrad/Kokoro's KModel tokenizes with filter(None, map(vocab.get, phonemes)), dropping any phoneme the 114-entry vocab has no id for. Dropping the mark gives b'V?n for "button", a correct reading; throwing gives no audio at all. Malformed UTF-8 above still throws. An unknown but well-formed phoneme does not. --- src/models/kokoro_tts/frontend.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/models/kokoro_tts/frontend.cpp b/src/models/kokoro_tts/frontend.cpp index 98f183e6..6bdfa82a 100644 --- a/src/models/kokoro_tts/frontend.cpp +++ b/src/models/kokoro_tts/frontend.cpp @@ -2,6 +2,8 @@ #include "engine/models/kokoro_tts/g2p_multilingual.h" +#include "engine/framework/debug/trace.h" + #include #include #include @@ -144,9 +146,23 @@ EncodedInputIds encode_input_ids_and_count( throw std::runtime_error("invalid UTF-8 continuation byte in Kokoro phoneme string"); } } - const auto it = assets.vocab.find(phonemes.substr(i, width)); + const std::string symbol = phonemes.substr(i, width); + const auto it = assets.vocab.find(symbol); if (it == assets.vocab.end()) { - throw std::runtime_error("Kokoro vocab is missing phoneme symbol: " + phonemes.substr(i, width)); + // Skipped, not fatal, matching the reference implementation: hexgrad/Kokoro's KModel + // tokenizes with `filter(None, map(vocab.get, phonemes))`, which drops any phoneme the + // 114-entry vocab has no id for. + // + // This matters because our OWN G2P produces such symbols for ordinary words: eSpeak-ng + // glottalises /t/ before a syllabic nasal, so "button" is `b'V?n` with a U+0329 + // syllabic mark the vocab does not carry. Throwing there loses the whole request; + // dropping the mark gives a correct reading of the word. + // + // Malformed UTF-8 above still throws — that is a real error. An unknown but + // well-formed phoneme is not. + engine::debug::trace_log_scalar("kokoro.skipped_phoneme", std::string_view(symbol)); + i += width; + continue; } encoded.ids.push_back(it->second); ++encoded.phoneme_count;