---
name: athanlab-voice
description: Generate natural Burmese (Myanmar) speech with the AthanLab MCP tools (athanlab_*). Use when the user wants Burmese or Myanmar text read aloud, a voice-over or narration for a video, an audiobook or podcast passage, a dialogue or multi-speaker script with a different voice per character, subtitles (SRT or VTT) timed to generated speech, or asks which AthanLab voices exist, what a text will cost or how many characters are left, or wants the audio sent to their AthanLab account to listen to later (AthanInbox: 'send it to my AthanLab', 'put it in my inbox', 'listen later'). Covers writing text so it is read correctly, casting voices, quoting the cost before spending, turning a script manifest into subtitles, a timeline or one combined audio track, and sending audio to AthanInbox.
compatibility: Needs the AthanLab MCP server (@athanlab/mcp) connected with an ATHANLAB_API_KEY from an approved AthanLab developer account with API access (Max plan or founder access), and network access to api.athanlab.com.
---

# AthanLab voice: Burmese text-to-speech

AthanLab turns Burmese text into natural speech through its Developer API. The `athanlab_*` MCP tools wrap that API: they create the job, wait for it, download the audio into a local folder and return the file path. When the API offers it, they can also send the audio to AthanInbox, the inbox of the user's AthanLab account, to listen to later on athanlab.com. Every job and every AthanInbox delivery spends the user's real AthanLab character balance, so treat calls that create audio like spending money.

AthanLab speaks Burmese and English, including Burmese with English words mixed in. It is not a tool for other languages.

## The tools

