---
title: "Sparrow Data Platform：从 DDL 到真实具身数据质检"
description: "用 MolmoAct2 与 UMI 两份真实数据跑通 DDL、版本化对象路径、Airflow + DockerOperator 质检，并记录被删的列、已知短板和踩过的契约坑。"
---

> Documentation Index
> Fetch the complete documentation index at: https://petwan.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Sparrow Data Platform：从 DDL 到真实具身数据质检

> **一句话**：平台的价值不是"多了一张表"，而是让每条产物都能回答三个问题——哪个工具版本产出的、输入是哪一版、失败了还能不能复现。
>
> **前置阅读**：机器人视频质检专题的[专题首页](/robot-video-qc/)、[帧级检测器](/robot-video-qc/frame-detectors/)、[跨模态配对](/robot-video-qc/camera-pairing/)。本文不重复检测器实现，讲怎么把这些检查装进一条可追溯的平台链路。

单条视频的质检脚本和平台之间，差的不是 UI，而是**责任边界**。脚本可以只输出一个 `quality_summary.json`；平台必须能回答：这份 JSON 是哪个版本的检测器、在哪台机器、对哪一版输入跑出来的，失败了以后还能不能复现。

下面按搭建顺序拆：DDL 怎么收敛、数据怎么进来、质检怎么跑、两类真实数据暴露了什么差异、踩了哪些坑、下一步缺什么。

## 1. 为什么需要这个平台

先把问题说具体。没有平台时，常见的状态是：

- 检测器代码在 git 里，但运行记录只在某次终端的滚动输出里；
- 三条 episode 的产物都叫 `quality_summary.json`，散在不同目录，重跑一次就覆盖；
- 同一个 `v1.0.0` 可能对应两个不同的镜像，因为镜像被重新构建过；
- 某个 episode 被判 `FAIL`，三个月后无法确定是数据坏了，还是当时阈值配错了。

这四件事指向同一个需求：**工具身份、输入身份、产物位置、运行结果**必须被结构化地记录下来。平台不是把文件换个地方存，而是把"这次判断"变成一条可复查的事实。

具体到机器人视频质检，平台至少要接住三样东西：

1. 一条 episode 的多路视频与低维数据；
2. 一次质检运行的配置、工具版本和输出；
3. 这条 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. 数据怎么进来：路径由平台签发

对象路径固定成：

```text
datasets/{dataset}/{tool}/{version}/{run_id}/{artifact}
```

实例：

```text
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 里按逻辑键取产物：

```text
KeyError: episode_spec
```

原因很简单：DAG 的契约期望 `output_key` 是**逻辑键**（`episode_spec`），扩展名由产物类型决定，不该出现在键里。文档没有把这件事写成硬约束，但代码按硬约束执行了。

第二个坑更贵：Episode / Artifact 是**只追加、不可删除**的。修好 key 之后，那两条失败记录无法清理，留下：

- 两条 `FAILED` Run；
- 一个废弃 Episode；
- 只能用 `/acceptance` 后缀重新发布一条新的验收记录。

这两件事的共同教训是：**契约先于便利**。

- 配置里的 key 不是文件名，不能让"看起来像个文件"的写法泄漏进逻辑层；
- 追加模型下，错误不会被删除，只会被覆盖或追加。失败记录是审计证据，但也是需要治理的债：要有 supersede / 迁移策略，否则 `FAILED` 会永久混在有效记录里。

如果重来一次，更合理的做法是在 schema 校验阶段就拒绝带扩展名的 `output_key`，而不是等 DAG 在运行时抛 `KeyError`。

## 7. 下一步：URDF → 投影 → YOLO，还缺三样

当前 QC 能判断"画面和时间轴是否可信"，但还判断不了"像素和三维空间是否对得上"。要把链路走到 URDF → UMI 相机投影 → YOLO 训练，缺的不是模型，而是三样基础件：

1. **标定制品**。URDF、相机内参、外参、畸变参数，需要版本化存储，并带 hash 与 provenance。没有版本化的标定，投影结果无法复现。
2. **工具契约里的 `calibration_ref` 与投影参数**。工具要能声明"我用了哪一版标定"，平台要能据此校验外参、复算投影。当前 schema 里一个相关字段都没有。
3. **标注侧与 label schema / 血缘绑定**。YOLO 的框不仅要绑定到图像，还要绑定到图像对应的 episode、相机、标定版本。否则模型指标下降时，无法区分是数据、标定还是标注的问题。

这三样缺任何一样，投影都只能是一次性的离线脚本，无法进入可追溯的质检链路。UMI 的单目无标定数据会让这件事更明显：没有 `calibration_ref`，平台只能输出 `UNKNOWN`，而不是假装完成了外参校验。

## 参考

- MolmoAct2 数据集：[allenai/MolmoAct2-BimanualYAM-Dataset](https://huggingface.co/datasets/allenai/MolmoAct2-BimanualYAM-Dataset)
- UMI 数据集：[lerobot/umi_cup_in_the_wild](https://huggingface.co/datasets/lerobot/umi_cup_in_the_wild)
- LeRobot v3.0 数据格式：[LeRobotDataset v3.0](https://huggingface.co/docs/lerobot/en/lerobot-dataset-v3)
- UMI 项目页：[Universal Manipulation Interface](https://umi-gripper.github.io/)
- UMI 论文：[arXiv:2402.10329](https://arxiv.org/abs/2402.10329)
- 站内前置阅读：[机器人视频质检专题](/robot-video-qc/)

Source: https://petwan.tech/dataplatform-build-and-real-qc/index.mdx
