fix(voice): preload VAD один раз — мгновенная реакция после «Космо»
All checks were successful
Deploy / deploy (push) Successful in 1m36s

В логах было видно: между 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 только при полном выключении ассистента.
This commit is contained in:
Cosmo
2026-04-27 10:55:18 +00:00
parent fddca5de66
commit 7e3c5072bb

View File

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