Skip to navigation

Streaming audio

Receive and play speech as it is generated.
View as Markdown

This guide covers HTTP output streaming with the full input text supplied in one request. For incremental input, use the TTS input streaming guide.

Send complete text to POST /v1/audio/speech with stream: true to receive audio progressively. With stream: false, the server generates the complete audio before returning it.

Play a streamed response

Set your API key as shown in the TTS quickstart and install FFmpeg. This example plays 24 kHz, signed 16-bit little-endian PCM, mono audio as it arrives:

set -o pipefail
curl --fail --silent --show-error --no-buffer \
https://api.narilabs.com/v1/audio/speech \
-H "Authorization: Bearer $NARI_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"model": "qwen3-tts",
"voice": "diana",
"input": "Hello, welcome to Nari Labs. This audio plays as it arrives.",
"stream": true,
"response_format": "pcm"
}' | ffplay -nodisp -autoexit -probesize 32 -analyzeduration 0 \
-f s16le -ar 24000 -ch_layout mono -i pipe:0

Streaming response

The response contains HTTP headers followed by a continuous binary audio body. For an input of Hello., a PCM response looks like this:

200 OK
Content-Type: audio/pcm
x-request-id: …
[PCM bytes][PCM bytes][PCM bytes] …

x-request-id identifies the request for tracing and support.

FormatBody
PCMRaw audio samples
WAVOne WAV header followed by audio samples

Streamed WAV has an unknown final length in its header. Use PCM if your player requires a finalized WAV length.

Read the bytes in order as they arrive. Read boundaries do not correspond to audio frames or separate files. The stream completes when the HTTP response body ends normally; there is no separate completion event.

Receive audio in Python

Install requests:

python -m pip install requests

This example reads incoming bytes and saves them to speech.pcm. It checks the status before writing audio and reports completion only after the response has been fully read.

import os
import requests
with requests.post(
"https://api.narilabs.com/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"},
json={
"model": "qwen3-tts",
"voice": "diana",
"input": "Hello, welcome to Nari Labs.",
"stream": True,
"response_format": "pcm",
},
stream=True,
timeout=(10, 60),
) as response:
response.raise_for_status()
print("Request:", response.headers["x-request-id"])
with open("speech.pcm", "wb") as audio:
for chunk in response.iter_content(chunk_size=4096):
audio.write(chunk)
print("Saved speech.pcm")

The JSON stream option enables server-side streaming. Python’s stream=True reads the response incrementally. chunk_size=4096 is a client read setting, not a server chunk-size guarantee. The timeout values are client settings, not API limits.

This complete example also requires FFmpeg. It passes each read to ffplay, lets buffered audio finish playing on normal completion, and stops the player if receiving audio fails.

import os
import subprocess
import requests
with requests.post(
"https://api.narilabs.com/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"},
json={
"model": "qwen3-tts",
"voice": "diana",
"input": "Hello, welcome to Nari Labs. This audio plays as it arrives.",
"stream": True,
"response_format": "pcm",
},
stream=True,
timeout=(10, 60),
) as response:
response.raise_for_status()
with subprocess.Popen([
"ffplay", "-nodisp", "-autoexit", "-loglevel", "error",
"-probesize", "32", "-analyzeduration", "0",
"-f", "s16le", "-ar", "24000", "-ch_layout", "mono", "-i", "pipe:0",
], stdin=subprocess.PIPE) as player:
try:
for chunk in response.iter_content(chunk_size=4096):
if chunk:
player.stdin.write(chunk)
player.stdin.flush()
player.stdin.close()
if player.wait() != 0:
raise RuntimeError("Audio playback failed")
except BaseException:
player.kill()
raise

Playback tips

  • Keep the receive loop responsive. Avoid slow unrelated work between reads. If playback runs separately, pass audio through a bounded queue so buffering cannot grow indefinitely.
  • Tune the playback buffer when needed. If audio stutters, increase the amount buffered before playback; if startup is too slow, reduce it. At this format, 100 ms is 4,800 bytes—an example conversion, not a required buffer size.
  • Preserve sample boundaries in custom decoders. Each PCM sample uses two bytes. If a read ends with one unmatched byte, keep it for the next read. ffplay handles this in the examples.

Interrupted streams

If the connection fails or a read times out, treat the audio as incomplete even if the response status was 200. Retrying starts generation from the beginning; interrupted streams cannot be resumed.

See Errors and troubleshooting for error codes and retry guidance.