Voiceover text: markup, normalization, billing
What the server strips from the text before generation, what still gets voiced, what you pay for, and how to check all of it for free.
Übersetzung in Arbeit
The text of a TTS order does not go to the generator as is: markup is stripped first, then the
text is normalized, and only then is it split into chunks. Each step changes both what you hear
and what you pay for; none of them is visible in the POST /orders
response.
Three text channels, one set of rules
Text is accepted by three equivalent fields — input_text at the root of the request, a .txt
inside input_files[] (up to 20 MB per file), and task_data.text for template-less services;
all three go through the same checks. For TTS, put the text in the top-level input_text,
not in task_data.
What is stripped from the voiceover
Three kinds of markup are recognized; none of them is read aloud verbatim, but only one is free:
| Kind | Examples | What it does | Free |
|---|---|---|---|
| Pause | {{pause=2.5}}, {{pause:500ms}}, {{pause}} | Silence; without a value, 1 second | Yes |
| SSML pause | <break time="2s"/> | The same in SSML notation | Unclear, see below |
| Direction to the narrator | [шёпот], [confident], [pause] | A hint about the reading style | Unclear, see below |
The difference is not cosmetic: {{…}} is our format — the worker builds the silence from it,
and the string never reaches the provider. Brackets and <break/> are the provider's own
syntax: the provider receives them in full and counts them toward its length limit, so they
take up space in the chunk: a 999-character chunk with [confident] inside holds 988
spoken characters.
Stolperfalle
Sources disagree on whether […] and <break/> are billed, and the disagreement is inside
the spec itself. “Billed,” say the description of the billable_length field (“only our
{{pause=2s}} pause tags are free; [шёпот] and <break/> go to the provider”) and the LLM
reference (§8.3). “Not billed,” say the descriptions of summary.total_effective_length in the
preview and of the regenerate request body. Budget for the more expensive reading, and take
the exact figure from the response: the free preview computes total_price_units with the same
pipeline as the order, and billable_length in GET /orders/{order}/items/{item}/text is the
source of truth for a chunk.
Any square brackets are markup, not just a list of keywords
Any […] span with no nested brackets and no line break inside counts as a direction
(regex /\[[^\[\]\n]+\]/u). A footnote [1], a [sic] note, a timecode [00:12] will not
be spoken — the model will read them as directions about the reading style.
Parentheses, angle-bracket tags, and asterisks, by contrast, are voiced and billed: the
narrator will read (пауза) and *шёпотом* aloud.
Pause caps
They are checked before the order is created; exceeding them returns 422, and no money is
reserved. No more than 500 pauses per text, no single pause longer than 10 seconds, and
no more than 3600 seconds of silence in total. Only {{pause}} and <break/> count; the
legacy form [pause=2s] does not.
A separate rejection applies to text with nothing pronounceable left after markup is stripped:
only tags → text_empty; only punctuation or only bracketed remarks → text_not_voiceable
(named paragraph_not_voiceable in the OpenAPI spec). Both are 422.
- Was passiert ist
- 422 with `reason: "text_empty"`. The order was not created and no money is reserved. When there is no text at the input in the first place (an empty field), the same case arrives differently — as ordinary validation, with the field in `errors` and no `reason`.
- Was jetzt zu tun ist
- Check whether the body has `errors`: if it does, the field simply was not filled in; if not, the text arrived but consisted entirely of markup. Parentheses and angle brackets, asterisks, and other typography, on the contrary, ARE voiced and billed — removing them will not help.
Normalization before chunking
Chunk offsets (start/end) are counted from the modified text, not from the text you sent:
| What | How |
|---|---|
\r\n, \r | → \n |
| Tab | → space |
| More than 3 line breaks in a row | → exactly 3 |
| More than 5 spaces in a row | → exactly 5 |
Привет.Мир | → Привет. Мир (voiceover only) |
The last rule (a space after ./!/? before a letter or digit) leaves decimals,
abbreviations, acronyms, URLs with a scheme, emails, and tag contents untouched. The trade-off:
a bare domain without a scheme (example.com mid-sentence) is indistinguishable from
Привет.Мир and will get a space — write such addresses with a scheme.
Paragraph mode (generation_mode)
By default, the text is cut sequentially by the technical chunk size, and paragraph boundaries
play no role. Paragraph mode first splits the text on blank lines, and then each paragraph
is chunked on its own. The key lives not inside tts_settings but at the root of
config_override (or at the root of the template's config — then the mode is permanent):
{ "template_id": "<UUID>", "input_text": "Первый абзац.\n\nВторой абзац.",
"config_override": { "generation_mode": "paragraph" } }The values are paragraph and default. Supported by elevenlabs and heygen; other
services silently ignore the key — there is no rejection, the mode simply does not turn on. It
is not listed in GET /templates/config-options: that endpoint lists service settings, while
the mode is set per order. At most 10 single-chunk paragraphs plus 5 multi-chunk paragraphs run
concurrently per order.
The response gains paragraphs[] (for each paragraph: paragraph_index, chunks_count,
completed_chunks, status, progress_percent) and a paragraph_index on chunks. Pitfalls:
paragraphs[].statushas its own vocabulary of four values (pending,in_progress,failed,completed); a chunk blocked by policy collapses intofailed;- only the original chunks (
item_type = chunk) are counted: a successful retry of a failed chunk does not change the paragraph status.retryandregenerationrecords haveparagraph_indexset tonull— get the paragraph from the parent viaparent_item_id.
Vorsicht
The minimum billable volume applies here to each paragraph
separately. Five short paragraphs cost five minimums, whereas the same text in the default
mode would cost one. The threshold is visible in the preview: summary.min_billable_chars.
Calculate all of this in advance, for free
POST /orders/chunks/preview is a dry run of the same pipeline without creating an order: it
costs no money and does not spend the key's token quota. summary returns voiced_length
(what will actually be voiced), total_effective_length, total_price_units,
min_billable_chars, and three diagnostic flags: voiced_empty (a future text_empty),
caps_ok with caps_exceeded (count/single/total), and has_blocked_content with
blocked_chunks[] — the last one shows blocklisted chunks before the order is even created
(see retries). Unlike POST /orders, the preview does not
fail on caps.
Preview Chunk Split
Richtig
Run the text through the preview and check voiced_length and total_price_units against your
expectations.
Falsch
Estimate the cost from the string length: markup, the minimum billable volume, and paragraph mode move the figure in both directions.
Diese Seite einer KI geben
Aktualisiert 04.09.2026 · Spezifikation 0.0.1