openapi: 3.1.0
info:
  title: Gandr TTS API
  version: "1.0"
  description: >-
    Real-time text-to-speech with instant voice cloning. Sub-150ms
    time-to-first-audio, gap-free streaming, multilingual. Full docs:
    https://gandr.ai/docs — machine-readable: /llms.txt.
    WebSocket endpoint (wss://tts.gandr.ai/ws) is documented in llms.txt
    (out of OpenAPI scope).
  contact:
    email: contact@gandr.ai
servers:
  - url: https://tts.gandr.ai
security:
  - apiKey: []
  - bearer: []
components:
  securitySchemes:
    apiKey: {type: apiKey, in: header, name: x-api-key}
    bearer: {type: http, scheme: bearer}
  schemas:
    Voice:
      oneOf:
        - type: object
          required: [mode, id]
          properties:
            mode: {const: id}
            id: {type: string, example: gandr-jenny}
        - type: object
          required: [mode, wav_b64]
          properties:
            mode: {const: clone}
            wav_b64:
              type: string
              description: base64 WAV reference, 5-30s clean speech, <=1.5MB.
    PronunciationEntry:
      type: object
      required: [text, pronunciation]
      properties:
        text: {type: string, example: tchoupitoulas}
        pronunciation: {type: string, example: chop-uh-TOO-liss}
    TTSRequest:
      type: object
      required: [transcript, voice]
      properties:
        transcript: {type: string, maxLength: 2000}
        language: {type: string, example: en}
        voice: {$ref: "#/components/schemas/Voice"}
        output_format:
          type: object
          properties:
            sample_rate: {type: integer, default: 24000, example: 8000}
        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: {$ref: "#/components/schemas/PronunciationEntry"}
        add_timestamps:
          oneOf:
            - {type: boolean}
            - {type: string, enum: [word, char, all]}
          default: false
          description: >-
            SSE and WebSocket — the final SSE event (or final WS JSON line)
            carries word_timestamps ({words, start, end}, seconds from audio
            start), aligned against the rendered audio with no
            streaming-latency cost. "char" or "all" additionally returns
            char_timestamps ({chars, start, end}) at sub-word granularity.
  responses:
    BadRequest: {description: Malformed body — the JSON names the field.}
    Unauthorized: {description: Missing or invalid API key.}
    RateLimited: {description: Per-key rate limit (120 req/min) or monthly quota.}
paths:
  /v1/tts/bytes:
    post:
      summary: One-shot WAV render (quality lane)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/TTSRequest"}
      responses:
        "200":
          description: Rendered audio.
          content:
            audio/wav:
              schema: {type: string, format: binary}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /v1/tts/sse:
    post:
      summary: Streaming synthesis over Server-Sent Events
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/TTSRequest"}
      responses:
        "200":
          description: >-
            text/event-stream. Audio events: data: {"data": "<base64 PCM16LE>"}.
            Final event: data: {"done": true, "ttfa_ms": int, "audio_ms": int}.
          content:
            text/event-stream:
              schema: {type: string}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /v1/voices:
    get:
      summary: List stock voices
      responses:
        "200":
          description: Voice catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  voices:
                    type: array
                    items:
                      type: object
                      properties:
                        id: {type: string, example: gandr-ava}
                        name: {type: string, example: Ava}
                        language: {type: string, example: multilingual}
  /v1/usage:
    get:
      summary: Month-to-date usage for the calling key
      responses:
        "200":
          description: Character count and request totals.
          content:
            application/json:
              schema: {type: object}
        "401": {$ref: "#/components/responses/Unauthorized"}
  /v1/prewarm:
    get:
      summary: Boot a worker in the background (returns instantly)
      responses:
        "200":
          description: '{"status": "warming"}'
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, example: warming}
