Text-to-Speech (synaplan-tts)
Voice output in Synaplan is an optional companion service: synaplan-tts, a self-hosted Piper HTTP API. The published image is ghcr.io/metadist/synaplan-tts.
Synaplan runs fully without it. The speaker control appears when the service answers. Speech input (Whisper) already ships with the main platform and does not need this container.
Bundled voices: English, German, Spanish, Turkish — baked into the image, no first-run download.
Why keep TTS independent?
Most installs never need spoken answers. Piper models are large (~60 MB each) and synthesis is CPU-heavy. A separate image means:
- The core Synaplan stack stays small. Chat, RAG, and widgets work without pulling voice models.
- You can run Piper on another machine (a LAN box, a GPU host) and point several Synaplan nodes at one speaker.
- Voice models update independently of the PHP/Vue app.
- Operators who want air-gapped / no-cloud speech keep audio on infrastructure they control, without sending text to ElevenLabs, Gemini, or similar.
If you prefer a cloud voice instead, set ELEVENLABS_API_KEY or pick a catalog TTS model (Gemini, Mistral Voxtral, xAI). Those stay inside the main app and do not use this service.
Install
Option A — same compose file (recommended)
The Synaplan compose files already define a tts profile. It is off by default:
cd synaplan
docker compose --profile tts up -d
Minimal stack:
docker compose -f docker-compose-minimal.yml --profile tts up -d
This pulls ghcr.io/metadist/synaplan-tts:latest and binds 127.0.0.1:10200. The backend default SYNAPLAN_TTS_URL=http://host.docker.internal:10200 finds it automatically.
Option B — standalone (this host or another)
docker run -d --name synaplan-tts \
-p 127.0.0.1:10200:10200 \
ghcr.io/metadist/synaplan-tts:latest
Or clone and compose:
git clone https://github.com/metadist/synaplan-tts.git
cd synaplan-tts
docker compose up -d
If TTS runs on another host, set the URL in backend/.env and restart backend + worker:
SYNAPLAN_TTS_URL=http://10.0.1.10:10200
Verify:
curl http://127.0.0.1:10200/health
You should see "voices_loaded": 4.
curl "http://127.0.0.1:10200/api/tts?text=Hallo+Welt&language=de" -o test_de.wav
Full API: synaplan-tts on GitHub.
Frontend language selects the voice
There is no separate voice picker. The spoken voice follows the conversation language:
- The chat UI sends the active frontend locale (
en,de,es,tr— Settings → language). - If the backend detects a different reply language, that short code wins.
PiperProvidermaps the code to a baked voice:
| UI / reply language | Voice |
|---|---|
| English | en_US-lessac-medium (lessac) |
| German | de_DE-thorsten-medium (thorsten) |
| Spanish | es_ES-davefx-medium (davefx) |
| Turkish | tr_TR-dfki-medium (dfki) |
An explicit voice= on /api/v1/tts/stream still overrides both. Unmapped languages fall back to the user's configured TEXT2SOUND model, then to English.
Adding more voices
The image must not be remounted over /voices — that hides the four built-in models. Extra voices go in EXTRA_VOICES_DIR (/voices-extra).
- Download a pair from Piper Voices:
<name>.onnxand<name>.onnx.json. - Put both files in the extra-voices volume (standalone repo:
./voices/; Synaplan profile: thetts_extra_voicesvolume). - Restart the TTS container.
GET /api/voiceslists every loaded model.
Helper in the synaplan-tts repo:
./download-voices.sh ru fa
docker compose restart piper
Russian and Persian are mapped in Synaplan already; they only speak after you add those files.
Notes for self-hosters
- Bind to a specific interface (default
127.0.0.1). Never0.0.0.0on a public NIC. - Firewall port
10200/tcpso only Synaplan nodes can reach it. - Synaplan auto-enables voice output when
/healthreturnsok— nothing to toggle in the app. - See Quickstart & Self-Hosting, Pitfall 3, for how this sits next to Qdrant and local models.