commit 8a99b388c4eb2a826d4f5a6b5163c88d95cacb4e Author: QiuSW <105186638@qq.com> Date: Mon Aug 3 17:22:22 2026 +0800 docs: initialize YoVision requirements and architecture diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3f925ef --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80de9f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +_reference/ +agent_sessions.txt diff --git a/Bell/.gitkeep b/Bell/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Bell/.gitkeep @@ -0,0 +1 @@ + diff --git a/Brain/.gitkeep b/Brain/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Brain/.gitkeep @@ -0,0 +1 @@ + diff --git a/Sense/.gitkeep b/Sense/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sense/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/raw/01-需求收集.md b/docs/raw/01-需求收集.md new file mode 100644 index 0000000..c3c136d --- /dev/null +++ b/docs/raw/01-需求收集.md @@ -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-通用场景应用方案》。 diff --git a/docs/raw/02-需求分析.md b/docs/raw/02-需求分析.md new file mode 100644 index 0000000..0209d0b --- /dev/null +++ b/docs/raw/02-需求分析.md @@ -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 场景包
规则模板 · 升级策略 · 话术 · 报表口径"] + L4["L4 业务编排
事件生成 · 去重防抖 · 预警状态机 · 处置流"] + L3["L3 能力层
检测 · 姿态 · 跟踪 · ReID · 属性 · (人脸)"] + L2["L2 流水线层
解码 · 抽帧 · 调度 · 故障隔离 · 触发式唤醒"] + L1["L1 接入层
设备台账 · ONVIF · 媒体路由 · 隧道 · 对账"] + L0["L0 基础设施
多租户 · 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["接入层
台账 · 隧道 · 媒体路由"] + B --> C{"触发判定
常态 / 运动 / 传感器"} + C -->|无触发| C + C -->|唤醒| D["推理流水线
检测 · 跟踪 · 姿态 · 属性"] + D --> E["规则引擎
主体过滤 × 时空 × 条件"] + E -->|未命中| F["丢弃
仅记指标"] + E -->|命中| G["事件生成器
回捞 pre-roll · 抓拍 · 元数据"] + G --> H{"去重 / 防抖 / 聚合"} + H -->|抑制| I["静默记录"] + H -->|通过| J["事件实例落库"] + J --> K["管理系统
看板 · 详情 · 处置"] + J --> L{"是否需要预警"} + L -->|是| M["预警状态机
投递 → 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-通用场景应用方案》的输入约束。任何技术选型若与这些结论冲突,应先回来修改本文档,而不是在方案里悄悄绕过。 diff --git a/docs/raw/03-通用场景应用方案.md b/docs/raw/03-通用场景应用方案.md new file mode 100644 index 0000000..d6ca725 --- /dev/null +++ b/docs/raw/03-通用场景应用方案.md @@ -0,0 +1,1255 @@ +# 🏗️ YoVision 智能视频事件平台 — 通用场景应用方案 + +> 版本 v0.4 · 2026-08-03 +> 上游约束:《02-需求分析》§1 的 C1–C9 +> 本文档描述**如何用一套底座支撑多个场景**,以及每个场景怎么落到配置上 + +--- + +# 1. 方案总纲 + +## 1.1 一句话 + +**一套通用底座(接入 → 流水线 → 能力 → 编排),场景差异全部收敛到「场景包」这一层配置里。** + +新增一个场景 = 写一个场景包(规则模板 + 升级策略 + 报表口径 + 话术),**不改核心代码**。这是《02》M5 的验收标准。 + +## 1.2 分层落地 + +```mermaid +flowchart TD + subgraph L5["L5 场景包(配置)"] + SP1["居家养老包"]:::pack + SP2["校园包"]:::pack + SP3["社区包"]:::pack + end + subgraph L4["L4 业务编排(Go)"] + RE["规则引擎"] --> EV["事件生成器
回捞 · 抓拍 · 去重"] + EV --> AL["预警状态机
ack · 升级 · 回执"] + AL --> DL["投递适配器
推送/短信/语音/Webhook"] + end + subgraph L3["L3 能力层(模型)"] + DET["检测+跟踪
YOLO"] --> POSE["姿态
YOLO-pose"] + DET --> ATTR["属性"] + DET --> REID["ReID 匿名同一性
默认手段"] + DET -.旁路.-> FACE["人脸比对
租户授权后"] + POSE --> TEMP["时序行为分析"] + end + subgraph L2["L2 流水线(Savant / 等价框架)"] + TRIG["触发器
运动/ONVIF事件/传感器"] --> PIPE["解码·抽帧·推理调度"] + BUF["磁盘缓冲"] --> PIPE + end + subgraph L1["L1 接入(Go + mediamtx)"] + DEV["设备台账"] --> RECON["对账器"] + RECON --> MTX["媒体路由"] + TUN["WireGuard 隧道"] --> DEV + end + subgraph L0["L0 基座"] + MT["多租户 · RBAC"] + ST["对象存储"] + OBS["Prometheus + Grafana + 审计"] + end + L5 -.驱动.-> L4 + L4 --> L3 --> L2 --> L1 --> L0 + classDef pack fill:#e8f0fe,stroke:#4285f4 +``` + +## 1.3 组件选型 + +| 层 | 组件 | 许可 | 角色 | 备注 | +| --- | --- | --- | --- | --- | +| L1 媒体路由 | **mediamtx** (bluenviron) | MIT | 协议互转、按需拉流、录制、鉴权回调、Prometheus | Go 单二进制 | +| L1 RTSP 库 | **gortsplib** | MIT | 自研 worker 拉流 + 帧级丢弃 | 同作者 | +| L1 ONVIF | **IOTechSystems/onvif** 或 use-go/onvif | Apache-2.0 | 设备控制 | 前者是 EdgeX device-onvif-camera 内部使用的增强版 | +| L1 API 客户端 | 自行用 **oapi-codegen** 从 mediamtx OpenAPI 生成 | — | 控制 mediamtx | ⚠️ 不依赖第三方 SDK 的更新节奏 | +| L1 边缘推流 | mediamtx 或 **go2rtc** | MIT | 边缘盒子 | 杂牌摄像头兼容性差时用 go2rtc 兜底 | +| L1 隧道 | **WireGuard** | GPL-2.0(内核态,不链接) | 控制面通道 | | +| L2 流水线 | **Savant** (insight-platform) | Apache-2.0 | 多路视频分析框架 | 基于 NVIDIA DeepStream;备选见 §6.2 | +| L3 检测 | **YOLO**(Ultralytics v8/v11) | AGPL-3.0 → **购 Enterprise License** | 目标检测(人、车、电动车、堆物多类) | 已决策沿用;授权采购见 §2.4 | +| L3 姿态 | **YOLO-pose**(COCO 17 点) | 同上 | 关键点 | 备选 RTMPose/RTMO(Apache-2.0,同为 COCO 17 点,可直接替换) | +| L3 跟踪 | ByteTrack / BoT-SORT | MIT | 多目标跟踪 | | +| L3 ReID | **ReID 模型(匿名同一性)** | 视模型,选 Apache/MIT 系 | 跨镜同一性,**默认手段** | 产出 `anon_id`,不涉及身份 | +| L3 人脸 | 人脸检测 + 特征提取(InsightFace 系等) | ⚠️ 权重许可需逐个核对 | 身份比对,**按租户授权开启** | 见 §2.8.3 | +| L3 向量检索 | pgvector 或 Milvus | 开源 | 底库比对 | 底库量小用 pgvector 即可 | +| L4 全部 | **自研 Go** | — | 规则、事件、预警、投递 | 业务定制度高,通用工具都要大改 | +| L4 投递 | Novu / Apprise / ntfy 或直连云厂商 | — | 只解决"怎么发" | 短信/语音**双供应商** | +| L0 数据库 | PostgreSQL | — | 唯一真相源 | | +| L0 存储 | MinIO / S3 兼容 | — | 证据文件 | | +| L0 可观测 | Prometheus + Grafana + Jaeger | — | 指标/看板/链路 | mediamtx 与 Savant 都出 Prometheus 指标,一个 Grafana 全收 | +| L0 运维告警 | Alertmanager | — | **仅运维**,不碰业务预警 | | + +### 明确不用的东西 + +| 组件 | 不用的理由 | +| --- | --- | +| **Grafana OnCall (OSS)** | 已于 2026-03-24 归档,仓库只读仅修 CVSS 7.0+;**移动推送、短信、电话在归档当日停止工作**(依赖 Grafana Cloud 中转)。送不出去的告警系统等于没有 | +| **完整 MiBeeNvr** | 可作实验室兼容性测试台与选择性源码参考,**不整仓进入生产**。它已支持 RTMP/SRT push-in、远程访问等跨网络能力,但其单体媒体、录像、SQLite、设备管理和 UI 与本项目的 MediaMTX/Sense/Bell 边界重叠;未见公开的 128 路多分片验收数据,也没有本项目需要的多租户真相源与配额模型 | +| **EdgeX Foundry(本期)** | 白送的能力约值一两周开发量,代价是长期多背 4 个服务;且它假设设备在同一局域网,与 NAT 场景冲突。接入雷达/门磁/Modbus 设备时再评估切换(切换成本低:ONVIF 部分两边几乎一样,对账器不用改) | + +## 1.4 NVR 开源项目选型结论(已定,2026-08-03) + +本项目不 fork 一个完整 NVR 作为生产系统。当前默认由客户已有 NVR 承担常态录像,YoVision 负责统一接入、AI 分析和事件证据,因此选型拆成三类:**MediaMTX 是生产媒体数据面,MiBeeNvr 是 Sense 的主源码参考,Frigate 是事件录像与回看交互参考**。 + +| 项目 | 许可 / 技术栈 | 与本项目的匹配 | 决策 | +| --- | --- | --- | --- | +| [MediaMTX](https://github.com/bluenviron/mediamtx) | MIT / Go | 协议路由、按需拉流、fMP4/MPEG-TS 录像、按时间回放、HTTP/JWT 鉴权、Control API、Prometheus;官方提供横向扩展示例 | **直接部署为生产依赖,不复制源码**;Sense 通过自行生成的 API 客户端驱动 | +| [MiBeeNvr](https://github.com/Mi-Bee-Studio/MiBeeNvr) | MIT / Go + Svelte | ONVIF、设备发现、按序列号重新定位 IP、健康监控、退避重连、REST API 与现代 UI 最接近 Sense;但完整系统与 MediaMTX/Bell 重叠,内部以单机 NVR 为中心 | **首选源码参考,按白名单选择性重写/移植,不整仓 fork,不加入 go.mod** | +| [Frigate](https://github.com/blakeblackshear/frigate) | MIT / Python + FFmpeg + go2rtc | 主/子码流角色、低成本运动过滤、pre/post-capture、事件保留策略和 Review/History 时间轴成熟;自带推理进程会与 Brain/Savant 重复 | **只借鉴事件录像语义和 UI 工作流,不引入其推理与进程架构** | +| [go2rtc](https://github.com/AlexxIT/go2rtc) | MIT / Go | 特殊流格式、WebRTC/MSE、双向语音与 RTSP restream 兼容性强 | **仅作为边缘兼容插件**;不成为中心真相源,管理页面默认不暴露 | +| [Moonfire NVR](https://github.com/scottlamb/moonfire-nvr) | GPL-3.0-or-later / Rust | 不解码、不转码的低开销录像索引和任意时间段 MP4 导出值得研究;仍为 pre-1.0 | **只看设计,不复制代码**,避免 GPL 传染和存储格式兼容风险 | +| [ZoneMinder](https://github.com/ZoneMinder/zoneminder) | GPL-2.0 / C++、Perl、PHP | 传统集成式 VMS,功能完整但技术栈、部署模型和系统边界与 Sense/Brain/Bell 差异大 | **不作为实现基线** | +| [Shinobi](https://gitlab.com/Shinobi-Systems/Shinobi) | 许可证按采用版本单独核验 / Node.js | 同属完整 VMS,主仓已迁至 GitLab;引入后仍需拆除其媒体、用户和事件体系 | **不作为实现基线** | + +核验资料:MediaMTX 官方[录像](https://mediamtx.org/docs/features/record)、[回放](https://mediamtx.org/docs/features/playback)、[横向扩展](https://mediamtx.org/docs/features/scalability)与 [Control API](https://mediamtx.org/docs/features/control-api);Frigate 官方[视频流水线](https://docs.frigate.video/frigate/video_pipeline/)与[录像保留策略](https://docs.frigate.video/configuration/record/)。MiBeeNvr 源码分析基线为本地 `_reference/mibeenvr` 的提交 `b8e7dd426d73d3b1910254c196d0937db9517a0f`(2026-08-01);实施前须重新核对上游版本、许可证与安全公告。 + +### MiBeeNvr 是否包含 MediaMTX 的功能 + +**部分包含,但不能等价替代。** MiBeeNvr 覆盖了单机 NVR 常用的摄像头接入、直播和录像功能;MediaMTX 的核心价值则是作为通用媒体路由器,为多个发布者/读者提供稳定 path、细粒度鉴权、会话控制和横向扩展。两者的功能交集很大,架构角色不同。 + +| 能力 | MiBeeNvr(当前分析基线) | MediaMTX | 对 YoVision 的影响 | +| --- | --- | --- | --- | +| ONVIF、设备发现、PTZ、序列号/IP 自愈 | **有,且更接近设备管理产品** | 不负责设备实体 | 借鉴 MiBeeNvr,落入 Sense | +| HLS/LL-HLS、WebRTC、浏览器直播 | 有,另含 HTTP-FLV/WebSocket | 有 HLS/WebRTC,侧重协议路由 | 初期界面演示可直接用 MiBeeNvr | +| RTMP/SRT 接入与 RTMP/RTSP 转推 | 有 | 有,且可在多种协议间发布、读取和代理 | 都能覆盖常见接入,但不能仅按协议名称判断可替换 | +| 通用 RTSP path 供 Brain、播放器、录像器共同读取 | 当前只确认摄像头 RTSP 输入与向外部目标转推,**未发现等价的通用 RTSP 读服务** | 核心能力:publish/read/proxy,多读者共享 | 含 Brain 的端到端版本应从 M1 引入 MediaMTX,否则 Brain 需要重复直连摄像头或再补一个媒体服务器 | +| 分段录像、保留、列表、时间轴与回放 | 有,MP4/SQLite/文件系统一体化 | 有 fMP4/MPEG-TS 录像与按时间回放,业务索引由上层负责 | 演示可用 MiBeeNvr;生产事件证据仍按 Sense/Bell 边界实现 | +| 鉴权 | 本地 BasicAuth、会话 token、API key | internal/HTTP/JWT,可按 path 与 publish/read/playback/api 等 action 控制 | MiBeeNvr 鉴权不能替代多租户 RBAC 与媒体动作授权 | +| 管理 API | 面向摄像头、录像、设置和 UI 的 NVR REST API | 面向 path、配置、发布者/读者和协议会话的 Control API,可查询/踢会话 | M1 对账器依赖 MediaMTX 的控制语义 | +| 横向分片与故障域 | 未见公开的多实例编排和 128 路分片验收 | 官方提供 origin/read-replica 与负载均衡方案 | 64/128 路不能以单体 MiBeeNvr 为扩展基线 | + +以上“未发现”是基于指定提交的源码与文档审查,不代表上游永久不支持;升级前必须重新核验。 + +### 前期能否只用 MiBeeNvr + +| 阶段/用途 | 是否允许只用 MiBeeNvr | 边界 | +| --- | --- | --- | +| **M0 实验室兼容性与 UI 演示**(3–5 款摄像头) | **可以,且优先直接运行而不是复制源码** | 只接自购测试设备;允许随 M0 结束丢弃,不形成生产数据 | +| 一次性内部 Alpha(最多 16 路) | 可以,但必须标记为 **disposable prototype** | 单站点、单用户、无生产 SLA;不承诺原地升级到 128 路 | +| **M1 首个可持续演进的生产骨架** | **不可以** | MediaMTX 必须进入;Sense 建立自有设备 ID、台账、API 客户端与对账器,Brain 以后只依赖稳定媒体地址 | +| M2/M3 默认 16 路真实试点 | 不可以 | MiBeeNvr 只保留为实验室测试台/源码参考;真实租户、配额、审计与事件契约不得落入其单机模型 | +| M4/M5 64/128 路 | 不可以 | 必须使用媒体/推理分片和故障隔离 | + +> **决策规则**:如果“初版”指可丢弃的验证版,可以只用 MiBeeNvr;如果“初版”指以后原地升级的第一个正式版本,MediaMTX 必须从 M1 进入。若确需修改 MiBeeNvr,应保留为独立 fork/目录及上游 Git 历史,不把整仓源码粘贴进 Sense。 + +### MiBeeNvr 验证版安全红线 + +当前分析提交在 `internal/api/handler.go` 的 `registerAnonymousRoutes` 中将录像 `download`、`merged` 回放端点注册为匿名且不受该文件中的公共限流组保护。这对隔离实验室演示可以接受,**对居家老人、学校和工厂真实视频不可接受**。 + +验证版必须满足: + +1. 仅监听隔离测试网或 localhost;若需远程访问,必须置于带认证的反向代理/VPN 后。 +2. 不接入真实住户、学生、员工视频;不得用匿名录像 URL 作为生产分享机制。 +3. 关闭不需要的 WebDAV、FTP、浏览器 AI、自动更新和公网管理端口,减少攻击面。 +4. 若进入任何真实试点,先把录像下载/回放纳入 Bell 鉴权与审计,或直接切换到 M1 的 MediaMTX + Sense 生产骨架。 + +### MiBeeNvr 借鉴白名单 + +| 可借鉴内容 | 落入本项目 | 处理方式 | +| --- | --- | --- | +| `internal/onvif/` 的发现、WS-Security、Profile 选择、设备时间处理 | `Sense/internal/onvif/` | 按本项目接口重写;只保留必要协议能力和测试向量 | +| `internal/camera/rediscovery*` 的序列号识别与 IP 自愈 | `Sense/internal/device/`、`probe/` | 身份仍以序列号为准;适配 Postgres 设备台账和对账状态机 | +| `internal/health/`、退避与冻结检测 | `Sense/internal/probe/` | 统一成指标和状态事件,不直接驱动录像/告警业务 | +| 摄像头表单、直播卡片、录像列表的交互模式 | `Bell/web/` | 只借鉴交互;按多租户 RBAC、分页和 128 路虚拟列表重做 | +| ONVIF、录像异常与存储故障测试用例的组织方式 | Sense 测试目录 | 复用测试思想和公开协议样例;不得携带真实设备密码/视频 | + +若逐行复制 MIT 源码,必须保留原版权和许可证声明,并在第三方清单记录文件、提交号和修改范围。优先采用“读懂后按 Sense 接口重写”,避免把 MiBeeNvr 的 `config/model/recorder/storage/event` 耦合链一起带入。 + +### 明确禁止复制的部分 + +- MiBeeNvr 的 `StreamHub`、HLS/WebRTC/RTMP/SRT 服务和录像主链:与 MediaMTX 重复。 +- MiBeeNvr 的 SQLite 真相源、单机 `CameraManager` 生命周期:与 Postgres、多租户、16→128 路分片冲突。 +- MiBeeNvr 的浏览器 AI 和 Frigate 的检测进程:AI 统一归 Brain/Savant。 +- 任一 NVR 的 BasicAuth/本地用户模型:身份、租户、RBAC 与审计统一归 Bell。 +- Frigate/Home Assistant/MQTT 的业务对象模型:只吸收事件录像与时间轴语义,不进入平台核心契约。 + +### 进入生产的验收门槛 + +1. 依赖固定版本与校验值,生成 SBOM 和第三方许可证清单;上游升级必须先过回归测试。 +2. M0 完成 3–5 款摄像头的 ONVIF/RTSP 兼容测试;M3 建立 16 路基线,M4/M5 分别完成 64/128 路分片验收。 +3. 分别测接入/录像带宽、同时解码、AI 推理和证据存储,不以“NVR 能录多少路”替代 AI 容量结论。 +4. MediaMTX、go2rtc 的管理 API/UI 默认只监听内网或 localhost,经 Bell/Sense 授权代理后才可访问。 + +--- + +# 2. 通用能力底座 + +## 2.1 接入层:控制面/数据面分离 + +这是解决 NAT 的关键,也是三个场景可以用同一套接入的原因。 + +```mermaid +flowchart LR + subgraph SITE["站点(家庭/学校/小区)"] + CAM["IP 摄像头"] + SENSOR["传感器
雷达/门磁/按钮"] + EDGE["边缘节点
推流 + 本地缓存 + WG"] + CAM --> EDGE + SENSOR --> EDGE + end + subgraph CENTER["中心平台"] + MTX["mediamtx 集群
(分片)"] + CTRL["控制面 Go 服务
台账 · 对账 · 鉴权回调"] + PIPE["推理流水线"] + BIZ["业务编排"] + DB[("PostgreSQL")] + OBJ[("对象存储")] + end + EDGE -->|"数据面:主动推流
RTSP announce / SRT"| MTX + CTRL -.->|"控制面:WireGuard 隧道
ONVIF 配置与探活"| EDGE + MTX -->|鉴权回调| CTRL + MTX --> PIPE --> BIZ + CTRL --> DB + BIZ --> DB + BIZ --> OBJ + BIZ --> APP["App / 短信 / 语音 / Webhook / 大屏"] +``` + +| 平面 | 方向 | 通道 | 特点 | +| --- | --- | --- | --- | +| 控制面 | 中心 → 设备 | WireGuard 隧道 | 流量极小,仅配置与探活时产生 | +| 数据面 | 边缘 → 中心 | 主动推流 | 不走隧道,避免隧道成为带宽瓶颈 | + +**控制面失联不影响视频推流**——这条解耦让隧道故障从 P0 降为 P2。 + +### IP 规划 + +每站点分配不冲突的内网段,设备地址确定化: + +``` +10.<站点高位>.<站点低位>.0/24 +摄像头固定从 .10 起分配,传感器从 .100 起 +``` + +设备地址可预测,不依赖自动发现,便于对账。**校园/社区场景若已有既定网段,在 `sites.subnet` 里登记实际值即可,逻辑不变。** + +### mediamtx 配置基线 + +```yaml +api: yes +apiAddress: :9997 +metrics: yes +metricsAddress: :9998 + +authMethod: http +authHTTPAddress: http://control-plane:8080/mtx-auth + +pathDefaults: + sourceOnDemand: yes # 有 reader 才拉流 + sourceOnDemandStartTimeout: 10s + 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: + "~^t(\\d+)-s(\\d+)-d(\\d+)$": # tenant-site-device,一条正则兜底 + source: publisher +``` + +> 用一条正则 path 兜底,**不要写 N 条静态配置**。 +> +> ⚠️ v1.19.x 的钩子命名为 `runOnAvailable` / `runOnUnavailable` / `runOnOnline` / `runOnOffline`,与旧版 `runOnReady` / `runOnNotReady` 不同。部署前核对所用版本的配置参考。 + +### 必须遵守的接入约定(血泪清单) + +| 约定 | 原因 | +| --- | --- | +| 设备身份用序列号,不用 IP | DHCP / Wi-Fi 漫游会换 IP | +| 建 path 前先查是否存在 | 盲目 add 会踢掉正在推流的 publisher | +| 改配置前比对哈希,没变绝不调 API | 任何 path 配置变更都会触发 reload,断开正在推流的 source,**即使改动与它无关** | +| 重试用指数退避 | 无事务,只能靠幂等重试 | +| 并发限流(每系统信号量 8) | 避免打爆 API;每系统独立预算,不共享 | +| 设备状态含 `pending_activation` | 批量开通必然有凭据待补的中间态 | +| API 改的配置不落盘 | mediamtx 重启后 API 添加的 path 全部消失,服务启动时必须从 DB replay | +| 探活用独立 ONVIF 调用 | 「设备离线」与「无人订阅」在 mediamtx 侧表现相同 | +| 开通时 `SetSystemDateAndTime`,定期校时 | 时钟漂移会让事件时间戳全乱,审计失效 | + +### 停用的四种粒度(副作用从轻到重) + +| 需求 | 做法 | 效果 | +| --- | --- | --- | +| 暂停分析,流保留 | **卸载推理 source** | 配合 sourceOnDemand 自动停止拉流,随时恢复 | +| 立即断开,允许重连 | `POST /v3/rtspsessions/kick/{id}` | 断一次,客户端会重连 | +| 永久拒绝 | 鉴权回调返回 401 + kick | 推不上来,无需改 mediamtx 配置 | +| 彻底移除 | `DELETE /v3/config/paths/delete/{name}` | path 消失,重启后靠自有库恢复 | + +标准组合:**先 kick 断开、再让鉴权拒绝** = 立刻生效且不会重连。 + +> ⚠️ **关键耦合**:`sourceOnDemand: yes` 的语义是「有 reader 才拉流」,而**推理 source 挂上去就是一个 reader**。因此「暂停分析」必须通过**卸载 source** 实现,不能只在推理插件里 `return`——后者会让 mediamtx 持续拉流、带宽白烧,而监控上看起来一切正常。 + +## 2.2 对账器:多系统一致性 + +数据库是唯一真相源,mediamtx / 推理流水线 / 存储都是被驱动的从属状态。 + +**水平触发(level-triggered)而非边沿触发**:对账器不处理"事件",只不断比对期望状态与实际状态并向期望收敛。这是处理部分失败的唯一可行方案。 + +```go +func (r *Reconciler) Sync(ctx context.Context) error { + desired := r.db.EnabledBindings() // 真相源 + if len(desired) == 0 && r.lastDesiredCount > 0 { + return ErrSuspiciousEmptyDesired // 安全闸:期望集合意外为空 + } + + mtxActual, err1 := r.mtx.ListPaths(ctx) // 分片内所有实例 + infActual, err2 := r.inference.ListSources(ctx) + if err1 != nil || err2 != nil { + return errors.Join(err1, err2) // 拿不到实际状态就不动作 + } + + plan := buildPlan(desired, mtxActual, infActual) + // plan 内部已按顺序约束排序: + // setup: mtx 先 → inference 后 (流必须先存在) + // teardown: inference 先 → mtx 后 (先停消费再断流) + // ⚠️ 建立与拆除是逆序的,很容易被后来人改错 + + for _, step := range plan { + if !step.DueForRetry(now) { continue } // 退避窗口未到 + if step.DesiredHash == step.AppliedHash { continue } // 没变,绝不调 API + + sem[step.System].Acquire(ctx, 1) // 每系统独立限流 + go func() { + defer sem[step.System].Release(1) + if err := step.Apply(ctx); err != nil { + r.db.MarkFailed(step, err) // 退避,不回滚 + return + } + r.db.MarkApplied(step) // applied_hash = desired_hash + }() + } + return nil +} +``` + +### 部分失败:不回滚,只收敛 + +典型场景:mediamtx path 建好了,推理 source 挂载失败。 + +- ❌ **错误做法**:回滚删掉 path。回滚本身也会失败,那时你需要回滚的回滚,无限递归。 +- ✅ **正确做法**:保留已成功的部分,把失败那行 `sync_state` 标为未收敛 + 退避重试。 + +代价是会有一个无人消费的 path 短暂存在——但因为 `sourceOnDemand`,没有 reader 就不拉流,**成本接近零**。这是 sourceOnDemand 给多系统对账带来的额外好处:**中间态是安全的**。 + +### 重试预算与安全闸 + +| 参数 | 值 | +| --- | --- | +| 初始退避 | 5s | +| 退避倍率 | 2×,上限 5min | +| 告警阈值 | `attempt > 5` 且持续 > 15min → 告警 | +| 放弃阈值 | **不放弃**。持续重试,靠告警引入人工 | +| 循环周期 | 正常 30s;有未收敛项降到 5s;每 10min 全量对账(含孤儿扫描) | +| 孤儿清理安全闸 | 单轮删除量 > 总量 10% 时**停止并告警**,不执行 | + +> 不设放弃阈值是故意的:对账器放弃了,设备就静默地不工作了,这在监护场景是不可接受的失败模式。 +> +> 安全闸防的是数据库连接异常或查询 bug 导致的"期望集合意外为空"——那会把全部路数拆掉。 + +### 孤儿资源三类 + +| 类型 | 成因 | 处理 | +| --- | --- | --- | +| mediamtx 有 path,DB 无 | 人工手改、旧版本残留 | 删除(先记日志,不静默) | +| 推理有 source,DB 无 | 同上 | 卸载 | +| DB 有 binding,两边都无且长期未收敛 | 开通失败后无人处理 | 告警,**不自动删 DB** | + +## 2.3 流水线层:故障隔离与触发式推理 + +### 为什么用框架而非完全自研 + +开源社区没有成品的「批量摄像头行为检测」(算法是差异化,不会有人开源),但有成熟的**多路视频分析流水线框架**。**算法是自己的,工程化用现成的。** + +选定 **Savant**,三个决定性理由: + +1. **故障隔离** —— 原生 DeepStream 中数据流与 pipeline 紧耦合,任一源崩溃则整个 pipeline 崩溃,重启加载模型要数秒且期间丢数据。Savant 的 adapter 通过 ZeroMQ 与框架通信,**adapter 挂掉不影响 pipeline**。家庭网络掉线是常态,这是规模化的生死线。 +2. **可配置降级** —— 实时模式(算力不足时主动丢帧)或高吞吐模式(保证处理全部数据)。**本系统选实时模式,丢帧优于积压崩溃。** +3. **Replay(按需与非线性处理)** —— REST 控制的录制/回放/转推服务,直接对应触发式二级确认与 pre-roll 回捞。 + +同一套代码可跑在 Jetson 和数据中心 GPU 上,与边缘/中心混合部署契合。 + +**代价**:必须 NVIDIA 硬件,与 DeepStream 版本强绑定,学习曲线陡,无企业支持。 + +### Pipeline 架构 + +``` +边缘推流 → mediamtx(汇聚,按需拉流) + ↓ RTSP + RTSP source adapter(多路复用 + 故障隔离) + ↓ ZeroMQ / Protobuf + Buffer Adapter(磁盘持久化缓冲,抗突发) ← 必须开启 + ↓ + 推理 module + ├─ 检测 + 跟踪(YOLO + ByteTrack) + ├─ 姿态(YOLO-pose,COCO 17 点) + ├─ 属性 / ReID + ├─ 人脸旁路(授权租户,异步不阻塞) + └─ Python 插件:时序窗口 + 行为判定(**自研部分**) + ↓ ZeroMQ / Protobuf + Go 业务编排(规则引擎 → 事件 → 预警) +``` + +**需要自己写的只有中间那个 Python 插件,前后两段是现成的。** + +### 关键配置决策 + +| 决策 | 说明 | +| --- | --- | +| **必须开启 Buffer Adapter** | adapter 与 module 之间的持久化事务性磁盘缓冲,抗流量突发,条目数与大小导出到 Prometheus。本场景天然有突发(夜里安静、早上集中活动、上下学时段)。**一开始就加上,别等出问题再补** | +| **AO-RTSP Sink 仅用于调试** | ⚠️ GeForce 系列显卡**同时硬件编码流数上限为 3**。给多路挂可视化回推会直接卡在 3 路,需 Quadro/Tesla/L4 类专业卡。**不要做成常态功能** | +| **开发期开 Development Server** | 改代码动态重载不重启 pipeline,可从 IDE 远程开发跑在 Jetson/服务器上的代码。原生 DeepStream 改一行要重启等几十秒 | +| **实时模式** | 算力不足时主动丢帧,不积压 | + +### 与 Go 控制面的对接契约 + +| 需求 | 通道 | 语言无关 | +| --- | --- | --- | +| 接收检测结果 | 订阅 sink 的 ZeroMQ 端点,解 protobuf | ✅ | +| 动态增删流 | 动态挂载/卸载 source 与 sink,无需重载 pipeline | ✅ | +| 回放/回捞(pre-roll) | Replay 服务的 REST 接口 | ✅ | +| 程序化灌数据/调试 | Client SDK(**仅 Python**) | ❌ | + +> **设计约束:优先走 ZeroMQ 和 REST 两条语言无关通道,Go 服务不得依赖 Python 中间层。** Client SDK 仅用于算法调试和测试。 +> +> 因为两边都是 API 驱动,**对账器可以同时驱动 mediamtx 和推理流水线**:开通 = 建 path + 挂 source;停用 = kick + 卸 source。逻辑写在同一个 reconciler 里。 + +### 触发式推理(成本的数量级优化) + +常态全量推理不可持续。三种触发源,按场景选: + +```mermaid +flowchart LR + T1["运动侦测
(软件,零成本)"] --> GATE{"触发门"} + T2["ONVIF 事件订阅
(摄像头侧)"] --> GATE + T3["传感器
雷达/门磁/按钮"] --> GATE + GATE -->|唤醒| INF["推理流水线
+ Replay 回捞前 N 秒"] + GATE -->|静默| IDLE["不解码 不推理"] +``` + +| 触发源 | 成本 | 可靠性 | 推荐场景 | +| --- | --- | --- | --- | +| 运动侦测(参考 Frigate 思路) | 零 | 中(光照/树叶会误触) | 通用兜底、无雷达时的过渡方案 | +| ONVIF 事件订阅 | 零 | 中 | 支持的型号,节省中心解码 | +| 毫米波雷达 | 硬件成本 | 高 | 家庭卧室/卫生间(**同时解决隐私**) | +| 常态全量 | 高 | 最高 | 仅高价值点位(校园危险区域、社区出入口) | + +> **重要澄清**:Savant 这类框架主要解决多路调度、复用与故障隔离,不会自动降低单帧推理成本。触发式推理可以显著降低平均占空比,但实际容量必须按模型、分辨率、FPS、batch、解码方式和硬件实测。 + +### 架构预留:触发入口独立 + +```go +// 所有触发源汇入同一个入口,接新传感器时不改动流控制逻辑 +type TriggerSource interface { + Name() string + Subscribe(ctx context.Context) (<-chan TriggerEvent, error) +} + +type TriggerEvent struct { + DeviceID int64 + SiteID int64 + Kind string // motion | onvif_event | radar | door | button + At time.Time + PreRoll time.Duration // 需要回捞多久 + Payload json.RawMessage +} +``` + +## 2.4 能力层:检测什么 + +### 能力清单与场景映射 + +| 能力 | 模型/方法 | 输出 | S1 | S2 | S3 | +| --- | --- | --- | :-: | :-: | :-: | +| 人体检测 + 跟踪 | **YOLO** + ByteTrack | bbox, track_id | ✅ | ✅ | ✅ | +| 姿态关键点 | **YOLO-pose** (COCO 17) | 17 点坐标 + 置信度 | ✅ | ✅ | ✅ | +| 跌倒/倒地 | 姿态 + 时序窗口 | 7 分类动作 | ✅ | ✅ | ✅ | +| 区域入侵 / 越线 | 几何判定(Zone) | 进入/离开/越线方向 | ○ | ✅ | ✅ | +| 停留 / 徘徊 | track 时长统计 | 停留秒数 | ✅ | ✅ | ✅ | +| 静止 / 无活动 | 位移统计 | 无位移时长 | ✅ | ○ | ✅ | +| 人群聚集 / 密度 | 检测框计数 + 空间聚类 | 人数、密度 | — | ✅ | ○ | +| 剧烈动作 / 打架 | 姿态时序 + 光流 | 分类置信度 | — | ✅ | ○ | +| 攀爬 / 翻越 | 姿态 + 高度变化 + Zone | 分类 | — | ✅ | ✅ | +| 非人目标 | 检测模型多类 | e_bike / obstacle | — | ○ | ✅ | +| **ReID 跨镜(匿名)** | ReID 模型 | `anon_id` | ○ | ✅ | ✅ | +| 身份比对 | 人脸底库(§2.8.3) | `identity{ref_id, group, score}` | 🔐 | 🔐 | 🔐 | + +✅ 核心 ○ 可选 — 不需要 🔐 需租户授权后启用 + +> **能力选择原则**:「同一性」问题走 **ReID**,「是谁」问题才走**人脸**。徘徊、尾随、跟随、跨镜追踪一律用 ReID 实现;人脸只用于白名单放行、关注名单、访客核验这类必须知道身份的场景。 + +### 算法分层(模型只输出观测,不做主观判定) + +| 层 | 内容 | 位置 | +| --- | --- | --- | +| 姿态/检测提取 | YOLO / YOLO-pose | 推理单元(TensorRT) | +| 单帧启发式 | 包围框宽高比、重心高度、躯干角度 | Python 插件 | +| 时序判定 | 30 帧滑窗,静态位置 + 动态速度特征 | Python 插件 | +| **规则判定** | **主体过滤 × 时空 × 条件组合** | **Go 规则引擎(不在插件里)** | +| 事件后处理 | 去重、防抖、多源融合 | Go 业务编排 | + +> **这条边界是《02》C4 的落地**:插件输出「有个人、姿态是躺倒、在多边形 A 内停留 12 秒」这类**客观观测**;规则引擎决定「这是否需要报警」。阈值、时段、联系人**一律不进插件**。 + +### 参考实现与经验 + +| 项目 | 借鉴点 | 注意 | +| --- | --- | --- | +| `GajuuzZ/Human-Falling-Detect-Tracks` | ST-GCN 每 30 帧预测动作,**7 分类**(站立/行走/坐下/躺下/起身/坐下动作/跌倒) | **七分类而非二分类是降误报的关键** | +| `kelsonbatista/fall-detection-ai-project` | 逐帧抽 128 维特征(静态位置 + 动态速度),30 帧窗口喂 LSTM | ⚠️ GPL-3.0,只读思路不抄代码 | +| `Y-B-Class-Projects/Human-Fall-Detection` | **负面经验**:约 500 张网络图片训 LSTM 完全学不会,最终退回 if-else 规则 | **小数据量下时序模型学不动,先解决数据再上模型** | +| Frigate | 运动侦测过滤后才跑推理 | 无雷达时的触发式过渡方案 | + +### 模型许可(已决策:继续用 YOLO) + +**主路径:使用 Ultralytics YOLO + 购买 Enterprise License。** + +| # | 必办事项 | 时限 | +| --- | --- | --- | +| 1 | 采购 Ultralytics Enterprise License | **M3 前完成** | +| 2 | 书面确认授权覆盖:私有化交付的每个客户实例、边缘盒子内的模型文件、基于官方权重二次训练的产出 | 采购时 | +| 3 | 人脸/ReID 模型的**权重许可**单独核对(常与代码许可不一致,部分权重仅限非商业用途) | 启用前 | + +**逃生通道(授权未获批时启用)**: + +| 环节 | 替代 | 迁移代价 | +| --- | --- | --- | +| 检测 | **YOLOX**(Megvii,Apache-2.0,仍属 YOLO 家族) | 低,接口相近 | +| 姿态 | **RTMPose / RTMO**(MMPose,Apache-2.0) | **极低,关键点定义同为 COCO 17 点,判定算法不用改** | + +> 为让逃生通道随时可用,**推理封装必须与具体模型解耦**:定义统一的 `Detector` / `PoseEstimator` 接口,模型只作为可替换实现。这条约束成本很低,但决定了授权谈判失败时是"换个实现"还是"重写一遍"。 +> +> 附带好处:RTMPose 通过 MMDeploy 支持 ONNXRuntime、TensorRT、ncnn、OpenVINO、**RKNN** 多后端。家庭场景的 RK3588 盒子若跑不动 YOLO,可在边缘侧单独换成 RTMPose 而不影响中心侧。 + +## 2.5 编排层:规则引擎 + +### 规则 DSL + +```yaml +# 规则的完整形态。场景包提供模板,租户/站点/设备逐级覆盖。 +rule: + id: R-SCH-003 + code: danger_zone_intrusion + name: 危险区域非授权进入 + scene_pack: campus@1.2 + enabled: true + dry_run: false # 试运行:命中但不通知,仅记录 + + scope: # 时空:哪些设备、哪些时段 + devices: [tag:lab_area] # 支持标签选择,不写死设备 ID + zones: [danger_zone_1] # 画面区域,仅设备级可定义 + schedule: + calendar: school_term # 引用日历(学期/课表/节假日) + windows: ["!class_time"] # 非上课时段(! 为取反) + + subject: # 主体:检测什么人/物 + class: person + filters: + - attr: age_group + in: [child, teen] + - identity: # 身份类,identity 为 null 时按 unknown 处理 + not_in_group: authorized_staff + on_unavailable: treat_as_unknown # ⚠️ 身份服务不可用时不得漏报 + + condition: # 条件:客观观测量的组合 + all: + - type: zone_enter + zone: danger_zone_1 + - type: dwell + gte: 3s + - type: not_accompanied_by + role: teacher + radius_px: 200 + + suppression: # 抑制 + cooldown: 300s + aggregate_by: [site_id, zone] + respect_silence_window: true + + evidence: # 证据规格 + pre_roll: 10s + post_roll: 15s + snapshots: 3 + include_observations: true # 结构化观测序列,供后续训练 + + action: + severity: high + event_kind: danger_zone_intrusion + notify: + contact_chain: campus_security # 引用站点的升级链 + start_level: 0 +``` + +### 三级覆盖语义 + +``` +场景包默认 ──继承──> 租户 ──覆盖──> 站点 ──覆盖──> 设备 +``` + +| 字段类型 | 覆盖语义 | 理由 | +| --- | --- | --- | +| 标量(阈值、时长、severity) | 就近覆盖 | 直觉一致 | +| 列表(联系人、通道、devices) | **整体替换,不合并** | 合并会意外扩大通知面 | +| 开关 enabled | 就近覆盖,下级可关闭上级规则 | 单点位豁免是常见需求 | +| Zone 坐标 | **只能设备级定义**,上级不可下发 | 像素坐标与具体机位绑定 | + +> 覆盖语义必须在文档和 UI 上明示(显示"此值继承自站点/租户"),否则运维会被"为什么这条没生效"折磨死。 + +### 规则生命周期 + +| 能力 | 说明 | +| --- | --- | +| 热生效 | 规则变更不重启 pipeline(一次重启影响同机全部路数) | +| 版本化 | `rule_versions` 表,记录变更人与时间,事故复盘用 | +| **试运行(dry_run)** | 命中但不通知,仅落库。新规则上线前评估误报量 | +| 质量指标 | 每条规则的命中量、误报率可查询 | +| 灰度 | 按站点比例放量 | + +> **试运行模式是被低估的需求。** 没有它,每条新规则上线都是拿真实用户做实验。 + +## 2.6 事件实例 + +### 定义 + +「命中规则的画面组成事件实例」的具体产物: + +```json +{ + "id": "evt_01J8X...", + "tenant_id": 1001, + "site_id": 20301, + "device_id": 5012, + "rule": { "id": "R-SCH-003", "version": 7, "code": "danger_zone_intrusion" }, + "kind": "danger_zone_intrusion", + "severity": "high", + "confidence": 0.87, + + "occurred_at": "2026-08-01T14:23:07.412Z", + "detected_at": "2026-08-01T14:23:08.905Z", + + "subject": { + "class": "person", + "track_id": "trk_88213", + "attributes": { "age_group": "teen" }, + "anon_id": "pid_7f3a91", + "identity": null, + "identity_status": "not_enabled" + }, + + "observation": { + "zone": "danger_zone_1", + "dwell_sec": 3.4, + "bbox_seq_uri": "s3://.../evt_01J8X/obs.jsonl", + "keypoint_seq_uri": "s3://.../evt_01J8X/pose.jsonl" + }, + + "evidence": { + "clip_uri": "s3://.../evt_01J8X/clip.mp4", + "clip_range": ["2026-08-01T14:22:57Z", "2026-08-01T14:23:22Z"], + "snapshot_uris": ["s3://.../s0.jpg", "s3://.../s1.jpg", "s3://.../s2.jpg"] + }, + + "dedup_key": "20301:danger_zone_1:danger_zone_intrusion:2026-08-01T14:20", + "aggregated_into": null, + + "outcome": "unknown", + "outcome_reason": null +} +``` + +### 三条硬约束 + +| 约束 | 理由 | +| --- | --- | +| **事件不可变** | 误判只能通过 `outcome` 标记,不能删改。合规与举证要求 | +| **`occurred_at` 与 `detected_at` 都要存** | 前者是事发时刻(决定证据回捞窗口),后者是判定时刻(决定 SLA 计算) | +| **必须含结构化观测序列** | 只有视频无法直接用于训练;`bbox_seq` 与 `keypoint_seq` 是《02》C9 数据闭环的原料 | +| **`anon_id` 与 `identity` 分开存** | 前者是 ReID 匿名标识(会话内有效),后者是人脸身份;不可互相顶替 | +| **`identity_status` 必须显式** | 取 `not_enabled` / `pending` / `matched` / `below_threshold` / `no_candidate` / `timeout`。只写 `null` 无法区分"没开这功能"和"比对失败",后者是需要排查的故障 | + +### 证据回捞(pre-roll) + +事件是"事后才知道"的——判定成立时,事发画面已经过去了。三种回捞方式: + +| 方式 | 实现 | 适用 | +| --- | --- | --- | +| 边缘环形缓冲 | 边缘节点持续录最近 N 秒到本地环 | 家庭(不上云,隐私最优) | +| 中心持续录制 | mediamtx `record: yes` + `recordDeleteAfter` | 校园/社区(允许常态录像) | +| Replay 服务回捞 | 推理框架的 Replay REST 接口 | 通用 | + +> ⚠️ pre-roll 的实际起点受 **GOP 长度**限制——只能从关键帧切。这是《01》§6.1 把"支持修改 GOP 长度"列为加分项的原因。GOP 2s 时,请求 10s pre-roll 实际可能拿到 10–12s。 + +### 去重与聚合 + +| 规则 | 参数 | 场景 | +| --- | --- | --- | +| 同设备冷却期 | 默认 5min | 同一事件多次触发 | +| 同站点聚合 | `aggregate_by` 键相同则合并 | 两个机位拍到同一次跌倒 | +| 已处置抑制 | 已 ack 期间不再新建同类 | 人已到现场 | +| 静默窗口 | 用户主动静默 | **强制限时(≤4h),到期前提醒,无"永久"选项** | + +> **永久静默是这类系统最常见的事故成因。** + +## 2.7 预警投递 + +### 状态机 + +``` +[触发] ─→ [投递中] ─→ [已送达] ─→ [已确认 ack] ─→ [处置中] ─→ [关闭] + │ │ │ │ + │ │ └─超时未 ack─→ [升级] ─→ 下一级投递 │ + │ └─投递失败─→ [升级] │ + └─防抖命中─→ [抑制] [标记误报] ←───┘ +``` + +**硬约束:告警先落库再投递。** 进程崩溃重启后扫描所有非终态告警并接续升级链——与对账器的水平触发是同一思路。 + +### 升级链配置(场景差异的主要载体) + +```yaml +contact_chain: + id: home_elderly_default + levels: + - level: 0 + timeout_sec: 30 + contacts: [{ role: family_primary, channels: [push] }] + - level: 1 + timeout_sec: 60 + contacts: [{ role: family_primary, channels: [sms, voice] }] + - level: 2 + timeout_sec: 90 + contacts: [{ role: backup, channels: [sms, voice] }] # 邻居/物业/护理员 + - level: 3 + timeout_sec: null + contacts: [{ role: call_center, channels: [voice, ticket] }] + time_policy: + - window: "08:00-22:00" + override: { level0_role: family_primary } + - window: "22:00-08:00" + override: { level0_role: cohabitant } # 夜间找同住者 +``` + +### 通道选型与冗余 + +**必须至少两条独立路径,且电话通道要能绕过互联网。** 推送依赖 APNs/FCM,短信依赖运营商,任何单一通道都可能整体故障。 + +| 层 | 选型 | 说明 | +| --- | --- | --- | +| 状态机、升级链、ack、审计 | **自研 Go** | 业务定制度高,通用工具都要大改 | +| 多通道投递 | Novu / Apprise / ntfy 或直连云厂商 | 只解决"怎么发" | +| 短信 + 语音 | 云厂商,**双供应商** | `provider` 字段就是为切换准备的 | + +### 三个不同的事实 + +| 事实 | 含义 | 字段 | +| --- | --- | --- | +| 发出去了 | 调用了上游 API | `sent_at` | +| 送达了 | 上游回执确认到达终端 | `receipt_at` | +| 被看到了 | 用户主动 ack | `alerts.acked_at` | + +> **没有回执就当失败并升级**,不做乐观假设。推送可能被系统杀掉、短信可能延迟、电话可能占线。 + +### 业务预警 vs 运维告警 + +| | 运维告警 | 业务预警 | +| --- | --- | --- | +| 方案 | Prometheus + Alertmanager | 自研状态机 + 复用投递通道 | +| 收件人 | 运维团队 | 家属 / 值班员 / 呼叫中心 | +| 延迟容忍 | 分钟级 | 秒级 | + +> **两套系统不得共用投递通道和值班配置**,避免运维噪声淡化业务预警。 + +## 2.8 同一性与身份:ReID 与人脸识别 + +### 2.8.1 两条路径的分工 + +```mermaid +flowchart LR + TRK["跟踪目标 Track"] --> Q{"需要回答什么"} + Q -->|"是不是同一个人"| REID["ReID 匿名同一性
默认开启"] + Q -->|"他是谁"| FACE["人脸比对
租户授权后开启"] + REID --> AID["anon_id
会话内有效"] + FACE --> IDT["identity{ref_id, group, score}"] + AID --> RULE["规则引擎"] + IDT --> RULE +``` + +| 能力 | 产出 | 生命周期 | 合规负担 | 典型用途 | +| --- | --- | --- | --- | --- | +| **ReID** | `anon_id` | 会话/时间窗内有效,**不跨天持久化为人员画像** | 中 | 徘徊、尾随、跟随、跨镜追踪、同一人多次出现计数 | +| **人脸** | `identity` | 持久(底库) | **高** | 白名单放行、关注名单、访客核验 | + +> **默认走 ReID。** 一条规则若想用 `identity`,需在评审中回答"为什么 anon_id 不够"。 + +### 2.8.2 ReID 实现要点 + +| 要点 | 做法 | +| --- | --- | +| 特征时效 | `anon_id` 绑定滑动时间窗(默认 30min),超窗即失效重编号,避免变相形成长期追踪 | +| 跨镜匹配范围 | 限定在同一站点内,不跨租户、不跨站点 | +| 存储 | 特征向量仅驻内存/短期缓存,**不入长期库** | +| 事件中的表现 | 只写 `anon_id` 字符串,不写特征向量 | + +> 第 1 条和第 3 条是有意的自我约束:ReID 之所以合规负担低于人脸,正是因为它不建立可长期关联到自然人的档案。若把 ReID 特征长期入库并跨天关联,它在实质上就变成了无同意的身份追踪——合规优势荡然无存。 + +### 2.8.3 人脸识别子系统 + +#### 架构:异步旁路,不在主链路上 + +```mermaid +flowchart TD + subgraph MAIN["主链路(实时,不可阻塞)"] + D["检测+跟踪"] --> P["姿态/行为"] --> R["规则引擎"] --> E["事件生成"] + end + subgraph SIDE["旁路(可延迟、可失败)"] + FD["人脸检测"] --> QC["质量筛选
角度·清晰度·遮挡·尺寸"] + QC --> FE["特征提取"] --> M["底库比对
向量检索"] + end + D -.抽取人脸 ROI.-> FD + M -.时间窗内回填.-> E + M --> LOG[("face_match_logs
只追加审计")] + GATE{"租户授权开关"} -.控制.-> SIDE +``` + +**为什么必须旁路**:把比对放进主链路,一次底库慢查询会拖垮整条 pipeline 的实时性;而跌倒、打架这类人身安全事件根本不需要知道是谁。 + +| 参数 | 值 | +| --- | --- | +| 回填时间窗 | 默认 2s,超窗事件以 `identity: null` 定稿 | +| 质量筛选 | 人脸像素 ≥ 64×64、偏航角 ≤ 30°、遮挡比 < 30%、清晰度阈值 | +| 比对失败/超时 | 不阻塞事件生成,记 `decision = no_candidate / timeout` | + +#### 授权是租户级主开关 + +```yaml +tenant_features: + tenant_id: 1001 + feature: face_recognition + enabled: true + authorized_by: "客户法务签署 · 2026-08-01" + expires_at: "2027-08-01" # 授权有到期日,到期需续签 + doc_uri: "s3://compliance/auth/1001-face.pdf" + scope: + sites: [20301] # 可进一步限定站点 + purposes: [whitelist, visitor] # 未含 watchlist 则关注名单功能不可用 +``` + +| 层级 | 语义 | +| --- | --- | +| 平台级 | 能力是否部署(私有化交付可整体不装) | +| **租户级** | 主开关。未授权租户在 **API 与 UI 层均不可见**,不是"看得到点不动" | +| 站点级 | 已授权租户内的生效范围(如只在校门口,不在教室) | +| 规则级 | 单条规则是否使用 `identity` 过滤 | + +#### 底库管理 + +| 要求 | 实现 | +| --- | --- | +| 独立 schema,独立加密密钥 | 与业务库分离,泄露面隔离 | +| **默认只存特征向量,不存原图** | 需展示原图的场景(访客核验)单独授权,原图分库存储 | +| **人员必须有有效期** | `valid_until` 非空约束;学生按学年、住户按合同、访客按当日 | +| 到期自动失效 | 定时任务将过期人员移出检索集合,不依赖人工清理 | +| 三类库分开 | `whitelist` / `watchlist` / `visitor`,阈值与处置策略各不相同 | +| 整体导出与彻底删除 | 客户终止服务时的义务,需留删除证明 | +| 每次比对与每次查询均审计 | `face_match_logs` 只追加,含操作人/设备/分数/决策 | + +#### 阈值分级(误识别后果不对称) + +| 库类型 | 假阳性后果 | 阈值 | 附加控制 | +| --- | --- | --- | --- | +| `whitelist` 白名单放行 | 陌生人被当成住户放行 → 安防失效 | 高 | — | +| `watchlist` 关注名单 | **无辜者被当成关注对象 → 对个人的实质伤害** | **最高** | **强制人工复核** | +| `visitor` 访客核验 | 核验错误 | 中 | 人工在场 | + +> **产品红线(L4 编排层硬约束)**:`watchlist` 类规则**不得自动触发对人的处置动作**(自动锁门、自动通报、自动广播),只能生成待人工确认的事件。这条写进代码,不靠流程约束。 + +#### 规则中的降级语义 + +```yaml +subject: + class: person + filters: + - identity: + library: campus_students + not_in_group: authorized_staff + min_score: 0.75 + on_unavailable: treat_as_unknown # 默认;另一取值 skip +``` + +| 取值 | 语义 | 适用 | +| --- | --- | --- | +| **`treat_as_unknown`(默认)** | 比对不可用/未命中时按"未知人员"处理,**倾向报警** | 监护与安防场景 | +| `skip` | 比对不可用时不产生事件,**倾向不报** | 仅用于低风险规则,需显式写明并有审批记录 | + +> 默认必须是 `treat_as_unknown`:人脸角度不佳、服务抖动、底库超时都会产生 null,若默认跳过,系统会在最需要报警的时候安静下来。 + +--- + +# 3. 场景包 + +## 3.1 场景包是什么 + +一个可版本化的配置集合,**不含代码**: + +``` +scene_packs/campus@1.2/ +├── manifest.yaml # 名称、版本、依赖的能力与最低平台版本 +├── rules/ # 规则模板(DSL) +│ ├── fight.yaml +│ ├── climb_fence.yaml +│ ├── danger_zone.yaml +│ └── ... +├── zones/ # 区域模板(相对坐标 + 引导文案,实施时按机位调) +├── chains/ # 升级链模板 +├── calendars/ # 日历/时段模板(学期、课表、作息) +├── copy/ # 通知话术模板(多语言) +├── dashboards/ # Grafana 看板 JSON + 报表口径 +└── onboarding/ # 实施checklist:机位建议、安装高度、俯角、光照要求 +``` + +## 3.2 居家养老包(S1) + +### 规则集 + +| 规则 | 条件 | 严重度 | 备注 | +| --- | --- | --- | --- | +| 跌倒 | 姿态 7 分类命中 fall,持续 ≥ 2s | critical | 核心 | +| 长时间静止 | 无位移 ≥ 30min(白天)/ 卫生间 ≥ 20min | high | 阈值按户可调 | +| 夜间离床未归 | 22:00–06:00 离开卧室 ≥ 20min 未返回 | high | 需雷达或门磁 | +| 陌生人入室 | 非住户人员出现 | medium | 优先 ReID;家属若要"认出保姆/子女"可授权开人脸白名单库 | +| 活动量异常 | 日活动量较基线下降 > 50% | low | 跨天统计,非实时 | + +### 场景特有约束 + +| 约束 | 实现 | +| --- | --- | +| 卧室、卫生间**禁装摄像头** | `devices.privacy_flag = true` 时系统拒绝创建 `device_type = camera`(代码级硬约束) | +| 平时不解码不上传 | 触发式推理 + `record: no` + 边缘环形缓冲 | +| 家属只能看自己家的告警关联片段 | RBAC:`family` 角色的证据访问范围 = `site_id` ∩ `alert_events` | +| 3 分钟内必有真人接手 | 4 级升级链(30s/60s/90s → 呼叫中心) | + +### 时间线 + +| 时刻 | 发生什么 | +| --- | --- | +| T+0s | 事件发生,一级触发源捕获(雷达/运动) | +| T+3s | 唤醒推理,回捞前 30s 画面 | +| T+10s | 判定成立,事件落库,首次投递(App 推送) | +| T+40s | 未 ack → 短信 + 语音电话 | +| T+100s | 未 ack → 备用联系人(邻居/物业/护理员) | +| T+190s | 未 ack → 呼叫中心人工介入 | + +### 部署形态 + +```mermaid +flowchart LR + subgraph HOME["用户家中"] + CAM["客厅 IP 摄像头"] + RADAR["毫米波雷达
卧室 / 卫生间"] + BOX["边缘盒子 RK3588
录像环缓冲 + 推流 + WG"] + CAM --> BOX + RADAR --> BOX + end + subgraph CLOUD["云端平台"] + MTX["mediamtx"] --> INF["推理"] + INF --> BIZ["业务编排"] + BIZ --> DB[("事件与审计库")] + end + BOX -->|"仅上传事件片段"| MTX + BIZ --> FAM["家属 App / 短信 / 语音"] + BIZ --> CC["呼叫中心"] +``` + +**一户一盒子,互不影响。** 断网时本地缓存,恢复后补传;断网本身触发运维告警。 + +## 3.3 校园包(S2) + +### 规则集 + +| 规则 | 条件 | 严重度 | 时段 | +| --- | --- | --- | --- | +| 打架/推搡 | 剧烈动作分类 + 人数 ≥ 2 + 持续 ≥ 3s | critical | 全天 | +| 翻越围墙 | 攀爬姿态 + 周界 Zone + 高度变化 | critical | 全天 | +| 危险区域闯入 | Zone 进入 + 停留 ≥ 3s + 非授权角色 | high | 非开放时段 | +| 尾随进校 | 门禁事件 1 次 + 检测到人数 ≥ 2 | medium | 上下学时段 | +| 上课时段徘徊 | 楼道 Zone 停留 ≥ 5min | low | 课表内 | +| 宿舍晚归 | 22:30 后进入宿舍楼 | low | 夜间 | +| 学生跌倒/晕倒 | 复用 S1 跌倒能力 | high | 全天 | +| 人群聚集 | 单 Zone 人数 ≥ N 且持续 ≥ 30s | medium | 可配 | + +### 场景特有 + +| 特点 | 方案 | +| --- | --- | +| **未成年人合规** | 人脸能力即使租户已授权,**校园场景仍需专项合规意见方可开启**(独立阻塞项);证据留存期限从严;查看权限按角色最小化;采集告示牌 | +| 危险区域的"授权人员"判定 | 优先用**角色标签 + 工牌/工服属性**;确需人脸时用 `whitelist` 库(教职工),学生不入库 | +| 尾随进校 | **ReID + 门禁事件融合**,不依赖人脸 | +| 课表/校历驱动 | `calendars/school_term.yaml` + 课表导入,规则用 `!class_time` 这类语义引用 | +| 值班室 7×24 有人 | 升级链只需 2 级(值班室 → 校领导),超时 2min/10min | +| 批量事件处理 | **需要看板与工单**(S1 不需要):列表、批量 ack、按区域筛选、当班交接 | +| 大屏联动 | Webhook 推到值班室大屏 + 声光 | +| 带宽充足 | 可中心部署,可用常态推理覆盖高价值点位 | + +### 部署形态 + +校内边缘节点(Jetson Orin NX 或 x86+GPU)承担推理,事件与元数据上中心平台;视频不出校(若客户要求)。 + +## 3.4 社区/物业包(S3) + +### 规则集 + +| 规则 | 条件 | 严重度 | 备注 | +| --- | --- | --- | --- | +| 周界入侵 | 越线(方向性)或 Zone 进入 | high | 夜间加权 | +| 电动车进电梯 | 检测 class = e_bike + 电梯 Zone | high | **非人目标** | +| 消防通道占用 | 静态变化检测 + 持续 ≥ 10min | medium | 需持续态表达 | +| 陌生人徘徊 | ReID 会话在单元门 Zone 停留 ≥ 5min | medium | **用 ReID 不用人脸** | +| 独居老人异常 | 连续 N 天未在出入口出现 | medium | 跨天统计 | +| 高空抛物 | — | — | ⚠️ 需专用仰拍机位与独立算法,**不在本包,建议独立立项** | + +### 场景特有 + +| 特点 | 方案 | +| --- | --- | +| 持续态事件(消防通道堆物) | 事件带 `state: ongoing`,整改后关闭;**不是每帧生成新事件** | +| 夜间可能无人值班 | 升级链 3 级:值班 → 主管 → 业主群/物业总值班 | +| 非人目标检测 | 检测模型需含 e_bike / obstacle 类,与人体检测共用 backbone | +| 陌生人判定 | **优先 ReID(匿名)**,避免人脸底库的合规负担 | + +## 3.5 场景包对比速查 + +| 维度 | 居家养老 | 校园 | 社区 | +| --- | --- | --- | --- | +| 单站点路数 | 1–3 | 默认 16,单站点 ≤128 | 默认 16,单站点 ≤128 | +| 部署 | 边缘盒子(必须) | 校内边缘/中心 | 混合 | +| 推理模式 | 触发式 | 混合(重点常态) | 触发式为主 | +| 常态上云录像 | ❌ 禁止 | ✅ | ✅ | +| 升级链级数 | 4 | 2 | 3 | +| 首级超时 | 30s | 2min | 1min | +| 接收端 | App(个人) | 值班大屏 + 工单 | App + 值班 | +| 同一性/身份 | ReID 为主;人脸可授权开白名单库 | ReID 为主;**人脸需专项合规意见**(未成年人) | ReID 为主;人脸可用于访客核验 | +| 误报容忍 | 极低 | 中 | 中 | +| 主要成本 | 硬件(一户一盒) | GPU | GPU + 存储 | + +--- + +# 4. 部署形态 + +## 4.1 三种形态 + +| 形态 | 推理位置 | 适用 | 优点 | 代价 | +| --- | --- | --- | --- | --- | +| **边缘为主** | 站点内 | 家庭、隐私敏感、带宽受限 | 视频不出户,带宽省 | 硬件成本×站点数,运维难 | +| **中心为主** | 中心机房 | 校园、社区(有专网) | 算力集中利用率高,好运维 | 带宽压力,视频出站点 | +| **混合** | 一级边缘 + 二级中心 | 大多数实际项目 | 成本与隐私平衡 | 架构复杂度 | + +**推荐默认:混合。** 边缘做触发与初筛(运动侦测、轻量检测),中心做重推理与二次确认。 + +## 4.2 容量与分片 + +容量规格分为 16 / 32 / 64 / 128 四档。默认交付 16 路,单站点本阶段上限 128 路;16 是默认配额,不是数据库或代码硬限制。 + +| 项 | 默认 16 路 | 128 路上限 | 设计要求 | +| --- | ---: | ---: | --- | +| 子码流 640×360 @ 512 kbps | ≈ 8 Mbps | ≈ 64 Mbps | 推理优先使用子码流 | +| 主码流按 4–6 Mbps | ≈ 64–96 Mbps | ≈ 512–768 Mbps | 录像与证据回捞预算;不默认重复保存客户全量录像 | +| 抽帧 5 fps | 80 帧/秒 | 640 帧/秒 | 解码与推理均需实测 | +| mediamtx | 1 个分片 | 建议 4 个 32 路分片起步 | 若需缩小故障域,可改为 8 个 16 路分片 | +| 推理 | 1 个 16 路逻辑分片 | 最多 8 个 16 路逻辑分片 | 多个逻辑分片可落在同一 Worker,实际比例由压测决定 | + +推荐配置: + +```yaml +capacity: + site_default_video_channels: 16 + site_max_video_channels: 128 + media_shard_max_streams: 32 + inference_profile_max_sources: 16 +``` + +`stream_bindings` 同时记录 `mtx_instance` 与 `inference_shard`。默认按 `site_id + device_id` 稳定分配;扩容时允许迁移,但同一设备任一时刻只能归属一个有效媒体分片和一个推理分片。 + +`sites.max_video_channels` 的唯一真相源在 Bell;Sense 在设备新增/启用写路径通过版本化内部 API 或只读投影执行配额校验,不跨 schema 直接写入。配额依赖暂时不可用时只拒绝新的容量变更,已有视频链路继续运行。 + +> **不要单实例硬扛 128 路**:扩容必须通过增加分片与 Worker 完成。单分片故障只允许影响该分片,不能拖垮整个站点。GPU 型号与数量不在架构文档写死,M1 用 5 路打通,M3 建立 16 路基线,M4/M5 再验证 64/128 路。 + +## 4.3 硬件选型 + +| 平台 | 用途 | 注意 | +| --- | --- | --- | +| RK3588 | 家庭边缘盒 | 6 TOPS NPU,无风扇低噪;模型需 MMDeploy 转 RKNN | +| Jetson Orin Nano/NX/AGX | 校园/社区边缘 | 与 Savant/DeepStream 原生契合,同一套代码 | +| x86 + L4 / A2 / Quadro | 中心推理 | ⚠️ **GeForce 卡同时硬件编码流数上限 3**,做可视化回推必须用专业卡 | + +--- + +# 5. 可观测与运维 + +## 5.1 一个 Grafana 全收 + +mediamtx 与 Savant 都原生导出 Prometheus 指标,**前期不用写一行前端代码**就有运营看板。 + +| 类别 | 关键指标 | +| --- | --- | +| 接入 | 设备在线率、path 数、推流断连次数、探活失败率 | +| **对账** | `reconciler_unconverged_bindings{system}`(持续 > 0 即异常)、`reconciler_orphans_detected{type}`、`reconciler_safety_brake_triggered`(**应当直接呼人**)、`reconciler_loop_duration_seconds`(超时 = 该分片了) | +| 流水线 | 每路人体检出率、推理延迟 P50/P99、丢帧率、buffer adapter 条目数与大小 | +| 业务 | 事件数(按规则/站点/类型)、**误报率(按规则)**、预警投递成功率、ack 时长 P50/P95、升级到各级的比例 | +| SLA | 事件 → 首次投递延迟、投递 → 回执延迟、全链路无响应比例 | + +> **把每路的检出率、事件候选数、推理延迟声明成自定义指标** → Grafana 上就是现成的运营看板。 + +## 5.2 追踪 + +OpenTelemetry → Jaeger,排查单帧处理延迟与端到端事件链路。 + +## 5.3 管理界面 + +生产组合(mediamtx + 自研服务)**没有现成的管理界面**,三个选择: + +1. **前期不做完整 UI** —— REST API + 脚本撑过接入验证期;即使默认只有 16 路,也不应依赖逐路手工配置。正式管理端从第一版起按 128 路设计分页、虚拟列表、筛选与批量操作。 +2. 临时用 go2rtc 的 1984 页面看流状态 —— ⚠️ **默认无密码,局域网内任何人可访问**,必须在配置里关闭或用防火墙隔离。 +3. **正式做时长在自研服务上** —— 因为要展示的是「租户 → 站点 → 设备 → 流状态」的业务视图;第三方面板展示的是「path 列表」,不是一回事。 + +> 社区面板(mediamtx-dashboard、lgcshy/mediamtx-ui、nabaco/mediamtx-ui、bcanfield/mediamtx-connect)均为独立进程通过 API 对接,可按需临时使用。mediamtx 作者明确表示精力有限、优先保证核心稳定。 + +## 5.4 管理系统与 App 的功能边界 + +| 端 | 核心功能 | +| --- | --- | +| **管理系统(Web)** | 租户/站点/设备管理、批量开通、规则配置(含试运行)、Zone 画图、升级链配置、事件看板与筛选、事件详情(视频+观测回放)、处置与工单、误报标记、统计报表、审计查询、RBAC | +| **App(家属/值班员)** | 预警接收与 ack、事件详情与视频回看、一键呼叫、误报反馈、限时静默、设备状态、(家属)多老人切换 | +| **大屏/值班台** | 实时事件流、地图/平面图点位、声光联动、当班交接 | +| **Webhook/开放 API** | 对接客户既有平台、门禁、消防、工单系统 | + +--- + +# 6. 演进路径与备选 + +## 6.1 里程碑 + +| 阶段 | 内容 | 出口标准 | +| --- | --- | --- | +| **M0** | 摄像头兼容性验证 + 需求定稿 | 可直接运行 MiBeeNvr 作为隔离实验室测试台;3–5 款型号跑通 GetProfiles/GetStreamUri/SetSystemDateAndTime,采购白名单归档;M0 代码不得作为生产基线 | +| **M1** | 接入骨架 + mediamtx | **MediaMTX 正式进入且不得由完整 MiBeeNvr 替代**;5 路自动建 path、探活、断线重建 | +| **M2** | 对账器 + 多租户 + 隧道 | 10 站点试点,至少一个站点完成 16 路开通/停用全流程,`unconverged = 0` 稳定 | +| **M3** | 推理 + 规则引擎 + 事件预警 | **默认 16 路**端到端稳定;跑通首个场景三类规则;拿到现场误报基线,数据闭环已收数据 | +| **M4** | 64 路分片 + 灰度 + 管理系统 | 64 路稳定运行;单分片故障不扩散;看板、批量操作与处置流可用 | +| **M5** | 128 路容量验收 + 第二、三场景包 + 触发式推理 + **人脸识别子系统** | **128 路横向扩展不改业务代码**;场景包为纯配置交付;人脸在一个已授权租户跑通(底库有效期、异步回填、降级、审计) | +| **M6** | 异构传感器 + 两级判定 | 误报率显著下降,视频常态采集下降 | + +### 代码归属:三个系统 + +本方案的实现拆为三个可独立开发的系统,**完整职责与目录结构见《08-三系统职责划分》**: + +| 系统 | 中文 | 层 | 语言 | 状态 | 首次交付 | +| --- | --- | --- | --- | --- | --- | +| **Sense** | 感知系统 | L1 接入 | Go | 有(设备台账) | M1 | +| **Brain** | 推理系统 | L2 流水线 + L3 算法 | Python / CUDA | **无状态** | M3 | +| **Bell** | 管理系统 | L4 业务 + L0 基座 | Go + 前端 | 有(事件、告警) | M3 → M4 | + +三者之间只有一条契约——**事件实例 JSON**(§2.6,规范本体见 `contracts/event-v0.1.schema.json`)。Sense 向 Brain 供流与信号,Brain 产出事件,Bell 消费事件。这条边界让推理侧换模型、换框架(Savant → Pipeless、YOLO → YOLOX)时,另两个系统一行不改。 + +> mediamtx 是独立二进制,不属于任何一个系统。Sense 管它的配置与生命周期。 + +#### M1 骨架(约 200 行起步,全部是生产代码,**只动 Sense**) + +``` +Sense/ + cmd/sense-api/main.go + internal/ + onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime + store/ 先 SQLite,schema 与生产 Postgres 保持一致 + mtx/ oapi-codegen 生成的客户端 + 薄封装 + reconcile/ 最小对账循环(幂等 + 退避) + probe/ 探活 goroutine +``` + +跑通之后就已经站在生产架构上了,后面是往上加(Postgres、多租户、隧道、分片),**不是推倒重来**。Brain 与 Bell 到 M3 才起步——那也是事件契约第一次被真实使用的时刻。 + +### 演示与验证阶段的使用红线 + +| 允许 | 禁止 | +| --- | --- | +| 实验室接自购测试摄像头做兼容性验证 | 接入真实住户/学校的摄像头 | +| M0 直接运行现成 NVR(如 MiBeeNvr)给干系人做效果演示 | 把它作为可持续演进的生产系统第一版上线;将匿名录像端点暴露到真实网络 | +| 按 §1.4 白名单阅读/移植 ONVIF、IP 自愈、健康检测和 UI 交互 | 复制媒体内核、SQLite 真相源、AI 或本地用户体系;作为长期依赖引入 go.mod | + +> 理由:现成 NVR 的目标通常是单机一体化交付,而本项目是多租户控制面 + 可分片媒体面 + 独立推理面。许可证宽松不等于架构适配或已通过 128 路压测;生产采用范围必须由本项目自己的兼容性、容量、安全和故障隔离测试决定。 + +## 6.2 备选与逃生通道 + +| 主选 | 备选 | 触发切换的条件 | +| --- | --- | --- | +| Savant (DeepStream) | **Pipeless**(Rust,不绑 NVIDIA,支持 ONNX Runtime/OpenVINO/TensorRT,HTTP 动态增删流) | 硬件选型变更、规模缩小、需信创环境 | +| **YOLO + Ultralytics Enterprise License** | YOLOX(检测)+ RTMPose(姿态),均 Apache-2.0 | 授权采购未获批;或边缘 NPU 跑不动 YOLO | +| 人脸识别(授权租户) | 纯 ReID + 角色标签/工牌属性 | 合规意见未通过,或客户撤回授权 | +| 自研设备管理 | EdgeX Foundry 最小服务集 | 确定接入雷达/门磁/Modbus 等异构设备时 | +| 中心推理 | 边缘推理 | 隐私要求提升或带宽成本超预期 | + +> **推理代码必须与框架解耦**——模型推理、时序判定、特征工程写成独立模块,框架只负责调度。这是切换成本能否控制的关键。 + +### EdgeX 最小服务集(若切换) + +``` +core-metadata 设备注册表 +core-command 命令下发入口 +device-onvif-camera ONVIF <-> REST +redis 消息总线 +(consul 可用 -cp=none 省掉) +``` + +砍掉 core-data、app-service-*、security 全套。⚠️ discovery 在本场景基本不可用:netscan 对大量 /24 段是数万次探测,应使用预定义 deviceList 或 REST 批量录入,但保留 `EnableStatusCheck` / `CheckStatusInterval` 做探活。另注意 EdgeX 3.x 起 API 路径是 `/api/v3`,网上大量 2.x 文档写的是 `/api/v2`。 + +--- + +# 7. 落地检查清单 + +开工前逐项确认: + +## 硬件与网络 +- [ ] 3–5 款候选摄像头完成 §2.1 必过项实测,白名单归档 +- [ ] 各场景站点的网络出口带宽、是否有公网 IP 已摸底 +- [ ] 边缘硬件选型确定(RK3588 / Jetson / x86+GPU) +- [ ] 中心 GPU 型号确认(若需可视化回推,**必须专业卡**) + +## 许可与合规 +- [ ] **Ultralytics Enterprise License 已采购**,覆盖私有化实例 / 边缘模型 / 二次训练权重 +- [ ] 人脸与 ReID 模型的**权重许可**已逐个核对(可能与代码许可不一致) +- [ ] 视频证据留存期限已由法务确认 +- [ ] 各角色查看范围的法定边界已确认 +- [ ] 被拍摄者知情同意的取得方式与留档流程已确定 + +## 人脸能力启用前(逐租户执行) +- [ ] 租户授权书已签署并录入 `tenant_features`(含用途、范围、**到期日**) +- [ ] 人脸信息的**单独同意**取得方式已确定(通常不能与一般隐私政策打包) +- [ ] 底库来源合法性由客户书面承诺 +- [ ] 底库人员的知情/查询/删除权响应流程与时限已定 +- [ ] **未成年人场景:专项合规意见已出**(S2 独立阻塞项) +- [ ] 人脸数据留存期限已单独配置(独立于视频证据) +- [ ] 终止服务时的底库彻底删除义务与证明方式已约定 + +## 架构 +- [ ] `devices` 表而非 `cameras`,含 `device_type` +- [ ] 设备身份用序列号 +- [ ] 触发入口独立成 handler +- [ ] `sync_state` 每系统一行 +- [ ] 对账器安全闸已实现并有指标 +- [ ] 隐私区域拒绝 camera 类型是**代码级**约束 +- [ ] 所有查询强制带 tenant_id,在数据访问层统一拦截 +- [ ] 推理封装与具体模型解耦(`Detector` / `PoseEstimator` 接口),逃生通道随时可用 +- [ ] 人脸比对为**异步旁路**,超时不阻塞事件生成 +- [ ] 依赖 identity 的规则默认 `on_unavailable: treat_as_unknown` +- [ ] `watchlist` 类规则**不得自动触发对人的处置动作**(代码级) +- [ ] 未授权租户在 API 与 UI 层均不可见人脸能力 +- [ ] `persons.valid_until` 非空约束 + 到期自动失效任务 +- [ ] 告警先落库再投递,重启可续跑升级链 +- [ ] 静默功能无"永久"选项 + +## 数据闭环 +- [ ] 事件证据含结构化观测序列(bbox_seq / keypoint_seq) +- [ ] 误报标记含归因分类 +- [ ] 标注队列从 M3 第一天就在收数据 +- [ ] 每条规则的命中量与误报率可查 + +--- + +> 本方案的所有技术决策均可追溯到《02-需求分析》的 C1–C9。若实施中发现某条决策不成立,应先回到《02》修改分析结论,再同步修订本文档——**不要在实现里悄悄绕过**。 diff --git a/docs/raw/04-目标客户-政府购买居家养老.md b/docs/raw/04-目标客户-政府购买居家养老.md new file mode 100644 index 0000000..128914f --- /dev/null +++ b/docs/raw/04-目标客户-政府购买居家养老.md @@ -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 唯一撑得起付费的逻辑。 diff --git a/docs/raw/05-目标客户-寄宿制学校.md b/docs/raw/05-目标客户-寄宿制学校.md new file mode 100644 index 0000000..f2f2650 --- /dev/null +++ b/docs/raw/05-目标客户-寄宿制学校.md @@ -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-目标客户-工厂园区安全生产》。 diff --git a/docs/raw/06-目标客户-工厂园区安全生产.md b/docs/raw/06-目标客户-工厂园区安全生产.md new file mode 100644 index 0000000..e74639b --- /dev/null +++ b/docs/raw/06-目标客户-工厂园区安全生产.md @@ -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 试运行收基线 → 用真实数据周报转化 → 年服务费锁定 → 同城口碑复制。 diff --git a/docs/raw/07-事件契约比对-silver_pose.md b/docs/raw/07-事件契约比对-silver_pose.md new file mode 100644 index 0000000..e90e99f --- /dev/null +++ b/docs/raw/07-事件契约比对-silver_pose.md @@ -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--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) diff --git a/docs/raw/08-三系统职责划分.md b/docs/raw/08-三系统职责划分.md new file mode 100644 index 0000000..31b7170 --- /dev/null +++ b/docs/raw/08-三系统职责划分.md @@ -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)。 diff --git a/docs/raw/contracts/README.md b/docs/raw/contracts/README.md new file mode 100644 index 0000000..fd2733f --- /dev/null +++ b/docs/raw/contracts/README.md @@ -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 任务流程走) | 待定 | diff --git a/docs/raw/contracts/event-v0.1.example-current.json b/docs/raw/contracts/event-v0.1.example-current.json new file mode 100644 index 0000000..136f286 --- /dev/null +++ b/docs/raw/contracts/event-v0.1.example-current.json @@ -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": {} +} diff --git a/docs/raw/contracts/event-v0.1.example-radar.json b/docs/raw/contracts/event-v0.1.example-radar.json new file mode 100644 index 0000000..ea497fb --- /dev/null +++ b/docs/raw/contracts/event-v0.1.example-radar.json @@ -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 + } +} diff --git a/docs/raw/contracts/event-v0.1.example-target.json b/docs/raw/contracts/event-v0.1.example-target.json new file mode 100644 index 0000000..4302346 --- /dev/null +++ b/docs/raw/contracts/event-v0.1.example-target.json @@ -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": {} +} diff --git a/docs/raw/contracts/event-v0.1.schema.json b/docs/raw/contracts/event-v0.1.schema.json new file mode 100644 index 0000000..ed77d14 --- /dev/null +++ b/docs/raw/contracts/event-v0.1.schema.json @@ -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--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" + } + } +} diff --git a/docs/raw/提案-寄宿制学校.html b/docs/raw/提案-寄宿制学校.html new file mode 100644 index 0000000..e936528 --- /dev/null +++ b/docs/raw/提案-寄宿制学校.html @@ -0,0 +1,783 @@ +校园夜间安全预警系统 · 提案 + + + +
+ +
+ + +
+
+

