Text-to-Speech (synaplan-tts)
Voice output in Synaplan is a companion service: synaplan-tts, a self-hosted Piper HTTP API. The published image is ghcr.io/metadist/synaplan-tts. It starts with the standard stack — make up brings spoken answers with no extra step.
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, French, 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 — with the stack (default)
The standard compose files start a tts service out of the box — no profile, no
extra step. The backend default SYNAPLAN_TTS_URL=http://tts:10200 points at that
service. Production (deploy/compose.yaml) ships the same service without a host
port; the development stack additionally publishes it on 127.0.0.1:10200 for
convenience. To hide spoken answers, stop the container:
docker compose stop tts
Verify the stack service from inside the backend container (works on both stacks):
docker compose exec -T backend php -r 'echo file_get_contents("http://tts:10200/health"), "\n";'
(On the development stack curl http://127.0.0.1:10200/health on the host works too.)
The minimal stack keeps TTS opt-in:
docker compose -f docker-compose-minimal.yml --profile tts up -d
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
A standalone container on the same laptop is reached as
http://host.docker.internal:10200 from inside the backend container.
Verify on the host that runs the standalone container:
curl http://127.0.0.1:10200/health
You should see "voices_loaded": 5.
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,fr,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-kerstin-low (kerstin) |
| Spanish | es_ES-davefx-medium (davefx) |
| French | fr_FR-siwis-medium (siwis) |
| 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 five 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.