Skip to content

fix(tts): make the ElevenLabs voice id field drive the voice that plays - #156

Merged
dyascj merged 1 commit into
mainfrom
fix/elevenlabs-custom-voice
Aug 25, 2026
Merged

fix(tts): make the ElevenLabs voice id field drive the voice that plays#156
dyascj merged 1 commit into
mainfrom
fix/elevenlabs-custom-voice

Conversation

@dyascj

@dyascj dyascj commented Aug 25, 2026

Copy link
Copy Markdown
Member

Fixes #153.

Root cause

Selecting ElevenLabs (settings dropdown or onboarding) sets speech.activeVoiceId to the first declared voice, Rachel 21m00Tcm4TlvDq8ikWAM. The "Custom Voice ID" box wrote to a different slot, providerConfigs.elevenlabs.voiceId, and the pipeline resolved activeVoiceId || ttsConfig.voiceId. Rachel is never empty, so every pasted id was shadowed. Reproduced against a mock ElevenLabs with the reporter's exact stored state: three requests to text-to-speech/21m00Tcm4TlvDq8ikWAM/stream while the custom id sat unused. That matches "ElevenLabs receives the requests, the voice does not change".

Change

  • The ElevenLabs voice field edits activeVoiceId through handleTTSVoiceChange, the same path every other provider uses, with the declared voices as a native datalist. Onboarding twin gets the same treatment.
  • One-time migration at startup (legacy-voice-migration.ts, decision in legacyVoiceToAdopt): if a legacy custom id exists and the active voice is empty or still the provider default, it moves across. The legacy slot is cleared either way so the migration can never fire twice or apply an ElevenLabs voice to another provider. Users who had a legacy id but are on a different TTS provider lose an id that was never in effect and will see the real slot when they switch back.
  • Pipeline reads one slot again; the dead getter and setter are gone from the settings store.
  • Second symptom from the screenshots: the TTS settings state now seeds its model list from the cache on init, so ElevenLabs shows the saved model instead of "Select model..." until someone hits refresh.

Verification

  • pnpm test 440 pass (5 new for legacyVoiceToAdopt), pnpm check 0 errors 0 warnings.
  • Live against the real ElevenLabs API on a Creator account:
    • Migration: seeded activeVoiceId Rachel plus a legacy custom id, reloaded, activeVoiceId became the custom id and the legacy slot was removed, API key kept.
    • Playback: chat turn produced three text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream calls (George, not in our declared list), all 200 audio/mpeg.
    • Fresh user: picked ElevenLabs from the dropdown (voice resets to Rachel, field shows it), pasted the custom id, activeVoiceId updated, model dropdown showed "Eleven Turbo v2.5" after refresh and again after a full reload without refresh.
    • Invalid id: request goes out with the typed id, ElevenLabs returns 400, the chat bar shows "ElevenLabs error 400: An invalid ID has been received for voice". Before this fix a bad id never left the machine.
    • Onboarding: Services step field shows the current voice, typing writes it trimmed, datalist present.

Notes

  • Touches the same voiceId: line in companion-chat.ts as Feature/multilingual tts #154; whichever merges second gets a one-line conflict.
  • Not in scope, worth their own issues: the desktop "Download Save File" button in the reporter's fourth screenshot does nothing under WebView2 (db/export.ts uses an anchor download), and ElevenLabsTTS defaults to Bella when the voice is empty while the registry's first voice is Rachel.

The ElevenLabs custom voice box wrote to the provider config while the
speech pipeline read the speech module's activeVoiceId, which the provider
switch had already set to the first declared voice. Every pasted id was
shadowed, so ElevenLabs users always heard Rachel (issue #153).

- the field now edits activeVoiceId through handleTTSVoiceChange like every
  other provider, with the declared voices offered as a datalist; same in the
  onboarding twin
- one-time migration at startup moves a legacy custom id into activeVoiceId
  when the active voice is still the provider default, then clears the old
  slot so it can never re-fire
- the pipeline reads a single slot again; the settings store loses the
  dead getter and setter
- the TTS settings state seeds its model list from the cache on init, so a
  provider without static models shows the saved model instead of the
  placeholder until refresh
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
utsuwa Ready Ready Preview Aug 25, 2026 1:58pm

Request Review

@dyascj
dyascj merged commit 2c79469 into main Aug 25, 2026
4 checks passed
@dyascj
dyascj deleted the fix/elevenlabs-custom-voice branch August 25, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] unable to change the voice through ElevenLabs, even if api key and voice id is good and ElevenLabs receive the requests

1 participant