Merge pull request #22 from boudra/local-streaming-speech-tts
voice: harden local voice agent path and UUID validation
This commit is contained in:
@@ -2,12 +2,13 @@ import { createContext, useContext, useState, ReactNode, useCallback, useEffect,
|
||||
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
|
||||
import type { SessionState } from "@/stores/session-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { randomUUID } from "expo-crypto";
|
||||
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 +124,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(() => {
|
||||
@@ -166,18 +167,9 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
console.log("[Voice] Mode enabled");
|
||||
|
||||
if (session?.client) {
|
||||
let voiceConversationId =
|
||||
(await AsyncStorage.getItem(VOICE_CONVERSATION_ID_STORAGE_KEY)) ?? null;
|
||||
if (!voiceConversationId) {
|
||||
voiceConversationId = randomUUID();
|
||||
await AsyncStorage.setItem(
|
||||
VOICE_CONVERSATION_ID_STORAGE_KEY,
|
||||
voiceConversationId
|
||||
);
|
||||
}
|
||||
await session.client.setVoiceConversation(true, voiceConversationId);
|
||||
await session.client.setVoiceMode(true);
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceConversation skipped: daemon unavailable");
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Voice] Failed to start:", error);
|
||||
@@ -200,9 +192,9 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
console.log("[Voice] Mode disabled");
|
||||
|
||||
if (session?.client) {
|
||||
await session.client.setVoiceConversation(false);
|
||||
await session.client.setVoiceMode(false);
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceConversation skipped: daemon unavailable");
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Voice] Failed to stop:", error);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user