File size: 13,029 Bytes
319eb16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
#!/usr/bin/env python3
"""
H2R dataset loader for the generic dataset converter for Robometer model training.
https://huggingface.co/datasets/dannyXSC/HumanAndRobot
Human2Robot: Learning Robot Actions from Paired Human-Robot Videos
This module contains H2R-specific logic for loading and processing HDF5 files.

Updated to support OXE-style streaming conversion: write videos and build
HF entries on the fly, and return a ready `datasets.Dataset` to be pushed
or saved by the caller.
"""

import os
from pathlib import Path
from typing import Any

import h5py
import numpy as np
from dataset_upload.helpers import (
    create_hf_trajectory,
    generate_unique_id,
    load_sentence_transformer_model,
)
from tqdm import tqdm
from datasets import Dataset


class H2RFrameLoader:
    """Pickle-able loader that reads H2R frames from an HDF5 dataset on demand.

    Stores only simple fields so it can be safely passed across processes.
    """

    def __init__(self, hdf5_path: str, convert_to_rgb: bool = True):
        self.hdf5_path = hdf5_path
        self.convert_to_rgb = convert_to_rgb

    def __call__(self) -> tuple[np.ndarray, np.ndarray]:
        """Load frames from HDF5 when called.

        Returns:
            np.ndarray of shape (T, H, W, 3), dtype uint8
        """
        with h5py.File(self.hdf5_path, "r") as f:
            human_frames = f["/cam_data/human_camera"][:]
            robot_frames = f["/cam_data/robot_camera"][:]

        if self.convert_to_rgb:
            human_frames = human_frames[..., [2, 1, 0]]
            robot_frames = robot_frames[..., [2, 1, 0]]

        # Ensure shape and dtype sanity
        if not isinstance(human_frames, np.ndarray) or human_frames.ndim != 4 or human_frames.shape[-1] != 3:
            raise ValueError(f"Unexpected frames shape for {self.hdf5_path}: {getattr(human_frames, 'shape', None)}")

        if not isinstance(robot_frames, np.ndarray) or robot_frames.ndim != 4 or robot_frames.shape[-1] != 3:
            raise ValueError(f"Unexpected frames shape for {self.hdf5_path}: {getattr(robot_frames, 'shape', None)}")

        # Ensure uint8
        if human_frames.dtype != np.uint8:
            human_frames = human_frames.astype(np.uint8, copy=False)
        if robot_frames.dtype != np.uint8:
            robot_frames = robot_frames.astype(np.uint8, copy=False)

        return human_frames, robot_frames


# Task mapping from folder names to task descriptions
FOLDER_TO_TASK_NAME = {
    "grab_both_cubes_v1": "pick up each cube individually and place them onto the plate",
    "grab_cube2_v1": "pick up the cube and place it onto the plate",
    "grab_cup_v1": "move the cup from left to right",
    "grab_pencil1_v1": "pick up the marker and place it on the plate",
    "grab_pencil2_v1": "pick up the marker and place it on the plate",
    "grab_pencil_v1": "pick up the marker and place it on the plate",
    "grab_two_cubes2_v1": "pick up the green cube and place it onto the plate",
    "grab_to_plate1_and_back_v1": "put the red cube on the darker plate",
    "grab_to_plate1_v1": "pick up the red cube and place it onto the darker plate",
    "grab_to_plate2_v1": "pick up the red cube and place it onto the lighter plate",
    "grab_to_plate2_and_back_v1": "put the red cube on the yellow plate",
    "grab_to_plate2_and_pull_v1": "put the cube on the plate, then pull the plate from bottom to top",
    "pull_plate_grab_cube": "pull the plate from bottom to top, then pick up the cube and place it onto the plate",
    "pull_plate_v1": "pull the plate from bottom to top",
    "push_box_common_v1": "push the box from left to right",
    "push_box_random_v1": "push the box from left to right",
    "push_box_two_v1": "push the tissues from left to right",
    "push_plate_v1": "push the plate from top to bottom",
    # "roll": "pick up the brush and write on the table", # skipped because it's weird
    # "writing": "write aimlessly on the desk", # skipped because writing aimlessly is not helpful for reward modeling
}


