mirror of https://github.com/Mai-with-u/MaiBot.git
187 lines
6.9 KiB
Python
187 lines
6.9 KiB
Python
"""
|
||
GSV2P 后端实现
|
||
使用 GSV2P 云端 API 进行语音合成
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
from typing import Optional, Dict, Any, Tuple
|
||
from .base import TTSBackendBase, TTSResult
|
||
from ..utils.file import TTSFileManager
|
||
from ..utils.session import TTSSessionManager
|
||
from ..config_keys import ConfigKeys
|
||
from src.common.logger import get_logger
|
||
|
||
logger = get_logger("tts_gsv2p")
|
||
|
||
# 重试配置
|
||
MAX_RETRIES = 5 # 最大重试次数
|
||
RETRY_DELAY = 3.0 # 重试间隔(秒)
|
||
|
||
|
||
class GSV2PBackend(TTSBackendBase):
|
||
"""
|
||
GSV2P 后端
|
||
|
||
使用 GSV2P 云端 API 进行高质量语音合成
|
||
"""
|
||
|
||
backend_name = "gsv2p"
|
||
backend_description = "GSV2P云端API语音合成"
|
||
support_private_chat = True
|
||
default_audio_format = "mp3"
|
||
|
||
def get_default_voice(self) -> str:
|
||
"""获取默认音色"""
|
||
return self.get_config(ConfigKeys.GSV2P_DEFAULT_VOICE, "原神-中文-派蒙_ZH")
|
||
|
||
def validate_config(self) -> Tuple[bool, str]:
|
||
"""验证配置"""
|
||
api_token = self.get_config(ConfigKeys.GSV2P_API_TOKEN, "")
|
||
if not api_token:
|
||
return False, "GSV2P后端缺少API Token配置"
|
||
return True, ""
|
||
|
||
async def _make_request(
|
||
self,
|
||
api_url: str,
|
||
request_data: Dict[str, Any],
|
||
headers: Dict[str, str],
|
||
timeout: int
|
||
) -> Tuple[bool, Any, str]:
|
||
"""
|
||
发送单次API请求
|
||
|
||
Returns:
|
||
(成功标志, 音频数据或None, 错误信息)
|
||
"""
|
||
session_manager = await TTSSessionManager.get_instance()
|
||
async with session_manager.post(
|
||
api_url,
|
||
json=request_data,
|
||
headers=headers,
|
||
backend_name="gsv2p",
|
||
timeout=timeout
|
||
) as response:
|
||
if response.status == 200:
|
||
content_type = response.headers.get('Content-Type', '')
|
||
audio_data = await response.read()
|
||
|
||
# 检查是否返回了JSON错误(服务端不稳定时会返回参数错误)
|
||
if 'application/json' in content_type:
|
||
try:
|
||
error_json = json.loads(audio_data.decode('utf-8'))
|
||
error_msg = error_json.get('error', {}).get('message', str(error_json))
|
||
# 参数错误通常是服务端临时问题,可以重试
|
||
return False, None, f"API返回错误: {error_msg}"
|
||
except Exception:
|
||
return False, None, "API返回异常响应"
|
||
|
||
# 验证音频数据
|
||
is_valid, error_msg = TTSFileManager.validate_audio_data(audio_data)
|
||
if not is_valid:
|
||
return False, None, f"音频数据无效: {error_msg}"
|
||
|
||
return True, audio_data, ""
|
||
else:
|
||
error_text = await response.text()
|
||
return False, None, f"API调用失败: {response.status} - {error_text[:100]}"
|
||
|
||
async def execute(
|
||
self,
|
||
text: str,
|
||
voice: Optional[str] = None,
|
||
**kwargs
|
||
) -> TTSResult:
|
||
"""
|
||
执行GSV2P语音合成(带重试机制)
|
||
|
||
Args:
|
||
text: 待转换的文本
|
||
voice: 音色名称
|
||
|
||
Returns:
|
||
TTSResult
|
||
"""
|
||
# 验证配置
|
||
is_valid, error_msg = self.validate_config()
|
||
if not is_valid:
|
||
return TTSResult(False, error_msg, backend_name=self.backend_name)
|
||
|
||
# 验证文本
|
||
if not text or not text.strip():
|
||
return TTSResult(False, "待合成的文本为空", backend_name=self.backend_name)
|
||
|
||
# 获取配置
|
||
api_url = self.get_config(ConfigKeys.GSV2P_API_URL, "https://gsv2p.acgnai.top/v1/audio/speech")
|
||
api_token = self.get_config(ConfigKeys.GSV2P_API_TOKEN, "")
|
||
timeout = self.get_config(ConfigKeys.GSV2P_TIMEOUT, 30)
|
||
|
||
if not voice:
|
||
voice = self.get_default_voice()
|
||
|
||
# 构建请求参数(注意:other_params 已被 API 废弃,不再支持)
|
||
request_data: Dict[str, Any] = {
|
||
"model": self.get_config(ConfigKeys.GSV2P_MODEL, "tts-v4"),
|
||
"input": text,
|
||
"voice": voice,
|
||
"response_format": self.get_config(ConfigKeys.GSV2P_RESPONSE_FORMAT, "mp3"),
|
||
"speed": self.get_config(ConfigKeys.GSV2P_SPEED, 1)
|
||
}
|
||
|
||
headers = {
|
||
"accept": "application/json",
|
||
"Authorization": f"Bearer {api_token}",
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
logger.info(f"{self.log_prefix} GSV2P请求: text='{text[:50]}...', voice={voice}")
|
||
logger.debug(f"{self.log_prefix} GSV2P完整请求参数: {json.dumps(request_data, ensure_ascii=False, indent=2)}")
|
||
|
||
last_error = ""
|
||
for attempt in range(1, MAX_RETRIES + 1):
|
||
try:
|
||
success, audio_data, error_msg = await self._make_request(
|
||
api_url, request_data, headers, timeout
|
||
)
|
||
|
||
if success and audio_data:
|
||
if attempt > 1:
|
||
logger.info(f"{self.log_prefix} GSV2P第{attempt}次重试成功")
|
||
|
||
logger.info(f"{self.log_prefix} GSV2P响应: 数据大小={len(audio_data)}字节")
|
||
|
||
# 使用统一的发送方法
|
||
audio_format = self.get_config(ConfigKeys.GSV2P_RESPONSE_FORMAT, "mp3")
|
||
return await self.send_audio(
|
||
audio_data=audio_data,
|
||
audio_format=audio_format,
|
||
prefix="tts_gsv2p",
|
||
voice_info=f"音色: {voice}"
|
||
)
|
||
else:
|
||
last_error = error_msg
|
||
if attempt < MAX_RETRIES:
|
||
logger.warning(f"{self.log_prefix} GSV2P请求失败 ({error_msg}), {RETRY_DELAY}秒后重试 (尝试 {attempt}/{MAX_RETRIES})")
|
||
await asyncio.sleep(RETRY_DELAY)
|
||
else:
|
||
logger.error(f"{self.log_prefix} GSV2P请求失败,已达最大重试次数: {error_msg}")
|
||
|
||
except asyncio.TimeoutError:
|
||
last_error = "API调用超时"
|
||
if attempt < MAX_RETRIES:
|
||
logger.warning(f"{self.log_prefix} GSV2P超时, {RETRY_DELAY}秒后重试 (尝试 {attempt}/{MAX_RETRIES})")
|
||
await asyncio.sleep(RETRY_DELAY)
|
||
else:
|
||
logger.error(f"{self.log_prefix} GSV2P超时,已达最大重试次数")
|
||
|
||
except Exception as e:
|
||
last_error = str(e)
|
||
logger.error(f"{self.log_prefix} GSV2P执行错误: {e}")
|
||
if attempt < MAX_RETRIES:
|
||
await asyncio.sleep(RETRY_DELAY)
|
||
else:
|
||
break
|
||
|
||
return TTSResult(False, f"GSV2P {last_error} (已重试{MAX_RETRIES}次)", backend_name=self.backend_name)
|