Files
banban/talkingq-url/implementations/minimax_tts.py
HycJack f9f1183e4b 休眠时段操作小程序加提示,博士角色语速调快一点,
刷自己卡没有留言情况下播放最后一条留言,增加角色切换提示音。
2026-05-26 01:50:23 +08:00

240 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import uuid
import aiohttp
import aiofiles
import binascii
import json
from typing import Optional, List, Any, AsyncGenerator
from config import settings
from interfaces.tts import TTS
from implementations.base_service import BaseService
from utils.logger import session_logger
from services.tts_audio_cleaner import TTSAudioCleaner
class MiniMaxTTS(TTS, BaseService):
def __init__(self, selected_role=None):
BaseService.__init__(self, "tts", selected_role)
self.api_key = settings.minimax_api_key
self.group_id = settings.minimax_group_id
self.base_url = settings.minimax_base_url
self.connected = False
self.device_id = "unknown"
self.session_id = "unknown"
self.voice_id = "cartoon-boy-01" # 默认音色
if selected_role and "minimax_voice_id" in selected_role:
self.voice_id = selected_role["minimax_voice_id"]
self.model = "speech-02-turbo"
self.selected_role = selected_role
self._client_session: Optional[aiohttp.ClientSession] = None
async def connect(self):
"""连接到MiniMax服务"""
if self._client_session is None or self._client_session.closed:
connector = aiohttp.TCPConnector(
limit=20,
limit_per_host=5,
force_close=True,
enable_cleanup_closed=True
)
timeout = aiohttp.ClientTimeout(total=120, connect=30)
self._client_session = aiohttp.ClientSession(
connector=connector,
timeout=timeout,
max_field_size=1024*1024,
max_line_size=10*1024*1024
)
self.connected = True
session_logger.info(
self.device_id,
self.session_id,
f"MiniMax TTS服务已连接使用音色: {self.voice_id}"
)
async def close(self):
"""关闭MiniMax服务连接"""
self.connected = False
if self._client_session and not self._client_session.closed:
await self._client_session.close()
async def tts(
self,
text: str,
output_file_prefix: Optional[str] = None,
tts_format: str = "mp3",
selected_role: dict = None,
session: Optional[Any] = None,
language: str = None,
) -> Optional[List[str]]:
"""
将文本转换为语音
返回值为包含音频URL的列表
"""
if selected_role:
self.selected_role = selected_role
if "minimax_voice_id" in selected_role:
self.voice_id = selected_role["minimax_voice_id"]
if session and hasattr(session, "device_id") and session.device_id:
self.device_id = session.device_id
if session and hasattr(session, "session_id") and session.session_id:
self.session_id = session.session_id
unique_id = f"{self.session_id}_{uuid.uuid4().hex[:8]}"
if output_file_prefix:
output_file = f"{output_file_prefix}.{tts_format}"
else:
await TTSAudioCleaner.prepare_output_directory()
tts_dir = os.path.join(settings.assets_dir, "tts_audio")
output_file = os.path.join(tts_dir, f"{unique_id}.{tts_format}")
try:
session_logger.info(
self.device_id,
self.session_id,
f"开始MiniMax流式语音合成文本长度: {len(text)},输出文件: {output_file}"
)
urls = await self._process_stream(text, output_file, language)
return urls if urls else None
except Exception as e:
session_logger.error(
self.device_id,
self.session_id,
f"MiniMax TTS合成失败: {str(e)}",
exc_info=True
)
return None
async def _process_stream(self, text: str, output_file: str, language: str = None) -> Optional[List[str]]:
"""使用流式方式处理文本并返回URL"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
speed = 1.0
if self.selected_role and "boshi" in self.selected_role:
speed = 2.0
payload = {
"model": self.model,
"text": text,
"stream": True,
"voice_setting": {
"voice_id": self.voice_id,
"speed": speed,
"vol": 2.0,
"pitch": 0
},
"audio_setting": {
"sample_rate": 16000,
"bitrate": 32000,
"format": "mp3",
"channel": 1
}
}
if language:
lang_mapping = {
"zh": "Chinese",
"en": "English",
"fr": "French",
"de": "German",
"es": "Spanish",
"yue": "Chinese,Yue"
}
payload["language_boost"] = lang_mapping.get(language, "auto")
else:
payload["language_boost"] = "auto"
url = f"{self.base_url}?GroupId={self.group_id}"
try:
session_logger.info(
self.device_id,
self.session_id,
f"发送MiniMax TTS流式请求: {url}"
)
audio_buffer = bytearray()
if self._client_session is None or self._client_session.closed:
await self.connect()
async with self._client_session.post(url, json=payload, headers=headers) as response:
if response.status != 200:
error_text = await response.text()
session_logger.error(
self.device_id,
self.session_id,
f"MiniMax TTS请求失败: {response.status}, {error_text}"
)
return None
session_logger.info(
self.device_id,
self.session_id,
"开始接收MiniMax TTS流式响应"
)
buffer = b""
chunk_size = 8192
async for chunk in response.content.iter_chunked(chunk_size):
buffer += chunk
while b'\n' in buffer:
line, buffer = buffer.split(b'\n', 1)
line = line.strip()
if not line or not line.startswith(b'data:'):
continue
try:
data_json = json.loads(line[5:])
if "data" in data_json and "audio" in data_json["data"]:
status = data_json["data"].get("status", 1)
if status == 1:
audio_hex = data_json["data"]["audio"]
audio_binary = binascii.unhexlify(audio_hex)
audio_buffer.extend(audio_binary)
if status == 2:
session_logger.info(
self.device_id,
self.session_id,
"MiniMax TTS流式合成完成"
)
except json.JSONDecodeError as e:
session_logger.warning(
self.device_id,
self.session_id,
f"JSON解析失败: {str(e)}"
)
except Exception as e:
session_logger.error(
self.device_id,
self.session_id,
f"处理MiniMax TTS流式响应出错: {str(e)}",
exc_info=True
)
async with aiofiles.open(output_file, 'wb') as f:
await f.write(audio_buffer)
relative_path = os.path.relpath(output_file, settings.assets_dir)
url = f"assets/{relative_path}"
session_logger.info(
self.device_id,
self.session_id,
f"MiniMax TTS流式合成完成生成URL: {url}"
)
return [url]
except Exception as e:
session_logger.error(
self.device_id,
self.session_id,
f"MiniMax TTS流式处理失败: {str(e)}",
exc_info=True
)
return None