docs: add fall detection harness foundation
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
> Silver Pose 的仓库级 AI coding agent 入口。开始任何工作前先读本文,再从 [docs/00-ai-start-here.md](docs/00-ai-start-here.md) 进入项目上下文。
|
||||||
|
|
||||||
|
## 项目定位
|
||||||
|
|
||||||
|
Silver Pose 是面向固定机位大厅或走廊的本地摔倒事件演示系统。它从海康摄像头 RTSP 视频流中识别全身可见的人员,在同一人员从站立或行走转为持续倒地时,显示红色姿态、播放声音、弹出警报并保存事件截图。
|
||||||
|
|
||||||
|
第一版用 Python 构建并验证;第二版在 Python 行为基线通过后迁移为 Go 部署版。模型 `best.pt` 仅用于人体姿态估计,不是摔倒分类器。
|
||||||
|
|
||||||
|
## 目录和事实来源
|
||||||
|
|
||||||
|
| 路径 | 角色 | 修改纪律 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `demo/` | 当前旧 Python 演示基线、模型和样例视频 | 未经任务明确授权不得重构或覆盖 |
|
||||||
|
| `v1/` | 新 Python V1 的正式实现目录 | 当前优先开发目标 |
|
||||||
|
| `v2/` | 新 Go V2 的正式实现目录 | 仅在 V1 验收和模型导出一致性验证后开始 |
|
||||||
|
| `docs/` | 需求、架构、任务和当前状态的权威记录 | 需求或代码现实变化时必须同步 |
|
||||||
|
| `progress.md` | 只追加的执行流水 | 每轮任务完成、部分完成或阻塞均追加 |
|
||||||
|
| `D:\PythonP\fall_detection` | `best.pt` 的训练工程,只读参考 | 不在本仓库任务中改写,训练事实见文档 |
|
||||||
|
|
||||||
|
项目事实以 `docs/`、当前可运行代码和真实验证结果为准;不得以历史聊天、`__pycache__/` 或未验证的旧结果推断事实。
|
||||||
|
|
||||||
|
## 强制工作流
|
||||||
|
|
||||||
|
1. 依次阅读 `docs/00-ai-start-here.md`、`01-vision.md`、`02-requirements.md`、`03-tech-stack.md`、`04-architecture.md`、`05-coding-rules.md`、`06-tasks.md`、`current-state.md` 和 `progress.md`。
|
||||||
|
2. 运行 `./init.ps1`,再运行当前任务要求的测试。基线失败时先修基线,不领取新功能。
|
||||||
|
3. 只领取 `docs/06-tasks.md` 中第一个依赖满足的 `TODO` 任务;改为 `DOING` 后开始。
|
||||||
|
4. 完成后必须运行验收命令,把结果追加到 `progress.md`,同步任务状态和 `current-state.md`,然后停止。
|
||||||
|
5. 结束前执行 `docs/clean-state-checklist.md`。
|
||||||
|
|
||||||
|
## 安全、范围和演示表述
|
||||||
|
|
||||||
|
- 不把真实海康 RTSP 用户名、密码、内网地址、摄像头序列号或客户视频写入仓库、文档、日志和截图文件名;使用环境变量或本地未跟踪配置。
|
||||||
|
- V1 只承诺固定俯视机位、全身大部分可见的大厅/走廊演示;不得对所有公共场所、所有摄像头或医疗级可靠性作未经验证的声明。
|
||||||
|
- 正常行走、坐下、弯腰、捡物和短暂停留是必须验证的不报警反例;安全模拟摔倒必须使用经同意的成年人和现场安全措施。
|
||||||
|
- `best.pt`、ONNX 导出物和截图属于构建或运行工件,不得在没有哈希、来源和验证记录的情况下替换。
|
||||||
|
- V2 不得直接读取 `.pt`;必须从锁定的 V1 模型导出 ONNX,并通过同一录像集的关键点、事件和延迟一致性测试后才可用于演示。
|
||||||
|
|
||||||
|
## 文档与验证纪律
|
||||||
|
|
||||||
|
- 需求改变先改 `docs/02-requirements.md`;模块职责、事件字段或状态改变先改 `docs/04-architecture.md` 和 `docs/api.md`。
|
||||||
|
- 任务完成的证据是可运行命令、可观察结果和 `progress.md` 记录,绝不是“代码已写”。
|
||||||
|
- 不新增数据库、云服务、账号系统、远程告警、模型训练或重量级框架,除非当前任务和需求明确要求。
|
||||||
|
- 不删除用户已有文件、重置工作区或覆盖模型;任何潜在破坏性操作先说明目标和原因。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Silver Pose 的仓库级规则、文档入口、范围边界和验证要求以 [AGENTS.md](AGENTS.md) 为准。
|
||||||
|
|
||||||
|
开始任务时先读 [AGENTS.md](AGENTS.md),再从 [docs/00-ai-start-here.md](docs/00-ai-start-here.md) 建立上下文。不要在本文件复制或分叉规则。
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Silver Pose
|
||||||
|
|
||||||
|
Silver Pose 是一个面向固定机位大厅或走廊的本地摔倒事件演示系统。目标是向客户演示“RTSP 实时视频 → 人体姿态与人员跟踪 → 突发倒地事件 → 红色画面、声音、弹窗和事件截图”的完整闭环。
|
||||||
|
|
||||||
|
> 当前阶段是重新建立工程化 V1。旧代码保留在 `demo/` 作为可运行基线;`v1/` 将存放 Python 正式实现;`v2/` 将在 V1 验收后存放 Go 部署实现。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
silver_pose/
|
||||||
|
├── demo/ # 旧 Python 演示基线,含 best.pt 与样例视频
|
||||||
|
├── v1/ # Python V1(待任务 T-101 创建)
|
||||||
|
├── v2/ # Go V2(V1 验收后才开始)
|
||||||
|
├── docs/ # Harness Coding 文档
|
||||||
|
├── AGENTS.md # AI coding agent 入口
|
||||||
|
├── CLAUDE.md # Claude 薄入口
|
||||||
|
├── init.ps1 # Windows 统一启动与基础验证入口
|
||||||
|
└── progress.md # 只追加执行流水
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前可验证基线
|
||||||
|
|
||||||
|
在 Windows PowerShell、仓库根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./init.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
它会检查当前 Python 环境并编译 `demo/`。图形界面可用:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location demo
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
运行 GUI 或 RTSP 演示前,必须在本机准备已安装的 PyQt5、OpenCV、NumPy 与 Ultralytics 环境。真实 RTSP 凭证不得提交。
|
||||||
|
|
||||||
|
## 文档入口
|
||||||
|
|
||||||
|
所有 agent 从 [docs/00-ai-start-here.md](docs/00-ai-start-here.md) 开始。人类可从 [docs/README.md](docs/README.md) 查看完整导航。
|
||||||
|
|
||||||
|
## 模型说明
|
||||||
|
|
||||||
|
`demo/best.pt` 是与 `D:\PythonP\fall_detection` 训练工程中 `best.pt` 哈希一致的 YOLOv8 Pose 权重:1 个 `person` 类,17 个 COCO 关键点。它提供姿态,不提供“摔倒”标签;摔倒由 V1/V2 的质量门控、跟踪和时序状态机判定。
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# AI 开发入口
|
||||||
|
|
||||||
|
> 每次开始编码前先读本文件。它只负责导航和流程;硬性规则见 [05-coding-rules.md](05-coding-rules.md)。
|
||||||
|
|
||||||
|
## 一句话定位
|
||||||
|
|
||||||
|
Silver Pose 是一个在固定俯视大厅或走廊中,从海康 RTSP 视频流识别人员突发倒地事件并现场报警的本地桌面演示系统。
|
||||||
|
|
||||||
|
V1 MVP 只做:RTSP 连接、人体姿态和人员 ID、时序摔倒事件、红色姿态/声音/弹窗/截图,以及可重复的正反例验收。
|
||||||
|
|
||||||
|
## 必读顺序
|
||||||
|
|
||||||
|
1. [../AGENTS.md](../AGENTS.md)
|
||||||
|
2. [项目愿景](01-vision.md)
|
||||||
|
3. [需求](02-requirements.md)
|
||||||
|
4. [技术栈](03-tech-stack.md)
|
||||||
|
5. [架构设计](04-architecture.md)
|
||||||
|
6. [编码规则](05-coding-rules.md)
|
||||||
|
7. [任务看板](06-tasks.md)
|
||||||
|
8. [当前实现状态](current-state.md)
|
||||||
|
9. [../progress.md](../progress.md)
|
||||||
|
|
||||||
|
## 固定开工流程
|
||||||
|
|
||||||
|
1. 确认仓库根目录是 `D:\opc_project\silver_pose`。
|
||||||
|
2. 运行 `./init.ps1`;失败时先记录和修复基线,不领取新功能。
|
||||||
|
3. 只领取 [06-tasks.md](06-tasks.md) 中第一个依赖均为 `DONE` 的 `TODO` 任务,并改为 `DOING`。
|
||||||
|
4. 先写任务指定的失败测试,再做最小实现;不在同一轮领取第二个任务。
|
||||||
|
5. 完成后运行验收命令,追加 `../progress.md`,更新 `06-tasks.md` 与 `current-state.md`。
|
||||||
|
6. 过一遍 [clean-state-checklist.md](clean-state-checklist.md),汇报结果后停止。
|
||||||
|
|
||||||
|
## 当前阶段和路径
|
||||||
|
|
||||||
|
- 当前阶段:V1 工程化起步。
|
||||||
|
- Phase 0:冻结 `demo/` 基线并建立 `v1/` 可验证骨架。
|
||||||
|
- Phase 1:验证 RTSP、Pose、跟踪和时序事件的最高风险链路。
|
||||||
|
- Phase 2:完成 PyQt 实时监控与报警截图闭环。
|
||||||
|
- Phase 3:在固定录像和实时流上完成客户演示验收。
|
||||||
|
- Phase 4:导出 ONNX、验证一致性并开始 Go V2。
|
||||||
|
|
||||||
|
## MVP 范围
|
||||||
|
|
||||||
|
只做:
|
||||||
|
|
||||||
|
- 固定俯视机位、全身大部分可见的大厅/走廊。
|
||||||
|
- 单机本地 RTSP 实时监控。
|
||||||
|
- 成人安全模拟摔倒,以及行走、坐下、弯腰、捡物等不报警反例。
|
||||||
|
- 1–3 秒内的确认报警、红色姿态、声音、弹窗和事件截图。
|
||||||
|
|
||||||
|
不做:
|
||||||
|
|
||||||
|
- 多摄像头统一管理、云端平台、账号体系或远程告警。
|
||||||
|
- 医疗诊断或所有公共场景的可靠性承诺。
|
||||||
|
- V1 未验收前的 Go 功能开发。
|
||||||
|
- 未经数据与评测支持的模型重训或模型替换。
|
||||||
|
|
||||||
|
## 事实来源
|
||||||
|
|
||||||
|
- 当前旧基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||||
|
- 模型训练参考:`D:\PythonP\fall_detection\train.py`、`convert_coco_to_yolo.py`、`runs\pose\coco-pose2\results.csv`。
|
||||||
|
- 产品范围和验收:本 `docs/` 目录。
|
||||||
|
- 实际 RTSP 地址与客户视频:仅本机未跟踪配置,不能提交。
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./init.ps1
|
||||||
|
python -m compileall -q demo
|
||||||
|
# T-101 后以 docs/03-tech-stack.md 的 v1 验证命令为准
|
||||||
|
```
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 项目愿景
|
||||||
|
|
||||||
|
## 核心目标
|
||||||
|
|
||||||
|
Silver Pose 要解决的是客户无法直观看到“姿态模型能否可靠形成摔倒报警事件”的问题。
|
||||||
|
|
||||||
|
> 让现场演示人员在固定机位大厅或走廊中,看到从实时视频、人体姿态和人员 ID,到突发倒地后的红色画面、声音、弹窗和事件截图的完整闭环。
|
||||||
|
|
||||||
|
它不是医疗诊断系统,也不是面向所有监控环境的通用安防平台;它首先是一个可被客户复现、可解释、可验证的本地摔倒事件演示系统。
|
||||||
|
|
||||||
|
## 目标用户
|
||||||
|
|
||||||
|
- **售前演示人员**:需要在客户现场稳定展示完整报警闭环。
|
||||||
|
- **客户现场负责人**:需要理解系统的适用范围、报警证据和截图留存。
|
||||||
|
- **研发与交付人员**:需要可复现的录像、配置和验收标准,避免以单帧偶然结果交付。
|
||||||
|
|
||||||
|
## 产品原则
|
||||||
|
|
||||||
|
- **事件优先**:识别“突然倒地且持续”,不把静态低姿态直接称为摔倒。
|
||||||
|
- **可解释优先**:每次报警都能显示人员 ID、姿态、状态和截图证据。
|
||||||
|
- **真实验证优先**:用 RTSP 或现场录像验证,不用未标注样例替代验收。
|
||||||
|
- **最小闭环优先**:先完成单机、单流、固定机位,不扩展云端或多摄像头。
|
||||||
|
- **语言迁移受证据约束**:Go V2 必须复现已验收的 Python 行为,而不是重新发明算法。
|
||||||
|
|
||||||
|
## 核心价值
|
||||||
|
|
||||||
|
| 价值点 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 可感知的报警 | 客户能同时看到视频红色标注、声音、弹窗和截图。 |
|
||||||
|
| 可解释的判断 | 报警来自同一 ID 的质量、姿态变化与持续倒地证据。 |
|
||||||
|
| 可复现的交付 | 固定录像、配置版本、事件结果和回归测试可反复运行。 |
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不承诺任意摄像头角度、严重遮挡、仅半身可见或所有公共场所的可靠识别。
|
||||||
|
- 不把 `best.pt` 的人体 Pose 指标表述为摔倒事件准确率。
|
||||||
|
- 不在 V1 前引入云服务、远程通知、账号体系、多机管理或模型重训。
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 需求
|
||||||
|
|
||||||
|
> 本文描述用户要获得什么以及如何验收;实现细节见 [04-architecture.md](04-architecture.md)。
|
||||||
|
|
||||||
|
## 业务现状
|
||||||
|
|
||||||
|
| 项 | 当前事实 |
|
||||||
|
| --- | --- |
|
||||||
|
| 用户 | 售前人员需要在客户现场证明系统能实时发现突发倒地并给出可见报警。 |
|
||||||
|
| 摄像头 | 海康网络摄像头,可提供 RTSP 流。 |
|
||||||
|
| 场地 | 固定机位大厅或走廊;约 1.6 米安装高度、向下俯视。 |
|
||||||
|
| 可见性 | 演示区要求人员从站立到倒地后头、躯干、膝盖和脚踝大多在画面内。 |
|
||||||
|
| 模型 | `demo/best.pt` 为 17 点人体姿态模型,不带摔倒类别。 |
|
||||||
|
| 演示动作 | 可安排成年人安全模拟摔倒,并安排行走、坐下、弯腰、捡物等反例。 |
|
||||||
|
| 时延 | 从开始倒地到报警的目标是 1–3 秒。 |
|
||||||
|
| 合规 | 不保存真实 RTSP 凭证;演示人员与视频采集须获现场同意。 |
|
||||||
|
|
||||||
|
## 用户角色
|
||||||
|
|
||||||
|
- **演示操作员**:配置本机摄像头,开始和停止监控,向客户解释事件。
|
||||||
|
- **客户观察者**:观看实时画面、报警和截图,了解适用边界。
|
||||||
|
- **研发人员**:通过录像与测试验证模型、事件逻辑和迁移一致性。
|
||||||
|
|
||||||
|
## 第一版 MVP
|
||||||
|
|
||||||
|
| 功能 | 用户获得的结果 | 优先级 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| RTSP 实时监控 | 操作员可连接一个已配置的海康 RTSP 流并看到实时画面及连接状态。 | P0 |
|
||||||
|
| 姿态与人员跟踪 | 全身可见人员显示骨架和稳定人员 ID;质量不足时明确为不确定而非报警。 | P0 |
|
||||||
|
| 突发倒地事件 | 同一人员从正常姿态快速转为倒地且持续时,在 1–3 秒内确认事件。 | P0 |
|
||||||
|
| 本地报警 | 确认事件后画面变红、播放声音、弹出窗口并保存标注截图。 | P0 |
|
||||||
|
| 演示回归 | 同一组正反例录像可重复验证报警、无报警和延迟。 | P0 |
|
||||||
|
|
||||||
|
## 后续迭代
|
||||||
|
|
||||||
|
| 功能 | 描述 | 阶段 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Go 部署版 | 用 Go 复现 V1 的 RTSP、ONNX、事件与报警行为。 | V2 |
|
||||||
|
| ONNX 一致性门槛 | 对相同录像验证 Python 与 Go 的关键点、事件和时延。 | V2 |
|
||||||
|
| 多摄像头与远程通知 | 多路管理、云端事件中心、远程告警。 | V3 |
|
||||||
|
| 特定场景模型适配 | 用海康俯视数据评测后再决定微调 Pose 或训练时序分类器。 | V3 |
|
||||||
|
|
||||||
|
## 核心用户故事
|
||||||
|
|
||||||
|
1. 作为演示操作员,我可以从本机安全配置中选择一个海康 RTSP 摄像头并开始预览。
|
||||||
|
2. 我可以看到每名全身可见人员的骨架、ID 与当前状态,而不是只有一帧“摔倒/正常”文字。
|
||||||
|
3. 当成年人安全模拟从站立或行走突然倒地时,系统在 1–3 秒内对该人员显示红色状态、播放声音、弹窗并保存截图。
|
||||||
|
4. 当人员坐下、弯腰、捡物或短暂停留时,系统不把该动作确认成摔倒事件。
|
||||||
|
5. 当同一人员持续倒地时,系统保持事件状态但不重复弹窗;恢复稳定站立后可回到正常状态。
|
||||||
|
|
||||||
|
## MVP 验收标准
|
||||||
|
|
||||||
|
- **RTSP**:有效流能在启动后显示画面;断流后在界面和日志中显示连接异常,不把断流误报为摔倒。
|
||||||
|
- **可见人员**:同一人员连续出现时保持同一 ID;关键关节质量不足时不进入确认报警。
|
||||||
|
- **突发摔倒**:每段经过批准的正例录像或现场动作,报警延迟记录在 1–3 秒范围内。
|
||||||
|
- **反例**:每段坐下、弯腰、捡物和短暂停留录像均不产生确认事件。
|
||||||
|
- **报警闭环**:每个确认事件只产生一次声音和弹窗,并保存一张含时间、ID、骨架和事件状态的截图。
|
||||||
|
- **可复现性**:验收录像、配置版本、事件结果和实际命令保存在项目约定位置或 `progress.md`,可在同一机器重复运行。
|
||||||
|
|
||||||
|
## 范围与风险
|
||||||
|
|
||||||
|
| 问题 | 决策 |
|
||||||
|
| --- | --- |
|
||||||
|
| V1 平台 | Windows 本地 PyQt5 桌面应用。 |
|
||||||
|
| 数据持久化 | 不建数据库;事件截图和 JSONL 日志保存在本机目录。 |
|
||||||
|
| 摄像头数量 | V1 单路 RTSP;多路管理不在范围内。 |
|
||||||
|
| 模型 | 使用已验证的 `best.pt` 或同一录像评测后选定的 Pose 权重。 |
|
||||||
|
| 报警方式 | 画面变红、Windows 本地声音、弹窗、截图;不含短信、电话、云端通知。 |
|
||||||
|
| V2 开始条件 | V1 事件级验收通过,且导出 ONNX 与 Python 输出完成一致性验证。 |
|
||||||
|
|
||||||
|
- 俯视角、远距离或遮挡会降低膝踝关键点可靠性;必须先以现场录像验证。
|
||||||
|
- `best.pt` 的 COCO Pose 指标不是摔倒事件指标;不能据此承诺公共场所全场景效果。
|
||||||
|
- 人员安全模拟摔倒要由现场负责人决定安全方式;系统演示不要求真实无保护跌倒。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 技术栈
|
||||||
|
|
||||||
|
> 本文是唯一的选型与命令速查表;模块职责见 [04-architecture.md](04-architecture.md)。
|
||||||
|
|
||||||
|
## V1:Python 验证版
|
||||||
|
|
||||||
|
| 维度 | 选型 | 状态 | 理由 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 运行时 | CPython 3.8.10 | 已验证 | 当前环境可导入现有依赖;升级 Python 需单独任务验证。 |
|
||||||
|
| 桌面 UI | PyQt5 | 已验证 | 现有 GUI 已使用,适合实时画面、弹窗和信号线程。 |
|
||||||
|
| 视频与图像 | OpenCV(含 FFmpeg) | 已验证 | 读取 RTSP、绘制叠加层和保存截图。 |
|
||||||
|
| Pose 推理 | Ultralytics 8.3.205 + YOLOv8 Pose | 已验证 | 当前 `best.pt` 能加载为 17 点 person Pose 模型。 |
|
||||||
|
| 跟踪 | Ultralytics ByteTrack | 已定 | 为时序事件提供人员 ID;实现前需用现场录像验证 ID 稳定性。 |
|
||||||
|
| 事件逻辑 | 纯 Python 模块 + pytest | 已定 | 与 GUI 解耦,便于正反例录像回归。 |
|
||||||
|
| 本地配置 | JSON 示例文件 + 未跟踪的 `config.local.json` | 已定 | 不把 RTSP 凭证提交仓库。 |
|
||||||
|
| 日志与截图 | JSONL + PNG/JPG 文件 | 已定 | 无数据库的可审计演示证据。 |
|
||||||
|
| 声音 | Windows `winsound` | 已定 | 不增加额外音频依赖。 |
|
||||||
|
| 测试 | pytest、compileall、录像回放 smoke | 已定 | 覆盖纯逻辑与可观察演示行为。 |
|
||||||
|
|
||||||
|
## V2:Go 部署版
|
||||||
|
|
||||||
|
| 维度 | 选型 | 状态 | 理由 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 语言 | Go | 已定 | 面向长时间 RTSP 运行、并发控制和单一交付物。 |
|
||||||
|
| 模型工件 | 固定输入尺寸的 ONNX Pose 模型 | 已定 | Go 不直接加载 `.pt`;模型由已验收 V1 权重导出并记录哈希。 |
|
||||||
|
| 推理运行时 | ONNX Runtime 或经 Spike 验证的等价 Go 运行时 | 待 T-302 决定 | 选择标准是 Windows 打包、Pose 输出、CPU/GPU 与持续运行稳定性。 |
|
||||||
|
| 视频输入与绘制 | Go 方案待 T-302 Spike 决定 | 待定 | 必须读取 RTSP、保留 BGR/RGB 与 letterbox 一致性,并满足现场显示。 |
|
||||||
|
| 演示 UI | 本地网页壳或轻量桌面壳待 T-302 Spike 决定 | 待定 | 不在未验证视频帧展示能力前锁定 UI 框架。 |
|
||||||
|
| 测试 | `go test ./...` + V1 回归录像 | 已定 | V2 行为必须与 V1 基线对照。 |
|
||||||
|
|
||||||
|
## 关键选型纪律
|
||||||
|
|
||||||
|
- V1 不引入新模型、数据库、云服务或重量级告警依赖。
|
||||||
|
- RTSP 凭证仅来自环境变量或未跟踪本地文件;示例配置只含变量名。
|
||||||
|
- V2 的 ONNX 预处理、NMS、关键点解析、坐标还原和阈值必须与 V1 录像基线一致。
|
||||||
|
- 如果 V2 需要 GPU 专用引擎,必须记录显卡、驱动、运行时版本和回退 CPU 路径;不能将硬件绑定伪装成通用稳定性。
|
||||||
|
|
||||||
|
## 当前可运行命令
|
||||||
|
|
||||||
|
| 用途 | 命令 |
|
||||||
|
| --- | --- |
|
||||||
|
| 基线检查 | `./init.ps1` |
|
||||||
|
| 旧代码编译检查 | `python -m compileall -q demo` |
|
||||||
|
| 旧 GUI 启动 | `Set-Location demo; python main.py` |
|
||||||
|
| V1 测试 | T-101 创建 `v1/requirements.txt` 和测试后写入真实 `pytest` 命令 |
|
||||||
|
| V2 测试 | T-302 创建 Go 模块后使用 `go test ./...` |
|
||||||
|
|
||||||
|
## 演进决策
|
||||||
|
|
||||||
|
- 先保持 Python 3.8.10 与现有依赖,避免为了升级环境打断演示基线;V1 验收后再评估支持期限与升级。
|
||||||
|
- `best.pt` 是 Pose 基线,不是默认永久模型;只有同一现场录像的量化比较能决定是否改用官方权重、提高输入尺寸或做数据适配。
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 架构设计
|
||||||
|
|
||||||
|
## 系统结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
海康 RTSP 流(凭证仅在本机)
|
||||||
|
↓
|
||||||
|
RTSP 采集器 / 帧时间戳
|
||||||
|
↓
|
||||||
|
Pose 推理适配器(person box + 17 keypoints)
|
||||||
|
↓
|
||||||
|
人员跟踪器(track_id)
|
||||||
|
↓
|
||||||
|
姿态质量门控 + 倒地证据提取
|
||||||
|
↓
|
||||||
|
每人摔倒事件状态机
|
||||||
|
↓
|
||||||
|
确认事件
|
||||||
|
├── PyQt 实时画面:红框、骨架、状态
|
||||||
|
├── 本地声音与弹窗
|
||||||
|
├── 标注截图
|
||||||
|
└── JSONL 事件日志
|
||||||
|
```
|
||||||
|
|
||||||
|
V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一配置、事件字段和录像集,但将 Pose 推理替换为 ONNX 适配器;不得改变事件语义。
|
||||||
|
|
||||||
|
## 模块职责
|
||||||
|
|
||||||
|
| 模块 | V1 位置 | 职责 | 不负责 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 应用入口 | `v1/app.py` | 装配配置、窗口、线程和依赖 | 推理细节、事件判定 |
|
||||||
|
| 配置 | `v1/config.py` | 解析示例和本地配置,校验非敏感字段 | 保存真实凭证 |
|
||||||
|
| 视频源 | `v1/video_source.py` | 打开、读取、重连 RTSP 或录像,附带时间戳 | Pose、报警 |
|
||||||
|
| Pose 适配器 | `v1/pose.py` | 统一返回 box、关键点、置信度 | 跟踪、摔倒业务结论 |
|
||||||
|
| 跟踪 | `v1/tracking.py` | 为连续人员输出 `track_id` | 根据姿态报警 |
|
||||||
|
| 质量与证据 | `v1/evidence.py` | 过滤低质量点,计算水平姿态、下移和持续性证据 | GUI 状态 |
|
||||||
|
| 状态机 | `v1/fall_state.py` | 管理每个 ID 的 NORMAL、SUSPECT、CONFIRMED、RECOVERING | 播放声音或存文件 |
|
||||||
|
| 报警工件 | `v1/alerts.py` | 对确认事件去重、播放声音、保存截图、写日志 | 推理或事件计算 |
|
||||||
|
| PyQt UI | `v1/gui.py` | 渲染帧、骨架、状态、设置和弹窗 | 直接读 RTSP 或写判定规则 |
|
||||||
|
| 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI |
|
||||||
|
|
||||||
|
## 状态模型
|
||||||
|
|
||||||
|
每个 `track_id` 独立维护状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NORMAL
|
||||||
|
└─ 高质量证据显示快速下移或倒地姿态 → SUSPECT
|
||||||
|
SUSPECT
|
||||||
|
├─ 倒地证据在配置时间窗内持续 → CONFIRMED(产生一次 FallEvent)
|
||||||
|
└─ 证据消失 → NORMAL
|
||||||
|
CONFIRMED
|
||||||
|
└─ 经配置冷却与恢复稳定站立 → RECOVERING
|
||||||
|
RECOVERING
|
||||||
|
├─ 恢复证据持续 → NORMAL
|
||||||
|
└─ 再次倒地证据 → SUSPECT
|
||||||
|
```
|
||||||
|
|
||||||
|
状态机只以秒和单调时间为准,不以固定帧数为准。这样 15 FPS、30 FPS、丢帧或录像回放速度变化不会改变 1–3 秒业务目标。
|
||||||
|
|
||||||
|
## 数据和文件
|
||||||
|
|
||||||
|
| 数据 | 位置 | 规则 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 公共配置示例 | `v1/config.example.json` | 可提交;只含 `rtsp_url_env`,不含 URL 或密码。 |
|
||||||
|
| 本地配置 | `v1/config.local.json` | 忽略提交;可引用环境变量。 |
|
||||||
|
| V1 模型 | `v1/models/best.pt` 或受控相对路径 | 记录 SHA-256、来源与验证日期。 |
|
||||||
|
| V2 模型 | `v2/assets/best.onnx` | 从锁定 V1 权重导出,记录导出命令、输入尺寸和 SHA-256。 |
|
||||||
|
| 事件截图 | `artifacts/events/YYYYMMDD/` | 文件名含事件 ID,不含客户姓名或 RTSP 地址。 |
|
||||||
|
| 事件日志 | `artifacts/events/YYYYMMDD/events.jsonl` | 一行一个 `FallEvent`。 |
|
||||||
|
| 回归录像 | `testdata/videos/` | 只保存经同意的演示素材;外部客户视频不提交。 |
|
||||||
|
| 回归标签 | `testdata/expected_events.json` | 每段录像的期望事件、非事件和最大延迟。 |
|
||||||
|
|
||||||
|
## 关键风险与应对
|
||||||
|
|
||||||
|
| 风险 | 原因 | 应对 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 单帧误报 | 当前 `demo/` 任一规则命中即报警 | V1 用质量门控、跟踪和时序状态机;反例录像必测。 |
|
||||||
|
| 俯视关键点不稳 | 1.6 米俯视、遮挡或远距离会影响膝踝 | 全身可见前提、ROI、质量拒绝和现场回归;必要时再采集数据。 |
|
||||||
|
| RTSP 断流 | 网络抖动或摄像头重连 | 视频源显式状态、退避重连、断流不报警。 |
|
||||||
|
| 同一事件重复报警 | CONFIRMED 状态持续多帧 | 每个事件 ID 仅执行一次报警副作用,恢复后才允许新事件。 |
|
||||||
|
| Go 行为漂移 | ONNX 预后处理与 Python 不同 | 导出后跑同一录像,比较关键点、事件数量、确认时间和截图。 |
|
||||||
|
| 模型误解 | Pose 指标被误当摔倒指标 | 文案仅说明姿态模型;事件级指标单独记录。 |
|
||||||
|
|
||||||
|
## 开发顺序
|
||||||
|
|
||||||
|
1. 冻结 `demo/`、创建 V1 配置和可验证骨架。
|
||||||
|
2. 用本地录像打通 Pose、质量门控、跟踪和状态机。
|
||||||
|
3. 加入 PyQt 实时画面、RTSP 重连、报警和截图。
|
||||||
|
4. 用正反例录像和现场流完成 V1 事件级验收。
|
||||||
|
5. 锁定模型并导出 ONNX,完成 Python/ONNX 一致性。
|
||||||
|
6. 由 Go V2 重复相同行为后,再作为客户演示部署版。
|
||||||
|
|
||||||
|
## 目标目录
|
||||||
|
|
||||||
|
```text
|
||||||
|
v1/
|
||||||
|
├── app.py
|
||||||
|
├── config.py
|
||||||
|
├── config.example.json
|
||||||
|
├── gui.py
|
||||||
|
├── video_source.py
|
||||||
|
├── pose.py
|
||||||
|
├── tracking.py
|
||||||
|
├── evidence.py
|
||||||
|
├── fall_state.py
|
||||||
|
├── alerts.py
|
||||||
|
├── models/
|
||||||
|
├── scripts/
|
||||||
|
└── tests/
|
||||||
|
v2/
|
||||||
|
├── cmd/silver-pose/
|
||||||
|
├── internal/
|
||||||
|
├── assets/
|
||||||
|
└── testdata/
|
||||||
|
```
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 编码规则
|
||||||
|
|
||||||
|
> 每次动代码前先读完本文件,并以 [需求](02-requirements.md)、[技术栈](03-tech-stack.md) 和 [架构设计](04-architecture.md) 为事实约束。
|
||||||
|
|
||||||
|
## 黄金法则
|
||||||
|
|
||||||
|
1. 不臆造摄像头、模型、数据集、事件字段或指标;不确定就检查或询问。
|
||||||
|
2. 一轮只完成一个任务;不顺手提前实现 V2、云端、多摄像头或模型训练。
|
||||||
|
3. 推理、跟踪、事件判断、UI 和报警副作用必须按架构分离。
|
||||||
|
4. 每次实现先写失败测试,再做最小实现,再运行通过测试。
|
||||||
|
5. 完成以真实验证证据为准,并记录到 [../progress.md](../progress.md)。
|
||||||
|
|
||||||
|
## 模型与视频规则
|
||||||
|
|
||||||
|
- `best.pt` 只代表 Pose 模型;禁止把 Pose 置信度、关键点可见率或单帧规则分数命名为“摔倒概率”。
|
||||||
|
- 关键点有效性不得使用 `confidence > 0` 作为唯一标准;阈值、可见性要求和退化行为必须由配置与测试覆盖。
|
||||||
|
- 几何计算保留浮点精度;所有反余弦输入夹紧到 `[-1, 1]`;不因零向量崩溃。
|
||||||
|
- 事件确认使用单调时间和秒,不能依赖视频固定 FPS。
|
||||||
|
- 同一 `track_id` 的报警必须幂等;同一事件不得逐帧重复弹窗、播放声音或覆盖截图。
|
||||||
|
- RTSP 断流、空帧、无人员、低质量姿态是明确状态,绝不当作摔倒。
|
||||||
|
|
||||||
|
## 配置与隐私规则
|
||||||
|
|
||||||
|
- 配置中所有影响判断的阈值必须真正传入事件逻辑;UI 显示的值必须就是生效值。
|
||||||
|
- 真实 RTSP URL、用户名、密码、摄像头序列号和客户视频不得入库、不得输出到日志。
|
||||||
|
- 只提交 `config.example.json`;`config.local.json`、事件截图、ONNX/模型导出物和客户录像须由 `.gitignore` 排除。
|
||||||
|
- 保存截图前只保留演示所需的时间、ID、状态和画面,不在文件名中写个人身份信息。
|
||||||
|
|
||||||
|
## V1 / V2 迁移规则
|
||||||
|
|
||||||
|
- V2 开始前,V1 必须有通过的正反例录像回归与现场 RTSP 验收记录。
|
||||||
|
- Go 不直接加载 `.pt`;导出 ONNX 后必须锁定模型哈希、输入尺寸、预处理、NMS 和关键点格式。
|
||||||
|
- V2 只有在同一回归录像集上满足约定的事件数量、确认时间和无报警案例后才能标为可演示。
|
||||||
|
- 不以“Go 更稳定”替代压力、断流和回归证据。
|
||||||
|
|
||||||
|
## 完成前检查
|
||||||
|
|
||||||
|
- [ ] 任务验收命令通过。
|
||||||
|
- [ ] 正反例或单元测试覆盖当前改动。
|
||||||
|
- [ ] 文档、配置和事件字段与代码一致。
|
||||||
|
- [ ] 没有提交凭证、客户视频、模型大文件或运行工件。
|
||||||
|
- [ ] `06-tasks.md`、`current-state.md` 和 `progress.md` 已同步。
|
||||||
|
- [ ] 已过 [clean-state-checklist.md](clean-state-checklist.md)。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 任务看板
|
||||||
|
|
||||||
|
> 一次只领取第一个依赖满足的 `TODO`。任务标为 `DONE` 前必须有真实验证命令和 `progress.md` 证据。
|
||||||
|
|
||||||
|
状态:`TODO` 待开始 · `DOING` 进行中 · `DONE` 已验收 · `BLOCKED` 受阻
|
||||||
|
|
||||||
|
## 已完成的基线
|
||||||
|
|
||||||
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| T-000 | 建立 Harness Coding 文档与旧基线快照 | - | `demo/` 编译通过;模型任务与关键点格式已检查;文档记录真实范围与下一步。 | DONE |
|
||||||
|
|
||||||
|
## Phase 1 · Python V1 地基与最高风险
|
||||||
|
|
||||||
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| T-101 | 创建 V1 包、依赖清单、示例配置和忽略规则 | T-000 | `v1/` 可导入;真实 RTSP 凭证被拒绝提交;`pytest` 能运行。 | TODO |
|
||||||
|
| T-102 | 实现可重连的视频源与录像回放适配器 | T-101 | 有效本地录像可按时间戳产帧;无效源进入连接错误状态且不崩溃。 | TODO |
|
||||||
|
| T-103 | 实现 Pose 适配器与模型来源校验 | T-102 | 输出 person box、17 点和置信度;错误模型或哈希不符时给出明确错误。 | TODO |
|
||||||
|
| T-104 | 实现人员跟踪与姿态质量门控 | T-103 | 连续人员维持 ID;低质量、缺失膝踝或空帧不会产生倒地候选。 | TODO |
|
||||||
|
| T-105 | 实现按 ID 的时序摔倒状态机 | T-104 | 正例在配置秒数内确认;坐下、弯腰、短时低姿态回到 NORMAL;事件副作用只触发一次。 | TODO |
|
||||||
|
|
||||||
|
## Phase 2 · V1 演示闭环
|
||||||
|
|
||||||
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| T-201 | 实现 PyQt 实时监控界面与状态叠加 | T-105 | 显示视频、骨架、ID、NORMAL/SUSPECT/CONFIRMED 和连接状态;UI 不直接执行事件计算。 | TODO |
|
||||||
|
| T-202 | 实现本地报警、弹窗、截图和 JSONL 日志 | T-201 | CONFIRMED 产生一次声音、一次弹窗、一张带标注截图和一条事件日志。 | TODO |
|
||||||
|
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | TODO |
|
||||||
|
| T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | TODO |
|
||||||
|
| T-205 | 固化客户演示脚本与 V1 发布包 | T-204 | 可按文档完成演示脚本,所有证据可复查,适用边界显示给操作员。 | TODO |
|
||||||
|
|
||||||
|
## Phase 3 · Go V2 迁移门槛与实现
|
||||||
|
|
||||||
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| T-301 | 锁定 V1 模型并导出 ONNX 一致性工件 | T-205 | 记录 `.pt` 与 ONNX 哈希、输入尺寸、导出命令;同一帧关键点与 Python 在约定容差内。 | TODO |
|
||||||
|
| T-302 | 完成 Go 推理与视频/UI 技术 Spike | T-301 | Windows 上可读取录像和 ONNX Pose;选定运行时与 UI 方案并记录取舍。 | TODO |
|
||||||
|
| T-303 | 实现 Go 的事件引擎与 V1 回归对比 | T-302 | 同一录像的确认事件、无报警案例和延迟满足 V1 基线。 | TODO |
|
||||||
|
| T-304 | 完成 Go 实时演示、报警和打包 | T-303 | 实时 RTSP、红色画面、声音、弹窗和截图可运行;新环境按文档启动。 | TODO |
|
||||||
|
|
||||||
|
## Backlog
|
||||||
|
|
||||||
|
- V3 多摄像头管理、远程通知和事件中心。
|
||||||
|
- 海康俯视场景数据评测后决定 Pose 微调或时序模型训练。
|
||||||
|
- GPU / TensorRT 等硬件专用优化,仅在 V2 CPU 结果不能满足验收时评估。
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
# Silver Pose V1 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SKILL: Use `subagent-driven-development` (recommended) or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a Python desktop V1 that detects a confirmed sudden-fall event from one RTSP or replayed video source and produces red overlay, sound, popup, and screenshot evidence within the agreed 1–3 second target.
|
||||||
|
|
||||||
|
**Architecture:** V1 keeps video acquisition, Pose inference, tracking, pose-quality evidence, per-person temporal state, UI rendering, and alert side effects in separate modules. All decisions are driven by one validated configuration object; the UI receives state and events but never decides a fall.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.8.10, PyQt5, OpenCV, Ultralytics 8.3.205, ByteTrack, pytest, JSON/JSONL, Windows winsound.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `v1/config.py` | Typed configuration and environment-variable resolution. |
|
||||||
|
| `v1/video_source.py` | Replay/RTSP frame source, timestamps, connection state and reconnect policy. |
|
||||||
|
| `v1/pose.py` | YOLO Pose adapter and model fingerprint verification. |
|
||||||
|
| `v1/tracking.py` | Stable person IDs. |
|
||||||
|
| `v1/evidence.py` | Pose-quality gate and scale-independent fall evidence. |
|
||||||
|
| `v1/fall_state.py` | Deterministic per-ID state machine and `FallEvent`. |
|
||||||
|
| `v1/alerts.py` | Idempotent sound, popup request, screenshot and JSONL artifact work. |
|
||||||
|
| `v1/gui.py` | PyQt display, controls and event presentation. |
|
||||||
|
| `v1/tests/` | Pure logic and replay tests. |
|
||||||
|
| `v1/scripts/replay_cases.py` | Replay labelled videos and emit event report. |
|
||||||
|
|
||||||
|
### Task 1: T-101 Create the V1 package, secure configuration and test baseline
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/__init__.py`
|
||||||
|
- Create: `v1/config.py`
|
||||||
|
- Create: `v1/config.example.json`
|
||||||
|
- Create: `v1/requirements.txt`
|
||||||
|
- Create: `v1/tests/test_config.py`
|
||||||
|
- Modify: `.gitignore`
|
||||||
|
- Modify: `init.ps1`, `docs/03-tech-stack.md`, `docs/current-state.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing configuration test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from v1.config import load_config
|
||||||
|
|
||||||
|
def test_load_config_resolves_rtsp_environment_variable(tmp_path, monkeypatch):
|
||||||
|
config_file = tmp_path / "config.json"
|
||||||
|
config_file.write_text(
|
||||||
|
'{"source":{"id":"cam","rtsp_url_env":"SILVER_POSE_RTSP_URL"},'
|
||||||
|
'"model":{"path":"models/best.pt","sha256":"abc","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"}}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("SILVER_POSE_RTSP_URL", "rtsp://example")
|
||||||
|
assert load_config(config_file).source_url == "rtsp://example"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_config.py -v`
|
||||||
|
Expected: FAIL because `v1.config` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the smallest configuration interface**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class ConfigError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EventConfig:
|
||||||
|
keypoint_confidence_threshold: float
|
||||||
|
suspect_window_seconds: float
|
||||||
|
confirm_window_seconds: float
|
||||||
|
recovery_window_seconds: float
|
||||||
|
cooldown_seconds: float
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AppConfig:
|
||||||
|
source_id: str
|
||||||
|
source_url: str
|
||||||
|
model_path: Path
|
||||||
|
model_sha256: str
|
||||||
|
confidence_threshold: float
|
||||||
|
event: EventConfig
|
||||||
|
event_dir: Path
|
||||||
|
|
||||||
|
def load_config(path: Path) -> AppConfig:
|
||||||
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
url = os.environ.get(raw["source"]["rtsp_url_env"])
|
||||||
|
if not url:
|
||||||
|
raise ConfigError("missing RTSP environment variable")
|
||||||
|
event_raw = raw["event"]
|
||||||
|
return AppConfig(
|
||||||
|
source_id=raw["source"]["id"],
|
||||||
|
source_url=url,
|
||||||
|
model_path=Path(raw["model"]["path"]),
|
||||||
|
model_sha256=raw["model"]["sha256"],
|
||||||
|
confidence_threshold=float(raw["model"]["confidence_threshold"]),
|
||||||
|
event=EventConfig(
|
||||||
|
keypoint_confidence_threshold=float(event_raw["keypoint_confidence_threshold"]),
|
||||||
|
suspect_window_seconds=float(event_raw["suspect_window_seconds"]),
|
||||||
|
confirm_window_seconds=float(event_raw["confirm_window_seconds"]),
|
||||||
|
recovery_window_seconds=float(event_raw["recovery_window_seconds"]),
|
||||||
|
cooldown_seconds=float(event_raw["cooldown_seconds"]),
|
||||||
|
),
|
||||||
|
event_dir=Path(raw["artifacts"]["event_dir"]),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.local.json`, `artifacts/`, `testdata/private/`, `*.onnx` and camera credentials must be ignored; `config.example.json` contains only `rtsp_url_env`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run baseline tests and update the standard command**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests -v`
|
||||||
|
Expected: PASS. Update `init.ps1` so it installs `v1/requirements.txt` and runs this command.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add .gitignore init.ps1 v1 docs/03-tech-stack.md docs/current-state.md docs/06-tasks.md progress.md
|
||||||
|
git commit -m "feat(v1): add secure configuration baseline"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: T-102 Implement replayable frame acquisition before RTSP
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/video_source.py`
|
||||||
|
- Create: `v1/tests/test_video_source.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write replay and broken-source tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_file_source_emits_monotonic_timestamps(sample_video):
|
||||||
|
source = VideoSource(sample_video, reconnect=False)
|
||||||
|
first = source.read()
|
||||||
|
second = source.read()
|
||||||
|
assert first.timestamp_monotonic < second.timestamp_monotonic
|
||||||
|
|
||||||
|
def test_missing_source_returns_error_state(tmp_path):
|
||||||
|
source = VideoSource(tmp_path / "missing.mp4", reconnect=False)
|
||||||
|
assert source.read().status == SourceStatus.ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_video_source.py -v`
|
||||||
|
Expected: FAIL because `VideoSource` and `SourceStatus` do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the source contract**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class SourceStatus(str, Enum):
|
||||||
|
CONNECTED = "connected"
|
||||||
|
RETRYING = "retrying"
|
||||||
|
ERROR = "error"
|
||||||
|
EOF = "eof"
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FramePacket:
|
||||||
|
image: Optional[np.ndarray]
|
||||||
|
timestamp_monotonic: float
|
||||||
|
status: SourceStatus
|
||||||
|
error: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
`read()` returns an error packet for failures; it never emits a synthetic person or fall event. For RTSP, reconnect with bounded backoff from configuration.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_video_source.py -v`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1/video_source.py v1/tests/test_video_source.py docs progress.md
|
||||||
|
git commit -m "feat(v1): add replayable video source"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: T-103 Add a verified Pose adapter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/pose.py`
|
||||||
|
- Create: `v1/tests/test_pose.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write adapter shape tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_pose_adapter_rejects_non_pose_model(tmp_path):
|
||||||
|
with pytest.raises(ModelValidationError):
|
||||||
|
PoseAdapter(tmp_path / "not-a-pose-model.pt", expected_sha256="abc")
|
||||||
|
|
||||||
|
def test_person_pose_has_seventeen_keypoints(fake_yolo_result):
|
||||||
|
poses = PoseAdapter.from_results(fake_yolo_result)
|
||||||
|
assert len(poses[0].keypoints) == 17
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_pose.py -v`
|
||||||
|
Expected: FAIL because `PoseAdapter` is missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement only the adapter contract**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Sequence, Tuple
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PersonPose:
|
||||||
|
box_xyxy: Tuple[float, float, float, float]
|
||||||
|
box_confidence: float
|
||||||
|
keypoints: Sequence[Keypoint]
|
||||||
|
|
||||||
|
class PoseAdapter:
|
||||||
|
def infer(self, image: np.ndarray) -> Sequence[PersonPose]:
|
||||||
|
results = self._model(image, conf=self._confidence, verbose=False)
|
||||||
|
return self.from_results(results[0])
|
||||||
|
```
|
||||||
|
|
||||||
|
At construction, hash the model, require task `pose`, class `person`, and exactly 17 three-value keypoints.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests and a real model smoke**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_pose.py -v; python -c "from ultralytics import YOLO; assert YOLO('demo/best.pt').task == 'pose'"`
|
||||||
|
Expected: PASS and no assertion error.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1/pose.py v1/tests/test_pose.py docs progress.md
|
||||||
|
git commit -m "feat(v1): add verified pose adapter"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: T-104 Build quality gating and tracking
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/evidence.py`
|
||||||
|
- Create: `v1/tracking.py`
|
||||||
|
- Create: `v1/tests/test_evidence.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write quality tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_missing_ankles_rejects_pose(person_pose_without_ankles):
|
||||||
|
quality = assess_pose_quality(person_pose_without_ankles, threshold=0.4)
|
||||||
|
assert quality.accepted is False
|
||||||
|
assert quality.reason == "required_joint_low_confidence"
|
||||||
|
|
||||||
|
def test_horizontal_body_is_evidence_not_event(horizontal_pose):
|
||||||
|
evidence = extract_evidence(horizontal_pose, previous=None)
|
||||||
|
assert evidence.horizontal_pose is True
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_evidence.py -v`
|
||||||
|
Expected: FAIL because quality and evidence functions are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement floating-point evidence**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def assess_pose_quality(pose: PersonPose, threshold: float) -> PoseQuality:
|
||||||
|
required = (5, 6, 11, 12, 13, 14, 15, 16)
|
||||||
|
if any(pose.keypoints[index].confidence < threshold for index in required):
|
||||||
|
return PoseQuality(False, "required_joint_low_confidence", 0)
|
||||||
|
return PoseQuality(True, "accepted", len(required))
|
||||||
|
```
|
||||||
|
|
||||||
|
Use float coordinates, clamped cosine inputs, torso-normalized vertical motion, and no boolean alarm result in this module.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_evidence.py -v`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1/evidence.py v1/tracking.py v1/tests/test_evidence.py docs progress.md
|
||||||
|
git commit -m "feat(v1): add pose quality evidence"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: T-105 Implement the deterministic per-person fall state machine
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/fall_state.py`
|
||||||
|
- Create: `v1/tests/test_fall_state.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write event timing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_confirmed_event_is_emitted_once_after_persistence():
|
||||||
|
machine = FallStateMachine(confirm_window_seconds=1.0, recovery_window_seconds=2.0)
|
||||||
|
assert machine.update("7", Evidence(True, True), now=0.0) == []
|
||||||
|
events = machine.update("7", Evidence(True, True), now=1.1)
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].track_id == "7"
|
||||||
|
assert machine.update("7", Evidence(True, True), now=1.2) == []
|
||||||
|
|
||||||
|
def test_brief_bend_returns_to_normal_without_event():
|
||||||
|
machine = FallStateMachine(confirm_window_seconds=1.0, recovery_window_seconds=2.0)
|
||||||
|
machine.update("7", Evidence(True, False), now=0.0)
|
||||||
|
assert machine.update("7", Evidence(False, False), now=0.3) == []
|
||||||
|
|
||||||
|
def test_confirmed_person_recovers_before_new_event_is_allowed():
|
||||||
|
machine = FallStateMachine(confirm_window_seconds=1.0, recovery_window_seconds=2.0)
|
||||||
|
machine.update("7", Evidence(True, True), now=0.0)
|
||||||
|
machine.update("7", Evidence(True, True), now=1.1)
|
||||||
|
machine.update("7", Evidence(True, False, True), now=1.2)
|
||||||
|
machine.update("7", Evidence(True, False, True), now=3.3)
|
||||||
|
assert machine.state_of("7") is FallState.NORMAL
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_fall_state.py -v`
|
||||||
|
Expected: FAIL because `FallStateMachine` is missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the four-state contract**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
class FallState(str, Enum):
|
||||||
|
NORMAL = "NORMAL"
|
||||||
|
SUSPECT = "SUSPECT"
|
||||||
|
CONFIRMED = "CONFIRMED"
|
||||||
|
RECOVERING = "RECOVERING"
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Evidence:
|
||||||
|
accepted: bool
|
||||||
|
is_fall_candidate: bool
|
||||||
|
is_recovery_candidate: bool = False
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Record:
|
||||||
|
state: FallState
|
||||||
|
suspect_started_at: Optional[float] = None
|
||||||
|
recovery_started_at: Optional[float] = None
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FallEvent:
|
||||||
|
event_id: str
|
||||||
|
track_id: str
|
||||||
|
confirmed_at: float
|
||||||
|
|
||||||
|
class FallStateMachine:
|
||||||
|
def __init__(self, confirm_window_seconds: float, recovery_window_seconds: float):
|
||||||
|
self._confirm_window_seconds = confirm_window_seconds
|
||||||
|
self._recovery_window_seconds = recovery_window_seconds
|
||||||
|
self._records: Dict[str, _Record] = {}
|
||||||
|
self._next_event_number = 1
|
||||||
|
|
||||||
|
def _new_event(self, track_id: str, now: float) -> FallEvent:
|
||||||
|
event = FallEvent(f"fall-{self._next_event_number:06d}", track_id, now)
|
||||||
|
self._next_event_number += 1
|
||||||
|
return event
|
||||||
|
|
||||||
|
def state_of(self, track_id: str) -> FallState:
|
||||||
|
return self._records[track_id].state
|
||||||
|
|
||||||
|
def update(self, track_id: str, evidence: Evidence, now: float) -> List[FallEvent]:
|
||||||
|
record = self._records.setdefault(track_id, _Record(state=FallState.NORMAL))
|
||||||
|
if not evidence.accepted:
|
||||||
|
return []
|
||||||
|
if record.state is FallState.NORMAL and evidence.is_fall_candidate:
|
||||||
|
record.state, record.suspect_started_at = FallState.SUSPECT, now
|
||||||
|
return []
|
||||||
|
if record.state is FallState.SUSPECT and not evidence.is_fall_candidate:
|
||||||
|
record.state, record.suspect_started_at = FallState.NORMAL, None
|
||||||
|
return []
|
||||||
|
if record.state is FallState.SUSPECT and now - record.suspect_started_at >= self._confirm_window_seconds:
|
||||||
|
record.state = FallState.CONFIRMED
|
||||||
|
return [self._new_event(track_id, now)]
|
||||||
|
if record.state is FallState.CONFIRMED and evidence.is_recovery_candidate:
|
||||||
|
record.state, record.recovery_started_at = FallState.RECOVERING, now
|
||||||
|
return []
|
||||||
|
if record.state is FallState.RECOVERING and evidence.is_fall_candidate:
|
||||||
|
record.state, record.recovery_started_at = FallState.CONFIRMED, None
|
||||||
|
return []
|
||||||
|
if record.state is FallState.RECOVERING and not evidence.is_recovery_candidate:
|
||||||
|
record.state, record.recovery_started_at = FallState.CONFIRMED, None
|
||||||
|
return []
|
||||||
|
if record.state is FallState.RECOVERING and now - record.recovery_started_at >= self._recovery_window_seconds:
|
||||||
|
record.state, record.recovery_started_at = FallState.NORMAL, None
|
||||||
|
return []
|
||||||
|
return []
|
||||||
|
```
|
||||||
|
|
||||||
|
Use per-ID state, not global `frame_has_fall`. A source error and a rejected pose must not advance the machine.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_fall_state.py -v`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1/fall_state.py v1/tests/test_fall_state.py docs progress.md
|
||||||
|
git commit -m "feat(v1): add temporal fall state machine"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: T-201 and T-202 Connect the event engine to PyQt and local artifacts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/gui.py`, `v1/alerts.py`, `v1/app.py`
|
||||||
|
- Create: `v1/tests/test_alerts.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write idempotent artifact test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_alert_manager_writes_one_screenshot_and_one_log_record(tmp_path, fall_event, frame):
|
||||||
|
manager = AlertManager(tmp_path)
|
||||||
|
first = manager.handle(fall_event, frame)
|
||||||
|
second = manager.handle(fall_event, frame)
|
||||||
|
assert first.screenshot_path.exists()
|
||||||
|
assert second.created is False
|
||||||
|
assert len((tmp_path / "events.jsonl").read_text().splitlines()) == 1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_alerts.py -v`
|
||||||
|
Expected: FAIL because `AlertManager` is missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement UI/event separation**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AlertManager:
|
||||||
|
def handle(self, event: FallEvent, annotated_frame: np.ndarray) -> AlertResult:
|
||||||
|
if event.event_id in self._handled:
|
||||||
|
return AlertResult(created=False, screenshot_path=None)
|
||||||
|
self._handled.add(event.event_id)
|
||||||
|
# save screenshot, append JSONL, then request sound and popup
|
||||||
|
return AlertResult(created=True, screenshot_path=path)
|
||||||
|
```
|
||||||
|
|
||||||
|
`gui.py` receives display frames and events through Qt signals. It renders state colors and popup requests but does not invoke inference or state transitions.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test and manual GUI smoke**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests/test_alerts.py -v; python -m v1.app --config v1/config.example.json --source testdata/videos/smoke.mp4`
|
||||||
|
Expected: pytest PASS; GUI shows source state and exits cleanly after replay.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1/gui.py v1/alerts.py v1/app.py v1/tests/test_alerts.py docs progress.md
|
||||||
|
git commit -m "feat(v1): add alerting desktop flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 7: T-203 through T-205 Establish event-level acceptance and V1 handoff
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `v1/scripts/replay_cases.py`
|
||||||
|
- Create: `testdata/expected_events.json`
|
||||||
|
- Modify: `docs/02-requirements.md`, `docs/current-state.md`, `docs/06-tasks.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create explicit expected cases**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cases": [
|
||||||
|
{"video":"fall-01.mp4","expected_event_count":1,"max_latency_seconds":3.0},
|
||||||
|
{"video":"sit-01.mp4","expected_event_count":0,"max_latency_seconds":0.0},
|
||||||
|
{"video":"bend-01.mp4","expected_event_count":0,"max_latency_seconds":0.0}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing replay assertion**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_replay_report_matches_expected_cases():
|
||||||
|
report = replay_cases(Path("testdata/expected_events.json"))
|
||||||
|
assert report.failed_cases == []
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement replay report generation**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaseResult:
|
||||||
|
video: str
|
||||||
|
expected_event_count: int
|
||||||
|
actual_event_count: int
|
||||||
|
max_latency_seconds: float
|
||||||
|
actual_latency_seconds: Optional[float]
|
||||||
|
```
|
||||||
|
|
||||||
|
The command must return nonzero if an event count differs or a positive case exceeds 3.0 seconds.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run V1 acceptance**
|
||||||
|
|
||||||
|
Run: `python -m pytest v1/tests -v; python v1/scripts/replay_cases.py --manifest testdata/expected_events.json`
|
||||||
|
Expected: all tests PASS; every expected positive has one event in 1–3 seconds; every listed negative has zero events.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit and mark V1 handoff**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add v1 testdata docs progress.md
|
||||||
|
git commit -m "test(v1): add fall event acceptance replay"
|
||||||
|
```
|
||||||
|
|
||||||
|
Only after this step, create the T-301 ONNX export task; do not start Go feature code earlier.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Silver Pose 文档导航
|
||||||
|
|
||||||
|
> Silver Pose 是面向固定机位大厅/走廊的本地摔倒事件演示系统;MVP 是 Python V1 的实时 RTSP 演示闭环。
|
||||||
|
|
||||||
|
- [../AGENTS.md](../AGENTS.md):仓库级 agent 入口与安全边界。
|
||||||
|
- [../CLAUDE.md](../CLAUDE.md):Claude 薄入口。
|
||||||
|
- [../progress.md](../progress.md):只追加的执行历史和验证证据。
|
||||||
|
- [AI 开发入口](00-ai-start-here.md):每轮开工阅读顺序和任务领取规则。
|
||||||
|
- [项目愿景](01-vision.md):目标用户、价值和非目标。
|
||||||
|
- [需求](02-requirements.md):用户故事与可观察验收标准。
|
||||||
|
- [技术栈](03-tech-stack.md):V1/V2 选型和运行命令。
|
||||||
|
- [架构设计](04-architecture.md):实时数据流、模块边界、状态模型和迁移门槛。
|
||||||
|
- [编码规则](05-coding-rules.md):实现前必须遵守的硬规则。
|
||||||
|
- [任务看板](06-tasks.md):按依赖顺序领取的唯一任务。
|
||||||
|
- [V1 实施计划](07-v1-implementation-plan.md):逐任务实现、测试和提交计划。
|
||||||
|
- [本地模块与事件合约](api.md):配置、帧、事件和副作用合约。
|
||||||
|
- [桌面页面结构](routes.md):V1/V2 的监控、设置和事件界面职责。
|
||||||
|
- [当前实现状态](current-state.md):代码现实、命令和下一个任务。
|
||||||
|
- [收尾检查清单](clean-state-checklist.md):会话结束前的恢复性检查。
|
||||||
|
- [方法对照表](method-map.md):常见失败模式对应的工件。
|
||||||
|
- [评审评分表](evaluator-rubric.md):单轮交付评审。
|
||||||
|
- [质量文档](quality-document.md):长期健康度快照。
|
||||||
|
|
||||||
|
## 文档维护规则
|
||||||
|
|
||||||
|
- 需求变化先更新 `02-requirements.md`。
|
||||||
|
- 模块、配置字段或事件语义变化先更新 `04-architecture.md` 和 `api.md`。
|
||||||
|
- 任务状态更新 `06-tasks.md`,真实执行证据追加到 `../progress.md`,当前快照覆盖 `current-state.md`。
|
||||||
|
- V1 通过事件级验收前,`v2/` 不得进入功能开发。
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
# 本地模块与事件合约
|
||||||
|
|
||||||
|
> Silver Pose 没有后端 API;本文定义 V1/V2 必须保持一致的本地配置、数据和事件合约。
|
||||||
|
|
||||||
|
## 配置合约
|
||||||
|
|
||||||
|
`v1/config.example.json` 的最小形状:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"id": "lobby-camera-01",
|
||||||
|
"rtsp_url_env": "SILVER_POSE_RTSP_URL",
|
||||||
|
"roi_normalized": [0.0, 0.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"path": "models/best.pt",
|
||||||
|
"sha256": "填写受控模型哈希",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `rtsp_url_env` 必填;应用从同名环境变量读取真实 URL。
|
||||||
|
- 数值是待现场录像校准的默认值;每个值必须真正进入事件逻辑。
|
||||||
|
- 缺少环境变量、模型不存在或哈希不符时,应用显示配置错误,不启动监控。
|
||||||
|
|
||||||
|
## 核心数据
|
||||||
|
|
||||||
|
```text
|
||||||
|
Keypoint = { x: float, y: float, confidence: float }
|
||||||
|
PersonPose = {
|
||||||
|
track_id: string,
|
||||||
|
detected_at_monotonic: float,
|
||||||
|
box_xyxy: [float, float, float, float],
|
||||||
|
box_confidence: float,
|
||||||
|
keypoints: Keypoint[17]
|
||||||
|
}
|
||||||
|
PoseQuality = {
|
||||||
|
accepted: bool,
|
||||||
|
reason: string,
|
||||||
|
visible_joint_count: int
|
||||||
|
}
|
||||||
|
FallEvent = {
|
||||||
|
event_id: string,
|
||||||
|
source_id: string,
|
||||||
|
track_id: string,
|
||||||
|
started_at_utc: string,
|
||||||
|
confirmed_at_utc: string,
|
||||||
|
latency_seconds: float,
|
||||||
|
state: "CONFIRMED",
|
||||||
|
evidence: {
|
||||||
|
horizontal_pose: bool,
|
||||||
|
rapid_vertical_change: bool,
|
||||||
|
persistence_seconds: float
|
||||||
|
},
|
||||||
|
screenshot_path: string,
|
||||||
|
config_version: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`FallEvent` 只在状态首次进入 `CONFIRMED` 时创建一次。连续帧更新 UI 状态,但不重复创建事件。
|
||||||
|
|
||||||
|
## 事件合约
|
||||||
|
|
||||||
|
| 事件 | 触发者 | 负载 | 结果 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `source.connected` | 视频源 | `source_id`、时间 | UI 显示在线。 |
|
||||||
|
| `source.error` | 视频源 | `source_id`、错误码、可重试标记 | UI 显示异常;不报警。 |
|
||||||
|
| `person.updated` | 跟踪与 Pose | `PersonPose`、`PoseQuality`、状态 | UI 绘制骨架与 ID。 |
|
||||||
|
| `fall.suspected` | 状态机 | `track_id`、开始时间 | UI 显示黄色疑似状态。 |
|
||||||
|
| `fall.confirmed` | 状态机 | `FallEvent` | 红色叠加、声音、弹窗、截图、JSONL。 |
|
||||||
|
| `fall.recovered` | 状态机 | `track_id`、时间 | UI 恢复绿色正常状态。 |
|
||||||
|
|
||||||
|
## 副作用边界
|
||||||
|
|
||||||
|
- 仅 `alerts` 模块可写事件截图、播放声音、弹窗和追加 JSONL。
|
||||||
|
- 仅 `video_source` 模块读 RTSP 或本地视频。
|
||||||
|
- 仅 `pose` 模块加载模型;UI 不得直接调用模型。
|
||||||
|
- V2 可以改变实现语言,但不得改变配置含义、`FallEvent` 字段和事件幂等语义。
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 干净收尾检查清单
|
||||||
|
|
||||||
|
> 每轮会话结束前逐项检查,确保下一轮 agent 不需要靠聊天记录恢复状态。
|
||||||
|
|
||||||
|
- [ ] `./init.ps1` 或当前任务要求的等价启动/验证命令已运行,结果已如实记录。
|
||||||
|
- [ ] 当前任务的 pytest、录像回归、Go 测试或手工验收已运行;失败没有被掩盖。
|
||||||
|
- [ ] `docs/06-tasks.md` 只存在一个或零个 `DOING`,且状态真实。
|
||||||
|
- [ ] 本轮命令、结果、阻塞和决策已追加到 `../progress.md`。
|
||||||
|
- [ ] `current-state.md` 已覆盖更新到真实目录、命令、模型和下一个任务。
|
||||||
|
- [ ] 未提交内容已检查;没有 RTSP 凭证、客户视频、模型导出物、事件截图或日志被暂存。
|
||||||
|
- [ ] V1/V2 公共事件合约变化已同步到 `04-architecture.md` 和 `api.md`。
|
||||||
|
- [ ] 若改了模型、阈值、输入尺寸或配置,已记录版本与回归结果。
|
||||||
|
- [ ] 若本轮是 V2,已运行 V1/Go 一致性回归,未将“可编译”误报为“可演示”。
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 当前实现状态
|
||||||
|
|
||||||
|
> 本文是可覆盖的当前快照;历史执行记录见 [../progress.md](../progress.md)。
|
||||||
|
|
||||||
|
## 当前快照
|
||||||
|
|
||||||
|
- 日期:2026-07-20
|
||||||
|
- 阶段:V1 工程化起步。
|
||||||
|
- 已验证环境:Windows PowerShell;Python 3.8.10;Ultralytics 8.3.205;PyQt5 可导入。
|
||||||
|
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||||
|
- V1 代码:`v1/` 目录存在但尚无实现。
|
||||||
|
- V2 代码:`v2/` 目录存在但尚无实现。
|
||||||
|
- 测试:暂无 pytest 测试;`python -m compileall -q demo` 已通过。
|
||||||
|
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||||
|
- 当前标准启动:`./init.ps1`。
|
||||||
|
- 当前标准验证:`python -m compileall -q demo`。
|
||||||
|
- 当前 blocker:V1 依赖清单、示例配置、回归录像及事件标签尚未创建;真实海康 RTSP 流尚未接入。
|
||||||
|
|
||||||
|
全局环境的 `pip check` 存在其他项目的包冲突,因此它不是 Silver Pose 的验收命令。`init.ps1` 只检查本项目实际导入的 OpenCV、NumPy、Ultralytics 与 PyQt5,并在命令非零退出时失败。
|
||||||
|
|
||||||
|
## 当前目录要点
|
||||||
|
|
||||||
|
| 路径 | 状态 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `demo/` | 已有 | 旧 Python GUI、Pose 模型与样例视频;当前单帧规则不适合作为正式事件逻辑。 |
|
||||||
|
| `v1/` | 待开发 | 新 Python V1 的正式实现目录。 |
|
||||||
|
| `v2/` | 待开发 | Go V2 目录;V1 验收前不实现功能。 |
|
||||||
|
| `docs/` | 已有 | Harness Coding 文档、任务和计划。 |
|
||||||
|
| `tests/` | 不适用 | V1 测试将在 `v1/tests/` 下创建。 |
|
||||||
|
| `artifacts/` | 待创建 | 本地事件截图和 JSONL,不提交。 |
|
||||||
|
|
||||||
|
## 任务状态
|
||||||
|
|
||||||
|
- 已完成:T-000(Harness 文档与旧基线快照)。
|
||||||
|
- 正在进行:无。
|
||||||
|
- 下一个可领取:T-101。
|
||||||
|
|
||||||
|
## 当前可运行内容
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./init.ps1
|
||||||
|
Set-Location demo
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已知事实和限制
|
||||||
|
|
||||||
|
- 当前 `demo/` 的摔倒判定来自单帧、硬编码阈值和任意规则短路;界面部分阈值不会传入实际逻辑。
|
||||||
|
- 当前模型是通用 person Pose 模型,不是摔倒事件分类模型。
|
||||||
|
- 当前机器在无 GPU 条件下,对 848×480 本地样例以 640 输入运行 Pose 的测量均值约为 64.5 ms/帧;这仅是模型推理,不是端到端 RTSP/GUI 性能承诺。
|
||||||
|
- V1 的客户演示范围限于固定俯视大厅/走廊、全身大部分可见、单路本地视频。
|
||||||
|
|
||||||
|
更新本文件时,必须同步 `06-tasks.md` 的任务状态,并向 `../progress.md` 追加真实验证记录。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 评审评分表
|
||||||
|
|
||||||
|
> 每个任务完成后,以 0–2 分评审本轮交付;总分不替代阻塞项判断。
|
||||||
|
|
||||||
|
| 维度 | 0 分 | 1 分 | 2 分 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 正确性 | 不符合任务验收 | 主路径可用但边界不完整 | 所有任务验收项可观察地满足 |
|
||||||
|
| 验证 | 无命令或只有口头描述 | 跑了部分验证 | 所有指定测试/录像/命令已跑并记录 |
|
||||||
|
| 事件可靠性 | 单帧或断流可误报 | 有部分门控 | 同 ID、时间窗、幂等报警和反例均验证 |
|
||||||
|
| 范围纪律 | 夹带无关功能 | 有少量未计划改动 | 只完成当前任务 |
|
||||||
|
| 可维护性 | 逻辑混入 GUI 或硬编码 | 有文档或测试缺口 | 模块边界、配置、合约和文档一致 |
|
||||||
|
| 交接准备度 | 下一轮无法复现 | 当前状态不完整 | task、progress、current state 和工件齐全 |
|
||||||
|
|
||||||
|
结论只能选一项:
|
||||||
|
|
||||||
|
- **Accept**:无 P0 缺口,所有验收与验证证据齐全。
|
||||||
|
- **Revise**:列出必须补的测试、文档或实现。
|
||||||
|
- **Block**:列出缺少的摄像头、录像、模型、运行时或用户决策。
|
||||||
|
|
||||||
|
V1 的“可演示”至少需要:正反例事件回归、RTSP 实流测试、1–3 秒延迟记录、声音/弹窗/截图证据。V2 额外需要 Python/Go 一致性证据。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 方法对照表
|
||||||
|
|
||||||
|
| 失败模式 | 实际表现 | 首要修复 | 工件 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 单帧误报 | 坐下、弯腰或遮挡就报警 | 检查质量门控和按 ID 的时序状态机 | [04-architecture.md](04-architecture.md)、`v1/fall_state.py`、回归录像 |
|
||||||
|
| 阈值假生效 | UI 数值改变但行为未变 | 将配置对象传入纯事件逻辑并写测试 | [api.md](api.md)、`v1/config.py`、`v1/tests/test_fall_state.py` |
|
||||||
|
| RTSP 不稳定 | 断流、卡帧或重连后误报 | 分离视频源状态与事件状态,增加重连测试 | `v1/video_source.py`、[02-requirements.md](02-requirements.md) |
|
||||||
|
| 模型被误解 | 把 Pose 置信度说成摔倒概率 | 只显示姿态质量,单列事件证据与事件指标 | [05-coding-rules.md](05-coding-rules.md)、[routes.md](routes.md) |
|
||||||
|
| Go 结果漂移 | 同一录像 Python/Go 报警时刻不同 | 固定 ONNX、预处理、NMS、关键点与事件回归 | T-301 至 T-303、[api.md](api.md) |
|
||||||
|
| 演示无法复现 | 现场“碰巧能报”但无证据 | 固定正反例录像、配置、延迟和截图 | `testdata/`、`expected_events.json`、`progress.md` |
|
||||||
|
| 任务范围失控 | V1 未完成就开发云端或 V2 | 只领取当前第一个 TODO | [06-tasks.md](06-tasks.md) |
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 质量文档
|
||||||
|
|
||||||
|
> 评级按当前事实记录;每个里程碑后更新。A 表示可验证且边界清晰,D 表示不可用于承诺的目标。
|
||||||
|
|
||||||
|
## 产品领域
|
||||||
|
|
||||||
|
| 领域 | 评级 | 验证状态 | 关键缺口 | 上次更新 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 旧单帧姿态演示 | C | `demo/` 编译通过、模型可加载 | 无时序、低质量门控与事件验收 | 2026-07-20 |
|
||||||
|
| RTSP 实时监控 | D | 未接入真实流 | 无重连、无配置和无验证 | 2026-07-20 |
|
||||||
|
| 摔倒事件判定 | D | 无 V1 实现 | 无 track ID、状态机、正反例录像 | 2026-07-20 |
|
||||||
|
| 报警与截图 | D | 旧 GUI 仅显示 | 无确认事件幂等、弹窗、音频、工件合约 | 2026-07-20 |
|
||||||
|
| Go V2 | D | 未开始 | 无 ONNX、一致性、运行时 Spike | 2026-07-20 |
|
||||||
|
|
||||||
|
## 架构层
|
||||||
|
|
||||||
|
| 层级 | 评级 | 关键缺口 | 上次更新 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Python UI 与线程 | C | 旧 GUI 混合推理、规则和显示 | 2026-07-20 |
|
||||||
|
| Pose 推理 | B | 权重可加载、关键点格式明确;缺少现场视角评测 | 2026-07-20 |
|
||||||
|
| 跟踪与事件核心 | D | 尚未从 UI/单帧规则中独立 | 2026-07-20 |
|
||||||
|
| 视频源适配 | D | 仅本地文件路径,未做 RTSP 韧性 | 2026-07-20 |
|
||||||
|
| 工件与回归 | D | 无版本化配置、截图合约和录像标签 | 2026-07-20 |
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
### 2026-07-20
|
||||||
|
|
||||||
|
- 建立 Harness Coding 文档与当前质量快照。
|
||||||
|
- 明确 V1 先验证事件闭环、V2 再迁移 Go 的门槛。
|
||||||
|
- 未提升代码评分;本次只建立事实来源和可验证路径。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 桌面页面与交互结构
|
||||||
|
|
||||||
|
> V1 是单窗口 PyQt5 桌面应用,没有 Web 路由。本文件规定页面职责和导航,V2 选择本地网页壳或桌面壳时必须保持同等用户流程。
|
||||||
|
|
||||||
|
## 页面 / 视图
|
||||||
|
|
||||||
|
| 视图 | MVP 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| 实时监控 | 选择本地安全配置、开始/停止流、显示视频、骨架、ID、状态、连接质量和 FPS。 |
|
||||||
|
| 设置面板 | 显示非敏感配置值,保存前校验;不得显示真实 RTSP 密码。 |
|
||||||
|
| 摔倒弹窗 | 仅首次 `fall.confirmed` 时显示人员 ID、确认时间、截图路径和确认按钮。 |
|
||||||
|
| 事件提示区 | 显示最近事件和截图保存结果;V1 不做长期历史数据库。 |
|
||||||
|
| 连接错误提示 | 显示断流或配置错误以及可重试状态;不得使用“正常”掩盖错误。 |
|
||||||
|
|
||||||
|
## 实时监控视图
|
||||||
|
|
||||||
|
- 绿色:`NORMAL`。
|
||||||
|
- 黄色:`SUSPECT`。
|
||||||
|
- 红色:`CONFIRMED`。
|
||||||
|
- 灰色:无人员、低质量或视频源不可用;必须附带原因文字。
|
||||||
|
- 每个可见人员显示稳定 `track_id`、骨架和状态,避免将全画面统一标成摔倒。
|
||||||
|
- 红色事件持续时维持可视状态,但弹窗和声音只出现一次。
|
||||||
|
|
||||||
|
## 交互规则
|
||||||
|
|
||||||
|
1. 点击“开始”前,校验模型文件、配置文件和 RTSP 环境变量。
|
||||||
|
2. 点击“停止”后,停止视频源、释放模型线程、停止刷新;不删除已保存事件。
|
||||||
|
3. 修改事件阈值后,UI 显示的值必须是下一次开始监控真正使用的配置版本。
|
||||||
|
4. 断流时显示错误与重连状态;恢复连接不自动生成摔倒事件。
|
||||||
|
5. 截图保存失败时,仍保留红色报警并在提示区记录错误。
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
# Silver Pose 的 Windows 统一启动与验证入口。
|
||||||
|
# 当前阶段验证 demo 基线;T-101 创建 v1 后,应同步更新本文件和 docs/03-tech-stack.md。
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Set-Location -Path $PSScriptRoot
|
||||||
|
|
||||||
|
$InstallCmd = 'python -c "import cv2, numpy, ultralytics, PyQt5; print(''required Python packages available'')"'
|
||||||
|
$VerifyCmd = "python -m compileall -q demo"
|
||||||
|
$StartCmd = "Set-Location demo; python main.py"
|
||||||
|
|
||||||
|
function Invoke-Checked {
|
||||||
|
param([string]$Label, [string]$Command)
|
||||||
|
Write-Host "==> $Label"
|
||||||
|
Invoke-Expression $Command
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "$Label failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> 当前目录: $($PWD.Path)"
|
||||||
|
Invoke-Checked -Label "检查 Silver Pose Python 依赖" -Command $InstallCmd
|
||||||
|
Invoke-Checked -Label "运行 demo 基线验证" -Command $VerifyCmd
|
||||||
|
|
||||||
|
Write-Host "==> 图形界面启动命令"
|
||||||
|
Write-Host " $StartCmd"
|
||||||
|
|
||||||
|
if ($env:RUN_START_COMMAND -eq "1") {
|
||||||
|
Write-Host "==> 启动应用"
|
||||||
|
Invoke-Expression $StartCmd
|
||||||
|
} else {
|
||||||
|
Write-Host "设置 RUN_START_COMMAND=1 后,init.ps1 会启动 GUI。"
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# 执行进度记录
|
||||||
|
|
||||||
|
> 本文件只追加记录任务执行、验证、阻塞和关键决策。当前目录、当前命令和下一任务以 [docs/current-state.md](docs/current-state.md) 为准。
|
||||||
|
|
||||||
|
## 记录格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 【YYYY-MM-DD】T-【编号】 【任务名】
|
||||||
|
|
||||||
|
- 状态:【DONE / BLOCKED / PARTIAL】
|
||||||
|
- 变更:【文件或模块】
|
||||||
|
- 验证:【真实命令和结果】
|
||||||
|
- 阻塞:【原因与决策人】
|
||||||
|
- 决策:【本轮确定的取舍】
|
||||||
|
- 下一步:【任务 ID 或待确认事项】
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
## 【2026-07-20】T-000 建立 Harness Coding 文档基线
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:创建仓库级 agent 入口、V1/V2 需求、技术栈、架构、任务看板、模块合约、当前状态和实施计划文档。
|
||||||
|
- 验证:`./init.ps1` 已通过:Python 3.8.10 可导入 OpenCV、NumPy、Ultralytics 8.3.205 与 PyQt5,且 `python -m compileall -q demo` 退出码 0;`demo/best.pt` 被识别为 `pose`、`kpt_shape=[17, 3]`、类别 `person`;20 个 Markdown 文档的本地链接均可解析,实施计划没有占位标记。全局 `pip check` 的其他项目冲突不作为本项目验收;`init.ps1` 检查实际依赖并对非零退出码失败。
|
||||||
|
- 阻塞:无代码 blocker;V1 依赖清单、RTSP 实流录像和现场验收录像尚未创建。
|
||||||
|
- 决策:`demo/` 作为旧基线保留;V1 使用 Python + PyQt5 + OpenCV + Ultralytics;V2 使用 Go,且必须以 V1 的录像回归集和 ONNX 一致性测试为迁移门槛。
|
||||||
|
- 下一步:T-101。
|
||||||
Reference in New Issue
Block a user