YOVISION · 校园安全预警系统

+

夜里出事,
第一个知道的应该是学校

+

面向寄宿制学校的视频行为预警方案 · 方案提案

+
+ 22:3006:00 +
+

那道没有人在看的窗口

+
+
+ + +
+
+

现状

+

摄像头装了,晚上谁在看?

+

这不是设备问题。绝大多数寄宿制学校的监控覆盖已经够用了,缺的是夜里那几个小时里,有人替你盯着这些画面。

+
+
240
在用摄像头(1200 人规模典型配置)
+
2
夜间在岗人数 · 宿管 1 门卫 1
+
16
值班屏能同时显示的画面数
+
0
凌晨真正被人看过的画面
+
+

监控让你事后查得到,但它不会在半夜叫醒任何人。

+
+
+ + +
+
+

情景推演 · 现在的夜晚

+

一个普通的周三夜里

+
    +
  1. 熄灯查寝完毕,两栋宿舍楼安静

    正常
  2. +
  3. 两名学生从操场东侧围墙翻出

    无人知晓
  4. +
  5. 值班屏画面正常轮巡,无人在岗查看

    无人知晓
  6. +
  7. 早操点名,两人不在队列

    发现异常
  8. +
  9. 学校组织人手开始寻找,调取录像回溯

    被动响应
  10. +
