Перейти к содержанию

Фоновый синтез длинного текста

Фоновая 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. Во всех последующих запросах его нужно передавать в заголовке:

X-TTS-Operation-Token: <token>

Token возвращается только один раз. Сохраните его вместе с id, не помещайте в URL и не записывайте в публичные логи. Отсутствующий или неверный token даёт 404, чтобы не раскрывать существование операции.

Создание приватной операции

POST /api/tts/v1/operations
Authorization: Api-Key <ключ>
Content-Type: multipart/form-data
Поле формы Обязательное По умолчанию Описание
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-ответ на запрос статуса ещё не означает, что синтез завершился успешно.

См. также