# Input streaming

Send text over a WebSocket as it is produced and receive audio continuously.

## When to use it

Use this API when the text is still being produced — typically an LLM’s token stream. Rather than buffering output into sentences and synthesizing one at a time, the server consumes fragments as they arrive and holds back only a few words: the model generates each audio chunk from everything already spoken plus a short lookahead, so it never needs a complete sentence.

- Speech starts about ten words into the stream, mid-sentence: 0.86 s to first audio versus 1.92 s for sentence buffering on the same simulated LLM stream
- Against a live LLM at typical API cadence, audio began ~450 ms after the model’s first token
- Prosody is continuous across chunk seams — each chunk is generated with the preceding audio as context, so there are no restarts or intonation resets at chunk boundaries
- The connection is opened once, not per utterance, so each reply skips DNS, TLS, and admission. Measured from an Indian client against production: first audio ~420 ms after connection open, token feed included

Two WebSocket endpoints expose this: a native one (recommended) and an ElevenLabs-compatible one for existing EL integrations. Both authenticate with your usual header on the upgrade request; see [Authentication](https://docs.kenpathlabs.com/authentication.md).

## Native WebSocket

```http
WS /v1/audio/speech/stream-input
```

Connect with query parameters, send text fragments as JSON messages the moment they’re produced, and read binary frames back: raw PCM, 16-bit LE mono at 24 kHz. The server chunks at safe boundaries and keeps rolling text-and-audio context, so chunk N+1 continues chunk N’s prosody. Don’t pre-chunk or sentence-split yourself.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `voice` | query | `-` | Voice id from GET /v1/voices: an sv_-prefixed id, e.g. sv_enhdbrj5 (Aanya). |
| `lang` | query | - | Language hint: any form works (`hi`, `hin`, `hindi`, `hi-IN`). Omit to auto-detect from script. |
| `mode` | query | `sentence` | Chunking strategy: `sentence` (waits for sentence boundaries) or `eager` (word-level, lowest latency; see below). |
| `speed` | query | `1.0` | Speaking speed, `0.7`–`1.5`, pitch-preserving. Applied continuously across the whole session, so chunk seams stay seamless. |
| `temperature …` | query | - | Sampling knobs ride as query params too. Omit them; the server fills serving-tuned defaults. |

### Messages you send

| Parameter | Type | Default | Description |
|---|---|---|---|
| `{"text": "fragment "}` | JSON | - | A text fragment, as small as a single token delta. Send them as fast as they arrive. |
| `{"flush": true}` | JSON | - | Force everything buffered out as audio now (for example at the end of an LLM paragraph). |
| `{"text": ""}` | JSON | - | End of stream. The server synthesizes the remainder, sends `{"type": "done"}`, and closes. |

```bash
pip install git+https://github.com/kenpath-labs/svara-python.git
```

```python
import asyncio
from openai import AsyncOpenAI
from svara import AsyncSvara

llm = AsyncOpenAI()
svara = AsyncSvara()                      # reads SVARA_API_KEY


async def speak(prompt: str):
    async def deltas():
        stream = await llm.chat.completions.create(
            model="gpt-4o-mini", stream=True,
            messages=[{"role": "user", "content": prompt}])
        async for event in stream:
            if event.choices[0].delta.content:
                yield event.choices[0].delta.content

    # One WebSocket; audio starts a few words into the LLM's output.
    async for audio in svara.speech.stream_input(deltas(), voice="sv_enhdbrj5"):
        player.feed(audio)                # PCM16 @ 24 kHz mono

asyncio.run(speak("Explain how rainbows form, in three sentences."))
```

```python
import asyncio, json, os, websockets
from openai import AsyncOpenAI

llm = AsyncOpenAI()
URL = "wss://api.kenpathlabs.com/v1/audio/speech/stream-input?voice=sv_enhdbrj5&mode=eager"  # voice: Aanya

async def speak(prompt: str):
    headers = {"Authorization": f"Bearer {os.environ['SVARA_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:

        async def feed():
            stream = await llm.chat.completions.create(
                model="gpt-4o-mini", stream=True,
                messages=[{"role": "user", "content": prompt}])
            async for event in stream:
                delta = event.choices[0].delta.content or ""
                if delta:
                    await ws.send(json.dumps({"text": delta}))
            await ws.send(json.dumps({"text": ""}))   # end of stream

        feeder = asyncio.create_task(feed())
        async for frame in ws:                        # binary = PCM16 @ 24 kHz
            if isinstance(frame, bytes):
                player.feed(frame)                    # your audio sink
            elif json.loads(frame).get("type") == "done":
                break
        await feeder

asyncio.run(speak("Explain how rainbows form, in three sentences."))
```

```javascript
import WebSocket from "ws";

const ws = new WebSocket(
  "wss://api.kenpathlabs.com/v1/audio/speech/stream-input?voice=sv_enhdbrj5&mode=eager", // voice: Aanya
  { headers: { Authorization: `Bearer ${process.env.SVARA_API_KEY}` } },
);

ws.on("open", async () => {
  for await (const delta of llmTokenStream()) {   // your LLM stream
    ws.send(JSON.stringify({ text: delta }));
  }
  ws.send(JSON.stringify({ text: "" }));          // end of stream
});

ws.on("message", (data, isBinary) => {
  if (isBinary) player.feed(data);                // PCM16 @ 24 kHz mono
  else if (JSON.parse(data).type === "done") ws.close();
});
```

## Eager mode

`mode=eager` is the lowest-latency configuration and maps directly onto how the model was trained. The server buffers incoming deltas at word granularity (mid-word token deltas are handled), and as soon as `chunk_words + peek_words` complete words exist it synthesizes the first `chunk_words` with the next few words attached as the lookahead peek:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `chunk_words` | query | `4` | Words per synthesis chunk. Smaller = earlier first audio, more chunk seams (they’re seamless, but each chunk has scheduling overhead). |
| `peek_words` | query | `2` | Lookahead words held back so each chunk knows what’s coming (clamped 1–5). The final chunk goes out with no peek, which is the model’s end-of-utterance signal. |
| `max_chunk_words` | query | `20` | Upper bound on a chunk once text has queued up — keeps a fast LLM from producing one enormous chunk. |

With defaults, speech starts roughly 6 words into the LLM’s output; typically well before the first sentence ends.

## ElevenLabs-compatible WebSocket

```http
WS /v1/text-to-speech/{voice_id}/stream-input
```

A drop-in implementation of the ElevenLabs realtime protocol, for existing EL integrations: the official EL SDKs’ WebSocket clients work unmodified. The full protocol is supported:

- **BOS**: `{"text": " ", "generation_config": {"chunk_length_schedule": [120,160,250,290]}}` (schedule values clamped 50-500; `auto_mode` supported)
- **Text**: `{"text": "fragment ", "try_trigger_generation": true}`, plus `{"flush": true}`; a bare space is a keepalive
- **EOS**: `{"text": ""}`
- **Audio frames**: `{"audio": "<base64>", "alignment": …, "normalizedAlignment": …}` in the negotiated `output_format` (query param, e.g. `pcm_24000`, `mp3_44100_128`), then `{"isFinal": true}` and a clean close (code 1000)
- `inactivity_timeout` query param: 20 s default, 180 s max

Character timings in `alignment` are chunk-relative and approximate: chunk boundaries are sample-exact, characters uniform within a chunk.

## Tips

- **Forward raw deltas.** Don’t sentence-split, batch, or “clean up” the LLM stream client-side; the server’s chunker is the one that knows the model’s trained format.
- **Buffer ~150 ms on playback.** A small jitter buffer on your audio sink absorbs network variance without hurting perceived latency.
- **Keep the socket warm for turns, not sessions.** Open the connection when a reply starts, close on `{"type": "done"}`. Each connection is one utterance with one prosodic arc.
- **Concurrent conversations need concurrent streams**: each open socket counts against your plan’s concurrency limit (see [Rate limits](https://docs.kenpathlabs.com/rate-limits.md)).
