AWS научила голосовых ИИ-агентов говорить с первого предложения на Kokoro-82M

AWS описала схему, при которой голосовой ИИ-агент не ждёт, пока модель дозаписывает весь ответ. Каждое готовое предложение сразу уходит в Kokoro-82M, развёрнутую на SageMaker AI, и озвучка начинается с первой фразы. Раньше клиент молчал около пяти секунд, хотя короткий ответ генерируется за 600 мс. Модель весит всего 82 млн параметров и работает на одном GPU ml.g5.xlarge. Между клиентом и контейнером держится одна двунаправленная WebSocket-сессия: аудио и JSON-команды идут по одному каналу, без base64 и лишних переподключений. • Base64 в SSE раздувает аудио примерно на 33%: MP3 48k — 308 304 байта, после кодирования 411 072 • Ошибка KeyError('text') возникает, потому что SageMaker шлёт только бинарные фреймы • Агент на Strands + Bedrock AgentCore в режиме bidi перекрывает генерацию и синтез

AI-обработка оригинала AWS Machine Learning Blog; редакция Hamidun News
AWS научила голосовых ИИ-агентов говорить с первого предложения на Kokoro-82M
Источник: AWS Machine Learning Blog. Коллаж: Hamidun News.
Слушать статью
Паспорт материала и цифры
ПараметрДанные
ПервоисточникAWS Machine Learning Blog ↗
Главный редактор
Ключевой факт #1Base64 в SSE раздувает аудио примерно на 33%: MP3 48k — 308 304 байта, после кодирования 411 072
Ключевой факт #2Ошибка KeyError('text') возникает, потому что SageMaker шлёт только бинарные фреймы
Ключевой факт #3Агент на Strands + Bedrock AgentCore в режиме bidi перекрывает генерацию и синтез

Пользователь задаёт вопрос голосовому агенту и ждёт. Ответ уже частично сгенерирован, но звука нет: интеграции text-to-speech обычно дожидаются конца всего текста. Одно предложение модель выдаёт за 600 мс, а длинный ответ целиком занимает около пяти секунд. Всё это время клиент слушает тишину. AWS в своём блоге показала, как убрать этот провал: слать каждое предложение в синтезатор Kokoro-82M на Amazon SageMaker AI, как только агент его закончил.

Малая модель, один GPU и два маршрута

Kokoro-82M содержит 82 млн параметров, и этого хватает, чтобы крутить её на одной видеокарточной инстанции среднего класса. Модель запускается в кастомном контейнере на real-time эндпоинте SageMaker AI, тип инстанса ml.g5.xlarge. Контейнер отдаёт два маршрута: POST /invocations для потокового ответа на один законченный ввод и WS /invocations-bidirectional-stream, где одно соединение живёт много сообщений в обе стороны. Контейнер должен объявлять Docker-метку com.amazonaws.sagemaker.capabilities.bidirectional-streaming=true.

Развёртывание сводится к двум командам на базе AWS CDK: `uv run speech-infra deploy kokoro-82m` поднимает эндпоинт (роли IAM, кэш весов в S3, образ контейнера, модель и конфигурацию), а `uv run agent-infra deploy --bedrock-model-id <id>` разворачивает агента на Strands в среде Amazon Bedrock AgentCore. Порядок важен: агент обращается к уже существующему эндпоинту. Нужен Python 3.12+, потому что HTTP/2-клиент поставляется в готовых wheel только для этих версий. Отдельное предупреждение авторов: в примере закреплена старая версия PyTorch с известной уязвимостью PYSEC-2026-2286 (исправлена в torch 2.10.0), и перед любым использованием за пределами proof-of-value её нужно обновить.

Три ловушки двунаправленного стрима

Первая ловушка — тип WebSocket-фрейма. SageMaker доставляет каждое сообщение клиента как бинарный фрейм. Типичный обработчик на Starlette вызывает receive_text(), читает message["text"] и падает с KeyError. Если общий обработчик исключений отправляет клиенту str(e), тот видит ошибку «'text'», которая похожа на пропущенное поле, а не на проблему с фреймингом. Эндпоинт при этом честно показывает InService: соединения принимает, на каждое сообщение отвечает, только ошибкой. Решение — принимать оба типа фреймов и декодировать bytes как UTF-8, если поля text нет.

Вторая — преждевременное закрытие сессии. После отправки аудио и управляющего фрейма synthesis_complete контейнер должен вернуться к receive() и ждать следующее сообщение. Закрывать соединение нужно только по явному {"type": "close"}. Иначе на каждое предложение понадобится новое подключение, и выигрыш по задержке исчезнет.

Третья — нарезка потока на предложения. Класс IncrementalSentenceChunker принимает текст порциями через feed(), по регулярному выражению отделяет законченные предложения и придерживает хвост. flush() в конце забирает остаток без финальной точки. Авторы честно оговаривают: это не параллельный синтез. Обработчик строго последователен — получить сообщение, синтезировать, отправить аудио, отправить synthesis_complete. Выигрыш только в том, что синтез первого предложения стартует сразу после его появления. На живом эндпоинте проверили три предложения по одной сессии:

  • «Hello there.» — 73 200 байт аудио, 1,525 с
  • «This is a post deployment check.» — 108 000 байт, 2,250 с
  • «It has three sentences.» — 98 400 байт, 2,050 с

SSE с base64 против бинарного мультиплексирования

Для доставки аудио в браузер AWS сравнила два подхода. Первый — привычный SSE: аудио кодируется в base64 и заворачивается в JSON-событие. На 24 клипах измерили цену этого решения (качество по Amazon Transcribe от кодировки не зависело):

  • Raw PCM: 2 396 400 байт, в base64 — 3 195 200, битрейт 384 кбит/с
  • MP3 48k: 308 304 байта, в base64 — 411 072, 49,5 кбит/с
  • AAC 32k fMP4: 258 039 байт, в base64 — 344 084, 41,6 кбит/с

