Files
yovision/docs/raw/07-事件契约比对-silver_pose.md
T

260 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 事件契约比对:silver_pose 实际输出 vs YoVision 设计
> 目的:确定 silver_pose(已验证的推理内核)与 YoVision(平台层)之间**唯一的那条契约**。
> 比对基准:silver_pose `v1/alerts.py` + `v2/internal/alert/dispatcher.go` + `v2/internal/store/store.go`;YoVision《03-通用场景应用方案》§2.6。
> 日期:2026-08-03
---
## 0. 结论摘要
| 项 | 结论 |
| --- | --- |
| v1 / v2 输出一致性 | ✅ Python 与 Go 的 JSONL 字段**完全相同**,契约已经稳定,可以直接当基线 |
| 与平台设计的关系 | silver_pose 的事件是平台事件的**真子集**,没有需要推翻的字段 |
| 真正的阻塞 | 不是字段缺失,是 **3 个语义冲突**(见 §3),必须先解决 |
| 接入点 | **已经存在**——`v1/alerts.py` 的 `AlertSink` 就是为注入外部副作用设计的,不需要改动已验收代码 |
| 反向吸收 | silver_pose 有 **5 处**设计文档没考虑到的东西,应该写回《03》§2.6 |
---
## 1. silver_pose 当前实际输出
### 1.1 JSONL(每确认事件一行)
`v1/alerts.py:197-208` 与 `v2/internal/alert/dispatcher.go:104-113`,字段一致:
```json
{
"event_id": "FALL-<session_id>-000001",
"track_id": "P-0001",
"config_version": "<配置版本串>",
"source_id": "<来源标识>",
"state": "CONFIRMED",
"confirmed_at_utc": "2026-07-20T10:31:22.417000+00:00",
"suspected_at_monotonic": 1043.21,
"confirmed_at_monotonic": 1045.68,
"latency_seconds": 2.47,
"screenshot": "20260720/FALL-abc-000001.png"
}
```
### 1.2 SQLite(仅 v2,`internal/store/store.go:35`)
JSONL 之外多出的列:
```
source_kind (默认 'rtsp')、camera_host、camera_port、camera_channel
```
> 注释里写明:**记录 host/port/channel,但永不记录密码**。
### 1.3 状态机(`v1/fall_state.py`)
`NORMAL → SUSPECT → CONFIRMED → RECOVERING`,只有进入 `CONFIRMED` 才产出 `FallEvent`。
`RECOVERING` 表示「确认摔倒后又自己起来了」,当前**不产出任何事件**。
---
## 2. 字段级比对
| silver_pose | YoVision《03》§2.6 | 状态 | 说明 |
| --- | --- | --- | --- |
| `event_id` | `id` | ⚠️ **需改造** | 生成方式不同,见 §3.2 |
| `track_id` | `subject.track_id` | ✅ 直接对应 | 语义一致 |
| `source_id` | `device_id` | ⚠️ 弱对应 | 前者是字符串标识,后者是平台设备实体主键,需映射表 |
| `state` | — | ⚠️ 语义冲突 | 见 §3.3 |
| — | `kind` | ❌ 缺 | 事件类型隐含在 `event_id` 的 `FALL-` 前缀里,必须显式化为 `"fall"` |
| `confirmed_at_utc` | `detected_at` | ✅ 直接对应 | 判定时刻,用于 SLA |
| `suspected_at_monotonic` | `occurred_at` | ❌ **不可用** | 单调时钟,跨进程无意义。见 §3.1 |
| `confirmed_at_monotonic` | — | ➖ 可丢弃 | 有 `confirmed_at_utc` 即可 |
| `latency_seconds` | (派生) | ✅ 保留 | 见 §4.3,建议平台侧也存 |
| `screenshot` | `evidence.snapshot_uris[0]` | ✅ 直接对应 | 相对路径 → URI |
| `config_version` | `rule.version` | ⚠️ 粒度不同 | 见 §4.2,两者都要留 |
| `camera_host/port/channel` | (由 `device_id` 引用) | ✅ 平台侧更优 | 平台不该在事件里冗余连接信息 |
| — | `tenant_id` / `site_id` | ❌ 缺 | 单机演示不需要,平台必须 |
| — | `rule{id,version,code}` | ❌ 缺 | silver_pose 无规则实体,只有全局配置 |
| — | `severity` | ❌ 缺 | 摔倒恒定 high,可由平台侧按 `kind` 补 |
| — | `confidence` | ❌ 缺 | 见 §3.4,**不是简单补字段** |
| — | `subject.attributes` | ❌ 缺 | 无属性识别 |
| — | `subject.anon_id` | ❌ 缺 | 无 ReID |
| — | `identity` / `identity_status` | ❌ 缺 | 可先恒为 `null` / `"not_enabled"` |
| — | `observation.bbox_seq_uri` | ❌ **重要缺口** | 见 §5.1 |
| — | `observation.keypoint_seq_uri` | ❌ **重要缺口** | 见 §5.1 |
| — | `evidence.clip_uri` / `clip_range` | ❌ **重要缺口** | 只有一张截图,无 pre-roll 视频 |
| — | `dedup_key` / `aggregated_into` | ❌ 缺 | 见 §3.5 |
| — | `outcome` / `outcome_reason` | ❌ **重要缺口** | 误报反馈闭环无落点 |
---
## 3. 五个语义冲突(不是补字段能解决的)
### 3.1 单调时钟 vs 墙钟 —— P0
`suspected_at_monotonic` / `confirmed_at_monotonic` 用的是 `time.monotonic()`。这在状态机内部是**正确选择**(不受系统对时影响,`fall_state.py:78` 还强制了单调递增校验),但它:
- 进程重启后归零,跨进程无意义
- 无法和平台的 `occurred_at` 对齐
- 无法用于证据回捞(回捞需要墙钟去定位录像位置)
**解决**:在事件产出处补一个墙钟事发时刻。
```
occurred_at = confirmed_at_utc - (confirmed_at_monotonic - suspected_at_monotonic)
= confirmed_at_utc - latency_seconds
```
这个换算精度足够(误差是单帧级别)。**保留** monotonic 字段做内部诊断,但契约上以 `occurred_at` 为准。
### 3.2 `event_id` 生成方式 —— P0
当前是 `FALL-{session_id}-{序号:06d}`(`fall_state.py:146-154`),会话内自增。
- 单机单路演示:没问题
- 平台扩展到 128 路、多分片/多节点:**序号会撞**。`session_id` 若不是全局唯一,两台边缘节点会产出同一个 `event_id`,而 `AlertDispatcher` 的去重恰好是按 `event_id` 做的 → 平台侧会静默丢弃真事件
**解决**:平台契约用 ULID(`evt_01J8X...`)。silver_pose 侧保留原 `event_id` 作为 `source_event_id` 字段,便于回溯本地文件名(截图就是按它命名的)。
### 3.3 `state` 字段语义被占用 —— P0
silver_pose 的 `state` 是**状态机状态**(`NORMAL/SUSPECT/CONFIRMED/RECOVERING`);平台事件里没有这个概念——平台事件恒等于「已确认的客观发生」,其后续流转是**告警**的状态机(《03》§2.7),两者不能混。
**解决**:silver_pose 的 `state` 不进平台事件顶层。映射为:
- `state == "CONFIRMED"` → 产出事件,`kind: "fall"`
- `state == "RECOVERING"` → **不是新事件**,是对已有事件的 `outcome` 输入(见 §4.1)
### 3.4 `confidence` 在这条链路上没有天然来源 —— P1
平台设计里 `confidence: 0.87` 隐含假设「有一个模型给出置信度」。但 silver_pose 的判定是**几何证据 + 时间窗状态机**(`evidence.py` 算躯干角度与髋部下坠比,`fall_state.py` 要求连续 1–3s 不中断),最终是布尔判定,没有一个自然的 0–1 分值。
**不要硬凑一个假的置信度。** 三个选项:
1. 置 `null`,并允许 schema 里 `confidence` 可空(推荐先这样)
2. 用可解释的替代量:`horizontal_angle_degrees` 距阈值的余量、`visible_joint_count`、`latency_seconds`(越接近确认窗口下限说明证据越"干脆")
3. 后续换成分类模型时再填
对应到《02》里「七类动作模型优于二分类」的结论——真正的 `confidence` 要等模型换代才有意义。
### 3.5 去重只在进程内存里 —— P1
`AlertDispatcher._seen_event_ids`(Python)/ `Dispatcher.seen`(Go,`dispatcher.go:34`)都是内存 `set`/`map`:
- 进程重启即丢失
- 两个机位拍到同一次跌倒 → 两条独立事件,无合并
- 无冷却期概念
这与《03》§2.6「同设备冷却期 / 同站点聚合 / 已处置抑制」是四个不同层次的去重。
**解决**:进程内去重**保留**(它防的是同帧重复写盘,是正确的);跨机位/跨时间的去重归**平台侧**,由 `dedup_key` 承担。silver_pose 只需在事件里带上足以构造 dedup_key 的原料(site + 位置 + kind + 时间桶)。
---
## 4. 反向吸收:silver_pose 比设计文档考虑得更周到的 5 处
这些应该写回《03》§2.6,是实战沉淀,设计文档里没有。
### 4.1 `RECOVERING` 状态是免费的 outcome 信号
《03》§2.6 的 `outcome` 只设计了人工来源(值班员标记误报)。但 silver_pose 的状态机已经能识别「确认摔倒后又自己站起来了」——这是**自动的、及时的**处置结果输入,比等人工标记快得多,而且正是家属/值班员最想知道的一件事。
**建议**:平台事件增加 `outcome` 的自动来源:
```
subject_recovered —— 由推理侧在 RECOVERING 稳定后回传
```
并在《03》§2.6 的 outcome 取值里显式列出「自动/人工」两种来源。
### 4.2 `config_version` 进事件
设计文档只有 `rule.version`(单条规则版本)。silver_pose 把**整个判定配置的版本**钉进每条事件(`fall_state.py` 构造时强制非空校验)。
价值:调完阈值后跑回归,能精确区分「哪些事件是旧配置产出的」。`rule.version` 粒度不够——阈值往往是全局的。
**建议**:《03》§2.6 事件结构中,`rule` 之外**增加顶层 `config_version`**。
### 4.3 `latency_seconds` 显式存储
可以由两个时间戳相减,但显式存有两个好处:一是 SLA 报表不用每次算,二是它是**判定质量的直接指标**(贴近确认窗口下限 = 证据干脆;贴近上限 = 勉强通过,是误报高发区)。
**建议**:保留为顶层字段,并作为误报排查的首选排序键。
### 4.4 证据文件命名的隐私纪律
`v1/alerts.py` 文件头注释:
> Artifact names use only the event ID and a date folder — never an RTSP address, credential, or client name.
这条纪律设计文档里**没有写**。截图/片段的文件名会出现在日志、URL、客服工单里,是最容易泄露客户身份和摄像头地址的地方。
**建议**:写入《03》§2.6「三条硬约束」表(升级为四条)。
### 4.5 存连接信息但绝不存密码
`store.go` 顶部注释明确:记录 camera_host / port / channel,**永不记录密码**。同样应写入约束表。
---
## 5. 缺口分档
### P0 —— 不补则平台无法工作(M1 前)
| # | 缺口 | 动作 |
| --- | --- | --- |
| 1 | 墙钟 `occurred_at` | 由 `confirmed_at_utc - latency_seconds` 换算 |
| 2 | 全局唯一 `id` | 平台侧生成 ULID,原 id 降为 `source_event_id` |
| 3 | 显式 `kind` | 恒为 `"fall"`,从前缀提升为字段 |
| 4 | `tenant_id` / `site_id` / `device_id` | 由 `source_id` 经映射表补全,映射表在平台侧 |
| 5 | `state` 不进事件顶层 | 按 §3.3 映射 |
### P1 —— 接入后很快会缺(M2–M3)
| # | 缺口 | 说明 |
| --- | --- | --- |
| 6 | `evidence.clip_uri` + `clip_range` | 现在只有一张截图。**一张截图不足以让值班员判断真假**,这是误报反馈闭环的前置条件 |
| 7 | `observation.bbox_seq_uri` / `keypoint_seq_uri` | 《02》C9 数据闭环的**唯一原料**。只有截图无法训练。silver_pose 内部已经有逐帧 keypoints,只是没落盘——这是**最低成本的高价值补齐** |
| 8 | `outcome` 回写通道 | 平台 → 推理侧的反向通道,目前完全没有 |
| 9 | `dedup_key` 原料 | 见 §3.5 |
### P2 —— 后续(M4+)
`severity` / `confidence` / `subject.attributes` / `anon_id` / `identity` / `aggregated_into`。
其中 `identity_status` 建议**立刻**恒填 `"not_enabled"`,成本为零,避免以后区分不了「没开」和「比对失败」。
---
## 6. 接入方案:不改已验收代码
**关键发现:接入点已经存在。**
`v1/alerts.py` 的 `AlertSink` 本来就是为注入外部副作用设计的(`play_sound` / `show_popup` 都是可注入的桌面适配器):
```python
class AlertSink:
"""Injectable output for sound and popup. Default implementation is silent."""
def play_sound(self, event: FallEvent) -> None: ...
def show_popup(self, record: AlertRecord) -> None: ...
```
所以平台接入 = **新增一个 sink 实现**,而不是改 `EventArtifactWriter`(它已通过单元测试与 T-000 验收):
```
FallEvent ──→ EventArtifactWriter ──→ 本地截图 + JSONL (现状,不动)
└─→ PlatformSink ──→ mapper ──→ YoVision event JSON ──→ 平台
```
`PlatformSink` 的职责只有三件:字段映射(§2 的表)、P0 补全(§5)、投递(HTTP POST 起步,后续换 ZeroMQ)。**投递失败必须落本地队列重试,不能阻塞主链路**——这与《03》§2.7「告警先落库再投递」是同一原则。
v2 Go 侧结构相同,`Dispatcher.Dispatch` 返回 `Record` 后加一个 publisher 即可,同样不动 `write`。
---
## 7. 下一步(按顺序)
1. **确认边界**:silver_pose 保持独立仓库,是「已验证的推理内核 + 单场景演示」;YoVision 是平台层。两者只通过本文档定义的事件 JSON 通信
2. **冻结 v0.1 契约**:把 §2 的映射表 + §5 的 P0 五项,写成一份 JSON Schema,两个仓库各放一份(内容相同),改动必须双向同步
3. **补 §4 的五处反向吸收**到《03》§2.6,特别是把隐私纪律(§4.4、§4.5)升格为硬约束
4. **P1 第 7 项优先做**:keypoint 序列落盘。它在推理侧几乎零成本(数据已在内存),但决定了《02》C9 数据闭环能否启动
5. `confidence` 保持 `null`,**不要造假值**(§3.4)