+
+ 事情发生 → 学校知情 + 7 小时 45 分 +
+
+
+ + +
+
+

情景推演 · 同一个夜晚,装了系统

+

同样是这堵墙

+
    +
  1. 熄灯查寝完毕,系统进入夜间规则

    布防
  2. +
  3. 围墙东侧检测到翻越动作

    命中规则
  4. +
  5. 值班室大屏弹窗 + 声光,附现场画面

    已送达
  6. +
  7. 宿管、保卫科手机同时推送

    已送达
  8. +
  9. 值班员点击确认,前往东侧围墙

    已确认
  10. +
  11. 学生在校外一百米处被截回

    已处置
  12. +
+
+ 事情发生 → 有人到场 + 4 分钟 +
+
+
+ + +
+
+

我们做什么

+

把已经装好的摄像头,
变成一个只在出事时叫人的系统

+

不增加盯屏岗位,不改变现有值班安排。平时它一句话不说;一旦命中规则,它会一直找人,直到有人确认。

+

不加人 · 不换设备 · 不增加值班负担

+
+
+ + +
+
+

先开这四条

+

夜间最该被叫醒的四件事

+
+
+ 全天 · 夜间加权 +

翻越围墙护栏

+

识别攀爬姿态与越线方向,区分「翻出去」和「路过」,避免把走读生放学误报成翻墙。

