Files
Qizhi Chen 7a8642edfc ♻️ Refactor LIBERO converter onto generic pipeline (#105)
* docs: update libero2lerobot readme

Co-authored-by: codex <codex@openai.com>

* refactor libero2lerobot converter

Co-authored-by: codex <codex@openai.com>

---------

Co-authored-by: codex <codex@openai.com>
2026-06-12 19:19:16 -07:00
..

LIBERO to LeRobot

LIBERO consists of 4 task suites and 130 tasks for studying LLDM. Specifically, the tasks in 3 of the 4 task suites vary only in one type of knowledge, while the last task suite requires transfer of entangled knowledge. (Copied from docs)

🚀 What's New in This Script

In this dataset, we have made several key improvements:

  • OpenVLA-based LIBERO Regeneration: Resolution enhancement, No-op action filtration, 180° RGB frame rotation, Failed trajectory filtering.
  • State Data Preservation: Maintained native LIBERO state information (accessible via states.ee_state, states.joint_state and etc.).
  • Robust Conversion Pipeline: Using the shared generic_converter pipeline with local and Ray DataTrove executors for high-speed dataset transformation and resumable conversion.

Dataset Structure of meta/info.json:

{
  "codebase_version": "v3.0", // latest lerobot format
  "robot_type": "franka", // specific robot type
  "fps": 20, // control frequency
  "features": {
    "observation.images.image": {
        "dtype": "video",
        "shape": [
            256,
            256,
            3
        ],
        "names": [
            "height",
            "width",
            "rgb"
        ],
        "info": {
            "video.height": 256,
            "video.width": 256,
            "video.codec": "av1",
            "video.pix_fmt": "yuv420p",
            "video.is_depth_map": false,
            "video.fps": 20,
            "video.channels": 3,
            "has_audio": false
        }
    },
    "observation.images.wrist_image": {
        "dtype": "video",
        "shape": [
            256,
            256,
            3
        ],
        "names": [
            "height",
            "width",
            "rgb"
        ],
        "info": {
            "video.height": 256,
            "video.width": 256,
            "video.codec": "av1",
            "video.pix_fmt": "yuv420p",
            "video.is_depth_map": false,
            "video.fps": 20,
            "video.channels": 3,
            "has_audio": false
        }
    },
    // for more state keys, see LiberoAdapter.features in libero_h5.py
    "observation.state": {
        "dtype": "float32",
        "shape": [
            8
        ],
        "names": {
            "motors": [
                "x",
                "y",
                "z",
                "axis_angle1",
                "axis_angle2",
                "axis_angle3",
                "gripper",
                "gripper"
            ]
        }
    },
    ...
    "action": {
        "dtype": "float32",
        "shape": [
            7
        ],
        "names": {
            "motors": [
                "x",
                "y",
                "z",
                "axis_angle1",
                "axis_angle2",
                "axis_angle3",
                "gripper"
            ]
        }
    },
    ...
  }
}

Installation

  1. Install LeRobot:
    Follow instructions in official repo.

  2. Install others:
    We use DataTrove for conversion. Install the Ray extra if you want distributed execution across multiple cores or nodes.

    pip install h5py
    pip install -U datatrove
    pip install -U "datatrove[ray]" # optional, for --executor ray
    

Get started

Note

This script supports converting LIBERO-style HDF5 directories to LeRobot. If you want to convert from RLDS to LeRobot, check openx2lerobot.

Download source code:

git clone https://github.com/Tavish9/any4lerobot.git
cd any4lerobot/libero2lerobot

Regenerate LIBERO Trajectory:

  1. Install LIBERO dependency
  2. Replace libero_90 with your target libero dataset.
  3. The converter feature schema expects 256x256x3 RGB observations. If your source HDF5 files are the original 128x128 LIBERO files, regenerate them first with --resolution 256, or update the image feature shapes in libero_h5.py to match your data.
python regenerate_libero_dataset.py \
    --resolution 256 \
    --libero_task_suite libero_90 \
    --libero_raw_data_dir /path/to/libero/datasets/libero_90 \
    --libero_target_dir /path/to/libero/datasets/libero_90_no_noops

Modify in convert.sh:

  1. --src-paths accepts one or more directories containing *.hdf5 LIBERO task files. To merge many suites into one LeRobot dataset, specify all source directories, for example --src-paths /path/libero_10 /path/libero_90.
  2. --output-path is the final aggregated LeRobot dataset root. Temporary per-task datasets are written next to it under <output-name>_temp and removed after aggregation.
  3. If you have installed datatrove[ray], use --executor ray for faster conversion. Increase --workers, --tasks-per-job, and --cpus-per-task if you have enough CPU and memory.
  4. To resume a previous conversion, pass the existing DataTrove log directory with --resume-dir /path/to/logs/....
  5. Use --debug for a small local smoke test. It converts only the first two tasks, forces local execution, and disables Hub upload.
  6. Use --repo-id <namespace/name> together with --push-to-hub to upload the aggregated dataset. Without --push-to-hub, --repo-id only controls the local aggregate repo id.
python libero_h5.py \
    --src-paths /path/to/libero/datasets/libero_90_no_noops \
    --output-path /path/to/local/libero_90_lerobot \
    --executor local \
    --tasks-per-job 3 \
    --workers 10

Execute the script:

For single node

bash convert.sh

For multi nodes (Install ray first)

Direct Access to Nodes (2 nodes in example)

On Node 1:

ray start --head --port=6379

On Node 2:

ray start --address='node_1_ip:6379'

On either Node, check the ray cluster status, and start the script

ray status
bash convert.sh

Slurm-managed System

#!/bin/bash
#SBATCH --job-name=ray-cluster
#SBATCH --ntasks=2
#SBATCH --nodes=2
#SBATCH --partition=partition

# Getting the node names
nodes=$(scontrol show hostnames "$SLURM_JOB_NODELIST")
nodes_array=($nodes)

head_node=${nodes_array[0]}
head_node_ip=$(srun --nodes=1 --ntasks=1 -w "$head_node" hostname --ip-address)

# if we detect a space character in the head node IP, we'll
# convert it to an ipv4 address. This step is optional.
if [[ "$head_node_ip" == *" "* ]]; then
IFS=' ' read -ra ADDR <<<"$head_node_ip"
if [[ ${#ADDR[0]} -gt 16 ]]; then
  head_node_ip=${ADDR[1]}
else
  head_node_ip=${ADDR[0]}
fi
echo "IPV6 address detected. We split the IPV4 address as $head_node_ip"
fi

port=6379
ip_head=$head_node_ip:$port
export ip_head
echo "IP Head: $ip_head"

echo "Starting HEAD at $head_node"
srun --nodes=1 --ntasks=1 -w "$head_node" \
    ray start --head \
    --node-ip-address="$head_node_ip" \
    --port=$port \
    --block &

sleep 10

# number of nodes other than the head node
worker_num=$((SLURM_JOB_NUM_NODES - 1))

for ((i = 1; i <= worker_num; i++)); do
    node_i=${nodes_array[$i]}
    echo "Starting WORKER $i at $node_i"
    srun --nodes=1 --ntasks=1 -w "$node_i" \
        ray start \
        --address "$ip_head" \
        --block &
    sleep 5
done

sleep 10

bash convert.sh

Other Community Supported Cluster Managers

See the doc for more details.