Все голосовые платформы решают, что человек договорил, по таймеру тишины. Я замерил, во что это выливается на живой русской речи, и получил цифру, которая меня удивила: из 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 мс — и я чуть не оставил их в отчёте. Причина: синтезированный файл начинается с полоски тишины. Я подавал его в детектор и мерил от старта файла, а не от первого звука речи. Срезал ведущую тишину — цифра сошлась.

Мораль простая: если меряете задержки на синтезированной речи, срезайте тишину с обоих концов. Иначе вы измеряете свой генератор, а не систему.

Что с этим делать

Если вы выбираете голосовую платформу: попросите на демо сказать «Здравствуйте», выдержать секундную паузу и продолжить вопрос. Половина решений ломается на этом при вас.

Если вы её строите: не оптимизируйте модель, пока в схеме фиксированный таймер. Сначала посмотрите, какая доля вашей задержки — это ожидание, и только потом ускоряйте вычисления.

Замеры воспроизводимые, набор реплик и код детектора могу выложить отдельно, если будет интерес. Возражения и контрпримеры в комментариях приветствую — особенно случаи, где правила должны сломаться.