Перейти к содержанию

Голосовое демо

Страница /voice-demo — открытый разговор с ботом о возможностях Speech Expert. Регистрация и API-ключ не нужны. Оформление использует оливковую палитру главной страницы и приборы радиостанции. Для микрофона нужен HTTPS либо localhost.

Демо распознаёт русский и узбекский и отвечает на определённом языке. Четыре финальных ASR-гипотезы оценивает та же LLM, которая формирует ответ; отдельного запроса классификации нет. Для unknown ответ русский: при понятном намерении бот отвечает по существу, иначе уточняет язык и просит повторить вопрос. Для mixed бот уточняет по-русски, на каком языке продолжить. На каждом ходе язык оценивается заново; история не закрепляет предыдущий язык принудительно.

Узбекский текст озвучивается потоковым Higgs с голосом madina-higgs (uz-UZ), русский — прежним голосом (ru-RU). Произношение узбекского голоса экспериментальное. Исправленная реплика пользователя сохраняет исходный язык, без перевода.

Для этого пути нужны уже запущенные русский FastConformer, Vosk, узбекский FastConformer (обычно порт 8044), общий Triton и Higgs. GigaAM UZ использует исходные FP32-веса в Triton: VOICE_DEMO_GIGAAM_UZ_BACKEND=triton, VOICE_DEMO_GIGAAM_UZ_TRITON_SERVER_URL=127.0.0.1:8201, VOICE_DEMO_GIGAAM_UZ_TRITON_MODEL=gigaam_uz_triton. Таймаут задаёт VOICE_DEMO_GIGAAM_UZ_TIMEOUT=30; отдельный worker на 8047 для текущего профиля не требуется. В репозитории устройство и проверка модели описаны во внутреннем файле internal_docs/gigaam-uz-triton.md. Молчаливого перехода к двум русским финалам при недоступности узбекских распознавателей нет.

