docs: initialize YoVision requirements and architecture

This commit is contained in:
QiuSW
2026-08-03 17:22:22 +08:00
commit 8a99b388c4
26 changed files with 6216 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
* text=auto
*.md text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.html text eol=lf
*.svg text eol=lf
*.png binary
*.pdf binary
+2
View File
@@ -0,0 +1,2 @@
_reference/
agent_sessions.txt
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+430
View File
@@ -0,0 +1,430 @@
# 📋 YoVision 智能视频事件平台 — 需求收集文档
> 版本 v0.3 · 2026-08-03
> 配套文档:《02-需求分析》《03-通用场景应用方案》
> 参考输入:`其他项目的文档/` 下三份跌倒检测项目文档(居家养老单场景),本文档将其经验泛化到多场景
---
# 1. 项目定位
## 1.1 一句话说明
**用开源 NVR 统一接入各类场景的 IP 摄像头,用深度学习模型识别场景中的指定人群与指定行为,命中规则的画面组装成「事件实例」推送到管理系统,预警信息按升级链推送到 App 与其他终端。**
## 1.2 与参考项目的关系
| 维度 | 参考项目(跌倒检测) | 本项目(YoVision) |
| --- | --- | --- |
| 场景 | 居家养老,单一 | 家庭 / 学校 / 社区 / 园区,多场景 |
| 检测目标 | 跌倒,单一行为 | 可配置的「人群 × 行为 × 区域 × 时段」组合 |
| 组织模型 | 家庭(扁平) | 租户 → 站点 → 区域 → 设备(多级、多租户) |
| 规模 | 200 户 / 250 路(历史参考项目) | **默认交付 16 路,单站点按 32 / 64 / 128 路横向扩展;本阶段上限 128 路** |
| 输出 | 跌倒告警 | 事件实例(结构化 + 证据)+ 分级预警 |
| 复用 | — | 接入层、对账器、推理流水线、预警状态机可整体复用 |
**结论:参考项目是本项目的一个场景实例。本项目要做的是把它拆成「通用底座 + 场景包」。** 这个判断决定了后续所有需求的分层方式。
## 1.3 容量决策(已定,2026-08-03)
| 项 | 决策 |
| --- | --- |
| 默认交付规格 | 单站点 **16 路视频设备** |
| 扩展档位 | 32 / 64 / 128 路 |
| 单站点本阶段上限 | **128 路**;超过 128 路需拆为多个逻辑站点或另行评估 |
| 扩容方式 | 增加媒体分片与推理 Worker,禁止靠单进程、单 GPU 硬扛 |
| 代码约束 | 16 只能是默认配额,数据库、规则、数组与循环中不得写死;配额必须可配置 |
推荐配置项:`site.max_video_channels` 默认 16、最大 128;`media_shard.max_streams` 与 `inference_profile.max_sources` 按硬件压测确定。平台管理、批量导入、监控和前端列表从第一版起按 128 路设计。
---
# 2. 干系人与诉求
## 2.1 干系人矩阵
| 干系人 | 角色 | 核心诉求 | 最怕什么 |
| --- | --- | --- | --- |
| 最终使用者(家属 / 校方安保 / 社区物业) | 预警接收与处置 | 该报的必报、报了必须有人知道 | 误报太多导致不再看 |
| 被监护/被拍摄对象(老人、学生、居民) | 数据主体 | 不被过度监视 | 隐私泄露、被"全天候盯梢" |
| 管理方(养老机构 / 学校 / 街道) | 运营与考核 | 事件可追溯、可统计、可导出 | 出事后拿不出记录 |
| 平台运维 | 系统可用性 | 设备在线率、告警不淹没 | 业务预警被运维噪声淹没 |
| 实施/交付 | 部署上线 | 装得快、开通批量化 | 摄像头型号五花八门装不上 |
| 算法团队 | 模型迭代 | 真实场景数据回流 | 只有公开数据集,与现场脱节 |
| 法务/合规 | 合规落地 | 知情同意、留存期限、权限边界 | 人脸识别越界 |
## 2.2 关键诉求冲突(需在需求分析阶段裁决)
| 冲突 | A 方 | B 方 | 初步倾向 |
| --- | --- | --- | --- |
| 全量录像 vs 隐私 | 管理方要"什么都能查" | 数据主体要"平时别看" | 事件驱动采集,非事件不上云 |
| 灵敏度 | 使用者要"绝不漏报" | 使用者要"别老误报" | 两级判定 + 误报反馈闭环 |
| 人脸识别 | 管理方要"认出是谁" | 合规要"最小必要" | **已定**:ReID 为默认手段,人脸按租户授权开启(见 §5.1) |
| 边缘算力成本 | 交付要"盒子便宜" | 算法要"跑得动模型" | 触发式推理,非常态全量 |
---
# 3. 需求来源与收集方法
| 来源 | 方法 | 状态 |
| --- | --- | --- |
| 参考项目文档 | 文档分析(三份 Notion 导出) | ✅ 已完成,见本文档全篇 |
| 场景走访 | 家庭 / 学校 / 社区各 2–3 个点位实地踏勘 | ⬜ 待执行 |
| 竞品与开源调研 | Frigate / ZoneMinder / Shinobi / MiBeeNvr / mediamtx / Savant | 🔶 NVR 选型已完成,见《03》§1.4;推理框架与商业竞品继续调研 |
| 硬件兼容性摸底 | 采购 3–5 款候选摄像头做 ONVIF 实测 | ⬜ 待执行(M0 出口) |
| 法务咨询 | 人脸识别、未成年人影像、留存期限 | ⬜ 待执行(阻塞 M3) |
| 现场误报采集 | 试点期真实误报案例库 | ⬜ 待执行(M3 首要产出) |
> **踏勘清单**(走访时必须带回的东西):点位平面图与摄像头安装高度/俯角、现场网络出口带宽与是否有公网 IP、现有 NVR/平台型号、值班人员的实际工作流(现在出事怎么处理)、他们目前最痛的三件事。
---
# 4. 场景与需求清单
## 4.1 场景总览
| 编号 | 场景 | 典型部署点 | 主要目标人群 | 网络特征 |
| --- | --- | --- | --- | --- |
| S1 | 居家养老 | 客厅、走廊 | 老人(独居/失能) | 家庭 NAT 后,带宽小,易掉线 |
| S2 | 校园 | 教学楼、操场、宿舍、围墙、实验室 | 学生、教职工、外来人员 | 校园专网,带宽充足,有机房 |
| S3 | 社区/物业 | 出入口、电梯、周界、楼道、垃圾站 | 住户、访客、陌生人 | 小区专网,可能有本地机房 |
| S4 | 园区/厂区(扩展) | 车间、仓库、施工区 | 员工、承包商、访客 | 企业网,合规要求相对宽松 |
## 4.2 各场景原始需求条目
需求编号规则:`RQ-<场景>-<序号>`。优先级:P0 必须 / P1 应该 / P2 可以。
### S1 居家养老
| 编号 | 需求 | 优先级 | 来源 |
| --- | --- | --- | --- |
| RQ-S1-01 | 检测老人跌倒并在 10s 内发出首次预警 | P0 | 参考项目 §10.10 |
| RQ-S1-02 | 长时间静止/无活动(如卫生间超时、多小时无位移)告警 | P0 | 参考项目 §10.4 `no_motion` |
| RQ-S1-03 | 夜间离床未归(起夜超时)告警 | P1 | 走访待确认 |
| RQ-S1-04 | 陌生人进入居所告警 | P1 | 走访待确认 |
| RQ-S1-05 | 卧室、卫生间**不得安装摄像头**,仅可用雷达等非成像传感器 | P0 | 参考项目 §3 隐私 |
| RQ-S1-06 | 平时不解码、不上传,仅事件片段上云 | P0 | 参考项目 §3 隐私 |
| RQ-S1-07 | 家属只能查看自己家、且与告警关联的片段 | P0 | 参考项目 §3 |
| RQ-S1-08 | 无人 ack 时逐级升级,最坏 3 分钟内有真人接手 | P0 | 参考项目 §10.3 |
### S2 校园
| 编号 | 需求 | 优先级 | 备注 |
| --- | --- | --- | --- |
| RQ-S2-01 | 打架/推搡/围观聚集检测 | P0 | 校园安全核心诉求 |
| RQ-S2-02 | 翻越围墙、攀爬护栏检测 | P0 | 周界 |
| RQ-S2-03 | 危险区域(实验室、配电房、水池、天台)非授权人员闯入 | P0 | 需区域 + 身份/角色判定 |
| RQ-S2-04 | 外来人员尾随进入校门 | P1 | 需与门禁事件融合 |
| RQ-S2-05 | 上课时段学生在楼道/校外徘徊 | P1 | 需课表时段配置 |
| RQ-S2-06 | 宿舍晚归/夜间离寝 | P1 | 需时段策略 |
| RQ-S2-07 | 学生跌倒/晕倒 | P1 | 复用 S1 能力 |
| RQ-S2-08 | 未成年人影像的采集与留存需单独合规方案 | P0 | 法务阻塞项 |
### S3 社区/物业
| 编号 | 需求 | 优先级 | 备注 |
| --- | --- | --- | --- |
| RQ-S3-01 | 周界入侵(翻墙、跨越警戒线) | P0 | |
| RQ-S3-02 | 电动车进入电梯/楼道 | P0 | 消防强诉求,目标非人 |
| RQ-S3-03 | 消防通道占用/堆物 | P0 | 静态变化检测,非人 |
| RQ-S3-04 | 高空抛物 | P1 | 需专用仰拍机位与轨迹算法,独立评估 |
| RQ-S3-05 | 陌生人在单元门口长时间徘徊 | P1 | 需 ReID + 停留时长 |
| RQ-S3-06 | 独居老人连续 N 天未出入 | P1 | 跨天统计,非实时 |
| RQ-S3-07 | 重点关注人员(黑名单)出现告警 | P1 | 强合规约束,需授权 |
### S4 园区/厂区(扩展,本期不实现)
未戴安全帽 / 未穿反光衣 / 区域入侵 / 离岗 / 吸烟明火。列出仅为验证架构可扩展性,M4 前不投入。
## 4.3 跨场景通用需求
### 4.3.1 接入与设备管理
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-C-01 | 支持 ONVIF/RTSP 标准 IP 摄像头接入,不绑定品牌 | P0 |
| RQ-C-02 | 支持摄像头位于 NAT 之后(家庭、无公网 IP 的小区)的接入 | P0 |
| RQ-C-03 | 设备身份用序列号而非 IP(DHCP/Wi-Fi 漫游会换 IP) | P0 |
| RQ-C-04 | 支持批量开通(脚本/CSV 导入);默认 16 路也不得依赖逐路手工配置,扩展到 128 路时操作量不得线性增加 | P0 |
| RQ-C-05 | 设备需有「待激活」中间态(批量开通时凭据待补) | P0 |
| RQ-C-06 | 设备探活与离线告警,离线超阈值主动通知 | P0 |
| RQ-C-07 | 开通时校时(ONVIF SetSystemDateAndTime),定期校准 | P0 |
| RQ-C-08 | 摄像头断线恢复后自动重连,不需人工干预 | P0 |
| RQ-C-09 | 架构需预留非视频传感器(毫米波雷达、门磁、紧急按钮)接入 | P1 |
### 4.3.2 分析与规则
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-C-10 | 检测能力与场景解耦,同一模型可被多场景规则复用 | P0 |
| RQ-C-11 | 规则支持「目标类型 × 属性/身份 × 区域 × 时段 × 行为 × 持续时长」组合 | P0 |
| RQ-C-12 | 规则可按租户/站点/单设备三级覆盖配置 | P0 |
| RQ-C-13 | 支持画区域(多边形)、画警戒线(方向性越线) | P0 |
| RQ-C-14 | 支持时段/日历(工作日、课表、节假日)策略 | P1 |
| RQ-C-15 | 规则变更不需重启推理流水线 | P1 |
| RQ-C-16 | 人脸识别/身份比对为**可选能力**,默认关闭,需按租户显式授权开启 | P0 |
### 4.3.3 事件实例
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-C-17 | 命中规则须生成事件实例,含:结构化元数据 + 抓拍图 + 视频片段 | P0 |
| RQ-C-18 | 视频片段须含事发前若干秒(pre-roll 回捞) | P0 |
| RQ-C-19 | 事件去重与防抖:同设备冷却、同站点聚合、已处置抑制 | P0 |
| RQ-C-20 | 事件推送到管理系统(Web),支持列表、筛选、详情、处置 | P0 |
| RQ-C-21 | 事件支持标记「误报」,误报样本自动进标注队列 | P0 |
| RQ-C-22 | 事件统计报表(按站点/类型/时段/处置结果) | P1 |
### 4.3.4 预警投递
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-C-23 | 预警须有 ack(确认)环节,未 ack 自动升级 | P0 |
| RQ-C-24 | 升级链、超时值、联系人、时段策略可按站点/租户配置 | P0 |
| RQ-C-25 | 至少两条独立投递路径,且需有绕过互联网的通道(电话/短信) | P0 |
| RQ-C-26 | 区分「已发出 / 已送达 / 已被看到」三个事实,无回执按失败处理 | P0 |
| RQ-C-27 | 支持 App 推送、短信、语音呼叫、Webhook、大屏/声光设备 | P0 |
| RQ-C-28 | 支持限时静默(维护窗口),**必须强制限时并自动恢复** | P0 |
| RQ-C-29 | 业务预警与运维告警**不得共用**投递通道与值班配置 | P0 |
| RQ-C-30 | 告警先落库再投递,进程重启后未完成的升级链能续跑 | P0 |
### 4.3.5 平台与运维
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-C-31 | 多租户隔离:数据、账号、配置、存储互不可见 | P0 |
| RQ-C-32 | RBAC:平台管理员 / 租户管理员 / 站点管理员 / 值班员 / 只读 | P0 |
| RQ-C-33 | 全链路审计日志,告警生命周期不可篡改留存 | P0 |
| RQ-C-34 | Prometheus 指标 + Grafana 看板(设备在线率、推理延迟、未收敛项) | P0 |
| RQ-C-35 | 支持边缘部署、中心部署、混合部署三种形态 | P1 |
| RQ-C-36 | 断网时边缘本地缓存事件,恢复后补传 | P0 |
---
# 5. 目标人群与检测能力清单
「指定人群」在实现上分三类,**合规风险与技术难度依次上升**,需求分析阶段必须逐条确认是否真的需要:
| 类别 | 判定依据 | 例子 | 是否需人脸 | 合规等级 |
| --- | --- | --- | --- | --- |
| A. 属性类 | 视觉外观属性 | 老人/儿童/成人、戴/未戴安全帽、穿工服 | 否 | 低 |
| B. 状态类 | 姿态与轨迹 | 跌倒、奔跑、聚集、徘徊、越线、滞留 | 否 | 低 |
| B+. 匿名同一性 | **ReID 跨镜再识别** | 同一人在此徘徊 10 分钟、跨机位跟随 | 否 | 中 |
| C. 身份类 | 人脸底库比对 | 住户/非住户、本校学生、黑白名单、具名人员 | **是** | **高** |
## 5.1 决策(已定,2026-08-01)
| 项 | 决策 |
| --- | --- |
| A、B 类 | P0,无争议 |
| **B+ ReID** | **默认手段**。所有"同一性"类需求(徘徊、尾随、跟随、跨镜追踪)一律优先用 ReID 实现 |
| **C 人脸识别** | **在方案范围内**,由客户/租户签署授权后启用;未授权租户系统层面不可开启 |
> **原则:能用 ReID 解决的,不上人脸。**
> ReID 产生的是会话内的匿名 track/person 标识,能回答"是不是同一个人在这徘徊 10 分钟",但不回答"他是谁"。它满足 RQ-S3-05(徘徊)、RQ-S2-04(尾随)等大部分需求,且不涉及生物识别信息的采集与存储。
>
> 人脸识别只用在**必须知道"是谁"**的需求上:白名单放行(本校学生/住户)、重点关注人员告警、访客登记核验。这类需求要逐条论证"为什么 ReID 不够"。
## 5.2 人脸识别启用的前置条件
授权不等于可以直接开。启用前必须齐备:
| # | 前置条件 | 责任方 |
| --- | --- | --- |
| 1 | 租户签署书面授权(含用途、范围、期限、底库来源合法性承诺) | 客户 + 法务 |
| 2 | 被采集人(或未成年人监护人)的知情同意取得方式与留档流程 | 客户 |
| 3 | 采集场所的告示义务已履行 | 客户 |
| 4 | 底库数据的来源、录入流程、删除流程已定义 | 产品 + 客户 |
| 5 | 底库加密存储、独立访问审计、最小授权已实现 | 研发 |
| 6 | 未成年人场景的专项合规意见 | 法务 |
> 校园场景(S2)涉及未成年人,即便有租户授权,第 6 项独立成阻塞条件。
## 5.3 派生需求(人脸识别)
| 编号 | 需求 | 优先级 |
| --- | --- | --- |
| RQ-FR-01 | 人脸能力按**租户**开关,未授权租户在 API 与 UI 层均不可见 | P0 |
| RQ-FR-02 | 底库人员档案管理:录入、分组(白名单/关注名单/访客)、有效期、注销 | P0 |
| RQ-FR-03 | 底库支持到期自动失效(如学生毕业、住户搬离、访客当日有效) | P0 |
| RQ-FR-04 | 人脸特征值加密存储,**不存原始人脸图**(或原图与特征分库、单独授权) | P0 |
| RQ-FR-05 | 每一次比对命中与查询均记入独立审计日志,可追溯操作人 | P0 |
| RQ-FR-06 | 比对不可用/未命中时规则仍须工作(降级为"未知人员"),**不得因此漏报** | P0 |
| RQ-FR-07 | 支持置信度阈值配置与人工复核(高风险动作前需二次确认) | P1 |
| RQ-FR-08 | 支持底库整体导出与彻底删除(客户终止服务时) | P0 |
| RQ-FR-09 | 人脸相关数据的留存期限独立于视频证据,可单独配置 | P1 |
---
# 6. 设备与环境约束调研
## 6.1 摄像头兼容性验证清单(沿用参考项目 M0 出口标准)
采购 3–5 个候选型号做实测,**这是整个项目第一件要做的事**。
### 必过项
| 验证项 | 通过标准 | 不通过的后果 |
| --- | --- | --- |
| GetProfiles | 返回至少两个 profile(主码流 + 子码流) | 无法自动获取子码流,需人工配置 |
| GetStreamUri | 返回可用的 RTSP URL | 无法自动化开通 |
| SetSystemDateAndTime | 调用成功且生效 | 事件时间戳不可信 |
| 子码流规格 | 可设为 640×360 或 704×576 左右 | 带宽和算力估算失效 |
| 编码格式 | 支持 H.264 | H.265 浏览器无法原生预览 |
| WS-Security 认证 | digest 或 usernametoken 任一可用 | 认证失败 |
| RTSP over TCP | 长时间稳定不断流 | 家庭/无线网络下 UDP 丢包严重 |
### 加分项
- 支持 ONVIF 事件订阅(移动侦测)—— 可作为触发式推理的一级信号
- 支持修改 GOP 长度 —— 影响 pre-roll 回捞的关键帧间隔
- 支持创建独立只读用户 —— 不必用 admin 凭据接入
- 支持 ONVIF Profile T / 双向音频 —— 社区场景的远程喊话
> 任何一项必过项失败的型号直接从采购清单划掉。**后期写兼容代码的成本远高于换型号。** 验证结果整理成表格,作为采购白名单归档。
## 6.2 网络环境差异
| 场景 | 摄像头可达性 | 上行带宽 | 结论 |
| --- | --- | --- | --- |
| 家庭 | NAT 后,无公网 IP | 20–50 Mbps 上行,共享 | 必须边缘侧主动推流,中心不可主动拉 |
| 校园 | 专网,通常有机房 | 充足 | 可中心部署,也可校内边缘部署 |
| 社区 | 专网,有无机房不确定 | 视物业投入 | 混合,按项目定 |
## 6.3 边缘硬件候选
| 平台 | 算力 | 适用 | 说明 |
| --- | --- | --- | --- |
| RK3588 | 6 TOPS NPU | 家庭单户、小型点位 | 无风扇、低噪、低功耗;模型需转 RKNN |
| Jetson Orin Nano/NX | 20–100 TOPS | 校园/社区中型边缘 | 与 Savant/DeepStream 原生契合 |
| x86 + 消费级 GPU | 高 | 中心机房 | ⚠️ GeForce 卡**同时硬件编码流数上限为 3**,做可视化回推时会卡死,需 L4/Quadro 类专业卡 |
---
# 7. 已知约束与前提
## 7.1 技术约束(来自参考项目实测经验,直接继承)
| 约束 | 后果 | 出处 |
| --- | --- | --- |
| mediamtx 通过 API 改的配置**不落盘**,重启即失效 | 设备台账必须在自有数据库,启动时 replay | 参考 §4.1 |
| 任何 path 配置变更会**踢掉当前发布者** | 改配置前必须比对哈希,没变绝不调 API | 参考 §4.1 |
| mediamtx 没有 enable/disable,只能删 path | 停用要靠鉴权回调返回 401 + kick | 参考 §4.1 |
| mediamtx 无 camera 实体,只有 path | 设备业务面必须自建 | 参考 §4.1 |
| 批量 API 调用无事务 | 只能幂等重试,不能回滚 | 参考 §6.2 |
| `sourceOnDemand` 下"挂上推理 source = 产生 reader = 开始拉流" | 暂停分析必须卸载 source,不能只在插件里 return | 参考 §6.4 |
| Savant 绑定 NVIDIA 硬件 + DeepStream 版本 | 硬件选型受限,需预留 Pipeless 备选 | 参考 §9.1 |
## 7.2 许可约束
| 组件 | 许可 | 影响 |
| --- | --- | --- |
| mediamtx / gortsplib | MIT | ✅ 可商用 |
| Kerberos Agent | MIT | ✅ 可商用 |
| GoAlert | Apache-2.0 | ✅ 可商用 |
| Savant | 开源 | ✅ 需复核具体条款 |
| **Ultralytics YOLO**(v8/v11,含 -pose) | **AGPL-3.0** | ⚠️ 已决策沿用 YOLO,**须购买 Enterprise License**,见下 |
| YOLOX (Megvii) | Apache-2.0 | ✅ YOLO 家族内的宽松许可选项,作为不购授权时的替代 |
| RTMPose / RTMO (MMPose) | Apache-2.0 | ✅ 姿态备选,COCO 17 点定义与 YOLO-pose 相同,判定算法可直接复用 |
| 人脸识别模型(InsightFace 等) | 视具体模型而定 | ⚠️ 需逐个核对;部分模型权重仅限非商业用途 |
## 7.2.1 模型许可决策(已定,2026-08-01)
**决策:继续使用 YOLO。**
由此产生的必办事项:
| # | 事项 | 时限 | 说明 |
| --- | --- | --- | --- |
| 1 | 购买 **Ultralytics Enterprise License** | **M3 前必须完成** | 本项目是面向付费用户的网络服务,落在 AGPL-3.0 第 13 条(网络交互)覆盖范围。不购授权而严格执行 AGPL,需开源整个服务端 |
| 2 | 授权覆盖范围核对 | 采购时 | 确认覆盖:私有化交付的每个客户实例、边缘盒子内的模型、二次训练产出的权重 |
| 3 | 若采购未获批,启用替代路径 | M3 前 | 检测换 **YOLOX**(Apache-2.0,仍属 YOLO 家族),姿态换 **RTMPose**(Apache-2.0,COCO 17 点,判定算法不用改) |
| 4 | 人脸识别模型许可单独核对 | 启用人脸能力前 | 常见开源人脸模型的权重许可与代码许可可能不一致 |
> 事项 3 是逃生通道,不是默认路径。把它写进文档是为了让"授权没批下来"不至于卡死 M3——迁移代价可控(关键点定义相同)。
## 7.3 合规约束(开工前需法务确认,**阻塞 M3**)
- [ ] 视频/图像证据的最长保存期限
- [ ] 各角色可查看范围的法定边界(家属能看什么、物业能看什么、校方能看什么)
- [ ] 被拍摄者(尤其被监护老人、未成年学生)的知情同意取得方式与留档
- [ ] 公共区域摄像头的告示牌义务
- [ ] 数据出境/第三方云服务使用的限制
**人脸识别专项**(客户已确认将授权使用,但授权 ≠ 合规完成,见 §5.2):
- [ ] 租户授权书模板(用途、范围、期限、底库来源合法性承诺)
- [ ] 人脸信息的单独同意取得方式(生物识别信息通常需要单独同意,不能与一般隐私政策打包)
- [ ] 底库人员的知情权、查询权、删除权的响应流程与时限
- [ ] **未成年人场景的专项意见**(S2 校园,独立阻塞项)
- [ ] 人脸数据的留存期限(独立于视频证据)
- [ ] 比对日志的留存与查阅权限
- [ ] 客户终止服务时的底库彻底删除义务与证明方式
---
# 8. 待确认问题清单
需要与客户/产品明确的开放问题,**答案会实质改变架构**:
| # | 问题 | 影响 |
| --- | --- | --- |
| Q1 | 首期落地哪一个场景?三个都做还是选一个打穿? | 决定 M1–M4 的取舍与工期 |
| Q2 | 是私有化交付(一个客户一套)还是 SaaS 多租户? | 决定多租户隔离深度、计费、升级方式 |
| ~~Q3~~ | ~~单个项目典型规模(路数)与总体目标规模?~~ **已答:默认 16 路,按 32 / 64 / 128 路横向扩展,单站点本阶段上限 128 路。见 §1.3** | ✅ 已闭环 |
| Q4 | 是否已有存量 NVR/平台需要对接(GB/T 28181 国标)? | 国标接入是独立工作量,需单列 |
| Q5 | 是否必须支持国产化信创环境(海光/鲲鹏 + 昇腾/寒武纪)? | 决定推理框架能否用 DeepStream/Savant |
| Q6 | 「管理系统」是本项目自研还是对接客户现有系统? | 决定前端工作量与 API 契约 |
| Q7 | 「App」是自研还是嵌入客户已有 App?推送用什么厂商通道? | 决定移动端工作量 |
| ~~Q8~~ | ~~是否需要人脸识别(C 类身份判定)?~~ **已答:需要,客户将授权。默认仍以 ReID 为主,人脸用于必须知道"是谁"的需求。见 §5.1–5.3** | ✅ 已闭环 |
| Q8-a | 人脸能力先在**哪个场景**落地?底库规模量级? | 决定底库存储与比对性能方案 |
| Q8-b | 底库数据由客户提供还是系统现场采集? | 决定录入模块工作量与合规责任划分 |
| Q9 | 家庭场景是否同步引入毫米波雷达做一级触发? | 决定 GPU 数量能否降一个数量级 |
| Q10 | 语音呼叫/短信是否有既定供应商? | 决定投递层对接工作量 |
| Q11 | 事件视频存哪(客户 NAS / 对象存储 / 公有云)?留多久? | 决定存储成本与合规 |
| Q12 | 验收标准如何定义?准确率/召回率的可接受阈值? | ⚠️ 必须在合同前定,见 §9 |
---
# 9. 验收期望(需求收集阶段的初步共识)
## 9.1 效果类指标的表述方式
> ⚠️ **不要在合同里承诺"跌倒检测准确率 ≥ 95%"这类数字。** 参考项目已记录的教训:公开数据集(Le2i / UR Fall)全是演员摆拍的表演性跌倒,真实老人多为缓慢滑落、部分遮挡,两者分布完全不同。在拿到现场基线之前,任何准确率承诺都无法验证。
建议表述方式:
| 阶段 | 承诺形式 |
| --- | --- |
| 试点期(M3) | 交付**现场实测基线报告**:真实召回率、每路每天误报数、误报归因分类 |
| 运营期 | 承诺**误报收敛趋势**(如:运营 3 个月后每路每天误报 ≤ N 次)而非绝对准确率 |
| 全程 | 承诺**系统性 SLA**(延迟、可用性、投递成功率),这些是可测可控的 |
## 9.2 系统性 SLA 目标(可承诺,待 M3 实测校准)
| 指标 | 目标 |
| --- | --- |
| 事件发生 → 首次投递 | < 10s |
| 首次投递 → 送达回执 | < 5s(推送)/ < 30s(短信) |
| 全链路无人响应 → 人工介入 | < 3min(S1 场景)/ 按场景配置 |
| 告警丢失率 | 0(持久化后才确认触发) |
| 设备在线率 | ≥ 99%(排除用户侧断网断电) |
| 平台可用性 | ≥ 99.5% |
---
# 10. 下一步
| 动作 | 责任方 | 阻塞什么 |
| --- | --- | --- |
| 回答 §8 剩余问题(Q1–Q7、Q8-a/b、Q9–Q12) | 产品 / 客户 | 需求分析定稿 |
| 采购 3–5 款摄像头做 §6.1 实测 | 硬件 / 交付 | M0 出口、所有接入开发 |
| 法务确认 §7.3 通用五项 | 法务 | M3 上线 |
| **法务确认 §7.3 人脸专项七项 + 出具授权书模板** | 法务 | **人脸能力启用(不阻塞 M3 主线)** |
| **采购 Ultralytics Enterprise License(§7.2.1)** | 采购 / 技术负责人 | **M3 前必须完成,否则走 YOLOX 替代路径** |
| 核对人脸识别模型的权重与代码许可 | 算法 | 人脸能力启用 |
| 三个场景各走访 2–3 个点位 | 产品 | 场景包定义 |
---
> 本文档只负责**收集与澄清**,不做方案裁决。需求的分层、取舍与规格化见《02-需求分析》;技术实现路径见《03-通用场景应用方案》。
+792
View File
@@ -0,0 +1,792 @@
# 🔍 YoVision 智能视频事件平台 — 需求分析文档
> 版本 v0.3 · 2026-08-03
> 上游:《01-需求收集》 · 下游:《03-通用场景应用方案》
> 本文档负责把原始需求**分层、规格化、发现矛盾、推导出架构性约束**,不负责技术选型细节
---
# 1. 分析结论摘要
先给结论,后面是推导过程。
| # | 结论 | 依据 |
| --- | --- | --- |
| C1 | 系统必须切成「**通用底座 + 场景包**」两层,场景包是配置与插件,不是分支代码 | 三个场景 60% 需求重合,但检测能力与升级策略完全不同(§3) |
| C2 | 事件(Event)与预警(Alert)是**两个独立实体**,不可合并 | 一次事件可产生多次预警、多条投递;一次预警可聚合多个事件(§5.2) |
| C3 | 「指定人群」的判定分四类并在**能力层解耦**;**ReID 为默认手段,人脸识别为按租户授权开启的可插拔能力** | 合规风险与技术依赖不同;多数"同一性"需求不需要知道"是谁"(§4.3、§4.4) |
| C4 | 规则引擎必须放在**推理之后、告警之前**的独立环节,不能编进模型插件 | 规则变更频率远高于模型,且需支持热更新与多级覆盖(§6) |
| C5 | 接入层必须**控制面/数据面分离**,且数据面为边缘主动推流 | 家庭场景摄像头在 NAT 后,中心不可达(§7.1) |
| C6 | 系统状态一致性靠**水平触发的对账器**,不靠事件驱动 | 三个下游系统(媒体路由、推理、存储)无分布式事务(§7.3) |
| C7 | 默认 16 路可用单媒体/推理分片起步;扩展到 128 路必须**横向分片**,触发式推理用于降低平均算力 | GPU 数量不能由路数直接推导,须按真实模型、分辨率、FPS、batch 与硬件压测(§8.1–8.2) |
| C8 | 业务预警系统的核心是 **ack 与升级**,不是"发消息" | 没有 ack 环节的告警系统等于没有(§9.2) |
| C9 | **误报反馈闭环是产品飞轮,必须在 M3 第一天就有**,不是后期优化项 | 真实误报案例是最稀缺的训练数据(§10) |
---
# 2. 需求分层
原始需求(《01》共 8 个场景需求组 + 36 条通用需求)按变化频率重新分层。**同一层内的东西一起变,不同层之间靠契约解耦。**
```mermaid
flowchart TD
L5["L5 场景包<br/>规则模板 · 升级策略 · 话术 · 报表口径"]
L4["L4 业务编排<br/>事件生成 · 去重防抖 · 预警状态机 · 处置流"]
L3["L3 能力层<br/>检测 · 姿态 · 跟踪 · ReID · 属性 · (人脸)"]
L2["L2 流水线层<br/>解码 · 抽帧 · 调度 · 故障隔离 · 触发式唤醒"]
L1["L1 接入层<br/>设备台账 · ONVIF · 媒体路由 · 隧道 · 对账"]
L0["L0 基础设施<br/>多租户 · RBAC · 存储 · 可观测 · 审计"]
L5 --> L4 --> L3 --> L2 --> L1 --> L0
```
| 层 | 变化频率 | 谁在改 | 改动方式 |
| --- | --- | --- | --- |
| L5 场景包 | 每周 | 实施/产品 | 配置,不发版 |
| L4 业务编排 | 每月 | 后端 | 发版 |
| L3 能力层 | 每季度 | 算法 | 模型热替换 |
| L2 流水线 | 半年 | 平台 | 发版 |
| L1 接入 | 半年 | 平台 | 发版 |
| L0 基础设施 | 年 | 平台 | 发版 |
> **这张表是本文档最重要的产出。** 如果一个需求变更需要同时改 L5 和 L1,说明分层错了。典型反例:把"上课时段"这个校历概念写进 mediamtx 的 path 配置。
---
# 3. 场景共性与差异分析
## 3.1 共性度量
把《01》§4.2 的场景需求逐条拆成原子能力,统计复用度:
| 原子能力 | S1 家庭 | S2 校园 | S3 社区 | 复用 |
| --- | :---: | :---: | :---: | :---: |
| 人体检测 + 跟踪 | ✅ | ✅ | ✅ | 3/3 |
| 姿态关键点 | ✅ | ✅ | ✅ | 3/3 |
| 跌倒判定 | ✅ | ✅ | ✅ | 3/3 |
| 区域入侵 / 越线 | ○ | ✅ | ✅ | 2/3 |
| 停留 / 徘徊时长 | ✅ | ✅ | ✅ | 3/3 |
| 静止 / 无活动 | ✅ | ○ | ✅ | 2/3 |
| 人群聚集 / 密度 | — | ✅ | ○ | 1.5/3 |
| 打架 / 剧烈动作 | — | ✅ | ○ | 1.5/3 |
| 攀爬 / 翻越 | — | ✅ | ✅ | 2/3 |
| 非人目标(电动车、堆物) | — | ○ | ✅ | 1.5/3 |
| ReID 跨镜跟踪(匿名) | ○ | ✅ | ✅ | 2.5/3 |
| 身份比对(人脸) | ○ | 🔐 | 🔐 | 授权后启用 |
| 高空抛物 | — | — | ✅ | 1/3 |
✅ 核心 ○ 可选 — 不需要 🔐 需租户授权 + 前置条件齐备(§4.4)
**结论**:前 6 项覆盖了三个场景的 P0 需求主体,构成**通用能力包**;后续按场景增量。高空抛物(RQ-S3-04)需要专用仰拍机位与独立轨迹算法,与其余能力无复用,**建议独立立项或后期外采,不进本期通用底座**。
## 3.2 差异分析(差异才是场景包要装的东西)
| 维度 | S1 家庭 | S2 校园 | S3 社区 |
| --- | --- | --- | --- |
| 单点位路数 | 1–3 | 100–500 | 50–300 |
| 部署形态 | 边缘盒子(必须) | 校内边缘或中心 | 混合 |
| 摄像头可达性 | NAT 后 | 专网可达 | 专网可达 |
| 预警接收人 | 家属(非专业、随时可能没看手机) | 安保值班室(7×24 有人) | 物业值班(夜间可能无人) |
| 升级链 | 4 级,含语音电话与呼叫中心 | 2 级,值班室 → 校领导 | 3 级,值班 → 主管 → 业主群 |
| 超时容忍 | 30s / 60s / 90s | 2min / 10min | 1min / 5min |
| 误报容忍 | **极低**(家属会卸载 App) | 中(有人筛) | 中 |
| 隐私敏感度 | **极高** | 高(未成年人) | 中 |
| 常态录像 | ❌ 禁止上云 | ✅ 允许 | ✅ 允许 |
> **关键洞察**:S1 的接收人是非专业个人,没有值班室兜底,所以升级链必须做到"穿透勿扰模式"级别;S2/S3 有值班室,升级链可以简化,但需要**批量事件的看板与工单**能力(S1 完全不需要)。**这两种形态的 UI 和交互是两个东西,不要试图用一套界面覆盖。**
---
# 4. 领域模型
## 4.1 核心实体关系
```mermaid
erDiagram
TENANT ||--o{ SITE : "拥有"
SITE ||--o{ AREA : "划分"
SITE ||--o{ DEVICE : "部署"
DEVICE ||--o{ STREAM_BINDING : "产生"
AREA ||--o{ ZONE : "标注(多边形/警戒线)"
DEVICE ||--o{ ZONE : "画在画面上"
TENANT ||--o{ RULE : "定义"
RULE }o--|| SCENE_PACK : "继承自"
RULE ||--o{ EVENT : "命中生成"
EVENT ||--o{ EVIDENCE : "附带"
EVENT }o--o{ ALERT : "触发/聚合"
ALERT ||--o{ DELIVERY : "投递"
ALERT ||--o{ ALERT_LOG : "审计"
SITE ||--o{ CONTACT_CHAIN : "配置"
ALERT }o--|| CONTACT_CHAIN : "按级投递"
```
## 4.2 实体职责界定
| 实体 | 定义 | 关键约束 |
| --- | --- | --- |
| **Tenant 租户** | 计费与数据隔离的最小单位 | 所有查询必须带 tenant_id,无例外 |
| **Site 站点** | 一个物理场所(一户 / 一所学校 / 一个小区) | 升级链、时段策略挂在这一层 |
| **Area 区域** | 站点内的逻辑分区(教学楼 / 单元 / 客厅) | 用于权限范围与报表口径 |
| **Device 设备** | **不叫 Camera**,`device_type` 取 camera/radar/door/button | 为异构传感器预留(继承参考项目 §4.5) |
| **Zone 画面区域** | 画在某路画面上的多边形或警戒线 | 与 Area 是两回事:Area 是物理概念,Zone 是像素坐标 |
| **Rule 规则** | 「主体 × 条件 × 时空 × 动作」的可配置组合 | 三级覆盖:租户默认 → 站点 → 设备 |
| **Event 事件** | 一次被判定成立的客观发生 | **不可变**。误判只能被标记,不能被删改 |
| **Alert 预警** | 一次需要人响应的通知过程 | 有生命周期状态机,可被 ack |
| **Delivery 投递** | 一次向某人某通道的发送尝试 | **独立实体**,一次 Alert 有多条 |
## 4.3 「指定人群」的建模
《01》§5 提出四类判定。分析后确定为**主体过滤器(Subject Filter)**,作用于跟踪目标而非规则整体:
```
Track(跟踪目标)
├─ class: person | vehicle | e_bike | object
├─ attributes: {age_group, posture, ppe, ...} ← A 类,模型直接产出
├─ states: {falling, running, loitering_sec, ...} ← B 类,时序分析产出
├─ anon_id: "pid_88213" | null ← B+ 类,ReID 匿名同一性,默认开
└─ identity: {ref_id, group, confidence} | null ← C 类,人脸,按租户授权开
```
| 判定类 | 实现位置 | 默认 | 开启条件 | 回答什么问题 |
| --- | --- | --- | --- | --- |
| A 属性 | 检测模型多头输出 / 独立分类器 | 开 | — | 他是什么样的人 |
| B 状态 | 时序窗口分析插件 | 开 | — | 他在做什么 |
| **B+ ReID** | ReID 模型,跨镜特征匹配 | **开(默认手段)** | — | **是不是同一个人** |
| C 身份 | 独立身份服务,异步比对后回填 | **关** | 租户授权 + §4.4 全部前置条件 | 他是谁 |
**设计原则(已定)**:
1. **能用 ReID 解决的不上人脸。** 徘徊、尾随、跟随、跨镜追踪这类"同一性"需求一律走 B+ 类。人脸只用于必须知道"是谁"的需求(白名单放行、关注名单告警、访客核验),且需逐条论证"为什么 ReID 不够"。
2. **`identity` 为 null 时规则必须仍能工作**,降级为"未知人员"。身份服务不可用、比对超时、人脸角度不佳都会产生 null——**不得因此漏报**。这条决定了规则 DSL 必须显式声明 `on_unavailable` 语义(见《03》§2.5)。
3. **`anon_id` 与 `identity` 是两个独立字段**,不可互相顶替。ReID 的 anon_id 只在会话/时间窗内有效,不是身份标识,也不应被持久化成长期人员画像。
## 4.4 身份识别(人脸)子系统的需求分析
客户已确认将授权使用人脸识别,因此该能力**进入方案范围**。但授权解决的是"能不能用",不解决"怎么用才安全"。分析出以下约束:
### 4.4.1 授权是租户级开关,不是全局开关
| 层级 | 语义 |
| --- | --- |
| 平台级 | 能力是否编译/部署(私有化交付时可整体不部署) |
| **租户级** | **主开关**。未授权租户在 API 与 UI 层均不可见该能力,不是"能看到但点不动" |
| 站点级 | 在已授权租户内进一步限定生效范围(如只在校门口,不在教室) |
| 规则级 | 单条规则是否使用 identity 过滤 |
> 未授权租户"看得到但用不了"是错误设计——它会诱导客户询问和绕过,也会让审计难以证明"该租户从未启用"。
### 4.4.2 底库是独立的高敏资产
| 需求 | 理由 |
| --- | --- |
| 底库与业务库**逻辑分离**,独立加密 | 泄露后果与一般业务数据不在一个量级 |
| **默认只存特征向量,不存原始人脸图** | 原图泄露可直接冒用;特征向量至少不可直接还原为可用照片。若业务必须留原图(如核验展示),原图单独分库 + 单独授权 |
| 人员档案必须有**有效期** | 学生毕业、住户搬离、访客当日有效——没有有效期的底库必然越积越脏 |
| 到期自动失效,不依赖人工清理 | 依赖运维记得清理是不可靠的 |
| 支持整体导出与彻底删除 | 客户终止服务时的义务 |
| 每次比对命中与每次底库查询均独立审计 | "谁查了谁"必须可追溯 |
### 4.4.3 比对是异步旁路,不在主判定链路上
```
主链路(必须实时):检测 → 跟踪 → 姿态/行为 → 规则 → 事件
↑
旁路(可延迟、可失败):人脸检测 → 质量筛选 → 特征提取 → 底库比对 → 回填 identity
```
**推导**:若把比对放进主链路,一次底库慢查询会拖垮整条 pipeline 的实时性;而人身安全类事件(跌倒、打架)根本不需要知道是谁。
派生需求:
| 编号 | 需求 |
| --- | --- |
| FR-ID-01 | 比对为异步旁路,超时/失败不阻塞事件生成 |
| FR-ID-02 | identity 回填有时间窗;超窗未回填则事件以 `identity: null` 定稿 |
| FR-ID-03 | 依赖 identity 的规则须声明 `on_unavailable`:`treat_as_unknown`(默认,倾向报警)或 `skip`(倾向不报) |
| FR-ID-04 | 人脸质量筛选(角度、清晰度、遮挡、尺寸)前置,低质量帧不送比对,避免误配 |
| FR-ID-05 | 置信度阈值可配;高风险动作(如触发告警、拒绝放行)前可要求人工复核 |
> FR-ID-03 的两个取值对应两种业务倾向。**默认必须是 `treat_as_unknown`**:在监护与安防场景,"不确定是谁"应当倾向于报警而非放过。若某条规则选 `skip`,需在规则上显式写明并有审批记录。
### 4.4.4 误识别的后果分级
人脸比对有两种错误,后果完全不同,阈值不能用同一个:
| 错误类型 | 场景 | 后果 | 阈值倾向 |
| --- | --- | --- | --- |
| 假阳性(认错人) | 白名单放行 | 陌生人被当成住户放行 → 安防失效 | **阈值调高**,宁可认不出 |
| 假阳性(认错人) | 关注名单告警 | 无辜者被当成关注对象告警 → **对个人的实质伤害** | **阈值调最高 + 强制人工复核** |
| 假阴性(认不出) | 白名单放行 | 住户被当成陌生人 → 体验差,但安全 | 可容忍 |
| 假阴性(认不出) | 关注名单告警 | 漏报 | 靠 ReID 与其他规则兜底 |
> **关注名单类规则不得自动触发对人的处置动作**(如自动锁门、自动通报),只能生成待人工确认的事件。这是产品红线,写进 L4 编排层的硬约束。
---
# 5. 核心流程分析
## 5.1 主流程
```mermaid
flowchart TD
A["摄像头 / 传感器"] --> B["接入层<br/>台账 · 隧道 · 媒体路由"]
B --> C{"触发判定<br/>常态 / 运动 / 传感器"}
C -->|无触发| C
C -->|唤醒| D["推理流水线<br/>检测 · 跟踪 · 姿态 · 属性"]
D --> E["规则引擎<br/>主体过滤 × 时空 × 条件"]
E -->|未命中| F["丢弃<br/>仅记指标"]
E -->|命中| G["事件生成器<br/>回捞 pre-roll · 抓拍 · 元数据"]
G --> H{"去重 / 防抖 / 聚合"}
H -->|抑制| I["静默记录"]
H -->|通过| J["事件实例落库"]
J --> K["管理系统<br/>看板 · 详情 · 处置"]
J --> L{"是否需要预警"}
L -->|是| M["预警状态机<br/>投递 → ack → 升级"]
M --> N["App / 短信 / 语音 / Webhook / 大屏"]
N --> O["处置结果回填"]
K --> O
O --> P["误报标记 → 标注队列 → 模型迭代"]
```
## 5.2 为什么 Event 和 Alert 必须分开(C2 的推导)
反例:把预警状态直接放在事件表上。会立刻遇到三个无法表达的情况:
| 情况 | 合并模型下的困境 |
| --- | --- |
| 客厅两个机位拍到**同一次**跌倒 | 两条事件、两次预警,家属收到两遍 |
| 消防通道堆物**持续存在** | 每帧都命中规则,要么事件爆炸,要么无法表达"仍未整改" |
| 一次事件在**不同时间**升级到不同人 | 事件表上放不下多级投递记录 |
| 值班员**批量确认** 20 条事件 | ack 语义作用在哪个粒度说不清 |
正确关系:
```
Event N ──┐
├──> Alert 1 ──> Delivery N
Event N ──┘
```
- **Event**:一次客观发生,不可变,是证据与统计的单位
- **Alert**:一次需要人响应的过程,可聚合多个事件,是 SLA 与考核的单位
## 5.3 预警生命周期状态机
```
[触发] ─→ [投递中] ─→ [已送达] ─→ [已确认 ack] ─→ [处置中] ─→ [关闭]
│ │ │ │
│ │ └─超时未 ack─→ [升级] ─→ 下一级投递 │
│ └─投递失败─→ [升级] │
└─防抖命中─→ [抑制] [标记误报] ←───┘
```
**硬约束(继承参考项目 §10.10)**:告警**先落库再投递**。进程崩溃重启后,扫描所有非终态告警并接续升级链——与 §7.3 对账器的"水平触发"是同一个思路。
## 5.4 三个不同的事实,不可混淆
| 事实 | 含义 | 字段 | 拿不到时的处理 |
| --- | --- | --- | --- |
| 发出去了 | 调用了上游 API | `sent_at` | — |
| 送达了 | 上游回执确认到达终端 | `receipt_at` | **当失败处理并升级**,不做乐观假设 |
| 被看到了 | 用户主动 ack | `acked_at` | 超时升级 |
---
# 6. 规则引擎需求分析
## 6.1 为什么必须独立(C4 的推导)
| 如果把规则写进模型插件 | 后果 |
| --- | --- |
| 客户要改一个停留时长阈值 | 要改 Python 代码、重启 pipeline、影响同机所有路 |
| 不同租户要不同阈值 | 插件里写 if tenant == ... |
| 要审计"这条事件是哪条规则判的" | 无从追溯 |
| 要做规则的灰度/回滚 | 做不了 |
**结论**:模型插件只输出**客观观测**(这里有个人、他的姿态是躺倒、他在多边形 A 内停留了 12 秒),规则引擎负责**主观判定**(这属于需要报警的情况)。这条边界要写进代码规范。
## 6.2 规则表达能力需求
一条规则必须能表达的五个维度:
| 维度 | 内容 | 例 |
| --- | --- | --- |
| **主体** | class + 属性过滤 + 身份过滤 | 人 且 未识别为本校学生 |
| **时空** | 哪些设备/Zone + 哪些时段/日历 | 实验室 B 区 且 非上课时段 |
| **条件** | 观测量的逻辑组合 + 持续时长 | 进入区域 且 停留 > 3s 且 无教师同行 |
| **抑制** | 冷却期、聚合键、静默窗口 | 同区域 5 分钟内不重复 |
| **动作** | 事件等级 + 证据规格 + 通知策略 | 高危 + 前 10s 后 15s + 升级链 A |
## 6.3 三级覆盖模型
```
场景包默认规则 ──继承──> 租户规则 ──覆盖──> 站点规则 ──覆盖──> 设备规则
```
覆盖语义必须明确定义(否则运维会被"为什么这条没生效"折磨死):
| 字段类型 | 覆盖语义 |
| --- | --- |
| 标量(阈值、时长) | 就近覆盖 |
| 列表(联系人、通道) | **替换**,不合并(合并会导致意外扩大通知面) |
| 开关(enabled) | 就近覆盖,且允许下级关闭上级下发的规则 |
| Zone 坐标 | 只能在设备级定义,上级不可下发 |
## 6.4 规则变更的非功能要求
| 要求 | 理由 |
| --- | --- |
| 热生效,不重启 pipeline | 一次重启影响同机全部路数 |
| 变更有版本与操作人记录 | 事故复盘时"谁把阈值改了" |
| 支持"试运行"模式:命中但不通知 | 新规则上线前评估误报量 |
| 支持按规则统计命中量与误报率 | 规则质量可度量,否则只能靠感觉调 |
> **试运行模式是被低估的需求。** 没有它,每条新规则上线都是拿真实用户做实验。
---
# 7. 接入层需求分析
## 7.1 NAT 约束推导(C5)
家庭场景摄像头在运营商 NAT + 家庭路由 NAT 之后,中心机房**无法主动发起连接**。校园/社区虽有专网,但不能假设每个项目都有公网 IP。
分析出的唯一可行结构:**控制面与数据面分离**(直接继承参考项目 §2.1)
| 平面 | 方向 | 通道 | 流量 |
| --- | --- | --- | --- |
| 控制面 | 中心 → 设备 | 边缘侧 WireGuard 隧道 | 极小,仅配置与探活 |
| 数据面 | 边缘 → 中心 | 边缘主动推流(RTSP announce / SRT / RTMP) | 大 |
派生需求:
| 编号 | 需求 |
| --- | --- |
| FR-ACC-01 | 每个站点分配不冲突的内网段,设备地址确定化(如 `10.<站点高位>.<站点低位>.0/24`,摄像头从 `.10` 起) |
| FR-ACC-02 | 控制面失联**不得**中断视频推流(两者已解耦),仅告警 |
| FR-ACC-03 | 边缘侧断网时本地缓存事件与证据,恢复后补传 |
| FR-ACC-04 | 推流鉴权走 HTTP 回调到自有服务,注销/欠费/隐私模式在业务库判断,媒体层零配置变更 |
## 7.2 设备身份与状态机
| 需求 | 理由 |
| --- | --- |
| 设备身份用**序列号**,不用 IP | DHCP 续租、Wi-Fi 漫游换 AP 都会换 IP |
| IP 变化时按序列号自动重定位并重连 | 家庭场景常态 |
| 设备状态含 `pending_activation` | 批量开通必然有凭据待补的中间态 |
| 探活用独立的 ONVIF 轻量调用,**不用流状态代替设备状态** | 「设备离线」与「无人订阅」在媒体层表现相同 |
设备状态机:
```
[录入] → [pending_activation] ──补齐凭据──> [active] ⇄ [offline]
│ │
└──────────────> [disabled] <──┘
```
## 7.3 对账器:一致性方案(C6 的推导)
一次设备开通需要在**多个独立系统**各做一次操作,且它们之间**没有分布式事务**:
```
数据库(唯一真相源)
├──> 媒体路由:建 path
├──> 推理流水线:挂 source
└──> 存储:建 bucket/前缀
```
### 为什么必须是水平触发而非事件驱动
| 方案 | 部分失败时 |
| --- | --- |
| 事件驱动(边沿触发) | 事件丢了就永久不一致,需要补偿事务,补偿也会失败 |
| **水平触发(对账)** | 不处理"事件",只不断比对期望与实际并向期望收敛。失败自动在下一轮重试 |
### 派生的硬性需求
| 编号 | 需求 | 理由 |
| --- | --- | --- |
| FR-REC-01 | 数据库是唯一真相源,下游均为被驱动的从属状态 | — |
| FR-REC-02 | 建 path 前先查是否存在,绝不盲目 add | 盲目 add 会踢掉正在推流的 publisher |
| FR-REC-03 | 改配置前比对 `desired_hash` / `applied_hash`,没变**绝不**调 API | 同上 |
| FR-REC-04 | 每个下游系统一行 sync_state,全部收敛才算开通成功 | 中间态必须可见 |
| FR-REC-05 | 开通顺序:媒体路由先 → 推理后;停用顺序:推理先 → 媒体路由后 | 建立与拆除逆序,写进代码注释 |
| FR-REC-06 | 部分失败**不回滚,只收敛**(退避重试) | 回滚本身会失败,导致回滚的回滚,无限递归 |
| FR-REC-07 | 每个下游系统独立限流预算(各 8 并发),不共享信号量 | 一个系统变慢会饿死另一个 |
| FR-REC-08 | 孤儿资源清理需有**安全闸**:单轮删除量 > 总量 10% 时停止并告警 | 防止"期望集合意外为空"把全部路数拆掉 |
| FR-REC-09 | 不设放弃阈值,持续重试 + 告警引入人工 | 对账器放弃 = 设备静默不工作,监护场景不可接受 |
| FR-REC-10 | 循环周期:正常 30s,有未收敛项降到 5s,另有 10min 全量对账(含孤儿扫描) | — |
### 必须暴露的指标
| 指标 | 用途 |
| --- | --- |
| `reconciler_unconverged_bindings{system}` | 核心健康指标,持续 > 0 即异常 |
| `reconciler_orphans_detected{type}` | 有人手改了生产系统 |
| `reconciler_safety_brake_triggered` | 安全闸触发,**应当直接呼人** |
| `reconciler_loop_duration_seconds` | 循环超时 = 规模到顶,该分片了 |
---
# 8. 非功能需求分析
## 8.1 容量模型
本项目以**单站点 16 路为默认交付规格**,支持 32 / 64 / 128 路扩展,单站点本阶段上限 128 路。16 是默认配额而非代码上限;128 路通过增加媒体分片与推理 Worker 实现,不把全部流压在一个进程或一块 GPU 上。
以下仅用于初步预算,最终以 M1–M5 的真实流水线压测为准:
| 项 | 默认 16 路 | 上限 128 路 | 说明 |
| --- | ---: | ---: | --- |
| 子码流 640×360 @ 512 kbps | ≈ 8 Mbps | ≈ 64 Mbps | 推理优先使用子码流 |
| 主码流按 4–6 Mbps | ≈ 64–96 Mbps | ≈ 512–768 Mbps | 常态录像与证据回捞的预算口径 |
| 抽帧 5 fps | 80 帧/秒 | 640 帧/秒 | GPU、解码器和 batch 均需实测 |
| 媒体分片 | 1 个 | 建议 4 个起步 | `media_shard.max_streams` 初始建议 32;可按故障域降为 16 |
| 推理分片 | 1 个逻辑分片 | 最多 8 个 16 路逻辑分片 | 一个 Worker 可承载几个分片由压测决定 |
容量必须按四个维度分别验收:接入/录像带宽、同时解码、AI 推理、证据存储。不能用“NVR 能录 128 路”推导“GPU 能分析 128 路”。连续录像时每 1 Mbps 约产生 10.8 GB/天;因此默认由客户已有 NVR 承担常态录像,YoVision 优先保存事件证据,避免重复存储全量视频。
> **实现约束**:`site.max_video_channels` 默认 16、最大 128;`media_shard.max_streams` 与 `inference_profile.max_sources` 为配置项。数据库、规则、数组、前端分页与批量操作中不得写死 16。
## 8.2 算力与触发式推理(C7 的推导)
| 模式 | 默认 16 路 | 扩展到 128 路 | 适用 |
| --- | --- | --- | --- |
| 常态全量推理 | 单逻辑推理分片起步 | 按 16 路切成最多 8 个逻辑分片,分配到多个 Worker | 高价值点位 |
| **触发式推理** | 保持同一分片结构,空闲时不解码/不推理 | 通过降低各分片占空比减少平均 GPU 需求 | 家庭、社区大部分点位 |
GPU 型号和数量不在需求阶段写死。M1 用 5 路打通,M3 以 16 路建立性能基线,再依次验证 64 路和 128 路;每档都以端到端 P95/P99 延迟、丢帧率和故障隔离为出口标准。
触发信号的三个来源(按成本与可靠性排序):
| 触发源 | 成本 | 可靠性 | 适用 |
| --- | --- | --- | --- |
| 运动侦测(软件,参考 Frigate 思路) | 零 | 中(光照变化、树叶晃动会误触) | 通用兜底 |
| 摄像头 ONVIF 事件订阅 | 零 | 中 | 支持的型号 |
| **毫米波雷达等传感器** | 硬件成本 | 高 | 家庭卧室/卫生间(且解决隐私) |
> **重要澄清**:流水线框架(Savant 等)主要解决多路调度、复用与故障隔离,不会自动降低单帧推理成本。触发式推理是降低平均算力的主要手段之一,但最终容量仍取决于模型、分辨率、FPS、batch、解码方式与硬件实测。
## 8.3 两级判定与误报(准确性需求的可实现形式)
单一信号源的误报都会让使用者很快失去信任:
| 信号源 | 典型误判 |
| --- | --- |
| 纯视觉 | 遮挡、逆光、坐下、弯腰、宠物、访客 |
| 纯雷达 | 快速坐下、弯腰 |
**两级串联是行业标准做法**:
- 一级(低成本、常开):宁可多报,不能漏报
- 二级(高成本、按需唤醒):审核一级的候选,剥离干扰
派生需求:架构必须支持"一级触发源"可插拔(运动侦测 / ONVIF 事件 / 雷达 / 门磁),且触发入口独立成 handler,接新传感器时不改动流控制逻辑。
## 8.4 分类粒度对误报的影响
参考实现的经验:`GajuuzZ/Human-Falling-Detect-Tracks` 对每个人轨迹每 30 帧预测动作,输出**站立/行走/坐下/躺下/起身/坐下动作/跌倒共 7 类**。
> **七分类而非二分类是降误报的关键。** 二分类模型面对"坐下"只能在"跌倒/非跌倒"之间硬选;七分类可以明确输出"这是坐下"。本项目所有行为判定模型均按此原则设计。
**负面经验**:`Y-B-Class-Projects/Human-Fall-Detection` 用约 500 张网络图片训 LSTM 完全学不会,最终退回 if-else 规则。→ **小数据量下时序模型学不动,先解决数据再上模型。**
## 8.5 可用性与降级
| 故障 | 要求 | 降级行为 |
| --- | --- | --- |
| 单路摄像头掉线 | 不影响其他路 | 告警 + 自动重连 |
| 推理节点崩溃 | 不影响接入与其他节点 | 该分片流量转移或降级为仅录制 |
| 算力不足 | 不得积压至崩溃 | **实时模式:主动丢帧优于积压** |
| 突发流量(早晨集中活动) | 不丢事件 | 持久化磁盘缓冲,抗突发 |
| 中心平台不可达 | 不丢事件 | 边缘缓存,恢复后补传 |
| 通知供应商故障 | 不丢预警 | 多供应商冗余,`provider` 字段为切换预留 |
## 8.6 安全需求
| 编号 | 需求 |
| --- | --- |
| NFR-SEC-01 | 设备凭据(ONVIF 用户名密码)加密存储,不落明文日志 |
| NFR-SEC-02 | 推流鉴权,未授权设备推不上来 |
| NFR-SEC-03 | 所有查询强制带 tenant_id,在数据访问层统一拦截,不依赖业务代码自觉 |
| NFR-SEC-04 | 证据文件访问用带时效的签名 URL,不可枚举 |
| NFR-SEC-05 | 管理界面不得对局域网默认无密码暴露(第三方面板的常见坑) |
| NFR-SEC-06 | 人脸底库(若启用)独立加密存储,独立审计,独立授权 |
## 8.7 合规需求
| 编号 | 需求 |
| --- | --- |
| NFR-CMP-01 | `alerts` 与 `alert_deliveries` **只追加不修改**,状态变更写事件表,独立备份 |
| NFR-CMP-02 | 每条预警须能回答:何时触发、基于什么证据、向谁发过、走什么通道、是否送达、谁在何时确认、处置结果、升到第几级、每级耗时 |
| NFR-CMP-03 | 证据留存期限可配置,到期自动清理并留清理记录 |
| NFR-CMP-04 | 数据主体的知情同意记录可查 |
| NFR-CMP-05 | 隐私区域(卧室、卫生间)在设备录入时即标记,系统层面拒绝配置摄像头类设备 |
> NFR-CMP-05 是**代码层面的硬约束**,不是流程约束。依赖实施人员"记得别装"是不可靠的。
---
# 9. 预警系统需求分析
## 9.1 两类告警必须彻底分开
| | 运维告警 | 业务预警 |
| --- | --- | --- |
| 触发源 | 设备离线、对账未收敛、GPU 过载、缓冲积压 | 规则命中 |
| 收件人 | 运维团队 | 家属 / 值班员 / 呼叫中心 |
| 延迟容忍 | 分钟级 | **秒级** |
| 漏报后果 | 服务降级 | 人身伤害 + 法律责任 |
| 方案 | Prometheus + Alertmanager(现成) | **自研状态机 + 复用投递通道** |
> **两套系统不得共用投递通道和值班配置**,否则运维噪声会淡化业务预警。这是本节最重要的一条。
## 9.2 ack 是核心,不是附加功能(C8)
「发出去」和「被人看到」是两回事。整个预警系统的设计围绕 **确认或升级** 这一对概念展开:
- 无 ack → 无法知道是否有人在处理 → 无法决定是否升级 → 升级链形同虚设
- 无 ack → 无法度量响应时长 → 无法做 SLA 与考核
- 无 ack → 出事后无法举证"我们报了且某人确认了"
## 9.3 升级链需求
| 需求 | 说明 |
| --- | --- |
| 层级数与超时值按站点可配 | 独居老人与有同住家属的策略应当不同 |
| 时段策略 | 白天找子女、夜间找同住者;校园白天找安保、夜间找宿管 |
| 通道冗余 | 至少两条独立路径,**且需有绕过互联网的通道** |
| 联系人可用性 | 支持轮值排班,而非固定人 |
**通道独立性分析**:推送依赖 APNs/FCM,短信依赖运营商,任何单一通道都可能整体故障。语音电话是唯一能穿透勿扰模式且不依赖互联网的通道,**在 S1 场景是必需项**。
## 9.4 去重与防抖
| 规则 | 参数 | 理由 |
| --- | --- | --- |
| 同设备冷却期 | 默认 5min | 避免同一事件多次触发 |
| 同站点聚合 | 多设备同时触发合并为一条 | 两个机位拍到同一次跌倒 |
| 已处置抑制 | 已 ack 期间不再新建同类 | 人已到现场 |
| 维护窗口 | 用户主动静默 | **必须强制限时(建议最长 4h)并到期前提醒** |
> **永久静默是这类系统最常见的事故成因。** 静默功能的限时是硬约束,不提供"永久"选项。
---
# 10. 数据闭环需求(C9)
## 10.1 为什么这是 P0 而非优化项
| 事实 | 后果 |
| --- | --- |
| 公开数据集(Le2i / UR Fall 等)是演员摆拍的表演性动作 | 与真实场景分布不符,训出来的模型现场表现差 |
| 真实误报案例是最稀缺的训练数据 | 只能靠运营积累,无法采购 |
| 没有闭环,模型不会随运营时间变好 | 产品没有护城河 |
## 10.2 闭环设计
```
用户点「误报」→ 自动把 evidence(视频片段 + 结构化观测序列)推进标注队列
用户确认「有效」→ 样本进正例库
↓
定期评估 → 规则阈值调整 / 模型重训 → 灰度上线(试运行模式)
```
**要求:从 M3 第一天就开始存,不要等到想训模型时才发现没数据。** `outcome = true_positive` 的样本同样是金矿。
## 10.3 派生的数据需求
| 编号 | 需求 |
| --- | --- |
| FR-DAT-01 | 事件证据须包含**结构化观测序列**(关键点、包围框、track_id 时序),不只是视频 |
| FR-DAT-02 | 误报标记须记录归因(遮挡/光照/宠物/坐下/其他),供分类统计 |
| FR-DAT-03 | 标注队列与训练数据的导出需符合 NFR-CMP-03 的留存与脱敏要求 |
| FR-DAT-04 | 每条规则的命中量、误报率可查询,作为规则质量指标 |
---
# 11. 数据模型(分析层)
在参考项目基础上扩展多租户与规则/事件。**字段级设计见《03》,此处只给结构与关键约束。**
```sql
-- ── 组织(Bell 为真相源;Sense 只读配额投影)────────
tenants(id, code, name, status, plan, created_at)
sites(id, tenant_id, code, name, scene_pack, subnet CIDR, timezone, status,
max_video_channels INT NOT NULL DEFAULT 16 CHECK (max_video_channels BETWEEN 1 AND 128))
areas(id, site_id, name, parent_id)
-- ── 设备(不叫 cameras,为异构传感器预留)────
devices(
id, tenant_id, site_id, area_id,
device_type, -- camera | radar | door | button
serial UNIQUE, -- ⚠️ 身份用序列号,不用 IP
vendor, model,
onvif_addr, onvif_user, onvif_secret, -- secret 加密存储
state, -- pending_activation | active | offline | disabled
privacy_flag, -- 隐私区域标记,为 true 时拒绝 camera 类型
last_seen_at
)
stream_bindings(id, device_id, mtx_instance, inference_shard, path_name, rtsp_url, profile_token, enabled,
UNIQUE(mtx_instance, path_name))
-- ── 对账(每下游系统一行)──────────────
sync_state(binding_id, system, desired_hash, applied_hash,
last_error, attempt, retry_after, updated_at,
PRIMARY KEY(binding_id, system))
-- system ∈ {'mediamtx','inference','storage'}
-- 全部行 desired_hash = applied_hash 才算真正开通
-- ── 规则 ──────────────────────────────
scene_packs(id, code, name, version, spec JSONB) -- 场景包模板
zones(id, device_id, name, geom_type, coords JSONB) -- 画面区域,仅设备级
rules(id, tenant_id, site_id, device_id, -- 三级覆盖,逐级可为 NULL
scene_pack_id, code, name, enabled, dry_run, -- dry_run = 试运行
spec JSONB, version, updated_by, updated_at)
rule_versions(rule_id, version, spec, updated_by, updated_at) -- 变更审计
-- ── 事件(不可变)──────────────────────
events(
id, tenant_id, site_id, device_id, rule_id, rule_version,
kind, severity, confidence,
occurred_at, detected_at, -- 发生时刻 vs 判定时刻,两者都要
subject JSONB, -- track_id / 属性 / anon_id(ReID) / identity + identity_status
observation JSONB, -- 触发时的客观观测量
evidence_uri, snapshot_uris,
dedup_key, aggregated_into, -- 聚合关系
outcome, -- true_positive | false_positive | unknown
outcome_reason, outcome_by, outcome_at
)
-- ── 身份识别(独立库/独立 schema,独立加密与审计)────
-- 仅在租户授权后创建;未授权租户无此 schema 的任何数据
tenant_features(tenant_id, feature, enabled, authorized_by,
authorized_at, expires_at, doc_uri) -- feature='face_recognition'
face_libraries(id, tenant_id, site_id, name, purpose, retention_days)
-- purpose ∈ {'whitelist','watchlist','visitor'};不同 purpose 的阈值与处置策略不同
persons(id, library_id, external_ref, display_name,
group_tags, valid_from, valid_until, -- ⚠️ 有效期必填,到期自动失效
consent_ref, -- 知情同意留档指针
created_by, created_at, revoked_at)
face_vectors(id, person_id, embedding VECTOR, model_ver,
quality_score, source, created_at) -- 默认只存向量
face_images(id, person_id, image_uri, created_at) -- 可选,单独授权才写入
face_match_logs(id, tenant_id, site_id, device_id, at,
person_id, score, threshold, decision, -- matched / below_threshold / no_candidate
event_id, reviewed_by, reviewed_at) -- 只追加,独立审计
-- ── 预警 ──────────────────────────────
alerts(id, tenant_id, site_id, kind, severity, state,
triggered_at, acked_by, acked_at, closed_at,
escalation_level, contact_chain_id)
alert_events(alert_id, event_id) -- 多对多
contact_chains(id, site_id, level, contact_name, channels JSONB,
active_hours, timeout_sec, UNIQUE(site_id, level))
alert_deliveries(id, alert_id, level, channel, provider,
sent_at, receipt_at, failed_reason)
alert_logs(id, alert_id, at, actor, from_state, to_state, detail JSONB) -- 只追加
CREATE INDEX ON alerts(state) WHERE state NOT IN ('closed','suppressed');
```
**关键约束复述**:
1. `devices` 而非 `cameras`——为雷达/门磁/按钮预留
2. 设备身份用 `serial`——IP 会变
3. `sync_state` 每系统一行——无分布式事务,中间态必须可见
4. `alert_deliveries` 是独立实体——一次预警多条投递,各自有回执
5. `events` 与 `alert_logs` 只追加——合规要求
6. `events.subject` 中 `anon_id`(ReID)与 `identity`(人脸)是两个独立字段,不可互相顶替
7. 人脸相关表独立 schema、独立加密、独立审计;`persons.valid_until` 必填,无"永久有效"选项
8. `sites.max_video_channels` 默认 16、最大 128;配额在写入设备时校验,分片容量由运行时配置与压测决定
9. `sites.max_video_channels` 由 Bell 持有,Sense 通过版本化内部 API 或只读投影校验设备新增/启用;不得跨 schema 直接写入
---
# 12. 优先级与里程碑映射
## 12.1 MoSCoW
| 级别 | 内容 |
| --- | --- |
| **Must** | 摄像头接入与台账、NAT 穿透、对账器、人体检测+跟踪+姿态、跌倒/区域入侵/停留三类规则、事件实例与证据、预警状态机与升级链、多租户与 RBAC、审计、误报反馈闭环 |
| **Should** | 触发式推理、**ReID(匿名同一性,默认手段)**、时段/日历策略、试运行模式、统计报表、边缘缓存补传、Grafana 看板 |
| **Could** | **人脸识别子系统**(租户授权开启:底库管理、异步比对旁路、审计)、人群聚集/打架检测、非人目标(电动车/堆物)、雷达接入、国标 GB/T 28181 对接、大屏与声光联动 |
| **Won't(本期)** | 高空抛物、S4 园区场景、车牌识别、**人脸在未成年人场景(S2)的启用**(待专项合规意见) |
> **人脸识别的排期说明**:客户已确认授权,能力进入方案范围,但它**不在主线关键路径上**(§4.4.3 已论证比对是异步旁路)。因此定为 Could 并安排在 M5——这样 M3 的端到端主线不被合规流程与底库模块阻塞。ReID 提到 Should 并在 M4 交付,先把"同一性"类需求兜住。
## 12.2 里程碑与出口标准
| 阶段 | 内容 | 出口标准 |
| --- | --- | --- |
| **M0** | 摄像头兼容性验证 + 需求定稿 | 可直接运行 MiBeeNvr 作为隔离实验室测试台;3–5 款型号跑通 GetProfiles/GetStreamUri/SetSystemDateAndTime,采购白名单归档;M0 代码不得作为生产基线;《01》§8 的 Q1–Q12 有答案 |
| **M1** | 接入骨架 + 媒体路由 | **MediaMTX 正式进入且不得由完整 MiBeeNvr 替代**;5 路自动建 path、探活、断线重建;设备台账可用 |
| **M2** | 对账器 + 多租户 + 隧道 | 10 个站点试点,至少一个站点完成 16 路开通/停用全流程;`unconverged = 0` 稳定 |
| **M3** | 推理接入 + 规则引擎 + 事件与预警 | **默认 16 路**端到端稳定运行;跑通一个场景的三类规则;拿到现场误报基线,反馈闭环已收数据 |
| **M4** | 64 路分片 + 灰度扩容 + 管理系统 | 64 路稳定运行;单分片故障不影响其他分片;看板、批量操作与处置流可用 |
| **M5** | 128 路容量验收 + 第二/第三场景包 + 触发式推理 + **人脸识别子系统** | **128 路通过横向分片稳定运行,扩容不改业务代码**;场景包为纯配置交付;人脸能力在一个已授权租户上跑通 |
| **M6(阶段二)** | 异构传感器接入 + 两级判定 | 误报率显著下降,视频常态采集下降 |
> **M5 的出口标准是本项目的架构验收点。** 如果新增场景需要改 L1–L4 的代码,说明 §2 的分层没做成,需要返工而不是继续叠加。
---
# 13. 风险清单
| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| **Ultralytics YOLO 商业授权未按时到位** | 已决策沿用 YOLO;未购授权则落在 AGPL-3.0 第 13 条,需开源整个服务端 | **M3 前完成采购**;逃生通道:检测换 YOLOX(Apache-2.0,同属 YOLO 家族)、姿态换 RTMPose(COCO 17 点,判定算法不用改) |
| 授权范围未覆盖全部形态 | 私有化实例、边缘盒子内模型、二次训练权重可能不在授权内 | 采购时逐项书面确认 |
| **人脸误识别造成对个人的实质伤害** | 无辜者被当成关注对象告警 | §4.4.4 分级阈值;关注名单类规则**不得自动触发对人的处置动作**,只生成待人工确认事件 |
| **人脸底库泄露** | 生物识别信息泄露,后果与一般数据不在一个量级 | 独立 schema + 独立加密 + 默认只存特征向量不存原图 + 独立访问审计 |
| 底库越积越脏(毕业/搬离人员未清) | 误配率上升,合规风险累积 | `valid_until` 必填,到期自动失效,无"永久有效"选项 |
| 人脸能力拖累主线工期 | M3 被合规流程阻塞 | §4.4.3 比对为异步旁路;排期在 M5,不进 M3 关键路径 |
| 人脸模型权重许可与代码许可不一致 | 侵权 | 启用前逐个核对权重许可 |
| **公开数据集与真实场景脱节** | 模型上线后表现远低于论文指标 | M3 首要产出是**真实误报案例库**,不是模型指标;不承诺绝对准确率 |
| 摄像头 ONVIF 实现差异 | 部分型号无法接入 | 采购前真机验证(M0),白名单管控 |
| 摄像头时钟漂移 | 事件时间戳全乱,审计失效 | 开通时校时,定期校准 |
| NAT 隧道不稳 | 控制面失联 | 已与数据面解耦,告警但不阻断推流 |
| path reload 踢流 | 用户看到卡顿 | 变更前比对哈希,变更集中在维护窗口 |
| H.265 兼容性 | 浏览器无法原生预览 | 统一设为 H.264 |
| **隐私合规不过** | 项目无法落地 | 尽早引入触发式采集减少常态视频;隐私区域代码级硬约束;法务前置 |
| 推理框架绑定 NVIDIA | 硬件选型受限,信创环境不可用 | 推理代码与框架解耦,预留 ONNX Runtime / Pipeless 备选 |
| 场景包泛化失败,变成三套代码 | 维护成本三倍 | M5 出口标准强制验收 |
| 值班员被误报淹没后忽略全部告警 | 真实事件被漏 | 试运行模式 + 规则质量指标 + 误报率看板 |
| 客户要求"什么都能查"的全量录像 | 与隐私需求冲突且成本高 | 需求收集期即明确事件驱动采集的边界 |
---
# 14. 需求追溯矩阵(节选)
| 原始需求 | 分析结论 | 实现层 | 里程碑 |
| --- | --- | --- | --- |
| RQ-C-02 NAT 后接入 | C5 控制面/数据面分离 | L1 | M1/M2 |
| RQ-C-03 序列号身份 | §7.2 设备状态机 | L1 | M1 |
| RQ-C-11 规则组合表达 | C4 独立规则引擎,§6.2 五维度 | L4 | M3 |
| RQ-C-12 三级覆盖 | §6.3 覆盖语义表 | L4/L5 | M3 |
| RQ-C-16 人脸按租户授权 | C3 四类判定;§4.4 租户级主开关 | L3 | M5 |
| RQ-FR-01 未授权租户不可见 | §4.4.1 不是"看得到点不动" | L0/L3 | M5 |
| RQ-FR-03 底库有效期自动失效 | §4.4.2 无"永久有效" | L3 | M5 |
| RQ-FR-04 只存特征不存原图 | §4.4.2 | L3 | M5 |
| RQ-FR-06 不可用时不得漏报 | §4.3 原则 2 + FR-ID-03 `on_unavailable` | L3/L4 | M5 |
| RQ-S3-05 陌生人徘徊 | §4.3 原则 1:走 ReID,不上人脸 | L3 | M4 |
| RQ-S2-04 尾随进校 | 同上,ReID + 门禁事件融合 | L3/L4 | M4 |
| RQ-C-17 事件实例 | C2 Event/Alert 分离 | L4 | M3 |
| RQ-C-19 去重防抖 | §9.4 四条规则 | L4 | M3 |
| RQ-C-21 误报标记 | C9 数据闭环 | L4 | **M3(不可延后)** |
| RQ-C-23 ack 与升级 | C8 预警状态机 | L4 | M3 |
| RQ-C-25 通道独立 | §9.3 语音电话必需 | L4 | M3 |
| RQ-C-28 限时静默 | §9.4 硬约束,无永久选项 | L4 | M3 |
| RQ-C-29 两类告警分离 | §9.1 | L0/L4 | M2 |
| RQ-C-30 先落库再投递 | §5.3 硬约束 | L4 | M3 |
| RQ-C-31 多租户 | §11 强制 tenant_id 拦截 | L0 | M2 |
| RQ-S1-05 隐私区域禁摄像头 | NFR-CMP-05 代码级约束 | L0/L1 | M2 |
| RQ-S1-08 3 分钟人工介入 | §9.3 升级链,站点级配置 | L5 | M3 |
| RQ-S2-08 未成年人合规 | §7.3 法务阻塞项 | — | M0 |
| RQ-S3-04 高空抛物 | §3.1 无复用,建议独立立项 | — | 移出本期 |
---
> 本文档的分析结论(§1 的 C1–C9)是《03-通用场景应用方案》的输入约束。任何技术选型若与这些结论冲突,应先回来修改本文档,而不是在方案里悄悄绕过。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,122 @@
# 🏛️ 目标客户群体 01 — 政府购买的居家养老监护
> 版本 v0.2 · 2026-08-03
> 市场定位:中国南方四线城市 · 付费方:民政局 / 卫健委(2G)
> 对应场景包:**S1 居家养老包**(《03-通用场景应用方案》§3.2)
---
# 1. 一句话定位
**卖给民政局的"独居老人跌倒无人知"问责解决方案,政府按户付费,老人和家属零付费。**
# 2. 客户画像
## 2.1 付费方与决策链
| 角色 | 单位 | 在决策中的位置 |
| --- | --- | --- |
| **出钱** | 民政局(养老服务科)、卫健委(老龄健康科) | 项目预算的申请与执行方 |
| 拍板 | 分管副市长 / 民政局长 | 试点项目签字 |
| 使用 | 街道 / 社区居家养老服务中心、签约照护员 | 日常接收预警、上门处置 |
| 受益 | 独居 / 留守 / 特困老人及其外地子女 | 不付费,但满意度影响项目续约 |
| 验收 | 民政局 + 第三方评估机构 | 决定运营费能否逐年续 |
**决策链特点**:2G 项目制。入口是"进入当年智慧养老/适老化改造的试点名单",出口是"验收报告 + 老人/家属满意度"。销售周期 6–12 个月,但一旦进入名单,单子完整、账期可预期。
## 2.2 为什么是四线城市的高付费群体
| 因素 | 说明 |
| --- | --- |
| **人口结构** | 四线城市是子女外流最严重的地方,独居/留守老人比例**高于一二线**,而照护人力更稀缺 |
| **问责刚性** | 特困供养、低保独居老人跌倒/离世多日无人发现,是当地民政系统的**问责事件**。付费动机是"不出事",不是改善型消费 |
| **资金来源明确** | 居家养老信息化专项、特殊困难老年人家庭适老化改造补贴(每户有定额)、长护险试点资金、福彩公益金 |
| **竞争洼地** | 头部安防厂商的重心在一二线智慧城市大单,四线城市的养老信息化项目往往由本地集成商拼凑交付,缺认真做算法闭环的供应商 |
## 2.3 客户的真实痛点(按客户语言)
1. "上面要求特困独居老人**每日探访**,我们人手根本跑不过来" → 系统替代日常巡访,异常才派人
2. "去年隔壁县出了老人去世一周才发现的事,通报了" → 无活动检测 + 升级链兜底
3. "以前装的摄像头就是个录像机,出了事才回头查" → 主动预警 vs 事后取证的差异,是方案讲解的核心
4. "老人嫌摄像头对着自己,子女也嫌" → **隐私设计是成交点**:卧室卫生间只装雷达、平时不上传、只推事件片段(《03》S1 包已有)
# 3. 卖什么
## 3.1 产品组合
| 层 | 内容 | 对应能力 |
| --- | --- | --- |
| 户内硬件 | 边缘盒子(RK3588) + 客厅摄像头 1–2 路 + (二期)卧室/卫生间毫米波雷达 | 《03》§3.2 部署形态,一户一盒互不影响 |
| 平台 | 事件管理系统(街道/社区端) + 处置工单 | L4 编排 + 管理系统 |
| 终端 | 家属 App(推送+ack) + 照护员端 + 民政驾驶舱大屏 | 《03》§5.4 |
| 服务 | 7×24 预警升级链、误报持续收敛、设备探活与上门维护 | 运营费的核心内容 |
**驾驶舱大屏是 2G 项目的必配项**——验收委员会看的就是它。辖区地图、在保老人数、今日预警数、平均响应时长,这些指标《03》§5.1 的 Prometheus 体系天然就有,前端做一层皮即可。
## 3.2 核心规则集(S1 包直接复用)
| 规则 | 预警对象 | 备注 |
| --- | --- | --- |
| 跌倒 | 照护员 → 家属 → 社区 → 呼叫中心 | 核心卖点,3 分钟必有人接手 |
| 长时间无活动 | 同上 | **对"去世多日无人发现"的直接回应,2G 场景权重高于跌倒** |
| 夜间离床未归 | 照护员 | 二期,需雷达 |
| 连续 N 天无活动量 | 社区网格员 | 跨天统计,替代每日探访打卡 |
> 注意与商业养老院场景的区别:这里的第一响应人是**社区照护员/网格员**而非家属——很多服务对象的子女在外地,升级链的 level 0 要按项目实际配置,这正是《03》§2.7 升级链按站点可配的意义。
## 3.3 收费模型
| 项 | 模式 | 说明 |
| --- | --- | --- |
| 建设费 | 一次性,按户 | 设备 + 安装 + 平台部署。对齐当地适老化改造的每户补贴定额来定价 |
| 运营费 | 按户/按年 | 预警服务 + 维护 + 呼叫中心。**这是利润和续约的关键,合同里必须与建设费分开** |
| 扩展 | 按新增户数滚动 | 试点 100–200 户 → 全区特困老人 → 普通独居老人自愿加装 |
> ⚠️ 只做一次性建设费的智慧养老项目,交付即死亡(设备一年后离线一半,验收后无人管)。**谈判底线:没有运营费的单子不接**,否则误报反馈闭环(《02》C9)收不到数据,产品也无从迭代。
# 4. 怎么打
## 4.1 销售路径
```
本地民政系统关系/合作伙伴
→ 摸清当年智慧养老、适老化改造预算盘子
→ 免费给 10–20 户特困老人试装(对齐 M2 试点)
→ 用 1–2 个真实处置案例(跌倒被及时发现)做汇报材料
→ 进入试点名单,拿 100–200 户示范项目
→ 验收 + 满意度调查 → 次年滚动扩面 + 邻县复制
```
## 4.2 打单要点
| 要点 | 做法 |
| --- | --- |
| 真实案例 > 技术参数 | 汇报材料的第一页是"X 月 X 日 X 街道张奶奶跌倒,系统 8 秒预警,照护员 6 分钟到场",不是准确率数字 |
| 隐私主动讲 | "卧室卫生间不装摄像头、平时视频不出户"写进方案首页——这是打消老人和子女抵触的关键,也是与"装监控"类竞品的差异 |
| 不承诺准确率 | 承诺响应 SLA(《01》§9.2)与误报收敛趋势,合同附实测基线报告——四线城市评审专家往往来自本地,更看重"出了事有没有人管" |
| 绑定考核指标 | 把系统指标对齐民政的考核口径(探访覆盖率、异常发现时长),让局里能拿系统数据向上汇报 |
## 4.3 风险
| 风险 | 缓解 |
| --- | --- |
| 财政支付能力弱、回款慢 | 首选有长护险试点或有专项转移支付的区县;合同分期与交付里程碑绑定 |
| 项目随主管领导调动而停摆 | 运营费合同签 3 年期;尽快做出跨街道的规模,提高替换成本 |
| 本地集成商低价抢单(纯摄像头+录像) | 标书里写入"主动预警响应时长""无活动检测"等功能性指标,拉开与录像方案的差距 |
| 老人拔电源/挡摄像头 | 设备探活告警(RQ-C-06)+ 照护员上门沟通流程;安装时的知情同意与讲解话术进 onboarding 清单 |
| 误报率高导致照护员疲于奔命 | 试运行模式先收基线再放量;误报反馈闭环从第一户就开始收数据 |
# 5. 与平台规划的对齐
| 项 | 对齐点 |
| --- | --- |
| 里程碑 | M2 的 10 站点试点 = 本群体的免费试装户;M3 误报基线 = 汇报材料的数据来源 |
| 场景包 | S1 包零修改可用;新增"连续 N 天无活动量"规则(跨天统计,已在 S3 包有同类) |
| 人脸识别 | **本群体不需要**。陌生人入室用 ReID 即可;不要在 2G 养老项目里主动引入人脸,徒增合规评审环节 |
| 硬件 | RK3588 边缘盒方案直接适用;二期雷达对应 M6 异构传感器 |
| 容量 | 本场景一户对应一个 1–3 路逻辑站点,100–200 户是同一租户下的多站点规模,不受“单站点上限 128 路”误导;控制面、列表和批量运维仍须按多站点分页设计 |
| 合规 | 被监护老人的知情同意(《01》§7.3)在本群体尤其重要——由社区与家属共同签署,留档进平台 |
---
> 姊妹文档:《05-目标客户-寄宿制学校》《06-目标客户-工厂园区安全生产》。三个群体的共性:**付费动机都是"不花钱就要担责"(政府问责/学校口碑/法定经费),而非自愿改善型消费**——这是四线城市安防 AI 唯一撑得起付费的逻辑。
+130
View File
@@ -0,0 +1,130 @@
# 🏫 目标客户群体 02 — 寄宿制学校
> 版本 v0.2 · 2026-08-03
> 市场定位:中国南方四线城市 · 付费方:民办学校自有资金 / 公办"平安校园"财政专项(2B + 2G)
> 对应场景包:**S2 校园包**(《03-通用场景应用方案》§3.3)
---
# 1. 一句话定位
**卖给校长的"夜间宿舍与围墙不出事"系统:民办学校保招生口碑,公办学校花掉平安校园专项——两边都是必须花的钱。**
# 2. 客户画像
## 2.1 为什么是寄宿制、为什么是四线城市
| 因素 | 说明 |
| --- | --- |
| **寄宿比例高** | 四线城市及下辖县乡的生源进城读书,寄宿制中学比例远高于一二线;学生 24 小时在校,学校承担全时段监护责任 |
| **夜间是事故高发段** | 晚归、翻墙外出(网吧)、宿舍打架、天台/危险区域——全部发生在值班力量最薄弱的时段 |
| **问责与口碑双重刚性** | 公办:出事校长负责,安全事故一票否决;民办:一次恶性事件直接摧毁下一年招生。**付费动机是保牌子,不是省人力** |
| **预算真实存在** | 公办有"平安校园"专项财政经费(必须花掉的预算);民办决策链短,校长一人可拍板,客单价能谈 |
## 2.2 两类客户的打法差异
| | 民办寄宿学校 | 公办学校 |
| --- | --- | --- |
| 决策人 | 校长/董事长,**一人拍板** | 教育局统采 + 校长申报 |
| 销售周期 | 1–3 个月 | 6–12 个月(招投标) |
| 价格敏感度 | 中(对比的是出事的代价) | 低(预算内花完即可),但低价竞标者多 |
| 打法 | **直销,先打样板校** | 拿着民办样板校进教育局,争取列入区级统采目录 |
| 角色 | 现金流与打磨产品 | 规模与利润 |
**顺序必须是先民办后公办**:民办给得起决策速度,让 M3–M4 阶段快速拿到真实校园误报数据;公办统采需要的案例和检测报告,从民办样板校来。
## 2.3 客户的真实痛点(按客户语言)
1. "宿管晚上查完寝就睡了,学生翻墙出去出了事,学校全责" → 周界翻越检测 + 值班室声光告警
2. "两个学生在厕所门口打起来,等老师知道已经进医院了" → 打架/剧烈动作检测,秒级推值班室
3. "家长天天问孩子到校没有、出校没有" → (谨慎承接,见 §4.3 增值项)
4. "上面检查要看监控全覆盖,但摄像头装了 300 个,根本没人盯" → **这是最普遍的现状:有监控无监看**。方案首期默认选 16 路高风险点位验证价值,单逻辑站点本阶段可横向扩到 128 路;超过 128 路按校区/楼栋拆成多个逻辑站点分期覆盖
# 3. 卖什么
## 3.1 产品组合
| 层 | 内容 | 对应能力 |
| --- | --- | --- |
| 复用存量 | **接入学校已装的 IP 摄像头**(ONVIF/RTSP),不强制换设备 | L1 接入层;M0 白名单验证扩展到存量设备兼容性排查 |
| 边缘算力 | 首期按默认 16 路配置边缘推理节点(Jetson/x86+GPU),视频不出校;扩容时增加媒体/推理 worker | 《03》§4.1 边缘为主形态;硬件数量以现场码流与模型压测为准 |
| 平台 | 值班室看板 + 工单 + 批量 ack + 当班交接 | 《02》§3.2 已识别:值班室形态 UI 与家庭 App 是两个东西 |
| 终端 | 值班室大屏 + 声光联动 + 保卫科/校领导 App | 《03》§5.4 |
| 服务 | 规则按学期调整(课表/寒暑假)、误报收敛、检查迎检报表 | 年服务费内容 |
**"复用存量摄像头"是四线学校的成交关键**——预算有限,重新布线换设备的方案直接出局。这正是平台不绑品牌(RQ-C-01)的商业价值。
## 3.2 核心规则集(S2 包,按夜间优先排序)
| 优先 | 规则 | 时段 | 说明 |
| --- | --- | --- | --- |
| P0 | 翻越围墙/护栏 | 全天,夜间加权 | 校长最怕的事故源头 |
| P0 | 宿舍晚归/夜间离寝 | 22:30–06:00 | 楼栋出入口机位 + 时段策略 |
| P0 | 打架/推搡/围观聚集 | 全天 | 课间与就寝前高发 |
| P0 | 危险区域闯入(天台/配电房/水池/实验室) | 非开放时段 | Zone + 时段 |
| P1 | 尾随进校 | 上下学时段 | **ReID + 门禁事件融合,不用人脸** |
| P1 | 上课时段楼道徘徊 | 课表内 | 需课表导入(场景包 calendars) |
| P1 | 学生跌倒/晕倒 | 全天 | 复用 S1 能力,操场/楼梯间 |
## 3.3 收费模型
| 项 | 模式 | 说明 |
| --- | --- | --- |
| 建设费 | 一次性 | 推理服务器 + 平台部署 + 存量摄像头接入调试 + Zone/课表配置 |
| 年服务费 | 按路数/按年 | 规则学期性调整、模型更新、误报收敛、迎检报表。**民办校谈 3 年锁定** |
| 教育局统采 | 按校打包 | 区级平台(多租户)+ 分校部署,L0 多租户能力的直接变现 |
# 4. 怎么打
## 4.1 销售路径
```
选 1–2 所本地有口碑焦虑的民办寄宿中学
→ 免费试运行一个月(只开翻墙+晚归+打架三条,dry_run 收基线)
→ 用"上周拦下 N 起翻墙、平均响应 X 秒"的周报转正式合同
→ 民办样板校 2–3 所
→ 带案例进教育局 → 平安校园专项 / 区级统采
```
试运行必须用 dry_run 模式(《02》§6.4):先收两周基线,把误报调到值班室可接受的量,再开真实告警。**第一周就用误报轰炸值班室,这个客户就永远丢了。**
## 4.2 打单要点
| 要点 | 做法 |
| --- | --- |
| 卖"响应"不卖"识别" | 演示的高潮不是检测框,是值班室大屏弹窗 + 声光 + 值班员手机同时响。校长买的是"有人管",不是 AI |
| 夜间演示 | 安排在晚自习后实地演示翻墙检测——在客户最痛的时段展示 |
| **主动讲"我们不做人脸"** | 未成年人合规是文档写死的阻塞项(《02》§13)。在学校场景把"全程 ReID 匿名、不建人脸库、不追踪具体学生身份"作为**卖点**讲——校长同样怕家长投诉隐私 |
| 迎检价值 | 平安校园检查时,系统报表(覆盖点位、告警处置率、响应时长)就是现成迎检材料 |
## 4.3 增值与边界
| 需求 | 态度 | 理由 |
| --- | --- | --- |
| 家长查看孩子到离校 | ⚠️ 谨慎 | 涉及逐个学生身份识别 = 人脸 + 未成年人双重合规。若做,走**校卡/门禁数据对接**(Webhook 开放 API),不走视觉识别 |
| 明厨亮灶(后厨监控) | ✅ 可接 | 有专项政策与预算,检测目标是着装/操作规范(属性类,无身份),S4 能力的低风险预演 |
| 课堂行为分析(学生专注度等) | ❌ 不接 | 争议大、合规风险高、与安全定位偏离 |
## 4.4 风险
| 风险 | 缓解 |
| --- | --- |
| **未成年人合规**(即便不做人脸) | 视频留存期限从严、查看权限最小化、告示牌,专项合规意见先行(《01》§7.3)——这在本群体是售前成本,不是售后 |
| 存量摄像头质量差(夜间宿舍区无补光) | 售前勘测出具点位报告:哪些路能用、哪些需补光/换机。**不承诺坏机位的检测效果** |
| 寒暑假期间系统闲置引发续费质疑 | 假期切换到周界/入侵模式(场景包 calendars 天然支持),全年有价值 |
| 低价竞标者用"录像+关键词"方案搅局 | 标书写入功能性指标:越线方向判定、行为分类、响应时长、误报率报告 |
| 学生识别出摄像头盲区规避 | 定期用系统数据复盘翻墙热点,调整机位建议——写进年服务内容 |
# 5. 与平台规划的对齐
| 项 | 对齐点 |
| --- | --- |
| 场景包 | S2 包直接复用;课表/校历(calendars)是本群体的核心配置工作量 |
| UI 形态 | 值班室看板 + 工单 + 批量 ack —— M4 管理系统需求的主要来源就是这个群体 |
| ReID | 尾随/徘徊全走 ReID(M4),**本群体人脸能力永不主动引入**,家长端需求走门禁数据对接 |
| 部署 | 边缘为主(视频不出校),对齐《03》§4.1;默认交付 16 路,单逻辑站点本阶段横向扩到 128 路。禁止承诺单服务器 100–300 路;超过 128 路按校区/楼栋拆分逻辑站点 |
| 多租户 | 教育局区级统采 = 多租户(L0)的第一个真实变现场景 |
---
> 姊妹文档:《04-目标客户-政府购买居家养老》《06-目标客户-工厂园区安全生产》。
@@ -0,0 +1,145 @@
# 🏭 目标客户群体 03 — 工厂与工业园区安全生产
> 版本 v0.2 · 2026-08-03
> 市场定位:中国南方四线城市 · 付费方:企业法定安全生产费用(2B)
> 对应场景包:**S4 园区/厂区包** —— ⚠️ 当前在《02-需求分析》§12.1 被列为 Won't(本期),本文档同时给出优先级调整建议(§6)
---
# 1. 一句话定位
**卖给厂长的"应急局检查过得去、安全经费花得掉"的 AI 监管员:三个客户群体中唯一有法定强制预算的。**
# 2. 客户画像
## 2.1 为什么高付费
| 因素 | 说明 |
| --- | --- |
| **法定强制预算** | 企业按营收比例**强制提取安全生产费用**,专款专用,必须花在安全上——花不掉反而是审计问题。采购安全监控系统是最顺手的合规支出 |
| **双重倒逼** | 应急管理局执法检查(隐患整改通知书)+ 安全生产责任险费率与事故记录挂钩。上了 AI 监管系统既是整改材料,也可能谈保费 |
| **产业结构契合** | 四线城市恰恰是中小制造业聚集地:建材、五金、化工、食品加工、木材——高危工序多、安全员配置少 |
| **一票否决压力** | 重大事故对企业主意味着停产整顿甚至刑责。付费动机与前两个群体同构:**不花钱就要担责** |
## 2.2 客户分层
| 层 | 对象 | 特点 | 打法 |
| --- | --- | --- | --- |
| A | 规上工厂(化工、建材、金属加工) | 有专职安全员、经费充足、检查频繁 | 直销,单厂 30–100 路 |
| B | 工业园区管委会 | 想做"智慧园区"政绩 + 统一安全监管 | 2G 打包,多租户平台覆盖园内企业 |
| C | 在建工地(住建口) | 智慧工地有政策要求(部分地区强制) | 跟本地智慧工地集成商合作出算法能力 |
**A 是主攻**:决策人是老板/厂长,一人拍板,销售周期短;B 是规模化路径;C 竞争激烈(智慧工地已是红海),只做能力输出不做总集。
## 2.3 客户的真实痛点(按客户语言)
1. "应急局上个月来查,开了整改单:动火区无人监管、行车下站人" → 区域闯入/滞留检测,整改材料直接引用系统
2. "安全员就两个人,车间八个,巡不过来" → AI 替代巡场,违规即拍照留证
3. "工人不戴安全帽,罚也罚了,没人盯就摘" → 未戴安全帽检测 + 车间大屏实时曝光,行为矫正
4. "出了工伤,家属说设备没防护,我们拿不出监管证据" → 事件实例的证据留存(不可篡改,《02》NFR-CMP-01)在劳资纠纷中是企业自证材料
# 3. 卖什么
## 3.1 产品组合
| 层 | 内容 | 对应能力 |
| --- | --- | --- |
| 复用存量 | 接入工厂已有监控(ONVIF/RTSP) | L1 接入层,同校园打法 |
| 边缘算力 | 首期按默认 16 路配置厂内推理节点,数据不出厂;扩容时增加媒体/推理 worker | 边缘为主形态;硬件数量以现场码流、模型与目标帧率压测为准 |
| 平台 | 违规事件台账 + 整改闭环工单 + 迎检报表 | L4 + 管理系统 |
| 终端 | 安全员 App + 车间曝光大屏 + 班组长通知 | 投递通道复用 |
| 服务 | 规则随工序调整、月度安全分析报告 | 年服务费 |
## 3.2 核心规则集(S4 包,全部为 A/B 属性-状态类,无身份)
| 优先 | 规则 | 检测类型 | 说明 |
| --- | --- | --- | --- |
| P0 | 未戴安全帽 | 属性(A 类) | 需求最普遍,验收最直观 |
| P0 | 危险区域闯入/滞留(动火区、行车下方、配电房) | Zone + 状态 | 直接对应整改单 |
| P0 | 未穿反光衣/工服 | 属性(A 类) | 同安全帽,一个模型多头输出 |
| P1 | 离岗检测(关键岗位无人超时) | Zone 内无人 + 时长 | 中控室、锅炉房 |
| P1 | 吸烟/明火 | 目标检测 | 化工、木材、纺织客户的强需求 |
| P1 | 人数管控(受限空间作业人数) | 计数 | 有作业票制度的工厂 |
**技术上是三个群体中最简单的**:安全帽/反光衣是成熟的检测任务,误报容忍度高(误报一次安全员看一眼即可,不像家庭场景误报会导致卸载 App),环境是固定机位良好光照——比跌倒检测低一个数量级的难度,交付快、验收标准清晰。
## 3.3 收费模型
| 项 | 模式 | 说明 |
| --- | --- | --- |
| 建设费 | 一次性 | 推理硬件 + 平台 + 存量接入 + Zone/规则配置 |
| 年服务费 | 按路/按年 | 规则调整 + 报表 + 模型更新。锚点:对比一个安全员的年薪,一套系统 7×24 盯全厂 |
| 园区版 | 管委会付平台费,企业付接入费 | 多租户,B 层打法 |
**发票科目就写"安全生产监控系统"**,让客户可直接列支安全生产费用——这是本群体独有的成交润滑剂,方案报价单要为此专门适配。
# 4. 怎么打
## 4.1 销售路径
```
本地应急局执法动态 / 安全生产协会 / 安责险保险公司
→ 锁定近期收到整改单的规上企业
→ 免费 2 周试运行(安全帽 + 危险区域两条规则,dry_run 收基线)
→ 周报:本周违规 N 次、高发时段、高发班组
→ 转正式合同 → 同行业口碑复制(四线城市同业老板圈子极小)
→ 园区管委会打包
```
**安责险保险公司是被低估的渠道**:保险公司有动机降低承保企业出险率,部分保司为投保企业补贴安全技防投入——找到当地承保安责险的保司谈联合推广,一次触达一批企业。
## 4.2 打单要点
| 要点 | 做法 |
| --- | --- |
| 对着整改单卖 | 方案第一页引用客户收到的整改通知书条目,逐条对应系统功能 |
| 违规数据说话 | 试运行周报的"本周违规 N 次+抓拍图"比任何 PPT 有效——老板第一次直观看到自己厂里的真实违规量 |
| 算人力账 | 一个安全员年成本 vs 系统年费,7×24 无死角 vs 两条腿巡场 |
| 大屏曝光的行为矫正价值 | 车间大屏实时曝光违规抓拍,一个月内违规率肉眼可见下降——这是续费时最硬的数据 |
## 4.3 边界与合规
| 项 | 态度 | 理由 |
| --- | --- | --- |
| 违规与具体工人身份绑定(考核扣款) | ⚠️ 谨慎 | 客户会强烈要求"认出是谁好罚款"。默认给**班组/区域级统计**;确要个人绑定,走工牌/定位卡对接,不走人脸。若客户坚持人脸,按《03》§2.8.3 完整授权流程(员工知情同意 + 工会程序),且 watchlist 红线同样适用:**系统不自动触发扣款,只生成待人工确认记录** |
| 工人隐私(休息区、更衣室) | 硬边界 | `privacy_flag` 代码级拒绝(NFR-CMP-05)同样适用于工厂 |
| 效率监控(工时、动作快慢) | ❌ 不接 | 偏离安全定位,劳资敏感,毁口碑 |
## 4.4 风险
| 风险 | 缓解 |
| --- | --- |
| 小厂付款能力/意愿差 | 只做规上企业与园区打包,不碰 20 路以下小单 |
| 智慧工地/安全帽检测已是红海 | 不打价格战,差异化在:整改闭环工单 + 迎检报表 + 误报收敛服务(红海产品普遍装完即弃,误报没人管) |
| 车间环境恶劣(粉尘、蒸汽、频闪) | 售前勘测出具点位可行性报告,不承诺坏点位效果(同校园打法) |
| 客户拿系统罚款引发劳资对立,反噬口碑 | 合同与培训中引导"曝光教育为主";个人绑定功能默认不开 |
# 5. 与平台规划的对齐
| 项 | 对齐点 |
| --- | --- |
| 能力复用 | 检测+跟踪+Zone+时段全部现成;新增的只是安全帽/反光衣/吸烟检测头(A 类属性,YOLO 多类直接支持)与离岗规则模板 |
| 数据闭环 | 误报容忍度高 + 客户配合标注(安全员每天在看),**是三个群体中数据飞轮转得最快的**——可反哺行为分析模型 |
| 人脸/ReID | 默认无身份;个人绑定走工牌对接;人脸仅在完整授权流程后按 watchlist 红线执行 |
| 部署 | 边缘为主,数据不出厂;默认交付 16 路,30–100 路项目按 32/64/128 档横向分片,禁止视为单实例容量;单逻辑站点本阶段上限 128 路 |
| 多租户 | 园区管委会版 = 多租户第二个变现场景(与教育局统采同构) |
# 6. ⚠️ 对现有规划的调整建议(需拍板)
《02-需求分析》§12.1 目前将 S4 列为 **Won't(本期)**,S3 社区列为场景之一。按本次四线城市付费能力分析:
| 对比 | S3 社区/物业 | S4 工厂/园区 |
| --- | --- | --- |
| 付费方 | 物业公司(0.5–1.5 元/㎡物业费,微利) | 企业法定安全经费(强制提取) |
| 付费意愿 | 弱,决策分散 | 强,老板一人拍板 |
| 技术难度 | 中(电动车、堆物、抛物) | **低**(安全帽、区域、属性类) |
| 交付周期 | 中 | 短 |
**建议**:
1. S4 与 S3 在 M5 的优先级**对调**:S4 场景包进 M5,S3 降为 Could(仅承接街道办"智慧社区"财政项目,不主动开拓物业直客)
2. S4 技术上最简单,恰好适合作为《02》M5 出口标准("场景包为纯配置交付,无需改核心代码")的**验收案例**
3. 若采纳,需同步修订:《01》§4.1 场景总览、《02》§12.1 MoSCoW 与 §12.2 里程碑、《03》§3 场景包章节
---
> 姊妹文档:《04-目标客户-政府购买居家养老》《05-目标客户-寄宿制学校》。三个群体共性:**付费动机都是"不花钱就要担责"**——政府问责、学校口碑、法定经费;四线城市安防 AI 只有问责链条撑得起付费。共同的销售模式:免费 dry_run 试运行收基线 → 用真实数据周报转化 → 年服务费锁定 → 同城口碑复制。
@@ -0,0 +1,259 @@
# 事件契约比对:silver_pose 实际输出 vs YoVision 设计
> 目的:确定 silver_pose(已验证的推理内核)与 YoVision(平台层)之间**唯一的那条契约**。
> 比对基准:silver_pose `v1/alerts.py` + `v2/internal/alert/dispatcher.go` + `v2/internal/store/store.go`;YoVision《03-通用场景应用方案》§2.6。
> 日期:2026-08-03
---
## 0. 结论摘要
| 项 | 结论 |
| --- | --- |
| v1 / v2 输出一致性 | ✅ Python 与 Go 的 JSONL 字段**完全相同**,契约已经稳定,可以直接当基线 |
| 与平台设计的关系 | silver_pose 的事件是平台事件的**真子集**,没有需要推翻的字段 |
| 真正的阻塞 | 不是字段缺失,是 **3 个语义冲突**(见 §3),必须先解决 |
| 接入点 | **已经存在**——`v1/alerts.py` 的 `AlertSink` 就是为注入外部副作用设计的,不需要改动已验收代码 |
| 反向吸收 | silver_pose 有 **5 处**设计文档没考虑到的东西,应该写回《03》§2.6 |
---
## 1. silver_pose 当前实际输出
### 1.1 JSONL(每确认事件一行)
`v1/alerts.py:197-208` 与 `v2/internal/alert/dispatcher.go:104-113`,字段一致:
```json
{
"event_id": "FALL-<session_id>-000001",
"track_id": "P-0001",
"config_version": "<配置版本串>",
"source_id": "<来源标识>",
"state": "CONFIRMED",
"confirmed_at_utc": "2026-07-20T10:31:22.417000+00:00",
"suspected_at_monotonic": 1043.21,
"confirmed_at_monotonic": 1045.68,
"latency_seconds": 2.47,
"screenshot": "20260720/FALL-abc-000001.png"
}
```
### 1.2 SQLite(仅 v2,`internal/store/store.go:35`)
JSONL 之外多出的列:
```
source_kind (默认 'rtsp')、camera_host、camera_port、camera_channel
```
> 注释里写明:**记录 host/port/channel,但永不记录密码**。
### 1.3 状态机(`v1/fall_state.py`)
`NORMAL → SUSPECT → CONFIRMED → RECOVERING`,只有进入 `CONFIRMED` 才产出 `FallEvent`。
`RECOVERING` 表示「确认摔倒后又自己起来了」,当前**不产出任何事件**。
---
## 2. 字段级比对
| silver_pose | YoVision《03》§2.6 | 状态 | 说明 |
| --- | --- | --- | --- |
| `event_id` | `id` | ⚠️ **需改造** | 生成方式不同,见 §3.2 |
| `track_id` | `subject.track_id` | ✅ 直接对应 | 语义一致 |
| `source_id` | `device_id` | ⚠️ 弱对应 | 前者是字符串标识,后者是平台设备实体主键,需映射表 |
| `state` | — | ⚠️ 语义冲突 | 见 §3.3 |
| — | `kind` | ❌ 缺 | 事件类型隐含在 `event_id` 的 `FALL-` 前缀里,必须显式化为 `"fall"` |
| `confirmed_at_utc` | `detected_at` | ✅ 直接对应 | 判定时刻,用于 SLA |
| `suspected_at_monotonic` | `occurred_at` | ❌ **不可用** | 单调时钟,跨进程无意义。见 §3.1 |
| `confirmed_at_monotonic` | — | ➖ 可丢弃 | 有 `confirmed_at_utc` 即可 |
| `latency_seconds` | (派生) | ✅ 保留 | 见 §4.3,建议平台侧也存 |
| `screenshot` | `evidence.snapshot_uris[0]` | ✅ 直接对应 | 相对路径 → URI |
| `config_version` | `rule.version` | ⚠️ 粒度不同 | 见 §4.2,两者都要留 |
| `camera_host/port/channel` | (由 `device_id` 引用) | ✅ 平台侧更优 | 平台不该在事件里冗余连接信息 |
| — | `tenant_id` / `site_id` | ❌ 缺 | 单机演示不需要,平台必须 |
| — | `rule{id,version,code}` | ❌ 缺 | silver_pose 无规则实体,只有全局配置 |
| — | `severity` | ❌ 缺 | 摔倒恒定 high,可由平台侧按 `kind` 补 |
| — | `confidence` | ❌ 缺 | 见 §3.4,**不是简单补字段** |
| — | `subject.attributes` | ❌ 缺 | 无属性识别 |
| — | `subject.anon_id` | ❌ 缺 | 无 ReID |
| — | `identity` / `identity_status` | ❌ 缺 | 可先恒为 `null` / `"not_enabled"` |
| — | `observation.bbox_seq_uri` | ❌ **重要缺口** | 见 §5.1 |
| — | `observation.keypoint_seq_uri` | ❌ **重要缺口** | 见 §5.1 |
| — | `evidence.clip_uri` / `clip_range` | ❌ **重要缺口** | 只有一张截图,无 pre-roll 视频 |
| — | `dedup_key` / `aggregated_into` | ❌ 缺 | 见 §3.5 |
| — | `outcome` / `outcome_reason` | ❌ **重要缺口** | 误报反馈闭环无落点 |
---
## 3. 五个语义冲突(不是补字段能解决的)
### 3.1 单调时钟 vs 墙钟 —— P0
`suspected_at_monotonic` / `confirmed_at_monotonic` 用的是 `time.monotonic()`。这在状态机内部是**正确选择**(不受系统对时影响,`fall_state.py:78` 还强制了单调递增校验),但它:
- 进程重启后归零,跨进程无意义
- 无法和平台的 `occurred_at` 对齐
- 无法用于证据回捞(回捞需要墙钟去定位录像位置)
**解决**:在事件产出处补一个墙钟事发时刻。
```
occurred_at = confirmed_at_utc - (confirmed_at_monotonic - suspected_at_monotonic)
= confirmed_at_utc - latency_seconds
```
这个换算精度足够(误差是单帧级别)。**保留** monotonic 字段做内部诊断,但契约上以 `occurred_at` 为准。
### 3.2 `event_id` 生成方式 —— P0
当前是 `FALL-{session_id}-{序号:06d}`(`fall_state.py:146-154`),会话内自增。
- 单机单路演示:没问题
- 平台扩展到 128 路、多分片/多节点:**序号会撞**。`session_id` 若不是全局唯一,两台边缘节点会产出同一个 `event_id`,而 `AlertDispatcher` 的去重恰好是按 `event_id` 做的 → 平台侧会静默丢弃真事件
**解决**:平台契约用 ULID(`evt_01J8X...`)。silver_pose 侧保留原 `event_id` 作为 `source_event_id` 字段,便于回溯本地文件名(截图就是按它命名的)。
### 3.3 `state` 字段语义被占用 —— P0
silver_pose 的 `state` 是**状态机状态**(`NORMAL/SUSPECT/CONFIRMED/RECOVERING`);平台事件里没有这个概念——平台事件恒等于「已确认的客观发生」,其后续流转是**告警**的状态机(《03》§2.7),两者不能混。
**解决**:silver_pose 的 `state` 不进平台事件顶层。映射为:
- `state == "CONFIRMED"` → 产出事件,`kind: "fall"`
- `state == "RECOVERING"` → **不是新事件**,是对已有事件的 `outcome` 输入(见 §4.1)
### 3.4 `confidence` 在这条链路上没有天然来源 —— P1
平台设计里 `confidence: 0.87` 隐含假设「有一个模型给出置信度」。但 silver_pose 的判定是**几何证据 + 时间窗状态机**(`evidence.py` 算躯干角度与髋部下坠比,`fall_state.py` 要求连续 1–3s 不中断),最终是布尔判定,没有一个自然的 0–1 分值。
**不要硬凑一个假的置信度。** 三个选项:
1. 置 `null`,并允许 schema 里 `confidence` 可空(推荐先这样)
2. 用可解释的替代量:`horizontal_angle_degrees` 距阈值的余量、`visible_joint_count`、`latency_seconds`(越接近确认窗口下限说明证据越"干脆")
3. 后续换成分类模型时再填
对应到《02》里「七类动作模型优于二分类」的结论——真正的 `confidence` 要等模型换代才有意义。
### 3.5 去重只在进程内存里 —— P1
`AlertDispatcher._seen_event_ids`(Python)/ `Dispatcher.seen`(Go,`dispatcher.go:34`)都是内存 `set`/`map`:
- 进程重启即丢失
- 两个机位拍到同一次跌倒 → 两条独立事件,无合并
- 无冷却期概念
这与《03》§2.6「同设备冷却期 / 同站点聚合 / 已处置抑制」是四个不同层次的去重。
**解决**:进程内去重**保留**(它防的是同帧重复写盘,是正确的);跨机位/跨时间的去重归**平台侧**,由 `dedup_key` 承担。silver_pose 只需在事件里带上足以构造 dedup_key 的原料(site + 位置 + kind + 时间桶)。
---
## 4. 反向吸收:silver_pose 比设计文档考虑得更周到的 5 处
这些应该写回《03》§2.6,是实战沉淀,设计文档里没有。
### 4.1 `RECOVERING` 状态是免费的 outcome 信号
《03》§2.6 的 `outcome` 只设计了人工来源(值班员标记误报)。但 silver_pose 的状态机已经能识别「确认摔倒后又自己站起来了」——这是**自动的、及时的**处置结果输入,比等人工标记快得多,而且正是家属/值班员最想知道的一件事。
**建议**:平台事件增加 `outcome` 的自动来源:
```
subject_recovered —— 由推理侧在 RECOVERING 稳定后回传
```
并在《03》§2.6 的 outcome 取值里显式列出「自动/人工」两种来源。
### 4.2 `config_version` 进事件
设计文档只有 `rule.version`(单条规则版本)。silver_pose 把**整个判定配置的版本**钉进每条事件(`fall_state.py` 构造时强制非空校验)。
价值:调完阈值后跑回归,能精确区分「哪些事件是旧配置产出的」。`rule.version` 粒度不够——阈值往往是全局的。
**建议**:《03》§2.6 事件结构中,`rule` 之外**增加顶层 `config_version`**。
### 4.3 `latency_seconds` 显式存储
可以由两个时间戳相减,但显式存有两个好处:一是 SLA 报表不用每次算,二是它是**判定质量的直接指标**(贴近确认窗口下限 = 证据干脆;贴近上限 = 勉强通过,是误报高发区)。
**建议**:保留为顶层字段,并作为误报排查的首选排序键。
### 4.4 证据文件命名的隐私纪律
`v1/alerts.py` 文件头注释:
> Artifact names use only the event ID and a date folder — never an RTSP address, credential, or client name.
这条纪律设计文档里**没有写**。截图/片段的文件名会出现在日志、URL、客服工单里,是最容易泄露客户身份和摄像头地址的地方。
**建议**:写入《03》§2.6「三条硬约束」表(升级为四条)。
### 4.5 存连接信息但绝不存密码
`store.go` 顶部注释明确:记录 camera_host / port / channel,**永不记录密码**。同样应写入约束表。
---
## 5. 缺口分档
### P0 —— 不补则平台无法工作(M1 前)
| # | 缺口 | 动作 |
| --- | --- | --- |
| 1 | 墙钟 `occurred_at` | 由 `confirmed_at_utc - latency_seconds` 换算 |
| 2 | 全局唯一 `id` | 平台侧生成 ULID,原 id 降为 `source_event_id` |
| 3 | 显式 `kind` | 恒为 `"fall"`,从前缀提升为字段 |
| 4 | `tenant_id` / `site_id` / `device_id` | 由 `source_id` 经映射表补全,映射表在平台侧 |
| 5 | `state` 不进事件顶层 | 按 §3.3 映射 |
### P1 —— 接入后很快会缺(M2–M3)
| # | 缺口 | 说明 |
| --- | --- | --- |
| 6 | `evidence.clip_uri` + `clip_range` | 现在只有一张截图。**一张截图不足以让值班员判断真假**,这是误报反馈闭环的前置条件 |
| 7 | `observation.bbox_seq_uri` / `keypoint_seq_uri` | 《02》C9 数据闭环的**唯一原料**。只有截图无法训练。silver_pose 内部已经有逐帧 keypoints,只是没落盘——这是**最低成本的高价值补齐** |
| 8 | `outcome` 回写通道 | 平台 → 推理侧的反向通道,目前完全没有 |
| 9 | `dedup_key` 原料 | 见 §3.5 |
### P2 —— 后续(M4+)
`severity` / `confidence` / `subject.attributes` / `anon_id` / `identity` / `aggregated_into`。
其中 `identity_status` 建议**立刻**恒填 `"not_enabled"`,成本为零,避免以后区分不了「没开」和「比对失败」。
---
## 6. 接入方案:不改已验收代码
**关键发现:接入点已经存在。**
`v1/alerts.py` 的 `AlertSink` 本来就是为注入外部副作用设计的(`play_sound` / `show_popup` 都是可注入的桌面适配器):
```python
class AlertSink:
"""Injectable output for sound and popup. Default implementation is silent."""
def play_sound(self, event: FallEvent) -> None: ...
def show_popup(self, record: AlertRecord) -> None: ...
```
所以平台接入 = **新增一个 sink 实现**,而不是改 `EventArtifactWriter`(它已通过单元测试与 T-000 验收):
```
FallEvent ──→ EventArtifactWriter ──→ 本地截图 + JSONL (现状,不动)
└─→ PlatformSink ──→ mapper ──→ YoVision event JSON ──→ 平台
```
`PlatformSink` 的职责只有三件:字段映射(§2 的表)、P0 补全(§5)、投递(HTTP POST 起步,后续换 ZeroMQ)。**投递失败必须落本地队列重试,不能阻塞主链路**——这与《03》§2.7「告警先落库再投递」是同一原则。
v2 Go 侧结构相同,`Dispatcher.Dispatch` 返回 `Record` 后加一个 publisher 即可,同样不动 `write`。
---
## 7. 下一步(按顺序)
1. **确认边界**:silver_pose 保持独立仓库,是「已验证的推理内核 + 单场景演示」;YoVision 是平台层。两者只通过本文档定义的事件 JSON 通信
2. **冻结 v0.1 契约**:把 §2 的映射表 + §5 的 P0 五项,写成一份 JSON Schema,两个仓库各放一份(内容相同),改动必须双向同步
3. **补 §4 的五处反向吸收**到《03》§2.6,特别是把隐私纪律(§4.4、§4.5)升格为硬约束
4. **P1 第 7 项优先做**:keypoint 序列落盘。它在推理侧几乎零成本(数据已在内存),但决定了《02》C9 数据闭环能否启动
5. `confidence` 保持 `null`,**不要造假值**(§3.4)
+184
View File
@@ -0,0 +1,184 @@
# 三系统职责划分:Sense / Brain / Bell
> 本文档回答一个问题:**一段代码该写进哪个目录。**
> 划分依据来自《03-通用场景应用方案》的 L0–L5 分层与组件选型,本文档只是把它按三个可独立开发的系统重新切分。
> 定稿:2026-08-03
---
## 1. 三系统总表
| | **Sense** | **Brain** | **Bell** |
| --- | --- | --- | --- |
| 中文 | 感知系统 | 推理系统 | 管理系统 |
| 小学生版 | 感觉到 | 想一想 | 打铃叫人 |
| 对应层 | L1 接入 | L2 流水线 + L3 算法 | L4 业务 + L0 基座 |
| 语言 | Go | Python / CUDA | Go + 前端 |
| 有无状态 | 有(设备台账) | **无状态** | 有(事件、告警) |
| DB schema | `sense` | 无 | `bell` |
| 契约角色 | 供流与信号 → Brain | **产出**事件 | **消费**事件 |
| 首次交付 | M1 | M3 | M3(最小)→ M4(完整) |
### 1.1 各自装什么
**Sense —— 把现场的流和信号稳定地拿进来并管住**
- 设备台账(`devices`,含 `modality`:video / radar / contact / button / wearable)
- ONVIF 客户端:连接、GetProfiles、GetStreamUri、SetSystemDateAndTime
- mediamtx API 客户端(**自行用 oapi-codegen 从其 OpenAPI 生成**,不依赖第三方 SDK)
- **对账器**:水平触发、`sync_state`、独立信号量、10% 孤儿删除安全闸、只收敛不回滚
- 探活与断线重建
- WireGuard 控制面隧道
- mediamtx 鉴权回调(401 + 踢流是四种停用粒度之一)
- 容量配额执行:设备新增/启用时读取 Bell 的站点配额,只拒绝超额变更;配额服务暂时不可用时不影响已有流
- 流绑定与媒体分片调度:维护 `mtx_instance`,默认 16 路交付;扩到 128 路时按 `media_shard.max_streams` 横向分片
- 边缘节点 agent:推流、本地环形缓冲、断网续传
- **设备型触发源**:雷达、门磁、按钮、ONVIF 事件订阅
- 非视频传感器的信号接收(signal plane,不走 mediamtx)
**Brain —— 看画面、出判定**
- Savant 流水线与适配器(ZeroMQ 边界,适配器故障隔离,实时模式丢帧)
- 模型:检测 / 姿态 / 跟踪 / ReID(`anon_id`)/(M5)人脸
- **`Detector` 与 `PoseEstimator` 接口解耦**——这是 AGPL 逃生通道的前提,换 YOLOX + RTMPose 时判定算法不动
- 判定内核:几何证据 + 时间窗状态机(从 silver_pose 抽取,两边共用)
- 事件 mapper:判定结果 → 事件契约 v0.1
- **像素级**运动侦测触发(要解码,所以在这里)
- 推理 worker 注册与分片:单逻辑推理分片默认最多 16 路;64/128 路由多个 worker 承担,路由变更不改判定代码
**Bell —— 记录、派发、追到人确认**
- 事件接收与校验(schema 校验 + 契约 README §5 那六条代码级断言 + 生成 ULID)
- 事件存储:**不可变**,误判只改 `outcome`
- 规则引擎 + 场景包加载
- 告警状态机:升级链、ack、抑制、静默(**≤4h,无永久选项**)
- 投递:push / 短信 / 语音,**双供应商**
- 误报反馈闭环:`outcome` 回写 Brain
- 多租户 RBAC、`tenant_features`(人脸授权开关:未授权时能力在 API 与 UI 中**不可见**,不是禁用)
- 审计日志
- 站点容量配额的唯一真相源:`site.max_video_channels` 默认 16、上限 128;提供版本化内部读取接口/投影给 Sense 执行
- 管理后台前端 + 大屏:按 128 路设计分页/虚拟列表、筛选与批量操作,不一次性加载全部视频
### 1.2 不属于任何一个目录的东西
| 组件 | 说明 |
| --- | --- |
| **mediamtx** | 独立二进制,外部依赖。Sense 管它的配置与生命周期,`Sense/deploy/` 放基线配置 |
| **PostgreSQL / MinIO / Prometheus** | 基础设施,独立部署清单 |
| **silver_pose** | 独立仓库,是 Brain 判定内核的来源与单机演示形态。**不改名、不合并**——它现在能卖,Brain 还不能 |
| **`_reference/`** | 只读参考源码(如 MiBeeNvr)。按《03》§1.4 白名单借鉴 ONVIF、IP 自愈、健康检测、测试组织与 UI 交互;禁止复制媒体内核、SQLite 真相源、AI/用户体系,禁止整仓进生产或加入 go.mod |
---
## 2. 目录结构
### 2.1 Sense(Go)
```
Sense/
├── cmd/
│ ├── sense-api/ 控制面服务:设备台账、鉴权回调、对账器
│ └── sense-agent/ 边缘节点:推流、环形缓冲、隧道
├── internal/
│ ├── device/ 设备台账(devices,含 modality)
│ ├── onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime
│ ├── mtx/ oapi-codegen 生成的 mediamtx 客户端 + 薄封装
│ ├── reconcile/ 对账循环(幂等 + 退避 + 安全闸)
│ ├── probe/ 探活
│ ├── trigger/ 设备型触发源(雷达/门磁/按钮/ONVIF 事件)
│ ├── tunnel/ WireGuard 控制面
│ ├── authcb/ mediamtx 鉴权回调
│ └── store/ M1 先 SQLite,schema 与生产 Postgres 保持一致
├── deploy/
│ └── mediamtx.yml 基线配置
└── api/openapi.yaml
```
### 2.2 Brain(Python / CUDA)
```
Brain/
├── pipeline/ Savant 流水线定义与适配器
├── models/
│ ├── detector/ Detector 接口 + YOLO / YOLOX 实现
│ ├── pose/ PoseEstimator 接口 + YOLO-pose / RTMPose 实现
│ ├── tracker/ ByteTrack / BoT-SORT
│ └── reid/ 匿名同一性,产出 anon_id
├── judge/
│ ├── evidence.py 几何证据(躯干角、髋部下坠比)
│ └── state.py 时间窗状态机 NORMAL/SUSPECT/CONFIRMED/RECOVERING
├── emit/
│ ├── mapper.py 判定结果 → 契约 v0.1
│ └── publisher.py 投递(HTTP 起步 → ZeroMQ);**失败落本地队列重试,不阻塞主链路**
├── trigger/ 像素级运动侦测
└── contracts/ ← 与 Bell/contracts/ 逐字节相同
```
### 2.3 Bell(Go + 前端)
```
Bell/
├── cmd/bell-api/
├── internal/
│ ├── ingest/ 接收事件、schema 校验、六条断言、生成 ULID
│ ├── event/ 事件存储(不可变,只允许追加 outcome)
│ ├── rule/ 规则引擎 + 场景包加载
│ ├── alert/ 告警状态机、升级链、ack、静默
│ ├── deliver/ push / 短信 / 语音,双供应商
│ ├── feedback/ 误报回流,outcome 回写 Brain
│ ├── tenant/ 多租户 RBAC、tenant_features
│ ├── audit/ 审计日志
│ └── store/ Postgres(schema: bell)
├── web/ 管理后台前端
├── packs/ 场景包(**纯配置** —— M5 架构验收点)
└── contracts/ ← 与 Brain/contracts/ 逐字节相同
```
> `contracts/` 只在 Brain 与 Bell 各存一份(生产者与消费者),**Sense 不需要**——它不碰事件契约。
---
## 3. 七条边界(不写死就会漂)
| # | 边界 | 定论 |
| --- | --- | --- |
| 1 | mediamtx 二进制归谁 | 独立进程。**Sense** 管配置与生命周期,部署清单放 `Sense/deploy/` |
| 2 | 触发源归谁 | 按性质切:**设备型**(雷达/门磁/按钮/ONVIF 事件)→ Sense,它们本来就是设备;**像素级**运动侦测 → Brain,它要解码 |
| 3 | pre-roll 证据切片谁裁 | 录制在 Sense(mediamtx),事件在 Brain 产出。**Bell 发起回捞**(它拥有事件),Sense 提供切片接口 |
| 4 | ULID 谁生成 | **Bell**。Brain 只填 `source_event_id`。见契约 README §4 |
| 5 | 一个 PostgreSQL 还是三个 | **一个实例,schema 分离**(`sense` / `bell`),Brain 无 schema。"DB 是唯一真相源"是对账器成立的前提,拆库即失效 |
| 6 | 16 路与 128 路分别指什么 | **16 路是默认交付规格,128 路是本阶段单逻辑站点上限**,都不是“单进程/单服务器保证值”。媒体与推理必须横向分片;单机承载量由分辨率、码率、帧率、模型和硬件基准测试决定 |
| 7 | 站点配额谁拥有、谁执行 | **Bell 拥有 `sites.max_video_channels`,Sense 在设备新增/启用写路径执行**。通过版本化内部 API 或只读投影同步,不允许 Sense 直接写 Bell schema;依赖暂时不可用时拒绝新变更,但已有视频链路继续运行 |
第 2、3、6、7 条是本文档新定的,若实施中发现更合适的切法,改这里并同步《02》《03》。
---
## 4. 标识符约定
```
仓库/目录 Sense Brain Bell
镜像 yov/sense:x.y yov/brain:x.y yov/bell:x.y
日志/指标前缀 sense_ brain_ bell_
三字母码 SEN BRN BEL
DB schema sense — bell
```
**命名规则:系统名不得是该系统领域模型里的实体名。**
`bell` 域内有 `alerts` 表 → 系统不能叫 Alert(`alert.alerts` 无法读,且撞 Prometheus 内置的 `ALERTS` 序列)。另两个同样干净:`sense` 域内是 `devices`/`streams`,`brain` 域内是 `models`/`detections`。
---
## 5. 里程碑与目录的对应
| 阶段 | 动哪个目录 |
| --- | --- |
| **M0** 摄像头兼容性验证 | 无生产代码;可直接运行 MiBeeNvr 等 `_reference/` 项目作为隔离实验室测试台,产物允许丢弃 |
| **M1** 5 路接入骨架 + mediamtx | **仅 Sense**;MediaMTX 正式进入,完整 MiBeeNvr 不得替代生产媒体数据面 |
| **M2** 对账器 + 多租户 + 隧道 + 16 路开通 | **仅 Sense**;至少一个站点验证默认 16 路配额与全流程 |
| **M3** 默认 16 路推理 + 规则引擎 + 事件预警 | **Brain + Bell 同时起步**,契约在此首次被真实使用 |
| **M4** 64 路分片 + 灰度 + 管理系统 | Sense 与 Brain 完成横向分片,Bell 完成 128 路规模下的列表与批量交互;验证单分片故障隔离 |
| **M5** 128 路容量验收 + 第二三场景包 + 触发式推理 + 人脸 | Sense/Brain 验证扩容不改业务代码;Bell 的 `packs/`(**纯配置**)+ Brain 的模型与触发 |
| **M6** 异构传感器 + 两级判定 | Sense 的 `trigger/` 与 `device/`(modality 扩展)+ Brain 两级判定 |
> M3 是契约第一次被真实使用的时刻。**在此之前契约允许原地修订,之后版本递增规则绝对生效**(契约 README §1)。
+138
View File
@@ -0,0 +1,138 @@
# 事件契约 v0.1(已冻结)
> **冻结日期**:2026-08-03
> **范围**:推理侧(silver_pose 及后续推理内核)→ YoVision 平台侧的唯一数据契约。
> **依据**:《07-事件契约比对-silver_pose.md》
| 文件 | 用途 |
| --- | --- |
| `event-v0.1.schema.json` | JSON Schema(draft 2020-12),规范本体 |
| `event-v0.1.example-current.json` | **现状**基线:silver_pose 今天就能产出的形态,P1 缺口全为 null |
| `event-v0.1.example-target.json` | **目标**形态:P1 补齐后,含 keypoint 序列、clip、自动 outcome |
| `event-v0.1.example-radar.json` | **非成像模态**:卫生间纯雷达跌倒,`snapshot_uris` 为空数组,门磁佐证 |
三个 example 都是**测试夹具**,两侧仓库的单元测试都应对它们做校验。
### 修订记录
| 日期 | 变更 | 理由 |
| --- | --- | --- |
| 2026-08-03 | 初次冻结 | — |
| 2026-08-03 | 新增 `sensors[]` 与 `observation.signal_seq_uri`;`evidence.snapshot_uris` 的 `minItems` 由 1 放宽为 0 | 感知系统后期接入毫米波雷达等非成像设备,原 schema 把「感知=摄像头」焊死在契约里,纯雷达事件无法表达。**本次为原地修订**,理由是当时零实现;首个生产者上线后,§1.2 的版本递增规则绝对生效,不再允许原地改 |
---
## 1. 冻结意味着什么
1. **本目录内容在两个仓库各存一份,内容必须逐字节相同。** 任何一侧修改,同一次提交内同步另一侧
2. `schema_version` 为 `"0.1"`。**破坏性变更必须递增版本并保留旧版本文件**,不得原地修改已冻结的 schema
3. 「破坏性」的定义:删除字段、收紧取值域、把可空改为必填、改变字段语义。新增可空字段与新增 `kind` 取值**不算**破坏性
4. 实验性字段一律放 `ext`,不占用顶层命名空间,也不触发版本升级
## 2. 三条设计决定(及其代价)
**① 所有顶层键必须存在,可为 null,但不得省略。**
「省略」与「显式 null」在下游无法区分,这是这类系统最常见的排查陷阱——`identity_status` 就是被这个问题逼出来的字段。代价是生产者要写一串 null,但生产者只有一个 mapper 函数。
**② 根对象 `additionalProperties: false`。**
未登记字段一律拒绝。防的是推理侧偷偷加字段、平台侧偷偷依赖,几个月后没人知道它是不是契约的一部分。逃生口是 `ext`。
**③ `confidence` 允许为 null,且现阶段必须为 null。**
几何证据 + 时间窗状态机的判定链路没有天然置信度。伪造一个常量会污染所有下游阈值调优。等换成分类模型再填。
## 3. kind 注册表
新增 `kind` 不需要升 schema 版本,但**必须在此表登记**。
| kind | 含义 | 默认 severity | 产出方 | 状态 |
| --- | --- | --- | --- | --- |
| `fall` | 人员摔倒确认 | `high` | silver_pose FSM 进入 CONFIRMED | ✅ 已上线 |
## 4. silver_pose → 契约 映射表
mapper 的完整实现规格。左列出处见《07》§1。
| 契约字段 | 来源 | 变换 |
| --- | --- | --- |
| `schema_version` | — | 常量 `"0.1"` |
| `id` | — | **平台侧**生成 ULID,加 `evt_` 前缀 |
| `source_event_id` | `event_id` | 原样。截图文件即按它命名,是回溯本地证据的唯一钥匙 |
| `tenant_id` / `site_id` / `device_id` | `source_id` | 经平台设备映射表解析。**事件中不得出现 RTSP 地址或凭据** |
| `sensors` | `source_id` | 单摄像头事件为 `[{device_id, "video", "primary"}]`;多传感器融合由推理侧列全 |
| `kind` | `event_id` 的 `FALL-` 前缀 | 提升为显式字段 `"fall"` |
| `severity` | — | 按 kind 查注册表 → `"high"` |
| `confidence` | — | 恒 `null`(见 §2③) |
| `detected_at` | `confirmed_at_utc` | 原样 |
| `occurred_at` | 换算 | `confirmed_at_utc - latency_seconds`。误差为单帧级 |
| `latency_seconds` | `latency_seconds` | 原样 |
| `config_version` | `config_version` | 原样 |
| `rule` | — | `null`(推理侧无规则引擎),平台侧按 kind 反查补全 |
| `subject.class` | — | 常量 `"person"` |
| `subject.track_id` | `track_id` | 原样 |
| `subject.attributes` | — | `{}` |
| `subject.anon_id` / `identity` | — | `null` |
| `subject.identity_status` | — | 常量 `"not_enabled"` |
| `observation.*` | — | 全 `null`(P1 补 keypoint_seq_uri;非成像模态补 signal_seq_uri) |
| `evidence.snapshot_uris` | `screenshot` | 相对路径 → URI,单元素数组。**非成像模态为 `[]`** |
| `evidence.clip_uri` / `clip_range` | — | `null`(P1 补) |
| `dedup_key` / `aggregated_into` | — | `null`,由平台侧构造 |
| `outcome` | `state` | `CONFIRMED` → `"unknown"`;`RECOVERING` 稳定后回传 `"subject_recovered"` + `outcome_source: "auto"` |
| `diagnostics.fsm_state` | `state` | 原样,**仅供排查** |
| `diagnostics.*_monotonic` | 同名字段 | 原样。**单调时钟跨进程无意义,不得用于任何时间计算** |
| `ext` | — | `{}` |
## 5. schema 拦不住的四件事(必须写代码校验)
已实测(`REJECT` = schema 能拦,`ACCEPT` = 拦不住):
schema **能**拦:省略必填键 / 非法 ULID / 顶层多余字段 / outcome 越界 / `config_version` 空串 / `snapshot_uris` 空数组 / 负 latency / kind 大写。
schema **拦不住**,需在 mapper 与平台入口各加一道断言:
| # | 校验 | 理由 |
| --- | --- | --- |
| 1 | `detected_at >= occurred_at` | 跨字段约束,JSON Schema 表达不了。倒序时间会让 SLA 统计出负数 |
| 2 | `\|detected_at - occurred_at - latency_seconds\| < 0.1s` | 三者冗余,必须自洽。不一致说明 mapper 用错了时钟 |
| 3 | `confidence is null`(现阶段) | schema 只能约束 0–1 区间,拦不住伪造值 |
| 4 | 证据文件名不含 IP、端口、凭据、客户名 | 只允许 `事件ID + 日期目录`。文件名会出现在日志、URL 与工单里 —— 这是 silver_pose `alerts.py` 头注释的既有纪律,此处升格为强制校验 |
| 5 | `sensors` 中恰有一个 `role == "primary"`,且其 `device_id == 顶层 device_id` | 数组约束,schema 表达不了。两个 primary 会让归因和去重都失去基准 |
| 6 | 若任一 `sensors[].modality == "video"`,则该 `device_id` 所在区域 `privacy_flag` 必须为 false | 隐私区域硬约束的运行时兜底。设备录入时已拦一道,此处是第二道 |
### 5.1 隐私区域约束的正确表述
原表述「隐私区域拒绝 `device_type = camera`」应改为**按模态判定**:
```
privacy_flag = true 的区域:
允许 modality ∈ {radar, contact, button, wearable}
拒绝 modality ∈ {video}
```
差别不是措辞。按设备类型判定,每接一种新设备就要重新问一次"它算不算摄像头";按模态判定,规则一次写死,新设备只需声明自己的 modality。《01》RQ-S1-05、《02》NFR-CMP-05、《03》§3.2 与落地清单四处的表述需同步。
## 6. 本地校验命令
```bash
pip install 'jsonschema>=4.18'
python3 - <<'PY'
import json
from jsonschema import Draft202012Validator, FormatChecker
s = json.load(open('event-v0.1.schema.json'))
Draft202012Validator.check_schema(s)
v = Draft202012Validator(s, format_checker=FormatChecker())
for f in ('event-v0.1.example-current.json', 'event-v0.1.example-target.json'):
errs = list(v.iter_errors(json.load(open(f))))
print(f, 'OK' if not errs else [e.message for e in errs])
PY
```
已于 2026-08-03 执行通过:schema 合法,两个 example 均校验通过。
## 7. 未决项
| # | 事项 | 决策人 |
| --- | --- | --- |
| 1 | `source_id` → `device_id` 映射表放在哪(平台 DB 表 / 边缘配置文件) | 待定 |
| 2 | 投递通道:HTTP POST 起步还是直接上 ZeroMQ | 待定,建议先 HTTP |
| 3 | `outcome` 回写的反向通道协议(平台 → 推理侧) | 待定 |
| 4 | silver_pose 侧本文件的落地方式(需按其 T-xxx 任务流程走) | 待定 |
@@ -0,0 +1,60 @@
{
"schema_version": "0.1",
"id": "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2B",
"source_event_id": "FALL-a3f9c1-000017",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5012,
"sensors": [{ "device_id": 5012, "modality": "video", "role": "primary" }],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T10:31:19.947Z",
"detected_at": "2026-08-03T10:31:22.417Z",
"latency_seconds": 2.47,
"config_version": "sp-v1-2026.07.20",
"rule": null,
"subject": {
"class": "person",
"track_id": "P-0001",
"attributes": {},
"anon_id": null,
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": null,
"dwell_sec": null,
"bbox_seq_uri": null,
"keypoint_seq_uri": null,
"signal_seq_uri": null
},
"evidence": {
"snapshot_uris": ["file:///events/20260803/FALL-a3f9c1-000017.png"],
"clip_uri": null,
"clip_range": null
},
"dedup_key": null,
"aggregated_into": null,
"outcome": "unknown",
"outcome_source": null,
"outcome_reason": null,
"diagnostics": {
"fsm_state": "CONFIRMED",
"suspected_at_monotonic": 1043.21,
"confirmed_at_monotonic": 1045.68,
"horizontal_angle_degrees": 21.4,
"visible_joint_count": 15
},
"ext": {}
}
@@ -0,0 +1,66 @@
{
"schema_version": "0.1",
"id": "evt_01J8XT7Q3R5S8V2W4X6Y9Z1A3B",
"source_event_id": "FALL-b7d2e4-000003",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5044,
"sensors": [
{ "device_id": 5044, "modality": "radar", "role": "primary" },
{ "device_id": 5045, "modality": "contact", "role": "corroborating" }
],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T02:14:08.300Z",
"detected_at": "2026-08-03T02:14:11.140Z",
"latency_seconds": 2.84,
"config_version": "radar-v1-2026.08.01",
"rule": { "id": "R-ELD-004", "version": 1, "code": "fall_confirmed_radar" },
"subject": {
"class": "person",
"track_id": "R-0002",
"attributes": {},
"anon_id": null,
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": "bathroom",
"dwell_sec": null,
"bbox_seq_uri": null,
"keypoint_seq_uri": null,
"signal_seq_uri": "s3://yov-evt/20260803/evt_01J8XT7Q.../radar_track.jsonl"
},
"evidence": {
"snapshot_uris": [],
"clip_uri": null,
"clip_range": null
},
"dedup_key": "20301:bathroom:fall:2026-08-03T02:10",
"aggregated_into": null,
"outcome": "unknown",
"outcome_source": null,
"outcome_reason": null,
"diagnostics": {
"fsm_state": "CONFIRMED",
"suspected_at_monotonic": 88214.02,
"confirmed_at_monotonic": 88216.86,
"horizontal_angle_degrees": null,
"visible_joint_count": null
},
"ext": {
"radar_doppler_peak_mps": -1.9,
"radar_target_height_m": 0.21
}
}
@@ -0,0 +1,63 @@
{
"schema_version": "0.1",
"id": "evt_01J8XR5N9P2Q4S7T1V3W5Y7Z9A",
"source_event_id": "FALL-a3f9c1-000018",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5012,
"sensors": [{ "device_id": 5012, "modality": "video", "role": "primary" }],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T11:04:41.120Z",
"detected_at": "2026-08-03T11:04:42.980Z",
"latency_seconds": 1.86,
"config_version": "sp-v1-2026.07.20",
"rule": { "id": "R-ELD-001", "version": 3, "code": "fall_confirmed" },
"subject": {
"class": "person",
"track_id": "P-0004",
"attributes": { "age_group": "senior" },
"anon_id": "pid_7f3a91",
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": "living_room",
"dwell_sec": null,
"bbox_seq_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../obs.jsonl",
"keypoint_seq_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../pose.jsonl",
"signal_seq_uri": null
},
"evidence": {
"snapshot_uris": [
"s3://yov-evt/20260803/evt_01J8XR5N.../s0.jpg",
"s3://yov-evt/20260803/evt_01J8XR5N.../s1.jpg"
],
"clip_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../clip.mp4",
"clip_range": ["2026-08-03T11:04:31.000Z", "2026-08-03T11:04:56.000Z"]
},
"dedup_key": "20301:living_room:fall:2026-08-03T11:00",
"aggregated_into": null,
"outcome": "subject_recovered",
"outcome_source": "auto",
"outcome_reason": "RECOVERING 状态持续 8s 后回落 NORMAL",
"diagnostics": {
"fsm_state": "RECOVERING",
"suspected_at_monotonic": 3021.44,
"confirmed_at_monotonic": 3023.30,
"horizontal_angle_degrees": 17.9,
"visible_joint_count": 17
},
"ext": {}
}
+283
View File
@@ -0,0 +1,283 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://yovision.local/contracts/event-v0.1.schema.json",
"title": "YoVision Event Instance v0.1",
"description": "推理侧 → 平台侧的唯一契约。冻结于 2026-08-03。所有顶层键必须存在(可为 null),不允许省略——省略与显式 null 无法区分,是这类系统最常见的排查陷阱。",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"id",
"source_event_id",
"tenant_id",
"site_id",
"device_id",
"sensors",
"kind",
"severity",
"confidence",
"occurred_at",
"detected_at",
"latency_seconds",
"config_version",
"rule",
"subject",
"observation",
"evidence",
"dedup_key",
"aggregated_into",
"outcome",
"outcome_source",
"outcome_reason",
"diagnostics",
"ext"
],
"properties": {
"schema_version": {
"description": "契约版本。破坏性变更必须递增主版本。",
"const": "0.1"
},
"id": {
"description": "平台侧生成的全局唯一事件 ID(ULID)。推理侧不得自行生成。",
"type": "string",
"pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
},
"source_event_id": {
"description": "推理侧原始事件 ID,如 silver_pose 的 FALL-<session>-000001。用于回溯本地截图文件名(截图即按它命名)。会话内唯一,全局不保证唯一——不得用作主键。",
"type": "string",
"pattern": "^[A-Za-z0-9_-]{1,128}$"
},
"tenant_id": { "type": "integer", "minimum": 1 },
"site_id": { "type": "integer", "minimum": 1 },
"device_id": {
"description": "主传感器的平台设备实体主键。由推理侧的 source_id 经平台映射表解析得到。事件中不得冗余 RTSP 地址或任何凭据。多传感器融合事件的完整来源见 sensors。",
"type": "integer",
"minimum": 1
},
"sensors": {
"description": "参与本次判定的全部传感器。单摄像头事件为单元素数组。恰好一个元素的 role 为 primary,且其 device_id 必须等于顶层 device_id。",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["device_id", "modality", "role"],
"properties": {
"device_id": { "type": "integer", "minimum": 1 },
"modality": {
"description": "设备模态。决定隐私区域准入:privacy_flag 为真的区域只允许非成像模态。",
"type": "string",
"enum": ["video", "radar", "contact", "button", "wearable", "other"]
},
"role": {
"description": "primary=判定主依据;corroborating=佐证(如雷达判跌倒、门磁佐证无人离开)。",
"type": "string",
"enum": ["primary", "corroborating"]
}
}
}
},
"kind": {
"description": "事件类型。取值登记在 contracts/README.md 的类型注册表中,新增类型不需要升 schema 版本。v0.1 已登记:fall。",
"type": "string",
"pattern": "^[a-z][a-z0-9_]{2,63}$"
},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"confidence": {
"description": "模型置信度。几何+状态机判定链路没有天然来源,必须填 null——不得用任意常量或阈值余量伪造。",
"type": ["number", "null"],
"minimum": 0,
"maximum": 1
},
"occurred_at": {
"description": "事发时刻(墙钟 UTC)。决定证据回捞窗口。推理侧若只有单调时钟,按 detected_at - latency_seconds 换算。",
"type": "string",
"format": "date-time"
},
"detected_at": {
"description": "判定成立时刻(墙钟 UTC)。决定 SLA 计算。必须 >= occurred_at。",
"type": "string",
"format": "date-time"
},
"latency_seconds": {
"description": "从可疑到确认的耗时。可由两时间戳相减,但显式存储:它是判定质量的直接指标——贴近确认窗口下限说明证据干脆,贴近上限是误报高发区,为误报排查的首选排序键。",
"type": "number",
"minimum": 0
},
"config_version": {
"description": "产出本事件时整套判定配置的版本。粒度高于 rule.version(阈值往往是全局的),用于调参后的回归对比。不得为空串。",
"type": "string",
"minLength": 1,
"maxLength": 128
},
"rule": {
"description": "命中的规则实体。推理侧无规则引擎时为 null,由平台侧按 kind 反查补全。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["id", "version", "code"],
"properties": {
"id": { "type": "string" },
"version": { "type": "integer", "minimum": 1 },
"code": { "type": "string" }
}
},
"subject": {
"type": "object",
"additionalProperties": false,
"required": ["class", "track_id", "attributes", "anon_id", "identity", "identity_status"],
"properties": {
"class": { "type": "string", "enum": ["person", "vehicle", "object"] },
"track_id": {
"description": "跟踪器内的短期标识,跨会话不保证稳定。",
"type": "string",
"minLength": 1
},
"attributes": {
"description": "A 类属性(年龄段、着装等)。未启用时为空对象,不是 null。",
"type": "object"
},
"anon_id": {
"description": "B+ 类 ReID 匿名标识,站点内会话级有效(≤30min),不做跨日长期关联。未启用为 null。",
"type": ["string", "null"]
},
"identity": {
"description": "C 类人脸身份。仅在租户已授权且比对命中时非 null。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["person_id", "library_id", "score"],
"properties": {
"person_id": { "type": "string" },
"library_id": { "type": "string" },
"score": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"identity_status": {
"description": "必须显式。只写 null 无法区分「没开这功能」与「比对失败」,后者是需要排查的故障。",
"type": "string",
"enum": ["not_enabled", "pending", "matched", "below_threshold", "no_candidate", "timeout"]
}
}
},
"observation": {
"description": "结构化观测。bbox/keypoint 序列是数据闭环的唯一原料——只有视频与截图无法用于训练。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["zone", "dwell_sec", "bbox_seq_uri", "keypoint_seq_uri", "signal_seq_uri"],
"properties": {
"zone": { "type": ["string", "null"] },
"dwell_sec": { "type": ["number", "null"], "minimum": 0 },
"bbox_seq_uri": {
"description": "视觉模态专用。非视觉事件为 null。",
"type": ["string", "null"],
"format": "uri"
},
"keypoint_seq_uri": {
"description": "COCO-17 关键点逐帧序列(JSONL)。视觉模态专用,P1 必补项。",
"type": ["string", "null"],
"format": "uri"
},
"signal_seq_uri": {
"description": "非视觉模态的结构化序列(雷达点云轨迹与多普勒、门磁状态变迁等,JSONL)。与 keypoint_seq_uri 平级——两者是各自模态的数据闭环原料,缺任一模态的序列,该模态就无法参与模型迭代。",
"type": ["string", "null"],
"format": "uri"
}
}
},
"evidence": {
"type": "object",
"additionalProperties": false,
"required": ["snapshot_uris", "clip_uri", "clip_range"],
"properties": {
"snapshot_uris": {
"description": "证据截图。**允许为空数组**:非成像模态(雷达、门磁)产出的事件本就没有画面,隐私区域更是禁止成像。不得据此假设每个事件都有图可看——值班台 UI 必须能渲染无画面事件。文件命名只允许包含事件 ID 与日期目录,绝不得含 RTSP 地址、凭据或客户名称,文件名会出现在日志、URL 与工单中。",
"type": "array",
"minItems": 0,
"items": { "type": "string", "format": "uri" }
},
"clip_uri": {
"description": "含 pre-roll 的证据片段。仅有截图不足以让值班员判断真假,是误报反馈闭环的前置条件。P1 必补项。",
"type": ["string", "null"],
"format": "uri"
},
"clip_range": {
"type": ["array", "null"],
"minItems": 2,
"maxItems": 2,
"items": { "type": "string", "format": "date-time" }
}
}
},
"dedup_key": {
"description": "跨机位/跨时间去重键,由平台侧构造。推理侧进程内按 source_event_id 的去重仍保留——它防的是同帧重复写盘,属不同层次。",
"type": ["string", "null"]
},
"aggregated_into": {
"description": "被合并入的事件 ID。非 null 时本事件不独立触发告警。",
"type": ["string", "null"],
"pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
},
"outcome": {
"description": "处置结果。事件不可变,误判只能通过本字段标记,不得删改。subject_recovered 由推理侧状态机自动回传(确认后自行起身),无需等人工。",
"type": "string",
"enum": [
"unknown",
"true_positive",
"false_positive",
"subject_recovered",
"duplicate",
"test"
]
},
"outcome_source": {
"type": ["string", "null"],
"enum": ["auto", "manual", null]
},
"outcome_reason": { "type": ["string", "null"] },
"diagnostics": {
"description": "推理侧内部诊断量,仅用于排查,平台不得依赖其语义。单调时钟跨进程无意义,不得用于任何时间计算。",
"type": ["object", "null"],
"additionalProperties": true,
"properties": {
"fsm_state": {
"type": "string",
"enum": ["NORMAL", "SUSPECT", "CONFIRMED", "RECOVERING"]
},
"suspected_at_monotonic": { "type": "number" },
"confirmed_at_monotonic": { "type": "number" },
"horizontal_angle_degrees": { "type": ["number", "null"] },
"visible_joint_count": { "type": ["integer", "null"], "minimum": 0, "maximum": 17 }
}
},
"ext": {
"description": "厂商/场景扩展位。根对象 additionalProperties=false,任何未登记字段一律放这里,避免为实验性字段升版本。",
"type": "object"
}
}
}
+783
View File
@@ -0,0 +1,783 @@
<title>校园夜间安全预警系统 · 提案</title>
<style>
:root{
/* 纸面:带极淡青灰偏色的账簿纸,非纯白非奶油 */
--paper: #EBEDE8;
--paper-raised: #F7F8F5;
--ink: #1B2A33;
--ink-soft: #566A76;
--rule: #C6CEC8;
/* 夜间:值班室 2 点钟的监视墙,固定不随主题反转 */
--night: #0F1A20;
--night-raised: #16232B;
--night-ink: #E4E9E5;
--night-soft: #8FA3AD;
--night-rule: #26343D;
/* 信号:校园钠灯 / 值班室警示灯 */
--sodium: #E9A13C;
/* 已处置:老黑板绿 */
--board: #3A7A72;
--font-body: "PingFang SC","Hiragino Sans GB","Microsoft YaHei","Source Han Sans SC","Noto Sans CJK SC","Heiti SC",sans-serif;
--font-display: "Songti SC","STSong","Source Han Serif SC","Noto Serif CJK SC","SimSun",serif;
--font-mono: ui-monospace,"SF Mono","Menlo","Consolas","Liberation Mono",monospace;
--pad: clamp(28px, 5vw, 88px);
--maxw: 1080px;
}
@media (prefers-color-scheme: dark){
:root{
--paper: #131B20;
--paper-raised: #1A242A;
--ink: #E3E8E4;
--ink-soft: #96A7B0;
--rule: #2B383F;
}
}
:root[data-theme="dark"]{
--paper: #131B20;
--paper-raised: #1A242A;
--ink: #E3E8E4;
--ink-soft: #96A7B0;
--rule: #2B383F;
}
:root[data-theme="light"]{
--paper: #EBEDE8;
--paper-raised: #F7F8F5;
--ink: #1B2A33;
--ink-soft: #566A76;
--rule: #C6CEC8;
}
*{ box-sizing:border-box; }
.deck{
font-family: var(--font-body);
height:100dvh;
overflow-y:auto;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
background: var(--paper);
}
.slide{
--slide-bg: var(--paper);
--slide-ink: var(--ink);
--slide-soft: var(--ink-soft);
--slide-rule: var(--rule);
scroll-snap-align: start;
min-height:100dvh;
padding: var(--pad);
display:flex;
flex-direction:column;
justify-content:center;
background: var(--slide-bg);
color: var(--slide-ink);
position:relative;
}
.slide.night{
--slide-bg: var(--night);
--slide-ink: var(--night-ink);
--slide-soft: var(--night-soft);
--slide-rule: var(--night-rule);
}
.wrap{ width:100%; max-width:var(--maxw); margin:0 auto; }
/* 眉标:借值班记录的抬头写法 */
.eyebrow{
font-family: var(--font-mono);
font-size:12px;
letter-spacing:.16em;
color: var(--slide-soft);
display:flex;
gap:14px;
align-items:center;
margin-bottom: clamp(20px,3.4vh,40px);
}
.eyebrow::after{
content:"";
flex:1;
height:1px;
background: var(--slide-rule);
}
h1{
font-family: var(--font-display);
font-weight:700;
font-size: clamp(34px, 6.2vw, 78px);
line-height:1.16;
letter-spacing:.01em;
text-wrap: balance;
margin:0;
}
h2{
font-family: var(--font-display);
font-weight:700;
font-size: clamp(28px, 4.4vw, 52px);
line-height:1.22;
text-wrap: balance;
margin:0 0 .5em;
}
.lede{
font-size: clamp(16px, 1.9vw, 21px);
line-height:1.75;
color: var(--slide-soft);
max-width: 34em;
margin:0;
}
.kicker{
font-size: clamp(17px, 2.1vw, 24px);
line-height:1.7;
max-width: 30em;
margin: clamp(24px,3.5vh,44px) 0 0;
padding-left: 20px;
border-left: 3px solid var(--sodium);
}
/* ── 封面 ── */
.s-cover h1{ font-size: clamp(38px, 7.4vw, 92px); }
.cover-sub{
font-size: clamp(15px,1.8vw,19px);
color: var(--slide-soft);
margin: clamp(20px,3vh,32px) 0 0;
letter-spacing:.02em;
}
.window{
margin-top: clamp(40px,7vh,80px);
display:flex;
align-items:center;
gap:16px;
font-family: var(--font-mono);
font-size:13px;
letter-spacing:.1em;
color: var(--slide-soft);
}
.window .bar{
flex:1;
height:1px;
background: linear-gradient(90deg, var(--slide-rule), var(--sodium), var(--slide-rule));
}
/* ── 数据条 ── */
.stats{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(180px,1fr));
gap: clamp(20px,3vw,48px);
margin: clamp(28px,4vh,48px) 0 0;
border-top:1px solid var(--slide-rule);
padding-top: clamp(22px,3vh,34px);
}
.stat .n{
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: clamp(32px,4.6vw,54px);
line-height:1;
letter-spacing:-.02em;
}
.stat .n.hi{ color: var(--sodium); }
.stat .l{
margin-top:10px;
font-size:14px;
line-height:1.6;
color: var(--slide-soft);
}
/* ── 值班日志 / 时间线 ── */
.log{
list-style:none;
margin: clamp(18px,3vh,30px) 0 0;
padding:0;
border-left:1px solid var(--slide-rule);
}
.log li{
display:grid;
grid-template-columns: 82px 1fr auto;
gap: clamp(12px,2vw,26px);
align-items:baseline;
padding: clamp(11px,1.7vh,17px) 0 clamp(11px,1.7vh,17px) clamp(16px,2.4vw,30px);
border-bottom:1px solid var(--slide-rule);
position:relative;
}
.log li::before{
content:"";
position:absolute;
left:-3px; top:50%;
width:5px; height:5px;
border-radius:50%;
background: var(--slide-rule);
transform: translateY(-50%);
}
.log li.hit::before{ background: var(--sodium); box-shadow:0 0 0 4px rgba(233,161,60,.16); }
.log time{
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: clamp(13px,1.5vw,16px);
color: var(--slide-soft);
letter-spacing:.02em;
}
.log li.hit time{ color: var(--sodium); }
.log p{
margin:0;
font-size: clamp(14px,1.7vw,18px);
line-height:1.6;
}
.tag{
font-size:12px;
letter-spacing:.06em;
color: var(--slide-soft);
white-space:nowrap;
}
.tag.blind{ opacity:.55; }
.tag.act{ color: var(--sodium); }
.tag.done{ color: var(--board); }
.total{
margin-top: clamp(22px,3.4vh,36px);
display:flex;
flex-wrap:wrap;
align-items:baseline;
gap: 6px 18px;
}
.total .lab{ font-size:15px; color: var(--slide-soft); }
.total .val{
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: clamp(28px,4.2vw,50px);
line-height:1;
color: var(--sodium);
letter-spacing:-.02em;
}
.total .val.good{ color: var(--board); }
/* ── 规则卡 ── */
.rules{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(230px,1fr));
gap:1px;
margin-top: clamp(24px,3.6vh,40px);
background: var(--slide-rule);
border:1px solid var(--slide-rule);
}
.rule{
background: var(--slide-bg);
padding: clamp(20px,2.6vw,30px);
display:flex;
flex-direction:column;
gap:10px;
}
.rule .when{
font-family: var(--font-mono);
font-size:12px;
letter-spacing:.08em;
color: var(--sodium);
}
.rule h3{
margin:0;
font-size: clamp(17px,2vw,21px);
font-weight:600;
letter-spacing:.01em;
}
.rule p{
margin:0;
font-size:14px;
line-height:1.65;
color: var(--slide-soft);
}
/* ── 链路 ── */
.chain{
display:flex;
flex-wrap:wrap;
align-items:stretch;
gap:10px;
margin: clamp(24px,3.6vh,40px) 0 0;
}
.step{
flex:1 1 150px;
border:1px solid var(--slide-rule);
padding: clamp(15px,2vw,20px);
display:flex;
flex-direction:column;
gap:8px;
}
.step.key{ border-color: var(--sodium); }
.step .t{
font-family: var(--font-mono);
font-size:11px;
letter-spacing:.1em;
color: var(--slide-soft);
}
.step.key .t{ color: var(--sodium); }
.step .d{ font-size: clamp(14px,1.6vw,17px); line-height:1.5; }
/* ── 要点列表 ── */
.points{
list-style:none;
margin: clamp(22px,3.4vh,38px) 0 0;
padding:0;
display:flex;
flex-direction:column;
gap: clamp(13px,1.9vh,19px);
max-width:44em;
}
.points li{
display:grid;
grid-template-columns: 18px 1fr;
gap:16px;
font-size: clamp(15px,1.8vw,19px);
line-height:1.65;
}
.points li::before{
content:"";
width:8px; height:1px;
background: var(--sodium);
margin-top:.85em;
}
.points .sub{
display:block;
font-size:14px;
color: var(--slide-soft);
margin-top:5px;
line-height:1.6;
}
/* ── 投入结构 ── */
.cost{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(280px,1fr));
gap: clamp(18px,2.6vw,30px);
margin: clamp(24px,3.6vh,40px) 0 0;
}
.cost .box{
border-top:2px solid var(--slide-ink);
padding-top:18px;
}
.cost .box.b2{ border-top-color: var(--sodium); }
.cost h3{
margin:0 0 4px;
font-size: clamp(17px,2vw,21px);
font-weight:600;
}
.cost .note{ font-size:13px; color: var(--slide-soft); margin:0 0 14px; }
.cost ul{ margin:0; padding-left:1.1em; display:flex; flex-direction:column; gap:8px; }
.cost li{ font-size:15px; line-height:1.6; color: var(--slide-soft); }
.foot{
margin-top: clamp(26px,4vh,44px);
padding-top:18px;
border-top:1px solid var(--slide-rule);
font-size:14px;
line-height:1.7;
color: var(--slide-soft);
max-width:46em;
}
/* ── 下一步 ── */
.steps{
list-style:none;
counter-reset: st;
margin: clamp(24px,3.6vh,42px) 0 0;
padding:0;
display:grid;
grid-template-columns: repeat(auto-fit, minmax(230px,1fr));
gap: clamp(18px,2.6vw,28px);
}
.steps li{
counter-increment: st;
border-top:1px solid var(--slide-rule);
padding-top:16px;
display:flex;
flex-direction:column;
gap:8px;
}
.steps li::before{
content: counter(st,decimal-leading-zero);
font-family: var(--font-mono);
font-size:12px;
letter-spacing:.1em;
color: var(--sodium);
}
.steps h3{ margin:0; font-size: clamp(16px,1.9vw,20px); font-weight:600; }
.steps p{ margin:0; font-size:14px; line-height:1.6; color: var(--slide-soft); }
/* ── HUD ── */
.hud{
position:fixed;
right: clamp(14px,2.2vw,26px);
bottom: clamp(14px,2.2vw,24px);
z-index:20;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size:12px;
letter-spacing:.1em;
color: var(--ink-soft);
background: color-mix(in srgb, var(--paper) 84%, transparent);
border:1px solid var(--rule);
padding:6px 11px;
backdrop-filter: blur(6px);
pointer-events:none;
}
.progress{
position:fixed;
top:0; left:0;
height:2px;
background: var(--sodium);
z-index:21;
width:0;
transition: width .28s ease;
}
.hint{
position:fixed;
left: clamp(14px,2.2vw,26px);
bottom: clamp(14px,2.2vw,24px);
z-index:20;
font-family: var(--font-mono);
font-size:11px;
letter-spacing:.08em;
color: var(--ink-soft);
opacity:.6;
pointer-events:none;
transition: opacity .4s ease;
}
.hint.gone{ opacity:0; }
/* ── 入场 ── */
.deck.js .slide .wrap > *{
opacity:0;
transform: translateY(14px);
transition: opacity .55s ease, transform .55s ease;
}
.deck.js .slide.in .wrap > *{ opacity:1; transform:none; }
.deck.js .slide.in .wrap > *:nth-child(2){ transition-delay:.07s; }
.deck.js .slide.in .wrap > *:nth-child(3){ transition-delay:.14s; }
.deck.js .slide.in .wrap > *:nth-child(4){ transition-delay:.21s; }
.deck.js .slide.in .wrap > *:nth-child(5){ transition-delay:.28s; }
@media (prefers-reduced-motion: reduce){
.deck{ scroll-behavior:auto; }
.deck.js .slide .wrap > *{ opacity:1; transform:none; transition:none; }
.progress{ transition:none; }
}
:focus-visible{ outline:2px solid var(--sodium); outline-offset:3px; }
@media (max-width:640px){
.log li{ grid-template-columns: 68px 1fr; }
.log .tag{ grid-column: 2; }
}
</style>
<div class="progress" id="prog"></div>
<div class="deck" id="deck">
<!-- 1 封面 -->
<section class="slide s-cover">
<div class="wrap">
<p class="eyebrow">YOVISION · 校园安全预警系统</p>
<h1>夜里出事,<br>第一个知道的应该是学校</h1>
<p class="cover-sub">面向寄宿制学校的视频行为预警方案 · 方案提案</p>
<div class="window">
<span>22:30</span><span class="bar"></span><span>06:00</span>
</div>
<p class="cover-sub">那道没有人在看的窗口</p>
</div>
</section>
<!-- 2 现状 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">现状</p>
<h2>摄像头装了,晚上谁在看?</h2>
<p class="lede">这不是设备问题。绝大多数寄宿制学校的监控覆盖已经够用了,缺的是夜里那几个小时里,有人替你盯着这些画面。</p>
<div class="stats">
<div class="stat"><div class="n">240</div><div class="l">在用摄像头(1200 人规模典型配置)</div></div>
<div class="stat"><div class="n">2</div><div class="l">夜间在岗人数 · 宿管 1 门卫 1</div></div>
<div class="stat"><div class="n">16</div><div class="l">值班屏能同时显示的画面数</div></div>
<div class="stat"><div class="n hi">0</div><div class="l">凌晨真正被人看过的画面</div></div>
</div>
<p class="kicker">监控让你事后查得到,但它不会在半夜叫醒任何人。</p>
</div>
</section>
<!-- 3 夜晚 A -->
<section class="slide night">
<div class="wrap">
<p class="eyebrow">情景推演 · 现在的夜晚</p>
<h2>一个普通的周三夜里</h2>
<ol class="log">
<li><time>22:30</time><p>熄灯查寝完毕,两栋宿舍楼安静</p><span class="tag">正常</span></li>
<li><time>23:10</time><p>两名学生从操场东侧围墙翻出</p><span class="tag blind">无人知晓</span></li>
<li><time>01:40</time><p>值班屏画面正常轮巡,无人在岗查看</p><span class="tag blind">无人知晓</span></li>
<li><time>05:50</time><p>早操点名,两人不在队列</p><span class="tag">发现异常</span></li>
<li><time>06:15</time><p>学校组织人手开始寻找,调取录像回溯</p><span class="tag">被动响应</span></li>
</ol>
<div class="total">
<span class="lab">事情发生 → 学校知情</span>
<span class="val">7 小时 45 分</span>
</div>
</div>
</section>
<!-- 4 夜晚 B -->
<section class="slide night">
<div class="wrap">
<p class="eyebrow">情景推演 · 同一个夜晚,装了系统</p>
<h2>同样是这堵墙</h2>
<ol class="log">
<li><time>22:30</time><p>熄灯查寝完毕,系统进入夜间规则</p><span class="tag">布防</span></li>
<li class="hit"><time>23:10:04</time><p>围墙东侧检测到翻越动作</p><span class="tag act">命中规则</span></li>
<li class="hit"><time>23:10:06</time><p>值班室大屏弹窗 + 声光,附现场画面</p><span class="tag act">已送达</span></li>
<li class="hit"><time>23:10:06</time><p>宿管、保卫科手机同时推送</p><span class="tag act">已送达</span></li>
<li><time>23:10:31</time><p>值班员点击确认,前往东侧围墙</p><span class="tag done">已确认</span></li>
<li><time>23:14</time><p>学生在校外一百米处被截回</p><span class="tag done">已处置</span></li>
</ol>
<div class="total">
<span class="lab">事情发生 → 有人到场</span>
<span class="val good">4 分钟</span>
</div>
</div>
</section>
<!-- 5 我们做什么 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">我们做什么</p>
<h2>把已经装好的摄像头,<br>变成一个只在出事时叫人的系统</h2>
<p class="lede">不增加盯屏岗位,不改变现有值班安排。平时它一句话不说;一旦命中规则,它会一直找人,直到有人确认。</p>
<p class="kicker">不加人 · 不换设备 · 不增加值班负担</p>
</div>
</section>
<!-- 6 规则 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">先开这四条</p>
<h2>夜间最该被叫醒的四件事</h2>
<div class="rules">
<div class="rule">
<span class="when">全天 · 夜间加权</span>
<h3>翻越围墙护栏</h3>
<p>识别攀爬姿态与越线方向,区分「翻出去」和「路过」,避免把走读生放学误报成翻墙。</p>
</div>
<div class="rule">
<span class="when">22:30 — 06:00</span>
<h3>晚归与夜间离寝</h3>
<p>宿舍楼出入口机位,就寝后进出即记录,可按年级与楼栋分别设定时段。</p>
</div>
<div class="rule">
<span class="when">全天</span>
<h3>打架推搡与围观聚集</h3>
<p>识别剧烈肢体动作与人群聚拢,课间与就寝前是高发时段,秒级推到值班室。</p>
</div>
<div class="rule">
<span class="when">非开放时段</span>
<h3>危险区域闯入</h3>
<p>天台、配电房、水池、实验室,在图上圈出范围与生效时段,进入并停留即告警。</p>
</div>
</div>
<p class="lede" style="margin-top:22px">操场跌倒、上课时段楼道徘徊、外来人员尾随进校,可在运行稳定后逐条加开。</p>
</div>
</section>
<!-- 7 响应链路 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">响应链路</p>
<h2>报出去不算数,有人确认才算</h2>
<div class="chain">
<div class="step"><span class="t">01 检测</span><span class="d">命中规则</span></div>
<div class="step"><span class="t">02 取证</span><span class="d">自动回捞事发前十秒画面</span></div>
<div class="step"><span class="t">03 送达</span><span class="d">值班室大屏 + 声光 + 手机</span></div>
<div class="step key"><span class="t">04 确认</span><span class="d">值班员点击确认并处置</span></div>
<div class="step key"><span class="t">05 升级</span><span class="d">超时无人确认,自动往上找人</span></div>
</div>
<p class="kicker">发出去和被人看到,是两回事。没有人确认,系统会自己升级到保卫科长、再到值班校领导——不会停在「已发送」。</p>
</div>
</section>
<!-- 8 不换设备 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">怎么落地</p>
<h2>不重新布线,不换品牌</h2>
<ul class="points">
<li><span>接入贵校现有的网络摄像头,支持标准协议即可,不限品牌型号。<span class="sub">开工前先做半天勘测,出一份点位报告:哪些机位现在就能用、哪些需要补光或调角度。不承诺坏机位的效果。</span></span></li>
<li><span>校内放一台推理服务器,视频与分析全部在校园网内完成。<span class="sub">视频不出校。上传到平台的只有告警事件本身和那一小段证据片段。</span></span></li>
<li><span>值班室加一块大屏和一套声光,其余沿用现有设备。</span></li>
<li><span>规则、区域、时段在网页上配置,学期变化自己就能改。</span></li>
</ul>
</div>
</section>
<!-- 9 不做人脸 -->
<section class="slide night">
<div class="wrap">
<p class="eyebrow">关于隐私</p>
<h2>我们不做人脸识别</h2>
<ul class="points">
<li><span>不建立学生人脸库,不采集学生面部特征。</span></li>
<li><span>系统判断的是「有人翻墙」,不是「谁翻了墙」。<span class="sub">告警推给值班员的是位置、时间和画面,由老师到现场自己看是哪位同学。</span></span></li>
<li><span>跨摄像头跟踪使用匿名标识,仅当次有效,不留档、不关联到具体学生。</span></li>
<li><span>未成年人影像的合规要求是硬线,也是家长最先问的问题。<span class="sub">这一页的内容,可以直接写进给家长的说明里。</span></span></li>
</ul>
</div>
</section>
<!-- 10 试运行 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">怎么开始</p>
<h2>先看两周数据,<br>再决定要不要开告警</h2>
<p class="lede">第一周就用误报轰炸值班室,这套系统在贵校就再也没人信了。所以试运行期只统计、不告警——不打扰任何人的现有值班。</p>
<ul class="points">
<li><span>两周后交一份周报:本周检测到翻越多少次、集中在哪个时段和哪段围墙。</span></li>
<li><span>晚归多少人次,集中在哪栋楼哪个门。</span></li>
<li><span>误报多少次,分别是什么原因,已经怎么调整。</span></li>
<li><span>你看完这份数据,再决定正式开哪几条规则。</span></li>
</ul>
</div>
</section>
<!-- 11 平时价值 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">不止夜里有用</p>
<h2>平安校园检查,报表直接导出</h2>
<ul class="points">
<li><span>点位覆盖情况、告警处置率、平均响应时长,按月按学期一键导出。<span class="sub">迎检材料不用再临时拼,系统里本来就在记。</span></span></li>
<li><span>寒暑假切换成周界模式,全年不闲置。</span></li>
<li><span>每月一次安全分析:高发时段、高发点位、机位调整建议。<span class="sub">哪段围墙被翻得最多,数据会告诉你——这比加派人手更省。</span></span></li>
<li><span>每一条告警的完整记录留存可查:什么时候报的、报给了谁、谁在几点确认、怎么处置的。</span></li>
</ul>
</div>
</section>
<!-- 12 投入 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">投入结构</p>
<h2>两笔钱,分开算</h2>
<div class="cost">
<div class="box">
<h3>建设费</h3>
<p class="note">一次性</p>
<ul>
<li>校内推理服务器</li>
<li>平台部署与调试</li>
<li>存量摄像头接入</li>
<li>区域、时段与课表配置</li>
<li>值班室大屏与声光</li>
</ul>
</div>
<div class="box b2">
<h3>年服务费</h3>
<p class="note">按路数计,逐年</p>
<ul>
<li>规则随学期与课表调整</li>
<li>误报持续收敛</li>
<li>模型更新</li>
<li>迎检报表与月度分析</li>
<li>远程运维与故障响应</li>
</ul>
</div>
</div>
<p class="foot">具体报价需现场勘测后出具。哪些点位现在就能用、哪些需要补光或换机位,勘测报告里会逐条写清楚——我们不对达不到条件的机位承诺效果,也不把这部分算进合同。</p>
</div>
</section>
<!-- 13 下一步 -->
<section class="slide">
<div class="wrap">
<p class="eyebrow">下一步</p>
<h2>三步,不影响现有值班</h2>
<ol class="steps">
<li>
<h3>现场勘测</h3>
<p>半天时间,走一遍围墙、宿舍楼出入口和危险区域,出具点位可行性报告。</p>
</li>
<li>
<h3>试运行两周</h3>
<p>只统计不告警,值班室照常运转,不增加任何人的工作。</p>
</li>
<li>
<h3>看数据决定</h3>
<p>拿到周报后,由学校决定正式开哪几条规则、覆盖哪些点位。</p>
</li>
</ol>
<p class="foot">YoVision 校园安全预警系统 · 本方案中的夜间时间线为情景推演,用于说明响应流程,非真实事件记录。</p>
</div>
</section>
</div>
<div class="hud"><span id="cur">01</span> / <span id="tot">13</span></div>
<div class="hint" id="hint">↑ ↓ 翻页</div>
<script>
(function(){
var deck = document.getElementById('deck');
var slides = Array.prototype.slice.call(deck.querySelectorAll('.slide'));
var cur = document.getElementById('cur');
var tot = document.getElementById('tot');
var prog = document.getElementById('prog');
var hint = document.getElementById('hint');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var index = 0;
deck.classList.add('js');
tot.textContent = String(slides.length).padStart(2,'0');
function pad(n){ return String(n).padStart(2,'0'); }
function setIndex(i){
index = i;
cur.textContent = pad(i+1);
prog.style.width = ((i+1)/slides.length*100) + '%';
}
if ('IntersectionObserver' in window){
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting){
e.target.classList.add('in');
setIndex(slides.indexOf(e.target));
}
});
}, { threshold: 0.55 });
slides.forEach(function(s){ io.observe(s); });
} else {
slides.forEach(function(s){ s.classList.add('in'); });
}
function go(i){
if (i < 0 || i >= slides.length) return;
slides[i].scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block:'start' });
if (hint) hint.classList.add('gone');
}
document.addEventListener('keydown', function(ev){
var k = ev.key;
if (k === 'ArrowDown' || k === 'ArrowRight' || k === 'PageDown' || k === ' '){
ev.preventDefault(); go(index + 1);
} else if (k === 'ArrowUp' || k === 'ArrowLeft' || k === 'PageUp'){
ev.preventDefault(); go(index - 1);
} else if (k === 'Home'){
ev.preventDefault(); go(0);
} else if (k === 'End'){
ev.preventDefault(); go(slides.length - 1);
}
});
deck.addEventListener('scroll', function(){
if (hint && deck.scrollTop > 40) hint.classList.add('gone');
}, { passive:true });
setIndex(0);
})();
</script>
+303
View File
@@ -0,0 +1,303 @@
<title>系统怎么工作 · 一张图看懂</title>
<style>
:root{
--paper: #EBEDE8;
--paper-raised: #F7F8F5;
--ink: #1B2A33;
--ink-soft: #566A76;
--rule: #C6CEC8;
--sodium: #E9A13C;
--board: #3A7A72;
--font-body: "PingFang SC","Hiragino Sans GB","Microsoft YaHei","Source Han Sans SC","Noto Sans CJK SC","Heiti SC",sans-serif;
--font-display: "Songti SC","STSong","Source Han Serif SC","Noto Serif CJK SC","SimSun",serif;
--font-mono: ui-monospace,"SF Mono","Menlo","Consolas","Liberation Mono",monospace;
}
@media (prefers-color-scheme: dark){
:root{
--paper:#131B20; --paper-raised:#1A242A;
--ink:#E3E8E4; --ink-soft:#96A7B0; --rule:#2B383F;
}
}
:root[data-theme="dark"]{
--paper:#131B20; --paper-raised:#1A242A;
--ink:#E3E8E4; --ink-soft:#96A7B0; --rule:#2B383F;
}
:root[data-theme="light"]{
--paper:#EBEDE8; --paper-raised:#F7F8F5;
--ink:#1B2A33; --ink-soft:#566A76; --rule:#C6CEC8;
}
*{ box-sizing:border-box; }
.page{
font-family: var(--font-body);
background: var(--paper);
color: var(--ink);
min-height:100dvh;
padding: clamp(28px, 5vw, 72px) clamp(20px, 4vw, 56px) clamp(40px, 6vw, 80px);
display:flex;
flex-direction:column;
gap: clamp(28px, 4vw, 52px);
}
header{ max-width: 62ch; }
.eyebrow{
font-family: var(--font-mono);
font-size:12px;
letter-spacing:.16em;
color: var(--ink-soft);
margin:0 0 14px;
}
h1{
font-family: var(--font-display);
font-weight:700;
font-size: clamp(28px, 4.6vw, 46px);
line-height:1.2;
letter-spacing:.01em;
text-wrap:balance;
margin:0 0 .45em;
}
.sub{
margin:0;
font-size: clamp(15px,1.7vw,18px);
line-height:1.75;
color: var(--ink-soft);
}
/* ── 主流程 ── */
.flow{
display:flex;
align-items:stretch;
gap: 0;
overflow-x:auto;
padding-bottom:6px;
}
.node{
flex: 1 1 0;
min-width: 176px;
background: var(--paper-raised);
border:1px solid var(--rule);
padding: clamp(18px,2vw,26px);
display:flex;
flex-direction:column;
gap:12px;
}
.node .top{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
}
.node svg{
width:26px; height:26px;
stroke: var(--ink);
stroke-width:1.4;
fill:none;
stroke-linecap:round;
stroke-linejoin:round;
flex:none;
}
.verb{
font-family: var(--font-display);
font-size: clamp(22px,2.6vw,30px);
font-weight:700;
line-height:1;
color: var(--sodium);
}
.node h2{
margin:0;
font-size: clamp(16px,1.8vw,19px);
font-weight:600;
letter-spacing:.01em;
}
.node .tech{
font-family: var(--font-mono);
font-size:11px;
letter-spacing:.06em;
color: var(--ink-soft);
margin:-6px 0 0;
}
.node p{
margin:0;
font-size: clamp(13.5px,1.5vw,15px);
line-height:1.7;
color: var(--ink-soft);
}
.arrow{
flex:none;
width: clamp(26px,3vw,48px);
display:flex;
align-items:center;
justify-content:center;
color: var(--rule);
}
.arrow svg{ width:100%; height:16px; stroke:currentColor; stroke-width:1.4; fill:none; }
/* ── 回流 ── */
.loop{
display:flex;
align-items:center;
gap: clamp(12px,2vw,20px);
border:1px dashed var(--rule);
padding: clamp(16px,2vw,22px) clamp(18px,2.4vw,28px);
background: transparent;
}
.loop .badge{
font-family: var(--font-mono);
font-size:11px;
letter-spacing:.1em;
color: var(--board);
border:1px solid var(--board);
padding:5px 9px;
white-space:nowrap;
}
.loop p{
margin:0;
font-size: clamp(14px,1.6vw,16px);
line-height:1.7;
color: var(--ink-soft);
}
.loop strong{ color: var(--ink); font-weight:600; }
/* ── 三条说明 ── */
.notes{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(240px,1fr));
gap: clamp(18px,2.6vw,34px);
border-top:1px solid var(--rule);
padding-top: clamp(22px,3vw,32px);
}
.note h3{
margin:0 0 8px;
font-size: clamp(15px,1.7vw,17px);
font-weight:600;
display:flex;
align-items:baseline;
gap:9px;
}
.note h3::before{
content:"";
width:9px; height:1px;
background: var(--sodium);
flex:none;
transform: translateY(-4px);
}
.note p{
margin:0;
font-size:14px;
line-height:1.75;
color: var(--ink-soft);
}
footer{
font-size:12.5px;
line-height:1.7;
color: var(--ink-soft);
border-top:1px solid var(--rule);
padding-top:16px;
}
@media (max-width: 860px){
.flow{ flex-direction:column; overflow-x:visible; }
.node{ min-width:0; }
.arrow{ width:100%; height:30px; }
.arrow svg{ width:16px; height:100%; transform: rotate(90deg); }
}
</style>
<div class="page">
<header>
<p class="eyebrow">YOVISION · 系统工作原理</p>
<h1>摄像头拍到 → 有人来管,中间发生了什么</h1>
<p class="sub">整套系统只做一件事:把「装了但没人看」的摄像头,变成「出事时会主动叫人」的系统。下面是完整的五步。</p>
</header>
<div class="flow">
<div class="node">
<div class="top">
<svg viewBox="0 0 24 24"><path d="M3 7.5h13l3.5-2v13L16 16.5H3z"/><circle cx="9" cy="12" r="2.6"/></svg>
<span class="verb">看</span>
</div>
<h2>摄像头</h2>
<p class="tech">IP CAMERA</p>
<p>装在围墙、走廊、客厅这些地方,对着现场。用你已经装好的,不用换新的。</p>
</div>
<div class="arrow"><svg viewBox="0 0 40 16" preserveAspectRatio="none"><path d="M0 8h34m-6-5 6 5-6 5"/></svg></div>
<div class="node">
<div class="top">
<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M6.5 7.5h.01M6.5 16.5h.01"/></svg>
<span class="verb">收</span>
</div>
<h2>录像主机</h2>
<p class="tech">NVR · 边缘盒子</p>
<p>把所有摄像头的画面汇到一起,存在本地。视频先留在现场,平时不往外传。</p>
</div>
<div class="arrow"><svg viewBox="0 0 40 16" preserveAspectRatio="none"><path d="M0 8h34m-6-5 6 5-6 5"/></svg></div>
<div class="node">
<div class="top">
<svg viewBox="0 0 24 24"><path d="M2 12s3.6-6 10-6 10 6 10 6-3.6 6-10 6-10-6-10-6Z"/><circle cx="12" cy="12" r="2.8"/></svg>
<span class="verb">判断</span>
</div>
<h2>AI 分析</h2>
<p class="tech">推理服务</p>
<p>替你盯着这些画面。有人翻墙、有人摔倒、有人进了不该进的地方——它认得出来,也分得清什么是正常走动。</p>
</div>
<div class="arrow"><svg viewBox="0 0 40 16" preserveAspectRatio="none"><path d="M0 8h34m-6-5 6 5-6 5"/></svg></div>
<div class="node">
<div class="top">
<svg viewBox="0 0 24 24"><rect x="2.5" y="4" width="19" height="12.5" rx="1.5"/><path d="M9 20h6M12 16.5V20M6.5 12l2.8-3 2.6 2.6L16 8"/></svg>
<span class="verb">记录</span>
</div>
<h2>管理后台</h2>
<p class="tech">值班台账</p>
<p>每一次异常变成一条记录,带上那一小段视频。谁在几点确认的、怎么处理的,全都留着,随时能查。</p>
</div>
<div class="arrow"><svg viewBox="0 0 40 16" preserveAspectRatio="none"><path d="M0 8h34m-6-5 6 5-6 5"/></svg></div>
<div class="node">
<div class="top">
<svg viewBox="0 0 24 24"><rect x="6.5" y="2.5" width="11" height="19" rx="2.2"/><path d="M10.5 18.5h3"/><path d="M19.5 5.5a5 5 0 0 1 0 5M21.8 3.5a8 8 0 0 1 0 9"/></svg>
<span class="verb">叫人</span>
</div>
<h2>手机 App</h2>
<p class="tech">预警投递</p>
<p>推到值班员和负责人手机上。<strong style="color:var(--ink);font-weight:600">没有人点确认,它会一直往上找人</strong>,不会停在「已发送」。</p>
</div>
</div>
<div class="loop">
<span class="badge">← 反馈回流</span>
<p>如果这次报错了,值班员点一下<strong>「误报」</strong>,这段画面会自动回到 AI 那里当作教材。<strong>用得越久,报得越准。</strong></p>
</div>
<div class="notes">
<div class="note">
<h3>平时不打扰</h3>
<p>没有异常的时候,系统一句话不说,也不往外传视频。只有出事的那一小段才会被留下来、发出去。</p>
</div>
<div class="note">
<h3>视频不出现场</h3>
<p>录像和 AI 分析都在本地完成。上传到平台的只有告警记录和作为证据的那一小段画面。</p>
</div>
<div class="note">
<h3>不认人,只认事</h3>
<p>系统判断的是「有人翻墙」,不是「谁翻了墙」。默认不做人脸识别,不建人脸库。</p>
</div>
</div>
<footer>YoVision 智能视频事件平台 · 通用场景流程说明 · 2026</footer>
</div>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+140
View File
@@ -0,0 +1,140 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200" font-family="Noto Sans CJK SC, Noto Sans SC, PingFang SC, Hiragino Sans, Microsoft YaHei, sans-serif">
<defs>
<style>
.serif { font-family: "Noto Serif CJK SC","Noto Serif SC","Songti SC",serif; }
.ink { fill:#1C2B33; }
.soft { fill:#4A5C66; }
.muted { fill:#93A2AA; }
.calm { fill:#5B7B8A; }
.amber { fill:#C8781F; }
.ic { fill:none; stroke:#5B7B8A; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; }
</style>
</defs>
<rect width="1600" height="1200" fill="#F4F5F2"/>
<!-- HEADER -->
<text x="70" y="70" class="calm" font-size="19" letter-spacing="5" font-weight="500">居家跌倒监护系统</text>
<text x="70" y="128" class="serif ink" font-size="50" font-weight="700">从摔倒发生,到有人赶到</text>
<text x="70" y="168" class="soft" font-size="21">摄像头平时不工作。只有雷达发现异常,系统才会看一眼。</text>
<line x1="70" y1="200" x2="1530" y2="200" stroke="#DCE0DC" stroke-width="1.5"/>
<!-- CARD 1 -->
<rect x="70" y="236" width="446" height="330" rx="10" fill="#FFFFFF" stroke="#E4E8E4" stroke-width="1.5"/>
<circle cx="123" cy="278" r="17" fill="#5B7B8A"/>
<text x="123" y="284" fill="#FFFFFF" font-size="16" font-weight="700" text-anchor="middle">1</text>
<text x="152" y="285" class="calm" font-size="19" font-weight="500" letter-spacing="1">家中感知 · 24 小时</text>
<g class="ic" opacity="0.55" transform="translate(454,256)">
<circle cx="6" cy="38" r="3" fill="#5B7B8A" stroke="none"/>
<path d="M6 26 A12 12 0 0 1 18 38"/>
<path d="M6 16 A22 22 0 0 1 28 38"/>
<path d="M6 6 A32 32 0 0 1 38 38"/>
</g>
<line x1="106" y1="328" x2="480" y2="328" stroke="#EDF0ED" stroke-width="1.5"/>
<text x="106" y="382" class="serif ink" font-size="29" font-weight="700">雷达日夜守护</text>
<text x="106" y="424" class="soft" font-size="19">卧室、卫生间安装毫米波雷达,</text>
<text x="106" y="458" class="soft" font-size="19">只感知动作,不成像。</text>
<text x="106" y="492" class="soft" font-size="19">摄像头平时保持休眠。</text>
<path d="M540 392 L552 402 L540 412" fill="none" stroke="#B9C4C9" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
<!-- CARD 2 -->
<rect x="576" y="236" width="446" height="330" rx="10" fill="#FFFFFF" stroke="#E4E8E4" stroke-width="1.5"/>
<circle cx="629" cy="278" r="17" fill="#5B7B8A"/>
<text x="629" y="284" fill="#FFFFFF" font-size="16" font-weight="700" text-anchor="middle">2</text>
<text x="658" y="285" class="calm" font-size="19" font-weight="500" letter-spacing="1">就地录像 · 3 秒内</text>
<g class="ic" opacity="0.55" transform="translate(960,256)">
<rect x="1" y="13" width="29" height="21" rx="4"/>
<circle cx="15.5" cy="23.5" r="6"/>
<path d="M30 18 L41 13 L41 34 L30 29 Z"/>
</g>
<line x1="612" y1="328" x2="986" y2="328" stroke="#EDF0ED" stroke-width="1.5"/>
<text x="612" y="382" class="serif ink" font-size="29" font-weight="700">唤醒摄像头</text>
<text x="612" y="424" class="soft" font-size="19">家中的小盒子立刻唤醒摄像头,</text>
<text x="612" y="458" class="soft" font-size="19">并自动回看事发前 30 秒。</text>
<text x="612" y="492" class="soft" font-size="19">录像保存在家里,不全部上云。</text>
<path d="M1046 392 L1058 402 L1046 412" fill="none" stroke="#B9C4C9" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
<!-- CARD 3 -->
<rect x="1082" y="236" width="446" height="330" rx="10" fill="#FFFFFF" stroke="#E4E8E4" stroke-width="1.5"/>
<circle cx="1135" cy="278" r="17" fill="#5B7B8A"/>
<text x="1135" y="284" fill="#FFFFFF" font-size="16" font-weight="700" text-anchor="middle">3</text>
<text x="1164" y="285" class="calm" font-size="19" font-weight="500" letter-spacing="1">智能判断 · 10 秒内</text>
<g class="ic" opacity="0.55" transform="translate(1466,256)">
<circle cx="22" cy="8" r="5"/>
<path d="M22 13 L22 27"/>
<path d="M10 22 L22 17 L34 22"/>
<path d="M13 40 L22 27 L31 40"/>
</g>
<line x1="1118" y1="328" x2="1492" y2="328" stroke="#EDF0ED" stroke-width="1.5"/>
<text x="1118" y="382" class="serif ink" font-size="29" font-weight="700">是摔倒,还是坐下</text>
<text x="1118" y="424" class="soft" font-size="19">AI 分析身体姿态,区分真正的</text>
<text x="1118" y="458" class="soft" font-size="19">摔倒与坐下、弯腰、宠物走动。</text>
<rect x="1118" y="486" width="374" height="54" rx="7" fill="#EEF2F0" stroke="#DFE6E2" stroke-width="1.2"/>
<text x="1138" y="519" fill="#5F7266" font-size="17">判断为误报 → 静默记录,不打扰家人</text>
<!-- connector 1 -->
<path d="M1305 578 L1305 616" stroke="#C8781F" stroke-width="2.4" stroke-linecap="round"/>
<path d="M1297 609 L1305 618 L1313 609" fill="none" stroke="#C8781F" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<text x="1288" y="604" class="amber" font-size="18" font-weight="500" text-anchor="end">确认摔倒</text>
<!-- BAND 4 : MANAGEMENT BACKEND -->
<rect x="70" y="634" width="1460" height="176" rx="10" fill="#FFFFFF" stroke="#E4E8E4" stroke-width="1.5"/>
<rect x="70" y="634" width="7" height="176" rx="3.5" fill="#5B7B8A"/>
<circle cx="130" cy="682" r="17" fill="#5B7B8A"/>
<text x="130" y="688" fill="#FFFFFF" font-size="16" font-weight="700" text-anchor="middle">4</text>
<text x="159" y="689" class="calm" font-size="19" font-weight="500" letter-spacing="1">管理后台 · 全程留痕</text>
<text x="113" y="736" class="serif ink" font-size="29" font-weight="700">所有事件,完整记录</text>
<text x="113" y="774" class="muted" font-size="18">家属与养老机构随时可查</text>
<line x1="600" y1="668" x2="600" y2="776" stroke="#EDF0ED" stroke-width="1.5"/>
<circle cx="638" cy="694" r="4" fill="#5B7B8A"/>
<text x="656" y="700" class="ink" font-size="20" font-weight="700">事件与视频证据</text>
<text x="656" y="732" class="muted" font-size="17">谁、在何时、发生了什么</text>
<circle cx="948" cy="694" r="4" fill="#5B7B8A"/>
<text x="966" y="700" class="ink" font-size="20" font-weight="700">设备状态监测</text>
<text x="966" y="732" class="muted" font-size="17">摄像头、雷达离线自动提醒</text>
<circle cx="1258" cy="694" r="4" fill="#5B7B8A"/>
<text x="1276" y="700" class="ink" font-size="20" font-weight="700">处置结果回填</text>
<text x="1276" y="732" class="muted" font-size="17">标记误报,系统越用越准</text>
<!-- connector 2 -->
<path d="M800 822 L800 860" stroke="#C8781F" stroke-width="2.4" stroke-linecap="round"/>
<path d="M792 853 L800 862 L808 853" fill="none" stroke="#C8781F" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<!-- ROW 5 : ESCALATION -->
<circle cx="87" cy="898" r="17" fill="#C8781F"/>
<text x="87" y="904" fill="#FFFFFF" font-size="16" font-weight="700" text-anchor="middle">5</text>
<text x="116" y="905" class="amber" font-size="19" font-weight="500" letter-spacing="1">逐级通知</text>
<text x="1530" y="905" class="amber" font-size="20" font-weight="700" text-anchor="end">最坏情况:3 分钟内一定有真人接手</text>
<text x="70" y="948" class="serif ink" font-size="29" font-weight="700">直到有人接手 —— 任何一级被确认,升级立即停止</text>
<rect x="70" y="1000" width="350" height="90" rx="8" fill="#E3EBEE"/>
<rect x="435" y="972" width="350" height="118" rx="8" fill="#F2EADD"/>
<rect x="800" y="944" width="350" height="146" rx="8" fill="#F7E3C7"/>
<rect x="1165" y="916" width="350" height="174" rx="8" fill="#C8781F"/>
<text x="98" y="1036" class="ink" font-size="24" font-weight="700">预警 App 推送</text>
<text x="98" y="1066" fill="#5B7B8A" font-size="18">0 – 30 秒</text>
<text x="463" y="1008" class="ink" font-size="24" font-weight="700">短信 + 语音电话</text>
<text x="463" y="1038" fill="#8A7458" font-size="18">30 – 60 秒 · 可穿透静音</text>
<text x="828" y="980" fill="#7A4A12" font-size="24" font-weight="700">备用联系人</text>
<text x="828" y="1010" fill="#8A6A3A" font-size="18">60 – 90 秒 · 邻居、护理员</text>
<text x="1193" y="952" fill="#FFFFFF" font-size="24" font-weight="700">呼叫中心人工介入</text>
<text x="1193" y="982" fill="#F7E3C7" font-size="18">90 秒后</text>
<g fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.85">
<rect x="1440" y="1010" width="34" height="54" rx="6"/>
<path d="M1451 1020 L1463 1020"/>
</g>
<!-- FOOTER -->
<line x1="70" y1="1128" x2="1530" y2="1128" stroke="#DCE0DC" stroke-width="1.5"/>
<text x="70" y="1162" class="muted" font-size="18">隐私优先:卧室与卫生间只装雷达,不装摄像头;摄像头平时不解码、不上传,仅在确认异常时上传相关片段。</text>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

@@ -0,0 +1,135 @@
# 📊 居家老人跌倒监护系统 — 方案流程说明(客户版)
> 来源:Notion · https://app.notion.com/p/3af401dc38a681a186a4fa7a8f5930a5
> 导出时间:2026-08-01
> 面向客户的方案说明材料 · 居家老人跌倒监护系统
# 一句话说明
**雷达 7×24 守护,AI 二次确认,确认跌倒后 3 分钟内必定有人响应。**
平时不看、不录、不传——只有在真正发生异常时,系统才会看一眼。
# 一、业务流程
```mermaid
flowchart TD
A["老人在家中正常生活"] --> B{"雷达持续监测<br/>姿态异常或长时间静止"}
B -->|一切正常| A
B -->|疑似跌倒| C["唤醒摄像头<br/>回捞事发前 30 秒画面"]
C --> D{"AI 姿态分析<br/>二次确认"}
D -->|判定为误报| E["静默记录<br/>不打扰家属"]
E --> A
D -->|确认跌倒| F["生成预警事件<br/>附带视频证据"]
F --> G["第一级:家属 App 推送"]
G --> H{"30 秒内确认?"}
H -->|已确认| M["家属查看视频<br/>前往处理"]
H -->|未确认| I["第二级:短信 + 语音电话"]
I --> J{"60 秒内确认?"}
J -->|已确认| M
J -->|未确认| K["第三级:备用联系人<br/>邻居 / 物业 / 护理员"]
K --> L{"90 秒内确认?"}
L -->|已确认| M
L -->|未确认| N["第四级:呼叫中心人工介入"]
M --> O["处置结果回填<br/>误报反馈持续优化系统"]
N --> O
```
## 关键时间线
| 时间 | 发生什么 |
| --- | --- |
| T+0s | 跌倒发生,雷达捕获 |
| T+3s | 摄像头唤醒,AI 开始分析 |
| T+10s | 确认跌倒,首次通知发出 |
| T+40s | 未响应 → 短信与电话 |
| T+100s | 未响应 → 备用联系人 |
| T+190s | 未响应 → 呼叫中心人工介入 |
**最坏情况下,三分钟内一定有真人接手。**
# 二、系统架构
```mermaid
flowchart LR
subgraph HOME["用户家中"]
CAM["IP 摄像头"]
RADAR["毫米波雷达<br/>卧室 / 卫生间"]
NVR["边缘盒子<br/>录像 + 设备管理"]
CAM --> NVR
RADAR --> NVR
end
subgraph CLOUD["云端平台"]
SAVANT["AI 分析引擎<br/>姿态识别 + 跌倒判定"]
CORE["业务核心服务<br/>事件 / 证据 / 权限"]
ALERT["预警调度引擎<br/>升级链 + 多通道"]
DB[("事件与审计库")]
SAVANT --> CORE
CORE --> ALERT
CORE --> DB
end
NVR -->|"仅上传事件片段"| SAVANT
ALERT --> FAM["家属 / 护理员 / 呼叫中心"]
CORE --> APP["家属 App"]
```
# 三、为什么这样设计
## 隐私:视频默认不出户
- 卧室、卫生间**只装雷达,不装摄像头**——雷达只感知人体轮廓与动作,不成像
- 客厅摄像头平时**不解码、不上传**,仅在雷达报警时上传那一小段
- 录像存在户内盒子,不全量上云
- 家属只能看到**自己家、且与告警相关**的片段
## 可靠:不依赖单一环节
| 风险 | 应对 |
| --- | --- |
| 家属没看手机 | 短信 + 语音电话自动升级,可穿透勿扰模式 |
| 家属不在本地 | 升级至备用联系人与呼叫中心 |
| 家庭断网 | 边缘盒子本地缓存,恢复后补传;断网本身触发运维告警 |
| 设备故障 | 每日自动探活,离线超阀值主动联系用户 |
| 单户故障 | 一户一盒子,互不影响 |
## 准确:两级判定降误报
单靠摄像头或单靠雷达,误报都会让家属很快失去信任。
- **一级(雷达)**:宁可多报,不能漏报
- **二级(AI 视频)**:审核一级的候选,剥离坐下、弯腰、宠物、访客等干扰
- **误报反馈**:家属标记的每一次误报都会回流系统,**越用越准**
## 可追溯:每一条告警都有完整记录
什么时间触发、基于什么证据、向谁发过、走的什么通道、是否送达、谁在何时确认、处置结果如何——全部不可篡改地留存,可导出备查。
# 四、技术基座
核心组件均基于成熟开源项目,**无厂商锁定,数据完全可控**:
| 环节 | 技术基座 | 说明 |
| --- | --- | --- |
| 边缘录像与设备管理 | Kerberos Agent | MIT 许可,可商用 |
| AI 分析流水线 | Savant | 开源,支持故障隔离与弹性扩容 |
| 预警调度 | GoAlert | Apache-2.0,由 Target 公司开源并在内部生产使用 |
| 边缘硬件 | RK3588 平台 | 无风扇设计,无噪音,低功耗 |
| 业务平台与家属 App | 自主开发 | 根据业务需求持续迭代 |
# 五、实施路径
```mermaid
flowchart LR
P1["试点验证<br/>10 户"] --> P2["小规模运营<br/>50 户"] --> P3["全量部署<br/>200 户"]
```
| 阶段 | 重点 | 产出 |
| --- | --- | --- |
| 试点 | 验证检测准确率与安装流程 | 真实误报基线、安装规范 |
| 小规模 | 验证告警响应链与运维能力 | SLA 实测数据、客服流程 |
| 全量 | 规模化与成本优化 | 正式运营 |
---
> 本文档为方案概述。具体技术实现、容量规划与风险控制见《线上生产开发方案》。
@@ -0,0 +1,183 @@
# 🧪 跌倒检测视频接入系统 — 演示与验证开发方案
> 来源:Notion · https://app.notion.com/p/3af401dc38a6815297c0f1645631e26f
> 导出时间:2026-08-01
> 版本 v1 · 配套文档:《线上生产开发方案》。本文档描述演示与前期验证阶段的做法,与生产架构的关系必须清晰界定。
# 1. 本阶段的目的(先划清边界)
演示阶段有两个完全不同的目标,容易被混为一谈,必须分开处理:
| 目标 | 用什么做 | 产出 |
| --- | --- | --- |
| A. 摄像头兼容性验证 | MiBeeNvr(现成 NVR) | 采购白名单 |
| B. 效果演示给非技术干系人 | MiBeeNvr | 可看的界面 |
| C. 架构风险验证 | 200 行自研骨架 + mediamtx | 生产代码 |
## 关键判断:MiBeeNvr 不是「v0 版本」
Mi-Bee-Studio/MiBeeNvr 在架构上替代的是 **mediamtx**(它自带 HLS/WebRTC/HTTP-FLV/RTMP/SRT 全套媒体服务),而不是生产方案里要自研的设备管理层。
```
演示版: MiBeeNvr(一个 exe,全包)
生产版: mediamtx + 生成的 API 客户端 + 自研骨架 + 对账器 + NAT 隧道
↑ 没有一行代码从演示版继承
```
它跑通了,对生产架构零验证。NAT 穿透、path 对账、按需拉流、多租户这些真正会翻车的地方,它一个都不涉及(它假设摄像头与自己在同一局域网)。
因此:不要把它包装成第一版系统然后计划后续替换,替换是推倒重来而非迭代。
# 2. 目标 A/B:MiBeeNvr 作为测试台
## 2.1 项目基本情况
| 项 | 值 |
| --- | --- |
| 仓库 | Mi-Bee-Studio/MiBeeNvr |
| 许可 | MIT |
| 技术栈 | Go + Svelte 5 + SQLite,CGO_ENABLED=0 单二进制 |
| Web UI | 端口 9090,内置登录、实时预览、录像、系统指标 |
| 规模定位 | 树莓派 3B / 1GB 内存可跑(homelab 场景) |
## 2.2 为什么适合做兼容性测试台
- ONVIF 摄像头接入局域网后后台自动登记,无需手动扫描
- 认证摄像头进入「待激活」状态等待补凭据;未认证的直接开始录制
- IP 自愈:摄像头换 IP(Wi-Fi 漫游换 AP)时按序列号自动重定位并重连,使用 unicast 探测,可跨路由子网
- 一条 [install.sh](http://install.sh),10 分钟起来,界面上直接看哪个型号能发现、能出流、能 PTZ
比自己写测试代码快得多,失败信息本身就有价值:某型号它都认不出来,说明该型号 ONVIF 实现有问题,直接从采购清单划掉。
## 2.3 部署
```bash
# 方式一:一键安装
curl -fsSL https://raw.githubusercontent.com/Mi-Bee-Studio/MiBeeNvr/main/install.sh | sudo bash
# 方式二:预编译二进制
wget https://github.com/Mi-Bee-Studio/MiBeeNvr/releases/latest/download/mibee-nvr-amd64
chmod +x mibee-nvr-amd64
./mibee-nvr-amd64 init --password <yourpassword>
./mibee-nvr-amd64 -config mibee-nvr.yaml
# 打开 http://localhost:9090
```
## 2.4 使用红线
| 允许 | 禁止 |
| --- | --- |
| 实验室环境接自购测试摄像头 | 接入真实住户家中的摄像头 |
| 给干系人做效果演示 | 作为生产系统的第一版上线 |
| 阅读源码借鉴设计 | 作为长期依赖引入 go.mod |
理由:项目 84 star / 10 fork / 1 watcher,且功能面异常宽(SRT、WebRTC WHEP、小米 TUTK P2P、WebDAV、FTP、浏览器端 ONNX 推理、时移录制等),一个体量如此的项目在短时间做出这么宽的面,大概率有大量 AI 辅助生成,不能假设它被真实压测过。本项目是养老场景,摄像头对着卧室和卫生间,未经充分验证的组件出安全问题后果不是技术问题。
## 2.5 值得借鉴的源码
```
internal/onvif/ ONVIF 封装(与 EdgeX device-onvif-camera 对比着读)
internal/camera/ 健康监控 + 自动修复状态机
internal/store/ SQLite schema 设计
```
重点看两处真实踩坑设计:IP 自愈(序列号而非 IP 作身份)和待激活状态机(批量开通的中间态)。这两点自己从零设计想不到。
前端 go:embed 打包成单二进制的做法,将来给自研服务加 Web UI 时可直接参考。
# 3. 目标 C:架构验证骨架(生产代码)
这部分是真正要写的东西,约 200 行,全部为生产代码而非丢弃品。
## 3.1 范围
```
mediamtx(配置文件里写死 5 路测试摄像头)
+
单个 Go 程序:
ONVIF 连接 -> GetStreamUri -> 调 mediamtx API 建 path -> 每 30s 探活
```
跑通之后就已经站在生产架构上了,后面是往上加(SQLite、对账、隧道、多租户),不是推倒。
## 3.2 代码结构
```
cmd/edge-agent/main.go
internal/
onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime
store/ 先用 SQLite,schema 与生产 Postgres 保持一致
mtx/ oapi-codegen 生成的客户端 + 薄封装
reconcile/ 最小对账循环(幂等 + 退避)
probe/ 探活 goroutine
```
## 3.3 从第一天就要遵守的约定
| 约定 | 原因 |
| --- | --- |
| 设备身份用序列号,不用 IP | DHCP / Wi-Fi 漫游会换 IP |
| 建 path 前先查是否存在 | 盲目 add 会踢掉正在推流的 publisher |
| 改配置前比对哈希 | 同上;没变就不调 API |
| 重试用指数退避 | 无事务,只能靠幂等重试 |
| 并发限流(信号量 8) | 避免打爆 mediamtx API |
| 设备状态含 pending_activation | 批量开通必然有凭据待补的中间态 |
## 3.4 mediamtx 侧最小配置
```yaml
api: yes
apiAddress: :9997
pathDefaults:
sourceOnDemand: yes
sourceOnDemandCloseAfter: 30s
rtspTransport: tcp
```
# 4. 摄像头兼容性验证清单(M0 出口标准)
采购 3-5 个候选型号,这是整个项目第一件要做的事。
## 4.1 必过项
| 验证项 | 通过标准 | 不通过的后果 |
| --- | --- | --- |
| GetProfiles | 返回至少两个 profile(主码流 + 子码流) | 无法自动获取子码流,需人工配置 |
| GetStreamUri | 返回可用的 RTSP URL | 无法自动化开通 |
| SetSystemDateAndTime | 调用成功且生效 | 事件时间戳不可信 |
| 子码流规格 | 可设为 640x360 或 704x576 左右 | 带宽和算力估算失效 |
| 编码格式 | 支持 H.264 | H.265 浏览器无法原生预览 |
| WS-Security 认证 | digest 或 usernametoken 任一可用 | 认证失败 |
| RTSP over TCP | 长时间稳定不断流 | 家庭网络下 UDP 丢包严重 |
## 4.2 加分项
- 支持 ONVIF 事件订阅(移动侦测)
- 支持修改 GOP 长度(影响回捞缓存的关键帧间隔)
- 支持创建独立的只读用户(不必用 admin 凭据接入)
## 4.3 结论落地
任何一项必过项失败的型号,直接从采购清单划掉。后期写兼容代码的成本远高于换型号。验证结果整理成表格,作为采购白名单归档。
# 5. 阶段产出如何流入生产
| 演示阶段产出 | 流向 |
| --- | --- |
| 采购白名单 | 生产方案的硬件选型 |
| 骨架代码(onvif / store / mtx / reconcile) | 直接成为生产代码基础 |
| MiBeeNvr 的设计借鉴(IP 自愈、待激活态) | 写进生产的对账器与状态机 |
| MiBeeNvr 的界面截图 | 干系人沟通材料 |
| MiBeeNvr 本身 | 不进入生产 |
# 6. 关于 Web 管理界面
生产组合(mediamtx + 自研服务)没有现成的管理界面。三个选择:
1. 前期不做 UI — 用 REST API + 脚本撑过验证期。250 路的开通本来就是批量脚本,不该手点。
2. 临时用 go2rtc 的 1984 页面看流状态 — 但它默认无密码,局域网内任何人可访问,必须在配置里关闭或用防火墙隔离。
3. 正式做时长在自研服务上 — 因为要展示的是「家庭 -> 摄像头 -> 流状态」的业务视图,第三方面板展示的是「path 列表」,不是一回事。
> 补充:mediamtx 本身没有管理后台,作者明确表示精力有限、优先保证核心稳定,欢迎社区贡献。社区面板有 mediamtx-dashboard、lgcshy/mediamtx-ui、nabaco/mediamtx-ui、bcanfield/mediamtx-connect 等,均为独立进程通过 API 对接,可按需临时使用。
@@ -0,0 +1,729 @@
# 🏗️ 跌倒检测视频接入系统 — 线上生产开发方案
> 来源:Notion · https://app.notion.com/p/3af401dc38a681c29985ffdbd0e9f3d1
> 导出时间:2026-08-01
> 版本 v1 · 面向 200 户 / 250 路 IP 摄像头的跌倒检测视频接入系统。本文档描述**生产环境**架构与开发规范。演示与验证阶段见配套文档《演示与验证开发方案》。
# 1. 目标与约束
| 项 | 值 |
| --- | --- |
| 接入规模 | 约 200 个家庭,250 路 IP 摄像头 |
| 业务目标 | 视频流分析实现摔倒动作检测 |
| 关键约束 | 摄像头位于家庭路由器 NAT 之后,中心机房无法主动访问 |
| 未来扩展 | 毫米波雷达等异构传感器(架构必须预留) |
| 技术栈 | Go 为主,媒体层复用成熟开源组件 |
# 2. 架构总览
系统分为三个平面,接缝要尽可能少。
| 平面 | 组件 | 职责 |
| --- | --- | --- |
| 流控制面 | mediamtx + 自研 Go 编排服务 | path 生命周期、按需拉流、录制、对账 |
| 设备业务面 | 自研 Go 服务(单 exe)+ Postgres | 家庭/摄像头台账、凭据、在线状态、开通与停用 |
| 感知面 | 自研推理服务 | 抽帧、姿态模型、跌倒判定 |
## 2.1 控制面与数据面分离(核心决策)
这是解决 NAT 问题的关键:
- **控制面走隧道**:每户边缘盒子跑 WireGuard,中心服务通过隧道访问摄像头的 ONVIF 端口。ONVIF 控制流量极小,只在配置和探活时产生。
- **数据面走推流**:视频不走隧道。边缘盒子主动把流推到中心 mediamtx(RTSP announce / SRT / RTMP)。
### IP 规划
每户分配一个不冲突的内网段,摄像头地址确定化:
```
10.<户号高位>.<户号低位>.0/24
摄像头固定从 .10 起分配
```
这样设备地址可预测,不依赖自动发现,也便于对账。
# 3. 组件选型
| 组件 | 仓库 | 角色 | 说明 |
| --- | --- | --- | --- |
| mediamtx | bluenviron/mediamtx | 媒体路由 | MIT,Go,单二进制。协议互转、按需拉流、录制、鉴权回调、Prometheus |
| gortsplib | bluenviron/gortsplib | RTSP 库 | mediamtx 作者同款。用于自研 worker 拉流并做帧级丢弃 |
| ONVIF 库 | IOTechSystems/onvif 或 use-go/onvif | 设备控制 | 前者是 EdgeX device-onvif-camera 内部使用的增强版 |
| API 客户端 | 自行用 oapi-codegen 从 mediamtx OpenAPI 生成 | mediamtx 控制 | 见 3.1 |
| 边缘推流 | mediamtx 或 go2rtc | 边缘盒子 | 杂牌摄像头兼容性差时用 go2rtc 兜底 |
## 3.1 关于 mediamtx-sdk-go
zbiljic/mediamtx-sdk-go 是个人项目,用 ogen 从官方 OpenAPI 规范生成。**建议自行生成客户端**,避免依赖第三方更新节奏:
```bash
# 从 mediamtx 仓库取 apidocs/openapi.yaml
oapi-codegen -package mtxapi -generate types,client openapi.yaml > mtxapi/client.go
```
好处是版本跟随实际部署的 mediamtx,不会因上游停更而受阻。
# 4. 关键设计决策
## 4.1 mediamtx 只管流,不管设备
mediamtx 的数据模型里没有 camera 实体,只有 path(名字 + source URL + 配置)。它不知道 URL 背后是哪个品牌、是否同一台设备的两条码流、这户是否欠费。
必须自建设备业务面。三条硬约束:
1. **API 改的配置不落盘** — mediamtx 重启后通过 API 添加的 path 全部消失。设备台账必须在自己的数据库,服务启动时 replay。
2. **改配置会踢掉当前发布者** — 任何通过 API 的 path 配置变更都会触发 path reload,断开正在推流的 source,即使改动本身与它无关。因此改配置前必须先判断「真的变了吗」,没变绝对不调 API。
3. **没有 enable/disable 开关** — 只能删除 path,不能停用。
## 4.2 按需拉流(sourceOnDemand)
```yaml
pathDefaults:
sourceOnDemand: yes
sourceOnDemandStartTimeout: 10s
sourceOnDemandCloseAfter: 30s
```
语义:只有 reader 连上来时才去连摄像头,没人看就断开。对本系统等价于「AI worker 订阅 = 开始分析,取消订阅 = 自动停止拉流」,是最省带宽和算力的开关方式。
## 4.3 停用某路摄像头的四种粒度
按副作用从轻到重:
| 需求 | 做法 | 效果 |
| --- | --- | --- |
| 暂停分析,流保留 | worker 取消订阅 | 配合 sourceOnDemand 自动停止拉流,随时恢复 |
| 立即断开,允许重连 | `POST /v3/rtspsessions/kick/{id}`(RTMP/SRT/WebRTC 各有对应接口) | 断一次,客户端会重连 |
| 永久拒绝 | 鉴权服务对该 path 返回 401 + kick | 推不上来,且无需改 mediamtx 配置 |
| 彻底移除 | `DELETE /v3/config/paths/delete/{name}` | path 消失,重启后靠自有库恢复 |
标准组合:先 kick 断开、再让鉴权拒绝 = 立刻生效且不会重连。
## 4.4 鉴权用 HTTP 回调
```yaml
authMethod: http
authHTTPAddress: http://control-plane:8080/mtx-auth
paths:
"~^home-.*$":
source: publisher
```
每次边缘端推流,mediamtx POST 到自有鉴权服务,带 user/password/path/IP/action。返回 200 或 401 即控制了「哪些摄像头允许接入」。注销、欠费、隐私模式全部在自有业务库判断,mediamtx 侧零配置变更。
## 4.5 为雷达预留
未来接入毫米波雷达后,架构会发生一次反转:雷达 7x24 常开做一级触发,视频做二级确认。
- 算力:常态 GPU 占用接近零,只在事件时唤醒推理。250 路常态推理需 2-4 张卡,改为触发式后一张即可。
- 隐私:卫生间、卧室只放雷达不放摄像头;客厅摄像头平时不解码不存储。这是养老场景能否落地的分水岭。
- 误报:纯视觉对遮挡/逆光/坐下误报高,纯雷达对快速坐下和弯腰误判,两级串联是行业标准做法。
当前阶段的设计要求:设备表结构不要写死为「摄像头表」,预留 device_type;编排服务的触发入口要独立成一个 handler,将来接雷达告警时不用改动流控制逻辑。
# 5. 数据模型
```sql
-- 家庭
CREATE TABLE households (
id BIGSERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
subnet CIDR NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 设备(不要命名为 cameras,为雷达预留)
CREATE TABLE devices (
id BIGSERIAL PRIMARY KEY,
household_id BIGINT NOT NULL REFERENCES households(id),
device_type TEXT NOT NULL,
serial TEXT UNIQUE NOT NULL,
vendor TEXT,
model TEXT,
onvif_addr TEXT,
onvif_user TEXT,
onvif_secret TEXT,
state TEXT NOT NULL,
last_seen_at TIMESTAMPTZ
);
-- 流绑定
CREATE TABLE stream_bindings (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT NOT NULL REFERENCES devices(id),
mtx_instance TEXT NOT NULL,
path_name TEXT NOT NULL,
rtsp_url TEXT,
profile_token TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE (mtx_instance, path_name)
);
-- 对账状态
CREATE TABLE sync_state (
binding_id BIGINT PRIMARY KEY REFERENCES stream_bindings(id),
desired_hash TEXT,
applied_hash TEXT,
last_error TEXT,
retry_after TIMESTAMPTZ
);
```
字段说明:device_type 取 camera / radar / button;state 取 pending_activation / active / disabled;onvif_secret 需加密存储;rtsp_url 存 ONVIF GetStreamUri 返回的子码流地址。
**设计要点**:以序列号而非 IP 作为设备身份。家庭 Wi-Fi 漫游或 DHCP 续租会导致 IP 变化,按序列号重定位是唯一可靠的做法。
# 6. 对账器(Reconciler)
系统最容易出竞态的地方,也是自研工作量的核心。
## 6.1 职责
数据库是唯一真相源,**mediamtx 和 Savant 都是被驱动的从属状态**。对账器持续把三者拉齐。
关键原则:**水平触发(level-triggered)而非边沿触发(edge-triggered)**。对账器不处理「事件」,只不断比对期望状态与实际状态并向期望收敛。这是处理部分失败的唯一可行方案(见 6.5)。
```go
func (r *Reconciler) Sync(ctx context.Context) error {
desired := r.db.EnabledBindings() // 真相源
actual := r.mtx.ListPaths(ctx) // 生成的 API 客户端
add, del, update := diff(desired, actual)
sem := semaphore.NewWeighted(8) // 限流,别打爆 API
for _, b := range add {
// 幂等:已存在则跳过,绝不盲目 add(会踢掉 publisher)
// 重试:指数退避,失败写回 sync_state.retry_after
}
for _, b := range update {
// 关键:先比对 desired_hash 与 applied_hash
// 没变就绝对不调 API
}
// del 同理
}
```
## 6.2 必须处理的边界情况
| 情况 | 处理 |
| --- | --- |
| mediamtx 重启 | path 数量骤降触发全量重建;也要有定时全量对账兜底 |
| 配置变更踢 publisher | 变更前比对哈希;确需变更时先标记为维护窗口 |
| API 批量失败 | 无事务。第 137 个失败时前 136 个已生效,必须幂等重试而非回滚 |
| 并发打满 | 信号量限流到 8 并发 |
| path 漂移 | 定时全量 diff,处理人工在 mediamtx 侧的手改 |
| 设备离线 vs 无人订阅 | 两者在 mediamtx 侧表现相同,靠自有探活区分 |
## 6.3 探活
独立 goroutine,对每个 active 设备定期做 ONVIF 轻量调用(如 GetSystemDateAndTime),指数退避,更新 devices.last_seen_at。**不要用 path 在线状态代替设备在线状态**。
## 6.4 双系统对账:mediamtx + Savant
引入 Savant(第 9 节)后,一个设备的完整开通需要在**两个独立系统**上各做一次操作,而两者之间**没有分布式事务**。
### 数据模型调整
第 5 节的 `sync_state` 拆成每系统一行:
```sql
DROP TABLE sync_state;
CREATE TABLE sync_state (
binding_id BIGINT NOT NULL REFERENCES stream_bindings(id),
system TEXT NOT NULL, -- 'mediamtx' | 'savant'
desired_hash TEXT,
applied_hash TEXT, -- NULL = 尚未生效
last_error TEXT,
attempt INT NOT NULL DEFAULT 0,
retry_after TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (binding_id, system)
);
-- 便于运维发现中间态
CREATE VIEW binding_health AS
SELECT b.id, b.path_name,
bool_and(s.desired_hash = s.applied_hash) AS converged,
max(s.attempt) AS max_attempt
FROM stream_bindings b JOIN sync_state s ON s.binding_id = b.id
WHERE b.enabled
GROUP BY b.id, b.path_name;
```
一个 binding 只有当**两行都收敛**时才算真正开通。`converged = false` 的就是中间态,必须在 Grafana 上可见。
### 操作顺序(不对称,必须遵守)
| 动作 | 顺序 | 理由 |
| --- | --- | --- |
| **开通** | mediamtx 先 → Savant 后 | 流必须先存在,Savant 才能挂上去;反之 Savant 会反复重试连不存在的源 |
| **停用** | Savant 先 → mediamtx 后 | 先停消费再断流;反之 Savant 会看到源断开并报错告警 |
建立和拆除是逆序的,这一点要写进代码注释,很容易被后来人改错。
### ⚠️ 关键耦合:Savant source = mediamtx reader
`sourceOnDemand: yes` 的语义是「有 reader 才拉流」。**Savant 挂上 source 就是一个 reader**,也就是说:
- 挂载 Savant source → mediamtx 立即开始拉流 → 产生带宽和连接开销
- 卸载 Savant source → 30s 后(`sourceOnDemandCloseAfter`)mediamtx 自动断流
因此:**「暂停分析」必须通过卸载 Savant source 实现,不能只在 Python 插件里 `return`**。后者会让 mediamtx 持续拉流、带宽白烧,而监控上看起来一切正常。
这也意味着第 4.3 节「停用粒度」表格里的第一行(暂停分析)具体实现就是卸载 Savant source。
## 6.5 部分失败:不回滚,只收敛
典型场景:mediamtx path 建好了,Savant source 挂载失败。
**错误做法**:回滚删掉 mediamtx path。回滚本身也会失败,那时候你需要回滚的回滚,无限递归。
**正确做法**:保留已成功的部分,把失败的那行 `sync_state` 标为未收敛 + 退避重试。下一轮对账会看到 mediamtx 已收敛(跳过)、Savant 未收敛(重试)。
代价:**会有一个无人消费的 mediamtx path 短暂存在**。因为 `sourceOnDemand` 的存在,这个 path 没有 reader 就不会拉流,**成本接近零**。这是 `sourceOnDemand` 给双系统对账带来的额外好处——中间态是安全的。
### 重试预算
| 参数 | 值 |
| --- | --- |
| 初始退避 | 5s |
| 退避倍率 | 2x,上限 5min |
| 告警阈值 | `attempt > 5` 且持续 > 15min → 告警 |
| 放弃阈值 | 不放弃。持续重试,靠告警引入人工 |
不设放弃阈值是故意的:对账器放弃了,设备就静默地不工作了,而这是养老监护场景不可接受的失败模式。
## 6.6 孤儿资源清理
对账不只是「把缺的补上」,还要「把多的删掉」。三类孤儿:
| 类型 | 成因 | 处理 |
| --- | --- | --- |
| mediamtx 有 path,DB 无 | 人工手改、旧版本残留 | 删除(但先日志,不静默) |
| Savant 有 source,DB 无 | 同上 | 卸载 |
| DB 有 binding,两边都无且长期未收敛 | 开通失败后无人处理 | 告警,不自动删 DB |
**清理动作必须有安全闸**:单轮对账删除量超过总量的 10%(如 25 路)时**停止并告警**,不执行。这道闸是防数据库连接异常或查询 bug 导致的「期望集合意外为空」——那会把 250 路全部拆掉。
## 6.7 完整循环
```go
func (r *Reconciler) Sync(ctx context.Context) error {
desired := r.db.EnabledBindings() // 真相源
if len(desired) == 0 && r.lastDesiredCount > 0 {
return ErrSuspiciousEmptyDesired // 6.6 安全闸
}
mtxActual, err1 := r.mtx.ListPaths(ctx) // 分片内所有实例
svActual, err2 := r.savant.ListSources(ctx)
if err1 != nil || err2 != nil {
return errors.Join(err1, err2) // 拿不到实际状态就不动作
}
plan := buildPlan(desired, mtxActual, svActual)
// plan 内部已按 6.4 的顺序约束排序:
// teardown: savant 先 → mtx 后
// setup: mtx 先 → savant 后
for _, step := range plan {
if !step.DueForRetry(now) { continue } // 退避窗口未到
if step.DesiredHash == step.AppliedHash { continue } // 没变,绝不调 API
sem.Acquire(ctx, 1) // 每系统独立限流
go func() {
defer sem.Release(1)
if err := step.Apply(ctx); err != nil {
r.db.MarkFailed(step, err) // 退避,不回滚
return
}
r.db.MarkApplied(step) // applied_hash = desired_hash
}()
}
}
```
**两个独立的限流预算**:mediamtx API 和 Savant API 各自 8 并发,不共享信号量。否则一个系统变慢会饿死另一个。
**循环周期**:正常 30s;有未收敛项时降到 5s;另有每 10min 一次的全量对账(含孤儿扫描)。
## 6.8 必须暴露的指标
| 指标 | 用途 |
| --- | --- |
| `reconciler_unconverged_bindings{system}` | 核心健康指标,持续 > 0 即异常 |
| `reconciler_apply_duration_seconds{system,action}` | 发现 API 变慢 |
| `reconciler_orphans_detected{type}` | 有人手改了生产系统 |
| `reconciler_safety_brake_triggered` | 安全闸触发,**应该直接呼叫**人 |
| `reconciler_loop_duration_seconds` | 循环超时 = 规模到顶,该分片了 |
# 7. mediamtx 配置要点
```yaml
api: yes
apiAddress: :9997
metrics: yes
metricsAddress: :9998
authMethod: http
authHTTPAddress: http://control-plane:8080/mtx-auth
pathDefaults:
sourceOnDemand: yes
sourceOnDemandCloseAfter: 30s
rtspTransport: tcp # 家庭网络下 UDP 丢包严重
record: no
recordDeleteAfter: 7d
runOnOnline: curl -s -X POST http://control-plane:8080/hooks/online?path=$MTX_PATH
runOnOffline: curl -s -X POST http://control-plane:8080/hooks/offline?path=$MTX_PATH
paths:
"~^home-(\\d+)-cam(\\d+)$":
source: publisher
```
用一条正则 path 兜底,不要写 250 条静态配置。
> 注意:v1.19.x 的钩子命名为 runOnAvailable / runOnUnavailable / runOnOnline / runOnOffline,与旧版 runOnReady / runOnNotReady 不同。部署前核对所用版本的配置参考。
# 8. 容量与分片
## 8.1 带宽
| 项 | 估算 |
| --- | --- |
| 250 路子码流 640x360 @ 512 kbps | 约 125 Mbps 常态入向 |
| 250 路主码流 | 1-1.5 Gbps(不可行,必须用子码流做检测) |
## 8.2 算力
- 抽帧到 5 fps:250 x 5 = 1250 帧/秒解码
- CPU 软解不可行,需 NVDEC 或边缘解码
- 用 gortsplib 在 Go 侧做帧级丢弃(只保留关键帧/按抽帧间隔),比起用 ffmpeg 子进程可省约 70% 解码开销
## 8.3 分片
- 单 mediamtx 实例建议承载 60-80 路,250 路分 3-4 个实例
- 分片键用家庭 ID,映射存在 stream_bindings.mtx_instance
- 不要单实例硬扛:进程挂掉影响面过大,且默认 writeQueueSize: 512 下 250 路的内存与 GC 压力不小
# 9. 感知面:Savant 推理流水线
> 本节取代第 2 节表格中「感知面 = 自研推理服务」的占位描述。
## 9.1 为什么用框架而非完全自研
开源社区没有「批量摄像头摔倒检测」的成品(算法是差异化,不会有人开源),但有成熟的**多路视频分析流水线框架**。算法是自己的,工程化用现成的。
选定 **Savant**(insight-platform/Savant),基于 NVIDIA DeepStream 的高层框架,声明式 YAML + Python 插件函数。三个决定性理由:
1. **故障隔离** — 原生 DeepStream 中数据流与 pipeline 紧耦合,任一源崩溃则整个 pipeline 崩溃,重启加载模型要数秒且期间丢数据。Savant 的 adapter 通过 ZeroMQ 与框架通信,adapter 挂掉不影响 pipeline。200 户家庭网络掉线是常态,**这是 250 路规模的生死线**。
2. **可配置降级策略** — 可设为实时模式(算力不足时主动丢数据)或高吞吐模式(保证处理全部数据)。本系统选**实时模式**,丢帧优于积压崩溃。
3. **Replay(按需与非线性处理)** — REST 控制的录制/回放/转推服务,直接对应阶段二的雷达触发式二级确认。
同一套代码可跑在 Jetson(Orin Nano/NX/AGX)和数据中心 GPU 上,与本方案的边缘/中心混合部署契合。
**代价**:必须 NVIDIA 硬件(Turing/Ampere/Ada/Hopper/Blackwell 或 Jetson),与 DeepStream 版本强绑定(需查对照表),学习曲线陡,无企业支持。
**备选**:Pipeless(Rust,不绑 NVIDIA,支持 ONNX Runtime/OpenVINO/TensorRT,HTTP 动态增删流)——若硬件选型变更或规模缩小时启用。
## 9.2 模型选型与许可警告
> ⚠️ **Ultralytics YOLO 是 AGPL-3.0**。YOLOv8/v11 系列仓库默认对所有用户采用 AGPL-3.0;要集成进商业产品和服务而不遵守其开源要求,必须购买 Enterprise License。本项目是面向付费用户的网络服务,落在 AGPL 第 13 条覆盖范围内,严格执行则需开源整个服务端。**必须在 M3 前做出决定:买授权,或换模型。**
推荐替代:**RTMPose / RTMO**(MMPose 项目,Apache-2.0)
- RTMPose-m:COCO 75.8% AP,GTX 1660 Ti + TensorRT 下 430+ FPS,Intel i7-11700 CPU + ONNXRuntime 下 90+ FPS
- 支持 ONNXRuntime、TensorRT、ncnn、OpenVINO、RKNN 多种部署后端(通过 MMDeploy)
- 关键点定义同为 COCO 17 点,**现有判定算法基本不用改**
- 430 FPS 的单卡吞吐意味着第 8 节估算的 1250 fps 需求可用 2-3 张中端卡满足
## 9.3 Pipeline 架构
```
边缘推流 → mediamtx(250 路汇聚,按需拉流)
↓ RTSP
Savant RTSP source adapter(多路复用 + 故障隔离)
↓ ZeroMQ / Protobuf
Buffer Adapter(磁盘持久化缓冲,抗突发)
↓
Savant module
├─ RTMPose / TensorRT(姿态关键点)
└─ Python 插件:时序窗口 + 跌倒判定(**自研部分**)
↓ ZeroMQ / Protobuf
Go 控制面(告警、去重、防抖、通知)
```
需要自己写的只有中间那个 Python 插件,前后两段是现成的。
## 9.4 与 Go 控制面的对接契约
一条 pipeline 是自给自足的服务,通过高性能流式 API 与外界通信,视频和元数据统一使用 Protocol Buffers 协议。
| 需求 | 通道 | 语言无关 |
| --- | --- | --- |
| 接收检测结果 | 直接订阅 sink 的 ZeroMQ 端点,解 protobuf | ✅ |
| 动态增删流 | 动态挂载/卸载 source 与 sink,无需重载 pipeline | ✅ |
| 回放/回捞 | Replay 服务的 REST 接口 | ✅ |
| 程序化灌数据/调试 | Client SDK(**仅 Python**,集成 OpenTelemetry) | ❌ |
**设计约束:优先走 ZeroMQ 和 REST 两条语言无关通道,Go 服务不得依赖 Python 中间层。** Client SDK 仅用于算法调试和测试。
因为两边都是 API 驱动,**第 6 节的对账器可以同时驱动 mediamtx 和 Savant**:开通 = 建 path + 挂 source;停用 = kick + 卸 source。逻辑写在同一个 reconciler 里。
## 9.5 关键配置决策
**必须开启 Buffer Adapter**。它实现了 adapter 与 module 之间的持久化事务性磁盘缓冲,用于支撑资源消耗不可预测的 pipeline、抗住流量突发,并把自身条目数和大小导出到 Prometheus。
> 本场景天然有突发:夜里安静无事,早上起床时段几十户同时有活动。**一开始就加上,别等出问题再补。**
**AO-RTSP Sink 仅用于调试**。它把处理后、带元数据标注的视频通过 RTSP 和 LL-HLS 播出去,用来直观确认某一路的处理效果。
> ⚠️ **硬件限制**:GeForce 系列显卡同时硬件编码的流数上限为 **3**。若给多路挂 AO-RTSP 可视化,消费级卡直接卡在 3 路,需 Quadro/Tesla/L4 类专业卡。**不要做成常态功能。**
**开发期开 Development Server**:支持改代码动态重载、不重启 pipeline,还能从 IDE 远程开发跑在 Jetson 或服务器上的代码。原生 DeepStream 改一行要重启加载模型等几十秒,这个工具能省大量时间。
## 9.6 可观测
Savant 没有管理界面(与 mediamtx 同构),但可观测完整:
- **Prometheus + Grafana**:pipeline 和 buffer adapter 导出运行指标,开发者可声明自定义指标与系统指标一起导出。**把每路的人体检出率、跌倒候选数、推理延迟声明成自定义指标** → Grafana 上就是现成运营看板
- **OpenTelemetry → Jaeger**:排查单帧处理延迟
- mediamtx 也出 Prometheus 指标,**一个 Grafana 全收**,前期不用写一行前端代码
## 9.7 算法分层
| 层 | 内容 | 位置 |
| --- | --- | --- |
| 姿态提取 | RTMPose,COCO 17 关键点 | Savant 推理单元 |
| 单帧启发式 | 包围框宽高比、重心高度、躯干角度 | Python 插件(现有算法) |
| 时序判定 | 30 帧滑窗,静态位置 + 动态速度特征 | Python 插件(待建) |
| 事件后处理 | 去重、防抖、多源融合 | Go 控制面 |
参考实现:
- `GajuuzZ/Human-Falling-Detect-Tracks` — ST-GCN 对每个人轨迹的每 30 帧预测动作,支持站立/行走/坐下/躺下/起身/坐下动作/跌倒共 7 类。**七分类而非二分类是降误报的关键**
- `kelsonbatista/fall-detection-ai-project` — YOLO-pose 逐帧抽 128 维特征(静态位置 + 动态速度),30 帧窗口喂 LSTM。注意 GPL-3.0
**负面经验**:`Y-B-Class-Projects/Human-Fall-Detection` 用约 500 张网络图片训 LSTM 完全学不会,最后退回 if-else 规则。**小数据量下时序模型学不动,先解决数据再上模型。**
## 9.8 本节风险
| 风险 | 缓解 |
| --- | --- |
| Ultralytics AGPL-3.0 污染商业代码 | M3 前决策:买授权或换 RTMPose |
| 公开数据集与真实场景脱节(Le2i/UR Fall 均为演员摆拍的表演性跌倒,真实老人常为缓慢滑落、部分遮挡) | M3 首要产出是**真实误报案例库**,不是模型指标 |
| Savant 学习曲线陡 | M1 阶段先用 5 路跑通一条 pipeline,测出单路真实吞吐再反推卡数 |
| 绑定 NVIDIA + DeepStream 版本 | 预留 Pipeless 作为备选;推理代码与框架解耦 |
| 常态全量推理成本 | **Savant 不降低算力需求**,只解决「怎么高效跑 250 路」。雷达一级触发仍是唯一的数量级优化,与 Replay 配套使用 |
**无雷达时的过渡方案**:参考 Frigate 的「先用运动侦测过滤、有变化才跑推理」思路,与雷达触发是同一逻辑。
# 10. 预警信息管理系统
> 接住第 9 节的输出:检测出来之后怎么办。**本节描述的是产品本身,不是辅助设施。**
## 10.1 两类告警必须彻底分开
| | 运维告警 | 业务预警 |
| --- | --- | --- |
| 触发源 | 设备离线、binding 未收敛、GPU 过载、缓冲积压 | 跌倒判定 |
| 收件人 | 运维团队 | 家属、护理员、呼叫中心 |
| 延迟容忍 | 分钟级 | **秒级** |
| 漏报后果 | 服务降级 | 人身伤害 + 法律责任 |
| 方案 | Prometheus + Alertmanager(现成) | **自研状态机 + 复用投递通道** |
运维那侧接 Alertmanager 就完事,一两天工作量,本节不再展开。**两套系统不得共用投递通道和值班配置**,避免运维噪声淡化业务预警。
## 10.2 告警生命周期状态机
```
[触发] ─→ [投递中] ─→ [已送达] ─→ [已确认 ack] ─→ [处置中] ─→ [关闭]
│ │ │ │
│ │ └─超时未 ack─→ [升级] ─→ 下一级投递 │
│ └─投递失败─→ [升级] │
└─防抖命中─→ [抑制] [标记误报] ←─┘
```
**没有 ack 环节的告警系统等于没有。** 发出去和被人看到是两回事,整个设计围绕「确认或升级」这一对概念展开。
## 10.3 分级升级链
```
家属 App 推送
│ 30s 无 ack
▼
家属短信 + 语音呼叫
│ 60s 无 ack
▼
备用联系人(邻居/物业/护理员)
│ 90s 无 ack
▼
呼叫中心人工介入
```
每户的联系人链、时段策略(白天找子女、夜间找同住者)、优先级都要可配。升级超时值需支持按户覆盖——独居老人和有同住家属的超时策略应该不同。
## 10.4 数据模型
```sql
CREATE TABLE alerts (
id BIGSERIAL PRIMARY KEY,
household_id BIGINT NOT NULL REFERENCES households(id),
device_id BIGINT REFERENCES devices(id),
kind TEXT NOT NULL, -- 'fall' | 'no_motion' | 'device_offline_business'
confidence REAL,
triggered_at TIMESTAMPTZ NOT NULL,
state TEXT NOT NULL, -- triggered/delivering/delivered/acked/handling/closed/suppressed
acked_by BIGINT,
acked_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
outcome TEXT, -- true_positive / false_positive / unknown
evidence_uri TEXT, -- 回捞的视频片段 + 姿态序列
escalation_level INT NOT NULL DEFAULT 0
);
CREATE TABLE contact_chains (
id BIGSERIAL PRIMARY KEY,
household_id BIGINT NOT NULL REFERENCES households(id),
level INT NOT NULL, -- 0,1,2,3
contact_name TEXT NOT NULL,
channels JSONB NOT NULL, -- [{type:'push',token:...},{type:'sms',no:...}]
active_hours TEXT, -- '08:00-22:00' 或 NULL 表示全天
timeout_sec INT NOT NULL DEFAULT 30,
UNIQUE (household_id, level)
);
CREATE TABLE alert_deliveries (
id BIGSERIAL PRIMARY KEY,
alert_id BIGINT NOT NULL REFERENCES alerts(id),
level INT NOT NULL,
channel TEXT NOT NULL, -- push / sms / voice / webhook
provider TEXT NOT NULL, -- 便于多供应商冗余
sent_at TIMESTAMPTZ,
receipt_at TIMESTAMPTZ, -- 回执:NULL = 未确认送达
failed_reason TEXT
);
CREATE INDEX ON alerts (state) WHERE state NOT IN ('closed','suppressed');
```
关键设计:**投递是独立实体而非 alerts 的字段**。一次告警会产生多条投递记录(多级×多通道),每条各自有回执状态。
## 10.5 投递回执:三个不同的事实
| 事实 | 含义 | 字段 |
| --- | --- | --- |
| 发出去了 | 调用了上游 API | `sent_at` |
| 送达了 | 上游回执确认到达终端 | `receipt_at` |
| 被看到了 | 用户主动 ack | `alerts.acked_at` |
**没有回执就当失败并升级**,不要乐观假设。推送可能被系统杀掉、短信可能延迟、电话可能占线。
## 10.6 通道选型与冗余
**必须至少两条独立路径,且电话通道要能绕过互联网。** 推送依赖 APNs/FCM,短信依赖运营商,任何单一通道都可能整体故障。
| 层 | 选型 | 说明 |
| --- | --- | --- |
| 状态机、升级链、ack、审计 | **自研**(Go) | 业务定制度高,通用工具都要大改 |
| 多通道投递 | Novu / Apprise / ntfy,或直接对接云厂商 | 只解决「怎么发」 |
| 短信 + 语音呼叫 | 云厂商(双供应商) | `provider` 字段就是为切换准备的 |
> ⚠️ **不要用 Grafana OnCall**。OSS 版已于 2026-03-24 归档,仓库只读,仅修 CVSS 7.0+ 漏洞。更关键的是移动推送、短信和电话在归档当日停止工作(那些通道依赖 Grafana Cloud 中转)。调度和升级链还能跑,但**送不出去的告警系统等于没有**。
若阶段二引入 ThingsBoard(接雷达时),其告警子系统 + 规则链可覆盖状态机的一大半,届时自研部分可收缩到只做升级链和电话对接。
## 10.7 去重与防抖
| 规则 | 参数 | 理由 |
| --- | --- | --- |
| 同设备冷却期 | 5min 内不重复告警 | 避免同一事件多次触发 |
| 同家庭聚合 | 多设备同时触发合并为一条 | 客厅两个机位拍到同一次跌倒 |
| 已处置抑制 | 已 ack 的告警期间不再新建 | 护理员已到现场 |
| 维护窗口 | 住户主动静默(如家庭聚会) | **需限时且到期自动恢复** |
静默功能必须强制限时(建议最长 4h)并在到期前提醒。永久静默是这类系统最常见的事故成因。
## 10.8 误报反馈闭环(产品飞轮)
家属点「误报」这个动作,要自动把 `evidence_uri` 对应的视频片段和姿态序列推进标注队列。
这条链路直接解决 9.8 节的数据集问题:公开数据集全是演员摆拍,而**真实误报案例是你最稀缺的训练数据**。没有这个闭环,模型不会随运营时间变好。
同理,`outcome = true_positive` 的样本是正例金矿。**从 M3 第一天就要存,不要等到想训模型时才发现没数据。**
## 10.9 审计与合规
每条告警的完整生命周期需**不可篡改地**留存:
- 什么时间触发、基于什么证据
- 向谁发过、走的什么通道、是否送达
- 谁在什么时间确认、处置结果
- 升级到第几级、每级耗时
一旦出事,「系统有没有报、几点报的、谁接的、多久响应」是法律问题而非技术问题。建议 `alerts` 和 `alert_deliveries` **只追加不修改**(状态变更写事件表),并独立备份。
另需确认的合规项(开工前法务确认):视频证据的保存期限、家属查看权限范围、被监护者本人的知情同意。
## 10.10 SLA 目标(待 M3 实测校准)
| 指标 | 目标 |
| --- | --- |
| 跌倒发生 → 首次投递 | < 10s |
| 首次投递 → 送达回执 | < 5s(推送)/ < 30s(短信) |
| 全链路无人响应 → 呼叫中心 | < 3min |
| 告警丢失率 | 0(持久化后才确认触发) |
最后一行是硬约束:**告警先落库再投递**。进程崩溃后重启,未完成的升级链要能从数据库恢复继续——这跟第 6 节对账器的「水平触发」是同一个思路,启动时扫描所有未终态告警并接续处理。
# 11. EdgeX 升级路径(可选,阶段二)
当前阶段不引入 EdgeX。理由:它白送的能力(设备注册表、探活、命令抽象)约值一两周开发量,但代价是长期多背 4 个服务、跨服务追日志,且它假设设备在同一局域网、与 NAT 场景冲突。
## 何时值得切换
当确定要接入毫米波雷达、门磁、紧急按钮等异构设备时。EdgeX 的 device-mqtt / device-modbus / device-snmp 能复用同一套设备模型,这是它唯一压倒性的优势。
## 切换成本低的原因
ONVIF 调用那部分代码两边几乎一样(EdgeX 的 device-onvif-camera 内部就是用 IOTechSystems/onvif),对账器也不用改。
## 若切换,最小服务集
```
core-metadata 设备注册表
core-command 命令下发入口
device-onvif-camera ONVIF <-> REST
redis 消息总线
(consul 可用 -cp=none 省掉)
```
砍掉 core-data、app-service-*、security 全套。注意 discovery 在本场景基本不可用:netscan 对 200 个 /24 段是 5 万次探测,不现实;应使用预定义 deviceList 或 REST 批量录入,但保留 EnableStatusCheck / CheckStatusInterval 做探活。
另注意:EdgeX 3.x 起 API 路径是 /api/v3,网上大量 2.x 文档写的是 /api/v2。
# 12. 风险清单
| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| 摄像头 ONVIF 实现差异 | 部分型号无法接入 | 采购前用真机验证,见演示方案文档 |
| 摄像头时钟漂移 | 事件时间戳全乱 | 开通时调 SetSystemDateAndTime,定期校时 |
| NAT 隧道不稳 | 控制面失联 | 控制面失联不影响视频推流(已解耦);告警但不阻断 |
| path reload 踢流 | 用户看到卡顿 | 配置变更前比对哈希;变更集中在维护窗口 |
| H.265 兼容性 | 浏览器无法预览 | 摄像头统一设为 H.264 |
| 隐私合规 | 项目无法落地 | 尽早引入雷达做一级触发,减少视频常态采集 |
# 13. 里程碑
| 阶段 | 内容 | 出口标准 |
| --- | --- | --- |
| M0 | 摄像头兼容性验证 | 3-5 个候选型号跑通 GetProfiles / GetStreamUri / SetSystemDateAndTime |
| M1 | 单 exe 骨架 + mediamtx | 5 路摄像头自动建 path、探活、断线重建 |
| M2 | 对账器 + Postgres + 隧道 | 10 户试点,含开通/停用全流程 |
| M3 | 抽帧 worker + 推理接入 | 端到端跌倒告警跑通,拿到误报率基线 |
| M4 | 分片 + 灰度扩容 | 250 路稳定运行 |
| M5(阶段二) | 雷达接入 + 两级判定 | 误报率显著下降,视频常态采集下降 |