2026-05-21 15:48:03 +08:00
# 手术室监控服务:客户端手术通信接口说明
## 能力概览
| **能力 ** | **说明 ** |
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **探活 ** | `GET /health` ,用于检查进程和数据库状态,详见 5.1 节。 |
| **开始手术 ** | `POST /client/surgeries/start` ,只有在开录确认成功后才返回 `200` 。 |
| **结束手术 ** | `POST /client/surgeries/end` ,只有在停录确认成功后才返回 `200` 。 |
| **查询结果 ** | `GET /client/surgeries/{surgery_id}/result` ,至少存在一条消耗明细时返回 `200` ;否则返回 `503` ,常见错误码为 `RESULT_NOT_READY` 。 |
2026-05-21 15:56:53 +08:00
| **待确认播报 ** | **官方浏览器客户端 ** (仓库 `clients/voice-confirmation/` ,可独立部署):同一 WebSocket 上推送 `voice_pending` (载荷与 `GET .../pending-confirmation` 成功体一致,另含 `type` );无队首时 `voice_pending_empty` ; **不轮询 GET**。**第三方**仍可用 `GET .../pending-confirmation` 拉取队首。 |
2026-05-21 15:48:03 +08:00
| **待确认答复 ** | `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` ,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。 |
## 1. 服务与基础信息
| **项目 ** | **说明 ** |
| ----------------------- | ------------------------------------------- |
| **协议 ** | `HTTP/HTTPS` |
| **端口 ** | `38080` ,生产环境以实际入口为准 |
| **路由 ** | 无全局前缀;业务接口位于 `/client/...` ,健康检查位于 `/health` |
| * * `start` / `end` 请求体** | JSON |
| * * `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
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 `POST /client/surgeries/start` 中传 `camera_ids` 。
| **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` | 探活 |
| 2 | `POST` | `/client/surgeries/start` | 开始手术 |
| 3 | `POST` | `/client/surgeries/end` | 结束手术 |
| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材 |
| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复 |
| 7 | `GET` | `/client/voice-terminals/{terminal_id}/assignment` | 可选:查询当前指派(调试或简易集成;**官方浏览器客户端仅用 WebSocket**) |
2026-05-21 15:56:53 +08:00
| 8 | `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. 流程
### 4.1 时序图
```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( 持久化后可查时返回)
```
### 4.2 状态图
```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
```
## 5. 接口详情
**路径参数 `surgery_id` **
| **约束项 ** | **说明 ** |
| ------- | --------- |
| **长度 ** | 固定 `6` |
| **字符集 ** | 仅数字 |
| **正则 ** | `^\d{6}$` |
**业务错误响应**
多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
```
{
"detail": {
"code": "错误码字符串",
"message": "人类可读说明",
"surgery_id": "123456"
}
}
```
### 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` 。
- `candidate_consumables` 为空时,服务端会展开为目录中的全部耗材名。
**请求体( JSON) **
| **字段 ** | **类型 ** | **必填 ** | **说明 ** |
| ----------------------- | ---------- | ------ | ----------------------------------- |
| `surgery_id` | `string` | 是 | 6 位数字 |
| `camera_ids` | `string[]` | 是 | 至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节 |
| `candidate_consumables` | `string[]` | 否 | 非空时仅这些名称参与自动记账与待确认;缺省或 `[]` 时使用全部候选 |
**响应体( 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": ["纱布", "缝线", "止血钳"]
}
```
**响应示例( 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` |
| **请求体 ** | 无 |
**业务说明**
- 仅当存在至少一条消耗明细时返回 `200` 。
- 无明细(包括已归档但零消耗)、手术未开始、未成功开录或当前尚不可查时,返回 `503` 。
- 上述 `503` 场景的常见错误码为 `RESULT_NOT_READY` 。
**响应体( 200) **
| **字段 ** | **类型 ** | **说明 ** |
| ------------ | -------- | ----------------------- |
| `surgery_id` | `string` | 手术号 |
| `status` | `string` | 成功时通常为 `completed` |
| `message` | `string` | 说明 |
| `details` | `array` | 消耗明细列表,字段见下文 |
| `summary` | `array` | 按 `item_id` 汇总的结果,字段见下文 |
**`details[]` 元素**
| **字段 ** | **类型 ** | **说明 ** |
| ----------- | --------- | ---------------------------------- |
| `item_id` | `string` | 物品 ID; 有目录时多为产品编码, 否则通常与名称或模型类名一致 |
| `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` 路径不符合约束 |
| `503` | `RESULT_NOT_READY` ,当前无可用明细或不可查 |
**响应示例( 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` | 当前有一条待确认 |
| `404` | 无待确认或手术未活跃;常见错误码为 `NO_PENDING_CONFIRMATION` |
| `422` | 例如话术为空导致无法 TTS; 错误码见响应, 如 `TTS_TEXT_EMPTY` |
| `503` | 语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED` 、`TTS_ERROR` |
### 5.6 提交待确认结果(医生语音)
**基本信息**
| **项目 ** | **内容 ** |
| ---------------- | ------------------------------------------------------------------------------- |
| **方法 ** | `POST` |
| **路径 ** | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
| **Content-Type ** | `multipart/form-data` |
**路径参数**
| **参数 ** | **约束 ** | **说明 ** |
| ----------------- | ---------- | -------------------------------- |
| `surgery_id` | 6 位数字 | 同 5.0 节 |
| `confirmation_id` | 长度 1 到 128 | 与 5.5 节响应中的 `confirmation_id` 一致 |
**请求体( multipart) **
| **字段名 ** | **类型 ** | **必填 ** | **说明 ** |
| ------- | ------ | ------ | ------------------------------------------------------ |
| `audio` | `file` | 是 | 单个 `.wav` 文件;建议使用 16 kHz 单声道 PCM; 非 `.wav` 扩展名会返回 `422` |
**业务说明**
音频上传至对象存储后执行 ASR 和候选解析。若识别为确认某个候选项,则记一条消耗;若识别为否认全部候选,则不记消耗。
**响应体( 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` | 已受理并完成解析 |
| `404` | 确认项不存在或手术未活跃;例如 `CONFIRMATION_NOT_FOUND` |
| `409` | 当前确认项已处理过;例如 `CONFIRMATION_ALREADY_RESOLVED` |
| `422` | 空文件、非 `.wav` 、`VOICE_AUDIO_INVALID` 、ASR/解析失败等,具体错误码见响应 |
| `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"
```
### 5.7 语音终端 assignment( HTTP, 可选)
**路径** `GET /client/voice-terminals/{terminal_id}/assignment`
2026-05-21 15:56:53 +08:00
仓库内 **手术室耗材语音确认浏览器客户端 ** ( `clients/voice-confirmation/` )仅通过 * * §5.8 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` |
### 5.8 语音终端 WebSocket
**路径** `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"}` 。
- 客户端可发送任意文本作心跳;服务端当前仅依赖 WebSocket 协议级 ping( 由网关或客户端库实现) 。