mirror of
https://github.com/huggingface/lerobot.git
synced 2026-05-27 06:29:47 +00:00
scripts: build_robocasa_composite_seen — aggregate 16 target tasks
RoboCasa 1.0 ships its target/human demos in LeRobot format (parquet +
mp4) as lerobot.tar archives distributed via Box. This script wraps
RoboCasa's own download_datasets helper to pull each of the 16
composite_seen tasks, opens each extracted directory as a
LeRobotDataset, and merges them into a single combined dataset via
merge_datasets (a thin wrapper over aggregate_datasets that revalidates
fps/robot_type/features, unifies task indices, concatenates videos and
parquet, and recomputes stats).
The 16-task slice corresponds exactly to the 'Composite-Seen' column of
the published RoboCasa365 leaderboard, so the resulting dataset is the
right substrate for an apples-to-apples pi05 vs pi052 comparison on
multi-step kitchen manipulation.
Usage:
uv run python -m lerobot.scripts.build_robocasa_composite_seen \
--output-dir=/data/lerobot/robocasa_composite_seen \
--hub-repo-id=${HF_USER}/robocasa_composite_seen \
--push-to-hub
Idempotent: re-running skips already-downloaded tasks. Defensive
fallbacks handle RoboCasa API drift in get_ds_path / download_datasets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a single combined LeRobotDataset from RoboCasa's 16 composite_seen tasks.
|
||||
|
||||
RoboCasa 1.0 already ships in LeRobot format (parquet + mp4), distributed as
|
||||
``lerobot.tar`` archives from Box. This script:
|
||||
|
||||
1. Downloads each composite_seen task's ``target/human`` archive via RoboCasa's
|
||||
official ``download_datasets`` helper (idempotent — skipped if already on
|
||||
disk).
|
||||
2. Opens each extracted directory as a ``LeRobotDataset``.
|
||||
3. Merges all 16 into one unified dataset via ``merge_datasets`` (a thin wrapper
|
||||
over ``aggregate_datasets`` that revalidates fps / robot_type / features,
|
||||
unifies task indices, concatenates videos and parquet, and recomputes stats).
|
||||
4. Optionally pushes the merged dataset to the Hub.
|
||||
|
||||
The result is one ~8,000-trajectory dataset where each episode carries its
|
||||
source task as the ``task`` field — ready for downstream annotation
|
||||
(subtasks / memory / VQA / tool calls) without per-task bookkeeping.
|
||||
|
||||
Usage::
|
||||
|
||||
uv run python -m lerobot.scripts.build_robocasa_composite_seen \\
|
||||
--output-dir=/data/lerobot/robocasa_composite_seen \\
|
||||
--hub-repo-id=${HF_USER}/robocasa_composite_seen \\
|
||||
--push-to-hub
|
||||
|
||||
Prereqs: ``robocasa`` and ``robosuite`` installed (see
|
||||
``docs/source/benchmarks/robocasa.mdx`` for the editable-install dance — they
|
||||
are not on PyPI and RoboCasa's own ``setup.py`` pins an old LeRobot version).
|
||||
|
||||
The 16 composite_seen tasks are the multi-step subset of the official
|
||||
RoboCasa365 target benchmark — exactly the slice used to compute the
|
||||
``Composite-Seen`` column of the leaderboard.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from lerobot.datasets.dataset_tools import merge_datasets
|
||||
from lerobot.datasets.lerobot_dataset import LeRobotDataset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Canonical 16 composite_seen tasks (RoboCasa365 target benchmark).
|
||||
# Order matches the leaderboard docs.
|
||||
COMPOSITE_SEEN_TASKS: list[str] = [
|
||||
"DeliverStraw",
|
||||
"GetToastedBread",
|
||||
"KettleBoiling",
|
||||
"LoadDishwasher",
|
||||
"PackIdenticalLunches",
|
||||
"PreSoakPan",
|
||||
"PrepareCoffee",
|
||||
"RinseSinkBasin",
|
||||
"ScrubCuttingBoard",
|
||||
"SearingMeat",
|
||||
"SetUpCuttingStation",
|
||||
"StackBowlsCabinet",
|
||||
"SteamInMicrowave",
|
||||
"StirVegetables",
|
||||
"StoreLeftoversInBowl",
|
||||
"WashLettuce",
|
||||
]
|
||||
|
||||
|
||||
def _require_robocasa() -> None:
|
||||
"""Fail fast with an actionable message if robocasa is missing.
|
||||
|
||||
RoboCasa is not on PyPI and is not a LeRobot extra — see the installation
|
||||
notes in ``docs/source/benchmarks/robocasa.mdx``.
|
||||
"""
|
||||
try:
|
||||
import robocasa # noqa: F401, PLC0415
|
||||
from robocasa.scripts import download_datasets as _dl # noqa: F401, PLC0415
|
||||
from robocasa.utils import dataset_registry as _reg # noqa: F401, PLC0415
|
||||
except ImportError as exc:
|
||||
sys.exit(
|
||||
"[build_robocasa_composite_seen] robocasa is not importable.\n"
|
||||
"Install it (and robosuite) per the LeRobot RoboCasa docs:\n"
|
||||
" git clone https://github.com/robocasa/robocasa.git ~/robocasa\n"
|
||||
" git clone https://github.com/ARISE-Initiative/robosuite.git ~/robosuite\n"
|
||||
" pip install -e ~/robocasa --no-deps\n"
|
||||
" pip install -e ~/robosuite\n"
|
||||
f"(original error: {exc})"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_task_root(task: str) -> Path:
|
||||
"""Resolve the local extracted ``LeRobotDataset`` root for a target/human task.
|
||||
|
||||
Uses RoboCasa's own ``dataset_registry`` so we follow whatever directory
|
||||
layout RoboCasa picks (currently ``v1.0/target/composite/<task>/<date>/``
|
||||
under ``robocasa.macros.DATASET_BASE_DIR``). Falls back to discovering the
|
||||
extracted directory if the helper's signature drifted between releases.
|
||||
"""
|
||||
from robocasa.utils import dataset_registry # noqa: PLC0415
|
||||
|
||||
# ``get_ds_path`` is the canonical helper. RoboCasa 1.0 signature is
|
||||
# ``get_ds_path(task, ds_type, return_info=False)`` with ``ds_type`` like
|
||||
# ``"human_im"`` (image-observation human demos). We try the common
|
||||
# ``split=`` kwarg first (newer registry); if it's rejected, fall back.
|
||||
try:
|
||||
ds_path = dataset_registry.get_ds_path(
|
||||
task=task,
|
||||
ds_type="human_im",
|
||||
return_info=False,
|
||||
split="target",
|
||||
)
|
||||
except TypeError:
|
||||
# Older registry — ds_type alone disambiguates target/human.
|
||||
ds_path = dataset_registry.get_ds_path(
|
||||
task=task,
|
||||
ds_type="human_im",
|
||||
return_info=False,
|
||||
)
|
||||
|
||||
root = Path(ds_path)
|
||||
# ``get_ds_path`` may return either the extracted dir or the .tar; normalize.
|
||||
if root.suffix == ".tar":
|
||||
root = root.parent
|
||||
return root
|
||||
|
||||
|
||||
def _download_task(task: str, *, overwrite: bool = False) -> Path:
|
||||
"""Download (or locate) a single target/human task and return its extracted root."""
|
||||
from robocasa.scripts import download_datasets as dl # noqa: PLC0415
|
||||
|
||||
# Try the documented programmatic API. The CLI is
|
||||
# python -m robocasa.scripts.download_datasets --tasks <T> --source human --split target
|
||||
# which is a thin wrapper over a function of the same name.
|
||||
if hasattr(dl, "download_datasets"):
|
||||
try:
|
||||
dl.download_datasets(
|
||||
tasks=[task],
|
||||
source="human",
|
||||
split="target",
|
||||
overwrite=overwrite,
|
||||
)
|
||||
except TypeError:
|
||||
# Older signature — drop the kwargs RoboCasa didn't have yet.
|
||||
dl.download_datasets(tasks=[task])
|
||||
else:
|
||||
# No public function — shell out to the CLI as a last resort. This
|
||||
# guarantees we use whatever entrypoint RoboCasa's authors maintain.
|
||||
import subprocess # noqa: PLC0415
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"robocasa.scripts.download_datasets",
|
||||
"--tasks",
|
||||
task,
|
||||
"--source",
|
||||
"human",
|
||||
"--split",
|
||||
"target",
|
||||
]
|
||||
if overwrite:
|
||||
cmd.append("--overwrite")
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
root = _resolve_task_root(task)
|
||||
if not root.exists():
|
||||
raise RuntimeError(
|
||||
f"Expected {root} after download, but it doesn't exist. "
|
||||
"RoboCasa may have changed its data layout — verify with "
|
||||
"`robocasa.utils.dataset_registry.get_ds_path()`."
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def _open_as_lerobot_dataset(task: str, root: Path) -> LeRobotDataset:
|
||||
"""Open an extracted RoboCasa target/human task as a ``LeRobotDataset``.
|
||||
|
||||
The placeholder ``repo_id`` (``robocasa/<task>_target_human``) is only used
|
||||
by the aggregator for logging and for the unified task table — the actual
|
||||
data is loaded from ``root``.
|
||||
"""
|
||||
repo_id = f"robocasa/{task}_target_human"
|
||||
return LeRobotDataset(repo_id=repo_id, root=root)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Aggregate the 16 RoboCasa composite_seen target tasks into one LeRobotDataset.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Local directory for the merged dataset (will be created).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hub-repo-id",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Hub repo_id for the merged dataset (e.g. ``yourname/"
|
||||
"robocasa_composite_seen``). Required for ``--push-to-hub``; also "
|
||||
"becomes the merged dataset's canonical ``repo_id``."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--push-to-hub",
|
||||
action="store_true",
|
||||
help="Push the merged dataset to the Hub after building. Requires "
|
||||
"``--hub-repo-id`` and a prior ``huggingface-cli login``.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--private",
|
||||
action="store_true",
|
||||
help="When pushing, create the Hub repo as private.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tasks",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Comma-separated task names to override the default 16 "
|
||||
"composite_seen list (useful for smoke-testing with 1–2 tasks).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-download",
|
||||
action="store_true",
|
||||
help="Skip the download step entirely; assume each task is already "
|
||||
"extracted on disk at the path ``dataset_registry.get_ds_path`` "
|
||||
"returns.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite-download",
|
||||
action="store_true",
|
||||
help="Force re-download even when a complete local extraction exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
type=str,
|
||||
default="INFO",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, args.log_level),
|
||||
format="[%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
tasks = (
|
||||
[t.strip() for t in args.tasks.split(",") if t.strip()]
|
||||
if args.tasks
|
||||
else list(COMPOSITE_SEEN_TASKS)
|
||||
)
|
||||
if not tasks:
|
||||
sys.exit("No tasks selected.")
|
||||
|
||||
if args.push_to_hub and not args.hub_repo_id:
|
||||
sys.exit("--push-to-hub requires --hub-repo-id.")
|
||||
|
||||
output_repo_id = args.hub_repo_id or "local/robocasa_composite_seen"
|
||||
logger.info(
|
||||
"Building merged RoboCasa dataset: %d tasks → %s (output dir: %s)",
|
||||
len(tasks),
|
||||
output_repo_id,
|
||||
args.output_dir,
|
||||
)
|
||||
|
||||
_require_robocasa()
|
||||
|
||||
# 1. Download (or locate) each task's extracted directory.
|
||||
task_roots: list[tuple[str, Path]] = []
|
||||
for i, task in enumerate(tasks, 1):
|
||||
logger.info("[%d/%d] %s", i, len(tasks), task)
|
||||
if args.skip_download:
|
||||
root = _resolve_task_root(task)
|
||||
if not root.exists():
|
||||
sys.exit(
|
||||
f"--skip-download set but extracted directory does not "
|
||||
f"exist for {task}: {root}"
|
||||
)
|
||||
else:
|
||||
root = _download_task(task, overwrite=args.overwrite_download)
|
||||
logger.info(" extracted at: %s", root)
|
||||
task_roots.append((task, root))
|
||||
|
||||
# 2. Open each as a LeRobotDataset (validation happens inside aggregator).
|
||||
datasets: list[LeRobotDataset] = []
|
||||
for task, root in task_roots:
|
||||
logger.info("Opening %s", task)
|
||||
ds = _open_as_lerobot_dataset(task, root)
|
||||
logger.info(
|
||||
" %s: %d episodes, %d frames, %d FPS",
|
||||
task,
|
||||
ds.num_episodes,
|
||||
ds.num_frames,
|
||||
ds.fps,
|
||||
)
|
||||
datasets.append(ds)
|
||||
|
||||
# 3. Merge — re-validates features/fps/robot_type, unifies tasks, concats
|
||||
# videos + parquet, recomputes stats.
|
||||
logger.info("Merging %d datasets into %s", len(datasets), output_repo_id)
|
||||
merged = merge_datasets(
|
||||
datasets=datasets,
|
||||
output_repo_id=output_repo_id,
|
||||
output_dir=args.output_dir,
|
||||
)
|
||||
logger.info(
|
||||
"Merged: %d episodes, %d frames across %d unique task strings",
|
||||
merged.num_episodes,
|
||||
merged.num_frames,
|
||||
len(merged.meta.tasks) if merged.meta.tasks is not None else 0,
|
||||
)
|
||||
|
||||
# 4. Push to Hub.
|
||||
if args.push_to_hub:
|
||||
logger.info("Pushing %s to the Hub (private=%s)", args.hub_repo_id, args.private)
|
||||
# ``upload_large_folder=True`` is the right mode for tens-of-GB
|
||||
# datasets — uses multipart uploads + resumable transfers.
|
||||
merged.push_to_hub(
|
||||
private=args.private,
|
||||
upload_large_folder=True,
|
||||
tags=["lerobot", "robocasa", "composite_seen", "manipulation"],
|
||||
)
|
||||
logger.info(
|
||||
"Push complete: https://huggingface.co/datasets/%s",
|
||||
args.hub_repo_id,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Skipping Hub push (no --push-to-hub). Merged dataset is at %s.",
|
||||
args.output_dir,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user