openapi: 3.1.0
info:
  title: Jutusta Speech API
  version: 1.0.0
  summary: ElevenLabs-compatible text-to-speech and speech-to-text for Estonian-first models.
  description: "An ElevenLabs-compatible HTTP API for the Jutusta speech stack. Covers text-to-speech (synchronous + streaming), voice clones, and speech-to-text in two flavours: a synchronous `POST /speech-to-text` (≤ 100 MB inline) and the async recognition flow — presigned upload via `POST /recognition/uploads` → R2 PUT → `POST /recognition/jobs/{job_id}/start` → poll `GET /recognition/jobs/{job_id}` (or have the result POSTed to a registered webhook). Most ElevenLabs SDKs work by pointing `baseUrl` at this service. See `x-compatibility` for known deviations."
  contact:
    name: Jutusta
    url: "https://jutusta.ee"
servers:
  -
    url: "https://staging.jutusta.ee/api/v1"
    description: This deployment
x-compatibility:
  emulates: api.elevenlabs.io/v1
  deviations:
    -
      field: output_format
      note: "Only `wav`, `pcm_16000`, `pcm_22050`, `pcm_24000`, `pcm_44100` are honoured. MP3 variants return 415."
    -
      field: voice_settings
      note: "Only `speed` is mapped. `stability`, `similarity_boost`, `style`, `use_speaker_boost` are accepted for compatibility and ignored."
    -
      field: previous_text / next_text
      note: "Accepted for ElevenLabs SDK compatibility and ignored. Voice continuity across sequential requests is not implemented today — each request is synthesized independently. The preset voice id + its hardcoded seed are the only consistency mechanism; see the `seed` field."
    -
      field: model_id
      note: "Only `jutusta_v1` is recognised for TTS. Other values are accepted and treated as `jutusta_v1`."
    -
      field: text-to-speech/stream-input
      note: "The WebSocket `stream-input` endpoint emits RAW PCM (s16le, 24 kHz, mono) as base64 strings — there is no WAV header on the wire and `output_format` is ignored. Voice clones are not yet accepted on the WS endpoint (the sidecar Worker has no R2/D1 bindings); clone ids return 404 with a pointer to `POST /text-to-speech/{voice_id}`. Idle sockets are closed after 60 s with code 1011."
    -
      field: speech-to-text/file
      note: "Inline `file` uploads are capped at 100 MB. Larger files must use the deployment-specific `/api/v1/recognition/uploads` presigned flow."
    -
      field: speech-to-text/model_id
      note: "Any non-empty model id is accepted; the engine is the Jutusta ASR model. `scribe_v1`/`scribe_v2` values are accepted for SDK compatibility."
    -
      field: speech-to-text/timestamps_granularity
      note: "`word` (default) and `none` are honoured. `character` falls back to `word`."
    -
      field: speech-to-text/additional_formats
      note: "Honoured for `srt`, `txt` and `segmented_json` — generated files are returned in the response `additional_formats` array (and on the webhook payload). `docx`, `html` and `pdf` are accepted-and-skipped. The numeric segmentation knobs (`max_characters_per_line`, `max_segment_chars`, `max_segment_duration_s`, `segment_on_silence_longer_than_s`) are ignored; the subtitle chunker uses its own heuristic. `srt` always carries timecodes and no speaker labels; `segmented_json` follows Jutusta's segments schema."
    -
      field: speech-to-text/ignored-fields
      note: tag_audio_events, num_speakers, temperature, seed, entity_detection, entity_redaction, entity_redaction_mode, no_verbatim, detect_speaker_roles, keyterms, use_multi_channel, file_format, enable_logging — all accepted for SDK compatibility and ignored.
    -
      field: speech-to-text/webhook-delivery
      note: "Webhooks are delivered on a best-effort basis from the Worker that handled the request. For long-running transcriptions where the request Worker is recycled before completion, delivery may be deferred until a subsequent `GET /recognition/jobs/{id}` poll observes the terminal transition, or until the cron-driven `webhook-poller` worker picks the row up on its next sweep."
    -
      field: speech-to-text/language_probability
      note: "The Jutusta ASR backend does not emit a calibrated language-detection confidence today, so `language_probability` is reported as `1` when a language was detected and `0` when `language_code` is `unknown`. Treat the value as a coarse \"detected/not-detected\" flag, not a real probability."
security:
  -
    XiApiKey: []
tags:
  -
    name: voices
    description: List and inspect available voices.
  -
    name: text-to-speech
    description: Synthesize speech from text.
  -
    name: speech-to-text
    description: Transcribe audio and video files.
  -
    name: recognition
    description: "Async transcription jobs: presigned R2 upload, start, list, and poll. Use this flow for files larger than the 100 MB inline cap on `/speech-to-text`, or any time you want progress reporting."
  -
    name: webhooks
    description: Manage webhook subscriptions used for async transcriptions.
  -
    name: summaries
    description: LLM-generated meeting summaries for finished transcription jobs. Templates are catalog-defined; callers pick one per request or rely on their per-user default.
  -
    name: translate
    description: Append-only chunk translation. Each call returns text to concatenate to what the caller has rendered so far — earlier output is never rewritten.
  -
    name: models
    description: Available TTS models.
