CoolFace
Datasetpublic

birbirll/g1-inspire-turn-page-v21

g1-inspire-turn-page-v21 A LeRobot v2.1 (per-episode) conversion of the public MLeggiero/g1-inspire-turn-page-twist2, which is LeRobot v3.0. Nothing was added, removed or resampled: same 38 episodes, same 20,990 frames, same numbers. Only the file layout, the video codec and the column split differ. Teleoperated Unitree G1 (29-DoF) + Inspire RH56DFTP hands, task turn the page of the notebook. 38 episodes / 20,990 frames @ 60 fps (5.8 min), one head camera at 1280×720, the left… See the full description on the dataset page: https://huggingface.co/datasets/birbirll/g1-inspire-turn-page-v21.

sourceHugging Facemitupdated 17d agoView on Hugging Face
0likes299downloads
Dataset Card

g1-inspire-turn-page-v21

A LeRobot v2.1 (per-episode) conversion of the public `MLeggiero/g1-inspire-turn-page-twist2`, which is LeRobot v3.0. Nothing was added, removed or resampled: same 38 episodes, same 20,990 frames, same numbers. Only the file layout, the video codec and the column split differ.

Teleoperated Unitree G1 (29-DoF) + Inspire RH56DFTP hands, task turn the page of the notebook. 38 episodes / 20,990 frames @ 60 fps (5.8 min), one head camera at 1280×720, the left hand does the work.

Three policies were fine-tuned on exactly this copy:

modelstackstepsfinal loss
`birbirll/g1-inspire-turn-page-n16`NVIDIA Isaac-GR00T N1.610,0000.040
`birbirll/g1-inspire-turn-page-starvla-n1d7`starVLA GR00T-N1.7 (CosmosGR00TN1d7)10,0000.0146
`birbirll/g1-inspire-turn-page-pi05`openpi π0.520,0000.0402

Why this conversion exists

Three concrete blockers stopped the v3.0 source from loading in our stacks:

  1. 1.Layout. v3.0 consolidates every episode into a few large files (data/chunk-000/file-000.parquet, meta/episodes/chunk-000/file-000.parquet, meta/tasks.parquet, and videos with several episodes concatenated per mp4). The GR00T LeRobotSingleDataset/LeRobotEpisodeLoader, the starVLA fork (pinned to lerobot_version v2.0) and openpi's LeRobot path all expect one parquet and one mp4 per episode.
  2. 2.AV1 video. The source videos are AV1. cv2/decord could not decode them on our machines (system ffmpeg with libdav1d can), so training would have failed at the first frame.
  3. 3.No `modality.json`. GR00T-family loaders need the state/action vectors split into named groups.

What changed, exactly

source (v3.0)here (v2.1)
parquet1 file, all episodesdata/chunk-000/episode_%06d.parquet, 38 files
video7 mp4s, episodes concatenated, AV1videos/chunk-000/observation.images.head_rgb/episode_%06d.mp4, 38 files, H.264 crf 18
episode metameta/episodes/chunk-000/file-000.parquetmeta/episodes.jsonl (+ meta/episodes_stats.jsonl)
tasksmeta/tasks.parquetmeta/tasks.jsonl
vectorsflat observation.state[48], action[49]split into modality columns (below)
modality map—meta/modality.json
size1.2 GB962 MB

Frame counts, episode boundaries and every numeric channel are byte-identical to the source (the converter asserts this: it re-reads each written parquet and compares the passthrough columns).

Layout

data/chunk-000/episode_000000.parquet … episode_000037.parquet
videos/chunk-000/observation.images.head_rgb/episode_000000.mp4 … episode_000037.mp4
meta/info.json  meta/episodes.jsonl  meta/episodes_stats.jsonl
meta/tasks.jsonl  meta/modality.json  meta/stats.json  meta/source_provenance.json

Columns

The recorder's two flat vectors are split so that each named group is addressable:

columnshapecontents
observation.state34dof_pos 29 body joints + imu_ang_vel 3 + imu_rp 2
observation.state.hand_left / .hand_right6 eachInspire registers 0–1000 (dead in this capture)
observation.state.neck2yaw, pitch (constant 0)
action35dof_pos_target 29 + root_lin_vel_xy 2 + root_height 1 + root_rp 2 + root_yaw_rate 1
action.hand_left / .hand_right6 eachInspire registers (only the two thumb dims move)
action.neck2yaw, pitch
observation.effort29per-joint torque estimate, N·m
observation.force12Inspire motor current, 6 per hand
observation.tactile.left / .right1062 eachraw taxels, byte-swapped (decode below)

The 29 body joints are in canonical TWIST2 order: left_leg 0–5, right_leg 6–11, waist 12–14, left_arm 15–21, right_arm 22–28. Per limb: legs hip_pitch, hip_roll, hip_yaw, knee, ankle_pitch, ankle_roll; waist yaw, roll, pitch; arms shoulder_pitch, shoulder_roll, shoulder_yaw, elbow, wrist_roll, wrist_pitch, wrist_yaw. Hands (both): pinky, ring, middle, index, thumb_bend, thumb_rot.

The split from the source's flat vectors is index-for-index:

state[48] -> observation.state          = state[0:34]     (body 29 + imu 5)
             observation.state.hand_left  = state[34:40]
             observation.state.hand_right = state[40:46]
             observation.state.neck       = state[46:48]

