Skip to content

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" 的 featurestats.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 / end 0-based、end-exclusive(Python slicing 语义;lerobot_episode_loader.py:329-330start_idx:end_idx[11]
  • 没有 video / annotation 时整块省略,不要写空对象。
  • video.<k>.original_key 必须出现在 info.json["features"](loader :427-429assert original_key in self.feature_config[12]),否则 GR00T 1.7 仍允许(:283-291 有按位置 auto-map fallback[13]),但行为不可预测,建议显式写。
  • annotation 块只在用户传 language modality 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.statefloat32 ndarray(D_state,)concat 后的 1-D state
actionfloat32 ndarray(D_action,)concat 后的 1-D action
timestampfloat32 scalar()自 episode start 的秒数
frame_indexint64 scalar()episode 内 0..T-1
episode_indexint64 scalar()episode id
indexint64 scalar()跨整个 dataset 的全局 frame index
task_indexint64 scalar()索引 tasks.jsonl

observation.state / action 单 frame 的值必须是 1-D np.ndarray,不能是 Python listlerobot_episode_loader.py:333isinstance(...) 决定走 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)


  1. 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/ 无匹配。 ↩︎

  2. Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:2"codebase_version": "v2.1"↩︎

  3. Isaac-GR00T/gr00t/data/stats.py:87-94(生成器 dict literal mean / std / min / max / q01 / q99)+ :104-112check_stats_validity() 显式 6 字段 loop)。详见 GR00T 1.7 Dataset 约束↩︎

  4. Isaac-GR00T/gr00t/data/stats.py:121-123for feature in le_features: if "float" in le_features[feature]["dtype"]: lowdim_features.append(feature),dtype 不含 "float" 的 feature 不进 stats。 ↩︎

  5. 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_datachunk_idx = episode_index // self.chunk_size (:360) 决定 chunk 号。 ↩︎

  6. IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:233"total_chunks": 0(常量)。 ↩︎

  7. IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:127-128cv2.VideoWriter_fourcc(*"mp4v") + cv2.VideoWriter(...) 写盘。 ↩︎

  8. Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:58 (observation.images.wrist) 与 :81 (observation.images.front) 的 "video.codec": "av1"↩︎

  9. 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)。 ↩︎

  10. Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:303-342_extract_joint_groups(),根据 modality.jsonstart / end:333-336x[start_idx:end_idx] 切片。 ↩︎

  11. Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:329-330start_idx = group_info["start"]; end_idx = group_info["end"],后续 x[start_idx:end_idx] 即标准 Python slice(end-exclusive)。 ↩︎

  12. 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)。 ↩︎

  13. Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:280-295 的 video key mapping block::283needs_mapping,:284-289 assert 数量相等,:290-291zip(config_keys, meta_keys) 位置映射。 ↩︎

  14. 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"]↩︎

  15. 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。 ↩︎

  16. Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:333-336if isinstance(df[original_key].iloc[0], np.ndarray): ...else: ...,ndarray 走 slice、否则整列直传。 ↩︎

  17. cd Isaac-GR00T && grep -rn 'next\.done\|next\.reward' gr00t/gr00t/ 子树下无任何匹配;demo dataset 的 parquet 列里也没有这两列。 ↩︎

  18. Isaac-GR00T/demo_data/cube_to_bowl_5/meta/info.json:55-64:78-87 的视频 feature 子对象 key 是 "info"(不是 "video_info")。 ↩︎

  19. IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py:227"codebase_version": "v2.0"↩︎

  20. convert_dataset.py:233"total_chunks": 0↩︎

  21. convert_dataset.py:127-128↩︎

  22. convert_dataset.py:236"splits": {"train": "0:100"},与真实 episode 数无关。 ↩︎

  23. convert_dataset.py:231"total_tasks": 2,常量。 ↩︎