TTS and Voice Output¶
Tryll supports server-side text-to-speech synthesis. Sherpa-ONNX hosts
Supertonic 3 and Pocket TTS. llama.cpp runs Orpheus and Maya1, whose audio
codes a shared SNAC 24 kHz ONNX decoder turns into sound on the CPU.
llama.cpp also runs Qwen3-TTS (Qwen3-TTS 12Hz 1.7B Base (Q4_K_M)). It has
no built-in voice: it clones one from a reference clip, which is required, and
speaks 10 languages. See TTS Models for
which catalog names are public and which controls each family accepts.
Two graph nodes produce speech:
- GenerateAndSpeak fuses LLM inference with TTS synthesis, streaming PCM audio frames alongside the text reply — lowest latency for a generated spoken answer.
- Speak is TTS-only: it voices the slot named
by its
inputparam with no LLM pass. The default isuser_message, the player's own words, so setinputto an upstream node's output (e.g. a CannedResponse).
Both stream the same PCM audio frames to the client and use the same playback components. Neither node holds a voice: both speak with the agent's one voice profile. They differ only in whether generation is involved.
The streaming pipeline¶
When a GenerateAndSpeak node runs:
- Format announcement — the first speaking node of the turn sends one
TtsAudioFormatFrame: sample rate, channels (1) and bits per sample (16). It is sent once per turn: a later speaking node in the same turn reuses it, because the agent has one voice. - Token generation — the language model produces tokens, forwarded
to the client as
AnswerTextdeltas (identical toGenerate). - Sentence segmentation — tokens accumulate until a sentence ends
and the text since the last cut is at least
min_sentence_charswide (a CJK or other 3–4-byte character counts as 2):.!?or…followed by a space or by the end of the text so far. A.right after a digit waits for the next character, so "3.14" stays whole.- At once: CJK
。!?。, Devanagari। ॥, Arabic؟, Urdu۔, Ethiopic።and‼. Closing quotes and brackets right after them (」』)”…) stay with the sentence. - With
first_chunk_max_charsset, the first chunk may also end at,;:followed by a space or at an em dash, or at the next space once it is that wide — never before 5 words ormin_sentence_chars. This has no effect on text without spaces, such as CJK. - Text that runs 16 KiB with no sentence end is cut at the last space, so the buffer stays bounded; the server logs a warning.
- TTS synthesis — each chunk is synthesized to mono int16 PCM at the
model's native sample rate and streamed as
TtsAudioFramemessages. At most one chunk is synthesizing at a time: the next finished chunk waits for it, and the text stream pauses briefly while it waits.
This keeps time-to-first-audio low: the NPC starts speaking as soon as the first chunk is ready, while the language model is still generating the rest of the answer.
Client Server
│ │
├─ SendMessageRequest ──────────►│
│◄── TtsAudioFormatFrame ────────┤ (node starts)
│◄── AnswerText … ───────────────┤ (LLM tokens)
│◄── TtsAudioFrame … ────────────┤ (first sentence)
│◄── AnswerText … ───────────────┤ (LLM still running)
│◄── TtsAudioFrame … ────────────┤ (next sentences)
│◄── TurnComplete ───────────────┤ (after the last audio)
Audio format¶
All TTS audio is mono signed-16-bit PCM at the sample rate reported in
the TtsAudioFormatFrame frame. The sample rate is model-specific: for
example 44 100 Hz for Supertonic 3 and 24 000 Hz for Orpheus and Maya1.
Always read it from the format frame. The Unity speaker converts the int16
samples to float32; the Unreal speaker queues the int16 PCM directly.
End of stream¶
TtsAudioFrame carries no is_final flag. The last frame arrives before
TurnComplete. The audio queue on the client drains naturally — the
playback component fills the audio buffer with silence on underrun until
the queue is empty.
Session configuration¶
There is no TTS engine to declare. Download a voice (a TTS model) in the
Model Manager and name it in the agent's voice profile (tts_model_name). The catalog
chooses the engine for each voice — Sherpa-ONNX for Supertonic / Pocket,
llama.cpp for Orpheus / Maya1 / Qwen3-TTS. A session warm-up hint is optional; see
Choose where models run.
Voice configuration¶
An agent has one voice, a voice profile,
passed when the agent is created. Every speaking node of the agent uses it, so
one workflow can serve many characters with different voices. A graph that
speaks must be given a voice: without one, agent creation fails with
3025 AgentVoiceRequired. An agent whose graph never speaks loads no TTS model,
even if it has a voice.
The profile holds:
| Field | Description |
|---|---|
tts_model_name |
Catalog voice name. Required; there is no default model. |
speaker_id |
Built-in voice index, 0 to the model's speaker count − 1. Supertonic 3: 0–9. Orpheus: 0–7 (Tara … Zoe). Maya1: 0–1 (or override with voice_description). Single-speaker models (Pocket TTS) must stay 0. |
reference_voice |
Reference clip (storage-root-relative) for voice-cloning models (Pocket TTS, Qwen3-TTS Base). Empty = model default voice (Qwen3-TTS Base has none). See Clone a Voice. |
voice_description |
Maya1 only: natural-language identity (age, accent, pitch, timbre). |
speed |
Speech-rate multiplier, 0.1–3.0. Orpheus, Maya1 and Qwen3-TTS: only 1.0. |
lang |
Synthesis language on models that choose it per call (Supertonic 3: 31 languages; Qwen3-TTS: 10). Empty = the session locale, then the catalog default. Rejected on fixed-language models. |
emit_visemes |
Send a lip-sync viseme track for every sentence the agent speaks. |
delivery_modes |
Named deliveries (angry, calm, …). See below. |
The profile is validated once, when it is set. A field the loaded model cannot
use fails with 3023 InvalidVoiceProfile rather than being ignored.
The nodes keep only their chunking settings, because chunking acts differently on streamed LLM text than on ready text:
| Node parameter | Description |
|---|---|
min_sentence_chars |
Minimum width of a chunk before a sentence end closes it (a CJK or other 3–4-byte character counts as 2). Default 12, two or three English words. Lower = shorter first-audio latency. |
first_chunk_max_chars |
First chunk only: also end it at a clause mark, and at the next space once it is this wide (never under 5 words or min_sentence_chars). 0 = off. 48 cut the model-side wait before the first audio by about 40% in our measurements. Listen to it with your voice first: a clause can end on a falling tone. |
Both are mutable with ChangeAgentParam (see
Change Agent Parameters at Runtime).
The voice itself is not: replace it with SetAgentVoice, which is accepted only
while the agent is idle.
Delivery modes¶
A delivery mode is a named way of speaking that a voice defines, such as
angry or whisper. Game code switches it with
SetAgentDeliveryMode(mode, intensity) at any time, even mid-turn; the change
applies from the next sentence. The built-in mode neutral is the plain voice.
What a mode can change depends on the model. Clone models switch to a different
recording of the same character. Maya1 adds a delivery clause such as "angry
tone at high intensity". Supertonic 3 and Orpheus have no emotion control, so a
mode can only change speed (Supertonic) or must be empty. A profile can still
define the same mode names on every voice, so game code that says angry works
for every character. See
Voice Profile for the
per-model table and what intensity does.
Do not encode identity or delivery as a spoken [emotion] prefix — that syntax
is not supported. Model-native <tag> tokens (for example Maya <laugh>) may
appear in the text the synthesizer hears; unknown tags are stripped from the
audio copy only and do not rewrite history.
If synthesis fails during a turn, the client gets an ErrorResponse with
4001 ("TTS synthesis failed") and the turn still completes; the text
reply is not affected.
Lip sync (visemes)¶
An agent whose voice has emit_visemes on also sends a viseme track from
every speaking node, Speak and GenerateAndSpeak alike: the mouth shapes that go with the speech, and when each one starts.
It uses the 15 standard Oculus visemes (sil for rest, PP for p/b/m, aa
for an open vowel, …), which map onto a face rig's blendshapes.
The server builds the track for each sentence as soon as that sentence is synthesized, from two inputs:
- the text — the words are turned into speech sounds with an English pronunciation dictionary, and each sound into a mouth shape;
- the synthesized audio — silences become rests (anchored to the sentence's punctuation where possible), the shapes are spread over the spoken stretches in proportion to typical sound lengths, and louder speech opens the mouth further.
The track also keeps lip closures on screen long enough to read, merges
shapes too short to see, and starts each shape slightly before its sound, as
real lips do. It arrives as one TtsVisemeFrame per sentence, after that
sentence's audio once its synthesis is done, so under normal buffering it
reaches the client before that audio plays. A sentence that produced no
audio sends no frame. Offsets are in samples from the start of the turn's
audio (its one format announcement), and keep counting across every speaking
node of the turn.
No extra model runs. What to expect:
- Timing is estimated, not measured per sound. Pauses, starts and ends follow the real audio closely; the shapes inside a stretch of speech are placed by proportion.
- English pronunciation. Text in other Latin-script languages gets approximate shapes from English spelling rules. Text in other scripts (Cyrillic, CJK, …) gets no shapes: the mouth stays at rest.
- Voice-agnostic. The track works with every TTS voice, since it only needs the text and the audio.
See Add Lip Sync to a Character for setup.
Client-side playback components¶
The Tryll Unity and Unreal packages ship a ready-made playback component that wires the TTS audio stream to the platform audio engine:
| Platform | Component | Audio backend |
|---|---|---|
| Unity | TryllSpeaker (MonoBehaviour) |
Streaming AudioClip with pcmReaderCallback |
| Unreal | UTryllSpeakerComponent (UActorComponent) |
USoundWaveProcedural + hidden UAudioComponent |
The Unreal speaker is always non-spatialized (2D). The Unity speaker plays
through its AudioSource and leaves that source's spatial settings alone.
For custom audio, such as your own spatializer, subscribe to the raw PCM:
TryllSpeaker.OnTtsPcm (Unity) or UTryllSpeakerComponent::OnTtsPcm
(Unreal). You can also replace the speaker entirely with
TryllAgent.SetOnTtsAudioFormat / SetOnTtsAudio; each holds one
callback, so setting it replaces the speaker's.
See also¶
- Add voice output to an agent — step-by-step guide for Unity and Unreal
- Filter LLM output artifacts — strip speaker prefixes and roleplay spans before TTS hears the stream
- Substitute agent variables in LLM output —
replace
__PLACEHOLDER__markers before TTS - Reference: TTS Models — shipped voices (Sherpa-ONNX and llama.cpp SNAC), family matrix, and licenses
- Reference: Voice Profile — the agent's voice, delivery modes, and per-model delivery
- Add emotions to a voice — one cloned clip per mood, switched with delivery modes
- Reference: GenerateAndSpeak node
- Reference: Speak node
- Reference: TryllSpeaker (Unity)
- Add lip sync to a character — play the viseme track on a face
- Reference: TryllLipSync (Unity)
- Use voice input — pair with TTS for full two-way voice interaction