199 lines
7.9 KiB
Python
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()
|