From 8b2aea144fc646562b256b7a0c49528fef3a3124 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 24 Aug 2026 04:39:36 +0800 Subject: [PATCH] Add MiniMax text to audio connector --- .../semantic_kernel/connectors/ai/README.md | 3 +- .../connectors/ai/minimax/__init__.py | 9 ++ .../prompt_execution_settings/__init__.py | 7 + ...inimax_text_to_audio_execution_settings.py | 22 ++++ .../ai/minimax/services/__init__.py | 5 + .../minimax/services/minimax_text_to_audio.py | 122 ++++++++++++++++++ .../ai/minimax/settings/__init__.py | 5 + .../ai/minimax/settings/minimax_settings.py | 21 +++ .../unit/connectors/ai/minimax/__init__.py | 1 + .../ai/minimax/test_minimax_text_to_audio.py | 30 +++++ 10 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 python/semantic_kernel/connectors/ai/minimax/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_text_to_audio_execution_settings.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/minimax_text_to_audio.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/settings/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py create mode 100644 python/tests/unit/connectors/ai/minimax/__init__.py create mode 100644 python/tests/unit/connectors/ai/minimax/test_minimax_text_to_audio.py diff --git a/python/semantic_kernel/connectors/ai/README.md b/python/semantic_kernel/connectors/ai/README.md index 6fe7510a0697..0d5bcf08639c 100644 --- a/python/semantic_kernel/connectors/ai/README.md +++ b/python/semantic_kernel/connectors/ai/README.md @@ -44,9 +44,10 @@ All base clients inherit from the [`AIServiceClientBase`](../../services/ai_serv | | [`HuggingFaceTextEmbedding`](./hugging_face/services/hf_text_embedding.py) | | Mistral AI | [`MistralAIChatCompletion`](./mistral_ai/services/mistral_ai_chat_completion.py) | | | [`MistralAITextEmbedding`](./mistral_ai/services/mistral_ai_text_embedding.py) | +| MiniMax | [`MiniMaxTextToAudio`](./minimax/services/minimax_text_to_audio.py) | | [Nvidia](./nvidia/README.md) | [`NvidiaTextEmbedding`](./nvidia/services/nvidia_text_embedding.py) | | Ollama | [`OllamaChatCompletion`](./ollama/services/ollama_chat_completion.py) | | | [`OllamaTextCompletion`](./ollama/services/ollama_text_completion.py) | | | [`OllamaTextEmbedding`](./ollama/services/ollama_text_embedding.py) | | Onnx | [`OnnxGenAIChatCompletion`](./onnx/services/onnx_gen_ai_chat_completion.py) | -| | [`OnnxGenAITextCompletion`](./onnx/services/onnx_gen_ai_text_completion.py) | \ No newline at end of file +| | [`OnnxGenAITextCompletion`](./onnx/services/onnx_gen_ai_text_completion.py) | diff --git a/python/semantic_kernel/connectors/ai/minimax/__init__.py b/python/semantic_kernel/connectors/ai/minimax/__init__.py new file mode 100644 index 000000000000..0c9458e0f1f6 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_text_to_audio_execution_settings import ( + MiniMaxTextToAudioExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.services.minimax_text_to_audio import MiniMaxTextToAudio +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings + +__all__ = ["MiniMaxSettings", "MiniMaxTextToAudio", "MiniMaxTextToAudioExecutionSettings"] diff --git a/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py new file mode 100644 index 000000000000..3a1602e9044a --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_text_to_audio_execution_settings import ( + MiniMaxTextToAudioExecutionSettings, +) + +__all__ = ["MiniMaxTextToAudioExecutionSettings"] diff --git a/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_text_to_audio_execution_settings.py b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_text_to_audio_execution_settings.py new file mode 100644 index 000000000000..b60794d5de5f --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_text_to_audio_execution_settings.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any + +from pydantic import Field + +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + + +class MiniMaxTextToAudioExecutionSettings(PromptExecutionSettings): + """Request settings for MiniMax text-to-audio (T2A v2).""" + + ai_model_id: str | None = Field(None, serialization_alias="model") + input: str | None = Field(None, serialization_alias="text") + stream: bool | None = None + language_boost: str | None = None + output_format: str | None = None + voice_setting: dict[str, Any] | None = None + pronunciation_dict: dict[str, Any] | None = None + audio_setting: dict[str, Any] | None = None + voice_modify: dict[str, Any] | None = None + subtitle_enable: bool | None = None diff --git a/python/semantic_kernel/connectors/ai/minimax/services/__init__.py b/python/semantic_kernel/connectors/ai/minimax/services/__init__.py new file mode 100644 index 000000000000..bfd674121df0 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax.services.minimax_text_to_audio import MiniMaxTextToAudio + +__all__ = ["MiniMaxTextToAudio"] diff --git a/python/semantic_kernel/connectors/ai/minimax/services/minimax_text_to_audio.py b/python/semantic_kernel/connectors/ai/minimax/services/minimax_text_to_audio.py new file mode 100644 index 000000000000..ef0019f91612 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/minimax_text_to_audio.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft. All rights reserved. + +import base64 +import binascii +from collections.abc import Mapping +from typing import Any, TypeVar + +import aiohttp +from pydantic import ValidationError + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_text_to_audio_execution_settings import ( + MiniMaxTextToAudioExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase +from semantic_kernel.contents.audio_content import AudioContent +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException + +T_ = TypeVar("T_", bound="MiniMaxTextToAudio") + + +class MiniMaxTextToAudio(TextToAudioClientBase): + """MiniMax speech-2.x text-to-audio HTTP service.""" + + api_key: str + base_url: str + default_headers: dict[str, str] + + def __init__( + self, + ai_model_id: str | None = None, + api_key: str | None = None, + service_id: str | None = None, + base_url: str | None = None, + default_headers: Mapping[str, str] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a MiniMax text-to-audio client.""" + try: + settings = MiniMaxSettings( + api_key=api_key, + text_to_audio_model_id=ai_model_id, + text_to_audio_base_url=base_url, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create MiniMax settings.", ex) from ex + if not settings.text_to_audio_model_id: + raise ServiceInitializationError("The MiniMax text to audio model ID is required.") + super().__init__( + ai_model_id=settings.text_to_audio_model_id, + service_id=service_id or settings.text_to_audio_model_id, + api_key=settings.api_key.get_secret_value(), + base_url=settings.text_to_audio_base_url, + default_headers=dict(default_headers or {}), + ) + + @classmethod + def from_dict(cls: type[T_], settings: dict[str, Any]) -> T_: + """Initialize a client from a settings dictionary.""" + return cls( + ai_model_id=settings.get("ai_model_id"), + api_key=settings.get("api_key"), + service_id=settings.get("service_id"), + base_url=settings.get("base_url"), + default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path"), + ) + + def get_prompt_execution_settings_class(self) -> type[PromptExecutionSettings]: + """Return the request settings type accepted by this client.""" + return MiniMaxTextToAudioExecutionSettings + + def service_url(self) -> str: + """Return the configured MiniMax speech endpoint.""" + return self.base_url + + async def get_audio_contents( + self, + text: str, + settings: PromptExecutionSettings | None = None, + **kwargs: Any, + ) -> list[AudioContent]: + """Convert text into audio using MiniMax's regional HTTP endpoint.""" + if settings is None: + request = MiniMaxTextToAudioExecutionSettings(ai_model_id=self.ai_model_id) + elif isinstance(settings, MiniMaxTextToAudioExecutionSettings): + request = settings + else: + request = self.get_prompt_execution_settings_from_settings(settings) + request.ai_model_id = request.ai_model_id or self.ai_model_id + request.input = text + payload = request.prepare_settings_dict() + payload.update(kwargs) + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + **self.default_headers, + } + try: + timeout = aiohttp.ClientTimeout(total=120) + async with ( + aiohttp.ClientSession(timeout=timeout, headers=headers) as session, + session.post(self.base_url, json=payload) as response, + ): + body = await response.json(content_type=None) + response.raise_for_status() + if body.get("base_resp", {}).get("status_code") not in (None, 0): + raise RuntimeError(body.get("base_resp", {}).get("status_msg", "MiniMax request failed")) + encoded = body.get("data", {}).get("audio") + if not encoded: + raise RuntimeError("MiniMax response did not contain audio data") + try: + audio = bytes.fromhex(encoded) + except (ValueError, TypeError, binascii.Error): + audio = base64.b64decode(encoded) + return [AudioContent(ai_model_id=request.ai_model_id, data=audio, data_format="base64", inner_content=body)] + except Exception as ex: + raise ServiceResponseException(f"{type(self)} service failed to generate audio", ex) from ex diff --git a/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py b/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py new file mode 100644 index 000000000000..473d2d2d0ade --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings + +__all__ = ["MiniMaxSettings"] diff --git a/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py new file mode 100644 index 000000000000..4741ff68e377 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import ClassVar + +from pydantic import SecretStr + +from semantic_kernel.kernel_pydantic import KernelBaseSettings + + +class MiniMaxSettings(KernelBaseSettings): + """MiniMax credentials and text-to-audio defaults. + + The ``MINIMAX_`` environment prefix is used for API clients. Set + ``MINIMAX_API_KEY`` and optionally ``MINIMAX_TEXT_TO_AUDIO_MODEL_ID``. + """ + + env_prefix: ClassVar[str] = "MINIMAX_" + + api_key: SecretStr + text_to_audio_model_id: str | None = None + text_to_audio_base_url: str = "https://api.minimax.io/v1/t2a_v2" diff --git a/python/tests/unit/connectors/ai/minimax/__init__.py b/python/tests/unit/connectors/ai/minimax/__init__.py new file mode 100644 index 000000000000..2a50eae89411 --- /dev/null +++ b/python/tests/unit/connectors/ai/minimax/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/tests/unit/connectors/ai/minimax/test_minimax_text_to_audio.py b/python/tests/unit/connectors/ai/minimax/test_minimax_text_to_audio.py new file mode 100644 index 000000000000..fc25ae50ca33 --- /dev/null +++ b/python/tests/unit/connectors/ai/minimax/test_minimax_text_to_audio.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax import MiniMaxTextToAudio, MiniMaxTextToAudioExecutionSettings + + +def test_text_to_audio_settings_prepare_request() -> None: + settings = MiniMaxTextToAudioExecutionSettings( + ai_model_id="speech-2.8-hd", + input="Hello", + output_format="mp3", + audio_setting={"sample_rate": 32000}, + ) + + assert settings.prepare_settings_dict() == { + "model": "speech-2.8-hd", + "text": "Hello", + "output_format": "mp3", + "audio_setting": {"sample_rate": 32000}, + } + + +def test_text_to_audio_from_environment(monkeypatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_TEXT_TO_AUDIO_MODEL_ID", "speech-2.8-hd") + + service = MiniMaxTextToAudio() + + assert service.ai_model_id == "speech-2.8-hd" + assert service.base_url == "https://api.minimax.io/v1/t2a_v2" + assert service.get_prompt_execution_settings_class() is MiniMaxTextToAudioExecutionSettings