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')