+
+
+ 22:30 — 06:00 +

晚归与夜间离寝

+

宿舍楼出入口机位,就寝后进出即记录,可按年级与楼栋分别设定时段。

+
+
+ 全天 +

打架推搡与围观聚集

+

识别剧烈肢体动作与人群聚拢,课间与就寝前是高发时段,秒级推到值班室。

+
+
+ 非开放时段 +

危险区域闯入

+

天台、配电房、水池、实验室,在图上圈出范围与生效时段,进入并停留即告警。

+
+
+

操场跌倒、上课时段楼道徘徊、外来人员尾随进校,可在运行稳定后逐条加开。

+
+
+ + +
+
+

响应链路

+

报出去不算数,有人确认才算

+
+
01 检测命中规则
+
02 取证自动回捞事发前十秒画面
+
03 送达值班室大屏 + 声光 + 手机
+
04 确认值班员点击确认并处置
+
05 升级超时无人确认,自动往上找人
+
+

发出去和被人看到,是两回事。没有人确认,系统会自己升级到保卫科长、再到值班校领导——不会停在「已发送」。

+
+
+ + +
+
+

怎么落地

+

不重新布线,不换品牌

+
    +
  • 接入贵校现有的网络摄像头,支持标准协议即可,不限品牌型号。开工前先做半天勘测,出一份点位报告:哪些机位现在就能用、哪些需要补光或调角度。不承诺坏机位的效果。
  • +
  • 校内放一台推理服务器,视频与分析全部在校园网内完成。视频不出校。上传到平台的只有告警事件本身和那一小段证据片段。
  • +
  • 值班室加一块大屏和一套声光,其余沿用现有设备。
  • +
  • 规则、区域、时段在网页上配置,学期变化自己就能改。
  • +
