MLeggiero/g1-inspire-turn-page-twist2
G1 + Inspire — "turn the page of the notebook" (TWIST2 high-level) Teleoperated Unitree G1 (29-DoF) + Inspire RH56DFTP hands manipulation data, converted to LeRobot v3.0 with the TWIST2 converter (deploy_real/convert_twist2_to_lerobot.py, --action_mode high_level). A left-handed, thin-deformable manipulation task: the robot slides a single notebook page off the stack and flips it over. Same recorder, same schema and the same 48/49-dim vector layout as… See the full description on the dataset page: https://huggingface.co/datasets/MLeggiero/g1-inspire-turn-page-twist2.
G1 + Inspire — "turn the page of the notebook" (TWIST2 high-level)
Teleoperated Unitree G1 (29-DoF) + Inspire RH56DFTP hands manipulation data, converted to LeRobot v3.0 with the TWIST2 converter (deploy_real/convert_twist2_to_lerobot.py, --action_mode high_level).
A left-handed, thin-deformable manipulation task: the robot slides a single notebook page off the stack and flips it over. Same recorder, same schema and the same 48/49-dim vector layout as `MLeggiero/g1-inspire-nut-in-box-twist2`, so the two can be concatenated — but this capture is the mirror-image case in two ways: the left hand does the work, and left-hand tactile is the live, rich channel (316/1062 taxels fire, peak 1957) rather than the near-silent one.
This capture carries one camera, the onboard head view. Two wrist-camera directories exist in the source recording and were deliberately not carried — see Cameras.
Read Known limitations before training on this — hand proprioception is dead and the tactile arrays need a byte swap.
Summary
- Task (1):
turn the page of the notebook - Episodes / frames: 38 / 20,990 @ 60 fps (5.8 min)
- Sessions: 1 capture session on 2026-08-28 (
20260828_1246); 4 of the 42 source episodes were dropped (see Provenance) - Cameras (1):
observation.images.head_rgb(1280×720, AV1, 60 fps) - observation.state: 48-dim · action: 49-dim · observation.effort: 29-dim · observation.force: 12-dim · observation.tactile.{left,right}: 1062-dim each
- Episode length: min 327, median 485, mean 552, max 1755 frames
The task
A white notebook sits on a cork-topped bench. The robot reaches in with its left arm, sets the flat of its hand and fingers on the top page, drags the page sideways until an edge lifts clear of the stack, and carries it over to the far side. The page is thin and deformable, so the useful signal is the contact pattern — where and how hard the hand presses — far more than the hand's joint configuration. Every source episode was labelled successful.
The right arm is present but idle and carries no task signal: mean per-episode joint std is 0.20–0.39 rad on the left arm versus 0.02–0.08 rad on the right.
Data fields & vector index
The 29 body joints appear everywhere in the 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 are hip_pitch, hip_roll, hip_yaw, knee, ankle_pitch, ankle_roll; waist is yaw, roll, pitch; arms are shoulder_pitch, shoulder_roll, shoulder_yaw, elbow, wrist_roll, wrist_pitch, wrist_yaw.
observation.state (float32[48]) — proprioception
0:34 is the recorder's state_body; the tail is state_hand_left, state_hand_right, state_neck concatenated in that order.
action (float32[49]) — high-level teleop target
0:35 is the recorder's action_body. These are the high-level teleop targets sent to the whole-body controller, not the RL policy's motor commands — the low-level 29-dim action_low_level stream is in the source recording but is not carried here (re-run the converter with --action_mode low_level for that).
action[0:29] is the commanded target for observation.state[0:29] at the next control step. The tails are not a delayed-observation pair: state[29:34] is measured IMU, action[29:35] is commanded root motion.
observation.effort (float32[29]) — per-joint torque
Estimated joint torque (tau_est) in N·m, same canonical 29-joint order as state[0:29]. This is the motor's internal estimate, not an external torque sensor — treat it as proportional to joint load rather than a calibrated value. All 29 dims are live in all 38 episodes; observed magnitude peaks at 48 N·m.
observation.force (float32[12]) — hand motor current
Inspire hand motor current per digit, force_hand_left (6) then force_hand_right (6), in the same finger order as the hand state: pinky, ring, middle, index, thumb_bend, thumb_rot. A load/current proxy, not a calibrated contact force. Mostly dead in this capture — see limitation 3.
observation.tactile.left / .right (int32[1062]) — raw Inspire taxels
The flat 1062-value RH56DFTP touch array per hand, un-pooled, stored as int32 (the source is uint16; int32 because parquet prefers signed integers). The stored values are byte-swapped — decode before use, see limitation 1.
Left is the working hand. After decoding, 316/1062 left taxels ever register against 91/1062 on the right, and left peak pressure is 1957 against 91 — roughly 3.5× the coverage and 20× the amplitude. Both arrays update at ~31–35 Hz (the recorder logs at 60 Hz, so readings repeat for 3–5 consecutive frames; this is sample-and-hold, not a stall). No episode has a static tactile array on either hand.
Region layout, in order, with grid shapes and dataset-wide coverage (taxels that ever fire / taxels in region) and peak decoded value:
The page-drag shows up on the left ring_pad, middle_nail/middle_pad and palm — the flat of the hand pressing the sheet — with the sharp peaks (1957 on ring_pad/middle_tip, 1346 on index_pad) at the moment the fingertip catches the page edge. little_tip is dead on both hands.
Other parquet columns
Cameras
One RGB stream, AV1 video at 60 fps:
The source recording also has rgb_wrist_left/ and rgb_wrist_right/ directories. Neither was carried, for different reasons:
rgb_wrist_right/never streamed. Every one of its 22,716 JPEGs across all 42 source episodes is the same byte-identical file (one md5). Despite the name, this camera was not on the right wrist at all — it was a fixed table mount on the left side of the workspace, and it froze on its first frame.rgb_wrist_left/did stream (~30 Hz), and despite the name it was on the right wrist — frame-to-frame image motion tracks right-arm joint velocity at r = 0.60–0.84 against 0.31–0.64 for the left arm. But the right arm is the idle one, and the lens pointed down and back at the floor and the robot's own torso rather than at the notebook, so it carries no view of the manipulation.
In short, the two wrist directories are mislabelled and uninformative for this task, so the dataset ships the head view only.
Loading
LeRobot v3.0 layout — needs lerobot >= 0.4 (built with 0.4.3).
from lerobot.datasets.lerobot_dataset import LeRobotDataset
ds = LeRobotDataset("MLeggiero/g1-inspire-turn-page-twist2")
f = ds[0]
f["observation.images.head_rgb"] # (3, 720, 1280)
f["observation.state"] # (48,)
f["action"] # (49,)
f["observation.effort"] # (29,) N·m
f["observation.force"] # (12,) hand motor current
f["observation.tactile.left"] # (1062,) int32, byte-swapped — see below
f["observation.tactile.right"] # (1062,) int32, byte-swappedDecoding tactile:
import numpy as np
def decode_taxels(v):
"""Undo the recorder's byte swap. Returns the true taxel counts."""
v = np.asarray(v, dtype=np.uint16)
return ((v & 0xFF) << 8) | (v >> 8)Reading the left hand's contact map as images:
REGIONS = { # name: (start, end, rows, cols)
"middle_nail": (379, 475, 12, 8),
"middle_pad": (475, 555, 10, 8),
"ring_pad": (290, 370, 10, 8),
"palm": (950, 1062, 8, 14),
}
t = decode_taxels(f["observation.tactile.left"])
maps = {k: t[a:b].reshape(r, c) for k, (a, b, r, c) in REGIONS.items()}Layout
meta/info.json LeRobot v3.0 descriptor (features, fps, paths)
meta/tasks.parquet the single task string
meta/episodes/chunk-000/*.parquet per-episode length, task, file/frame ranges, per-episode stats
meta/stats.json aggregated per-feature mean/std/min/max/q01…q99
data/chunk-000/file-000.parquet all 38 episodes
videos/observation.images.head_rgb/chunk-000/file-0NN.mp4 AV1v3.0 packs many episodes per file; use meta/episodes/ to map an episode to its file and frame range rather than assuming one file per episode.
Units & conventions
Body and neck angles in radians; hand values are raw Inspire RH56DFTP register units (0–1000, 1000 = open), not radians. root_height in meters, root velocities in m/s and rad/s in the robot-local frame. Effort in N·m. Hand force in raw motor-current units. Tactile in raw (byte-swapped) taxel counts.
Known limitations
Properties of the capture, not of the conversion. Stated plainly so you can decide what to train on.
- Tactile values are byte-swapped. Raw taxels are 12-bit per the recorder schema, but the stored uint16s are little/big-endian flipped: every value's low byte is ≤ 7 and the maximum stored value is 62,724. Decoding with
((v & 0xFF) << 8) | (v >> 8)brings the whole dataset into range (observed max 1957 left, 91 right). Carried through as-recorded rather than silently fixed; applydecode_taxelsabove. This matches the nut-in-box dataset's behaviour. - Hand proprioception is dead.
state[40:46](right hand) is constant within every one of the 38 episodes.state[34:40](left hand) is barely better: pinky is constant in 38/38, and the remaining five dims vary in only 7–28 of 38 episodes with negligible spread. The recorder's hand-state stream was not tracking during capture. Mask `state[34:46]` — and note the contrast with tactile, a separate sensor path that is healthy. - Hand force is mostly dead.
force_hand_rightis a nonzero constant in every frame of every episode.force_hand_leftvaries in 18/38 episodes but is zero in 556 of 125,940 cells overall. Carried for schema parity with the nut-in-box dataset; not a usable training signal here. - Action hand channel is nearly constant.
action[35:41](left hand): the four fingers are constant in all 38 episodes; onlythumb_bend(varies in 31/38) andthumb_rot(34/38) move, 34 distinct vectors dataset-wide.action[41:47](right hand) toggles its two thumb dims between exactly 2 distinct vectors across the whole dataset. The hand was held in a fixed page-dragging posture; the arm, not the fingers, does the work. - Neck state is frozen, neck action is not.
state[46:48]is 0 for every frame, whileaction[47:49]varies normally — the neck was commanded but never read back. - Single-arm task. Only the left arm manipulates. Right-arm state/action are present and vary slightly (postural), but carry no task signal.
- All 38 episodes are labelled successful. No negative examples.
- Body state, action, effort and vision are healthy. All 29
state[0:29]dims, all 5 IMU dims, all 6 root-command dims, all 29 body action dims, both neck action dims and all 29 effort dims vary in every episode. The usable learning signal is body joints + root command + left-hand tactile + effort + head vision. - Head video repeats frames. The head camera runs at ~30 Hz while the recorder logs at 60 Hz, so each frame appears about twice. Timestamps are still a true 60 Hz grid.
Not carried from the recording
- Both wrist cameras. One is a frozen single frame, the other is mislabelled and points away from the task — see Cameras.
- Depth. The recording has an empty
depth/directory and everydepthfield isNone— no depth was captured, so there is none to carry. - Low-level actions.
action_low_level(29-dim motor commands); use--action_mode low_level. - Per-frame recorder clocks.
t_img/t_action/t_state;timestamphere is derived asframe_index / 60.
Provenance
Single capture session, 20260828_1246. 4 of the 42 source episodes were dropped: episode_0026 (43 frames), episode_0027 (15) and episode_0037 (75) are sub-1.3-second stubs, and episode_0041's cameras froze for its last 1055 of 1593 frames. Remaining episodes map in order:
Reproduce with:
python deploy_real/convert_twist2_to_lerobot.py \
--data_dir "<recording root>/20260828_1246" \
--output_dir <out> \
--repo_id MLeggiero/g1-inspire-turn-page-twist2 \
--fps 60 --action_mode high_level --use_videos \
--hand_type inspire \
--include_tactile on --include_effort on --include_force \
--exclude_episodes episode_0026,episode_0027,episode_0037,episode_0041 \
--task_name "turn the page of the notebook"The recorder's own info.date is 2026-08-28; its text.goal field still holds the collection stack's placeholder (pick up the red cup) and was not used — the task string above was set explicitly at conversion time. Its info.image block reports width 720 / height 1280, which is transposed; the head frames are 1280×720.
Author: MLeggiero. Data collected on a Unitree G1 + Inspire RH56DFTP teleoperation rig, using the open-source TWIST2 collection stack from the YanjieZe repository (tooling credit only). Captured 2026-08-28. Released under the MIT License.
