Skip to content

fix(gradium): stop voice_id default from overriding an explicit voice (1/2) - #7813

Merged
longcw merged 1 commit into
livekit:mainfrom
hhugo-gradium:fix/gradium-tts-voice-selection
Oct 20, 2026
Merged

longcw merged 1 commit into
livekit:mainfrom
hhugo-gradium:fix/gradium-tts-voice-selection

Conversation

@hhugo-gradium

@hhugo-gradium hhugo-gradium commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

TTS(voice="...") was silently ignored. voice_id defaulted to a fixed id and the setup message sent both voice and voice_id whenever set. The Gradium API resolves voice_id to an embedding first and only falls back to the voice name when there is no embedding, so an explicit voice never reached the model, and update_options(voice=...) had the same problem.

  • voice and voice_id both default to None; the previous default id is applied only when neither is given (DEFAULT_VOICE_ID).
  • update_options(voice=...) clears voice_id and vice versa.
  • Behaviour change: passing both to the constructor or to update_options now raises ValueError. It used to be accepted, with voice discarded.

What each default means

  • No selector: voice_id is DEFAULT_VOICE_ID, the catalog voice "Harper", unchanged from before. feat(gradium): default the TTS voice to Everly #7874 (2/2), stacked on this PR, changes that default to Everly.
  • voice="default": the embedding packaged with the TTS model itself. This is a different speaker from Harper, and it is the only voice name the public API can reach today, since other names must be loaded into the worker directly. voice_id is the right selector for catalog and custom voices.

Testing

  • New tests/test_plugin_gradium_tts_voice.py (unit category, 7 tests) covering the default, each selector alone, the exclusivity error, and both update_options directions.
  • ruff, ruff format --check and mypy --strict clean on the plugin.
  • Live against api.gradium.ai: on main, TTS(voice="does-not-exist") synthesizes normally because the default voice_id wins; on this branch the server rejects it. Until the error-surfacing change in fix(gradium): surface TTS server errors, close codes and transport errors (1/2) #7672 lands, that rejection reads as the generic "no audio frames were pushed" rather than the server's own message.

Split out of #7672.

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Oct 14, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

`voice_id` defaulted to a fixed id and the setup message sent both
`voice` and `voice_id` whenever set. The server resolves `voice_id` to
an embedding first and only falls back to the `voice` name when there is
none, so `TTS(voice="...")` and `update_options(voice=...)` silently kept
the default voice.

Default both to None, apply the default id only when neither is given,
reject passing both, and have update_options clear the other selector.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hhugo-gradium
hhugo-gradium force-pushed the fix/gradium-tts-voice-selection branch from 283af3e to b93b771 Compare October 14, 2026 11:34
@hhugo-gradium
hhugo-gradium marked this pull request as ready for review October 14, 2026 12:31
@hhugo-gradium
hhugo-gradium requested a review from a team as a code owner October 14, 2026 12:31

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +109 to +110
if voice is None and voice_id is None:
voice_id = DEFAULT_VOICE_ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Explicit null selects the catalog voice

With voice_id=None and no voice, TTS substitutes Harper's ID instead of omitting both selectors. The provider uses Harper rather than its model-default voice.

Learn more

A Gradium setup message includes voice_id only when its stored value is not None in ChunkedStream._run and SynthesizeStream._run_ws. Previously, passing voice_id=None with no name left both selectors absent, allowing the provider to choose its model voice. The new fallback treats an explicit None exactly like an omitted argument and inserts the catalog ID.

Example: An agent constructs TTS(api_key="...", voice_id=None) to use its deployed model voice. The setup now sends voice_id="4SZHfMpw-p46Ywgs" and selects Harper instead.

Recommended fix: Distinguish an omitted voice_id from explicit None using a sentinel default, and apply DEFAULT_VOICE_ID only for the omitted case when voice is absent. Keep explicit None as an absent selector in both setup messages.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@hhugo-gradium hhugo-gradium changed the title fix(gradium): stop voice_id default from overriding an explicit voice fix(gradium): stop voice_id default from overriding an explicit voice (1/2) Oct 16, 2026
@hhugo-gradium

Copy link
Copy Markdown
Contributor Author

cc @longcw

@longcw
longcw merged commit 077f3dd into livekit:main Oct 20, 2026
17 checks passed
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.

3 participants