Почему не работал голос и что именно исправили
Первый отказ был в передаче звука: браузер получал ответ от Core, но не мог установить медиасоединение. Когда этот участок заработал, проявилась вторая ошибка - одну запись распознавали два обработчика. Эти причины устранены разными изменениями. Качество распознавания при сильном шуме и один таймаут агента требуют отдельной работы.
Не одна ошибка, а несколько независимых отказов
Проверялся обычный интерфейс карты и чата, а не Desktop и не отдельная страница healthcheck. Контрольная запись проходила через браузерный WebRTC, публичный TURN relay, Core, собственный ASR bridge и агента. Поэтому правильный текст и ответ относятся к тому же пути, которым пользуется голосовая кнопка.
| Наблюдение | Что установлено | Результат |
|---|---|---|
| Статус подключения вводил в заблуждение | HTTP и NATS не доказывают работу микрофона и медиасоединения. | Исправлено Аудио и события показываются отдельно. |
| STUN 701 | Отказ отдельных попыток. Не доказан как причина всего инцидента. | Не устранён полностью Предупреждение осталось при работающем relay. |
| ICE через TURN не устанавливался | NAT менял внешний порт; aioice игнорировал DATA indication от нового адреса. | Исправлено Пакет поступает в штатный ICE handler. |
| NoneType после таймаута | STUN retry отправлял пакет после закрытия UDP-транспорта. | Исправлено Ожидания отменяются до закрытия. |
| Необработанный CHANNEL_BIND 403 | Coturn запрещал частный peer, а клиент оставлял исключение и ожидающие отправки. | Обработка исправлена Ограничение Coturn сохранено. |
| Два результата одной записи | Два обычных подписчика получали один NATS stream; Core игнорировал env prefix. | Исправлено Свой namespace для Core, bridge и status. |
| Health проверял legacy-поток | После разделения namespace служебная проба могла проверять другой bridge. | Исправлено Те же prefixes переданы в /status/. |
| Пропуск первого слова, ошибки на шуме | В одной пробе потеряно "Сколько"; при SNR 0 dB WER 47.1%. | Не исправлено Причина пропуска пока не локализована. |
| Один ответ завис на 270 секунд | ASR завершился; routing LLM ответил, но финальный вызов не наблюдался. | Причина неизвестна Успешные повторы не закрывают дефект. |
В документе слово "исправлено" означает: есть конкретное изменение и проверка его поведения. Это не обещание отсутствия будущих отказов. Состояние описано на дату испытаний, а не подтверждено новым запросом при написании отчёта.
Сервисные credentials, полные SDP, аудио и cookies сюда не включены. Код и журнал проверок закреплены на commit 590708a; последний runtime-rollout использовал 98eead5. Снимки настоящие, сделаны во время предыдущих испытаний 02.10.2026; иллюстрации схем построены для объяснения и не выдаются за снимки сервиса.
Где проходит звук, а где только команды
Передача начинается с согласования соединения. Браузер отправляет SDP в POST /core/offer и получает SDP answer. Это описание будущего соединения, а не аудиофайл. После выбора ICE-пары устанавливается DTLS; уже затем идут защищённые аудиопакеты SRTP. В успешной live-пробе выбран удалённый кандидат relay - публичный адрес Coturn, выделенный для Core.
| Граница | Что передаётся | Как проверялось |
|---|---|---|
| Браузер → Core | POST /core/offer, JSON с sdp, type="offer", session_id. Ответ: SDP answer. Запись не вложена в этот HTTP-запрос. | HTTP status и принятие remote description. |
| Браузер → relay → Core | Opus в SRTP; ICE/STUN и DTLS - служебные протоколы. Capture: 48 kHz mono. В описании Opus наблюдался RTP clock 48 kHz, 2 ch; это не доказательство стереозахвата. | ICE и DTLS connected, selected remote relay, растущие RTP counters. |
| Core → ASR bridge | Бинарный пакет: 4 байта длины JSON в big-endian, UTF-8 JSON, затем PCM16LE / 16 kHz / mono. Типичный аудиофрейм 20 ms: 640 байт PCM, без WAV-заголовка. | Код resampler и упаковки; ASR-result с ID и длительностью. |
| ASR → агент → браузер | JSON с текстом и идентификаторами фразы/сессии; запрос к workflow; события и ответ в том же чате. | Одна финальная фраза → один agent turn → ответ "4". |
# src_core/processors/whisper_stream_node.py:83-97
raw_bytes = np.asarray(pcm, dtype="<i2").tobytes() # PCM: signed 16-bit, little-endian.
meta_bytes = json.dumps(meta, ensure_ascii=False).encode("utf-8")
payload = len(meta_bytes).to_bytes(4, "big") + meta_bytes + raw_bytes
# Первые 4 байта говорят, где заканчивается JSON и начинается звук.
# WAV/FLAC и Base64 в этом stream-пакете отсутствуют.
Источник: WhisperStreamNode.handle_frame(); преобразование формата задаёт handle_track(). Закрытый ASR-бэкенд описан по контракту и результатам, его внутренний алгоритм здесь не объявляется известным.
Интерфейс смешивал соединение с сервером и передачу звука
Как проявлялась и как нашли
В интерфейсе нельзя было понять, что именно подключено: сервер событий, HTTP signaling или голос. После добавления измерений стала видна конкретная ситуация: POST /core/offer → 200, NATS connected, но ICE/DTLS закрыты, выбранной пары нет и RTP не идёт. Значит, сервер принял описание соединения, но сам звук не получил рабочего маршрута.
Причина неверной оценки подтверждается прежним кодом: isConnected() возвращал !!session, а session создавалась после negotiate(), без ожидания media. Затем UI писал "Подключено". Обработчик NATS менял ту же строку статуса и мог перезаписать оценку аудио сообщением "События подключены". Источники до исправления: session.ts:104 и assistantApp.ts:166.
Это проверялось состояниями браузерного peer и getStats(), а не только серверным healthcheck. Затем в локальном UI-стенде воспроизвели HTTP 503, запрет микрофона, разрыв peer и отдельный разрыв NATS. Последний тест особенно важен: канал событий может упасть, пока аудиосоединение ещё работает.
Что изменили
В строке чата теперь независимые оценки "Аудио" и "События". Аудио считается подключённым только при connectionState=connected и ICE connected/completed. В сворачиваемой панели показываются реальные поля, журнал до 200 записей и JSON-экспорт. Ошибка автоматически раскрывает панель и остаётся в чате отдельной карточкой с источником и временем.
// diagnostics.ts:8 - HTTP status и NATS здесь не используются.
if (pc.connectionState === "connected"
&& ["connected", "completed"].includes(pc.iceConnectionState)) {
return "connected";
}
// Для остальных переходов есть отдельные failed/disconnected/connecting.
Отдельная ошибка сроков ожидания
Сбор ICE и HTTP-обмен первоначально съедали общий срок на подключение. Медиатаймер мог закончиться до того, как Core вернул SDP answer. Сроки разделили: signaling получает iceGatherTimeoutMs + 20000 ms, затем после принятия answer начинается отдельное ожидание media на 20 секунд. При временном disconnected даётся 8 секунд на восстановление; при окончательном отказе закрываются микрофон, peer, VAD и таймеры.
Панель не превращает отказ в успех. Она перестаёт выдавать исправный канал событий за исправный голос и показывает, на какой границе нужно продолжить поиск.
Изменения: 2e5aa37 - панель и статусы; d4432be - отдельные сроки signaling/media. Код: session.ts:123, diagnostics.ts:5, connectionDiagnosticsUi.ts. Проверки: frontend suite.
STUN 701: заметное предупреждение, но не диагноз всего соединения
В журнале было ICE candidate error 701: STUN host lookup received error для stun:stun.gis-master.ru:3478. В других попытках встречался binding timeout. Это реальные отказы отдельных STUN-попыток. Однако по ним нельзя заключить, что весь Coturn исчез или что причина точно в IPv6.
Как проверяли
Сопоставили DNS/доступность собственного сервиса, кандидаты из SDP и фактический результат ICE. В браузере появлялись srflx-кандидаты - значит, хотя бы часть обращений к STUN давала результат. После исправления TURN соединение получило connected, выбранный relay и растущие RTP-счётчики, хотя предупреждения 701 отдельных попыток остались в журнале.
Что сделали и чего не утверждаем
Не пытались выдать переключение на IPv4 за доказанное устранение причины. Core получил собственный аутентифицированный TURN через TCP, чтобы появился публичный relay без публикации случайных UDP-портов контейнера. Затем нашли и исправили несовместимость приёма TURN DATA. Именно этот путь дал рабочее медиасоединение.
# stack/webrtc.drs:323-327; фрагмент конфигурации, не готовый секрет.
WEBRTC_ICE_SERVERS_JSON: JSON.generate([
{ urls: 'stun:stun.gis-master.ru:3478' },
{ urls: 'turn:stun.gis-master.ru:3478?transport=tcp',
username: 'webrtc-komaroff-core',
credential: ENV.fetch('WEBRTC_TURN_PASSWORD') }
])
# TURN credential остаётся на сервере и не попадает в настройки frontend.
701 не скрыт и не объявлен исправленным. Проверять нужно выбранную пару и доставку RTP. Даже при рабочем relay полезно отдельно исследовать предупреждения, но они уже не объясняют полный отказ наблюдаемого сеанса.
Сервис: собственный Coturn, public endpoint stun.gis-master.ru:3478. Во время проверки relay использовал 178.20.41.87 и диапазон 49160-49259. Это описание проверенной конфигурации, не универсальные адреса для другой установки. Commit: 1de618c; DRS-конфигурация Core.
TURN принимал пакеты от нового NAT-порта, а Core их не видел
Почему простой TURN не помог сразу
NAT на стороне браузера выбирал внешний порт в зависимости от адреса назначения. Порт, полученный через STUN и указанный в SDP, отличался от порта, с которого браузер реально отправлял ICE Binding к relay. В наблюдаемых попытках в SDP были порты 37932/43982, а на публичном интерфейсе сервера пакеты к relay 49199 приходили с 10958/15129.
Это не обязательно ошибка NAT. ICE умеет обнаружить такой фактический адрес как peer-reflexive. Но для этого проверенный Binding request должен дойти до ICE-обработчика Core. Для ещё не привязанного к TURN-каналу peer Coturn передаёт входной пакет как DATA indication. В закреплённом aioice 0.10.2 этот вариант не доходил до обработчика: библиотека работала с ChannelData, но игнорировала нужную DATA indication.
Как нашли
Сначала проверили доступность TURN и появление relay-кандидата. Это подтвердило выделение relay, но media всё ещё не устанавливалось. Затем пассивное наблюдение заголовков на серверном интерфейсе показало фактический порт входного ICE. Сравнение с SDP направило поиск в доставку пакета от Coturn к aioice, а чтение библиотечного обработчика показало отсутствующий путь DATA indication.
Гипотезу проверили отдельно: настоящий Chromium с relay-only peer и aiortc, без production ASR. До исправления соединение оставалось в ожидании и звук не декодировался; с адаптером получены connected и 50 декодированных аудиофреймов. После поставки тот же механизм проверен уже в live-чате.
Как исправили, не ослабляя защиту
Добавили узкий адаптер aioice_compat.py для TCP и UDP TURN. Он проверяет STUN envelope, magic cookie, fingerprint при его наличии, XOR-адрес peer, длины и padding атрибутов. Не принимает отсутствующие или повторные DATA/peer. Внутренний пакет передаётся без изменения штатному ICE handler, который по-прежнему проверяет USERNAME и MESSAGE-INTEGRITY.
# aioice_compat.py:83-90 - существенная часть приёма.
if data[:2] == b"\x00\x17": # TURN DATA / INDICATION.
decoded = decode_data_indication(data) # Валидация внешнего пакета.
if decoded is not None and self.receiver is not None:
payload, peer = decoded
self.receiver.datagram_received(payload, peer)
# Штатный ICE handler проверяет подпись и обнаруживает новый адрес.
Адаптер подключается в Core до создания peer. Файлы установленной библиотеки не переписываются; замена классов действует внутри процесса Core. Версия aioice==0.10.2 закреплена: при обновлении нужно проверить, нужен ли ещё адаптер и совместим ли он с новым transport API.
После исправления live-соединение выбрало публичный relay, установило ICE/DTLS и передавало RTP. Первая успешная голосовая проба ещё дала два результата ASR: это уже другая ошибка, разобранная в разделе 6.
Commit 04c4485; decoder и протоколы; включение адаптера; тесты формата, TCP/UDP и ICE-авторизации.
Вернуться к схеме NATПосле отказа соединения STUN retry обращался к закрытому сокету
В Core после неудачного подключения появлялись ошибки NoneType в пути отправки. Стек и порядок событий показали: попытка уже завершалась, UDP transport был закрыт, но таймер незаконченной STUN-транзакции ещё пытался повторить sendto. Это засоряло журнал и осложняло поиск исходной причины.
Такой traceback нельзя читать как доказательство того, что именно сокет изначально сорвал ICE. Он возникает при завершении уже неудачной попытки. Поэтому исправление отмены и исправление TURN DATA проверялись отдельно.
Изменение и проверка
ClosingStunProtocol учитывает активные задачи request. При закрытии отменяет незавершённые задачи, ожидает их окончание и только затем вызывает стандартное закрытие транспорта. Так finally внутри транзакции успевает отменить таймер повторной отправки.
# aioice_compat.py:117-123; текущая close-задача исключается из списка.
tasks = [task for task in getattr(self, "_core_requests", ())
if task is not current and not task.done()]
for task in tasks:
task.cancel() # Сначала попросить закончить ожидания.
await asyncio.gather(*tasks, return_exceptions=True)
await super().close() # Потом закрыть UDP transport.
Автотест закрывает протокол с незаконченной STUN-транзакцией и проверяет отсутствие поздней отправки. В проверенном текущем Core после live-проб не было прежних NoneType и Task exception was never retrieved. Старые блоки Insight относятся к предыдущим task и не исчезают из истории после rollout.
Commit 04c4485; ClosingStunProtocol; test_pending_transaction_cancelled_before_udp_close.
Отказ CHANNEL_BIND 403 превращался в необработанные задачи
Почему Coturn отвечал 403
ICE пробует несколько адресов, включая локальные кандидаты. Собственный Coturn не должен пересылать через публичный relay трафик к запрещённым частным, loopback и link-local адресам. Для такого peer операция CHANNEL_BIND возвращала 403. В проверенных попытках это был отказ адресной политики, а не доказательство неправильного пароля TURN.
Ошибка клиента состояла в другом: отправка запускалась фоновой задачей, её исключение не обрабатывалось, ожидающие отправки оставались в очереди, а повторные попытки снова обращались к тому же запрещённому peer.
Как нашли и исправили
Сопоставили 403 с типом ICE-кандидата, поведением Coturn и текущим traceback aioice. В адаптере сделали обработку только явного ERROR-CODE=403: peer запоминается в рамках одной TURN allocation, ожидающие отправки отменяются, повторный bind для него не запускается. Для других адресов соединение продолжает обычный перебор пар.
# aioice_compat.py:71-81; существенный фрагмент.
except stun.TransactionFailed as exc:
code = exc.response.attributes.get("ERROR-CODE", (None, ""))[0]
if code != 403:
raise # Например, ошибка 500 не скрывается.
denied.add(addr) # Только этот peer и только эта allocation.
for waiter in self.peer_connect_waiters.pop(addr, ()):
waiter.cancel()
logger.warning("TURN denied an ICE peer (CHANNEL_BIND 403); candidate skipped")
TCP/UDP-тесты проверяют параллельные waiters, отсутствие повторного bind, нормальную ChannelData-отправку и сохранение ошибки 500. В текущем runtime вместо необработанных исключений оставались понятные предупреждения о пропущенном кандидате; публичная рабочая пара при этом устанавливалась.
Не разрешали private peer в Coturn, не отключали authentication и не подавляли все сетевые ошибки. Появление этого предупреждения при рабочем relay допустимо; новая ошибка другого кода остаётся видимой.
Commit 50f714f; DataIndicationMixin.send_data(); тесты отказа и concurrent waiters.
Одну запись получали два независимых ASR bridge
Как отличили двойное распознавание от двойного отображения
Контрольный файл проиграли один раз, а получили два разных phrase_id с одинаковой длительностью 2.144 s и два обращения к агенту. Это не один и тот же JSON, дважды нарисованный в интерфейсе. Read-only проверка runtime нашла двух потребителей одного потока: собственный web-rtc-komaroff_py_faster_whisper и соседний py_faster_whisper_stt_whisper_to_nats.
Оба подключены к web-rtc-komaroff_audio_nats:4222, stream mode включён, подписка обычная, без queue, на inference.whisper.stream.>. Обычная NATS-подписка доставляет опубликованное сообщение каждому подходящему подписчику. Контейнеры работали устойчиво около девяти дней: краткое пересечение старого и нового task при rollout не объясняло эти результаты.
Почему одного изменения DRS было недостаточно
В stack существовали env-параметры prefixes, но Python Core использовал жёстко заданные константы. Поэтому даже правильная переменная окружения не меняла реальный subject. Исправление связало код и конфигурацию: Core читает env, а DRS задаёт один namespace и producer, и собственному bridge.
# До исправления Core использовал legacy-константы.
ASR_IN_PREFIX = "inference.whisper.stream."
ASR_OUT_PREFIX = "inference.whisper.text."
# settings.py:30-31 - после исправления, с совместимыми defaults.
ASR_IN_PREFIX = os.getenv("ASR_IN_PREFIX", "inference.whisper.stream.")
ASR_OUT_PREFIX = os.getenv("ASR_OUT_PREFIX", "inference.whisper.text.")
# stack/webrtc.drs:16-17 - namespace конкретно нашего live-stack.
ASR_IN_PREFIX = "webrtc.komaroff.asr.stream."
ASR_OUT_PREFIX = "webrtc.komaroff.asr.text."
На последнем контроле одна запись дала одну финальную ASR-фразу и один ответ агента. pending и финальный результат с одинаковым phrase_id считаются двумя состояниями одной фразы, а не двумя распознаваниями.
Не добавляли фильтр одинакового текста по TTL. Пользователь может повторить "Сколько будет два плюс два?", и это должен быть новый допустимый запрос. Исправлен источник двойной обработки, а не замаскирован её результат.
Commit 252aea4; settings.py; общие prefixes в DRS; тест совместимости defaults и namespace.
Вернуться к схеме двух bridge/status/ должен проверять тот же bridge, что и настоящий голос
После отделения ASR обнаружилась связанная несогласованность: собственные Core и bridge уже используют новый stream, а служебная controlled-error проба могла остаться на legacy-prefix. Она способна получить ответ соседнего обработчика и показать здоровый участок, который пользовательский звук больше не использует.
Здесь не было доказательства, что неверный healthcheck сам ломал аудио. Проблема в достоверности диагностики. При проверке всех DRS env-map те же ASR_IN_PREFIX и ASR_OUT_PREFIX передали в сервис страницы status. Тест теперь требует три согласованных пары параметров: Core, собственный live bridge и status probe.
# stack/webrtc.drs:629-630 - env служебного status-сервиса.
ASR_IN_PREFIX: ASR_IN_PREFIX,
ASR_OUT_PREFIX: ASR_OUT_PREFIX,
# Значения берутся из общей конфигурации, а не копируются новой строкой.
# Проверка ответа на плохой пакет всё равно не доказывает распознавание речи.
Commit 98eead5; test_core_reads_the_same_prefixes_as_the_deployed_bridge. Проверено также преобразование DRS в Compose и выбранные env в runtime. Полные environment не включались в отчёт.
Успешный WebRTC ещё не означает точное распознавание
После восстановления передачи стали измеримы ошибки самой транскрипции. В первой пробе после разделения потоков пропало начальное "Сколько". В последней повторной пробе исходный 2+2.flac распознан полностью: "Сколько будет 2 плюс 2? Ответь коротко." Это полезный положительный контроль, но одного повтора недостаточно, чтобы объявить потерю начала устранённой.
Что проверили на шуме
Использовали одну синтетическую русскую запись длиной 8.1038125 s, 16 kHz mono. Текст известен заранее: "Сколько будет два плюс два? Ответь коротко. Сегодня утром инженер проверил микрофон, соединение и качество распознавания речи." К ней добавляли белый гауссов шум с seed 20261002. SNR вычислялся по RMS всей записи, включая паузы; общий scale предотвращал клиппинг.
| Live-проба | Финальных фраз | Длительность | Ошибки WER | Ответ агента |
|---|---|---|---|---|
| Исходный 2+2.flac | 1 | 2.176 s | 0 / 7 слов | 4 |
| Синтетическая речь, 20 dB | 1 | 7.968 s | 0 / 17, 0% | 4 |
| Та же речь, 10 dB | 1 | 7.968 s | 0 / 17, 0% | 4 |
| Та же речь, 0 dB | 1 | 7.872 s | 8 / 17, 47.1% | 4 - не подтверждает ASR |
При 0 dB результат был: "только будет 2 плюс 2, а треть коротко. Сегодня утром на джене проверим микрофон. Приединили и качество распознавания речь." Нормализация для WER убирала регистр и пунктуацию, приравнивала 2 к "два"; остальные слова не исправлялись. WER считает замены, удаления и вставки относительно 17 слов исходного текста, а не оценивает смысл ответа.
Ответ "4" при испорченной транскрипции не делает распознавание правильным. Вопрос уже был в истории чата; модель могла восстановить ожидаемый смысл. Поэтому точность ASR измеряется до агента по эталонной записи.
Что могло повлиять и что пока не доказано
Silero VAD выделяет интервалы речи; он не очищает запись от шума. В проверенной конфигурации VAD_THRESHOLD=0.8 и SPEECH_PAD_MS=30. Малый запас перед фразой - кандидат для проверки пропущенного начала, но причина ещё не подтверждена сравнением исходного PCM и вырезанного WAV.
Тестовые записи подавались через временный AudioContext вместо физического микрофона, после установки media, с тишиной до и после. Штатные WebRTC, SDP, Core и ASR не подменялись. Но эта подача не проходит обычную обработку физического захвата; в панели AEC/NS/AGC показаны как "не сообщено". Поэтому эти испытания не доказывают качество браузерного шумоподавления или эхоподавления.
Как нужно устранять, а не просто менять параметры
- На разрешённой эталонной выборке сопоставить исходный PCM, интервалы VAD и фактически отправленный ASR-файл. Сначала найти, на каком этапе исчезает первое слово.
- Сравнить несколько порогов и padding на одних записях. Отдельно считать потерянные начала, ложные фразы на шуме и дополнительную задержку.
- Проверить настоящий микрофон с включённым/выключенным browser NS. Для нативного клиента отдельно сравнить Sonora; её AEC требует опорного сигнала динамиков и согласования задержки.
- Оценивать WER, p95 задержки и CPU. Не выбирать обработчик по одному красивому ответу LLM.
Источники параметров: DRS VAD/padding; наблюдения и таблица: закреплённый журнал проверок. Sonora в этом исправлении не внедрялась.
Агент не завершил один ход за 270 секунд
В первой пробе после разделения namespace media оставалось подключённым и ASR завершился, но агент не прислал финальный ответ. Интерфейс показал timeout через 270 s и вернул возможность говорить. Этот отказ находится после распознавания, поэтому его нельзя объяснить STUN 701 или отсутствием RTP.
Как локализовали
По идентификаторам связали ASR, workflow request и LLM routing. Для этого хода в LLM-логе есть routing_decision длительностью 8843 ms, но вызов final_response не появился. Read-only health с валидными metadata вернул healthy; hashes кода Ruby runtime совпали с checkout. После отказа отдельный текстовый запрос, три шумные голосовые пробы и финальный контроль получили ответ.
Идентификаторы для повторного поиска в Insight
Начало workflow: 02.10.2026 10:22:08 UTC / 13:22:08 MSK.
ASR phrase: id-c5e7994c9863
Workflow request: id-0c49db5202ba
LLM routing: id-4a360d218040
Что проверили и чего не исправляли
В отдельной локальной пробе использовали настоящий NATS и настоящий Ruby runtime, заменив только LLM тестовым ответчиком. Три последовательных графа завершились, шесть LLM request/reply прошли в ожидаемом порядке, request ID совпали. Значит, базовый механизм await/reply работает на этом тесте. Это не воспроизведение единичного production-зависания.
Ruby-граф не меняли, сервис не перезапускали ради маскировки симптома. Не установлены ни deadlock, ни медленный GPU, ни потерянный NATS reply. Для следующего отказа нужно добавить безопасные lifecycle-логи: выбранная нода, начало/конец каждого внешнего await, request ID и отправка reply. В них не нужны текст, аудио или credentials.
Код границ: request/timeout агента; GraphRunner.run(); Service.safe_respond(); изолированная NATS-проба.
Что видно до исправления и после него
Это реальные снимки live-страницы. Они показывают UI и доступные браузеру поля, но не заменяют серверный журнал и code review. Изображения встроены в HTML; при переносе документа папка со снимками не нужна.
До: выбранной пары нет, пакеты не отправляются
Промежуточно: TURN настроен, но media ещё не работает
После: media подключено, контрольная фраза дошла до агента
id-578255ccb46c, 2.176 s, одна финальная ASR-фраза и один ответ. Наличие 701 на этом же снимке показывает, почему его нельзя автоматически считать отказом всего ICE. Голосовой тест затем остановлен; временные тестовые обёртки удалены.При итоговом контроле подтверждались не только надпись "Подключено", но и selected remote relay, DTLS connected, рост исходящих RTP и корреляция ASR с этим чатом. Озвучивание ответа и физический AEC в этот критерий не входили.
Какие тесты закрывают конкретные причины
| Уровень | Результат на 02.10.2026 | Что не доказывает |
|---|---|---|
| Core unit / contract | 39 Python-тестов и 36 subtests: ICE-конфигурация, DATA parsing, TCP/UDP, integrity, retry cleanup, 403 и ASR prefixes. | Доступность реальной внешней сети и точность ASR. |
| Ruby | 25 RSpec-примеров. Отдельно настоящий NATS: три графа, шесть stub LLM replies и корреляция request ID. | Отсутствие редких production-таймаутов или качество модели. |
| Frontend | 23 новых проверки, Vitest cleanup, TypeScript, build и runtime settings. Локальный UI: connected, 503, denied, разрывы, экспорт, клавиатура и mobile. | Готовность production ASR; в UI fixture NATS/HTTP управляемые. |
| Настоящий media transport | Chromium + native aiortc: connected и 50 декодированных аудиофреймов. Core HTTP / PCM integration; NATS sink в этой пробе заменён. | End-to-end модель и агент. |
| DRS → Compose | Ruby syntax и три согласованные env-map prefixes; секретные значения не выводились. | Что этот Compose уже запущен на сервере. |
| Runtime | Выбранные env Core/bridge/status, текущий task и hashes двух Core-файлов сверены с checkout. | Доставку конкретного пользовательского звука. |
| Live end-to-end | 2+2.flac и три шумные записи в том же чате, RTP → ASR → agent. 1 final phrase / 1 turn; severe-noise ошибки измерены отдельно. | Статистическую надёжность на всех устройствах и помещениях. |
Как повторить безопасные локальные проверки
Команды выполнять из корня checkout. Они не предназначены для серверного read-only tty и не развёртывают production. Зависимости устанавливаются в отдельное окружение; готовые файлы проекта не переписываются.
# Создать отдельное Python-окружение в checkout, не менять системный Python.
python3 -m venv .test-venv
# Установить тестовые зависимости из закреплённого файла репозитория.
.test-venv/bin/python -m pip install -r tests/requirements.txt
# Проверить Python Core и остальные contract-тесты проекта.
.test-venv/bin/python -m pytest -q tests
# Запустить штатный frontend check; npm-зависимости должны быть установлены.
npm --prefix src_front run check
Для Ruby request/reply готов отдельный ruby_nats_workflow.rb с русскими комментариями. Он использует настоящий локальный брокер без рабочих credentials и подменяет только LLM. Команды запуска контейнеров приведены в DOCS/DIAGNOSTICS.md.
Ошибки тестового стенда, не дефекты production
При подготовке одной DRS-пробы было неверное значение fixture Basic Auth; тестовый формат исправлен на username:password. В начальной Ruby-пробе runtime.version=0 нарушал контракт; после использования версии от 1 проверка прошла. Буферизация вывода наблюдателя задерживала строку ready до завершения процесса. Эти случаи нельзя выдавать за новые причины live-отказа или за исправления рабочего сервиса.
Почему зелёная сборка сама по себе была недостаточна
Каждое подтверждение относится к своему уровню. Commit доказывает, что изменение сохранено в Git. Job build подтверждает выпуск образа. Только runtime-проверка показывает, что нужный код и параметры реально работают в контейнере. И только live-запись подтверждает весь голосовой путь.
| Этап | Факт |
|---|---|
| Последний рабочий rollout | Commit 98eead5b3add0f02bdb551eec4208bbae0c35f5c, pipeline 4848, test jobs 9501/9502/9503, build 9504 и manual deploy 9505 - успешны. |
| Core task, сверенный после rollout | m7v49epafs0ad3u2l3xa2uir3; container 2040e9c7b18e. Выбранные env и hashes settings.py, aioice_compat.py совпали с checkout. |
| Новая документация и локальная проба | Commit 590708a577a8aab16b883affab8d47e83a6893ea; pipeline 4850: Python 9508, front 9509, Ruby 9510, build 9511 - success. Deploy 9512 оставлен manual. |
| Почему не новый deploy | 590708a меняет отчёт проверок и test probe, а не runtime-конфигурацию. Новый deploy не запускался. Pipeline 4850 целиком нельзя называть success: его итоговое состояние manual. |
Контрольные hashes Core-файлов
# Сверка выполнена в read-only режиме, только указанные файлы.
settings.py:
36a3800c1cd0ba5ad207c322632e5c70b7c6110a1c0a2959f5b637e29beefdaf
aioice_compat.py:
f26705e850216428112f73905d134edc7ecadd479bd30b2925124d48f9138be1При rollout свежая deploy-job также потребовала недостающие обязательные настройки CI. Согласованные переменные были добавлены в свой проект; значения не включены в этот файл. Это препятствие поставке конфигурации, а не самостоятельная причина потери аудио. После поставки env проверены непосредственно в нужных сервисах.
Серверные терминалы gis-master tty и giscloud tty разрешены владельцем строго read-only. Через них не менялись firewall, файлы, credentials и не выполнялись restart/deploy. Изменения поставлялись через согласованный CI.
В этом документе перечислены результаты уже выполненных испытаний. При составлении самого HTML новые commits, push, rollout, аудиозапросы и перезапуски приложения не выполнялись. Исходный dirty checkout и соседний ASR stack не изменялись.
Что проверять следующим и по какому критерию принимать
| Проблема | Способ локализации / изменения | Измеримый результат |
|---|---|---|
| Потеря начала фразы | Сравнить PCM до VAD, интервалы и WAV после вырезания. Потом A/B порога/padding на эталоне. | Число потерянных первых слов, WER и p95 задержки. Сначала подтвердить границу потери. |
| Сильный шум | Эталон разных голосов/шумов, физический mic A/B browser NS. Для нативного клиента - отдельная Sonora-проба с playback reference. | WER на одинаковых SNR, ложные ASR-фразы на шуме, latency и CPU. Sonora не заменяет Silero. |
| Редкий workflow timeout | Коррелированные start/end внешних await, переходов нод и NATS reply без payload; повторные последовательные и параллельные ходы. | Установленная граница зависания, доля timeout и p95/p99 длительности. Не принимать просто по healthy. |
| Оставшиеся 701 | Сопоставить попытку/interface/address family с успешной выбранной парой. Отдельно проверять DNS/STUN для нужной сети. | Доля успешных ICE-соединений, время connect и воспроизводимый диагноз 701. Не выключать журнал. |
| Будущее обновление aioice | Прогнать DATA/ChannelData/integrity/403/cleanup regression suite и real relay-probe на новой версии. | Нет потери NAT-пакетов и утечек задач; убрать адаптер, только когда штатная реализация подтверждена. |
Эти пункты - предложения, не выполненные изменения. Сейчас доказано восстановление проверенного голосового пути и устранение двойной обработки; не доказана безотказность при любых сетях, шуме и длительной работе.
Где смотреть каждое решение
Ссылки закреплены на конкретном commit, чтобы дальнейшие изменения main не меняли объяснение. Нужна авторизация в GitLab. Без сети остаются схемы, снимки и встроенные фрагменты; внешние страницы исходников в HTML не копировались целиком.
Core: входной звук и TURN
| Файл / функция | Роль | Проверка |
|---|---|---|
| aioice_compat.py:19 decode_data_indication | Внешний TURN DATA → точный payload и peer; отклонение неверных envelope/атрибутов. | test_core_aioice_compat.py:36-100 |
| aioice_compat.py:60 DataIndicationMixin | Приём DATA в ICE; обработка явно запрещённого peer при отправке. | TCP/UDP, ChannelData, 403 waiters, 500, integrity |
| aioice_compat.py:103 ClosingStunProtocol | Отмена и ожидание STUN request до socket close. | test_core_aioice_compat.py:180 |
| handle_offer.py:65 _webrtc_pc_create | Подключение адаптера, RTCConfiguration и создание audio peer. | Core HTTP + aiortc integration |
| handle_track.py:41 WhisperStreamNode:76 | Resample в 16 kHz mono, binary PCM+JSON stream и JSON reply. | PCM integration + live ASR correlation |
Frontend и конфигурация
| Источник | Назначение |
|---|---|
| session.ts | Connect/dispose, независимые сроки, runtime peer-state, stats, mute. |
| negotiate.ts:26 | JSON SDP offer, отменяемый fetch, answer validation. |
| diagnostics.ts | Статус, getStats, реальные getSettings, redaction и safe endpoint. |
| connectionDiagnosticsUi.ts chatUi.ts | Панель, сворачивание, журнал/экспорт и карточки ошибок. |
| settings.py:28 stack/webrtc.drs | Env-reading и согласованный namespace Core/bridge/status; серверная TURN-конфигурация. |
Последовательность поставленных изменений
2e5aa37- правдивые UI-статусы и диагностика;d4432be- разделение сроков.1de618c- аутентифицированный TURN Core через TCP.04c4485- DATA indication и отмена STUN до закрытия.50f714f- локальная обработка запрещённых TURN peer.252aea4- отдельный ASR stream;98eead5- тот же namespace для status.590708a- закреплённое описание результатов и Ruby/NATS probe; без нового runtime deploy.
Основной журнал: DOCS/DIAGNOSTICS.md. Live Core-логи: Insight src_core; Coturn: Insight coturn. Важно выбирать текущий task: старые ошибки остаются в истории.
Что означают термины в этом разборе
| Термин | Простое объяснение |
|---|---|
| Signaling / SDP | Обмен описанием соединения: какие медиа, адреса, протоколы и параметры согласованы. Аудио в SDP не передаётся. |
| ICE / candidate pair | Проверка возможных маршрутов между участниками и выбор одной работающей пары адресов. |
| STUN / srflx | Получение внешнего адреса за NAT. srflx - адрес, замеченный STUN-сервером; он не обязан быть пригодным для любого назначения. |
| TURN / relay | Сервис пересылки, предоставляющий публичный адрес. relay - адрес его выделенной allocation, а не новый распознаватель речи. |
| prflx | Фактический адрес peer, найденный по авторизованной ICE-проверке, а не только объявленный в SDP. |
| DATA indication / ChannelData | Две формы TURN-передачи. DATA содержит атрибуты peer и payload; ChannelData использует уже привязанный номер канала. |
| DTLS / SRTP / RTP clock | DTLS согласует защиту медиа, SRTP передаёт защищённые аудиопакеты. RTP clock нужен для временных меток, не описывает сам по себе физический микрофон. |
| PCM16LE / Opus | PCM16LE - сырые 16-битные аудиосэмплы, little-endian. Opus - сжатый аудиокодек WebRTC. Это разные представления одного звука. |
| NATS subject / queue | Именованный адрес сообщений. Обычный subject доставляет всем подписчикам; queue group распределяет между своими участниками. В этом исправлении выбран отдельный namespace. |
| VAD / ASR / NS / AEC | VAD ищет речь и паузы; ASR превращает речь в текст; NS подавляет шум; AEC уменьшает эхо воспроизведения. Одно не заменяет другое. |
| SNR / WER | SNR - соотношение мощности речи и шума; ниже хуже. WER - число замен, вставок и удалений слов, делённое на число слов эталона. |
| Task / image / commit | Commit - версия исходников; image - собранный пакет; task - конкретный запущенный экземпляр сервиса. Их нужно связывать проверкой, а не предполагать совпадение. |
Как пользоваться этим файлом
HTML самодостаточен: стили, код навигации, схемы и четыре исходных снимка находятся внутри. Перенесите один файл и откройте его в браузере. Интернет нужен только для переходов в GitLab, Insight и live-чат; доступ к закрытому исходнику требует вашего входа.
Оглавление ведёт во все разделы и подсвечивает текущий. Ссылки из SVG работают как обычные anchors; при переходе пояснение получает фокус. Back/Forward и прямые ссылки с #section сохраняются. На телефоне большие таблицы и схемы прокручиваются внутри своего блока, а не растягивают страницу. Для печати меню скрывается, основной текст, таблицы, схемы и снимки остаются.
Визуальная проверка этого HTML не выполнена. Разрешённый браузер отказался открывать file:-адрес по политике безопасности. Запрет не обходился через локальный сервер, другой браузер или CDP. Мобильный вид, работа интерактивных контролов и печать пока не подтверждены в браузере.
Локальная структурная проверка выполнена отдельно: 18 разделов и 18 пунктов меню, 28 ссылок из SVG-блоков и 62 внутренних перехода имеют существующие цели; ID не повторяются. Четыре исходных изображения встроены и совпадают с файлами по SHA-256; 31 уникальная ссылка на исходники и line anchors проверены по Git checkout, как и семь ссылок на commits. Синтаксис JavaScript проверен, внешних зависимостей изображений, CSS и скриптов нет. Это проверка структуры, не подмена визуальной приёмки.
Встроенные фрагменты показывают существенную часть реального кода; поясняющие русские комментарии добавлены здесь, без изменения исходников. Скриншоты не перерисованы. Метрики относятся только к описанной выборке и не экстраполируются на все устройства.