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
This commit is contained in:
Kevin
2026-04-27 09:22:46 +08:00
parent 8a6bfe9100
commit e4c6127619
13 changed files with 130 additions and 40 deletions

View File

@@ -8,7 +8,7 @@
scripts/demo_client/
server.py # 基于 stdlib 的静态服务器;额外暴露 /labels.json
index.html # 单文件页面(原生 JS零构建依赖
fake_rtsp_from_file.py # 无真摄像头时:把本地视频循环发布为 RTSPffmpeg + Docker MediaMTX
fake_rtsp_from_file.py # 无真摄像头时:把本地视频按文件时长推一次到 RTSPffmpeg + Docker MediaMTX
```
## 调试:无真实摄像头,用录好的视频模拟 RTSP
@@ -53,10 +53,10 @@ python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
发布失败时,可尝试把输入转码后再推流(示例,需自行调整):
```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
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 服务端。)
(仍须先自行启动 MediaMTX 或等价 RTSP 服务端;上例为**播完即止**,若要循环请加 `-stream_loop -1`。)
Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放** 为路1/路2 指定文件,并配合下面 **一键开录** 上传,无需在页面里手抄 `python3` / `export` 命令。若必须完全手跑 `fake_rtsp_from_file.py`,请在上文命令示例与 `export VIDEO_RTSP_URLS_JSON=...` 方式自行在终端完成。

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Publish local video file(s) as looping RTSP stream(s) (fake camera) for local dev.
"""Publish local video file(s) to RTSP once per file (fake camera) for local dev.
The Operation Room server only opens RTSP URLs (OpenCV); there is no video-upload API.
This script does NOT change the application backend: it runs ffmpeg + a small
@@ -103,7 +103,7 @@ def _parse_stream_arg(spec: str) -> tuple[str, Path, str]:
def main() -> int:
parser = argparse.ArgumentParser(
description="Loop video file(s) to RTSP URL(s) (dev fake camera; no backend code change).",
description="Play each video file once to an RTSP URL (dev fake camera; no backend code change).",
)
parser.add_argument(
"video",
@@ -198,7 +198,6 @@ def main() -> int:
"ffmpeg",
"-hide_banner", "-loglevel", "info",
"-re",
"-stream_loop", "-1",
"-i", str(fp),
"-c", "copy",
"-f", "rtsp",
@@ -225,7 +224,11 @@ def main() -> int:
h = u.replace("127.0.0.1", "host.docker.internal", 1)
print(f" {cam}: {h}", file=sys.stderr)
print("---", file=sys.stderr)
print("Fake RTSP running (Ctrl+C to stop; MediaMTX container removed on exit).", file=sys.stderr)
print(
"Fake RTSP running: each file plays once; script exits when ffmpeg ends "
"(Ctrl+C to stop early; MediaMTX container removed on exit).",
file=sys.stderr,
)
def on_sigint(_sig: int, _frame) -> None:
for p in procs:
@@ -240,8 +243,8 @@ def main() -> int:
try:
while True:
time.sleep(0.5)
for p in procs:
if p.poll() is not None:
for p in procs:
if p.poll() is not None:
print(
f"[fake-rtsp] ffmpeg ended (code {p.returncode}), stopping all.",
file=sys.stderr,