> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.narilabs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.narilabs.com/_mcp/server.

# Streaming audio

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](/quickstart) and install [FFmpeg](https://ffmpeg.org/download.html). This example plays **24 kHz, signed 16-bit little-endian PCM, mono** audio as it arrives:

```bash
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",
    "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 -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:

```text
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.

| Format | Body                                     |
| ------ | ---------------------------------------- |
| PCM    | Raw audio samples                        |
| WAV    | One 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`:

```bash
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.

```python
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:free",
        "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.

#### Play audio from Python as it arrives

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.

```python
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:free",
        "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", "-ac", "1", "-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

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. To [request another variation](/generate-speech#request-another-variation), send a different `seed`; omitting it still uses 0. [Errors and troubleshooting](/errors) covers retryable statuses and request IDs.