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.
- Pick a voice first:
beatra.voices.listreturns preset and cloned voices withvoice_idand preview URLs. - If a model, language, format, or price matters, call
beatra.models.listwithcapability: "text_to_speech". Otherwise usemodel: "auto"or omitmodel. - Create one opaque
client_request_idand submit the final arguments once. - Poll the returned task with
beatra.tasks.getuntil 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 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'sscript_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 withmarkup_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 withmarkup_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.tagslists the tags a model renders; on a model without them the tag is removed (never read aloud) and amarkup_degradedwarning 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,vfor ü, 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, withmarkup_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, withunknown_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, withmarkup_adjusted; a spoken text over 100 characters keeps the whole group as text, withunknown_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'semotionis the starting emotion, and the values are the same. Only models whose card hasscript_markup.inline_emotionfollow it sentence by sentence; other models drop the marker, useemotionfor the whole request, and returnmarkup_degradednaming the models that support it, if any. An invalid value is dropped (never read aloud) withmarkup_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 withmarkup_degradednaming the models that do, if any. - A near-miss spelling is read as the canonical token (
{laughs}and{Laugh}as{laugh}), withmarkup_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, withunknown_markup. Text in square or round brackets ([laughs],(softly)) is customer text passed through unchanged; when it looks like a stage direction,unrecognized_markupsays 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像在耳边说悄悄话一样,轻声地说orread it like breaking news. Keep it to a sentence or two: a direction longer thandirection_max_charactersis ignored with a warning. How it comes through depends on the model, the voice and the text; a volume request is better made withvolume.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 lastprevious_text_tail_characterscharacters. 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:
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,sichuanortianjin(Cantonese islanguage: "yue-HK"). Only voices whoselanguages_supportedlistsyuespeak 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,hiandes-eswork only with Chinese (zh_…) preset voices. Long texts accept onlyzh-cn,en,es-mx,idandpt-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 |
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.
| Model | Credits per 1,000 weighted characters |
|---|---|
elevenlabs-v4-turbo | 240 |
elevenlabs-v4 | 450 |
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. Omitlanguageor useautofor 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_ratefor Opus. FLAC is unavailable. PCM results reportmime_typeaudio/L16and a realduration_seconds. A very long WAV or PCM request whose audio would exceed the per-result size limit is rejected before charging with 422unsupported_optiononformat; 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. directionand nondefaultspeed,volume, orpitchare ignored with anoption_ignoredwarning.emotionsupports the seven emotions other thanfluent. 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.fluentis ignored withoption_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 withmarkup_degraded(the card publishes [0, 0]); a malformed pause is dropped by the general rule, withmarkup_adjusted.{pron}is read as written, withmarkup_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.fluentis ignored with a warning, and later paragraphs get no emotion tag. Performance markers follow the card'sscript_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: truepromises 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_textandmodel_options.elevenlabs.next_textare 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.
| Error | details.reason | Meaning and what to do |
|---|---|---|
429 rate_limited | speech_inflight_limit | Two 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_unavailable | voice_temporarily_busy | The 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_unavailable | voice_temporarily_unavailable | This 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 error | When |
|---|---|
model_rate_limited | The model's capacity is full. |
model_unavailable | The model's allowance is used up; available_at gives the recovery time when known. |
invalid_request | The model rejected the text as too long. |
model_output_invalid | The model returned unusable audio. |
voice_retired | The voice retired after the request was accepted. |
voice_unavailable, or model_unavailable with reason voice_temporarily_busy / voice_temporarily_unavailable | A 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