Skip to content

fix(speak): honour the voice profile's language instead of forcing English - #1054

Open
Zeptoniator wants to merge 1 commit into
jamiepine:mainfrom
Zeptoniator:fix/speak-profile-language
Open

fix(speak): honour the voice profile's language instead of forcing English#1054
Zeptoniator wants to merge 1 commit into
jamiepine:mainfrom
Zeptoniator:fix/speak-profile-language

Conversation

@Zeptoniator

@Zeptoniator Zeptoniator commented Aug 17, 2026

Copy link
Copy Markdown

Problem

A voice profile carries a language field, but neither speak surface ever reads it. Both build their GenerationRequest with a hardcoded English fallback:

# backend/routes/speak.py
language=data.language or "en",

# backend/mcp_server/tools.py
language=language,   # -> _speak(...) -> language or "en"

So a profile created with language="fr" is still synthesised as English unless every caller passes language= explicitly.

This hurts the MCP path most. An agent calling voicebox.speak resolves its profile through the per-client binding — it never sees the profile object, so it has no way to know the profile's language and cannot pass the argument either. Every agent-triggered generation on a non-English profile comes out with an English accent.

Reproduction

Create the Kokoro French preset and bind it to a client:

curl -X POST localhost:17493/profiles -H 'Content-Type: application/json' \
  -d '{"name":"Siwis","language":"fr","voice_type":"preset",
       "preset_engine":"kokoro","preset_voice_id":"ff_siwis",
       "default_engine":"kokoro"}'

curl -X PUT localhost:17493/mcp/bindings -H 'Content-Type: application/json' \
  -d '{"client_id":"claude-code","profile_id":"<id>","default_engine":"kokoro"}'

curl -X POST localhost:17493/speak -H 'Content-Type: application/json' \
  -H 'X-Voicebox-Client-Id: claude-code' \
  -d '{"text":"Bonjour, je suis la voix française de Voicebox."}'

Before: "language": "en" — French text read with an English accent.
After: "language": "fr".

Fix

The fallback chain becomes explicit argument → resolved profile's language → "en", matching how engine and personality already consult the resolved binding.

- language=data.language or "en",
+ language=data.language or profile.language or "en",
- language=language,
+ language=language or vp.language,

The "en" backstop is kept, so profiles with no language set behave exactly as before.

Tests

Adds backend/tests/test_speak_language.py — 6 tests covering both surfaces (REST POST /speak and the voicebox.speak MCP tool):

  • falls back to the profile's language when the caller omits it
  • an explicit language= argument still wins over the profile
  • still defaults to "en" when the profile has no language

Verified the tests fail without the fix: the two fallback tests fail on the unpatched tree, the four precedence/backstop tests pass in both states as intended.

$ pytest backend/tests/test_speak_language.py -q
6 passed

$ pytest backend/tests/test_mcp_speak.py backend/tests/test_client_id_middleware.py \
         backend/tests/test_speak_language.py -q
26 passed

Manually verified end to end on Linux + CUDA with the Kokoro engine.

Notes

  • No CHANGELOG.md entry: the file header says it is compiled automatically during the release workflow and manual edits are overwritten. Happy to add one if maintainers prefer.
  • Unrelated, spotted while running the suite: backend/tests/test_profile_duplicate_names.py fails at collection on a clean checkout with ImportError: attempted relative import beyond top-level package. Not touched here.

Checklist

  • Code follows style guidelines
  • Changes tested
  • No breaking changes
  • Documentation updated — n/a, no user-facing surface change
  • CHANGELOG.md updated — see note above

Summary by CodeRabbit

  • Bug Fixes

    • Speech generation now automatically uses the selected voice profile’s language when no language is specified.
    • Falls back to English when neither an explicit language nor profile language is available.
    • Explicitly selected languages continue to take priority.
  • Tests

    • Added coverage for language selection across REST and MCP speech interfaces.

…glish

Both speak surfaces built their GenerationRequest with a hardcoded "en"
fallback and never consulted the resolved profile, so a profile created
with language="fr" was still synthesised as English unless the caller
passed language= explicitly.

This hurts the MCP path most: an agent calling voicebox.speak has no way
to know the bound profile's language, so it cannot pass the argument
either. Every agent-triggered generation on a non-English profile came
out with an English accent.

The fallback chain is now explicit argument -> resolved profile's
language -> "en", which matches how engine and personality already
consult the resolved binding. The "en" backstop is kept so profiles with
no language set behave exactly as before.

Adds backend/tests/test_speak_language.py covering both surfaces: the
fallback, explicit-argument precedence, and the unchanged "en" default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d3bdd54-383e-4cb4-88b0-107252b82373

📥 Commits

Reviewing files that changed from the base of the PR and between 51f49de and bc2de36.

📒 Files selected for processing (3)
  • backend/mcp_server/tools.py
  • backend/routes/speak.py
  • backend/tests/test_speak_language.py

Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The REST /speak endpoint and MCP voicebox.speak tool now select the requested language, the resolved voice profile language, or "en". Tests cover all precedence cases for both interfaces.

Changes

Speak language fallback

Layer / File(s) Summary
Language resolution
backend/routes/speak.py, backend/mcp_server/tools.py
Both speech paths use the explicit language first, then the resolved profile language, and finally "en".
Language resolution tests
backend/tests/test_speak_language.py
Tests verify explicit-language precedence, profile-language fallback, and "en" fallback for REST and MCP requests.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to bc2de

The PR makes both speak surfaces honor the voice profile's language while preserving explicit caller overrides and the English fallback; no actionable merge-blocking risk remains after normal checks and review.

Possibly related issues

  • jamiepine/voicebox issue 1049: The MCP language fallback change directly addresses the issue objective.

Possibly related PRs

  • jamiepine/voicebox#1050: It implements the same MCP profile-language fallback and relates to the added REST behavior and tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: using the voice profile's language instead of forcing English.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant