# Beatra Skill + MCP — AI Director

> Let the AI Director read the text and direct the performance before synthesis, so the voice sounds like a real person.
> Self-contained workflow guide for AI agents.
> Human-readable page: https://docs.beatra.ai/en/docs/capabilities/speech/ai-director

- Primary surface: the Universal Beatra Skill with authenticated MCP.
- Install or reconnect: https://beatra.ai/install.md
- MCP endpoint: `https://mcp.beatra.ai/mcp`
- Billable action: `beatra.speech.prepare_script`
- Interaction model: create one opaque `client_request_id`, submit the finalized arguments exactly once, then poll the returned `task_id` with `beatra.tasks.get` until terminal.
- Local media: call `beatra.assets.upload`, perform the returned HTTPS PUT exactly, and use the artifact ID from the PUT response.

REST/OpenAPI is the fallback protocol path, not the primary agent packaging.

---

Let the AI Director read the text and direct the performance before synthesis, so the voice sounds like a real person.

The AI Director reads the text and the user's requirements, then returns a script that is ready to
synthesize. It adds emotion, performance, sound effects, pauses and pronunciation in the form the
target voice can actually perform. It only adds direction: it never changes a single word of the
text. Without a voice, it also recommends a suitable voice and model and explains why.

## Primary agent path

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

1. Put the text in `input` and the user's wishes for the result, if any, in `instructions`. Pass the
   voice or model when the user named one; otherwise omit them and let the AI Director recommend.
2. Create one opaque `client_request_id`, submit once, and poll with `beatra.tasks.get` until the
   task is terminal.
3. Call `beatra.speech.synthesize` with each returned script's `synthesis` as the arguments,
   unchanged, plus your own `client_request_id`. A script whose `synthesis` is `null` cannot be
   synthesized as is; its warnings say why.
4. When the user wants a change ("more tension in the second paragraph"), add that sentence to
   `instructions` (keep the earlier requirements), call again, then synthesize again.

## Tool arguments

```json
{
  "input": "The night was still. He pushed the door open. \"Is anyone there?\" No one answered.",
  "instructions": "A little suspenseful; whisper the question.",
  "client_request_id": "script-opaque-1"
}
```

- `input`: the text, in any supported language, up to 50,000 characters. Markup already in the text
  (such as `{pause 0.5}`) is kept as written; the AI Director only adds to it. When the text is
  longer than the voice or model you named takes in one call (10,000 characters for ElevenLabs), the
  call is refused with the same 422 error as synthesis, and nothing is charged.
- `instructions`: optional requirements in natural language, in any language, up to 2,000
  characters. When given, they come first, and whatever they do not mention is performed naturally
  by the meaning of the text; without them, the whole text is.
- `voice`, `model`: optional, with the same meaning and defaults as in `beatra.speech.synthesize`.
  With neither, the AI Director recommends both. With a voice only, it picks among that voice's
  models. With a model only, it recommends a voice available on that model.
- `language`: optional; usually omit it (`auto`, the default, detects the language). Send it only
  when the user names the language or a dialect the text cannot show, such as Cantonese (`yue`).
- `context`: optional and advanced. When a long text is directed in parts, pass the text around the
  part as `{"before": "…", "after": "…"}`, each up to 2,000 characters. It is used only to understand
  the tone and is never annotated, read aloud or returned. When the target model supports it, the end
  of `before` is suggested as `synthesis.previous_text` so the tone carries over.

## Result

When the task succeeds, `output` holds a list of scripts:

