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:

  1. The chat UI sends the active frontend locale (en, de, es, tr — Settings → language).
  2. If the backend detects a different reply language, that short code wins.
  3. PiperProvider maps 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).

  1. Download a pair from Piper Voices: <name>.onnx and <name>.onnx.json.
  2. Put both files in the extra-voices volume (standalone repo: ./voices/; Synaplan profile: the tts_extra_voices volume).
  3. Restart the TTS container.
  4. GET /api/voices lists 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). Never 0.0.0.0 on a public NIC.
  • Firewall port 10200/tcp so only Synaplan nodes can reach it.
  • Synaplan auto-enables voice output when /health returns ok — nothing to toggle in the app.
  • See Quickstart & Self-Hosting, Pitfall 3, for how this sits next to Qdrant and local models.