beatraDocs
Speech & voices

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.

To make the voice sound more like a real person, first let the AI Director read the text and direct its tone, pauses and emotion with beatra.speech.prepare_script, then synthesize the script it returns with this tool.

AI guide

MCP integration guide for AI

For custom API integrations, copy this guide into Claude, Codex, or another coding assistant.

Tool arguments

{
  "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, preset or cloned. 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 selected model's constraints.input_max_characters limit and 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. Malformed markup never fails a request either: what can be corrected is read as the canonical token, and the rest is dropped or read as written, with a markup_adjusted warning that gives the reason and the correct form. Only a request with markup but no text to read is rejected with 422 before charging, on every model. 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 MiniMax, 10 seconds on Doubao; a longer pause is cut to the limit with a warning). Seconds may carry a unit: {pause 500ms}, {pause 0.5s}. ElevenLabs renders no pauses: it removes them with markup_degraded (see ElevenLabs v4 and v4 Turbo). A malformed pause (such as {pause} or {pause abc}) and a pause at the start or end of the text are dropped, and of adjacent pauses (no speakable text between them) only the first is kept, each with markup_adjusted.
  • 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. A malformed reading (missing, not pinyin, or a syllable count that does not match) reads the word as written, with markup_adjusted; syllables separated by spaces are joined. A reading in several parts where any part is not pinyin (usually a missing closing brace) keeps the whole group as text, with unknown_markup. 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. A missing spoken text reads the word as written, with markup_adjusted; a spoken text over 100 characters keeps the whole group as text, with unknown_markup. 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.
  • {emotion <emotion>} — inline emotion: from this point on, speak with this emotion until the next {emotion …}. The request's emotion is the starting emotion, and the values are the same. Only models whose card has script_markup.inline_emotion follow it sentence by sentence; other models drop the marker, use emotion for the whole request, and return markup_degraded naming the models that support it, if any. An invalid value is dropped (never read aloud) with markup_adjusted. Example: Let's go. {emotion happy}Look, the sea! {emotion sad}Too bad we leave tomorrow.
  • Performance markers — apply to the sentence that follows and do not carry over to the next paragraph. The available markers are the model card's script_markup.performance; only models that list them perform them, and other models drop them with markup_degraded naming the models that do, if any.
  • A near-miss spelling is read as the canonical token ({laughs} and {Laugh} as {laugh}), with markup_adjusted; so is a known token in full-width braces ({pause 1}), while other full-width braces are plain text. Unknown brace content (such as {name}), and markup-like groups with something extra ({pause: 1000}, {pause and think}), stay in the text as written, with unknown_markup. Text in square or round brackets ([laughs], (softly)) is customer text passed through unchanged; when it looks like a stage direction, unrecognized_markup says so; for emotion and performance words it suggests a canonical token only when the current model performs it.

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, and pitch apply to the whole request and cannot target one sentence or word; so does emotion, unless the card has script_markup.inline_emotion. 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. It does not check the two-task ElevenLabs limit or current capacity.

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. ElevenLabs is the exception: its offsets point into your text with markup removed and each {sub} shown as its written word; without markup this is exactly the text you submitted. 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; output.characters reports the characters the engine read, so the two counts can differ. On ElevenLabs, output.characters is the billed weighted count.

Models, controls, and cost

Common controls are input (required; use the selected model's character limit), 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. MiniMax and Doubao may deliver audio without subtitles after a runtime subtitle failure; ElevenLabs requires the requested subtitles and fails with a refund if they cannot be delivered). 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 code point 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:

{
  "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.

CodeLanguageCodeLanguage
zhChinesetrTurkish
yueCantonesenlDutch
enEnglishukUkrainian
esSpanishthThai
frFrenchplPolish
ruRussianroRomanian
deGermanelGreek
ptPortuguesecsCzech
arArabicfiFinnish
itItalianhiHindi
jaJapanesebgBulgarian
koKoreandaDanish
idIndonesianheHebrew
viVietnamesemsMalay
faPersianskSlovak
svSwedishhrCroatian
filFilipinohuHungarian
noNorwegianslSlovenian
caCatalannnNynorsk
taTamilafAfrikaans

ElevenLabs v4 and v4 Turbo

elevenlabs-v4 and elevenlabs-v4-turbo are open to every account and appear in the default model catalog, together with 80 preset voices (see voices). elevenlabs-v4 is the most expressive of the two. elevenlabs-v4-turbo uses the same voices and languages, is faster, and costs about half. v4 keeps training, so the same voice can change over time, and the same seed is not guaranteed to reproduce after a model update.

ModelCredits per 1,000 weighted characters
elevenlabs-v4-turbo240
elevenlabs-v4450

An ElevenLabs voice with model: "auto" prefers Turbo among its compatible models. The model is fixed before admission; a failure never switches the voice or model.

Length and counting

A request accepts at most 10,000 characters, counted as Unicode code points, for the original text and for the compiled text (markup applied) separately. Other engines accept up to 50,000. Over-limit input returns 422 invalid_request before charging; shorten the text, use less markup, or drop emotion, and validate again. A request is never automatically split into several audio files.

Every code point counts once for the limit, so a visible symbol can count more than one:

  • A Chinese character, a letter, or a single-code-point emoji counts 1.
  • Emoji sequences count every code point: 👍🏽 = 2, ❤️ = 2, 🇨🇳 = 2, 1️⃣ = 3, 👨‍👩‍👧‍👦 = 7.
  • Combining marks count separately: Devanagari vowel signs, Arabic diacritics, and decomposed accents each add one.

Billing uses the same rule with weights: each Han ideograph counts 2 and every other code point counts 1. Credits are charged on the larger of the original text and the compiled text, both weighted. On ElevenLabs, output.characters is this billed weighted count.

Formats, languages, and controls

  • Languages: en, zh, ja, ko, es, pt, ar, hi, fr, it. Omit language or use auto for automatic recognition; other explicit languages are rejected before charging.
  • Formats: MP3 at 22,050 / 24,000 / 44,100 Hz (default 44,100); WAV and raw PCM at 8,000 / 16,000 / 22,050 / 24,000 / 32,000 Hz (default 24,000); Opus at 48,000 Hz. Omit sample_rate for Opus. FLAC is unavailable. PCM results report mime_type audio/L16 and a real duration_seconds. A very long WAV or PCM request whose audio would exceed the per-result size limit is rejected before charging with 422 unsupported_option on format; this applies only for long Chinese, Japanese or Korean text (about 7,500 characters or more) at 32,000 Hz. Use MP3 or Opus, a lower sample rate, or shorter text.
  • direction and nondefault speed, volume, or pitch are ignored with an option_ignored warning.
  • emotion supports the seven emotions other than fluent. Beatra adds an emotion tag (such as [sad]) at the start of each paragraph (paragraphs are separated by a blank line). The emotion starts at the tag and may fade over a long paragraph, so split long text with blank lines where it matters. The tags make the sent text longer, so they can raise the billed count (billing takes the larger of the submitted and the sent text), and they count toward the 10,000-character limit. fluent is ignored with option_ignored. All 13 sound tags ({laugh} and the rest) are supported and voice the sound where they are placed. Effects vary with the voice and the text; breathing sounds such as {sigh} and {breath} are light and may be hard to hear.
  • No pauses: {pause} is removed with markup_degraded (the card publishes [0, 0]); a malformed pause is dropped by the general rule, with markup_adjusted. {pron} is read as written, with markup_degraded.
  • Inline emotion works sentence by sentence: {emotion <emotion>} inserts the emotion tag where it is written, and each later paragraph starts with the current emotion again. fluent is ignored with a warning, and later paragraphs get no emotion tag. Performance markers follow the card's script_markup.performance; both models perform all ten: {whisper}, {shout}, {excited}, {sarcastic}, {curious}, {tearful}, {playful}, {nervous}, {tired} and {gentle}. The tag goes right before the next spoken text and applies to the sentence that follows, never past the end of the paragraph. Inserted tags never appear in subtitles and count toward the sent text.
  • {sub word spoken text} speaks the replacement and shows the original word in subtitles. Square brackets you write are customer text: they are passed through unchanged and stay in subtitles; they are not Beatra markup. The engine may perform them as a sound or a style.
  • subtitle_enable: true promises sentence subtitles from measured character timestamps. ElevenLabs subtitle offsets (text_begin, text_end) point into your text with markup removed and each {sub} shown as its written word; without markup this is exactly the text you submitted. If valid subtitles cannot be built, the task fails and this synthesis is refunded; there is no audio-only success, and no paid transcription replaces missing timestamps.
  • previous_text and model_options.elevenlabs.next_text are context only and are never billed. Beatra sends the last 100 characters of the previous text and the first 100 of the next text.

model_options.elevenlabs accepts stability and similarity_boost (finite numbers from 0 to 1), seed (integer 0–4,294,967,295), next_text (string), and apply_text_normalization. The model card's constraints.limitations lists the currently available normalization values. Both models currently enable only auto; on and off are ignored with a warning. Unknown keys are ignored with a warning; invalid values of known keys are rejected before charging.

Before confirming a charge, validate the complete final request. Validate returns the weighted count and the price and checks the same syntax and limits, but it does not check the two-task limit or current capacity, so a request that validates can still receive the errors below. A 2,000-character clip took about one minute in testing; a 10,000-character clip may take several minutes. Keep polling the same task.

Limits and errors

Each account may have at most two ElevenLabs speech tasks in progress, across elevenlabs-v4 and elevenlabs-v4-turbo. Free private clone previews count toward the two; MiniMax and Doubao tasks do not.

Errors before charge. Nothing is charged and no task is created, so a retry may reuse the same Idempotency-Key / client_request_id or use a new one. voice_unavailable is the exception: it is permanent and not retryable.

Errordetails.reasonMeaning and what to do
429 rate_limitedspeech_inflight_limitTwo ElevenLabs speech tasks are already in progress. Wait for one to finish. retry_after_seconds is 20; over REST a Retry-After header carries the same seconds (MCP has no headers; use details.retry_after_seconds).
503 model_unavailablevoice_temporarily_busyThe cloned voice is being prepared or restored. Retry after retry_after_seconds (about 30 seconds; a few minutes while a previous restore is being checked).
503 model_unavailablevoice_temporarily_unavailableThis cloned voice cannot be restored right now. Other voices are not affected. available_at gives the recovery time when known.
422 invalid_request, unsupported_option, voice_unavailable, or voice_retired—Fix the request, or see Voice retirement below and the clone page.

Errors after the task is accepted are task errors. All of them are refunded in full, and a later retry needs a new client_request_id or Idempotency-Key:

Task errorWhen
model_rate_limitedThe model's capacity is full.
model_unavailableThe model's allowance is used up; available_at gives the recovery time when known.
invalid_requestThe model rejected the text as too long.
model_output_invalidThe model returned unusable audio.
voice_retiredThe voice retired after the request was accepted.
voice_unavailable, or model_unavailable with reason voice_temporarily_busy / voice_temporarily_unavailableA cloned voice could not be prepared at execution; same meaning as before charge, and the task is refunded.

Voice retirement

A preset voice scheduled for retirement stays listed and usable until its date. The voice object then carries retirement: {"retire_at": "<ISO 8601 UTC>", "replacement_voice_id": "<id or null>"}; the field is absent when nothing is scheduled. A synthesis with such a voice succeeds and carries a voice_retiring warning (Chinese text, field voice) with the date and the recommended replacement when known. After the date the request returns 422 voice_retired with details.retirement_at and, when known, details.replacement_voice_id. The check runs at admission and again at execution; a hit at execution fails the task and refunds it. The replacement is a suggestion: Beatra never substitutes a voice. A cloned voice that cannot be restored from its saved sample returns 422 voice_unavailable; this is permanent, so do not retry and contact support. The synthesis is not charged, and the clone fee is not refunded.

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. Skill + MCP is the recommended integration; use REST for custom, non-agent applications.

Last updated

Text to speech | Beatra Docs