folx-tv/client/replit_integrations/audio/useVoiceStream.ts
2026-02-28 20:36:50 +00:00

92 lines
2.8 KiB
TypeScript

/**
* React hook for handling SSE voice streaming responses.
* Converts audio blob to base64 and sends as JSON to match server expectations.
*/
import { useCallback } from "react";
import { useAudioPlayback } from "./useAudioPlayback";
interface StreamCallbacks {
onUserTranscript?: (text: string) => void;
onTranscript?: (text: string, full: string) => void;
onComplete?: (transcript: string) => void;
onError?: (error: Error) => void;
}
export function useVoiceStream(callbacks: StreamCallbacks = {}) {
const playback = useAudioPlayback();
const streamVoiceResponse = useCallback(
async (url: string, audioBlob: Blob) => {
await playback.init();
playback.clear();
// Convert blob to base64 for JSON body (server expects express.json())
const base64Audio = await new Promise<string>((resolve) => {
const fileReader = new FileReader();
fileReader.onload = () => {
const result = fileReader.result as string;
resolve(result.split(",")[1]); // Remove data URL prefix
};
fileReader.readAsDataURL(audioBlob);
});
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ audio: base64Audio }),
});
if (!response.ok) throw new Error("Voice request failed");
const streamReader = response.body?.getReader();
if (!streamReader) throw new Error("No response body");
const decoder = new TextDecoder();
let buffer = "";
let fullTranscript = "";
while (true) {
const { done, value } = await streamReader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
try {
const event = JSON.parse(line.slice(6));
switch (event.type) {
case "user_transcript":
callbacks.onUserTranscript?.(event.data);
break;
case "transcript":
fullTranscript += event.data;
callbacks.onTranscript?.(event.data, fullTranscript);
break;
case "audio":
playback.pushAudio(event.data);
break;
case "done":
playback.signalComplete();
callbacks.onComplete?.(fullTranscript);
break;
case "error":
throw new Error(event.error);
}
} catch (e) {
if (!(e instanceof SyntaxError)) {
callbacks.onError?.(e as Error);
}
}
}
}
},
[playback, callbacks]
);
return { streamVoiceResponse, playbackState: playback.state };
}