rpx_benchmark.dataset_hub.downloader
Selective download from the RPX HuggingFace dataset repo.
Resolves a (task, split, modalities, label_versions) request into a
list of glob patterns and hands them to snapshot_download. The HF
cache is content-addressed by file hash, so re-pulling a previously
fetched modality is a no-op — that's how delta downloads work for free
when the team switches tasks on already-cached scenes.
Wire-up::
pull = download_for_task(
task="segmentation", split="easy", repo_id="IRVLUTD/RPX",
)
pull.local_dir # local snapshot dir, ready to feed to a loader
pull.manifest_table # pyarrow.Table — per-frame metadata for the slice
pull.bytes_fetched # ≈ what the network actually moved (cache hits subtract)
1"""Selective download from the RPX HuggingFace dataset repo. 2 3Resolves a ``(task, split, modalities, label_versions)`` request into a 4list of glob patterns and hands them to ``snapshot_download``. The HF 5cache is content-addressed by file hash, so re-pulling a previously 6fetched modality is a no-op — that's how delta downloads work for free 7when the team switches tasks on already-cached scenes. 8 9Wire-up:: 10 11 pull = download_for_task( 12 task="segmentation", split="easy", repo_id="IRVLUTD/RPX", 13 ) 14 pull.local_dir # local snapshot dir, ready to feed to a loader 15 pull.manifest_table # pyarrow.Table — per-frame metadata for the slice 16 pull.bytes_fetched # ≈ what the network actually moved (cache hits subtract) 17""" 18 19from __future__ import annotations 20 21import json 22from dataclasses import dataclass 23from pathlib import Path 24from typing import ( 25 TYPE_CHECKING, 26 Any, 27 Iterable, 28 List, 29 Mapping, 30 Optional, 31 Sequence, 32 Set, 33) 34 35from ..exceptions import ConfigError, DownloadError 36from ..logging_utils import get_logger 37from .manifest import SCHEMA_VERSION 38from .packer import SCENE_ROOT_BY_TYPE 39from .recipes import ( 40 DEFAULT_REPO_ID, 41 QUESTIONNAIRE, 42 SceneType, 43 TaskRecipe, 44 resolve_recipe, 45) 46 47#: Scene types whose scenes carry a real difficulty split (ego inherits its 48#: split from its mos sibling via a shared scene_id — see ego_layout.py). 49_SPLIT_BEARING_TYPES = (SceneType.MULTI_OBJECT, SceneType.EGO) 50 51if TYPE_CHECKING: # pragma: no cover - only for type hints 52 import pyarrow as pa 53 54 55log = get_logger(__name__) 56 57 58# Modalities the packer treats as raw — no version suffix in the repo path. 59# Must stay in sync with packer.RAW_MODALITIES. 60_RAW_MODALITIES = frozenset({"rgb", "depth", "fisheye"}) 61 62# Modalities that live OUTSIDE the per-(scene, phase) tar shards, under 63# the shared ``objects_meta/<object_id>/`` directory. Their pull patterns 64# are derived from the per-scene mask_to_object lookups, not from 65# scene-rooted globs. 66_SHARED_MODALITIES = frozenset({QUESTIONNAIRE}) 67 68 69@dataclass(frozen=True) 70class DownloadResult: 71 """Outcome of one selective pull.""" 72 73 repo_id: str 74 revision: Optional[str] 75 task: str 76 split: Optional[str] 77 scene_type: SceneType 78 local_dir: Path 79 allow_patterns: List[str] 80 matched_scenes: List[str] 81 bytes_fetched: int 82 files_fetched: int 83 cache_hits: int = 0 84 manifest_table: Optional["pa.Table"] = None 85 86 87def _hub(): 88 try: 89 import huggingface_hub as hf # noqa: WPS433 90 except ImportError as e: 91 raise DownloadError( 92 "huggingface_hub is required for RPX downloads.", 93 hint="Install with: pip install 'rpx-benchmark[hub]'", 94 ) from e 95 return hf 96 97 98def _arrow(): 99 try: 100 import pyarrow as pa # noqa: WPS433 101 import pyarrow.parquet as pq # noqa: WPS433 102 except ImportError as e: 103 raise DownloadError( 104 "pyarrow is required to read the dataset manifest.", 105 hint="Install with: pip install 'rpx-benchmark[hub]'", 106 ) from e 107 return pa, pq 108 109 110# --------------------------------------------------------------------- # 111# Manifest helpers (pulled and read first; small file, always cheap) 112# --------------------------------------------------------------------- # 113 114 115def _fetch_manifest( 116 hf, 117 repo_id: str, 118 revision: Optional[str], 119 cache_dir: Optional[Path], 120) -> tuple[Path, Path]: 121 """Download just ``manifest/{frames_v1.parquet, current.json}``.""" 122 parquet_path = hf.hf_hub_download( 123 repo_id=repo_id, 124 repo_type="dataset", 125 filename=f"manifest/frames_{SCHEMA_VERSION}.parquet", 126 revision=revision, 127 cache_dir=str(cache_dir) if cache_dir else None, 128 ) 129 current_path = hf.hf_hub_download( 130 repo_id=repo_id, 131 repo_type="dataset", 132 filename="manifest/current.json", 133 revision=revision, 134 cache_dir=str(cache_dir) if cache_dir else None, 135 ) 136 return Path(parquet_path), Path(current_path) 137 138 139def _filter_manifest( 140 table: "pa.Table", 141 scene_type: SceneType, 142 split: Optional[str], 143) -> "pa.Table": 144 pa, _ = _arrow() 145 import pyarrow.compute as pc # noqa: WPS433 146 147 mask = pc.equal(table["scene_type"], pa.scalar(scene_type.value)) 148 if split is not None and scene_type in _SPLIT_BEARING_TYPES: 149 mask = pc.and_(mask, pc.equal(table["split"], pa.scalar(split))) 150 return table.filter(mask) 151 152 153# --------------------------------------------------------------------- # 154# Pattern resolution 155# --------------------------------------------------------------------- # 156 157 158def _resolve_label_version( 159 modality: str, 160 current: Mapping[str, Any], 161 explicit: Mapping[str, str], 162) -> str: 163 """Pick the label version for ``modality``: explicit > current.json > 'v1'.""" 164 if modality in explicit: 165 return explicit[modality] 166 versions = current.get("label_versions", {}) 167 return versions.get(modality, "v1") 168 169 170def _scene_root_for(scene_type: SceneType) -> str: 171 return SCENE_ROOT_BY_TYPE[scene_type] 172 173 174def _phase_glob_segment(scene_type: SceneType) -> str: 175 """The glob segment standing in for "phase" in an allow_pattern. 176 177 ego now nests under the SAME scenes/<scene>/ root as mos (as a 178 literal "ego" segment alongside the numeric 0/1/2/... phases -- see 179 packer.phase_segment), so a bare "*" would match BOTH: a mos download 180 would incidentally also sweep up scenes/<scene>/ego/rgb.tar, and vice 181 versa. "[0-9]*" matches only digit-leading (numeric-phase) segments; 182 ego's literal "ego" doesn't match it, and "ego" is used verbatim for 183 ego scene_type -- so each family's pull stays scoped to itself. 184 """ 185 return "ego" if scene_type is SceneType.EGO else "[0-9]*" 186 187 188def _build_allow_patterns( 189 matched_scenes: Iterable[str], 190 scene_type: SceneType, 191 modalities: Iterable[str], 192 current: Mapping[str, Any], 193 label_versions: Mapping[str, str], 194) -> List[str]: 195 """One glob per (scene, modality). Shared modalities (questionnaire) 196 are *not* expanded here — see :func:`_shared_artefact_patterns` for 197 the per-object dedup pull. 198 """ 199 root = _scene_root_for(scene_type) 200 phase_seg = _phase_glob_segment(scene_type) 201 patterns: List[str] = [] 202 for scene in sorted(matched_scenes): 203 for m in sorted(modalities): 204 if m in _SHARED_MODALITIES: 205 continue # handled by _shared_artefact_patterns 206 if m in _RAW_MODALITIES: 207 patterns.append(f"{root}/{scene}/{phase_seg}/{m}.tar") 208 else: 209 v = _resolve_label_version(m, current, label_versions) 210 patterns.append(f"{root}/{scene}/{phase_seg}/labels/{m}/{v}.tar") 211 return patterns 212 213 214def _shared_artefact_patterns(modalities: Iterable[str]) -> List[str]: 215 """Build allow_patterns for the ``objects_meta/`` layer. 216 217 For now we pull *all* per-object questionnaires when a recipe asks 218 for ``questionnaire`` — they are tiny (~220 small JSON files). A 219 future optimisation would resolve the exact set of object IDs from 220 the matched scenes' ``mask_to_object.json`` files (already in the 221 sam2_meta tars), but the saving is negligible. 222 """ 223 patterns: List[str] = [] 224 if QUESTIONNAIRE in modalities: 225 patterns.append("objects_meta/*/questionnaire.json") 226 return patterns 227 228 229# --------------------------------------------------------------------- # 230# Public API 231# --------------------------------------------------------------------- # 232 233 234def download_for_task( 235 task: str, 236 split: Optional[str] = None, 237 repo_id: str = DEFAULT_REPO_ID, 238 *, 239 revision: Optional[str] = None, 240 cache_dir: Optional[Path] = None, 241 extra_modalities: Sequence[str] = (), 242 label_versions: Optional[Mapping[str, str]] = None, 243 scene_type: Optional[SceneType] = None, 244 max_workers: int = 8, 245 return_manifest: bool = True, 246) -> DownloadResult: 247 """Pull just the tar shards needed to run ``task`` on ``split``. 248 249 Parameters 250 ---------- 251 task : str 252 Recipe key from :data:`MULTI_OBJECT_TASK_RECIPES` or 253 :data:`SINGLE_OBJECT_TASK_RECIPES`. 254 split : str, optional 255 ``"easy" | "medium" | "hard"``. Required for multi-object tasks, 256 forbidden for single-object tasks (which have no splits). 257 repo_id : str 258 HuggingFace dataset repo (default: ``IRVLUTD/RPX``). 259 extra_modalities : Sequence[str] 260 Add modalities beyond the recipe's defaults (e.g. ``["masks_aux"]`` 261 if you want the visualisation overlays alongside ``masks``). 262 label_versions : Mapping[str, str], optional 263 Override which label version to pull for one or more modalities. 264 Default behaviour is to follow the repo's ``manifest/current.json``. 265 scene_type : SceneType, optional 266 Disambiguates when a task name exists in both recipe tables. 267 268 Returns 269 ------- 270 DownloadResult 271 ``local_dir`` is the local snapshot root; combine with 272 ``allow_patterns`` and ``manifest_table`` to feed a downstream 273 loader. ``bytes_fetched`` reports the size on disk after the pull 274 (for true delta accounting prefer your own ``du -sh``). 275 """ 276 hf = _hub() 277 recipe: TaskRecipe = resolve_recipe(task, scene_type=scene_type) 278 279 if recipe.scene_type is SceneType.SINGLE_OBJECT and split is not None: 280 raise ConfigError( 281 f"task {task!r} targets single-object scenes, which have no splits.", 282 hint="Drop the --split argument for single-object recipes.", 283 ) 284 if recipe.scene_type in _SPLIT_BEARING_TYPES and split is None: 285 raise ConfigError( 286 f"task {task!r} targets {recipe.scene_type.value} scenes — pick a split.", 287 hint="Pass split='easy' | 'medium' | 'hard'.", 288 ) 289 290 parquet_path, current_path = _fetch_manifest( 291 hf, 292 repo_id, 293 revision, 294 cache_dir, 295 ) 296 current = json.loads(current_path.read_text(encoding="utf-8")) 297 _, pq = _arrow() 298 table = pq.read_table(parquet_path) 299 sliced = _filter_manifest(table, recipe.scene_type, split) 300 301 matched_scenes: Set[str] = set(sliced["scene_id"].to_pylist()) 302 if not matched_scenes: 303 raise DownloadError( 304 f"No scenes matched task={task!r} split={split!r} on {repo_id}.", 305 hint=( 306 "Verify the manifest has rows for this scene_type/split. " 307 "If the dataset was uploaded without a `splits=...` argument " 308 "to build_frame_manifest, multi-object rows will all have " 309 "split=null." 310 ), 311 ) 312 313 modalities = set(recipe.all_modalities()) | set(extra_modalities) 314 explicit = dict(label_versions or {}) 315 patterns = _build_allow_patterns( 316 matched_scenes, 317 recipe.scene_type, 318 modalities, 319 current, 320 explicit, 321 ) 322 patterns.extend(_shared_artefact_patterns(modalities)) 323 324 log.info( 325 "download plan: task=%s split=%s scenes=%d modalities=%s patterns=%d", 326 task, 327 split, 328 len(matched_scenes), 329 sorted(modalities), 330 len(patterns), 331 ) 332 333 try: 334 local_dir = hf.snapshot_download( 335 repo_id=repo_id, 336 repo_type="dataset", 337 allow_patterns=patterns, 338 revision=revision, 339 cache_dir=str(cache_dir) if cache_dir else None, 340 max_workers=max_workers, 341 ) 342 except Exception as e: 343 raise DownloadError( 344 f"snapshot_download failed for {repo_id}: {e}", 345 hint=( 346 "Re-run to retry; HF caches partial downloads. If auth is the " 347 "issue, run `hf auth login` and confirm read access." 348 ), 349 ) from e 350 351 local = Path(local_dir) 352 353 # Mirror the post-fetch state that hub.download_split produces: extract 354 # every tar shard to flat per-modality dirs so downstream consumers 355 # (loader, viz, custom scripts) hit pure disk reads instead of opening 356 # and seeking tars per frame. Idempotent — files already on disk skip. 357 from ..hub import _extract_snapshot_tars 358 _extract_snapshot_tars(local) 359 360 files = [p for p in local.rglob("*") if p.is_file()] 361 bytes_ = sum(p.stat().st_size for p in files) 362 363 return DownloadResult( 364 repo_id=repo_id, 365 revision=revision, 366 task=task, 367 split=split, 368 scene_type=recipe.scene_type, 369 local_dir=local, 370 allow_patterns=patterns, 371 matched_scenes=sorted(matched_scenes), 372 bytes_fetched=bytes_, 373 files_fetched=len(files), 374 cache_hits=0, # HF doesn't expose per-call hit count 375 manifest_table=sliced if return_manifest else None, 376 )
70@dataclass(frozen=True) 71class DownloadResult: 72 """Outcome of one selective pull.""" 73 74 repo_id: str 75 revision: Optional[str] 76 task: str 77 split: Optional[str] 78 scene_type: SceneType 79 local_dir: Path 80 allow_patterns: List[str] 81 matched_scenes: List[str] 82 bytes_fetched: int 83 files_fetched: int 84 cache_hits: int = 0 85 manifest_table: Optional["pa.Table"] = None
Outcome of one selective pull.
235def download_for_task( 236 task: str, 237 split: Optional[str] = None, 238 repo_id: str = DEFAULT_REPO_ID, 239 *, 240 revision: Optional[str] = None, 241 cache_dir: Optional[Path] = None, 242 extra_modalities: Sequence[str] = (), 243 label_versions: Optional[Mapping[str, str]] = None, 244 scene_type: Optional[SceneType] = None, 245 max_workers: int = 8, 246 return_manifest: bool = True, 247) -> DownloadResult: 248 """Pull just the tar shards needed to run ``task`` on ``split``. 249 250 Parameters 251 ---------- 252 task : str 253 Recipe key from :data:`MULTI_OBJECT_TASK_RECIPES` or 254 :data:`SINGLE_OBJECT_TASK_RECIPES`. 255 split : str, optional 256 ``"easy" | "medium" | "hard"``. Required for multi-object tasks, 257 forbidden for single-object tasks (which have no splits). 258 repo_id : str 259 HuggingFace dataset repo (default: ``IRVLUTD/RPX``). 260 extra_modalities : Sequence[str] 261 Add modalities beyond the recipe's defaults (e.g. ``["masks_aux"]`` 262 if you want the visualisation overlays alongside ``masks``). 263 label_versions : Mapping[str, str], optional 264 Override which label version to pull for one or more modalities. 265 Default behaviour is to follow the repo's ``manifest/current.json``. 266 scene_type : SceneType, optional 267 Disambiguates when a task name exists in both recipe tables. 268 269 Returns 270 ------- 271 DownloadResult 272 ``local_dir`` is the local snapshot root; combine with 273 ``allow_patterns`` and ``manifest_table`` to feed a downstream 274 loader. ``bytes_fetched`` reports the size on disk after the pull 275 (for true delta accounting prefer your own ``du -sh``). 276 """ 277 hf = _hub() 278 recipe: TaskRecipe = resolve_recipe(task, scene_type=scene_type) 279 280 if recipe.scene_type is SceneType.SINGLE_OBJECT and split is not None: 281 raise ConfigError( 282 f"task {task!r} targets single-object scenes, which have no splits.", 283 hint="Drop the --split argument for single-object recipes.", 284 ) 285 if recipe.scene_type in _SPLIT_BEARING_TYPES and split is None: 286 raise ConfigError( 287 f"task {task!r} targets {recipe.scene_type.value} scenes — pick a split.", 288 hint="Pass split='easy' | 'medium' | 'hard'.", 289 ) 290 291 parquet_path, current_path = _fetch_manifest( 292 hf, 293 repo_id, 294 revision, 295 cache_dir, 296 ) 297 current = json.loads(current_path.read_text(encoding="utf-8")) 298 _, pq = _arrow() 299 table = pq.read_table(parquet_path) 300 sliced = _filter_manifest(table, recipe.scene_type, split) 301 302 matched_scenes: Set[str] = set(sliced["scene_id"].to_pylist()) 303 if not matched_scenes: 304 raise DownloadError( 305 f"No scenes matched task={task!r} split={split!r} on {repo_id}.", 306 hint=( 307 "Verify the manifest has rows for this scene_type/split. " 308 "If the dataset was uploaded without a `splits=...` argument " 309 "to build_frame_manifest, multi-object rows will all have " 310 "split=null." 311 ), 312 ) 313 314 modalities = set(recipe.all_modalities()) | set(extra_modalities) 315 explicit = dict(label_versions or {}) 316 patterns = _build_allow_patterns( 317 matched_scenes, 318 recipe.scene_type, 319 modalities, 320 current, 321 explicit, 322 ) 323 patterns.extend(_shared_artefact_patterns(modalities)) 324 325 log.info( 326 "download plan: task=%s split=%s scenes=%d modalities=%s patterns=%d", 327 task, 328 split, 329 len(matched_scenes), 330 sorted(modalities), 331 len(patterns), 332 ) 333 334 try: 335 local_dir = hf.snapshot_download( 336 repo_id=repo_id, 337 repo_type="dataset", 338 allow_patterns=patterns, 339 revision=revision, 340 cache_dir=str(cache_dir) if cache_dir else None, 341 max_workers=max_workers, 342 ) 343 except Exception as e: 344 raise DownloadError( 345 f"snapshot_download failed for {repo_id}: {e}", 346 hint=( 347 "Re-run to retry; HF caches partial downloads. If auth is the " 348 "issue, run `hf auth login` and confirm read access." 349 ), 350 ) from e 351 352 local = Path(local_dir) 353 354 # Mirror the post-fetch state that hub.download_split produces: extract 355 # every tar shard to flat per-modality dirs so downstream consumers 356 # (loader, viz, custom scripts) hit pure disk reads instead of opening 357 # and seeking tars per frame. Idempotent — files already on disk skip. 358 from ..hub import _extract_snapshot_tars 359 _extract_snapshot_tars(local) 360 361 files = [p for p in local.rglob("*") if p.is_file()] 362 bytes_ = sum(p.stat().st_size for p in files) 363 364 return DownloadResult( 365 repo_id=repo_id, 366 revision=revision, 367 task=task, 368 split=split, 369 scene_type=recipe.scene_type, 370 local_dir=local, 371 allow_patterns=patterns, 372 matched_scenes=sorted(matched_scenes), 373 bytes_fetched=bytes_, 374 files_fetched=len(files), 375 cache_hits=0, # HF doesn't expose per-call hit count 376 manifest_table=sliced if return_manifest else None, 377 )
Pull just the tar shards needed to run task on split.
Parameters
task : str
Recipe key from MULTI_OBJECT_TASK_RECIPES or
SINGLE_OBJECT_TASK_RECIPES.
split : str, optional
"easy" | "medium" | "hard". Required for multi-object tasks,
forbidden for single-object tasks (which have no splits).
repo_id : str
HuggingFace dataset repo (default: IRVLUTD/RPX).
extra_modalities : Sequence[str]
Add modalities beyond the recipe's defaults (e.g. ["masks_aux"]
if you want the visualisation overlays alongside masks).
label_versions : Mapping[str, str], optional
Override which label version to pull for one or more modalities.
Default behaviour is to follow the repo's manifest/current.json.
scene_type : SceneType, optional
Disambiguates when a task name exists in both recipe tables.
Returns
DownloadResult
local_dir is the local snapshot root; combine with
allow_patterns and manifest_table to feed a downstream
loader. bytes_fetched reports the size on disk after the pull
(for true delta accounting prefer your own du -sh).