feat(tts): expose speed parameter in text_to_speech tool
Add optional 'speed' parameter (0.25-4.0) to the text_to_speech tool schema and handler. When provided by the model, it overrides the config-level tts.speed setting, enabling per-request speed control without config changes. Use cases: - Language learning: slow playback (0.5x) for pronunciation practice - Accessibility: adjustable speed for hearing preferences - Content review: accelerated playback for long text The speed value is clamped to [0.25, 4.0] and injected into tts_config before dispatching to any provider (Edge, OpenAI, MiniMax, etc.), so all existing provider-level speed handling works transparently. Includes 3 new tests for tool-level speed injection, clamping, and config preservation when speed is not specified.
This commit is contained in:
parent
d9336e7453
commit
8171e8ebb3
|
|
@ -238,3 +238,70 @@ class TestMinimaxTtsLegacyTextToSpeech:
|
|||
_, output = self._run({}, tmp_path, monkeypatch)
|
||||
with open(output, "rb") as f:
|
||||
assert f.read() == b"\x00\x01\x02\x03"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool-level speed parameter (text_to_speech_tool speed injection)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestToolLevelSpeed:
|
||||
"""Verify that the speed parameter on text_to_speech_tool injects into config."""
|
||||
|
||||
def test_speed_injected_into_config(self, tmp_path, monkeypatch):
|
||||
"""When speed is passed to the tool, it overrides config speed."""
|
||||
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)), \
|
||||
patch("tools.tts_tool._load_tts_config", return_value={"provider": "openai", "openai": {}}), \
|
||||
patch("tools.tts_tool._get_provider", return_value="openai"), \
|
||||
patch("tools.tts_tool._resolve_command_provider_config", return_value=None), \
|
||||
patch("tools.tts_tool._resolve_max_text_length", return_value=4096), \
|
||||
patch("tools.tts_tool._generate_openai_tts") as mock_gen, \
|
||||
patch("gateway.session_context.get_session_env", return_value=""):
|
||||
from tools.tts_tool import text_to_speech_tool
|
||||
text_to_speech_tool("Hello", str(tmp_path / "out.mp3"), speed=0.7)
|
||||
|
||||
# Verify the tts_config passed to the generator has speed=0.7
|
||||
call_args = mock_gen.call_args
|
||||
config_passed = call_args[0][2] # (text, output_path, tts_config)
|
||||
assert config_passed["speed"] == 0.7
|
||||
|
||||
def test_speed_clamped_range(self, tmp_path, monkeypatch):
|
||||
"""Speed values outside 0.25-4.0 are clamped."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||
|
||||
with patch("tools.tts_tool._load_tts_config", return_value={"provider": "openai", "openai": {}}), \
|
||||
patch("tools.tts_tool._get_provider", return_value="openai"), \
|
||||
patch("tools.tts_tool._resolve_command_provider_config", return_value=None), \
|
||||
patch("tools.tts_tool._resolve_max_text_length", return_value=4096), \
|
||||
patch("tools.tts_tool._generate_openai_tts") as mock_gen, \
|
||||
patch("gateway.session_context.get_session_env", return_value=""):
|
||||
from tools.tts_tool import text_to_speech_tool
|
||||
text_to_speech_tool("Hello", str(tmp_path / "out.mp3"), speed=10.0)
|
||||
|
||||
config_passed = mock_gen.call_args[0][2]
|
||||
assert config_passed["speed"] == 4.0
|
||||
|
||||
def test_no_speed_preserves_config(self, tmp_path, monkeypatch):
|
||||
"""When speed is None, config is not mutated."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||
original_config = {"provider": "openai", "openai": {}, "speed": 1.5}
|
||||
|
||||
with patch("tools.tts_tool._load_tts_config", return_value=original_config), \
|
||||
patch("tools.tts_tool._get_provider", return_value="openai"), \
|
||||
patch("tools.tts_tool._resolve_command_provider_config", return_value=None), \
|
||||
patch("tools.tts_tool._resolve_max_text_length", return_value=4096), \
|
||||
patch("tools.tts_tool._generate_openai_tts") as mock_gen, \
|
||||
patch("gateway.session_context.get_session_env", return_value=""):
|
||||
from tools.tts_tool import text_to_speech_tool
|
||||
text_to_speech_tool("Hello", str(tmp_path / "out.mp3"), speed=None)
|
||||
|
||||
config_passed = mock_gen.call_args[0][2]
|
||||
assert config_passed.get("speed") == 1.5 # original config preserved
|
||||
assert original_config.get("speed") == 1.5 # original not mutated
|
||||
|
|
|
|||
|
|
@ -2567,6 +2567,7 @@ def _generate_kittentts(text: str, output_path: str, tts_config: Dict[str, Any])
|
|||
def text_to_speech_tool(
|
||||
text: str,
|
||||
output_path: Optional[str] = None,
|
||||
speed: Optional[float] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Convert text to speech audio.
|
||||
|
|
@ -2581,6 +2582,7 @@ def text_to_speech_tool(
|
|||
Args:
|
||||
text: The text to convert to speech.
|
||||
output_path: Optional custom save path. Defaults to ~/voice-memos/<timestamp>.mp3
|
||||
speed: Optional playback speed multiplier (0.25-4.0). Overrides config.yaml.
|
||||
|
||||
Returns:
|
||||
str: JSON result with success, file_path, and optionally MEDIA tag.
|
||||
|
|
@ -2597,6 +2599,13 @@ def text_to_speech_tool(
|
|||
return tool_error("Text is empty after TTS cleanup", success=False)
|
||||
|
||||
tts_config = _load_tts_config()
|
||||
|
||||
# When the model supplies a speed parameter, inject it into the config
|
||||
# so all downstream provider functions pick it up uniformly.
|
||||
if speed is not None:
|
||||
clamped = max(0.25, min(4.0, float(speed)))
|
||||
tts_config = dict(tts_config) # shallow copy to avoid mutating the cache
|
||||
tts_config["speed"] = clamped
|
||||
provider = _get_provider(tts_config)
|
||||
|
||||
# User-declared command provider (type: command under tts.providers.<name>)
|
||||
|
|
@ -3320,6 +3329,10 @@ TTS_SCHEMA = {
|
|||
"output_path": {
|
||||
"type": "string",
|
||||
"description": f"Optional custom file path to save the audio. Defaults to {display_hermes_home()}/audio_cache/<timestamp>.mp3"
|
||||
},
|
||||
"speed": {
|
||||
"type": "number",
|
||||
"description": "Playback speed multiplier. 1.0 = normal, 0.5 = very slow (language learning), 2.0 = fast. Range: 0.25-4.0. Overrides the speed configured in config.yaml."
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
|
|
@ -3332,7 +3345,8 @@ registry.register(
|
|||
schema=TTS_SCHEMA,
|
||||
handler=lambda args, **kw: text_to_speech_tool(
|
||||
text=args.get("text", ""),
|
||||
output_path=args.get("output_path")),
|
||||
output_path=args.get("output_path"),
|
||||
speed=args.get("speed")),
|
||||
check_fn=check_tts_requirements,
|
||||
emoji="🔊",
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue