ADR-0009: SAPI5 voices on Windows
- Status: accepted
- Date: 2026-09-25
- Status update (Saturday, September 26, 2026): Implemented. The duplicated host protocol was merged into
textweaver-enginehost(ADR-0012);cargo xtask hostsbuilds both SAPI hosts. OneCore voices are listed through SAPI5 by default ([speech.sapi] onecore). Code Factory's Eloquence SAPI voices are hidden unless the user opts in (ADR-0007), and the backend reports capabilities per voice.
Context
Windows users have SAPI5 voices from many sources: Microsoft's desktop voices (David, Zira), voices bundled with reading software (for example VW Paul, Kate, and James), and eSpeak's SAPI5 wrapper. Some are registered only for 32-bit programs (under WOW6432Node), so a 64-bit process cannot load them.
A probe on 2026-09-25 (System.Speech, 64-bit and 32-bit PowerShell, output to WAV files) found that every one of these voices reports a word-boundary event for each word, with its character position and audio position: Microsoft David and Zira (64- and 32-bit), VW Paul, Kate, and James (32-bit only), and eSpeak (32-bit only). David and Zira expand "9:30 a.m." into four events pointing at the same source range; eSpeak and the VW voices report sub-token ranges ("Dr", "9", "30"). ETI-Eloquence through SAPI is the exception (one event per sentence, ADR-0007) and is not the route for Eloquence.
Decision
- A crate,
textweaver-sapi, provides asapibackend. Each voice runs in a host process,textweaver-sapi-host, built twice:x86_64-pc-windows-msvcfor 64-bit voices andi686-pc-windows-msvcfor 32-bit-only voices. The backend lists the voices of both registries (64-bit first when a voice exists in both) and starts the matching host. - The host drives
ISpVoicethrough thewindowscrate, synthesizes into a memory stream, and reportsSPEI_WORD_BOUNDARYevents with their audio stream offsets, sending PCM and events over a pipe as the ECI host does (ADR-0007). The backend plays the audio (rodio) and emitsWord { byte_range, audio_ms }, so SAPI voices get audio-clock highlighting, native pause and resume, and WAV export. - Character positions from SAPI are UTF-16 offsets into the text the host sent; the backend maps them to UTF-8 byte ranges of the utterance.
- Rate, pitch, and volume map onto SAPI's
-10..10rate and0..100volume and XML pitch markup, with a measured wpm calibration per voice family. - Eloquence SAPI voices are listed but flagged as having no word timing; Eloquence is served by the
ecibackend. - A host process isolates the reader from crashes in third-party engines, and lets 32-bit voices work in a 64-bit textweaver.
Consequences
- Windows builds produce two host binaries (
cargo xtask sapi-host); CI builds the 32-bit host on the Windows runner. - The host protocol duplicates the ECI host's from an earlier stage of the project; the two are merged into one shared engine-host protocol at integration.
- Automated real-voice tests use Microsoft David, Zira, and eSpeak; voices bundled with other products are the user's to enable.
Addendum, 0.1.0-beta.3
The separate textweaver-sapi-host programs described above are gone from the packages. A 64-bit voice now runs in a copy of tw or the app started in its hidden --engine-host mode, and a 32-bit voice runs in the one 32-bit host, textweaver-host-x86.exe. See the addendum to ADR-0007.
See also
- Speech engines and voices: SAPI5 and OneCore voices.
- ADR-0012: The engine host: the shared host protocol.
- Documentation index