主题
LeRobot dataset Schema:L3 最终产物
L3 把 L2 的 HDF5 转成 GR00T N1.7 直接消费的 LeRobot dataset 目录。本文件讲产物 schema 的速查版,转换器实现细节见 l3/README.md。
本文件描述的是"GR00T N1.7 实际期望的混合 schema",不是 lerobot 库 v2.0 / v2.1 的纯 spec。 两者有出入时以 GR00T 1.7 为准——硬性约束的源码层 verify 见 GR00T 1.7 Dataset 约束。
通用 lerobot 库版本调研在 LeRobot 版本研究;为什么"GR00T 兼容 v2.0/v2.1"那条旧结论不再成立、改成"GR00T 1.7 不读 codebase_version + 强依赖全局 stats.json",见后者 §5。
文件布局
data/lerobot/<dataset_name>/
├── meta/
│ ├── info.json codebase_version="v2.1"(GR00T 1.7 不读,纯礼仪), fps, features, total_*
│ ├── episodes.jsonl 每行 {episode_index, tasks, length, ...自定义字段}
│ ├── tasks.jsonl 每行 {task_index, task}
│ ├── stats.json ★ 全局 stats,GR00T 1.7 hard assert 必存在(不是 v2.1 的 episodes_stats.jsonl)
│ └── modality.json ★ GR00T-specific:state/action 切片定义
├── data/chunk-000/episode_000000.parquet
├── data/chunk-000/episode_000001.parquet
└── videos/chunk-000/observation.images.<cam>/episode_000000.mp4关键事实
codebase_version = "v2.1"写就行——Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:147-200整个 metadata 加载流程不读这个字段[1]。NVIDIA 自家 demo (Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:2) 写的是"v2.1"[2],跟着写就好。- stats 走全局
meta/stats.json,schema 严格对齐 GR00T 自带的gr00t/data/stats.py:每个 float feature 含 6 个字段mean/std/min/max/q01/q99(带 q01/q99,不带 count)[3]。注意这跟 lerobot v2.1 的episodes_stats.jsonl字段对不上(后者是min/max/mean/std/count)——GR00T 1.7 是混合体,不是纯 v2.1。详见 GR00T 1.7 Dataset 约束 §2。 - stats 只写
info.json["features"]里 dtype 含"float"的 feature(stats.py:122[4]):本仓库 lift_cube 当前是observation.state/action/timestamp三个;int64/bool/video不写。 chunks_size = 1000:每个 chunk 最多 1000 episodes,超过新建chunk-001/[5]。total_chunks = ceil(total_episodes / chunks_size):必须真实计算,不能像 IsaacLab 自带convert_dataset.py那样硬编码 0[6]。- 视频编码
h264 + yuv420p:对齐 NVIDIA 公开 dataset,不用 IsaacLab 自带的mp4v[7]。(注意 demo dataset 用的是av1[8],但 av1 编码慢,对齐 h264 也能用,无关 GR00T loader 行为)。 episodes.jsonl可加自定义字段:lerobot_episode_loader.py:166-211只用length(其他字段不读但保留)[9]。本仓库写source / src_file / src_demo用于多来源合并;GR00T 不读,安全。
modality.json(最关键也最容易出错)
GR00T 强依赖此文件把 1-D concat 的 observation.state / action 切回语义字段。Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:303-342 是 _extract_joint_groups,它按下面的 start:end slice[10]。
最小结构(lift_cube 当前):
json
{
"state": {
"joint_pos": { "start": 0, "end": 9 },
"joint_vel": { "start": 9, "end": 18 },
"object_position": { "start": 18, "end": 21 },
"target_object_position": { "start": 21, "end": 28 },
"last_action": { "start": 28, "end": 36 }
},
"action": {
"ee_pose": { "start": 0, "end": 7 },
"gripper": { "start": 7, "end": 8 }
}
}加 cam / language 时扩:
json
{
...,
"video": {
"front_cam": {"original_key": "observation.images.front_cam"}
},
"annotation": {
"human.action.task_description": {"original_key": "task_index"}
}
}要点:
start/end0-based、end-exclusive(Python slicing 语义;lerobot_episode_loader.py:329-330的start_idx:end_idx)[11]。- 没有 video / annotation 时整块省略,不要写空对象。
video.<k>.original_key必须出现在info.json["features"](loader:427-429的assert original_key in self.feature_config[12]),否则 GR00T 1.7 仍允许(:283-291有按位置 auto-map fallback[13]),但行为不可预测,建议显式写。annotation块只在用户传languagemodality config 时被读(:369-382)[14];纯 task 字符串走tasks.jsonl + task_index不需要这块。
parquet 列
每个 episode 一个 parquet 文件,列对齐 Isaac-GR00T/demo_data/cube_to_bowl_5/data/chunk-000/episode_000000.parquet[15]:
| 列 | dtype | 单 frame 形状 | 含义 |
|---|---|---|---|
observation.state | float32 ndarray | (D_state,) | concat 后的 1-D state |
action | float32 ndarray | (D_action,) | concat 后的 1-D action |
timestamp | float32 scalar | () | 自 episode start 的秒数 |
frame_index | int64 scalar | () | episode 内 0..T-1 |
episode_index | int64 scalar | () | episode id |
index | int64 scalar | () | 跨整个 dataset 的全局 frame index |
task_index | int64 scalar | () | 索引 tasks.jsonl |
observation.state / action 单 frame 的值必须是 1-D np.ndarray,不能是 Python list(lerobot_episode_loader.py:333 的 isinstance(...) 决定走 vector slice 还是 scalar 直传)[16]。
不写的列:
next.done/next.reward:lerobot 库自己用,GR00T 1.7 不引用[17],demo dataset 也没有。多写一份 stats 反而占磁盘。- 任何用户自定义列:GR00T
_extract_joint_groups通过original_key找列,所以理论上可以加任意多observation.<x>列,但当前 modality.json 顶层只 state/action,多加的列只是被 stats 算进去再被忽略。
info.json features 模板
json
{
"observation.state": {
"dtype": "float32",
"shape": [STATE_DIM],
"names": ["joint_pos_0", "joint_pos_1", ..., "last_action_7"]
},
"action": {
"dtype": "float32",
"shape": [ACTION_DIM],
"names": ["ee_pose_x", ..., "gripper"]
},
"observation.images.front_cam": {
"dtype": "video",
"shape": [H, W, 3],
"names": ["height", "width", "channels"],
"info": {
"video.height": H,
"video.width": W,
"video.codec": "h264",
"video.pix_fmt": "yuv420p",
"video.is_depth_map": false,
"video.fps": 30,
"video.channels": 3,
"has_audio": false
}
},
"timestamp": {"dtype": "float32", "shape": [1], "names": null},
"frame_index": {"dtype": "int64", "shape": [1], "names": null},
"episode_index": {"dtype": "int64", "shape": [1], "names": null},
"index": {"dtype": "int64", "shape": [1], "names": null},
"task_index": {"dtype": "int64", "shape": [1], "names": null}
}注意 video feature 的子字段 key 是 "info" 不是 "video_info"(demo dataset 用的就是 "info"[18],跟 lerobot 库 v2 spec 命名差一个字)。
与 IsaacLab 自带转换器的差异
IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py 是参考实现,但本仓库故意修复它的几处偏离:
| IsaacLab convert_dataset.py | 本仓库 | 原因 |
|---|---|---|
codebase_version="v2.0" 硬编码[19] | "v2.1" | 对齐 GR00T 1.7 demo dataset |
total_chunks=0 硬编码[20] | 真实计算 | 符合 v2 规范 |
cv2.VideoWriter(*"mp4v")[21] | h264 + yuv420p | 对齐 NVIDIA 公开 dataset |
splits={"train":"0:100"} 硬编码[22] | 真实 episode 数 | 不写虚假 split |
total_tasks=2 硬编码[23] | 真实 task 数 | 防止 loader 警告 |
来源(footnote)
Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:147-200的_load_metadata()整段读 5 个 metadata 文件 + 6 个 info 字段(features/data_path/video_path/mask_path/chunks_size/fps),无任何codebase_version引用;grep -rn 'codebase_version' Isaac-GR00T/gr00t/无匹配。 ↩︎Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:2写"codebase_version": "v2.1"。 ↩︎Isaac-GR00T/gr00t/data/stats.py:87-94(生成器 dict literalmean / std / min / max / q01 / q99)+:104-112(check_stats_validity()显式 6 字段 loop)。详见 GR00T 1.7 Dataset 约束。 ↩︎Isaac-GR00T/gr00t/data/stats.py:121-123的for feature in le_features: if "float" in le_features[feature]["dtype"]: lowdim_features.append(feature),dtype 不含 "float" 的 feature 不进 stats。 ↩︎Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:7的"chunks_size": 1000;loader 读到self.chunk_size = self.info_meta["chunks_size"](lerobot_episode_loader.py:199),_load_parquet_data用chunk_idx = episode_index // self.chunk_size(:360) 决定 chunk 号。 ↩︎IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:233写"total_chunks": 0(常量)。 ↩︎IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:127-128用cv2.VideoWriter_fourcc(*"mp4v")+cv2.VideoWriter(...)写盘。 ↩︎Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:58(observation.images.wrist) 与:81(observation.images.front) 的"video.codec": "av1"。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:166-211含_load_metadata()中读episodes.jsonl(:166-168) 和get_episode_lengths()(:202-212),后者只引用ep_meta["length"](:211)。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:303-342的_extract_joint_groups(),根据modality.json的start / end在:333-336做x[start_idx:end_idx]切片。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:329-330的start_idx = group_info["start"]; end_idx = group_info["end"],后续x[start_idx:end_idx]即标准 Python slice(end-exclusive)。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:427-429在_load_video_data()中assert original_key in self.feature_config, f"Original key {original_key} not found in feature config"(注意:这是真正的硬 assert 位置;:280-291是 mapping fallback,不是 assert)。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:280-295的 video key mapping block::283算needs_mapping,:284-289assert 数量相等,:290-291按zip(config_keys, meta_keys)位置映射。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:369-382在_load_parquet_data()中被if "language" in self.modality_configs:(:369) 包住——只在用户传 language modality config 时才读modality.json["annotation"]。 ↩︎python -c "import pandas as pd; df=pd.read_parquet('Isaac-GR00T/demo_data/cube_to_bowl_5/data/chunk-000/episode_000000.parquet'); print(list(df.columns))"→['action', 'observation.state', 'timestamp', 'frame_index', 'episode_index', 'index', 'task_index'];action/observation.state单 cell 是np.ndarray shape=(6,) dtype=float32,后五列是 scalar。 ↩︎Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:333-336的if isinstance(df[original_key].iloc[0], np.ndarray): ...else: ...,ndarray 走 slice、否则整列直传。 ↩︎cd Isaac-GR00T && grep -rn 'next\.done\|next\.reward' gr00t/在gr00t/子树下无任何匹配;demo dataset 的 parquet 列里也没有这两列。 ↩︎Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:55-64与:78-87的视频 feature 子对象 key 是"info"(不是"video_info")。 ↩︎IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:227写"codebase_version": "v2.0"。 ↩︎见
convert_dataset.py:233的"total_chunks": 0。 ↩︎见
convert_dataset.py:127-128。 ↩︎convert_dataset.py:236写"splits": {"train": "0:100"},与真实 episode 数无关。 ↩︎convert_dataset.py:231写"total_tasks": 2,常量。 ↩︎