{
  "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"
          }
        }
      }
    }
  }
}