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
58 changes: 58 additions & 0 deletions sdk/python/agentfield/media_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,64 @@ async def clone_voice(
raw_response={"upload": upload_data, "clone": clone_data},
)

async def design_voice(
self,
prompt: str,
voice_id: str,
model: Optional[str] = None,
**kwargs,
) -> MultimodalResponse:
"""Create a voice from a natural-language description."""
import os

import aiohttp

api_key = self._api_key or os.environ.get("MINIMAX_API_KEY")
if not api_key:
raise ValueError(
"MiniMax API key required. Set MINIMAX_API_KEY or pass api_key "
"to MiniMaxProvider."
)
if not isinstance(prompt, str) or not prompt.strip():
raise ValueError("MiniMax voice design requires a prompt")
if not isinstance(voice_id, str) or not voice_id.strip():
raise ValueError("MiniMax voice design requires a voice_id")

body: Dict[str, Any] = {"prompt": prompt, "voice_id": voice_id}
if model is not None:
send_model = self._strip_prefix(model).strip()
if not send_model:
raise ValueError("MiniMax voice design requires a model")
body["model"] = send_model
body.update(kwargs)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
timeout = aiohttp.ClientTimeout(total=120.0)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(
f"{self._base_url}/voice_design",
headers=headers,
json=body,
) as response:
data = await self._read_response(
response, "design", service="voice design"
)

designed_voice_id = data.get("voice_id")
if not designed_voice_id and isinstance(data.get("data"), dict):
designed_voice_id = data["data"].get("voice_id")
if not isinstance(designed_voice_id, str) or not designed_voice_id:
raise RuntimeError("MiniMax voice design returned no voice_id")
return MultimodalResponse(
text=designed_voice_id,
audio=None,
images=[],
files=[],
raw_response=data,
)

async def generate_music(
self,
prompt: str,
Expand Down
78 changes: 78 additions & 0 deletions sdk/python/tests/test_minimax_voice_design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pytest

from agentfield.media_providers import MINIMAX_CN_BASE_URL, MINIMAX_GLOBAL_BASE_URL, MiniMaxProvider


class FakeResponse:
def __init__(self, payload, status=200):
self.payload = payload
self.status = status

async def json(self):
return self.payload

async def text(self):
return str(self.payload)

async def __aenter__(self):
return self

async def __aexit__(self, *args):
return False


class CaptureSession:
def __init__(self, response):
self.response = response
self.calls = []

def post(self, url, **kwargs):
self.calls.append((url, kwargs))
return self.response

async def __aenter__(self):
return self

async def __aexit__(self, *args):
return False


@pytest.mark.asyncio
@pytest.mark.parametrize("base_url", [MINIMAX_GLOBAL_BASE_URL, MINIMAX_CN_BASE_URL])
async def test_design_voice_posts_prompt_and_voice_id(monkeypatch, base_url):
session = CaptureSession(
FakeResponse({"voice_id": "designed-voice", "base_resp": {"status_code": 0}})
)
monkeypatch.setattr("aiohttp.ClientSession", lambda **kwargs: session)

result = await MiniMaxProvider(api_key="unit-value", base_url=base_url).design_voice(
prompt="A calm, warm narrator",
voice_id="narrator-1",
)

assert session.calls[0][0] == f"{base_url}/voice_design"
assert session.calls[0][1]["headers"]["Authorization"] == "Bearer unit-value"
assert session.calls[0][1]["json"] == {
"prompt": "A calm, warm narrator",
"voice_id": "narrator-1",
}
assert result.text == "designed-voice"
assert result.raw_response["base_resp"]["status_code"] == 0


@pytest.mark.asyncio
async def test_design_voice_validates_inputs_and_status(monkeypatch):
provider = MiniMaxProvider(api_key="unit-value")
with pytest.raises(ValueError, match="prompt"):
await provider.design_voice(" ", "voice")
with pytest.raises(ValueError, match="voice_id"):
await provider.design_voice("A voice", "")

session = CaptureSession(
FakeResponse(
{"base_resp": {"status_code": 1004, "status_msg": "invalid prompt"}}
)
)
monkeypatch.setattr("aiohttp.ClientSession", lambda **kwargs: session)
with pytest.raises(RuntimeError, match="invalid prompt"):
await provider.design_voice("A voice", "voice")
Loading