Files
orisys-sdk/docs/stream-contract.zh.md
T
ykcheungab f6f3c45285 feat: 新增单路流类型合同 stream-contract/1
冻结逐帧身份、采集时钟、测量头和事件信封:FrameIdentity、FrameClock、FrameStamp、MeasurementFrame、StreamEvent,并从包根导出。采用严格读取:未知字段和未列出的版本一律拒绝,任何字段变更都升版本,读取方在 SUPPORTED_CONTRACT_VERSIONS 中列出可读版本;未知事件 kind 仍兼容。事件可哈希,kind 为 lower_snake。合同写入 docs/stream-contract.zh.md;不接入现有运行时。窄套件 139 项、lint 通过。
2026-10-21 11:03:53 +08:00

177 lines
9.6 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.
# 单路流类型合同(`stream-contract/1`)
用途:reference。范围:`orisys.stream_contract` 冻结的逐帧身份、采集时钟、测量头和事件信封。
状态:T03 已验收(done),2026-10-21。只冻结类型、校验和 JSON 往返,不接入现有 Sensor、StreamPipeline 或 ForceSession;T05、T06、T08、T11、T13–T15 复用这些头,不再各自另立字段。
## 为什么先冻结共享头
阶段 0 修复的是参考版本传递和复位线程归属;阶段 1 要让图像块、力会话、单路入口以及 Studio 的记录与分析使用同一份逐帧身份和时标。若每层各写一套字段,就会重新出现“同一 PTS 的两个源帧被当成重复”“文件名代替标定版本”“处理耗时冒充源时间”这类旧缺陷。本任务把这些共享字段冻结成可 JSON 往返的 frozen 数据类;后续任务只增加各自负载,不改已有字段含义。
## 稳定性规则
- `STREAM_CONTRACT_VERSION = "stream-contract/1"` 写入每个可持久化对象的 `to_dict()`。采用严格读取(政策 A):**任何新增、改名、删除或改类型的字段都升版本**,不存在同版本新增字段。
- 读取方只读 `SUPPORTED_CONTRACT_VERSIONS` 中显式列出的版本,并拒绝未列出的版本和未知字段;写方要加字段时必须升版本,并把新版本加入读取方列表。
- 增加新事件 `kind` 属于兼容变化:消费者忽略不认识的 `kind`,不因未知事件失败;但 `kind` 本身的名称格式受信封合同约束(见下)。
- 构造即校验;非法数据立即失败,不生成“看起来合理”的默认值。缺失组身份不伪造同步,缺失时钟不补假时间。
- 本任务不改变已发布接口:`Sensor.reset()`、`StreamPipeline` 快照和当前 `ForceSession` 行为不变;T06 重做会话时才改为直接消费这里定义的测量头。
## 帧身份
`FrameIdentity` 表达“哪一路的哪一帧”,字段如下。
| 字段 | 类型 | 说明 |
|---|---|---|
| `sensor_id` | `str` | 逻辑 Sensor 身份,非空 |
| `session_id` | `str` | 一次运行/录制的会话身份,非空 |
| `frame_id` | `int` | 该路连续输出序号,`>= 0` |
| `source_frame_id` | `int \| None` | 源自己的帧号;源没有编号时为 `None`,有编号时必须保留原值 |
| `acquisition_id` | `str \| None` | 可选组身份之一,与 `sync_group_id` 同时出现或同时缺失 |
| `sync_group_id` | `str \| None` | 可选组身份之一,不得只给一半 |
规则:
- 去重、缓存和迟到判断以源身份为准;时间戳不是身份。两个源帧可以有相同的 PTS,它们仍是两帧。
- 相同时间戳不触发去重、清零、复位或缓存替换;入口如何忽略重复输入属于 T08,本任务只保证类型层面可区分。
- 组身份缺失表示本路独立,不表示已同步;不伪造 acquisition/sync 值。
- 组身份即使存在,也只说明采集组归属;跨时钟映射和同步容差仍需后续计划实测,不能由字段存在推断。
## 采集时钟
`FrameClock` 保存一帧的采集时间和它的时间来源。
| 字段 | 类型 | 说明 |
|---|---|---|
| `capture_time_ns` | `int` | 采集时间,整数纳秒,`>= 0` |
| `clock_id` | `str` | 时钟域标识,例如 `host_monotonic`、`video:recording-4` |
| `capture_time_basis` | `str` 枚举 | 见下表 |
| `source_pts` | `int \| float \| None` | 源时间戳原值(单位由源声明),与处理时间无关 |
`capture_time_basis` 的取值:
| 取值 | 含义 |
|---|---|
| `capture_monotonic` | 实时源的主机单调采集/接收时间 |
| `recorded_signal` | 录像保存的信号时间(回放/debug) |
| `video_pts` | 容器真实 PTS 换算成纳秒 |
| `estimated_source` | 无可靠 PTS,按源帧号和名义帧率推算;消费方必须当作估算值 |
规则:
- 不能用“处理计数 × 名义周期”冒充 `video_pts`,不用离线处理耗时作为信号时间,也不伪造递增 PTS。没有可信 PTS 时明确标为 `estimated_source`。
- `source_pts` 保留原值,不因换算成 `capture_time_ns` 而丢弃;发布日期、写入时间由结果信封另记,不写进采集时间。
- 时钟域之间不隐式换算;需要换算时由上层显式声明来源。
## 帧头 FrameStamp
`FrameStamp` 是身份加时钟的扁平帧头,可持久化形式如下;`identity` 和 `clock` 属性返回对应的两个组件,`from_parts()` 从组件构造。
```json
{
"schema": "stream-contract/1",
"sensor_id": "FINGER-001",
"session_id": "run-7",
"frame_id": 12,
"source_frame_id": 41,
"capture_time_ns": 1600000000000000000,
"clock_id": "host_monotonic",
"capture_time_basis": "capture_monotonic",
"source_pts": 41.5,
"acquisition_id": null,
"sync_group_id": null
}
```
## 测量头 MeasurementFrame
| 字段 | 类型 | 说明 |
|---|---|---|
| `stamp` | `FrameStamp` | 本帧身份与采集时钟 |
| `source_dt_s` | `float \| None` | 与同一 Sensor 上一条实际处理帧之间的源时间差,单位秒;第一帧为 `None`,其余 `>= 0` |
| `reference_generation` | `int` | 计算该帧光流时生效的参考版本,`>= 0` |
`source_dt_s` 的定义和边界:
- 用声明的采集时钟计算源时间差,不是处理速度,也不是相邻输出序号的固定周期。
- 相同源时间(`0.0`)是合法值,表示两帧时间相等;不能因此把两帧合并。
- 长间隔只产生警告和质量标记,不自动复位、清零或改滤波设计帧率;阈值由后续实测确定(K4.5、K9)。
- `reference_generation` 由图像线程在该帧计算光流时写入,随后沿用到结果;不能在 HHD 输出或 Studio 消费时读取全局最新计数补标签(K8.1)。迟到旧版本的样本不能进入新零点基线。
测量负载(flow、HHD 场、接触、场标定特征)以及力结果、批次和记录格式不在本任务冻结;T05、T06 定义负载并在本头上扩展,T11、T13–T15 复用同一身份和时标。
## 事件信封 StreamEvent
`StreamEvent` 是状态变化事件的公共信封:参考复位、归零进度、滤波切换、长间隔、跳帧等都由后续任务登记具体 `kind` 与 `payload` 字段。
| 字段 | 类型 | 说明 |
|---|---|---|
| `kind` | `str` | 稳定的 lower_snake 事件名,必须匹配 `^[a-z][a-z0-9_]*$`;未知 kind 必须被消费者忽略而不是报错 |
| `stamp` | `FrameStamp \| None` | 事件所属帧;无帧事件为 `None` |
| `payload` | `Mapping[str, Any]` | 只允许 JSON 类型,构造时校验并冻结为只读深拷贝 |
| `event_version` | `int` | 信封版本,`>= 1`;信封结构不兼容变化时递增 |
`payload` 拒绝 `NaN`、无穷、`bytes`、集合和非字符串键,因为这些值无法无损通过 `json.dumps`/`json.loads` 往返。事件序列化后必须能原样读回,记录才能真正复现在线状态。`StreamEvent` 可哈希:`payload` 冻结但不参与哈希,哈希只由 `kind`、`stamp` 和 `event_version` 决定,因此相等事件哈希相同;不同 payload 的事件允许落在同一哈希桶。
```json
{
"schema": "stream-contract/1",
"kind": "source_gap",
"event_version": 1,
"stamp": {
"schema": "stream-contract/1",
"sensor_id": "BOX-3",
"session_id": "run-7",
"frame_id": 3,
"source_frame_id": 12,
"capture_time_ns": 1600000000000000000,
"clock_id": "host_monotonic",
"capture_time_basis": "capture_monotonic",
"source_pts": null,
"acquisition_id": null,
"sync_group_id": null
},
"payload": {"source_dt_s": 0.5, "dropped_frames": 3}
}
```
## JSON 往返与严格校验
每个可持久化类型提供 `to_dict()` 和 `from_dict()`。`to_dict()` 带 `schema`;`from_dict()` 只接受 `SUPPORTED_CONTRACT_VERSIONS` 中显式列出的版本,并校验必填字段、未知字段和字段类型,损坏的记录立即失败而不是被部分接受。下面的例子可以直接运行:
```python
import json
from orisys import FrameClock, FrameIdentity, FrameStamp, MeasurementFrame
stamp = FrameStamp.from_parts(
FrameIdentity(
sensor_id="FINGER-001",
session_id="run-7",
frame_id=12,
source_frame_id=41,
),
FrameClock(
capture_time_ns=1_600_000_000_000_000_000,
clock_id="host_monotonic",
capture_time_basis="capture_monotonic",
),
)
text = json.dumps(MeasurementFrame(stamp, source_dt_s=0.033).to_dict())
measurement = MeasurementFrame.from_dict(json.loads(text))
assert measurement.stamp == stamp
```
未知字段被拒绝,是为了让不同版本的记录不会静默错配。采用政策 A:字段变更(包括新增字段)一律升 `STREAM_CONTRACT_VERSION`,同时把新版本加入读取方的 `SUPPORTED_CONTRACT_VERSIONS`;同版本内不新增字段。
## 不包含什么
- 不实现 SensorStream 入口、实时最新选帧、完整回放和背压(T08)。
- 不实现图像块拆分、测量负载、ForceSession 重做、零点与输出滤波(T04–T07)。
- 不实现批次、配组、多流调度(后续多流计划)。
- 不实现录制/回放格式、Studio 迁移和离线重新滤波(T11、T13–T15)。
- 不修改现有 `Sensor`、`StreamPipeline` 和当前 `ForceSession`;现有 `ForceSession.FrameStamp` 在 T06 重做时被本合同的帧头替换。
## 验证
`tests/test_stream_contract.py` 覆盖 T03 的四项验收:逐路身份与时钟、组字段可缺但不半组、同 PTS 帧可区分、`source_dt_s` 与事件 JSON 往返;同时覆盖 frozen 校验、严格版本/未知字段拒绝(政策 A)、`SUPPORTED_CONTRACT_VERSIONS`、事件可哈希、`kind` 的 lower_snake 格式和非法负载拒绝。类型本身不启动传感器、流水线或会话。