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:
kk
2026-07-09 20:13:36 +08:00
committed by GitHub
parent 5711460831
commit be964a5f25
5 changed files with 179 additions and 8 deletions
@@ -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:
+7
View File
@@ -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