feat: 语音确认、联调与运维增强

- 语音:序数解析(第一个/第二个等)、解析失败计数与 API detail.retry_remaining;
  百度 ASR 固定 dev_pid 为普通话;SurgeryPipelineError 支持 extra 并入 HTTP detail。
- Demo:demo 路由与假 RTSP、客户端 index 与 README;BackendResolver 与配置调整。
- 可观测:消耗 TSV 日志、语音文件日志、终端 Markdown 辅助;相关测试与依赖更新。
- 注意:.env 仍被 gitignore,本地密钥不会进入本提交。

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-23 14:24:20 +08:00
parent 42720f81cf
commit 0c05463617
39 changed files with 3030 additions and 143 deletions

View File

@@ -6,10 +6,77 @@
```
scripts/demo_client/
server.py # 基于 stdlib 的静态服务器;额外暴露 /labels.json
index.html # 单文件页面(原生 JS零构建依赖
server.py # 基于 stdlib 的静态服务器;额外暴露 /labels.json
index.html # 单文件页面(原生 JS零构建依赖
fake_rtsp_from_file.py # 无真摄像头时:把本地视频循环发布为 RTSPffmpeg + Docker MediaMTX
```
## 调试:无真实摄像头,用录好的视频模拟 RTSP
监控服务**只从 RTSP URL 拉流**`cv2.VideoCapture`**没有**「上传视频文件」的 HTTP 接口;在不改 Python 后端的前提下,只能让「摄像头地址」指向一个**真实可连的 RTSP 源。
推荐做法:在**本机**把视频文件用 **ffmpeg** 推到本机上的 **RTSP 服务**(脚本用 Docker 启动 [MediaMTX](https://github.com/bluenviron/mediamtx)),得到 `rtsp://127.0.0.1:<端口>/<路径>`,再通过**环境变量**告诉后端(**只改配置,不改仓库里的后端代码**
**单路**(一个文件、一个 `camera_id`,兼容旧命令):
```bash
# 依赖ffmpeg、Docker首次会拉取 bluenviron/mediamtx
cd /path/to/operation-room-monitor-server
python3 scripts/demo_client/fake_rtsp_from_file.py /path/to/recording.mp4 --port 18554 --path demo
```
**两路**(两路不同视频、两个 `camera_id`**一个** MediaMTX、**两路** ffmpeg每路用不同的 `RTSP_PATH`
```bash
python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
--stream 'or-cam-01|./a.mp4|demo1' \
--stream 'or-cam-02|./b.mp4|demo2'
```
`--stream` 格式为 `CAMERA_ID|文件路径|RTSP_PATH`(竖线分隔,整条加引号),生成的 `VIDEO_RTSP_URLS_JSON` 会同时包含 `or-cam-01``or-cam-02`
在**另一终端**启动监控服务前 `source` 或手动 `export` 上述变量,使 `POST /client/surgeries/start` 里使用的 `camera_ids`(如 `or-cam-01,or-cam-02`)能解析到对应 URL。Demo 页里「将 camera_id 填到开始手术」可一键同步两路 id。
### 监控在 Docker、假 RTSP 在宿主机(推荐联调拓扑)
常见安排是:**假摄像头脚本**`fake_rtsp_from_file.py` + ffmpeg + MediaMTX在**宿主机**终端里跑,推流地址是 `rtsp://127.0.0.1:<端口>/...`**监控 API 服务**在 **Docker 容器**里跑,容器里的进程要访问宿主机上的 RTSP应使用
- **macOS / Windows Docker Desktop**`rtsp://host.docker.internal:<端口>/<路径>`
- **Linux**`host.docker.internal` 可能未预置,可任选其一:
- 给该服务容器加 `--add-host=host.docker.internal:host-gateway`Docker 20.10+),或
- 直接把 URL 写成宿主在 **docker0/桥接网** 上可达的局域网 IP`192.168.x.x`),保证从容器内 `curl`/`ffprobe` 能通
`docker-compose` 里可将 `VIDEO_RTSP_URLS_JSON` 写进 `environment:` 或 env 文件;**不要**在仅容器可解析的配置里写 `127.0.0.1` 去指宿主机上的 RTSP`127.0.0.1` 在容器内是容器自己)。
若监控与假 RTSP **都在宿主机同一系统**里直接跑(非容器),则用 `rtsp://127.0.0.1:...` 即可;否则应使用上面「容器连宿主」的写法。
发布失败时,可尝试把输入转码后再推流(示例,需自行调整):
```bash
ffmpeg -re -stream_loop -1 -i recording.mp4 -c:v libx264 -pix_fmt yuv420p -f rtsp -rtsp_transport tcp rtsp://127.0.0.1:18554/demo
```
(仍须先自行启动 MediaMTX 或等价 RTSP 服务端。)
Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放** 为路1/路2 指定文件,并配合下面 **一键开录** 上传,无需在页面里手抄 `python3` / `export` 命令。若必须完全手跑 `fake_rtsp_from_file.py`,请在上文命令示例与 `export VIDEO_RTSP_URLS_JSON=...` 方式自行在终端完成。
## 一键开录(不再手抄命令)
在 §4.1 勾选 **「一键联调」** 后,在「调试」里为**路1/路2**各选一段视频,再点 **开始手术**,浏览器会把两路视频 **multipart 上传到监控 API**`POST /internal/demo/orchestrate-and-start`),由服务进程依次:
1. 落盘两路视频到临时目录
2. 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSP`fake_rtsp_from_file.py` 等效)
3.`{"or-cam-01":"rtsp://127.0.0.1:…","or-cam-02":"rtsp://127.0.0.1:…"}` 写入 `VIDEO_RTSP_URLS_JSON_FILE`(与开录/拉流同进程,固定本机回环;`DEMO_ORCHESTRATOR_RTSP_JSON_HOST` 仅影响你**手配**假流、给另一进程读 JSON 的用法)
4. 调用与普通开录相同逻辑
**需同时满足**
- `.env``DEMO_ORCHESTRATOR_ENABLED=true`(并重启 API
- 已设置 `VIDEO_RTSP_URLS_JSON_FILE` 指向**可写**的 JSON 文件Docker 中请用 **bind-mount** 到容器内同一路径
- **运行 `main.py` 的进程**能执行本机 `docker``ffmpeg`(与手动跑 `fake_rtsp_from_file` 相同)。**仅将 API 放 Docker、且不挂载** ` /var/run/docker.sock` 时,容器内往往无法为你在宿主机起 MediaMTX此时请继续用手动假流方式。
由于每次解析都会重新读取 `video_rtsp_url_map()`,覆盖 JSON 后**无需重启**主服务即可被下一次开录用到。
## 运行方式
```bash
@@ -35,6 +102,7 @@ open http://localhost:38081/
- §4.3 `GET /client/surgeries/{id}/result` — 以表格渲染 `details``summary`
- §4.4 `GET /client/surgeries/{id}/pending-confirmation` — 支持手动拉取与 2s 自动轮询
- §4.5 `POST .../resolve` — 本地麦克风录音 → 16 kHz 单声道 WAV → `multipart/form-data` 上传
- **调试:无摄像头** — 两路视频选择与 `camera_id`;一键联调见上文;手跑假流见 `fake_rtsp_from_file.py` 与本文「调试:无真实摄像头」
右侧「响应日志」按时间倒序展示每次请求的 method/url/status/body便于联调截图。