diff --git a/.fern/metadata.json b/.fern/metadata.json index 0ac594f..aa8b64a 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -14,5 +14,5 @@ }, "exclude_types_from_init_exports": true }, - "originGitCommit": "35ad41f85a53fa379f604440fb6fa833a5f70fe7" + "originGitCommit": "a2ae04ddfa09f1ddb164b918ced2d8c776900eae" } \ No newline at end of file diff --git a/.fern/replay.lock b/.fern/replay.lock index da5026f..85a3d20 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -96,7 +96,13 @@ generations: cli_version: unknown generator_versions: fernapi/fern-python-sdk: 4.37.0 -current_generation: 8a93abaa5eee00b00218511e8d0ce24e0a0ae1f2 + - commit_sha: 1841f1b88327d8d6f1ec8b622ae9591e0e0b68db + tree_hash: 6fff2537857b89497f3c1e49d8a872dbad767de5 + timestamp: 2026-10-29T07:59:21.250Z + cli_version: unknown + generator_versions: + fernapi/fern-python-sdk: 4.37.0 +current_generation: 1841f1b88327d8d6f1ec8b622ae9591e0e0b68db forgotten_hashes: - sha256:19124603cb0683406e750ffda0725786727208c0f7ddb481c256a0990b2c5585 patches: @@ -105,7 +111,7 @@ patches: original_commit: 13d8e0683ffa08bcfc9a381eb9604adf5abdbe0b original_message: "chore: ignore local venv and planning docs" original_author: plutoless - base_generation: 8a93abaa5eee00b00218511e8d0ce24e0a0ae1f2 + base_generation: 1841f1b88327d8d6f1ec8b622ae9591e0e0b68db files: - .gitignore patch_content: | @@ -134,7 +140,7 @@ patches: original_commit: a065088b4c45e2ca7e1bb91b1a899b8444800121 original_message: "fix(vendors): send Speechmatics STT key instead of api_key" original_author: digitallysavvy - base_generation: 8a93abaa5eee00b00218511e8d0ce24e0a0ae1f2 + base_generation: 1841f1b88327d8d6f1ec8b622ae9591e0e0b68db files: - src/agora_agent/types/speechmatics_asr_params.py patch_content: | @@ -277,7 +283,7 @@ patches: original_commit: 33a436b3bdb6f2a6fe4d8987b6536c2f0ac2862e original_message: "fix: address v2.8.0 release review findings" original_author: digitallysavvy - base_generation: 8a93abaa5eee00b00218511e8d0ce24e0a0ae1f2 + base_generation: 1841f1b88327d8d6f1ec8b622ae9591e0e0b68db files: - src/agora_agent/agents/types/start_agents_request_properties_advanced_features.py patch_content: | diff --git a/docs/concepts/vendors.md b/docs/concepts/vendors.md index 91e4fbb..e2ea30a 100644 --- a/docs/concepts/vendors.md +++ b/docs/concepts/vendors.md @@ -74,6 +74,7 @@ Used with `agent.with_tts()`. Each TTS vendor produces audio at a specific sampl | `TypecastTTS` | Typecast | `api_key`, `voice_id`, `model` | — | | `SarvamTTS` | Sarvam | `api_key` | — | | `XaiTTS` | xAI | `api_key`, `language` | Configurable | +| `SonioxTTS` | Soniox | `api_key` | Configurable | For `RimeTTS`, omitting `credential_mode` selects BYOK validation. Set `credential_mode=CredentialMode.MANAGED` to use managed credentials. See the [RimeTTS vendor reference](../reference/vendors.md#rimetts) for both configurations. @@ -84,6 +85,7 @@ Used with `agent.with_tts()` when routing to `Area.CN`. Use `MiniMaxCNTTS` and ` | Class | Provider | Required Parameters | Sample Rate | |---|---|---|---| | `MiniMaxCNTTS` | MiniMax (CN) | `model`, `voice_id` or `timber_weights`; `key` typically required | — | +| `MiniMaxDuplexTTS` | MiniMax Duplex (CN only) | `key`, `model`, `voice_id` | Configurable | | `TencentTTS` | Tencent | `app_id`, `secret_id`, `secret_key`, `voice_type` | — | | `BytedanceTTS` | ByteDance | `token`, `app_id`, `cluster`, `voice_type` | — | | `MicrosoftCNTTS` | Microsoft Azure (CN) | `key`, `region`, `voice_name` | 8000, 16000, 24000, or 48000 Hz | diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 2e9b221..e90fad0 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -429,6 +429,17 @@ Omitting the mode retains BYOK behavior. An explicit mode is serialized as top-l `key` and `group_id` are required together for BYOK. `url` is optional. In both BYOK and managed modes, exactly one of `voice_id` or `timber_weights` must be provided. Without `key`, `model` must be one of the supported Agora-managed MiniMax models. +### `SonioxTTS` + +Global streaming TTS helper (`vendor: "soniox"`). Only `api_key` is required. Optional fields are `base_url`, `model`, `language`, `voice`, `sample_rate`, `speed` (0.7–1.3), `reduce_silence`, `return_timestamps`, `client_reference_id`, `dump`, `skip_patterns`, and `additional_params`. Named options override matching additional parameters. Omitted values remain with the provider's defaults. + + +```python +from agora_agent import SonioxTTS + +tts = SonioxTTS(api_key='your-soniox-key') +``` + ### `MistralTTS` | Parameter | Type | Required | Default | Description | @@ -739,6 +750,31 @@ CN TTS classes use explicit names when they differ from the global implementatio All CN TTS vendor classes support `skip_patterns` and `additional_params`. +#### `MiniMaxDuplexTTS` + +Import from `agora_agent.cn`. This helper currently has only a CN implementation and emits `vendor: "minimax_duplex"`. `key`, `model`, and `voice_id` are required; it does not infer an Agora-managed MiniMax preset or accept `timber_weights` as a voice replacement. + + +```python +from agora_agent.cn import MiniMaxDuplexTTS, StepFunSTT + +tts = MiniMaxDuplexTTS( + key='your-minimax-key', + model='speech-2.8-turbo', + voice_id='female-shaonv', + sample_rate=24000, + audio_format='pcm', + channels=1, +) +stt = StepFunSTT(api_key='your-stepfun-key') +``` + +Voice options `speed`, `vol`, `pitch`, `emotion`, and `latex_read` serialize under `params.voice_setting`, following the existing CN MiniMax helper. `sample_rate`, `audio_format`, and `channels` serialize under `params.audio_setting` (with `audio_format` mapped to `format`). + +Other optional fields are `url`, `pronunciation_dict`, `voice_modify` (`pitch`, `intensity`, `timbre`, `sound_effects`), `language_boost`, `text_normalization_mode`, `subtitle_enable`, `subtitle_type`, `continuous_sound`, `session_id`, `skip_patterns`, and `additional_params`. Explicit options override additional parameters and preserve `False` and `0`. + +`StepFunSTT` is a CN ASR helper (`vendor: "stepfun"`). It requires `api_key`, accepts `additional_params`, and gets the wire recognition language from the agent's turn detection configuration. + #### `MiniMaxCNTTS` | Parameter | Type | Required | Default | Description | diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index 1a29ed4..9d63d9a 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -79,6 +79,7 @@ RimeTTS, SarvamSTT, SarvamTTS, + SonioxTTS, SmallestAISTT, SmallestAITTS, SpeechmaticsSTT, diff --git a/src/agora_agent/agentkit/__init__.py b/src/agora_agent/agentkit/__init__.py index 0e658e8..a1526e4 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -204,6 +204,7 @@ MicrosoftCNSTT, MicrosoftCNTTS, MiniMaxCNTTS, + MiniMaxDuplexTTS, MicrosoftSTT, MicrosoftTTS, MiniMaxTTS, @@ -222,6 +223,7 @@ SampleRate, SarvamSTT, SarvamTTS, + SonioxTTS, SmallestAISTT, SmallestAITTS, TypecastTTS, @@ -244,6 +246,7 @@ FengmingSTT, SenseTimeAvatar, StepFunTTS, + StepFunSTT, TencentLLM, TencentSTT, TencentTTS, @@ -445,6 +448,7 @@ "FishAudioTTS", "MiniMaxTTS", "MiniMaxCNTTS", + "MiniMaxDuplexTTS", "MistralTTS", "TypecastTTS", "TencentTTS", @@ -452,9 +456,11 @@ "CosyVoiceTTS", "BytedanceDuplexTTS", "StepFunTTS", + "StepFunSTT", "MurfTTS", "SarvamTTS", "SmallestAITTS", + "SonioxTTS", "XaiTTS", "SpeechmaticsSTT", "DeepgramSTT", diff --git a/src/agora_agent/agentkit/regional_agent.py b/src/agora_agent/agentkit/regional_agent.py index e5ba3fc..73fb78e 100644 --- a/src/agora_agent/agentkit/regional_agent.py +++ b/src/agora_agent/agentkit/regional_agent.py @@ -12,12 +12,14 @@ DeepSeekLLM, FengmingSTT, MiniMaxTTS as MiniMaxCNTTS, + MiniMaxDuplexTTS, MicrosoftSTT as MicrosoftCNSTT, MicrosoftTTS as MicrosoftCNTTS, QwenOmni, SenseTimeAvatar, SpatiusAvatar, StepFunTTS, + StepFunSTT, TencentLLM, TencentSTT, TencentTTS, @@ -70,14 +72,15 @@ GenericTTS, RimeTTS, SarvamTTS, + SonioxTTS, SmallestAITTS, TypecastTTS, XaiTTS, ) from .vendors.avatar import AkoolAvatar, AnamAvatar, GenericAvatar, HeyGenAvatar, LiveAvatarAvatar -CNSTT = typing.Union[TencentSTT, FengmingSTT, MicrosoftCNSTT, XfyunSTT, XfyunBigModelSTT, XfyunDialectSTT] -CNTTS = typing.Union[MiniMaxCNTTS, TencentTTS, BytedanceTTS, MicrosoftCNTTS, CosyVoiceTTS, BytedanceDuplexTTS, StepFunTTS, GenericTTS] +CNSTT = typing.Union[TencentSTT, FengmingSTT, MicrosoftCNSTT, XfyunSTT, XfyunBigModelSTT, XfyunDialectSTT, StepFunSTT] +CNTTS = typing.Union[MiniMaxCNTTS, MiniMaxDuplexTTS, TencentTTS, BytedanceTTS, MicrosoftCNTTS, CosyVoiceTTS, BytedanceDuplexTTS, StepFunTTS, GenericTTS] CNLLM = typing.Union[AliyunLLM, BytedanceLLM, DeepSeekLLM, TencentLLM] CNMLLM = QwenOmni CNAvatar = typing.Union[SenseTimeAvatar, SpatiusAvatar] @@ -98,6 +101,7 @@ XaiSTT, ] GlobalTTS = typing.Union[ + SonioxTTS, GeminiTTS, MicrosoftTTS, ElevenLabsTTS, diff --git a/src/agora_agent/agentkit/vendors/__init__.py b/src/agora_agent/agentkit/vendors/__init__.py index 89bf147..05c5ffe 100644 --- a/src/agora_agent/agentkit/vendors/__init__.py +++ b/src/agora_agent/agentkit/vendors/__init__.py @@ -36,10 +36,12 @@ FengmingSTT, MicrosoftSTT as MicrosoftCNSTT, MicrosoftTTS as MicrosoftCNTTS, + MiniMaxDuplexTTS, MiniMaxTTS as MiniMaxCNTTS, QwenOmni, SenseTimeAvatar, SpatiusAvatar, + StepFunSTT, StepFunTTS, TencentLLM, TencentSTT, @@ -84,6 +86,7 @@ RimeTTS, SarvamTTS, SmallestAITTS, + SonioxTTS, TypecastTTS, XaiTTS, ) @@ -124,11 +127,13 @@ "FishAudioTTS", "MiniMaxTTS", "MiniMaxCNTTS", + "MiniMaxDuplexTTS", "MistralTTS", "TypecastTTS", "MurfTTS", "SarvamTTS", "SmallestAITTS", + "SonioxTTS", "GenericTTS", "XaiTTS", "MicrosoftCNTTS", @@ -137,6 +142,7 @@ "CosyVoiceTTS", "BytedanceDuplexTTS", "StepFunTTS", + "StepFunSTT", "SpeechmaticsSTT", "DeepgramSTT", "MicrosoftSTT", diff --git a/src/agora_agent/agentkit/vendors/catalog.py b/src/agora_agent/agentkit/vendors/catalog.py index 7ee986b..5913bf9 100644 --- a/src/agora_agent/agentkit/vendors/catalog.py +++ b/src/agora_agent/agentkit/vendors/catalog.py @@ -50,6 +50,7 @@ GenericTTS, RimeTTS, SarvamTTS, + SonioxTTS, SmallestAITTS, TypecastTTS, XaiTTS, @@ -109,6 +110,7 @@ def __init__( "xai": XaiGrok, }, tts={ + "soniox": SonioxTTS, "gemini": GeminiTTS, "microsoft": MicrosoftTTS, "elevenlabs": ElevenLabsTTS, @@ -142,6 +144,7 @@ def __init__( CN_VENDOR_NAMESPACE = VendorNamespace( asr={ "fengming": cn_vendors.FengmingSTT, + "stepfun": cn_vendors.StepFunSTT, "tencent": cn_vendors.TencentSTT, "microsoft": cn_vendors.MicrosoftSTT, "xfyun": cn_vendors.XfyunSTT, @@ -159,6 +162,7 @@ def __init__( }, tts={ "minimax": cn_vendors.MiniMaxTTS, + "minimax_duplex": cn_vendors.MiniMaxDuplexTTS, "tencent": cn_vendors.TencentTTS, "bytedance": cn_vendors.BytedanceTTS, "microsoft": cn_vendors.MicrosoftTTS, diff --git a/src/agora_agent/agentkit/vendors/cn.py b/src/agora_agent/agentkit/vendors/cn.py index 3675051..3681a53 100644 --- a/src/agora_agent/agentkit/vendors/cn.py +++ b/src/agora_agent/agentkit/vendors/cn.py @@ -16,6 +16,22 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator +class StepFunSTTOptions(BaseModel): + model_config = ConfigDict(extra="forbid") + + api_key: str = Field(..., min_length=1, repr=False) + additional_params: Optional[Dict[str, Any]] = None + + +class StepFunSTT(StepFunSTTOptions, _BaseSTTCompat): + """StepFun ASR for mainland China.""" + + def to_config(self) -> Dict[str, Any]: + params = dict(self.additional_params or {}) + params["api_key"] = self.api_key + return {"vendor": "stepfun", "params": params} + + class TencentSTTOptions(BaseModel): model_config = ConfigDict(extra="forbid") @@ -435,6 +451,64 @@ def to_config(self) -> Dict[str, Any]: return result +class MiniMaxDuplexTTSOptions(BaseModel): + model_config = ConfigDict(extra="forbid") + + key: str = Field(..., min_length=1, repr=False) + model: str = Field(..., min_length=1) + voice_id: str = Field(..., min_length=1) + url: Optional[str] = None + speed: Optional[float] = None + vol: Optional[float] = None + pitch: Optional[float] = None + emotion: Optional[str] = None + latex_read: Optional[bool] = None + sample_rate: Optional[int] = None + audio_format: Optional[str] = None + channels: Optional[int] = None + pronunciation_dict: Optional[Dict[str, Any]] = None + voice_modify: Optional[Dict[str, Any]] = None + language_boost: Optional[str] = None + text_normalization_mode: Optional[str] = None + subtitle_enable: Optional[bool] = None + subtitle_type: Optional[str] = None + continuous_sound: Optional[bool] = None + session_id: Optional[str] = None + additional_params: Optional[Dict[str, Any]] = None + skip_patterns: Optional[List[int]] = None + + +class MiniMaxDuplexTTS(MiniMaxDuplexTTSOptions, _BaseTTSCompat): + """MiniMax duplex streaming TTS, currently available only in mainland China.""" + + def to_config(self) -> Dict[str, Any]: + params: Dict[str, Any] = dict(self.additional_params or {}) + voice_setting: Dict[str, Any] = {"voice_id": self.voice_id} + for field in ("speed", "vol", "pitch", "emotion", "latex_read"): + value = getattr(self, field) + if value is not None: + voice_setting[field] = value + params.update({"key": self.key, "model": self.model, "voice_setting": voice_setting}) + audio_setting: Dict[str, Any] = {} + for field, wire_field in (("sample_rate", "sample_rate"), ("audio_format", "format"), ("channels", "channels")): + value = getattr(self, field) + if value is not None: + audio_setting[wire_field] = value + if audio_setting: + params["audio_setting"] = audio_setting + for field in ( + "url", "pronunciation_dict", "voice_modify", "language_boost", "text_normalization_mode", + "subtitle_enable", "subtitle_type", "continuous_sound", "session_id", + ): + value = getattr(self, field) + if value is not None: + params[field] = value + config: Dict[str, Any] = {"vendor": "minimax_duplex", "params": params} + if self.skip_patterns is not None: + config["skip_patterns"] = self.skip_patterns + return config + + class MiniMaxTTSOptions(BaseModel): model_config = ConfigDict(extra="forbid") diff --git a/src/agora_agent/agentkit/vendors/namespaces.py b/src/agora_agent/agentkit/vendors/namespaces.py index b3d982b..757874f 100644 --- a/src/agora_agent/agentkit/vendors/namespaces.py +++ b/src/agora_agent/agentkit/vendors/namespaces.py @@ -47,6 +47,7 @@ GenericTTS, RimeTTS, SarvamTTS, + SonioxTTS, SmallestAITTS, TypecastTTS, XaiTTS, @@ -70,6 +71,7 @@ class GlobalSTTVendors: class CNSTTVendors: + stepfun = cn_vendors.StepFunSTT fengming = cn_vendors.FengmingSTT tencent = cn_vendors.TencentSTT microsoft = cn_vendors.MicrosoftSTT @@ -111,6 +113,7 @@ class CNMLLMVendors: class GlobalTTSVendors: + soniox = SonioxTTS gemini = GeminiTTS microsoft = MicrosoftTTS elevenlabs = ElevenLabsTTS @@ -134,6 +137,7 @@ class GlobalTTSVendors: class CNTTSVendors: + minimax_duplex = cn_vendors.MiniMaxDuplexTTS minimax = cn_vendors.MiniMaxTTS tencent = cn_vendors.TencentTTS bytedance = cn_vendors.BytedanceTTS diff --git a/src/agora_agent/agentkit/vendors/region.py b/src/agora_agent/agentkit/vendors/region.py index d4944fc..ca02204 100644 --- a/src/agora_agent/agentkit/vendors/region.py +++ b/src/agora_agent/agentkit/vendors/region.py @@ -11,6 +11,7 @@ CN_ASR_VENDORS: typing.Tuple[str, ...] = ( "fengming", + "stepfun", "tencent", "microsoft", "xfyun", @@ -35,6 +36,7 @@ CN_TTS_VENDORS: typing.Tuple[str, ...] = ( "minimax", + "minimax_duplex", "tencent", "bytedance", "microsoft", @@ -44,6 +46,7 @@ "generic", ) GLOBAL_TTS_VENDORS: typing.Tuple[str, ...] = ( + "soniox", "gemini", "microsoft", "elevenlabs", diff --git a/src/agora_agent/agentkit/vendors/tts.py b/src/agora_agent/agentkit/vendors/tts.py index 6e5a60b..d7c02fd 100644 --- a/src/agora_agent/agentkit/vendors/tts.py +++ b/src/agora_agent/agentkit/vendors/tts.py @@ -8,6 +8,36 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +class SonioxTTSOptions(BaseModel): + model_config = ConfigDict(extra="forbid") + + api_key: str = Field(..., min_length=1, repr=False) + base_url: Optional[str] = None + model: Optional[str] = None + language: Optional[str] = None + voice: Optional[str] = None + sample_rate: Optional[int] = None + speed: Optional[float] = Field(default=None, ge=0.7, le=1.3) + reduce_silence: Optional[bool] = None + return_timestamps: Optional[bool] = None + client_reference_id: Optional[str] = None + dump: Optional[bool] = None + additional_params: Optional[Dict[str, Any]] = None + skip_patterns: Optional[List[int]] = None + + +class SonioxTTS(SonioxTTSOptions, BaseTTS): + """Soniox streaming TTS. Only api_key is required.""" + + def to_config(self) -> Dict[str, Any]: + params = dict(self.additional_params or {}) + params.update(self.model_dump(exclude_none=True, exclude={"additional_params", "skip_patterns"})) + config: Dict[str, Any] = {"vendor": "soniox", "params": params} + if self.skip_patterns is not None: + config["skip_patterns"] = self.skip_patterns + return config + + class GeminiTTSModels: """Gemini 3.8 Flash TTS model.""" diff --git a/src/agora_agent/cn.py b/src/agora_agent/cn.py index da91d7a..83ca326 100644 --- a/src/agora_agent/cn.py +++ b/src/agora_agent/cn.py @@ -21,10 +21,12 @@ FengmingSTT, MicrosoftSTT, MicrosoftTTS, + MiniMaxDuplexTTS, MiniMaxTTS, QwenOmni, SenseTimeAvatar, SpatiusAvatar, + StepFunSTT, StepFunTTS, TencentLLM, TencentSTT, @@ -38,6 +40,7 @@ # STT "FengmingSTT", "MicrosoftSTT", + "StepFunSTT", "TencentSTT", "XfyunBigModelSTT", "XfyunDialectSTT", @@ -47,6 +50,7 @@ "BytedanceTTS", "CosyVoiceTTS", "MicrosoftTTS", + "MiniMaxDuplexTTS", "MiniMaxTTS", "StepFunTTS", "TencentTTS", diff --git a/src/agora_agent/core/client_wrapper.py b/src/agora_agent/core/client_wrapper.py index 4f480c9..9006871 100644 --- a/src/agora_agent/core/client_wrapper.py +++ b/src/agora_agent/core/client_wrapper.py @@ -26,10 +26,10 @@ def __init__( def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "agora-agents/v2.13.0", + "User-Agent": "agora-agents/v2.14.0", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "agora-agents", - "X-Fern-SDK-Version": "v2.13.0", + "X-Fern-SDK-Version": "v2.14.0", **(self.get_custom_headers() or {}), } headers["Authorization"] = httpx.BasicAuth(self._get_username(), self._get_password())._auth_header diff --git a/src/agora_agent/types/asr.py b/src/agora_agent/types/asr.py index dc5461d..d4bbdba 100644 --- a/src/agora_agent/types/asr.py +++ b/src/agora_agent/types/asr.py @@ -22,6 +22,7 @@ from .sarvam_asr_params import SarvamAsrParams from .smallest_ai_asr_params import SmallestAiAsrParams from .speechmatics_asr_params import SpeechmaticsAsrParams +from .stepfun_asr_params import StepfunAsrParams from .tencent_asr_params import TencentAsrParams from .x_ai_asr_params import XAiAsrParams from .xfyun_asr_params import XfyunAsrParams @@ -301,6 +302,21 @@ class Config: extra = pydantic.Extra.allow +class Asr_Stepfun(UncheckedBaseModel): + vendor: typing.Literal["stepfun"] = "stepfun" + language: typing.Optional[AsrLanguage] = None + params: StepfunAsrParams + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + Asr = typing_extensions.Annotated[ typing.Union[ Asr_Ares, @@ -321,6 +337,7 @@ class Config: Asr_XfyunBigmodel, Asr_XfyunDialect, Asr_Smallestai, + Asr_Stepfun, ], UnionMetadata(discriminant="vendor"), ] diff --git a/src/agora_agent/types/minimax_duplex_tts.py b/src/agora_agent/types/minimax_duplex_tts.py new file mode 100644 index 0000000..3a8df68 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .minimax_duplex_tts_params import MinimaxDuplexTtsParams + + +class MinimaxDuplexTts(UncheckedBaseModel): + """ + MiniMax duplex streaming Text-to-Speech configuration. + """ + + batch: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to enable batch processing for MiniMax duplex streaming TTS. Omit this field to use the server default of true. + """ + + enable_words: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to enable word-level output for MiniMax duplex streaming TTS. Omit this field to leave it unset in the request. + """ + + params: MinimaxDuplexTtsParams + skip_patterns: typing.Optional[typing.List[int]] = pydantic.Field(default=None) + """ + Controls whether the TTS module skips bracketed content when reading LLM response text. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/minimax_duplex_tts_params.py b/src/agora_agent/types/minimax_duplex_tts_params.py new file mode 100644 index 0000000..b6ccff7 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts_params.py @@ -0,0 +1,87 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .minimax_duplex_tts_params_audio_setting import MinimaxDuplexTtsParamsAudioSetting +from .minimax_duplex_tts_params_pronunciation_dict import MinimaxDuplexTtsParamsPronunciationDict +from .minimax_duplex_tts_params_voice_modify import MinimaxDuplexTtsParamsVoiceModify +from .minimax_duplex_tts_params_voice_setting import MinimaxDuplexTtsParamsVoiceSetting + + +class MinimaxDuplexTtsParams(UncheckedBaseModel): + """ + MiniMax duplex streaming TTS configuration parameters. + """ + + key: str = pydantic.Field() + """ + MiniMax API key. + """ + + url: typing.Optional[str] = pydantic.Field(default=None) + """ + WebSocket endpoint (e.g., wss://api.minimax.cn/ws/v1/t2a_v2_bidi). + """ + + model: str = pydantic.Field() + """ + MiniMax model identifier (e.g., speech-2.8-turbo). + """ + + voice_setting: MinimaxDuplexTtsParamsVoiceSetting + audio_setting: typing.Optional[MinimaxDuplexTtsParamsAudioSetting] = pydantic.Field(default=None) + """ + Audio output settings. + """ + + language_boost: typing.Optional[str] = pydantic.Field(default=None) + """ + Language boost mode (e.g., Chinese). + """ + + text_normalization_mode: typing.Optional[str] = pydantic.Field(default=None) + """ + Text normalization mode (e.g., basic). + """ + + pronunciation_dict: typing.Optional[MinimaxDuplexTtsParamsPronunciationDict] = pydantic.Field(default=None) + """ + Custom pronunciation dictionary settings. + """ + + voice_modify: typing.Optional[MinimaxDuplexTtsParamsVoiceModify] = pydantic.Field(default=None) + """ + Voice modification settings. + """ + + subtitle_enable: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to enable subtitles. + """ + + subtitle_type: typing.Optional[str] = pydantic.Field(default=None) + """ + Subtitle granularity (e.g., word). + """ + + continuous_sound: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to enable continuous sound. + """ + + session_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional MiniMax provider session identifier. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/minimax_duplex_tts_params_audio_setting.py b/src/agora_agent/types/minimax_duplex_tts_params_audio_setting.py new file mode 100644 index 0000000..2d57ef3 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts_params_audio_setting.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class MinimaxDuplexTtsParamsAudioSetting(UncheckedBaseModel): + """ + Audio output settings. + """ + + sample_rate: typing.Optional[int] = pydantic.Field(default=None) + """ + Audio sample rate in Hz. + """ + + format: typing.Optional[str] = pydantic.Field(default=None) + """ + Audio output format (e.g., pcm). + """ + + channels: typing.Optional[int] = pydantic.Field(default=None) + """ + Number of audio channels. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/minimax_duplex_tts_params_pronunciation_dict.py b/src/agora_agent/types/minimax_duplex_tts_params_pronunciation_dict.py new file mode 100644 index 0000000..195e152 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts_params_pronunciation_dict.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class MinimaxDuplexTtsParamsPronunciationDict(UncheckedBaseModel): + """ + Custom pronunciation dictionary settings. + """ + + tone: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Pronunciation override list. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/minimax_duplex_tts_params_voice_modify.py b/src/agora_agent/types/minimax_duplex_tts_params_voice_modify.py new file mode 100644 index 0000000..53443b1 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts_params_voice_modify.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class MinimaxDuplexTtsParamsVoiceModify(UncheckedBaseModel): + """ + Voice modification settings. + """ + + pitch: typing.Optional[float] = pydantic.Field(default=None) + """ + Voice modification pitch adjustment. + """ + + intensity: typing.Optional[float] = pydantic.Field(default=None) + """ + Voice intensity adjustment. + """ + + timbre: typing.Optional[float] = pydantic.Field(default=None) + """ + Voice timbre adjustment. + """ + + sound_effects: typing.Optional[str] = pydantic.Field(default=None) + """ + Sound effect preset (e.g., spacious_echo). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/minimax_duplex_tts_params_voice_setting.py b/src/agora_agent/types/minimax_duplex_tts_params_voice_setting.py new file mode 100644 index 0000000..179f1f7 --- /dev/null +++ b/src/agora_agent/types/minimax_duplex_tts_params_voice_setting.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class MinimaxDuplexTtsParamsVoiceSetting(UncheckedBaseModel): + voice_id: str = pydantic.Field() + """ + Voice identifier (e.g., female-shaonv). + """ + + speed: typing.Optional[float] = pydantic.Field(default=None) + """ + Speech speed multiplier. + """ + + vol: typing.Optional[float] = pydantic.Field(default=None) + """ + Voice volume multiplier. + """ + + pitch: typing.Optional[float] = pydantic.Field(default=None) + """ + Voice pitch adjustment. + """ + + emotion: typing.Optional[str] = pydantic.Field(default=None) + """ + Emotion preset. + """ + + latex_read: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to read LaTeX expressions. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/soniox_tts.py b/src/agora_agent/types/soniox_tts.py new file mode 100644 index 0000000..0a1a2eb --- /dev/null +++ b/src/agora_agent/types/soniox_tts.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .soniox_tts_params import SonioxTtsParams + + +class SonioxTts(UncheckedBaseModel): + """ + Soniox Text-to-Speech configuration. + """ + + params: SonioxTtsParams + skip_patterns: typing.Optional[typing.List[int]] = pydantic.Field(default=None) + """ + Controls whether the TTS module skips bracketed content when reading LLM response text. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/soniox_tts_params.py b/src/agora_agent/types/soniox_tts_params.py new file mode 100644 index 0000000..2bc0a1a --- /dev/null +++ b/src/agora_agent/types/soniox_tts_params.py @@ -0,0 +1,77 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SonioxTtsParams(UncheckedBaseModel): + """ + Soniox TTS configuration parameters. Only api_key is required. + """ + + api_key: str = pydantic.Field() + """ + Soniox API key. + """ + + base_url: typing.Optional[str] = pydantic.Field(default=None) + """ + WebSocket endpoint for Soniox streaming speech synthesis. + """ + + model: typing.Optional[str] = pydantic.Field(default=None) + """ + Soniox TTS model name. + """ + + language: typing.Optional[str] = pydantic.Field(default=None) + """ + Language code for speech synthesis. + """ + + voice: typing.Optional[str] = pydantic.Field(default=None) + """ + Soniox voice name. + """ + + sample_rate: typing.Optional[int] = pydantic.Field(default=None) + """ + Output audio sample rate in Hz. + """ + + speed: typing.Optional[float] = pydantic.Field(default=None) + """ + Speech rate multiplier, from 0.7 to 1.3 inclusive. + """ + + reduce_silence: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to reduce silence in synthesized speech. + """ + + return_timestamps: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to return timestamps for synthesized speech. + """ + + client_reference_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Client-provided reference identifier. + """ + + dump: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether to enable diagnostic dumping. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/stepfun_asr.py b/src/agora_agent/types/stepfun_asr.py new file mode 100644 index 0000000..171900b --- /dev/null +++ b/src/agora_agent/types/stepfun_asr.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .asr_language import AsrLanguage +from .stepfun_asr_params import StepfunAsrParams + + +class StepfunAsr(UncheckedBaseModel): + """ + StepFun ASR configuration. + """ + + language: typing.Optional[AsrLanguage] = None + params: StepfunAsrParams + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/stepfun_asr_params.py b/src/agora_agent/types/stepfun_asr_params.py new file mode 100644 index 0000000..72053d9 --- /dev/null +++ b/src/agora_agent/types/stepfun_asr_params.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class StepfunAsrParams(UncheckedBaseModel): + """ + StepFun ASR configuration parameters. + """ + + api_key: str = pydantic.Field() + """ + StepFun API key. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/agora_agent/types/tts.py b/src/agora_agent/types/tts.py index 16fc70f..c2bfc24 100644 --- a/src/agora_agent/types/tts.py +++ b/src/agora_agent/types/tts.py @@ -24,6 +24,7 @@ from .gradium_tts_params import GradiumTtsParams from .hume_ai_tts_params import HumeAiTtsParams from .microsoft_tts_params import MicrosoftTtsParams +from .minimax_duplex_tts_params import MinimaxDuplexTtsParams from .minimax_tts_params import MinimaxTtsParams from .mistral_tts_params import MistralTtsParams from .murf_tts_params import MurfTtsParams @@ -32,6 +33,7 @@ from .rime_tts_params import RimeTtsParams from .sarvam_tts_params import SarvamTtsParams from .smallest_ai_tts_params import SmallestAiTtsParams +from .soniox_tts_params import SonioxTtsParams from .stepfun_tts_params import StepfunTtsParams from .tencent_tts_params import TencentTtsParams from .typecast_tts_params import TypecastTtsParams @@ -113,6 +115,23 @@ class Config: extra = pydantic.Extra.allow +class Tts_MinimaxDuplex(UncheckedBaseModel): + vendor: typing.Literal["minimax_duplex"] = "minimax_duplex" + batch: typing.Optional[bool] = None + enable_words: typing.Optional[bool] = None + params: MinimaxDuplexTtsParams + skip_patterns: typing.Optional[typing.List[int]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + class Tts_Murf(UncheckedBaseModel): vendor: typing.Literal["murf"] = "murf" params: MurfTtsParams @@ -418,6 +437,21 @@ class Config: extra = pydantic.Extra.allow +class Tts_Soniox(UncheckedBaseModel): + vendor: typing.Literal["soniox"] = "soniox" + params: SonioxTtsParams + skip_patterns: typing.Optional[typing.List[int]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + Tts = typing_extensions.Annotated[ typing.Union[ Tts_Tencent, @@ -425,6 +459,7 @@ class Config: Tts_Microsoft, Tts_Elevenlabs, Tts_Minimax, + Tts_MinimaxDuplex, Tts_Murf, Tts_Cartesia, Tts_Openai, @@ -445,6 +480,7 @@ class Config: Tts_Mistral, Tts_Typecast, Tts_Smallestai, + Tts_Soniox, ], UnionMetadata(discriminant="vendor"), ] diff --git a/tests/custom/test_agentkit_vendors.py b/tests/custom/test_agentkit_vendors.py index 0fd81a0..56a5c67 100644 --- a/tests/custom/test_agentkit_vendors.py +++ b/tests/custom/test_agentkit_vendors.py @@ -8,13 +8,16 @@ GenericTTS, OpenAI, OpenAIRealtime, - OpenAIGPTLive as ProductionOpenAIGPTLive, SmallestAISTT, SpatiusAvatar, XaiGrok, XaiSTT, XaiTTS, ) +from agora_agent.agentkit.vendors import ( + OpenAIGPTLive as ProductionOpenAIGPTLive, +) +from agora_agent.cn import StepFunSTT def test_smallest_ai_stt_serializes_generated_contract() -> None: @@ -397,3 +400,16 @@ def test_spatius_avatar_serializes() -> None: "region": "cn-beijing", }, } + +def test_stepfun_stt_minimal_config_and_parameter_precedence() -> None: + assert StepFunSTT(api_key="k").to_config() == {"vendor": "stepfun", "params": {"api_key": "k"}} + assert StepFunSTT(api_key="k", additional_params={"api_key": "overridden", "future_param": 42}).to_config() == { + "vendor": "stepfun", "params": {"api_key": "k", "future_param": 42}, + } + + +def test_stepfun_stt_requires_api_key() -> None: + with pytest.raises(ValidationError): + StepFunSTT(api_key="") + with pytest.raises(ValidationError): + StepFunSTT.model_validate({}) diff --git a/tests/custom/test_regional_vendors.py b/tests/custom/test_regional_vendors.py index 10516a4..9343c48 100644 --- a/tests/custom/test_regional_vendors.py +++ b/tests/custom/test_regional_vendors.py @@ -16,13 +16,14 @@ RtzrSTT, SmallestAISTT, SmallestAITTS, + SonioxTTS, SpatiusAvatar, TencentSTT, XaiGrok, XaiSTT, XaiTTS, ) -from agora_agent.agentkit.regional_agent import GlobalSTT, GlobalTTS +from agora_agent.agentkit.regional_agent import CNSTT, CNTTS, GlobalSTT, GlobalTTS from agora_agent.agentkit.vendors.catalog import CN_VENDOR_NAMESPACE, GLOBAL_VENDOR_NAMESPACE from agora_agent.agentkit.vendors.namespaces import CNSTTVendors, CNTTSVendors, GlobalSTTVendors, GlobalTTSVendors from agora_agent.agentkit.vendors.region import ( @@ -33,6 +34,7 @@ GLOBAL_AVATAR_VENDORS, GLOBAL_TTS_VENDORS, ) +from agora_agent.cn import MiniMaxDuplexTTS, StepFunSTT def _client(area: Area) -> AgentClient: @@ -283,3 +285,16 @@ def test_smallest_ai_configs_reach_global_request_properties() -> None: assert properties.asr.params.sentence_timestamps == "false" assert properties.asr.params.eou_timeout_ms == 480 assert properties.tts is not None and properties.tts.vendor == "smallestai" + +def test_speech_vendor_regional_catalogs_and_type_unions() -> None: + assert CN_VENDOR_NAMESPACE.tts["minimax_duplex"] is CNTTSVendors.minimax_duplex is MiniMaxDuplexTTS + assert CN_VENDOR_NAMESPACE.asr["stepfun"] is CNSTTVendors.stepfun is StepFunSTT + assert GLOBAL_VENDOR_NAMESPACE.tts["soniox"] is GlobalTTSVendors.soniox is SonioxTTS + assert "minimax_duplex" in CN_TTS_VENDORS + assert "minimax_duplex" not in GLOBAL_TTS_VENDORS + assert "minimax_duplex" not in GLOBAL_VENDOR_NAMESPACE.tts + assert "stepfun" in CN_ASR_VENDORS + assert MiniMaxDuplexTTS in get_args(CNTTS) + assert MiniMaxDuplexTTS not in get_args(GlobalTTS) + assert StepFunSTT in get_args(CNSTT) + assert SonioxTTS in get_args(GlobalTTS) diff --git a/tests/custom/test_request_body.py b/tests/custom/test_request_body.py index d7fe988..75ed613 100644 --- a/tests/custom/test_request_body.py +++ b/tests/custom/test_request_body.py @@ -26,6 +26,7 @@ from agora_agent import ( Agent, + AgentClient, Agora, AmazonBedrock, AmazonSTT, @@ -74,7 +75,7 @@ ) from agora_agent.agentkit import AgentSession from agora_agent.agentkit.presets import resolve_session_presets -from agora_agent.cn import QwenOmni +from agora_agent.cn import MiniMaxDuplexTTS, QwenOmni, StepFunSTT from agora_agent.types.speechmatics_asr_params import SpeechmaticsAsrParams # --------------------------------------------------------------------------- @@ -1619,3 +1620,18 @@ def test_generated_filler_words_context_limits_reach_request_properties() -> Non assert generated is not None assert generated.context_message_limit == 6 assert generated.history_character_limit == 1200 + +def test_cn_agent_preserves_duplex_and_stepfun_in_generated_properties() -> None: + client = AgentClient(area=Area.CN, app_id="0" * 32, app_certificate="1" * 32) + tts = MiniMaxDuplexTTS(key="k", model="speech-2.8-turbo", voice_id="v", subtitle_enable=False) + props = ( + Agent(client=client, turn_detection={"language": "zh-CN"}) + .with_stt(StepFunSTT(api_key="step-key")) + .with_llm(OpenAI(model="gpt-4o-mini")) + .with_tts(tts) + .to_properties(channel="room", token="token", agent_uid="1", remote_uids=["100"]) + .model_dump(exclude_none=True) + ) + assert props["tts"] == tts.to_config() + assert props["asr"] == {"vendor": "stepfun", "params": {"api_key": "step-key"}, "language": "zh-CN"} + assert "minimax_speech_2_8_turbo" not in props.get("preset", []) diff --git a/tests/custom/test_tts_vendors.py b/tests/custom/test_tts_vendors.py index 917a210..b6ebe24 100644 --- a/tests/custom/test_tts_vendors.py +++ b/tests/custom/test_tts_vendors.py @@ -2,6 +2,7 @@ import httpx import pytest +from pydantic import TypeAdapter, ValidationError from agora_agent import ( Agent, @@ -26,8 +27,11 @@ RimeTTS, SarvamTTS, SmallestAITTS, + SonioxTTS, TypecastTTS, ) +from agora_agent.cn import MiniMaxDuplexTTS +from agora_agent.types.tts import Tts def test_smallest_ai_tts_serializes_generated_contract() -> None: @@ -639,3 +643,92 @@ def handle_request(request: httpx.Request) -> httpx.Response: ) def test_tts_http_request_preserves_vendor_specific_wire_keys(tts, expected_tts: dict) -> None: assert _capture_start_request(tts) == expected_tts + +def test_minimax_duplex_full_config_matches_generated_schema() -> None: + extra = {"key": "overridden", "continuous_sound": True, "future_param": "kept"} + tts = MiniMaxDuplexTTS( + key="mm-key", model="speech-2.8-turbo", voice_id="female-shaonv", + url="wss://api.minimax.cn/ws/v1/t2a_v2_bidi", + speed=1.2, vol=0, pitch=0, emotion="happy", latex_read=False, + sample_rate=24000, audio_format="pcm", channels=1, + pronunciation_dict={"tone": ["测试/(ce4)(shi4)"]}, + voice_modify={"pitch": 0, "intensity": 0, "timbre": 0, "sound_effects": "spacious_echo"}, + language_boost="Chinese", text_normalization_mode="basic", + subtitle_enable=False, subtitle_type="word", continuous_sound=False, + session_id="session-1", skip_patterns=[], additional_params=extra, + ) + expected = { + "vendor": "minimax_duplex", + "params": { + "key": "mm-key", "model": "speech-2.8-turbo", "url": "wss://api.minimax.cn/ws/v1/t2a_v2_bidi", + "voice_setting": {"voice_id": "female-shaonv", "speed": 1.2, "vol": 0, "pitch": 0, "emotion": "happy", "latex_read": False}, + "audio_setting": {"sample_rate": 24000, "format": "pcm", "channels": 1}, + "pronunciation_dict": {"tone": ["测试/(ce4)(shi4)"]}, + "voice_modify": {"pitch": 0, "intensity": 0, "timbre": 0, "sound_effects": "spacious_echo"}, + "language_boost": "Chinese", "text_normalization_mode": "basic", + "subtitle_enable": False, "subtitle_type": "word", "continuous_sound": False, + "session_id": "session-1", "future_param": "kept", + }, + "skip_patterns": [], + } + assert tts.to_config() == expected + adapter: TypeAdapter[Tts] = TypeAdapter(Tts) + assert adapter.validate_python(expected).model_dump(exclude_none=True) == expected + assert tts.resolved_sample_rate == 24000 + assert extra == {"key": "overridden", "continuous_sound": True, "future_param": "kept"} + + +def test_minimax_duplex_minimal_config_omits_optional_fields() -> None: + assert MiniMaxDuplexTTS(key="k", model="m", voice_id="v").to_config() == { + "vendor": "minimax_duplex", "params": {"key": "k", "model": "m", "voice_setting": {"voice_id": "v"}}, + } + + +def test_soniox_all_options_and_parameter_precedence() -> None: + tts = SonioxTTS( + api_key="k", base_url="wss://tts.soniox.com", model="model", language="en", voice="voice", + sample_rate=24000, speed=0.7, reduce_silence=False, return_timestamps=False, + client_reference_id="ref", dump=False, skip_patterns=[1], + additional_params={"api_key": "overridden", "reduce_silence": True, "future_param": 42}, + ) + expected = { + "vendor": "soniox", + "params": { + "api_key": "k", "base_url": "wss://tts.soniox.com", "model": "model", "language": "en", "voice": "voice", + "sample_rate": 24000, "speed": 0.7, "reduce_silence": False, "return_timestamps": False, + "client_reference_id": "ref", "dump": False, "future_param": 42, + }, + "skip_patterns": [1], + } + assert tts.to_config() == expected + adapter: TypeAdapter[Tts] = TypeAdapter(Tts) + assert adapter.validate_python(expected).model_dump(exclude_none=True) == expected + assert tts.resolved_sample_rate == 24000 + + +def test_soniox_minimal_config_omits_optional_fields() -> None: + assert SonioxTTS(api_key="k").to_config() == {"vendor": "soniox", "params": {"api_key": "k"}} + + +@pytest.mark.parametrize("field", ["key", "model", "voice_id"]) +def test_minimax_duplex_requires_key_model_and_voice(field: str) -> None: + options = {"key": "k", "model": "m", "voice_id": "v"} + options[field] = "" + with pytest.raises(ValidationError): + MiniMaxDuplexTTS(**options) + del options[field] + with pytest.raises(ValidationError): + MiniMaxDuplexTTS(**options) + + +def test_soniox_requires_api_key() -> None: + with pytest.raises(ValidationError): + SonioxTTS(api_key="") + with pytest.raises(ValidationError): + SonioxTTS.model_validate({}) + + +@pytest.mark.parametrize("speed", [0.6, 1.4, float("nan")]) +def test_soniox_rejects_invalid_speed(speed: float) -> None: + with pytest.raises(ValidationError): + SonioxTTS(api_key="k", speed=speed)