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

  1. Query parameter  wss://tts.girikon.ai/ws/tts?api_key=YOUR_KEY
  2. Authorization header  Authorization: Bearer YOUR_KEY
  3. Custom header  x-api-key: YOUR_KEY
  4. First JSON message  { "api_key": "YOUR_KEY", "text": "...", ... }
Browsers cannot set custom headers on a WebSocket handshake, so for browser clients use the query-parameter or the first-message methods. Server-side clients (Node, Python, curl) can use any method.

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

  1. Open a WebSocket connection to wss://tts.girikon.ai/ws/tts (append ?api_key=... if auth is enabled).
  2. Send exactly one JSON message containing your request (see below).
  3. Receive a stream of JSON events. Audio arrives in chunk_audio events as base64-encoded data.
  4. The stream ends with a done event, 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"
}
FieldTypeDescription
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.

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.

ParamRangeEffect
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.
Overrides only support 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)

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

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