> ## Documentation Index
> Fetch the complete documentation index at: https://polargrid.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Text-to-Speech

> Convert text to natural-sounding speech

# Text-to-Speech

Generate audio from text. PolarGrid serves `kokoro-82m` (preset voice catalog) and `tada-3b-ml` (voice-cloning). The endpoint returns audio in the container format requested via `response_format` — supported values are `pcm`, `wav`, and `mp3`.

<Note>
  Edge endpoints accept your `pg_*` API key as a bearer token. See [Authentication](/authentication) for details. The cURL examples below pin Toronto (`yto-01`) for concreteness — substitute another region or discover the fastest one via `GET https://autorouter.polargrid.ai/v1/route`. See [API Overview](/api-reference/overview#picking-a-region) for both patterns.
</Note>

## Create Speech

```
POST /v1/audio/speech
```

Generate audio from input text.

### Request Body

| Parameter          | Type    | Required | Default | Description                                                                                                                                                                           |
| ------------------ | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`            | string  | Yes      | —       | TTS model: `kokoro-82m` or `tada-3b-ml`                                                                                                                                               |
| `input`            | string  | Yes      | —       | Text to convert                                                                                                                                                                       |
| `voice`            | string  | Yes      | —       | Voice to use (see below)                                                                                                                                                              |
| `voice_transcript` | string  | No       | —       | **`tada-3b-ml` only.** The exact text spoken in the `voice` reference clip. Required when `voice` is a URL or base64 WAV; not needed for `voice: "default"`. Ignored by `kokoro-82m`. |
| `language`         | string  | No       | `en`    | **`tada-3b-ml` only.** Target synthesis language: `en`, `fr`, `de`, `es`, `it`, `pt`, `pl`, `ja`, `ar`, `zh`. Ignored by `kokoro-82m`.                                                |
| `response_format`  | string  | No       | `pcm`   | Container format: `pcm`, `wav`, or `mp3`. See [Audio Format](#audio-format) below.                                                                                                    |
| `stream`           | boolean | No       | `false` | When `true`, returns chunked audio. See [Streaming](#streaming). Only `pcm` and `opus` are valid `response_format` values when streaming.                                             |
| `speed`            | number  | No       | 1.0     | Speed multiplier (0.25-4.0). `tada-3b-ml` honors this only in batch mode — streaming TADA requires `speed: 1.0`.                                                                      |

The gateway returns `400 Bad Request` if `input` is empty/whitespace-only, `voice` is empty, or `response_format` is anything other than `pcm`, `wav`, or `mp3`. Synthesis failures (invalid voice ID, upstream error) return `502 Bad Gateway` — never an empty `200`.

#### Input length limits

`input` length is capped per model and enforced **before** synthesis; over-limit requests return `413 Payload Too Large` with a message naming the model and its limit (e.g. `Input too long: maximum 850 characters for tada-3b-ml`). The limit is applied after the gateway strips surrounding quotes and code/markdown artifacts, so it counts the text actually synthesized.

| Model        | Max `input` characters |
| ------------ | ---------------------- |
| `tada-3b-ml` | 850                    |
| `kokoro-82m` | 4096                   |

<Note>
  `tada-3b-ml` has a lower limit because longer inputs can exhaust GPU memory mid-synthesis. The 850-character cap is a deterministic, documented contract that keeps identical requests behaving identically regardless of server load. Split longer text into multiple requests and concatenate the audio client-side.
</Note>

### Voices

`tada-3b-ml` is a voice-cloning model — see the [model page](/models#hume-ai-tada) for how to provide a reference voice; it does not use the preset voice IDs below.

The `kokoro-82m` model exposes eight preset voices through the PolarGrid SDKs:

| Voice ID      | Accent / gender          |
| ------------- | ------------------------ |
| `af_bella`    | American English, female |
| `af_sarah`    | American English, female |
| `am_adam`     | American English, male   |
| `am_michael`  | American English, male   |
| `bf_emma`     | British English, female  |
| `bf_isabella` | British English, female  |
| `bm_george`   | British English, male    |
| `bm_lewis`    | British English, male    |

See the [Voice AI guide](/guides/voice) for how these map to Kokoro-82M and a link to the full upstream voice list.

<Note>
  **TADA output is deterministic.** `tada-3b-ml` uses diffusion-based synthesis with a fixed random seed, so the same `input` text + same `voice` reference always produces byte-identical audio. This is by design -- the fixed seed guarantees consistent voice identity and timing across requests, which matters for voice-agent pipelines where prosody shifts between calls would be jarring. There is no server-side cache: every request runs full inference and is billed accordingly, even when the output matches a previous call. A user-controllable `seed` parameter is planned for a future API version to let callers introduce deliberate prosody variation.
</Note>

### Audio Format

Audio is generated at **24 kHz, 16-bit, mono**. The container is chosen by `response_format`:

| `response_format` | Content-Type | Body                                                        | Streaming?                                                                                 |
| ----------------- | ------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `pcm` *(default)* | `audio/pcm`  | Raw signed 16-bit little-endian samples, no header.         | Yes — chunks stream as they're synthesized.                                                |
| `wav`             | `audio/wav`  | A standard RIFF/WAVE container wrapping the PCM samples.    | No — buffered until synthesis completes (a valid WAV header needs the total sample count). |
| `mp3`             | `audio/mpeg` | MP3 at 128 kbps CBR (encoded server-side via `libmp3lame`). | No — buffered, then encoded in one shot.                                                   |

PCM is the lowest-latency choice and the recommended format for real-time voice-agent pipelines. Pick `wav` if you need a playable file with no client-side post-processing, or `mp3` if bandwidth matters more than first-byte latency.

<Warning>
  **REST and SDK defaults differ.** Raw HTTP requests to `/v1/audio/speech` default to `audio/pcm` (headerless 24 kHz 16-bit LE samples) when `response_format` is omitted. The PolarGrid JavaScript and Python SDKs default batch requests to `mp3` and streaming requests to `opus` for OpenAI-style behavior. If you switch between raw `curl`/HTTP and an SDK without setting `response_format` explicitly, you will get different audio containers. Always pass `response_format` to get deterministic output regardless of calling method.
</Warning>

`opus`, `aac`, and `flac` from the OpenAI spec are **not yet supported** in batch mode — requesting them returns `400`. For streaming, `opus` is supported (see below); for batch, transcode PCM client-side if you need one of those:

```bash theme={null}
ffmpeg -f s16le -ar 24000 -ac 1 -i speech.pcm speech.opus
```

### Streaming

Pass `stream: true` to receive chunked audio over a single HTTP response — first bytes typically arrive in under 300 ms, well before synthesis finishes. The PolarGrid SDKs default streaming requests to `response_format: 'opus'`; raw HTTP callers get `pcm` when `response_format` is omitted (the gateway's lowest-latency default).

**Streamable formats:**

| `response_format` | Content-Type             | Streamable? | Use when                                                       |
| ----------------- | ------------------------ | ----------- | -------------------------------------------------------------- |
| `pcm`             | `audio/pcm`              | ✓ chunked   | Real-time voice-agent pipelines — lowest first-byte latency.   |
| `opus`            | `audio/ogg; codecs=opus` | ✓ chunked   | Bandwidth-constrained clients — server-side encoded at 48 kHz. |
| `wav`             | —                        | ✗           | RIFF header needs the full sample count up front.              |
| `mp3`             | —                        | ✗           | Frame alignment incompatible with sub-300 ms TTFB.             |

Streaming `wav` or `mp3` returns `400 Bad Request`.

**Streamable models:**

| Model        | Streaming?      | Notes                                                                 |
| ------------ | --------------- | --------------------------------------------------------------------- |
| `kokoro-82m` | ✓ `pcm`, `opus` | —                                                                     |
| `tada-3b-ml` | ✓ `pcm`, `opus` | Per-token via decoupled Triton handler. See TADA speed warning below. |

<Warning>
  **TADA streaming: `speed` must be 1.0.** The `tada-3b-ml` model does not support `speed` values other than `1.0` in streaming mode. Setting any other value (e.g., `speed: 1.5`) returns a `400 Bad Request` error from the gateway. If you need speed control with TADA, use batch mode (`stream: false`) instead.
</Warning>

**Response headers:**

* `X-Polargrid-Stream: 1` — set on every streaming response.
* `X-Polargrid-Sample-Rate: 24000` — PCM sample rate; Opus is resampled to 48 kHz inside the Ogg container.

#### Detecting truncated streams

A mid-stream Triton or upstream failure closes the connection cleanly — there is no in-band error frame. Clients observe one of:

* A `ReadError` / `IncompleteRead` / `ChunkedEncodingError` raised by the HTTP client.
* For `opus`, an Ogg stream that never sees the `end-of-stream` flag on its final page.

Treat any of these as a synthesis failure and retry. The PolarGrid SDKs surface these as exceptions from the async iterator; they do not silently terminate.

#### Streaming example

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.yto-01.edge.polargrid.ai/v1/audio/speech \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    --no-buffer \
    -d '{
      "model": "kokoro-82m",
      "input": "Streaming hello from PolarGrid!",
      "voice": "af_bella",
      "response_format": "opus",
      "stream": true
    }' \
    --output stream.ogg
  ```

  ```javascript JavaScript theme={null}
  for await (const chunk of client.textToSpeechStream({
    model: 'kokoro-82m',
    input: 'Streaming hello from PolarGrid!',
    voice: 'af_bella',
    responseFormat: 'opus',
  })) {
    audioPlayer.appendChunk(chunk);
  }
  ```

  ```python Python theme={null}
  async for chunk in client.text_to_speech_stream({
      "model": "kokoro-82m",
      "input": "Streaming hello from PolarGrid!",
      "voice": "af_bella",
      "response_format": "opus",
  }):
      audio_player.append_chunk(chunk)
  ```
</CodeGroup>

<Warning>
  **Not supported in v1:**

  * **Cartesia-compatible WebSocket TTS** (`wss://api.cartesia.ai/tts/websocket` shape). PolarGrid streaming TTS uses chunked HTTP only. Customers porting from Cartesia must swap the transport layer.
  * **WebSocket TTS endpoint of any kind.** There is no `/v1/audio/speech/ws`. PolarGrid exposes WebSockets only for completions (`/v1/completions/ws`) and PersonaPlex's full-duplex voice pipeline.
  * **Streaming WAV and MP3.** WAV requires the full sample count for its RIFF header; MP3 frame alignment can't hit sub-300 ms TTFB. Request `pcm` or `opus` for streaming.

  If your existing pipeline depends on any of the above, file a request in the [PolarGrid roadmap](https://github.com/PolarGrid-AI/polargrid-monorepo/issues).
</Warning>

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  # Edge endpoints accept your pg_* API key as a bearer token — see Authentication
  curl -X POST https://api.yto-01.edge.polargrid.ai/v1/audio/speech \
    -H "Authorization: Bearer pg_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "kokoro-82m",
      "input": "Hello from PolarGrid!",
      "voice": "af_bella",
      "response_format": "wav"
    }' \
    --output speech.wav
  ```

  ```javascript JavaScript theme={null}
  const audioBuffer = await client.textToSpeech({
    model: 'kokoro-82m',
    input: 'Hello from PolarGrid!',
    voice: 'af_bella',
    responseFormat: 'wav',
  });

  // Fully-formed RIFF/WAVE container — playable directly in browsers and
  // most audio libraries with no post-processing.
  import { writeFile } from 'fs/promises';
  await writeFile('speech.wav', Buffer.from(audioBuffer));
  ```

  ```python Python theme={null}
  audio_buffer = await client.text_to_speech({
      "model": "kokoro-82m",
      "input": "Hello from PolarGrid!",
      "voice": "af_bella",
      "response_format": "wav",
  })

  with open("speech.wav", "wb") as f:
      f.write(audio_buffer)
  ```
</CodeGroup>

### Response

Returns the requested container as a binary body, with `Content-Type` set to `audio/pcm`, `audio/wav`, or `audio/mpeg` to match `response_format`.

See [Streaming](#streaming) above for the streaming TTS contract, supported formats and models, and code samples.
