mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-30 17:34:27 +08:00
feat(minimax tts): expose all voice/audio parameters on the tts subcommand (#360)
* # feat(minimax tts): expose all voice/audio parameters on the tts subcommand ## Summary The MiniMax TTS backend (`cli_anything/minimax/utils/minimax_backend.py`) already supported a full set of voice and audio parameters, but the CLI exposed only `--text`, `--model`, `--voice`, and `--output`. Every other field was hardcoded, so users had to fork the harness to change speech speed, volume, pitch, sample rate, bitrate, audio format, or channel layout. This change promotes all seven hardcoded parameters to first-class CLI options on the `tts` subcommand, with click-level range and choice validation, and adds a regression test module. ## What changed ### `cli_anything/minimax/utils/minimax_backend.py` - `tts_synthesize(...)` gains 7 new parameters: `speed`, `vol`, `pitch`, `sample_rate`, `bitrate`, `audio_format`, `channel`. - The hardcoded `voice_setting` and `audio_setting` blocks now read from the new parameters. Defaults match the previous hardcoded values, so the change is fully backward compatible at the API level. ### `cli_anything/minimax/minimax_cli.py` - The `tts` Click command gains 7 new options: - `--speed` (FloatRange 0.5..2.0, default 1.0) - `--vol` (FloatRange 0.0..10.0, default 1.0) - `--pitch` (IntRange -12..12, default 0) - `--sample-rate` (Choice: 8000/16000/22050/24000/32000/44100, default 32000) - `--bitrate` (Choice: 32000/64000/128000/256000, default 128000) - `--format` (Choice: mp3/pcm/flac, default mp3) - `--channel` (Choice: 1/2, default 1) - The `tts_synthesize(...)` call is updated to forward the new options. ### `cli_anything/minimax/tests/test_tts_extended.py` (new) - 4 mock-based tests: - `test_tts_default_voice_audio_settings` — guards backward-compatible defaults - `test_tts_custom_voice_setting` — speed / vol / pitch propagation - `test_tts_custom_audio_setting` — sample_rate / bitrate / format / channel - `test_tts_voice_id_propagates` — regression guard for voice + speed combo ## Verification ```bash # Apply patches (from repo root) patch -p0 < pr-minimax-tts/01-backend.patch patch -p0 < pr-minimax-tts/02-cli.patch cp pr-minimax-tts/03-tests-test_tts_extended.py \ cli_anything/minimax/tests/test_tts_extended.py # Run the new tests cd minimax/agent-harness PYTHONPATH=. python3 -m pytest cli_anything/minimax/tests/test_tts_extended.py -v # 4 passed # Inspect the new surface PYTHONPATH=. python3 -m cli_anything.minimax.minimax_cli tts --help ``` ## Backward compatibility - API call signature gains keyword-only-ish params with default values identical to the previous hardcoded values, so any existing caller of `tts_synthesize(api_key, text, model, voice, output_path)` keeps working unchanged. - CLI behavior is unchanged when none of the new options are passed. ## Related - Skill surfaces this command under `cli-anything-minimax tts` — `skills/cli-anything-minimax/SKILL.md` should mention the new flags in a follow-up doc pass. * docs(minimax): document new tts options in README and SKILL.md Companion to #360. Surfaces the new --speed/--vol/--pitch/--sample-rate/ --bitrate/--format/--channel flags in both: - minimax/agent-harness/cli_anything/minimax/README.md (TTS options table) - skills/cli-anything-minimax/SKILL.md (TTS options block) No code change; just user-facing doc sync. --------- Co-authored-by: hhdhh <hhdhh@users.noreply.github.com>
This commit is contained in:
@@ -56,10 +56,32 @@ cli-anything-minimax tts --text "Hello, world!" --output hello.mp3
|
||||
# Use turbo model
|
||||
cli-anything-minimax tts --text "Fast speech" --model speech-2.8-turbo --output fast.mp3
|
||||
|
||||
# Fine-grained voice & audio control
|
||||
cli-anything-minimax tts --text "Slow and quiet" \
|
||||
--speed 0.8 --vol 0.5 --pitch -2 \
|
||||
--sample-rate 44100 --bitrate 256000 --format flac --channel 2 \
|
||||
--output slow.flac
|
||||
|
||||
# List available voices
|
||||
cli-anything-minimax voices
|
||||
```
|
||||
|
||||
#### TTS options
|
||||
|
||||
| Option | Range / Choices | Default | Description |
|
||||
|--------|-----------------|---------|-------------|
|
||||
| `--text` / `-t` | (required) | — | Text to synthesize |
|
||||
| `--model` | model id | `speech-2.8-hd` | TTS model |
|
||||
| `--voice` | voice id | `English_Graceful_Lady` | Voice preset |
|
||||
| `--output` / `-o` | path | `output.mp3` | Output audio file |
|
||||
| `--speed` | 0.5 .. 2.0 | `1.0` | Speech speed multiplier |
|
||||
| `--vol` | 0.0 .. 10.0 | `1.0` | Volume |
|
||||
| `--pitch` | -12 .. 12 | `0` | Pitch shift in semitones |
|
||||
| `--sample-rate` | 8000 / 16000 / 22050 / 24000 / 32000 / 44100 | `32000` | Audio sample rate |
|
||||
| `--bitrate` | 32000 / 64000 / 128000 / 256000 | `128000` | Audio bitrate |
|
||||
| `--format` | mp3 / pcm / flac | `mp3` | Output container |
|
||||
| `--channel` | 1 / 2 | `1` | 1 = mono, 2 = stereo |
|
||||
|
||||
### Session & Config
|
||||
|
||||
```bash
|
||||
|
||||
@@ -256,9 +256,59 @@ def stream(ctx, prompt, model_opt=None, temperature=None, max_tokens=None):
|
||||
show_default=True,
|
||||
help="Output audio file path",
|
||||
)
|
||||
@click.option(
|
||||
"--speed",
|
||||
type=click.FloatRange(0.5, 2.0),
|
||||
default=1.0,
|
||||
show_default=True,
|
||||
help="Speech speed (0.5-2.0).",
|
||||
)
|
||||
@click.option(
|
||||
"--vol",
|
||||
type=click.FloatRange(0.0, 10.0),
|
||||
default=1.0,
|
||||
show_default=True,
|
||||
help="Volume (0.0-10.0).",
|
||||
)
|
||||
@click.option(
|
||||
"--pitch",
|
||||
type=click.IntRange(-12, 12),
|
||||
default=0,
|
||||
show_default=True,
|
||||
help="Pitch shift in semitones (-12..12).",
|
||||
)
|
||||
@click.option(
|
||||
"--sample-rate",
|
||||
type=click.Choice(["8000", "16000", "22050", "24000", "32000", "44100"]),
|
||||
default="32000",
|
||||
show_default=True,
|
||||
help="Audio sample rate.",
|
||||
)
|
||||
@click.option(
|
||||
"--bitrate",
|
||||
type=click.Choice(["32000", "64000", "128000", "256000"]),
|
||||
default="128000",
|
||||
show_default=True,
|
||||
help="Audio bitrate.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"audio_format",
|
||||
type=click.Choice(["mp3", "pcm", "flac"]),
|
||||
default="mp3",
|
||||
show_default=True,
|
||||
help="Output audio format.",
|
||||
)
|
||||
@click.option(
|
||||
"--channel",
|
||||
type=click.Choice(["1", "2"]),
|
||||
default="1",
|
||||
show_default=True,
|
||||
help="1=mono, 2=stereo.",
|
||||
)
|
||||
@click.pass_context
|
||||
@handle_error
|
||||
def tts(ctx, text, model_opt, voice, output_path):
|
||||
def tts(ctx, text, model_opt, voice, output_path, speed, vol, pitch, sample_rate, bitrate, audio_format, channel):
|
||||
"""Synthesize text to speech using MiniMax TTS."""
|
||||
parent_key = ctx.obj.get("api_key") if ctx.obj else None
|
||||
api_key = get_api_key(parent_key)
|
||||
@@ -269,6 +319,13 @@ def tts(ctx, text, model_opt, voice, output_path):
|
||||
model=model_opt,
|
||||
voice=voice,
|
||||
output_path=output_path,
|
||||
speed=speed,
|
||||
vol=vol,
|
||||
pitch=pitch,
|
||||
sample_rate=int(sample_rate),
|
||||
bitrate=int(bitrate),
|
||||
audio_format=audio_format,
|
||||
channel=int(channel),
|
||||
)
|
||||
|
||||
output_data = {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Tests for the extended tts_synthesize parameters (speed / vol / pitch / audio settings).
|
||||
|
||||
The new params were previously hardcoded inside tts_synthesize and only the
|
||||
backend payload used them. These tests guard the new public surface.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from cli_anything.minimax.utils.minimax_backend import tts_synthesize
|
||||
|
||||
|
||||
def _empty_sse_post():
|
||||
"""Build a requests.post mock that returns an empty SSE stream."""
|
||||
mock_post = MagicMock()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.raise_for_status = lambda: None
|
||||
mock_resp.iter_content.return_value = []
|
||||
mock_post.return_value = mock_resp
|
||||
return mock_post
|
||||
|
||||
|
||||
def _captured_payload(mock_post):
|
||||
return mock_post.call_args.kwargs["json"]
|
||||
|
||||
|
||||
def test_tts_default_voice_audio_settings():
|
||||
with patch("requests.post", _empty_sse_post()) as mock_post:
|
||||
tts_synthesize(api_key="key", text="hi")
|
||||
payload = _captured_payload(mock_post)
|
||||
|
||||
assert payload["voice_setting"]["speed"] == 1.0
|
||||
assert payload["voice_setting"]["vol"] == 1.0
|
||||
assert payload["voice_setting"]["pitch"] == 0
|
||||
assert payload["audio_setting"]["sample_rate"] == 32000
|
||||
assert payload["audio_setting"]["bitrate"] == 128000
|
||||
assert payload["audio_setting"]["format"] == "mp3"
|
||||
assert payload["audio_setting"]["channel"] == 1
|
||||
|
||||
|
||||
def test_tts_custom_voice_setting():
|
||||
with patch("requests.post", _empty_sse_post()) as mock_post:
|
||||
tts_synthesize(api_key="key", text="hi", speed=1.5, vol=3.0, pitch=4)
|
||||
payload = _captured_payload(mock_post)
|
||||
|
||||
assert payload["voice_setting"]["speed"] == 1.5
|
||||
assert payload["voice_setting"]["vol"] == 3.0
|
||||
assert payload["voice_setting"]["pitch"] == 4
|
||||
|
||||
|
||||
def test_tts_custom_audio_setting():
|
||||
with patch("requests.post", _empty_sse_post()) as mock_post:
|
||||
tts_synthesize(
|
||||
api_key="key",
|
||||
text="hi",
|
||||
sample_rate=44100,
|
||||
bitrate=256000,
|
||||
audio_format="flac",
|
||||
channel=2,
|
||||
)
|
||||
payload = _captured_payload(mock_post)
|
||||
|
||||
assert payload["audio_setting"]["sample_rate"] == 44100
|
||||
assert payload["audio_setting"]["bitrate"] == 256000
|
||||
assert payload["audio_setting"]["format"] == "flac"
|
||||
assert payload["audio_setting"]["channel"] == 2
|
||||
|
||||
|
||||
def test_tts_voice_id_propagates():
|
||||
with patch("requests.post", _empty_sse_post()) as mock_post:
|
||||
tts_synthesize(
|
||||
api_key="key", text="hi", voice="English_Lucky_Robot", speed=0.8
|
||||
)
|
||||
payload = _captured_payload(mock_post)
|
||||
|
||||
assert payload["voice_setting"]["voice_id"] == "English_Lucky_Robot"
|
||||
assert payload["voice_setting"]["speed"] == 0.8
|
||||
@@ -183,6 +183,13 @@ def tts_synthesize(
|
||||
model: str = "speech-2.8-hd",
|
||||
voice: str = "English_Graceful_Lady",
|
||||
output_path: Optional[str] = None,
|
||||
speed: float = 1.0,
|
||||
vol: float = 1.0,
|
||||
pitch: int = 0,
|
||||
sample_rate: int = 32000,
|
||||
bitrate: int = 128000,
|
||||
audio_format: str = "mp3",
|
||||
channel: int = 1,
|
||||
) -> bytes:
|
||||
"""Synthesize text to speech using MiniMax TTS API (SSE stream, hex-encoded audio)."""
|
||||
api_key = _require_api_key(api_key)
|
||||
@@ -193,15 +200,15 @@ def tts_synthesize(
|
||||
"stream": True,
|
||||
"voice_setting": {
|
||||
"voice_id": voice,
|
||||
"speed": 1,
|
||||
"vol": 1,
|
||||
"pitch": 0,
|
||||
"speed": speed,
|
||||
"vol": vol,
|
||||
"pitch": pitch,
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 32000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"channel": 1,
|
||||
"sample_rate": sample_rate,
|
||||
"bitrate": bitrate,
|
||||
"format": audio_format,
|
||||
"channel": channel,
|
||||
},
|
||||
}
|
||||
try:
|
||||
|
||||
@@ -45,6 +45,13 @@ cli-anything-minimax tts --text "Hello world" --output hello.mp3
|
||||
cli-anything-minimax --json chat --prompt "Hello"
|
||||
```
|
||||
|
||||
#### TTS options
|
||||
|
||||
`--text` (required) / `--model` / `--voice` / `--output` plus voice and audio
|
||||
controls: `--speed` (0.5..2.0), `--vol` (0..10), `--pitch` (-12..12),
|
||||
`--sample-rate` (8k/16k/22.05k/24k/32k/44.1k), `--bitrate` (32k/64k/128k/256k),
|
||||
`--format` (mp3/pcm/flac), `--channel` (1/2). See `tts --help` for defaults.
|
||||
|
||||
## Command Groups
|
||||
|
||||
### Chat
|
||||
|
||||
Reference in New Issue
Block a user