```json
{
  "type": "speech_script",
  "model": "ai-director",
  "directed": true,
  "scripts": [
    {
      "synthesis": {
        "input": "{emotion calm}The night was still. He pushed the door open. {emotion fearful}{whisper}\"Is anyone there?\" No one answered.",
        "voice": "voice_01JX...",
        "model": "elevenlabs-v4-turbo",
        "language": "en"
      },
      "summary": "A tense opening with a whispered question",
      "voice_selection": {
        "source": "recommended",
        "voice": "voice_01JX...",
        "model": "elevenlabs-v4-turbo",
        "name": "Arthur",
        "preview_url": "https://cdn.beatra.ai/voices/previews/voice_01JX....mp3",
        "reason": "A mature male voice for narration that performs line by line."
      },
      "alternatives": [
        {"voice": "voice_01JY...", "model": "minimax-speech-2.8-hd", "name": "Barb",
         "preview_url": "https://cdn.beatra.ai/voices/previews/voice_01JY....mp3", "reason": "A young female voice for audiobooks."}
      ],
      "estimate": {"weighted_characters": 96, "price_credits": "24"},
      "warnings": []
    }
  ],
  "warnings": []
}
```

- `scripts`: one synthesize call per script. Today there is always exactly one; later there can be
  several (for example one per speaker), so handle each.
- `synthesis`: the arguments for synthesizing this script; pass it unchanged to
  `beatra.speech.synthesize` (plus `client_request_id`). `input` is the script with its markup; then
  come `voice`, `model`, `language`, and the whole-script controls the target voice actually follows:
  `emotion`, `direction`, `previous_text` (a control it does not follow is left out). It is `null`
  when the script cannot be synthesized as is: there is no usable voice, the voice cannot read the
  script, or the validation could not run this time (retry later); the warnings say which.
- `summary`: a one-line description of the performance that you can tell the user as is, free of
  parameter names and markup. It is in the language of `instructions`, or of the text when there are
  no instructions.
- `voice_selection`: the voice of this script, with `voice`, `model`, the voice's `name` and a
  `preview_url` to listen to. `source` is `recommended` when the AI Director chose it, with the
  `reason`; it is `given` for the voice you passed, with `reason` `null`. The whole field is `null`
  when there is no voice.
- `alternatives`: when the voice was recommended, up to 3 other voices with the same fields as
  `voice_selection` (without `source`); empty when you passed the voice.
- `estimate`: the exact price of synthesizing this script, the same as the free validation; it is
  `null` whenever `synthesis` is.
- `directed`: `false` means the AI Director could not direct the text this time. The script is the
  original text and the direction is not charged; when it has a `synthesis`, it can still be
  synthesized directly.
- When no voice can be recommended (warning `no_voice_available`), `synthesis`, `voice_selection` and
  `estimate` are `null`: pick a voice with `beatra.voices.list` and call again with that `voice`.

To switch to an alternative voice within 2 days, change only `voice` (and `model` if needed), keep
every other argument, and call again with a new `client_request_id`. The AI Director reuses its
earlier reading, never charges twice for the same reading, and only rewrites the script for the new
voice. The balance check and the limit of two in progress still apply to this call, although the
reused reading is not charged.

## Performance on each model

The AI Director only writes what the target model can perform:

- ElevenLabs: emotion changes line by line, performances such as whispering and shouting, and sound
  effects such as laughter and sighs.
- MiniMax: one emotion for the whole script, plus pauses, pronunciation and sound effects.
- Doubao: one emotion for the whole script, plus pauses and pronunciation; whether a voice follows
  emotion, acting notes and previous text depends on the voice.

When the chosen voice cannot perform an effect the user asked for, the AI Director performs the
rest and says in a warning what it could not do and what it used instead. For line-by-line
performance, choose a voice that performs line by line.

## Warnings

The top-level `warnings` are the AI Director's notes for the user: pass them on. Each script's
`warnings` are the free validation's notes for that script and are usually empty.

