Restored to '84d7de2da64f8f2fd1e2d64959817c64c8fb7c04'
Replit-Restored-To: 84d7de2da6
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user