Repository navigation
Add MiniMax text-to-audio connector #14324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
|
Comment on lines
+109
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On any non-2xx response, |
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings | ||
|
|
||
| __all__ = ["MiniMaxSettings"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
|
Comment on lines
+22
to
+26
|
||
|
|
||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api_keyis declared as a bare field on thisKernelBaseModelsubclass and is populated with theunwrapped secret (
api_key=settings.api_key.get_secret_value()at line 56). Because the client is aPydantic model, the plaintext key is then included in
repr(self),model_dump(), andmodel_dump_json():Any diagnostic logging,
str(service)in a stack trace, or serialization of the service objectwrites the raw MiniMax key in cleartext. Every other connector avoids this by handing the secret to
an SDK client rather than storing it as a model field, and
MiniMaxSettings.api_keyis already aSecretStrwhose protection is discarded here. Store the credential as aSecretStror aPrivateAttr(or mark itField(exclude=True, repr=False)) so it is not exposed via repr/dump.