+
+
+ + +
+
+

关于隐私

+

我们不做人脸识别

+
    +
  • 不建立学生人脸库,不采集学生面部特征。
  • +
  • 系统判断的是「有人翻墙」,不是「谁翻了墙」。告警推给值班员的是位置、时间和画面,由老师到现场自己看是哪位同学。
  • +
  • 跨摄像头跟踪使用匿名标识,仅当次有效,不留档、不关联到具体学生。
  • +
  • 未成年人影像的合规要求是硬线,也是家长最先问的问题。这一页的内容,可以直接写进给家长的说明里。
  • +
+
+
+ + +
+
+

怎么开始

+

先看两周数据,
再决定要不要开告警

+

第一周就用误报轰炸值班室,这套系统在贵校就再也没人信了。所以试运行期只统计、不告警——不打扰任何人的现有值班。

+
    +
  • 两周后交一份周报:本周检测到翻越多少次、集中在哪个时段和哪段围墙。
  • +
  • 晚归多少人次,集中在哪栋楼哪个门。
  • +
  • 误报多少次,分别是什么原因,已经怎么调整。
  • +
  • 你看完这份数据,再决定正式开哪几条规则。
  • +
+
+
+ + +
+
+

不止夜里有用

+

平安校园检查,报表直接导出

+
    +
  • 点位覆盖情况、告警处置率、平均响应时长,按月按学期一键导出。迎检材料不用再临时拼,系统里本来就在记。
  • +
  • 寒暑假切换成周界模式,全年不闲置。
  • +
  • 每月一次安全分析:高发时段、高发点位、机位调整建议。哪段围墙被翻得最多,数据会告诉你——这比加派人手更省。
  • +
  • 每一条告警的完整记录留存可查:什么时候报的、报给了谁、谁在几点确认、怎么处置的。
  • +
