一句话:平台的价值不是“多了一张表”,而是让每条产物都能回答三个问题——哪个工具版本产出的、输入是哪一版、失败了还能不能复现。
前置阅读:机器人视频质检专题的专题首页、帧级检测器、跨模态配对。本文不重复检测器实现,讲怎么把这些检查装进一条可追溯的平台链路。
单条视频的质检脚本和平台之间,差的不是 UI,而是责任边界。脚本可以只输出一个 quality_summary.json;平台必须能回答:这份 JSON 是哪个版本的检测器、在哪台机器、对哪一版输入跑出来的,失败了以后还能不能复现。
下面按搭建顺序拆:DDL 怎么收敛、数据怎么进来、质检怎么跑、两类真实数据暴露了什么差异、踩了哪些坑、下一步缺什么。
1. 为什么需要这个平台
先把问题说具体。没有平台时,常见的状态是:
- 检测器代码在 git 里,但运行记录只在某次终端的滚动输出里;
- 三条 episode 的产物都叫
quality_summary.json,散在不同目录,重跑一次就覆盖; - 同一个
v1.0.0可能对应两个不同的镜像,因为镜像被重新构建过; - 某个 episode 被判
FAIL,三个月后无法确定是数据坏了,还是当时阈值配错了。
这四件事指向同一个需求:工具身份、输入身份、产物位置、运行结果必须被结构化地记录下来。平台不是把文件换个地方存,而是把“这次判断”变成一条可复查的事实。
具体到机器人视频质检,平台至少要接住三样东西:
- 一条 episode 的多路视频与低维数据;
- 一次质检运行的配置、工具版本和输出;
- 这条 episode 当前是否被接收(accept / reject),以及为什么。
2. DDL 怎么定:同一事实只存一处
最初的设计里,tool_versions 试图把所有表达方式都存下来:version_kind、version_label、image_uri、package_name、package_version、image_digest。这些列看起来“信息丰富”,实际是同一个事实的四种写法。
收敛后的核心形态(示意,省略审计字段):
| 列 | 决策 | 理由 |
|---|---|---|
version |
保留,v1.0.0 语义 tag,作为唯一身份 |
人可读、可比较、可排序;评审和排障都靠它 |
source_uri |
保留,完整镜像名;tag 必须等于 version |
能解析来源,同时用约束消灭“版本名和 tag 不一致” |
image_digest |
保留,可空,只用于复现 | sha256: 是机器身份,人不可读,不参与业务判断 |
version_kind |
删除 | DOCKER / GIT / PKG 可由 source_uri 形态推导 |
version_label |
删除 | 与 version 重复,只是叫法不同 |
image_uri |
删除 | 与 source_uri 重复 |
package_name / package_version |
删除 | 本质是把 source_uri 再拆成两列,换一种冗余 |
核心判断是:
- 身份要人可读。
docker:sha256:...适合做校验,不适合做主语。一个 episode 的质检结果应该能被人直接说成”robot_video_qc的v1.0.0跑的”。 - 冗余会产生两套真相。
version_label和version一旦允许不同步,查询时就要决定信谁;image_uri和source_uri同理。删除重复列比写同步逻辑便宜。 - 推导优于存储。工具类型不是业务事实,是
source_uri的形态。读取时推导即可,写库时不必固化。 - digest 只服务复现。它回答“当时到底跑的是哪块镜像”,不回答“这是哪个版本”。因此它可以为空,也不能当主键。
写入侧的契约是:source_uri 的 tag 必须与 version 相等;(tool_id, version) 唯一。这样“版本”只有一个来源, digest 只在需要逐字节复现时出现。
3. 数据怎么进来:路径由平台签发
对象路径固定成:
datasets/{dataset}/{tool}/{version}/{run_id}/{artifact}实例:
datasets/molmoact2/robot_video_qc/v1.0.0/<run_id>/quality_summary.json关键不在这个模板本身,而在谁来拼。路径前缀由 API 的 object-layout 签发,工具只拿到一个前缀,再往里写自己的产物。工具不自己拼 datasets/...。
这样做有两个直接收益:
- 改布局(换 bucket、加租户前缀、调整层级)只改一处,不用在所有工具里搜字符串;
- 工具无法“发明”一个平台不认识的路径,产物的可发现性由平台保证。
真实取样
本轮用的是 allenai/MolmoAct2-BimanualYAM-Dataset。本机到 huggingface.co 不可达,因此经 hf-mirror.com 拉取同一 revision:
| 项 | 实测值 |
|---|---|
| 取样文件 | 14 个 |
| 总字节 | 198,562,402 |
| 取样 episode | 31673 / 31689 / 31682 |
| LFS 校验 | 13 个 LFS 文件 sha256 与 HF 元数据 0 mismatch |
镜像不是“另一个数据源”,而是同一 revision 的传输通道。sha256 对齐之后,平台记录的是源数据集与 revision,而不是镜像域名。
4. 质检怎么跑:Airflow + DockerOperator
质检任务按 episode 拆成容器任务:Airflow 负责编排、重试与状态,DockerOperator 负责把检测器镜像拉起来跑一条 episode。平台记录的是 run,不是日志。
选择 DockerOperator 而不是在 Airflow worker 里直接跑 Python,理由很实际:
- 检测器依赖 OpenCV / ffmpeg,和调度器的 Python 环境隔离,升级不会互相影响;
- “这次运行用了哪个工具版本”可以直接落到
tool_versions,镜像 tag 与version绑定; - 单条 episode 失败只影响一个 task,重跑成本可控。
真实结果
三条 MolmoAct2 episode 与一条 UMI episode 的实际输出:
| 数据集 | Episode | 帧数 | 时长 | 分辨率 | fps | 状态 |
|---|---|---|---|---|---|---|
| MolmoAct2 | 31673 | 1319 | 43.97 s | 640×360 | 30 | PASS |
| MolmoAct2 | 31689 | 1628 | 54.27 s | 640×360 | 30 | PASS |
| MolmoAct2 | 31682 | 1603 | 53.43 s | 640×360 | 30 | PASS |
| UMI | 取样 episode | 400 | 40 s | 224×224 | 10 | PASS |
三条 MolmoAct2 的 coverage_ratio 落在 0.9992–0.9994;UMI 为 0.9975。这里的 coverage_ratio 衡量的是声明时长与实际时间跨度之比,不是“解码了多少帧”。时间戳在四条数据上均单调。
已知短板
这一轮质检通过了,但不等于覆盖了所有问题。三处必须写明:
- 只校验名义恒定帧率。当前检查基于
nominal_cfr_frame_clock:按声明的 30 / 10 fps 推时间轴,没有做容器 PTS 校验。可变帧率、PTS 抖动或容器时间戳错误,可能在这套检查下仍然显示 PASS。 - 不校验 state / action 语义。维度、时间轴、覆盖率能查;“动作是否合理”“夹爪是否真的夹住”不在检查范围。
- UMI 无标定,无法验证外参。这不是漏做,而是输入里就没有标定制品。
5. 两类数据的差异:同一个工具,不同的结论强度
| 维度 | MolmoAct2 | UMI |
|---|---|---|
| 许可 | Apache-2.0 | Apache-2.0 |
| 规模 | 32,246 episodes / 76,046,658 frames | 1,447 episodes / 699,432 frames |
| 相机 | 3 路,640×360 | 1 路,224×224 |
| fps | 30 | 10 |
observation.state |
14 维 | 7 维 |
| action | 有 action 列,14 维 |
无 action 列;只有 end_pose / start_pos / gripper_width |
| 标定 | card 未提及 URDF / 内外参 | 22 个文件里 URDF / 内参 / 外参关键词命中 0 |
| 本轮取样 | 14 个文件,198,562,402 字节 | 5 个文件,239,045,003 字节 |
UMI 的情况值得单独说:它的项目定位就是 calibration-free,用腕部单目相机和手持夹爪采集,不依赖机器人基座标定。因此数据集里没有 URDF、内参、外参,不是“数据质量差”,而是另一类契约。
这对平台的含义是:质检能力必须按输入能力分支,而不是按数据集名字硬编码。
- 有
action才做 action 相关检查;没有就跳过并记录“不适用”; - 有标定制品才做外参 / 投影校验;没有就输出
UNKNOWN,而不是FAIL; - 同一套
PASS在两类数据上代表的置信度不同,产物里要能看出“检查了哪些项、跳过了哪些项”。
6. 踩坑:契约比文档更硬
第一次 ingest 时,output_key 写成了带扩展名的 episode_spec.json。DAG 里按逻辑键取产物:
KeyError: episode_spec原因很简单:DAG 的契约期望 output_key 是逻辑键(episode_spec),扩展名由产物类型决定,不该出现在键里。文档没有把这件事写成硬约束,但代码按硬约束执行了。
第二个坑更贵:Episode / Artifact 是只追加、不可删除的。修好 key 之后,那两条失败记录无法清理,留下:
- 两条
FAILEDRun; - 一个废弃 Episode;
- 只能用
/acceptance后缀重新发布一条新的验收记录。
这两件事的共同教训是:契约先于便利。
- 配置里的 key 不是文件名,不能让“看起来像个文件”的写法泄漏进逻辑层;
- 追加模型下,错误不会被删除,只会被覆盖或追加。失败记录是审计证据,但也是需要治理的债:要有 supersede / 迁移策略,否则
FAILED会永久混在有效记录里。
如果重来一次,更合理的做法是在 schema 校验阶段就拒绝带扩展名的 output_key,而不是等 DAG 在运行时抛 KeyError。
7. 下一步:URDF → 投影 → YOLO,还缺三样
当前 QC 能判断“画面和时间轴是否可信”,但还判断不了“像素和三维空间是否对得上”。要把链路走到 URDF → UMI 相机投影 → YOLO 训练,缺的不是模型,而是三样基础件:
- 标定制品。URDF、相机内参、外参、畸变参数,需要版本化存储,并带 hash 与 provenance。没有版本化的标定,投影结果无法复现。
- 工具契约里的
calibration_ref与投影参数。工具要能声明“我用了哪一版标定”,平台要能据此校验外参、复算投影。当前 schema 里一个相关字段都没有。 - 标注侧与 label schema / 血缘绑定。YOLO 的框不仅要绑定到图像,还要绑定到图像对应的 episode、相机、标定版本。否则模型指标下降时,无法区分是数据、标定还是标注的问题。
这三样缺任何一样,投影都只能是一次性的离线脚本,无法进入可追溯的质检链路。UMI 的单目无标定数据会让这件事更明显:没有 calibration_ref,平台只能输出 UNKNOWN,而不是假装完成了外参校验。
参考
- MolmoAct2 数据集:allenai/MolmoAct2-BimanualYAM-Dataset
- UMI 数据集:lerobot/umi_cup_in_the_wild
- LeRobot v3.0 数据格式:LeRobotDataset v3.0
- UMI 项目页:Universal Manipulation Interface
- UMI 论文:arXiv:2402.10329
- 站内前置阅读:机器人视频质检专题