2026-07-20 22:05:55 +08:00
|
|
|
|
# 本地模块与事件合约
|
|
|
|
|
|
|
|
|
|
|
|
> Silver Pose 没有后端 API;本文定义 V1/V2 必须保持一致的本地配置、数据和事件合约。
|
|
|
|
|
|
|
|
|
|
|
|
## 配置合约
|
|
|
|
|
|
|
|
|
|
|
|
`v1/config.example.json` 的最小形状:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"source": {
|
|
|
|
|
|
"id": "lobby-camera-01",
|
|
|
|
|
|
"rtsp_url_env": "SILVER_POSE_RTSP_URL",
|
2026-07-21 20:54:34 +08:00
|
|
|
|
"mode": "stream",
|
2026-07-20 22:05:55 +08:00
|
|
|
|
"roi_normalized": [0.0, 0.0, 1.0, 1.0]
|
|
|
|
|
|
},
|
|
|
|
|
|
"model": {
|
|
|
|
|
|
"path": "models/best.pt",
|
2026-07-21 09:45:42 +08:00
|
|
|
|
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
2026-07-20 22:05:55 +08:00
|
|
|
|
"confidence_threshold": 0.25
|
|
|
|
|
|
},
|
|
|
|
|
|
"event": {
|
|
|
|
|
|
"keypoint_confidence_threshold": 0.4,
|
|
|
|
|
|
"suspect_window_seconds": 0.5,
|
|
|
|
|
|
"confirm_window_seconds": 1.0,
|
|
|
|
|
|
"recovery_window_seconds": 2.0,
|
|
|
|
|
|
"cooldown_seconds": 10.0
|
|
|
|
|
|
},
|
|
|
|
|
|
"artifacts": {
|
|
|
|
|
|
"event_dir": "../artifacts/events"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-21 22:38:21 +08:00
|
|
|
|
- 来源二选一:`rtsp_url_env`(从同名环境变量读取真实 URL)**或**结构化字段 `host`/`port`(默认 554)/`channel`(默认 `101`,`102` 为子码流)/`username`/`password`;两者都会在内存中解析出 RTSP URL。结构化凭证只允许出现在**未跟踪的** `config.local.json`;公开示例 `config.example.json` 只能用 `rtsp_url_env`,禁止任何 `host`/`username`/`password`/完整 URL(有测试守卫)。
|
|
|
|
|
|
- 结构化模式下应用用 `build_rtsp_url` 拼接,并对用户名/密码做 RFC 3986 百分号编码,因此密码含 `@`、`:`、`/` 也不会破坏 URL 解析。
|
2026-07-21 20:54:34 +08:00
|
|
|
|
- `source.mode` 显式声明来源类型,取值 `stream`(默认,实时 RTSP,允许有界重连)或 `replay`(本地录像,EOF 不重放);由配置决定,不再按 URL 前缀猜测。
|
2026-07-21 22:56:14 +08:00
|
|
|
|
- 抓流低延迟调优(非凭证,可进公开示例):`source.transport`(`tcp` 默认 / `udp`)、`source.timeout_seconds`(默认 5)、`source.low_latency`(默认 true)。应用用 `build_ffmpeg_options` 拼成 `OPENCV_FFMPEG_CAPTURE_OPTIONS`,并在打开流之前写入 `os.environ`(OpenCV 在打开时读取该环境变量);因此无需再靠外部脚本设置该变量。
|
2026-07-21 22:38:21 +08:00
|
|
|
|
- 凭证(`host`/`username`/`password`/解析出的 URL)绝不进入 `runtime_config_version`、事件 JSONL、日志或截图文件名。
|
2026-07-21 20:54:34 +08:00
|
|
|
|
- `model.confidence_threshold` 是模型检测置信度,可在设置草稿中调整,并在下次开始监控时经 `PoseAdapter.set_confidence_threshold` 真正生效。
|
2026-07-21 11:57:22 +08:00
|
|
|
|
- 数值是待现场录像校准的默认值;每个值必须真正进入事件逻辑:`keypoint_confidence_threshold` 决定姿态质量门槛;`suspect_window_seconds` 限制快速下移到水平姿态的最大间隔;`confirm_window_seconds` 是水平倒地候选需持续的确认时间;`recovery_window_seconds` 是恢复姿态需持续的时间;`cooldown_seconds` 是确认事件后允许开始恢复判断前的最短等待时间。示例中的全零 SHA-256 只占位配置形状,T-103 必须以受控模型的真实哈希替换并验证后才能启动推理。
|
2026-07-21 23:29:16 +08:00
|
|
|
|
- 灵敏度参数(默认放宽,适配低位俯视):`require_rapid_drop`(默认 false,持续水平即可进入疑似,不再强制先检测到快速下移);`require_lower_body`(默认 false,质量门控只强制肩+髋,膝踝可缺);`horizontal_angle_threshold_degrees`(默认 45,躯干与水平夹角≤此值算水平)。放宽后由 `confirm_window_seconds` 作为主要防误报闸——弯腰、捡物不会持续水平到确认窗满。三者进入 `config_version`,可经设置调整并追溯。
|
2026-07-20 22:05:55 +08:00
|
|
|
|
- 缺少环境变量、模型不存在或哈希不符时,应用显示配置错误,不启动监控。
|
|
|
|
|
|
|
2026-07-22 21:13:03 +08:00
|
|
|
|
### V2 Go 运行配置
|
|
|
|
|
|
|
|
|
|
|
|
`v2/config.example.json` 是 V2 的公开配置形状。它保留 V1 的来源、模型、事件和工件语义,但将 V2 交付依赖显式写入 `model.onnx`、`model.ort_dll` 和 `tools.ffmpeg`/`tools.ffprobe`:
|
|
|
|
|
|
|
|
|
|
|
|
- `source` 只接受 `id`、`rtsp_url_env`、`transport`、`timeout_seconds` 和 `low_latency`。任何 `url`、`rtsp_url`、`host`、`username` 或 `password` 字段都会被拒绝,即使它们为空;实际流地址只从指定环境变量进入进程内存。
|
|
|
|
|
|
- `model.sha256` 必须是锁定 ONNX 的 64 位十六进制 SHA-256;`confidence_threshold` 是 Pose 框过滤阈值。配置相对路径相对于配置文件解析,但只在启动预检时要求实际文件存在。
|
|
|
|
|
|
- `event` 字段与 V1 的默认事件语义相同。V2 使用 `source.id`、模型 SHA、模型置信度和这些事件字段生成 `cfg-<sha256>`;URL、工具路径和工件路径均不进入该版本值。
|
|
|
|
|
|
- `artifacts.event_dir` 只保存本地 PNG/JSONL 事件证据。V2 发布脚本生成的 `runtime-manifest.json` 记录 ONNX Runtime、FFmpeg、FFprobe 和 ONNX 的来源文件哈希;示例 `runtime-manifest.example.json` 不是交付证据。
|
|
|
|
|
|
|
2026-07-20 22:05:55 +08:00
|
|
|
|
## 核心数据
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Keypoint = { x: float, y: float, confidence: float }
|
|
|
|
|
|
PersonPose = {
|
|
|
|
|
|
box_xyxy: [float, float, float, float],
|
|
|
|
|
|
box_confidence: float,
|
|
|
|
|
|
keypoints: Keypoint[17]
|
|
|
|
|
|
}
|
2026-07-21 09:53:03 +08:00
|
|
|
|
TrackedPersonPose = {
|
|
|
|
|
|
track_id: string,
|
|
|
|
|
|
detected_at_monotonic: float,
|
|
|
|
|
|
pose: PersonPose
|
|
|
|
|
|
}
|
2026-07-20 22:05:55 +08:00
|
|
|
|
PoseQuality = {
|
|
|
|
|
|
accepted: bool,
|
|
|
|
|
|
reason: string,
|
|
|
|
|
|
visible_joint_count: int
|
|
|
|
|
|
}
|
2026-07-21 09:57:00 +08:00
|
|
|
|
PoseEvidence = {
|
|
|
|
|
|
accepted: bool,
|
|
|
|
|
|
horizontal_pose: bool,
|
|
|
|
|
|
rapid_vertical_change: bool,
|
|
|
|
|
|
horizontal_angle_degrees: float | null,
|
|
|
|
|
|
hip_center_y: float | null,
|
|
|
|
|
|
torso_length: float | null,
|
|
|
|
|
|
reason: string
|
|
|
|
|
|
}
|
2026-07-20 22:05:55 +08:00
|
|
|
|
FallEvent = {
|
|
|
|
|
|
event_id: string,
|
|
|
|
|
|
track_id: string,
|
2026-07-21 11:57:22 +08:00
|
|
|
|
config_version: string,
|
2026-07-21 09:59:53 +08:00
|
|
|
|
suspected_at_monotonic: float,
|
|
|
|
|
|
confirmed_at_monotonic: float,
|
2026-07-20 22:05:55 +08:00
|
|
|
|
latency_seconds: float,
|
2026-07-21 09:59:53 +08:00
|
|
|
|
state: "CONFIRMED"
|
2026-07-20 22:05:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-21 11:57:22 +08:00
|
|
|
|
`FallEvent` 是状态机的纯内存确认事件,只在状态首次进入 `CONFIRMED` 时创建一次。连续帧更新 UI 状态,但不重复创建事件。`config_version` 是由运行配置快照计算的非敏感版本标识;T-202 的 `alerts` 会在不改变事件幂等语义的前提下,为截图/JSONL 记录补充来源、UTC 时间和证据。
|
2026-07-20 22:05:55 +08:00
|
|
|
|
|
2026-07-21 20:54:34 +08:00
|
|
|
|
`event_id` 带每次监控运行的会话前缀(`FALL-<session>-NNNNNN`),跨监控重启全局唯一;因此同一天目录内的截图不会被覆盖,`events.jsonl` 也不会出现同 id 不同内容的记录。
|
|
|
|
|
|
|
2026-07-21 19:49:20 +08:00
|
|
|
|
T-202 的 `alerts` 按 `event_id` 去重,对首次 CONFIRMED 只触发一次副作用(声音、弹窗、截图、JSONL 一行)。JSONL 记录如下,文件名与字段不含 RTSP 地址、凭证或客户姓名:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
AlertRecord(JSONL) = {
|
|
|
|
|
|
event_id: string,
|
|
|
|
|
|
track_id: string,
|
|
|
|
|
|
config_version: string,
|
|
|
|
|
|
source_id: string,
|
|
|
|
|
|
state: "CONFIRMED",
|
|
|
|
|
|
confirmed_at_utc: string, # ISO-8601 UTC
|
|
|
|
|
|
suspected_at_monotonic: float,
|
|
|
|
|
|
confirmed_at_monotonic: float,
|
|
|
|
|
|
latency_seconds: float,
|
|
|
|
|
|
screenshot: string # 相对 event_dir 的 "YYYYMMDD/FALL-xxxxxx.png"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-21 09:53:03 +08:00
|
|
|
|
`PersonPose` 是 T-103 的纯模型输出,不带人员 ID;T-104 的跟踪模块产生 `TrackedPersonPose` 后,才允许事件证据按人员连续积累。
|
|
|
|
|
|
|
2026-07-21 09:49:28 +08:00
|
|
|
|
## 视频来源帧合约
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
FramePacket = {
|
|
|
|
|
|
image: ndarray | null,
|
|
|
|
|
|
timestamp_monotonic: float,
|
|
|
|
|
|
status: "connected" | "retrying" | "error" | "eof" | "closed",
|
|
|
|
|
|
error: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-21 11:57:22 +08:00
|
|
|
|
- `SourceMode.REPLAY` 的录像优先使用容器时间戳;首帧时间戳无效或倒退时,回退为帧序号/FPS,保证回放时间单调;到达 EOF 后保持 EOF,不重放。
|
|
|
|
|
|
- `SourceMode.STREAM` 的实时流以成功读帧时的单调时钟计时;读取失败进入有界指数退避重连,不使用 `CAP_PROP_POS_MSEC` 作为事件时间。
|
2026-07-21 09:49:28 +08:00
|
|
|
|
- `retrying`、`error`、`eof` 和 `closed` 都没有图像,且绝不伪造人员、姿态或摔倒事件。
|
|
|
|
|
|
- 可重连来源以有界指数退避重新打开;断流不推进状态机的证据时间。
|
|
|
|
|
|
|
2026-07-20 22:05:55 +08:00
|
|
|
|
## 事件合约
|
|
|
|
|
|
|
|
|
|
|
|
| 事件 | 触发者 | 负载 | 结果 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| `source.connected` | 视频源 | `source_id`、时间 | UI 显示在线。 |
|
|
|
|
|
|
| `source.error` | 视频源 | `source_id`、错误码、可重试标记 | UI 显示异常;不报警。 |
|
2026-07-21 09:57:00 +08:00
|
|
|
|
| `person.updated` | 跟踪与 Pose | `TrackedPersonPose`、`PoseQuality`、`PoseEvidence` | UI 绘制骨架与 ID。 |
|
2026-07-20 22:05:55 +08:00
|
|
|
|
| `fall.suspected` | 状态机 | `track_id`、开始时间 | UI 显示黄色疑似状态。 |
|
|
|
|
|
|
| `fall.confirmed` | 状态机 | `FallEvent` | 红色叠加、声音、弹窗、截图、JSONL。 |
|
|
|
|
|
|
| `fall.recovered` | 状态机 | `track_id`、时间 | UI 恢复绿色正常状态。 |
|
|
|
|
|
|
|
|
|
|
|
|
## 副作用边界
|
|
|
|
|
|
|
|
|
|
|
|
- 仅 `alerts` 模块可写事件截图、播放声音、弹窗和追加 JSONL。
|
|
|
|
|
|
- 仅 `video_source` 模块读 RTSP 或本地视频。
|
|
|
|
|
|
- 仅 `pose` 模块加载模型;UI 不得直接调用模型。
|
|
|
|
|
|
- V2 可以改变实现语言,但不得改变配置含义、`FallEvent` 字段和事件幂等语义。
|