mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix: adapt MiMo STT to V2.5 models and reject non-WAV audio payloads (#9118)
* fix: adapt MiMo STT to V2.5 models and reject non-WAV audio The MiMo-V2 series went offline on 2026-06-30, so the default STT model mimo-v2-omni fails for every default configuration. Switch the default to mimo-v2.5-asr, the dedicated speech recognition model whose official docs use exactly the bare input_audio payload this provider sends. For non-ASR multimodal models such as mimo-v2.5, the audio understanding docs require a text instruction alongside the audio, so restore the system/user transcription prompts for that model family only. Also validate that the resolved audio payload really is RIFF/WAVE before calling the API: when a platform voice file (e.g. Tencent SILK from QQ) slips through the WAV conversion chain unchanged, fail locally with an actionable error instead of the opaque HTTP 400 from the API. Fixes #9113 * fix: accept unpadded MiMo wav headers --------- Co-authored-by: tangtaizong666 <212687958+tangtaizong666@users.noreply.github.com>
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import asyncio
|
||||
import base64
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.provider.sources.mimo_api_common import (
|
||||
MiMoAPIError,
|
||||
_validate_wav_payload,
|
||||
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=="
|
||||
MIMO_STT_TEST_WAV_HEADER = b"RIFF\x24\x08\x00\x00WAVEfmt "
|
||||
MIMO_STT_TEST_AUDIO_BASE64 = base64.b64encode(MIMO_STT_TEST_WAV_HEADER).decode()
|
||||
MIMO_STT_TEST_AUDIO_DATA_URL = f"data:audio/wav;base64,{MIMO_STT_TEST_AUDIO_BASE64}"
|
||||
|
||||
|
||||
def _make_tts_provider(overrides: dict | None = None) -> ProviderMiMoTTSAPI:
|
||||
@@ -33,7 +37,7 @@ def _make_stt_provider(overrides: dict | None = None) -> ProviderMiMoSTTAPI:
|
||||
provider_config = {
|
||||
"id": "test-mimo-stt",
|
||||
"type": "mimo_stt_api",
|
||||
"model": "mimo-v2-omni",
|
||||
"model": "mimo-v2.5-asr",
|
||||
"api_key": "test-key",
|
||||
}
|
||||
if overrides:
|
||||
@@ -196,7 +200,8 @@ async def test_mimo_tts_get_audio_handles_empty_choices():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_payload_includes_audio_only(monkeypatch):
|
||||
async def test_mimo_stt_asr_model_payload_includes_audio_only(monkeypatch):
|
||||
"""专用 ASR 模型按官方语音识别文档只传 input_audio,不带任何提示词。"""
|
||||
provider = _make_stt_provider(
|
||||
{
|
||||
"mimo-stt-system-prompt": "system prompt",
|
||||
@@ -248,10 +253,91 @@ async def test_mimo_stt_payload_includes_audio_only(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
def test_mimo_stt_default_model_is_v25_asr():
|
||||
"""mimo-v2-omni 已于 2026-06-30 下线,默认模型应为 mimo-v2.5-asr。"""
|
||||
provider = ProviderMiMoSTTAPI(
|
||||
provider_config={
|
||||
"id": "test-mimo-stt",
|
||||
"type": "mimo_stt_api",
|
||||
"api_key": "test-key",
|
||||
},
|
||||
provider_settings={},
|
||||
)
|
||||
try:
|
||||
assert provider.model_name == "mimo-v2.5-asr"
|
||||
finally:
|
||||
asyncio.run(provider.terminate())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_multimodal_model_payload_includes_transcription_prompts(
|
||||
monkeypatch,
|
||||
):
|
||||
"""非 ASR 模型(如 mimo-v2.5)按官方音频理解文档要求携带 system 与 text 指令。"""
|
||||
provider = _make_stt_provider({"model": "mimo-v2.5"})
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
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":"transcribed text"}}]}'
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"choices": [{"message": {"content": "transcribed text"}}]}
|
||||
|
||||
async def fake_post(_url, headers=None, json=None):
|
||||
captured["json"] = json
|
||||
return _Response()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.provider.sources.mimo_stt_api_source.prepare_audio_input",
|
||||
fake_prepare_audio_input,
|
||||
)
|
||||
provider.client = SimpleNamespace(post=fake_post)
|
||||
|
||||
result = await provider.get_text("/tmp/test.wav")
|
||||
|
||||
assert result == "transcribed text"
|
||||
assert captured["json"]["messages"] == [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are a speech transcription assistant. "
|
||||
"Transcribe the spoken content from the audio exactly "
|
||||
"and return only the transcription text."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {
|
||||
"data": MIMO_STT_TEST_AUDIO_DATA_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"Please transcribe the content of the audio "
|
||||
"and return only the transcription text."
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_prepare_audio_input_returns_data_url(monkeypatch):
|
||||
class _ResolvedAudio:
|
||||
base64_data = "ZmFrZQ=="
|
||||
base64_data = MIMO_STT_TEST_AUDIO_BASE64
|
||||
mime_type = "audio/wav"
|
||||
format = "wav"
|
||||
|
||||
@@ -284,6 +370,41 @@ async def test_mimo_stt_prepare_audio_input_returns_data_url(monkeypatch):
|
||||
assert cleanup_paths == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_prepare_audio_input_rejects_non_wav_payload(monkeypatch):
|
||||
"""上游 SILK→WAV 转换静默失败时应本地报错,而不是把坏字节发给 API(#9113)。"""
|
||||
silk_base64 = base64.b64encode(b"\x02#!SILK_V3" + b"\x00" * 16).decode()
|
||||
|
||||
class _ResolvedAudio:
|
||||
base64_data = silk_base64
|
||||
mime_type = "audio/wav"
|
||||
format = "wav"
|
||||
|
||||
def to_data_url(self):
|
||||
return f"data:audio/wav;base64,{silk_base64}"
|
||||
|
||||
class _Resolver:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
async def to_base64_data(self, **_kwargs):
|
||||
return _ResolvedAudio()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.provider.sources.mimo_api_common.MediaResolver",
|
||||
_Resolver,
|
||||
)
|
||||
|
||||
with pytest.raises(MiMoAPIError, match="SILK"):
|
||||
await prepare_audio_input("/tmp/test.wav")
|
||||
|
||||
|
||||
def test_mimo_stt_wav_validation_accepts_unpadded_base64_header():
|
||||
wav_base64 = base64.b64encode(MIMO_STT_TEST_WAV_HEADER).decode().rstrip("=")
|
||||
|
||||
_validate_wav_payload(wav_base64, "/tmp/test.wav")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mimo_stt_get_text_uses_reasoning_content(monkeypatch):
|
||||
provider = _make_stt_provider()
|
||||
|
||||
Reference in New Issue
Block a user