> This page is for Models.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Text-to-speech

> Send text, receive 24 kHz PCM audio frames as each sentence is synthesized, with natural pauses, cancellation and per-request completion messages.

Connect to `wss://tts.hams.ai/ws` with your API key in the `Authorization` header (see
[Authentication and limits](/models/authentication)). You send prompts as JSON text messages and
receive binary audio frames followed by a JSON completion message. The model speaks Saudi Arabic,
including text that mixes in English.

## Audio format

The server returns raw audio with no WAV or MP3 header. Write the bytes straight to a PCM sink, or
wrap them in a container yourself.

| Property               | Value                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------- |
| Encoding               | Signed 16-bit little-endian PCM                                                       |
| Sample rate            | 24,000 Hz                                                                             |
| Channels               | Mono                                                                                  |
| Audio frame            | 2,048 bytes (1,024 samples, about 43 ms); the last frame of a sentence may be shorter |
| Pause after a sentence | 14,400 bytes of silence (300 ms) after `.`, `؟`, `!`, `?` or `。`                      |
| Pause after a clause   | 7,200 bytes of silence (150 ms) after `،`, `,`, `;`, `:`, brackets or dashes          |

Pauses are sent as plain binary frames between sentences, never after the last one, and only when
the sentence ends in punctuation. Play every binary frame in the order it arrives and the pacing
takes care of itself.

## Sending a prompt

One JSON text message per prompt:

```json
{"type": "prompt", "text": "أهلاً وسهلاً بكم في هامس.", "voice": "yasmeen", "request_id": "req-001"}
```

| Field                       | Required | Description                                                                                                                                          |
| --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                      | yes      | Always `"prompt"`                                                                                                                                    |
| `text`                      | yes      | The text to speak, up to 5,000 characters. It must contain at least one Arabic or Latin letter; whitespace-only or punctuation-only text is rejected |
| `voice`                     | no       | `yasmeen` (default, female), `rima` (female) or `abdulaziz` (male). An unknown name is rejected                                                      |
| `request_id`                | no       | Echoed on the `done` frame so you can match completions to prompts                                                                                   |
| `params.nfe_step`           | no       | Diffusion steps, integer ≥ 1, default `8`. Fewer steps are faster; below `8` the model starts to hallucinate                                         |
| `params.sway_sampling_coef` | no       | Sampling coefficient, default `-1.0`                                                                                                                 |

The server splits the text into sentences and synthesizes them one after another, so the first
audio arrives after the first sentence rather than after the whole text. Prompts queue in order on
the connection: send an assistant's reply sentence by sentence as it is produced and the audio
plays back in sequence. Keep one socket open for the whole session.

## Receiving audio

For each prompt the server sends, in order:

1. The binary audio frames of the first sentence, then a pause frame if the sentence ends in
   punctuation, then the frames of the next sentence, and so on.
2. One `done` text message when the whole prompt has been synthesized:

```json
{
  "type": "done",
  "sample_rate": 24000,
  "model": "hams-ai/habibi-tts-saudi-combined",
  "voice": "yasmeen",
  "chunks": 3,
  "duration": 2.45,
  "rtf": 0.23,
  "request_id": "req-001"
}
```

`chunks` is the number of sentences the text was split into, `duration` the seconds of audio
produced, and `rtf` the real-time factor (synthesis time divided by audio duration; below `1` means
faster than real time). There is no acknowledgement before the audio starts, and audio frames carry
no metadata: a binary message is always audio for the prompt currently being synthesized.

## Cancelling for barge-in

To stop the prompt being synthesized, for example because the caller started speaking, send:

```json
{"type": "cancel"}
```

Synthesis stops at the current sentence, audio not yet sent is dropped, and no `done` is sent for
that prompt. Prompts already queued behind it are still synthesized; cancel again if they should
not play. Sending `cancel` while nothing is in flight does nothing. The connection stays usable.

## Errors

Errors are text messages with the message in `error`, and the `request_id` of the prompt when
there is one:

```json
{"type": "error", "error": "Unknown voice 'xyz'. available: ['yasmeen', 'rima', 'abdulaziz']", "request_id": "req-001"}
```

| `error`                                                                                                             | Cause                                                                                     |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `Invalid request format`                                                                                            | The message was not valid JSON (sent without a `request_id`)                              |
| `Missing or invalid field: type …`                                                                                  | `type` was not `"prompt"`                                                                 |
| `Text must not be empty`                                                                                            | Empty or whitespace-only `text`                                                           |
| `Text exceeds maximum length of 5000 characters`                                                                    | `text` too long                                                                           |
| `Text must contain at least one letter`                                                                             | Punctuation- or digit-only `text`                                                         |
| `Unknown voice '…'`                                                                                                 | `voice` is not one of the three voices                                                    |
| `params must be an object`, `params.nfe_step must be an integer >= 1`, `params.sway_sampling_coef must be a number` | Bad `params`                                                                              |
| `Inference timed out`                                                                                               | A sentence took longer than 30 seconds to synthesize; the rest of the prompt is abandoned |

An error never closes the connection; send the next prompt on the same socket. Handshake failures
and the connection limit are described in [Authentication and limits](/models/authentication).

## Health

`GET https://tts.hams.ai/health` returns the service state, including `active_connections` and
`max_connections`, and a `status` of `ready`, `starting` or `degraded`.

## Python example

Streams one prompt and saves the audio to a raw PCM file (24 kHz, mono, s16le).

```python
import asyncio
import json
import os

import websockets

URL = "wss://tts.hams.ai/ws"
API_KEY = os.environ["HAMS_TTS_API_KEY"]


async def synthesize(text: str, out_path: str, voice: str = "yasmeen") -> None:
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps(
            {"type": "prompt", "text": text, "voice": voice, "request_id": "req-001"},
            ensure_ascii=False,
        ))
        with open(out_path, "wb") as pcm:
            async for message in ws:
                if isinstance(message, bytes):
                    pcm.write(message)  # audio frames and pauses alike
                    continue
                event = json.loads(message)
                if event.get("type") == "error":
                    raise RuntimeError(event["error"])
                if event.get("type") == "done":
                    print("done:", event)
                    break


asyncio.run(synthesize("أهلاً بك، هذا اختبار مباشر للصوت.", "out.pcm"))
```

Play the result with `ffplay -f s16le -ar 24000 -ac 1 out.pcm`.