TTS synthesis service
Responsibilities and startup
The TTS server is a Flask HTTP service that synthesizes audio and timing metadata. ROS speech orchestration is a separate layer: it calls synthesis, schedules visemes and gestures, and selects where audio plays. Calling synthesis directly does not itself publish a robot speech command.
The current supporting source branch is sdk3. Use its actual server.py contract; older examples in the repository README do not describe this handler's URL shape.
TTS_CONFIG_FILE must name an existing YAML file at import/startup; otherwise startup raises RuntimeError("TTS config file is not found"). That file supplies viseme mapping configuration. --voice_paths loads Python modules containing a voices mapping. A vendor may map directly to a TTSBase implementation or to a dictionary keyed by voice. A running server with no loaded voices can answer ping but cannot synthesize.
| Setting | Default or meaning |
|---|---|
--host |
localhost; container entrypoints can override it |
--port |
10001; current Python 3 service deployment may override to 10002 |
--tts-output-dir |
$HR_DIR/ttsserver, with HR_DIR defaulting to ~/.hr |
--voice_paths |
One or more plugin directories; required operational input for voice discovery |
TTS_CONFIG_FILE |
Required viseme/configuration YAML path |
HR_SINGING_DIR |
Optional singing assets used during setup |
Provider credentials and licenses are inputs to installed voice plugins. Do not infer provider availability from the built image alone. Keep voice files and licenses outside the documentation site.
GET /ping
No parameters. Returns JSON with a stable application-level success code:
{"response":{"code":0,"message":"pong"}}
export TTS_URL='http://TTS_HOST:TTS_PORT'
curl --fail-with-body "$TTS_URL/ping"
The handler adds Access-Control-Allow-Origin: *. This is a liveness probe, not a test of voices, provider credentials, synthesis or downstream playback.
GET /
The vendor is a path segment, not a query parameter. Supply voice and text as query parameters. format defaults to wav; provider implementations determine supported behavior. Other query values are forwarded to the voice implementation, except text, vmap, and vparam, which are removed from the forwarded parameter dictionary. voice and format remain in that dictionary.
curl --fail-with-body --get "$TTS_URL/azure" \
--data-urlencode 'voice=CONFIGURED_VOICE' \
--data-urlencode 'text=Hello from the robot.' \
--data-urlencode 'format=wav'
The example selects a provider only if its plugin and voice are installed. It is not a claim that a particular cloud voice is provisioned.
Response field inside response |
Meaning |
|---|---|
error |
Empty on normal success; application failure message otherwise |
phonemes |
Provider phoneme timing data |
visemes |
Mapped visemes when a mapping exists |
markers, words, nodes |
Provider timing, text markers, and structured synthesis nodes |
text, id |
Synthesis text and identifier |
format |
Requested format value |
duration |
Duration reported by synthesis data |
data |
Base64-encoded bytes read from the generated output file; may be empty |
The top-level response is {"response": {...}}, not raw WAV bytes. The handler deletes the temporary output file after reading it. A valid synthesis response can lack visemes when a vendor mapping is unavailable. Check error and audio content rather than relying solely on HTTP 200.
Decode a previously saved response locally:
import base64
import json
from pathlib import Path
payload = json.loads(Path("tts-response.json").read_text())["response"]
if payload.get("error"):
raise RuntimeError(payload["error"])
Path("speech.wav").write_bytes(base64.b64decode(payload["data"], validate=True))
Error and side-effect behavior
- Unknown vendor/voice:
response.errorcontainsCan't get api <vendor>:<voice>. - Provider exception: the synthesis call is caught and its message is returned through
error. - Empty provider result:
No TTS dataunless a more specific error exists. - Missing text or voice is not validated by a typed request model; failures depend on lookup and provider behavior.
- Some later file/marker errors are raised outside the synthesis exception handler and can produce HTTP 500.
- A text marker naming an audio file can replace the generated output with an existing server-side file. Treat this interface as privileged; do not expose it publicly just because the documentation is public.
- Synthesis can incur provider cost and create/delete files. No idempotency-key or server cancellation endpoint is implemented in these two handlers. A timed-out client cannot assume synthesis was canceled.
Visemes and troubleshooting
The source mapping includes Azure, CereProc, Acapela, Polly, ElevenLabs variants and singing. Different ElevenLabs modes use different phoneme-to-viseme mapping families. Verify that the selected plugin and mapping names agree.
For failures, check startup config first, loaded voice modules second, then provider credentials/network/license state. If ping succeeds but speech fails, inspect application error and server logs. If audio succeeds but the mouth does not move, inspect the ROS TTS/animation pipeline rather than retrying synthesis blindly.
Evidence: ttsserver/ttsserver/server.py, ttsbase.py, and head-ros-nodes/docker-compose.ros2.yml. Route declarations preserve exact inspected handlers. No live provider synthesis was performed for this guide.