refactor(app): replace voice hooks with voice runtime and audio engine architecture
This commit is contained in:
@@ -1,44 +1,124 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Buffer } from "buffer";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
|
||||
import { createAudioEngine } from "@/voice/audio-engine";
|
||||
|
||||
import type { DictationAudioSource, DictationAudioSourceConfig } from "./use-dictation-audio-source.types";
|
||||
|
||||
let nextDictationAudioSourceInstanceId = 1;
|
||||
|
||||
function getDictationTraceStack(): string | undefined {
|
||||
const stack = new Error().stack;
|
||||
if (!stack) {
|
||||
return undefined;
|
||||
}
|
||||
return stack
|
||||
.split("\n")
|
||||
.slice(2, 7)
|
||||
.map((line) => line.trim())
|
||||
.join(" | ");
|
||||
}
|
||||
|
||||
export function useDictationAudioSource(config: DictationAudioSourceConfig): DictationAudioSource {
|
||||
const onPcmSegmentRef = useRef(config.onPcmSegment);
|
||||
const onErrorRef = useRef(config.onError);
|
||||
const [volume, setVolume] = useState(0);
|
||||
const sourceIdRef = useRef<number | null>(null);
|
||||
const engineRef = useRef<ReturnType<typeof createAudioEngine> | null>(null);
|
||||
|
||||
if (sourceIdRef.current === null) {
|
||||
sourceIdRef.current = nextDictationAudioSourceInstanceId++;
|
||||
console.log("[DictationAudioSource] instance_created", {
|
||||
sourceId: sourceIdRef.current,
|
||||
stack: getDictationTraceStack(),
|
||||
});
|
||||
}
|
||||
|
||||
const sourceId = sourceIdRef.current;
|
||||
|
||||
console.log("[DictationAudioSource] render", {
|
||||
sourceId,
|
||||
hasEngine: Boolean(engineRef.current),
|
||||
});
|
||||
|
||||
const getOrCreateEngine = useCallback(() => {
|
||||
if (engineRef.current) {
|
||||
return engineRef.current;
|
||||
}
|
||||
|
||||
console.log("[DictationAudioSource] create_engine", {
|
||||
sourceId,
|
||||
stack: getDictationTraceStack(),
|
||||
});
|
||||
engineRef.current = createAudioEngine(
|
||||
{
|
||||
onCaptureData: (pcm) => {
|
||||
onPcmSegmentRef.current(Buffer.from(pcm).toString("base64"));
|
||||
},
|
||||
onVolumeLevel: (level) => {
|
||||
setVolume(level);
|
||||
},
|
||||
onError: (error) => {
|
||||
onErrorRef.current?.(error);
|
||||
},
|
||||
},
|
||||
{
|
||||
traceLabel: `dictation:${sourceId}`,
|
||||
}
|
||||
);
|
||||
return engineRef.current;
|
||||
}, [sourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
onPcmSegmentRef.current = config.onPcmSegment;
|
||||
onErrorRef.current = config.onError;
|
||||
}, [config.onPcmSegment, config.onError]);
|
||||
|
||||
const speechmatics = useSpeechmaticsAudio({
|
||||
enableContinuousStreaming: true,
|
||||
onAudioSegment: ({ audioData }) => {
|
||||
onPcmSegmentRef.current(audioData);
|
||||
},
|
||||
onError: (err) => {
|
||||
onErrorRef.current?.(err);
|
||||
},
|
||||
volumeThreshold: 0.3,
|
||||
silenceDuration: 2000,
|
||||
speechConfirmationDuration: 300,
|
||||
detectionGracePeriod: 200,
|
||||
});
|
||||
useEffect(() => {
|
||||
console.log("[DictationAudioSource] mount", {
|
||||
sourceId,
|
||||
});
|
||||
return () => {
|
||||
console.log("[DictationAudioSource] unmount", {
|
||||
sourceId,
|
||||
});
|
||||
};
|
||||
}, [sourceId]);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
await speechmatics.start();
|
||||
}, [speechmatics]);
|
||||
console.log("[DictationAudioSource] start", {
|
||||
sourceId,
|
||||
});
|
||||
const engine = getOrCreateEngine();
|
||||
await engine.initialize();
|
||||
await engine.startCapture();
|
||||
}, [getOrCreateEngine, sourceId]);
|
||||
|
||||
const stop = useCallback(async () => {
|
||||
await speechmatics.stop();
|
||||
}, [speechmatics]);
|
||||
console.log("[DictationAudioSource] stop", {
|
||||
sourceId,
|
||||
hasEngine: Boolean(engineRef.current),
|
||||
});
|
||||
await engineRef.current?.stopCapture();
|
||||
setVolume(0);
|
||||
}, [sourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const engine = engineRef.current;
|
||||
engineRef.current = null;
|
||||
console.log("[DictationAudioSource] destroy_engine", {
|
||||
sourceId,
|
||||
hadEngine: Boolean(engine),
|
||||
});
|
||||
void engine?.destroy().catch(() => undefined);
|
||||
};
|
||||
}, [sourceId]);
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
volume: speechmatics.volume,
|
||||
volume,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user