+
+
+ + +
+
+

投入结构

+

两笔钱,分开算

+
+
+

建设费

+

一次性

+
    +
  • 校内推理服务器
  • +
  • 平台部署与调试
  • +
  • 存量摄像头接入
  • +
  • 区域、时段与课表配置
  • +
  • 值班室大屏与声光
  • +
+
+
+

年服务费

+

按路数计,逐年

+
    +
  • 规则随学期与课表调整
  • +
  • 误报持续收敛
  • +
  • 模型更新
  • +
  • 迎检报表与月度分析
  • +
  • 远程运维与故障响应
  • +
+
+
+

具体报价需现场勘测后出具。哪些点位现在就能用、哪些需要补光或换机位,勘测报告里会逐条写清楚——我们不对达不到条件的机位承诺效果,也不把这部分算进合同。

+
+
+ + +
+
+

下一步

+

三步,不影响现有值班

+
    +
  1. +

    现场勘测

    +

    半天时间,走一遍围墙、宿舍楼出入口和危险区域,出具点位可行性报告。

    +
  2. +
  3. +

    试运行两周

    +

    只统计不告警,值班室照常运转,不增加任何人的工作。

    +
  4. +
  5. +

    看数据决定

    +

    拿到周报后,由学校决定正式开哪几条规则、覆盖哪些点位。

    +
  6. +
