TTS input streaming

Send text incrementally and receive audio chunks as they are generated.
View as Markdown

Send text and receive generated audio on the same WebSocket. Output streams automatically; there is no separate stream: true option.

Connect

Open wss://api.narilabs.com/v1/text-to-speech/{voice_id}/stream-input?model_id=qwen3-tts with Authorization: Bearer YOUR_API_KEY in the handshake. Use a voice ID supported by the selected model. qwen3-tts is Standard; qwen3-tts-fast is Fast.

Connect from your backend: browser WebSocket clients cannot set an Authorization header. Keep x-request-id from the handshake for troubleshooting. The WebSocket API reference lists optional query parameters and message fields.

Send text and flush

Send JSON text messages in this order:

{"text":" "}
{"text":"Hello from "}
{"text":"Nari Labs.","flush":true}
{"text":"This is a second segment.","flush":true}
{"text":""}

Send each line as a separate message. Start with exactly one space; it is not billed. Later text fragments join without added spaces, so include spaces between words yourself. Audio may arrive before a flush when enough text is available; short input may wait for more text or a flush. flush: true completes a segment and keeps the connection open; empty text ends input. Continue receiving until the connection finishes.

Only automatic generation is supported. Omit auto_mode or set it to true; false, generation_config, generator_config, and try_trigger_generation are rejected.

Receive audio

Receive messages concurrently while sending text:

{"audio":"AAAAAA==","isFinal":false}

Decode audio from base64 into 24 kHz, 16-bit little-endian, mono PCM. There is no WAV header. Append chunks in arrival order.

The final message is:

{"isFinal":true}

isFinal completes the connection. Individual flushes have no completion event; see Request logs for their results.

Python example: two flushes

Install websockets version 14 or later and set NARI_API_KEY. This script saves both segments as one speech.pcm file.

import asyncio
import base64
import json
import os
from pathlib import Path
from websockets.asyncio.client import connect
async def main():
url = "wss://api.narilabs.com/v1/text-to-speech/diana/stream-input?model_id=qwen3-tts"
headers = {"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"}
async with connect(url, additional_headers=headers) as ws:
print("Request ID:", ws.response.headers.get("x-request-id"))
await ws.send(json.dumps({"text": " "}))
async def send_text():
messages = [
{"text": "Hello from "},
{"text": "Nari Labs.", "flush": True},
{"text": "This is a second segment.", "flush": True},
{"text": ""},
]
for message in messages:
await ws.send(json.dumps(message))
async def receive_audio():
with Path("speech.pcm").open("wb") as output:
async for raw in ws:
event = json.loads(raw)
if "error" in event:
raise RuntimeError(f"{event['error']}: {event.get('message', '')}")
if event.get("audio"):
output.write(base64.b64decode(event["audio"], validate=True))
if event.get("isFinal"):
return
raise RuntimeError("Connection ended before isFinal; audio is incomplete")
await asyncio.gather(send_text(), receive_audio())
if __name__ == "__main__":
asyncio.run(main())

Play the PCM file with:

ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 speech.pcm

If the connection ends before isFinal, the audio may be incomplete. See WebSocket failures and Usage and billing.