paths:
  /voices:
    get:
      tags:
        - voices
      summary: List voices
      operationId: listVoices
      description: "Returns all preset voices plus any voice clones owned by the caller. Cloned voices are marked with `category: \"cloned\"`."
      responses:
        200:
          description: Available voices for the caller.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoicesList"
        401:
          $ref: "#/components/responses/Unauthorized"
  /voices/add:
    post:
      tags:
        - voices
      summary: Create a voice clone
      operationId: addVoice
      description: "Creates a voice clone owned by the API key holder. Accepts the audio file under the standard ElevenLabs `files` field; the deployment-specific `audio` field is also accepted. Send one sample of 6–30 seconds of clean speech. The returned `voice_id` can be used directly with `/text-to-speech/{voice_id}`."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/AddVoiceRequest"
            encoding:
              files:
                contentType: "audio/*"
              audio:
                contentType: "audio/*"
      responses:
        200:
          description: Voice clone created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddVoiceResponse"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        409:
          $ref: "#/components/responses/Conflict"
        413:
          $ref: "#/components/responses/PayloadTooLarge"
        502:
          $ref: "#/components/responses/UpstreamError"
  /voices/{voice_id}:
    parameters:
      -
        $ref: "#/components/parameters/VoiceId"
    get:
      tags:
        - voices
      summary: Get voice
      operationId: getVoice
      responses:
        200:
          description: Single voice.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Voice"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
  /text-to-speech/{voice_id}:
    parameters:
      -
        $ref: "#/components/parameters/VoiceId"
      -
        $ref: "#/components/parameters/OutputFormat"
    post:
      tags:
        - text-to-speech
      summary: Synthesize speech
      operationId: textToSpeech
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TextToSpeechRequest"
      responses:
        200:
          description: Synthesized audio bytes.
          headers:
            x-audio-duration-ms:
              description: Approximate duration of the returned audio in milliseconds.
              schema:
                type: string
            x-audio-sample-rate:
              description: Sample rate of the returned audio.
              schema:
                type: string
          content:
            audio/wav:
              schema:
                type: string
                format: binary
            audio/basic:
              schema:
                type: string
                format: binary
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        415:
          $ref: "#/components/responses/UnsupportedMediaType"
        502:
          $ref: "#/components/responses/UpstreamError"
  /text-to-speech/{voice_id}/stream:
    parameters:
      -
        $ref: "#/components/parameters/VoiceId"
      -
        $ref: "#/components/parameters/OutputFormat"
    post:
      tags:
        - text-to-speech
      summary: Synthesize speech (streamed)
      description: "Same request contract as `POST /text-to-speech/{voice_id}` with one extra body field: `sentence_chunking` (boolean, default `true`). When sentence chunking is on, the input is split into sentence-sized segments and each segment is synthesized independently. The Worker stitches the resulting PCM into a single streaming WAV, so playback can begin after the first segment is generated instead of after the entire text. Pass `sentence_chunking: false` to fall back to a single synth call (same engine as the non-stream endpoint, with chunked transfer for the wire). The response sets `x-streaming-mode: sentence` and `x-sentence-count: <n>` when chunking is in effect, and the WAV header carries placeholder length fields (0xFFFFFFFF) because total duration is not known up-front.\n\n**Mid-stream failures.** Errors that occur on the first segment are returned with the usual HTTP status (4xx/5xx) before any audio is sent. Once the response has started, the only signal we can give is closing the stream early: a transient upstream failure on segment N is logged server-side and the connection is closed after segment N-1. Robust clients should compare the WAV byte count (or decoded duration) against the expected length and treat a short read as a partial result."
      operationId: textToSpeechStream
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TextToSpeechRequest"
      responses:
        200:
          description: Streamed audio bytes.
          headers:
            x-streaming-mode:
              description: "`sentence` when chunking is active, `single` when a single synth was returned."
              schema:
                type: string
                enum:
                  - sentence
                  - single
            x-sentence-count:
              description: "Number of sentence-level segments concatenated into the response (only present in `sentence` mode)."
              schema:
                type: string
            x-audio-sample-rate:
              description: Sample rate of the returned audio.
              schema:
                type: string
          content:
            audio/wav:
              schema:
                type: string
                format: binary
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        415:
          $ref: "#/components/responses/UnsupportedMediaType"
        502:
          $ref: "#/components/responses/UpstreamError"
  /text-to-speech/{voice_id}/stream-input:
    parameters:
      -
        $ref: "#/components/parameters/VoiceId"
      -
        $ref: "#/components/parameters/StreamInputApiKeyQuery"
      -
        $ref: "#/components/parameters/StreamInputLanguageQuery"
      -
        $ref: "#/components/parameters/StreamInputLanguageHeader"
    get:
      tags:
        - text-to-speech
      summary: Synthesize speech (WebSocket stream-input)
      operationId: textToSpeechStreamInput
      description: "WebSocket endpoint that mirrors ElevenLabs `stream-input`. Open the connection with `wss://…/api/v1/text-to-speech/{voice_id}/stream-input`, then push JSON text frames as your LLM emits tokens; the server returns base64-encoded PCM audio frames. Voice continuity across chunks is handled server-side: when the spoken language is Estonian, the precomputed JVC1 anchor bound to the preset id keeps identity stable; for any other language (set via the `?language=` query parameter or `X-Speak-Language` header) the preset's stored (seed, instruct) pair drives a fresh generative render so the Estonian audition timbre doesn't leak through. Either way the client never carries voice tokens.\n\n**Authentication.** Send `Authorization: Bearer sk_jt_…` or `xi-api-key: sk_jt_…` on the upgrade request, OR pass `?xi_api_key=sk_jt_…` on the URL (browsers cannot set custom headers on a `WebSocket()` upgrade). URL-embedded keys appear in CF Worker access logs — server-side clients should always use the header.\n\n**Client → server frames** (JSON text):\n- `{\"text\": \" \"}` — init (literal single space). Voice metadata is already resolved server-side from the path.\n- `{\"text\": \"Hello \"}` — append to the synth buffer. Synthesized when a sentence boundary is reached and the buffer is ≥ 40 chars, or unconditionally at 240 chars.\n- `{\"text\": \"...\", \"try_trigger_generation\": true}` — synth the current buffer immediately. Use sparingly; mid-clause splits hurt prosody.\n- `{\"text\": \"...\", \"voice_settings\": {\"speed\": 1.1}}` — `speed` is honored on any frame and clamped to [0.5, 2]. Other `voice_settings` fields are accepted and ignored (see `x-compatibility`).\n- `{\"flush\": true}` (with or without `text`) — force-synth the buffered text immediately and keep the session open. Use this at the end of each LLM response in a voice-agent loop; the socket survives for the next response.\n- `{\"text\": \"\"}` — end of stream: flush the remaining buffer, emit `{audio:null,isFinal:true}`, and close cleanly with code 1000.\n\n**Server → client frames** (JSON text):\n- `{\"audio\": \"<base64 PCM s16le 24 kHz mono>\", \"isFinal\": false}` — one per synthesized chunk. Audio is RAW PCM with no WAV header; decode by reading int16 little-endian samples at 24000 Hz.\n- `{\"audio\": null, \"flushDone\": true}` — sent once a `flush` has fully drained its audio. Marks the end of that flushed segment; the session stays open for the next response. Older clients that only read `audio`/`isFinal` can ignore it.\n- `{\"audio\": null, \"isFinal\": true}` — sent right before a clean close (1000), only on end of stream (`{\"text\": \"\"}`).\n- `{\"error\": {\"code\": \"...\", \"message\": \"...\"}}` — sent before a 1011 close when the upstream synth fails mid-session.\n\n**Voice clones are not yet supported on this endpoint** — clone ids return 404 with a pointer to `POST /text-to-speech/{voice_id}`. This is a deployment limitation (the WS sidecar Worker has no R2/D1 bindings) and will be lifted in a later release.\n\n**Idle timeout.** The server closes the socket with code 1011 after 60 seconds with no inbound frame. Voice-agent clients pace by LLM token output, so longer gaps mean the client is gone."
      x-websocket: true
      x-message-schemas:
        client:
          $ref: "#/components/schemas/StreamInputClientFrame"
        serverAudio:
          $ref: "#/components/schemas/StreamInputAudioFrame"
        serverError:
          $ref: "#/components/schemas/StreamInputErrorFrame"
      responses:
        101:
          description: Switching Protocols — WebSocket upgrade succeeded. See the description for the frame protocol.
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          description: Unknown preset voice id, or a voice-clone id (clones are not yet supported on the WS endpoint).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        503:
          description: WebSocket sidecar (TTS_SESSION binding) not configured on this deployment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /speech-to-text:
    post:
      tags:
        - speech-to-text
      summary: Transcribe an audio or video file
      operationId: speechToText
      description: "Transcribe an audio/video file uploaded inline or referenced by an https URL. Returns the transcript synchronously by default. Pass `webhook=true` together with a previously-registered `webhook_id` to have the transcript delivered to your endpoint instead — the immediate response then contains only `task_id`/`transcription_id`. Inline files are capped at 100 MB; for larger inputs use the `/recognition/uploads` presigned-PUT flow."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/SpeechToTextRequest"
            encoding:
              file:
                contentType: "audio/*, video/*"
      responses:
        200:
          description: Synchronous transcription result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SpeechToTextResponse"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        413:
          $ref: "#/components/responses/PayloadTooLarge"
        415:
          $ref: "#/components/responses/UnsupportedMediaType"
        502:
          $ref: "#/components/responses/UpstreamError"
  /recognition/uploads:
    post:
      tags:
        - recognition
      summary: Create a presigned upload for a transcription job
      operationId: createRecognitionUpload
      description: "Reserves a job row and returns a presigned R2 `PUT` URL that the client uses to upload the source file directly (30-minute expiry). On a successful PUT, call `POST /recognition/jobs/{job_id}/start` to enqueue transcription. Anonymous callers are allowed up to 25 MB; authed callers go up to the deployment cap."
      security:
        -
{}
        -
          XiApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUploadRequest"
      responses:
        201:
          description: Job row created and presigned PUT URL issued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateUploadResponse"
        400:
          $ref: "#/components/responses/BadRequest"
        413:
          $ref: "#/components/responses/PayloadTooLarge"
        415:
          $ref: "#/components/responses/UnsupportedMediaType"
  /recognition/jobs:
    get:
      tags:
        - recognition
      summary: List recent transcription jobs
      operationId: listRecognitionJobs
      description: Returns recent jobs owned by the caller. Most-recent first. Requires authentication.
      parameters:
        -
          name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        200:
          description: Recognition job summaries.
          content:
            application/json:
              schema:
                type: object
                required:
                  - jobs
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/RecognitionJobSummary"
        401:
          $ref: "#/components/responses/Unauthorized"
  /recognition/jobs/{job_id}:
    parameters:
      -
        $ref: "#/components/parameters/JobId"
    get:
      tags:
        - recognition
      summary: Get a transcription job
      operationId: getRecognitionJob
      description: "Returns the current state of a transcription job. Poll this until `state` is one of `succeeded`, `failed`, or `cancelled`. On `succeeded`, `result` carries the transcript; on `failed`, `error` is populated."
      security:
        -
{}
        -
          XiApiKey: []
      responses:
        200:
          description: Job status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecognitionJobStatus"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
  /recognition/jobs/{job_id}/start:
    parameters:
      -
        $ref: "#/components/parameters/JobId"
    post:
      tags:
        - recognition
      summary: Start a transcription job after upload
      operationId: startRecognitionJob
      description: "Call after the presigned R2 PUT has landed. Enqueues the job and transitions it from `uploading` to `pending`. Returns immediately; poll `GET /recognition/jobs/{job_id}` for progress."
      security:
        -
{}
        -
          XiApiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartJobRequest"
      responses:
        202:
          description: Job enqueued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartJobResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          $ref: "#/components/responses/Conflict"
  /recognition/speakers:
    get:
      tags:
        - recognition
      summary: List saved speakers
      operationId: listSpeakers
      description: "Returns the caller’s saved speaker library — voiceprints enrolled from earlier diarized transcriptions and matched automatically when `speaker_recognition` is requested."
      responses:
        200:
          description: Saved speakers.
          content:
            application/json:
              schema:
                type: object
                required:
                  - speakers
                properties:
                  speakers:
                    type: array
                    items:
                      $ref: "#/components/schemas/SpeakerProfile"
        401:
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - recognition
      summary: Save a speaker from a finished job
      operationId: enrollSpeaker
      description: "Enrolls a voiceprint from a succeeded diarized job that ran with speaker recognition enabled. `speakerLabel` is the raw diarization label (e.g. `SPEAKER_00`). The voiceprint is biometric data: it is stored only on this explicit call and can be deleted at any time via `DELETE /recognition/speakers/{id}`. No audio is stored or re-processed."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - jobId
                - speakerLabel
                - name
              properties:
                jobId:
                  type: string
                speakerLabel:
                  type: string
                  examples:
                    - SPEAKER_00
                name:
                  type: string
                  maxLength: 200
      responses:
        201:
          description: Speaker saved.
          content:
            application/json:
              schema:
                type: object
                required:
                  - speaker
                properties:
                  speaker:
                    $ref: "#/components/schemas/SpeakerProfile"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          $ref: "#/components/responses/Conflict"
  /recognition/speakers/{id}:
    parameters:
      -
        name: id
        in: path
        required: true
        schema:
          type: string
        description: Saved speaker id.
    patch:
      tags:
        - recognition
      summary: Rename a saved speaker
      operationId: renameSpeaker
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 200
      responses:
        200:
          description: Speaker renamed.
          content:
            application/json:
              schema:
                type: object
                required:
                  - speaker
                properties:
                  speaker:
                    $ref: "#/components/schemas/SpeakerProfile"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
    delete:
      tags:
        - recognition
      summary: Delete a saved speaker
      operationId: deleteSpeaker
      description: Permanently deletes the voiceprint (hard delete of the biometric data). Future transcriptions will no longer recognise this speaker.
      responses:
        200:
          description: Speaker deleted.
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
  /webhooks:
    get:
      tags:
        - webhooks
      summary: List webhook subscriptions
      operationId: listWebhooks
      responses:
        200:
          description: Active webhook subscriptions for the caller.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhooksList"
        401:
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - webhooks
      summary: Create a webhook subscription
      operationId: createWebhook
      description: "Registers a webhook endpoint. The response includes the signing `secret` exactly once — store it server-side and use it to verify the `ElevenLabs-Signature` header on incoming deliveries."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookCreateRequest"
      responses:
        201:
          description: Subscription created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookCreated"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
  /webhooks/{id}:
    parameters:
      -
        name: id
        in: path
        required: true
        schema:
          type: string
        description: "Webhook subscription id (returned at creation as `webhook_id`)."
    delete:
      tags:
        - webhooks
      summary: Revoke a webhook subscription
      operationId: deleteWebhook
      responses:
        200:
          description: Subscription revoked.
          content:
            application/json:
              schema:
                type: object
                required:
                  - webhook_id
                  - revoked
                properties:
                  webhook_id:
                    type: string
                  revoked:
                    type: boolean
                    enum:
                      - true
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
  /recognition/jobs/{job_id}/summary:
    parameters:
      -
        $ref: "#/components/parameters/JobId"
    get:
      tags:
        - summaries
      summary: List summaries for a job
      operationId: listJobSummaries
      description: Returns every summary generated for the job so far, oldest first. A job can carry multiple summaries — typically one per template the caller has run.
      security:
        -
{}
        -
          XiApiKey: []
      responses:
        200:
          description: Existing summaries for the job.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SummariesListResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - summaries
      summary: Generate a summary
      operationId: generateJobSummary
      description: "Generates a new summary for a finished transcription job using the chosen template. If `templateId` is omitted the caller’s default template is used (see `GET /summary-templates`). The job must be in `state: \"succeeded\"`. Each call creates a fresh summary row — there is no implicit dedup, so it is the caller’s responsibility to avoid duplicate work if regenerating with the same template."
      security:
        -
{}
        -
          XiApiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SummaryGenerateRequest"
      responses:
        200:
          description: Summary generated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SummaryResponse"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          $ref: "#/components/responses/Conflict"
        502:
          $ref: "#/components/responses/UpstreamError"
  /summary-templates:
    get:
      tags:
        - summaries
      summary: List summary templates
      operationId: listSummaryTemplates
      description: Returns the catalog of summary templates plus the caller’s default. Templates are catalog-defined — there is no create or delete here.
      security:
        -
{}
        -
          XiApiKey: []
      responses:
        200:
          description: Template catalog and caller’s default.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplatesCatalogResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
    put:
      tags:
        - summaries
      summary: Set default summary template
      operationId: setDefaultSummaryTemplate
      description: "Sets the caller’s preferred default template. Future `POST /recognition/jobs/{job_id}/summary` calls without a `templateId` will use this template."
      security:
        -
{}
        -
          XiApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetDefaultTemplateRequest"
      responses:
        200:
          description: Updated catalog with new default.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplatesCatalogResponse"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
  /translate/chunk:
    post:
      tags:
        - translate
      summary: Translate a transcript chunk
      operationId: translateChunk
      description: "Translates the `new_source` chunk from `source` to `target`, given the tail of the translation rendered so far as `previous` context. The response carries `appended` — text the caller concatenates to whatever it has already shown. Earlier translation is never rewritten; the model is instructed not to repeat or revise it. Useful for live transcripts where ASR text grows in place. Same-language requests short-circuit and echo `new_source`.\n\n**Streaming.** Send `Accept: text/event-stream` (or `?stream=true`) to receive translation as a token-by-token SSE stream instead of waiting for the full chunk. The wire format is:\n\n```\nevent: delta\ndata: {\"appended\":\"Hello\"}\n\nevent: delta\ndata: {\"appended\":\", how are you?\"}\n\nevent: done\ndata: {\"model\":\"@cf/mistralai/mistral-small-3.1-24b-instruct\",\"source\":\"et\",\"target\":\"en\"}\n```\n\nConcatenate `appended` from each `delta` frame to render translation live. `event: done` terminates the stream cleanly. On model failure or rate-limit, the server emits one `event: error\\ndata: {\"code\":\"…\",\"message\":\"…\"}` frame before closing. The default (JSON) response shape is unchanged."
      security:
        -
{}
        -
          XiApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TranslateChunkRequest"
      responses:
        200:
          description: "Translated chunk to append. JSON by default; `text/event-stream` when negotiated."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranslateChunkResponse"
            text/event-stream:
              schema:
                type: string
                description: "Server-Sent Events stream of `delta` / `done` / `error` frames. Each `delta` frame carries `{ \"appended\": \"<text>\" }`; concatenate across frames. The terminal `done` frame carries `{ \"model\", \"source\", \"target\" }`."
                example: "event: delta\ndata: {\"appended\":\"Hello\"}\n\nevent: delta\ndata: {\"appended\":\", world.\"}\n\nevent: done\ndata: {\"model\":\"@cf/mistralai/mistral-small-3.1-24b-instruct\",\"source\":\"et\",\"target\":\"en\"}\n\n"
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        502:
          $ref: "#/components/responses/UpstreamError"
  /models:
    get:
      tags:
        - models
      summary: List models
      operationId: listModels
      security: []
      responses:
        200:
          description: Available models.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelsList"
