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:
FuShang114
2026-06-27 15:04:41 +08:00
committed by GitHub
parent 0ef790d289
commit b8f4c7d515
7 changed files with 119 additions and 74 deletions
-12
View File
@@ -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
View File
@@ -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