action[49] -> action                    = action[0:35]    (body 29 + root 6)
              action.hand_left          = action[35:41]
              action.hand_right         = action[41:47]
              action.neck               = action[47:49]

meta/modality.json names the groups the GR00T-family loaders read:

json
{"state":  {"left_leg":[0,6], "right_leg":[6,12], "waist":[12,15], "left_arm":[15,22],
            "right_arm":[22,29], "imu_ang_vel":[29,32], "imu_rp":[32,34],
            "hand_left":[0,6] -> observation.state.hand_left,
            "hand_right":[0,6] -> observation.state.hand_right},
 "action": {"left_leg":[0,6], "right_leg":[6,12], "waist":[12,15], "left_arm":[15,22],
            "right_arm":[22,29], "root_lin_vel_xy":[29,31], "root_height":[31,32],
            "root_rp":[32,34], "root_yaw_rate":[34,35],
            "hand_left":[0,6] -> action.hand_left,
            "hand_right":[0,6] -> action.hand_right,
            "neck":[0,2] -> action.neck},
 "video":  {"head": -> observation.images.head_rgb}}

(written with real start/end integers and original_key strings; shown compressed here.)

Reproducing the conversion

The converter is a script in a private repository, so here is precisely what it does. Any equivalent implementation reproduces this dataset from the public source.

  1. 1.Pull the source: huggingface-cli download MLeggiero/g1-inspire-turn-page-twist2 --repo-type dataset --local-dir <SRC>.
  2. 2.Read <SRC>/meta/info.json (features, fps), meta/episodes/chunk-000/file-000.parquet (per-episode length, dataset_from_index/dataset_to_index, and per-stream from_timestamp/to_timestamp), and meta/tasks.parquet. Note: newer LeRobot writes tasks.parquet with the task string as the index and a single task_index column — not a task column. Handle both.
  3. 3.De-consolidate the parquet: slice rows [dataset_from_index, dataset_to_index) per episode, split the flat vectors into the columns above, rewrite episode_index/index for the new numbering, and write data/chunk-000/episode_%06d.parquet.
  4. 4.Slice + re-encode the video, frame-exact: seek to (start_frame - 0.5) / fps and take exactly length frames with -frames:v, encoding -c:v libx264 -crf 18 -preset fast -pix_fmt yuv420p. Seeking to the raw from_timestamp is not safe: it is a rounded decimal (e.g. 27.616667 for frame 1657 at 60 fps) that lands after the target frame's presentation time, so ffmpeg silently starts one frame late. The half-frame-earlier seek lands unambiguously in the gap before it. (Validated by pixel-comparing output frame 0 against source frames n−3…n+3: MSE 5.1 for the intended frame versus 1034+ for its neighbours.)
  5. 5.Write the meta files: info.json with codebase_version: v2.1 and the per-episode data_path/video_path templates, episodes.jsonl, tasks.jsonl, modality.json, and stats.json recomputed over the frames actually written.
  6. 6.Add `episodes_stats.jsonl` (openpi's LeRobot loader requires it for a v2.1 dataset): run the official LeRobot v2.0→v2.1 stats conversion, which computes per-episode statistics including sampled video frames.

Verify with the loader you intend to train with. For GR00T:

python
from gr00t.data.dataset.lerobot_episode_loader import LeRobotEpisodeLoader
ds = LeRobotEpisodeLoader(dataset_path="<DST>", modality_configs=cfg, video_backend="torchcodec")
# 38 episodes, lengths equal to meta/episodes.jsonl, one video group "head"

Read this before training

  • —Hand proprioception is dead. observation.state.hand_left/right are constant within every episode (recorder readback failure). Drop them; do not feed them.
  • —Only two hand dimensions move. In action.hand_left, four of the six registers sit at 1000 (open) for the entire dataset; only thumb_bend and thumb_rot vary. Under min/max normalisation the constant dims are masked to 0, which is the safe behaviour; under a percentile normalisation they are not defined.
  • —The neck is constant at 0, and observation.force is mostly dead.
  • —Tactile is byte-swapped exactly as the recorder wrote it. Decode before use: v = np.asarray(v, np.uint16); ((v & 0xFF) << 8) | (v >> 8) → 0–4095. The left hand is the live one (316/1062 taxels ever fire, peak 1957, against 91/1062 and peak 91 on the right).
  • —The right arm is idle: per-episode joint std 0.02–0.08 rad against 0.20–0.39 on the left.
  • —One camera. The source recording had two wrist cameras that were deliberately not carried.
  • —Success labels are the recorder's. All 38 episodes are labelled successful and we did not independently review them. On a sibling capture from the same rig, an equivalent claim held for only 29 of 41 episodes once reviewed frame by frame. Treat the labels as unverified.
  • —Episode lengths vary a lot: 327 to 1755 frames, median 485.

Provenance and credit

The recording, the task and every number are MLeggiero's, captured 2026-08-28 with the TWIST2 converter (--action_mode high_level) and published as MLeggiero/g1-inspire-turn-page-twist2 under MIT. This repository is a format conversion of it, made 2026-09-09, and carries the same licence. meta/source_provenance.json records the source repository and commit this copy was built from.