diff --git a/frontend/src/components/VoiceOverlay.vue b/frontend/src/components/VoiceOverlay.vue index 470a648..447aebb 100644 --- a/frontend/src/components/VoiceOverlay.vue +++ b/frontend/src/components/VoiceOverlay.vue @@ -12,6 +12,7 @@ import { ref, onMounted, onUnmounted, computed } from 'vue' import { useVoiceRecorder } from '@/composables/useVoiceRecorder' import { useVoiceAudio } from '@/composables/useVoiceAudio' +import { useSilenceDetector } from '@/composables/useSilenceDetector' import { apiPost, apiSSEStream, getVoiceStatus, transcribeAudio, synthesiseSpeech } from '@/api/client' // ─── Voice service availability ────────────────────────────────────────────── @@ -64,6 +65,7 @@ const isBusy = computed(() => phase.value !== 'idle' && phase.value !== 'error') // ─── Composables ───────────────────────────────────────────────────────────── const recorder = useVoiceRecorder() const audio = useVoiceAudio() +const silenceDetector = useSilenceDetector() // ─── Core PTT flow ──────────────────────────────────────────────────────────── async function startPtt() { @@ -79,9 +81,13 @@ async function startPtt() { return } phase.value = 'recording' + if (recorder.stream.value) { + silenceDetector.start(recorder.stream.value, stopPtt) + } } async function stopPtt() { + silenceDetector.stop() if (phase.value !== 'recording') return phase.value = 'transcribing' @@ -187,7 +193,14 @@ async function stopPtt() { assistantMessageId // consumed; suppress lint } +function onBtnClick() { + if (phase.value === 'error') { phase.value = 'idle'; return } + if (phase.value === 'recording') { stopPtt(); return } + if (phase.value === 'idle') { startPtt() } +} + function cancelAll() { + silenceDetector.stop() recorder.stopRecording().catch(() => {}) audio.stop() phase.value = 'idle' @@ -263,7 +276,7 @@ onUnmounted(() => { {{ errorMsg || 'Error' }}