Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions tests/tools/test_tts_speed.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,58 @@ def test_speed_clamped_high(self, tmp_path, monkeypatch):
assert kwargs["speed"] == 4.0


# ---------------------------------------------------------------------------
# OpenAI TTS language (lang_code for OpenAI-compatible endpoints)
# ---------------------------------------------------------------------------

class TestOpenaiTtsLangCode:
def _run(self, tts_config, tmp_path, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
mock_response = MagicMock()
mock_client = MagicMock()
mock_client.audio.speech.create.return_value = mock_response
mock_cls = MagicMock(return_value=mock_client)

with patch("tools.tts_tool._import_openai_client", return_value=mock_cls), \
patch("tools.tts_tool._resolve_openai_audio_client_config",
return_value=("test-key", None, False)):
from tools.tts_tool import _generate_openai_tts
_generate_openai_tts("Hola", str(tmp_path / "out.mp3"), tts_config)
return mock_client.audio.speech.create

def test_default_no_extra_body(self, tmp_path, monkeypatch):
"""No language config => no extra_body kwarg in create call."""
create = self._run({}, tmp_path, monkeypatch)
kwargs = create.call_args[1]
assert "extra_body" not in kwargs

def test_language_forwarded_as_lang_code(self, tmp_path, monkeypatch):
"""tts.openai.language is forwarded as extra_body lang_code."""
create = self._run({"openai": {"language": "es"}}, tmp_path, monkeypatch)
kwargs = create.call_args[1]
assert kwargs["extra_body"] == {"lang_code": "es"}

def test_empty_language_omitted(self, tmp_path, monkeypatch):
"""Empty language string => extra_body omitted."""
create = self._run({"openai": {"language": ""}}, tmp_path, monkeypatch)
kwargs = create.call_args[1]
assert "extra_body" not in kwargs

def test_global_language_not_forwarded(self, tmp_path, monkeypatch):
"""Only tts.openai.language is honored, not a top-level tts.language."""
create = self._run({"language": "es"}, tmp_path, monkeypatch)
kwargs = create.call_args[1]
assert "extra_body" not in kwargs

def test_language_coexists_with_speed(self, tmp_path, monkeypatch):
"""language and speed are forwarded independently."""
create = self._run({"openai": {"language": "es", "speed": 2.0}},
tmp_path, monkeypatch)
kwargs = create.call_args[1]
assert kwargs["extra_body"] == {"lang_code": "es"}
assert kwargs["speed"] == 2.0


# ---------------------------------------------------------------------------
# MiniMax TTS (t2a_v2 endpoint: nested voice_setting/audio_setting,
# JSON response with hex-encoded audio. Falls back to the legacy
Expand Down
3 changes: 3 additions & 0 deletions tools/tts_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ def _generate_openai_tts(text: str, output_path: str, tts_config: Dict[str, Any]
if custom_base_url:
base_url = custom_base_url
speed = float(oai_config.get("speed", tts_config.get("speed", 1.0)))
language = oai_config.get("language")

# The managed OpenAI audio gateway only proxies MANAGED_OPENAI_TTS_MODELS.
# A model set for direct OpenAI (e.g. "tts-1-hd") 400s there with
Expand Down Expand Up @@ -1065,6 +1066,8 @@ def _generate_openai_tts(text: str, output_path: str, tts_config: Dict[str, Any]
}
if speed != 1.0:
create_kwargs["speed"] = max(0.25, min(4.0, speed))
if language:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add a regression test for this branch using the existing mocked OpenAI TTS kwargs harness in tests/tools/test_tts_speed.py: assert extra_body == {"lang_code": "es"} when configured and that it is omitted when language is unset.

create_kwargs["extra_body"] = {"lang_code": language}
response = client.audio.speech.create(**create_kwargs)

response.stream_to_file(output_path)
Expand Down
3 changes: 3 additions & 0 deletions website/docs/user-guide/features/tts.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ tts:
voice: "alloy" # alloy, echo, fable, onyx, nova, shimmer
base_url: "https://api.openai.com/v1" # Override for OpenAI-compatible TTS endpoints
speed: 1.0 # 0.25 - 4.0
# language: "es" # Sent as lang_code — only for OpenAI-compatible endpoints that support it (e.g. Kokoro)
minimax:
model: "speech-2.8-hd" # speech-2.8-hd (default), speech-2.8-turbo
voice_id: "English_Graceful_Lady" # See https://platform.minimax.io/faq/system-voice-id
Expand Down Expand Up @@ -99,6 +100,8 @@ tts:

**Speed control**: The global `tts.speed` value applies to all providers by default. Each provider can override it with its own `speed` setting (e.g., `tts.openai.speed: 1.5`). Provider-specific speed takes precedence over the global value. Default is `1.0` (normal speed).

**Language (OpenAI-compatible endpoints)**: `tts.openai.language` is forwarded to the endpoint as a `lang_code` request parameter. It is intended for OpenAI-compatible TTS servers that support `lang_code` — for example [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI), where `language: "es"` selects the Spanish phonemizer instead of the English default. Leave it unset when using the official OpenAI API, which does not accept this parameter. When unset, nothing extra is sent.

### Gemini Persona Prompts

Gemini TTS can follow natural-language performance direction. Set `tts.gemini.persona_prompt_file` to a local Markdown or text file that describes the voice persona. The file can include Gemini-style sections such as `AUDIO PROFILE`, `SCENE`, `DIRECTOR'S NOTES`, `SAMPLE CONTEXT`, and `TRANSCRIPT`.
Expand Down