feat(v2): add realtime command and release tooling
This commit is contained in:
@@ -23,10 +23,10 @@
|
||||
| --- | --- | --- | --- |
|
||||
| 语言 | Go | 已定 | 面向长时间 RTSP 运行、并发控制和单一交付物。 |
|
||||
| 模型工件 | 固定输入尺寸的 ONNX Pose 模型 | 已定 | Go 不直接加载 `.pt`;模型由已验收 V1 权重导出并记录哈希。 |
|
||||
| 推理运行时 | `github.com/yalue/onnxruntime_go` v1.31.0 + ONNX Runtime CPU DLL | Spike 已验证 | Windows 上以显式 DLL 路径加载锁定的 1×3×640×640 Pose ONNX,已完成一次真实录像首帧推理;该包装器的 C API 为 26,V2 构建必须启用 CGO/MinGW,并在发布包锁定匹配 DLL 的来源与 SHA-256。 |
|
||||
| 推理运行时 | `github.com/yalue/onnxruntime_go` v1.31.0 + ONNX Runtime CPU DLL | 本机发布目录已验证 | Windows 上以显式 DLL 路径加载锁定的 1×3×640×640 Pose ONNX,已完成一次真实录像首帧推理;该包装器的 C API 为 26,V2 构建必须启用 CGO/MinGW。`build-v2-release.ps1` 已从显式本机 DLL 生成 SHA-256 清单;现场 RTSP 推理仍待验收。 |
|
||||
| 视频输入 | 受控的 `ffmpeg.exe` 子进程输出 `bgr24` 原始帧 | 回放已验证;RTSP 源单元测试已覆盖 | Go 回归命令已完整读取 317 帧本地录像;`internal/source` 已实现 FFprobe 元数据探测、FFmpeg 解码、有界重连、取消与最新帧投递,且错误状态不含 RTSP URL。避免未配置的 GoCV/OpenCV 开发绑定。T-304 仍须随应用锁定 FFmpeg/FFprobe 的版本、哈希,并在现场验证真实流重连。 |
|
||||
| 图像预处理与 Pose 解析 | Go 实现的 BGR→RGB、CHW、640 letterbox、NMS、17 点及坐标还原 | 合成契约与本机漏检回放已验证 | 采用 Ultralytics 的 114 补边、双线性缩放后转 uint8 的语义;Go 首帧输出两个人,与 V1 对应结果的 box 坐标差在已记录的 6 像素容差内。`demo/1.mp4` 是真实摔倒但在现有 V1 配置下漏检,不能称为“无报警反例”;仍需经同意、符合固定俯视大厅/走廊边界的正例录像完成事件级确认。 |
|
||||
| 演示 UI | `github.com/lxn/walk`(Windows 原生控件) | Spike 已验证 | 双 Tab 骨架可在 `CGO_ENABLED=0` 编译,避免 Fyne/OpenGL 首次编译的高复杂度;正式版仍须实现浅灰蓝底、白色卡片,红色仅表示确认摔倒。 |
|
||||
| 演示 UI | `github.com/lxn/walk`(Windows 原生控件) | 操作窗口已编译 | 浅灰蓝双 Tab、最新帧、来源/事件状态、脱敏设置、开始/停止、确认声音与弹窗入口已实现;避免 Fyne/OpenGL 首次编译的高复杂度。现场需验证实际键盘、缩放、声音与弹窗。 |
|
||||
| 测试 | `go test ./...` + V1 回归录像 | 已定 | V2 行为必须与 V1 基线对照。 |
|
||||
|
||||
## 关键选型纪律
|
||||
@@ -51,6 +51,8 @@
|
||||
| V2 ONNX/视频 Spike | `Set-Location v2; $env:CGO_ENABLED='1'; go run ./cmd/spike --ffmpeg <ffmpeg.exe> --video <local-video> --width <w> --height <h> --onnx assets\best.onnx --ort-dll <onnxruntime.dll>` |
|
||||
| V2 录像事件回归 | `Set-Location v2; $env:CGO_ENABLED='1'; go run ./cmd/regression --ffmpeg <ffmpeg.exe> --video <local-video> --width <w> --height <h> --fps <fps> --onnx assets\best.onnx --ort-dll <onnxruntime.dll>` |
|
||||
| V2 Walk UI 编译 Spike | `Set-Location v2; $env:CGO_ENABLED='0'; go build ./cmd/ui-spike` |
|
||||
| V2 正式应用 | `Set-Location v2; $env:CGO_ENABLED='1'; go run ./cmd/silver-pose --config <local-config.json>` |
|
||||
| V2 发布目录 | `Set-Location v2; ./scripts/build-v2-release.ps1 -OnnxPath <locked-onnx> -OrtDllPath <release-dll> -FfmpegPath <ffmpeg.exe> -FfprobePath <ffprobe.exe> -OutputDirectory <empty-folder>` |
|
||||
|
||||
## 演进决策
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
- 已验证环境:Windows PowerShell;Python 3.8.10;Ultralytics 8.3.205;PyQt5 可导入。注意:当前 WSL/Linux 环境未安装 PyQt5,且其 ultralytics/torch 版本与该基线不兼容(模型前向不可用),故 GUI 可视化与真实模型端到端渲染只在 Windows 验证;本环境用 Qt-free 的 `view_model.py` 单元测试与假适配器管线冒烟覆盖视图逻辑。
|
||||
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||
- V1 代码:已建立安全配置、显式 Replay/Stream 视频源、Pose、轻量跟踪、质量/几何证据、倒地领域规则、按 ID 四态事件机及 `v1/pipeline.py` 事件管线;运行事件带非敏感 `config_version`,缺帧/低质量/断流会中断证据确认。新增 `v1/view_model.py`(Qt-free 监控视图状态与设置草稿三份隔离)、`v1/gui.py` 薄 PyQt5 双 Tab 外壳与 `v1/app.py` 装配(`FrameWorker` 只发出已判定的 `FrameAnalysis`,窗口只渲染)。新增 `v1/alerts.py`:按 `event_id` 去重,对首次 CONFIRMED 保存带标注截图、追加 JSONL 事件行,并经可注入 `AlertSink`(Windows 侧 `QtAlertSink` 提供声音与一次性弹窗)触发一次声音/弹窗。新增 `v1/camera.py`(RTSP URL 构建 + 有界连接测试抓帧)与 `config.py` 的结构化来源(host/端口/通道/账号/密码,凭证百分号编码)、`write_local_camera_source`;设置页新增摄像头连接分组、测试连接与预览,以及传输协议/连接超时/低延迟三项抓流调优(由 `config` 存值、`app` 开流前写入 `OPENCV_FFMPEG_CAPTURE_OPTIONS`)。真实海康流已用 `VideoSource(STREAM)` 连通(1920×1080)。
|
||||
- V2 代码:T-303 已落地 `internal/pose`(Ultralytics 114 letterbox、BGR→RGB/CHW、YOLO Pose NMS、17 点及坐标还原)与 `internal/fall`(中心点跟踪、质量/几何证据、策略、四态状态机和事件 ID);`cmd/regression` 使用 FFmpeg 顺序回放本机录像并将 ONNX Pose 接入事件引擎。用户已手动确认 T-303 完成,但其缺少部署边界内真实正例录像的事实仍保留在诊断记录中。T-304 已新增安全 V2 配置、FFprobe/FFmpeg 有界重连源、后台监控装配、叠加 PNG/JSONL 证据和浅色 Walk 双 Tab 操作窗口;窗口只接收完成帧和可靠告警队列。正式 `cmd/silver-pose`、发布包及现场 RTSP/声音/弹窗目视验收尚未完成,V2 仍不能作为客户演示版。
|
||||
- V2 代码:T-303 已落地 `internal/pose`(Ultralytics 114 letterbox、BGR→RGB/CHW、YOLO Pose NMS、17 点及坐标还原)与 `internal/fall`(中心点跟踪、质量/几何证据、策略、四态状态机和事件 ID);`cmd/regression` 使用 FFmpeg 顺序回放本机录像并将 ONNX Pose 接入事件引擎。用户已手动确认 T-303 完成,但其缺少部署边界内真实正例录像的事实仍保留在诊断记录中。T-304 已新增安全 V2 配置、FFprobe/FFmpeg 有界重连源、后台监控装配、叠加 PNG/JSONL 证据、浅色 Walk 双 Tab 操作窗口和 `cmd/silver-pose` 正式装配命令;窗口只接收完成帧和可靠告警队列。`v2/scripts/build-v2-release.ps1` 已在本机从显式 ONNX/DLL/FFmpeg/FFprobe 输入生成临时发布目录与哈希清单。现场 RTSP、声音、弹窗、截图和安全模拟摔倒的目视验收尚未完成,V2 仍不能作为客户演示版。
|
||||
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md、docs/ui/silver-pose-v1-prototype.html 与 docs/ui/silver-pose-v2-prototype.html 已建立。v2 HTML 是符合正式浅色 Windows 规范的当前视觉参考:浅灰蓝底、白色卡片,红色只表示确认摔倒、其弹窗和事件证据;文件名中的 v2 只表示原型设计修订,不能理解为 Go V2 实现已开始。v1 HTML 保留为历史深色对照。两者均使用顶部双 Tab、设置草稿与状态交互,且画面、事件和时间都是模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
||||
- 测试:`python -m compileall -q v1 demo` 已通过(含 `gui.py`、`app.py`、`alerts.py`、`camera.py` 语法);`./init.ps1` 当前运行 V1 测试为 88 passed、1 skipped。T-303 的 `CGO_ENABLED=1 go test ./...` 通过。对同一 317 帧本机录像,V1 回放得到 253 个有人帧、263 人次、0 个 CONFIRMED;Go 回放得到 255 个有人帧、265 人次、0 个 CONFIRMED。该录像经用户确认是摔倒正例,二者的 0 事件是漏检,不是“无报警反例”通过:V1 在 2.400–2.467 秒仅有 3 帧横向候选,随后倒地人体的 Pose 置信度降至 0.012,并在 3.000 秒帧以 0.01 推理仍无任何人体姿态。它是室外斜视且有字幕遮挡,超出当前固定俯视大厅/走廊验收边界;降低检测阈值至 0.10 仍为 0 事件。人次差异来自 OpenCV/FFmpeg 解码像素不同,不能据此声称 Go 已完成真实正例一致性。Go 合成正例契约和 V1 状态机均在 1.81 秒时产生一个 `CONFIRMED`。Walk UI Spike 以 `CGO_ENABLED=0 go build ./cmd/ui-spike` 编译通过。这些不表示摔倒识别准确率或完整 Go 客户演示已验收。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||
- 当前标准启动:`./init.ps1`。
|
||||
- 当前标准验证:`python -m compileall -q demo`。
|
||||
- 当前 blocker:T-303 已由用户手动确认关闭,但仍缺少一段经同意、无敏感信息且符合固定俯视大厅/走廊、全身大部分可见边界的本机正例录像及其 V1 确认事件/延迟基线;因此不能将其表述为真实正例录像级一致性证明。`demo/1.mp4` 虽是真实摔倒,但已诊断为 Pose 漏检,不能作为此项验收正例。T-304 还须锁定 ONNX Runtime DLL、FFmpeg/FFprobe 的版本、来源和哈希;现场 RTSP 验收仍需要本机未跟踪凭证和现场网络,二者均不得提交。
|
||||
- 当前 blocker:T-303 已由用户手动确认关闭,但仍缺少一段经同意、无敏感信息且符合固定俯视大厅/走廊、全身大部分可见边界的本机正例录像及其 V1 确认事件/延迟基线;因此不能将其表述为真实正例录像级一致性证明。`demo/1.mp4` 虽是真实摔倒,但已诊断为 Pose 漏检,不能作为此项验收正例。T-304 的本机发布清单已锁定 ONNX Runtime DLL、FFmpeg/FFprobe 和 ONNX 哈希;最终现场 RTSP、声音、弹窗、事件截图/JSONL、断流恢复与安全摔倒验收仍需要未跟踪凭证、现场网络与现场人员,均不得提交。
|
||||
|
||||
全局环境的 `pip check` 存在其他项目的包冲突,因此它不是 Silver Pose 的验收命令。`init.ps1` 只检查本项目实际导入的 OpenCV、NumPy、Ultralytics 与 PyQt5,并在命令非零退出时失败。
|
||||
|
||||
@@ -46,6 +46,9 @@ python main.py
|
||||
Set-Location ..\v2
|
||||
$env:CGO_ENABLED='1'
|
||||
go run ./cmd/regression --help
|
||||
|
||||
# V2 正式应用(需未提交 config.local.json 与环境变量 RTSP)
|
||||
go run ./cmd/silver-pose --config config.local.json
|
||||
```
|
||||
|
||||
## 已知事实和限制
|
||||
|
||||
@@ -319,7 +319,7 @@ git commit -m "feat(v2): implement operator monitoring window"
|
||||
- Modify: `docs/06-tasks.md`
|
||||
- Modify: `progress.md`
|
||||
|
||||
- [ ] **Step 1: Write the command preflight test**
|
||||
- [x] **Step 1: Write the command preflight test**
|
||||
|
||||
```go
|
||||
func TestValidateStartupRejectsMissingRuntimeWithoutLeakingSource(t *testing.T) {
|
||||
@@ -328,12 +328,12 @@ func TestValidateStartupRejectsMissingRuntimeWithoutLeakingSource(t *testing.T)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify it fails before adding the command package**
|
||||
- [x] **Step 2: Verify it fails before adding the command package**
|
||||
|
||||
Run: `Set-Location v2; go test ./cmd/silver-pose -run TestValidateStartup -v`
|
||||
Expected: FAIL because the command package is absent.
|
||||
|
||||
- [ ] **Step 3: Implement preflight, lifecycle and release assembly**
|
||||
- [x] **Step 3: Implement preflight, lifecycle and release assembly**
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -347,7 +347,7 @@ func main() {
|
||||
|
||||
`build-v2-release.ps1` must require `-OnnxPath`, `-OrtDllPath`, `-FfmpegPath`, `-FfprobePath` and `-OutputDirectory`; it must reject a non-empty target directory, build `silver-pose.exe` with CGO/MinGW, copy the four explicit binaries plus `config.example.json`, write `runtime-manifest.json` with SHA-256 and tool version output, and fail when a required input is missing. The runbook must document a local ignored `config.local.json`, setting the RTSP environment variable outside the repository, normal/reconnect/confirmed demonstrations, where event evidence is written, a 1–3 second stated target, and the fixed-view scope limitation.
|
||||
|
||||
- [ ] **Step 4: Run automated and release-folder smoke verification**
|
||||
- [x] **Step 4: Run automated and release-folder smoke verification**
|
||||
|
||||
Run: `Set-Location v2; $env:CGO_ENABLED='1'; go test ./...; go build ./cmd/silver-pose`
|
||||
Expected: PASS.
|
||||
@@ -363,11 +363,11 @@ Expected: PASS.
|
||||
Run: launch the release executable with a local configuration and approved live RTSP stream.
|
||||
Expected: live frame and skeleton/ID; disconnect causes reconnect text without fall alert; one approved fall produces red overlay, one sound, one acknowledgment popup, one PNG and one JSONL line; normal activity does not create a confirmed event.
|
||||
|
||||
- [ ] **Step 6: Commit delivery evidence**
|
||||
- [x] **Step 6: Commit command and release-tooling slice**
|
||||
|
||||
```powershell
|
||||
git add v2/cmd/silver-pose v2/scripts docs/03-tech-stack.md docs/04-architecture.md docs/current-state.md docs/06-tasks.md docs/v2-demo-runbook.md progress.md
|
||||
git commit -m "feat(v2): deliver realtime demo and release tooling"
|
||||
git commit -m "feat(v2): add realtime command and release tooling"
|
||||
```
|
||||
|
||||
## Plan self-review
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Silver Pose V2 现场演示与发布手册
|
||||
|
||||
## 适用范围
|
||||
|
||||
本手册只适用于单路、固定机位、向下俯视且人员全身大部分可见的大厅或走廊。系统用 Pose 关键点和持续倒地事件规则触发报警,不是医疗诊断,也不承诺室外斜视、强遮挡、字幕视频或任意公共场景的效果。
|
||||
|
||||
## 发布前构建
|
||||
|
||||
在 `v2/` 目录执行下列命令,所有路径均为本机可信发布输入。脚本要求目标目录不存在或为空,并生成 `runtime-manifest.json` 记录实际 SHA-256 和工具版本:
|
||||
|
||||
```powershell
|
||||
./scripts/build-v2-release.ps1 `
|
||||
-OnnxPath <locked-best.onnx> `
|
||||
-OrtDllPath <release-onnxruntime.dll> `
|
||||
-FfmpegPath <ffmpeg.exe> `
|
||||
-FfprobePath <ffprobe.exe> `
|
||||
-OutputDirectory <empty-release-folder>
|
||||
```
|
||||
|
||||
发布目录包含 `silver-pose.exe`、`runtime/`、公开 `config.example.json`、本手册和运行时哈希清单。不要把真实摄像头地址、密码、客户录像、事件截图或本机 `config.local.json` 放进发布目录。
|
||||
|
||||
## 本机安全配置
|
||||
|
||||
1. 从 `config.example.json` 复制为未提交的 `config.local.json`。
|
||||
2. 使用发布目录中的 `runtime-manifest.json` 把 `model.sha256` 填为 `runtime/best.onnx` 的实际 SHA-256。
|
||||
3. 在当前 PowerShell 会话设置 RTSP 环境变量;不要把命令、地址或密码复制进仓库、截图或 JSONL。
|
||||
|
||||
```powershell
|
||||
$env:SILVER_POSE_RTSP_URL = '<本机安全 RTSP 地址>'
|
||||
.\silver-pose.exe --config .\config.local.json
|
||||
```
|
||||
|
||||
设置页只显示环境变量名称和是否就绪,不显示解析后的地址或密码。开始监控后,配置快照固定;若要修改事件参数,请停止监控、修改本地文件,再重新开始。
|
||||
|
||||
## 客户演示顺序
|
||||
|
||||
1. 启动后确认“视频流:在线”,实时画面出现人员骨架和 ID;正常状态不是红色。
|
||||
2. 演示走动、坐下、弯腰、捡物和短暂停留,确认没有 `CONFIRMED`、声音、弹窗或新证据。
|
||||
3. 在现场负责人同意与安全防护下,安排成年人在画面中从站立/行走转为持续倒地。目标确认延迟是 1–3 秒;确认后应出现红色姿态和边框、一次 Windows 声音、一次确认弹窗,以及一张 PNG 和一条 JSONL。
|
||||
4. 保持倒地画面,确认同一 `event_id` 不重复声音、弹窗、截图或 JSONL。恢复稳定站立后才允许新的事件。
|
||||
5. 暂停流或断开网络,确认界面显示正在重连且没有红色摔倒告警;恢复流后回到在线。
|
||||
|
||||
事件证据位于本机 `artifacts/events/YYYYMMDD/`:PNG 文件名仅含事件 ID,`events.jsonl` 可核查来源 ID、配置版本、确认时间和延迟。它们是本地工件,不提交。
|
||||
|
||||
## 交付验收记录
|
||||
|
||||
记录发布目录的 `runtime-manifest.json`、Windows/Go 版本、摄像头安装高度与角度、正反例动作结果、确认延迟、断流恢复结果和操作者。不要记录 RTSP URL、账号、密码、序列号或客户个人信息。
|
||||
@@ -514,3 +514,12 @@
|
||||
- 阻塞:T-304 实现阶段无代码 blocker;最终现场验收仍需要未跟踪的本机 RTSP 凭证、网络和现场视频。发布依赖的 ONNX Runtime DLL、FFmpeg/FFprobe 还需锁定来源、版本与 SHA-256。
|
||||
- 决策:T-304 继续使用 Go、Walk、受控 FFmpeg 和 ONNX Runtime;保持单路固定机位边界、浅色 Windows UI,红色仅用于 CONFIRMED 与关联报警证据。
|
||||
- 下一步:审查 V2 当前 Spike、V1 报警契约和 UI 规格,编写并执行 T-304 的可测试实施计划。
|
||||
|
||||
## 【2026-07-22】T-304 Go 实时演示、报警和打包(实现部分完成)
|
||||
|
||||
- 状态:DOING(代码与本机发布目录已完成;现场演示验收待执行)。
|
||||
- 变更:新增 `v2/internal/config`(仅环境变量 RTSP、脱敏配置版本)、`internal/source`(FFprobe/FFmpeg、有界重连、最新帧)、`internal/render`/`internal/alert`(姿态叠加、一次 PNG/JSONL)、`internal/monitor`(后台 Pose→事件→证据,画面与告警分离队列)、`internal/ui` 的浅色 Walk 双 Tab 窗口、`cmd/silver-pose`、发布脚本与 V2 现场手册。红色只由 `CONFIRMED` 使用;来源错误不进入事件引擎且不含 URL。
|
||||
- 验证:`CGO_ENABLED=1 go test ./...` 通过;`CGO_ENABLED=1 go build ./cmd/silver-pose` 通过;缺少 `SILVER_POSE_RTSP_URL` 的命令冒烟以非零退出且只显示环境变量名。`build-v2-release.ps1` 已从本机显式输入生成忽略的 `artifacts/v2-release-check`,包含 `silver-pose.exe`、ONNX、ONNX Runtime DLL、FFmpeg、FFprobe、公开配置、手册和 `runtime-manifest.json`。清单记录 ONNX SHA-256 `fac42d41a2ae3108dc42b3a2097f299e27d7f4055cb6b26a319fad0b0a3f286e`、DLL/工具哈希与 FFmpeg 版本。
|
||||
- 阻塞:最终验收需现场未跟踪 RTSP 凭证、可用海康流、现场网络与经同意的安全模拟动作;当前不能伪造实时画面、声音、弹窗、截图或 1–3 秒正例延迟。`demo/1.mp4` 继续只作为域外漏检风险样本。
|
||||
- 决策:发布脚本不从 PATH、Python 包或 Go 缓存隐式取运行依赖,全部由显式参数输入并记录哈希;事件队列不随最新帧丢弃,确保 UI 不因慢刷新遗漏确认告警。
|
||||
- 下一步:用本机 `config.local.json` 与现场 RTSP 启动发布目录程序,按 `docs/v2-demo-runbook.md` 逐项验证在线/反例/确认/断流恢复,再决定是否将 T-304 置 DONE。
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"silverpose/v2/internal/alert"
|
||||
"silverpose/v2/internal/config"
|
||||
"silverpose/v2/internal/fall"
|
||||
"silverpose/v2/internal/monitor"
|
||||
"silverpose/v2/internal/source"
|
||||
"silverpose/v2/internal/spike"
|
||||
"silverpose/v2/internal/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.local.json", "local V2 JSON configuration without RTSP credentials")
|
||||
flag.Parse()
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
log.Fatal("配置无效:", err)
|
||||
}
|
||||
if err := validateStartup(cfg); err != nil {
|
||||
log.Fatal("启动预检失败:", err)
|
||||
}
|
||||
controller := &controller{config: cfg}
|
||||
window, err := ui.NewWindow(settingsFor(cfg), ui.Commands{Start: controller.Start, Stop: controller.Stop})
|
||||
if err != nil {
|
||||
log.Fatal("创建操作窗口失败:", err)
|
||||
}
|
||||
controller.window = window
|
||||
window.Run()
|
||||
controller.Stop()
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
config config.Config
|
||||
window *ui.Window
|
||||
mu sync.Mutex
|
||||
cancel func()
|
||||
runID uint64
|
||||
}
|
||||
|
||||
func (controller *controller) Start() {
|
||||
controller.mu.Lock()
|
||||
if controller.cancel != nil {
|
||||
controller.mu.Unlock()
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
controller.cancel = cancel
|
||||
controller.runID++
|
||||
runID := controller.runID
|
||||
controller.mu.Unlock()
|
||||
go controller.run(ctx, cancel, runID)
|
||||
}
|
||||
|
||||
func (controller *controller) Stop() {
|
||||
controller.mu.Lock()
|
||||
cancel := controller.cancel
|
||||
controller.cancel = nil
|
||||
controller.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *controller) run(ctx context.Context, cancel func(), runID uint64) {
|
||||
defer func() {
|
||||
controller.mu.Lock()
|
||||
if controller.runID == runID {
|
||||
controller.cancel = nil
|
||||
}
|
||||
controller.mu.Unlock()
|
||||
cancel()
|
||||
}()
|
||||
runtime, err := spike.OpenRuntime(controller.config.Model.ONNXPath, controller.config.Model.ONNXRuntimeDLLPath)
|
||||
if err != nil {
|
||||
controller.window.ReportIssue("无法初始化 ONNX Runtime")
|
||||
return
|
||||
}
|
||||
engine, err := fall.NewEngine(fall.EngineConfig{
|
||||
KeypointConfidenceThreshold: controller.config.Event.KeypointConfidenceThreshold,
|
||||
SuspectWindowSeconds: controller.config.Event.SuspectWindowSeconds,
|
||||
ConfirmWindowSeconds: controller.config.Event.ConfirmWindowSeconds,
|
||||
RecoveryWindowSeconds: controller.config.Event.RecoveryWindowSeconds,
|
||||
CooldownSeconds: controller.config.Event.CooldownSeconds,
|
||||
RequireRapidDrop: controller.config.Event.RequireRapidDrop,
|
||||
RequireLowerBody: controller.config.Event.RequireLowerBody,
|
||||
HorizontalAngleThresholdDegrees: controller.config.Event.HorizontalAngleThresholdDegrees,
|
||||
ConfigVersion: controller.config.RuntimeConfigVersion,
|
||||
SessionID: time.Now().UTC().Format("20060102-150405"),
|
||||
})
|
||||
if err != nil {
|
||||
runtime.Close()
|
||||
controller.window.ReportIssue("无法初始化摔倒事件引擎")
|
||||
return
|
||||
}
|
||||
stream := source.Start(ctx, source.Config{
|
||||
SourceURL: controller.config.Source.URL, FFmpegPath: controller.config.Tools.FFmpegPath,
|
||||
FFprobePath: controller.config.Tools.FFprobePath, Transport: controller.config.Source.Transport,
|
||||
Timeout: controller.config.Source.Timeout, LowLatency: controller.config.Source.LowLatency,
|
||||
}, source.Dependencies{})
|
||||
monitored, err := monitor.New(
|
||||
stream, runtime, engine, controller.config.Model.ConfidenceThreshold,
|
||||
alert.NewDispatcher(controller.config.Artifacts.EventDirectory, controller.config.Source.ID), time.Now,
|
||||
)
|
||||
if err != nil {
|
||||
runtime.Close()
|
||||
controller.window.ReportIssue("无法启动监控管线")
|
||||
return
|
||||
}
|
||||
go monitored.Run(ctx)
|
||||
updates, alerts := monitored.Updates(), monitored.Alerts()
|
||||
for updates != nil || alerts != nil {
|
||||
select {
|
||||
case update, open := <-updates:
|
||||
if !open {
|
||||
updates = nil
|
||||
continue
|
||||
}
|
||||
controller.window.Present(update)
|
||||
case record, open := <-alerts:
|
||||
if !open {
|
||||
alerts = nil
|
||||
continue
|
||||
}
|
||||
controller.window.PresentAlert(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateStartup(cfg config.Config) error {
|
||||
for _, path := range []string{cfg.Model.ONNXPath, cfg.Model.ONNXRuntimeDLLPath, cfg.Tools.FFmpegPath, cfg.Tools.FFprobePath} {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
return fmt.Errorf("必需运行文件缺失或不可读")
|
||||
}
|
||||
}
|
||||
actual, err := sha256File(cfg.Model.ONNXPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法校验 ONNX 模型")
|
||||
}
|
||||
if !strings.EqualFold(actual, cfg.Model.SHA256) {
|
||||
return fmt.Errorf("ONNX 模型哈希与配置不匹配")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sha256File(path string) (string, error) {
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
digest := sha256.New()
|
||||
if _, err := io.Copy(digest, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", digest.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func settingsFor(cfg config.Config) ui.Settings {
|
||||
return ui.Settings{
|
||||
SourceEnvironment: cfg.Source.RTSPURLEnv,
|
||||
ModelSHA256: cfg.Model.SHA256,
|
||||
RuntimeSummary: "ONNX Runtime、FFmpeg 和 FFprobe 已通过启动预检",
|
||||
EventSummary: fmt.Sprintf("确认 %.1f 秒;水平角 %.0f°;快速下移=%t;下肢=%t",
|
||||
cfg.Event.ConfirmWindowSeconds, cfg.Event.HorizontalAngleThresholdDegrees,
|
||||
cfg.Event.RequireRapidDrop, cfg.Event.RequireLowerBody),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"silverpose/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestValidateStartupRejectsMissingRuntimeWithoutLeakingSource(t *testing.T) {
|
||||
err := validateStartup(config.Config{
|
||||
Source: config.SourceConfig{URL: "rtsp://operator:secret@example/Streaming/Channels/101"},
|
||||
Model: config.ModelConfig{ONNXPath: "missing-model.onnx", SHA256: strings.Repeat("a", 64), ONNXRuntimeDLLPath: "missing-runtime.dll"},
|
||||
Tools: config.ToolConfig{FFmpegPath: "missing-ffmpeg.exe", FFprobePath: "missing-ffprobe.exe"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing runtime error")
|
||||
}
|
||||
if strings.Contains(err.Error(), "rtsp://") || strings.Contains(err.Error(), "secret") {
|
||||
t.Fatalf("startup error leaked source: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,17 @@ func (window *Window) PresentAlert(record alert.Record) {
|
||||
})
|
||||
}
|
||||
|
||||
// ReportIssue communicates a recoverable startup/worker issue without using
|
||||
// critical-red fall language or exposing configuration values.
|
||||
func (window *Window) ReportIssue(message string) {
|
||||
window.main.Synchronize(func() {
|
||||
window.model.ConnectionText = "视频流:" + message
|
||||
window.model.Critical = false
|
||||
_ = window.connectionLabel.SetText(window.model.ConnectionText)
|
||||
_ = window.stateLabel.SetText("检测状态:正常")
|
||||
})
|
||||
}
|
||||
|
||||
func highestState(result fall.FrameResult) fall.State {
|
||||
state := fall.Normal
|
||||
for _, person := range result.People {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $OnnxPath,
|
||||
[Parameter(Mandatory = $true)] [string] $OrtDllPath,
|
||||
[Parameter(Mandatory = $true)] [string] $FfmpegPath,
|
||||
[Parameter(Mandatory = $true)] [string] $FfprobePath,
|
||||
[Parameter(Mandatory = $true)] [string] $OutputDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Resolve-RequiredFile([string] $Path, [string] $Name) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Name does not exist or is not a file: $Path"
|
||||
}
|
||||
return (Resolve-Path -LiteralPath $Path).Path
|
||||
}
|
||||
|
||||
function Get-ReleaseFileInfo([string] $Path, [string] $Name) {
|
||||
$item = Get-Item -LiteralPath $Path
|
||||
return [ordered]@{
|
||||
name = $Name
|
||||
sha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
bytes = $item.Length
|
||||
}
|
||||
}
|
||||
|
||||
$onnx = Resolve-RequiredFile $OnnxPath 'ONNX model'
|
||||
$ort = Resolve-RequiredFile $OrtDllPath 'ONNX Runtime DLL'
|
||||
$ffmpeg = Resolve-RequiredFile $FfmpegPath 'FFmpeg executable'
|
||||
$ffprobe = Resolve-RequiredFile $FfprobePath 'FFprobe executable'
|
||||
$target = [System.IO.Path]::GetFullPath($OutputDirectory)
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
$existing = Get-ChildItem -LiteralPath $target -Force
|
||||
if ($existing.Count -gt 0) {
|
||||
throw "OutputDirectory must be empty: $target"
|
||||
}
|
||||
} else {
|
||||
New-Item -ItemType Directory -Path $target | Out-Null
|
||||
}
|
||||
|
||||
$scriptRoot = Split-Path -Parent $PSCommandPath
|
||||
$v2Root = Split-Path -Parent $scriptRoot
|
||||
$runtime = Join-Path $target 'runtime'
|
||||
New-Item -ItemType Directory -Path $runtime | Out-Null
|
||||
|
||||
$oldCgo = $env:CGO_ENABLED
|
||||
$env:CGO_ENABLED = '1'
|
||||
try {
|
||||
Push-Location $v2Root
|
||||
go build -trimpath -ldflags '-s -w' -o (Join-Path $target 'silver-pose.exe') ./cmd/silver-pose
|
||||
} finally {
|
||||
Pop-Location
|
||||
$env:CGO_ENABLED = $oldCgo
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $onnx -Destination (Join-Path $runtime 'best.onnx')
|
||||
Copy-Item -LiteralPath $ort -Destination (Join-Path $runtime 'onnxruntime.dll')
|
||||
Copy-Item -LiteralPath $ffmpeg -Destination (Join-Path $runtime 'ffmpeg.exe')
|
||||
Copy-Item -LiteralPath $ffprobe -Destination (Join-Path $runtime 'ffprobe.exe')
|
||||
Copy-Item -LiteralPath (Join-Path $v2Root 'config.example.json') -Destination (Join-Path $target 'config.example.json')
|
||||
Copy-Item -LiteralPath (Join-Path (Split-Path -Parent $v2Root) 'docs\v2-demo-runbook.md') -Destination (Join-Path $target 'v2-demo-runbook.md')
|
||||
|
||||
$ffmpegVersion = (& (Join-Path $runtime 'ffmpeg.exe') -version | Select-Object -First 1)
|
||||
$ffprobeVersion = (& (Join-Path $runtime 'ffprobe.exe') -version | Select-Object -First 1)
|
||||
$manifest = [ordered]@{
|
||||
format = 'silver-pose-v2-runtime-1'
|
||||
generated_at_utc = [DateTime]::UtcNow.ToString('o')
|
||||
go_version = (& go version)
|
||||
ffmpeg_version = $ffmpegVersion
|
||||
ffprobe_version = $ffprobeVersion
|
||||
files = @(
|
||||
(Get-ReleaseFileInfo (Join-Path $target 'silver-pose.exe') 'silver-pose.exe'),
|
||||
(Get-ReleaseFileInfo (Join-Path $runtime 'best.onnx') 'runtime/best.onnx'),
|
||||
(Get-ReleaseFileInfo (Join-Path $runtime 'onnxruntime.dll') 'runtime/onnxruntime.dll'),
|
||||
(Get-ReleaseFileInfo (Join-Path $runtime 'ffmpeg.exe') 'runtime/ffmpeg.exe'),
|
||||
(Get-ReleaseFileInfo (Join-Path $runtime 'ffprobe.exe') 'runtime/ffprobe.exe')
|
||||
)
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $target 'runtime-manifest.json') -Encoding UTF8
|
||||
|
||||
Write-Output "V2 release created: $target"
|
||||
Reference in New Issue
Block a user