Фоновый синтез длинного текста¶
Фоновая TTS-операция предназначена для лекций, сценариев и других длинных текстов. Сервер принимает до 40 000 символов, один раз нормализует весь исходный текст, выделяет предложения и смысловые границы, упаковывает их в фрагменты с жёстким лимитом 500 символов, синтезирует последовательно и собирает один WAV. Паузы выбираются по типу границы и учитывают тишину, которую уже вернула TTS-модель.
В отличие от обычного POST /api/tts/v1, HTTP-соединение не нужно
держать открытым до конца синтеза. Клиент получает id, опрашивает состояние,
а затем скачивает полный или частичный результат. Обновление страницы не
останавливает задачу: вернуться к ней можно по сохранённому id.
Как формируется аудио¶
Сервис сохраняет порядок исходного текста, делит его по смысловым границам и подбирает паузы между соседними фрагментами. Числа, даты и сокращения могут быть нормализованы для естественного произношения. Служебная разметка Higgs и HTML не разрезается посередине.
Короткие соседние абзацы могут быть объединены, а заголовок начинает новый раздел. Для диалога каждая роль обрабатывается отдельно, поэтому реплики разных голосов не смешиваются. На стыках сервис учитывает естественную тишину и мягко выравнивает громкость соседних фрагментов.
Совместимость с Yandex SpeechKit¶
Это расширение Speech Expert и не wire-compatible с Yandex SpeechKit TTS v3.
В Yandex методы
UtteranceSynthesis
и
StreamSynthesis
возвращают поток синтезируемого аудио в рамках текущего gRPC-вызова:
UtteranceSynthesis использует server streaming, а StreamSynthesis —
двунаправленный streaming. Ни один из этих TTS-методов не возвращает
долгоживущий объект Operation.
Формат объекта операции похож на используемый в SpeechKit для фоновых задач, но маршруты, параметры, публичный token и семантика возобновления относятся только к Speech Expert.
Сценарий работы¶
sequenceDiagram
participant C as Клиент
participant A as Speech Expert API
participant W as Фоновый TTS
C->>A: POST /operations
A-->>C: Operation {id, done: false}
A->>W: поставить задачу в очередь
loop Пока done=false
C->>A: GET /operations/{id}
A-->>C: прогресс и число готовых фрагментов
end
W-->>A: полный WAV или частичный WAV
C->>A: GET /operations/{id}/audio
A-->>C: audio/wav
Если пользователь останавливает синтез, вызовите :cancel. Сервер завершит
операцию, соберёт уже готовые последовательные фрагменты и сделает их доступными
через тот же маршрут /audio. Позже отменённую или ошибочную операцию можно
продолжить через :resume.
Маршруты¶
| Действие | Приватный API | Публичный API |
|---|---|---|
| Создать | POST /api/tts/v1/operations |
POST /api/public/tts/v1/operations |
| Получить статус | GET /api/tts/v1/operations/{id} |
GET /api/public/tts/v1/operations/{id} |
| Остановить | POST /api/tts/v1/operations/{id}:cancel |
POST /api/public/tts/v1/operations/{id}:cancel |
| Продолжить | POST /api/tts/v1/operations/{id}:resume |
POST /api/public/tts/v1/operations/{id}:resume |
| Скачать WAV | GET /api/tts/v1/operations/{id}/audio |
GET /api/public/tts/v1/operations/{id}/audio |
Приватные маршруты требуют API-ключ или активную cookie-сессию портала.
Операция доступна только создавшему её пользователю; обращение другого
пользователя возвращает 404.
Публичные маршруты доступны без аккаунта только на выделенном домене
ai-consilium.ru. При создании возвращается секретный token. Во всех
последующих запросах его нужно передавать в заголовке:
Token возвращается только один раз. Сохраните его вместе с id, не помещайте в
URL и не записывайте в публичные логи. Отсутствующий или неверный token даёт
404, чтобы не раскрывать существование операции.
Создание приватной операции¶
| Поле формы | Обязательное | По умолчанию | Описание |
|---|---|---|---|
text |
условно | — | Одноголосый текст и поддерживаемая Higgs-разметка; до 40 000 символов по умолчанию. Передайте ровно одно из полей text и script |
script |
условно | — | JSON-сценарий с ролями и репликами; передайте ровно одно из полей text и script |
voice |
условно | — | Идентификатор из GET /api/tts/v1/voices для одноголосого text. В режиме script голос задаётся у каждой роли, а верхнеуровневое поле voice недопустимо |
lang |
нет | ru-RU |
Язык; фактическая поддержка зависит от выбранного голоса |
speed |
нет | 1.0 |
Скорость от 0.1 до 3.0 |
pause_between_ms |
нет | 300 |
Базовый масштаб смысловых пауз, от 0 до 5000 мс; при адаптивном режиме это не фиксированная добавка |
chunk_max_characters |
нет | — | Принимается для совместимости с UI, но игнорируется; размер задаёт сервер |
format |
нет | wav |
Сейчас допустим только wav |
sampleRateHertz |
нет | 48000 |
8000 или 48000; WAV каждого фрагмента пересэмплируется до склейки |
bit_resolution |
нет | 16 |
Допустимо только 16 |
audio_channels |
нет | mono |
Допустимо только mono |
pcm_format |
нет | LINEAR16 |
Допустимо только LINEAR16 |
Поле ssml в фоновом методе отсутствует. Поддерживаемые Higgs-теги можно
передавать непосредственно в text.
При TTS_ADAPTIVE_PAUSE_ENABLED=true значение pause_between_ms=300 даёт
ориентиры 100 мс внутри вынужденно разделённой фразы, 220 мс после
предложения, 350 мс после абзаца и 500 мс после раздела. Другой базовый
параметр пропорционально масштабирует эти значения. При сборке сервер измеряет
тихие хвост и начало соседних WAV и добавляет только недостающую тишину; после
последнего фрагмента пауза не добавляется. Значение 0 отключает добавляемые
межфрагментные паузы.
Для телефонных сценариев передайте sampleRateHertz=8000: результат и любая
доступная после остановки часть будут WAV PCM16, 8 кГц, mono. Частота не влияет
на расчёт стоимости; списание по-прежнему выполняется по длительности готовых
сегментов.
curl -sS -X POST "https://api.speech.example.com/api/tts/v1/operations" \
-H "Authorization: Api-Key $API_KEY" \
--form-string "text=Длинный текст лекции..." \
--form-string "voice=elena-speech-expert-tts" \
--form-string "speed=1.0" \
--form-string "pause_between_ms=300" \
--form-string "sampleRateHertz=8000"
Синтез по ролям¶
Приватный endpoint также принимает структурированный сценарий в form-поле
script. Он предназначен для диалогов, подкастов и постановочного чтения, где
разные реплики нужно последовательно озвучить разными голосами и собрать в один
WAV. Сценарий содержит до 8 ролей и до 500 реплик:
{
"version": 1,
"roles": [
{
"id": "host",
"name": "Ведущий",
"voice": "serg-speech-expert-tts"
},
{
"id": "expert",
"name": "Эксперт",
"voice": "elena-speech-expert-tts"
}
],
"segments": [
{
"roleId": "host",
"text": "Сегодня обсуждаем синтез речи по ролям."
},
{
"roleId": "expert",
"text": "Начнём с главного: двоеточие внутри реплики не меняет голос."
}
]
}
roles[].id должны быть уникальны, каждая реплика обязана ссылаться на
существующую роль, а каждый voice — быть доступен текущему пользователю. Можно
использовать как встроенные, так и собственные готовые голоса. Общий лимит
считается по произносимому тексту segments[].text и по умолчанию равен 40 000
символов.
В веб-интерфейсе тот же сценарий вводится проще:
[Ведущий]
Сегодня обсуждаем синтез речи по ролям.
[Эксперт]
Начнём с главного: двоеточие внутри реплики не меняет голос.
Маркеры в квадратных скобках являются форматом редактора, а не произносимым текстом: UI преобразует их в структурированный JSON перед отправкой. Роль распознаётся только по маркеру в начале строки, поэтому обычные двоеточия, списки и Markdown не создают смену говорящего.
Пример запроса:
ROLE_SCRIPT='{"version":1,"roles":[{"id":"host","name":"Ведущий","voice":"serg-speech-expert-tts"},{"id":"expert","name":"Эксперт","voice":"elena-speech-expert-tts"}],"segments":[{"roleId":"host","text":"Сегодня обсуждаем синтез речи по ролям."},{"roleId":"expert","text":"Начнём с главного: это одна реплика."}]}'
curl -sS -X POST "https://api.speech.example.com/api/tts/v1/operations" \
-H "Authorization: Api-Key $API_KEY" \
--form-string "script=$ROLE_SCRIPT" \
--form-string "speed=1.0" \
--form-string "pause_between_ms=300"
Каждая реплика нормализуется и планируется отдельно: короткие предложения и абзацы одной реплики могут войти в общий фрагмент, но границы разных ролей никогда не объединяются. Остановка, частичное скачивание и продолжение сохраняют правильный порядок и звучание. Списание остаётся посегментным и учитывает фактический голос фрагмента.
Ответ — объект операции:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Long-form TTS synthesis",
"createdAt": "2026-08-05T14:00:00+00:00",
"createdBy": "user@example.com",
"modifiedAt": "2026-08-05T14:00:00+00:00",
"done": false,
"metadata": {
"kind": "tts_v1_long_async",
"status": "queued",
"progress_stage": "queued",
"progress_percent": 0,
"completed_chunks": 0,
"total_chunks": 12,
"current_chunk": 1,
"partial_available": false,
"result_available": false,
"resumable": false,
"text_length": 5730,
"pause_between_ms": 300,
"adaptive_pauses": true,
"chunk_planner": "pending",
"public": false,
"filename": "synthesis.wav",
"partial": false,
"format": "wav",
"sample_rate_hertz": 48000,
"bit_resolution": 16,
"audio_channels": "mono",
"pcm_format": "LINEAR16"
},
"response": null,
"error": null
}
Создание публичной операции¶
Публичный endpoint использует один голос по референсу, поэтому поля
reference_audio и reference_text обязательны. Поля voice нет.
Запрос должен быть multipart/form-data.
Ролевые сценарии этим endpoint не поддерживаются. Переданное поле script
возвращает 400.
| Поле формы | Обязательное | Ограничение |
|---|---|---|
text |
да | До 40 000 символов по умолчанию |
reference_audio |
да | Аудио от 1 до 19 секунд; upload до 16 МиБ |
reference_text |
да | Точная расшифровка референса, до 1000 символов |
lang |
нет | По умолчанию ru-RU |
speed |
нет | От 0.1 до 3.0, по умолчанию 1.0 |
pause_between_ms |
нет | Базовый масштаб адаптивных пауз от 0 до 5000, по умолчанию 300 |
seed |
нет | Целое число от 0 до 2^63 - 1 |
max_new_tokens |
нет | Верхний предел от 1 до 2048; фактический бюджет может быть меньше для отдельных фрагментов |
temperature |
нет | От 0 до 5 |
top_p |
нет | Больше 0 и не больше 1 |
top_k |
нет | Целое число от 0 до 1000 |
lora |
нет | none, base или публичный адаптер voice1 |
lora_strength |
нет | От 0 до 1; для voice1 значение по умолчанию — 0.25 |
Поля выходного формата и chunk_max_characters принимаются по тому же
контракту, что в приватном методе. Результат всегда WAV PCM16 mono с выбранной
частотой 8 или 48 кГц.
curl -sS -X POST "https://ai-consilium.ru/api/public/tts/v1/operations" \
--form-string "text=Длинный текст лекции..." \
-F "reference_audio=@reference.wav" \
--form-string "reference_text=Точный текст, произнесённый в референсе." \
--form-string "pause_between_ms=300"
В ответе рядом с полями операции находится token:
{
"id": "0c18257f-4929-4ceb-9410-609541a6f20e",
"done": false,
"metadata": {
"kind": "tts_v1_long_async",
"status": "queued",
"public": true
},
"response": null,
"error": null,
"token": "секретное-значение"
}
Статус и прогресс¶
# Приватная операция
curl -sS \
"https://api.speech.example.com/api/tts/v1/operations/$OPERATION_ID" \
-H "Authorization: Api-Key $API_KEY"
# Публичная операция
curl -sS \
"https://ai-consilium.ru/api/public/tts/v1/operations/$OPERATION_ID" \
-H "X-TTS-Operation-Token: $OPERATION_TOKEN"
Основные поля:
| Поле | Значение |
|---|---|
done |
false во время очереди/синтеза, true при успехе, отмене или ошибке |
metadata.status |
queued, running, completed, cancelled или failed |
metadata.progress_stage |
Текущий этап: queued, preprocessing, synthesizing, adaptive_split, interrupted, completed, cancelled или failed |
metadata.progress_percent |
Процент от 0 до 100; терминальная ошибка и отмена также дают 100 |
completed_chunks / total_chunks |
Число завершённых и общее число серверных фрагментов |
current_chunk |
Номер текущего фрагмента, начиная с 1 |
chunk_planner |
Использованный планировщик: llm, deterministic, mixed, legacy или pending |
text_normalization |
Результат однократной подготовки исходника: changed, unchanged или pending |
adaptive_pauses |
Включён ли подбор пауз по типам смысловых границ и краевой тишине |
partial_available |
Есть хотя бы один готовый последовательный фрагмент |
result_available |
Готов полный WAV |
resumable |
Операцию можно продолжить через :resume |
duration_ms |
Длительность собранного полного или частичного WAV, когда она уже известна |
partial |
Доступный результат является неполным |
Не используйте только done или progress_percent для определения успеха.
Проверяйте metadata.status == "completed" и отсутствие error.
При успешном завершении:
{
"done": true,
"metadata": {
"status": "completed",
"progress_percent": 100,
"partial_available": true,
"result_available": true,
"resumable": false,
"duration_ms": 184320,
"partial": false
},
"response": {
"audio_url": "/api/tts/v1/operations/550e8400-e29b-41d4-a716-446655440000/audio",
"partial": false,
"duration_ms": 184320
},
"error": null
}
Остановка и частичный результат¶
curl -sS -X POST \
"https://api.speech.example.com/api/tts/v1/operations/$OPERATION_ID:cancel" \
-H "Authorization: Api-Key $API_KEY"
Публичный вариант использует публичный URL и
X-TTS-Operation-Token. Отмена идемпотентна: для уже завершённой операции
возвращается её текущее состояние.
После отмены done=true, metadata.status="cancelled" и
metadata.resumable=true. Если успел завершиться хотя бы один фрагмент,
сервер собирает частичный WAV, выставляет partial_available=true и возвращает:
{
"done": true,
"metadata": {
"status": "cancelled",
"partial_available": true,
"result_available": false,
"resumable": true,
"partial": true
},
"response": {
"audio_url": "/api/tts/v1/operations/550e8400-e29b-41d4-a716-446655440000/audio",
"partial": true,
"duration_ms": 42750
},
"error": {
"code": 499,
"message": "TTS operation was cancelled",
"details": [{"type": "Cancelled"}]
}
}
Незавершённый текущий фрагмент в частичный файл не попадает. Если ни один
фрагмент ещё не готов, response=null, а скачивание вернёт 409.
Во время активного синтеза счётчик готовых фрагментов уже может быть больше
нуля, но скачиваемый partial гарантированно собирается при отмене или ошибке.
Возобновление¶
curl -sS -X POST \
"https://api.speech.example.com/api/tts/v1/operations/$OPERATION_ID:resume" \
-H "Authorization: Api-Key $API_KEY"
Метод продолжает отменённую или ошибочную операцию с первого незавершённого
фрагмента. Уже оплаченные и сохранённые фрагменты повторно не синтезируются.
Для незавершённой активной операции вызов безопасен. Завершённая успешная
операция возвращает 409. Возобновление доступно до очистки данных операции по
TTL.
Скачивание¶
curl -fS \
"https://api.speech.example.com/api/tts/v1/operations/$OPERATION_ID/audio" \
-H "Authorization: Api-Key $API_KEY" \
--output synthesis.wav
Ответ имеет Content-Type: audio/wav и один из вариантов имени:
tts-{id}-complete.wav— полный результат;tts-{id}-partial.wav— частичный результат.
Заголовок X-TTS-Partial: true|false позволяет определить вариант без
анализа имени файла. Для приватного ответа установлен
Cache-Control: private, no-store, для публичного — Cache-Control: no-store.
Если собранного аудио пока нет, сервер возвращает 409.
Возврат к операции¶
Операция выполняется на сервере и не зависит от вкладки браузера. Для
восстановления UI достаточно сохранить id, а для публичного режима также
token. Встроенный интерфейс хранит эти значения и параметры формы в
localStorage до 24 часов; сами аудиобайты там не хранятся.
Хранение данных¶
По умолчанию исходный текст, аудиофрагменты, итоговый файл и публичный голосовой референс хранятся 24 часа после завершения или остановки операции. Очистка выполняется периодически, поэтому фактический момент удаления может быть немного позже.
После очистки:
- исходные и результирующие файлы удаляются;
responseочищается;partial_available,result_availableиresumableстановятсяfalse;- в metadata появляются
content_purged=trueиcontent_purged_at.
Запись приватной операции остаётся доступной владельцу для истории, но скачать
или продолжить её уже нельзя. После TTL публичные маршруты возвращают 404.
Владелец приватной операции может удалить аудио и данные для восстановления
раньше TTL через POST /api/operations/{id}:purge. Удаление необратимо; после
него скачивание и :resume недоступны. Для публичных анонимных операций
отдельного ручного purge-маршрута нет — их данные удаляет TTL-очистка.
Лимиты и тарификация¶
| Ограничение | Значение |
|---|---|
Длина text |
40 000 символов |
| Жёсткий максимум фрагмента | 500 символов |
| Хранение результата и референса | 24 часа |
| Публичные незавершённые операции | 1 на клиента |
| Публичные операции до TTL-очистки | 2 на клиента |
| Публичные create-запросы | 30 за 60 секунд на IP |
| Публичный статус, остановка и продолжение | 90 запросов за 60 секунд на IP |
| Публичное скачивание аудио | 6 запросов за 60 секунд на IP |
| Полный размер публичного request body | 20 МиБ |
| Публичный референс | До 16 МиБ и от 1 до 19 секунд |
Для приватной операции стоимость учитывается по фактической длительности каждого готового фрагмента. При отмене незавершённая часть не списывается, уже готовые фрагменты остаются оплаченными, а добавленная между ними тишина не тарифицируется. Публичный анонимный режим не использует кредиты аккаунта.
Лимит сохранённых публичных операций учитывает активные и завершённые задачи до их автоматической очистки. После удаления данных слот освобождается.
При недостатке кредитов приватная операция завершится ошибкой. Если к этому моменту есть готовые фрагменты, их можно скачать как partial и продолжить операцию после пополнения баланса.
Ошибки¶
| HTTP-код | Когда возникает |
|---|---|
400 |
Пустой/слишком длинный текст, неверные параметры или референс, неподдерживаемый формат |
401 |
Нет приватной аутентификации |
404 |
Операция не найдена, принадлежит другому пользователю либо неверен публичный token |
409 |
Аудио ещё не собрано либо операцию нельзя возобновить: она успешно завершена, истекла или её данные очищены |
429 |
Превышен публичный rate limit или допустимое число операций |
Ошибка самого фонового синтеза записывается в объект операции: done=true,
metadata.status="failed", а error.code содержит код ошибки синтеза. Поэтому
успешный HTTP-ответ на запрос статуса ещё не означает,
что синтез завершился успешно.