mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat: add MiniMax-M2.7-highspeed model and MiniMax TTS support
- Add MiniMax-M2.7-highspeed constant to const.py and MODEL_LIST - Update MinimaxBot default model from MiniMax-M2.1 to MiniMax-M2.7 - Add MinimaxVoice TTS provider (voice/minimax/minimax_voice.py) - Supports speech-2.8-hd and speech-2.8-turbo models - SSE streaming with hex-decoded audio chunks - Reuses MINIMAX_API_KEY - Register MinimaxVoice in voice factory - Add unit tests (14 tests, all passing) - Update README with MiniMax-M2.7-highspeed and TTS configuration
This commit is contained in:
@@ -213,6 +213,7 @@ cow install-browser
|
|||||||
+ 添加 `"speech_recognition": true` 将开启语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
|
+ 添加 `"speech_recognition": true` 将开启语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
|
||||||
+ 添加 `"group_speech_recognition": true` 将开启群组语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配 group_chat_prefix 和 group_chat_keyword, 支持语音触发画图);
|
+ 添加 `"group_speech_recognition": true` 将开启群组语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配 group_chat_prefix 和 group_chat_keyword, 支持语音触发画图);
|
||||||
+ 添加 `"voice_reply_voice": true` 将开启语音回复语音(同时作用于私聊和群聊)
|
+ 添加 `"voice_reply_voice": true` 将开启语音回复语音(同时作用于私聊和群聊)
|
||||||
|
+ 使用 MiniMax TTS:设置 `"text_to_voice": "minimax"`,并配置 `minimax_api_key`;可通过 `"tts_voice_id"` 指定发音人(如 `English_Graceful_Lady`),`"text_to_voice_model"` 指定模型(如 `speech-2.8-hd`、`speech-2.8-turbo`)
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -357,7 +358,7 @@ sudo docker logs -f chatgpt-on-wechat
|
|||||||
"minimax_api_key": ""
|
"minimax_api_key": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- `model`: 可填写 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
- `model`: 可填写 `MiniMax-M2.7、MiniMax-M2.7-highspeed、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
||||||
- `minimax_api_key`:MiniMax 平台的 API-KEY,在 [控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建
|
- `minimax_api_key`:MiniMax 平台的 API-KEY,在 [控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建
|
||||||
|
|
||||||
方式二:OpenAI 兼容方式接入,配置如下:
|
方式二:OpenAI 兼容方式接入,配置如下:
|
||||||
@@ -370,7 +371,7 @@ sudo docker logs -f chatgpt-on-wechat
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
- `bot_type`: OpenAI 兼容方式
|
- `bot_type`: OpenAI 兼容方式
|
||||||
- `model`: 可填 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
- `model`: 可填 `MiniMax-M2.7、MiniMax-M2.7-highspeed、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
||||||
- `open_ai_api_base`: MiniMax 平台 API 的 BASE URL
|
- `open_ai_api_base`: MiniMax 平台 API 的 BASE URL
|
||||||
- `open_ai_api_key`: MiniMax 平台的 API-KEY
|
- `open_ai_api_key`: MiniMax 平台的 API-KEY
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ QWQ_PLUS = "qwq-plus"
|
|||||||
|
|
||||||
# MiniMax
|
# MiniMax
|
||||||
MINIMAX_M2_7 = "MiniMax-M2.7" # MiniMax M2.7 - Latest
|
MINIMAX_M2_7 = "MiniMax-M2.7" # MiniMax M2.7 - Latest
|
||||||
|
MINIMAX_M2_7_HIGHSPEED = "MiniMax-M2.7-highspeed" # MiniMax M2.7 highspeed
|
||||||
MINIMAX_M2_5 = "MiniMax-M2.5" # MiniMax M2.5
|
MINIMAX_M2_5 = "MiniMax-M2.5" # MiniMax M2.5
|
||||||
MINIMAX_M2_1 = "MiniMax-M2.1" # MiniMax M2.1
|
MINIMAX_M2_1 = "MiniMax-M2.1" # MiniMax M2.1
|
||||||
MINIMAX_M2_1_LIGHTNING = "MiniMax-M2.1-lightning" # MiniMax M2.1 极速版
|
MINIMAX_M2_1_LIGHTNING = "MiniMax-M2.1-lightning" # MiniMax M2.1 极速版
|
||||||
@@ -175,7 +176,7 @@ MODEL_LIST = [
|
|||||||
QWEN36_PLUS, QWEN35_PLUS, QWEN3_MAX, QWEN_MAX, QWEN_PLUS, QWEN_TURBO, QWEN_LONG,
|
QWEN36_PLUS, QWEN35_PLUS, QWEN3_MAX, QWEN_MAX, QWEN_PLUS, QWEN_TURBO, QWEN_LONG,
|
||||||
|
|
||||||
# MiniMax
|
# MiniMax
|
||||||
MiniMax, MINIMAX_M2_7, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
|
MiniMax, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
|
||||||
|
|
||||||
# GLM
|
# GLM
|
||||||
ZHIPU_AI, GLM_5_TURBO, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
|
ZHIPU_AI, GLM_5_TURBO, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MinimaxBot(Bot):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.args = {
|
self.args = {
|
||||||
"model": conf().get("model") or "MiniMax-M2.1",
|
"model": conf().get("model") or "MiniMax-M2.7",
|
||||||
"temperature": conf().get("temperature", 0.3),
|
"temperature": conf().get("temperature", 0.3),
|
||||||
"top_p": conf().get("top_p", 0.95),
|
"top_p": conf().get("top_p", 0.95),
|
||||||
}
|
}
|
||||||
|
|||||||
184
tests/test_minimax_provider.py
Normal file
184
tests/test_minimax_provider.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
"""
|
||||||
|
Unit tests for MiniMax provider additions:
|
||||||
|
- MiniMax-M2.7-highspeed constant in const.py
|
||||||
|
- Default model update in MinimaxBot
|
||||||
|
- MinimaxVoice TTS provider
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch, PropertyMock
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMinimaxConst(unittest.TestCase):
|
||||||
|
"""Test that MiniMax-M2.7-highspeed is properly registered in const.py."""
|
||||||
|
|
||||||
|
def test_m2_7_highspeed_constant_defined(self):
|
||||||
|
from common import const
|
||||||
|
self.assertTrue(hasattr(const, "MINIMAX_M2_7_HIGHSPEED"))
|
||||||
|
self.assertEqual(const.MINIMAX_M2_7_HIGHSPEED, "MiniMax-M2.7-highspeed")
|
||||||
|
|
||||||
|
def test_m2_7_constant_defined(self):
|
||||||
|
from common import const
|
||||||
|
self.assertEqual(const.MINIMAX_M2_7, "MiniMax-M2.7")
|
||||||
|
|
||||||
|
def test_m2_7_highspeed_in_model_list(self):
|
||||||
|
from common import const
|
||||||
|
self.assertIn("MiniMax-M2.7-highspeed", const.MODEL_LIST)
|
||||||
|
|
||||||
|
def test_m2_7_in_model_list(self):
|
||||||
|
from common import const
|
||||||
|
self.assertIn("MiniMax-M2.7", const.MODEL_LIST)
|
||||||
|
|
||||||
|
def test_minimax_provider_key_defined(self):
|
||||||
|
from common import const
|
||||||
|
self.assertEqual(const.MiniMax, "minimax")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMinimaxBotDefaultModel(unittest.TestCase):
|
||||||
|
"""Test that MinimaxBot defaults to MiniMax-M2.7."""
|
||||||
|
|
||||||
|
def test_default_model_is_m2_7(self):
|
||||||
|
# Patch conf() to return empty config
|
||||||
|
mock_conf = MagicMock()
|
||||||
|
mock_conf.get = MagicMock(side_effect=lambda key, default=None: default)
|
||||||
|
|
||||||
|
with patch("models.minimax.minimax_bot.conf", return_value=mock_conf):
|
||||||
|
with patch("models.minimax.minimax_bot.SessionManager"):
|
||||||
|
from models.minimax import minimax_bot
|
||||||
|
# Reload to pick up patches
|
||||||
|
import importlib
|
||||||
|
importlib.reload(minimax_bot)
|
||||||
|
with patch("models.minimax.minimax_bot.conf", return_value=mock_conf):
|
||||||
|
bot = minimax_bot.MinimaxBot.__new__(minimax_bot.MinimaxBot)
|
||||||
|
bot.args = {
|
||||||
|
"model": mock_conf.get("model") or "MiniMax-M2.7",
|
||||||
|
}
|
||||||
|
self.assertEqual(bot.args["model"], "MiniMax-M2.7")
|
||||||
|
|
||||||
|
def test_default_model_string(self):
|
||||||
|
"""Verify the fallback string literal in minimax_bot.py is MiniMax-M2.7."""
|
||||||
|
import ast
|
||||||
|
bot_path = os.path.join(os.path.dirname(__file__), "..", "models", "minimax", "minimax_bot.py")
|
||||||
|
with open(bot_path) as f:
|
||||||
|
source = f.read()
|
||||||
|
# Verify MiniMax-M2.7 is in the source (not M2.1)
|
||||||
|
self.assertIn("MiniMax-M2.7", source)
|
||||||
|
self.assertNotIn('"MiniMax-M2.1"', source)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMinimaxVoice(unittest.TestCase):
|
||||||
|
"""Test MinimaxVoice TTS provider."""
|
||||||
|
|
||||||
|
def _make_voice(self, api_key="test-key", api_base="https://api.minimax.io/v1"):
|
||||||
|
mock_conf = MagicMock()
|
||||||
|
def conf_get(key, default=None):
|
||||||
|
return {
|
||||||
|
"minimax_api_key": api_key,
|
||||||
|
"minimax_api_base": api_base,
|
||||||
|
}.get(key, default)
|
||||||
|
mock_conf.get = conf_get
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
from voice.minimax.minimax_voice import MinimaxVoice
|
||||||
|
return MinimaxVoice()
|
||||||
|
|
||||||
|
def test_instantiation(self):
|
||||||
|
voice = self._make_voice()
|
||||||
|
self.assertIsNotNone(voice)
|
||||||
|
|
||||||
|
def test_api_base_strips_v1_suffix(self):
|
||||||
|
voice = self._make_voice(api_base="https://api.minimax.io/v1")
|
||||||
|
self.assertEqual(voice.api_base, "https://api.minimax.io")
|
||||||
|
|
||||||
|
def test_api_base_no_trailing_slash(self):
|
||||||
|
voice = self._make_voice(api_base="https://api.minimax.io")
|
||||||
|
self.assertEqual(voice.api_base, "https://api.minimax.io")
|
||||||
|
|
||||||
|
def test_voice_to_text_not_supported(self):
|
||||||
|
voice = self._make_voice()
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
voice.voiceToText("dummy.wav")
|
||||||
|
|
||||||
|
def test_text_to_voice_success(self):
|
||||||
|
"""Test textToVoice with mocked SSE stream response."""
|
||||||
|
import os
|
||||||
|
os.makedirs("tmp", exist_ok=True)
|
||||||
|
|
||||||
|
# Build fake SSE stream bytes
|
||||||
|
audio_hex = bytes([0x49, 0x44, 0x33]).hex() # "ID3" MP3 magic bytes
|
||||||
|
sse_line = f'data: {{"data": {{"audio": "{audio_hex}", "status": 2}}}}\n\n'
|
||||||
|
done_line = "data: [DONE]\n\n"
|
||||||
|
fake_body = (sse_line + done_line).encode("utf-8")
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
mock_response.iter_lines.return_value = [
|
||||||
|
line.encode("utf-8") for line in (sse_line + done_line).splitlines() if line
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_conf = MagicMock()
|
||||||
|
def conf_get(key, default=None):
|
||||||
|
return {
|
||||||
|
"minimax_api_key": "test-key",
|
||||||
|
"minimax_api_base": "https://api.minimax.io",
|
||||||
|
}.get(key, default)
|
||||||
|
mock_conf.get = conf_get
|
||||||
|
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
with patch("voice.minimax.minimax_voice.requests.post", return_value=mock_response):
|
||||||
|
from voice.minimax import minimax_voice
|
||||||
|
import importlib
|
||||||
|
importlib.reload(minimax_voice)
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
voice = minimax_voice.MinimaxVoice()
|
||||||
|
from bridge.reply import ReplyType
|
||||||
|
reply = voice.textToVoice("Hello, world!")
|
||||||
|
self.assertEqual(reply.type, ReplyType.VOICE)
|
||||||
|
self.assertTrue(reply.content.endswith(".mp3"))
|
||||||
|
|
||||||
|
def test_text_to_voice_no_audio_returns_error(self):
|
||||||
|
"""Test that empty SSE stream returns an ERROR reply."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
mock_response.iter_lines.return_value = []
|
||||||
|
|
||||||
|
mock_conf = MagicMock()
|
||||||
|
def conf_get(key, default=None):
|
||||||
|
return {
|
||||||
|
"minimax_api_key": "test-key",
|
||||||
|
"minimax_api_base": "https://api.minimax.io",
|
||||||
|
}.get(key, default)
|
||||||
|
mock_conf.get = conf_get
|
||||||
|
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
with patch("voice.minimax.minimax_voice.requests.post", return_value=mock_response):
|
||||||
|
from voice.minimax import minimax_voice
|
||||||
|
import importlib
|
||||||
|
importlib.reload(minimax_voice)
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
voice = minimax_voice.MinimaxVoice()
|
||||||
|
from bridge.reply import ReplyType
|
||||||
|
reply = voice.textToVoice("Hello")
|
||||||
|
self.assertEqual(reply.type, ReplyType.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVoiceFactory(unittest.TestCase):
|
||||||
|
"""Test that minimax is registered in the voice factory."""
|
||||||
|
|
||||||
|
def test_minimax_voice_factory(self):
|
||||||
|
mock_conf = MagicMock()
|
||||||
|
mock_conf.get = MagicMock(return_value=None)
|
||||||
|
with patch("voice.minimax.minimax_voice.conf", return_value=mock_conf):
|
||||||
|
from voice.factory import create_voice
|
||||||
|
voice = create_voice("minimax")
|
||||||
|
from voice.minimax.minimax_voice import MinimaxVoice
|
||||||
|
self.assertIsInstance(voice, MinimaxVoice)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -54,4 +54,8 @@ def create_voice(voice_type):
|
|||||||
from voice.tencent.tencent_voice import TencentVoice
|
from voice.tencent.tencent_voice import TencentVoice
|
||||||
|
|
||||||
return TencentVoice()
|
return TencentVoice()
|
||||||
|
elif voice_type == "minimax":
|
||||||
|
from voice.minimax.minimax_voice import MinimaxVoice
|
||||||
|
|
||||||
|
return MinimaxVoice()
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|||||||
0
voice/minimax/__init__.py
Normal file
0
voice/minimax/__init__.py
Normal file
106
voice/minimax/minimax_voice.py
Normal file
106
voice/minimax/minimax_voice.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
"""
|
||||||
|
MiniMax TTS voice service
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from bridge.reply import Reply, ReplyType
|
||||||
|
from common.log import logger
|
||||||
|
from config import conf
|
||||||
|
from voice.voice import Voice
|
||||||
|
|
||||||
|
|
||||||
|
MINIMAX_TTS_VOICES = [
|
||||||
|
"English_Graceful_Lady",
|
||||||
|
"English_Insightful_Speaker",
|
||||||
|
"English_radiant_girl",
|
||||||
|
"English_Persuasive_Man",
|
||||||
|
"English_Lucky_Robot",
|
||||||
|
"English_expressive_narrator",
|
||||||
|
"Chinese_Warm_Woman",
|
||||||
|
"Chinese_Gentle_Man",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MinimaxVoice(Voice):
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = conf().get("minimax_api_key")
|
||||||
|
self.api_base = conf().get("minimax_api_base") or "https://api.minimax.io"
|
||||||
|
# Strip trailing /v1 if present so we can always append /v1/t2a_v2
|
||||||
|
self.api_base = self.api_base.rstrip("/")
|
||||||
|
if self.api_base.endswith("/v1"):
|
||||||
|
self.api_base = self.api_base[:-3]
|
||||||
|
|
||||||
|
def voiceToText(self, voice_file):
|
||||||
|
"""MiniMax does not provide an ASR endpoint; raise NotImplementedError."""
|
||||||
|
raise NotImplementedError("MiniMax voice-to-text is not supported")
|
||||||
|
|
||||||
|
def textToVoice(self, text):
|
||||||
|
try:
|
||||||
|
model = conf().get("text_to_voice_model") or "speech-2.8-hd"
|
||||||
|
voice_id = conf().get("tts_voice_id") or "English_Graceful_Lady"
|
||||||
|
|
||||||
|
url = f"{self.api_base}/v1/t2a_v2"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"text": text,
|
||||||
|
"stream": True,
|
||||||
|
"voice_setting": {
|
||||||
|
"voice_id": voice_id,
|
||||||
|
"speed": 1,
|
||||||
|
"vol": 1,
|
||||||
|
"pitch": 0,
|
||||||
|
},
|
||||||
|
"audio_setting": {
|
||||||
|
"sample_rate": 32000,
|
||||||
|
"bitrate": 128000,
|
||||||
|
"format": "mp3",
|
||||||
|
"channel": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, json=payload, stream=True, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Parse SSE stream and collect hex-encoded audio chunks
|
||||||
|
audio_chunks = []
|
||||||
|
buffer = ""
|
||||||
|
for raw in response.iter_lines():
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
line = raw.decode("utf-8") if isinstance(raw, bytes) else raw
|
||||||
|
if not line.startswith("data:"):
|
||||||
|
continue
|
||||||
|
json_str = line[5:].strip()
|
||||||
|
if not json_str or json_str == "[DONE]":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
event_data = json.loads(json_str)
|
||||||
|
audio_hex = event_data.get("data", {}).get("audio")
|
||||||
|
if audio_hex:
|
||||||
|
audio_chunks.append(bytes.fromhex(audio_hex))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not audio_chunks:
|
||||||
|
logger.error("[MINIMAX] TTS returned no audio data")
|
||||||
|
return Reply(ReplyType.ERROR, "语音合成失败,未获取到音频数据")
|
||||||
|
|
||||||
|
audio_data = b"".join(audio_chunks)
|
||||||
|
file_name = "tmp/" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + str(random.randint(0, 1000)) + ".mp3"
|
||||||
|
with open(file_name, "wb") as f:
|
||||||
|
f.write(audio_data)
|
||||||
|
|
||||||
|
logger.info(f"[MINIMAX] textToVoice success, file={file_name}")
|
||||||
|
return Reply(ReplyType.VOICE, file_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[MINIMAX] textToVoice error: {e}")
|
||||||
|
return Reply(ReplyType.ERROR, "遇到了一点小问题,请稍后再试")
|
||||||
Reference in New Issue
Block a user