feat: align surgery API with schemas and extend client tooling

- Refactor app API and schemas; adjust surgery pipeline, repository, and session manager.

- Improve consumption TSV logging and consumable vision integration; trim voice resolution.

- Add Baidu Face 1:N search script, .env.example entries, and client API integration doc.

- Update demo client, staging checklist, surgery interface doc, and related tests; add sample face image.

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-23 16:09:20 +08:00
parent 0c05463617
commit 69980d8073
20 changed files with 994 additions and 610 deletions

View File

@@ -115,10 +115,9 @@ def test_get_result_200(api_app: FastAPI) -> None:
SurgeryConsumptionDetail(
item_id="纱布",
item_name="纱布",
quantity=1,
qty=1,
doctor_id="vision",
timestamp=ts,
source="vision",
),
]
)
@@ -129,6 +128,13 @@ def test_get_result_200(api_app: FastAPI) -> None:
body = r.json()
assert body["surgery_id"] == "123456"
assert len(body["details"]) == 1
assert list(body["details"][0].keys()) == [
"item_id",
"item_name",
"qty",
"doctor_id",
"timestamp",
]
assert body["summary"][0]["total_quantity"] == 1
@@ -148,21 +154,24 @@ def test_pending_confirmation_200_and_404(api_app: FastAPI) -> None:
surgery_id="123456",
confirmation_id="cid",
prompt_text="请确认",
prompt_audio_mp3_base64="//uQ",
options=[],
model_top1_label="x",
model_top1_confidence=0.4,
created_at=ts,
)
pipeline_ok = MagicMock()
pipeline_ok.get_pending_confirmation_for_client = MagicMock(return_value=payload)
pipeline_ok.get_pending_confirmation_for_client = AsyncMock(return_value=payload)
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline_ok
client = TestClient(api_app)
r = client.get("/client/surgeries/123456/pending-confirmation")
assert r.status_code == 200
assert r.json()["confirmation_id"] == "cid"
body_ok = r.json()
assert body_ok["confirmation_id"] == "cid"
assert body_ok["prompt_audio_mp3_base64"] == "//uQ"
pipeline_none = MagicMock()
pipeline_none.get_pending_confirmation_for_client = MagicMock(return_value=None)
pipeline_none.get_pending_confirmation_for_client = AsyncMock(return_value=None)
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline_none
client2 = TestClient(api_app)
r2 = client2.get("/client/surgeries/123456/pending-confirmation")
@@ -193,60 +202,6 @@ 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(
@@ -284,47 +239,3 @@ def test_resolve_maps_surgery_pipeline_error_to_http(api_app: FastAPI) -> None:
)
assert r.status_code == 404
assert r.json()["detail"]["code"] == "CONFIRMATION_NOT_FOUND"
def test_internal_voice_status_404_and_200(api_app: FastAPI) -> None:
p_none = MagicMock()
p_none.voice_status = MagicMock(return_value=None)
api_app.dependency_overrides[get_surgery_pipeline] = lambda: p_none
client = TestClient(api_app)
r = client.get("/internal/surgeries/123456/voice-status")
assert r.status_code == 404
p_ok = MagicMock()
p_ok.voice_status = MagicMock(
return_value={
"voice_enabled": True,
"pending_queue_approx": 2,
"last_prompt_snippet": "hi",
"last_asr_text": "纱布",
"last_error": None,
}
)
api_app.dependency_overrides[get_surgery_pipeline] = lambda: p_ok
client2 = TestClient(api_app)
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)