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.
/api/tts/v1/custom-voicesUpload 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.
/api/tts/v1/audio/speechStart 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.
/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.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | Yes | Text to synthesize. Maximum length is 200 characters. |
language | string | No | Supported values are `en`, `zh`, `vi`, `th`, `ms`, `lo`, `ja`, and `ko`. Defaults to `en`; Thai, Malay, and Lao should use `omnivoice`. |
model | string | No | Supported 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. |
seed | integer | No | Optional deterministic seed from `0` to `2147483647`. Omit or send a negative value for random generation. |
f5_sample | string | No | Legacy optional reference voice path for `vieneu_v2`. For OmniVoice, CosyTTS, or Qwen Viet TTS, use `clone_audio` instead. |
vieneu_expression | string | No | Only used when `model=vieneu_v2` and `language=vi`. Supported values are `natural` and `storytelling`. |
speaker | string | No | Optional built-in speaker for `cosytts` or `qwen_viet_tts` when no clone audio is supplied. Examples: `Ryan`, `Vivian`, `yen_nhi`, `my_van`. |
instruct | string | No | Optional 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_audio | string | No | Optional 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`. |
consent | boolean | string | Maybe | Required 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_text | string | No | Optional 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
Samplecurl -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
Samplecurl -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
Samplecurl -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
Samplecurl -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}
Samplecurl -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
| Field | Type | Required | Notes |
|---|---|---|---|
taskId | string | Always on success | Celery task id returned by the start request. Use it to poll the status endpoint. |
statusPath | string | Always on success | Relative polling path for the public TTS status endpoint. |
state | string | On status responses | Celery state such as `PENDING`, `STARTED`, `SUCCESS`, or `FAILURE`. |
ready / successful | boolean / boolean | null | On status responses | When `ready=true` and `successful=true`, read the generated audio from `result.url`. |
result.url | string | When successful | Relative or absolute URL of the generated WAV file. |
result.metadata | object | When successful | Includes the resolved `text`, `language`, `model`, `duration`, optional `seed`, and model-specific metadata. |
error | string | On failure | Failure reason returned when the job fails. |
Retry-After | HTTP response header | Queued, pending, or 429 | Minimum 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
SampleHTTP/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
}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.
