Все голосовые платформы решают, что человек договорил, по таймеру тишины. Я замерил, во что это выливается на живой русской речи, и получил цифру, которая меня удивила: из 1275 мс задержки перед ответом 1200 мс — это ожидание секундомера, и только 75 мс — работа самой модели.
Ниже — методика, замеры и решение, которое даёт 316 мс вместо 1200 при двух обрывах вместо тридцати восьми. Всё воспроизводимо, детектор — на правилах, без обучения и без GPU.
Проблема: тишина — плохой признак конца мысли
Речевые API определяют конец реплики через VAD — детектор голосовой активности — с порогом молчания. У OpenAI Realtime это silence_duration_ms, у Яндекса он же, у коммерческих платформ вроде русскоязычных обзвонщиков это ручка в интерфейсе с подписью «Пауза для завершения речи».
Логика простая: замолчал на N миллисекунд — значит, договорил.
Проблема в том, что человек молчит в двух совершенно разных ситуациях: когда закончил мысль и когда подбирает слово. Акустически это одно и то же. Различить их по громкости невозможно в принципе — различие лежит в том, ЧТО было сказано до паузы.
Методика замера
Чтобы результат не зависел от моей дикции, речь я синтезировал: взял SpeechKit, получил сырой PCM 16 кГц, срезал хвостовую тишину (иначе замер врёт — об этом ниже) и подавал в речевой API кусками по 20 мс, как это делает телефонная линия.
Фраза — типовая для входящего звонка, с естественной паузой после приветствия:
«Здравствуйте!» → пауза → «Подскажите, шкаф Норд есть в наличии и когда сможете привезти?»
Прогонял через Yandex Realtime API (протокол совместим с OpenAI Realtime), меняя только silence_duration_ms.
await ws.send(json.dumps({"type": "session.update", "session": { "type": "realtime", "output_modalities": ["audio"], "audio": { "input": { "format": {"type": "audio/pcm", "rate": 16000}, "turn_detection": {"type": "server_vad", "silence_duration_ms": SIL}, }, "output": {"format": {"type": "audio/pcm", "rate": 16000}, "voice": "alena"}, }}}))
"type": "realtime", "output_modalities": ["audio"], "audio": { "input": { "format": {"type": "audio/pcm", "rate": 16000}, "turn_detection": {"type": "server_vad", "silence_duration_ms": SIL}, }, "output": {"format": {"type": "audio/pcm", "rate": 16000}, "voice": "alena"}, }}}))
Замерял время от последнего отправленного чанка речи до первого чанка аудио в ответе.
Результаты
|
silence_duration_ms |
Первый звук ответа |
Что агент расслышал |
|---|---|---|
|
400 |
3 мс |
только «здравствуйте» |
|
800 |
5 мс |
только «здравствуйте» |
|
1200 |
1275 мс |
всю фразу целиком |
На 400 и 800 мс агент отвечал практически мгновенно — но отвечал он на приветствие. Вопроса про шкаф он не слышал вообще: VAD сработал в паузе после «Здравствуйте!», буфер закоммитился, и всё, что человек сказал дальше, ушло в следующий ход или в никуда.
На 1200 мс фраза распозналась целиком и ответ пришёл по существу. Ценой секунды с четвертью тишины в трубке.
И отдельное наблюдение: на 1200 мс обрыв всё равно случался — примерно через раз. Прогоняя семь голосов подряд одной и той же фразой, я получил три ответа «Здравствуйте, чем могу помочь?» вместо ответа по делу. То есть таймер не просто медленный — он ещё и недетерминированный, потому что естественные паузы плавают.
Главная цифра
Вычитаем: 1275 мс задержки минус 1200 мс порога = 75 мс на распознавание, генерацию ответа и синтез речи.
Речевая модель работает практически мгновенно. Всё, что собеседник воспринимает как «система тормозит», — это ожидание секундомера, которое мы сами и задали.
Отсюда вывод: оптимизировать здесь нечего. Ни более быстрая модель, ни более близкий регион, ни стриминг не дадут ничего, пока в схеме стоит фиксированный таймер. Задержку создаёт не вычисление, а решение о том, что человек договорил.
Решение: смотреть на смысл, а не на тишину
«Мне нужно» — очевидно не конец фразы, независимо от того, сколько человек молчит после. «Сколько стоит доставка до Екатеринбурга» — очевидно конец. Различие видно в тексте, который уже распознан.
Я собрал детектор на правилах русского синтаксиса. Он не заменяет таймер, а решает, какой таймер взять:
-
договорил → короткое ожидание (250 мс);
-
не договорил → длинное (1400 мс), человек ещё думает;
-
непонятно → среднее (700 мс).
Что именно ловят правила
Висящие служебные слова. Союзы, предлоги, частицы в конце: «мне нужно два шкафа и», «я хотел бы уточнить по». После них фраза физически не может закончиться.
Инфинитив без дополнения. «Сколько будет стоить» — глагол требует объекта. С оговоркой: часть глаголов самодостаточна («можно перезвонить» — целая просьба), поэтому нужен белый список.
Заходы и приветствия. «Здравствуйте», «скажите пожалуйста», «у меня такой вопрос» — анонс реплики, сама мысль впереди. Это ровно тот случай, на котором ломается таймер 400 мс.
Заминки. «Ну это самое», «как бы вам сказать», «сейчас посмотрю» — человек тянет время и точно продолжит.
Диктовка номера. Считаем числительные подряд с конца: семь и больше — телефон назван целиком, меньше — ещё диктует. Иначе агент влезает в середину номера.
Разговорные хвосты вопроса. «Это робот что ли», «а по цене что» — формально кончаются на частицу, но это законченный вопрос. Проверяются до правила о служебных словах, иначе оно даст ложное срабатывание.
HANGING = {"и", "а", "но", "или", "что", "чтобы", "если", "когда", "в", "на", "за", "для", "до", "от", "из", "с", "по", "у", ...}def classify(text: str) -> Decision: w = normalize(text).split() last = w[-1] if text.endswith(QUESTION_TAILS): # «что ли», «а что» return Decision(DONE, 250) if numeral_tail(w) >= 7: # телефон продиктован return Decision(DONE, 250) if last in HANGING: return Decision(CONTINUES, 1400) if last.endswith("ть") and last not in SELF_SUFFICIENT: return Decision(CONTINUES, 1400) ...
"в", "на", "за", "для", "до", "от", "из", "с", "по", "у", ...}def classify(text: str) -> Decision: w = normalize(text).split() last = w[-1] if text.endswith(QUESTION_TAILS): # «что ли», «а что» return Decision(DONE, 250) if numeral_tail(w) >= 7: # телефон продиктован return Decision(DONE, 250) if last in HANGING: return Decision(CONTINUES, 1400) if last.endswith("ть") and last not in SELF_SUFFICIENT: return Decision(CONTINUES, 1400) ...
Как мерил результат
Набор — 72 реплики из реальной телефонной лексики, размеченные вручную: закончена мысль или нет. Внутрифразовую паузу колебания взял 900 мс (типичная для разговорной речи).
Метрика важна не меньше решения. Считать среднее ожидание по всем репликам подряд — самообман: молчание внутри чужой фразы человек не замечает, он в этот момент сам думает. Поэтому задержка считается только по репликам, где собеседник договорил — это то, что он реально ощущает.
|
Стратегия |
Обрывов |
Молчание перед ответом |
|---|---|---|
|
таймер 400 мс |
38 |
400 мс |
|
таймер 800 мс |
38 |
800 мс |
|
таймер 1200 мс |
0 |
1200 мс |
|
по смыслу |
2 |
316 мс |
Точность решения — 97%, 70 из 72. Работает за микросекунды, без обращения к модели, без сети и без GPU, то есть в бюджет разговора не добавляет ничего.
Где правила не справляются
Честно про оставшиеся два случая: «нет, вы меня не поняли» и «мы вчера с вашим менеджером». Грамматически обе фразы завершены — и правила говорят «отвечай». Но человек, сказавший «нет, вы меня не поняли», гарантированно продолжит.
Это уже не синтаксис, а прагматика: намерение говорящего. Правилами такое не берётся, здесь нужна небольшая обученная модель. Планирую взять готовый семантический детектор конца реплики и сравнить на том же наборе — если разница окажется в пределах пары процентов, правила выигрывают по простоте и скорости.
Отдельная ловушка: замер, который врёт
Первые цифры реакции у меня получились 420 мс вместо честных 180 мс — и я чуть не оставил их в отчёте. Причина: синтезированный файл начинается с полоски тишины. Я подавал его в детектор и мерил от старта файла, а не от первого звука речи. Срезал ведущую тишину — цифра сошлась.
Мораль простая: если меряете задержки на синтезированной речи, срезайте тишину с обоих концов. Иначе вы измеряете свой генератор, а не систему.
Что с этим делать
Если вы выбираете голосовую платформу: попросите на демо сказать «Здравствуйте», выдержать секундную паузу и продолжить вопрос. Половина решений ломается на этом при вас.
Если вы её строите: не оптимизируйте модель, пока в схеме фиксированный таймер. Сначала посмотрите, какая доля вашей задержки — это ожидание, и только потом ускоряйте вычисления.
Замеры воспроизводимые, набор реплик и код детектора могу выложить отдельно, если будет интерес. Возражения и контрпримеры в комментариях приветствую — особенно случаи, где правила должны сломаться.
ссылка на оригинал статьи https://habr.com/ru/articles/1068162/