- Refactor app API and schemas; adjust surgery pipeline, repository, and session manager. - Improve consumption TSV logging and consumable vision integration; trim voice resolution. - Add Baidu Face 1:N search script, .env.example entries, and client API integration doc. - Update demo client, staging checklist, surgery interface doc, and related tests; add sample face image. Made-with: Cursor
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 # 无真摄像头时:把本地视频循环发布为 RTSP(ffmpeg + Docker MediaMTX)
调试:无真实摄像头,用录好的视频模拟 RTSP
监控服务只从 RTSP URL 拉流(cv2.VideoCapture),没有「上传视频文件」的 HTTP 接口;在不改 Python 后端的前提下,只能让「摄像头地址」指向一个**真实可连的 RTSP 源。
推荐做法:在本机把视频文件用 ffmpeg 推到本机上的 RTSP 服务(脚本用 Docker 启动 MediaMTX),得到 rtsp://127.0.0.1:<端口>/<路径>,再通过环境变量告诉后端(只改配置,不改仓库里的后端代码):
单路(一个文件、一个 camera_id,兼容旧命令):
# 依赖: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):
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:... 即可;否则应使用上面「容器连宿主」的写法。
发布失败时,可尝试把输入转码后再推流(示例,需自行调整):
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),由服务进程依次:
- 落盘两路视频到临时目录
- 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSP(与
fake_rtsp_from_file.py等效) - 把
{"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 的用法) - 调用与普通开录相同逻辑
需同时满足:
.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 后无需重启主服务即可被下一次开录用到。
运行方式
# 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— 支持手动拉取与 2s 自动轮询 - §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://。
页面采用:
navigator.mediaDevices.getUserMedia拿到单声道音轨AudioContext+ScriptProcessorNode捕获 Float32 原始 PCM(输入采样率取决于系统,如 48 kHz)- 前端把样本线性降采样到 16 kHz、转 Int16 并拼 RIFF/WAVE 头
- 产出
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)