Offer Eleven v3 alongside the v2 models, each with its own settings - #148
Merged
Conversation
…ings Eleven v3 was deliberately absent from the catalog because it could not return character alignment, and word highlighting depends on it. Probing the live API shows that is no longer true: both eleven_v3 and eleven_v3_conversational return alignment from /with-timestamps at pcm_24000, so they can join the picker without losing read-along. The two generations are grouped in Settings and every v2 model stays. Flash v2.5 remains the default — half the credits, and steadiest on the short passages the segmenter produces. Each model now carries its own synthesis knobs, persisted per model id in the elevenlabs-model-options setting, and only the ones that model actually honours are shown. The support matrix comes from GET /v1/models plus measurement, not the docs: v3 accepts `speed` and ignores it (identical audio at 0.7 and 1.2, where Flash tracked both exactly), and reports can_use_style false, so v3 exposes stability alone — the three named points ElevenLabs documents. v3 Conversational adds speaker boost; Multilingual v2 exposes all five. Cached audio is preserved. Untouched options send no voice_settings at all, which is exactly what the code did before, and elevenLabsRevision() returns the bare model id so existing cache entries still match. Moving any knob appends a sorted suffix to the revision, which flows into the audio cache key and re-synthesizes that variant. The wire always receives the bare model id. Also guards a request longer than the model's limit (v3 caps at 5,000 characters against Flash's 40,000) before spending credits, and persists the options with $state.snapshot — nested $state proxies cannot be structured-cloned into IndexedDB, and the resulting DataCloneError rejected silently while the UI looked correct until a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Eleven v3 is now selectable. It was deliberately excluded from the catalog because it could not return character alignment, and read-along highlighting depends on it. That is no longer true — probing the live API shows both
eleven_v3andeleven_v3_conversationalreturn alignment from/with-timestampsatpcm_24000.The picker in Settings → Voice now groups two generations. Nothing was removed: Flash v2.5, Turbo v2.5, and Multilingual v2 all stay, and Flash v2.5 remains the default — half the credits, and steadiest on the ~280-character passages the segmenter produces. Switching to v3 is one click.
Each model carries its own settings, persisted per model id in the new
elevenlabs-model-optionssetting. You can leave v3 on Creative while Multilingual keeps speaker boost off; the two don't interfere.Only the knobs a model actually honours are rendered. The support matrix comes from
GET /v1/modelsplus measurement, not from the docs:Notable findings behind that table, all measured against the live API rather than read off documentation:
speedand ignores it. 0.7 and 1.2 both produced identical 2.88 s audio, where Flash tracked them exactly (4.04 s vs 2.32 s). Exposing a speed slider for v3 would have been a lie, so it is off.can_use_style: false, and plain v3 reportscan_use_speaker_boost: false.previous_text/next_textwithunsupported_model, so the usual long-form stitching trick is unavailable there.Why the cache is safe
This is the part most worth a careful look. Existing users must not silently re-generate (and re-pay for) audio they already have.
voice_settingsat all — byte-for-byte what the code did before this PR, which means the voice's own saved settings still apply.elevenLabsRevision()returns the bare model id, so every pre-existingAudioVariantMetastill matches and coverage resolves exactly as before.#k=v,…suffix. That flows throughspeechVariant().revisionintovariantSignature(),matchesCurrentVariant(), andcacheKey(), so only the tuned variant re-synthesizes.modelId; sendingeleven_v3#stability=1would be a 400.Setting a knob back to its default collapses the revision to the bare id again and the original cached audio is reused.
Other fixes in this PR
$state.snapshotwhen persisting. Nested$stateproxies cannot be structured-cloned into IndexedDB. The first version of this code threwDataCloneErrorinside a floating promise: the UI updated correctly and nothing persisted, visible only after a reload. Caught by driving the live preview — types and tests were green.Verification
npm run lintnpm run check— 1838 files, 0 errors, 0 warningsnpm run test:coverage— 464 unit tests, thresholds metnpm run buildnpm run test:e2e— 44 passed{ model_id: "eleven_v3", voice_settings: { stability: 0 } }— bare model id on the wire, only the field v3 honours.eleven_v3heldstability: 0whileeleven_multilingual_v2heldspeakerBoost: false, simultaneously, surviving reload.New tests cover stability snapping, clamping, unsupported-knob suppression, the untouched-sends-nothing invariant, per-model request bodies, revision stability, and the character-limit guard.
Review checklist
voice_settingsobject, sent only when the user has tuned something.elevenlabs-model-options), absent-reads-as-{}, no migration needed. No stored audio is invalidated by this PR alone. No new model licenses.input[type=range]inside<label>; the v3 stability picker is arole="group"of buttons witharia-pressed, matching the existing model picker; the checkbox row keeps the 44px touch target. Colors reuse the existing settings tokens, so light/dark and contrast are inherited. No new animation..engine-modelcard styling, so it reads as part of that section rather than as a new surface.🤖 Generated with Claude Code