folx-tv/client/replit_integrations/audio/useVoiceStream.ts
sebastjanartic 308e602c73 Fix hero card aspect ratio and add horoscope generation functionality
Introduce horoscope generation via OpenAI API, including new API endpoints and database schema. Adjust card components in `home.tsx` to use `aspect-[16/9]` for consistent image sizing, resolving previous height stretching issues. Update dependencies in `package.json`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ca1aa952-242c-43c1-9e28-47aed39cee1b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/413891e8-d784-4bea-b9f5-91a5a68316b4/nTLKCC5
Replit-Helium-Checkpoint-Created: true
2026-02-28 20:25:58 +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 };
}