components:
  securitySchemes:
    XiApiKey:
      type: apiKey
      in: header
      name: xi-api-key
      description: "Personal API key issued at `/app/developers`. Also accepted as `Authorization: Bearer sk_jt_…`."
  parameters:
    VoiceId:
      name: voice_id
      in: path
      required: true
      description: "Voice identifier. Pick a preset id from the dropdown — or pass a voice clone id you created via `POST /voices/add` (clone ids look like `vc_xxx`). The full preset list is also available at `GET /voices`."
      schema:
        type: string
        enum:
          - mari
          - liina
          - kadri
          - tiina
          - eve
          - annika
          - katrin
          - kart
          - piret
          - jaan
          - mart
          - andres
          - priit
          - tarmo
          - rein
          - urmas
          - tonu
          - marko
          - raivo
          - kaur
          - siim
          - taavi
          - ott
          - henri
          - margus
          - sander
          - toomas
          - indrek
          - madis
          - kalev
          - tuuli
          - heli
          - triin
          - maarja
          - linda
          - karin
          - anneli
          - karim-ar
          - aytac-az
          - stoyan-bg
          - edin-bs
          - jordi-ca
          - jan-cs
          - mikkel-da
          - maximilian-de
          - nikos-el
          - oliver-en
          - mateo-es
          - kristjan-et
          - tapio-fi
          - antoine-fr
          - brais-gl
          - yonatan-he
          - arjun-hi
          - ivan-hr
          - bence-hu
          - budi-id
          - lorenzo-it
          - hiroshi-ja
          - minjun-ko
          - justas-lt
          - janis-lv
          - goran-mk
          - ahmad-ms
          - daan-nl
          - erik-no
          - piotr-pl
          - joao-pt
          - andrei-ro
          - aleksei-ru
          - tomas-sk
          - luka-sl
          - nikola-sr
          - lars-sv
          - arun-ta
          - somchai-th
          - mehmet-tr
          - taras-uk
          - faisal-ur
          - minh-vi
          - wei-zh
          - james-en
          - william-en
          - henry-en
          - edward-en
          - charles-en
          - thomas-en
          - sophie-en
          - emily-en
          - hannah-en
          - layla-ar
          - aysel-az
          - maria-bg
          - selma-bs
          - nuria-ca
          - klara-cs
          - freja-da
          - hanna-de
          - maria-el
          - lucia-es
          - sini-fi
          - elise-fr
          - tamar-he
          - ananya-hi
          - ana-hr
          - eszter-hu
          - dewi-id
          - sakura-ja
          - soyeon-ko
          - egle-lt
          - elina-lv
          - marija-mk
          - nur-ms
          - eva-nl
          - ingrid-no
          - aleksandra-pl
          - ines-pt
          - ioana-ro
          - anastasia-ru
          - eva-sk
          - maja-sl
          - jelena-sr
          - linnea-sv
          - anjali-ta
          - suda-th
          - aylin-tr
          - iryna-uk
          - aisha-ur
          - mai-vi
          - lin-zh
          - mikhail-ru
          - bohdan-uk
          - klaus-de
          - juhani-fi
          - dmitri-ru
          - andriy-uk
          - hans-de
          - mikko-fi
          - olga-ru
          - oksana-uk
          - anna-de
          - helmi-fi
          - yulia-uk
          - lena-de
          - riikka-fi
          - tatyana-ru
          - daria-uk
          - greta-de
          - pirjo-fi
        default: mari
        example: mari
      example: mari
    OutputFormat:
      name: output_format
      in: query
      required: false
      description: Requested audio format. Only wav / pcm variants are honoured today; mp3 variants return 415.
      schema:
        type: string
        enum:
          - wav
          - pcm_16000
          - pcm_22050
          - pcm_24000
          - pcm_44100
    JobId:
      name: job_id
      in: path
      required: true
      description: "Recognition job id returned by `POST /recognition/uploads`."
      schema:
        type: string
    StreamInputApiKeyQuery:
      name: xi_api_key
      in: query
      required: false
      description: "Fallback API-key transport for the WebSocket `stream-input` endpoint. Browsers cannot set custom headers on a `WebSocket()` upgrade, so the key may be passed on the URL. The query value lands in CF Worker access logs — server-side clients should always use the `Authorization` or `xi-api-key` header instead. Either spelling (`xi_api_key` or `xi-api-key`) is accepted."
      schema:
        type: string
    StreamInputLanguageQuery:
      name: language
      in: query
      required: false
      description: "Target language the synthesized speech will be in (ISO-639-1 code or English name). Preset voices were rated from Estonian audition recordings, so cloning their JVC1 anchor for non-Estonian text drags the Estonian timbre along. When this parameter is set to a non-Estonian language, the server regenerates the voice from the preset's stored (seed, instruct) pair in the target language instead of cloning. Defaults to Estonian when omitted."
      schema:
        type: string
    StreamInputLanguageHeader:
      name: X-Speak-Language
      in: header
      required: false
      description: "Same as the `language` query parameter, for server-side clients that prefer headers. When both are set, the query parameter wins."
      schema:
        type: string
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Malformed request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UnsupportedMediaType:
      description: Requested output format not supported by this deployment.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UpstreamError:
      description: Upstream synthesis engine failed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Resource conflict (e.g. duplicate voice name).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PayloadTooLarge:
      description: Request body exceeds the deployment limit.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required:
        - detail
      properties:
        detail:
          type: object
          required:
            - status
            - message
          properties:
            status:
              type: string
              example: unauthorized
            message:
              type: string
    Voice:
      type: object
      required:
        - voice_id
        - name
        - category
      properties:
        voice_id:
          type: string
        name:
          type: string
        category:
          type: string
          enum:
            - premade
            - cloned
          description: "ElevenLabs-style category. `cloned` voices are owned by the API-key holder."
        description:
          type:
            - string
            - null
        preview_url:
          type:
            - string
            - null
          format: uri
          description: "Public URL for `premade` voices; for `cloned` voices the URL requires the same `xi-api-key` (or session cookie) used to list voices."
        labels:
          type: object
          additionalProperties:
            type: string
          description: Free-form key/value metadata (gender, accent, etc.).
    AddVoiceRequest:
      type: object
      required:
        - name
        - files
      properties:
        name:
          type: string
          maxLength: 60
          description: Display name. Must be unique among the caller’s active clones.
        files:
          type: array
          items:
            type: string
            format: binary
          description: Reference audio files. The first file is used; additional files are ignored. 6–30 seconds of clean speech, max 10 MB.
        audio:
          type: string
          format: binary
          description: "Legacy alias for the first item in `files`. Ignored if `files` is present."
        language:
          type: string
          description: "ISO language code of the reference audio (defaults to `et`). Influences the preview generated for the clone."
          examples:
            - et
            - en
        previewText:
          type: string
          description: Sentence the sidecar speaks back as the clone preview. Defaults to a language-appropriate canned sentence.
        refText:
          type: string
          description: Optional transcript of the reference audio. When omitted, the sidecar transcribes it.
    AddVoiceResponse:
      type: object
      required:
        - voice_id
        - requires_verification
      properties:
        voice_id:
          type: string
        requires_verification:
          type: boolean
          description: "Always `false` on this deployment; field retained for SDK compatibility."
    VoicesList:
      type: object
      required:
        - voices
      properties:
        voices:
          type: array
          items:
            $ref: "#/components/schemas/Voice"
    VoiceSettings:
      type: object
      description: "ElevenLabs voice-settings object. Only `speed` is mapped by this engine; other fields are accepted for SDK compatibility and ignored."
      properties:
        stability:
          type: number
          minimum: 0
          maximum: 1
        similarity_boost:
          type: number
          minimum: 0
          maximum: 1
        style:
          type: number
          minimum: 0
          maximum: 1
        use_speaker_boost:
          type: boolean
        speed:
          type: number
          minimum: 0.5
          maximum: 2
    TextToSpeechRequest:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 8000
          description: "Input text to synthesize. Hard cap of 8000 characters (≈ 6 min of audio). Inputs above this return `400 text_too_long`. Splitting longer scripts into separate requests is the supported pattern.\n\n**Expressive tags (optional).** The text may carry inline markup to add emotion, delivery style, or sound effects. Wrap a passage in a block tag on its own lines to render it with that quality, or drop a single inline marker to insert an effect:\n- Emotion blocks: `[raev]` (fury), `[aukartus]` (wonder), `[rahulolu]` (serenity), `[sihikindlus]` (resolve), `[kurbus]` (sorrow), `[ehmatus]` (astonishment). Example: `[kurbus]\\nI never said goodbye.\\n[/kurbus]`\n- Style blocks: `[huue]` (holler), `[sosin]` (whisper), `[laul]` (singing).\n- Inline effects: `[naer]` (chuckle), `[ohe]` (exhale). Example: `Oh, you again [naer]`.\n\nA request containing any of these tags is rendered by the expressive synthesis path and always returns WAV (`output_format` is ignored for tagged input; on `/stream` it returns `x-streaming-mode: sentence`). Untagged text uses the standard engine. Bracketed words outside this set are treated as literal text. `voice_settings.speed` is ignored for tagged input."
        model_id:
          type: string
          default: jutusta_v1
        language_code:
          type: string
          description: "ISO language code (e.g. `et`, `en`). Defaults to voice language."
          examples:
            - et
            - en
        seed:
          type: integer
          minimum: 0
          description: Accepted for ElevenLabs SDK compatibility; currently ignored. Preset voices resolve identity from a precomputed JVC1 anchor, so a per-request seed has no effect on the output.
        voice_settings:
          $ref: "#/components/schemas/VoiceSettings"
        previous_text:
          type: string
        next_text:
          type: string
        sentence_chunking:
          type: boolean
          default: true
          description: "Stream endpoint only (`/text-to-speech/{voice_id}/stream`). When `true` the Worker splits `text` into sentence-sized segments, synthesizes each segment with one diffusion call, and stitches the PCM into a single streaming WAV. This lowers time-to-first-audio for long inputs at the cost of small extra pauses at segment boundaries. Silently ignored on the non-stream endpoint."
    StreamInputClientFrame:
      type: object
      description: "JSON text frame sent by the client over the `stream-input` WebSocket. Mirrors the ElevenLabs stream-input protocol. Frames with no `text` field are ignored unless they carry `flush` or `voice_settings`."
      properties:
        text:
          type: string
          description: "Text to append to the synth buffer. Use a literal single space (`\" \"`) on the first frame; an empty string (`\"\"`) flushes and ends the session."
        try_trigger_generation:
          type: boolean
          default: false
          description: "When `true`, force-synth the current buffer instead of waiting for a sentence boundary."
        flush:
          type: boolean
          default: false
          description: "When `true`, force-synth the buffered text immediately and keep the session open (ElevenLabs semantics). May be sent with or without a `text` field. To end the session, send `{\"text\": \"\"}` instead."
        voice_settings:
          $ref: "#/components/schemas/VoiceSettings"
        xi_api_key:
          type: string
          description: Accepted in-frame for SDK compatibility and ignored — auth is resolved on the WebSocket upgrade.
    StreamInputAudioFrame:
      type: object
      description: "Server → client audio frame. `audio` carries base64-encoded RAW PCM (s16le, 24 kHz, mono). Chunk frames carry `isFinal:false`. A `flush` closes with a `{\"audio\":null,\"flushDone\":true}` marker (session stays open); the terminal frame has `isFinal:true` and `audio:null`, followed by a clean close (code 1000)."
      properties:
        audio:
          type:
            - string
            - null
          description: "Base64-encoded RAW PCM bytes (signed 16-bit little-endian, 24 kHz, mono). `null` on the `flushDone` and terminal frames."
        isFinal:
          type: boolean
          description: "`true` on the terminal frame, just before the connection is closed cleanly. Absent on `flushDone` markers."
        flushDone:
          type: boolean
          description: "`true` on the marker sent after a `flush` has fully drained its audio; the session stays open for the next response."
    StreamInputErrorFrame:
      type: object
      required:
        - error
      description: Server → client error frame. Sent before a 1011 close when an upstream synthesis call fails mid-session.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              example: upstream_error
            message:
              type: string
    Model:
      type: object
      required:
        - model_id
        - name
      properties:
        model_id:
          type: string
        name:
          type: string
        description:
          type:
            - string
            - null
        can_do_text_to_speech:
          type: boolean
        can_do_voice_conversion:
          type: boolean
        languages:
          type: array
          items:
            type: object
            required:
              - language_id
              - name
            properties:
              language_id:
                type: string
              name:
                type: string
    ModelsList:
      type: object
      required:
        - models
      properties:
        models:
          type: array
          items:
            $ref: "#/components/schemas/Model"
    SpeechToTextRequest:
      type: object
      required:
        - model_id
      description: "Multipart form. Exactly one of `file` or `cloud_storage_url` is required. Fields listed under `x-compatibility` are accepted for SDK compatibility and silently ignored."
      properties:
        model_id:
          type: string
          description: "Accepted for SDK compatibility (e.g. `scribe_v1`, `scribe_v2`). The engine is the Jutusta ASR model regardless of value."
        file:
          type: string
          format: binary
          description: Audio/video file, max 100 MB inline.
        cloud_storage_url:
          type: string
          format: uri
          description: "https URL to an audio/video file fetched server-side. Mutually exclusive with `file`."
        source_url:
          type: string
          format: uri
          description: "Alias of `cloud_storage_url`."
        language_code:
          type: string
          description: "ISO-639-1 language code; `auto` for detection."
          examples:
            - et
            - en
            - auto
        diarize:
          type: boolean
          default: false
        speaker_recognition:
          type: boolean
          default: false
          description: "Jutusta extension. With `diarize=true`, matches diarized speakers against the saved speaker library of the account and adds a top-level `matched_speakers` object to the response. `words[].speaker_id` is never rewritten."
        timestamps_granularity:
          type: string
          enum:
            - none
            - word
            - character
          default: word
        webhook:
          type: boolean
          default: false
          description: "When `true`, the response is `SpeechToTextWebhookResponse` and the transcription result is POSTed to the registered webhook URL on completion."
        webhook_id:
          type: string
          description: "Required when `webhook=true`. Must reference an active subscription owned by the caller."
        webhook_metadata:
          type: string
          description: Opaque caller metadata (≤ 16 KB) round-tripped on the webhook payload.
        tag_audio_events:
          type: boolean
          default: true
          description: Accepted; currently ignored.
        num_speakers:
          type: integer
          description: Accepted; currently ignored.
        temperature:
          type: number
          description: Accepted; currently ignored.
        seed:
          type: integer
          description: Accepted; currently ignored.
        additional_formats:
          type: string
          description: "JSON-encoded array of export-format requests, matching the ElevenLabs `additional_formats` field. Each element is an object with a `format` discriminator. Honoured formats: `srt`, `txt`, `segmented_json`; the generated files come back in the response `additional_formats` array. Other ElevenLabs formats (`docx`, `html`, `pdf`) are accepted-and-skipped, and the numeric segmentation knobs (`max_characters_per_line`, `max_segment_chars`, `max_segment_duration_s`, `segment_on_silence_longer_than_s`) are ignored — our subtitle chunker uses its own heuristic. `include_speakers` / `include_timestamps` are honoured for `txt` and inherent to `segmented_json`; `srt` always carries timecodes and does not embed speaker labels. `segmented_json` follows Jutusta's segments schema, not ElevenLabs' internal shape."
    SpeechToTextResponse:
      oneOf:
        -
          $ref: "#/components/schemas/SpeechToTextChunkResponse"
        -
          $ref: "#/components/schemas/SpeechToTextWebhookResponse"
    AdditionalFormat:
      type: object
      required:
        - requested_format
        - file_extension
        - content_type
        - is_base64_encoded
        - content
      properties:
        requested_format:
          type: string
          example: srt
        file_extension:
          type: string
          example: srt
        content_type:
          type: string
          example: application/x-subrip
        is_base64_encoded:
          type: boolean
          description: "Always `false` for the supported text formats (srt/txt/segmented_json); `content` is the raw file body."
        content:
          type: string
    SpeechToTextChunkResponse:
      type: object
      required:
        - language_code
        - language_probability
        - text
        - words
      properties:
        language_code:
          type: string
          example: et
        language_probability:
          type: number
          minimum: 0
          maximum: 1
        text:
          type: string
        words:
          type: array
          items:
            $ref: "#/components/schemas/SpeechToTextWord"
        channel_index:
          type:
            - integer
            - null
        additional_formats:
          type:
            - array
            - null
          items:
            $ref: "#/components/schemas/AdditionalFormat"
          description: "Generated export files for the `additional_formats` requested on the input. `null` when none were requested; an entry is present per honoured format (`srt`, `txt`, `segmented_json`)."
        transcription_id:
          type:
            - string
            - null
        entities:
          type:
            - array
            - null
        audio_duration_secs:
          type:
            - number
            - null
        matched_speakers:
          type:
            - object
            - null
          additionalProperties:
            $ref: "#/components/schemas/SpeakerMatch"
          description: "Jutusta extension, present only when the job ran with `speaker_recognition=true`. Maps diarization labels (the `words[].speaker_id` values) to saved speakers that matched confidently."
    SpeakerMatch:
      type: object
      required:
        - name
        - confidence
        - profileId
      properties:
        name:
          type: string
          description: Saved speaker name.
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Cosine similarity of the winning voiceprint.
        profileId:
          type: string
          description: "Id of the matched saved speaker (see `/recognition/speakers`)."
    SpeakerProfile:
      type: object
      required:
        - id
        - name
        - created_at
      properties:
        id:
          type: string
        name:
          type: string
          maxLength: 200
        created_at:
          type: integer
          description: Unix ms.
        quality:
          type:
            - number
            - null
          description: "How well this voiceprint separated the speaker from others in the source recording (0–1); `null` for single-speaker sources."
        speech_seconds:
          type:
            - number
            - null
          description: Seconds of clean speech the voiceprint was computed from.
    SpeechToTextWord:
      type: object
      required:
        - text
        - type
      properties:
        text:
          type: string
        type:
          type: string
          enum:
            - word
            - spacing
            - audio_event
        start:
          type:
            - number
            - null
        end:
          type:
            - number
            - null
        speaker_id:
          type:
            - string
            - null
        logprob:
          type: number
        characters:
          type:
            - array
            - null
    SpeechToTextWebhookResponse:
      type: object
      required:
        - message
        - request_id
      properties:
        message:
          type: string
        request_id:
          type: string
        transcription_id:
          type:
            - string
            - null
    WebhookCreateRequest:
      type: object
      required:
        - url
      properties:
        name:
          type: string
          maxLength: 80
        url:
          type: string
          format: uri
          description: https endpoint that will receive POSTs.
        events:
          type: array
          items:
            type: string
            enum:
              - speech_to_text.transcription_completed
              - speech_to_text.transcription_failed
          description: "Defaults to `[\"speech_to_text.transcription_completed\"]`."
    Webhook:
      type: object
      required:
        - webhook_id
        - name
        - url
        - secret_prefix
        - events
        - created_at
        - revoked
      properties:
        webhook_id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        secret_prefix:
          type: string
          description: Prefix of the signing secret. The full secret is only returned at creation time.
        events:
          type: array
          items:
            type: string
        created_at:
          type: integer
          description: Unix ms.
        revoked:
          type: boolean
    WebhookCreated:
      allOf:
        -
          $ref: "#/components/schemas/Webhook"
        -
          type: object
          required:
            - secret
          properties:
            secret:
              type: string
              description: Plaintext signing secret. Returned exactly once; store it server-side.
    WebhooksList:
      type: object
      required:
        - webhooks
      properties:
        webhooks:
          type: array
          items:
            $ref: "#/components/schemas/Webhook"
    JobState:
      type: string
      enum:
        - uploading
        - pending
        - running
        - succeeded
        - failed
        - cancelled
      description: "Lifecycle state. `uploading` is the initial state after `POST /recognition/uploads` until `…/start` is called. `succeeded`/`failed`/`cancelled` are terminal."
    CreateUploadRequest:
      type: object
      required:
        - filename
        - content_type
        - size_bytes
      properties:
        filename:
          type: string
          description: Original filename, used for display only.
        content_type:
          type: string
          description: MIME type of the upload. Must be an allowed audio/video type.
          examples:
            - audio/wav
            - audio/mpeg
            - audio/mp4
            - video/mp4
        size_bytes:
          type: integer
          minimum: 1
          description: Total byte length of the file. 25 MB cap for anonymous callers; deployment cap for authed callers.
    CreateUploadResponse:
      type: object
      required:
        - job_id
        - upload_url
        - r2_key
        - expires_at
      properties:
        job_id:
          type: string
        upload_url:
          type: string
          format: uri
          description: "Presigned R2 PUT URL. Send the file body with `Content-Type` matching the value posted as `content_type`."
        r2_key:
          type: string
          description: Object key inside R2 (informational).
        expires_at:
          type: integer
          description: Unix-second expiry of the presigned URL (30 minutes from issuance).
    StartJobRequest:
      type: object
      description: "All fields optional; omit the body or send `{}` to accept defaults."
      properties:
        language:
          type: string
          description: "ISO-639-1 language code, or `auto` to auto-detect."
          examples:
            - et
            - en
            - auto
        diarization:
          type: boolean
          default: true
        forced_alignment:
          type: boolean
          default: true
          description: "When `true` (the default), the result carries per-word `start`/`end` timings (`segments[].words[]`). Set `false` to skip alignment and return segment-level timings only."
        fa_method:
          type: string
          enum:
            - xlsr
            - kaldi
            - none
          default: xlsr
          description: "Forced-alignment backend when `forced_alignment=true`."
        duration_hint_seconds:
          type: number
          minimum: 0
          description: Optional client-side hint to improve ETA estimation.
    StartJobResponse:
      type: object
      required:
        - job_id
        - state
      properties:
        job_id:
          type: string
        state:
          $ref: "#/components/schemas/JobState"
        eta_seconds:
          type:
            - number
            - null
        audio_duration_seconds:
          type:
            - number
            - null
    RecognitionWord:
      type: object
      description: "One aligned word. `start`/`end` (seconds from the start of the recording) are present when the job ran with `forced_alignment=true`."
      properties:
        word_with_punctuation:
          type: string
          description: "Surface form including trailing punctuation, e.g. `Tere,`."
        word:
          type: string
          description: Bare word without punctuation.
        punctuation:
          type:
            - string
            - null
        start:
          type:
            - number
            - null
          description: Seconds from recording start.
        end:
          type:
            - number
            - null
          description: Seconds from recording start.
        confidence:
          type:
            - number
            - null
        logprob:
          type:
            - number
            - null
    RecognitionSegment:
      type: object
      required:
        - start
        - end
        - text
      properties:
        start:
          type: number
          description: Segment start, seconds from recording start.
        end:
          type: number
          description: Segment end, seconds from recording start.
        text:
          type: string
        speaker:
          type:
            - string
            - null
          description: "Diarization label (`SPEAKER_00`…) when the job ran with `diarization=true`."
        words:
          type:
            - array
            - null
          items:
            $ref: "#/components/schemas/RecognitionWord"
          description: "Per-word timings; present when the job ran with `forced_alignment=true`."
    RecognitionResult:
      type: object
      required:
        - segments
      description: "Raw recognition result. Segment-grained, with per-word timings nested under `segments[].words[]`."
      properties:
        segments:
          type: array
          items:
            $ref: "#/components/schemas/RecognitionSegment"
        language_detected:
          type:
            - string
            - null
        text:
          type:
            - string
            - null
          description: "Full transcript as plain text, when the backend provides it. Otherwise join `segments[].text`."
    RecognitionJobSummary:
      type: object
      required:
        - job_id
        - state
        - language
        - original_filename
        - size_bytes
        - created_at
        - updated_at
      properties:
        job_id:
          type: string
        state:
          $ref: "#/components/schemas/JobState"
        language:
          type: string
        original_filename:
          type: string
        size_bytes:
          type: integer
        duration_seconds:
          type:
            - number
            - null
        progress:
          type:
            - number
            - null
          minimum: 0
          maximum: 1
          description: Fractional progress (0–1) while running.
        created_at:
          type: integer
          description: Unix ms.
        updated_at:
          type: integer
          description: Unix ms.
    RecognitionJobStatus:
      type: object
      required:
        - job_id
        - state
        - language
        - original_filename
        - size_bytes
        - created_at
        - updated_at
      properties:
        job_id:
          type: string
        state:
          $ref: "#/components/schemas/JobState"
        progress:
          type:
            - number
            - null
          minimum: 0
          maximum: 1
        eta_seconds:
          type:
            - number
            - null
        audio_duration_seconds:
          type:
            - number
            - null
        language:
          type: string
        language_detected:
          type:
            - string
            - null
        original_filename:
          type: string
        size_bytes:
          type: integer
        result:
          description: "Populated when `state` is `succeeded`. Shape matches `RecognitionResult` (segment-grained, with per-word timings under `segments[].words[]` when the job ran with `forced_alignment=true`). Note: this differs from the flattened `SpeechToTextChunkResponse` returned by `POST /speech-to-text`."
          oneOf:
            -
              $ref: "#/components/schemas/RecognitionResult"
            -
              type: null
        matched_speakers:
          type:
            - object
            - null
          additionalProperties:
            $ref: "#/components/schemas/SpeakerMatch"
          description: "Saved speakers recognised in this job, keyed by diarization label. `null` unless the job ran with diarization + speaker recognition and the account has saved speakers."
        error:
          description: "Populated when `state` is `failed`."
          oneOf:
            -
              type: object
              required:
                - code
              properties:
                code:
                  type: string
                detail:
                  type:
                    - string
                    - null
            -
              type: null
        created_at:
          type: integer
        updated_at:
          type: integer
    Summary:
      type: object
      required:
        - id
        - jobId
        - templateId
        - text
        - model
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        jobId:
          type: string
        templateId:
          type: string
          description: "Catalog id of the template used. Listed by `GET /summary-templates`."
        text:
          type: string
          description: Generated summary text.
        model:
          type: string
          description: "Identifier of the LLM that produced the summary (e.g. `@cf/mistralai/mistral-small-3.1-24b-instruct`)."
        createdAt:
          type: integer
          description: Unix ms.
        updatedAt:
          type: integer
          description: Unix ms.
    SummariesListResponse:
      type: object
      required:
        - summaries
      properties:
        summaries:
          type: array
          items:
            $ref: "#/components/schemas/Summary"
    SummaryResponse:
      type: object
      required:
        - summary
      properties:
        summary:
          $ref: "#/components/schemas/Summary"
    SummaryGenerateRequest:
      type: object
      properties:
        templateId:
          type:
            - string
            - null
          description: "Template to apply. Omit (or pass `null`) to use the caller’s default template."
    SummaryTemplate:
      type: object
      required:
        - id
        - label
        - description
      properties:
        id:
          type: string
        label:
          type: string
        description:
          type: string
    TemplatesCatalogResponse:
      type: object
      required:
        - templates
        - default_template_id
      properties:
        templates:
          type: array
          items:
            $ref: "#/components/schemas/SummaryTemplate"
        default_template_id:
          type: string
          description: "Caller’s current default template id. Falls back to the catalog default until `PUT /summary-templates` is called."
    SetDefaultTemplateRequest:
      type: object
      required:
        - templateId
      properties:
        templateId:
          type: string
          description: Catalog id of the template to set as default.
    TranslateChunkRequest:
      type: object
      required:
        - source
        - target
        - new_source
      properties:
        source:
          type: string
          description: "BCP-47 / Whisper-style language code of `new_source`. `auto` is rejected — resolve before calling."
          examples:
            - et
            - en
        target:
          type: string
          description: Target language code.
          examples:
            - en
            - et
        new_source:
          type: string
          minLength: 1
          description: New transcript chunk to translate. Capped at ~8000 characters per call.
        previous:
          type:
            - string
            - null
          description: Tail of the translation rendered so far. Used as context — the model is instructed not to repeat or revise it. Truncated to the last ~2000 characters server-side.
    TranslateChunkResponse:
      type: object
      required:
        - appended
        - model
        - source
        - target
      properties:
        appended:
          type: string
          description: Text to append to the already-rendered translation. Concatenate verbatim.
        model:
          type: string
          description: "Identifier of the LLM that produced the chunk (e.g. `@cf/mistralai/mistral-small-3.1-24b-instruct`). Same-language requests echo `identity`."
        source:
          type: string
        target:
          type: string