wip: snapshot full local workspace state
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
This commit is contained in:
@@ -1,149 +1,149 @@
|
||||
"""MiniMax 语音生成适配器 (T2A V2)。"""
|
||||
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.services.adapters.base import AdapterConfig, BaseAdapter
|
||||
from app.services.adapters.registry import AdapterRegistry
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# MiniMax API 配置
|
||||
DEFAULT_API_URL = "https://api.minimaxi.com/v1/t2a_v2"
|
||||
DEFAULT_MODEL = "speech-2.6-turbo"
|
||||
|
||||
@AdapterRegistry.register("tts", "minimax")
|
||||
class MiniMaxTTSAdapter(BaseAdapter[bytes]):
|
||||
"""MiniMax 语音生成适配器。
|
||||
|
||||
需要配置:
|
||||
- api_key: MiniMax API Key
|
||||
- minimax_group_id: 可选 (取决于使用的模型/账户类型)
|
||||
"""
|
||||
|
||||
adapter_type = "tts"
|
||||
adapter_name = "minimax"
|
||||
|
||||
def __init__(self, config: AdapterConfig):
|
||||
super().__init__(config)
|
||||
self.api_url = DEFAULT_API_URL
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
text: str,
|
||||
voice_id: str | None = None,
|
||||
model: str | None = None,
|
||||
speed: float | None = None,
|
||||
vol: float | None = None,
|
||||
pitch: int | None = None,
|
||||
emotion: str | None = None,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
"""生成语音。"""
|
||||
# 1. 优先使用传入参数
|
||||
# 2. 其次使用 Adapter 配置里的 default
|
||||
# 3. 最后使用系统默认值
|
||||
model = model or self.config.model or DEFAULT_MODEL
|
||||
|
||||
cfg = self.config.extra_config or {}
|
||||
|
||||
voice_id = voice_id or cfg.get("voice_id") or "male-qn-qingse"
|
||||
speed = speed if speed is not None else (cfg.get("speed") or 1.0)
|
||||
vol = vol if vol is not None else (cfg.get("vol") or 1.0)
|
||||
pitch = pitch if pitch is not None else (cfg.get("pitch") or 0)
|
||||
emotion = emotion or cfg.get("emotion")
|
||||
group_id = kwargs.get("group_id") or settings.minimax_group_id
|
||||
|
||||
url = self.api_url
|
||||
if group_id:
|
||||
url = f"{self.api_url}?GroupId={group_id}"
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"text": text,
|
||||
"stream": False,
|
||||
"voice_setting": {
|
||||
"voice_id": voice_id,
|
||||
"speed": speed,
|
||||
"vol": vol,
|
||||
"pitch": pitch,
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 32000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"channel": 1
|
||||
}
|
||||
}
|
||||
|
||||
if emotion:
|
||||
payload["voice_setting"]["emotion"] = emotion
|
||||
|
||||
start_time = time.time()
|
||||
logger.info("minimax_generate_start", text_length=len(text), model=model)
|
||||
|
||||
result = await self._call_api(url, payload)
|
||||
|
||||
# 错误处理
|
||||
if result.get("base_resp", {}).get("status_code") != 0:
|
||||
error_msg = result.get("base_resp", {}).get("status_msg", "未知错误")
|
||||
raise ValueError(f"MiniMax API 错误: {error_msg}")
|
||||
|
||||
# Hex 解码 (关键逻辑,从 primary.py 迁移)
|
||||
hex_audio = result.get("data", {}).get("audio")
|
||||
if not hex_audio:
|
||||
raise ValueError("API 响应中未找到音频数据 (data.audio)")
|
||||
|
||||
try:
|
||||
audio_bytes = bytes.fromhex(hex_audio)
|
||||
except ValueError:
|
||||
raise ValueError("MiniMax 返回的音频数据不是有效的 Hex 字符串")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(
|
||||
"minimax_generate_success",
|
||||
elapsed_seconds=round(elapsed, 2),
|
||||
audio_size_bytes=len(audio_bytes),
|
||||
)
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""检查 Minimax API 是否可用。"""
|
||||
try:
|
||||
# 尝试生成极短文本
|
||||
await self.execute("Hi")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=10),
|
||||
retry=retry_if_exception_type((httpx.HTTPError, httpx.TimeoutException)),
|
||||
reraise=True,
|
||||
)
|
||||
async def _call_api(self, url: str, payload: dict) -> dict:
|
||||
"""调用 API,带重试机制。"""
|
||||
timeout = self.config.timeout_ms / 1000
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
"""MiniMax 语音生成适配器 (T2A V2)。"""
|
||||
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.services.adapters.base import AdapterConfig, BaseAdapter
|
||||
from app.services.adapters.registry import AdapterRegistry
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# MiniMax API 配置
|
||||
DEFAULT_API_URL = "https://api.minimaxi.com/v1/t2a_v2"
|
||||
DEFAULT_MODEL = "speech-2.6-turbo"
|
||||
|
||||
@AdapterRegistry.register("tts", "minimax")
|
||||
class MiniMaxTTSAdapter(BaseAdapter[bytes]):
|
||||
"""MiniMax 语音生成适配器。
|
||||
|
||||
需要配置:
|
||||
- api_key: MiniMax API Key
|
||||
- minimax_group_id: 可选 (取决于使用的模型/账户类型)
|
||||
"""
|
||||
|
||||
adapter_type = "tts"
|
||||
adapter_name = "minimax"
|
||||
|
||||
def __init__(self, config: AdapterConfig):
|
||||
super().__init__(config)
|
||||
self.api_url = DEFAULT_API_URL
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
text: str,
|
||||
voice_id: str | None = None,
|
||||
model: str | None = None,
|
||||
speed: float | None = None,
|
||||
vol: float | None = None,
|
||||
pitch: int | None = None,
|
||||
emotion: str | None = None,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
"""生成语音。"""
|
||||
# 1. 优先使用传入参数
|
||||
# 2. 其次使用 Adapter 配置里的 default
|
||||
# 3. 最后使用系统默认值
|
||||
model = model or self.config.model or DEFAULT_MODEL
|
||||
|
||||
cfg = self.config.extra_config or {}
|
||||
|
||||
voice_id = voice_id or cfg.get("voice_id") or "male-qn-qingse"
|
||||
speed = speed if speed is not None else (cfg.get("speed") or 1.0)
|
||||
vol = vol if vol is not None else (cfg.get("vol") or 1.0)
|
||||
pitch = pitch if pitch is not None else (cfg.get("pitch") or 0)
|
||||
emotion = emotion or cfg.get("emotion")
|
||||
group_id = kwargs.get("group_id") or settings.minimax_group_id
|
||||
|
||||
url = self.api_url
|
||||
if group_id:
|
||||
url = f"{self.api_url}?GroupId={group_id}"
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"text": text,
|
||||
"stream": False,
|
||||
"voice_setting": {
|
||||
"voice_id": voice_id,
|
||||
"speed": speed,
|
||||
"vol": vol,
|
||||
"pitch": pitch,
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 32000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"channel": 1
|
||||
}
|
||||
}
|
||||
|
||||
if emotion:
|
||||
payload["voice_setting"]["emotion"] = emotion
|
||||
|
||||
start_time = time.time()
|
||||
logger.info("minimax_generate_start", text_length=len(text), model=model)
|
||||
|
||||
result = await self._call_api(url, payload)
|
||||
|
||||
# 错误处理
|
||||
if result.get("base_resp", {}).get("status_code") != 0:
|
||||
error_msg = result.get("base_resp", {}).get("status_msg", "未知错误")
|
||||
raise ValueError(f"MiniMax API 错误: {error_msg}")
|
||||
|
||||
# Hex 解码 (关键逻辑,从 primary.py 迁移)
|
||||
hex_audio = result.get("data", {}).get("audio")
|
||||
if not hex_audio:
|
||||
raise ValueError("API 响应中未找到音频数据 (data.audio)")
|
||||
|
||||
try:
|
||||
audio_bytes = bytes.fromhex(hex_audio)
|
||||
except ValueError:
|
||||
raise ValueError("MiniMax 返回的音频数据不是有效的 Hex 字符串")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(
|
||||
"minimax_generate_success",
|
||||
elapsed_seconds=round(elapsed, 2),
|
||||
audio_size_bytes=len(audio_bytes),
|
||||
)
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""检查 Minimax API 是否可用。"""
|
||||
try:
|
||||
# 尝试生成极短文本
|
||||
await self.execute("Hi")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=10),
|
||||
retry=retry_if_exception_type((httpx.HTTPError, httpx.TimeoutException)),
|
||||
reraise=True,
|
||||
)
|
||||
async def _call_api(self, url: str, payload: dict) -> dict:
|
||||
"""调用 API,带重试机制。"""
|
||||
timeout = self.config.timeout_ms / 1000
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
Reference in New Issue
Block a user