| |
| """ |
| RoboFail dataset loader for the generic dataset converter for Robometer model training. |
| This module contains RoboFail-specific logic for loading and processing data files. |
| """ |
|
|
| from pathlib import Path |
|
|
| import numpy as np |
| from dataset_upload.helpers import generate_unique_id |
| from robometer.data.video_helpers import load_video_frames |
| from tqdm import tqdm |
|
|
|
|
| class RoboFailFrameLoader: |
| """Pickle-able loader that reads RoboFail frames from files on demand. |
| |
| Stores only simple fields so it can be safely passed across processes. |
| Supports both HDF5 and video file formats. |
| """ |
|
|
| def __init__(self, file_path: str): |
| self.file_path = file_path |
|
|
| def __call__(self) -> np.ndarray: |
| """Load frames from file when called. |
| |
| Returns: |
| np.ndarray of shape (T, H, W, 3), dtype uint8 |
| """ |
| return load_video_frames(Path(self.file_path)) |
|
|
|
|
| |
| FOLDER_TO_TASK_NAME = { |
| "putAppleBowl1": "put apple in bowl", |
| "putAppleBowl2": "put apple in bowl", |
| "putAppleBowl3": "put apple in bowl", |
| "boilWater1": "boil water in a pot", |
| "boilWater2": "boil water in a pot", |
| "boilWater3": "boil water in a pot", |
| "boilWater4": "boil water in a pot", |
| "cutCarrot1": "cut carrot", |
| "cutCarrot2": "cut carrot", |
| "putPearDrawer1": "put pear in drawer", |
| "putPearDrawer2": "put pear in drawer", |
| "putPearDrawer3": "put pear in drawer", |
| "sauteeCarrot1": "cook carrot", |
| "sauteeCarrot2": "cook carrot", |
| "sauteeCarrot3": "cook carrot", |
| "sauteeCarrot4": "cook carrot", |
| "secureObjects1": "secure objects", |
| "putFruitsBowl1": "put all visible fruits in bowl", |
| "putFruitsBowl2": "put all fruits in bowl", |
| "makeCoffee1": "make coffee", |
| "makeCoffee2": "make coffee", |
| "makeCoffee3": "make coffee", |
| "heatPot1": "pre-heat pot", |
| "heatPot2": "pre-heat pot", |
| "appleInFridge1": "store apple in a bowl and put in a fridge", |
| "appleInFridge2": "store apple in a bowl and put in a fridge", |
| "appleInFridge3": "store apple in a bowl and put in a fridge", |
| "appleInFridge4": "store apple in a bowl and put in a fridge", |
| "heatPotato1": "serve a bowl of potato on table that was heated using a microwave", |
| "heatPotato2": "serve a bowl of potato on table that was heated using a microwave", |
| } |
|
|
|
|
| def _get_task_name_from_folder(folder_name: str) -> str: |
| """Convert folder name to task name using the mapping.""" |
| |
| if folder_name in FOLDER_TO_TASK_NAME: |
| return FOLDER_TO_TASK_NAME[folder_name] |
|
|
| |
| for folder_key, task_name in FOLDER_TO_TASK_NAME.items(): |
| if folder_key in folder_name or folder_name in folder_key: |
| return task_name |
|
|
| |
| task = folder_name.replace("_", " ").replace("-", " ") |
| return task.strip() |
|
|
|
|
| def _discover_robofail_files(dataset_path: Path) -> list[tuple[Path, str]]: |
| """Discover all video files in the RoboFail dataset structure. |
| |
| Expected structure: |
| robofail/real_data/ |
| folder_name_1/ |
| videos/ |
| color.mp4 |
| folder_name_2/ |
| videos/ |
| color.mp4 |
| ... |
| |
| Returns: |
| List of tuples: (video_file_path, task_name) |
| """ |
| trajectory_files: list[tuple[Path, str]] = [] |
|
|
| |
| real_data_path = dataset_path / "real_data" |
| if not real_data_path.exists(): |
| print(f"Warning: real_data directory not found at {real_data_path}") |
| return trajectory_files |
|
|
| for folder in real_data_path.iterdir(): |
| if not folder.is_dir(): |
| continue |
|
|
| folder_name = folder.name |
| task_name = _get_task_name_from_folder(folder_name) |
|
|
| |
| video_path = folder / "videos" / "color.mp4" |
| if video_path.exists(): |
| trajectory_files.append((video_path, task_name)) |
| else: |
| print(f"Warning: No color.mp4 found in {folder}/videos/") |
|
|
| return trajectory_files |
|
|
|
|
| def load_robofail_dataset(dataset_path: str, max_trajectories: int | None = None) -> dict[str, list[dict]]: |
| """Load RoboFail dataset and organize by task. |
| |
| Args: |
| dataset_path: Path to the RoboFail dataset directory (should contain real_data/) |
| max_trajectories: Maximum number of trajectories to load (None for all) |
| |
| Returns: |
| Dictionary mapping task names to lists of trajectory dictionaries |
| """ |
|
|
| print(f"Loading RoboFail dataset from: {dataset_path}") |
| print("=" * 100) |
| print("LOADING ROBOFAIL DATASET") |
| print("=" * 100) |
|
|
| dataset_path = Path(dataset_path) |
| if not dataset_path.exists(): |
| raise FileNotFoundError(f"RoboFail dataset path not found: {dataset_path}") |
|
|
| traj_files = _discover_robofail_files(dataset_path) |
| print(f"Found {len(traj_files)} trajectory files") |
|
|
| task_data: dict[str, list[dict]] = {} |
| loaded_count = 0 |
|
|
| for video_file, task_name in tqdm(traj_files, desc="Processing RoboFail trajectories"): |
| if max_trajectories is not None and loaded_count >= max_trajectories and max_trajectories != -1: |
| break |
|
|
| try: |
| |
| frame_loader = RoboFailFrameLoader( |
| file_path=str(video_file), |
| ) |
|
|
| |
| |
| actions = None |
|
|
| |
| task_description = task_name |
|
|
| |
| trajectory = { |
| "frames": frame_loader, |
| "actions": actions, |
| "is_robot": True, |
| "quality_label": "failure", |
| "preference_group_id": None, |
| "preference_rank": None, |
| "task": task_description, |
| "id": generate_unique_id(), |
| } |
|
|
| task_data.setdefault(task_name, []).append(trajectory) |
| loaded_count += 1 |
|
|
| except Exception as e: |
| print(f"Error loading trajectory {video_file}: {e}") |
| continue |
|
|
| total_trajectories = sum(len(v) for v in task_data.values()) |
| print(f"Loaded {total_trajectories} trajectories from {len(task_data)} tasks") |
|
|
| return task_data |
|
|