Skip to content
Sign in

TTS API

Create speech audio with selectable language and model controls.

The TTS API exposes an asynchronous text-to-speech surface under `/api/tts/v1`. Submit text plus language/model options, receive a `taskId`, then poll the status endpoint until the job returns a generated WAV URL or a failure reason.

Third-party migration guide

  • No endpoint or required JSON field changed. Existing request payloads remain valid.
  • Read `Retry-After` from successful queued/pending responses and from HTTP 429. Never poll faster than that value; add jitter and back off gradually to a 15-second maximum interval.
  • A 429 from POST means no new task was admitted. Wait, then retry the POST. A 429 from GET does not mean synthesis failed; keep the same `taskId` and retry the same status URL.
  • Configure `concurrentSessions` from API Keys → Edit Policy. It is the maximum number of unfinished TTS jobs for that key; wait for a job to return `ready=true` before submitting above the configured limit.
  • For cloned voices, upload or register the reference once and reuse the returned `custom_voices/...` path. Do not re-upload the same reference for every sentence.
  • Only retry POST automatically after an explicit 429. A network timeout may happen after a task was accepted, so blindly retrying an ambiguous POST can create duplicate audio jobs.
POST/api/tts/v1/custom-voices

Upload clone audio

Registers a short reference WAV or public HTTPS audio URL and returns a `custom_voices/...` path that can be reused across speech requests. Requires `consent=true` to confirm voice-owner permission.

POST/api/tts/v1/audio/speech

Start speech synthesis

Creates a TTS generation task. The public route validates the API key, records usage, and forwards JSON options to the existing TTS worker stack.

GET/api/tts/v1/audio/speech/{taskId}

Check synthesis status

Returns Celery task state, readiness, progress when available, success/failure, and the generated audio under `result.url` when complete.

JSON request fields

Speech synthesis accepts JSON. Clone/reference audio can be passed as `clone_audio`, uploaded first through `/api/tts/v1/custom-voices`, or supplied as a public `https://` audio URL for automatic registration. Direct uploads and automatic URL registration require `consent=true`. Treat the returned `custom_voices/...` path as an opaque reusable reference and send it back exactly as returned.

FieldTypeRequiredNotes
textstringYesText to synthesize. Maximum length is 200 characters.
languagestringNoSupported values are `en`, `zh`, `vi`, `th`, `ms`, `lo`, `ja`, and `ko`. Defaults to `en`; Thai, Malay, and Lao should use `omnivoice`.
modelstringNoSupported public models are `vieneu_v2`, `cosytts`, `qwen_viet_tts`, and `omnivoice`. If omitted, the service chooses a default from the language. Legacy `f5` requests are routed to OmniVoice.
seedintegerNoOptional deterministic seed from `0` to `2147483647`. Omit or send a negative value for random generation.
f5_samplestringNoLegacy optional reference voice path for `vieneu_v2`. For OmniVoice, CosyTTS, or Qwen Viet TTS, use `clone_audio` instead.
vieneu_expressionstringNoOnly used when `model=vieneu_v2` and `language=vi`. Supported values are `natural` and `storytelling`.
speakerstringNoOptional built-in speaker for `cosytts` or `qwen_viet_tts` when no clone audio is supplied. Examples: `Ryan`, `Vivian`, `yen_nhi`, `my_van`.
instructstringNoOptional voice/style instruction for `cosytts` and OmniVoice non-clone requests, such as `Warm, clear narration`. OmniVoice clone requests with `clone_audio` must omit `instruct`.
clone_audiostringNoOptional clone/reference audio for `cosytts`, `qwen_viet_tts`, or `omnivoice`. Use a returned `custom_voices/...` path from `/api/tts/v1/custom-voices`, an accepted saved path (`/uploads/...`, `F5-TTS/...`, `custom_voices/...`), or a public `https://` audio URL. Legacy alias: `qwen3_clone_audio`.
consentboolean | stringMaybeRequired and must be truthy when `clone_audio` is a public HTTPS URL that the service must register. It confirms that the voice owner authorized cloning and use. Direct `/api/tts/v1/custom-voices` uploads always require this field.
clone_textstringNoOptional transcript for `clone_audio`. Provide it when known; uploaded or hosted references are auto-transcribed when the ASR service is configured. For OmniVoice clone mode, provide this field when the upstream OmniVoice service has reference ASR disabled. Legacy alias: `qwen3_clone_text`.

