Skip to content
Lumean API

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.

Translation in progress

This page has not been translated into your language yet — the version below is in English. The text and the code samples are the same.

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:

KindExamplesWhat it doesFree
Pause{{pause=2.5}}, {{pause:500ms}}, {{pause}}Silence; without a value, 1 secondYes
SSML pause<break time="2s"/>The same in SSML notationUnclear, see below
Direction to the narrator[шёпот], [confident], [pause]A hint about the reading styleUnclear, 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.

Gotcha

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.

422text_emptytext_empty — nothing to voice
What happened
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`.
What to do right now
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.
Open the code page

Normalization before chunking

Chunk offsets (start/end) are counted from the modified text, not from the text you sent:

WhatHow
\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[].status has its own vocabulary of four values (pending, in_progress, failed, completed); a chunk blocked by policy collapses into failed;
  • only the original chunks (item_type = chunk) are counted: a successful retry of a failed chunk does not change the paragraph status. retry and regeneration records have paragraph_index set to null — get the paragraph from the parent via parent_item_id.

Careful

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.

Do

Run the text through the preview and check voiced_length and total_price_units against your expectations.

Don't

Estimate the cost from the string length: markup, the minimum billable volume, and paragraph mode move the figure in both directions.

Feed this page to an AI

Open .mdllm.md

Updated 04/09/2026 · spec 0.0.1