Потоковое распознавание без перебивания
Что меняется
Для голосового чата CPU-мост теперь может работать в ASR_MODE=streaming.
Он последовательно проверяет новые 32-мс окна Silero, а не пересканирует всю
накопленную запись. У каждого подключения собственное состояние VAD. Веса ONNX
общие и неизменяемые; рекуррентное состояние не делится между чатами.
После обнаружения речи мост передаёт 200 мс предзаписи и следующие PCM-кадры отдельному GPU-worker. Worker повторно декодирует ограниченное окно, выдавая гипотезы по мере поступления речи. После 450 мс паузы мост запрашивает final. Из-за 32-мс окон граница паузы округляется; смешанное окно на границе речи добавляет ещё до 32 мс. Final относится к фразе, а не к закрытию транспорта.
Core: PCM 16 кГц → CPU-мост: Silero → NATS inference → GPU: faster-whisper
↑ ↓
└──────── partial / final ───────┘
↓
Core → один черновик UI / один запрос агенту
При ответе агента микрофон выключается. Частичные гипотезы не запускают агента.
Отмену ответа и перебивание этот путь не реализует. Файловый LinTO HTTP runner
и страницы транскрипции/диаризации не переведены на streaming. По умолчанию
остаётся прежний ASR_MODE=batch.
Wire contract
Пакет: [длина JSON: u32 BE][JSON UTF-8][PCM16 LE]. PCM — mono, 16000 Гц,
2 байта на сэмпл. Это не WAV/FLAC, не Opus и не Base64. Декодирование контейнера
и ресэмплинг выполняются до упаковки. Один frame содержит не больше 200 мс.
{
"type": "frame",
"seq": 42,
"session_id": "uuid-чата",
"stream_id": "uuid-подключения",
"sample_rate": 16000,
"channels": 1,
"sample_width": 2,
"traceparent": "00-<32 hex trace id>-<16 hex span id>-01"
}
На GPU-плече добавляются phrase_id и speech_samples; seq начинает новую
последовательность для каждой фразы. JSON заголовок ограничен 4 KiB. Stream,
session и phrase IDs не могут содержать NATS wildcard или точку.
| Плечо | Subject |
|---|---|
| Core → CPU-мост | webrtc.komaroff.asr.stream.<session> |
| CPU → локальный inference export | to.inference.webrtc.asr.audio.<stream> |
| GPU получает | inference.webrtc.asr.audio.<stream> |
| GPU публикует | inference.webrtc.asr.events.<stream> |
| CPU получает через import | from.inference.webrtc.asr.events.<stream> |
| CPU → Core | webrtc.komaroff.asr.text.<session> |
{
"version": 1,
"result_type": "partial",
"session_id": "uuid-чата",
"stream_id": "uuid-подключения",
"phrase_id": "uuid-фразы",
"revision": 2,
"text": "два плюс"
}
Revision 1 — пустое уведомление CPU-моста о начале черновика; GPU продолжает
с revision 2. final, rejected и error терминальны. Ошибка содержит
короткий code, а не строку исключения. Пустой final не отправляется агенту.
Core и UI проверяют session, stream generation, revision и терминальную запись.
Повторные final игнорируются в рамках текущего подключения. Ledger ограничен
2048 записями; это не долговечная exactly-once гарантия NATS после рестарта.
Очереди и ошибки
VAD-очередь ограничена 32000 сэмплами. GPU ограничивает отдельно необработанные сэмплы двумя секундами и их возраст двумя секундами; сохранённое акустическое окно ограничено восемью секундами. Оно не является очередью новых данных. Одновременно допускается восемь фраз; один decoder обслуживает их по очереди.
Пропуск seq, переполнение или разрыв завершают повреждённую фразу с ошибкой. После ошибки CPU-мост не распознаёт её оставшийся хвост: он ждёт полную паузу и только потом принимает следующую фразу. На GPU abort делает tombstone, поэтому запоздалый результат декодирования не возвращает закрытую фразу. Фразы "Продолжение следует" и "Субтитры сделаны..." не запрещаются по тексту. Приём зависит от наличия речи и показателей decoder, а не от списка слов.
Модели и автономная поставка
Начальная модель — large-v3-turbo, русский язык, beam 1, temperature 0,
без condition_on_previous_text. Веса и Silero скачиваются только в model
stage сборки. Ревизии закреплены в
prepare_models.py. Runtime использует
локальный путь, local_files_only и offline-переменные. Нет файла модели —
нет готового worker, а не загрузка из интернета во время запроса.
Worker запускается неактивным: ASR_ACCEPT_REQUESTS=0. После загрузки модели,
warm-up и подключения к NATS /healthz возвращает ready=true, но
accepts_requests=false. При потере NATS readiness становится false.
Офлайн-проверка модели на машине разработчика:
# Выполнять из корня этого repository; зависимости — в отдельном venv.
python3 -m venv .asr-venv
.asr-venv/bin/pip install -r src/requirements.bridge.txt -r src/requirements.asr-worker.txt
# Эта команда предназначена для подготовки поставки и требует сети.
.asr-venv/bin/python -m src.streaming_asr.prepare_models --output ./models/asr
# Само распознавание использует уже подготовленные локальные веса.
HF_HUB_OFFLINE=1 .asr-venv/bin/python scripts/benchmark-streaming-asr.py \
--file /путь/к/контролю.flac --model ./models/asr/large-v3-turbo \
--vad ./models/asr/vad/silero_vad.onnx --show-text
Проба через поставленные CPU-мост и GPU-worker, без вызова агента:
# NATS_URL передаётся из одобренного credential channel, не из аргумента CLI.
# Probe генерирует отдельные session/stream IDs; Core-чату они не принадлежат.
.asr-venv/bin/python scripts/probe-streaming-asr.py --file /путь/к/контролю.flac
У probe собственные монотонные часы. Его roundtrip не следует выдавать за
серверную задержку после акустического конца речи. --show-text печатает текст
только локально; по умолчанию остаются числа и тип результата.
OTLP и замеры
Trace context проходит в JSON заголовке, не в PCM. voice.audio_receive связан
с asr.vad_queue, asr.vad, asr.gpu_queue, asr.decode, asr.result,
asr.publish и voice.agent_request. Correlation/request IDs не заменяются.
Exporter работает в фоне, имеет ограниченную очередь и таймаут. Текст, аудио,
SDP и credentials не входят в атрибуты. Метрики используют только короткие
stage/result/code labels. first_partial измеряется от обнаружения речи;
speech_final — от поступления последнего речевого PCM-кадра до терминального
результата, включая паузу VAD; endpoint_final исключает ожидание паузы.
Это часы CPU-моста. Браузер отдельно показывает событие → следующий UI-кадр
в панели подключения. Смешивать эти часы вычитанием timestamp нельзя.
Проверено 2026-10-02 - 2026-10-03 и что ещё не принято
- 150 Python tests passed, включая настоящий локальный NATS с двумя аккаунтами, два чата, sequence gap и восстановление только после паузы.
- После watchdog/переподключения хвост повреждённой фразы остаётся в карантине до паузы. Явный EOF закрывает generation; поздние кадры её не открывают. Health проверяет не только NATS, но и работающий decode loop; переполнение очереди декодирования даёт terminal error, а не потерянное ожидание.
- Символический PCM-контроль длинной фразы сохраняет все 80 токенов без дублей при ограничении окна. Он проверяет управление буфером, а не точность модели.
- OTLP HTTP-пакеты получены тестовым collector; секретный маркер и текстовый маркер не попали в payload. Отказ collector не заблокировал audio-path spans.
- Настоящие локальные Silero и faster-whisper turbo INT8 на CPU: чистый
2+2.flac, белый шум 10 dB и gain 0.1 дали осмысленный контрольный вопрос;raven.flacдал непустой принятый результат. Для этих личных записей нет вручную проверенного полного word-level эталона, поэтому WER не заявляется. - Синтетический eSpeak контроль имеет известный текст. На чистом звуке WER 25%, с белым шумом 10 dB — 50%; фраза "Продолжение следует" не была заблокирована. Это показывает оставшиеся ошибки точности, а не обещание качества Codex.
Обновление 2026-10-03: worker поставлен на H100 в отдельный webrtc-asr,
использует одобренный существующий MIG 0:2. Source image 8f9b2768, версия
0.1.203, actual manifest
sha256:e0f2171d47dbb10a9e8f3d810a5c01edc6c89f130ffd0f41fa52c6296cb3025d.
Readiness, warm-up, NATS и реальный обмен inference подтверждены отдельно.
Voice CPU-мост работает в ASR_MODE=streaming, с этим же source image.
Geometry MIG, LinTO и общий rag-stack deploy не менялись.
После исправления OTLP 2+2.flac через CPU/Silero + GPU дал 4 непустых partial
и один правильный final; первый текст через 1.29 с от начала paced file probe.
Параллельные raven.flac и синтетический контроль с белым шумом 10 dB дали
по одному final без ошибок очереди/seq. В синтетической речи остались две
ошибки словоформ. Личный transcript Raven не выводился. Эти пробы не создают
Core-чата и не вызывают агента. Заявлять общий WER или качество Codex нельзя.
Ещё нужны: production traces/метрики в Tempo/Grafana, p95 на прогретом GPU, сравнение large-v3 на том же эталоне, длинные фразы и полный WebRTC/UI/agent end-to-end. Сохранённый dashboard не является доказательством этих результатов; Grafana сейчас снова требует входа Slim Auth. Файловая проба не проверяет браузерный AEC/NS и Opus ingress. Ручной UI-сценарий остаётся за владельцем.
Первые реальные сборки выявили две ошибки поставки. .dockerignore исключал
новые requirements-файлы: добавлены точные исключения и тест контекста. Затем
системный pip GPU-образа упал в get_topological_weights с AssertionError.
Worker получает собственный venv и pip 25.3,
а image build выполняет pip check и импорт его библиотек. Похожее падение
resolver зафиксировано в issue pip.
Прохождение этих проверок не заменяет CUDA/readiness-пробу на выделенном GPU.
CPU-образ при сборке выполняет одно окно Silero на включённых в поставку весах,
чтобы отличить наличие файла от рабочей ONNX-модели.
Pipeline 4862
для 5be9a292ff13f52d4295e81f9be410d765b7175b завершился успешно.
Job 9546
выполнила проверки библиотек и упакованной Silero, затем опубликовала
stt_whisper_to_nats и webrtc_streaming_asr в канал
streaming-asr-no-barge-in:latest. Это тестовая ветка, не main и не rollout.
Поздний docs-only commit не меняет исходники этих образов.
Адресная CI-цепочка
Ошибка первой live-пробы 2026-10-03
Worker впервые поставлен в отдельный webrtc-asr на существующий MIG 0:2.
Прогрев прошёл, /healthz сообщил готовность модели и NATS. После разрешения
запросов изолированная проба 2+2.flac через WSS не получила текст: пришли
audio_queue_overflow, затем sequence_gap и stream_disconnected.
Лог конкретной задачи показал первичную причину: View("webrtc.asr.duration")
передавал строку как instrument_type, а SDK ожидал Python type. Проверка
isinstance падала при первой записи метрики, до добавления PCM в decoder.
Watchdog видел накопление очереди; его собственный счётчик тоже падал и
останавливал worker. Новый контейнер получал хвост уже начатой последовательности.
Ошибки очереди и seq были следствиями, не доказательством нехватки GPU или
неверных прав NATS.
Исправление использует явный instrument_name. Metric wrappers прекращают
записывать неисправный instrument после одного предупреждения, но не прерывают
приём PCM и публикацию terminal event. Значение исключения не логируется.
Тест сначала воспроизвёл падение на старом коде. Теперь проверяются все четыре
реальные OTLP-метрики, отказ collector и завершение фразы при искусственной
поломке metric SDK. 153 tests passed. GPU image выполняет такую же запись
метрик в своём Python 3.10 runtime при сборке. Результат повторной live-пробы
фиксируется после публикации исправленного образа, не выводится из этих tests.
Дополнительный regression проверяет pacing probe: publish не ожидает
сетевой RTT после каждого 20-мс кадра; flush выполняется на границах.
154 tests passed и pipeline 4873 подтвердил checks. Этот commit меняет только
probe/tests вне image context: build-labels выбрал services: {} и не публиковал
новый image. Исправленный main:latest опубликован именно job 9574 pipeline 4871,
source 8f9b2768. Последующая task/manifest-проверка отличила настоящий rollout
от успешного повторного deploy неизменённого tag с --resolve-image=never.
streaming checks проверяет новый контракт, локальный NATS и OTLP перед сборкой.
build streaming asr публикует только CPU-мост и новый GPU-worker через
streaming-compose.yml. Прежние сборка всех
моделей и deploy остаются manual: их не нужно запускать для этой поставки.
Образы имеют обычный branch:latest; digest используется для проверки, не
вписывается в DRS. Новый worker ставится отдельной job из rag-stack и сначала
не принимает запросы. Core/UI должны быть поставлены до включения streaming.
Общий образ CPU-моста используется несколькими сервисами. Даже адресная
публикация этого образа может привести к их обновлению registry_listener.
Поэтому совместимость ASR_MODE=batch обязательна, а публикация образа — не
доказательство переключения голосового чата.
Code map
| Файл | Ответственность |
|---|---|
| protocol.py | Валидация бинарного пакета и типов результата |
| vad.py | Однократная обработка окон, preroll, endpoint, quarantine |
| bridge.py | Изоляция потоков, очереди и NATS namespace |
| worker.py | Локальный decoder, scheduler, partial/final, readiness |
| telemetry.py | W3C context, spans, метрики и background export |
| probe-streaming-asr.py | Изолированный файловый NATS probe без агента |
| benchmark-streaming-asr.py | Offline модель, VAD, шум и WER по заданному эталону |
| streaming-compose.yml | Адресная публикация двух ASR-образов |
Тесты контракта: tests/test_streaming_asr.py; broker/account mapping:
tests/test_streaming_nats_integration.py; настоящий OTLP HTTP export:
tests/test_streaming_telemetry.py. Старые файловые tests остаются в suite.