# Text to speech
> Synthesize speech from text with a preset or cloned voice.

Synthesize speech from text with a preset or cloned voice.

## Primary agent path

Use the Universal Skill with the MCP tool `beatra.speech.synthesize`. The tool
is billable and returns an asynchronous task.

1. Pick a voice first: `beatra.voices.list` returns preset and cloned voices
   with `voice_id` and preview URLs.
2. If a model, language, format, or price matters, call `beatra.models.list`
   with `capability: "text_to_speech"`. Otherwise use `model: "auto"` or omit
   `model`.
3. Create one opaque `client_request_id` and submit the final arguments once.
4. Poll the returned task with `beatra.tasks.get` until it is terminal.

## AI guide

AI integration guide: https://docs.beatra.ai/llms/audio-text-to-speech.md

## Tool arguments

```json
{
  "voice": "voice_01JX...",
  "input": "Welcome to Beatra — make your first call in three minutes.",
  "language": "en-US",
  "format": "mp3",
  "speed": 1.0,
  "client_request_id": "tts-opaque-1"
}
```

`voice` accepts any `voice_id` from [voices](https://docs.beatra.ai/en/docs/capabilities/speech/voices),
preset or [cloned](https://docs.beatra.ai/en/docs/capabilities/speech/voice-clone). A voice belongs to
one engine (its `compatible_models`), and on some models what a voice follows
differs from voice to voice; its `persona` says so. Streaming and real-time
conversation are not part of this capability.

## Speech markup

`input` may contain a closed set of tokens. Tokens count toward the 50,000
character limit and toward weighted-character billing. The live vocabulary and
ranges are `constraints.script_markup` on the model card from
`beatra.models.list`. The same markup works on every speech model; what a
model cannot render is degraded (dropped or ignored) with a warning, never
approximated. The model-specific options further down this page are
`model_options` fields, not markup.

- `{pause <seconds>}` — silence, at most two decimal places, within the
  model card's `script_markup.pause.range` (up to 99.99 seconds on some
  models, 10 seconds on others; a longer pause is rejected before billing).
  The request is rejected when a pause sits at the start or end of the text,
  or when two pauses have no speakable content between them.
- Sound tags: `{laugh}` a laugh, `{chuckle}` a chuckle, `{sigh}` a sigh,
  `{breath}` an audible breath, `{cough}` a cough, `{clear-throat}` a throat
  clear, `{gasp}` a gasp, `{groan}` a groan, `{inhale}` an inhale, `{exhale}`
  an exhale, `{pant}` panting, `{sniff}` a sniff, `{hmm}` a hesitant "hmm".
  `script_markup.tags` lists the tags a model renders; on a model without
  them the tag is removed (never read aloud) and a `markup_degraded` warning
  says so.
- `{pron <word> <reading>}` — pin the reading of this one occurrence of a
  word; the same word elsewhere keeps its default reading, and one request may
  give the same character different readings. Readings are pinyin:
  syllables written together, each ending with a tone digit 1–5, `v` for ü,
  one syllable per Chinese character. Other notations are not supported.
  Example: `孩子{pron 长 zhang3}大了，头发也很{pron 长 chang2}。`
- `{sub <word> <spoken text>}` — speak this occurrence of a word as the given
  text, up to 100 characters; subtitles still show the word. Use it for brand
  names, abbreviations, English words, and digits that must be read one by
  one. Example: `{sub W3C 万维网联盟}`.
- `{{` — a literal left brace: a `{{` in the text is spoken as one `{`, and
  the pair itself is never parsed as a token. Text after it is still parsed
  normally, so escape only the left brace and write right braces as they are.

`constraints.limitations` on each model's card states that model's limits in
plain sentences; read it for the model you call. Examples:

- Volume, speed, pitch, and emotion apply to the whole request and cannot
  target one sentence or word. For local variation, split into multiple calls
  (each with its own parameters and its own audio file) or rewrite the text
  (ellipses, short clauses, wording).
- On some models, long texts (over 3,000 characters, which take the async
  path) do not get high-quality number normalization, or drop `{pause}` and
  `{pron}` markup (with a warning). Split long texts at paragraph breaks into
  requests of at most 3,000 characters when that matters.

## Performance direction and previous text

Two optional fields describe how a clip should sound, on every model that
follows them; the model card lists each one under `controls` (with
`constraints.direction_max_characters` and
`constraints.previous_text_tail_characters`):

- `direction` — natural-language direction for the whole request, for example
  `像在耳边说悄悄话一样，轻声地说` or `read it like breaking news`. Keep it to a
  sentence or two: a direction longer than `direction_max_characters` is
  ignored with a warning. How it comes through depends on the model, the voice
  and the text; a volume request is better made with `volume`.
- `previous_text` — the text that comes right before this clip (the previous
  line of a dialogue, the previous paragraph), so separately synthesized clips
  keep a continuous tone. Any length is accepted; the model uses only its last
  `previous_text_tail_characters` characters. It is never spoken.

Neither field is billed, and neither ever makes a request fail. A model, a
voice, or a path that does not follow a field ignores it and returns an
`option_ignored` warning; the audio is still produced. On some models these
controls depend on the voice (for example, character voices may follow
neither); `constraints.limitations` on the card says which. `emotion` works the same
way: a model or voice that cannot render the requested emotion ignores it
with a warning. To learn whether a voice follows these controls before a
billable call, send the same request to validate (below): it returns the same
warnings, free.

Before a billable submit, call `beatra.speech.validate` or
`POST /v1/audio/speech/validate`. It runs the same checks and warnings and
returns the weighted character count and the credit price; it does not create
a task, charge credits, or call the speech engine. It does not take an
`Idempotency-Key`.

Warnings, when present, are on the 202 response (including an idempotent
replay), `GET /v1/tasks/{task_id}`, `beatra.tasks.get`, and the customer
callback field `task.warnings`. The task list, `expand`, account detail, and
admin responses do not promise them. `text_begin` and `text_end` in a subtitle
file are offsets into the text the engine actually read — the text with
markup compiled into the engine's own form or removed — and are not mapped
back to the submitted text. Where a subtitle segment lines up with that text,
its `text` shows a `{pron}` or `{sub}` word as written and leaves sound tags
out. Billing counts characters as described under
[Models, controls, and cost](#models-controls-and-cost); `output.characters`
reports the characters the engine read, so the two counts can differ.

## Models, controls, and cost

Common controls are `input` (required, ≤50,000 characters), `voice`
(required), `language`, `format` (default `mp3`; the card's
`constraints.formats` lists the others the model renders), `speed` (0.5–2.0),
`volume` (within the card's `constraints.volume` range, for example >0–10 or
0.5–2; a value outside it is rejected before billing), `pitch` (−1–1, normalized: 0 keeps the
voice's original pitch, −1 the lowest, 1 the highest the selected model
supports; use decimals like 0.25 for subtle tuning), `emotion`,
`sample_rate`, and `subtitle_enable` (default `false`; set `true` to receive
sentence-level subtitles with the audio as `output.subtitles` — free; a model
that cannot time subtitles for the chosen voice or language rejects the request
before billing, and `constraints.limitations` names those cases; if a subtitle
file cannot be produced at run time, the audio is still delivered, without
`output.subtitles`). `language` is a hint some models only partly use:
`constraints.limitations` says how the selected model treats it. Use each only according to the
selected model's metadata. Not every format supports every sample rate (for
example `opus` renders at 8/16/24 kHz on some models and only at its fixed
48 kHz on others, where it takes no `sample_rate`): read
`constraints.sample_rates_by_format` on the model card instead of assuming the
flat rate list applies to every format.

`beatra.models.list` is the current source for selectable models, supported
languages, formats, sample rates, and prices. Do not copy a model
catalog or price table into an agent prompt.

Speech is prepaid from the request estimate and billed **linearly** by
weighted characters (quoted per 1,000): each Han ideograph counts as 2 and
every other character counts as 1. The count is taken on the submitted text,
markup included. On a model whose engine bills the control text it receives by
length, the text sent to the engine is counted when it is longer: there,
pauses and readings become control tags, and a request that carries them (or
whose text begins with `<speak`) is sent whole in that form (`&`, `<` and `>`
escaped). Validate returns the exact count.

## Model-specific options

`model_options` carries options that only one model family runs, named by
family. The model card lists the keys it runs under `controls` (for example
`model_options.doubao.explicit_dialect`). Keys for another family have no
effect; a model may say so with an `option_ignored` warning.

### MiniMax: pronunciation dictionary

For rare or polyphonic Chinese characters, pin the exact reading instead of
rewriting the text with a homophone:

```json
{
  "model_options": {
    "minimax": {
      "pronunciation_dict": {
        "tone": ["湛/(zhan4)", "晞/(xi1)", "厌/(yan1)"]
      }
    }
  }
}
```

The format is `character/(pinyin+tone digit)`, with tone digits 1–4 for the
four tones and 5 for the neutral tone. A dictionary entry applies to every
occurrence of the word in the request; to fix one occurrence, use the
`{pron}` markup above. MiniMax models only; other models ignore this field.

### Doubao

- `model_options.doubao.explicit_dialect` — `beijing`, `dongbei`, `henan`,
  `shaanxi`, `shanghai`, `sichuan` or `tianjin` (Cantonese is
  `language: "yue-HK"`). Only voices whose `languages_supported` lists `yue`
  speak dialects; other voices are rejected before billing.
- `model_options.doubao.explicit_language` — forces the reading language
  (`zh-cn`, `en`, `ja`, `es-mx`, `ko`, `de`, `fr`, …). This is a filter, not a
  hint: sentences in other languages are skipped, and a voice given text it
  cannot read produces no audio (the task fails and is refunded). `nl`, `no`,
  `uk`, `fi`, `da`, `cs`, `hu`, `el`, `ro`, `hi` and `es-es` work only with
  Chinese (`zh_…`) preset voices. Long texts accept only `zh-cn`, `en`,
  `es-mx`, `id` and `pt-br`.
- `model_options.doubao.silence_duration` — silence appended after the last
  word, 0–30,000 milliseconds.
- `model_options.doubao.latex_parser` — `"v2"` reads LaTeX formulas aloud
  (education scripts).
- `model_options.doubao.pronunciation_dict` — `{"tone": ["北京/(bei3)(jing1)",
  "omg/oh my god"]}`: up to 5,000 rules applied to every occurrence of the
  word (a reading, or a replacement text). To fix one occurrence, use `{pron}`.

When the text uses `{pause}` or `{pron}`, the markup wins: `latex_parser` and
`pronunciation_dict` are ignored with a warning, because the engine cannot
combine them with the form that markup compiles to.

## Languages

`language` is an optional provider-neutral BCP-47 tag such as `zh-CN`,
`yue-HK`, or `en-US`. Beatra matches the primary language subtag against the
selected model; omit the field to let the voice or provider choose. Pass the
exact value `auto` only when you explicitly want automatic language detection
(`constraints.language_auto` on the model card says whether the model supports
it). An unsupported language is rejected instead of being silently replaced.

How a model uses `language` differs. Some use it as a recognition hint for
the voice. Others read Chinese and English by default and use `language` to
turn on language recognition for anything else (a Chinese voice may read
nothing in, say, Japanese without it) and to pick a dialect (`yue`); on those,
each voice reads only the languages in its `languages_supported`. A
`language` the voice does not speak comes back with an `option_ignored`
warning, except where the result could only fail: Cantonese on a voice
without it, or a long text in a language the voice cannot read, is rejected
before billing. `constraints.limitations` on the card says which applies.

Current speech models cover up to the 40 languages below, each model a part
of them; `constraints.supported_languages` from `beatra.models.list` is the
live, model-specific source.

| Code | Language | Code | Language |
| --- | --- | --- | --- |
| `zh` | Chinese | `tr` | Turkish |
| `yue` | Cantonese | `nl` | Dutch |
| `en` | English | `uk` | Ukrainian |
| `es` | Spanish | `th` | Thai |
| `fr` | French | `pl` | Polish |
| `ru` | Russian | `ro` | Romanian |
| `de` | German | `el` | Greek |
| `pt` | Portuguese | `cs` | Czech |
| `ar` | Arabic | `fi` | Finnish |
| `it` | Italian | `hi` | Hindi |
| `ja` | Japanese | `bg` | Bulgarian |
| `ko` | Korean | `da` | Danish |
| `id` | Indonesian | `he` | Hebrew |
| `vi` | Vietnamese | `ms` | Malay |
| `fa` | Persian | `sk` | Slovak |
| `sv` | Swedish | `hr` | Croatian |
| `fil` | Filipino | `hu` | Hungarian |
| `no` | Norwegian | `sl` | Slovenian |
| `ca` | Catalan | `nn` | Nynorsk |
| `ta` | Tamil | `af` | Afrikaans |

## Task status and recovery

`queued` and `running` are not failures. Keep polling the same `task_id`; never
submit a replacement because work is still running. On a lost create response,
retry the identical arguments with the same `client_request_id`. If any input
changes, use a new ID. Failed tasks automatically refund the charged credits.
Return the audio artifact, resolved model, and charged credits only from the
terminal task response; artifact URLs are CDN addresses — copy them to your own
storage promptly.

## REST API

Direct protocol integrations may use `POST /v1/audio/speech` and the shared
task endpoints. Follow the generated [API operation](https://docs.beatra.ai/en/docs/api-reference/operations/audio/text_to_speech_v1_audio_speech_post).
Skill + MCP is the recommended integration; use REST for custom, non-agent
applications.
