Now that we can synthesize a full utterance, we're going to surface the audio as it synthesizes. For low-latency voice, the reader wants each chunk as soon as the engine produces it.
textToSpeech({ stream: true }) returns { buffer, bufferStream, done } instead of just { buffer }. The samples live on result.bufferStream as an AsyncGenerator<number>.
Setting stream: true is what flips the result shape to { buffer, bufferStream, done }. You would call it like so:
const result = textToSpeech({
modelId,
text: "Streaming chunks as the engine synthesizes them is the right latency for low-latency voice.",
inputType: "text",
stream: true,
});Iterating result.bufferStream with for await is the canonical way to consume samples:
let totalSamples = 0;
for await (const sample of result.bufferStream) {
void sample;
totalSamples += 1;
}
console.log(`▸ Streamed ${totalSamples} samples`);Each number is a single PCM sample. Iterating with for await yields samples in the order they were synthesized. result.buffer is empty when stream: true; the samples live on the generator, not the promise.
result.done is a Promise<boolean> that resolves when synthesis finishes. Await it from a separate branch if you need to know when the stream terminates.
Note: this is a different shape from
textToSpeechStream, which is a duplex session for piping tokens from a streaming LLM into TTS. UsetextToSpeech({ stream: true })when you already have the full text and want a stream of audio chunks.
textToSpeech({ modelId, text, inputType: "text", stream: true }).result.bufferStream with for await (const sample of result.bufferStream) and count the samples.result.buffer; it stays empty when streaming.$ Run your code to see results
$