Skip to navigation

Text-to-speech

Connect to wss://tts.hams.ai/ws with your API key in the Authorization header (see Authentication and limits). 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.

PropertyValue
EncodingSigned 16-bit little-endian PCM
Sample rate24,000 Hz
ChannelsMono
Audio frame2,048 bytes (1,024 samples, about 43 ms); the last frame of a sentence may be shorter
Pause after a sentence14,400 bytes of silence (300 ms) after ., ؟, !, ? or 。
Pause after a clause7,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:

{"type": "prompt", "text": "أهلاً وسهلاً بكم في هامس.", "voice": "yasmeen", "request_id": "req-001"}
FieldRequiredDescription
typeyesAlways "prompt"
textyesThe 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
voicenoyasmeen (default, female), rima (female) or abdulaziz (male). An unknown name is rejected
request_idnoEchoed on the done frame so you can match completions to prompts
params.nfe_stepnoDiffusion steps, integer ≥ 1, default 8. Fewer steps are faster; below 8 the model starts to hallucinate
params.sway_sampling_coefnoSampling 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:
{
"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:

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

{"type": "error", "error": "Unknown voice 'xyz'. available: ['yasmeen', 'rima', 'abdulaziz']", "request_id": "req-001"}
errorCause
Invalid request formatThe message was not valid JSON (sent without a request_id)
Missing or invalid field: type …type was not "prompt"
Text must not be emptyEmpty or whitespace-only text
Text exceeds maximum length of 5000 characterstext too long
Text must contain at least one letterPunctuation- 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 numberBad params
Inference timed outA 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.

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

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.