Для возврата к прежнему HTTP-worker явно выберите VOICE_DEMO_GIGAAM_UZ_BACKEND=http и запустите вручную ./run_gigaam_uz.ps1 из корня проекта. Только этот режим использует VOICE_DEMO_GIGAAM_UZ_URL (по умолчанию http://127.0.0.1:8047/transcribe).

После ручного перезапуска API /api/voice-demo/config должен показывать asr_hypothesis_count: 5, asr_final_hypothesis_count: 4, supported_languages: ["ru", "uz"] и language_detection: "llm".

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

Предложение продукта

На этой же странице под демо расположен блок #voice-product: голосовой интерфейс для сайта, приложения или существующего AI-агента. В начале страницы есть переход к нему. Основной CTA «Обсудить пилот» ведёт на /contact?topic=api; дополнительные ссылки — на быстрый старт API и обсуждение размещения в контуре. Рекламная атрибуция сохраняется через общий captureContactAttribution, без подмены исходной кампании внутренними UTM-метками и без регистрации конверсии по простому нажатию ссылки.

Сценарии описаны как предмет пилота и внедрения. Отдельного публичного API универсального голосового агента, готовых CRM/телефонных коннекторов, фиксированной задержки или полной локальности демо этот блок не обещает. Состав решения, стоимость, сроки и размещение согласуются под задачу.

Локальная проверка кандидата на перебивание

По умолчанию браузер проверяет речь во время ответа через Silero VAD в отдельном Web Worker. ONNX-модель и однопоточный ONNX Runtime Web загружаются с нашего /static/vendor/, без CDN, GPU и отдельного запроса аудио на сервер VAD. Загрузка и прогрев происходят при подключении микрофона, до начала захвата.

Энергетический детектор по-прежнему предлагает кандидата после 160 мс активности и сразу приглушает ответ. Silero получает уже накопленный pre-roll (до 400 мс) и последующие PCM16@16kHz блоки; повторного ожидания 160 мс нет. Начальная настройка подтверждения — два последовательных полных окна по 32 мс с вероятностью речи не ниже 0,5. Потоковый ASR работает параллельно. Окончательная отмена ответа требует и положительного VAD, и подтверждения ASR; финальность потоковой гипотезы учитывается, поэтому для однословного финала не нужно ждать двух других моделей.

Обычная реплика, начатая вне ответа бота, работает как прежде. Если кандидат завершился без подтверждения речи, его текст не становится следующим вопросом. Каждый кандидат имеет отдельный идентификатор; состояние Silero сбрасывается между кандидатами, PCM-буферы ASR не отсоединяются при передаче в Worker. При недоступности модели, ошибке или зависании Worker остаётся прежняя проверка перебивания через ASR. Завершение диалога уничтожает Worker.

Для сравнения используйте /voice-demo?vad=off; обычный /voice-demo включает проверку. Состояние контроллера содержит vadStatus (loading, ready, fallback, disabled) и metrics.vadCheckMs: время от кандидата RMS до подтверждения Silero, а не полную задержку от начала речи до тишины в динамике. Названия моделей в пользовательском интерфейсе не показываются.

Короткие подтверждения вроде «угу» фильтруются одинаково в течение всего текущего ответа: при ожидании первого PCM, во время звучания и в паузе между TTS-фразами. Отсутствие запланированного звука само по себе не разрешает отменять ответ по короткому подтверждению. Обычная реплика вне ответа бота не фильтруется этим правилом.

Для узбекского ответа ранний русский поток подтверждает только явную команду остановки вроде «стоп». Остальные кандидаты ждут финальных гипотез, чтобы ошибочная русская версия узбекского подтверждения не останавливала ответ раньше проверки. В узбекских финалах короткие ha, xo'p, yaxshi считаются подтверждениями. Поэтому обычное перебивание узбекской фразой может срабатывать позже русского.

Silero не различает намерение перебить, короткое «угу» и речь из динамика. Эхоподавление браузера и текстовые правила остаются необходимыми. Порог RMS, короткие ответы на границе TTS и точное продолжение приостановленного звука требуют отдельных экспериментов; эта итерация добавляет проверку наличия речи.

Проверка 3 октября: 74 Node-теста голосового демо прошли. В Chrome реальный WASM Worker прошёл 21 проверку на записях/синтетических сигналах: три повтора «стоп», «подожди», «угу» подтверждены как речь, тишина, белый шум, короткий щелчок и тон 1000 Гц отвергнуты. Два окна выбраны после того, как три окна отсекли короткое «угу». Медиана вычисления одного 32-мс кадра по тестовым сигналам — 0,4–0,7 мс, p95 — 0,5–0,9 мс в финальном прогоне на этом компьютере. Это вычисление VAD, не полная акустическая задержка перебивания и не результат на телефоне. Реальное эхо и одновременную речь нужно проверять с микрофоном. Ассеты занимают около 13,3 МиБ и кэшируются браузером; первичная загрузка зависит от соединения. Материалы проверки: artifacts/voice-demo-20261003/silero/.

Отдельный LLM-провайдер демо

Прямой Google настраивается только для голосового демо:

VOICE_DEMO_LLM_PROVIDER=google
VOICE_DEMO_LLM_MODEL=gemini-3.5-flash-lite
VOICE_DEMO_LLM_API_KEY_FILE=E:/gemini_api_key.txt
# Необязательная альтернатива файлу; реальный ключ сюда не приведён.
VOICE_DEMO_LLM_API_KEY=

Endpoint фиксирован: https://generativelanguage.googleapis.com/v1beta/openai; запросы идут в /chat/completions с reasoning_effort=minimal. Модель, провайдер и учётные данные выбирает сервер. Если отдельные Google credentials демо не заданы, используются только GEMINI_API_KEY / GEMINI_API_KEY_FILE; ключи VseGPT и суммаризации к Google не отправляются. Явно выбранный файл ключа демо, который отсутствует, не читается или пуст, даёт configured=false без перехода к другому ключу.

Файл принимает сам ключ либо строку GEMINI_API_KEY=... / VOICE_DEMO_LLM_API_KEY=... (также GOOGLE_API_KEY и API_KEY), с UTF-8 BOM или без него. Пустое значение переменной VOICE_DEMO_LLM_API_KEY_FILE= снимает переопределение пути; это отличается от выбранного файла с пустым содержимым.

Без новых настроек провайдер по умолчанию — vsegpt, а отдельные model/key/file равны None: сохраняются прежняя модель LLM_SUMMARIZATION_MODEL и цепочка ключей summarization → correction → VseGPT. Для подключения Google только к демо не меняйте LLM_SUMMARIZATION_* или LLM_CORRECTION_*: они продолжают обслуживать суммаризацию, коррекцию и связанные операции. Для отката удалите четыре VOICE_DEMO_LLM_* переопределения или верните provider=vsegpt, убрав отдельные Google model/key/file.

Пул Google сохраняет свободные соединения до 120 секунд, VseGPT — до 0,5 секунды. Автоповторов POST генерации нет. После изменения настроек или Python-кода пользователь вручную перезапускает основной native API согласно AGENTS.md; сохранение конфигурации само по себе не подтверждает её применение работающим процессом. Проверить выбранную конфигурацию можно через /api/voice-demo/config.

Сравнение LLM 3 октября 2026

Конечный бенчмарк scripts/benchmark_voice_demo_llms.py использует тот же VoiceDemoService, системный промпт и три текстовые гипотезы. Основной набор: 8 запросов на модель (в том числе три повтора общего вопроса), temperature 0.2, max_tokens 400, последовательное чередование моделей. Первый фрагмент ответа отправляется в существующий локальный incremental TTS параллельно с оставшейся генерацией. Границы первого фрагмента сверены с браузерным SpokenSentences. Это задержка от запроса LLM до первого PCM, без ASR, VAD и воспроизведения на телефоне.

Модель Завершено Медиана первого текста Медиана первой фразы Медиана первого PCM
google/gemini-3.5-flash-lite-minimal 8/8 2186 мс 2364 мс 2981 мс
deepseek/deepseek-v4-flash-0731-alt-fast 8/8 2384 мс 2772 мс 3400 мс

В этих ответах обе модели не выдавали reasoning-блоков. Настоящие конфликты 15/50, приватность, CRM, перебивание и отсутствие подтверждённых цен обработаны корректно. Косметический кейс «15 / пятнадцать / 15» выявил дефект у обеих моделей: DeepSeek придумывал альтернативу 50 либо лишнее уточнение, текущая модель необоснованно отвечала про дайджест. Два дополнительных повтора подтвердили проблему; новый конфликт 17/70 обе модели корректно уточнили дважды. Это небольшой синтетический набор, а не оценка общей точности. Промпт и рабочая модель не изменены.

GLM z-ai/glm-4.7-xfast с текущим лимитом выдавал reasoning и обрывался; reasoning_effort: none и thinking: {type: disabled} не отключили его через проверенный маршрут VseGPT. Предварительный прогон с thinking и лимитом 1600 был остановлен после просьбы пользователя тестировать без рассуждений.

Подробности: artifacts/voice-demo-20261003/llm-no-thinking-full/results.json и artifacts/voice-demo-20261003/llm-no-thinking-asr-followup/results.json.

Дополнительные кандидаты VseGPT

В отдельном прогоне выполнено 54 реальных запроса: по 9 для текущей модели и пяти кандидатов, включая новый конфликт 17/70. Промпт 2026-10-03.3, лимит 400 токенов, temperature 0.2 и клиент остались теми же. Модели чередовались последовательно; каждая законченная первая фраза отправлялась в работающий локальный TTS.

Модель Завершено Первый текст, медиана Первая фраза, медиана Первый PCM, медиана Приемлемые ответы
google/gemini-3.5-flash-lite-minimal 9/9 2121 мс 2271 мс 2908 мс 9/9
qwen/qwen3.8-27b 9/9 2820 мс 2843 мс 3417 мс 8/9
google/gemini-3.1-flash-lite 9/9 2352 мс 2616 мс 3267 мс 8/9
openai/gpt-4.1-nano 9/9 2261 мс 2419 мс 3097 мс 3/9
meta-llama/llama-3.3-70b-instruct-superfast 9/9 2504 мс 4285 мс 5231 мс 7/9
meta-llama/llama-4-scout-superfast 1/9 11149 мс* 12486 мс* 13165 мс* 1/1 завершённого

* Scout: единственное успешное измерение; остальные восемь запросов дали provider_busy (клиент так отображает upstream HTTP 429). Доступность остальных моделей — 9/9; все 46 запущенных TTS завершились без ошибки. Медианы включают семантически неудачные ответы, поэтому качество показано отдельно.

Qwen подменила распознавание 15 записей лимитом дайджеста, Gemini 3.1 необоснованно переспрашивала о дайджесте. Nano трижды не ответила на общий вопрос, придумала конфликт 15/50 при согласованных 15, ушла в уточнение после перебивания и не объяснила отсутствие подтверждённых цены/SLA. Llama 3.3 лишний раз уточняла согласованные 15 и в вопросе о цене/задержке ответила только про цену. Текущая модель прошла все девять запросов этого прогона; её ранее обнаруженная нестабильность на косметических различиях остаётся ограничением.

В потоках не было reasoning/reasoning_content; скрытые reasoning-токены большинство ответов не раскрывает. Девять запросов включают три повтора общего вопроса: это сравнение на небольшом наборе, не оценка общей точности моделей. Текущая модель в этом прогоне быстрее всех кандидатов по медиане первой фразы и первого PCM; рабочая конфигурация не изменена.

Отдельная диагностика четырьмя GET /models выявила общий расход времени: первый запрос нового клиента до заголовков — 1341/1177 мс, следующий через то же соединение — 279/285 мс; TLS занял 1051/867 мс. На момент этой диагностики API создавал и закрывал HTTP-клиент на каждой реплике. Последующая проверка ниже выявила ограничение по допустимой длительности простоя соединения.

Исходные ответы, события и WAV: artifacts/voice-demo-20261003/llm-fast-candidates-full/results.json. Ручная оценка и ограничения: artifacts/voice-demo-20261003/llm-fast-candidates-full/quality-review.json.

Повторное использование соединений LLM

VoiceDemoService теперь лениво создаёт один HTTP-клиент на процесс API с пулом до четырёх соединений. Промпт, история и Authorization задаются отдельно для каждого запроса. Закрытие отдельного ответа или перебивание не закрывает общий клиент и другие диалоги. Пул закрывается в lifespan приложения, в том числе после ошибки запуска; автоповтора POST генерации нет.

Для HTTP/1.1 недостаточно получить SSE [DONE]: нужно дочитать HTTP EOF, чтобы HTTPX вернул соединение в пул. После подтверждённого полного ответа хвост дочитывается не более 250 мс. Зависший или оборванный хвост закрывается, а уже завершённый ответ сохраняется. Response освобождается до отправки события done браузеру и дополнительно закрывается в finally при отмене.

Ограничение VseGPT на проверенном маршруте Windows: свободное соединение сохраняется только 0,5 секунды. Эксперимент с 120 секундами дал три ошибки Server disconnected without sending a response после пауз 14–16 секунд. Отдельные GET-проверки подтвердили успешный reuse после 0/0,2/0,5/1 секунды (по три пары), но ошибки после 2/4/6/10 секунд. Точный серверный таймаут из этого не следует. HTTP/2 не был согласован через ALPN. Поэтому после обычной разговорной паузы клиент открывает новое соединение при следующем запросе.

Финальная проверка с expiry 0,5 с: 12/12 запросов завершились. Из шести запросов общего клиента один установил начальное соединение, три использовали существующее, два корректно переподключились после простоя около пяти секунд. Все запросы использовали текущую модель и один и тот же вопрос о возможностях сервиса; свежие и общие клиенты чередовались. Медианы:

Режим Запросов Первый текст Первая фраза
Новый клиент на каждый запрос 6 2158 мс 2360 мс
Подтверждённое повторное соединение 3 1276 мс 1409 мс

Эти замеры не включают ASR и TTS. Выигрыш около 0,9 секунды относится к запросам подряд, а не к произвольным паузам в голосовом разговоре. Постоянные пинги, дополнительные генерации для прогрева и повторные платные запросы не добавлены.

Контроль с паузами 12 секунд между запросами и реальным TTS прошёл 3/3. Фактический простой соединения составил 13,9–15,7 секунды: обе следующие реплики корректно открыли новый TCP/TLS без ошибки. Первый PCM — 2828/2938/3009 мс от запроса LLM; ускорение обычного диалога с такими паузами этой правкой не подтверждено.

Тесты: 59 проверок сервиса, API и lifespan без БД; включены отмена одного из двух потоков, продолжение соседнего запроса, ошибки/таймауты, EOF, зависший хвост и закрытие пула. Применение Python-изменений требует ручного перезапуска API.

Артефакты в artifacts/voice-demo-20261003/: llm-connection-reuse/results.json — эксперимент с длинным expiry, не итоговая конфигурация; llm-connection-idle-probe.json — GET/HEAD-диагностика; llm-connection-safe-burst/results.json — сравнение после исправления; llm-connection-safe-pauses/results.json — контроль разговорных пауз с TTS.

Прямой Google и Gemma

На проверенном маршруте Google OpenAI-compatible после пауз 12 секунд два запроса по повторно использованному соединению дали первую готовую фразу за 698 и 731 мс. Новый TCP/TLS в этих двух запросах не создавался. Это небольшая проверка совместимого endpoint, не обещание задержки любого голосового диалога: исходные события и ответы.

В основном сравнении native Google и VseGPT по шести запросам с общим клиентом медиана первой фразы составила 904 против 2096 мс; эти медианы включают начальное соединение и фактические переподключения. В отдельном парном прогоне с Gemma 26B и паузами 12 секунд Gemini Flash-Lite дала медианы 666 мс до текста и 796 мс до первой фразы, Gemma — 1020 и 1586 мс. Условия и кэш отражены в артефактах; эти native-результаты не подменяют замер совместимого endpoint. Все числа относятся только к LLM, без ASR, TTS и браузерного воспроизведения. Источники: Google/VseGPT и парное сравнение Gemma.

Интеграционная проверка VoiceDemoService через Google OpenAI-compatible и реальный incremental Higgs завершилась 3/3. При фактическом простое соединения 14–16 секунд повторные запросы сохранили TCP/TLS: первая фраза пришла за 784/798 мс, первый PCM — за 1524/1399 мс от запроса LLM. Начальное соединение дало первый PCM за 3226 мс. Здесь TTS включён, ASR и браузерное воспроизведение не измерялись. События и WAV. Прогон вызывает обновлённый Python-клиент в отдельном конечном процессе и работающий TTS; он не подтверждает перезапуск основного API.

Путь реплики

  1. Браузер запрашивает mono-микрофон с echo cancellation, noise suppression и AGC. AudioWorklet передаёт звук небольшими блоками; при отсутствии поддержки есть fallback на ScriptProcessor. Ресемплер сохраняет позицию между блоками.
  2. После 160 мс устойчивой звуковой активности начинается реплика. Сохраняются 400 мс предварительного звука, чтобы не потерять начало слова. Пауза 700 мс завершает реплику, непрерывный отрезок ограничен 22 секундами. Вместе с pre-roll и последним блоком аудио он должен укладываться в серверный предел 24 секунды.
  3. Один и тот же PCM16 mono 16 кГц направляется в T-One по WebSocket. После завершения реплики браузер отправляет один WAV; сервер одновременно вызывает русский FastConformer recovered 2.11, настоящий Vosk 0.62, узбекский FastConformer и GigaAM UZ с исходными FP32-весами в общем Triton. Все получают одинаковые аудиосэмплы. LLM ждёт четыре финала и завершение потока; отдельные гипотезы в интерфейсе скрыты. Ошибка одного финального распознавателя отменяет остальные ветки и возвращает ошибку реплики. Пустой распознанный текст остаётся пустым, без подмены источника.
  4. Сервер передаёт четыре финальных гипотезы, отдельную вспомогательную потоковую версию и ограниченную историю провайдеру из VOICE_DEMO_LLM_PROVIDER. Отдельные настройки демо позволяют использовать прямой Google; без них сохраняются прежние настройки суммаризации/VseGPT, описанные выше. Клиент не выбирает модель, URL или ключ.
  5. NDJSON-поток сначала сообщает определённый язык и язык ответа. Метаданные не озвучиваются. Затем законченные предложения/короткие части до 240 символов направляются в Higgs. Следующая часть может синтезироваться, пока предыдущая ещё воспроизводится. Аудио выдаётся настоящим incremental путём, в PCM16 mono 16 кГц; в браузере планируется не более двух секунд вперёд.
  6. В том же запросе LLM после ответа возвращает скорректированную реплику пользователя. Сначала в диалоге виден предварительный финал ASR (с приоритетом узбекских версий после узбекского ответа), затем он заменяется коррекцией. Следующий запрос использует исправленный текст в истории. Генерация коррекции не предшествует первым фразам ответа и не создаёт второй LLM-запрос. Спорные числа, имена и намерения не исправляются догадкой: бот просит уточнение, а исходная реплика сохраняется. При перебивании запоздалые коррекции игнорируются.

Vosk 0.62 вызывается через отдельный приватный worker: это offline Zipformer2 Transducer из имеющихся ONNX-весов, с greedy-декодированием. Старое публичное имя Vosk в основном API по-прежнему перенаправляется на T-One; демо этот alias не использует. Служебные идентификаторы источников соответствуют реальным вызовам. Открытые demo-методы вызывают модели напрямую; платные пользовательские API и их авторизация не изменены.

Входной буфер WebSocket ограничен пятью секундами PCM (160 000 байт), независимо от размера браузерных пакетов. При заполнении сервер ждёт освобождения места, создавая backpressure; остановившийся потребитель ограничен таймаутом 3 секунды. Это устраняет зависимость от прежнего лимита в 32 сообщения: пакеты AudioWorklet по 20 мс раньше оставляли только 640 мс очереди, включая предварительно накопленный звук. Общие ограничения на длительность и число входных сообщений сохранены.

Worker Vosk 0.62

Целевой режим — отдельный native Windows worker на GPU0, 127.0.0.1:8046; браузер обращается к нему через demo API. Используется локальный каталог E:/vosk-model-ru-0.62. SHA-256 всех четырёх файлов модели проверяются при запуске. Запуск вручную в обычном PowerShell из E:/five-api:

.\run_vosk062_gpu.ps1

Консоль остаётся открытой. Native-сервисы запускает и перезапускает пользователь согласно AGENTS.md. Для конечной проверки без HTTP listener предусмотрен -PreflightOnly; этот режим не запускает сервер. Рабочая среда — Anaconda Python 3.11 и изолированный Sherpa 1.12.40 CUDA12/cuDNN9; PyTorch загружает CUDA DLL. Greedy-декодирование, два CPU-потока, без внешней языковой модели. Автоматического перехода на CPU нет. Обычный старт проверяет выбранный CUDA provider и прогревает модель; -PreflightOnly дополнительно подтверждает реальные операции энкодера по целой трассе ORT. Эти виды проверки различаются в /health полем execution_trace_verified. Повреждённые из-за коллизий имён трассы decoder/joiner не используются как доказательство CUDA; сведения об исключённых файлах есть в диагностическом отчёте. Worker допускает одно распознавание одновременно, иначе возвращает 429.

Native GPU worker проверен конечным TestClient без HTTP listener: синтетическая реплика 3,08 секунды распознана правильно за 52,35 мс вычислений, 53,83 мс внутри запроса. Это прогретый единичный замер без сетевой задержки. Трасса подтвердила Conv и MatMul энкодера на CUDA: native-vosk062-worker-v3.json.

Основной API использует VOICE_DEMO_VOSK_URL=http://127.0.0.1:8046/transcribe и VOICE_DEMO_VOSK_TIMEOUT=30 по умолчанию. Если API работает внутри Docker, ему нужно явно задать достижимый адрес worker вместо собственного loopback. Предыдущая попытка Docker CUDA завершилась ошибкой выделения памяти до распознавания. Docker CPU был проверен отдельно; его измерения в vosk062-live.json и three-asr-smoke.json не описывают native GPU. Другие модельные контейнеры не останавливались. Docker-конфигурация сохранена как альтернативный способ запуска. Полные сведения о среде и диагностике: vosk062_service/README.md.

База знаний и промпт

app/infrastructure/services/voice_demo_prompt.py хранит версию базы знаний, список проверенных локальных источников и системный промпт. База описывает ASR и языки, файлы и поток, диаризацию, отчёты и сущности, TTS и клонирование голоса, словарь произношений, студию, перевод/переозвучку, API, размещение в контуре и маршруты внешней обработки. Возможности продукта отделены от действий, которые доступны самому разговорному демо.

Обычно бот отвечает 1–3 короткими предложениями, приводит пример для задачи пользователя и не перечисляет весь каталог. Ему запрещено выдумывать цены, SLA, готовые интеграции и гарантии. Три ASR-текста являются альтернативными гипотезами одной реплики; косметические расхождения можно игнорировать, существенные различия в числах, именах и намерении требуют уточнения. Простое большинство голосов не считается доказательством правильного варианта.

Перебивание

Звуковая активность сама по себе не означает намерение перебить. Во время ответа она только приглушает плеер. Две распознанные лексемы или короткая команда «стоп»/«подожди»/«нет» подтверждают отмену раньше финальной расшифровки. Однословный содержательный запрос подтверждается финальным ASR. Короткие «угу», «ага», «да», «понятно» во время воспроизведения считаются подтверждением слушателя; ответ продолжается. Неподтверждённое приглушение ограничено 1,5 с.

Подтверждённое перебивание и нажатие большой кнопки микрофона отменяют LLM/TTS fetch, останавливают все уже запланированные AudioBufferSource и очищают очередь. Идентификатор текущего ответа защищает новый ход от поздних событий старого. В контекст LLM попадают только полностью прозвучавшие предложения бота; текст прерванного ответа остаётся виден с отметкой в журнале. Микрофон продолжает работу.

Это первая проверяемая эвристика, а не обученный детектор намерения перебить. Качество подавления эха зависит от браузера и оборудования; для первой проверки рекомендуются наушники. Порог активности и паузу предстоит настроить на живых разговорах. Окончание по паузе не является семантическим определением конца мысли.

Публичные методы

Метод Назначение
GET /api/voice-demo/config Безопасные сведения о фиксированных моделях
WS /api/voice-demo/stt/ws {}, затем PCM16 16 кГц mono, затем {"event":"eof"}
POST /api/voice-demo/stt Multipart audio: WAV PCM16 mono 16 кГц → hypotheses:{fastconformer,vosk,fastconformer_uz,gigaam_uz}
POST /api/voice-demo/chat JSON hypotheses:{streaming,fastconformer,vosk,fastconformer_uz,gigaam_uz}, history, interrupted
POST /api/voice-demo/tts/stream JSON {"text":"…","language":"ru"}; язык ru или uz, голос выбирает сервер

LLM сначала отвечает событием language с language (ru, uz, mixed, unknown) и response_language (uz только при language=uz, иначе ru). Далее следуют события delta, необязательный user_transcript, done или error; EOF без done — ошибка. user_transcript содержит text (до 4000 символов), заменяет конкретную пользовательскую реплику, не попадает в TTS или ответ ассистента. Старый сервер без language несовместим с новым клиентом: сначала перезапустите API, затем обновите страницу. Недопустимая или отсутствующая метка блокирует озвучку.

Промпт требует префикс LANGUAGE:<метка>\nANSWER:, затем обычный ответ и <user_transcript>Исправленная реплика</user_transcript>. При неопределённости внутри блока — null; JSON-строка также принимается. Сервер отделяет блок от произносимого текста даже при разрезанном по токенам маркере; повреждённый блок отбрасывается. Коррекция публикуется только после успешного завершения провайдера (stop + [DONE]). Бюджет — 400 токенов для ответа плюс резерв под длину исходной реплики (128–4000 токенов); общий предел сырого вывода остаётся 8000 символов. В журнал текст не записывается. ASR WebSocket завершает успешную реплику кодом 1000 после финалов. TTS подтверждает режим и формат заголовками X-TTS-Streaming-Mode: incremental и X-Audio-*. Длина TTS-запроса — до 400 символов, WAV-аудиовход — от 0,1 до 24 секунд. Клиентский сеанс автоматически завершается через 10 минут.

На сервере ограничены размеры тела запроса, история, время обработки, число одновременных операций и частота обращений. Ограничения действуют в пределах процесса; при нескольких workers общий лимит следует выставлять на прокси. Идентификатор клиента получается из ASGI request.client.host, без самостоятельного доверия к заголовку X-Forwarded-For. Uvicorn должен доверять forwarded headers только реальному reverse proxy, который перезаписывает входящие заголовки.

Demo-обработчики не создают аккаунтные записи и не списывают пользовательские кредиты. Расходы на публичное демо несёт сервис. Текст диалога передаётся внешнему LLM-провайдеру. Аудио ему не передаётся. На демо-странице технические сведения о провайдере и отдельные гипотезы распознавания не отображаются.

Диагностика

Журнал этапов демо — logs/voice-demo.log, с ротацией по 2 МБ и двумя архивами. События включают начало/завершение этапа, объём аудио, длительность, коды ошибок и случайный идентификатор запроса. Аудиосэмплы, распознанный текст, ответы LLM, API-ключи и IP-адреса туда не записываются. Тесты изолируют этот журнал от реальных запросов. После добавления диагностики нужен ручной перезапуск API.

Get-Content E:/five-api/logs/voice-demo.log -Tail 80 -Wait

Для публичного адреса также доступен nginx access log: runtime/nginx/logs/speech-expert.access.log. Код 101 означает состоявшееся WebSocket-подключение, а не успешное распознавание; 499 означает закрытие HTTP соединения клиентом до ответа. По одной записи 499 нельзя установить причину ошибки или считать её отказом модели.

При разборе мобильной попытки 3 октября 2026 зафиксированы WebSocket 101 и финальный POST 499; до LLM/TTS попытка не дошла. Точный текст первоначальной ошибки не сохранился. Обычные проверки публичного HTTPS-пути прошли, однако регрессионный тест воспроизвёл отдельный дефект старой очереди: одинаковые 1,2 секунды PCM при задержанном потребителе проходили пакетами по 100 мс и обрывались пакетами по 20 мс. Новый буфер проходит оба варианта, проверены ожидание освобождения места, таймаут остановившегося потребителя и отмена. После исправления прошли 94 серверных теста. Проверка обновлённого ASGI-кода с реальным T-One без запуска нового сервера также прошла: 154 пакета по 20 мс, начальный всплеск 1,2 с, семь partial и один final, close 1000, ресурсы освобождены: buffer-smoke.json. Для проверки работы именно на мобильном устройстве нужна повторная попытка с живым микрофоном на обновлённом API.

После перезапуска в 17:14:53 мск публичная проверка снова прошла: через https://www.speech-expert.ru получены три гипотезы, ответ LLM и incremental TTS. Отдельный WS-прогон с 20 мс кадрами и начальным всплеском 1,2 с завершился close 1000 с семью partial и одним final. Отчёты: public-ws-after-restart.json, public-after-buffer-fix/smoke.json. Мобильная попытка в 17:15:39 завершилась нажатием кнопки «Завершить», что подтвердил пользователь. Остановка сеанса отменила ожидающее распознавание и загрузку WAV до вызова финальных моделей; буфер не переполнялся (пик 35 200 байт). Причина отмены этой попытки установлена по подтверждению пользователя: один код закрытия WebSocket 1000 её не определяет, поскольку клиент использует его и при очистке после ошибки. Успешные публичные smoke проверяют синтетическую цепочку; успешный разговор через живой микрофон и акустическое перебивание пока не подтверждены.

Проверка и применение

node --test static/tests/voice-demo*.test.mjs
$env:POSTGRES_PASSWORD='unit-test-only'
.venv/Scripts/python.exe -X utf8 -m pytest app/tests/api/test_voice_demo.py app/tests/api/test_voice_demo_speech.py app/tests/infrastructure/test_voice_demo_service.py app/tests/infrastructure/test_voice_demo_output.py app/tests/infrastructure/test_voice_demo_vosk.py app/tests/infrastructure/test_voice_demo_gigaam_uz.py app/tests/infrastructure/test_voice_demo_telemetry.py -q
.venv/Scripts/python.exe -X utf8 -m pytest vosk062_service/tests -q
.venv/Scripts/python.exe -X utf8 scripts/smoke_voice_demo.py --base-url http://127.0.0.1:8001

Smoke — конечный клиентский скрипт: он не запускает сервисы, синтезирует тестовый вопрос, проверяет поток и четыре финальные ASR-гипотезы, LLM и ответ TTS без авторизации. Для узбекского используйте --language uz; для сохранения обоих прогонов укажите разные --output-dir. Он использует реальные ресурсы моделей и внешний LLM. По умолчанию результат — artifacts/voice-demo-20261004/language-integration/live-smoke.

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

Новые Python-маршруты требуют перезапуска API. Сервисы вне Docker перезапускает пользователь вручную согласно AGENTS.md. Статика не требует сборки; браузер нужно обновить. В app/main.py явно задан JavaScript MIME для .mjs, поскольку Windows может возвращать text/plain, и тогда браузер не исполняет модули.

Проверено 3 октября 2026 после добавления Vosk: 85 серверных тестов демо, 43 клиентских теста и 19 тестов worker; Ruff без замечаний. Конечный тест через TestClient без запуска сервера и БД вызвал реальные T-One, FastConformer, Vosk, настроенную Gemini Flash Lite и incremental Higgs. Все три ASR вернули «какие задачи решает ваш сервис». Две финальные гипотезы получены за 312 мс, полный ответ LLM — за 2547 мс; ресурсы освободились. TestClient буферизует HTTP, поэтому эти числа не измеряют время первого токена или первого аудиоблока. Отчёт: three-asr-smoke.json.

Два отдельных живых запроса проверили оговорки on-premise/CRM и уточнение конфликтующих ASR-чисел «15 или 50», несмотря на большинство 2:1. Первый токен пришёл примерно через 2,2 секунды: prompt-smoke.json.

После ручного запуска native GPU Vosk и перезапуска основного API выполнен сквозной HTTP/WebSocket smoke без авторизации. Все три ASR вернули правильный вопрос, LLM ответила о возможностях продукта, Higgs отдал ответ 26 аудиоблоками. Первый блок ответа получен через 663 мс от начала TTS-запроса, последний — через 4881 мс; длительность звука 13,68 с. Первый токен LLM — через 2083 мс. Это единичный замер через localhost, не гарантия задержки. Скрипт проверяет каждую ступень цепочки; браузер дополнительно начинает TTS по мере готовности фраз LLM. Отчёт: live-native-gpu/smoke.json. Worker подтвердил provider: cuda, после запроса активных распознаваний нет: runtime-health.json.

/api/voice-demo/config возвращает asr_hypothesis_count: 3 и knowledge_version: "2026-10-03.3", страница доступна без авторизации, .mjs отдаётся как JavaScript. Обновлённая страница проверена в Chrome: UI-модуль исполняется, три ASR-строки видны, ошибок JavaScript/MIME нет; микрофон при этой проверке не включался. Макет ранее проверен на ширинах 390 и 1440 px. Разговор через настоящий микрофон и акустическое перебивание остаются ручной проверкой. Если страница была открыта до перезапуска, нажмите Ctrl+F5.