Base64 добавляет почти ожидаемые 33%. Есть и менее очевидная проблема: границы SSE-событий (пустая строка) не совпадают с границами MP3-фреймов и с тем, как транспорт режет данные. В одном замере четыре логических MP3-фрейма пришли шестью чанками, два из которых обрывались посреди фрейма. Потребитель, считающий каждый чанк целым, ломается на здоровом трафике. Лечится буфером: копим байты и обрабатываем кадр, только когда нашли разделитель `\n\n`.

Второй подход — на участке до модели вообще отказаться от base64 и SSE. Бинарное аудио и JSON-команды идут по одной двунаправленной сессии, а различаются одним правилом: если чанк целиком парсится как JSON-объект, это управляющий фрейм, иначе аудио. Проверка по первому байту не годится: PCM может начинаться с 0x7b, то есть с «{». Но целый PCM-чанк валидным JSON не станет практически никогда.

Агент целиком

В полной конфигурации агент на Strands с моделью из Amazon Bedrock работает в Bedrock AgentCore runtime и использует три канала: SSE к браузеру, потоковый вызов Bedrock для текста, двунаправленную сессию с Kokoro для синтеза. В запросе есть поле mode: «bidi» или «batch». В run_bidi параллельно работают две задачи: одна читает поток Bedrock и скармливает чанкеру готовые предложения, другая читает ответы Kokoro и выдаёт аудио. Обе кладут результат в общую очередь, поэтому звук ранних предложений может уйти вызывающему, пока Bedrock ещё пишет поздние. run_batch ждёт весь текст и делает один вызов синтеза — честная база для сравнения.

На участке от агента к вызывающему base64 всё равно остаётся: entrypoint BedrockAgentCoreApp обязан отдавать сериализуемые в JSON словари, которые SDK сам превращает в `data: {json}` фреймы. Бинарный фрейм туда не передать. Эта цена одинакова для обоих режимов, так что разница в замерах — чистый тайминг первого аудиочанка.

Что выбрать на практике

Если у фронтенда уже работает SSE для текста, base64 внутри события — прагматичный выбор: +33% к размеру и буфер для сборки, зато инфраструктура готовая. Если голосовой канал строится с нуля или критична задержка, единая бинарная схема с JSON-управлением убирает обе издержки и использует соединение, уже несущее текст. В любом случае стоит тестировать именно бинарные фреймы, проверять сборку по границам MP3-кадров и не закрывать сессию после каждого предложения. А перед продакшеном — обновить закреплённый PyTorch.

Сюжет

Вопросы и ответы по материалу

Что известно по теме: малая модель, один GPU и два маршрута?

Kokoro-82M содержит 82 млн параметров, и этого хватает, чтобы крутить её на одной видеокарточной инстанции среднего класса. Модель запускается в кастомном контейнере на real-time эндпоинте SageMaker AI, тип инстанса ml.g5.xlarge. Контейнер отдаёт два маршрута: POST /invocations для потокового ответа на один законченный ввод и WS /invocations-bidirectional-stream, где одно соединение живёт много сообщений в обе стороны. Контейнер должен объявлять Docker-метку com.amazonaws.sagemaker.capabilities.bidirectional-streaming=true. Развёртывание сводится к двум командам на базе AWS

Что известно по теме: три ловушки двунаправленного стрима?

Первая ловушка — тип WebSocket-фрейма. SageMaker доставляет каждое сообщение клиента как бинарный фрейм. Типичный обработчик на Starlette вызывает receive_text(), читает message["text"] и падает с KeyError. Если общий обработчик исключений отправляет клиенту str(e), тот видит ошибку «'text'», которая похожа на пропущенное поле, а не на проблему с фреймингом. Эндпоинт при этом честно показывает InService: соединения принимает, на каждое сообщение отвечает, только ошибкой. Решение — принимать оба типа фреймов и декодировать bytes как UTF-8, если поля text нет. Вторая — прежде

Что известно по теме: SSE с base64 против бинарного мультиплексирования?

Для доставки аудио в браузер AWS сравнила два подхода. Первый — привычный SSE: аудио кодируется в base64 и заворачивается в JSON-событие. На 24 клипах измерили цену этого решения (качество по Amazon Transcribe от кодировки не зависело): Raw PCM: 2 396 400 байт, в base64 — 3 195 200, битрейт 384 кбит/с MP3 48k: 308 304 байта, в base64 — 411 072, 49,5 кбит/с AAC 32k fMP4: 258 039 байт, в base64 — 344 084, 41,6 кбит/с Base64 добавляет почти ожидаемые 33%. Есть и менее очевидная проблема: границы SSE-событий (пустая строка) не совпадают с границами MP3-фреймов и с тем, как тран

ЖХ
Жемал Хамидун — CPO Alpina Digital, продакт-менеджер AlpinaGPT. Делает AI-продукты с 2019 года. Hamidun News — pet project, который вырос в production-агрегатор с 6000+ читателей.
✦ ИИ-агенты на практике · Академия & Nomad

От чтения про ИИ-агентов — к запуску своего агента за вечер

Пошаговая сборка автономных агентов и MCP-связок в Hamidun Academy (academy.hamidun.com) (72 часа бесплатно) или мгновенный запуск готовых мультишаговых агентов в облаке Nomad.

Попробовать Академию (72ч бесплатно) →Запустить Nomad Agent
Что вы думаете?
Загружаем комментарии…