feat(v1): add demo runbook and release package
This commit is contained in:
@@ -18,7 +18,7 @@ silver_pose/
|
||||
└── progress.md # 只追加执行流水
|
||||
```
|
||||
|
||||
## 当前可验证基线
|
||||
## V1 启动与验证
|
||||
|
||||
在 Windows PowerShell、仓库根目录运行:
|
||||
|
||||
@@ -26,14 +26,31 @@ silver_pose/
|
||||
./init.ps1
|
||||
```
|
||||
|
||||
它会检查当前 Python 环境并编译 `demo/`。图形界面可用:
|
||||
它会检查当前 Python 环境、编译旧 `demo/` 基线并运行 V1 测试。首次部署先安装锁定依赖:
|
||||
|
||||
```powershell
|
||||
Set-Location demo
|
||||
python main.py
|
||||
python -m pip install -r v1/requirements.txt
|
||||
```
|
||||
|
||||
运行 GUI 或 RTSP 演示前,必须在本机准备已安装的 PyQt5、OpenCV、NumPy 与 Ultralytics 环境。真实 RTSP 凭证不得提交。
|
||||
将 `v1/config.release.example.json`(发布包中生成)或 `v1/config.example.json` 复制为未跟踪的 `v1/config.local.json`,仅在本机填写摄像头来源与凭证。然后启动 V1:
|
||||
|
||||
```powershell
|
||||
python -m v1.app
|
||||
```
|
||||
|
||||
运行 GUI 或 RTSP 演示前,必须在本机准备已安装的 PyQt5、OpenCV、NumPy、Ultralytics 与 Pillow 环境。真实 RTSP 凭证、客户视频、截图和 JSONL 事件工件不得提交。
|
||||
|
||||
## 客户演示与发布包
|
||||
|
||||
客户演示步骤、证据复查和适用边界见 [docs/demo-runbook.md](docs/demo-runbook.md)。构建发布包时,模型必须提供受控 SHA-256;脚本只复制 V1 代码、公开示例、运行手册和锁定模型,不复制本地配置或事件工件:
|
||||
|
||||
```powershell
|
||||
$modelSha = (Get-FileHash -Algorithm SHA256 demo/best.pt).Hash.ToLower()
|
||||
pwsh -File v1/scripts/build-v1-release.ps1 -ModelPath demo/best.pt -ModelSha256 $modelSha -Output dist/SilverPose-V1
|
||||
python -m v1.release verify --package dist/SilverPose-V1
|
||||
```
|
||||
|
||||
发布包和现场演示仅覆盖固定俯视、全身大部分可见、单路大厅或走廊;`best.pt` 是人体姿态模型,不是摔倒分类器,也不构成医疗级或全场景可靠性承诺。
|
||||
|
||||
## 文档入口
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
| T-202 | 实现本地报警、弹窗、截图和 JSONL 日志 | T-201 | CONFIRMED 产生一次声音、一次弹窗、一张带标注截图和一条事件日志。 | DONE |
|
||||
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | DONE |
|
||||
| T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | DONE |
|
||||
| T-205 | 固化客户演示脚本与 V1 发布包 | T-204 | 可按文档完成演示脚本,所有证据可复查,适用边界显示给操作员。 | DOING |
|
||||
| T-205 | 固化客户演示脚本与 V1 发布包 | T-204 | 可按文档完成演示脚本,所有证据可复查,适用边界显示给操作员。 | DONE |
|
||||
| T-206 | 结构化摄像头配置、连接测试与设置页预览 | T-201 | 配置支持 host/端口/通道/账号/密码 结构化来源并拼出 RTSP(凭证百分号编码);公开示例保持无凭证、`config_version` 不含凭证;设置页可掩码输入、点击测试连接,连通后抓拍一帧显示在预览区。 | DONE |
|
||||
| T-207 | 抓流低延迟参数进配置与设置页 | T-206 | 配置支持 transport(tcp/udp)/连接超时/低延迟,程序拼 `OPENCV_FFMPEG_CAPTURE_OPTIONS` 并在开流前注入;设置页用下拉/数字/勾选而非原始字符串;启动可不依赖 run 脚本设环境变量。 | DONE |
|
||||
| T-208 | 每帧摔倒判定诊断叠加 | T-201 | 管线暴露每人策略证据;实时画面角落叠加每人 `accepted/horizontal/角度/rapid/candidate/state`(拒绝时显示原因),便于定位真实摔倒未确认卡在哪一步;诊断字符串构建 Qt-free 可测。 | DONE |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-07-22
|
||||
- 阶段:V1 事件管线、PyQt 双 Tab 界面、本地报警工件、结构化摄像头配置/连接测试、抓流低延迟参数与每帧判定诊断叠加已建立;T-204 已验收,T-205(客户演示脚本与 V1 发布包)进行中。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸。
|
||||
- 阶段:V1 客户演示闭环已验收;T-205 已完成,V2 只能在 T-301 的 ONNX 一致性门槛后开始。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸。
|
||||
- 已验证环境: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)。
|
||||
@@ -15,7 +15,7 @@
|
||||
- 模型:`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-205 尚未完成客户演示脚本、启动预检与发布包说明。T-204 已记录两条现场确认事件(1.84s、1.86s,均在目标区间)和走动、坐下、弯腰、蹲下捡物、站立停留均不报警的反例结果;现场录像和事件工件保留本机,不提交仓库。GUI 可视化、声音、弹窗与设置页测试连接的像素级观感仍应在 Windows 目视复核。
|
||||
- 当前 blocker:V2 尚未达到 T-301 的模型导出与一致性门槛;现场录像和事件工件保留本机,不提交仓库。GUI 可视化、声音、弹窗与设置页测试连接的像素级观感仍应在 Windows 目视复核。
|
||||
|
||||
全局环境的 `pip check` 存在其他项目的包冲突,因此它不是 Silver Pose 的验收命令。`init.ps1` 只检查本项目实际导入的 OpenCV、NumPy、Ultralytics 与 PyQt5,并在命令非零退出时失败。
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
## 任务状态
|
||||
|
||||
- 已完成:T-000、T-101~T-106(V1 地基与事件管线)、T-201(PyQt 双 Tab)、T-202(本地报警/截图/JSONL)、T-203(海康 RTSP 与断流恢复)、T-204(正反例录像与现场验收)、T-206(结构化摄像头配置与连接测试)、T-207(抓流低延迟参数)、T-208(每帧诊断叠加)、T-209(放宽俯视灵敏度)、T-210(截图 ASCII 标注)、T-211(灵敏度设置页)、T-212(实时流最新帧)、T-213(截图中文标注)。
|
||||
- 正在进行:T-205(客户演示脚本与 V1 发布包)。
|
||||
- 下一个可领取:无;完成 T-205 后才可开始 V2 模型导出门槛 T-301。
|
||||
- 正在进行:无。
|
||||
- 下一个可领取:T-301(锁定 V1 模型并导出 ONNX 一致性工件)。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Silver Pose V1 客户演示手册
|
||||
|
||||
## 启动前检查
|
||||
|
||||
1. 使用 Windows PowerShell 进入发布包根目录,运行 `./init.ps1`;依赖检查、旧基线编译和 V1 测试必须通过。
|
||||
2. 运行 `python -m v1.release verify --package .`,确认发布清单和 `v1/models/best.pt` 的 SHA-256 未被替换。
|
||||
3. 将 `v1/config.release.example.json` 复制为未跟踪的 `v1/config.local.json`,只在本机配置摄像头来源;不得将账号、密码、地址、客户录像或事件工件提交、发送或投屏。
|
||||
4. 在设置页确认摄像头连接测试可抓取预览帧;确认低延迟、确认窗口和灵敏度参数与本次现场记录一致。
|
||||
5. 确认演示区为固定俯视的单路大厅或走廊,人员头、躯干、膝盖和脚踝大部分可见;安全模拟必须由经同意成年人按现场安全措施完成。
|
||||
|
||||
## 现场演示步骤
|
||||
|
||||
1. 运行 `python -m v1.app`,在设置页确认来源已就绪后开始监控。
|
||||
2. 先演示走动、坐下、弯腰、蹲下捡物和站立停留;这些动作不应出现确认摔倒的红色状态、声音或弹窗。
|
||||
3. 由现场负责人安排安全模拟从站立或行走到持续倒地;观察同一人员的骨架、ID、疑似状态和确认后的红色画面。
|
||||
4. 确认事件发生后,向客户展示一次声音、一次弹窗、带姿态标注的截图和一条本地事件记录。
|
||||
5. 可选演示断流:停止来源后界面显示重连状态且不报警;恢复来源后继续预览与检测。
|
||||
|
||||
## 证据复查
|
||||
|
||||
1. 在演示结束后运行 `python -m v1.report artifacts/events`。
|
||||
2. 核对每个确认事件的人员 ID、确认时间、`latency_seconds` 和相对截图路径;本项目的现场目标为 1–3 秒。
|
||||
3. 每个事件仅应有一条 JSONL 记录和一张事件截图;正常活动期间不应产生新的确认事件。
|
||||
4. 截图和 JSONL 只保留在本机约定的工件目录,按客户现场的数据管理要求处理。
|
||||
|
||||
## 适用边界
|
||||
|
||||
- 本演示只覆盖固定俯视、单路、全身大部分可见的大厅或走廊。
|
||||
- `best.pt` 是人体姿态模型,不是摔倒分类器;系统结论来自姿态质量、人员连续性和时间状态机。
|
||||
- 系统不是医疗诊断产品,不承诺所有公共场所、所有视角、严重遮挡、半身人员或多人密集交叉时的可靠性。
|
||||
- 客户应以现场批准的正反例和事件记录评估本机位表现,不应将本演示结果外推为通用准确率承诺。
|
||||
@@ -0,0 +1,173 @@
|
||||
# T-205 V1 Demo Release Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 生成不含客户数据的 V1 演示包,并让现场操作员能按固定脚本完成预检、演示、证据复查和适用边界说明。
|
||||
|
||||
**Architecture:** `v1/release.py` 负责可测试的发布包构建与校验;它只复制 V1 运行代码、公开示例、依赖、运行手册和经 SHA-256 校验的模型。`v1/scripts/build-v1-release.ps1` 只是 Windows 命令行入口。运行手册定义现场步骤,不保存或输出 RTSP 凭证。
|
||||
|
||||
**Tech Stack:** CPython 3.8、PowerShell、`hashlib`、`json`、`shutil`、pytest、PyQt5 V1 应用。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 可测试的发布包构建器
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `v1/release.py`
|
||||
- Create: `v1/tests/test_release.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,定义受控模型和非敏感清单**
|
||||
|
||||
```python
|
||||
def test_build_release_copies_locked_model_and_excludes_local_credentials(tmp_path):
|
||||
model = tmp_path / "best.pt"
|
||||
model.write_bytes(b"locked-model")
|
||||
output = tmp_path / "SilverPose-V1"
|
||||
|
||||
manifest = build_release_package(ROOT, model, sha256_file(model), output)
|
||||
|
||||
assert (output / "v1" / "models" / "best.pt").read_bytes() == b"locked-model"
|
||||
assert manifest["model_sha256"] == sha256_file(model)
|
||||
assert not list(output.rglob("config.local.json"))
|
||||
assert not list(output.rglob("events.jsonl"))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py -v`
|
||||
|
||||
Expected: 因 `v1.release` 或 `build_release_package` 不存在而失败。
|
||||
|
||||
- [ ] **Step 3: 实现最小构建器**
|
||||
|
||||
```python
|
||||
def build_release_package(project_root, model_path, expected_sha256, output_dir):
|
||||
_validate_model(model_path, expected_sha256)
|
||||
_copy_tree(project_root / "v1", output_dir / "v1", ignored={"config.local.json", "__pycache__"})
|
||||
_copy_file(project_root / "README.md", output_dir / "README.md")
|
||||
_copy_file(project_root / "docs" / "demo-runbook.md", output_dir / "docs" / "demo-runbook.md")
|
||||
_copy_model(model_path, output_dir / "v1" / "models" / "best.pt")
|
||||
return _write_manifest(output_dir, expected_sha256)
|
||||
```
|
||||
|
||||
清单只包含格式版本、生成时间、模型 SHA-256、包内相对文件路径和文件 SHA-256;不得写入 RTSP URL、账号、密码、客户视频路径或事件工件路径。
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### Task 2: Windows 发布入口和启动命令
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `v1/scripts/build-v1-release.ps1`
|
||||
- Modify: `init.ps1`
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,定义发布清单校验**
|
||||
|
||||
```python
|
||||
def test_verify_release_rejects_a_tampered_model(tmp_path):
|
||||
output = _build_test_release(tmp_path)
|
||||
(output / "v1" / "models" / "best.pt").write_bytes(b"changed")
|
||||
|
||||
with pytest.raises(ReleaseError, match="model SHA-256"):
|
||||
verify_release_package(output)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py::test_verify_release_rejects_a_tampered_model -v`
|
||||
|
||||
Expected: `verify_release_package` 尚未定义而失败。
|
||||
|
||||
- [ ] **Step 3: 实现清单校验和 PowerShell 入口**
|
||||
|
||||
```powershell
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ModelPath,
|
||||
[Parameter(Mandatory = $true)][string]$ModelSha256,
|
||||
[string]$Output = "dist/SilverPose-V1"
|
||||
)
|
||||
python -m v1.release build --model-path $ModelPath --model-sha256 $ModelSha256 --output $Output
|
||||
```
|
||||
|
||||
`verify_release_package` 必须重新计算包内模型和清单列出的每个文件 SHA-256。`init.ps1` 的 GUI 启动提示改为 `python -m v1.app`,但继续执行旧 `demo/` 的只读编译基线。README 列出依赖安装、无凭证配置方式、V1 启动命令和发布脚本命令。
|
||||
|
||||
- [ ] **Step 4: 运行针对性测试**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### Task 3: 客户演示与证据复查手册
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `docs/demo-runbook.md`
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: 写手册静态守卫测试**
|
||||
|
||||
```python
|
||||
def test_demo_runbook_covers_preflight_evidence_and_scope():
|
||||
text = (ROOT / "docs" / "demo-runbook.md").read_text(encoding="utf-8")
|
||||
for heading in ("启动前检查", "现场演示步骤", "证据复查", "适用边界"):
|
||||
assert heading in text
|
||||
assert "rtsp://" not in text
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py::test_demo_runbook_covers_preflight_evidence_and_scope -v`
|
||||
|
||||
Expected: 手册不存在而失败。
|
||||
|
||||
- [ ] **Step 3: 编写手册**
|
||||
|
||||
手册必须给出:Windows/依赖/模型哈希/本地配置/摄像头连接测试的预检;演示正常活动和安全模拟摔倒的顺序;确认红色姿态、声音、弹窗和截图的观察点;`python -m v1.report artifacts/events` 的证据复查;断流不报警的观察;固定俯视、全身大部分可见、单路大厅或走廊的范围限制;以及不作医疗级、全场景或 Pose 模型即摔倒分类器的表述。
|
||||
|
||||
- [ ] **Step 4: 运行手册守卫测试**
|
||||
|
||||
Run: `python -m pytest v1/tests/test_release.py -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### Task 4: 构建并复查无敏感工件的演示包
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/06-tasks.md`
|
||||
- Modify: `docs/current-state.md`
|
||||
- Modify: `progress.md`
|
||||
|
||||
- [ ] **Step 1: 使用锁定模型构建临时发布包**
|
||||
|
||||
Run: `$releaseRoot = Join-Path $env:TEMP "SilverPose-V1-Release"; $modelSha = (Get-FileHash -Algorithm SHA256 demo/best.pt).Hash.ToLower(); pwsh -File v1/scripts/build-v1-release.ps1 -ModelPath demo/best.pt -ModelSha256 $modelSha -Output $releaseRoot`
|
||||
|
||||
Expected: 输出 `release-manifest.json`,模型、代码、公开示例和手册存在,且构建目录中无 `config.local.json`、`events.jsonl`、`artifacts/` 或 RTSP 地址。
|
||||
|
||||
- [ ] **Step 2: 校验发布包**
|
||||
|
||||
Run: `$releaseRoot = Join-Path $env:TEMP "SilverPose-V1-Release"; $modelSha = (Get-FileHash -Algorithm SHA256 demo/best.pt).Hash.ToLower(); pwsh -File v1/scripts/build-v1-release.ps1 -ModelPath demo/best.pt -ModelSha256 $modelSha -Output $releaseRoot; python -m v1.release verify --package $releaseRoot`
|
||||
|
||||
Expected: `release package verified`。
|
||||
|
||||
- [ ] **Step 3: 运行全量验证**
|
||||
|
||||
Run: `./init.ps1`, `python -m pytest v1/tests -v`, `python -m compileall -q v1 demo`, `git diff --check`。
|
||||
|
||||
Expected: 所有测试通过,编译退出码 0,差异检查无输出。
|
||||
|
||||
- [ ] **Step 4: 更新任务与执行记录并提交**
|
||||
|
||||
将 T-205 标为 DONE,记录实际构建/校验命令和“不包含客户工件”的结果;提交代码、测试、手册和任务文档。临时发布目录保持在忽略目录内,不提交。
|
||||
|
||||
## 自检
|
||||
|
||||
- 覆盖:发布包、模型哈希、篡改检测、Windows 入口、演示步骤、证据复查、适用边界和无敏感工件。
|
||||
- 任务没有未定义的函数或占位步骤;每项生产行为均先有失败测试。
|
||||
- 发布包验证不依赖真实 RTSP,也不打印或写入凭证。
|
||||
@@ -9,7 +9,7 @@ Set-Location -Path $PSScriptRoot
|
||||
$InstallCmd = 'python -c "import cv2, numpy, ultralytics, PyQt5; print(''required Python packages available'')"'
|
||||
$VerifyCmd = "python -m compileall -q demo"
|
||||
$V1TestCmd = "python -m pytest v1/tests -v"
|
||||
$StartCmd = "Set-Location demo; python main.py"
|
||||
$StartCmd = "python -m v1.app"
|
||||
|
||||
function Invoke-Checked {
|
||||
param([string]$Label, [string]$Command)
|
||||
@@ -22,7 +22,11 @@ function Invoke-Checked {
|
||||
|
||||
Write-Host "==> 当前目录: $($PWD.Path)"
|
||||
Invoke-Checked -Label "检查 Silver Pose Python 依赖" -Command $InstallCmd
|
||||
Invoke-Checked -Label "运行 demo 基线验证" -Command $VerifyCmd
|
||||
if (Test-Path -LiteralPath "demo") {
|
||||
Invoke-Checked -Label "运行 demo 基线验证" -Command $VerifyCmd
|
||||
} else {
|
||||
Write-Host "==> 发布包未包含 demo/,跳过旧基线编译"
|
||||
}
|
||||
Invoke-Checked -Label "运行 V1 单元测试" -Command $V1TestCmd
|
||||
|
||||
Write-Host "==> 图形界面启动命令"
|
||||
|
||||
@@ -380,6 +380,15 @@
|
||||
- 决策:演示脚本必须明确固定俯视、全身大部分可见、单路大厅/走廊的适用边界,不将 Pose 模型或现场结果表述为医疗级或全场景可靠性。
|
||||
- 下一步:盘点当前启动/配置/报告工具,先为预检和演示清单写可测试契约,再实现发布说明。
|
||||
|
||||
## 【2026-07-22】T-205 固化客户演示脚本与 V1 发布包(完成)
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:新增 `docs/demo-runbook.md`、`v1/release.py` 与 `v1/scripts/build-v1-release.ps1`;README 与 init 启动提示改为 V1。发布包只包含 V1 代码、公开示例、手册和 SHA-256 锁定模型,不包含本地配置或事件工件。
|
||||
- 验证:临时发布包通过 `python -m v1.release verify`;包内 `./init.ps1` 为 88 passed、1 skipped,仓库 `./init.ps1`、`python -m pytest v1/tests -q`、`python -m compileall -q v1 demo` 和 `git diff --check` 均通过。
|
||||
- 阻塞:无。真实凭证、录像、截图和 JSONL 保持本机工件。
|
||||
- 决策:客户演示按运行手册执行并明确适用边界;V2 仍等待 T-301 ONNX 一致性验证。
|
||||
- 下一步:T-301。
|
||||
|
||||
## 【2026-07-21】T-211 检测灵敏度参数进设置页
|
||||
|
||||
- 状态:DOING
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"""Build and verify a credential-free Silver Pose V1 release directory."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable
|
||||
|
||||
|
||||
class ReleaseError(ValueError):
|
||||
"""Raised when a release package cannot be safely created or verified."""
|
||||
|
||||
|
||||
_EXCLUDED_NAMES = {"config.local.json", "artifacts", "__pycache__", ".pytest_cache", "models"}
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def build_release_package(
|
||||
project_root: Path, model_path: Path, expected_sha256: str, output_dir: Path
|
||||
) -> Dict:
|
||||
"""Copy the V1 runtime and a locked model without local customer artifacts."""
|
||||
|
||||
root = Path(project_root).resolve()
|
||||
source_model = Path(model_path).resolve()
|
||||
output = Path(output_dir).resolve()
|
||||
expected = _validate_model(source_model, expected_sha256)
|
||||
if output.exists():
|
||||
raise ReleaseError("output directory already exists: {0}".format(output))
|
||||
|
||||
output.mkdir(parents=True)
|
||||
_copy_runtime_tree(root / "v1", output / "v1")
|
||||
_copy_required_file(root / "README.md", output / "README.md")
|
||||
_copy_required_file(root / "init.ps1", output / "init.ps1")
|
||||
_copy_required_file(root / "docs" / "demo-runbook.md", output / "docs" / "demo-runbook.md")
|
||||
destination_model = output / "v1" / "models" / "best.pt"
|
||||
destination_model.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(source_model), str(destination_model))
|
||||
_write_release_config(root / "v1" / "config.example.json", output / "v1", expected)
|
||||
manifest = _manifest(output, expected)
|
||||
(output / "release-manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8"
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def verify_release_package(package_dir: Path) -> Dict:
|
||||
package = Path(package_dir).resolve()
|
||||
manifest_path = package / "release-manifest.json"
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise ReleaseError("cannot read release manifest: {0}".format(exc))
|
||||
for relative_path, expected in manifest.get("files", {}).items():
|
||||
actual_path = package / relative_path
|
||||
if not actual_path.is_file() or sha256_file(actual_path) != expected:
|
||||
raise ReleaseError("release file SHA-256 does not match: {0}".format(relative_path))
|
||||
model = package / "v1" / "models" / "best.pt"
|
||||
if not model.is_file() or sha256_file(model) != manifest.get("model_sha256"):
|
||||
raise ReleaseError("model SHA-256 does not match release manifest")
|
||||
return manifest
|
||||
|
||||
|
||||
def _validate_model(model_path: Path, expected_sha256: str) -> str:
|
||||
expected = str(expected_sha256).lower()
|
||||
if len(expected) != 64 or any(char not in "0123456789abcdef" for char in expected):
|
||||
raise ReleaseError("expected model SHA-256 must be 64 hexadecimal characters")
|
||||
if not model_path.is_file():
|
||||
raise ReleaseError("model file does not exist: {0}".format(model_path))
|
||||
if sha256_file(model_path) != expected:
|
||||
raise ReleaseError("model SHA-256 does not match expected value")
|
||||
return expected
|
||||
|
||||
|
||||
def _copy_runtime_tree(source: Path, destination: Path) -> None:
|
||||
if not source.is_dir():
|
||||
raise ReleaseError("V1 source directory does not exist: {0}".format(source))
|
||||
shutil.copytree(
|
||||
str(source),
|
||||
str(destination),
|
||||
ignore=shutil.ignore_patterns(*_EXCLUDED_NAMES),
|
||||
)
|
||||
|
||||
|
||||
def _copy_required_file(source: Path, destination: Path) -> None:
|
||||
if not source.is_file():
|
||||
raise ReleaseError("required release file does not exist: {0}".format(source))
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(source), str(destination))
|
||||
|
||||
|
||||
def _write_release_config(source: Path, destination_dir: Path, model_sha256: str) -> None:
|
||||
raw = json.loads(source.read_text(encoding="utf-8"))
|
||||
raw["model"]["sha256"] = model_sha256
|
||||
(destination_dir / "config.release.example.json").write_text(
|
||||
json.dumps(raw, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _manifest(package: Path, model_sha256: str) -> Dict:
|
||||
files = {}
|
||||
for path in _files(package):
|
||||
files[path.relative_to(package).as_posix()] = sha256_file(path)
|
||||
return {
|
||||
"format": "silver-pose-v1-release-1",
|
||||
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||
"model_sha256": model_sha256,
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def _files(root: Path) -> Iterable[Path]:
|
||||
for path in sorted(root.rglob("*")):
|
||||
if path.is_file() and path.name != "release-manifest.json":
|
||||
yield path
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Build or verify a Silver Pose V1 release")
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
build = commands.add_parser("build")
|
||||
build.add_argument("--project-root", default=".")
|
||||
build.add_argument("--model-path", required=True)
|
||||
build.add_argument("--model-sha256", required=True)
|
||||
build.add_argument("--output", required=True)
|
||||
verify = commands.add_parser("verify")
|
||||
verify.add_argument("--package", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
if args.command == "build":
|
||||
manifest = build_release_package(
|
||||
Path(args.project_root), Path(args.model_path), args.model_sha256, Path(args.output)
|
||||
)
|
||||
print("release package built: {0}".format(args.output))
|
||||
print("model SHA-256: {0}".format(manifest["model_sha256"]))
|
||||
return 0
|
||||
verify_release_package(Path(args.package))
|
||||
print("release package verified: {0}".format(args.package))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,13 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ModelPath,
|
||||
[Parameter(Mandatory = $true)][string]$ModelSha256,
|
||||
[string]$Output = "dist/SilverPose-V1"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
Set-Location $projectRoot
|
||||
python -m v1.release build --project-root $projectRoot --model-path $ModelPath --model-sha256 $ModelSha256 --output $Output
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "V1 release package build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
|
||||
from v1.release import build_release_package, main, sha256_file
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_build_release_copies_locked_model_and_excludes_local_credentials(tmp_path):
|
||||
model = tmp_path / "best.pt"
|
||||
model.write_bytes(b"locked-model")
|
||||
output = tmp_path / "SilverPose-V1"
|
||||
|
||||
manifest = build_release_package(ROOT, model, sha256_file(model), output)
|
||||
|
||||
assert (output / "v1" / "models" / "best.pt").read_bytes() == b"locked-model"
|
||||
assert manifest["model_sha256"] == sha256_file(model)
|
||||
assert not list(output.rglob("config.local.json"))
|
||||
assert not list(output.rglob("events.jsonl"))
|
||||
|
||||
|
||||
def test_demo_runbook_covers_preflight_evidence_and_scope():
|
||||
text = (ROOT / "docs" / "demo-runbook.md").read_text(encoding="utf-8")
|
||||
|
||||
for heading in ("启动前检查", "现场演示步骤", "证据复查", "适用边界"):
|
||||
assert heading in text
|
||||
assert "rtsp://" not in text
|
||||
|
||||
|
||||
def test_release_command_verifies_a_built_package(tmp_path, capsys):
|
||||
model = tmp_path / "best.pt"
|
||||
model.write_bytes(b"locked-model")
|
||||
output = tmp_path / "SilverPose-V1"
|
||||
build_release_package(ROOT, model, sha256_file(model), output)
|
||||
|
||||
assert main(["verify", "--package", str(output)]) == 0
|
||||
assert "release package verified" in capsys.readouterr().out
|
||||
Reference in New Issue
Block a user