asyncapi: 3.0.0
info:
  title: Gandr TTS WebSocket API
  version: "1.0"
  description: >-
    Real-time streaming synthesis over a held WebSocket — lowest-latency path,
    designed for live calls and agents: keep one socket open across a call and
    send one JSON message per utterance. REST endpoints are specified in
    /openapi.yml; full docs at /docs.html and /llms.txt.
servers:
  production:
    host: tts.gandr.ai
    protocol: wss
    description: >-
      Connect to wss://tts.gandr.ai/ws with header
      "Authorization: Bearer gnd_...".
channels:
  ws:
    address: /ws
    messages:
      synthesize:
        $ref: "#/components/messages/Synthesize"
      audioFrame:
        $ref: "#/components/messages/AudioFrame"
      renderStats:
        $ref: "#/components/messages/RenderStats"
operations:
  sendUtterance:
    action: send
    channel: {$ref: "#/channels/ws"}
    summary: One JSON message per utterance (voice_wav_b64 on the first only).
    messages:
      - $ref: "#/channels/ws/messages/synthesize"
  receiveAudio:
    action: receive
    channel: {$ref: "#/channels/ws"}
    summary: >-
      Binary PCM16LE frames (mono, output_sample_rate), then one JSON line of
      render stats per utterance.
    messages:
      - $ref: "#/channels/ws/messages/audioFrame"
      - $ref: "#/channels/ws/messages/renderStats"
components:
  messages:
    Synthesize:
      name: synthesize
      contentType: application/json
      payload:
        type: object
        required: [text]
        properties:
          text: {type: string, maxLength: 2000}
          lang: {type: string, example: en}
          voice_id:
            type: string
            example: gandr-jenny
            description: Stock or previously-cloned voice ID.
          voice_wav_b64:
            type: string
            description: >-
              base64 WAV reference (5-30s clean speech, <=1.5MB). Send on the
              first utterance only; the fingerprint is cached for the call.
          expressiveness: {type: number, minimum: 0.25, maximum: 2.0, default: 0.3, description: "Prosody energy. Default 0.3 (flat, professional). Toward 0.9-1.0 with low cfg_weight gives a measurably livelier read."}
          temperature: {type: number, minimum: 0.1, maximum: 1.2, default: 0.45}
          cfg_weight: {type: number, minimum: 0.2, maximum: 1.0, default: 0.6}
          speed: {type: number, minimum: 0.6, maximum: 1.5, default: 1.0}
          volume: {type: number, minimum: 0.5, maximum: 2.0, default: 1.0}
          pronunciation_dict:
            type: array
            items:
              type: object
              required: [text, pronunciation]
              properties:
                text: {type: string}
                pronunciation: {type: string}
          output_sample_rate: {type: integer, default: 24000, example: 8000}
          add_timestamps:
            oneOf:
              - {type: boolean}
              - {type: string, enum: [word, char, all]}
            default: false
            description: >-
              The final JSON line for this utterance also carries
              word_timestamps ("char" or "all" additionally
              char_timestamps), aligned against the rendered audio with no
              streaming-latency cost.
    AudioFrame:
      name: audioFrame
      summary: Binary PCM16LE audio frame (mono, output_sample_rate).
      contentType: application/octet-stream
      payload: {type: string, format: binary}
    RenderStats:
      name: renderStats
      contentType: application/json
      summary: Final JSON line after each utterance's audio.
      payload:
        type: object
        properties:
          ttfa_ms: {type: integer, description: Server time to first audio.}
          audio_ms: {type: integer, description: Total audio duration emitted.}
          truncated:
            type: boolean
            description: >-
              Present and true only if the stream ended early on a worker
              fault — retry the remainder.
          word_timestamps:
            type: object
            description: >-
              Present when add_timestamps was set on the utterance:
              {words, start, end} — seconds from audio start, aligned
              against the rendered audio.
          char_timestamps:
            type: object
            description: >-
              Present with add_timestamps "char" or "all":
              {chars, start, end} at sub-word granularity.
          word_timestamps_error:
            type: string
            description: >-
              Present instead of timestamps if alignment failed for this
              render; the audio itself is unaffected.
