92 lines
2.8 KiB
TypeScript
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 };
|
|
}
|