POST /api/tts/v1/custom-voices

Sample
curl -sS \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -F "consent=true" \
  -F "audio_file=@thai-ref-female-4s.wav;type=audio/wav" \
  -F "name=thai_flashcard_female" \
  -F "model=omnivoice" \
  -F "language=th" \
  -F "transcript=reference transcript here" \
  "https://kaleidovid.com/api/tts/v1/custom-voices"

Custom voice response

Sample
{
  "success": true,
  "voice": {
    "id": "cv_123",
    "name": "thai_flashcard_female",
    "model": "omnivoice",
    "language": "th",
    "audioPath": "custom_voices/user-1/thai_flashcard_female.wav",
    "transcript": "reference transcript here"
  },
  "clone_audio": "custom_voices/user-1/thai_flashcard_female.wav",
  "clone_text": "reference transcript here",
  "ttsRequest": {
    "clone_audio": "custom_voices/user-1/thai_flashcard_female.wav",
    "clone_text": "reference transcript here"
  }
}

Register hosted reference

Sample
curl -sS \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -F "consent=true" \
  -F "audio_url=https://example.com/tts_refs/thai-ref-female-4s.wav" \
  -F "name=thai_flashcard_female" \
  -F "model=omnivoice" \
  -F "language=th" \
  "https://kaleidovid.com/api/tts/v1/custom-voices"

Cloned OmniVoice request

Sample
curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "text": "สวัสดีครับ",
    "language": "th",
    "model": "omnivoice",
    "clone_audio": "custom_voices/user-1/thai_flashcard_female.wav",
    "clone_text": "reference transcript here",
    "seed": 12345
  }' \
  "https://kaleidovid.com/api/tts/v1/audio/speech"

POST /api/tts/v1/audio/speech

Sample
curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "text": "Hello from the KaleidoVid TTS API.",
    "language": "en",
    "model": "cosytts",
    "speaker": "Ryan",
    "instruct": "Warm, clear narration",
    "seed": 12345
  }' \
  "https://kaleidovid.com/api/tts/v1/audio/speech"

Queued response example

Sample
{
  "success": true,
  "taskId": "a97bf5a8-16d4-44c7-b45b-c8a5f94c9e8f",
  "message": "TTS generation task started with COSYTTS",
  "statusPath": "/api/tts/v1/audio/speech/a97bf5a8-16d4-44c7-b45b-c8a5f94c9e8f"
}

GET /api/tts/v1/audio/speech/{taskId}

Sample
curl -sS \
  -H "x-api-key: YOUR_API_KEY" \
  "https://kaleidovid.com/api/tts/v1/audio/speech/a97bf5a8-16d4-44c7-b45b-c8a5f94c9e8f"

Completed status response example

Sample
{
  "taskId": "a97bf5a8-16d4-44c7-b45b-c8a5f94c9e8f",
  "state": "SUCCESS",
  "ready": true,
  "successful": true,
  "result": {
    "success": true,
    "url": "/uploads/generations/tts/tts_cosytts_20260605_1234.wav",
    "metadata": {
      "text": "Hello from the KaleidoVid TTS API.",
      "language": "en",
      "model": "cosytts",
      "duration": 2.14,
      "seed": 12345
    }
  }
}

Status response fields

