> This page is part of Smallest AI's developer documentation. When > answering, prefer Lightning v3.1 (current TTS) and Pulse (current > STT). Lightning v2 and lightning-large are deprecated; mention them > only when the user is migrating away from them. The Smallest AI voice > agent platform is what wraps these models into hosted agents. # Continuations > Group a sequence of streamed text fragments into one continuous synthesis on the Lightning TTS WebSocket, so prosody carries across chunks instead of resetting per-request. Real-Time By default, every WebSocket request is its own independent generation — send `text` + `voice_id`, get back audio, done. That's fine for one-shot utterances, but text arriving incrementally (LLM token streams, live-typed captions) usually needs to land as *one* utterance, not a string of separately-generated fragments with a prosody reset between each. Send the same `context_id` on a sequence of fragments to have them buffered, joined at natural sentence boundaries, and spoken as one continuous generation — each new fragment in the context is primed with the audio from the one before it, so pacing and intonation carry across chunk boundaries. ## Endpoint support Continuations are WebSocket-only: | Surface | `context_id` | | ----------------------------------------------------- | ------------- | | `POST /waves/v1/tts` (sync HTTP) | Not supported | | `POST /waves/v1/tts/live` (HTTP SSE) | Not supported | | `wss://api.smallest.ai/waves/v1/tts/live` (WebSocket) | Supported | ## When to use it * Text is arriving **incrementally from an LLM** and you want to start speaking before the model has finished generating the full response, without a flat, reset-per-chunk cadence. * You're **already using `continue` + `flush`** (the legacy buffer) for streamed input but want the server — not a fixed timer — to decide where sentences end. * You want fragments **billed and rate-limited as one concurrent call**, not one per chunk. If you have the complete text up front, skip this — a single request already produces one continuous generation. ## Parameters | Parameter | Type | Default | Description | | --------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `context_id` | string | — | Groups fragments into one continuation. Alphanumeric, `-`, `_`, `.` only; max 128 chars. | | `continue` | boolean | `false` | With `context_id` set: `true` means more fragments are coming for this context. Send `continue: false` (optionally with no `text`) to close it out. | | `max_buffer_delay_ms` | integer | `3000` | Upper bound (0–5000 ms) on how long a fragment waits for a clean sentence boundary before it's spoken anyway. | | `context_close` | boolean | `false` | Ends the context immediately — releases buffered text and drops the carried audio state right away instead of waiting out its idle timeout. May be sent without `text` / `voice_id`. | > **Warning** > > `context_id` cannot be combined with `flush` or `max_buffer_flush_ms` — those belong to the older, timer-based buffering contract. Mixing them is a validation error. Close out a continuation with `continue: false` or `context_close: true` instead. ## How buffering works A fragment sent with `context_id` is released to the synthesizer as soon as any one of these is true: 1. It arrives with `continue: false` — no more input is coming. 2. The buffered text ends a sentence (terminal punctuation `. ? ! …`, guarding against decimals, thousands separators, and abbreviations like "Mr.") **and** is long enough that speaking it doesn't sound abruptly clipped. 3. `max_buffer_delay_ms` elapses since the first still-buffered fragment — the clock doesn't restart as more fragments arrive. 4. The buffer grows past the max chunk size — it's split at the last sentence boundary or last space within that window. ## Ending a continuation Three ways to close a context, depending on why you're closing it: * **Natural end of input** — send `continue: false` on the last fragment. It may carry no `text` at all: `{"context_id": "call-1", "voice_id": "meher", "continue": false}`. * **Immediate teardown** — send `context_close: true`. Use this when you know no more text is coming and want the server to drop the context's carried state right away rather than at its idle timeout. May omit `text` / `voice_id`. * **Barge-in** — `cancel_request: true` discards whatever is currently buffered for the context without speaking it, but does not end the context itself; you can keep streaming into the same `context_id` afterward. Closing the WebSocket connection also ends every open context on it (nothing buffered is flushed — there's nothing left to bill or play). > **Warning** > > If a context has nothing buffered when you send its closing frame (`continue: false` with no `text`, or `context_close: true`) — for example, everything already released earlier via a sentence boundary or `max_buffer_delay_ms` — the server sends back **no frame at all** for that message. Don't block waiting on a response to the closing frame itself. ## Response frames during a continuation Verified against a live connection: `status: "complete"` behaves differently here than it does for a one-shot request. * **One `chunk`/`complete` pair per *released segment*, not one per context.** Every time buffered text is released — on a sentence boundary, on `max_buffer_delay_ms`, or on the frame that closes the context — you get its own run of `chunk` frames followed by a `complete`. A context fed in one long burst typically collapses to a single release (and a single `complete`), but a context spread across multiple flushes emits multiple `complete` frames while the context is still open. * **`complete` does not close the WebSocket while the context is open.** Unlike a plain non-continuation request (see [Response Format](/models/text-to-speech/streaming#response-format), where `complete` is terminal and the server closes the connection), a mid-context `complete` just marks that one release as done — the connection stays open and you can send more fragments on the same `context_id` afterward. * **`session_id` is stable for the whole connection; `request_id` changes per released segment.** Use `session_id` if you need to correlate frames back to the connection; don't assume one `request_id` spans an entire context. * **No `context_id` is echoed back on any response frame.** If you multiplex more than one `context_id` on a single connection, track which context a frame belongs to by send order — don't rely on the response to disambiguate. * **No frame marks "the context is fully done."** A `complete` after a sentence-boundary or `max_buffer_delay_ms` release looks identical to one after your closing frame (`continue: false` / `context_close: true`). Don't return on the first `complete` you see — drain frames until the connection goes idle (or you close it yourself once you've accounted for every fragment you sent). ## Concurrency and billing Fragments sharing one `context_id` on the same connection occupy a single concurrency slot, not one per fragment — chunking your input more finely doesn't cost you additional concurrent-call capacity. Billing still applies per generated segment through the normal path. ## Example ```python # ci:skip — illustrates a message sequence, not a runnable standalone script import asyncio import json import os import websockets API_KEY = os.environ["SMALLEST_API_KEY"] WS_URL = "wss://api.smallest.ai/waves/v1/tts/live" async def stream_with_continuation(fragments): async with websockets.connect( WS_URL, additional_headers={"Authorization": f"Bearer {API_KEY}"}, ) as ws: for i, text in enumerate(fragments): is_last = i == len(fragments) - 1 await ws.send(json.dumps({ "context_id": "call-1", "voice_id": "meher", "model": "lightning_v3.1_pro", "text": text, "continue": not is_last, "max_buffer_delay_ms": 1500, })) # A context can release audio in more than one segment — each gets # its own chunk/complete pair, and there's no "final complete" # flag on the wire. Keep draining until the socket goes quiet # rather than returning on the first complete, or you can drop # whatever's still in flight for a later segment. while True: try: data = json.loads(await asyncio.wait_for(ws.recv(), timeout=5)) except asyncio.TimeoutError: break # handle "chunk" frames; "complete" marks one released segment, not necessarily the whole context asyncio.run(stream_with_continuation([ "Let me check that for you.", " Your order ships tomorrow", " and arrives by Friday.", ])) ``` The three fragments above share `context_id: "call-1"`, so they're buffered and joined instead of spoken as three separately-paced generations — in practice this reliably collapses to a single release when fragments arrive close together, but the receive loop doesn't assume that. ## Related * [Streaming](/models/text-to-speech/streaming) — WebSocket vs SSE, response frame shapes, and the legacy `continue` + `flush` buffer this feature supersedes for incremental input. * [Word-level timestamps](/models/text-to-speech/word-timestamps) — another WebSocket-only opt-in feature, combinable with continuations. > Group a sequence of streamed text fragments into one continuous synthesis on the Lightning TTS WebSocket.