Spaces:
Running on Zero
Running on Zero
File size: 7,320 Bytes
34f3bc9 | 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 | """Thin adapter wrapper around `RoboInterVQADataset` so it can be plumbed
through the existing `build_dataset` → `TransformedAdapterDataset` pipeline.
VQA repos do not have a LeRobot info.json. Detection in `create_adapter`:
the repo root must contain a `meta/vqa_manifest.json` file describing which
JSON files to load and which task family they belong to.
`vqa_manifest.json` schema (minimal):
{
"task_family": "understanding" | "generation_short" | "generation_traj"
| "task_planning",
"image_root_subdir": "Understanding/image/train/droid", // relative to repo root
"json_files": [
"Understanding/meta/train/droid/contact_decide.json",
"Understanding/meta/train/droid/grounding_choice.json"
]
}
The image_root_subdir resolves relative paths in the JSON `images` field
(LLaVA-style path). Image paths inside JSON typically include the same prefix
already, so `image_root_subdir` is usually the repo root itself ("").
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from .base import BaseAdapter
@dataclass
class _VQAAdapterMeta:
"""Mimic the ``DatasetMeta`` interface that build_dataset uses.
Satisfies the full surface (``total_episodes``, ``total_frames``,
``camera_keys``, ``episodes``, ``require_schema()``) with minimal correct
values: VQA has no robot trajectory (one "episode" per sample, no shard
frame ranges, cameras from the schema's ``image_mapping``).
"""
schema: Any
stats: dict
fps: float = 1.0
robot_type: str = "vqa_no_robot"
total_episodes: int = 0
total_frames: int = 0
video_keys: tuple = ()
camera_keys: tuple = ()
features: dict = None
# MultiLeRobotDataset reads meta.episodes["dataset_from_index"/"to_index"].
# VQA samples aren't packed into shards, so expose empty parallel arrays.
episodes: dict = None
def __post_init__(self):
if self.features is None:
self.features = {}
if self.episodes is None:
self.episodes = {
"dataset_from_index": [],
"dataset_to_index": [],
}
def require_schema(self):
"""Return ``self.schema`` or raise if it is still ``None``.
Mirrors ``DatasetMeta.require_schema`` so generic code can enforce the
schema-present invariant uniformly across LeRobot and VQA adapters.
"""
if self.schema is None:
from src.schema.errors import SchemaDiscoveryError
raise SchemaDiscoveryError(
"_VQAAdapterMeta.schema is None after adapter construction. "
"VQAAdapter must resolve a schema (override_schema, or "
"manifest schema_name) before handing meta to the pipeline."
)
return self.schema
class VQAAdapter(BaseAdapter):
"""Adapter wrapping RoboInterVQADataset, satisfying the LeRobot adapter
surface that build_dataset uses (.meta with .schema/.stats, .num_episodes,
.num_frames, __len__/__getitem__). A ``BaseAdapter`` subclass so generic
factory consumers get the full documented contract.
"""
def __init__(
self,
repo_id: str,
root: str,
override_schema: Any = None,
# Other adapter kwargs accepted for API compatibility (ignored by VQA):
delta_timestamps: dict | None = None,
image_transforms=None,
external_stats: dict | None = None,
use_external_stats: bool = False,
video_backend: str | None = None,
episode_filter=None,
):
self.repo_id = repo_id
self.root = Path(root)
# VQA normalization stats are the built-in identity VQA_STATS
# (state/action are zero-filled). Fail loud rather than silently ignore
# an external-stats override so callers don't believe it took effect.
if external_stats or use_external_stats:
raise ValueError(
"VQAAdapter does not support external_stats: VQA samples have "
"zero-filled state/action and MUST use the built-in identity "
f"VQA_STATS (got external_stats={external_stats!r}, "
f"use_external_stats={use_external_stats!r}). Remove the "
"external-stats override for VQA repos, or exclude this repo "
"from external-stats resolution."
)
manifest_path = self.root / "meta" / "vqa_manifest.json"
if not manifest_path.is_file():
raise FileNotFoundError(
f"VQAAdapter expects {manifest_path} (vqa_manifest.json missing). "
f"See src/adapters/vqa_adapter.py docstring for schema."
)
with open(manifest_path) as f:
manifest = json.load(f)
# Resolve schema: caller override > manifest `schema_name`. Require an
# explicit schema_name (no silent default) so a corpus with different
# semantics isn't bound to the wrong schema.
if override_schema is not None:
schema = override_schema
else:
from src.schema.registry import resolve as _resolve_schema
schema_name = manifest.get("schema_name")
if not schema_name:
raise ValueError(
f"VQA manifest {manifest_path} is missing required "
"'schema_name'. Add an explicit schema_name (e.g. "
"\"robointer_vqa\") to the manifest, or pass "
"override_schema. Refusing to default silently so a "
"non-robointer_vqa corpus is not bound to the wrong schema."
)
schema = _resolve_schema(schema_name)
from src.dataset.adapters.robointer_vqa_adapter import RoboInterVQADataset
json_paths = [
os.path.join(root, jp) for jp in manifest["json_files"]
]
image_root = os.path.join(root, manifest.get("image_root_subdir", ""))
task_family = manifest["task_family"]
self._dataset = RoboInterVQADataset(
json_paths=json_paths,
image_root=image_root,
task_family=task_family,
schema=schema,
)
# One "episode" per VQA sample so PI0Mixture n^0.43 weighting and the
# inherited num_episodes/num_frames properties (read from meta) work.
n = len(self._dataset)
# VQA has no on-disk video; the only image slot is the schema's
# image_mapping target(s) (e.g. observation.images.primary).
camera_keys = tuple(
getattr(schema, "image_mapping", None) or {}
)
# Identity stats: state/action are zero-filled so identity normalization
# leaves them at zero. Populating observation.state/action/action_abs
# also satisfies the action_mode='abs' guard.
from schemas.robointer_vqa import VQA_STATS as _VQA_STATS
self.meta = _VQAAdapterMeta(
schema=schema,
stats=dict(_VQA_STATS),
total_episodes=n,
total_frames=n,
camera_keys=camera_keys,
)
def __len__(self) -> int:
return len(self._dataset)
def __getitem__(self, idx: int) -> dict:
return self._dataset[idx]
|