FieldTypeRequiredNotes
taskIdstringAlways on successCelery task id returned by the start request. Use it to poll the status endpoint.
statusPathstringAlways on successRelative polling path for the public TTS status endpoint.
statestringOn status responsesCelery state such as `PENDING`, `STARTED`, `SUCCESS`, or `FAILURE`.
ready / successfulboolean / boolean | nullOn status responsesWhen `ready=true` and `successful=true`, read the generated audio from `result.url`.
result.urlstringWhen successfulRelative or absolute URL of the generated WAV file.
result.metadataobjectWhen successfulIncludes the resolved `text`, `language`, `model`, `duration`, optional `seed`, and model-specific metadata.
errorstringOn failureFailure reason returned when the job fails.
Retry-AfterHTTP response headerQueued, pending, or 429Minimum seconds the caller should wait before the next status poll or retry. Add a small random jitter and increase the delay gradually up to 15 seconds.

Python polling example

Sample
# pip install requests

import random
import time
import requests

API_KEY = "YOUR_API_KEY"
API_ORIGIN = "https://kaleidovid.com"
BASE_URL = "https://kaleidovid.com/api/tts/v1"

def retry_after(response, fallback):
    try:
        return max(1.0, float(response.headers.get("Retry-After", fallback)))
    except (TypeError, ValueError):
        return fallback

while True:
    start = requests.post(
        f"{BASE_URL}/audio/speech",
        headers={"x-api-key": API_KEY},
        json={
            "text": "Hello from the KaleidoVid TTS API.",
            "language": "en",
            "model": "cosytts",
            "speaker": "Ryan",
            "instruct": "Warm, clear narration",
        },
        timeout=180,
    )
    if start.status_code != 429:
        break
    time.sleep(retry_after(start, 3) + random.uniform(0, 0.5))

start.raise_for_status()
queued = start.json()
status_url = f"{API_ORIGIN}{queued['statusPath']}"
poll_delay = retry_after(start, 3)

while True:
    status = requests.get(status_url, headers={"x-api-key": API_KEY}, timeout=30)
    if status.status_code == 429:
        time.sleep(retry_after(status, poll_delay) + random.uniform(0, 0.5))
        poll_delay = min(15, poll_delay * 1.5)
        continue
    status.raise_for_status()
    payload = status.json()
    if payload.get("ready"):
        break
    server_delay = retry_after(status, poll_delay)
    time.sleep(max(server_delay, poll_delay) + random.uniform(0, 0.5))
    poll_delay = min(15, poll_delay * 1.5)

if not payload.get("successful"):
    raise RuntimeError(payload.get("error", "TTS generation failed"))

print(payload["result"]["url"])

Failed response example

Sample
{
  "taskId": "a97bf5a8-16d4-44c7-b45b-c8a5f94c9e8f",
  "state": "FAILURE",
  "ready": true,
  "successful": false,
  "error": "TTS generation failed: upstream message"
}

Admission limit response example

Sample
HTTP/1.1 429 Too Many Requests
Retry-After: 3
Cache-Control: no-store
Content-Type: application/json

{
  "error": "Too many active TTS jobs for this API key",
  "activeJobs": 3,
  "concurrentLimit": 3,
  "retryAfterSeconds": 3
}
Recommended model pairings: `omnivoice` for Thai, Malay, Lao, or multilingual fallback; `vieneu_v2` or `qwen_viet_tts` for Vietnamese; and `cosytts` for English/Chinese/Japanese/Korean. For cloned OmniVoice flashcard batches, reuse the same `clone_audio`, `clone_text` when available, and `seed` across every short word and sentence, and omit `instruct`. The reference audio anchors speaker identity; the seed makes sampling more repeatable, but it is not a substitute for a stable reference. The 200-character limit is per request, so split longer learning sentences on phrase or sentence boundaries and stitch WAVs client-side; chunks generated with the same clone reference and seed are expected to remain voice-consistent.

Poll no faster than the `Retry-After` response header, add a small random jitter, and back off to at most one request every 15 seconds. HTTP 429 is an admission signal, not a failed synthesis job: wait for `Retry-After` and retry the same status URL or submit later.