Skip to navigation

Speech-to-text

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

Audio format

PropertyValue
EncodingSigned 16-bit little-endian PCM
Sample rate16,000 Hz
ChannelsMono
Frame size20–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.

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:

{"type":"set_server_vad","enabled":true}
{"type":"set_emit_envelope","enabled":true}
OptionWhat it doesRecommendation
set_server_vadDetects utterance boundaries in a continuous stream of short framestrue for microphone, telephony, LiveKit and Pipecat streams
set_emit_envelopeReturns structured JSON with is_final, and enables interim resultstrue 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.

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.

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.

Results

The server returns UTF-8 JSON text messages:

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

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.