diff --git a/packages/app/src/contexts/voice-context.tsx b/packages/app/src/contexts/voice-context.tsx index 83fa5deee..0f4cb147c 100644 --- a/packages/app/src/contexts/voice-context.tsx +++ b/packages/app/src/contexts/voice-context.tsx @@ -8,6 +8,10 @@ import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; const VOICE_CONVERSATION_ID_STORAGE_KEY = "@paseo:voice-conversation-id"; const KEEP_AWAKE_TAG = "paseo:voice"; +const VOICE_VAD_VOLUME_THRESHOLD = 0.18; +const VOICE_VAD_SILENCE_DURATION_MS = 1400; +const VOICE_VAD_SPEECH_CONFIRMATION_MS = 120; +const VOICE_VAD_DETECTION_GRACE_PERIOD_MS = 700; interface VoiceContextValue { isVoiceMode: boolean; @@ -123,10 +127,10 @@ export function VoiceProvider({ children }: VoiceProviderProps) { console.error("[Voice] Cannot handle error - setMessages not available from SessionState"); } }, - volumeThreshold: 0.3, - silenceDuration: 2000, - speechConfirmationDuration: 300, - detectionGracePeriod: 200, + volumeThreshold: VOICE_VAD_VOLUME_THRESHOLD, + silenceDuration: VOICE_VAD_SILENCE_DURATION_MS, + speechConfirmationDuration: VOICE_VAD_SPEECH_CONFIRMATION_MS, + detectionGracePeriod: VOICE_VAD_DETECTION_GRACE_PERIOD_MS, }); useEffect(() => { diff --git a/packages/app/src/voice/speech-segmenter.test.ts b/packages/app/src/voice/speech-segmenter.test.ts index c9b0278a5..a02428a26 100644 --- a/packages/app/src/voice/speech-segmenter.test.ts +++ b/packages/app/src/voice/speech-segmenter.test.ts @@ -88,4 +88,37 @@ describe("SpeechSegmenter", () => { expect(lastCall.isLast).toBe(true); expect(lastCall.audioData.length).toBeGreaterThan(0); }); + + it("keeps detection alive across a brief pause and confirms short utterances", () => { + const onSpeechStart = vi.fn(); + const detectingChanges: boolean[] = []; + + const segmenter = new SpeechSegmenter( + { + enableContinuousStreaming: false, + volumeThreshold: 0.18, + silenceDurationMs: 1400, + speechConfirmationMs: 120, + detectionGracePeriodMs: 700, + minChunkDurationMs: 100, + pcmSampleRate: 1000, + }, + { + onSpeechStart, + onDetectingChange: (v) => detectingChanges.push(v), + } + ); + + const t0 = 20_000; + + segmenter.pushVolumeLevel(0.4, t0); + segmenter.pushPcmChunk(mkPcmBytes(20)); + segmenter.pushVolumeLevel(0.0, t0 + 80); + segmenter.pushPcmChunk(mkPcmBytes(20)); + segmenter.pushVolumeLevel(0.4, t0 + 140); + segmenter.pushPcmChunk(mkPcmBytes(20)); + + expect(detectingChanges).toContain(true); + expect(onSpeechStart).toHaveBeenCalledTimes(1); + }); });