Repository navigation
Voice G2P hardening: misaki number bounds, shared eSpeak owner, CI labels - #71
Merged
Merged
Conversation
cardinal() indexed kScales[g] for every three-digit group, so a numeral with more than 42 significant digits read past the 14-entry table. Only two branches of get_number checked number_fits(); the ordinal, multi-dot, currency and float branches did not. cardinal() now reads such a number digit by digit, and every branch rejects it the way the guarded ones do. Regression cases cover the float, currency, ordinal and multi-dot paths. lexicon.cpp also includes <cerrno> for the errno it reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each guarded the process-global libespeak-ng with its own mutex and its own init flag, so concurrent calls raced and each could run on the voice the other selected. tools/espeak-shared/eliza-espeak-shared.h now owns the lock and the init record (header-only, one instance per linked image); each component re-selects its own voice under it before espeak_TextToPhonemes. An explicitly configured data path wins over the default one. test-espeak-shared runs both phonemizers from two threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI runs ctest -L main, and no kokoro or paradee test carried the label. Adds it to test-kokoro-cancel, -layer-options, -istft, -layers-portable, test-paradee-phase-lock and the misaki mini-lexicon unit test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to post-merge review of the misaki-g2p / Kokoro / Paradee voice stack. Three commits, one per concern.
1. misaki number reading: out-of-bounds read past the named scales
cardinal()(tools/misaki-g2p/src/numbers.cpp) indexedkScales[g](14 entries) for every three-digit group.get_numberonly checkednumber_fits()in two branches. The ordinal, multi-dot, currency and float branches went straight tocardinal().Evidence: with the pre-fix code under
-fsanitize=address,undefined -fno-sanitize-recover=all,misaki-g2p-clionIt is <60 nines>.5 units.,Pay $<60 nines> now.andThe <60 nines>th one.each reportsnumbers.cpp:58:46: runtime error: index 19 out of bounds for type 'const char *[14]'. The multi-dot formIt is 1.1<59 zeros>.5hits the same line.Fix:
cardinal()is now safe on its own. PastkMaxGroupsit reads the number digit by digit.number_fits()guard the two existing branches use. The ordinal and float branches check before the call.extend_num(currency and multi-dot) marks the word unreadable. The number is then rejected exactly as in the already-guarded branches, matching misaki/num2words, which raises there.test_misaki_g2p.cppcover the three sentences above, the multi-dot form and an oversized cents part, plus directnumber_cardinal/number_ordinal/number_floatcases. The pre-fix code fails the new tests under UBSan (index 14 out of bounds). With the fix, the ASan/UBSan unit run passes with eSpeak both OFF and ON, and the CLI cases report no sanitizer errors.Also:
lexicon.cppusederrnowithout<cerrno>. Added.2. One libespeak-ng lock and init owner for misaki and Kokoro
libespeak-ng is process-global and not thread-safe. misaki (
src/espeak.cpp) and Kokoro (src/kokoro-phonemes.cpp) each had their own mutex and their own "initialised" flag. In the fusedlibelizainferencethey ran unserialised against each other. Each could also end up on the other's voice: Kokoro selectsen-usonce, while misaki selectsgmw/en-USonly on (re)initialisation.Fix:
tools/espeak-shared/eliza-espeak-shared.howns the one lock and the init record (data dir plus current voice). Under the lock, each component callsprepare(lock, data_dir, voice, err), which initialises libespeak-ng if needed and re-selects that component's voice whenever the other one changed it. Only then does it callespeak_TextToPhonemes.ESPEAK_DATA_PATH). misaki passes the explicitmisaki_g2p_opendirectory. An explicitly configured path wins and later default-path callers keep it. This is documented in the header.kokoro_libandmisaki_g2ptherefore share it without a target dependency between them. misaki stays buildable standalone, and no extra static archive is added for the mobile slices to collect. Each component adds the include dir only when it links libespeak-ng, so builds without eSpeak are unchanged.misaki_g2p_espeak_sharedruns misaki (out-of-lexicon words, so every call goes through eSpeak) and Kokorophonemize_ipaconcurrently from two threads, 200 iterations each, and compares every result with the single-threaded one. It exits 77 when either side lacks libespeak-ng.misaki: 11/200 differ, kokoro: 74/200 differ(corrupted output; libespeak-ng itself is not TSan-instrumented, so the damage shows as wrong results).misaki: 0/200 differ, kokoro: 0/200 differunder TSan (-DLLAMA_SANITIZE_THREAD=ON,halt_on_error=1, no reports), both with an explicit data dir and with the default one.3. CI labels
CI runs
ctest -L main, and no kokoro or paradee test carried that label. AddedLABELS maintotest-kokoro-cancel,test-kokoro-layer-options,test-kokoro-istft,test-kokoro-layers-portable,test-paradee-phase-lock,misaki_g2p_mini_lexiconandmisaki_g2p_unit. All are offline, weight-free and need no eSpeak. The misaki unit test builds its lexicon from committedtests/data. Excluded: the network-fetchingmisaki_g2p_lexicon, the agreement gates,test-paradee-api/-parity(GGUF), and the eSpeak-dependent kokoro tests. Release timings: layers-portable 17 s (microbench), all others under 1.5 s.Verification (macOS arm64, Metal)
cmake -G Ninja -DLLAMA_BUILD_TESTS=ON -DLLAMA_BUILD_KOKORO=ON -DLLAMA_BUILD_OMNIVOICE=ON -DLLAMA_FATAL_WARNINGS=ON -DLLAMA_CURL=OFF -DLLAMA_BUILD_SERVER=OFF. Everything builds with-Werrorexcepttest-chat.cpp, which already fails on the base with this flag set (mtmd.hnot found with the server off).ctest -R "kokoro|misaki|paradee|eliza-inference|omnivoice": 21/21 pass.test-paradee-api/-parityskip (no GGUF).ctest -L main -R "kokoro|misaki|paradee|eliza-inference": 9/9 pass.-fsanitize=address,undefined -fno-sanitize-recover=all:misaki_g2p_unitpasses withMISAKI_G2P_ESPEAK=OFFand=ON.test-espeak-sharedpasses as described above.🤖 Generated with Claude Code