feat: follow-up dialog window — VAD stays active 8s after agent response
All checks were successful
Deploy / deploy (push) Successful in 1m37s
All checks were successful
Deploy / deploy (push) Successful in 1m37s
This commit is contained in:
@@ -38,6 +38,8 @@ export default function VoiceController() {
|
|||||||
const wakeRef = useRef<WakeWordDetector | null>(null)
|
const wakeRef = useRef<WakeWordDetector | null>(null)
|
||||||
const vadRef = useRef<any>(null)
|
const vadRef = useRef<any>(null)
|
||||||
const busyRef = useRef(false)
|
const busyRef = useRef(false)
|
||||||
|
const followUpRef = useRef(false)
|
||||||
|
const followUpTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
vlog('[VoiceController] mounted, state=idle, ждём тап на микрофон')
|
vlog('[VoiceController] mounted, state=idle, ждём тап на микрофон')
|
||||||
@@ -46,6 +48,8 @@ export default function VoiceController() {
|
|||||||
// (НЕ destroy — иначе следующий wake снова будет ждать 1-2с на инициализацию).
|
// (НЕ destroy — иначе следующий wake снова будет ждать 1-2с на инициализацию).
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
vlog('[voice] cancel — пауза VAD')
|
vlog('[voice] cancel — пауза VAD')
|
||||||
|
followUpRef.current = false
|
||||||
|
if (followUpTimerRef.current) { clearTimeout(followUpTimerRef.current); followUpTimerRef.current = null }
|
||||||
try { vadRef.current?.pause?.() } catch {}
|
try { vadRef.current?.pause?.() } catch {}
|
||||||
busyRef.current = false
|
busyRef.current = false
|
||||||
try { wakeRef.current?.resume?.() } catch {}
|
try { wakeRef.current?.resume?.() } catch {}
|
||||||
@@ -54,8 +58,36 @@ export default function VoiceController() {
|
|||||||
}
|
}
|
||||||
window.addEventListener('voice-cancel', onCancel)
|
window.addEventListener('voice-cancel', onCancel)
|
||||||
|
|
||||||
|
const onFollowUp = () => {
|
||||||
|
if (!wakeRef.current) return // ассистент выключен
|
||||||
|
vlog('[voice] follow-up window открыто (8s)')
|
||||||
|
followUpRef.current = true
|
||||||
|
// Очистить предыдущий таймер если был
|
||||||
|
if (followUpTimerRef.current) clearTimeout(followUpTimerRef.current)
|
||||||
|
// Запустить VAD сразу без wake word
|
||||||
|
if (vadRef.current) {
|
||||||
|
try { vadRef.current.start() } catch {}
|
||||||
|
}
|
||||||
|
setState('recording')
|
||||||
|
emitLocal('wake', AGENT)
|
||||||
|
// Через 8 секунд — если никто не заговорил — вернуться к wake
|
||||||
|
followUpTimerRef.current = setTimeout(() => {
|
||||||
|
if (followUpRef.current) {
|
||||||
|
followUpRef.current = false
|
||||||
|
vlog('[voice] follow-up timeout — возврат к wake word')
|
||||||
|
try { vadRef.current?.pause?.() } catch {}
|
||||||
|
try { wakeRef.current?.resume?.() } catch {}
|
||||||
|
setState('listening')
|
||||||
|
emitLocal('idle', AGENT)
|
||||||
|
}
|
||||||
|
}, 8000)
|
||||||
|
}
|
||||||
|
window.addEventListener('voice-follow-up', onFollowUp)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('voice-cancel', onCancel)
|
window.removeEventListener('voice-cancel', onCancel)
|
||||||
|
window.removeEventListener('voice-follow-up', onFollowUp)
|
||||||
|
if (followUpTimerRef.current) clearTimeout(followUpTimerRef.current)
|
||||||
try { vadRef.current?.destroy?.() } catch {}
|
try { vadRef.current?.destroy?.() } catch {}
|
||||||
try { wakeRef.current?.stop?.() } catch {}
|
try { wakeRef.current?.stop?.() } catch {}
|
||||||
vadRef.current = null
|
vadRef.current = null
|
||||||
@@ -96,11 +128,18 @@ export default function VoiceController() {
|
|||||||
emitLocal('error', AGENT, 'Не получилось')
|
emitLocal('error', AGENT, 'Не получилось')
|
||||||
} finally {
|
} finally {
|
||||||
busyRef.current = false
|
busyRef.current = false
|
||||||
// VAD на паузу — переиспользуем при следующем wake (без re-init).
|
// Если активен follow-up режим — НЕ переключаемся на wake, ответ сам откроет новое окно
|
||||||
try { vadRef.current?.pause?.() } catch {}
|
if (!followUpRef.current) {
|
||||||
// Wake возобновляем — снова слушаем фоном.
|
try { vadRef.current?.pause?.() } catch {}
|
||||||
try { wakeRef.current?.resume?.() } catch {}
|
try { wakeRef.current?.resume?.() } catch {}
|
||||||
setState((s) => (s === 'busy' ? 'listening' : s))
|
setState((s) => (s === 'busy' ? 'listening' : s))
|
||||||
|
}
|
||||||
|
// Сбросить follow-up флаг — новое окно откроется после TTS
|
||||||
|
followUpRef.current = false
|
||||||
|
if (followUpTimerRef.current) {
|
||||||
|
clearTimeout(followUpTimerRef.current)
|
||||||
|
followUpTimerRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ export default function VoiceOverlay() {
|
|||||||
setText(evt.text || '')
|
setText(evt.text || '')
|
||||||
clearDismiss()
|
clearDismiss()
|
||||||
if (evt.text) {
|
if (evt.text) {
|
||||||
playTTS(evt.text, currentAgent, () => scheduleDismiss(4000))
|
playTTS(evt.text, currentAgent, () => {
|
||||||
|
// Сигнализируем VoiceController что можно слушать follow-up
|
||||||
|
window.dispatchEvent(new CustomEvent('voice-follow-up'))
|
||||||
|
scheduleDismiss(9000)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
scheduleDismiss(4000)
|
scheduleDismiss(4000)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user