Голосовые системы

Потоковое распознавание без перебивания

Что меняется

Для голосового чата 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.

Исходник

Скачать Markdown · К материалам исследования