+

YoVision 校园安全预警系统 · 本方案中的夜间时间线为情景推演,用于说明响应流程,非真实事件记录。

+
+
+ +
+ +
01 / 13
+
↑ ↓ 翻页
+ + diff --git a/docs/raw/流程图-系统怎么工作.html b/docs/raw/流程图-系统怎么工作.html new file mode 100644 index 0000000..ce320d1 --- /dev/null +++ b/docs/raw/流程图-系统怎么工作.html @@ -0,0 +1,303 @@ +系统怎么工作 · 一张图看懂 + + + +
+ +
+

YOVISION · 系统工作原理

+

摄像头拍到 → 有人来管,中间发生了什么

+

整套系统只做一件事:把「装了但没人看」的摄像头,变成「出事时会主动叫人」的系统。下面是完整的五步。

+
+ +
+ +
+
+ + 看 +
+

摄像头

+

IP CAMERA

+

装在围墙、走廊、客厅这些地方,对着现场。用你已经装好的,不用换新的。

+
+ +
+ +
+
+ + 收 +
+

录像主机

+

NVR · 边缘盒子

+

把所有摄像头的画面汇到一起,存在本地。视频先留在现场,平时不往外传。

+
+ +
+ +
+
+ + 判断 +
+

AI 分析

+

推理服务

