> 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.

# Speech-to-text

> Stream 16 kHz PCM audio to the recognizer and receive interim and final transcripts as JSON.

Connect to `wss://voice.hams.ai/asr/ws` with your API key in the `Authorization` header (see
[Authentication and limits](/models/authentication)), send audio as binary messages, and read
transcripts back as JSON text messages on the same socket.

## Audio format

| Property    | Value                                                                         |
| ----------- | ----------------------------------------------------------------------------- |
| Encoding    | Signed 16-bit little-endian PCM                                               |
| Sample rate | 16,000 Hz                                                                     |
| Channels    | Mono                                                                          |
| Frame size  | 20–100 ms of audio per binary message; 100 ms (3,200 bytes) is a good default |

The frame size is yours to choose: it is simply how much audio you put in each binary message.
Smaller frames add WebSocket overhead; larger frames add buffering delay on your side before the
audio is sent.

> **Warning**
>
> Do not send WAV headers, MP3, Opus, stereo audio, or audio at another sample rate. Decode and
> resample those formats to headerless 16 kHz mono PCM before sending.

## Session options

Immediately after connecting, send these two text messages:

```json
{"type":"set_server_vad","enabled":true}
{"type":"set_emit_envelope","enabled":true}
```

| Option              | What it does                                                         | Recommendation                                                |
| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------- |
| `set_server_vad`    | Detects utterance boundaries in a continuous stream of short frames  | `true` for microphone, telephony, LiveKit and Pipecat streams |
| `set_emit_envelope` | Returns structured JSON with `is_final`, and enables interim results | `true` for every new integration                              |

Both default to `false` for compatibility with older clients, and both apply only to the
connection they are sent on. With the envelope off, the server sends each final transcript as a
bare text message containing only the transcript, and never sends interim results.

> **Note**
>
> Audio can also be sent as a JSON text message, `{"type":"audio","audio":"<base64 PCM>"}`, which
> is what the HAMS STT SDK does. Binary frames are simpler and smaller; both carry the same 16 kHz
> mono PCM.

#### Continuous stream (server VAD on)

Send 20–100 ms frames back-to-back for as long as the stream lasts, at real-time pace. The
server finds the start and end of each utterance itself and returns results per utterance.
Keep the one socket open for the whole session.

#### Pre-segmented utterances (server VAD off)

Use this only when your application has already split the audio into complete utterances.
Send each utterance as **one** binary message, not as a sequence of short frames, and read the
result for it before sending the next.

## Results

The server returns UTF-8 JSON text messages:

```json
{"type":"Results","is_final":false,"text":"النص الجزئي"}
{"type":"Results","is_final":true,"text":"النص النهائي"}
```

* `is_final: false` is an interim result. It may change as more audio arrives; use it for live
  captions and nothing else.
* `is_final: true` is the completed result for the detected utterance.
* `text` may be empty when no speech was recognized.

Use only final results for records and for anything that triggers a business action.

## Ending a stream

The last utterance is finalized once the server sees silence after it. Before closing, send about
half a second of zero-valued frames, wait for the final result, then close the socket.

## Python example

Streams a headerless 16 kHz mono PCM file in real time and prints interim and final results.

```python
import asyncio
import json
import os

import websockets

URL = "wss://voice.hams.ai/asr/ws"
API_KEY = os.environ["HAMS_STT_API_KEY"]
FRAME_BYTES = 3200  # 100 ms at 16 kHz, mono, 16-bit


async def receive_results(websocket):
    async for message in websocket:
        result = json.loads(message)
        if result.get("type") != "Results":
            continue
        text = result.get("text", "")
        if result.get("is_final"):
            print("final:", text)
        else:
            print("interim:", text)


async def transcribe(path):
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with websockets.connect(URL, additional_headers=headers) as websocket:
        await websocket.send('{"type":"set_server_vad","enabled":true}')
        await websocket.send('{"type":"set_emit_envelope","enabled":true}')

        receiver = asyncio.create_task(receive_results(websocket))

        with open(path, "rb") as audio:
            while frame := audio.read(FRAME_BYTES):
                await websocket.send(frame)
                await asyncio.sleep(len(frame) / 2 / 16000)

        # Trailing silence so the last utterance can be finalized
        for _ in range(5):
            await websocket.send(bytes(FRAME_BYTES))
            await asyncio.sleep(0.1)

        await asyncio.sleep(0.5)
        await websocket.close()
        await receiver


asyncio.run(transcribe("audio.pcm"))
```

To produce `audio.pcm` from any recording: `ffmpeg -i input.wav -f s16le -ar 16000 -ac 1 audio.pcm`.