Files
hunjing/yuanqi/tests/test_asr.py
T
2026-08-21 16:00:13 +08:00

185 lines
6.7 KiB
Python

"""
ASR 引擎和端点测试。
"""
from __future__ import annotations
import io
import wave
from unittest.mock import patch
import pytest
from src.endpoints.asr import _ensure_loaded, _validate_format
from src.engine.asr import _SUPPORTED_FORMATS, SenseVoiceASREngine
def _make_test_wav(duration_sec: float = 0.5, sample_rate: int = 16000) -> bytes:
"""生成测试用的 WAV 文件字节(静音音频)。"""
buf = io.BytesIO()
num_samples = int(duration_sec * sample_rate)
with wave.open(buf, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(b"\x00\x00" * num_samples)
return buf.getvalue()
# ── 测试 _SUPPORTED_FORMATS ────────────────────────────────────────────────
class TestSupportedFormats:
def test_common_formats_present(self) -> None:
"""应包含常见音频格式。"""
for fmt in ("wav", "mp3", "m4a", "flac", "ogg", "opus", "aac"):
assert fmt in _SUPPORTED_FORMATS, f"缺少常用格式: {fmt}"
# ── 测试 _validate_format ──────────────────────────────────────────────────
class TestValidateFormat:
def test_supported_format(self) -> None:
"""支持的格式返回扩展名。"""
assert _validate_format("audio.wav") == "wav"
assert _validate_format("speech.mp3") == "mp3"
@pytest.mark.asyncio
async def test_no_extension_raises(self) -> None:
"""无扩展名应抛出异常。"""
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
_validate_format("no_ext")
assert exc.value.status_code == 400
assert str(exc.value.detail["code"]) == "unsupported_format"
@pytest.mark.asyncio
async def test_unsupported_format(self) -> None:
"""不支持的格式应抛出异常。"""
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
_validate_format("video.avi")
assert exc.value.status_code == 400
assert str(exc.value.detail["code"]) == "unsupported_format"
# ── 测试 SenseVoiceASREngine ──────────────────────────────────────────────
class TestSenseVoiceASREngine:
def test_init(self) -> None:
"""引擎初始化应使用默认配置。"""
engine = SenseVoiceASREngine()
assert engine.name == "asr"
assert engine.model_loaded is False
assert engine._model is None
@pytest.mark.asyncio
async def test_health_unloaded(self) -> None:
"""模型未加载时 health 应返回 unavailable。"""
engine = SenseVoiceASREngine()
h = await engine.health()
assert h["status"] == "unavailable"
@pytest.mark.asyncio
async def test_load_without_funasr(self) -> None:
"""未安装 funasr 时加载应优雅失败。"""
engine = SenseVoiceASREngine()
# 模拟 funasr 不可用
with patch.dict("sys.modules", {"funasr": None}):
await engine.load()
assert engine.model_loaded is False
@pytest.mark.asyncio
async def test_transcribe_without_model(self) -> None:
"""模型未加载时 transcribe 返回空结果。"""
engine = SenseVoiceASREngine()
result = await engine.transcribe(b"fake-audio", format="wav")
assert result["text"] == ""
assert result["model"] == engine._model_name
@pytest.mark.asyncio
async def test_normalize_language(self) -> None:
"""语言归一化应正确映射。"""
engine = SenseVoiceASREngine()
assert engine._normalize_language(None) == "auto"
assert engine._normalize_language("") == "auto"
assert engine._normalize_language("auto") == "auto"
assert engine._normalize_language("zh") == "zh"
assert engine._normalize_language("en") == "en"
assert engine._normalize_language("ja") == "ja"
assert engine._normalize_language("ko") == "ko"
assert engine._normalize_language("yue") == "yue"
# 不支持的应回退 auto
assert engine._normalize_language("zz") == "auto"
@pytest.mark.asyncio
async def test_unload(self) -> None:
"""卸载应清理模型引用。"""
engine = SenseVoiceASREngine()
await engine.unload()
assert engine.model_loaded is False
assert engine._model is None
@pytest.mark.asyncio
async def test_close(self) -> None:
"""close 应调用 unload。"""
engine = SenseVoiceASREngine()
await engine.close()
assert engine.model_loaded is False
# ── 测试端点逻辑 ──────────────────────────────────────────────────────────
class TestEndpoints:
def test_ensure_loaded_returns_503_when_engine_none(self) -> None:
"""引擎未初始化时返回 503。"""
with pytest.raises(Exception):
# 模拟 asr_engine 为 None 的场景
with patch("src.endpoints.asr.asr_engine", None):
_ensure_loaded()
@pytest.mark.asyncio
async def test_asr_endpoint_no_engine(self) -> None:
"""ASR 引擎未就绪时应有适当的错误响应状态码。"""
from fastapi import HTTPException
with patch("src.endpoints.asr.asr_engine", None):
with pytest.raises(HTTPException) as exc:
_ensure_loaded()
assert exc.value.status_code == 503
# ── 集成测试(需要实际模型,标记为跳过) ────────────────────────
@pytest.mark.skip(reason="需要真实 SenseVoiceSmall 模型和 FunASR 环境")
class TestIntegrationASR:
"""集成测试 — 需要实际模型文件。"""
@pytest.mark.asyncio
async def test_transcribe_wav(self) -> None:
"""转写 WAV 音频。"""
engine = SenseVoiceASREngine()
await engine.load()
assert engine.model_loaded
audio = _make_test_wav()
result = await engine.transcribe(audio, format="wav")
assert isinstance(result["text"], str)
@pytest.mark.asyncio
async def test_transcribe_with_language_hint(self) -> None:
"""指定语言提示的转写。"""
engine = SenseVoiceASREngine()
await engine.load()
audio = _make_test_wav()
result = await engine.transcribe(audio, format="wav", language="zh")
assert isinstance(result["text"], str)