xvla-vanilla-backup / scripts /make_roboreal_meta.py
JackLiu0406's picture
Xuan vanilla X-VLA backup: full folder, intermediate ckpts thinned to every-20k + each run's final
eb23c20 verified
Raw
History Blame Contribute Delete
12 kB
#!/usr/bin/env python3
"""Generate X-VLA metadata for RoboReal HDF5 episodes.
Expected dataset layout:
<dataset-root>/
<task-name>/
data/
episode0.hdf5
...
Or:
<dataset-root>/
<task-name>/
<variant>/
data/
episode0.hdf5
...
Or:
<dataset-root>/
<domain>/
<task-name>/
<variant>/
data/
episode0.hdf5
...
Examples:
/shared_work/xuan/dataset/roboreal_data/study/move_cup/clean/data/episode0.hdf5
/shared_work/xuan/dataset/roboreal_data/office/move_items_around/office_d10/data/episode0.hdf5
The generated metadata aggregates all matching episode files into a single
X-VLA JSON so training can consume either a study subtree or the whole
RoboReal tree at once.
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from make_robotwin_meta import build_instruction_maps, pair_episodes_with_instructions
DEFAULT_OBSERVATION_KEYS = [
"observation/countertop_camera/rgb",
"observation/left_camera/rgb",
"observation/right_camera/rgb",
"observation/head_camera/rgb",
]
CAMERA_PRIORITY = ["countertop_camera", "left_camera", "right_camera", "head_camera"]
DEFAULT_IGNORED_DIRS = [".cache", ".git", "__pycache__"]
HDF5_SIGNATURE = b"\x89HDF\r\n\x1a\n"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate X-VLA metadata for RoboReal datasets.",
)
parser.add_argument(
"--dataset-root",
required=True,
help="Root RoboReal directory containing task/data, task/variant/data, or domain/task/variant/data leaves.",
)
parser.add_argument(
"--output",
required=True,
help="Path to the output metadata JSON.",
)
parser.add_argument(
"--dataset-name",
default="robotwin2_clean",
choices=["robotwin2_clean", "robotwin2_abs_ee"],
help="Dataset key registered in X-VLA.",
)
parser.add_argument(
"--glob",
default="*.hdf5",
help="Glob pattern for episode files inside each data directory.",
)
parser.add_argument(
"--observation-key",
nargs="+",
default=None,
help="Image dataset keys inside each HDF5 file. If omitted, infer from the first episode and prefer head/left/right cameras when present.",
)
parser.add_argument(
"--ignore-dir",
nargs="+",
default=DEFAULT_IGNORED_DIRS,
help="Directory names to skip anywhere under dataset root.",
)
parser.add_argument(
"--variant",
nargs="*",
default=None,
help="Optional allowlist of dataset variants to include, e.g. clean study_d6.",
)
instr = parser.add_argument_group("instruction sources")
instr.add_argument(
"--language-instruction-key",
help="HDF5 dataset key for the instruction.",
)
instr.add_argument(
"--default-instruction",
help="Single instruction shared by every episode.",
)
instr.add_argument(
"--instruction-map-json",
help="Pre-built JSON file mapping episode path/name/stem to instruction text.",
)
instr.add_argument(
"--instruction-split",
default="seen",
choices=["seen", "unseen"],
help="Which split to use from per-episode instruction JSONs (default: seen).",
)
return parser.parse_args()
def task_to_instruction(task_name: str) -> str:
base = task_name
for suffix in ("_clean", "_demo_clean", "_demo_randomized", "_randomized"):
if base.endswith(suffix):
base = base[: -len(suffix)]
break
text = base.replace("_", " ").strip()
text = re.sub(r"\s+", " ", text)
if not text:
raise ValueError(f"Could not derive instruction from task name: {task_name}")
return text[0].upper() + text[1:] + "."
def should_skip(path: Path, ignored_dirs: set[str]) -> bool:
return any(part in ignored_dirs for part in path.parts)
def is_valid_hdf5(path: Path) -> bool:
try:
with open(path, "rb") as f:
return f.read(len(HDF5_SIGNATURE)) == HDF5_SIGNATURE
except OSError:
return False
def infer_observation_keys(representative_hdf5s: list[str]) -> list[str]:
"""Return camera keys present in ALL given HDF5 files (intersection)."""
import h5py
common: set[str] | None = None
for path in representative_hdf5s:
with h5py.File(path, "r") as f:
if "observation" not in f:
raise KeyError(f"'observation' group not found in {path}")
available = {
camera_name
for camera_name, camera_group in f["observation"].items()
if isinstance(camera_group, h5py.Group) and "rgb" in camera_group
}
common = available if common is None else common & available
if not common:
raise ValueError(
f"No observation/*/rgb datasets common across all representative episodes"
)
ordered = [name for name in CAMERA_PRIORITY if name in common]
ordered.extend(sorted(name for name in common if name not in ordered))
return [f"observation/{camera_name}/rgb" for camera_name in ordered]
def find_instruction_dir(data_dir: Path) -> Path | None:
for candidate in (data_dir / "instructions", data_dir.parent / "instructions"):
if candidate.is_dir():
return candidate
return None
def iter_data_dirs(dataset_root: Path, ignored_dirs: set[str]) -> list[tuple[Path, str]]:
data_dirs: list[tuple[Path, str]] = []
for data_dir in sorted(dataset_root.rglob("data")):
if not data_dir.is_dir():
continue
if should_skip(data_dir, ignored_dirs):
continue
relative = data_dir.relative_to(dataset_root)
layout = parse_layout(relative)
if layout is None:
continue
data_dirs.append((data_dir, layout))
return data_dirs
def parse_layout(relative: Path) -> str | None:
parts = relative.parts
if not parts or parts[-1] != "data":
return None
if len(parts) == 2:
return "task"
if len(parts) == 3:
return "task_variant"
if len(parts) == 4:
return "domain_task_variant"
return None
def collect_datasets(
dataset_root: Path,
glob_pattern: str,
ignored_dirs: set[str],
variants: set[str] | None,
) -> tuple[
list[str],
list[str],
list[tuple[str, Path | None]],
dict[str, str],
dict[str, int],
dict[str, int],
int,
]:
datalist: list[str] = []
representatives: list[str] = []
episode_pairs: list[tuple[str, Path | None]] = []
instruction_map: dict[str, str] = {}
dataset_counts: dict[str, int] = {}
skipped_counts: dict[str, int] = {}
datasets_with_instruction_dir = 0
for data_dir, layout in iter_data_dirs(dataset_root, ignored_dirs):
relative = data_dir.relative_to(dataset_root)
if layout == "task":
domain_name = dataset_root.name
task_name, leaf_name = relative.parts
variant_name = "default"
elif layout == "task_variant":
domain_name = dataset_root.name
task_name, variant_name, leaf_name = relative.parts
else:
domain_name, task_name, variant_name, leaf_name = relative.parts
if variants is not None and variant_name not in variants:
continue
raw_episodes = sorted(p.resolve() for p in data_dir.rglob(glob_pattern))
episodes = [str(p) for p in raw_episodes if is_valid_hdf5(p)]
skipped = len(raw_episodes) - len(episodes)
if skipped:
dataset_key = f"{domain_name}/{task_name}/{variant_name}"
skipped_counts[dataset_key] = skipped
if not episodes:
continue
instruction = task_to_instruction(task_name)
dataset_key = f"{domain_name}/{task_name}/{variant_name}"
dataset_counts[dataset_key] = len(episodes)
datalist.extend(episodes)
representatives.append(episodes[0])
instr_dir = find_instruction_dir(data_dir)
if instr_dir is not None:
datasets_with_instruction_dir += 1
episode_pairs.extend(pair_episodes_with_instructions(episodes, instr_dir))
for episode in episodes:
instruction_map[episode] = instruction
return (
datalist,
representatives,
episode_pairs,
instruction_map,
dataset_counts,
skipped_counts,
datasets_with_instruction_dir,
)
def main() -> None:
args = parse_args()
dataset_root = Path(args.dataset_root).expanduser().resolve()
output = Path(args.output).expanduser().resolve()
variants = set(args.variant) if args.variant else None
ignored_dirs = set(args.ignore_dir)
(
datalist,
representatives,
episode_pairs,
derived_instruction_map,
dataset_counts,
skipped_counts,
datasets_with_instruction_dir,
) = collect_datasets(
dataset_root=dataset_root,
glob_pattern=args.glob,
ignored_dirs=ignored_dirs,
variants=variants,
)
if not datalist:
raise FileNotFoundError(
f"No files matched {args.glob!r} under {dataset_root}"
)
observation_keys = args.observation_key or infer_observation_keys(representatives)
meta = {
"dataset_name": args.dataset_name,
"observation_key": observation_keys,
"datalist": datalist,
}
if args.language_instruction_key:
meta["language_instruction_key"] = args.language_instruction_key
elif args.default_instruction:
meta["default_instruction"] = args.default_instruction
elif args.instruction_map_json:
with open(args.instruction_map_json, "r", encoding="utf-8") as f:
meta["instruction_map"] = json.load(f)
elif datasets_with_instruction_dir == len(dataset_counts):
print(f"Building instruction maps (split={args.instruction_split!r})...")
instruction_map, lang_aug_map = build_instruction_maps(
episode_pairs,
split=args.instruction_split,
)
meta["instruction_map"] = instruction_map
if lang_aug_map:
meta["lang_aug_map"] = lang_aug_map
elif datasets_with_instruction_dir:
raise ValueError(
"Found instructions/ directories for only part of the dataset tree. "
"Use an explicit instruction source, or make instruction files "
"available for every included dataset."
)
else:
meta["instruction_map"] = derived_instruction_map
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
f.write("\n")
print(f"Wrote {len(datalist)} episodes to {output}")
print(f"Datasets: {len(dataset_counts)}")
print(f"Observation keys: {observation_keys}")
if "language_instruction_key" in meta:
print(f"Instruction source: HDF5 key {meta['language_instruction_key']}")
elif "default_instruction" in meta:
print("Instruction source: shared default instruction")
elif args.instruction_map_json:
print(f"Instruction source: JSON map {args.instruction_map_json}")
elif "lang_aug_map" in meta:
print(f"Instruction source: per-episode instruction JSONs ({len(meta['lang_aug_map'])} augmented entries)")
else:
print("Instruction source: task-name-derived fallback")
for dataset_key, count in dataset_counts.items():
print(f" {dataset_key}: {count}")
if skipped_counts:
print("Skipped non-HDF5 files:")
for dataset_key, count in skipped_counts.items():
print(f" {dataset_key}: {count}")
if __name__ == "__main__":
main()