mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-01 15:32:49 +08:00
fix: handle MiMo STT audio and reasoning output (#8938)
* fix: handle MiMo STT audio and reasoning output * fix: 移除 MiMo STT 的系统和用户提示词配置 --------- Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
@@ -1594,8 +1594,6 @@ CONFIG_METADATA_2 = {
|
||||
"api_key": "",
|
||||
"api_base": "https://api.xiaomimimo.com/v1",
|
||||
"model": "mimo-v2-omni",
|
||||
"mimo-stt-system-prompt": "You are a speech transcription assistant. Transcribe the spoken content from the audio exactly and return only the transcription text.",
|
||||
"mimo-stt-user-prompt": "Please transcribe the content of the audio and return only the transcription text.",
|
||||
"timeout": "20",
|
||||
"proxy": "",
|
||||
},
|
||||
@@ -2598,16 +2596,6 @@ CONFIG_METADATA_2 = {
|
||||
"type": "int",
|
||||
"hint": "超时时间,单位为秒。",
|
||||
},
|
||||
"mimo-stt-system-prompt": {
|
||||
"description": "系统提示词",
|
||||
"type": "string",
|
||||
"hint": "用于指导 MiMo STT 转录行为的 system prompt。",
|
||||
},
|
||||
"mimo-stt-user-prompt": {
|
||||
"description": "用户提示词",
|
||||
"type": "string",
|
||||
"hint": "附加给 MiMo STT 的用户提示词,用于约束返回结果格式。",
|
||||
},
|
||||
"openai-tts-voice": {
|
||||
"description": "voice",
|
||||
"type": "string",
|
||||
|
||||
@@ -11,13 +11,6 @@ DEFAULT_MIMO_TTS_MODEL = "mimo-v2-tts"
|
||||
DEFAULT_MIMO_TTS_VOICE = "mimo_default"
|
||||
DEFAULT_MIMO_TTS_SEED_TEXT = "Hello, MiMo, have you had lunch?"
|
||||
DEFAULT_MIMO_STT_MODEL = "mimo-v2-omni"
|
||||
DEFAULT_MIMO_STT_SYSTEM_PROMPT = (
|
||||
"You are a speech transcription assistant. "
|
||||
"Transcribe the spoken content from the audio exactly and return only the transcription text."
|
||||
)
|
||||
DEFAULT_MIMO_STT_USER_PROMPT = (
|
||||
"Please transcribe the content of the audio and return only the transcription text."
|
||||
)
|
||||
|
||||
|
||||
class MiMoAPIError(Exception):
|
||||
@@ -74,7 +67,7 @@ async def prepare_audio_input(audio_source: str) -> tuple[str, list[Path]]:
|
||||
)
|
||||
if audio_data is None:
|
||||
raise ValueError(f"Invalid audio data: {describe_media_ref(audio_source)}")
|
||||
return audio_data.base64_data, []
|
||||
return audio_data.to_data_url(), []
|
||||
|
||||
|
||||
def cleanup_files(paths: list[Path]) -> None:
|
||||
|
||||
@@ -4,8 +4,6 @@ from ..register import register_provider_adapter
|
||||
from .mimo_api_common import (
|
||||
DEFAULT_MIMO_API_BASE,
|
||||
DEFAULT_MIMO_STT_MODEL,
|
||||
DEFAULT_MIMO_STT_SYSTEM_PROMPT,
|
||||
DEFAULT_MIMO_STT_USER_PROMPT,
|
||||
MiMoAPIError,
|
||||
build_api_url,
|
||||
build_headers,
|
||||
@@ -32,14 +30,6 @@ class ProviderMiMoSTTAPI(STTProvider):
|
||||
self.api_base = provider_config.get("api_base", DEFAULT_MIMO_API_BASE)
|
||||
self.proxy = provider_config.get("proxy", "")
|
||||
self.timeout = normalize_timeout(provider_config.get("timeout", 20))
|
||||
self.system_prompt = provider_config.get(
|
||||
"mimo-stt-system-prompt",
|
||||
DEFAULT_MIMO_STT_SYSTEM_PROMPT,
|
||||
)
|
||||
self.user_prompt = provider_config.get(
|
||||
"mimo-stt-user-prompt",
|
||||
DEFAULT_MIMO_STT_USER_PROMPT,
|
||||
)
|
||||
self.set_model(provider_config.get("model", DEFAULT_MIMO_STT_MODEL))
|
||||
self.client = create_http_client(self.timeout, self.proxy)
|
||||
|
||||
@@ -48,10 +38,6 @@ class ProviderMiMoSTTAPI(STTProvider):
|
||||
payload = {
|
||||
"model": self.model_name,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": self.system_prompt,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
@@ -61,10 +47,6 @@ class ProviderMiMoSTTAPI(STTProvider):
|
||||
"data": audio_data_url,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": self.user_prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -88,9 +70,10 @@ class ProviderMiMoSTTAPI(STTProvider):
|
||||
data = response.json()
|
||||
choices = data.get("choices") or []
|
||||
first_choice = choices[0] if choices else {}
|
||||
content = first_choice.get("message", {}).get("content", "")
|
||||
message = (first_choice or {}).get("message") or {}
|
||||
content = message.get("content") or message.get("reasoning_content") or ""
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
raise MiMoAPIError(f"MiMo STT API returned empty transcription: {data}")
|
||||
raise MiMoAPIError("MiMo STT API returned empty transcription")
|
||||
return content.strip()
|
||||
finally:
|
||||
cleanup_files(cleanup_paths)
|
||||
|
||||
@@ -1586,14 +1586,6 @@
|
||||
"description": "Timeout",
|
||||
"hint": "Timeout in seconds."
|
||||
},
|
||||
"mimo-stt-system-prompt": {
|
||||
"description": "System prompt",
|
||||
"hint": "System prompt used to guide MiMo STT transcription behavior."
|
||||
},
|
||||
"mimo-stt-user-prompt": {
|
||||
"description": "User prompt",
|
||||
"hint": "Additional user prompt sent to MiMo STT to constrain the returned transcription format."
|
||||
},
|
||||
"openai-tts-voice": {
|
||||
"description": "voice",
|
||||
"hint": "OpenAI TTS voice. OpenAI defaults: 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'."
|
||||
|
||||
@@ -1583,14 +1583,6 @@
|
||||
"description": "Таймаут (сек)",
|
||||
"hint": "Максимальное время ожидания ответа."
|
||||
},
|
||||
"mimo-stt-system-prompt": {
|
||||
"description": "Системный промпт",
|
||||
"hint": "System prompt, который управляет поведением MiMo STT при распознавании."
|
||||
},
|
||||
"mimo-stt-user-prompt": {
|
||||
"description": "Пользовательский промпт",
|
||||
"hint": "Дополнительный user prompt для MiMo STT, который помогает задать формат результата."
|
||||
},
|
||||
"openai-tts-voice": {
|
||||
"description": "API Base URL",
|
||||
"hint": "Голоса OpenAI TTS: alloy, echo и др."
|
||||
|
||||
@@ -1588,14 +1588,6 @@
|
||||
"description": "超时时间",
|
||||
"hint": "超时时间,单位为秒。"
|
||||
},
|
||||
"mimo-stt-system-prompt": {
|
||||
"description": "系统提示词",
|
||||
"hint": "用于指导 MiMo STT 转录行为的 system prompt。"
|
||||
},
|
||||
"mimo-stt-user-prompt": {
|
||||
"description": "用户提示词",
|
||||
"hint": "附加给 MiMo STT 的用户提示词,用于约束返回结果格式。"
|
||||
},
|
||||
"openai-tts-voice": {
|
||||
"description": "voice",
|
||||
"hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'"
|
||||
|
||||
+115
-10
@@ -3,10 +3,16 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.provider.sources.mimo_api_common import MiMoAPIError, build_headers
|
||||
from astrbot.core.provider.sources.mimo_api_common import (
|
||||
MiMoAPIError,
|
||||
build_headers,
|
||||
prepare_audio_input,
|
||||
)
|
||||
from astrbot.core.provider.sources.mimo_stt_api_source import ProviderMiMoSTTAPI
|
||||
from astrbot.core.provider.sources.mimo_tts_api_source import ProviderMiMoTTSAPI
|
||||
|
||||
MIMO_STT_TEST_AUDIO_DATA_URL = "data:audio/wav;base64,ZmFrZQ=="
|
||||
|
||||
|
||||
def _make_tts_provider(overrides: dict | None = None) -> ProviderMiMoTTSAPI:
|
||||
provider_config = {
|
||||
@@ -190,7 +196,7 @@ async def test_mimo_tts_get_audio_handles_empty_choices():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_payload_includes_audio_and_prompt(monkeypatch):
|
||||
async def test_mimo_stt_payload_includes_audio_only(monkeypatch):
|
||||
provider = _make_stt_provider(
|
||||
{
|
||||
"mimo-stt-system-prompt": "system prompt",
|
||||
@@ -201,7 +207,7 @@ async def test_mimo_stt_payload_includes_audio_and_prompt(monkeypatch):
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_prepare_audio_input(_audio_source: str):
|
||||
return "ZmFrZQ==", []
|
||||
return MIMO_STT_TEST_AUDIO_DATA_URL, []
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
@@ -227,13 +233,85 @@ async def test_mimo_stt_payload_includes_audio_and_prompt(monkeypatch):
|
||||
result = await provider.get_text("/tmp/test.wav")
|
||||
|
||||
assert result == "transcribed text"
|
||||
assert captured["json"]["messages"][0]["content"] == "system prompt"
|
||||
assert captured["json"]["messages"][1]["content"][0]["type"] == "input_audio"
|
||||
assert (
|
||||
captured["json"]["messages"][1]["content"][0]["input_audio"]["data"]
|
||||
== "ZmFrZQ=="
|
||||
assert captured["json"]["messages"] == [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {
|
||||
"data": MIMO_STT_TEST_AUDIO_DATA_URL,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_prepare_audio_input_returns_data_url(monkeypatch):
|
||||
class _ResolvedAudio:
|
||||
base64_data = "ZmFrZQ=="
|
||||
mime_type = "audio/wav"
|
||||
format = "wav"
|
||||
|
||||
def to_data_url(self):
|
||||
return MIMO_STT_TEST_AUDIO_DATA_URL
|
||||
|
||||
class _Resolver:
|
||||
def __init__(self, audio_source, **kwargs):
|
||||
assert audio_source == "/tmp/test.wav"
|
||||
assert kwargs == {
|
||||
"media_type": "audio",
|
||||
"default_suffix": ".wav",
|
||||
}
|
||||
|
||||
async def to_base64_data(self, **kwargs):
|
||||
assert kwargs == {
|
||||
"strict": True,
|
||||
"target_format": "wav",
|
||||
}
|
||||
return _ResolvedAudio()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.provider.sources.mimo_api_common.MediaResolver",
|
||||
_Resolver,
|
||||
)
|
||||
assert captured["json"]["messages"][1]["content"][1]["text"] == "user prompt"
|
||||
|
||||
audio_data, cleanup_paths = await prepare_audio_input("/tmp/test.wav")
|
||||
|
||||
assert audio_data == MIMO_STT_TEST_AUDIO_DATA_URL
|
||||
assert cleanup_paths == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_get_text_uses_reasoning_content(monkeypatch):
|
||||
provider = _make_stt_provider()
|
||||
|
||||
async def fake_prepare_audio_input(_audio_source: str):
|
||||
return MIMO_STT_TEST_AUDIO_DATA_URL, []
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
text = '{"choices":[{"message":{"content":"","reasoning_content":"转写结果"}}]}'
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"choices": [
|
||||
{"message": {"content": "", "reasoning_content": "转写结果"}}
|
||||
]
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.provider.sources.mimo_stt_api_source.prepare_audio_input",
|
||||
fake_prepare_audio_input,
|
||||
)
|
||||
provider.client = SimpleNamespace(post=_fake_post(_Response()))
|
||||
|
||||
assert await provider.get_text("/tmp/test.wav") == "转写结果"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -241,7 +319,7 @@ async def test_mimo_stt_get_text_handles_empty_choices(monkeypatch):
|
||||
provider = _make_stt_provider()
|
||||
|
||||
async def fake_prepare_audio_input(_audio_source: str):
|
||||
return "ZmFrZQ==", []
|
||||
return MIMO_STT_TEST_AUDIO_DATA_URL, []
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
@@ -263,6 +341,33 @@ async def test_mimo_stt_get_text_handles_empty_choices(monkeypatch):
|
||||
await provider.get_text("/tmp/test.wav")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_get_text_handles_null_message(monkeypatch):
|
||||
provider = _make_stt_provider()
|
||||
|
||||
async def fake_prepare_audio_input(_audio_source: str):
|
||||
return MIMO_STT_TEST_AUDIO_DATA_URL, []
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
text = '{"choices":[{"message":null}]}'
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"choices": [{"message": None}]}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.provider.sources.mimo_stt_api_source.prepare_audio_input",
|
||||
fake_prepare_audio_input,
|
||||
)
|
||||
provider.client = SimpleNamespace(post=_fake_post(_Response()))
|
||||
|
||||
with pytest.raises(MiMoAPIError, match="returned empty transcription"):
|
||||
await provider.get_text("/tmp/test.wav")
|
||||
|
||||
|
||||
def _fake_post(response):
|
||||
async def _post(*_args, **_kwargs):
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user