From 7e3c5072bb92fd6e43a24f5ca3c16b5b8dbcc0c1 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Mon, 27 Apr 2026 10:55:18 +0000 Subject: [PATCH] =?UTF-8?q?fix(voice):=20preload=20VAD=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=20=D1=80=D0=B0=D0=B7=20=E2=80=94=20=D0=BC=D0=B3?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BA=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=C2=AB=D0=9A=D0=BE=D1=81=D0=BC=D0=BE=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В логах было видно: между wake-trigger и реальным VAD recording проходило 1-2с (Loading VAD... → finished loading → started micVAD). Каждый cancel дополнительно destroy'ил VAD, и следующий wake снова ждал инициализацию. Теперь: - VAD создаётся один раз в paused-режиме сразу после wake.start() (в фоне, не блокирует UI). - На каждый wake → vad.start() мгновенно. - onSpeechEnd → vad.pause() (был implicit pause; явно ставим). - voice-cancel → vad.pause(), а не destroy. Wake продолжает слушать. - destroy только при полном выключении ассистента. --- components/VoiceController.tsx | 40 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/components/VoiceController.tsx b/components/VoiceController.tsx index 3b6e429..68c68e7 100644 --- a/components/VoiceController.tsx +++ b/components/VoiceController.tsx @@ -69,13 +69,11 @@ export default function VoiceController() { useEffect(() => { console.log('[VoiceController] mounted, state=idle, ждём тап на микрофон') - // Кнопка X в overlay шлёт voice-cancel → прерываем активную запись фразы, - // но wake-word оставляем слушать в фоне (если он включён). + // Кнопка X в overlay шлёт voice-cancel → ставим VAD на паузу + // (НЕ destroy — иначе следующий wake снова будет ждать 1-2с на инициализацию). const onCancel = () => { - console.log('[voice] cancel — прерываю запись') + console.log('[voice] cancel — пауза VAD') try { vadRef.current?.pause?.() } catch {} - try { vadRef.current?.destroy?.() } catch {} - vadRef.current = null busyRef.current = false try { wakeRef.current?.resume?.() } catch {} setState((s) => (wakeRef.current ? 'listening' : 'idle')) @@ -125,14 +123,18 @@ export default function VoiceController() { emitLocal('error', AGENT, 'Не получилось') } finally { busyRef.current = false - // После обработки — возвращаем wake-режим (если активен) + // VAD на паузу — переиспользуем при следующем wake (без re-init). + try { vadRef.current?.pause?.() } catch {} + // Wake возобновляем — снова слушаем фоном. try { wakeRef.current?.resume?.() } catch {} setState((s) => (s === 'busy' ? 'listening' : s)) } } - // Создание VAD по запросу: либо после wake-детекта, либо после ручного тапа. - const startVAD = async () => { + // Однократная инициализация VAD. Создаётся в paused-состоянии и переиспользуется + // на каждый wake — без этого пауза до первой записи ~1-2с. + const initVAD = async () => { + if (vadRef.current) return try { const { MicVAD } = await import('@ricky0123/vad-web') const vad = await MicVAD.new({ @@ -147,16 +149,15 @@ export default function VoiceController() { negativeSpeechThreshold: 0.45, minSpeechMs: 160, redemptionMs: 750, - onSpeechStart: () => { - emitLocal('wake', AGENT) - }, + onSpeechStart: () => emitLocal('wake', AGENT), onSpeechEnd: handleSpeechEnd, }) vadRef.current = vad - vad.start() + // Не вызываем start — ждём пока wake-word триггернёт. + console.log('[voice] VAD preloaded (paused)') } catch (e: any) { console.error('[voice] VAD init failed:', e?.name, e?.message, e) - setState('error') + // Не вырубаем wake — может на ручной trigger ещё попробуем emitLocal('error', AGENT, `VAD: ${e?.message?.slice(0, 60) || 'init'}`) } } @@ -168,12 +169,9 @@ export default function VoiceController() { try { wakeRef.current?.pause?.() } catch {} setState('recording') emitLocal('wake', AGENT) - // Если VAD ещё не готов — создаём; иначе reset+start. - if (!vadRef.current) { - await startVAD() - } else { - try { vadRef.current.start?.() } catch {} - } + // VAD должен быть уже preloaded — мгновенный старт. + if (!vadRef.current) await initVAD() + try { vadRef.current?.start?.() } catch {} } const start = async () => { @@ -215,6 +213,10 @@ export default function VoiceController() { await wake.start() wakeRef.current = wake setState('listening') + + // Прелоадим VAD в фоне — после первого wake реакция будет мгновенной, + // вместо +1-2с на загрузку Silero VAD. + initVAD().catch((e) => console.warn('[voice] VAD preload failed', e)) } catch (e: any) { console.error('[wake] init failed:', e) setState('error')