Skip to content

Shared eSpeak-ng phonemizer for model frontends - #502

Merged
0xShug0 merged 3 commits into
0xShug0:mainfrom
mirek190:refactor/shared-espeak-ng
Sep 11, 2026
Merged

0xShug0 merged 3 commits into
0xShug0:mainfrom
mirek190:refactor/shared-espeak-ng

Conversation

@mirek190

@mirek190 mirek190 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces a reusable engine::audio::EspeakPhonemizer, migrates SanoTTS and Inflect v2 to it, and adds an optional self-contained eSpeak-ng build for the CLI and server.

The default build behavior remains unchanged: users can provide an installed shared eSpeak-ng library and matching data. With AUDIOCPP_STATIC_ESPEAK=ON, audio.cpp downloads the pinned eSpeak-ng 1.52.0 source archive, verifies its SHA-256, builds it statically, and links the phonemizer code into both executables. No espeak-ng.dll or libespeak-ng.so is required in that configuration.

Shared phonemizer runtime

  • Centralizes dynamic-library discovery, symbol/path validation, initialization errors, clause processing, and cursor-progress checks.
  • Uses one process-wide runtime and mutex because eSpeak's translator, selected voice, and returned text buffer are global state.
  • Reselects the requesting frontend's voice on each call.
  • Safely switches library/data configurations and recovers after failed switches.
  • Keeps model-specific normalization, voice fallback, punctuation handling, IPA cleanup, and token mapping inside each model frontend.
  • Preserves existing explicit eSpeak library/data session options.

SanoTTS's E2M and Piper paths and Inflect v2 now share this implementation. Kokoro remains in its separate preview work and is not migrated by this PR.

Optional static eSpeak-ng integration

Configure with:

cmake -S . -B build/static-espeak -DAUDIOCPP_STATIC_ESPEAK=ON
cmake --build build/static-espeak --config Release --target audiocpp_cli audiocpp_server

The integration builds eSpeak in an isolated CMake project and disables components unnecessary for phonemization: Sonic, audio playback, MBROLA, asynchronous synthesis, and speechPlayer. The upstream host tool compiles the phoneme tables and all 114 language dictionaries. Cross-compiling this option is currently rejected because data generation requires a runnable host tool.

The resulting executable directory contains:

audiocpp_cli[.exe]
audiocpp_server[.exe]
espeak-ng-data.bin
licenses/espeak-ng/

The license directory includes upstream COPYING, the verified source archive, the integration scripts, and documentation needed to reproduce the bundled component.

Separate eSpeak data package

espeak-ng-data.bin contains the compiled dictionaries and phoneme tables. It is not a neural model and contains no weights or audio recordings. Internally it uses audio.cpp's embedded-file GGUF metadata layout with zero tensors, but the .bin name distinguishes this shared runtime resource from downloadable model GGUFs.

The build creates and stages this file automatically; users do not need to run the packer manually. Both CLI and server locate it beside their executable. For backward compatibility, the runtime also accepts espeak-ng-data.gguf, an unpacked espeak-ng-data directory, or an explicit model espeak_data_path pointing to any of those forms.

The package is about 17.5 MiB and currently uncompressed. audiocpp_espeak_pack is also available for manually repacking compatible eSpeak-ng 1.52.0 data.

Safe shared extraction cache

eSpeak itself requires ordinary filesystem data, so the package is materialized into a per-user cache shared by CLI and server:

  • Windows: %LOCALAPPDATA%/audio.cpp/espeak-data/
  • Linux/macOS: $XDG_CACHE_HOME/audio.cpp/espeak-data/, falling back to $HOME/.cache/audio.cpp/espeak-data/

Cache entries are content-keyed and fully compared before reuse. Extraction validates package metadata, file counts, required files, byte ranges, portable paths, duplicate/case-colliding names, traversal attempts, symlinks, and unsupported versions. A complete staging directory is published atomically, so concurrent processes cannot observe partially extracted data. Changed packages or damaged cache entries create a new revision rather than modifying files another process may be using.

Automatic cache eviction is not included; old entries can be removed while no audio.cpp process is using them.

Validation

