Streaming audio

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

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:free",
> "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 -ac 1 -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: …
x-usage-input-characters: 6
[PCM bytes][PCM bytes][PCM bytes] …

x-request-id identifies the request. x-usage-input-characters reports the input character count; Usage and billing explains how it is measured.

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.

1import os
2
3import requests
4
5with requests.post(
6 "https://api.narilabs.com/v1/audio/speech",
7 headers={"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"},
8 json={
9 "model": "qwen3-tts:free",
10 "input": "Hello, welcome to Nari Labs.",
11 "stream": True,
12 "response_format": "pcm",
13 },
14 stream=True,
15 timeout=(10, 60),
16) as response:
17 response.raise_for_status()
18 print("Request:", response.headers["x-request-id"])
19 print("Input characters:", response.headers["x-usage-input-characters"])
20 with open("speech.pcm", "wb") as audio:
21 for chunk in response.iter_content(chunk_size=4096):
22 audio.write(chunk)
23
24print("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.

1import os
2import subprocess
3
4import requests
5
6with requests.post(
7 "https://api.narilabs.com/v1/audio/speech",
8 headers={"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"},
9 json={
10 "model": "qwen3-tts:free",
11 "input": "Hello, welcome to Nari Labs. This audio plays as it arrives.",
12 "stream": True,
13 "response_format": "pcm",
14 },
15 stream=True,
16 timeout=(10, 60),
17) as response:
18 response.raise_for_status()
19 with subprocess.Popen([
20 "ffplay", "-nodisp", "-autoexit", "-loglevel", "error",
21 "-probesize", "32", "-analyzeduration", "0",
22 "-f", "s16le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
23 ], stdin=subprocess.PIPE) as player:
24 try:
25 for chunk in response.iter_content(chunk_size=4096):
26 if chunk:
27 player.stdin.write(chunk)
28 player.stdin.flush()
29 player.stdin.close()
30 if player.wait() != 0:
31 raise RuntimeError("Audio playback failed")
32 except BaseException:
33 player.kill()
34 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

Errors detected before response headers are sent return a non-2xx status with a JSON body. Check the status before treating the body as audio.

After headers are sent, the status cannot change. A connection error or read timeout while receiving audio means the request has not completed successfully, even if the status was 200 and some speech has played. No JSON error is appended to the audio.

Do not mark partial audio as a completed result. The file-writing example may leave a partial speech.pcm after a read failure; the playback example stops its player. A retry sends the full text again and can repeat speech already heard. There is no resume offset. Errors and troubleshooting covers retryable statuses and request IDs.