| `code` | Meaning |
|---|---|
| `direction_unavailable` | The AI Director could not direct all or part of the text. Those sentences come back as written, and that part is not charged. `field: "voice"` means the voice you named cannot read this script (for example, the text is in a language the voice does not speak); choose another voice. |
| `effect_unavailable` | The chosen voice cannot perform an effect the user asked for, or the effect could not be kept because the text is close to the voice's per-call length limit. `effect` names it and `substitute` says what was used instead (`null` when nothing was). `effect: "voice_gender"` means no voice of the requested gender reads this language yet, so the best other voice was recommended. |
| `instruction_unsupported` | The instructions ask for something not yet supported, such as translation, rewriting the text or assigning several speakers. That part is ignored. |
| `no_voice_available` | No voice can be recommended: none reads this language, the model you named has no voice for this language, the text is longer than every voice's limit, or the voices that read it are temporarily unavailable. The script still comes back. |

## Billing and task status

The AI Director reasons before it annotates and is charged after completion from actual input,
output and reasoning tokens. The price and an estimate per 1,000 characters are in the capability
catalog, `GET /v1/catalog/capabilities/speech_script`. To start, the balance must cover the estimate
for the text, counting at least 1,000 characters, plus the estimate of any other AI Director task in
progress on the account: at 146 credits per 1,000 characters, a 10,000-character text needs 1,460.
Otherwise the call returns 402 `insufficient_balance` with `details.reason: director_estimate` and
nothing is created. The charge is the actual usage, usually below the estimate.
The direction is not charged when `directed` is `false` or when a reading from the last 2 days is
reused. When `directed` is `true`, it is charged from actual tokens, including when the AI Director
finds the text needs no added direction and returns it unchanged. Synthesis is charged separately as
text to speech; each script's `estimate` shows that price.

An account can have at most two AI Director tasks in progress at a time, and the platform runs a
limited number at once. A third request returns 429 `rate_limited` with
`details.reason: director_inflight_limit` (over MCP, the plan's tasks-at-a-time limit can answer
first); while the AI Director is at capacity, a request returns 429 with
`details.reason: director_busy`. Neither starts a task or charges anything. For
`director_inflight_limit`, wait for one of the account's AI Director tasks to finish, then submit
again; for `director_busy`, retry after `details.retry_after_seconds` (over REST, the `Retry-After`
header carries the same seconds). If the AI Director is temporarily unavailable, the call returns
503 `model_unavailable`; there is no model to switch to, so retry later.

`queued` and `running` are not failures; keep polling the same `task_id`. On a lost create
response, retry the identical arguments with the same `client_request_id`; if any input changes,
use a new ID. The task list (`GET /v1/tasks`) shows only a summary of the result: it is marked
`truncated: true` and has no `synthesis`. To get a script you can synthesize, read the task by ID
(`beatra.tasks.get` or `GET /v1/tasks/{task_id}`). The AI Director's result is a script, not a
creation: it does not appear in your creations, and the console shows it as "AI Director" in
activity and billing.

## REST API

Direct protocol integrations may use `POST /v1/audio/speech/scripts` and the shared task endpoints,
and send each script's `synthesis` unchanged as the body of `POST /v1/audio/speech`.
Follow the generated [API operation](https://docs.beatra.ai/en/docs/api-reference/operations/audio/prepare_speech_script_v1_audio_speech_scripts_post).
Skill + MCP is the recommended integration; use REST for custom, non-agent applications.

---

## Appendix: shared Skill + MCP contract

### Retry identity

One logical generation has one `client_request_id`. If the create response is
lost, retry the exact same arguments with that ID. If any prompt, media, model,
or output control changes, use a new ID. Never create another paid task merely
because the first task is `queued` or `running`.

### Polling and billing truth

Poll with `beatra.tasks.get` using bounded backoff. Stop at `succeeded`,
`failed`, or `canceled`. Only the task response is authoritative for the
resolved model, artifacts, charged credits, and refunded credits.

### Recovery

Preserve structured Beatra error codes. Reconnect on authentication failure;
stop and request a balance action on insufficient balance; refresh current
constraints with `beatra.models.list` after model validation; request a new
upload grant after expiration or MIME/length mismatch. Changed generation
arguments always require a new `client_request_id`.
