Part 2

TTS: voice & model selection

The bot's voice identity (TTS panel, top section): which engine speaks, which model, which voice, and the one rule that overrides them all — on-prem pins everything to FreyaTTS regardless of what the dashboard says.

Step 5

Provider live

What it is. Which TTS engine synthesizes speech: ElevenLabs, FreyaTTS (self-hosted), Cartesia, OpenAI, or a custom OpenAI-compatible endpoint. This is the first branch in the whole TTS build.

ttsConfig.provider range string, min 1 / max 50 default Cartesia (cloud) · Freya (on-prem)

Runtime. base_service.py:833 reads and lowercases the value, then branches at :864,873,885,897,909. When settings.server.is_on_premise is true (base_service.py:864), it always builds FreyaTTSService — the dashboard provider field is ignored on-prem.

Symptom it fixes: "the voice sounds robotic / wrong accent" — usually a provider+voice mismatch, often a test agent silently falling back to ElevenLabs because ENVIRONMENT wasn't set to on-premise.

Step 6

Model live

What it is. The specific model within the provider (e.g. ElevenLabs eleven_flash_v2_5, Cartesia sonic-2).

ttsConfig.model range string | null, max 100 default sonic-2 (cloud) · freya-tts (on-prem)

Runtime. For the Freya path the model name is effectively hardcoded to freya-tts (base_service.py:911); for custom providers it is passed through as free text.

Symptom it fixes: "responses are slow" can be partly a heavyweight TTS model; flash/turbo variants shave hundreds of ms.

Step 7

Voice (picker vs custom voice ID) live

What it is. The actual voice. Either pick from the provider's gallery or paste a custom voice ID. This is the only TTS field marked required.

ttsConfig.voiceId range string, min 1 / max 100, required default a Cartesia UUID (cloud) · leyla (on-prem)

Runtime. base_service.py:838,868,901. The cloud Freya branch maps specific "magic" UUIDs to either FreyaTTSService(voice="leyla") or named ElevenLabs voices (voice map at base_service.py:922-978); unknown IDs fall back to a default ElevenLabs voice. On-prem (:864) builds FreyaTTSService(voice=voiceId or TTS_VOICE_ID), so the UUID mapping is bypassed entirely and your voiceId is taken as a literal Freya voice name.

Symptom it fixes: "we want a female/male voice" / "this isn't the voice we approved" — set voiceId to the agreed voice.

Try it — voice picker resolution

Pick a voice and see how the runtime resolves it. The same selection resolves differently depending on whether the agent is running cloud or on-prem — that nuance is what trips up test agents.

voiceId → resolved voice
Pick a voice.

Reminder: on-prem forces FreyaTTSService regardless of provider/voiceId (base_service.py:864) — the ElevenLabs fallback can only ever happen on the cloud branch (:909).

Ask Claude Code: "Show me every voiceId UUID that maps to a named voice in base_service.py:922-978 and what each one resolves to (FreyaTTS leyla vs which ElevenLabs voice), then tell me what an unrecognized UUID falls back to."
Step 8

Language live

What it is. The spoken language for TTS, or multi for multilingual.

ttsConfig.language range language code or multi default validator: multi · seeded base config: en

Runtime. base_service.py:834-835,860. Drives whether a LanguageTextFilter is added (it is skipped when multi). FreyaTTS disallows multi (enforced in the UI, tts-config-panel.tsx:842). Note the default mismatch: the validator defaults to multi while the seeded base config uses en (see Part 10).

Symptom it fixes: "it reads phone numbers as one giant number" — you can't even enable the TC/phone normalizers until language is tr.

Try it — language gate preview

Language is more than a voice setting: it gates two downstream behaviors. Switch the code and watch what unlocks or breaks.

language → downstream effects
Pick a language.
Step 9

Custom base URL & API key live

What it is. For provider = custom, the OpenAI-compatible TTS endpoint and its key.

ttsConfig.baseUrl ttsConfig.apiKey range baseUrl string|null max 2048 · apiKey string|null max 500 default both null

Runtime. Routed through the OpenAI-TTS path (base_service.py:897, OpenAITTSService); the key is resolved via the encrypted _get_api_key helper.

Symptom it fixes: niche; usually a partner who insists on their own voice stack.

Advanced provider settings tuning

ttsConfig.additionalSettings — speed / stability / similarityBoost These tune delivery and ship as {speed:1, stability:0.5, similarity_boost:0.75} (ElevenLabs InputParams at base_service.py:938-940). Raise speed slightly for impatient callers; raise stability if the voice wavers.
ttsConfig.additionalSettings.speed default 1 ttsConfig.additionalSettings.stability default 0.5 ttsConfig.additionalSettings.similarity_boost default 0.75

Try it — on-prem override explainer

This is the single most important rule in Part 2: the runtime provider branch is decided by ENVIRONMENT / is_on_premise, not the dashboard. Toggle it and see what the dashboard fields actually do.

ENVIRONMENT branch
Ask Claude Code: "In base_service.py, trace create_tts_service from the provider read at :833 through the is_on_premise check at :864 and the cloud-freya fallback at :909, and tell me exactly when a dashboard voiceId gets ignored."

Checkpoint

A test agent keeps speaking in an ElevenLabs voice even though you set FreyaTTS Leyla. Why?

Because the runtime provider branch is decided by ENVIRONMENT / is_on_premise, not the dashboard field (base_service.py:864). If the agent isn't running with ENVIRONMENT=on-premise, the cloud branch runs and an unrecognized voiceId falls back to ElevenLabs (base_service.py:909 onward). Fix the env, not the dashboard.