| Tool | What it does | Costs characters? |
|---|---|---|
| `athanlab_usage` | Plan, whether the key can create jobs, balance, this key's monthly budget, jobs running and the concurrency limit | No |
| `athanlab_list_voices` | Voices the key can use (`source: athanlab` official, `source: user` the user's own) and `default_voice_id` | No |
| `athanlab_preview_voice` | A signed sample URL for one voice; `save: true` also downloads it | No |
| `athanlab_quote` | Dry run: characters, how many pieces (`dispatches`) the text becomes, `spendable`, `sufficient` | No |
| `athanlab_speak` | One text → one audio file (job created, polled, downloaded) | Yes, the text's length |
| `athanlab_speak_script` | Many lines → one file per line + `manifest.json` (+ optional `combined.wav`) | Yes, the sum of all lines |
| `athanlab_get_job`, `athanlab_list_jobs` | Status of one job, or a page of jobs (filter by status) | No |
| `athanlab_download_audio` | Downloads a finished job's audio (MP3 or WAV) | No, it was paid for at creation |
| `athanlab_cancel_job` | Stops a job still processing; the unprocessed share is refunded | Refunds |
| `athanlab_send_to_inbox` | A title and its text (or your own chapters) → a delivery in the user's own AthanInbox, on athanlab.com; returns at once with `delivery_id`, `status` and `app_url` | Yes, every chapter, up front; failed chapters are refunded |
| `athanlab_get_delivery` | Status of one AthanInbox delivery: each chapter's status, characters charged and refunded | No |

`athanlab_send_to_inbox` and `athanlab_get_delivery` appear only when the API offers AthanInbox. If they are not in your tool list, AthanInbox is not available: say so and offer local files instead.

## Before you spend

1. **Check the account once** with `athanlab_usage`. If `entitled` is false the key is read-only (no new jobs). Look at `spendable` and, for agent keys, `key.remaining` — the most this key may still spend this month.
2. **Quote first** with `athanlab_quote` whenever the text is long, was not written by you, or the user cares about cost. A quote is free and also catches text the API would refuse. (It does count against the key's write limit, 60 requests a minute by default, so do not quote every line of a long script yourself — `athanlab_speak_script` quotes for you.)
3. **Ask before big jobs.** `athanlab_speak_script` refuses scripts over 2,000 characters or 20 lines unless you pass `confirm: true`, and returns the quote instead. Show that quote to the user (characters, lines, what they have left) and get an explicit yes before calling again with `confirm: true`. Never set `confirm: true` on a large script without that yes.
4. **Know the price.** A job costs its text's length in characters, counted in UTF-16 units: a Burmese or Latin character is 1, an emoji 2. Writing numbers out as words costs nothing extra. Dry runs, refused requests and replays are free; a failed job is refunded in full; a cancelled job keeps only the share already generated. Generating the same text again is a new job and costs again.

## Writing text for speech

The API reads exactly the text it gets. These habits make it sound natural and keep a job from being refused.

**Short sentences, with their punctuation.** End every sentence with `။`, `!` or `?`, and keep sentences short. Text is spoken sentence by sentence; a sentence too long for one piece is cut at a `၊`, then at a space — mid-word only as a last resort — so a long stretch without punctuation breaks in odd places. A piece that still cannot be made short enough is refused before any charge with `422 text_too_long`: “Part of the text is too long to speak in one piece. Add sentence punctuation and retry.” A quote's `dispatches` tells you how many pieces the text becomes.

**Burmese and English.** Burmese, English, and Burmese with English words mixed in all work. Made-up names and brand names (for example `AthanLab`) can come out wrong — spell them the way they sound in Burmese (`အသံလက်ဘ်`) and listen to a sample first.

**Marks the voice does not read.** Quotation marks, brackets and `…` are not spoken well. Remove them, or put a `၊` where you want a short pause. Text that is only emoji, punctuation or whitespace has nothing to read: it is refused before any charge with `422 unspeakable_text`.

**Numbers.** `number_mode` sets how digits are read. Each mode reading `ကျပ် ၁၂၃၄`:

| `number_mode` | What it reads | `ကျပ် ၁၂၃၄` is spoken as |
|---|---|---|
| `smart` | The default. Burmese digits as Burmese words — digit by digit after a leading zero and for runs of 9 or more digits, with ဒဿမ for a decimal point. Latin digits (1234) are left as written. | ကျပ် တစ်ထောင့်နှစ်ရာ့သုံးဆယ့်လေး |
| `place` | Every number, in Burmese or Latin digits, as Burmese place words (up to 8 digits; longer runs digit by digit). | ကျပ် တစ်ထောင့်နှစ်ရာ့သုံးဆယ့်လေး |
| `digits` | Burmese, one digit at a time: phone numbers, codes, IDs. | ကျပ် တစ် နှစ် သုံး လေး |
| `english` | English number words. | ကျပ် one thousand two hundred thirty four |
| `english_digits` | English, one digit at a time. | ကျပ် one two three four |

More of what `smart` does:

- `ဖုန်း ၀၉၁၂၃` → ဖုန်း သုည ကိုး တစ် နှစ် သုံး
- `၃.၅ ကီလို` → သုံး ဒဿမ ငါး ကီလို
- `ကျပ် 1234` → ကျပ် 1234
- `၁၂၃၄၅၆၇၈၉` → တစ် နှစ် သုံး လေး ငါး ခြောက် ခုနစ် ရှစ် ကိုး

So in `smart` mode, Latin digits stay as written: write the number in Burmese digits, or pick `place`, `digits`, `english` or `english_digits` when the text has Latin digits. Phone numbers, codes and IDs read best with `digits`. The written-out text must stay within 10,000 characters; beyond that the job is refused with `text_too_long`.

**Limits.** At most 5,000 characters per job. Split anything longer.

**Scripts, dialogue and video.** Split long scripts into sentence-sized lines, one job per line, each with its speaker's voice. Every line then has its own audio file to place on a timeline, retime or replace, and fixing one line regenerates — and costs — only that line. `athanlab_speak_script` does exactly this.

## Casting voices for dialogue

1. Call `athanlab_list_voices`. Official voices come first (`source: athanlab`), then the user's own library (`source: user`). `default_voice_id` is the voice a job uses when you name none.
2. Before a long or important script, let the user hear candidates: `athanlab_preview_voice` returns a sample URL (valid about an hour); with `save: true` it also downloads the sample into the output folder. Previews are free.
3. **Map each speaker to one `voice_id` and keep it for the whole project.** Pass the map as `voices` to `athanlab_speak_script` (for example `{"narrator": "<voice id>", "Aye": "<voice id>"}`) and reuse the same map for later scenes, retakes and sequels. Never let a character change voice between lines or sessions. `manifest.json` records the `voice_id` of every line, so read it back before adding scenes.
4. **Contrast.** Give characters who talk to each other clearly different voices, and keep the narrator distinct from every character. Use the voice names and categories, and the previews — do not guess a voice from its name alone. If there are more characters than distinct voices, tell the user rather than silently reusing one.
5. A line with its own `voice_id` overrides the speaker map; use that only for a deliberate exception.
6. Use only voices from `athanlab_list_voices`. Voices in a user's library follow AthanLab's consent rules — their own voice, or one they have the speaker's explicit permission to use — so never suggest cloning or imitating a real person.

## One clip: `athanlab_speak`

Pass `text` and optionally `voice_id`, `output_format` (`wav` by default, or `mp3` for smaller files), `number_mode` and `filename`. It returns the local `path`, the `job_id`, `duration_seconds`, `characters_charged` and the `output_format`. `speech_start_seconds`, `speech_end_seconds` and `segments` (speech timing) are null until the API supports speech timing; when they are filled in, prefer them for tight subtitle cues.

Files go into the server's output folder only; a `filename` is reduced to a plain name with the right extension.

Speaking speed (`speed`) and pronunciation respellings (`replacements`) appear as parameters only once the API supports them. If a tool's input schema has no such parameter, the feature is not available yet: respell words in the text itself instead.

If `athanlab_speak` returns a `job_id` but no file (it stopped waiting, or the download failed), the job is already paid for. **Do not call `athanlab_speak` again** — that creates and charges a second job. Check it with `athanlab_get_job` and fetch it with `athanlab_download_audio` once it has succeeded.

## Scripts: `athanlab_speak_script` → `manifest.json`

1. Write the script as `lines`: each `{id, speaker, text}` (plus `voice_id` only for an exception). One sentence or one short turn per line, at most 200 lines per call. Give every line a short `id` such as `l003` (it becomes part of the file name) and always set `speaker`.
2. Pass `voices` (speaker → voice_id), `output_format`, and **always an `out_subdir`** per scene or project, for example `ep01-scene02`. Without it the folder name is derived from the script's content, so any edit starts a fresh folder and pays for every line again.
3. Call it without `confirm` first. For a large script it returns `status: "confirmation_required"` with the quote and creates nothing; show the quote to the user and call again with the same arguments and `confirm: true` once they agree.
4. It runs as many lines at once as the account allows, writes one audio file per line, and writes `manifest.json` in the same folder. `status` is `completed`, `partial` (some lines failed), `stopped` (an account problem, see below) or `timed_out` (still running when it stopped waiting — call again with the same arguments to finish).

**Resuming and editing.** Re-run the same call with the same `out_subdir`: finished lines are skipped and not charged again, jobs still running are picked up, and only the rest are created — so an interrupted script just finishes. Lines are matched by position:

- A line whose text, voice or number mode changed is regenerated, and charged, automatically.
- Inserting or removing a line in the middle shifts every later line and regenerates them all. Add new lines at the end, or give a new scene its own `out_subdir`.
- A deleted audio file is downloaded again from the same job, free, within AthanLab's 30-day retention. After that the audio is gone: the next run quotes the line again as a new one (a new charge, counted towards the confirmation threshold like any other line), so show that quote to the user too. For a genuinely new take of an unchanged line, remove that line's entry from `manifest.json` and re-run (a new charge).
- Renaming a line (a new `id`) does not regenerate it: its audio is downloaded again under the new file name, free.

**One call per folder at a time.** Never run `athanlab_speak_script` twice in parallel for the same `out_subdir`. A second call while the first is still running returns `script_running` and does nothing; wait for the first call to return, then re-run if anything is left.

**When it stops early.** It stops creating new jobs on `plan_required`, `insufficient_characters` or `key_budget_exceeded` and reports which lines are done. Tell the user what to do (see Errors below), and re-run after they have fixed it; finished lines are kept.

**The timeline.** Each manifest entry has `index`, `id`, `speaker`, `voice_id`, `text`, `file`, `job_id`, `characters_charged`, `duration_seconds`, `start_seconds` and `end_seconds`. All lines sit on one timeline starting at 0: the next line starts `gaps.sentence` seconds (default 0.4) after the previous one ends when the speaker stays the same, and `gaps.turn` seconds (default 0.55) when the speaker changes. Example:

| Line | Speaker | Duration | Gap before | start → end |
|---|---|---|---|---|
| 0 | narrator | 2.10 s | — | 0.000 → 2.100 |
| 1 | narrator | 1.80 s | sentence 0.40 | 2.500 → 4.300 |
| 2 | Aye | 1.50 s | turn 0.55 | 4.850 → 6.350 |
| 3 | Aye | 1.20 s | sentence 0.40 | 6.750 → 7.950 |
| 4 | narrator | 2.00 s | turn 0.55 | 8.500 → 10.500 |

Slower, more deliberate pacing: raise the gaps (for example `{"sentence": 0.6, "turn": 0.9}`). Snappier dialogue: lower them.

**One track.** With `output_format: "wav"` and `combine: true` it also writes `combined.wav`: every line laid end to end with exactly those gaps as silence, so the manifest times are times in `combined.wav`. Combining needs WAV; for an MP3 deliverable, combine in WAV and convert afterwards.

## Send to AthanInbox: `athanlab_send_to_inbox`

AthanInbox is the inbox of the user's AthanLab account, on the web at https://athanlab.com/dashboard/inbox. `athanlab_send_to_inbox` sends a title and its text there, on the account that owns the API key — never anyone else's. AthanLab makes the audio in the background, and the user listens in AthanInbox on athanlab.com, where the progress shows meanwhile. AthanInbox comes to the AthanLab mobile app in a later update: until then, do not promise a phone notification or tell the user to look in the app.

- **When.** Use it when the user wants to listen later: “send it to my AthanLab”, “put it in my inbox”, a long article, a chapter of a book, a daily briefing. If they ask for it on their phone, say AthanInbox is on athanlab.com for now (a phone browser works at https://athanlab.com/dashboard/inbox) and comes to the AthanLab mobile app in a later update. For an audio file on this machine (a video, an edit, a podcast track), use `athanlab_speak` or `athanlab_speak_script`: AthanInbox gives you no local file.
- **Only if the tool is there.** If `athanlab_send_to_inbox` is not in your tool list, tell the user AthanInbox is not available here and offer local files instead.
- **Write for speech.** Every rule in “Writing text for speech” applies. Give it a short `title` on one line: it is what the user sees in AthanInbox.
- **Text or chapters.** Pass `text`, and anything over 5,000 characters is split into chapters at paragraph and sentence ends — put a blank line between sections so the chapters break there. Or pass your own `chapters` (up to 50, each with an optional `title`) at the natural breaks of a book or an article. One delivery takes at most 100,000 characters; send more as a second delivery.
- **Sources.** When you summarise articles or pages, pass them as `sources` (https links, with a short title each): the user can open them from the delivery. AthanLab never fetches them.
- **Quote, ask, confirm.** Above 2,000 characters or 20 chapters the tool returns `status: "confirmation_required"` with a quote and sends nothing. Show the user the characters, the chapters and what they can spend, and get an explicit yes before calling again with the same arguments and `confirm: true`.
- **It returns at once.** The result has `delivery_id`, `status` (`queued` at first) and `app_url`. Tell the user it is in AthanInbox on athanlab.com, and give `app_url` as the link to open on a computer. Do not poll with `athanlab_get_delivery` unless they ask how it is going; `wait_seconds` (up to 300) waits inside the call when they want to hear back now.
- **Never send it twice.** Calling again with the same arguments within 24 hours returns the first delivery (`replayed: true`) and charges nothing, so a retry is safe. Use `send_again: true` only when the user explicitly asks for another copy: it is charged again.
- **It stays in AthanInbox** until the user deletes it there; it does not expire after 30 days like API jobs.
- **Partial results.** If `status` is `partially_succeeded`, some chapters failed and were refunded. Offer to send only the failed chapters again as a new delivery (pass them as `chapters`).
- **Limits.** An account has at most 3 deliveries in progress at a time. Another one is refused with `concurrency_limit` and nothing is charged: wait until one is ready, then send it again with the same arguments.

## Subtitles from the manifest

Make one cue per manifest line, from `start_seconds` to `end_seconds`, with that line's `text` (the text that was spoken). Sort by `start_seconds`.

SRT (`HH:MM:SS,mmm`, comma before the milliseconds):

```
1
00:00:00,000 --> 00:00:02,100
မင်္ဂလာပါ။

2
00:00:02,500 --> 00:00:04,300
အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။
```

WebVTT is the same cues without the numbers, under a `WEBVTT` first line and with a dot before the milliseconds (`00:00:02.500 --> 00:00:04.300`); a `<v narrator>` prefix on the cue text names the speaker.

- The times are file spans: a cue starts with the audio file, which can begin with a short silence. If the job's speech timing is available (non-null `speech_start_seconds`), add it to `start_seconds` for a tighter cue.
- Keep cues readable: at most two short lines per cue. Short script lines make short cues; if one line is long, break the cue at a `၊` or a space, or split it into two cues dividing the time in proportion to the characters.
- Times are in seconds from the start of `combined.wav`, or of the voice track if you place the files yourself. If the voice starts later in the video, add that offset to every cue.

## Placing audio in a video

- **Simplest:** use `combined.wav` as the voice-over track and the SRT/VTT built from the same manifest.
- **Per line:** place each line's file at its `start_seconds` on the voice track of the editor or composition. Retime freely; to change words, regenerate only that line (see Resuming).
- **ffmpeg**, if available: add the voice to a video with `ffmpeg -i video.mp4 -i combined.wav -map 0:v -map 1:a -c:v copy -af apad -c:a aac -shortest out.mp4`. `apad` pads the voice with silence so the video keeps its full length; a voice track longer than the video is cut at its end. To burn subtitles in, use the `subtitles=subs.srt` filter with a font that has Burmese glyphs (for example `force_style='FontName=Noto Sans Myanmar'`).

## Errors and retry etiquette

The tools already retry what is safe to retry — rate limits, the concurrency limit, server restarts and DigitalOcean's 504 page that stands in for a 503 — with the same Idempotency-Key, so a retry can never create a second job. **Do not wrap them in your own retry loop.** When a tool reports an error, read its code and action line:

| Code | Meaning | What to do |
|---|---|---|
| `plan_required` (402) | Keys are read-only: no Max plan and no founder access | Tell the user; existing jobs, downloads and usage still work |
| `insufficient_characters` (402) | Not enough characters; nothing was charged | Top up Athan Tokens or wait for the monthly reset |
| `key_budget_exceeded` (429) | This key hit its monthly budget; nothing was charged | The owner raises the budget on the Developer API page — do not retry |
| `invalid_api_key` (401); `missing_api_key`, `invalid_api_key_format` (checked locally, nothing sent) | The key is wrong, revoked, expired, missing or mistyped | Never retry: 20 failed key checks in 5 minutes block the whole network for 10 minutes. Ask the user to fix `ATHANLAB_API_KEY` and restart the client |
| `scope_missing` (403) | The key lacks a permission | The key needs `speech:write`, `speech:read`, `voices:read` or `usage:read` |
| `developer_pending`, `developer_rejected`, `developer_suspended`, `account_suspended` (403) | Account status | Tell the user; see the Developer API page or support@athanlab.com |
| `unspeakable_text`, `text_too_long`, `validation_error` (422/400) | The request needs changing | Fix the text or parameters (see Writing text), then call again |
| `concurrency_limit` (429) | Too many jobs running, or (from `athanlab_send_to_inbox`) 3 AthanInbox deliveries already in progress | Jobs: handled automatically; do not start parallel scripts on one account. Deliveries: nothing was charged; wait until one is ready, then send again with the same arguments |
| `too_many_chapters` (checked locally, nothing sent) | The delivery needs more chapters than one delivery takes (50 of 5,000 characters) | Merge short chapters, shorten the text, or send the rest as a second delivery |
| `deliveries_unavailable` (checked locally) | AthanInbox is not available on the API right now; nothing was sent or charged | Tell the user; offer local files with `athanlab_speak` or `athanlab_speak_script`, or try later |
| `payload_too_large` (413, or checked locally) | The request is too large | Send a shorter text, or split a delivery into two |
| `script_running` (checked locally, nothing sent) | Another `athanlab_speak_script` call is already working on this `out_subdir` | Wait for that call to return, then call again with the same arguments; never start the same script twice at once |

- A job that ends `failed` is refunded in full; resubmitting it is a new job and a new charge.
- Follow work in progress with `athanlab_list_jobs` and `status: "processing"` rather than polling jobs one by one. Cancel jobs the user no longer wants with `athanlab_cancel_job`.
- Retention: API jobs keep their audio and input text for 30 days (`410 audio_expired` after that), so keep the downloaded files. AthanInbox deliveries stay until the user deletes them in AthanInbox.
- Never ask the user to paste an API key into the conversation: it belongs in the MCP server's environment. If a tool says the key is missing or malformed, ask the user to set `ATHANLAB_API_KEY` and restart the client.
- Agent keys should have a low monthly budget, a short expiry and only the permissions they need. A `key_budget_exceeded` stop is that safety net working, not something to work around.