def _get_task_name_from_folder(folder_name: str) -> str:
    """Convert folder name to task name using the mapping."""
    # First try to find exact match
    if folder_name in FOLDER_TO_TASK_NAME:
        return FOLDER_TO_TASK_NAME[folder_name]
    else:
        return None


def _discover_h2r_files(dataset_path: Path) -> list[tuple[Path, str]]:
    """Discover all video files in the H2R dataset structure.

    Expected structure:
    dataset_path/
        folder_name_1/
            hdf5_file_1.hdf5
            hdf5_file_2.hdf5
            hdf5_file_3.hdf5
            ...
        folder_name_2/
            hdf5_file_1.hdf5
            hdf5_file_2.hdf5
            hdf5_file_3.hdf5
            ...
        ...

    Returns:
        List of tuples: (hdf5_file_path, task_name)
    """
    trajectory_files: list[tuple[Path, str]] = []
    for folder in dataset_path.iterdir():
        if folder.is_dir():
            for file in folder.glob("*.hdf5"):
                trajectory_files.append((file, folder.name))

    return trajectory_files


def _stable_shard_for_index(index: int, shard_modulus: int = 1000) -> str:
    """Deterministically bucket an index into a shard directory name.

    Matches the naming style used in the OXE loader for consistent layout.
    """
    try:
        idx = int(index)
    except Exception:
        idx = abs(hash(str(index)))
    shard_index = idx // shard_modulus
    return f"shard_{shard_index:04d}"


def _build_h2r_video_paths(
    output_dir: str,
    dataset_label: str,
    episode_idx: int,
    role: str,
) -> tuple[str, str]:
    shard_dir = _stable_shard_for_index(episode_idx)
    episode_dir = os.path.join(output_dir, dataset_label.lower(), shard_dir, f"episode_{episode_idx:06d}")
    os.makedirs(episode_dir, exist_ok=True)
    filename = f"clip@{role}.mp4"
    full_path = os.path.join(episode_dir, filename)
    rel_path = os.path.join(dataset_label.lower(), shard_dir, f"episode_{episode_idx:06d}", filename)
    return full_path, rel_path


def _process_single_h2r_file(args):
    """Worker to process a single H2R HDF5 file into up to two entries.

    Returns a list of entries (human and/or robot), each with relative frame paths.
    """
    (
        file_path,
        folder_name,
        ep_idx,
        dataset_name,
        output_dir,
        max_frames,
        fps,
        task,
        lang_vec,
    ) = args

    entries: list[dict[str, Any]] = []

    # Load frames for this file (human and robot)
    human_frames, robot_frames = H2RFrameLoader(str(file_path))()

    # HUMAN entry
    full_h_path, rel_h_path = _build_h2r_video_paths(
        output_dir=output_dir,
        dataset_label=dataset_name,
        episode_idx=ep_idx,
        role="human",
    )
    human_traj = {
        "id": generate_unique_id(),
        "frames": human_frames,
        "task": task,
        "is_robot": False,
        "quality_label": "successful",
        "preference_group_id": None,
        "preference_rank": None,
    }
    human_entry = create_hf_trajectory(
        traj_dict=human_traj,
        video_path=full_h_path,
        lang_vector=lang_vec,
        max_frames=max_frames,
        dataset_name=dataset_name,
        use_video=True,
        fps=fps,
    )
    if human_entry:
        human_entry["frames"] = rel_h_path
        entries.append(human_entry)

    # ROBOT entry
    full_r_path, rel_r_path = _build_h2r_video_paths(
        output_dir=output_dir,
        dataset_label=dataset_name,
        episode_idx=ep_idx,
        role="robot",
    )
    robot_traj = {
        "id": generate_unique_id(),
        "frames": robot_frames,
        "task": task,
        "is_robot": True,
        "quality_label": "successful",
        "preference_group_id": None,
        "preference_rank": None,
    }
    robot_entry = create_hf_trajectory(
        traj_dict=robot_traj,
        video_path=full_r_path,
        lang_vector=lang_vec,
        max_frames=max_frames,
        dataset_name=dataset_name,
        use_video=True,
        fps=fps,
    )
    if robot_entry:
        robot_entry["frames"] = rel_r_path
        entries.append(robot_entry)

    return entries


