Skip to navigation

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.