- 新增 Alembic 初始迁移、领域明细模型及归档持久化与重试链路\n- 拆分视频会话注册表、分类处理、推理时间窗聚合与流处理\n- 消耗日志:TSV/Markdown 含 top2/top3;item_id 优先产品编码;待确认记「待确认」行,语音确认后落正式行并更新汇总\n- 待确认时内存/DB 明细为占位行,确认后替换;拒绝时移除占位\n- 分类 probs 先 detach/cpu 再转 NumPy,修复 MPS/CUDA 上推理被静默跳过\n- 补充集成测试、归档与设备张量等单测 Made-with: Cursor
597 lines
16 KiB
Markdown
597 lines
16 KiB
Markdown
<style>
|
||
/*
|
||
* Obsidian 单文件修复:
|
||
* 只复制本 Markdown 到 Obsidian 时,也尽量保证中文在预览和 PDF 导出中可见。
|
||
*/
|
||
|
||
.markdown-rendered,
|
||
.markdown-preview-view,
|
||
.markdown-source-view.mod-cm6 .cm-contentContainer,
|
||
.markdown-source-view.mod-cm6 .cm-scroller,
|
||
.mermaid,
|
||
.mermaid svg,
|
||
.mermaid svg text,
|
||
.mermaid svg tspan,
|
||
.mermaid svg foreignObject {
|
||
font-family:
|
||
"PingFang SC",
|
||
"Hiragino Sans GB",
|
||
"Noto Sans CJK SC",
|
||
"Source Han Sans SC",
|
||
"Microsoft YaHei",
|
||
sans-serif !important;
|
||
font-variant-ligatures: none !important;
|
||
}
|
||
|
||
.markdown-rendered code,
|
||
.markdown-rendered pre,
|
||
.markdown-preview-view code,
|
||
.markdown-preview-view pre,
|
||
code,
|
||
pre,
|
||
kbd,
|
||
samp {
|
||
font-family:
|
||
"Sarasa Mono SC",
|
||
"Maple Mono SC",
|
||
"SF Mono",
|
||
"Menlo",
|
||
"Monaco",
|
||
"Consolas",
|
||
"PingFang SC",
|
||
monospace !important;
|
||
font-variant-ligatures: none !important;
|
||
}
|
||
|
||
@media print {
|
||
body,
|
||
.markdown-preview-view,
|
||
.markdown-rendered,
|
||
.markdown-preview-section,
|
||
.markdown-rendered p,
|
||
.markdown-rendered li,
|
||
.markdown-rendered blockquote,
|
||
.markdown-rendered table,
|
||
.markdown-rendered thead,
|
||
.markdown-rendered tbody,
|
||
.markdown-rendered tr,
|
||
.markdown-rendered th,
|
||
.markdown-rendered td,
|
||
.markdown-rendered strong,
|
||
.markdown-rendered em,
|
||
.markdown-rendered span,
|
||
.markdown-rendered div,
|
||
.markdown-rendered h1,
|
||
.markdown-rendered h2,
|
||
.markdown-rendered h3,
|
||
.markdown-rendered h4,
|
||
.markdown-rendered h5,
|
||
.markdown-rendered h6,
|
||
.markdown-rendered .callout,
|
||
.markdown-rendered .callout-title,
|
||
.markdown-rendered .callout-content,
|
||
.mermaid,
|
||
.mermaid svg,
|
||
.mermaid svg text,
|
||
.mermaid svg tspan,
|
||
.mermaid svg foreignObject {
|
||
font-family:
|
||
"PingFang SC",
|
||
"Hiragino Sans GB",
|
||
"Noto Sans CJK SC",
|
||
"Source Han Sans SC",
|
||
"Microsoft YaHei",
|
||
sans-serif !important;
|
||
font-variant-ligatures: none !important;
|
||
}
|
||
|
||
code,
|
||
pre,
|
||
kbd,
|
||
samp,
|
||
.markdown-rendered code,
|
||
.markdown-rendered pre,
|
||
.markdown-rendered pre code,
|
||
.markdown-rendered .inline-code {
|
||
font-family:
|
||
"Sarasa Mono SC",
|
||
"Maple Mono SC",
|
||
"SF Mono",
|
||
"Menlo",
|
||
"Monaco",
|
||
"Consolas",
|
||
"PingFang SC",
|
||
monospace !important;
|
||
font-variant-ligatures: none !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
# 手术室监控服务:客户端手术通信接口说明
|
||
|
||
面向对接本 FastAPI 服务的客户端(HIS、手麻、工作站等)。字段、状态码与返回模型以 `/docs` 和 `/openapi.json` 为准,本文用于摘要和流程说明。
|
||
|
||
> [!summary] 常用响应模型
|
||
> - `SurgeryApiResponse`
|
||
> - `SurgeryResultResponse`
|
||
> - `SurgeryPendingConfirmationResponse`
|
||
> - 业务错误外形见 `SurgeryClientErrorResponse`
|
||
> - 内部演示页:`scripts/demo_client/`(仅供演示,不作为对外契约)
|
||
|
||
## 能力概览
|
||
|
||
- 探活:`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 后入账或关闭。该录音与播报音频无关。
|
||
|
||
> [!important] HTTP 约定
|
||
> - `start` 和 `end` 使用 `POST` + `application/json`
|
||
> - `result` 使用 `GET`
|
||
> - `resolve` 使用 `POST` + `multipart/form-data`
|
||
> - `surgery_id` 固定为 6 位数字,正则为 `^\d{6}$`
|
||
> - `resolve` 路径中的 `confirmation_id` 必须与待确认接口返回值一致
|
||
> - `camera_ids` 必须与第 2 节清单及运维配置完全一致
|
||
|
||
## 1. 服务与基础信息
|
||
|
||
- 协议:`HTTP/HTTPS`
|
||
- 端口:`38080`,生产环境以实际入口为准
|
||
- 路由:无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health`
|
||
- `start` / `end` 请求体:JSON
|
||
- `resolve` 请求体:`multipart/form-data`,字段名为 `audio`
|
||
- 在线文档:`/docs`、`/redoc`
|
||
|
||
## 2. 摄像头 ID 与 RTSP
|
||
|
||
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境(例如 JSON 映射或环境变量)。业务程序不在客户端保存 RTSP,客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`。
|
||
|
||
配置格式示例见 `app/resources/camera_rtsp_urls.sample.json`。配置项细节见 `.env.example` 与 `docs/video-backends.md`。
|
||
|
||
**摄像头映射示例**
|
||
|
||
- `or-cam-01`
|
||
- RTSP:`rtsp://...`(由现场或 NVR 文档整理后交给运维)
|
||
- 备注:术间、机位
|
||
- `or-cam-02`
|
||
- RTSP:`...`
|
||
- 备注:`...`
|
||
|
||
> [!warning] 对接要求
|
||
> - `camera_ids` 必须与运维配置中的 key 完全一致
|
||
> - RTSP 不应硬编码在客户端业务程序中
|
||
|
||
> [!tip] 联调建议
|
||
> - 运维配置完成后,客户端使用上面清单中的 `camera_id` 调用 `start` 验证是否返回 `200`
|
||
> - 若返回 `503` 且 `detail.code = RECORDING_CANNOT_START`,优先核对 ID 拼写以及监控服务器侧网络连通性
|
||
|
||
## 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`:提交待确认结果(WAV)
|
||
|
||
## 4. 流程
|
||
|
||
### 4.1 时序图
|
||
|
||
```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 或 404
|
||
opt 有待确认
|
||
Client->>Client: 播放 prompt_audio_mp3_base64
|
||
Client->>API: POST .../resolve(multipart 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(持久化后可查时返回)
|
||
```
|
||
|
||
### 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. 接口详情
|
||
|
||
以下按“基本信息 -> 请求 -> 响应 -> 状态码”组织,与 OpenAPI 中 `tags: client` 和 `health` 一致。
|
||
|
||
### 5.0 通用约定
|
||
|
||
**路径参数 `surgery_id`**
|
||
|
||
- 长度:固定 `6`
|
||
- 字符集:仅数字
|
||
- 正则:`^\d{6}$`
|
||
|
||
**业务错误响应**
|
||
|
||
多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
|
||
|
||
```json
|
||
{
|
||
"detail": {
|
||
"code": "错误码字符串",
|
||
"message": "人类可读说明",
|
||
"surgery_id": "123456"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.1 探活
|
||
|
||
**基本信息**
|
||
|
||
- 方法:`GET`
|
||
- 路径:`/health`
|
||
- 请求体:无
|
||
|
||
**响应说明**
|
||
|
||
- `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`
|
||
- 若同一 `surgery_id` 存在尚未落库的历史归档,服务端会先尝试写入数据库;失败时可能返回 `503`(如 `RECORDING_CANNOT_START`),以避免静默丢数据
|
||
- `candidate_consumables` 为空时,服务端会展开为目录 Excel 中的全部商品名,或在未配置目录时展开为分类模型的全部类名
|
||
|
||
**请求体(JSON)**
|
||
|
||
- `surgery_id`
|
||
- 类型:`string`
|
||
- 必填:是
|
||
- 说明:6 位数字,与路径规则一致
|
||
- `camera_ids`
|
||
- 类型:`string[]`
|
||
- 必填:是
|
||
- 说明:至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节
|
||
- `candidate_consumables`
|
||
- 类型:`string[]`
|
||
- 必填:否
|
||
- 说明:非空时仅这些名称参与自动记账与待确认;缺省或 `[]` 时使用全部候选
|
||
|
||
**响应体(200)**
|
||
|
||
- `surgery_id`
|
||
- 类型:`string`
|
||
- 说明:与请求一致
|
||
- `status`
|
||
- 类型:`string`
|
||
- 说明:成功时通常为 `accepted`
|
||
- `message`
|
||
- 类型:`string`
|
||
- 说明:说明文案
|
||
|
||
**状态码**
|
||
|
||
- `200`:开录已确认
|
||
- `422`:参数校验失败,例如 `surgery_id` 非 6 位或 `camera_ids` 为空数组
|
||
- `503`:开录未确认或录制子系统故障;`detail.code` 常见为 `RECORDING_CANNOT_START`
|
||
|
||
**请求示例**
|
||
|
||
```json
|
||
{
|
||
"surgery_id": "123456",
|
||
"camera_ids": ["or-cam-01", "or-cam-02"],
|
||
"candidate_consumables": ["纱布", "缝线", "止血钳"]
|
||
}
|
||
```
|
||
|
||
**响应示例(200)**
|
||
|
||
```json
|
||
{
|
||
"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` 示例为 `摄像头录制已停止,手术已结束。`
|
||
|
||
**状态码**
|
||
|
||
- `200`:停录已确认
|
||
- `422`:参数校验失败
|
||
- `503`:停录未确认或故障;`detail.code` 常见为 `RECORDING_NOT_STOPPED`
|
||
|
||
**请求示例**
|
||
|
||
```json
|
||
{
|
||
"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`
|
||
|
||
**状态码**
|
||
|
||
- `200`:至少有一条明细
|
||
- `422`:`surgery_id` 路径不符合约束
|
||
- `503`:`RESULT_NOT_READY`,当前无可用明细或不可查
|
||
|
||
**响应示例(200)**
|
||
|
||
```json
|
||
{
|
||
"surgery_id": "123456",
|
||
"status": "completed",
|
||
"message": "查询成功。",
|
||
"details": [
|
||
{
|
||
"item_id": "HC001",
|
||
"item_name": "纱布",
|
||
"qty": 1,
|
||
"doctor_id": "D1001",
|
||
"timestamp": "2026-04-21T10:30:00+08:00"
|
||
}
|
||
],
|
||
"summary": [
|
||
{
|
||
"item_id": "HC001",
|
||
"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`
|
||
- 说明:该选项对应的置信度
|
||
|
||
**状态码**
|
||
|
||
- `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 键,便于追溯
|
||
|
||
**状态码**
|
||
|
||
- `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 示例**
|
||
|
||
```bash
|
||
curl -sS -X POST \
|
||
"http://<主机>:38080/client/surgeries/123456/pending-confirmation/<confirmation_id>/resolve" \
|
||
-F "audio=@/path/to/voice.wav;type=audio/wav"
|
||
```
|