| 4 | * Hook for text-to-speech playback via the TTS API. |
| 5 | */ |
| 6 | export function useTTS() { |
| 7 | const [playingId, setPlayingId] = useState<string | null>(null) |
| 8 | const audioRef = useRef<HTMLAudioElement | null>(null) |
| 9 | |
| 10 | const speak = useCallback(async (text: string, id: string) => { |
| 11 | // Stop any currently playing audio |
| 12 | if (audioRef.current) { |
| 13 | audioRef.current.pause() |
| 14 | audioRef.current = null |
| 15 | } |
| 16 | |
| 17 | setPlayingId(id) |
| 18 | |
| 19 | try { |
| 20 | const response = await fetch('/demo/api/tts', { |
| 21 | method: 'POST', |
| 22 | headers: { 'Content-Type': 'application/json' }, |
| 23 | body: JSON.stringify({ |
| 24 | text, |
| 25 | voice: 'nova', |
| 26 | model: 'tts-1', |
| 27 | format: 'mp3', |
| 28 | }), |
| 29 | }) |
| 30 | |
| 31 | if (!response.ok) { |
| 32 | const errorData = await response.json() |
| 33 | throw new Error(errorData.error || 'TTS failed') |
| 34 | } |
| 35 | |
| 36 | const result = await response.json() |
| 37 | |
| 38 | // Convert base64 to audio and play |
| 39 | const audioData = atob(result.audio) |
| 40 | const bytes = new Uint8Array(audioData.length) |
| 41 | for (let i = 0; i < audioData.length; i++) { |
| 42 | bytes[i] = audioData.charCodeAt(i) |
| 43 | } |
| 44 | const blob = new Blob([bytes], { type: result.contentType }) |
| 45 | const url = URL.createObjectURL(blob) |
| 46 | |
| 47 | const audio = new Audio(url) |
| 48 | audioRef.current = audio |
| 49 | |
| 50 | audio.onended = () => { |
| 51 | URL.revokeObjectURL(url) |
| 52 | setPlayingId(null) |
| 53 | audioRef.current = null |
| 54 | } |
| 55 | |
| 56 | audio.onerror = () => { |
| 57 | URL.revokeObjectURL(url) |
| 58 | setPlayingId(null) |
| 59 | audioRef.current = null |
| 60 | } |
| 61 | |
| 62 | await audio.play() |
| 63 | } catch (error) { |