+

替你盯着这些画面。有人翻墙、有人摔倒、有人进了不该进的地方——它认得出来,也分得清什么是正常走动。

+
+ +
+ +
+
+ + 记录 +
+

管理后台

+

值班台账

+

每一次异常变成一条记录,带上那一小段视频。谁在几点确认的、怎么处理的,全都留着,随时能查。

+
+ +
+ +
+
+ + 叫人 +
+

手机 App

+

预警投递

+

推到值班员和负责人手机上。没有人点确认,它会一直往上找人,不会停在「已发送」。

+
+ +
+ +
+ ← 反馈回流 +

如果这次报错了,值班员点一下「误报」,这段画面会自动回到 AI 那里当作教材。用得越久,报得越准。

+
+ +
+
+

平时不打扰

+

没有异常的时候,系统一句话不说,也不往外传视频。只有出事的那一小段才会被留下来、发出去。

+
+
+

视频不出现场

+

录像和 AI 分析都在本地完成。上传到平台的只有告警记录和作为证据的那一小段画面。

+
+
+

不认人,只认事

+

系统判断的是「有人翻墙」,不是「谁翻了墙」。默认不做人脸识别,不建人脸库。

+
+
+ +
YoVision 智能视频事件平台 · 通用场景流程说明 · 2026
+ +
diff --git a/docs/raw/跌倒监护系统流程图.pdf b/docs/raw/跌倒监护系统流程图.pdf new file mode 100644 index 0000000..824ab24 Binary files /dev/null and b/docs/raw/跌倒监护系统流程图.pdf differ diff --git a/docs/raw/跌倒监护系统流程图.png b/docs/raw/跌倒监护系统流程图.png new file mode 100644 index 0000000..2059548 Binary files /dev/null and b/docs/raw/跌倒监护系统流程图.png differ diff --git a/docs/raw/跌倒监护系统流程图.svg b/docs/raw/跌倒监护系统流程图.svg new file mode 100644 index 0000000..a002910 --- /dev/null +++ b/docs/raw/跌倒监护系统流程图.svg @@ -0,0 +1,140 @@ + + + + + + + + +居家跌倒监护系统 +从摔倒发生,到有人赶到 +摄像头平时不工作。只有雷达发现异常,系统才会看一眼。 + + + + + +1 +家中感知 · 24 小时 + + + + + + + +雷达日夜守护 +卧室、卫生间安装毫米波雷达, +只感知动作,不成像。 +摄像头平时保持休眠。 + + + + + + +2 +就地录像 · 3 秒内 + + + + + + +唤醒摄像头 +家中的小盒子立刻唤醒摄像头, +并自动回看事发前 30 秒。 +录像保存在家里,不全部上云。 + + + + + + +3 +智能判断 · 10 秒内 + + + + + + + +是摔倒,还是坐下 +AI 分析身体姿态,区分真正的 +摔倒与坐下、弯腰、宠物走动。 + +判断为误报 → 静默记录,不打扰家人 + + + + +确认摔倒 + + + + + +4 +管理后台 · 全程留痕 +所有事件,完整记录 +家属与养老机构随时可查 + + + +事件与视频证据 +谁、在何时、发生了什么 + + +设备状态监测 +摄像头、雷达离线自动提醒 + + +处置结果回填 +标记误报,系统越用越准 + + + + + + + +5 +逐级通知 +最坏情况:3 分钟内一定有真人接手 +直到有人接手 —— 任何一级被确认,升级立即停止 + + + + + + +预警 App 推送 +0 – 30 秒 + +短信 + 语音电话 +30 – 60 秒 · 可穿透静音 + +备用联系人 +60 – 90 秒 · 邻居、护理员 + +呼叫中心人工介入 +90 秒后 + + + + + + + + +隐私优先:卧室与卫生间只装雷达,不装摄像头;摄像头平时不解码、不上传,仅在确认异常时上传相关片段。 + diff --git a/docs/其他项目的文档/居家老人跌倒监护系统 — 方案流程说明(客户版).md b/docs/其他项目的文档/居家老人跌倒监护系统 — 方案流程说明(客户版).md new file mode 100644 index 0000000..5ee8a63 --- /dev/null +++ b/docs/其他项目的文档/居家老人跌倒监护系统 — 方案流程说明(客户版).md @@ -0,0 +1,135 @@ +# 📊 居家老人跌倒监护系统 — 方案流程说明(客户版) + +> 来源:Notion · https://app.notion.com/p/3af401dc38a681a186a4fa7a8f5930a5 +> 导出时间:2026-08-01 + +> 面向客户的方案说明材料 · 居家老人跌倒监护系统 + +# 一句话说明 + +**雷达 7×24 守护,AI 二次确认,确认跌倒后 3 分钟内必定有人响应。** + +平时不看、不录、不传——只有在真正发生异常时,系统才会看一眼。 + +# 一、业务流程 + +```mermaid +flowchart TD + A["老人在家中正常生活"] --> B{"雷达持续监测
姿态异常或长时间静止"} + B -->|一切正常| A + B -->|疑似跌倒| C["唤醒摄像头
回捞事发前 30 秒画面"] + C --> D{"AI 姿态分析
二次确认"} + D -->|判定为误报| E["静默记录
不打扰家属"] + E --> A + D -->|确认跌倒| F["生成预警事件
附带视频证据"] + F --> G["第一级:家属 App 推送"] + G --> H{"30 秒内确认?"} + H -->|已确认| M["家属查看视频
前往处理"] + H -->|未确认| I["第二级:短信 + 语音电话"] + I --> J{"60 秒内确认?"} + J -->|已确认| M + J -->|未确认| K["第三级:备用联系人
邻居 / 物业 / 护理员"] + K --> L{"90 秒内确认?"} + L -->|已确认| M + L -->|未确认| N["第四级:呼叫中心人工介入"] + M --> O["处置结果回填
误报反馈持续优化系统"] + N --> O +``` + +## 关键时间线 + +| 时间 | 发生什么 | +| --- | --- | +| T+0s | 跌倒发生,雷达捕获 | +| T+3s | 摄像头唤醒,AI 开始分析 | +| T+10s | 确认跌倒,首次通知发出 | +| T+40s | 未响应 → 短信与电话 | +| T+100s | 未响应 → 备用联系人 | +| T+190s | 未响应 → 呼叫中心人工介入 | + +**最坏情况下,三分钟内一定有真人接手。** + +# 二、系统架构 + +```mermaid +flowchart LR + subgraph HOME["用户家中"] + CAM["IP 摄像头"] + RADAR["毫米波雷达
卧室 / 卫生间"] + NVR["边缘盒子
录像 + 设备管理"] + CAM --> NVR + RADAR --> NVR + end + subgraph CLOUD["云端平台"] + SAVANT["AI 分析引擎
姿态识别 + 跌倒判定"] + CORE["业务核心服务
事件 / 证据 / 权限"] + ALERT["预警调度引擎
升级链 + 多通道"] + 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["试点验证
10 户"] --> P2["小规模运营
50 户"] --> P3["全量部署
200 户"] +``` + +| 阶段 | 重点 | 产出 | +| --- | --- | --- | +| 试点 | 验证检测准确率与安装流程 | 真实误报基线、安装规范 | +| 小规模 | 验证告警响应链与运维能力 | SLA 实测数据、客服流程 | +| 全量 | 规模化与成本优化 | 正式运营 | + +--- + +> 本文档为方案概述。具体技术实现、容量规划与风险控制见《线上生产开发方案》。 diff --git a/docs/其他项目的文档/跌倒检测视频接入系统 — 演示与验证开发方案.md b/docs/其他项目的文档/跌倒检测视频接入系统 — 演示与验证开发方案.md new file mode 100644 index 0000000..1034dc8 --- /dev/null +++ b/docs/其他项目的文档/跌倒检测视频接入系统 — 演示与验证开发方案.md @@ -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 +./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 对接,可按需临时使用。 diff --git a/docs/其他项目的文档/跌倒检测视频接入系统 — 线上生产开发方案.md b/docs/其他项目的文档/跌倒检测视频接入系统 — 线上生产开发方案.md new file mode 100644 index 0000000..905d535 --- /dev/null +++ b/docs/其他项目的文档/跌倒检测视频接入系统 — 线上生产开发方案.md @@ -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(阶段二) | 雷达接入 + 两级判定 | 误报率显著下降,视频常态采集下降 |