Speech engines, neural voices, and runtimes: what to adopt next
This report surveys the open neural voices, the Rust inference runtimes, and the speech-path engineering that textweaver could adopt after 0.1.0-alpha.7, with performance as the first criterion. It is for the maintainers who decide what goes into alpha.8 and alpha.9, and for anyone running a spike on a new voice or runtime. Written Friday, October 2, 2026, from the repository and the sources listed at the end. Numbers measured on other people's machines are marked "reported"; what could not be checked is marked "unverified".
Where textweaver stands
- Piper voices run in process on RTen 0.26.0 (
Cargo.toml,Cargo.lock). Word timing is the duration predictor'sw_ceiltensor, read as a second graph output (crates/textweaver-piper/src/model.rs). Rate maps to the length scale; pitch is a rubato windowed-sinc resample with 128 taps (crates/textweaver-piper/src/synth.rs). - Synthesis is a sentence at a time, with the first clause of a long first sentence on its own (
crates/textweaver-piper/src/text.rs), on a worker thread, played through the shared playback client (crates/textweaver-piper/src/backend.rs,crates/textweaver-enginehost/src/playback.rs). - The device output is rodio 0.22.2 over cpal 0.17.3 (
crates/textweaver-enginehost/src/audio.rs). No buffer size is set, so rodio's default applies: it "configures a default buffer size of 100ms latency regardless of the system default" [26]. - The service keeps two chunks of lookahead in the engine and normalizes one more (
crates/textweaver-speech/src/queue.rs,DEFAULT_LOOKAHEAD = 2;service.rs,refill). Pause is native for Piper (Caps::PAUSEinbackend.rs). A rate or pitch change restarts, because Piper lacksLIVE_RATE(respeak_with_new_paramsinservice.rs). - Measured on a Ryzen 5 5600G (ADR-0023): real-time factor 0.13 with libespeak-ng phonemes and 0.18 with the pure-Rust phonemizer; first audio 77 ms for two words and 274 ms for a 60-character clause, before the device buffer.
synth.rsmentionscargo xtask piper-bench, butxtask/src/main.rshas no such subcommand;cargo xtask benchtimes "open to first speech" with a silent bench-clock backend (xtask/src/bench.rs), not Piper.crates/textweaver-piper/src/phonemes.rshas no phoneme cache.
1. Open neural voices that fit a student's laptop
The bar: no GPU, no network after the download, a licence textweaver can at least download under, and some way to time words. The last point is what divides the field. VITS-style models (Piper, MeloTTS) and StyleTTS 2 descendants (Kokoro, Kitten) have a duration predictor whose output gives each phoneme's length, so a word's start is a sum. Flow-matching and language-model voices (Supertonic, ZipVoice, Pocket TTS, Chatterbox, Orpheus, OuteTTS) emit audio with no alignment, so the highlight would fall back to the timer pacer of ADR-0003.
| Voice | Licence (code / weights) | Size | CPU real-time factor (reported) | Word timing | Phonemizer | Languages | ONNX for a pure-Rust runtime |
|---|---|---|---|---|---|---|---|
| Piper (OHF-Voice/piper1-gpl 1.8.0, September 4, 2026) [1][2] | GPL-3.0 / per voice (CC0, CC BY, some non-commercial) | 20 to 140 MB per voice | 0.13 on Ryzen 5 5600G (ADR-0023) | yes, w_ceil durations |
espeak-ng | about 30 | yes; runs on RTen today |
| Kokoro-82M (hexgrad) [3][4] | Apache-2.0 / Apache-2.0 | 82M; ONNX 326 MB fp32, 92 MB int8 (reported [20]) | 0.16 on M3 Pro (reported [5]) | yes, pred_dur in the pipeline [4]; ONNX export with a duration output in kokoro-onnx [6] |
misaki G2P, espeak-ng fallback | 9 | yes; rten-examples/src/kokoro.rs exists [7] |
| Supertonic 3 (Supertone) [8][9] | MIT / OpenRAIL-M | 99M | 0.20 at 16 threads (reported [9]) | none; "does not include" alignment [8] | own frontend, no espeak-ng stated | 31 | yes; sherpa-onnx int8 export [10] |
| Kitten TTS (KittenML) [11][12] | Apache-2.0 / Apache-2.0 | nano 15M (25 MB int8), micro 40M, mini 80M | nano int8 0.12, mini 0.26 on M4 Pro via ort (reported [12]) | not documented | espeak-ng, required | English | yes |
| Matcha-TTS [13] | MIT | small (count not stated) | "very fast", ODE steps configurable | yes, alignments | not stated (icefall exports use espeak-ng, unverified) | English models; others in sherpa-onnx | yes, with vocoder embedded |
| MeloTTS [14] | MIT | not stated | "CPU real-time" | VITS lineage, durations exist (exposure unverified) | per language (unverified) | EN, ES, FR, ZH, JA, KO | sherpa-onnx exports (unverified) |
| Pocket TTS (Kyutai) [15] | MIT / MIT | 100M | about 6x real time on 2 cores of an M4 (reported); 200 ms first chunk | no; a community fork adds it | tokenizer, no phonemes | 7 | community export only |
| ZipVoice (k2-fsa) [16] | Apache-2.0 | 123M | not stated | no | pypinyin for Chinese | ZH, EN | yes, with int8 |
| Chatterbox-Nano / Turbo [17] | MIT | 110M / 350M | Nano 3x real time on 8 cores (reported) | no | tokenizer | EN (multilingual 500M model) | not documented |
| OuteTTS 1.0 [19] | Apache-2.0 | 0.6B, 1B | llama.cpp on CPU, below real time (unverified) | no | LLM tokens | several | GGUF, not ONNX |
| StyleTTS 2 | MIT code; Kokoro's parent lineage [3] | about 100M | similar to Kokoro | yes, duration predictor | espeak-ng | EN | community exports (unverified) |
| Orpheus 150M to 3B, Dia 1.6B, Zonos 1.6B and 8B [18]; Parler-TTS 880M [20]; NeuTTS Air 0.5B [21] | Apache-2.0 (NeuTTS: not stated) | GPU class | below real time on a laptop CPU | no | tokens | no | |
| F5-TTS 336M, Fish Speech, XTTS-v2 467M [20] | CC-BY-NC 4.0, CC BY-NC-SA 4.0, Coqui Public Model License | GPU class | no | no; non-commercial licences may_bundle refuses |
Notes on the table:
- Piper's upstream moved from rhasspy/piper (archived October 2025, MIT) to OHF-Voice/piper1-gpl (GPL-3.0), which added Japanese (1.7.0) and Thai (1.8.0) phonemizers [1][2]. Piper has no SSML;
--sentence-silenceis the only pause control [22]. - Kokoro's pipeline computes
start_tsandend_tsper token frompred_dur, at 600 frames per second of 24 kHz audio [4]. The stock ONNX export outputs onlywaveform[7], but kokoro-onnx'sscripts/export.pyadds a duration output, andkokoro-v1.1-zh.onnxships with one [6]. That is the trick textweaver already plays with Piper'sw_ceil. - Supertonic's weights are OpenRAIL-M, a licence with use restrictions that
Licence::may_bundleincrates/textweaver-piper/src/catalog.rsdoes not recognize, and it has no alignment anyway. - sherpa-onnx (Apache-2.0, C++ with Rust bindings) packages Piper, Matcha-TTS, Kokoro, ZipVoice, Pocket TTS, and Supertonic exports [23]. Its catalogue is the best index of which models have working ONNX graphs; its C++ runtime is not one textweaver would link.
Which two or three are worth a spike
- Kokoro-82M on RTen. Apache-2.0 weights that
may_bundlecan accept, nine languages, word timing from a duration output, and an RTen example in the tree (rten-examples/src/kokoro.rs, which takes IPA and theaf_heart.binstyle vector) [7]. The spike: export withscripts/export.pyfrom https://github.com/thewh1teagle/kokoro-onnx (duration output), load on RTen 0.26, feed IPA fromcrates/textweaver-piper/src/phonemes.rs, and map durations to words assynth.rsdoes. Risks: 326 MB in memory at fp32 (reported), and an int8 export whose weights use all 8 bits hits the AVX2 saturation ADR-0023 patched for Whisper. misaki's English G2P would need a Rust port (misaki-rsis on crates.io as a dependency ofkokoro-en), or Kokoro takes espeak IPA at a quality cost. - Kitten TTS nano. Apache-2.0, 25 MB at int8, English, espeak-ng input like Piper, 0.12 real-time factor through ort (reported) [12]. The only candidate small enough to ship inside a Linux tarball for machines where a 60 MB download is a problem. The spike: load the nano int8 ONNX on RTen and look for a duration tensor. Repository: https://github.com/KittenML/KittenTTS.
- Matcha-TTS on RTen only if the first two fail on operator coverage: MIT, alignments available, ONNX export with the vocoder embedded [13]. Lower voice quality than Kokoro.
Not worth a spike for the highlight: Supertonic, Pocket TTS, ZipVoice, and Chatterbox-Nano. They are fast enough, but they time nothing.
2. Inference runtimes from Rust
| Runtime | Pure Rust | VITS and Kokoro graphs | int8 | fp16 | CPU threading | Model load | Notes |
|---|---|---|---|---|---|---|---|
| RTen 0.26.0, August 29, 2026 [24][25] | yes | yes (Piper runs; Kokoro example) | yes: MatMulInteger, ConvInteger, dynamic quantization fusions; 4-bit MatMulNBits |
weights up-converted to f32 at load; no f16 compute | thread pool sized to physical or performance cores | Model::load copies; load_mmap is unsafe; external_data_static embeds data without copying |
AVX2, AVX-512, NEON, WASM SIMD |
| ort 2.0.0-rc.13 (ONNX Runtime 1.28) [27][28] | no, C++ | yes, widest coverage | yes, with VNNI and AMX kernels | limited on CPU | intra-op pool | memory map available | default feature downloads Microsoft binaries that may carry telemetry; static linking "not recommended" by Microsoft; CPU package about 21 MB installed (Arch 1.22.2) [29] |
| tract 0.23.8, September 21, 2026 [30][31] | yes | no: dynamic Range for shape construction fails on VITS graphs (#2928), RandomNormalLike unimplemented |
yes | no | rayon | copies | good for fixed-shape CNNs |
| candle 0.11.0 | mostly; tokenizers pulls Oniguruma C (ADR-0023) |
needs a hand-written model, not an ONNX graph | yes, GGUF | yes | rayon | safetensors mmap | a tensor library, not a graph runtime |
| burn 0.21.0 with burn-onnx 0.22.0-pre [32] | yes | converts ONNX to Rust code at build time; 134 of 160 operators (84%) | partial (unverified) | yes on wgpu | ndarray or wgpu backends | compiled in | long compile times; each voice export becomes code |
| wonnx 0.5.1 | yes | GPU only | no | archived September 1, 2026 [32] | |||
| Lele, OxiONNX, tpt-infer, yscv-onnx [33] | yes | unproven | varies | all announced in 2026; none has a VITS or Kokoro example |
What changed in RTen since textweaver pinned 0.26, from the Unreleased section of its changelog [24]: the .rten format is deprecated and off by default (textweaver builds with onnx_format and without rten_format in Cargo.toml, so nothing changes); external_data_static embeds a model in the binary without a copy (useful only if a voice were bundled); compile time and binary size fall by sharing operator instantiations across element types (worth taking, since RTen builds at opt-level = 3 even in dev and build time rose "by a few minutes cold" per ADR-0023); and fixes in ScatterND, buffer pooling, and quantization saturation in the generic SIMD path. From 0.25 and 0.26: shape inference on by default, input validation, f16 models loaded by up-conversion, GRU/LSTM sped up, Conv and BatchNormalization fused, and "unused operator outputs are now removed during graph optimization" [24], so asking for w_ceil as a second output costs nothing extra, as model.rs assumes.
Still open: no AVX-VNNI path, so the 7-bit int8 weight patch in crates/textweaver-dictation/src/onnx_patch.rs stays; no f16 compute, so an f16 export saves disk but not time; load_mmap stays unsafe, so model.rs reads the file into memory, one copy of the model (63 MB for a medium voice) and about 0.1 s of load (ADR-0023).
Keep RTen. ort would buy VNNI int8 and the widest operator coverage at the price of a 20 MB C++ library, a download at build time, and a second build system on every platform. tract cannot run the graphs. burn-onnx would turn each voice into generated code. Re-measure after each RTen release: Piper real-time factor and first audio, Whisper base.en int8 real-time factor, and OCR detection time (the Cargo.toml comment records 0.11 s per page in release), with the measurement described in section 3.
3. Performance engineering for the speech path
Sentence streaming versus chunking
textweaver already streams: the worker synthesizes one sentence, sends it, then the next, and the first clause of a long first sentence goes alone (crates/textweaver-piper/src/text.rs, chunks). This is the right shape: a VITS run is one shot per input, so latency scales with the chunk, and the clause tail punctuation is put back so the model keeps comma intonation across the split. Two refinements. The clause split applies only to the first sentence of an utterance (LONG_FIRST_SENTENCE in text.rs); splitting the first clause of every chunk longer than about 80 characters would cap first-audio synthesis near the 77 to 150 ms measured for short inputs, at one more model run per long sentence. And the sentence can be phonemized once and the model run on clause slices, since phoneme ids are already tracked by owner word (phonemes.rs, prepare).
Lookahead depth versus memory
A 22,050 Hz 16-bit sentence of eight seconds is about 350 KB. Three chunks in flight is about 1 MB, nothing beside a 63 MB model. Memory is not the constraint; CPU under load is. ADR-0023 measured the real-time factor rising from 0.13 to 1.0 when five builds shared the machine, and near 1 two sentences of lookahead is the whole margin before a gap. The lookahead is a ServiceConfig field (service.rs), so the fix is adaptive, not larger: the worker reports each chunk's elapsed and audio length (Chunk in synth.rs), and the backend asks for one more chunk when elapsed / audio over the last two chunks exceeds 0.5, capped at four so a cursor move does not discard a minute of synthesis.
First audio under 150 ms on a stop-and-restart
The restart cases for Piper are a rate or pitch change (respeak_with_new_params), a skip, a cursor move while reading, and an announcement inside a reading. The budget today is phonemization (a few ms with libespeak-ng; the pure-Rust port reports 606 ns to the first phoneme [34]), a model run on the first chunk (77 to 274 ms measured, ADR-0023), and rodio's 100 ms default buffer [26]. The device buffer alone eats two thirds of the target. Four changes, in order of return:
- Set the output buffer.
DeviceSinkBuilder::with_buffer_size(cpal::BufferSize::Fixed(n))incrates/textweaver-enginehost/src/audio.rs. At 22,050 Hz, 1,024 frames is 46 ms; rodio's upgrade notes recommend 1,024 to 2,048 for media playback [35]. Fall back to the default when the device refuses, and keep the stall watchdog (STALL_TIMEOUTinservice.rs) as the safety net. Expected gain: 50 to 75 ms on every first word, for every engine that plays through the shared client (Eloquence, SAPI, DECtalk, Piper). - Splice instead of restart on a rate change. The playback client knows each queued word's first sample (
Chunk.words). On a rate change, keep playing the current chunk, synthesize from a word at least 400 ms ahead at the new length scale, and swap the feed at that word's sample. The highlight never stops and no word repeats. This touchesservice.rs(respeak_with_new_paramsgains a backend hook),backend.rs(aResynthesize { from_word }command to the worker), andplayback.rs(replace queued samples from an offset). The generation rules of ADR-0003 stay: the replacement carries the same utterance id. - Cache the last chunks' audio by word offset. A resume from a cursor move inside the last sentence, or a "say again", slices the cached audio from that word's first sample instead of synthesizing. Per voice, rate, and pitch; the last four chunks, about 1.5 MB.
- Cache phonemes. An LRU in
phonemes.rskeyed by clause text, voice phoneme map, and phonemizer, holding ids and owners. Phonemization is about 0.05 of the real-time factor on the pure-Rust path (0.18 against 0.13, ADR-0023), so a cache saves about a quarter of the synthesis time on re-reads and restarts, and all of it for repeated announcements ("Next heading", "heading level 2").
Prosody across sentence boundaries
Piper models sentence-final intonation from the trailing ., !, or ? that text.rs appends, so a sentence per chunk keeps it right. The only discontinuity is the first-clause split, where the clause ends in a comma and the model does not see the rest. If listening tests on en_US-joe-medium find the comma pause too long, trim 30 to 60 ms of trailing silence from the first clause, measured from the last w_ceil frame. Piper's own program inserts --sentence-silence between sentences [22]; textweaver does not, and the model's own end-of-sentence silence is enough, and it halves at 2x rate, which fast readers want.
Resampling and pitch
Pitch is the only resampling in the Piper path, and only when the pitch is not zero. The cost is 128 taps per output sample, about 2.8 million multiply-adds per second of audio, well under a millisecond with rubato's AVX or NEON kernels [36]. rubato notes that "each halving of the sinc function length nearly doubles the speed" [36], but the gain is not worth measuring until the model run shrinks. Whisper's 16 kHz resample uses rubato's FFT resampler (ADR-0023), the fastest for a fixed ratio [36]. rubato 5.0.1 (October 1, 2026) is a patch over the 5.0.0 textweaver uses. To avoid resampling, the playback client already opens the device at the voice's rate (set_sample_rate in backend.rs), so the OS mixer does the only other conversion; a Kokoro voice at 24 kHz would do the same.
cpal directly versus rodio
rodio adds a mixer, a Source trait, and a sink over cpal, and textweaver uses little of it: FeedSource in audio.rs is one endless source. Going to cpal directly would remove one crate and the 100 ms default, but textweaver would then own stream callbacks, device enumeration, and the tone generator on every platform. The buffer-size call gets the latency gain without that; keep rodio.
How to measure
Add cargo xtask bench --speech (or the piper-bench subcommand that synth.rs already names) in xtask/src/bench.rs: run textweaver_piper::synth::measure on a fixed 120-word paragraph with the voice named by TEXTWEAVER_PIPER_BENCH_VOICE, and skip with a note when none is installed. Report model load, first audio, real-time factor, and a new restart number: start a reading, change the rate at 1.5 s, and time from the command to the first new sample leaving the feed, through the silent timed output in audio.rs so CI never plays audio. Print, gate nothing, compare across RTen releases. The existing open_to_first_speech_ms stays as the application-path number.
Formant and concatenative engines
Fast screen-reader users want a formant engine at 500 words per minute and above, with word events. The free ones:
- eSpeak NG as a library. textweaver loads
libespeak-ngat run time on Linux (docs/speech.md). It gives word events with audio positions, and it parses SSML:break,mark,emphasis,prosody(rate, pitch, volume),say-as,voice,s, andp[37]. - eSpeak NG as a pure-Rust port. The
espeak-ngcrate 0.2.0 (September 5, 2026, GPL-3.0-or-later) reimplements eSpeak NG with data from 1.53.0-dev, 123 dictionaries for 151 languages, phonemization and audio synthesis, and a reported 380x real-time throughput [34]. textweaver uses it only as Piper's English phonemizer (bundled-data-eninCargo.toml). It could become anespeak-rsbackend on every platform, with word timing from the synthesizer itself. Known gaps per its README: prefix stripping, mid-word language switching, some number formatting [34]. Its bit-identical claim is a claim to test. - DECtalk. The GitHub source carries FONIX's proprietary notice: use "authorized only pursuant to a valid written license" [38]. It stays bring-your-own, as
docs/dectalk.mdsays. - Eloquence. Proprietary. There is no free Eloquence.
- RHVoice. LGPL-2.1 core, GPL-3 with MAGE; statistical parametric, not formant; SSML, 30 or more languages; speaks through SAPI5, Speech Dispatcher, and NVDA [39]. textweaver reaches it today through the
sapiandspeechdbackends, with those APIs' word or index events. Voice licences differ per voice (unverified). - Flite (BSD-style, Carnegie Mellon [40]) and SVOX Pico (Apache-2.0, six languages [41]) have no SSML and no documented word events (unverified). Neither is worth a backend while eSpeak NG covers the fast-reader case.
SSML-style pauses and emphasis
eSpeak NG parses SSML [37]; Piper has none, only --sentence-silence [22]; Kokoro reads punctuation and nothing else [42]; Supertonic has expression tags such as laugh and breath, not SSML [9]. Eloquence, SAPI, and DECtalk take their own control sequences through their hosts (not researched here).
textweaver can do pauses without any engine's help, because for the engines that matter it owns the clock. A pause becomes a span in the narration plan (SpanKind in textweaver-core, where "heading level 2" already lives as inserted speech), and the service turns it into a silent utterance of N milliseconds: the shared playback client (playback.rs) pushes N ms of zeros into the feed, which keeps the audio clock honest and pause and stop working; a timer-paced or host-owned engine waits N ms before the next speak. Effect on the offset map: none for the words. A silent utterance carries no source range, so Position reports None as inserted speech does, the previous word keeps its highlight, and the next utterance's audio_ms values start from its own first sample, as every utterance's do. Only a pause inside one utterance would shift later audio_ms values by N; the plan should split the utterance instead. Emphasis is a span with its own rate or pitch, a separate utterance with scaled VoiceParams, which is what char_rate_scale already does for single characters. For Piper, pitch is a resample, so emphasis is best a 15 percent longer length scale plus a 100 ms pause before the span.
4. Whisper dictation and OCR on the same runtime
| Model | Licence | Size | CPU real-time factor (reported) | Word timestamps |
|---|---|---|---|---|
| Whisper tiny / base / small [43] | MIT | 39M / 74M / 244M | 0.04 / 0.07 / 0.17 on an M1 with whisper.cpp [44] | via cross-attention alignment heads and DTW; sherpa-onnx's turbo export adds a cross_attention_weights decoder output for it [45] |
| Whisper large-v3-turbo [43] | MIT | 809M | below real time on CPU unless int4 or int8 [46] | same |
| distil-large-v3 [46] | MIT | about 756M | 6x faster than large-v3 (reported) | same |
| Moonshine [47] | MIT (streaming models); community licence for legacy non-English | tiny and base; counts unverified here | about 5x real time on CPU for the 245M streaming model (reported [48]) | not documented |
| Parakeet TDT 0.6B v3 [48] | CC-BY-4.0 (unverified here) | 600M | 10.8 to 12.9x real time on CPU via ONNX (reported) | yes, documented word and segment stamps |
| Kyutai STT [49] | CC-BY-4.0 | 1B, 2.6B | GPU examples only | yes, word level |
textweaver runs base.en int8 on RTen (crates/textweaver-dictation/src/rten_whisper.rs). Keep it: the smallest model that transcribes dictation reliably, with the 7-bit patch for AVX2 saturation. small.en int8 is the next step if accuracy complaints come in, at about 2.5x the time. Word timestamps are not needed for dictation; they would matter only for aligning an audiobook to its text, which is not on the roadmap. Parakeet is the one to watch if that changes: faster than Whisper on CPU with stamps built in, but 600M and exported for ONNX Runtime.
OCR: ocrs 0.13.1 (September 13, 2026; Apache-2.0 or MIT; Latin alphabet only; on RTen) [50] is what crates/textweaver-ocr/src/ocrs_engine.rs runs, with paddle.rs and tesseract.rs beside it. The Cargo.toml comment records detection at 0.11 s per page in release. PaddleOCR is Apache-2.0; PP-OCRv5 mobile ONNX exports are small (detection 5 MB, recognition 7.5 to 16 MB, reported [51]), and PP-OCRv6 claims a 5.2x CPU speedup over v5 [52]. TrOCR (62M small, 334M base) is line-level and needs a detector [53]; rten-examples/src/trocr.rs runs it on RTen [7]. The next OCR step is a PP-OCRv5 or v6 mobile recognizer on RTen for non-Latin scripts, measured per page against ocrs.
5. The twelve most valuable speech-side improvements
Ranked by expected gain per unit of work. Sizes: small is hours, medium a day or two, large a week or more.
| # | Improvement | Evidence | Touches | Size | Target | Budget | Measure |
|---|---|---|---|---|---|---|---|
| 1 | Output buffer of about 46 ms (1,024 frames at 22,050 Hz), default as fallback | rodio defaults to 100 ms [26]; audio.rs sets none |
crates/textweaver-enginehost/src/audio.rs |
small | alpha.8 | first audio -50 to -75 ms on every host engine and Piper; no memory or size change | restart timing in cargo xtask bench --speech |
| 2 | cargo xtask bench --speech: load, first audio, real-time factor, restart latency |
synth.rs names a piper-bench that main.rs lacks |
xtask/src/bench.rs, synth.rs (measure) |
small | alpha.8 | none at run time | its own numbers, per RTen release |
| 3 | Phoneme cache (LRU by clause, voice map, phonemizer) | about a quarter of the pure-Rust path's time (ADR-0023); no cache today | crates/textweaver-piper/src/phonemes.rs |
small | alpha.8 | about 100 KB per 1,000 clauses; synthesis -25% on repeats | measure run twice on one text |
| 4 | Keep the last four chunks' audio; resume by word sample offset | Chunk.words holds each word's first sample |
crates/textweaver-piper/src/backend.rs worker |
small | alpha.8 | 1.5 MB; 0 ms of synthesis on resume inside the last sentence | restart timing with a cursor move inside the sentence |
| 5 | Splice at a word boundary on a rate or pitch change | respeak_with_new_params restarts; the client knows word samples |
service.rs, backend.rs, playback.rs |
medium | alpha.9 | no gap; one extra model run per change, on the worker | silence between last old and first new sample, target 0 |
| 6 | Pure-Rust eSpeak NG backend with word events, all platforms | the crate synthesizes at a reported 380x real time, 151 languages [34]; used only for English phonemes today | new backend in crates/textweaver-speech/src/backends/, Cargo.toml features |
medium | alpha.9 | English data already embedded, other languages as a download; real-time factor expected below 0.01 | tw backends; word timing against libespeak-ng on 154 words as in ADR-0023 |
| 7 | Adaptive lookahead, two to four chunks, from measured real-time factor | factor reached 1.0 under load (ADR-0023); lookahead is a config field |
queue.rs, service.rs, Piper backend reporting elapsed |
small | alpha.9 | up to 1.5 MB more audio; no latency change | soak test beside a build; the stall watchdog never fires |
| 8 | Pauses as silent utterances, emphasis as scaled spans | engines own the clock through the shared client; roadmap lists SSML-style pauses | crates/textweaver-core span kinds, service.rs, playback.rs |
medium | alpha.9 | zero samples are free; words' offset maps unchanged | recording-backend tests around a pause; tw speak --json |
| 9 | Kokoro-82M on RTen with word timing from a duration output | Apache-2.0; RTen example [7]; kokoro-onnx export adds durations [6] | new crates/textweaver-kokoro sharing phonemes.rs and the playback path |
large | later | 326 MB fp32 or 92 MB int8 in memory (reported); real-time factor expected 0.2 to 0.4 | measure; timing error after pauses as in ADR-0023 |
| 10 | Kitten TTS nano int8 on RTen, a 25 MB English voice | Apache-2.0; 0.12 real-time factor via ort (reported [12]); espeak-ng input | the module from 9; crates/textweaver-piper/src/catalog.rs |
medium | later | 25 MB download; under 100 MB in memory | measure; a duration tensor decides word timing |
| 11 | Full eSpeak NG language data as a download for non-English Piper voices | only bundled-data-en is built in (Cargo.toml) |
phonemes.rs, download.rs, data folder |
medium | alpha.9 | download size to measure; binary unchanged | diff Spanish and German phonemes against libespeak-ng |
| 12 | Next RTen release, plus an int8 dynamic-quantized Piper export | compile time and binary size fall [24]; int8 kernels exist, VNNI does not | Cargo.toml, model.rs; onnx_patch.rs if weights saturate |
medium | alpha.9 | model file -60 to -75%; AVX2 saturation risk | measure before and after; the 7-bit patch test |
Everything above keeps the input thread free: synthesis on the Piper worker, playback on the audio thread, the service on the speech thread (ADR-0003). Nothing adds GUI or TUI state; the only visible change is a shorter wait after a key press.
Sources
- OHF-Voice/piper1-gpl releases: https://github.com/OHF-Voice/piper1-gpl/releases
- OHF-Voice/piper1-gpl repository: https://github.com/OHF-Voice/piper1-gpl
- hexgrad/kokoro repository: https://github.com/hexgrad/kokoro
- Kokoro pipeline timestamps (
pred_dur,MAGIC_DIVISOR): https://raw.githubusercontent.com/hexgrad/kokoro/main/kokoro/pipeline.py - Kokoro TTS review with CPU timing: https://www.visionstory.ai/open-source/kokoro-tts
- kokoro-onnx pull request 197, timestamps from a duration output: https://github.com/thewh1teagle/kokoro-onnx/pull/197
- rten-examples source list (kokoro.rs, piper.rs, whisper.rs, trocr.rs): https://github.com/robertknight/rten/tree/main/rten-examples/src
- supertone-inc/supertonic repository: https://github.com/supertone-inc/supertonic
- Supertonic 3 local TTS notes (real-time factor 0.200 at 16 threads): https://localclaw.io/tts/supertonic-3
- sherpa-onnx Supertonic int8 export: https://huggingface.co/csukuangfj2/sherpa-onnx-supertonic-tts-int8-2026-03-06
- KittenML/KittenTTS repository: https://github.com/KittenML/KittenTTS
- kitten_tts_rs (ort, espeak-ng, real-time factors): https://github.com/second-state/kitten_tts_rs
- Matcha-TTS repository: https://github.com/shivammehta25/Matcha-TTS
- MeloTTS repository: https://github.com/myshell-ai/MeloTTS
- kyutai-labs/pocket-tts repository: https://github.com/kyutai-labs/pocket-tts
- k2-fsa/ZipVoice repository: https://github.com/k2-fsa/ZipVoice
- resemble-ai/chatterbox repository: https://github.com/resemble-ai/chatterbox
- Open-source TTS comparison (Orpheus, Dia, Zonos sizes and licences): https://pinggy.io/blog/best_open_source_self_hosted_text_to_speech_models/
- edwko/OuteTTS repository: https://github.com/edwko/OuteTTS
- Open-weight TTS comparison (Kokoro ONNX sizes, XTTS, F5, Fish, Parler licences): https://builderai.tools/blog/open-weight-text-to-speech-models-2026
- NeuTTS Air project page: https://github.com/goodtab/neutts-air
- Piper "Insert Pause" discussion (no SSML,
--sentence-silence): https://github.com/rhasspy/piper/discussions/199 - k2-fsa/sherpa-onnx repository: https://github.com/k2-fsa/sherpa-onnx
- RTen changelog: https://raw.githubusercontent.com/robertknight/rten/main/CHANGELOG.md
- RTen README (SIMD, threads, quantization): https://github.com/robertknight/rten
- rodio default buffer size of 100 ms (
src/stream.rs): https://raw.githubusercontent.com/RustAudio/rodio/master/src/stream.rs - ort on crates.io: https://crates.io/crates/ort
- ort releases (ONNX Runtime 1.28): https://github.com/pykeio/ort/releases
- Arch Linux onnxruntime-cpu package size: https://www.archlinux.de/packages/x86_64/onnxruntime-cpu
- tract issue 2928, dynamic Range in a VITS graph: https://github.com/sonos/tract/issues/2928
- sonos/tract repository: https://github.com/sonos/tract
- Rust inference runtimes index (wonnx archived, burn-onnx coverage): https://arewelearningyet.com/inference/
- Lele announcement, compile ONNX to Rust: https://users.rust-lang.org/t/lele-bare-metal-ml-inference-engine-in-pure-rust-compile-onnx-into-rust/138195
- espeak-ng-rs, pure-Rust eSpeak NG: https://github.com/eugenehp/espeak-ng-rs
- rodio UPGRADE notes (DeviceSinkBuilder, buffer sizes): https://github.com/RustAudio/rodio/blob/master/UPGRADE.md
- rubato README (resampler types, SIMD, sinc length): https://github.com/HEnquist/rubato
- eSpeak NG SSML support: https://raw.githubusercontent.com/espeak-ng/espeak-ng/master/docs/markup.md
- DECtalk LICENCE (FONIX): https://raw.githubusercontent.com/dectalk/dectalk/develop/LICENCE
- RHVoice repository: https://github.com/RHVoice/RHVoice
- Flite COPYING: https://raw.githubusercontent.com/festvox/flite/master/COPYING
- SVOX Pico TTS: https://github.com/naggety/picotts
- Kokoro prosody from punctuation only: https://deapi.ai/blog/kokoro-tts-guide-how-to-control-41-voices-with-nothing-but-punctuation
- openai/whisper model table: https://github.com/openai/whisper
- Whisper CPU benchmarks on Apple Silicon: https://justvoice.ai/blog/whisper-benchmark-apple-silicon-m3-m4
- sherpa-onnx Whisper turbo int8 with cross-attention output: https://huggingface.co/cohearo/sherpa-onnx-whisper-large-v3-turbo-attention-int8
- Whisper and distil-whisper on CPU: https://www.simplismart.ai/blog/deploy-whisper-v3-turbo-using-vox-box
- moonshine-ai/moonshine repository: https://github.com/moonshine-ai/moonshine
- Local STT models 2026 (Moonshine and Parakeet CPU figures): https://openvoxai.com/blog/best-local-stt-transcription-models-2026
- Kyutai delayed-streams-modeling (STT): https://github.com/kyutai-labs/delayed-streams-modeling
- robertknight/ocrs repository: https://github.com/robertknight/ocrs
- kreuzberg-paddle-ocr (PP-OCRv5 ONNX sizes): https://lib.rs/crates/kreuzberg-paddle-ocr
- PaddlePaddle/PaddleOCR repository: https://github.com/PaddlePaddle/PaddleOCR
- TrOCR model sizes: https://github.com/microsoft/unilm/tree/master/trocr