feat(tools-pve/asr-pipeline): SQLite model config, task delete/rerefine, multi-recording review, lexicon CRUD
This commit is contained in:
@@ -5,6 +5,7 @@ import os
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
@@ -20,6 +21,7 @@ os.environ["ASR_NOTIFY"] = "0"
|
||||
os.environ["ASR_GIT_SYNC"] = "0"
|
||||
|
||||
from asrpipe import lexicon, providers, review, segment # noqa: E402
|
||||
from asrpipe.config import CFG # noqa: E402
|
||||
from asrservice import config as svc_cfg, db as svc_db, web as svc_web # noqa: E402
|
||||
|
||||
|
||||
@@ -33,6 +35,28 @@ class ProviderAndServiceTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_sqlite_settings_override_config_dynamically(self):
|
||||
svc_db.save_settings({
|
||||
"asr_provider": "soniox",
|
||||
"litellm_base_url": "https://api.opfo.xyz/v1",
|
||||
"litellm_api_key": "sk-sqlite-dynamic-key",
|
||||
"clean_model": "aistudio/gemini-flash-latest",
|
||||
"draft_model": "openai/gpt-sol-latest",
|
||||
"soniox_api_key": "soniox-db-secret",
|
||||
"use_hindsight": "0",
|
||||
})
|
||||
self.assertEqual(CFG.asr_provider, "soniox")
|
||||
self.assertEqual(CFG.litellm_base_url, "https://api.opfo.xyz/v1")
|
||||
self.assertEqual(CFG.endpoint, "https://api.opfo.xyz/v1/chat/completions")
|
||||
self.assertEqual(CFG.transcriptions_endpoint, "https://api.opfo.xyz/v1/audio/transcriptions")
|
||||
self.assertEqual(CFG.api_key(), "sk-sqlite-dynamic-key")
|
||||
self.assertEqual(CFG.clean_model, "aistudio/gemini-flash-latest")
|
||||
self.assertEqual(CFG.draft_model, "openai/gpt-sol-latest")
|
||||
self.assertEqual(CFG.soniox_api_key, "soniox-db-secret")
|
||||
self.assertFalse(CFG.use_hindsight)
|
||||
# Restore default for subsequent tests
|
||||
svc_db.save_settings({"asr_provider": "qwen3cam", "use_hindsight": "1"})
|
||||
|
||||
def test_soniox_token_diarization_and_segment_parse(self):
|
||||
payload = {
|
||||
"tokens": [
|
||||
@@ -135,20 +159,76 @@ class ProviderAndServiceTests(unittest.TestCase):
|
||||
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",
|
||||
def test_lexicon_crud_operations(self):
|
||||
# Entity CRUD
|
||||
lexicon.upsert_entity("测试生物", "公司", company="测试集团", role="子公司", related="李四")
|
||||
ents = lexicon.list_entities(q="测试生物")
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0]["company"], "测试集团")
|
||||
|
||||
lexicon.update_entity("测试生物", "测试生物科技", "公司", company="新集团", role="总部", related="李四,王五")
|
||||
ents2 = lexicon.list_entities(q="测试生物科技")
|
||||
self.assertEqual(len(ents2), 1)
|
||||
self.assertEqual(ents2[0]["company"], "新集团")
|
||||
self.assertTrue(lexicon.delete_entity("测试生物科技"))
|
||||
self.assertEqual(len(lexicon.list_entities(q="测试生物科技")), 0)
|
||||
|
||||
# Alias & Correction CRUD
|
||||
lexicon.learn_alias("老测试", "测试人", kind="人名", origin="manual", note="初始")
|
||||
lexicon.update_alias("老测试", "测试人", "小测试", "测试人", kind="人名", confirmed=1, note="已改")
|
||||
aliases = lexicon.list_aliases(q="小测试")
|
||||
self.assertEqual(len(aliases), 1)
|
||||
self.assertEqual(aliases[0]["note"], "已改")
|
||||
self.assertTrue(lexicon.delete_alias("小测试", "测试人"))
|
||||
|
||||
lexicon.learn_correction("误识词", "正词", note="c1")
|
||||
lexicon.update_correction("误识词", "正词", "新误识词", "正词", confirmed=1, note="c2")
|
||||
cors = lexicon.list_corrections(q="新误识词")
|
||||
self.assertEqual(len(cors), 1)
|
||||
self.assertEqual(cors[0]["note"], "c2")
|
||||
self.assertTrue(lexicon.delete_correction("新误识词", "正词"))
|
||||
|
||||
# Relation CRUD
|
||||
lexicon.upsert_relation("张三", "任职于", "洛启生物", confidence=0.8, source="manual")
|
||||
lexicon.update_relation("张三", "任职于", "洛启生物", "manual", "张三", "实控人", "洛启生物", confidence=0.95, active=1)
|
||||
rels = lexicon.list_relations(q="张三")
|
||||
self.assertTrue(any(r["predicate"] == "实控人" for r in rels))
|
||||
self.assertTrue(lexicon.delete_relation("张三", "实控人", "洛启生物", "manual"))
|
||||
|
||||
def test_db_web_endpoints_multi_review_and_job_delete(self):
|
||||
job1_id = "0123456789abcdef01234567"
|
||||
job2_id = "fedcba987654321001234567"
|
||||
t1 = svc_cfg.TRANSCRIPTS / f"{job1_id}.txt"
|
||||
t2 = svc_cfg.TRANSCRIPTS / f"{job2_id}.txt"
|
||||
t1.write_text("【speaker_0】录音一内容\n", encoding="utf-8")
|
||||
t2.write_text("【speaker_0】录音二内容\n", encoding="utf-8")
|
||||
|
||||
svc_db.create_job(
|
||||
job_id=job1_id,
|
||||
original_name="meeting1.m4a",
|
||||
stored_name=f"20261102-{job1_id}.m4a",
|
||||
title="第一场尽调会",
|
||||
context="验证多录音确认",
|
||||
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_db.update_job(job1_id, "review", "待确认", transcript=str(t1))
|
||||
|
||||
svc_db.create_job(
|
||||
job_id=job2_id,
|
||||
original_name="meeting2.m4a",
|
||||
stored_name=f"20261102-{job2_id}.m4a",
|
||||
title="第二场投决会",
|
||||
context="验证批量操作",
|
||||
size=4096,
|
||||
asr_provider="qwen3cam",
|
||||
)
|
||||
svc_db.update_job(job2_id, "review", "待确认", transcript=str(t2))
|
||||
|
||||
q1 = review.enqueue(str(t1), "第一场问题A?", {"kind": "draft_question", "candidates": ["选项A1", "选项A2"]})
|
||||
q2 = review.enqueue(str(t1), "第一场问题B?", {"kind": "draft_question", "candidates": ["选项B1"]})
|
||||
q3 = review.enqueue(str(t2), "第二场问题C?", {"kind": "draft_question", "candidates": ["选项C1"]})
|
||||
|
||||
svc_web.AGENT_TOKEN = "test-agent-token"
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), svc_web.Handler)
|
||||
@@ -164,6 +244,17 @@ class ProviderAndServiceTests(unittest.TestCase):
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
return resp.status, resp.read().decode("utf-8")
|
||||
|
||||
def post_form(path: str, data: dict[str, str]) -> tuple[int, str]:
|
||||
encoded = urllib.parse.urlencode(data).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}{path}",
|
||||
data=encoded,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
method="POST",
|
||||
)
|
||||
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)
|
||||
@@ -173,22 +264,57 @@ class ProviderAndServiceTests(unittest.TestCase):
|
||||
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"]]
|
||||
prov_ids = [p["id"] for p in json.loads(body)["providers"]]
|
||||
self.assertEqual(prov_ids, ["qwen3cam", "soniox", "gemini", "openai", "grok"])
|
||||
|
||||
status, body = fetch(f"/jobs/{job_id}/")
|
||||
# Multi-recording review page shows both recordings grouped
|
||||
status, body = fetch("/review/")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn("架构周会", body)
|
||||
self.assertIn("soniox", body)
|
||||
self.assertIn("第一场尽调会", body)
|
||||
self.assertIn("第二场投决会", body)
|
||||
self.assertIn("第一场问题A?", body)
|
||||
self.assertIn("第二场问题C?", body)
|
||||
|
||||
status, body = fetch("/lexicon")
|
||||
# Batch submit both questions for recording 1
|
||||
status, body = post_form(f"/jobs/{job1_id}/review/", {
|
||||
"question_ids": f"{q1},{q2}",
|
||||
f"preset_{q1}": "选项A1",
|
||||
f"custom_{q2}": "自定义回答B",
|
||||
"batch_submit": "1",
|
||||
})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn("词库", body)
|
||||
self.assertEqual(len(svc_web.pending(svc_db.get_job(job1_id))), 0)
|
||||
self.assertEqual(len(svc_web.pending(svc_db.get_job(job2_id))), 1)
|
||||
|
||||
status, body = fetch("/settings")
|
||||
# Edit job metadata
|
||||
status, body = post_form(f"/jobs/{job1_id}/meta", {
|
||||
"title": "第一场尽调会(已改名)",
|
||||
"context": "更新后的背景",
|
||||
"provider": "openai",
|
||||
"model": "openai/gpt-4o-transcribe-diarize",
|
||||
"next_action": "save",
|
||||
})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn("LiteLLM", body)
|
||||
self.assertIn("第一场尽调会(已改名)", body)
|
||||
|
||||
# Save settings via Web UI POST
|
||||
status, body = post_form("/settings", {
|
||||
"litellm_base_url": "http://192.168.50.49:4000/v1",
|
||||
"clean_model": "aistudio/gemini-flash-lite-latest",
|
||||
"keyword_model": "aistudio/gemini-flash-lite-latest",
|
||||
"draft_model": "codex/gpt-sol-latest",
|
||||
"asr_provider": "soniox",
|
||||
"use_hindsight": "1",
|
||||
})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(CFG.asr_provider, "soniox")
|
||||
|
||||
# Delete job 2 and verify its pending review question is also cleaned up
|
||||
status, _ = post_form(f"/jobs/{job2_id}/delete", {"delete_files": "1"})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIsNone(svc_db.get_job(job2_id))
|
||||
self.assertFalse(t2.exists())
|
||||
self.assertNotIn(q3, [r["id"] for r in review.pending(200)])
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
Reference in New Issue
Block a user