Files
infra-docker-configs/servers/tools-pve/asr-pipeline/tests/test_service.py
T
Codex 2367c5eb4e
Build image / build (push) Canceled after 0s
Build ASR Pipeline / image (push) Canceled after 0s
feat(tools-pve/asr-pipeline): multi-backend ASR web app with LiteLLM routing
2026-11-01 22:07:57 -08:00

199 lines
7.9 KiB
Python

from __future__ import annotations
import json
import os
import tempfile
import threading
import unittest
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
from unittest import mock
TMP = tempfile.TemporaryDirectory()
os.environ["ASR_DATA_DIR"] = str(Path(TMP.name) / "data")
os.environ["ASR_STATE_DIR"] = str(Path(TMP.name) / "state")
os.environ["ASR_VAULT"] = str(Path(TMP.name) / "vault")
os.environ["ASR_RECORDINGS_ROOT"] = str(Path(TMP.name) / "recordings")
os.environ["ASR_AGENT_TOKEN"] = "test-agent-token"
os.environ["ASR_NOTIFY"] = "0"
os.environ["ASR_GIT_SYNC"] = "0"
from asrpipe import lexicon, providers, review, segment # noqa: E402
from asrservice import config as svc_cfg, db as svc_db, web as svc_web # noqa: E402
class ProviderAndServiceTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
svc_cfg.ensure_dirs()
svc_db.connect().close()
def tearDown(self):
self.tmp.cleanup()
def test_soniox_token_diarization_and_segment_parse(self):
payload = {
"tokens": [
{"text": "大家", "speaker": "1"},
{"text": "好,", "speaker": "1"},
{"text": "今天", "speaker": "1"},
{"text": "讨论", "speaker": "1"},
{"text": "预算。", "speaker": "1"},
{"text": "收到,", "speaker": "2"},
{"text": "我来", "speaker": "2"},
{"text": "汇报。", "speaker": "2"},
]
}
text = providers.transcript_from_soniox_response(payload, model="stt-async-v5")
parsed_turns, meta = segment.parse(text)
self.assertEqual(meta["asr_provider"], "soniox")
self.assertEqual(meta["asr_model"], "stt-async-v5")
self.assertEqual(len(parsed_turns), 2)
self.assertEqual(parsed_turns[0].speaker, "speaker_0")
self.assertEqual(parsed_turns[0].text, "大家好,今天讨论预算。")
self.assertEqual(parsed_turns[1].speaker, "speaker_1")
self.assertEqual(parsed_turns[1].text, "收到,我来汇报。")
def test_soniox_context_includes_lexicon_and_intake(self):
lexicon.learn_alias("lite网关", "LiteLLM", note="test")
ctx = providers.build_soniox_context(
title="网关评审",
user_context="讨论 LiteLLM 与 Soniox 集成",
)
self.assertIn("terms", ctx)
self.assertIn("LiteLLM", ctx["terms"])
self.assertIn("讨论 LiteLLM", ctx.get("text", ""))
def test_openai_and_grok_diarized_transcriptions(self):
audio = self.root / "sample.mp3"
audio.write_bytes(b"ID3-fake-audio")
def fake_openai_multipart(url, *, fields, file_field, file_path, headers=None, timeout=900):
return {
"segments": [
{"speaker": "A", "text": "第一句发言。"},
{"speaker": "A", "text": "继续补充。"},
{"speaker": "B", "text": "第二位发言。"},
]
}
with mock.patch("asrpipe.providers._http_multipart", side_effect=fake_openai_multipart), \
mock.patch("asrpipe.providers.probe_duration_seconds", return_value=30), \
mock.patch.object(providers.CFG, "api_key", return_value="sk-test"):
res = providers.transcribe_openai(
audio,
title="OpenAI 测试",
model="openai/gpt-4o-transcribe-diarize",
)
turns, meta = segment.parse(res.transcript_text)
self.assertEqual(meta["asr_provider"], "openai")
self.assertEqual(len(turns), 2)
self.assertIn("第一句发言。继续补充。", turns[0].text)
self.assertEqual(turns[1].speaker, "speaker_1")
def fake_grok_multipart(url, *, fields, file_field, file_path, headers=None, timeout=900):
return {
"utterances": [
{"speaker": 0, "text": "Grok 说话人零。"},
{"speaker": 1, "text": "Grok 说话人一。"},
]
}
with mock.patch("asrpipe.providers._http_multipart", side_effect=fake_grok_multipart), \
mock.patch("asrpipe.providers.probe_duration_seconds", return_value=30), \
mock.patch.object(providers.CFG, "grok_api_key", "xai-test"):
gres = providers.transcribe_grok(
audio,
title="Grok 测试",
)
gturns, gmeta = segment.parse(gres.transcript_text)
self.assertEqual(gmeta["asr_provider"], "grok")
self.assertEqual(len(gturns), 2)
self.assertEqual(gturns[0].speaker, "speaker_0")
self.assertEqual(gturns[1].speaker, "speaker_1")
def test_online_speaker_confirmation_updates_draft_without_voiceprint_server(self):
drafts_dir = review.CFG.drafts
drafts_dir.mkdir(parents=True, exist_ok=True)
draft = drafts_dir / "online-draft.md"
draft.write_text(
"---\n"
"title: 线上模型会议\n"
"status: draft\n"
"asr_provider: soniox\n"
"voiceprint_session: ''\n"
"voiceprint_speakers: {}\n"
"---\n\n"
"【speaker_1】发言内容\n",
encoding="utf-8",
)
with mock.patch.object(review.CFG, "voiceprint_enroll_url", "http://127.0.0.1:9/unreachable"):
enrolled = review._confirm_voiceprint(str(draft), "speaker_1", "张三")
self.assertFalse(enrolled)
updated = draft.read_text(encoding="utf-8")
self.assertIn("speaker_1: 张三", updated)
def test_db_and_web_endpoints(self):
job_id = "0123456789abcdef01234567"
job = svc_db.create_job(
job_id=job_id,
original_name="meeting.m4a",
stored_name=f"20261102-{job_id}.m4a",
title="架构周会",
context="验证 Web API",
size=2048,
asr_provider="soniox",
asr_model="stt-async-v5",
)
self.assertEqual(job["asr_provider"], "soniox")
self.assertEqual(job["asr_model"], "stt-async-v5")
svc_web.AGENT_TOKEN = "test-agent-token"
server = ThreadingHTTPServer(("127.0.0.1", 0), svc_web.Handler)
port = server.server_address[1]
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
def fetch(path: str, headers: dict[str, str] | None = None) -> tuple[int, str]:
req = urllib.request.Request(
f"http://127.0.0.1:{port}{path}",
headers=headers or {},
)
with urllib.request.urlopen(req, timeout=5) as resp:
return resp.status, resp.read().decode("utf-8")
status, body = fetch("/health")
self.assertEqual(status, 200)
self.assertEqual(json.loads(body)["ok"], True)
status, body = fetch(
"/api/v1/providers",
headers={"Authorization": "Bearer test-agent-token"},
)
self.assertEqual(status, 200)
prov_data = json.loads(body)
prov_ids = [p["id"] for p in prov_data["providers"]]
self.assertEqual(prov_ids, ["qwen3cam", "soniox", "gemini", "openai", "grok"])
status, body = fetch(f"/jobs/{job_id}/")
self.assertEqual(status, 200)
self.assertIn("架构周会", body)
self.assertIn("soniox", body)
status, body = fetch("/lexicon")
self.assertEqual(status, 200)
self.assertIn("词库", body)
status, body = fetch("/settings")
self.assertEqual(status, 200)
self.assertIn("LiteLLM", body)
finally:
server.shutdown()
server.server_close()
if __name__ == "__main__":
unittest.main()