| name | generate-voiceover-elevenlabs-v3 |
| description | generate a voiceover mp3 using ElevenLabs eleven_v3 TTS from a v3-tagged script, interactively confirming which approved voice from VOICES.md to use, then applying ffmpeg atempo post-process to hit the target duration (v3 ignores the speed voice_setting). |
generate-voiceover-elevenlabs-v3
Purpose
Render a v3-tagged script (typically produced by format-script-to-style) into an mp3 using ElevenLabs' eleven_v3 model, picking an approved voice from VOICES.md, then applying ffmpeg atempo post-process to hit the desired pace.
Why a v3-specific skill (vs. the existing create-voiceover-elevenlabs): v3 interprets audio tags expressively but silently ignores the speed voice_setting (see memory feedback_v3_ignores_speed.md). Pace control must be post-processed. This skill bakes that in.
Inputs
tagged_script: the v3-tagged script text (from the calling agent or format-script-to-style).
target_duration_seconds (optional): if provided, the skill computes the required atempo factor and applies it. If absent, no post-process is applied.
- Voice selection: interactive — the skill presents the approved voices from
VOICES.md and asks the user to choose. The chosen voice's stored v3 settings are used as defaults.
output_dir: where to save outputs. Defaults to <project>/working/voiceover/ or skills/test-runs/<timestamp>/generate-voiceover-elevenlabs-v3/ for test runs.
Workflow
Prefer the bundled runnable script when invoked programmatically:
python3 skills/atoms/voiceover/generate-voiceover-elevenlabs-v3/scripts/generate.py \
--voice-id <id> \
--script-file <tagged-script.md> \
--target-duration <seconds> \
--output-dir <dir> \
--name <basename>
The script handles env loading, the v3 API call, ffprobe duration measurement, atempo computation/clamping, and manifest writing. The interactive workflow below is what an agent should do when invoking the skill via chat (vs. the script-direct path):
- Load and present voices. Read
/Users/0xhbam/Desktop/Cursor/content-goose/VOICES.md. Show the list to the user and ask which to use. Extract the chosen voice's voiceId and the v3 settings line if present.
- Confirm the script. Show the tagged script to the user and confirm before any paid API call. (Per
feedback_media_pipeline_gates.md: show before render.)
- Resolve the ElevenLabs API key. The key lives in
gtm-goose/.env as ELEVEN_LABS_API_KEY (underscore). Alias to ELEVENLABS_API_KEY for the call:
export ELEVENLABS_API_KEY=$(grep '^ELEVEN_LABS_API_KEY' /Users/0xhbam/Desktop/Cursor/gtm-goose/.env | sed 's/.*=[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
- Call ElevenLabs v3 TTS:
curl -sS -X POST "https://api.elevenlabs.io/v1/text-to-speech/<VOICE_ID>?output_format=mp3_44100_128" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d "$(python3 -c "import json,sys; print(json.dumps({'text': sys.argv[1], 'model_id': 'eleven_v3', 'voice_settings': <voice_settings_from_VOICES.md>}))" "$TAGGED_SCRIPT")" \
-o "<output_dir>/<name>.mp3"
Use mp3_44100_128 (higher formats are tier-gated; see learning from 2026-05-16 test).
- Measure rendered duration:
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "<name>.mp3"
- Apply atempo post-process if
target_duration_seconds was supplied:
- Compute
atempo_factor = rendered_duration / target_duration_seconds.
- Clamp to
[1.0, 1.25]. Above 1.25 voice starts to sound chipmunky; surface to user instead of pushing further (alternative: trim script and re-render).
Output
<name>.mp3 — raw v3 render
<name>_atempo<factor>.mp3 — atempo-corrected render (only if target_duration_seconds was supplied and factor > 1.05)
<name>.manifest.json — generation parameters and final paths
Quality Checks
- Voice selected is present in
VOICES.md.
- HTTP 200 from the ElevenLabs API; output mp3 is valid (use
file and ffprobe to confirm non-zero duration).
- If
target_duration_seconds supplied: final mp3 duration is within ±10% of target.
- atempo factor stayed within the clamp range — if it didn't, the skill stopped and surfaced.
- API key was loaded from
gtm-goose/.env (not hardcoded, not asked from the user).
Failure Modes
403 subscription_required on output_format — drop to mp3_44100_128. Higher formats are Creator-tier-gated.
401 invalid_api_key — key was not loaded correctly. Re-check the env-var name (note: it's ELEVEN_LABS_API_KEY with underscore in the source .env).
- Voice id not in
VOICES.md — ask the user to confirm and offer to add it to VOICES.md with clone-voice-elevenlabs or search-voices-elevenlabs.
- Rendered duration is much longer than target (atempo factor > 1.25 required) — stop and offer to trim the script or switch model to
eleven_multilingual_v2.
- Audio tags appear unspoken in the rendered audio — the model didn't interpret them as tags. Likely cause: tag syntax error or tag stacking. Re-check the recipe.
- Source script contains 4-byte unicode (some emojis) that breaks JSON serialization — strip emojis from the script body before calling.