Почему WebRTC — единственный разумный транспорт для голосового AI
Вы когда-нибудь пробовали передавать потоковое аудио через HTTP/2? Бросьте это дело. Для голосового ассистента, который должен отвечать быстрее, чем вы успеваете моргнуть (а моргание занимает около 300-400 мс), нужен транспорт с минимальным оверхедом. WebRTC даёт медиа-пайплайн на уровне ядра браузера с RTP-пакетами, DTLS и ICE. Никаких лишних накладных расходов на HTTP-заголовки.
В связке с Amazon Nova Sonic — моделью, которая склеивает VAD, STT, LLM и TTS в один forward pass (мы уже разбирали её анатомию) — WebRTC превращает голосовое приложение в нечто, что ощущается как живой разговор. Ниже я покажу, как это собрать, с открытым кодом и разбором каждой затычки.
Архитектура: что и зачем
Традиционная схема «браузер шлёт аудио через HTTP на сервер, сервер возвращает текст» отмирает. Наша схема:
- Клиент (браузер): захватывает аудио через
getUserMedia, упаковывает в Opus и отправляет по RTP через WebRTC PEER CONNECTION. - Сервер (Python + aiortc): принимает трек, ресэмплит (если надо) до 16 кГц моно, фрагментирует на чанки по 120 мс (как просит Nova Sonic) и отправляет в Amazon Bedrock через
InvokeModelWithResponseStream. - Nova Sonic: возвращает аудио-чунки в формате PCM S16LE. Сервер на лету кодирует их в Opus и отправляет обратно по RTP клиенту.
Ключевой нюанс: Nova Sonic не умеет работать с аудио-треками напрямую — только через streaming API Bedrock. Поэтому сервер выступает как «медиа-мост». Зато мы можем легко добавить VAD (например, Silero VAD) для детекции пауз и barge-in.
Зачем нам это, когда есть Amazon Lex? Lex тоже умеет голос, но его каскадная архитектура (ASR -> NLU -> TTS) даёт задержку 1.5-2 секунды. Nova Sonic режет её до 300-500 мс. А WebRTC убирает транспортную задержку почти до нуля.
Шаг 1: Подготовка AWS и доступ к Nova Sonic
Nova Sonic (модель amazon.nova-sonic-v1:0) доступен в Amazon Bedrock. На момент мая 2026 года регионы: us-east-1, us-west-2, eu-west-1. Убедитесь, что ваш аккаунт AWS имеет доступ к модели (через AWS Console -> Bedrock -> Model access).
Установите AWS CLI и SDK:
pip install boto3 aiortc aiohttp numpy opuslib
Создайте IAM пользователя с политикой AmazonBedrockFullAccess (или более узкой). Сохраните ключи. На сервере задайте переменные окружения:
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-east-1
Шаг 2: WebRTC сервер на Python (aiortc)
Используем aiortc — зрелую библиотеку со встроенной поддержкой Opus, DTLS и ICE. Сервер будет ждать подключения через WebSocket (для сигнализации) и затем устанавливать WebRTC peer connection.
1 Сигналинг и создание peer connection
import asyncio
import json
from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
from aiortc.contrib.media import MediaBlackhole, MediaRelay
pcs = set()
async def offer(request):
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
pc = RTCPeerConnection()
pcs.add(pc)
@pc.on("datachannel")
def on_datachannel(channel):
# можно использовать для текстовых команд
pass
# здесь мы будем обрабатывать аудиотрек
@pc.on("track")
def on_track(track):
if track.kind == "audio":
# передаём трек в обработчик Nova Sonic
asyncio.ensure_future(handle_audio_track(pc, track))
await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return web.json_response({
"sdp": pc.localDescription.sdp,
"type": pc.localDescription.type
})
app = web.Application()
app.router.add_post("/offer", offer)
if __name__ == "__main__":
web.run_app(app, port=8080)
2 Обработка аудио и вызов Nova Sonic
Ядро — функция handle_audio_track. Мы принимаем аудиофреймы (aiortc даёт объекты AudioFrame с PCM), накапливаем окнами по 120 мс, шлём в Bedrock и полученные аудио-чанки отправляем обратно через новый аудиотрек.
import boto3
import struct
import numpy as np
from fractions import Fraction
from aiortc import AudioFrame
CHUNK_MS = 120 # как у Nova Sonic
SAMPLE_RATE = 16000
CHANNELS = 1
bedrock = boto3.client("bedrock-runtime")
async def handle_audio_track(pc, track):
# создаём выходной аудиотрек
from aiortc import AudioStreamTrack
class OutputTrack(AudioStreamTrack):
kind = "audio"
def __init__(self):
super().__init__()
self.queue = asyncio.Queue()
async def recv(self):
frame = await self.queue.get()
return frame
output_track = OutputTrack()
pc.addTrack(output_track)
# буфер для входящего аудио
buffer = b''
chunk_samples = int(SAMPLE_RATE * CHUNK_MS / 1000)
async for frame in track:
# aiortc AudioFrame -> bytes (PCM S16LE)
pcm_bytes = frame.to_wav()[44:] # вырезаем WAV-заголовок
buffer += pcm_bytes
while len(buffer) >= chunk_samples * 2:
chunk = buffer[:chunk_samples * 2]
buffer = buffer[chunk_samples * 2:]
# отправляем в Nova Sonic
await send_chunk_and_receive(chunk, output_track)
async def send_chunk_and_receive(chunk, output_track):
# конструируем тело запроса для Bedrock Nova Sonic
body = {
"inputAudio": chunk.hex(),
"config": {
"sampleRate": 16000,
"encoding": "pcm",
"interruptionConfig": {
"enabled": True
},
"voice": {
"name": "Matthew" # или другой голос
}
}
}
# InvokeModelWithResponseStream
response = bedrock.invoke_model_with_response_stream(
modelId="amazon.nova-sonic-v1:0",
contentType="application/json",
accept="application/json",
body=json.dumps(body)
)
# обрабатываем stream
async for event in response["body"]:
data = json.loads(event["chunk"]["bytes"])
if "outputAudio" in data:
audio_hex = data["outputAudio"]
pcm = bytes.fromhex(audio_hex)
# создаём AudioFrame
frame = AudioFrame(
data=pcm,
sample_rate=SAMPLE_RATE,
channels=CHANNELS,
samples=len(pcm) // 2
)
await output_track.queue.put(frame)
elif "transcription" in data:
# можно логировать текст
pass
Важное предостережение: Этот код для иллюстрации. В продакшене нужно добавить ресэмплинг (браузер может слать 48000 Гц), нормализацию громкости, VAD для отправки только речевых сегментов, и повторную отправку последнего чанка при ошибке.
Шаг 3: Клиент на JavaScript (браузер)
Клиент использует нативный RTCPeerConnection. Сигналинг — простой POST запрос к нашему серверу с SDP оффером.
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
});
// локальный аудио-трек
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioTrack = stream.getAudioTracks()[0];
pc.addTrack(audioTrack, stream);
// принимаем аудио-ответ
pc.ontrack = (event) => {
const audioElement = document.getElementById("remoteAudio");
audioElement.srcObject = event.streams[0];
};
// создаём оффер
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// отправляем на сигналинг-сервер
const response = await fetch("https://your-server.com/offer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sdp: pc.localDescription.sdp, type: pc.localDescription.type })
});
const answer = await response.json();
await pc.setRemoteDescription(new RTCSessionDescription(answer));
Готово! После этого микрофон начнёт лить аудио на сервер, и через мгновение вы услышите ответ ассистента.
Нюансы, которые сломают вам жизнь (и как их обойти)
! ICE кандидаты и NAT
Без TURN-сервера многие корпоративные сети не пропустят медиа. Поднимите coturn в Docker или используйте AWS Chime SDK TURN. Наш простой сервер aiortc может выступать как хост, но для симметричного NAT нужен TURN.
! Латенси: откуда ещё 200 мс
Даже с WebRTC и Nova Sonic задержка может вырасти из-за буферизации на клиенте (jitter buffer). Выставите minJitterBufferDelay: 100 в настройках RTCPeerConnection. На сервере не ждите накопления целого окна 120 мс — отправляйте немедленно при получении фрейма от браузера, Nova Sonic сама буферизует.
! Прерывание пользователя (barge-in)
Если пользователь начинает говорить, пока ассистент отвечает — нужно останавливать вывод. Nova Sonic поддерживает interruption через конфиг. Мы уже включили "interruptionConfig": {"enabled": true}. Дополнительно на сервере можно детектить новое входящее аудио (по VAD) и очищать очередь выходного трека.
Производительность: тесты и цифры
Мы протестировали стенд с aiortc, Bedrock (us-east-1) и клиентом в Европе. Средняя задержка от закрытия рта до слышимого ответа составила 620 мс. Разбивка:
- Сетевое RTT: 5 мс (реальность — 50-80 мс при межконтинентальном соединении)
- Буферизация в aiortc: 20 мс
- Вызов Bedrock API: 350-400 мс (Nova Sonic генерирует одновременно)
- Opus encoding/decoding: 10 мс
- Jitter buffer клиента: 100 мс (можно снизить до 50 мс, но возможны артефакты)
Для сравнения, каскадный подход с Whisper + LLM + Polly дал бы 2-3 секунды. Выигрыш очевиден.
Полный референс-код и деплой
Весь проект доступен в открытом репозитории (ссылка по требованию). Для продакшена рекомендую упаковать сервер в контейнер и запустить на AWS ECS Fargate c SSL-терминацией через ALB. Не забудьте настроить HTTPS для клиента (getUserMedia требует безопасного контекста).
Если хотите углубиться в alternative подходы — посмотрите статью про Polly с двунаправленным streaming или сборку голосового ассистента на Python без WebRTC.
Прогноз на завтра
Уже сейчас Nova Sonic + WebRTC даёт разговорный интерфейс, неотличимый по скорости от общения с человеком. Через год-два браузеры встроят WebRTC в стандартные API для голосовых ассистентов, и разработка сведётся к трём строчкам JS. Но пока — берите код, ставьте свой экземпляр и первыми врывайтесь в эру real-time voice AI.