# 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](https://docs.beatra.cn/en/docs/capabilities/speech/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

AI integration guide: https://docs.beatra.cn/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.cn/en/docs/capabilities/speech/voices),
preset or [cloned](https://docs.beatra.cn/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 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](#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:

```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 |

## 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](https://docs.beatra.cn/en/docs/capabilities/speech/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`.
  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](https://docs.beatra.cn/en/docs/capabilities/speech/voice-clone)
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](https://docs.beatra.cn/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.
