Telnyx ships over 700 Ultra voices across 36 languages with sub-100ms time-to-first-byte. The voices are not the problem. Hearing them is.
The docs list three. The Voices API returns 4,000+ across every provider. Voice pickers play a fixed sample sentence per voice. None of that tells you how a voice handles emotion, pacing, or character inside a real scene.
So I built a small app that lets you do exactly that. You write a short scene with a few characters, assign each character a different Telnyx Ultra voice and an SSML emotion, and render the whole thing into one MP3. Every voice speaks in character, in context, in one continuous audio file.
The Telnyx code example is:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/multi-character-narrator-python
The Use Case
Voice pickers exist. They play a fixed sample sentence per voice. What they do not do is let you hear a voice inside a real scene — a tense argument, a calm narrator, a panicked character, a reassuring guide — because a single sample sentence does not tell you how a voice handles emotion, pacing, or character.
This example solves that. You write a short scene with a few characters. Each character gets a different Telnyx Ultra voice. Each character gets an SSML emotion. You hit render. The app fans out parallel TTS calls, stitches the per-line audio in script order, and plays you one continuous MP3 with every voice speaking in character.
The default scene is the Ides of March from Julius Caesar. Five characters, ten lines, five distinct voices, five different emotions:
- Cassius — determined, plotting the assassination
- Caesar — surprised, realizing the betrayal
- Brutus — apologetic, justifying the act
- Mark Antony — angry, mourning the fallen leader
- Narrator — calm, setting the scene
One render, one MP3, every voice in context. That is the demo.
The Eight Curated Ultra Voices
The app ships with eight pre-built Telnyx Ultra voices curated for the most common use cases. Each one is a real Telnyx voice with a UUID voice ID that works on the REST endpoint.
| Voice | Gender | Language | Best Use Case | Sound Profile |
|---|---|---|---|---|
| Asher | Male | en | Voice Assistants & Media | Smooth, dynamic, podcaster-style tone |
| Callie | Female | en | Coaching & Onboarding | High energy, encouraging, friendly tone |
| Clara | Female | en-US | General Purpose IVR/AI | Clear, standard US accent, versatile pacing |
| Howard | Male | en-US | Conversational Agents | Deep, reassuring, highly trustworthy |
| Allie | Female | en-US | Casual & Interactive AI | Conversational flow, natural pauses |
| Jasper | Male | en-GB | Finance & Healthcare | Calm, authoritative, precise delivery |
| Skyler | Neutral | en | Modern Brand Voice | Casual, tech-forward, friendly vibe |
| Arvin | Male | en | Navigation & Directives | Steady, clear cadence for detailed guidance |
Pick any of the eight for any character. Click Preview to hear the voice with the selected emotion before rendering the whole scene.
Twenty Ultra SSML Emotions
Ultra supports inline SSML emotion tags placed before the text:
<emotion value="excited" />Great news — your order shipped early!
The app exposes all twenty Ultra SSML emotions as a per-character dropdown. Primary emotions: angry, excited, content, sad, scared. Additional: happy, enthusiastic, curious, calm, grateful, affectionate, sarcastic, surprised, confident, hesitant, apologetic, determined, frustrated, disappointed.
Each character in the default Julius Caesar scene is auto-assigned an emotion that fits the role. Cassius is determined. Caesar is surprised. Brutus is apologetic. Mark Antony is angry. The Narrator is calm. Same voice, different emotion, different delivery — all from one inline SSML tag per line.
How It Was Made
The app is a single Flask file with an inline browser UI. No phone number, no webhook, no Cloud Storage, no database. One env var: TELNYX_API_KEY.
The pipeline
POST /narrate (script with speaker labels)
-> parse script into ordered lines
-> map speaker -> voice (8 curated Ultra voices, overridable)
-> map speaker -> emotion (20 Ultra SSML emotions, overridable)
-> parallel fan-out: one REST TTS call per line
POST /v2/text-to-speech/speech
text_type=ssml, output_type=binary_output
<emotion value="..." /> wrapping when emotion set
-> stitch per-line MP3 bytes in script order
-> store in memory (1h TTL)
-> return project_id + per_line_ttfb_ms + audio_url
-> GET /audio/<project_id>.mp3 streams the stitched MP3
Why REST, not WebSocket
Ultra is REST-only on the public WebSocket. A 403 on wss://api.telnyx.com/v2/text-to-speech/speech is intentional. The app uses POST /v2/text-to-speech/speech with output_type: binary_output so it can measure true time-to-first-byte per line. Base64 mode would hide the real latency.
Why UUIDs, not display names
Ultra voice IDs are UUIDs in the Telnyx.Ultra.<uuid> format, not short display names like Telnyx.Ultra.Clara. Short names return 400 on the REST endpoint. The Voices API at GET /v2/text-to-speech/voices returns all available voices with their UUIDs — over 700 Ultra voices alone, filterable by provider == "telnyx" and id | startswith("Telnyx.Ultra.").
The app ships with eight curated UUIDs so the demo works out of the box, but the dropdown is easy to extend with any voice from the Voices API.
Parallel fan-out with per-line error isolation
The app uses ThreadPoolExecutor to render every line in parallel. If one line fails (e.g. an invalid voice override), the response includes an errors array and the stitched audio contains only the successful lines in script order. A failed line does not lose the whole render.
The browser UI
The UI auto-detects speakers as you type. Each speaker gets a voice dropdown, an emotion dropdown, and a Preview button that renders a short sample line in the selected voice with the selected emotion. The render button fans out the parallel TTS calls, stitches the result, and autoplays the MP3.
Run It
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/multi-character-narrator-python
cp .env.example .env # fill in TELNYX_API_KEY
pip install -r requirements.txt
python app.py # starts on http://127.0.0.1:5050
Open the browser UI. The default Julius Caesar script is pre-loaded. Pick voices, pick emotions, preview, render, play.
Where This Goes Next
The app is a starting point. Add more voices from the 700+ Ultra voices available via the Voices API. Add more languages — Ultra covers 36, and the same script-render-stitch pipeline works for any of them via language_boost. Add Cloud Storage for persistent, shareable audio URLs. Add more sample scripts — audiobook chapters, podcast intros, e-learning role-plays, game cinematics — any multi-speaker content where you want to hear voices in context.
The point is the same: hear Telnyx voices in a real scene, not a sample sentence. Everything else follows from that.
Top comments (0)