2026-05-21 15:48:03 +08:00
# 手术室监控服务:客户端手术通信接口说明
2026-05-25 09:36:09 +08:00
## Changelog
对接方请按部署版本核对本节;**已发布路径与字段语义以正文为准**,本节仅记录相对上一版的客户端需改动点。
2026-05-26 13:11:30 +08:00
### 2026-05-26( 续)
**行为 / 运维**
| 项 | 变更 |
| --- | --- |
| `POST /client/surgeries/start` | 默认 RTSP 开录等待由 45s 调至 **90s ** ;客户端 HTTP 超时建议 ≥ **300s ** |
| 服务端日志 | 应用日志持久化至 `backend/logs/app/server.log` , **7 天轮转/保留**; access 日志隐藏 `GET /health` 200 与 pending 轮询 404 |
### 2026-05-26
**文档**
| 接口 | 变更 | 客户端需改动 |
| ---- | ---- | ------------ |
| `POST /internal/demo/offline-batch` 及配套 GET | **新增 ** §5.7– §5.9:链路 3 离线 MP4 上传、耗时轮询、标注视频下载 | 非 RTSP 集成时可走上传视频路径;仍用 `GET /client/surgeries/{surgery_id}/result` 查消耗,**无需** `start` / `end` |
2026-05-25 09:36:09 +08:00
### 2026-05-25
**行为对齐(实现已与正文一致)**
| 接口 | 变更 | 客户端需改动 |
| ---- | ---- | ------------ |
| `GET /client/surgeries/{surgery_id}/result` | 无明细时的 `503` **统一**返回 `detail.code = RESULT_NOT_READY` ( `detail.message` 仍为人可读原因) | 勿再依赖已废弃的 `SURGERY_NOT_STARTED` 、`SURGERY_STARTING` 、`SURGERY_IN_PROGRESS_NO_DETAILS` 、`SURGERY_ENDED_NO_CONSUMPTION` 等细分码;判断 `503` 时用 `RESULT_NOT_READY` |
| `POST .../pending-confirmation/{confirmation_id}/resolve` | ASR/解析可重试失败(如 `VOICE_ASR_FAILED` )改回 **HTTP 422 ** ,不再返回 `200` + `status: "failed"` | 删除对 `200` /`status=failed` /`error_code` 的处理;可重试失败按 `422` + `detail.code` 重录上传 |
| `POST /client/surgeries/start` → `candidate_consumables` | 新增:可仅传**产品编码**( `label_id` ,与 `consumable_classifier_labels.yaml` 一致) | 可传 `["14764-2-4"]` 或 `[{"消耗品编号":"14764-2-4"}]` ;服务端解析为类名后参与推理,响应与其它字段不变 |
**未变更**
2026-05-26 13:11:30 +08:00
- `/client/...` 路由路径、HTTP 方法、`surgery_id` 约束、成功体字段名与 §5 各节描述一致。
2026-05-25 09:36:09 +08:00
---
2026-05-21 15:48:03 +08:00
## 能力概览
| **能力 ** | **说明 ** |
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **探活 ** | `GET /health` ,用于检查进程和数据库状态,详见 5.1 节。 |
2026-05-26 13:11:30 +08:00
| **开始手术 ** | `POST /client/surgeries/start` ,只有在开录确认成功后才返回 `200` ( **链路 1 · 真 RTSP**)。 |
| **结束手术 ** | `POST /client/surgeries/end` ,只有在停录确认成功后才返回 `200` (链路 1) 。 |
| **上传视频 ** | `POST /internal/demo/offline-batch` ,上传单路 MP4 离线推理(**链路 3**) ; **无需** `start` / `end` ,不触发语音终端;详见 5.7 节。需运维开启 `DEMO_ORCHESTRATOR_ENABLED=true` 。 |
| **查询结果 ** | `GET /client/surgeries/{surgery_id}/result` ,至少存在一条消耗明细时返回 `200` ;否则返回 `503` , `detail.code` 为 `RESULT_NOT_READY` 。链路 1 / 3 共用。 |
| **待确认播报 ** | **官方浏览器客户端 ** (仓库 `clients/voice-confirmation/` ,可独立部署):同一 WebSocket 上推送 `voice_pending` (载荷与 `GET .../pending-confirmation` 成功体一致,另含 `type` );无队首时 `voice_pending_empty` ; **不轮询 GET**。**第三方**仍可用 `GET .../pending-confirmation` 拉取队首。**链路 3 无待确认。** |
2026-05-21 15:48:03 +08:00
| **待确认答复 ** | `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` ,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。 |
## 1. 服务与基础信息
| **项目 ** | **说明 ** |
| ----------------------- | ------------------------------------------- |
| **协议 ** | `HTTP/HTTPS` |
| **端口 ** | `38080` ,生产环境以实际入口为准 |
2026-05-26 13:11:30 +08:00
| **路由 ** | 无全局前缀;业务接口位于 `/client/...` ,健康检查位于 `/health` ;链路 3 上传位于 `/internal/demo/...` |
2026-05-21 15:48:03 +08:00
| * * `start` / `end` 请求体** | JSON |
2026-05-26 13:11:30 +08:00
| **链路 3 上传请求体 ** | `multipart/form-data` ,字段见 5.7 节 |
2026-05-21 15:48:03 +08:00
| * * `resolve` 请求体** | `multipart/form-data` ,字段名为 `audio` |
| **在线文档 ** | `/docs` 、`/redoc` |
2026-05-21 15:56:53 +08:00
| **语音确认官方页面 ** | 仓库 `clients/voice-confirmation/` (静态资源,与 API 分宿;需为浏览器配置 CORS, 见该目录 `README.md` ) |
2026-05-21 15:48:03 +08:00
局域网部署时, API 应监听或发布到可被语音终端访问的地址(例如 `0.0.0.0:38080` 发布为 `http://192.168.1.100:38080` )。语音确认页面的 **服务端 Base URL ** 必须填写该 API 根地址;浏览器端会用同一主机和端口生成 WebSocket 地址:`ws://192.168.1.100:38080/client/voice-terminals/ws?terminal_id=...` 。如果语音页与 API 不同源,需配置 CORS 放行语音页来源。
## 2. 摄像头 ID 与 RTSP
2026-05-26 13:11:30 +08:00
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 **链路 1 ** 的 `POST /client/surgeries/start` 中传 `camera_ids` 。**链路 3 上传视频不需要 `camera_ids` 。**
2026-05-21 15:48:03 +08:00
| **camera_id ** | **RTSP ** | **备注 ** |
| ------------- | -------- | -------- |
| `or-cam-01` | `rtsp://admin:Aa183137@192.168.3.2:554/Streaming/Channels/101` | 测试手术室,主码流 ch.101 |
| `or-cam-02` | `rtsp://admin:Aa183137@192.168.3.3:554/Streaming/Channels/101` | 同上 |
| `or-cam-03` | `rtsp://admin:Aa183137@192.168.3.4:554/Streaming/Channels/101` | 同上 |
| `or-cam-04` | `rtsp://admin:Aa183137@192.168.3.5:554/Streaming/Channels/101` | 同上 |
2026-05-21 16:02:25 +08:00
**Docker 部署**: API 在容器内拉流时,上表这类**术间摄像头局域网 IP**通常可继续使用(出站经宿主机路由,须宿主机已能访问该网段)。若 RTSP 实际跑在**宿主机本机**( 假流等) , URL 中的主机应使用 `host.docker.internal` ,勿写 `127.0.0.1` ;详见 `docs/video-backends.md` 与 `backend/docker-compose.yml` 。
2026-05-21 15:48:03 +08:00
## 3. HTTP 路由一览
| **序号 ** | **方法 ** | **路径 ** | **说明 ** |
| ------ | ------ | ------------------------------------------------------------------------------- | ------- |
| 1 | `GET` | `/health` | 探活 |
2026-05-26 13:11:30 +08:00
| 2 | `POST` | `/client/surgeries/start` | 开始手术(链路 1) |
| 3 | `POST` | `/client/surgeries/end` | 结束手术(链路 1) |
| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果(链路 1 / 3 共用) |
| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材(链路 1) |
| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复(链路 1) |
| 7 | `POST` | `/internal/demo/offline-batch` | **链路 3 ** :上传 MP4 离线推理(需 `DEMO_ORCHESTRATOR_ENABLED=true` ) |
| 8 | `GET` | `/internal/demo/offline-batch/{surgery_id}/timing` | 链路 3: 查询文本 / 标注视频各阶段耗时 |
| 9 | `GET` | `/internal/demo/offline-batch/{surgery_id}/visualization` | 链路 3: 下载标注 MP4( 可选) |
| 10 | `GET` | `/internal/demo/recording-modes-status` | 可选:探测链路 3 是否已启用 |
| 11 | `GET` | `/client/voice-terminals/{terminal_id}/assignment` | 可选:查询当前指派(调试或简易集成;**官方浏览器客户端仅用 WebSocket**) |
| 12 | `WS` | `/client/voice-terminals/ws?terminal_id=...` | 语音终端长连接,接收开录/停录指派(**推荐**;与 `clients/voice-confirmation` 共用) |
2026-05-21 15:48:03 +08:00
| | | | |
**术间与语音终端绑定(服务端配置)**
- **唯一配置源**:环境变量 * * `OR_SITE_CONFIG_JSON_FILE` ** 指向手术室 **站点 JSON ** ( UTF-8) , 须同时包含 `video_rtsp_urls` 与 `voice_or_room_bindings` (见仓库 `app/resources/or_site_config.sample.json` )。`voice_or_room_bindings` 为数组,每项含 `or_room_id` 、`camera_ids` 、`voice_terminal_id` ; `camera_ids` 在数组内须唯一,`voice_terminal_id` 全局唯一。
- **`POST /client/surgeries/start` ** 在 **HTTP 200 且开录已成功 ** 后:用请求体中的 `camera_ids` 在 `voice_or_room_bindings` 中解析终端(**精确匹配**术间 camera 集合,或 **开录路集为某术间 camera 集合的子集 ** 时匹配该术间);命中则向对应 `voice_terminal_id` 推送 * * `action":"start"` **(并更新 assignment) ; 未配置站点文件、或数组为空、或未命中则仅打日志, 不影响 200。
- **`POST /client/surgeries/end` ** 在停录 **HTTP 200 ** 后:向该手术会话记录的终端推送 * * `action":"end"` **(并清除 assignment) 。
- 推送 JSON 形如:`{"type":"voice_assignment","action":"start"|"end","surgery_id":"123456"}` 。
- **待确认队列(与 HTTP GET 成功体对齐)**:有新队首或队首变化时推送 `{"type":"voice_pending", "surgery_id":"...", "confirmation_id":"...", "pending_queue_length":1, "pending_queue_position":1, "pending_cumulative_ordinal":1, "prompt_text":"...", "prompt_audio_mp3_base64":"...", "options":[...], "model_top1_label":"...", "model_top1_confidence":0.0, "created_at":"..."}` (字段与 `GET /client/surgeries/{surgery_id}/pending-confirmation` 的 200 响应相同,并多一个 `type` ) 。FIFO 无待确认项时推送 `{"type":"voice_pending_empty","surgery_id":"123456"}` 。触发时机包括:开录指派后(后台任务)、视觉入队待确认后、医生 `resolve` 成功弹出队首后、终端 **WebSocket 刚连接且当前已有指派 ** 时(先补发 `voice_assignment` start, 再补发队首或 empty) 。
- **多 worker**:当前实现为进程内内存;多 Uvicorn worker 时需 sticky session 或 Redis 等另行同步。
## 4. 流程
2026-05-26 13:11:30 +08:00
### 4.1 时序图(链路 1 · 真 RTSP)
2026-05-21 15:48:03 +08:00
```mermaid
sequenceDiagram
participant Client as 客户端
participant Server as 服务端
Client->>Server: POST /client/surgeries/start
Note over Client,Server: body: surgery_id, camera_ids, candidate_consumables
Server-->>Client: 200 accepted( 开录已确认)
par 术中
loop 轮询结果
Client->>Server: GET .../result
Server-->>Client: 200 或 503 RESULT_NOT_READY
end
loop 轮询待确认
Client->>Server: GET .../pending-confirmation
Server-->>Client: 200 或 404
opt 有待确认
Client->>Client: 播放 prompt_audio_mp3_base64
Client->>Server: POST .../resolve( multipart audio)
Server-->>Client: 200 accepted
end
end
end
Client->>Server: POST /client/surgeries/end
Server-->>Client: 200 accepted( 停录已确认)
Client->>Server: GET .../result
Server-->>Client: 200( 持久化后可查时返回)
```
2026-05-26 13:11:30 +08:00
### 4.2 状态图(链路 1)
2026-05-21 15:48:03 +08:00
```mermaid
flowchart LR
A[未开始] -->|start 200| B[录制 / 推理中]
B -->|result 200| C[有消耗数据可查]
B -->|pending 200| D[待确认]
D -->|resolve| B
B -->|end 200| E[已结束]
E -->|result 200| C
```
2026-05-26 13:11:30 +08:00
### 4.3 时序图(链路 3 · 上传 MP4)
与 §4.1 并行存在,适用于**已有完整术间录像**、无需实时拉流与语音待确认的场景。
```mermaid
sequenceDiagram
participant Client as 客户端
participant Server as 服务端
opt 探测(可选)
Client->>Server: GET /internal/demo/recording-modes-status
Server-->>Client: demo_recording_modes_enabled
end
Client->>Server: POST /internal/demo/offline-batch
Note over Client,Server: multipart: surgery_id, video1, candidate_consumables_json, include_visualization
Server-->>Client: 200 accepted( TSV 推理已完成)
Client->>Server: GET /client/surgeries/{surgery_id}/result
Server-->>Client: 200 或 503 RESULT_NOT_READY
opt include_visualization=true
loop 直至 video_status=ready 或 failed
Client->>Server: GET .../offline-batch/{surgery_id}/timing
Server-->>Client: video_status pending / ready / failed
end
Client->>Server: GET .../offline-batch/{surgery_id}/visualization
Server-->>Client: video/mp4
end
```
**与链路 1 的差异**
| 项目 | 链路 1( RTSP) | 链路 3( 上传 MP4) |
| ---- | -------------- | ------------------ |
| 开录 / 停录 | 需要 `start` / `end` | **不需要 ** |
| 语音终端 / 待确认 | 有 | **无 ** (结果直接入库) |
| 查结果 | `GET .../result` | 同上 |
| 服务端开关 | RTSP 站点配置 | 另需 `DEMO_ORCHESTRATOR_ENABLED=true` |
2026-05-21 15:48:03 +08:00
## 5. 接口详情
**路径参数 `surgery_id` **
| **约束项 ** | **说明 ** |
| ------- | --------- |
| **长度 ** | 固定 `6` |
| **字符集 ** | 仅数字 |
| **正则 ** | `^\d{6}$` |
**业务错误响应**
2026-05-26 13:11:30 +08:00
`/client/...` 多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
2026-05-21 15:48:03 +08:00
```
{
"detail": {
"code": "错误码字符串",
"message": "人类可读说明",
"surgery_id": "123456"
}
}
```
2026-05-26 13:11:30 +08:00
§5.7– §5.9(链路 3) 的部分校验失败返回 FastAPI 默认 `detail` **字符串 ** (无 `code` 包装),见各节状态码表。
2026-05-21 15:48:03 +08:00
### 5.1 探活
**基本信息**
| **项目 ** | **内容 ** |
| ------- | --------- |
| **方法 ** | `GET` |
| **路径 ** | `/health` |
| **请求体 ** | 无 |
**响应说明**
| **HTTP ** | **说明 ** | **响应体示例 ** |
| -------- | ----------- | ------------------------------------------------ |
| `200` | 进程正常且数据库可连通 | `{"status":"ok","database":"connected"}` |
| `503` | 数据库不可用(降级) | `{"status":"degraded","database":"unavailable"}` |
### 5.2 开始手术
**基本信息**
| **项目 ** | **内容 ** |
| ---------------- | --------------------------------- |
| **方法 ** | `POST` |
| **路径 ** | `/client/surgeries/start` |
| **Content-Type ** | `application/json; charset=utf-8` |
**业务说明**
- 服务端会为 `camera_ids` 中的每个摄像头建立拉流与推理任务,只有在确认开录成功(如首帧就绪)后才返回 HTTP `200` 。
2026-05-26 13:11:30 +08:00
- 本接口**同步阻塞**直至开录确认;默认 RTSP 等待约 **95 秒/次 ** 、最多 **3 次重试 ** 。客户端 HTTP 超时建议 * * ≥ 300 秒**( Apipost 等工具默认 30– 60 秒易触发 `ESOCKETTIMEDOUT` )。服务端可通过 `VIDEO_OPEN_TIMEOUT_SEC` 调大(默认 90) 。
2026-05-21 15:48:03 +08:00
2026-05-26 13:11:30 +08:00
- `candidate_consumables` 缺省或 `[]` 时,服务端会展开为 `consumable_classifier_labels.yaml` 中的全部类名(无有效 yaml 时开录失败)。
- 非空时,每项可为**耗材名称**或**产品编码**( `label_id` );编码通过 yaml 解析为类名后再参与推理与白名单。亦支持医院导出对象(见下表)。
- **41 类完整类名与产品编码对照**:见 [`docs/耗材产品编码与类名对照表.md` ](耗材产品编码与类名对照表.md )(须用完整类名或编码,口语简称如「纱布」不会自动映射)。
2026-05-21 15:48:03 +08:00
**请求体( JSON) **
| **字段 ** | **类型 ** | **必填 ** | **说明 ** |
| ----------------------- | ---------- | ------ | ----------------------------------- |
| `surgery_id` | `string` | 是 | 6 位数字 |
| `camera_ids` | `string[]` | 是 | 至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节 |
2026-05-25 09:36:09 +08:00
| `candidate_consumables` | `string[]` 或对象数组 | 否 | 非空时仅这些名称/编码参与自动记账与待确认;缺省或 `[]` 时使用全部候选。字符串可为类名或 `label_id` ;对象见下 |
**`candidate_consumables` 数组元素**
| **形式 ** | **示例 ** | **说明 ** |
| -------- | -------- | -------- |
2026-05-26 13:11:30 +08:00
| 名称字符串 | `"医用纱布敷料"` | 须与 yaml / 模型类名**完全一致**(见对照表) |
2026-05-25 09:36:09 +08:00
| 编码字符串 | `"14764-2-4"` | 与 yaml 中 `label_id` 一致,服务端解析为类名 |
| 导出对象(名称) | `{"消耗品编号":"14764-2-4","名称":"一次性使用手术单"}` | 以 `名称` (或 `name` )为准 |
| 导出对象(仅编号) | `{"消耗品编号":"14764-2-4"}` | 按编号解析为类名 |
2026-05-21 15:48:03 +08:00
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| ------------ | -------- | ----------------- |
| `surgery_id` | `string` | 与请求一致 |
| `status` | `string` | 成功时通常为 `accepted` |
| `message` | `string` | 说明文案 |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | -------------------------------------------------------- |
| `200` | 开录已确认 |
| `422` | 参数校验失败,例如 `surgery_id` 非 6 位或 `camera_ids` 为空数组 |
| `503` | 开录未确认或录制子系统故障;`detail.code` 常见为 `RECORDING_CANNOT_START` |
**请求示例**
```
{
"surgery_id": "123456",
"camera_ids": ["or-cam-01", "or-cam-02", "or-cam-03", "or-cam-04"],
"candidate_consumables": ["纱布", "缝线", "止血钳"]
}
```
2026-05-25 09:36:09 +08:00
仅传产品编码时:
```
{
"surgery_id": "123456",
"camera_ids": ["or-cam-01"],
"candidate_consumables": ["14764-2-4", "8036-5-22"]
}
```
2026-05-21 15:48:03 +08:00
**响应示例( 200) **
```
{
"surgery_id": "123456",
"status": "accepted",
"message": "摄像头录制已开始,手术已启动。"
}
```
### 5.3 结束手术
**基本信息**
| **项目 ** | **内容 ** |
| ---------------- | --------------------------------- |
| **方法 ** | `POST` |
| **路径 ** | `/client/surgeries/end` |
| **Content-Type ** | `application/json; charset=utf-8` |
**业务说明**
停止该 `surgery_id` 关联的全部摄像头任务,只有在确认停录完成后才返回 `200` 。
**请求体( JSON) **
| **字段 ** | **类型 ** | **必填 ** | **说明 ** |
| ------------ | -------- | ------ | ------ |
| `surgery_id` | `string` | 是 | 6 位数字 |
**响应体( 200) **
字段含义与 5.2 节一致,`message` 示例为 `摄像头录制已停止,手术已结束。`
**状态码**
| **HTTP ** | **说明 ** |
| -------- | -------------------------------------------------- |
| `200` | 停录已确认 |
| `422` | 参数校验失败 |
| `503` | 停录未确认或故障;`detail.code` 常见为 `RECORDING_NOT_STOPPED` |
**请求示例**
```
{
"surgery_id": "123456"
}
```
### 5.4 查询手术结果
**基本信息**
| **项目 ** | **内容 ** |
| -------- | --------------------------------------- |
| **方法 ** | `GET` |
| **路径 ** | `/client/surgeries/{surgery_id}/result` |
| **路径参数 ** | `surgery_id` |
| **请求体 ** | 无 |
**业务说明**
2026-05-25 09:36:09 +08:00
- 仅当存在至少一条消耗明细时返回 `200` 。
2026-05-21 15:48:03 +08:00
2026-05-25 09:36:09 +08:00
- 无明细(包括已归档但零消耗)、手术未开始、未成功开录或当前尚不可查时,返回 `503` 。
2026-05-21 15:48:03 +08:00
2026-05-25 09:36:09 +08:00
- 上述 `503` 的 `detail.code` 为 * * `RESULT_NOT_READY` **; `detail.message` 说明具体原因(如未开始、进行中尚无明细、已结束无消耗等)。
2026-05-21 15:48:03 +08:00
2026-05-26 13:11:30 +08:00
- **链路 3**:离线 batch 完成后结果直接写入数据库,通过本接口查询;**无需**先调用 `start` / `end` 。若 batch 识别结果为零条明细,本接口仍返回 `503` 。
2026-05-21 15:48:03 +08:00
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| ------------ | -------- | ----------------------- |
| `surgery_id` | `string` | 手术号 |
| `status` | `string` | 成功时通常为 `completed` |
| `message` | `string` | 说明 |
| `details` | `array` | 消耗明细列表,字段见下文 |
| `summary` | `array` | 按 `item_id` 汇总的结果,字段见下文 |
**`details[]` 元素**
| **字段 ** | **类型 ** | **说明 ** |
| ----------- | --------- | ---------------------------------- |
2026-05-26 13:11:30 +08:00
| `item_id` | `string` | 物品 ID; 有 yaml 时多为 `label_id` ,否则通常与类名一致 |
2026-05-21 15:48:03 +08:00
| `item_name` | `string` | 物品名称 |
| `qty` | `integer` | 本条记录数量,当前恒为 `1` ;一次识别或一次人工确认只追加一条明细 |
| `doctor_id` | `string` | 记账关联的医生或系统标识 |
| `timestamp` | `string` | ISO 8601 时间(`date-time` ) |
**`summary[]` 元素**
| **字段 ** | **类型 ** | **说明 ** |
| ---------------- | --------- | -------------------------- |
| `item_id` | `string` | 与明细一致 |
| `item_name` | `string` | 名称,通常取该 `item_id` 首条明细中的名称 |
| `total_quantity` | `integer` | 该物品在本台手术中的合计数量,`>= 0` |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | ------------------------------ |
| `200` | 至少有一条明细 |
| `422` | `surgery_id` 路径不符合约束 |
2026-05-25 09:36:09 +08:00
| `503` | `detail.code` 为 `RESULT_NOT_READY` ; `message` 为具体原因 |
2026-05-21 15:48:03 +08:00
**响应示例( 200) **
```
{
"surgery_id": "123456",
"status": "completed",
"message": "查询成功。",
"details": [
{
"item_id": "19246-3-14",
"item_name": "医用纱布敷料",
"qty": 1,
"doctor_id": "6611",
"timestamp": "2026-04-21T10:30:00+08:00"
}
],
"summary": [
{
"item_id": "19246-3-14",
"item_name": "医用纱布敷料",
"total_quantity": 1
}
]
}
```
### 5.5 拉取待确认耗材
**基本信息**
| **项目 ** | **内容 ** |
| -------- | ----------------------------------------------------- |
| **方法 ** | `GET` |
| **路径 ** | `/client/surgeries/{surgery_id}/pending-confirmation` |
| **路径参数 ** | `surgery_id` |
| **请求体 ** | 无 |
**业务说明**
- 返回当前 FIFO 队首的一条低置信度识别任务。
- `prompt_audio_mp3_base64` 与 `prompt_text` 内容一致,为标准 Base64 的 MP3 字符串(无换行)。
- 客户端解码后应按 `audio/mpeg` 播放。
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| ------------------------- | -------- | ------------------------- |
| `surgery_id` | `string` | 手术号 |
| `confirmation_id` | `string` | 待确认项 ID; 提交 5.6 节接口时原样放入路径 |
| `pending_queue_length` | `int` | 当前 FIFO 中仍为 pending 的条数(含本条) |
| `pending_queue_position` | `int` | 本条在 `pending_fifo` 中的排队序号( 1-based, 队首为 1) |
| `pending_cumulative_ordinal` | `int` | 本场手术中待确认任务累计入队序号(第几条入队) |
| `prompt_text` | `string` | 播报或展示用语,与 MP3 内容一致 |
| `prompt_audio_mp3_base64` | `string` | MP3 的 Base64 |
| `options` | `array` | 候选项列表,字段见下文 |
| `model_top1_label` | `string` | 模型原始 Top1 类名,可能不在本台候选内 |
| `model_top1_confidence` | `number` | Top1 置信度 |
| `created_at` | `string` | 创建时间( ISO 8601) |
**`options[]` 元素**
| **字段 ** | **类型 ** | **说明 ** |
| ------------ | -------- | ---------- |
| `label` | `string` | 展示给医生的选项名称 |
| `confidence` | `number` | 该选项对应的置信度 |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | ----------------------------------------------------- |
| `200` | 当前有一条待确认 |
2026-05-26 13:11:30 +08:00
| `404` | 无待确认或手术未活跃;`detail.code` 为 `NO_PENDING_CONFIRMATION` |
| `422` | 例如话术为空导致无法 TTS; `detail.code` 如 `TTS_TEXT_EMPTY` |
2026-05-21 15:48:03 +08:00
| `503` | 语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED` 、`TTS_ERROR` |
### 5.6 提交待确认结果(医生语音)
**基本信息**
| **项目 ** | **内容 ** |
| ---------------- | ------------------------------------------------------------------------------- |
| **方法 ** | `POST` |
| **路径 ** | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
| **Content-Type ** | `multipart/form-data` |
**路径参数**
| **参数 ** | **约束 ** | **说明 ** |
| ----------------- | ---------- | -------------------------------- |
2026-05-26 13:11:30 +08:00
| `surgery_id` | 6 位数字 | 同 §5 路径参数 |
2026-05-21 15:48:03 +08:00
| `confirmation_id` | 长度 1 到 128 | 与 5.5 节响应中的 `confirmation_id` 一致 |
**请求体( multipart) **
| **字段名 ** | **类型 ** | **必填 ** | **说明 ** |
| ------- | ------ | ------ | ------------------------------------------------------ |
| `audio` | `file` | 是 | 单个 `.wav` 文件;建议使用 16 kHz 单声道 PCM; 非 `.wav` 扩展名会返回 `422` |
**业务说明**
2026-05-26 13:11:30 +08:00
音频上传至对象存储后执行 ASR 和候选解析。若识别为确认某个候选项, 则记一条消耗; 若识别为否认全部候选, 则不记消耗。ASR/解析可重试失败时队首待确认项**不弹出**,便于客户端重录。
2026-05-21 15:48:03 +08:00
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| ------------------ | ---------------- | ------------------------ |
| `surgery_id` | `string` | 手术号 |
| `confirmation_id` | `string` | 待确认 ID |
| `status` | `string` | 成功时为 `accepted` |
| `message` | `string` | 说明 |
| `resolved_label` | `string \| null` | 确认后的耗材名称;否认全部候选时为 `null` |
| `rejected` | `boolean` | 是否否认全部候选,不记消耗时为 `true` |
| `asr_text` | `string \| null` | 语音识别文本 |
| `audio_object_key` | `string \| null` | 对象存储中的原始 WAV 键,便于追溯 |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | ------------------------------------------------------------------------------------- |
| `200` | 已受理并完成解析 |
2026-05-26 13:11:30 +08:00
| `404` | 确认项不存在或手术未活跃;`detail.code` 如 `CONFIRMATION_NOT_FOUND` |
| `409` | 当前确认项已处理过;`detail.code` 如 `CONFIRMATION_ALREADY_RESOLVED` |
| `422` | 空文件、非 `.wav` 、`VOICE_AUDIO_INVALID` 、**ASR/解析可重试失败**(如 `VOICE_ASR_FAILED` 、`VOICE_TEXT_EMPTY` 、`VOICE_PARSE_FAILED` )等 |
2026-05-21 15:48:03 +08:00
| `503` | MinIO、百度等依赖不可用; 例如 `MINIO_NOT_CONFIGURED` 、`MINIO_UPLOAD_FAILED` 、`BAIDU_NOT_CONFIGURED` |
**cURL 示例**
```
curl -sS -X POST \
"http://<主机>:38080/client/surgeries/123456/pending-confirmation/<confirmation_id>/resolve" \
-F "audio=@/path/to/voice .wav;type=audio/wav"
```
2026-05-26 13:11:30 +08:00
### 5.7 上传手术视频(链路 3 · 离线 batch)
**基本信息**
| **项目 ** | **内容 ** |
| ---------------- | --------------------------------- |
| **方法 ** | `POST` |
| **路径 ** | `/internal/demo/offline-batch` |
| **Content-Type ** | `multipart/form-data` |
**前置条件**
- 服务端环境变量 * * `DEMO_ORCHESTRATOR_ENABLED=true` **。未开启时本路径返回 * * `404` **, `detail` 为 `Demo recording modes disabled (set DEMO_ORCHESTRATOR_ENABLED=true).` 。
- 可先调用 * * `GET /internal/demo/recording-modes-status` ** 查看 `demo_recording_modes_enabled` 是否为 `true` (该探测接口始终注册,不依赖上述开关)。
**业务说明**
- 上传**单路完整 MP4**(字段名 `video1` ),服务端调用离线算法包(`algorithm_subprocesses/5.15/main.py` )完成整段推理,解析 TSV 后将消耗明细**直接写入数据库**。
- **不**启动 RTSP 实时会话,**不**调用 `start` / `end` , **不**触发语音终端或待确认队列。
- 请求在服务端**同步阻塞**直至 TSV 推理结束才返回 `200` ;长视频可能耗时较久,客户端应设置足够长的 HTTP 超时。
- 相同视频内容( SHA-256) + 相同候选清单会命中结果缓存(跨 `surgery_id` 复用),响应 `message` 中含 `cache=hit` 或 `cache=miss` 。
- `candidate_consumables_json` 的语义与 5.2 节 `candidate_consumables` 一致(名称、产品编码、医院导出对象);缺省 `"[]"` 时展开为 yaml 全部类名。
**请求体( multipart) **
| **字段名 ** | **类型 ** | **必填 ** | **说明 ** |
| ------- | ------ | ------ | ------ |
| `surgery_id` | `string` | 是 | 6 位数字 |
| `video1` | `file` | 是 | 单路完整 MP4; 空文件返回 `422` |
| `candidate_consumables_json` | `string` | 否 | JSON 数组字符串,默认 `"[]"` ;格式同 5.2 节候选清单 |
| `include_visualization` | `boolean` | 否 | 是否后台生成标注 MP4; 默认 `false` 。为 `true` 时响应含 `visualization_url` ,标注视频异步生成,需轮询 5.8 节 |
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| -------- | -------- | -------- |
| `surgery_id` | `string` | 与请求一致 |
| `status` | `string` | 成功时为 `accepted` |
| `message` | `string` | 含 `rows=` 行数、`cache=hit|miss` 、医生识别摘要等 |
| `visualization_url` | `string \| null` | `include_visualization=true` 时为相对路径 `/internal/demo/offline-batch/{surgery_id}/visualization` ;否则为 `null` |
| `doctor_name` | `string \| null` | 算法输出的医生姓名(若识别到) |
| `doctor_id` | `string \| null` | 医生 ID( 若识别到) |
| `doctor_display` | `string \| null` | 展示用医生信息 |
| `text_duration_sec` | `number` | TSV / 主流程耗时(秒,保留 3 位小数) |
| `video_duration_sec` | `number \| null` | 标注视频耗时;未请求或仍在后台时为 `null` |
| `total_duration_sec` | `number` | 已完成阶段的合计耗时(秒) |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | -------- |
| `200` | 离线推理已完成,结果已入库(可能为零条明细) |
| `404` | Demo 模式未启用 |
| `422` | `surgery_id` 非 6 位、`candidate_consumables_json` 非法、`video1` 为空等 |
| `500` | 上传落盘失败 |
| `503` | 离线 batch 子进程失败;`detail` 形如 `offline batch failed: ...` |
**后续步骤**
1. 调用 * * `GET /client/surgeries/{surgery_id}/result` **( 5.4 节)查询消耗明细。
2. 若 `include_visualization=true` ,轮询 * * `GET .../offline-batch/{surgery_id}/timing` **( 5.8 节)直至 `video_status` 为 `ready` 或 `failed` ,再 * * `GET .../visualization` **( 5.9 节)下载 MP4。
**cURL 示例**
```
curl -sS -X POST "http://<主机>:38080/internal/demo/offline-batch" \
-F "surgery_id=123456" \
-F "video1=@/path/to/surgery .mp4;type=video/mp4" \
-F 'candidate_consumables_json=["14764-2-4","8036-5-22"]' \
-F "include_visualization=true"
```
**响应示例( 200) **
```
{
"surgery_id": "123456",
"status": "accepted",
"message": "非实时精确视频处理完成; rows=3 cache=miss; 医生=张三 (6611);标注视频后台生成中(完成后刷新 visualization URL, 24 小时内有效)",
"visualization_url": "/internal/demo/offline-batch/123456/visualization",
"doctor_name": "张三",
"doctor_id": "6611",
"doctor_display": "张三 (6611)",
"text_duration_sec": 842.5,
"video_duration_sec": null,
"total_duration_sec": 842.5
}
```
### 5.8 查询离线 batch 各阶段耗时
**基本信息**
| **项目 ** | **内容 ** |
| -------- | ------ |
| **方法 ** | `GET` |
| **路径 ** | `/internal/demo/offline-batch/{surgery_id}/timing` |
| **路径参数 ** | `surgery_id` ( 6 位数字) |
| **请求体 ** | 无 |
**业务说明**
- 在 5.7 节上传完成后可用;用于轮询标注视频是否生成完毕。
- `video_status` : `skipped` (未请求标注视频)、`pending` (后台生成中)、`ready` (可下载)、`failed` (生成失败)。
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| -------- | -------- | -------- |
| `surgery_id` | `string` | 手术号 |
| `text_duration_sec` | `number` | TSV 主流程耗时(秒) |
| `video_duration_sec` | `number \| null` | 标注视频耗时;未完成时为 `null` |
| `total_duration_sec` | `number` | 合计耗时(秒) |
| `video_status` | `string` | `skipped` / `pending` / `ready` / `failed` |
**状态码**
| **HTTP ** | **说明 ** |
| -------- | -------- |
| `200` | 有该 `surgery_id` 的 timing 记录 |
| `404` | Demo 未启用,或尚无 timing 记录 |
| `422` | `surgery_id` 不符合约束 |
### 5.9 获取离线 batch 标注视频
**基本信息**
| **项目 ** | **内容 ** |
| -------- | ------ |
| **方法 ** | `GET` |
| **路径 ** | `/internal/demo/offline-batch/{surgery_id}/visualization` |
| **路径参数 ** | `surgery_id` ( 6 位数字) |
| **请求体 ** | 无 |
**业务说明**
- 仅当 5.7 节 `include_visualization=true` 且后台生成成功时可用。
- 响应为 * * `video/mp4` ** 文件流,文件名 `{surgery_id}_result_vis.mp4` 。
- 标注文件默认保留 **24 小时 ** ( `VIDEO_BATCH_VIS_TTL_HOURS` ),过期后返回 `404` 。
**状态码**
| **HTTP ** | **说明 ** |
| -------- | -------- |
| `200` | MP4 文件流 |
| `404` | Demo 未启用、尚未生成或已过期 |
| `422` | `surgery_id` 不符合约束 |
### 5.10 语音终端 assignment( HTTP, 可选)
2026-05-21 15:48:03 +08:00
**路径** `GET /client/voice-terminals/{terminal_id}/assignment`
2026-05-26 13:11:30 +08:00
仓库内 **手术室耗材语音确认浏览器客户端 ** ( `clients/voice-confirmation/` )仅通过 * * §5.11 WebSocket** 接收指派,**不调用**本接口。此处供运维脚本、未实现 WS 的第三方临时拉取 `active_surgery_id` 。
2026-05-21 15:48:03 +08:00
**响应 200**
| **字段 ** | **类型 ** | **说明 ** |
| -------- | -------- | -------- |
| `voice_terminal_id` | `string` | 与路径一致 |
| `active_surgery_id` | `string \| null` | 当前指派手术 6 位号;无指派时为 `null` |
2026-05-26 13:11:30 +08:00
### 5.11 语音终端 WebSocket
2026-05-21 15:48:03 +08:00
**路径** `GET ws://<主机>:<端口>/client/voice-terminals/ws?terminal_id=<终端ID>` ( HTTPS 部署时使用 `wss://` )
**说明**
- 连接成功后,若服务端已有该终端的 assignment, 会立即收到一条 * * `action":"start"` ** 的 JSON( 与下文推送格式一致) 。
- 术中由服务端在 * * `start` / `end` 成功后** 向已连接终端推送 JSON: `{"type":"voice_assignment","action":"start"|"end","surgery_id":"123456"}` 。
2026-05-26 13:11:30 +08:00
- 客户端可发送任意文本作心跳;服务端当前仅依赖 WebSocket 协议级 ping( 由网关或客户端库实现) 。