9.3 KiB
Desktop CPU speech trial
Desktop Read aloud defaults to Silero v5_5_ru / Xenia for Russian when its local runtime is installed. Silero runs in a persistent CPU-only Python process and handles its own stress and ё restoration. English and mixed-script chunks use Supertonic 3 / F1 through ONNX Runtime's CPU provider. Mixed passages retain Supertonic's Russian pronunciation limitations. Plain Supertonic is retained as an audition baseline. Initial listening feedback found its Russian word stress unacceptable. An optional RUAccent CPU stage now adds stress and restores ё before synthesis; its +vowel notation is converted to a Cyrillic vowel followed by U+0301. Manual minimal-pair listening showed stress changes, but this is not a comprehensive pronunciation-quality evaluation. Android retains its existing system TTS implementation.
Run
From the repository root:
python3 scripts/download-tts-model.py
./gradlew :composeApp:run
The script downloads about 401 MB into the ignored .local/tts/supertonic-3
directory. Downloads are revision-pinned and verified against the committed file
manifest. It reuses valid files and replaces incomplete downloads only after hash
verification. No Python packages are needed. The model license is saved alongside
its weights.
Open a book and choose Read aloud from the reader menu. Playback highlights the current sentence, follows the reading position, and exposes Stop, previous/next sentence, and voice settings. Silero offers Xenia (default), Baya, Aidar, Kseniya and Eugene. The Supertonic engines offer F1–F5 and M1–M5. Engine and voice choices last for this application session; each launch starts with Xenia if installed.
The model loads lazily on first playback. One chunk is prepared ahead while audio
plays. Stop, skipping, and voice changes cancel the old playback and lookahead;
Supertonic inference checks cancellation between model stages/denoising steps;
cancelling Silero inference terminates its worker, which restarts on demand. Long
sentences are split at whitespace into at most 300-character chunks. English and
Russian are selected from their scripts; mixed-script chunks use the model's
language-agnostic na mode. This heuristic is not general language detection.
For a model installed elsewhere, set YEREED_TTS_MODEL_DIR (or JVM property
yereed.tts.modelDir) to the directory containing onnx/ and voice_styles/.
Without an override the app searches parent directories for the workspace model,
then falls back to ~/.toread/tts/supertonic-3. Packaged applications can use that
home-directory location. The application itself never downloads models or sends
text over the network. The first Gradle build and model setup require downloads.
Optional RUAccent stage (desktop trial)
python3 scripts/setup-ruaccent.py
./gradlew :composeApp:run
This creates a private Python 3.12 runtime under .local/ruaccent-runtime and
fetches about 726 MB of revision-pinned RUAccent assets into .local/tts/ruaccent.
It does not change system Python. The bootstrap supports Linux x86_64 directly;
on other systems install uv first. The dependency versions and model hashes
are recorded under scripts/. This desktop trial uses RUAccent 1.5.8.3,
turbo3.1, the full dictionary/rule pipeline, and CPU execution with four threads.
This is a desktop quality experiment, not an Android footprint recommendation.
After setup, restart the reader. Supertonic 3 + RUAccent becomes the default
engine; select Supertonic 3 in read-aloud settings for a direct comparison.
English bypasses the accentor. Already marked Russian chunks are preserved.
The accentor is a persistent local child process communicating over stdin/stdout;
Hugging Face and Transformers run in offline mode. Book contents are never sent
to a server. Set YEREED_RUACCENT_HOME to override its installation directory.
Stop/skip kills a pending accentor request when necessary; the next request
restarts the worker. Displayed book text and stored files are unchanged.
The model recognizes U+0301, and preprocessing preserves it. For example,
з+амок becomes за́мок, not a Latin accented-vowel substitution. Predicted
stress can still be wrong; model-token support does not guarantee compliance.
Audition / benchmark
./gradlew :composeApp:ttsSample
# Also play the four samples through the desktop audio device:
./gradlew :composeApp:ttsSample -PttsPlay=true
# Controlled manual stress pairs, same words/noise seed:
./gradlew :composeApp:ttsSample -PttsStress=true -PttsPlay=true
# Original plain Russian passage, then RUAccent + Supertonic:
./gradlew :composeApp:ttsSample -PttsRuAccent=true -PttsPlay=true
WAV files and their exact unaccented texts go to composeApp/build/tts-samples/.
The sample task uses the same inference and audio code as the reader without
opening or changing the library. It prints model-load time, synthesis time,
audio duration, and real-time factor (generation seconds / audio seconds).
Initial measurements on an Intel Core Ultra 9 275HX, four CPU threads, eight steps, speed 1.05: 1.075 seconds model load; English/Russian F1/M1 samples generated 11.6–15.3 seconds of audio in 2.4–3.3 seconds (RTF 0.20–0.24). These are short local samples, not phone/battery or long-book benchmarks. Synthesis completion and non-silent waveforms do not establish pronunciation quality.
Dependencies and provenance
- Supertonic source: the Kotlin inference code adapts the MIT Java example; attribution is in licenses/supertonic-code-MIT.txt.
- Supertonic 3 model:
revision
aafc6e32416a594460b32413efc49d7fe4ce6d46, OpenRAIL-M weights. Upstream development/support ended in September 2026. - ONNX Runtime 1.23.1, standard CPU artifact; no CUDA/GPU provider.
- JSON loading uses Jackson; PCM playback uses Java Sound.
Run ./gradlew :composeApp:jvmTest for text-boundary, PCM conversion, queue,
lookahead, cancellation, voice-change, and missing-model/error tests, together
with existing desktop/common tests. Model inference is exercised separately by
ttsSample, so ordinary tests do not download weights or use speakers.
The isolated virtual-display capture did not provide a usable UI image; rendered GUI verification remains incomplete. No further screen access is used.
RUAccent measurements on the same host: about 5.9 seconds cold load, 30–43 ms
for the two warm short passages, and about 2.1 GiB resident memory for the full
accentor worker alone. The combined passage took 2.98 seconds of TTS synthesis
for 16.42 seconds of audio, excluding accentor startup. In that passage RUAccent
incorrectly chose за́мок for the door lock; on a shorter explicit door-lock
sentence it chose замо́к. These are limited observations, not an accuracy score.
Silero Russian audition
User listening feedback rejected Supertonic's Russian quality. A separate CPU
audition uses official Silero v5_5_ru, with its own stress, ё, stress-homograph
and ё-homograph processing enabled. It does not use RUAccent. The same model is
now the preferred desktop Russian engine. The Russian model is CC BY-NC-SA 4.0, unlike Silero's MIT CIS
base models; see upstream.
Setup from the repository root (requires uv; our existing local copy is
.local/tools/uv):
export UV_CACHE_DIR="$PWD/.local/uv-cache"
export UV_PYTHON_INSTALL_DIR="$PWD/.local/python"
export UV_SYSTEM_CERTS=true
.local/tools/uv venv --python 3.12.14 .local/silero-runtime
.local/tools/uv pip sync --python .local/silero-runtime/bin/python scripts/silero-requirements.txt --index-url https://download.pytorch.org/whl/cpu
mkdir -p .local/tts/silero-v5_5-ru
curl -fL https://models.silero.ai/models/tts/ru/v5_5_ru.pt -o .local/tts/silero-v5_5-ru/model.pt.part
mv .local/tts/silero-v5_5-ru/model.pt.part .local/tts/silero-v5_5-ru/model.pt
.local/silero-runtime/bin/python scripts/silero-sample.py
python3 - <<'PY'
from pathlib import Path
import json
root = Path.cwd()
(root / '.local/tts/silero-v5_5-ru/runtime.json').write_text(json.dumps({
'python': str(root / '.local/silero-runtime/bin/python')
}) + '\n')
PY
# Restart the reader after setup, or check its actual Kotlin/Python bridge:
./gradlew :composeApp:ttsSample -PttsSilero=true
Outputs: composeApp/build/tts-samples/silero-*.wav, matching input .txt
files, and silero-benchmark.json with timings, settings and the downloaded
model's SHA-256. The script uses CPU-only PyTorch, four threads and 48 kHz PCM.
Default voices are Xenia, Baya and Aidar; --voices xenia limits the audition.
Each reads the earlier library passage and a separate unaccented ё/context
challenge. Pronunciation quality must be judged by listening.
The desktop integration discovers silero-v5_5-ru beside the Supertonic model
directory; YEREED_SILERO_HOME can override it. runtime.json points to the
isolated Python executable. The worker script is bundled in the application;
model loading and inference are local. Both models are needed for bilingual use.
Initial audition on the same desktop: model load 0.38 seconds; six samples
generated 13.2–15.5 seconds of audio in 0.36–0.82 seconds (RTF 0.027–0.059,
including the first synthesis). All outputs were finite and non-silent.
Downloaded model SHA-256:
50081637b602126ee06cb3bc8a744d25651d2da149ee8864b9a379bfdd934437.