Files
operating-room-monitor-server/scripts/demo_client/README.md
Kevin e4c6127619 feat: consumption log top1 + elapsed since recording; RTSP play once
- Add top1_name/top1_conf to TSV and show top1–3 in pending markdown
- Add 相对开录 column and pass since_recording_start from surgery start
- Track surgery_started_wall and format_elapsed_mmss_since in session registry
- Remove ffmpeg stream_loop from synthetic/demo fake RTSP (play once)
- Fix fake_rtsp_from_file poll loop indentation; update README
- Extend consumption TSV tests; add face test PNGs under tests/faces

Made-with: Cursor
2026-04-27 09:22:46 +08:00

133 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Demo Client
一个浏览器里的单页 demo用于手动触发 `app/api.py` 里的 5 个 `/client/*` 接口,覆盖开始/结束手术、查询结果、拉取待确认耗材,以及**本地麦克风录 WAV 并上传**语音确认接口。
## 结构
```
scripts/demo_client/
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 -i recording.mp4 -c:v libx264 -pix_fmt yuv420p -f rtsp -rtsp_transport tcp rtsp://127.0.0.1:18554/demo
```
(仍须先自行启动 MediaMTX 或等价 RTSP 服务端;上例为**播完即止**,若要循环请加 `-stream_loop -1`。)
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
# 1) 启动后端(默认 38080。CORS 中间件在 settings.demo_cors_enabled=True 时自动挂载。
uv run python main.py
# 2) 启动 demo 客户端静态服务(默认 127.0.0.1:38081
python scripts/demo_client/server.py
# 或指定端口:
python scripts/demo_client/server.py -p 9000 --host 0.0.0.0
# 3) 浏览器访问:
open http://localhost:38081/
```
页面顶部的「服务端 Base URL」默认是 `http://localhost:38080`;如果后端部署在其他主机/端口,直接改这里即可。
## 页面包含什么
- `GET /health` 连通性检查
- §4.1 `POST /client/surgeries/start` — 含 `surgery_id` 校验、`camera_ids` 多值输入、`candidate_consumables` 标签编辑器(初始值从 `/labels.json` 载入,可增删)
- §4.2 `POST /client/surgeries/end`
- §4.3 `GET /client/surgeries/{id}/result` — 以表格渲染 `details``summary`
- §4.4 `GET /client/surgeries/{id}/pending-confirmation` — 支持手动拉取与 **10s** 自动轮询(请求串行排队,避免与 §4.5 上传后紧接拉取竞态)
- §4.5 `POST .../resolve` — 本地麦克风录音 → 16 kHz 单声道 WAV → `multipart/form-data` 上传
- **调试:无摄像头** — 两路视频选择与 `camera_id`;一键联调见上文;手跑假流见 `fake_rtsp_from_file.py` 与本文「调试:无真实摄像头」
右侧「响应日志」按时间倒序展示每次请求的 method/url/status/body便于联调截图。
## 关于 `/labels.json`
`server.py` 在进程启动时读 `app/resources/consumable_classifier_labels.yaml``names` 映射并返回 `{"labels": [...]}`。优先用 `PyYAML`(主项目依赖已间接引入),缺失时回退到手写的最小 YAML 解析器(仅兼容该文件的已知形状)。
## 关于麦克风
浏览器的 `getUserMedia` 仅在**安全上下文**可用,对 demo 实际意味着:
- **可以用**`http://localhost``http://127.0.0.1``https://...`
- **不能用**:直接 `file://` 双击打开 `index.html`,或通过非本机 `http://` 访问 —— 所以这里用了独立的静态 HTTP 服务器而不是 `file://`
页面采用:
1. `navigator.mediaDevices.getUserMedia` 拿到单声道音轨
2. `AudioContext` + `ScriptProcessorNode` 捕获 Float32 原始 PCM输入采样率取决于系统如 48 kHz
3. 前端把样本线性降采样到 16 kHz、转 Int16 并拼 RIFF/WAVE 头
4. 产出 `Blob("audio/wav")` 后可以「下载 wav调试」或直接「上传并确认」
## 关闭 CORS生产环境
`app/config.py` 新增:
- `DEMO_CORS_ENABLED`(默认 `True`,生产请在 `.env` 里置 `false`
- `DEMO_CORS_ORIGINS`(默认 `*`,可写 `http://my-host:38081,https://or-demo.example.com`