vla-sft-code-dreamzero / scripts /data /convert_lerobot_to_gear.py
poet70's picture
Upload folder using huggingface_hub
ac29381 verified
Raw
History Blame Contribute Delete
24.9 kB
"""
Convert a standard LeRobot v2 dataset to the GEAR/DreamZero training format.
This script takes a dataset collected with LeRobot v2 and generates/augments the
metadata files required by DreamZero's training pipeline:
- meta/modality.json (state/action/video/annotation key mapping)
- meta/embodiment.json (embodiment tag for the training pipeline)
- meta/stats.json (dataset-level statistics: mean, std, min, max, q01, q99)
- meta/relative_stats_dreamzero.json (relative action statistics)
- meta/tasks.jsonl (task descriptions)
- meta/episodes.jsonl (episode-level metadata)
The script does NOT modify parquet files or videos -- it only creates metadata.
Usage:
# Auto-detect state/action structure, default embodiment tag 'xdof':
python scripts/data/convert_lerobot_to_gear.py --dataset-path ./Dataset/my_robot_data
# Explicit modality mapping via JSON:
python scripts/data/convert_lerobot_to_gear.py \\
--dataset-path ./Dataset/my_robot_data \\
--embodiment-tag xdof \\
--state-keys '{"joint_pos": [0, 6], "gripper_pos": [6, 7]}' \\
--action-keys '{"joint_pos": [0, 6], "gripper_pos": [6, 7]}' \\
--relative-action-keys joint_pos \\
--task-key annotation.task
# Copy to a new output directory instead of modifying in-place:
python scripts/data/convert_lerobot_to_gear.py \\
--dataset-path ./Dataset/my_robot_data \\
--output-path ./Dataset/my_robot_data_gear
"""
from __future__ import annotations
import argparse
import json
import logging
import shutil
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from tqdm import tqdm
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger(__name__)
VALID_EMBODIMENT_TAGS = [
"real_gr1_arms_only", "real_gr1_arms_only_annotated",
"real_gr1_arms_waist", "real_gr1_arms_waist_annotated",
"dexmg_gr1_arms_only_inspire", "dexmg_gr1_arms_only_fourier",
"dexmg_gr1_arms_waist_fourier",
"robocasa_single_arm", "onex_eve_gripper",
"robocasa_gr1_arms_only_inspire_hands", "robocasa_gr1_arms_only_fourier_hands",
"robocasa_gr1_fixed_lower_body_inspire_hands", "robocasa_gr1_fixed_lower_body_fourier_hands",
"robocasa_panda_omron",
"robocasa_bimanual_panda_parallel_gripper", "robocasa_bimanual_panda_inspire_hand",
"oxe_droid", "oxe_fractal", "oxe_language_table", "oxe_bridge",
"real_panda_single_arm", "hot3d_hands_only",
"gr1_unified", "robocasa_gr1_arms_waist_fourier_hands",
"agibot", "lapa", "oxe_mutex", "oxe_roboset", "oxe_plex",
"dream", "yam", "xdof",
"gr1_unified_segmentation", "language_table_sim", "gr1_isaac",
"sim_behavior_r1_pro", "mecka_hands", "real_r1_pro_sharpa", "libero_sim",
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def load_info(dataset_path: Path) -> dict:
info_path = dataset_path / "meta" / "info.json"
if not info_path.exists():
log.error("meta/info.json not found at %s", info_path)
sys.exit(1)
with open(info_path) as f:
return json.load(f)
def get_parquet_paths(dataset_path: Path, info: dict) -> list[Path]:
pattern = info.get("data_path", "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet")
total_episodes = info["total_episodes"]
chunks_size = info.get("chunks_size", 1000)
paths = []
for ep_idx in range(total_episodes):
chunk_idx = ep_idx // chunks_size
p = dataset_path / pattern.format(episode_chunk=chunk_idx, episode_index=ep_idx)
if p.exists():
paths.append(p)
return sorted(paths)
def detect_features(info: dict) -> dict:
"""Return categorised feature names from info.json."""
features = info.get("features", {})
state_keys = [k for k in features if k.startswith("observation.state")]
action_keys = [k for k in features if k == "action" or k.startswith("action.")]
video_keys = [k for k in features if features[k].get("dtype") == "video"]
annotation_keys = [k for k in features if k.startswith("annotation")]
return {
"state": state_keys,
"action": action_keys,
"video": video_keys,
"annotation": annotation_keys,
"features": features,
}
def parse_key_mapping(raw: str | None) -> dict[str, list[int]] | None:
"""Parse a JSON string like '{"joint_pos": [0, 6], "gripper": [6, 7]}'."""
if raw is None:
return None
try:
mapping = json.loads(raw)
except json.JSONDecodeError as e:
log.error("Invalid JSON for key mapping: %s", e)
sys.exit(1)
for name, bounds in mapping.items():
if not isinstance(bounds, list) or len(bounds) != 2:
log.error("Each entry must be [start, end]. Got %s for '%s'", bounds, name)
sys.exit(1)
return mapping
# ---------------------------------------------------------------------------
# Modality JSON
# ---------------------------------------------------------------------------
def build_modality_json(
info: dict,
detected: dict,
state_mapping: dict[str, list[int]] | None,
action_mapping: dict[str, list[int]] | None,
task_key: str | None,
) -> dict:
"""Build the modality.json structure expected by GEAR/DreamZero."""
features = detected["features"]
modality: dict = {"state": {}, "action": {}, "video": {}, "annotation": {}}
# --- State ---
state_col = detected["state"][0] if detected["state"] else None
if state_col and state_mapping:
for name, (start, end) in state_mapping.items():
dtype = features[state_col].get("dtype", "float64")
modality["state"][name] = {
"original_key": state_col,
"start": start,
"end": end,
"rotation_type": None,
"absolute": True,
"dtype": dtype,
"range": None,
}
elif state_col:
shape = features[state_col].get("shape", [1])
dim = shape[0] if isinstance(shape, list) else shape
dtype = features[state_col].get("dtype", "float64")
modality["state"]["state"] = {
"original_key": state_col,
"start": 0,
"end": dim,
"rotation_type": None,
"absolute": True,
"dtype": dtype,
"range": None,
}
# --- Action ---
action_col = detected["action"][0] if detected["action"] else None
if action_col and action_mapping:
for name, (start, end) in action_mapping.items():
dtype = features[action_col].get("dtype", "float64")
modality["action"][name] = {
"original_key": action_col,
"start": start,
"end": end,
"rotation_type": None,
"absolute": True,
"dtype": dtype,
"range": None,
}
elif action_col:
shape = features[action_col].get("shape", [1])
dim = shape[0] if isinstance(shape, list) else shape
dtype = features[action_col].get("dtype", "float64")
modality["action"]["action"] = {
"original_key": action_col,
"start": 0,
"end": dim,
"rotation_type": None,
"absolute": True,
"dtype": dtype,
"range": None,
}
# --- Video ---
for vk in detected["video"]:
short_name = vk.replace("observation.images.", "")
modality["video"][short_name] = {"original_key": vk}
# --- Annotation ---
if task_key:
short = task_key.replace("annotation.", "")
modality["annotation"][short] = {"original_key": task_key}
else:
for ak in detected["annotation"]:
short = ak.replace("annotation.", "")
modality["annotation"][short] = {"original_key": ak}
return modality
# ---------------------------------------------------------------------------
# Stats computation
# ---------------------------------------------------------------------------
def compute_stats(parquet_paths: list[Path], columns: list[str]) -> dict:
"""Compute mean/std/min/max/q01/q99 for numeric columns across all episodes."""
all_data: dict[str, list] = {col: [] for col in columns}
for pp in tqdm(parquet_paths, desc="Computing stats"):
df = pd.read_parquet(pp)
for col in columns:
if col not in df.columns:
continue
arr = np.stack(df[col].values)
if arr.ndim == 1:
arr = arr.reshape(-1, 1)
all_data[col].append(arr)
stats = {}
for col in columns:
if not all_data[col]:
continue
data = np.concatenate(all_data[col], axis=0).astype(np.float64)
stats[col] = {
"mean": np.mean(data, axis=0).tolist(),
"std": np.std(data, axis=0).tolist(),
"min": np.min(data, axis=0).tolist(),
"max": np.max(data, axis=0).tolist(),
"q01": np.quantile(data, 0.01, axis=0).tolist(),
"q99": np.quantile(data, 0.99, axis=0).tolist(),
}
return stats
def compute_relative_stats(
parquet_paths: list[Path],
modality: dict,
relative_action_keys: list[str],
action_horizon: int = 24,
) -> dict:
"""Compute relative-action statistics: (action - reference_state) for each key.
This replicates the logic in groot/vla/data/dataset/lerobot.py
_calculate_relative_stats_for_key.
"""
stats: dict = {}
for rel_key in relative_action_keys:
if rel_key not in modality["action"]:
log.warning("Relative action key '%s' not found in action modality, skipping", rel_key)
continue
if rel_key not in modality["state"]:
log.warning(
"Relative action key '%s' has no matching state key -- "
"relative stats require a corresponding state key with the same name. Skipping.",
rel_key,
)
continue
action_meta = modality["action"][rel_key]
state_meta = modality["state"][rel_key]
all_relative = []
for pp in tqdm(parquet_paths, desc=f"Relative stats [{rel_key}]"):
df = pd.read_parquet(pp)
action_col = action_meta["original_key"]
state_col = state_meta["original_key"]
if action_col not in df.columns or state_col not in df.columns:
continue
action_data = np.stack(df[action_col].values).astype(np.float64)
state_data = np.stack(df[state_col].values).astype(np.float64)
if action_data.ndim == 1:
action_data = action_data.reshape(-1, 1)
if state_data.ndim == 1:
state_data = state_data.reshape(-1, 1)
a_start, a_end = action_meta["start"], action_meta["end"]
s_start, s_end = state_meta["start"], state_meta["end"]
action_slice = action_data[:, a_start:a_end]
state_slice = state_data[:, s_start:s_end]
traj_len = len(df)
usable = traj_len - action_horizon
for i in range(max(usable, 0)):
ref_state = state_slice[i]
chunk_end = min(i + action_horizon, traj_len)
actions = action_slice[i:chunk_end]
relative = actions - ref_state
all_relative.extend(relative)
if not all_relative:
log.warning("No relative actions computed for '%s'", rel_key)
continue
data = np.array(all_relative)
stats[rel_key] = {
"max": np.max(data, axis=0).tolist(),
"min": np.min(data, axis=0).tolist(),
"mean": np.mean(data, axis=0).tolist(),
"std": np.std(data, axis=0).tolist(),
"q01": np.quantile(data, 0.01, axis=0).tolist(),
"q99": np.quantile(data, 0.99, axis=0).tolist(),
}
return stats
# ---------------------------------------------------------------------------
# Tasks & episodes
# ---------------------------------------------------------------------------
def build_tasks(parquet_paths: list[Path], task_key: str | None) -> list[dict]:
"""Build tasks.jsonl entries from the dataset."""
if task_key is None:
return [{"task_index": 0, "task": ""}]
task_set: dict[str, int] = {}
for pp in tqdm(parquet_paths, desc="Extracting tasks"):
df = pd.read_parquet(pp)
if task_key not in df.columns:
continue
for val in df[task_key].unique():
text = str(val) if not isinstance(val, str) else val
if text not in task_set:
task_set[text] = len(task_set)
if not task_set:
return [{"task_index": 0, "task": ""}]
return [{"task_index": idx, "task": text} for text, idx in sorted(task_set.items(), key=lambda x: x[1])]
def build_episodes(parquet_paths: list[Path], info: dict, task_key: str | None, tasks: list[dict]) -> list[dict]:
"""Build episodes.jsonl entries."""
task_text_to_idx = {t["task"]: t["task_index"] for t in tasks}
episodes = []
for ep_idx, pp in enumerate(tqdm(parquet_paths, desc="Building episodes")):
df = pd.read_parquet(pp)
length = len(df)
ep_tasks: list[str] = []
if task_key and task_key in df.columns:
unique_tasks = df[task_key].unique()
for t in unique_tasks:
text = str(t) if not isinstance(t, str) else t
if text and text in task_text_to_idx:
ep_tasks.append(text)
if not ep_tasks:
ep_tasks = [""]
episodes.append({
"episode_index": ep_idx,
"tasks": ep_tasks,
"length": length,
})
return episodes
# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------
def validate_dataset(dataset_path: Path, info: dict, modality: dict) -> list[str]:
"""Run basic validation and return a list of warnings."""
warnings = []
# Check required directories
for subdir in ["data", "videos", "meta"]:
if not (dataset_path / subdir).exists():
warnings.append(f"Missing directory: {subdir}/")
# Check at least one video key exists
if not modality["video"]:
warnings.append("No video features detected -- DreamZero requires at least one camera view")
# Check state/action exist
if not modality["state"]:
warnings.append("No state modality keys defined")
if not modality["action"]:
warnings.append("No action modality keys defined")
# Check total_episodes > 0
if info.get("total_episodes", 0) == 0:
warnings.append("total_episodes is 0 in info.json")
# Check FPS
if info.get("fps") is None:
warnings.append("fps not set in info.json")
return warnings
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Convert a LeRobot v2 dataset to GEAR/DreamZero training format.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--dataset-path", type=str, required=True, help="Path to the LeRobot v2 dataset")
parser.add_argument("--output-path", type=str, default=None, help="Output path (default: modify in-place)")
parser.add_argument(
"--embodiment-tag", type=str, default="xdof",
help=f"Embodiment tag (default: xdof). Valid: {', '.join(sorted(set(VALID_EMBODIMENT_TAGS)))}"
)
parser.add_argument(
"--state-keys", type=str, default=None,
help='JSON mapping of state sub-keys to [start, end] index ranges, '
'e.g. \'{"joint_pos": [0, 6], "gripper_pos": [6, 7]}\''
)
parser.add_argument(
"--action-keys", type=str, default=None,
help='JSON mapping of action sub-keys to [start, end] index ranges'
)
parser.add_argument(
"--relative-action-keys", type=str, nargs="*", default=None,
help="Action sub-key names to compute relative stats for (e.g. joint_pos gripper_pos). "
"Each key must also exist in --state-keys. If omitted, skips relative stats."
)
parser.add_argument("--task-key", type=str, default=None, help="Column name for language annotations (auto-detected if not set)")
parser.add_argument("--fps", type=float, default=None, help="Override FPS (default: use dataset FPS from info.json)")
parser.add_argument("--action-horizon", type=int, default=24, help="Action horizon for relative stats (default: 24)")
parser.add_argument("--force", action="store_true", help="Overwrite existing GEAR metadata files")
args = parser.parse_args()
dataset_path = Path(args.dataset_path).resolve()
if not dataset_path.exists():
log.error("Dataset path does not exist: %s", dataset_path)
sys.exit(1)
# Validate embodiment tag
if args.embodiment_tag not in VALID_EMBODIMENT_TAGS:
log.error(
"Invalid embodiment tag '%s'. Valid tags:\n %s",
args.embodiment_tag,
"\n ".join(sorted(set(VALID_EMBODIMENT_TAGS))),
)
sys.exit(1)
# Output path handling
if args.output_path:
output_path = Path(args.output_path).resolve()
if output_path != dataset_path:
log.info("Copying dataset to %s", output_path)
if output_path.exists():
if not args.force:
log.error("Output path already exists. Use --force to overwrite.")
sys.exit(1)
shutil.rmtree(output_path)
shutil.copytree(dataset_path, output_path)
dataset_path = output_path
else:
output_path = dataset_path
meta_dir = output_path / "meta"
meta_dir.mkdir(parents=True, exist_ok=True)
# 1. Load info.json
info = load_info(dataset_path)
detected = detect_features(info)
log.info("Dataset: %s", dataset_path.name)
log.info(" Episodes: %d", info.get("total_episodes", 0))
log.info(" FPS: %s", info.get("fps", "not set"))
log.info(" State columns: %s", detected["state"])
log.info(" Action columns: %s", detected["action"])
log.info(" Video features: %d camera(s)", len(detected["video"]))
log.info(" Annotation columns: %s", detected["annotation"])
if args.fps is not None:
info["fps"] = args.fps
with open(output_path / "meta" / "info.json", "w") as f:
json.dump(info, f, indent=4)
log.info(" Overriding FPS to %s", args.fps)
# Parse user-provided key mappings
state_mapping = parse_key_mapping(args.state_keys)
action_mapping = parse_key_mapping(args.action_keys)
# Auto-detect task key if not provided
task_key = args.task_key
if task_key is None and detected["annotation"]:
for candidate in ["annotation.task", "annotation.language.language_instruction"]:
if candidate in detected["annotation"]:
task_key = candidate
break
if task_key is None:
task_key = detected["annotation"][0]
log.info(" Auto-detected task key: %s", task_key)
# 2. Build modality.json
modality = build_modality_json(info, detected, state_mapping, action_mapping, task_key)
modality_path = meta_dir / "modality.json"
if modality_path.exists() and not args.force:
log.info(" modality.json already exists, skipping (use --force to overwrite)")
else:
with open(modality_path, "w") as f:
json.dump(modality, f, indent=4)
log.info(" Wrote modality.json (%d state keys, %d action keys, %d video keys)",
len(modality["state"]), len(modality["action"]), len(modality["video"]))
# 3. Write embodiment.json
embodiment = {"robot_type": args.embodiment_tag, "embodiment_tag": args.embodiment_tag}
embodiment_path = meta_dir / "embodiment.json"
if embodiment_path.exists() and not args.force:
log.info(" embodiment.json already exists, skipping")
else:
with open(embodiment_path, "w") as f:
json.dump(embodiment, f, indent=4)
log.info(" Wrote embodiment.json (tag=%s)", args.embodiment_tag)
# 4. Get parquet file paths
parquet_paths = get_parquet_paths(output_path, info)
if not parquet_paths:
log.error("No parquet files found. Check dataset structure.")
sys.exit(1)
log.info(" Found %d parquet files", len(parquet_paths))
# 5. Compute stats.json
stats_path = meta_dir / "stats.json"
numeric_cols = detected["state"] + detected["action"]
if "timestamp" in info.get("features", {}):
numeric_cols.append("timestamp")
if stats_path.exists() and not args.force:
log.info(" stats.json already exists, skipping")
else:
log.info(" Computing dataset statistics...")
stats = compute_stats(parquet_paths, numeric_cols)
with open(stats_path, "w") as f:
json.dump(stats, f, indent=4)
log.info(" Wrote stats.json (%d features)", len(stats))
# 6. Compute relative_stats_dreamzero.json
rel_stats_path = meta_dir / "relative_stats_dreamzero.json"
if args.relative_action_keys:
if rel_stats_path.exists() and not args.force:
log.info(" relative_stats_dreamzero.json already exists, skipping")
else:
log.info(" Computing relative action statistics for keys: %s", args.relative_action_keys)
rel_stats = compute_relative_stats(
parquet_paths, modality, args.relative_action_keys,
action_horizon=args.action_horizon,
)
if rel_stats:
with open(rel_stats_path, "w") as f:
json.dump(rel_stats, f, indent=4)
log.info(" Wrote relative_stats_dreamzero.json (%d keys)", len(rel_stats))
else:
log.warning(" No relative stats computed (check key names match between state and action)")
else:
log.info(" Skipping relative stats (no --relative-action-keys provided)")
# 7. Build tasks.jsonl
tasks_path = meta_dir / "tasks.jsonl"
if tasks_path.exists() and not args.force:
log.info(" tasks.jsonl already exists, skipping")
else:
tasks = build_tasks(parquet_paths, task_key)
with open(tasks_path, "w") as f:
for t in tasks:
f.write(json.dumps(t) + "\n")
log.info(" Wrote tasks.jsonl (%d tasks)", len(tasks))
# 8. Build episodes.jsonl
episodes_path = meta_dir / "episodes.jsonl"
if episodes_path.exists() and not args.force:
log.info(" episodes.jsonl already exists, skipping")
else:
tasks = []
if tasks_path.exists():
with open(tasks_path) as f:
for line in f:
tasks.append(json.loads(line.strip()))
if not tasks:
tasks = [{"task_index": 0, "task": ""}]
episodes = build_episodes(parquet_paths, info, task_key, tasks)
with open(episodes_path, "w") as f:
for ep in episodes:
f.write(json.dumps(ep) + "\n")
log.info(" Wrote episodes.jsonl (%d episodes)", len(episodes))
# 9. Validation
warnings = validate_dataset(output_path, info, modality)
if warnings:
log.warning("Validation warnings:")
for w in warnings:
log.warning(" - %s", w)
else:
log.info("Validation passed -- no warnings")
# Summary
print("\n" + "=" * 60)
print("Conversion complete!")
print(f" Output: {output_path}")
print(f" Embodiment tag: {args.embodiment_tag}")
print(f" State keys: {list(modality['state'].keys())}")
print(f" Action keys: {list(modality['action'].keys())}")
print(f" Video keys: {list(modality['video'].keys())}")
print(f" Task key: {task_key or '(none)'}")
if args.relative_action_keys:
print(f" Relative action keys: {args.relative_action_keys}")
print("=" * 60)
print("\nNext steps:")
print(" 1. Create a YAML data config in groot/vla/configs/data/dreamzero/")
print(" 2. Add modality configs to base_48_wan_fine_aug_relative.yaml")
print(" 3. Create a training script in scripts/train/")
print(" See docs/CUSTOM_EMBODIMENT_TRAINING.md for the full guide.")
if __name__ == "__main__":
main()