Wyoming protocol server for
BlueTTS (the blue-onnx ONNX
inference package), for use as a Home Assistant
text-to-speech provider.
Inspired by and partially based on wyoming_pocket_tts, which this project follows for its overall structure (Python package + Home Assistant app packaging, event handler design).
- Multilingual: English, Spanish, German, Italian, Hebrew. Voices are language-independent style embeddings, so any voice works with any language.
- Incremental audio streaming: long replies are split into sentence/paragraph
chunks and streamed to the client as each chunk finishes synthesizing,
rather than waiting for the entire reply (
supports_synthesize_streaming). Home Assistant plays audio as it arrives instead of after the whole clip is ready. - Zero-shot voice cloning from a short reference
.wavclip (see Voices below) — not available in the default (Alpine) image; use the:latest-cloningimage tag instead if you need it (see Docker). - CPU only: ONNX Runtime, no PyTorch dependency, no GPU required.
- Models download automatically on first start.
- Ships both as a Python package (installable with
uv) and a Home Assistant app.
Settings → Apps → Install app → ⋮ (three dots) → Repositories → Add this
repository's URL, then install "Wyoming BlueTTS" from the store. It
auto-discovers into Home Assistant via the Wyoming protocol. Supervisor pulls
the same pre-built, Alpine-based image published to ghcr.io (see
Docker below) rather than building locally, so installs and
updates are fast — but this means the app does not support zero-shot
voice cloning (see Features). If you need cloning, install
standalone via Docker instead, using the :latest-cloning image tag.
git clone https://github.com/snabb/wyoming_bluetts.git
uv tool install ./wyoming_bluetts
wyoming-bluetts --voices female1 --debugMust be installed with uv, not plain pip. This project pins its
blue-onnx dependency to a specific git commit via [tool.uv.sources]
(see AGENTS.md), because the published PyPI blue-onnx wheel is
missing required files as of this writing. uv honors that pin when building
from this project's pyproject.toml; plain pip has no equivalent mechanism
and would silently install the broken PyPI blue-onnx instead.
docker run --rm -p 10200:10200 \
-v ./models:/data/models \
-v ./voices:/share/tts-voices \
ghcr.io/snabb/wyoming_bluetts:latestSee docker-compose.yml for a persistent deployment example, including a comment showing how to bind to a specific interface (e.g. a WireGuard IP) instead of all interfaces, if Home Assistant reaches this host over a VPN. Add it in Home Assistant via Settings → Devices & Services → Add integration → "Wyoming Protocol".
The published image (and the Home Assistant app, which builds from the same
Dockerfile) is Alpine-based — smaller than a typical glibc image, but it
can't support zero-shot .wav voice cloning: that feature needs a
librosa/numba/llvmlite/scipy/scikit-learn dependency chain, and
numba/llvmlite don't build on musl (see AGENTS.md).
Precomputed style JSON custom voices work in every build regardless.
If you need cloning, pull the pre-built variant instead (glibc-based, cloning on by default):
docker pull ghcr.io/snabb/wyoming_bluetts:latest-cloningor build Dockerfile.cloning yourself:
docker build -f Dockerfile.cloning -t wyoming-bluetts:cloning .| Flag | Default | Description |
|---|---|---|
--host |
0.0.0.0 |
Host to bind to |
--port |
10200 |
Port to bind to |
--voices |
(empty) | Comma-separated voices to preload + advertise; empty = advertise all, load on demand |
--voices-dir |
/share/tts-voices |
Folder for custom voice style JSON / wav samples |
--models-dir |
/data/models |
Folder for the auto-downloaded ONNX model bundle |
--languages |
en,es,de,it |
Comma-separated languages to advertise |
--default-language |
en |
Language used when a request doesn't resolve one |
--total-step |
5 |
Flow-matching diffusion steps (quality/speed tradeoff) |
--cfg-scale |
4.0 |
Classifier-free guidance scale |
--speed |
1.0 |
Speech speed multiplier |
--speak-decimal-points / --no-speak-decimal-points |
enabled | Rewrite decimals like 3.5 to 3 point 5 before synthesis (en/es/de/it only; see Limitations) |
--debug |
off | Verbose logging |
Two built-in presets ship with this package: female1, male1 (vendored
from BlueTTS's voices/).
Custom voices go in --voices-dir (default /share/tts-voices):
- A precomputed style JSON (e.g. exported with BlueTTS's own tooling) — works today.
- A clean, 5-15 second mono reference
.wavclip, cloned automatically on first use via zero-shot voice conversion, then cached to<voices-dir>/.bluetts_cache/<name>.jsonso cloning only runs once. Only available via the:latest-cloningimage tag or a build fromDockerfile.cloning(see Docker) — the default published (Alpine) image and the Home Assistant app can't support it at all, and log a clear warning and fall back to the default voice if a.wav-only voice is requested.
To use any custom voice (style JSON or .wav) from Home Assistant, pass its
filename (without extension) as the voice option — it does not need to be
in --voices/the app's "Voices to load" list, which only controls what's
preloaded at startup and advertised in the UI's voice picker. For example, a
tts.speak service call:
action: tts.speak
data:
entity_id: tts.bluetts_remote
media_player_entity_id: media_player.living_room
message: Hello there
options:
voice: my_custom_voiceen, es, de, it by default. he (Hebrew) is also supported but not
enabled by default — it needs an extra ~20 MB grapheme-to-phoneme model
(renikud); add it via --languages en,es,de,it,he (or your own subset) to
enable it, and it'll download automatically alongside the main model bundle.
If that download fails, Hebrew is dropped from the advertised languages and
the rest still work.
- BlueTTS's underlying
tts()call synthesizes one sentence/paragraph chunk at a time, not frame-by-frame — so streaming here is sentence-level, not as fine-grained as engines with a native audio-frame streaming API. Still, playback of a multi-sentence reply starts after the first chunk rather than waiting for the whole thing. - CPU only;
use_gpu=Trueis not implemented upstream yet. - espeak's number reading (used for en/es/de/it) expands a decimal like
3.5into the words for3and5but leaves the literal.between them instead of converting it to a word like "point" — it plays as a silent pause, easily mistaken for a sentence break.--speak-decimal-points(on by default) rewrites decimals before synthesis to avoid this; disable it with--no-speak-decimal-pointsif you'd rather hear the raw espeak behavior. Hebrew is unaffected either way: it doesn't use espeak's number reading at all.
uv sync --all-extras --dev
prek run --all-files
uv run -m pytestSee CONTRIBUTING.md and AGENTS.md.
Structure and packaging approach follow wyoming_pocket_tts by araa47 (MIT), the template this project was built from.
MIT. Uses BlueTTS (MIT) and the Wyoming protocol (MIT).