Download Isaac-GR00T/scripts/download_droid_sample.py from Timsty/groot_deployment: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/Timsty/groot_deployment/resolve/main/Isaac-GR00T/scripts/download_droid_sample.py
- Command line
-
hf download hf://Timsty/groot_deployment/Isaac-GR00T/scripts/download_droid_sample.py
-
curl -L -o download_droid_sample.py https://huggingface.co/Timsty/groot_deployment/resolve/main/Isaac-GR00T/scripts/download_droid_sample.py
14.9 kB
| #!/usr/bin/env python3 | |
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """ | |
| Download a small DROID sample dataset from HuggingFace and convert it to | |
| GR00T LeRobot v2 format suitable for inference with the base model. | |
| The full DROID dataset (lerobot/droid_1.0.1) is ~358 GB with 95k+ episodes | |
| in LeRobot v3.0 format. This script downloads only the first data/video chunks, | |
| then extracts a handful of episodes into the v2.0 per-episode format. | |
| Prerequisites: | |
| uv pip install jsonlines # if not already installed | |
| Usage: | |
| python scripts/download_droid_sample.py | |
| python scripts/download_droid_sample.py --num-episodes 5 --output-dir demo_data/droid_sample | |
| After running, test with: | |
| uv run python scripts/deployment/standalone_inference_script.py \\ | |
| --model-path nvidia/GR00T-N1.7-3B \\ | |
| --dataset-path demo_data/droid_sample \\ | |
| --embodiment-tag OXE_DROID_RELATIVE_EEF_RELATIVE_JOINT \\ | |
| --traj-ids 0 1 --inference-mode pytorch --action-horizon 8 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| from pathlib import Path | |
| import subprocess | |
| from gr00t.data.embodiment_tags import EmbodimentTag | |
| from gr00t.data.state_action.droid_frame import compute_eef_9d | |
| from gr00t.data.stats import generate_rel_stats, generate_stats | |
| import jsonlines | |
| import numpy as np | |
| import pyarrow.parquet as pq | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") | |
| logger = logging.getLogger(__name__) | |
| REPO_ID = "lerobot/droid_1.0.1" | |
| DEFAULT_OUTPUT_DIR = "demo_data/droid_sample" | |
| DEFAULT_NUM_EPISODES = 3 | |
| # The 2 cameras used by the OXE_DROID model config. | |
| # (The dataset also has exterior_2_left, but the model only uses 2 cameras.) | |
| VIDEO_KEYS = [ | |
| "observation.images.exterior_1_left", | |
| "observation.images.wrist_left", | |
| ] | |
| def download_droid_files(cache_dir: Path) -> None: | |
| """Download minimal files from the DROID v3.0 dataset.""" | |
| from huggingface_hub import hf_hub_download | |
| logger.info("Downloading DROID v3.0 metadata and first chunks...") | |
| # Source stats.json is skipped; stats are regenerated from the rebuilt | |
| # 17D data in extract_episodes (source layout differs). | |
| files_to_download = [ | |
| "meta/info.json", | |
| "meta/tasks.parquet", | |
| "meta/episodes/chunk-000/file-000.parquet", | |
| "data/chunk-000/file-000.parquet", | |
| ] | |
| # Download video file-000 for each camera we need | |
| for video_key in VIDEO_KEYS: | |
| files_to_download.append(f"videos/{video_key}/chunk-000/file-000.mp4") | |
| for fname in files_to_download: | |
| logger.info(f" {fname}...") | |
| hf_hub_download( | |
| repo_id=REPO_ID, | |
| repo_type="dataset", | |
| filename=fname, | |
| local_dir=str(cache_dir), | |
| ) | |
| def extract_episodes(cache_dir: Path, output_dir: Path, num_episodes: int) -> None: | |
| """Convert downloaded v3.0 data to GR00T LeRobot v2.0 format.""" | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| meta_dir = output_dir / "meta" | |
| meta_dir.mkdir(exist_ok=True) | |
| # Load source info | |
| with open(cache_dir / "meta" / "info.json") as f: | |
| source_info = json.load(f) | |
| fps = source_info.get("fps", 15) | |
| # ── Load episodes metadata (v3.0 parquet format) ── | |
| episodes_pq = cache_dir / "meta" / "episodes" / "chunk-000" / "file-000.parquet" | |
| episodes_df = pq.read_table(episodes_pq).to_pandas() | |
| # Only take episodes from file-000 (the chunk we downloaded) | |
| episodes = [] | |
| for _, row in episodes_df.iterrows(): | |
| if len(episodes) >= num_episodes: | |
| break | |
| # Skip episodes whose data is in a different file | |
| if int(row["data/file_index"]) != 0: | |
| continue | |
| episodes.append(row) | |
| if not episodes: | |
| raise RuntimeError("No episodes found in first data chunk") | |
| # ── Load tasks (v3.0: parquet with task text as index, task_index as column) ── | |
| tasks_df = pq.read_table(cache_dir / "meta" / "tasks.parquet").to_pandas() | |
| tasks_df_reset = tasks_df.reset_index() | |
| # columns after reset: ['index' (= task text), 'task_index'] | |
| task_text_col = tasks_df_reset.columns[0] # the task text column | |
| logger.info(f"Extracting {len(episodes)} episodes (fps={fps})") | |
| # ── Read the consolidated data parquet ── | |
| data_path = cache_dir / "data" / "chunk-000" / "file-000.parquet" | |
| table = pq.read_table(data_path) | |
| df = table.to_pandas() | |
| # ── Create per-episode parquet files ── | |
| data_chunk_dir = output_dir / "data" / "chunk-000" | |
| data_chunk_dir.mkdir(parents=True, exist_ok=True) | |
| episode_records = [] | |
| task_indices_used = set() | |
| for ep_row in episodes: | |
| ep_idx = int(ep_row["episode_index"]) | |
| ep_df = df[df["episode_index"] == ep_idx].copy() | |
| if len(ep_df) == 0: | |
| logger.warning(f"Episode {ep_idx} has no data rows, skipping") | |
| continue | |
| ep_length = len(ep_df) | |
| new_ep_idx = len(episode_records) | |
| lang = ( | |
| str(ep_df["language_instruction"].iloc[0]) | |
| if "language_instruction" in ep_df.columns | |
| else "" | |
| ) | |
| logger.info(f" Episode {ep_idx} -> {new_ep_idx}: {ep_length} frames, task={lang[:60]!r}") | |
| if "task_index" in ep_df.columns: | |
| task_indices_used.update(ep_df["task_index"].unique().tolist()) | |
| ep_df = ep_df.copy() | |
| ep_df["episode_index"] = new_ep_idx | |
| ep_df["index"] = range(len(ep_df)) | |
| # Compute eef_9d (XYZ + rot6d) from cartesian_position (XYZ + euler) | |
| # for both state and action, as the model expects 17D = eef_9d(9) + gripper(1) + joints(7) | |
| for prefix in ["observation.state", "action"]: | |
| cart_col = f"{prefix}.cartesian_position" | |
| if cart_col in ep_df.columns: | |
| cart = np.stack(ep_df[cart_col].values) # (T, 6) | |
| eef_9d = compute_eef_9d(cart) # (T, 9) | |
| ep_df[f"{prefix}.eef_9d"] = list(eef_9d) | |
| # Rebuild concatenated observation.state = [eef_9d(9), gripper(1), joint(7)] = 17D | |
| state_parts = [] | |
| for col in [ | |
| "observation.state.eef_9d", | |
| "observation.state.gripper_position", | |
| "observation.state.joint_position", | |
| ]: | |
| if col in ep_df.columns: | |
| vals = ep_df[col].values | |
| arr = np.stack([np.atleast_1d(v) for v in vals]) | |
| state_parts.append(arr) | |
| if state_parts: | |
| new_state = np.concatenate(state_parts, axis=-1) # (T, 17) | |
| ep_df["observation.state"] = list(new_state) | |
| # Rebuild concatenated action = [eef_9d(9), gripper(1), joint(7)] = 17D | |
| action_parts = [] | |
| for col in ["action.eef_9d", "action.gripper_position", "action.joint_position"]: | |
| if col in ep_df.columns: | |
| vals = ep_df[col].values | |
| arr = np.stack([np.atleast_1d(v) for v in vals]) | |
| action_parts.append(arr) | |
| if action_parts: | |
| new_action = np.concatenate(action_parts, axis=-1) # (T, 17) | |
| ep_df["action"] = list(new_action) | |
| ep_parquet = data_chunk_dir / f"episode_{new_ep_idx:06d}.parquet" | |
| ep_df.to_parquet(ep_parquet, index=False) | |
| episode_records.append( | |
| { | |
| "episode_index": new_ep_idx, | |
| "tasks": list(ep_row["tasks"]) if "tasks" in ep_row.index else [], | |
| "length": ep_length, | |
| "_src_row": ep_row, # keep for video timestamp lookup | |
| } | |
| ) | |
| if not episode_records: | |
| raise RuntimeError("No episodes could be extracted") | |
| # ── Extract per-episode video segments using timestamps from episodes metadata ── | |
| for video_key in VIDEO_KEYS: | |
| video_chunk_dir = output_dir / "videos" / "chunk-000" / video_key | |
| video_chunk_dir.mkdir(parents=True, exist_ok=True) | |
| source_video = cache_dir / "videos" / video_key / "chunk-000" / "file-000.mp4" | |
| if not source_video.exists(): | |
| logger.warning(f"Video not found: {source_video}, skipping") | |
| continue | |
| for rec in episode_records: | |
| new_ep_idx = rec["episode_index"] | |
| ep_row = rec["_src_row"] | |
| from_ts = float(ep_row[f"videos/{video_key}/from_timestamp"]) | |
| to_ts = float(ep_row[f"videos/{video_key}/to_timestamp"]) | |
| duration = to_ts - from_ts | |
| out_video = video_chunk_dir / f"episode_{new_ep_idx:06d}.mp4" | |
| # Try stream copy first (fast), fall back to re-encode for AV1 | |
| cmd = [ | |
| "ffmpeg", | |
| "-y", | |
| "-loglevel", | |
| "error", | |
| "-ss", | |
| f"{from_ts:.6f}", | |
| "-i", | |
| str(source_video), | |
| "-t", | |
| f"{duration:.6f}", | |
| "-c", | |
| "copy", | |
| str(out_video), | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| cmd_reencode = [ | |
| "ffmpeg", | |
| "-y", | |
| "-loglevel", | |
| "error", | |
| "-ss", | |
| f"{from_ts:.6f}", | |
| "-i", | |
| str(source_video), | |
| "-t", | |
| f"{duration:.6f}", | |
| "-c:v", | |
| "libx264", | |
| "-crf", | |
| "23", | |
| "-preset", | |
| "fast", | |
| str(out_video), | |
| ] | |
| subprocess.run(cmd_reencode, check=True) | |
| logger.info( | |
| f" {video_key} ep{new_ep_idx}: re-encoded ({rec['length']} frames, {from_ts:.1f}s-{to_ts:.1f}s)" | |
| ) | |
| else: | |
| logger.info( | |
| f" {video_key} ep{new_ep_idx}: copied ({rec['length']} frames, {from_ts:.1f}s-{to_ts:.1f}s)" | |
| ) | |
| # ── Write meta files ── | |
| # Clean up internal fields before writing | |
| for rec in episode_records: | |
| del rec["_src_row"] | |
| # meta/episodes.jsonl | |
| with jsonlines.open(meta_dir / "episodes.jsonl", mode="w") as writer: | |
| for rec in episode_records: | |
| writer.write(rec) | |
| # meta/tasks.jsonl | |
| with jsonlines.open(meta_dir / "tasks.jsonl", mode="w") as writer: | |
| for _, row in tasks_df_reset.iterrows(): | |
| tidx = int(row["task_index"]) | |
| if tidx in task_indices_used: | |
| writer.write({"task_index": tidx, "task": str(row[task_text_col])}) | |
| # meta/info.json | |
| info = { | |
| "codebase_version": "v2.1", | |
| "robot_type": "droid", | |
| "total_episodes": len(episode_records), | |
| "total_frames": sum(r["length"] for r in episode_records), | |
| "fps": fps, | |
| "data_path": "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet", | |
| "video_path": "videos/chunk-{episode_chunk:03d}/{video_key}/episode_{episode_index:06d}.mp4", | |
| "chunks_size": 1000, | |
| "splits": {"train": f"0:{len(episode_records)}"}, | |
| "features": { | |
| "observation.images.exterior_1_left": { | |
| "dtype": "video", | |
| "shape": [180, 320, 3], | |
| }, | |
| "observation.images.wrist_left": { | |
| "dtype": "video", | |
| "shape": [180, 320, 3], | |
| }, | |
| "observation.state": {"dtype": "float32", "shape": [17]}, | |
| "action": {"dtype": "float32", "shape": [17]}, | |
| "task_index": {"dtype": "int64", "shape": [1]}, | |
| }, | |
| } | |
| with open(meta_dir / "info.json", "w") as f: | |
| json.dump(info, f, indent=2) | |
| # meta/modality.json | |
| modality = { | |
| "state": { | |
| "eef_9d": {"start": 0, "end": 9}, | |
| "gripper_position": {"start": 9, "end": 10}, | |
| "joint_position": {"start": 10, "end": 17}, | |
| }, | |
| "action": { | |
| "eef_9d": {"start": 0, "end": 9}, | |
| "gripper_position": {"start": 9, "end": 10}, | |
| "joint_position": {"start": 10, "end": 17}, | |
| }, | |
| "video": { | |
| "exterior_1_left": {"original_key": "observation.images.exterior_1_left"}, | |
| "wrist_left": {"original_key": "observation.images.wrist_left"}, | |
| }, | |
| "annotation": { | |
| "language.language_instruction": {"original_key": "task_index"}, | |
| }, | |
| } | |
| with open(meta_dir / "modality.json", "w") as f: | |
| json.dump(modality, f, indent=2) | |
| # Regenerate stats from the rebuilt columns (needs info.json + modality.json). | |
| logger.info(" Generating stats.json and relative_stats.json from rebuilt data...") | |
| generate_stats(output_dir) | |
| generate_rel_stats(output_dir, EmbodimentTag.OXE_DROID_RELATIVE_EEF_RELATIVE_JOINT) | |
| logger.info(f"\nDataset created at: {output_dir}") | |
| logger.info(f" Episodes: {len(episode_records)}") | |
| logger.info(f" Total frames: {sum(r['length'] for r in episode_records)}") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Download a small DROID sample dataset for GR00T inference testing.", | |
| ) | |
| parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR) | |
| parser.add_argument("--num-episodes", type=int, default=DEFAULT_NUM_EPISODES) | |
| parser.add_argument("--cache-dir", default=None) | |
| args = parser.parse_args() | |
| cache_dir = Path(args.cache_dir or "/tmp/droid_download_cache") | |
| output_dir = Path(args.output_dir) | |
| if output_dir.exists(): | |
| logger.info(f"Output already exists: {output_dir} — delete it to regenerate.") | |
| return | |
| download_droid_files(cache_dir) | |
| extract_episodes(cache_dir, output_dir, args.num_episodes) | |
| logger.info("\nTo run inference:") | |
| logger.info( | |
| f" uv run python scripts/deployment/standalone_inference_script.py \\\n" | |
| f" --model-path nvidia/GR00T-N1.7-3B \\\n" | |
| f" --dataset-path {output_dir} \\\n" | |
| f" --embodiment-tag OXE_DROID_RELATIVE_EEF_RELATIVE_JOINT \\\n" | |
| f" --traj-ids 1 2 --inference-mode pytorch --action-horizon 8" | |
| ) | |
| if __name__ == "__main__": | |
| main() | |