From 998f4f34970d917e19f06578edf000b12015166a Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Thu, 22 Oct 2026 16:36:33 +0200 Subject: [PATCH] feat: default to eleven_v4 for say and eleven_v4_turbo for new agents `say` now uses eleven_v4, agent templates use eleven_v4_turbo, and the generated text-to-speech and text-to-dialogue skills tell agents to pass --model-id eleven_v4 rather than falling back to the API's older defaults. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- cli/elevenlabs/workflow/say.rs | 11 +++--- cli/elevenlabs/workflow/skills.rs | 51 +++++++++++++++++++++++++++ cli/elevenlabs/workflow/skills/say.md | 19 +++++----- cli/elevenlabs/workflow/templates.rs | 4 +-- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6c79a0c..b61551f 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ elevenlabs say "one off" --voice JBFqnCBsd6RMkjVDRZzb --model eleven_flash_v2_5 elevenlabs say "save it" --output out.mp3 # write a file, skip playback ``` -Defaults are stored in `~/.elevenlabs/config.json` alongside [data residency](#data-residency). Out of the box `say` uses `eleven_v3` — the most expressive model, and the only family that reads [audio tags](https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices#audio-tags) like `elevenlabs say "[whispers] it worked"` as delivery rather than as words. The audio format follows the player unless `--output-format` or an `--output` filename says otherwise. +Defaults are stored in `~/.elevenlabs/config.json` alongside [data residency](#data-residency). Out of the box `say` uses `eleven_v4` — the most expressive model, and (with v3) the only family that reads [audio tags](https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices#audio-tags) like `elevenlabs say "[whispers] it worked"` as delivery rather than as words. The audio format follows the player unless `--output-format` or an `--output` filename says otherwise. Playback shells out to whichever player is on the box, preferring ones that read stdin so audio starts before the download finishes: `ffplay`, `mpv`, `afplay` (macOS), `paplay`/`aplay` (Linux), `Media.SoundPlayer` (Windows). Install [ffmpeg](https://ffmpeg.org) or [mpv](https://mpv.io) if none are present, or point `--player` at your own. diff --git a/cli/elevenlabs/workflow/say.rs b/cli/elevenlabs/workflow/say.rs index 59c4f4a..8dcb9c2 100644 --- a/cli/elevenlabs/workflow/say.rs +++ b/cli/elevenlabs/workflow/say.rs @@ -32,12 +32,11 @@ use super::settings; /// George — the voice the co-generated SDK uses in its own doctests, so a /// first run with no config still produces something sensible. pub(super) const DEFAULT_VOICE_ID: &str = "JBFqnCBsd6RMkjVDRZzb"; -/// The most expressive model, and the only family that honors audio tags -/// (`[whispers]`, `[laughs]`) rather than reading them aloud as text. Costs -/// roughly 0.8s more to first audio than `eleven_flash_v2_5`, which stays the -/// swap when speed matters more than delivery: -/// `elevenlabs say config model eleven_flash_v2_5`. -pub(super) const DEFAULT_MODEL_ID: &str = "eleven_v3"; +/// The most expressive model, and part of the v3/v4 families that honor audio +/// tags (`[whispers]`, `[laughs]`) rather than reading them aloud as text. +/// `eleven_v4_turbo` keeps the tags and is the swap when speed matters more +/// than delivery: `elevenlabs say config model eleven_v4_turbo`. +pub(super) const DEFAULT_MODEL_ID: &str = "eleven_v4"; pub(super) const MP3_FORMAT: &str = "mp3_44100_128"; const WAV_FORMAT: &str = "wav_44100"; diff --git a/cli/elevenlabs/workflow/skills.rs b/cli/elevenlabs/workflow/skills.rs index ca04304..7df9acc 100644 --- a/cli/elevenlabs/workflow/skills.rs +++ b/cli/elevenlabs/workflow/skills.rs @@ -162,6 +162,32 @@ elevenlabs feedback missing-capability "" ``` "#; +/// Appended to the named group skills, keyed by their emitted directory. Left +/// out, `model_id` falls to the API's default (`eleven_multilingual_v2` for +/// speech, `eleven_v3` for dialogue), which trails the model we recommend, and +/// the spec-derived files never name a model for an agent to choose instead. +const MODEL_HINTS: &[(&str, &str)] = &[ + ( + "elevenlabs-text-to-speech", + r#" +## Model + +Pass `--model-id eleven_v4`, the most expressive model, which also reads audio +tags like `[whispers]` as delivery. Without it the API falls back to an older +default. For real-time streaming, use `eleven_v4_turbo` instead. +"#, + ), + ( + "elevenlabs-text-to-dialogue", + r#" +## Model + +Pass `--model-id eleven_v4`, the most expressive model, which also reads audio +tags like `[laughs]` as delivery. Without it the API falls back to `eleven_v3`. +"#, + ), +]; + /// Every file `generate-skills` should write, spec-derived ones first. fn skill_files(ctx: &AppContext) -> Result, CliError> { let shared = PathBuf::from(SHARED_SKILL).join("SKILL.md"); @@ -169,6 +195,7 @@ fn skill_files(ctx: &AppContext) -> Result, CliError> { let mut appended = false; let mut pointers = 0usize; + let mut hinted = 0usize; for (path, content) in files.iter_mut() { if *path == shared { // Only when the emitter actually produced the no-auth text. If a @@ -180,6 +207,13 @@ fn skill_files(ctx: &AppContext) -> Result, CliError> { content.push_str(FEEDBACK_SECTION); appended = true; } else { + if let Some((_, hint)) = MODEL_HINTS + .iter() + .find(|(dir, _)| path.starts_with(dir)) + { + content.push_str(hint); + hinted += 1; + } content.push_str(GAP_POINTER); pointers += 1; } @@ -201,6 +235,15 @@ fn skill_files(ctx: &AppContext) -> Result, CliError> { ))); } + if hinted != MODEL_HINTS.len() { + // A group was renamed upstream, and its skill would silently go back + // to recommending nothing. + return Err(CliError::Other(anyhow::anyhow!( + "expected {} skills to carry a model hint, found {hinted}", + MODEL_HINTS.len() + ))); + } + files.extend(CUSTOM_SKILLS.iter().map(|(name, content)| { ( PathBuf::from(format!("{BIN_NAME}-{name}")).join("SKILL.md"), @@ -296,6 +339,14 @@ mod tests { ); } + #[test] + fn model_hints_recommend_v4() { + for (dir, hint) in MODEL_HINTS { + assert!(dir.starts_with(&format!("{BIN_NAME}-")), "{dir}"); + assert!(hint.contains("--model-id eleven_v4"), "{dir}"); + } + } + #[test] fn the_section_states_the_pii_rule() { assert!(FEEDBACK_SECTION.contains("Never put personal data")); diff --git a/cli/elevenlabs/workflow/skills/say.md b/cli/elevenlabs/workflow/skills/say.md index 7256209..d65bc2b 100644 --- a/cli/elevenlabs/workflow/skills/say.md +++ b/cli/elevenlabs/workflow/skills/say.md @@ -1,6 +1,6 @@ --- name: elevenlabs-say -description: Speak text aloud from the terminal with `elevenlabs say`. Use when asked to say/speak/read something out loud, announce or narrate a result, play text as audio, generate a quick voiceover or MP3 from text, make spoken output sound excited/whispered/emotional via v3 audio tags, or set a default voice/model/player for the CLI. +description: Speak text aloud from the terminal with `elevenlabs say`. Use when asked to say/speak/read something out loud, announce or narrate a result, play text as audio, generate a quick voiceover or MP3 from text, make spoken output sound excited/whispered/emotional via v4 audio tags, or set a default voice/model/player for the CLI. --- # `elevenlabs say` @@ -54,13 +54,14 @@ elevenlabs say config voice --unset # back to the built-in | Key | Config field | Built-in default | |---|---|---| | `voice` | `say.voice_id` | `JBFqnCBsd6RMkjVDRZzb` (George) | -| `model` | `say.model_id` | `eleven_v3` | +| `model` | `say.model_id` | `eleven_v4` | | `output-format` | `say.output_format` | follows the player | | `player` | `say.player` | first one found on `PATH` | -`eleven_v3` is the most expressive model and the only family that understands -the audio tags below. `eleven_flash_v2_5` is the swap when latency matters -more than delivery — roughly a second quicker to first audio. +`eleven_v4` is the most expressive model, and the v4 and v3 families are the +only ones that understand the audio tags below. `eleven_v4_turbo` is the swap +when latency matters more than delivery — it is built for real time and still +reads tags. `eleven_flash_v2_5` is quicker still, but reads tags as words. Find voice IDs with `elevenlabs voices search`, and model IDs with `elevenlabs models list`. @@ -79,10 +80,10 @@ elevenlabs say "one off" \ Precedence is always **flag > config file > built-in default**. -## Audio tags (v3 only) +## Audio tags (v4 and v3 only) -`eleven_v3` reads inline square-bracket directions as stage directions rather -than as words, which is what makes a line land as speech instead of narration: +`eleven_v4` (like `eleven_v4_turbo` and `eleven_v3`) reads inline +square-bracket directions as stage directions rather than as words, which is what makes a line land as speech instead of narration: ```bash elevenlabs say "[excited] The build passed!" @@ -99,7 +100,7 @@ Three rough families: - **Experimental** — `[strong French accent]`, `[sings]`, and effects such as `[applause]` or `[gunshot]` -Tags belong to the v3 family. On `eleven_flash_v2_5`, `eleven_turbo_v2_5` or +Tags belong to the v4 and v3 families. On `eleven_flash_v2_5`, `eleven_turbo_v2_5` or `eleven_multilingual_v2` they are not directions — they get read out as literal text, so `[excited] hello` is spoken as the word "excited" followed by "hello". diff --git a/cli/elevenlabs/workflow/templates.rs b/cli/elevenlabs/workflow/templates.rs index 93c0f27..f827d8b 100644 --- a/cli/elevenlabs/workflow/templates.rs +++ b/cli/elevenlabs/workflow/templates.rs @@ -39,7 +39,7 @@ const DEFAULT_TEMPLATE_JSON: &str = r##"{ "asr": { "quality": "high", "provider": "scribe_realtime", "user_input_audio_format": "pcm_16000", "keywords": [] }, "turn": { "turn_timeout": 7.0, "silence_end_call_timeout": -1.0, "mode": "turn" }, "tts": { - "model_id": "eleven_flash_v2", + "model_id": "eleven_v4_turbo", "voice_id": "cjVigY5qzO86Huf0OWal", "supported_voices": [], "agent_output_audio_format": "pcm_16000", @@ -174,7 +174,7 @@ fn minimal_template(name: &str) -> Value { "text_only": false }, "tts": { - "model_id": "eleven_flash_v2", + "model_id": "eleven_v4_turbo", "voice_id": "cjVigY5qzO86Huf0OWal" } },