def convert_h2r_dataset_to_hf(
    dataset_path: str,
    dataset_name: str,
    output_dir: str,
    max_trajectories: int | None = None,
    max_frames: int = 64,
    fps: int = 10,
    num_workers: int = -1,
) -> Dataset:
    """Convert the H2R dataset to HF format by writing videos directly.

    This mirrors the OXE loader's streaming approach: iterate files, write videos,
    assemble entries, and return a Dataset at the end.
    """

    if dataset_name is None:
        raise ValueError("dataset_name is required")

    base_path = Path(dataset_path)
    if not base_path.exists():
        raise FileNotFoundError(f"H2R dataset path not found: {base_path}")

    discovered = _discover_h2r_files(base_path)
    if len(discovered) == 0:
        # Return an empty dataset with expected columns
        return Dataset.from_dict({
            "id": [],
            "task": [],
            "lang_vector": [],
            "data_source": [],
            "frames": [],
            "is_robot": [],
            "quality_label": [],
            # keep schema compatible with helpers/create_hf_trajectory usage
            "preference_group_id": [],
            "preference_rank": [],
        })

    # Language model and cache (avoid recomputing for identical tasks)
    lang_model = load_sentence_transformer_model()
    lang_cache: dict[str, Any] = {}

    # Determine workers and batching (match OXE approach to control memory)
    if num_workers == -1:
        try:
            from multiprocessing import cpu_count as _cpu_count

            num_workers = min(_cpu_count(), 8)
        except Exception:
            num_workers = 1
    elif num_workers == 0:
        num_workers = 1

    batch_size = 64

    entries: list[dict[str, Any]] = []
    produced_pairs = 0  # count by file; each file can produce up to 2 entries
    max_limit = float("inf") if (max_trajectories is None or max_trajectories == -1) else int(max_trajectories)

    print(f"Found {len(discovered)} HDF5 files; processing in batches of {batch_size} with {num_workers} workers...")

    # Process files in batches
    file_batch: list[tuple[Path, str]] = []
    info_batch: list[tuple[int, str, Any]] = []  # (ep_idx, task, lang_vec)

    for ep_idx, (file_path, folder_name) in enumerate(tqdm(discovered, desc="Queuing H2R files")):
        if produced_pairs >= max_limit:
            break

        task = _get_task_name_from_folder(folder_name)
        if task is None:
            print("Skipping file: ", file_path)
            continue
        if task not in lang_cache:
            lang_cache[task] = lang_model.encode(task)
        lang_vec = lang_cache[task]

        file_batch.append((file_path, folder_name))
        info_batch.append((ep_idx, task, lang_vec))

        if len(file_batch) >= batch_size or ep_idx + 1 == len(discovered):
            # Build worker args
            worker_args = list(
                zip(
                    [f for (f, _) in file_batch],
                    [fn for (_, fn) in file_batch],
                    [i for (i, _, _) in info_batch],
                    [dataset_name] * len(file_batch),
                    [output_dir] * len(file_batch),
                    [max_frames] * len(file_batch),
                    [fps] * len(file_batch),
                    [t for (_, t, _) in info_batch],
                    [lv for (_, _, lv) in info_batch],
                    strict=False,
                )
            )

            if num_workers == 1:
                # Sequential processing
                for args in worker_args:
                    entries.extend(_process_single_h2r_file(args))
                    produced_pairs += 1
                    if produced_pairs >= max_limit:
                        break
            else:
                from multiprocessing import Pool

                with Pool(processes=num_workers) as pool:
                    results = list(
                        tqdm(
                            pool.imap_unordered(_process_single_h2r_file, worker_args),
                            total=len(worker_args),
                            desc=f"Processing batch (workers={num_workers})",
                        )
                    )
                for res in results:
                    entries.extend(res)
                    produced_pairs += 1
                    if produced_pairs >= max_limit:
                        break

            # Clear batch
            file_batch = []
            info_batch = []

    if not entries:
        return Dataset.from_dict({
            "id": [],
            "task": [],
            "lang_vector": [],
            "data_source": [],
            "frames": [],
            "is_robot": [],
            "quality_label": [],
            "preference_group_id": [],
            "preference_rank": [],
        })

    return Dataset.from_list(entries)