fix(voice): preload VAD один раз — мгновенная реакция после «Космо»
All checks were successful
Deploy / deploy (push) Successful in 1m36s
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:
@@ -69,13 +69,11 @@ export default function VoiceController() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[VoiceController] mounted, state=idle, ждём тап на микрофон')
|
console.log('[VoiceController] mounted, state=idle, ждём тап на микрофон')
|
||||||
|
|
||||||
// Кнопка X в overlay шлёт voice-cancel → прерываем активную запись фразы,
|
// Кнопка X в overlay шлёт voice-cancel → ставим VAD на паузу
|
||||||
// но wake-word оставляем слушать в фоне (если он включён).
|
// (НЕ destroy — иначе следующий wake снова будет ждать 1-2с на инициализацию).
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
console.log('[voice] cancel — прерываю запись')
|
console.log('[voice] cancel — пауза VAD')
|
||||||
try { vadRef.current?.pause?.() } catch {}
|
try { vadRef.current?.pause?.() } catch {}
|
||||||
try { vadRef.current?.destroy?.() } catch {}
|
|
||||||
vadRef.current = null
|
|
||||||
busyRef.current = false
|
busyRef.current = false
|
||||||
try { wakeRef.current?.resume?.() } catch {}
|
try { wakeRef.current?.resume?.() } catch {}
|
||||||
setState((s) => (wakeRef.current ? 'listening' : 'idle'))
|
setState((s) => (wakeRef.current ? 'listening' : 'idle'))
|
||||||
@@ -125,14 +123,18 @@ export default function VoiceController() {
|
|||||||
emitLocal('error', AGENT, 'Не получилось')
|
emitLocal('error', AGENT, 'Не получилось')
|
||||||
} finally {
|
} finally {
|
||||||
busyRef.current = false
|
busyRef.current = false
|
||||||
// После обработки — возвращаем wake-режим (если активен)
|
// VAD на паузу — переиспользуем при следующем wake (без re-init).
|
||||||
|
try { vadRef.current?.pause?.() } catch {}
|
||||||
|
// Wake возобновляем — снова слушаем фоном.
|
||||||
try { wakeRef.current?.resume?.() } catch {}
|
try { wakeRef.current?.resume?.() } catch {}
|
||||||
setState((s) => (s === 'busy' ? 'listening' : s))
|
setState((s) => (s === 'busy' ? 'listening' : s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание VAD по запросу: либо после wake-детекта, либо после ручного тапа.
|
// Однократная инициализация VAD. Создаётся в paused-состоянии и переиспользуется
|
||||||
const startVAD = async () => {
|
// на каждый wake — без этого пауза до первой записи ~1-2с.
|
||||||
|
const initVAD = async () => {
|
||||||
|
if (vadRef.current) return
|
||||||
try {
|
try {
|
||||||
const { MicVAD } = await import('@ricky0123/vad-web')
|
const { MicVAD } = await import('@ricky0123/vad-web')
|
||||||
const vad = await MicVAD.new({
|
const vad = await MicVAD.new({
|
||||||
@@ -147,16 +149,15 @@ export default function VoiceController() {
|
|||||||
negativeSpeechThreshold: 0.45,
|
negativeSpeechThreshold: 0.45,
|
||||||
minSpeechMs: 160,
|
minSpeechMs: 160,
|
||||||
redemptionMs: 750,
|
redemptionMs: 750,
|
||||||
onSpeechStart: () => {
|
onSpeechStart: () => emitLocal('wake', AGENT),
|
||||||
emitLocal('wake', AGENT)
|
|
||||||
},
|
|
||||||
onSpeechEnd: handleSpeechEnd,
|
onSpeechEnd: handleSpeechEnd,
|
||||||
})
|
})
|
||||||
vadRef.current = vad
|
vadRef.current = vad
|
||||||
vad.start()
|
// Не вызываем start — ждём пока wake-word триггернёт.
|
||||||
|
console.log('[voice] VAD preloaded (paused)')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[voice] VAD init failed:', e?.name, e?.message, e)
|
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'}`)
|
emitLocal('error', AGENT, `VAD: ${e?.message?.slice(0, 60) || 'init'}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,12 +169,9 @@ export default function VoiceController() {
|
|||||||
try { wakeRef.current?.pause?.() } catch {}
|
try { wakeRef.current?.pause?.() } catch {}
|
||||||
setState('recording')
|
setState('recording')
|
||||||
emitLocal('wake', AGENT)
|
emitLocal('wake', AGENT)
|
||||||
// Если VAD ещё не готов — создаём; иначе reset+start.
|
// VAD должен быть уже preloaded — мгновенный старт.
|
||||||
if (!vadRef.current) {
|
if (!vadRef.current) await initVAD()
|
||||||
await startVAD()
|
try { vadRef.current?.start?.() } catch {}
|
||||||
} else {
|
|
||||||
try { vadRef.current.start?.() } catch {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
@@ -215,6 +213,10 @@ export default function VoiceController() {
|
|||||||
await wake.start()
|
await wake.start()
|
||||||
wakeRef.current = wake
|
wakeRef.current = wake
|
||||||
setState('listening')
|
setState('listening')
|
||||||
|
|
||||||
|
// Прелоадим VAD в фоне — после первого wake реакция будет мгновенной,
|
||||||
|
// вместо +1-2с на загрузку Silero VAD.
|
||||||
|
initVAD().catch((e) => console.warn('[voice] VAD preload failed', e))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[wake] init failed:', e)
|
console.error('[wake] init failed:', e)
|
||||||
setState('error')
|
setState('error')
|
||||||
|
|||||||
Reference in New Issue
Block a user