153 lines
5.3 KiB
Python
153 lines
5.3 KiB
Python
"""Download a LeRobot dataset from HuggingFace Hub and prepare it for the dataloader.
|
|
|
|
This script:
|
|
1. Downloads a LeRobot dataset from HuggingFace Hub.
|
|
2. Loads it into Rerun via the built-in LeRobot importer (`log_file_from_path`).
|
|
3. Splits the resulting archive into one RRD per episode.
|
|
4. Registers the per-episode RRDs to a catalog server instance.
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from huggingface_hub import snapshot_download
|
|
|
|
import rerun as rr
|
|
|
|
DEFAULT_REPO_ID = "rerun/so101-pick-and-place"
|
|
DEFAULT_OUTPUT_DIR = Path(__file__).resolve().parent / "data"
|
|
APPLICATION_ID = "lerobot"
|
|
|
|
_EPISODE_RE = re.compile(r"^(episode_)(\d+)$")
|
|
|
|
|
|
def _zero_pad_episode_id(rec_id: str, width: int = 5) -> str:
|
|
"""Turn `episode_1` into `episode_00001` so segments sort lexicographically."""
|
|
m = _EPISODE_RE.match(rec_id)
|
|
if m:
|
|
return f"{m.group(1)}{int(m.group(2)):0{width}d}"
|
|
return rec_id
|
|
|
|
|
|
def download_dataset(repo_id: str, dest: Path) -> Path:
|
|
"""Download a LeRobot dataset from HuggingFace Hub into *dest* and return its path."""
|
|
print(f"Downloading {repo_id} to {dest} …")
|
|
local_dir = snapshot_download(repo_id=repo_id, repo_type="dataset", local_dir=dest)
|
|
return Path(local_dir)
|
|
|
|
|
|
def lerobot_to_combined_rrd(dataset_dir: Path, combined_rrd: Path) -> None:
|
|
"""Use Rerun's built-in LeRobot importer to turn the dataset into a single RRD."""
|
|
print(f"Converting {dataset_dir} -> {combined_rrd}")
|
|
with rr.RecordingStream(APPLICATION_ID) as rec:
|
|
rec.save(str(combined_rrd))
|
|
rec.log_file_from_path(str(dataset_dir))
|
|
|
|
|
|
def split_into_episode_rrds(combined_rrd: Path, rrd_dir: Path) -> list[Path]:
|
|
"""Split a combined RRD archive into one RRD per episode.
|
|
|
|
Returns the paths of the written per-episode RRDs.
|
|
"""
|
|
rrd_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
reader = rr.experimental.RrdReader(str(combined_rrd))
|
|
recordings = reader.recordings()
|
|
print(f"Archive contains {len(recordings)} recordings")
|
|
|
|
episode_paths: list[Path] = []
|
|
for entry in recordings:
|
|
store = reader.store(store=entry)
|
|
# Skip metadata-only recordings (e.g. the "root" recording that only carries properties).
|
|
if not store.schema().entity_paths():
|
|
continue
|
|
|
|
episode_id = _zero_pad_episode_id(entry.recording_id)
|
|
rrd_path = rrd_dir / f"{episode_id}.rrd"
|
|
|
|
with rr.RecordingStream(APPLICATION_ID, recording_id=episode_id, send_properties=False) as rec:
|
|
rec.save(str(rrd_path))
|
|
rec.send_chunks(store)
|
|
episode_paths.append(rrd_path)
|
|
print(f" wrote {rrd_path} ({rrd_path.stat().st_size / (1024 * 1024):.1f} MB)")
|
|
|
|
return episode_paths
|
|
|
|
|
|
def register_to_catalog(
|
|
rrd_paths: list[Path],
|
|
*,
|
|
catalog_url: str,
|
|
dataset_name: str,
|
|
) -> None:
|
|
"""Register per-episode RRDs to a catalog server instance.
|
|
|
|
Uses absolute file:// URIs so the catalog can read the RRDs directly from the local filesystem.
|
|
"""
|
|
print(f"\nRegistering {len(rrd_paths)} episodes to {catalog_url} as dataset '{dataset_name}' …")
|
|
client = rr.catalog.CatalogClient(catalog_url)
|
|
dataset = client.create_dataset(dataset_name, exist_ok=True)
|
|
|
|
uris = [f"file://{p.resolve()}" for p in rrd_paths]
|
|
on_duplicate = rr.catalog.OnDuplicateSegmentLayer(rr.catalog.OnDuplicateSegmentLayer.REPLACE)
|
|
dataset.register(uris, on_duplicate=on_duplicate).wait()
|
|
print(" registration done")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument(
|
|
"--repo-id",
|
|
default=DEFAULT_REPO_ID,
|
|
help=f"HuggingFace dataset repo id (default: {DEFAULT_REPO_ID}).",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=DEFAULT_OUTPUT_DIR,
|
|
help=f"Directory to store downloaded dataset and output RRDs (default: {DEFAULT_OUTPUT_DIR}).",
|
|
)
|
|
parser.add_argument(
|
|
"--catalog-url",
|
|
default="rerun+http://127.0.0.1:51234",
|
|
help="Rerun catalog URL to register episodes with. Pass an empty string to skip registration.",
|
|
)
|
|
parser.add_argument(
|
|
"--dataset-name",
|
|
default=None,
|
|
help="Name of the dataset to create/use in the catalog (default: derived from --repo-id).",
|
|
)
|
|
parser.add_argument(
|
|
"--keep-combined",
|
|
action="store_true",
|
|
help="Keep the intermediate combined RRD after splitting (useful for debugging).",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
output_dir = args.output_dir
|
|
repo_slug = args.repo_id.replace("/", "_")
|
|
dataset_dir = output_dir / "lerobot" / repo_slug
|
|
rrd_dir = output_dir / "rrds" / repo_slug
|
|
combined_rrd = output_dir / f"{repo_slug}_combined.rrd"
|
|
|
|
download_dataset(args.repo_id, dataset_dir)
|
|
lerobot_to_combined_rrd(dataset_dir, combined_rrd)
|
|
episode_paths = split_into_episode_rrds(combined_rrd, rrd_dir)
|
|
|
|
if not args.keep_combined:
|
|
combined_rrd.unlink()
|
|
|
|
print(f"\nWrote {len(episode_paths)} per-episode RRDs to {rrd_dir}")
|
|
|
|
if args.catalog_url:
|
|
dataset_name = args.dataset_name or repo_slug
|
|
register_to_catalog(episode_paths, catalog_url=args.catalog_url, dataset_name=dataset_name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|