feat: 语音确认、联调与运维增强
- 语音:序数解析(第一个/第二个等)、解析失败计数与 API detail.retry_remaining; 百度 ASR 固定 dev_pid 为普通话;SurgeryPipelineError 支持 extra 并入 HTTP detail。 - Demo:demo 路由与假 RTSP、客户端 index 与 README;BackendResolver 与配置调整。 - 可观测:消耗 TSV 日志、语音文件日志、终端 Markdown 辅助;相关测试与依赖更新。 - 注意:.env 仍被 gitignore,本地密钥不会进入本提交。 Made-with: Cursor
This commit is contained in:
@@ -193,6 +193,60 @@ def test_resolve_non_wav_422(api_app: FastAPI) -> None:
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_prompt_audio_200(api_app: FastAPI) -> None:
|
||||
pipeline = MagicMock()
|
||||
pipeline.get_pending_prompt_audio_mp3 = AsyncMock(return_value=b"\xff\xfb\x90")
|
||||
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
||||
client = TestClient(api_app)
|
||||
r = client.get("/client/surgeries/123456/pending-confirmation/cid1/prompt-audio")
|
||||
assert r.status_code == 200
|
||||
assert r.content == b"\xff\xfb\x90"
|
||||
assert "mpeg" in (r.headers.get("content-type") or "")
|
||||
pipeline.get_pending_prompt_audio_mp3.assert_awaited_once_with(
|
||||
surgery_id="123456",
|
||||
confirmation_id="cid1",
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_text_200(api_app: FastAPI) -> None:
|
||||
pipeline = MagicMock()
|
||||
pipeline.resolve_pending_confirmation_from_client_text = AsyncMock(
|
||||
return_value=VoiceResolveResult(
|
||||
resolved_label="纱布",
|
||||
rejected=False,
|
||||
asr_text="第一个",
|
||||
audio_object_key=None,
|
||||
message="ok",
|
||||
)
|
||||
)
|
||||
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
||||
client = TestClient(api_app)
|
||||
r = client.post(
|
||||
"/client/surgeries/123456/pending-confirmation/cid/resolve-text",
|
||||
json={"recognized_text": "第一个"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["resolved_label"] == "纱布"
|
||||
assert body["asr_text"] == "第一个"
|
||||
pipeline.resolve_pending_confirmation_from_client_text.assert_awaited_once()
|
||||
|
||||
|
||||
def test_resolve_text_maps_surgery_error(api_app: FastAPI) -> None:
|
||||
pipeline = MagicMock()
|
||||
pipeline.resolve_pending_confirmation_from_client_text = AsyncMock(
|
||||
side_effect=SurgeryPipelineError("VOICE_PARSE_FAILED", "无法匹配")
|
||||
)
|
||||
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
||||
client = TestClient(api_app)
|
||||
r = client.post(
|
||||
"/client/surgeries/123456/pending-confirmation/cid/resolve-text",
|
||||
json={"recognized_text": "随便说说"},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
assert r.json()["detail"]["code"] == "VOICE_PARSE_FAILED"
|
||||
|
||||
|
||||
def test_resolve_200(api_app: FastAPI) -> None:
|
||||
pipeline = MagicMock()
|
||||
pipeline.resolve_pending_confirmation_from_audio = AsyncMock(
|
||||
@@ -255,3 +309,22 @@ def test_internal_voice_status_404_and_200(api_app: FastAPI) -> None:
|
||||
r2 = client2.get("/internal/surgeries/123456/voice-status")
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["pending_queue_approx"] == 2
|
||||
|
||||
|
||||
def test_internal_voice_audits_200_empty(api_app: FastAPI) -> None:
|
||||
pipeline = MagicMock()
|
||||
pipeline.list_voice_audits = AsyncMock(return_value=([], 0))
|
||||
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
||||
client = TestClient(api_app)
|
||||
r = client.get(
|
||||
"/internal/surgeries/123456/voice-audits",
|
||||
params={"limit": 1, "offset": 0},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
j = r.json()
|
||||
assert j["surgery_id"] == "123456"
|
||||
assert j["total"] == 0
|
||||
assert j["limit"] == 1
|
||||
assert j["offset"] == 0
|
||||
assert j["items"] == []
|
||||
pipeline.list_voice_audits.assert_awaited_once_with("123456", limit=1, offset=0)
|
||||
|
||||
Reference in New Issue
Block a user