xvla-vanilla-backup / scripts /make_robotwin_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
9.81 kB
#!/usr/bin/env python3
"""Generate X-VLA metadata JSON for RoboTwin HDF5 episodes.
Single-task mode (--dataset-root points at a data/ directory):
python make_robotwin_meta.py \\
--dataset-root .../place_can_basket/aloha-agilex_clean_50/data \\
--output meta/place_can_basket.json
Multi-task mode (--dataset-root points at the top-level directory that
contains one subdirectory per task):
python make_robotwin_meta.py \\
--dataset-root /work/xuan/dataset/robotwin_dataset \\
--output meta/robotwin_all.json \\
--data-subdir aloha-agilex_clean_50
Instructions are auto-detected from sibling 'instructions/' directories.
"""
import argparse
import json
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate X-VLA metadata for RoboTwin HDF5 episodes.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--dataset-root", required=True,
help="Top-level directory containing task folders (multi-task), "
"or a single data/ directory (single-task).",
)
parser.add_argument("--output", required=True,
help="Where to write the X-VLA metadata JSON.")
parser.add_argument(
"--dataset-name",
default="robotwin2_clean",
choices=["robotwin2_clean", "robotwin2_abs_ee"],
help="Dataset key registered in X-VLA (default: robotwin2_clean).",
)
parser.add_argument(
"--data-subdir", default=None,
help="Subdirectory name that identifies each task's dataset "
"(e.g. 'aloha-agilex_clean_50'). When set, enables multi-task "
"discovery: the script looks for <task>/<data-subdir>/data/ "
"under --dataset-root.",
)
parser.add_argument(
"--glob", default="*.hdf5",
help="Glob pattern for episode files (default: *.hdf5).",
)
parser.add_argument(
"--observation-key", nargs="+",
default=[
"observation/countertop_camera/rgb",
"observation/left_camera/rgb",
"observation/right_camera/rgb",
],
help="Image dataset keys inside each HDF5 file.",
)
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 discover_tasks(root: Path, data_subdir: str) -> list[tuple[str, Path, Path]]:
"""Find all <task>/<data_subdir>/{data/,instructions/} under root.
Returns list of (task_name, data_dir, instruction_dir).
"""
tasks = []
for task_dir in sorted(root.iterdir()):
if not task_dir.is_dir():
continue
base = task_dir / data_subdir
data_dir = base / "data"
instr_dir = base / "instructions"
if data_dir.is_dir():
tasks.append((task_dir.name, data_dir, instr_dir if instr_dir.is_dir() else None))
return tasks
def build_instruction_maps(
episodes: list[tuple[str, Path | None]],
split: str = "seen",
) -> tuple[dict[str, str], dict[str, list[str]]]:
"""Build instruction_map and lang_aug_map from per-episode JSON files.
Args:
episodes: list of (hdf5_absolute_path, instruction_json_path_or_None)
split: "seen" or "unseen"
Returns:
instruction_map: {hdf5_absolute_path: canonical_instruction}
lang_aug_map: {canonical_instruction: [all paraphrases]}
"""
instruction_map: dict[str, str] = {}
lang_aug_map: dict[str, list[str]] = {}
missing = []
for hdf5_path, json_path in episodes:
if json_path is None or not json_path.exists():
missing.append(hdf5_path)
continue
with open(json_path, "r", encoding="utf-8") as f:
instr_data = json.load(f)
if split not in instr_data:
available = list(instr_data.keys())
raise KeyError(
f"Split '{split}' not found in {json_path}. Available: {available}"
)
instructions = instr_data[split]
if not instructions:
raise ValueError(
f"Empty instruction list for split '{split}' in {json_path}"
)
canonical = instructions[0]
instruction_map[hdf5_path] = canonical
if len(instructions) > 1:
lang_aug_map[canonical] = instructions
if missing:
raise FileNotFoundError(
f"Missing instruction JSONs for {len(missing)} episodes, "
f"e.g.: {missing[:3]}"
)
return instruction_map, lang_aug_map
def collect_single_task(
data_dir: Path, glob_pattern: str,
) -> tuple[list[str], Path | None]:
"""Collect episodes from a single data directory.
Returns (datalist, instruction_dir_or_None).
"""
datalist = sorted(str(p.resolve()) for p in data_dir.rglob(glob_pattern))
instr_dir = None
for candidate in [data_dir / "instructions", data_dir.parent / "instructions"]:
if candidate.is_dir():
instr_dir = candidate
break
return datalist, instr_dir
def pair_episodes_with_instructions(
datalist: list[str], instr_dir: Path | None,
) -> list[tuple[str, Path | None]]:
"""Pair each HDF5 path with its corresponding instruction JSON."""
pairs = []
for hdf5_path in datalist:
json_path = None
if instr_dir is not None:
json_path = instr_dir / f"{Path(hdf5_path).stem}.json"
pairs.append((hdf5_path, json_path))
return pairs
def main() -> None:
args = parse_args()
dataset_root = Path(args.dataset_root).expanduser().resolve()
output = Path(args.output).expanduser().resolve()
all_datalist: list[str] = []
all_episode_pairs: list[tuple[str, Path | None]] = []
has_instructions = False
if args.data_subdir:
# --- Multi-task discovery ---
tasks = discover_tasks(dataset_root, args.data_subdir)
if not tasks:
raise FileNotFoundError(
f"No task directories with '{args.data_subdir}/data/' "
f"found under {dataset_root}"
)
for task_name, data_dir, instr_dir in tasks:
datalist = sorted(
str(p.resolve()) for p in data_dir.rglob(args.glob)
)
if not datalist:
print(f" WARNING: no HDF5 files in {data_dir}, skipping {task_name}")
continue
pairs = pair_episodes_with_instructions(datalist, instr_dir)
all_datalist.extend(datalist)
all_episode_pairs.extend(pairs)
if instr_dir is not None:
has_instructions = True
print(f" [{task_name}] {len(datalist)} episodes"
f"{' + instructions' if instr_dir else ''}")
print(f"Discovered {len(tasks)} tasks, "
f"{len(all_datalist)} total episodes")
else:
# --- Single-task mode ---
datalist, instr_dir = collect_single_task(dataset_root, args.glob)
if not datalist:
raise FileNotFoundError(
f"No files matched {args.glob!r} under {dataset_root}"
)
all_datalist = datalist
all_episode_pairs = pair_episodes_with_instructions(datalist, instr_dir)
has_instructions = instr_dir is not None
meta: dict = {
"dataset_name": args.dataset_name,
"observation_key": args.observation_key,
"datalist": all_datalist,
}
# --- Resolve instructions ---
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 has_instructions:
print(f"Building instruction maps (split={args.instruction_split!r})...")
instruction_map, lang_aug_map = build_instruction_maps(
all_episode_pairs, split=args.instruction_split,
)
meta["instruction_map"] = instruction_map
if lang_aug_map:
meta["lang_aug_map"] = lang_aug_map
else:
raise ValueError(
"No instruction source found. Use one of:\n"
" --language-instruction-key (HDF5 key)\n"
" --default-instruction (single string)\n"
" --instruction-map-json (pre-built JSON)\n"
"Or ensure 'instructions/' directories exist alongside data/."
)
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")
n_episodes = len(all_datalist)
n_instr = len(meta.get("instruction_map", {}))
n_aug = len(meta.get("lang_aug_map", {}))
print(f"\nWrote {n_episodes} episodes to {output}")
if n_instr:
print(f" instruction_map: {n_instr} entries (keyed by absolute path)")
if n_aug:
print(f" lang_aug_map: {n_aug} entries (for training augmentation)")
if __name__ == "__main__":
main()