Как устроен VAD в Apex Voice: не потерять начало фразы
Silero в Web Worker, буфер перед аудиогейтом и восстановление после сбоев: из каких решений складывается голосовая активация микрофона.
В голосовом приложении автоматическое включение микрофона кажется простой функцией: человек заговорил — передаём звук, замолчал — перестаём. Но между этими состояниями есть начало слова, короткие паузы, шум клавиатуры и вкладка, которую пользователь свернул во время разговора.
В Apex Voice VAD — Voice Activity Detection, определение речевой активности — поэтому устроен не как один переключатель. Это цепочка обработки звука, принятия решения и управления передачей. Ниже — разбор текущей клиентской реализации, а не обещание идеального распознавания в любых условиях.
Определять речь, а не просто громкость
В клиенте используется Silero VAD: ONNX-модель выполняется через ONNX Runtime Web с WASM-бэкендом. На вход поступают аудиофреймы, на выходе получаем оценку вероятности речи.
Основной путь обработки вынесен в Web Worker. AudioWorklet передаёт ему PCM-аудио через MessagePort, а worker возвращает решение об открытии аудиогейта. Обновление интерфейса получает отдельное сообщение: визуальная подсветка говорящего не должна быть обязательным посредником между моделью и звуком.
Перед анализом сигнал приводится к 16 кГц. Worker ожидает фрейм из 512 отсчётов — это 32 мс аудио. Число описывает размер входного окна, а не полную задержку разговора: к нему добавляются вычисления, буферизация и передача по сети.
Проверка громкости здесь тоже есть, но в другой роли. Если RMS ниже 0,001, фрейм обрабатывается как тишина без запуска модели. Это предварительный фильтр почти пустого сигнала, а не замена распознавания речи.
Почему недостаточно одного порога
Оценка модели может колебаться возле границы. Если открывать и закрывать передачу по одному значению, короткие изменения будут постоянно переключать состояние.
В Apex Voice пороги начала и продолжения речи различаются. Для начала требуется более высокая уверенность, для продолжения — более низкая. Такой разрыв, или гистерезис, помогает удерживать уже начавшуюся реплику.
Есть и проверка последовательности: для открытия нужны два речевых фрейма. Закрытие устроено осторожнее: сначала набираются четыре фрейма тишины, затем начинается отсчёт дополнительного ожидания — 1200 мс. Если речь возобновилась, ожидание сбрасывается.
Это намеренный компромисс. Передача не закрывается на каждой паузе между словами, но и после окончания реплики остаётся открытой некоторое время. Эти параметры нельзя считать универсальными для любого микрофона и шумового окружения; в интерфейсе предусмотрена настройка чувствительности.
Как сохранить начало слова
Детектор не может принять решение раньше, чем получит аудио для анализа. Если начать пропускать только новые отсчёты после срабатывания VAD, начало фразы может остаться за закрытым гейтом.
В клиентской сессии для этого задаётся pre-roll в 180 мс. AudioWorklet держит кольцевой буфер и выводит задержанный сигнал. Пока модель анализирует свежий звук, его начало ещё находится в буфере. Когда гейт открывается, оно получает шанс попасть в исходящий поток.
Здесь важно не обещать невозможного: буфер не гарантирует сохранение каждого звука при любой задержке вычислений. И это не бесплатное улучшение — текущая реализация задерживает аудиосигнал примерно на длину pre-roll. Речь идёт о выборе между дополнительной задержкой и риском обрезать начало реплики.
VAD не имеет права отменять mute
Решение «похоже на речь» не означает «можно передавать звук».
В worklet разрешение передачи отделено от решения детектора. Целевой коэффициент усиления становится ненулевым только тогда, когда разрешена передача и открыт VAD-гейт. Пользовательский mute, серверный mute и право публикации проверяются отдельно от речевой активности.
Это существенная граница ответственности: модель классифицирует сигнал, но не определяет права пользователя. При этом закрытый гейт не равнозначен освобождению микрофона — аудиозахват и разрешение пропускать сигнал являются разными состояниями.
Что происходит при сбоях
Одного удачного запуска модели недостаточно. В worker есть таймаут вычисления, heartbeat и ограничение очереди десятью фреймами. Если обработка отстаёт, старые ожидающие фреймы отбрасываются: решение о речи несколько секунд назад уже мало помогает текущему разговору.
При ошибке worker закрывает свой гейт и сообщает о сбое. На уровне клиентской сессии предусмотрены резервный анализ и механизмы восстановления. Переключения микрофона сериализованы, чтобы запуск новой сессии не конфликтовал с остановкой старой.
Отдельная тонкость — скрытая вкладка. Вынос вычислений из UI-потока уменьшает зависимость от отрисовки, но не даёт абсолютной гарантии работы при любых ограничениях браузера или ОС. Поэтому проверки зависания учитывают фоновое состояние, а восстановление имеет задержку и защиту от слишком частых перезапусков.
Намеренную тишину на выходе гейта также нельзя автоматически считать поломкой: иначе исправно работающий VAD сам провоцировал бы восстановление.
Где здесь сервер
В проекте предусмотрен и серверный агент с Silero, который отправляет события начала и конца речи через LiveKit. Это отдельный режим, а не обязательный участник каждого клиентского решения.
Клиент принимает такие события только от участника типа AGENT и проверяет их формат. Локальный speaking-статус в VAD-режиме остаётся за клиентской сессией: серверные события не должны спорить с ней и вызывать мерцание индикатора.
Главное — поведение всей цепочки
Самая интересная часть этой реализации находится вокруг модели: сохранить начало слова, пережить короткую паузу, не обойти mute и восстановиться после сбоя.
Следующий полезный шаг для оценки качества — воспроизводимые записи с тихой речью, клавиатурой и разными микрофонами, а также измерения задержки и ложных срабатываний. Без таких измерений корректнее говорить о принятых инженерных решениях, а не о процентах точности или превосходстве над другими приложениями.