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

@@ -0,0 +1,213 @@
# 手术室监控服务 — 客户端 HTTP API 对接说明
本文档面向**集成我方 FastAPI 后端的客户系统**HIS、手麻、护理工作站、自研终端等说明如何通过 HTTP 调用完成「手术开始 / 结束」「耗材结果查询」「低置信度耗材语音确认」等能力。
> **说明**:仓库内 `scripts/demo_client/` 等演示页面仅用于**内部联调与测试**,不代表生产对接规范;生产环境请按本文档与 OpenAPI 契约自行实现客户端。
---
## 1. 服务与发现
| 项目 | 说明 |
| ------- | ------------------------------------------------------------- |
| 默认监听 | 应用默认 `0.0.0.0:38080`(以实际部署为准,可能经反向代理改写路径或端口) |
| 基础路径 | 路由**无全局前缀**;下文路径均为相对服务根路径 |
| OpenAPI | 服务启动后可访问 `**/docs`Swagger UI**、`**/redoc`** 获取实时 Schema 与试调 |
| 健康检查 | `GET /health`:探活与数据库连通性(降级时可能返回 503 |
| 跨域 CORS | 仅当服务端开启演示用 CORS 配置时对浏览器页面生效;**服务端对接通常不受 CORS 限制** |
认证方式以部署约定为准;当前公开路由未在文档层强制 API Key若贵方环境增加了网关鉴权请在请求头中按网关要求携带。
---
## 2. 客户端 API 一览(`/client/...`
所有「客户集成」接口均位于 `**/client`** 命名空间下。
| 方法 | 路径 | 摘要 |
| ------ | ------------------------------------------------------------------------------- | ------------------------- |
| `POST` | `/client/surgeries/start` | 开始手术:确认摄像头开录成功后返回 |
| `POST` | `/client/surgeries/end` | 结束手术:确认停录成功后返回 |
| `GET` | `/client/surgeries/{surgery_id}/result` | 查询该台手术的耗材明细与汇总 |
| `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取一条待确认项(含 TTS 音频 Base64 |
| `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 上传医生语音 WAV完成确认或否认 |
路径参数 `**surgery_id`**:固定 **6 位数字**(正则 `^\d{6}$`)。
---
## 3. 端到端业务流程(推荐时序)
以下为客户系统与手术室监控服务之间的**推荐调用顺序**与并行关系。
```mermaid
sequenceDiagram
participant Client as 客户系统
participant API as 监控服务 API
Client->>API: POST /client/surgeries/start
Note over Client,API: body: surgery_id, camera_ids, candidate_consumables
API-->>Client: 200 accepted开录已确认
par 术中轮询
loop 按需轮询
Client->>API: GET .../result
API-->>Client: 200 明细+汇总 或 503 RESULT_NOT_READY
end
loop 语音确认闭环(若启用)
Client->>API: GET .../pending-confirmation
API-->>Client: 200 待确认+MP3 或 404 无待确认
opt 有待确认
Client->>Client: 播放 prompt_audio_mp3_base64
Client->>API: POST .../resolvemultipart audio
API-->>Client: 200 accepted
end
end
end
Client->>API: POST /client/surgeries/end
API-->>Client: 200 accepted停录已确认
Client->>API: GET .../result
API-->>Client: 200 归档后持久化结果(若可查)
```
### 3.1 手术生命周期(状态视角)
```mermaid
flowchart LR
A[未开始] -->|POST start 成功| B[录制中 / 推理中]
B -->|GET result 可 200| C[可查消耗]
B -->|GET pending 可 200| D[有待确认]
D -->|POST resolve| B
B -->|POST end 成功| E[已结束]
E -->|GET result| C
```
---
## 4. 接口说明与请求/响应要点
### 4.1 `POST /client/surgeries/start`
**Content-Type**`application/json`
**请求体(摘要)**
| 字段 | 类型 | 说明 |
| ----------------------- | -------- | ------------------------------------------------------------- |
| `surgery_id` | string | 6 位数字手术号 |
| `camera_ids` | string[] | 至少 1 个;需与贵方 RTSP/SDK 映射中的 **摄像头 ID** 一致 |
| `candidate_consumables` | string[] | 可选;**本台手术可能用到的耗材名称清单**。服务端仅对该清单内耗材做自动记账与待确认追问;为空则只做拉流推理、不写入消耗 |
**成功200**`SurgeryApiResponse``status` 一般为 `accepted`,表示**服务端已确认开录完成**。
**失败**:常见 `**503 Service Unavailable`**`detail` 内含业务 `code`(如录制子系统未就绪、开录未确认等)。开录/停录类错误会按服务端配置**自动重试**;仍失败则返回最后一次错误信息。
### 4.2 `POST /client/surgeries/end`
**Content-Type**`application/json`
**请求体**`{ "surgery_id": "123456" }`
**成功200**:停录已确认。
**失败**`503` 同 start表示未能在确认摄像头全部停录后完成请求。
### 4.3 `GET /client/surgeries/{surgery_id}/result`
**幂等只读**。术中返回当前内存已记账结果;结束后返回数据库持久化结果(以服务端实现为准)。
**成功200**`SurgeryResultResponse`
- `details`:多行消耗明细,字段顺序为 `**item_id``item_name``qty``doctor_id``timestamp`**
- `summary`:按 `item_id` 汇总的 `total_quantity`
`**503`**`detail.code === "RESULT_NOT_READY"` — 尚无该手术可查询结果(未开始、未成功开录或暂无可返回数据)。
### 4.4 `GET /client/surgeries/{surgery_id}/pending-confirmation`
用于**低置信度识别**的人工确认闭环(需服务端启用语音确认及相关配置)。
**成功200**:包含 `confirmation_id``prompt_text`、候选项 `options``prompt_audio_mp3_base64`(与话术一致的 **MP3 标准 Base64**,无换行)、模型 Top1 等。
`**404`**:当前无待确认或手术未在进行(`NO_PENDING_CONFIRMATION`)。
`**422` / `503`**话术为空、音频无效、ASR/TTS/MinIO/百度语音等异常时返回,详见响应 `detail.code`
### 4.5 `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`
**Content-Type**`multipart/form-data`
**表单字段**
| 字段 | 说明 |
| ------- | -------------------------------------------------------- |
| `audio` | 单个 `**.wav`** 文件;建议 16 kHz 单声道 PCM其他格式服务端可能尝试 ffmpeg 转码 |
**成功200**`SurgeryPendingConfirmationResolveResponse` — 含 `resolved_label``rejected``asr_text``audio_object_key` 等。
**常见 HTTP 状态**`404`(确认项不存在或已失效)、`409`(已处理)、`422`(音频/解析问题)、`503`(外部依赖故障)。
---
## 5. 错误约定
FastAPI 对 `HTTPException` 的 JSON 外形一般为:
```json
{
"detail": {
"code": "业务错误码",
"message": "人类可读说明",
"surgery_id": "123456"
}
}
```
部分错误可能在 `detail` 中附带额外字段(如重试剩余次数),请以实际响应与 OpenAPI 为准。
**校验错误422**:请求体或路径不符合 Pydantic 校验时FastAPI 可能返回标准 `422` 验证错误结构(与上表「对象型 detail」不同请客户端分别解析。
---
## 6. 与内部演示能力的关系(非客户必选)
以下路由用于**内部一键联调**,客户生产系统**无需依赖**
| 路径 | 说明 |
| ------------------------------------------- | -------------------------------------------------- |
| `GET /internal/demo/orchestrator-status` | 探测演示编排是否开启、RTSP 配置文件是否设置等 |
| `POST /internal/demo/orchestrate-and-start` | 仅在服务端开启 `DEMO_ORCHESTRATOR_ENABLED` 时注册;用于演示环境串联开录 |
客户正式对接应直接调用 `**/client/...`**,并在贵方环境中配置真实的 `camera_ids` 与视频后端映射。
---
## 7. 联调建议
1. 先调用 `**GET /health`** 确认服务与数据库可用。
2.`**POST /client/surgeries/start`** 验证 `camera_ids` 与现场 RTSP/SDK 配置一致,避免 503。
3. `**candidate_consumables**` 与实际手术耗材名称尽量与院内目录或模型标签对齐,减少待确认次数。
4. 结果查询 `**503**` 时建议**退避重试**(术中数据尚未就绪属正常现象)。
5.`**/docs`** 导出或对照契约测试,与贵方 CI 中的契约测试对齐。
---
## 8. 文档修订
接口行为以部署实例的 **OpenAPI`/docs`** 与代码为准;字段含义补充见仓库内 `app/schemas.py` 中各模型的 `description`

View File

@@ -23,7 +23,6 @@
- [ ] 客户端对 `prompt_text` **TTS 播报**,采集医生回答为 **WAV**
- [ ] `POST .../pending-confirmation/{confirmation_id}/resolve``multipart` 字段名 `audio`
- [ ] 确认后明细中出现 `source=voice`;否认不增加明细
- [ ] (可选)`GET /internal/surgeries/{id}/voice-status` 查看队列与最近 ASR 摘要
4. **结束手术** `POST /client/surgeries/end`
- [ ] 返回 `200`,摄像头任务停止

View File

@@ -7,11 +7,11 @@
## 1. 概述
| 能力 | 说明 |
| ---- | ---------------------------------------------------- |
| 开始手术 | 请求开始手术;服务端启动摄像头录制,**仅在确认开录完成后**返回 HTTP 200。 |
| 结束手术 | 请求结束手术;服务端停止摄像头录制,**仅在确认停录完成后**返回 HTTP 200。 |
| 查询结果 | 根据手术 6 位号查询消耗明细与汇总;**仅在已开录且至少已有一条消耗明细后**返回 HTTP 200。 |
| 能力 | 说明 |
| ----- | ------------------------------------------------------ |
| 开始手术 | 请求开始手术;服务端启动摄像头录制,**仅在确认开录完成后**返回 HTTP 200。 |
| 结束手术 | 请求结束手术;服务端停止摄像头录制,**仅在确认停录完成后**返回 HTTP 200。 |
| 查询结果 | 根据手术 6 位号查询消耗明细与汇总;**仅在已开录且至少已有一条消耗明细后**返回 HTTP 200。 |
| 待确认耗材 | 低置信度时服务端排队一条待确认任务客户端拉取话术TTS并在医生确认后回传**不阻塞**后续视频推理。 |
@@ -42,13 +42,13 @@
## 3. 接口列表
| 序号 | 方法 | 路径 | 说明 |
| --- | ------ | --------------------------------------- | ------ |
| 1 | `POST` | `/client/surgeries/start` | 开始手术 |
| 2 | `POST` | `/client/surgeries/end` | 结束手术 |
| 3 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
| 4 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取一条待确认耗材 |
| 5 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生确认结果 |
| 序号 | 方法 | 路径 | 说明 |
| --- | ------ | ------------------------------------------------------------------------------- | --------- |
| 1 | `POST` | `/client/surgeries/start` | 开始手术 |
| 2 | `POST` | `/client/surgeries/end` | 结束手术 |
| 3 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
| 4 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取一条待确认耗材 |
| 5 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生确认结果 |
---
@@ -71,15 +71,15 @@
**请求体字段:**
| 字段名 | 类型 | 必填 | 说明 |
| ----------------------- | ---------- | --- | ---------------------- |
| `surgery_id` | `string` | 是 | 手术 6 位号,必须为 6 位数字。 |
| `camera_ids` | `string[]` | 是 | 摄像头 ID 列表,至少 1 个元素;须与服务端配置的 RTSP 映射键一致(示例见 `app/resources/camera_rtsp_urls.sample.json`)。 |
| `candidate_consumables` | `string[]` | 否 | 本台手术允许记账的耗材名称清单。**为空或未传则不会写入任何消耗**(仅拉流推理);非空时自动记账与待确认仅针对清单内名称。 |
| 字段名 | 类型 | 必填 | 说明 |
| ----------------------- | ---------- | --- | ----------------------------------------------------------------------------------------- |
| `surgery_id` | `string` | 是 | 手术 6 位号,必须为 6 位数字。 |
| `camera_ids` | `string[]` | 是 | 摄像头 ID 列表,至少 1 个元素;须与服务端配置的 RTSP 映射键一致(示例见 `app/resources/camera_rtsp_urls.sample.json`)。 |
| `candidate_consumables` | `string[]` | 否 | 本台手术允许记账的耗材名称清单。**为空或未传则不会写入任何消耗**(仅拉流推理);非空时自动记账与待确认仅针对清单内名称。 |
**说明:** 若该 `surgery_id` 在服务端仍存在**尚未写入数据库**的上一台手术内存归档,开始新会话前会先尝试落库;落库失败则返回 **503**`RECORDING_CANNOT_START`),避免静默丢失数据。
**请求示例:**
```json
@@ -223,7 +223,7 @@ Host: <主机>:<端口>
| `quantity` | `integer` | 是 | 本条记录对应的消耗数量(非负整数)。 |
| `doctor_id` | `string` | 是 | 医生 ID。 |
| `timestamp` | `string` | 是 | 记录时间,**ISO 8601**JSON 中为 ISO 格式字符串,与 OpenAPI 中 `date-time` 一致)。 |
| `source` | `string` | 否 | `vision` 自动识别;`voice` 医生通过待确认接口确认。 |
| `source` | `string` | 否 | `vision` 自动识别;`voice` 医生通过待确认接口确认。 |
`**summary[]` 中每一项(汇总行):**
@@ -285,10 +285,12 @@ Host: <主机>:<端口>
**失败HTTP 404** 无待确认项或手术未在进行。`detail.code` 示例:`NO_PENDING_CONFIRMATION`
| 项目 | 内容 |
| --- | --- |
| 方法 | `GET` |
| 路径 | `/client/surgeries/{surgery_id}/pending-confirmation` |
| 项目 | 内容 |
| --- | ----------------------------------------------------- |
| 方法 | `GET` |
| 路径 | `/client/surgeries/{surgery_id}/pending-confirmation` |
**响应字段(节选):** `confirmation_id``prompt_text``options[]``label` + `confidence`)、`model_top1_label``model_top1_confidence``created_at`
@@ -298,17 +300,21 @@ Host: <主机>:<端口>
**用途:** 客户端采集医生回答的 **WAV 音频**并上传;服务端将音频存入 MinIO、调用百度 ASR 识别、解析 4.4 返回的候选项;**确认**则记一条 `source=voice` 的消耗明细,**否认**则关闭该待确认项且不记账。
| 项目 | 内容 |
| --- | --- |
| 方法 | `POST` |
| 路径 | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
| `Content-Type` | `multipart/form-data` |
| 项目 | 内容 |
| -------------- | ------------------------------------------------------------------------------- |
| 方法 | `POST` |
| 路径 | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
| `Content-Type` | `multipart/form-data` |
**请求体multipart**
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `audio` | 文件 | 是 | 医生语音 **`.wav`**;建议 16 kHz 单声道 PCM其他格式服务端可尝试用 ffmpeg 转码。 |
| 字段 | 类型 | 必填 | 说明 |
| ------- | --- | --- | -------------------------------------------------------- |
| `audio` | 文件 | 是 | 医生语音 `**.wav`**;建议 16 kHz 单声道 PCM其他格式服务端可尝试用 ffmpeg 转码。 |
**成功响应HTTP 200** `SurgeryPendingConfirmationResolveResponse``resolved_label``rejected``asr_text``audio_object_key` 等(与 OpenAPI 一致)。
@@ -367,7 +373,7 @@ Host: <主机>:<端口>
| 版本 | 日期 | 说明 |
| --- | ---------- | ---------------------------------------------------------------- |
| 1.6 | 2026-04-21 | 待确认耗材接口;候选清单硬约束;查询结果需至少一条明细;客户端侧人工确认。 |
| 1.6 | 2026-04-21 | 待确认耗材接口;候选清单硬约束;查询结果需至少一条明细;客户端侧人工确认。 |
| 1.5 | 2026-04-21 | 开始/结束手术:录制流水线失败时重试,仍失败再 503可配置 `SURGERY_RECORDING_`*。 |
| 1.4 | 2026-04-21 | 与 OpenAPI 对齐:开始/结束/查询的 200/503 条件及 `SurgeryClientErrorResponse`。 |
| 1.3 | 2026-04-21 | 结束手术:仅在实际停录确认后返回 HTTP 200否则 503。 |