Tested on Windows x64 with MSVC 19.43 in a Release CPU build using eSpeak-ng 1.52.0:

  • Static audiocpp_cli and audiocpp_server build successfully.
  • Import inspection confirms neither executable depends on an eSpeak DLL.
  • 5/5 focused CTest tests pass: data package, shared phonemizer, static frontend concurrency, SanoTTS frontend, and Inflect v2 frontend.
  • 21/21 frontend token sequences are identical between dynamic eSpeak-ng 1.52.0 and the statically linked implementation using the same generated data.
  • Coverage includes 5 SanoTTS prompts, 5 Inflect prompts, and 11 Piper language cases: English, Spanish, French, German, Italian, Portuguese, Polish, Russian, Hindi, Vietnamese, and Indonesian.
  • A portable directory containing only the probe executable and espeak-ng-data.bin passes the same 21/21 comparison.
  • 200 concurrent cross-model requests pass against serial reference results.
  • The package test covers binary round-trip data, concurrent extraction, unchanged-cache reuse, damaged-cache recovery, package-update invalidation, legacy .gguf compatibility, and path-traversal rejection.
  • The shared runtime test covers missing dependencies/symbols/voices, failed-switch recovery, clause joining, phoneme modes, cursor progress, temporary-client destruction, and 1,000 concurrent calls.
  • git diff --check passes.

Not tested on Linux and macOS

@mirek190 mirek190 changed the title Extract shared eSpeak-ng phonemizer for model frontends Shared eSpeak-ng phonemizer for model frontends Sep 10, 2026
@mirek190
mirek190 marked this pull request as draft September 10, 2026 11:05
@mirek190

mirek190 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Pushed commit 4d44495 with the static eSpeak-ng integration and executable-adjacent espeak-ng-data.bin package. The PR description now documents the architecture, compatibility behavior, extraction cache, validation results.

@mirek190
mirek190 marked this pull request as ready for review September 10, 2026 23:37
@mirek190

Copy link
Copy Markdown
Contributor Author

I think is ready .... now we have a unified platform for using audio models based on eSpeak-ng.
Not like we have currently that each model had a separate implementation.

@0xShug0
0xShug0 merged commit eb1a569 into 0xShug0:main Sep 11, 2026
7 of 8 checks passed
@0xShug0

0xShug0 commented Sep 11, 2026

Copy link
Copy Markdown
Owner

@mirek190 PR merged! Super clean refactor (PI + Qwen 😄 ?), and this is a big step. I’m now more comfortable taking in more phoneme-based models.

Glad you made static eSpeak opt-in. GPL-3.0 is viral.

@mirek190

mirek190 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@mirek190 PR merged! Super clean refactor (PI + Qwen 😄 ?), and this is a big step. I’m now more comfortable taking in more phoneme-based models.

Glad you made static eSpeak opt-in. GPL-3.0 is viral.

For that one I used codex-cli ;-)
PI agent is working on something else.

cunba-ai pushed a commit to cunba-ai/audio.cpp that referenced this pull request Sep 11, 2026
Merge of PR #33 (8 upstream commits). Head-on collision resolved:
upstream 0xShug0#502 introduced a shared EspeakPhonemizer with a
process-global Service singleton (per-instance phonemizer objects hold
only config; nothing tears down eSpeak per model), which subsumes the
fork's espeak_lifetime.h refcount fix for the multi-voice LRU SIGSEGV
observed in sound-rs — the crashing code path (per-instance
espeak_Terminate + FreeLibrary) no longer exists. Resolution: take
upstream's sanotts/inflect_v2 frontends verbatim, delete the fork's
espeak_lifetime.h and its unit test. CMakeLists conflict: keep both the
fork-only embed blocks (VAD + audio utilities incl. GTCRN) and
upstream's new AUDIOCPP_STATIC_ESPEAK hook. Also brings Sortformer v2.1
streaming diarization (new community model), Colab WebUI notebook +
CUDA T4 release build (0xShug0#501), Vulkan fill dispatch fix (0xShug0#508), fish_audio
HIP stream perf (0xShug0#500), voxtral warmbench fix. fork_regression 10/10
green, loader catalog in sync, embed intact (~34.8MB .rdata).

Generated-by: zcode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants