Overview
Girikon TTS is a low-latency, streaming text-to-speech service. It accepts text via a WebSocket connection, plans emotion-aware prosody per sentence, synthesizes audio with the Koyal engine, and streams the audio back as it is produced so the client can begin playback within a few hundred milliseconds.
Base URL https://tts.girikon.ai
WebSocket WS wss://tts.girikon.ai/ws/tts
Authentication
The WebSocket endpoint can be protected by an API key. Authentication is enabled
on the server when the TTS_API_KEYS environment variable is set
(comma-separated list of valid keys). When enabled, every connection MUST present
a valid key via one of the following methods (checked in this order):
- Query parameter
wss://tts.girikon.ai/ws/tts?api_key=YOUR_KEY - Authorization header
Authorization: Bearer YOUR_KEY - Custom header
x-api-key: YOUR_KEY - First JSON message
{ "api_key": "YOUR_KEY", "text": "...", ... }
If authentication fails, the server replies with:
{ "event": "error", "message": "missing or invalid api key" }
and closes the socket with code 1008 (policy violation).
List Voices
REST GET /voices
Returns the available voices, default voice, and per-voice metadata.
curl https://tts.girikon.ai/voices
{
"model": "koyal-v1",
"default": "ankur",
"voices": [
{
"id": "ankur",
"label": "Ankur",
"folder": "Ankur_HI_EN",
"sample_rate": 22050,
"backend": "cuda"
},
{
"id": "shweta",
"label": "Shweta",
"folder": "Shweta_HI_EN",
"sample_rate": 22050,
"backend": "cuda"
}
]
}
Use the id field as the value for the speaker field
in your WebSocket payload.
Connecting
- Open a WebSocket connection to
wss://tts.girikon.ai/ws/tts(append?api_key=...if auth is enabled). - Send exactly one JSON message containing your request (see below).
- Receive a stream of JSON events.
Audio arrives in
chunk_audioevents as base64-encoded data. - The stream ends with a
doneevent, after which the server closes the socket.
Request Payload
The first (and only) JSON message you send must be a TTS request:
{
"text": "Sir, सबसे पहले तो आपका बहुत-बहुत शुक्रिया कि आपने हमारी website पर inquiry की।",
"lang": "hi",
"model": "koyal-v1",
"speaker": "ankur",
"stream_format": "pcm",
"overrides": { "pitch_bias_st": 0.0, "gain_db": 0.0 },
"abbreviation_config": {},
"lexicon_config": {},
"api_key": "YOUR_KEY"
}
| Field | Type | Description |
|---|---|---|
text required |
string | The text to synthesize. May mix Hindi and English (code-switched). Sentence boundaries are detected automatically. |
lang required |
string | Language code (e.g. "hi", "en"). Drives normalization (numbers, dates, abbreviations). |
model optional |
string | Model family. Defaults to "koyal-v1" (the only one currently deployed). |
speaker optional |
string | Voice id from /voices. Defaults to the model's default voice. |
stream_format optional |
string |
"pcm" (default) — raw 16-bit little-endian mono PCM at 22050 Hz.
A one-time format event is sent before audio chunks."wav" — each chunk is a self-contained WAV file (full sentence buffered).
Higher latency, but easier to play / save.
|
overrides optional |
object | Manual prosody overrides. When present, these replace the planner's per-emotion values for the same fields. See Parameter Reference. |
abbreviation_config optional |
object<string,string> | Custom abbreviation expansions applied after normalization. e.g. { "BHK": "बेडरूम हॉल किचन" }. |
lexicon_config optional |
object<string,string> | Custom word-to-pronunciation overrides (replacement text fed to the phonemizer). |
api_key optional |
string | API key (only required if server has authentication enabled and you didn't supply it via query/header). |
Server Events
Every server message is a JSON object with an event field. You
will receive these in roughly the following order. Audio chunks may interleave
with plan / step_timings events as the pipeline pushes
sentences out as soon as they're synthesized.
- start — pipeline accepted the request. Contains the initial timings dict.
- format — sent once in PCM mode before the first audio chunk.
Fields:
encoding("pcm_s16le"),sample_rate,channels,bits_per_sample. - plan — per-sentence emotion plan. Fields:
sentence_index,chunks(array of{ index, text, emotion, intensity, confidence, params, pause_after_ms }). - chunk_audio — one audio chunk. Fields:
index,sub_index,encoding,audio_base64,emotion,intensity,gap_before_ms,voice.
For PCM mode manychunk_audioevents arrive per sentence (low-latency streaming). For WAV mode, exactly onechunk_audioper sentence containing a full WAV. - step_timings — per-sentence timing breakdown
(
normalize_ms,plan_ms,prep_wait_ms,synth_first_pcm_ms,synth_total_ms, etc.). - latency — overall summary at end of stream. Includes
tts_inference_ms,first_chunk_ms,total_ms, and aper_sentence[]array with finalchunk_params(length_scale, noise_scale, noise_w, gain_db, pitch_bias_st) that were actually used for synthesis. - done — pipeline finished, server is closing the socket.
- error — fatal error.
messagefield describes the cause.
Parameter Reference
The voice's emotional delivery is controlled by five low-level Koyal parameters.
By default the emotion planner chooses these per sentence based on the
detected emotion + intensity, so you don't need to set them. If you want manual
control, supply them in the overrides object.
| Param | Range | Effect |
|---|---|---|
length_scale |
0.85 – 1.25 | Speech pace. Lower = faster. 1.00 is the model's native pace. Excited/urgent emotions drop to ~0.87; sad/thoughtful rise to ~1.10. |
noise_scale |
0.55 – 0.92 | Prosody / pitch contour variability. Biggest knob for naturalness. Low (0.6) = flat/monotone (sad, calm). High (0.85+) = expressive, melodic (excited, surprised). 0.70 is a balanced neutral. |
noise_w |
0.65 – 0.92 | Phoneme-duration jitter. Adds human-like micro-timing irregularity. ~0.80 is natural; below 0.70 sounds metronomic. |
pitch_bias_st |
−1.5 – +1.5 semitones | Post-synthesis pitch shift (applied only in WAV mode). Positive = higher (excited, questioning); negative = lower (serious, sad). Beyond ±1.5 ST coloration becomes audible. |
gain_db |
−4 – +4 dB | Output loudness adjustment. Applied inline on PCM frames and in the WAV file. Use sparingly; emotional dynamics rarely need more than ±3 dB. |
pitch_bias_st and gain_db today.
To bypass the planner entirely you can set "intensity": 0 in your
own emotion-planning workflow — but the recommended path is to let the planner
drive the voice and use overrides only for global volume / pitch trim.
Other server-side knobs (not client-controllable)
- Inter-chunk pauses — scaled per emotion (excited × 0.6, sad × 1.5, etc.).
- End-of-sentence breath silence — extra pause inserted after each sentence (sad ≈ 520 ms, excited ≈ 160 ms).
- Sentence chunking — long inputs are split at sentence terminators (
. ! ? ।). - Bilingual Devanagari workaround — automatic; the trailing
।is replaced with a stronger prosodic break so short Hindi words after English context aren't dropped.
Supported Emotions
The emotion planner labels each sentence with one of the following. You don't pass these directly — they are inferred from the text. Each maps to a tuned preset (pace + prosody + pitch + gain + pause).
neutral happy excited surprised sad angry fear disgust calm announcement curious serious thoughtful confident friendly empathetic urgent questioning
JavaScript Example (browser)
const API_KEY = "YOUR_KEY";
const ws = new WebSocket(`wss://tts.girikon.ai/ws/tts?api_key=${API_KEY}`);
ws.onopen = () => {
ws.send(JSON.stringify({
text: "Sir, सबसे पहले तो आपका बहुत-बहुत शुक्रिया कि आपने inquiry की।",
lang: "hi",
speaker: "ankur",
stream_format: "pcm",
}));
};
let sampleRate = 22050;
const pcmQueue = [];
ws.onmessage = (msg) => {
const evt = JSON.parse(msg.data);
switch (evt.event) {
case "format":
sampleRate = evt.sample_rate;
break;
case "chunk_audio":
const bytes = Uint8Array.from(atob(evt.audio_base64), c => c.charCodeAt(0));
const pcm = new Int16Array(bytes.buffer);
pcmQueue.push(pcm);
// ...feed into AudioContext / AudioWorklet for playback...
break;
case "latency":
console.log("latency summary", evt);
break;
case "done":
console.log("stream complete");
break;
case "error":
console.error("server error:", evt.message);
break;
}
};
Python Example
import asyncio, json, base64, wave, websockets
API_KEY = "YOUR_KEY"
URL = f"wss://tts.girikon.ai/ws/tts?api_key={API_KEY}"
async def main():
async with websockets.connect(URL) as ws:
await ws.send(json.dumps({
"text": "Sir, सबसे पहले तो आपका बहुत-बहुत शुक्रिया कि आपने inquiry की।",
"lang": "hi",
"speaker": "ankur",
"stream_format": "pcm",
}))
pcm_parts, sr = [], 22050
async for raw in ws:
evt = json.loads(raw)
if evt["event"] == "format":
sr = evt["sample_rate"]
elif evt["event"] == "chunk_audio":
pcm_parts.append(base64.b64decode(evt["audio_base64"]))
elif evt["event"] == "done":
break
elif evt["event"] == "error":
raise RuntimeError(evt["message"])
with wave.open("out.wav", "wb") as w:
w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
w.writeframes(b"".join(pcm_parts))
asyncio.run(main())
Errors
| Cause | Behavior |
|---|---|
| Invalid or missing API key | {"event":"error","message":"missing or invalid api key"}, then close code 1008. |
| Malformed JSON payload | {"event":"error","message":"..."}, then close. |
| Unknown voice / model | Server falls back to the default voice and continues. |
| Internal synthesis error | {"event":"error","message":"..."}, then close. |
© Girikon — TTS Streaming API. For support, contact your account manager.