Short answer / recommendation
Use a small Python batch script that: 1) reads a CSV/JSON manifest with language, voice_id (or alias), text and optional variant tags; 2) deduplicates by text+voice fingerprint; 3) calls the ElevenLabs text-to-speech endpoint with a bounded thread/process pool (3–5 workers); 4) applies exponential backoff on 429s and honors Retry-After / X-RateLimit-* headers; 5) outputs fixed-format audio files (WAV 48 kHz mono or MP3 192 kbps) named with a deterministic convention so Canva timelines import cleanly.
Why this approach
- Deterministic filenames + dedupe avoids re-generation costs. - Bounded concurrency keeps you under typical API rate limits. - Backoff + header checks avoid hard failures and automatic retries.
Decision criteria (pick by your constraints)
- Budget vs speed: higher concurrency speeds up work but increases cost and risk of hitting rate limits. If budget-limited, generate serially. - Output quality: use WAV/48kHz + LUFS normalization if editors need consistent loudness. - Team size & workflow stage: small teams prefer manual review per batch (serial); large teams can parallelize with QC checks.
Practical filename convention (recommended)
lang-voiceVariant-hexSHA1(truncatedText)-duration_sec-timestamp.wav
Examples
en-US-Joy_v2-1a2b3c-12s-20260625T1530.wav
es-ES-MaleA-4d5e6f-08s-20260625T1530.wav
Notes: include language code and voice variant for clarity, a short fingerprint of text to keep names short, and duration for quick timeline placement in Canva.
Implementation outline (Python, high level)
1. Input: CSV/JSON with columns: id, language, voice_id, variant, text, desired_format, safe_filename_hint. 2. For each row compute key = sha1(text + voice_id + variant). Skip if file exists. 3. Worker pool (ThreadPoolExecutor with max_workers=3–5). Each worker: a) POST to ElevenLabs TTS endpoint /v1/text-to-speech/{voice_id} with your text and settings. b) If response.status == 200 save binary audio; else if 429 or 503 apply exponential backoff + jitter. c) If response includes Retry-After or X-RateLimit-Remaining headers, respect them. d) On success, run an optional normalization step (ffmpeg/sox) to enforce sample rate, channels, and LUFS.
Retry/backoff pseudocode
attempt = 0
while attempt < MAX_TRIES:
resp = request()
if resp.status_code == 200: break
if resp.status_code == 429:
wait = resp.headers.get('Retry-After') or base * (2 ** attempt) + random_jitter
sleep(wait)
else: handle other 4xx/5xx (maybe fail after small retries)
attempt += 1
Best-for and avoid-if
- Best-for: teams with many short clips (5–60s) that need consistent naming and reproducible builds. - Avoid if: you need real-time streaming TTS for interactive apps (this is a batch workflow) or if you can’t afford per-character costs.
Practical checklist before running
1. Build manifest (CSV/JSON). 2. Map human voice names to voice_id via the voices endpoint. 3. Choose concurrency (start with 3). 4. Implement dedupe by text+voice hash. 5. Implement exponential backoff + header checks. 6. Save in WAV 48kHz mono (or MP3 if size matters). 7. Normalize loudness (-16 LUFS) and trim silence if needed. 8. Verify imports in Canva (drop a few sample files into a timeline to check sync). 9. Log API usage and costs.
When to use ChatGPT
Use ChatGPT to produce the manifest or to rewrite texts into language-appropriate variants and test prompts, but keep API keys and generation in your script.
If you want, I can provide a compact Python starter script (requests + ThreadPoolExecutor + ffmpeg-normalize calls) wired to ElevenLabs — tell me your preferred sample rate, concurrency, and whether you want WAV or MP3 output.