- 用 OR_SITE_CONFIG_JSON_FILE 统一术间配置(video_rtsp_urls + voice_or_room_bindings) - VoiceTerminalHub:assignment、WS 推送与 HTTP 查询;开录/停录后 notify - 一键联调 orchestrate-and-start 与 /client/surgeries/start 共用指派逻辑,修复 demo 路径不发 WS - 语音桌面端:SIGINT 退出、shutdown 清理、仅 WS 指派、固定 pending 轮询间隔、界面仅保留录音时长 - 新增/调整契约与绑定测试,文档与示例配置同步 Made-with: Cursor
457 lines
21 KiB
Markdown
457 lines
21 KiB
Markdown
# 手术室监控服务:客户端手术通信接口说明
|
||
|
||
## 能力概览
|
||
|
||
| **能力** | **说明** |
|
||
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **探活** | `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`。 |
|
||
| **待确认播报** | `GET /client/surgeries/{surgery_id}/pending-confirmation`,拉取队首低置信度任务,返回话术文本和 MP3 Base64。 |
|
||
| **待确认答复** | `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` |
|
||
|
||
## 2. 摄像头 ID 与 RTSP
|
||
|
||
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`。
|
||
|
||
| **camera_id** | **RTSP** | **备注** |
|
||
| ------------- | -------------------------------- | ------ |
|
||
| `or-cam-01` | `rtsp://...`(由现场或 NVR 文档整理后交给运维) | 术间、机位 |
|
||
| `or-cam-02` | `...` | `...` |
|
||
|
||
## 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**) |
|
||
| 8 | `WS` | `/client/voice-terminals/ws?terminal_id=...` | 语音桌面终端长连接,接收开录/停录指派(**推荐**) |
|
||
| | | | |
|
||
|
||
**术间与语音终端绑定(服务端配置)**
|
||
|
||
- **唯一配置源**:环境变量 **`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"}`。
|
||
- **多 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"],
|
||
"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 节接口时原样放入路径 |
|
||
| `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`
|
||
|
||
仓库内 **手术室耗材语音确认桌面客户端** 仅通过 **§5.8 WebSocket** 接收指派,**不调用**本接口。此处供运维脚本、未实现 WS 的第三方临时拉取 `active_surgery_id`。
|
||
|
||
**响应 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(由网关或客户端库实现)。 |