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
Пользователь задаёт вопрос голосовому агенту и ждёт. Ответ уже частично сгенерирован, но звука нет: интеграции 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.
- 29 окт. 2026 г.NVIDIA Nemotron 3.5 Super VL: 120 млрд параметров, 12 млрд активных, запуск на AWS
- 29 окт. 2026 г.Cloudflare выпустила open-source модели Clef: 98,5% на BFCL и 38,8 мс
- 29 окт. 2026 г.Anthropic в S-1 показала: 11 900 клиентов платят от $100 тысяч в год
- 28 окт. 2026 г.Keystone убрал имя модели из кода: open-source LLM-шлюз добавляет 0,64 мс
Вопросы и ответы по материалу
Что известно по теме: малая модель, один 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-фреймов и с тем, как тран
От чтения про ИИ-агентов — к запуску своего агента за вечер
Пошаговая сборка автономных агентов и MCP-связок в Hamidun Academy (academy.hamidun.com) (72 часа бесплатно) или мгновенный запуск готовых мультишаговых агентов в облаке Nomad.
Главное из мира ИИ — раз в неделю
7 ключевых событий недели, отобранных вручную. Без шума, репостов и пресс-релизов.
Готово! Проверьте почту — мы отправили подтверждение.