TTS

Text and sampling

Normalization, segmentation, and the sampling knobs the worker will accept.

Two things shape the output beyond the voice: what the text is expanded into before synthesis, and how the model samples. Both are set when the session is created and neither changes mid-socket.

Text normalization#

normalize_text defaults to true. It expands the written forms the model was never trained to voice (digits, dates, clock times, versions, fractions, percentages, acronyms) into the words a Vietnamese speaker would actually say.

in   Ngày 23/8/2024 lúc 15h30, giảm 25%

out  Ngày hai mươi ba tháng tám năm hai nghìn không trăm hai mươi tư
     lúc mười lăm giờ ba mươi phút, giảm hai mươi lăm phần trăm

It runs on the server, before segmentation, because expansion changes the length of the text: chunking first would size every chunk against the written form and then speak the expanded one.

Turn it off if you normalize your own text. Normalization is not idempotent: VN-215 expands to VN 2 1 5, and a second pass reads that back as Việt Nam hai một năm. Anything forwarding text that has already been expanded should send normalize_text: false.

Segmentation#

With segment_mode: "auto" the server splits your text. The splitter is the model’s own and it is tuned for long-form: it packs sentences toward a duration budget rather than emitting one per sentence.

  • chunk_seconds is that budget, 15 by default. Lower it if your callers send whole paragraphs and you care more about time-to-first-audio than about seam quality.
  • segment_mode: "none" says the text is exactly one segment. Over the model’s input limit it fails the connection with 4003 segment_too_long, so only use it when you have already split the text yourself.
  • Sending one sentence per synthesize sidesteps the question entirely, and is what a voice agent streaming a language model’s output should be doing anyway.

Sampling#

All optional. A caller who sets none gets the worker’s defaults, which are the ones the model was tuned with. Reach for these when you have a specific problem, not as a matter of course.

ParameterDefaultWhat it does
temperature0.8Audio sampling temperature. Higher is more varied and less stable; 0 is greedy.
repetition_penalty1.2Per-codebook penalty over the lane’s history. 1.0 turns it off, which measurably raises word error rate and leaves more dead air.
topk25Top-k truncation on each codebook. 0 for none.
topp0.95Nucleus threshold on each codebook.
text_temperature1.0Temperature for the control channel, which decides where an utterance ends.
text_topk50Top-k on the control channel.
text_topp1.0Nucleus threshold on the control channel.
min_frames4Frames a segment must produce before it may stop.
max_frames400Runaway guard. Clamped down to the worker’s own ceiling, since it sizes a preallocated cache: it can be lowered but never raised.
eoa_extra_frames1Frames kept past the end of the utterance, preserving the last phone’s release.
POST /api/v1/realtime/stream/sessionjson
{
  "voice_id": "maichi",
  "format": "pcm_s16le",
  "sample_rate": 24000,
  "normalize_text": true,
  "chunk_seconds": 15,
  "sampling": {
    "temperature": 0.7,
    "repetition_penalty": 1.2,
    "topk": 25,
    "topp": 0.95
  }
}

Three things about sampling#

  • They are per socket, not per message. The worker resolves them once at connect and every context on that socket decodes under them. Open a second socket to change one.
  • They are requests, not settings. The worker clamps every value to its own bounds and is the only process that knows what they are. A value outside the range above is clamped, not refused.
  • An unknown name is a 422. The session route lists what it accepts rather than dropping a typo silently several services away.

cfg_scale and seed from the studio’s advanced panel are not supported here. The batched decoder implements no classifier-free guidance, and one random stream serves the whole batch, so a per-request seed would not mean anything.