arizuko › components › ttsd
ttsd
ttsd accepts one OpenAI-style speech request and forwards it to the text-to-speech engine that you configure.
ttsd gives every TTS engine one request format
ttsd translates for text-to-speech engines: text goes in and audio comes out, always in one request format. If you replace the engine behind it, nothing else changes.
ttsd is a thin reverse proxy in front of a text-to-speech backend. It accepts POST /v1/audio/speech in the request format of OpenAI, {model, voice, input, response_format}. It forwards the request unchanged to the configured backend and streams the audio bytes back to the caller.
It also serves GET /v1/voices, which passes through to backends that list voices, and GET /health, which returns 503 when the backend is unreachable.
ttsd keeps the backend choice out of routd
arizuko speaks one TTS protocol: the protocol of OpenAI. ttsd fixes that contract at the daemon boundary, so routd and the send_voice MCP tool never see which backend runs. The default is the bundled Kokoro-FastAPI container. Change TTS_BACKEND_URL to Piper, Coqui, the OpenAI cloud or any other server that speaks /v1/audio/speech, and no caller code changes.
ttsd also gives one health signal. The readiness probe of Kokoro and the 401 of OpenAI on an unauthenticated HEAD would each need their own check. ttsd hides that and reports one {status:"ok"|"disconnected"} shape, the same as every other arizuko adapter.
Each request makes one call to the backend
ttsd runs an httputil.NewSingleHostReverseProxy against TTS_BACKEND_URL:
- It receives
POST /v1/audio/speechwith the OpenAI body. - The director copies
URL.Pathunchanged onto the backend URL. - It streams the response, audio bytes of any
Content-Type, back to the caller. - On a dial error, it returns
502 tts backend unreachable.
The /health handler checks the /health of the backend with a 3-second timeout. For a backend without one, it sends HEAD /. routd does not read this health, because TTS_ENABLED is fixed at boot, so it is for you and your monitoring. When the backend is down, send_voice fails with a synthesis error, and the agent sends text instead.
ttsd sits between routd and the engine
agent
│ send_voice(chat_jid, text, voice)
▼
routd ──► POST TTS_BASE_URL/v1/audio/speech
│
▼
ttsd ──► POST TTS_BACKEND_URL/v1/audio/speech
│
▼
kokoro / piper / openai cloud
│
▼ audio bytes (ogg, mp3, …)
routd caches at <data_dir>/tts/<hash>.ogg, hands to adapter
- Inputs
- HTTP from routd, or from any other caller that speaks the OpenAI TTS format.
- Outputs
- Audio bytes from the backend.
- Dependencies
- A reachable backend at
TTS_BACKEND_URL.
concepts/voice covers the full voice flow in both directions: transcription, voice selection, the cache and delivery on each platform.
ttsd runs alone
ttsd runs alone. It has no database, no auth and no admin page: it is a reverse proxy configured by environment variables. Put proxyd or another auth layer in front of it when you expose it.
# bundled Kokoro backend
docker run -d --name kokoro -p 8881:8880 \
ghcr.io/remsky/kokoro-fastapi-cpu:latest
docker run -d --name ttsd -p 8880:8880 \
-e TTS_BACKEND_URL=http://localhost:8881 \
arizuko-ttsd:latest
# probe
curl -s localhost:8880/health
curl -s -X POST localhost:8880/v1/audio/speech \
-H 'content-type: application/json' \
-d '{"model":"kokoro","voice":"af_sky","input":"hello","response_format":"mp3"}' \
--output hello.mp3
Three environment variables configure ttsd
TTSD_ADDR- The listen address,
:8880by default. TTS_BACKEND_URL- The TTS backend compatible with OpenAI,
http://kokoro:8880by default. LOG_LEVEL- One of
debug,info,warnorerror.
reference/env has the full list and the defaults.
Three sources cover the details
- concepts/voice: transcription, voice selection, the cache, delivery on each platform, and what happens when ttsd is down.
- [ttsd/README.md]: the endpoint contract and the backend options.
- concepts/routing: routd, the caller that hashes, caches and sends the audio.