docs: initialize YoVision requirements and architecture

This commit is contained in:
QiuSW
2026-08-03 17:22:22 +08:00
commit 8a99b388c4
26 changed files with 6216 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# 事件契约 v0.1(已冻结)
> **冻结日期**:2026-08-03
> **范围**:推理侧(silver_pose 及后续推理内核)→ YoVision 平台侧的唯一数据契约。
> **依据**:《07-事件契约比对-silver_pose.md》
| 文件 | 用途 |
| --- | --- |
| `event-v0.1.schema.json` | JSON Schema(draft 2020-12),规范本体 |
| `event-v0.1.example-current.json` | **现状**基线:silver_pose 今天就能产出的形态,P1 缺口全为 null |
| `event-v0.1.example-target.json` | **目标**形态:P1 补齐后,含 keypoint 序列、clip、自动 outcome |
| `event-v0.1.example-radar.json` | **非成像模态**:卫生间纯雷达跌倒,`snapshot_uris` 为空数组,门磁佐证 |
三个 example 都是**测试夹具**,两侧仓库的单元测试都应对它们做校验。
### 修订记录
| 日期 | 变更 | 理由 |
| --- | --- | --- |
| 2026-08-03 | 初次冻结 | — |
| 2026-08-03 | 新增 `sensors[]` 与 `observation.signal_seq_uri`;`evidence.snapshot_uris` 的 `minItems` 由 1 放宽为 0 | 感知系统后期接入毫米波雷达等非成像设备,原 schema 把「感知=摄像头」焊死在契约里,纯雷达事件无法表达。**本次为原地修订**,理由是当时零实现;首个生产者上线后,§1.2 的版本递增规则绝对生效,不再允许原地改 |
---
## 1. 冻结意味着什么
1. **本目录内容在两个仓库各存一份,内容必须逐字节相同。** 任何一侧修改,同一次提交内同步另一侧
2. `schema_version` 为 `"0.1"`。**破坏性变更必须递增版本并保留旧版本文件**,不得原地修改已冻结的 schema
3. 「破坏性」的定义:删除字段、收紧取值域、把可空改为必填、改变字段语义。新增可空字段与新增 `kind` 取值**不算**破坏性
4. 实验性字段一律放 `ext`,不占用顶层命名空间,也不触发版本升级
## 2. 三条设计决定(及其代价)
**① 所有顶层键必须存在,可为 null,但不得省略。**
「省略」与「显式 null」在下游无法区分,这是这类系统最常见的排查陷阱——`identity_status` 就是被这个问题逼出来的字段。代价是生产者要写一串 null,但生产者只有一个 mapper 函数。
**② 根对象 `additionalProperties: false`。**
未登记字段一律拒绝。防的是推理侧偷偷加字段、平台侧偷偷依赖,几个月后没人知道它是不是契约的一部分。逃生口是 `ext`。
**③ `confidence` 允许为 null,且现阶段必须为 null。**
几何证据 + 时间窗状态机的判定链路没有天然置信度。伪造一个常量会污染所有下游阈值调优。等换成分类模型再填。
## 3. kind 注册表
新增 `kind` 不需要升 schema 版本,但**必须在此表登记**。
| kind | 含义 | 默认 severity | 产出方 | 状态 |
| --- | --- | --- | --- | --- |
| `fall` | 人员摔倒确认 | `high` | silver_pose FSM 进入 CONFIRMED | ✅ 已上线 |
## 4. silver_pose → 契约 映射表
mapper 的完整实现规格。左列出处见《07》§1。
| 契约字段 | 来源 | 变换 |
| --- | --- | --- |
| `schema_version` | — | 常量 `"0.1"` |
| `id` | — | **平台侧**生成 ULID,加 `evt_` 前缀 |
| `source_event_id` | `event_id` | 原样。截图文件即按它命名,是回溯本地证据的唯一钥匙 |
| `tenant_id` / `site_id` / `device_id` | `source_id` | 经平台设备映射表解析。**事件中不得出现 RTSP 地址或凭据** |
| `sensors` | `source_id` | 单摄像头事件为 `[{device_id, "video", "primary"}]`;多传感器融合由推理侧列全 |
| `kind` | `event_id` 的 `FALL-` 前缀 | 提升为显式字段 `"fall"` |
| `severity` | — | 按 kind 查注册表 → `"high"` |
| `confidence` | — | 恒 `null`(见 §2③) |
| `detected_at` | `confirmed_at_utc` | 原样 |
| `occurred_at` | 换算 | `confirmed_at_utc - latency_seconds`。误差为单帧级 |
| `latency_seconds` | `latency_seconds` | 原样 |
| `config_version` | `config_version` | 原样 |
| `rule` | — | `null`(推理侧无规则引擎),平台侧按 kind 反查补全 |
| `subject.class` | — | 常量 `"person"` |
| `subject.track_id` | `track_id` | 原样 |
| `subject.attributes` | — | `{}` |
| `subject.anon_id` / `identity` | — | `null` |
| `subject.identity_status` | — | 常量 `"not_enabled"` |
| `observation.*` | — | 全 `null`(P1 补 keypoint_seq_uri;非成像模态补 signal_seq_uri) |
| `evidence.snapshot_uris` | `screenshot` | 相对路径 → URI,单元素数组。**非成像模态为 `[]`** |
| `evidence.clip_uri` / `clip_range` | — | `null`(P1 补) |
| `dedup_key` / `aggregated_into` | — | `null`,由平台侧构造 |
| `outcome` | `state` | `CONFIRMED` → `"unknown"`;`RECOVERING` 稳定后回传 `"subject_recovered"` + `outcome_source: "auto"` |
| `diagnostics.fsm_state` | `state` | 原样,**仅供排查** |
| `diagnostics.*_monotonic` | 同名字段 | 原样。**单调时钟跨进程无意义,不得用于任何时间计算** |
| `ext` | — | `{}` |
## 5. schema 拦不住的四件事(必须写代码校验)
已实测(`REJECT` = schema 能拦,`ACCEPT` = 拦不住):
schema **能**拦:省略必填键 / 非法 ULID / 顶层多余字段 / outcome 越界 / `config_version` 空串 / `snapshot_uris` 空数组 / 负 latency / kind 大写。
schema **拦不住**,需在 mapper 与平台入口各加一道断言:
| # | 校验 | 理由 |
| --- | --- | --- |
| 1 | `detected_at >= occurred_at` | 跨字段约束,JSON Schema 表达不了。倒序时间会让 SLA 统计出负数 |
| 2 | `\|detected_at - occurred_at - latency_seconds\| < 0.1s` | 三者冗余,必须自洽。不一致说明 mapper 用错了时钟 |
| 3 | `confidence is null`(现阶段) | schema 只能约束 0–1 区间,拦不住伪造值 |
| 4 | 证据文件名不含 IP、端口、凭据、客户名 | 只允许 `事件ID + 日期目录`。文件名会出现在日志、URL 与工单里 —— 这是 silver_pose `alerts.py` 头注释的既有纪律,此处升格为强制校验 |
| 5 | `sensors` 中恰有一个 `role == "primary"`,且其 `device_id == 顶层 device_id` | 数组约束,schema 表达不了。两个 primary 会让归因和去重都失去基准 |
| 6 | 若任一 `sensors[].modality == "video"`,则该 `device_id` 所在区域 `privacy_flag` 必须为 false | 隐私区域硬约束的运行时兜底。设备录入时已拦一道,此处是第二道 |
### 5.1 隐私区域约束的正确表述
原表述「隐私区域拒绝 `device_type = camera`」应改为**按模态判定**:
```
privacy_flag = true 的区域:
允许 modality ∈ {radar, contact, button, wearable}
拒绝 modality ∈ {video}
```
差别不是措辞。按设备类型判定,每接一种新设备就要重新问一次"它算不算摄像头";按模态判定,规则一次写死,新设备只需声明自己的 modality。《01》RQ-S1-05、《02》NFR-CMP-05、《03》§3.2 与落地清单四处的表述需同步。
## 6. 本地校验命令
```bash
pip install 'jsonschema>=4.18'
python3 - <<'PY'
import json
from jsonschema import Draft202012Validator, FormatChecker
s = json.load(open('event-v0.1.schema.json'))
Draft202012Validator.check_schema(s)
v = Draft202012Validator(s, format_checker=FormatChecker())
for f in ('event-v0.1.example-current.json', 'event-v0.1.example-target.json'):
errs = list(v.iter_errors(json.load(open(f))))
print(f, 'OK' if not errs else [e.message for e in errs])
PY
```
已于 2026-08-03 执行通过:schema 合法,两个 example 均校验通过。
## 7. 未决项
| # | 事项 | 决策人 |
| --- | --- | --- |
| 1 | `source_id` → `device_id` 映射表放在哪(平台 DB 表 / 边缘配置文件) | 待定 |
| 2 | 投递通道:HTTP POST 起步还是直接上 ZeroMQ | 待定,建议先 HTTP |
| 3 | `outcome` 回写的反向通道协议(平台 → 推理侧) | 待定 |
| 4 | silver_pose 侧本文件的落地方式(需按其 T-xxx 任务流程走) | 待定 |
@@ -0,0 +1,60 @@
{
"schema_version": "0.1",
"id": "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2B",
"source_event_id": "FALL-a3f9c1-000017",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5012,
"sensors": [{ "device_id": 5012, "modality": "video", "role": "primary" }],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T10:31:19.947Z",
"detected_at": "2026-08-03T10:31:22.417Z",
"latency_seconds": 2.47,
"config_version": "sp-v1-2026.07.20",
"rule": null,
"subject": {
"class": "person",
"track_id": "P-0001",
"attributes": {},
"anon_id": null,
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": null,
"dwell_sec": null,
"bbox_seq_uri": null,
"keypoint_seq_uri": null,
"signal_seq_uri": null
},
"evidence": {
"snapshot_uris": ["file:///events/20260803/FALL-a3f9c1-000017.png"],
"clip_uri": null,
"clip_range": null
},
"dedup_key": null,
"aggregated_into": null,
"outcome": "unknown",
"outcome_source": null,
"outcome_reason": null,
"diagnostics": {
"fsm_state": "CONFIRMED",
"suspected_at_monotonic": 1043.21,
"confirmed_at_monotonic": 1045.68,
"horizontal_angle_degrees": 21.4,
"visible_joint_count": 15
},
"ext": {}
}
@@ -0,0 +1,66 @@
{
"schema_version": "0.1",
"id": "evt_01J8XT7Q3R5S8V2W4X6Y9Z1A3B",
"source_event_id": "FALL-b7d2e4-000003",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5044,
"sensors": [
{ "device_id": 5044, "modality": "radar", "role": "primary" },
{ "device_id": 5045, "modality": "contact", "role": "corroborating" }
],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T02:14:08.300Z",
"detected_at": "2026-08-03T02:14:11.140Z",
"latency_seconds": 2.84,
"config_version": "radar-v1-2026.08.01",
"rule": { "id": "R-ELD-004", "version": 1, "code": "fall_confirmed_radar" },
"subject": {
"class": "person",
"track_id": "R-0002",
"attributes": {},
"anon_id": null,
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": "bathroom",
"dwell_sec": null,
"bbox_seq_uri": null,
"keypoint_seq_uri": null,
"signal_seq_uri": "s3://yov-evt/20260803/evt_01J8XT7Q.../radar_track.jsonl"
},
"evidence": {
"snapshot_uris": [],
"clip_uri": null,
"clip_range": null
},
"dedup_key": "20301:bathroom:fall:2026-08-03T02:10",
"aggregated_into": null,
"outcome": "unknown",
"outcome_source": null,
"outcome_reason": null,
"diagnostics": {
"fsm_state": "CONFIRMED",
"suspected_at_monotonic": 88214.02,
"confirmed_at_monotonic": 88216.86,
"horizontal_angle_degrees": null,
"visible_joint_count": null
},
"ext": {
"radar_doppler_peak_mps": -1.9,
"radar_target_height_m": 0.21
}
}
@@ -0,0 +1,63 @@
{
"schema_version": "0.1",
"id": "evt_01J8XR5N9P2Q4S7T1V3W5Y7Z9A",
"source_event_id": "FALL-a3f9c1-000018",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5012,
"sensors": [{ "device_id": 5012, "modality": "video", "role": "primary" }],
"kind": "fall",
"severity": "high",
"confidence": null,
"occurred_at": "2026-08-03T11:04:41.120Z",
"detected_at": "2026-08-03T11:04:42.980Z",
"latency_seconds": 1.86,
"config_version": "sp-v1-2026.07.20",
"rule": { "id": "R-ELD-001", "version": 3, "code": "fall_confirmed" },
"subject": {
"class": "person",
"track_id": "P-0004",
"attributes": { "age_group": "senior" },
"anon_id": "pid_7f3a91",
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": "living_room",
"dwell_sec": null,
"bbox_seq_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../obs.jsonl",
"keypoint_seq_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../pose.jsonl",
"signal_seq_uri": null
},
"evidence": {
"snapshot_uris": [
"s3://yov-evt/20260803/evt_01J8XR5N.../s0.jpg",
"s3://yov-evt/20260803/evt_01J8XR5N.../s1.jpg"
],
"clip_uri": "s3://yov-evt/20260803/evt_01J8XR5N.../clip.mp4",
"clip_range": ["2026-08-03T11:04:31.000Z", "2026-08-03T11:04:56.000Z"]
},
"dedup_key": "20301:living_room:fall:2026-08-03T11:00",
"aggregated_into": null,
"outcome": "subject_recovered",
"outcome_source": "auto",
"outcome_reason": "RECOVERING 状态持续 8s 后回落 NORMAL",
"diagnostics": {
"fsm_state": "RECOVERING",
"suspected_at_monotonic": 3021.44,
"confirmed_at_monotonic": 3023.30,
"horizontal_angle_degrees": 17.9,
"visible_joint_count": 17
},
"ext": {}
}
+283
View File
@@ -0,0 +1,283 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://yovision.local/contracts/event-v0.1.schema.json",
"title": "YoVision Event Instance v0.1",
"description": "推理侧 → 平台侧的唯一契约。冻结于 2026-08-03。所有顶层键必须存在(可为 null),不允许省略——省略与显式 null 无法区分,是这类系统最常见的排查陷阱。",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"id",
"source_event_id",
"tenant_id",
"site_id",
"device_id",
"sensors",
"kind",
"severity",
"confidence",
"occurred_at",
"detected_at",
"latency_seconds",
"config_version",
"rule",
"subject",
"observation",
"evidence",
"dedup_key",
"aggregated_into",
"outcome",
"outcome_source",
"outcome_reason",
"diagnostics",
"ext"
],
"properties": {
"schema_version": {
"description": "契约版本。破坏性变更必须递增主版本。",
"const": "0.1"
},
"id": {
"description": "平台侧生成的全局唯一事件 ID(ULID)。推理侧不得自行生成。",
"type": "string",
"pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
},
"source_event_id": {
"description": "推理侧原始事件 ID,如 silver_pose 的 FALL-<session>-000001。用于回溯本地截图文件名(截图即按它命名)。会话内唯一,全局不保证唯一——不得用作主键。",
"type": "string",
"pattern": "^[A-Za-z0-9_-]{1,128}$"
},
"tenant_id": { "type": "integer", "minimum": 1 },
"site_id": { "type": "integer", "minimum": 1 },
"device_id": {
"description": "主传感器的平台设备实体主键。由推理侧的 source_id 经平台映射表解析得到。事件中不得冗余 RTSP 地址或任何凭据。多传感器融合事件的完整来源见 sensors。",
"type": "integer",
"minimum": 1
},
"sensors": {
"description": "参与本次判定的全部传感器。单摄像头事件为单元素数组。恰好一个元素的 role 为 primary,且其 device_id 必须等于顶层 device_id。",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["device_id", "modality", "role"],
"properties": {
"device_id": { "type": "integer", "minimum": 1 },
"modality": {
"description": "设备模态。决定隐私区域准入:privacy_flag 为真的区域只允许非成像模态。",
"type": "string",
"enum": ["video", "radar", "contact", "button", "wearable", "other"]
},
"role": {
"description": "primary=判定主依据;corroborating=佐证(如雷达判跌倒、门磁佐证无人离开)。",
"type": "string",
"enum": ["primary", "corroborating"]
}
}
}
},
"kind": {
"description": "事件类型。取值登记在 contracts/README.md 的类型注册表中,新增类型不需要升 schema 版本。v0.1 已登记:fall。",
"type": "string",
"pattern": "^[a-z][a-z0-9_]{2,63}$"
},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"confidence": {
"description": "模型置信度。几何+状态机判定链路没有天然来源,必须填 null——不得用任意常量或阈值余量伪造。",
"type": ["number", "null"],
"minimum": 0,
"maximum": 1
},
"occurred_at": {
"description": "事发时刻(墙钟 UTC)。决定证据回捞窗口。推理侧若只有单调时钟,按 detected_at - latency_seconds 换算。",
"type": "string",
"format": "date-time"
},
"detected_at": {
"description": "判定成立时刻(墙钟 UTC)。决定 SLA 计算。必须 >= occurred_at。",
"type": "string",
"format": "date-time"
},
"latency_seconds": {
"description": "从可疑到确认的耗时。可由两时间戳相减,但显式存储:它是判定质量的直接指标——贴近确认窗口下限说明证据干脆,贴近上限是误报高发区,为误报排查的首选排序键。",
"type": "number",
"minimum": 0
},
"config_version": {
"description": "产出本事件时整套判定配置的版本。粒度高于 rule.version(阈值往往是全局的),用于调参后的回归对比。不得为空串。",
"type": "string",
"minLength": 1,
"maxLength": 128
},
"rule": {
"description": "命中的规则实体。推理侧无规则引擎时为 null,由平台侧按 kind 反查补全。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["id", "version", "code"],
"properties": {
"id": { "type": "string" },
"version": { "type": "integer", "minimum": 1 },
"code": { "type": "string" }
}
},
"subject": {
"type": "object",
"additionalProperties": false,
"required": ["class", "track_id", "attributes", "anon_id", "identity", "identity_status"],
"properties": {
"class": { "type": "string", "enum": ["person", "vehicle", "object"] },
"track_id": {
"description": "跟踪器内的短期标识,跨会话不保证稳定。",
"type": "string",
"minLength": 1
},
"attributes": {
"description": "A 类属性(年龄段、着装等)。未启用时为空对象,不是 null。",
"type": "object"
},
"anon_id": {
"description": "B+ 类 ReID 匿名标识,站点内会话级有效(≤30min),不做跨日长期关联。未启用为 null。",
"type": ["string", "null"]
},
"identity": {
"description": "C 类人脸身份。仅在租户已授权且比对命中时非 null。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["person_id", "library_id", "score"],
"properties": {
"person_id": { "type": "string" },
"library_id": { "type": "string" },
"score": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"identity_status": {
"description": "必须显式。只写 null 无法区分「没开这功能」与「比对失败」,后者是需要排查的故障。",
"type": "string",
"enum": ["not_enabled", "pending", "matched", "below_threshold", "no_candidate", "timeout"]
}
}
},
"observation": {
"description": "结构化观测。bbox/keypoint 序列是数据闭环的唯一原料——只有视频与截图无法用于训练。",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["zone", "dwell_sec", "bbox_seq_uri", "keypoint_seq_uri", "signal_seq_uri"],
"properties": {
"zone": { "type": ["string", "null"] },
"dwell_sec": { "type": ["number", "null"], "minimum": 0 },
"bbox_seq_uri": {
"description": "视觉模态专用。非视觉事件为 null。",
"type": ["string", "null"],
"format": "uri"
},
"keypoint_seq_uri": {
"description": "COCO-17 关键点逐帧序列(JSONL)。视觉模态专用,P1 必补项。",
"type": ["string", "null"],
"format": "uri"
},
"signal_seq_uri": {
"description": "非视觉模态的结构化序列(雷达点云轨迹与多普勒、门磁状态变迁等,JSONL)。与 keypoint_seq_uri 平级——两者是各自模态的数据闭环原料,缺任一模态的序列,该模态就无法参与模型迭代。",
"type": ["string", "null"],
"format": "uri"
}
}
},
"evidence": {
"type": "object",
"additionalProperties": false,
"required": ["snapshot_uris", "clip_uri", "clip_range"],
"properties": {
"snapshot_uris": {
"description": "证据截图。**允许为空数组**:非成像模态(雷达、门磁)产出的事件本就没有画面,隐私区域更是禁止成像。不得据此假设每个事件都有图可看——值班台 UI 必须能渲染无画面事件。文件命名只允许包含事件 ID 与日期目录,绝不得含 RTSP 地址、凭据或客户名称,文件名会出现在日志、URL 与工单中。",
"type": "array",
"minItems": 0,
"items": { "type": "string", "format": "uri" }
},
"clip_uri": {
"description": "含 pre-roll 的证据片段。仅有截图不足以让值班员判断真假,是误报反馈闭环的前置条件。P1 必补项。",
"type": ["string", "null"],
"format": "uri"
},
"clip_range": {
"type": ["array", "null"],
"minItems": 2,
"maxItems": 2,
"items": { "type": "string", "format": "date-time" }
}
}
},
"dedup_key": {
"description": "跨机位/跨时间去重键,由平台侧构造。推理侧进程内按 source_event_id 的去重仍保留——它防的是同帧重复写盘,属不同层次。",
"type": ["string", "null"]
},
"aggregated_into": {
"description": "被合并入的事件 ID。非 null 时本事件不独立触发告警。",
"type": ["string", "null"],
"pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
},
"outcome": {
"description": "处置结果。事件不可变,误判只能通过本字段标记,不得删改。subject_recovered 由推理侧状态机自动回传(确认后自行起身),无需等人工。",
"type": "string",
"enum": [
"unknown",
"true_positive",
"false_positive",
"subject_recovered",
"duplicate",
"test"
]
},
"outcome_source": {
"type": ["string", "null"],
"enum": ["auto", "manual", null]
},
"outcome_reason": { "type": ["string", "null"] },
"diagnostics": {
"description": "推理侧内部诊断量,仅用于排查,平台不得依赖其语义。单调时钟跨进程无意义,不得用于任何时间计算。",
"type": ["object", "null"],
"additionalProperties": true,
"properties": {
"fsm_state": {
"type": "string",
"enum": ["NORMAL", "SUSPECT", "CONFIRMED", "RECOVERING"]
},
"suspected_at_monotonic": { "type": "number" },
"confirmed_at_monotonic": { "type": "number" },
"horizontal_angle_degrees": { "type": ["number", "null"] },
"visible_joint_count": { "type": ["integer", "null"], "minimum": 0, "maximum": 17 }
}
},
"ext": {
"description": "厂商/场景扩展位。根对象 additionalProperties=false,任何未登记字段一律放这里,避免为实验性字段升版本。",
"type": "object"
}
}
}