openapi: 3.1.0
info:
  title: Voice Prompt Generator API
  version: 1.1.0
  description: |
    Turns a typed IVR script plus a chosen voice into a telephony-ready audio
    file using ElevenLabs text-to-speech. Built for Broadband Solutions'
    Smart PABX.

    ### Typical flow
    1. `GET /voices` — show the caller a voice picker (each voice includes a
       `preview_url` sample clip you can play directly).
    2. `POST /tts` — send the script and chosen voice. The response contains a
       time-limited `audio_url`; download it and save the file with the
       returned `file_extension` (lowercase) into your prompt library.

    Identical requests are cached: repeating a `POST /tts` with the same text,
    voice, model, format and settings returns instantly with `cached: true`.

    ### Audio formats
    The default `wav_8000` (WAV, PCM 16-bit, mono, 8 kHz) plays natively on
    Asterisk phone systems with no conversion. `GET /formats` lists every
    supported format. Some high-bitrate formats require a higher ElevenLabs
    plan (noted per format); requesting one the plan doesn't include returns
    a 400 error from the upstream provider.
  contact:
    name: Leigh Brunner
    email: leighbrunner@gmail.com

servers:
  - url: https://93i4u23tw7.execute-api.ap-southeast-2.amazonaws.com
    description: Demo environment (Leigh's AWS)
  - url: "{baseUrl}"
    description: Production (Broadband Solutions AWS)
    variables:
      baseUrl:
        default: https://api.example.com

security:
  - ApiKeyAuth: []

tags:
  - name: Voices
  - name: Generation
  - name: Service

paths:
  /health:
    get:
      operationId: getHealth
      summary: Liveness check
      description: No authentication required.
      security: []
      tags: [Service]
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  service: { type: string }
              example: { ok: true, service: voice-prompt-generator }

  /voices:
    get:
      operationId: listVoices
      summary: List available voices
      description: >
        Returns every voice on the connected ElevenLabs account, flattened for
        a picker UI. `preview_url` is a ready-to-play MP3 sample hosted by
        ElevenLabs. The list is cached server-side for one hour.
      tags: [Voices]
      responses:
        "200":
          description: Voice catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  voices:
                    type: array
                    items: { $ref: "#/components/schemas/Voice" }
                  count: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorised" }

  /formats:
    get:
      operationId: listFormats
      summary: List supported output formats
      description: >
        Every `output_format` value accepted by `POST /tts`, with file
        extension, content type, a human label, and the minimum ElevenLabs
        plan where one applies.
      tags: [Voices]
      responses:
        "200":
          description: Format catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  formats:
                    type: array
                    items: { $ref: "#/components/schemas/Format" }
                  count: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorised" }

  /tts:
    post:
      operationId: generateSpeech
      summary: Generate a voice prompt
      description: >
        Converts `text` to speech in the chosen voice and returns a
        time-limited download URL (default 1 hour). Results are cached by
        content: identical requests return instantly with `cached: true`.
      tags: [Generation]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TtsRequest" }
            examples:
              minimal:
                summary: Phone-system WAV with defaults
                value:
                  text: "Welcome to Grand Hotel. For reservations, press one."
                  voice_id: "6wdSVG3CMjPfAthsnMv9"
              customised:
                summary: High-quality MP3 with tuned delivery
                value:
                  text: "Welcome to Grand Hotel. For reservations, press one."
                  voice_id: "6wdSVG3CMjPfAthsnMv9"
                  model_id: eleven_multilingual_v2
                  output_format: mp3_44100_128
                  voice_settings:
                    stability: 0.55
                    similarity_boost: 0.75
                    speed: 0.95
      responses:
        "200":
          description: Prompt generated (or served from cache).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TtsResult" }
        "400":
          description: Validation failed (bad text/voice/model/format/settings).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorised" }
        "404":
          description: Voice id not found on the connected account.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          description: Upstream concurrency limit hit (retried once internally first).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "502":
          description: Upstream (ElevenLabs) failure.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

  responses:
    Unauthorised:
      description: Missing or invalid x-api-key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Voice:
      type: object
      properties:
        id: { type: string, examples: ["6wdSVG3CMjPfAthsnMv9"] }
        name: { type: string, examples: ["Makoto - Japanese male"] }
        description: { type: ["string", "null"] }
        category: { type: ["string", "null"], examples: [professional] }
        gender: { type: ["string", "null"], examples: [male] }
        age: { type: ["string", "null"], examples: [young] }
        accent: { type: ["string", "null"], examples: [kanto] }
        language: { type: ["string", "null"], description: Primary language code., examples: [ja] }
        locale: { type: ["string", "null"], examples: [ja-JP] }
        languages:
          type: array
          items: { type: string }
          description: All language codes this voice is verified for.
        preview_url:
          type: ["string", "null"]
          description: Ready-to-play MP3 sample hosted by ElevenLabs.

    Format:
      type: object
      properties:
        id: { type: string, examples: [wav_8000] }
        label: { type: string, examples: ["WAV 8 kHz (phone system default)"] }
        group: { type: string, enum: [telephony, wav, pcm, mp3, opus] }
        file_extension: { type: string, examples: [".wav"] }
        content_type: { type: string, examples: [audio/wav] }
        min_elevenlabs_plan:
          type: string
          description: Present when the format needs a minimum ElevenLabs plan.
        default: { type: boolean }

    VoiceSettings:
      type: object
      description: >
        Optional delivery/quality tuning, passed through to ElevenLabs.
        Omitted fields use the voice's stored defaults.
      additionalProperties: false
      properties:
        stability:
          type: number
          minimum: 0
          maximum: 1
          description: >
            Consistency of delivery. Low values sound more expressive and vary
            between takes; high values sound steady and even. 0.5 is a sensible
            middle; higher suits phone menus.
        similarity_boost:
          type: number
          minimum: 0
          maximum: 1
          description: >
            How tightly the output adheres to the original voice. Very high
            values can reproduce recording artefacts; lower slightly if output
            sounds distorted.
        style:
          type: number
          minimum: 0
          maximum: 1
          description: >
            Exaggerates the speaker's style. 0 is fastest and most stable;
            raise only for character-heavy reads.
        speed:
          type: number
          minimum: 0.7
          maximum: 1.2
          description: >
            Speaking-rate multiplier. 1.0 is normal; 0.9 to 0.95 works well for
            IVR menus where callers need time to take options in.
        use_speaker_boost:
          type: boolean
          description: >
            Boosts similarity to the original speaker at a small latency cost.
            Usually leave on.

    TtsRequest:
      type: object
      required: [text, voice_id]
      properties:
        text:
          type: string
          maxLength: 1000
          description: The script to speak. Limit configurable server-side (default 1000 chars).
        voice_id:
          type: string
          pattern: "^[A-Za-z0-9]{8,64}$"
          description: A voice id from GET /voices.
        model_id:
          type: string
          enum: [eleven_multilingual_v2, eleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3]
          default: eleven_multilingual_v2
        output_format:
          type: string
          description: >
            Audio container/quality. The telephony group (wav_8000, ulaw_8000,
            alaw_8000, pcm_8000) plays natively on Asterisk phone systems.
            GET /formats returns this same list with labels and plan
            requirements (wav_44100 and pcm_44100 need the ElevenLabs Pro
            plan; mp3_44100_192 needs Creator).
          default: wav_8000
          enum:
            - wav_8000
            - ulaw_8000
            - alaw_8000
            - pcm_8000
            - wav_16000
            - wav_22050
            - wav_24000
            - wav_44100
            - pcm_16000
            - pcm_22050
            - pcm_24000
            - pcm_44100
            - mp3_22050_32
            - mp3_24000_48
            - mp3_44100_32
            - mp3_44100_64
            - mp3_44100_96
            - mp3_44100_128
            - mp3_44100_192
            - opus_48000_32
            - opus_48000_64
            - opus_48000_96
            - opus_48000_128
            - opus_48000_192
        voice_settings: { $ref: "#/components/schemas/VoiceSettings" }

    TtsResult:
      type: object
      properties:
        audio_url:
          type: string
          description: Presigned download URL, valid for one hour by default.
        file_extension: { type: string, examples: [".wav"] }
        content_type: { type: string, examples: [audio/wav] }
        cached:
          type: boolean
          description: True when served from the content cache without regenerating.
        characters: { type: integer }
        voice_id: { type: string }
        model_id: { type: string }
        output_format: { type: string }
        voice_settings:
          oneOf:
            - { $ref: "#/components/schemas/VoiceSettings" }
            - { type: "null" }
        sha256:
          type: string
          description: Content hash; identical inputs always produce the same hash.
        bytes: { type: integer }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              examples: [bad_request]
            message: { type: string }
