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()