初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""元炁健康检查端点测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_healthz_returns_200() -> None:
|
||||
"""健康检查端点应始终返回 200(即使在未加载模型时也返回可用状态信息)。"""
|
||||
resp = client.get("/healthz")
|
||||
assert resp.status_code == status.HTTP_200_OK
|
||||
|
||||
data = resp.json()
|
||||
assert data["status"] in ("ok", "degraded", "error")
|
||||
assert "engines" in data
|
||||
assert "llm" in data["engines"]
|
||||
assert "embedding" in data["engines"]
|
||||
assert "reranker" in data["engines"]
|
||||
assert "version" in data
|
||||
assert isinstance(data["loaded_models"], list)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""元炁 Reranker 端点测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_rerank_without_model_returns_503() -> None:
|
||||
"""Reranker 端点未加载模型时应返回 503。"""
|
||||
resp = client.post(
|
||||
"/v1/rerank",
|
||||
json={
|
||||
"model": "test-model",
|
||||
"query": "测试查询",
|
||||
"documents": ["文档A", "文档B", "文档C"],
|
||||
},
|
||||
)
|
||||
# 未加载模型时返回 503;安装后若模型就绪则返回 200
|
||||
assert resp.status_code in (
|
||||
status.HTTP_200_OK,
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
)
|
||||
|
||||
|
||||
def test_rerank_invalid_body() -> None:
|
||||
"""Reranker 端点应对无效请求体返回 422。"""
|
||||
resp = client.post(
|
||||
"/v1/rerank",
|
||||
json={"model": "test", "query": "test"},
|
||||
# 缺少 documents 字段
|
||||
)
|
||||
assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
Reference in new issue
Block a user