diff --git a/public/src/tts.js b/public/src/tts.js index c966203..fd7a255 100644 --- a/public/src/tts.js +++ b/public/src/tts.js @@ -178,14 +178,30 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode // If MSE isn't available, use progressive playback: // Start playing after first chunk arrives instead of waiting for full download if (!canStream) { - console.log(`Using progressive playback (no MSE for ${requestFormat})`); + console.log(`Progressive playback mode (no MSE for ${requestFormat})`); + + // Check if ReadableStream is available + if (!response.body) { + console.warn("Response body not streamable, falling back to full buffer"); + const audioBlob = await response.blob(); + const audioUrl = URL.createObjectURL(audioBlob); + const audio = new Audio(audioUrl); + audio.playbackRate = rate; + setLastTTS(text, { audio, blob: audioBlob }); + return { audio, blob: audioBlob, blobUrl: audioUrl, streamed: false, done: Promise.resolve({ blob: audioBlob, blobUrl: audioUrl }) }; + } + const reader = response.body.getReader(); const chunks = []; let totalBytes = 0; let audio = null; - let currentBlobUrl = null; + let audioReady = null; + let audioReadyResolve = null; - const done = new Promise(async (resolve, reject) => { + // Promise that resolves when audio element is created + audioReady = new Promise((resolve) => { audioReadyResolve = resolve; }); + + const done = (async () => { try { while (true) { const { done: readerDone, value } = await reader.read(); @@ -193,16 +209,17 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode chunks.push(value); totalBytes += value.byteLength; + console.log(`Progressive: received ${totalBytes} bytes`); - // Start playback after accumulating some data (~8KB) - // This gives the decoder enough to parse headers and begin + // Start playback after accumulating enough data for decoder headers if (!audio && totalBytes > 8192) { const partialBlob = new Blob(chunks, { type: AUDIO_FORMAT.mime }); - currentBlobUrl = URL.createObjectURL(partialBlob); - audio = new Audio(currentBlobUrl); + const partialUrl = URL.createObjectURL(partialBlob); + audio = new Audio(partialUrl); audio.playbackRate = rate; audio.play().catch(e => console.warn("Auto-play blocked:", e.message)); console.log(`Progressive playback started at ${totalBytes} bytes`); + audioReadyResolve(audio); } } @@ -211,12 +228,14 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode const fullBlobUrl = URL.createObjectURL(fullBlob); if (!audio) { - // Very short audio - never hit the threshold + // Short audio — never hit the 8KB threshold audio = new Audio(fullBlobUrl); audio.playbackRate = rate; audio.play().catch(e => console.warn("Auto-play blocked:", e.message)); + console.log(`Progressive: short audio, playing full ${totalBytes} bytes`); + audioReadyResolve(audio); } else { - // Replace with complete audio to enable seeking + // Swap to complete blob for seeking/duration support const currentTime = audio.currentTime; const wasPlaying = !audio.paused; audio.src = fullBlobUrl; @@ -225,40 +244,29 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode if (wasPlaying) audio.play().catch(() => {}); } - if (currentBlobUrl && currentBlobUrl !== fullBlobUrl) { - URL.revokeObjectURL(currentBlobUrl); - } - console.log(`Progressive playback complete: ${totalBytes} bytes`); setLastTTS(text, { audio, blob: fullBlob }); - resolve({ blob: fullBlob, blobUrl: fullBlobUrl }); + return { blob: fullBlob, blobUrl: fullBlobUrl }; } catch (error) { console.error("Progressive playback error:", error); - reject(error); + // If audio was never created, unblock the waiter + if (!audio) audioReadyResolve(null); + throw error; } - }); + })(); - // Return immediately — audio will be set once first chunks arrive - // Use a proxy object so callers get the audio once it's ready - const result = { audio: null, blob: null, blobUrl: null, streamed: true, done }; + // Wait for audio element to be created (first ~8KB) + const readyAudio = await audioReady; + if (!readyAudio) { + throw new Error("Progressive playback failed to create audio"); + } + + const result = { audio: readyAudio, blob: null, blobUrl: null, streamed: true, done }; done.then(({ blob, blobUrl }) => { result.blob = blob; result.blobUrl = blobUrl; - }).catch(() => {}); - - // Wait for audio element to be created (first chunks) - await new Promise((resolve) => { - const check = () => { - if (audio) { - result.audio = audio; - resolve(); - } else { - setTimeout(check, 50); - } - }; - check(); - }); + }).catch((err) => console.error("Progressive done error:", err)); return result; }