oopsie-data-tools 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oopsie_data_tools/__init__.py +7 -0
- oopsie_data_tools/annotation_tool/__init__.py +5 -0
- oopsie_data_tools/annotation_tool/annotation_schema.py +266 -0
- oopsie_data_tools/annotation_tool/annotator_server.py +850 -0
- oopsie_data_tools/annotation_tool/episode_recorder.py +648 -0
- oopsie_data_tools/annotation_tool/rollout_annotator.py +333 -0
- oopsie_data_tools/annotation_tool/ui/annotator.html +2240 -0
- oopsie_data_tools/cli.py +894 -0
- oopsie_data_tools/init_wizard.py +329 -0
- oopsie_data_tools/skill/SKILL.md +98 -0
- oopsie_data_tools/skill/reference/conversion.md +257 -0
- oopsie_data_tools/skill/reference/format.md +112 -0
- oopsie_data_tools/skill/reference/robot-profile.md +104 -0
- oopsie_data_tools/skill/reference/setup.md +216 -0
- oopsie_data_tools/skill/reference/troubleshooting.md +97 -0
- oopsie_data_tools/test/__init__.py +1 -0
- oopsie_data_tools/test/conftest.py +174 -0
- oopsie_data_tools/test/fixtures/__init__.py +0 -0
- oopsie_data_tools/test/fixtures/make_invalid.py +633 -0
- oopsie_data_tools/test/fixtures/make_valid.py +406 -0
- oopsie_data_tools/test/test_annotation_completeness.py +154 -0
- oopsie_data_tools/test/test_annotation_schema.py +190 -0
- oopsie_data_tools/test/test_annotator_server.py +240 -0
- oopsie_data_tools/test/test_annotator_server_guards.py +146 -0
- oopsie_data_tools/test/test_bulk_inference_end_to_end.py +112 -0
- oopsie_data_tools/test/test_cli_config.py +111 -0
- oopsie_data_tools/test/test_cli_tools.py +438 -0
- oopsie_data_tools/test/test_contributor_config.py +38 -0
- oopsie_data_tools/test/test_conversion_utils_annotations.py +75 -0
- oopsie_data_tools/test/test_credentials_location.py +106 -0
- oopsie_data_tools/test/test_diversity.py +33 -0
- oopsie_data_tools/test/test_episode_recorder.py +335 -0
- oopsie_data_tools/test/test_episode_video_paths.py +173 -0
- oopsie_data_tools/test/test_hf_upload.py +234 -0
- oopsie_data_tools/test/test_init_wizard.py +193 -0
- oopsie_data_tools/test/test_install_skill.py +120 -0
- oopsie_data_tools/test/test_migrate_taxonomy_v2.py +255 -0
- oopsie_data_tools/test/test_new_profile.py +84 -0
- oopsie_data_tools/test/test_paths.py +137 -0
- oopsie_data_tools/test/test_python38_compat.py +79 -0
- oopsie_data_tools/test/test_record_step_purity.py +127 -0
- oopsie_data_tools/test/test_robot_setup.py +244 -0
- oopsie_data_tools/test/test_rollout_annotator.py +134 -0
- oopsie_data_tools/test/test_rotation_utils.py +126 -0
- oopsie_data_tools/test/test_validate.py +494 -0
- oopsie_data_tools/utils/__init__.py +1 -0
- oopsie_data_tools/utils/claude_skill.py +96 -0
- oopsie_data_tools/utils/contributor_config.py +91 -0
- oopsie_data_tools/utils/conversion_utils.py +220 -0
- oopsie_data_tools/utils/h5.py +60 -0
- oopsie_data_tools/utils/h5_inspect.py +167 -0
- oopsie_data_tools/utils/hf_limits.py +22 -0
- oopsie_data_tools/utils/hf_upload.py +235 -0
- oopsie_data_tools/utils/log.py +35 -0
- oopsie_data_tools/utils/migrate_taxonomy_v2.py +215 -0
- oopsie_data_tools/utils/paths.py +162 -0
- oopsie_data_tools/utils/restructure.py +402 -0
- oopsie_data_tools/utils/robot_profile/__init__.py +1 -0
- oopsie_data_tools/utils/robot_profile/robot_profile.py +240 -0
- oopsie_data_tools/utils/robot_profile/rotation_utils.py +119 -0
- oopsie_data_tools/utils/robot_profile/template.py +108 -0
- oopsie_data_tools/utils/validation/__init__.py +5 -0
- oopsie_data_tools/utils/validation/annotation_completeness.py +67 -0
- oopsie_data_tools/utils/validation/diversity.py +93 -0
- oopsie_data_tools/utils/validation/episode_data.py +54 -0
- oopsie_data_tools/utils/validation/episode_loader.py +201 -0
- oopsie_data_tools/utils/validation/episode_validator.py +315 -0
- oopsie_data_tools/utils/validation/errors.py +17 -0
- oopsie_data_tools/utils/validation/validation_utils.py +88 -0
- oopsie_data_tools-0.2.0.dist-info/METADATA +131 -0
- oopsie_data_tools-0.2.0.dist-info/RECORD +74 -0
- oopsie_data_tools-0.2.0.dist-info/WHEEL +4 -0
- oopsie_data_tools-0.2.0.dist-info/entry_points.txt +2 -0
- oopsie_data_tools-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Semantic validation of EpisodeData.
|
|
2
|
+
|
|
3
|
+
All checks here operate on in-memory data (numpy arrays, VideoInfo structs)
|
|
4
|
+
with no file I/O. This makes the same validation callable from:
|
|
5
|
+
- The HDF5 validation pipeline (after episode_loader produces EpisodeData)
|
|
6
|
+
- EpisodeRecorder pre-save (build EpisodeData from in-memory buffers first)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from oopsie_data_tools.annotation_tool.annotation_schema import (
|
|
17
|
+
OUTCOME_SUCCESS,
|
|
18
|
+
OUTCOMES,
|
|
19
|
+
SUCCESS_THRESHOLD,
|
|
20
|
+
)
|
|
21
|
+
from oopsie_data_tools.utils.validation.episode_data import EpisodeData
|
|
22
|
+
from oopsie_data_tools.utils.validation.errors import EpisodeValidationError
|
|
23
|
+
|
|
24
|
+
MAX_IMAGE_SIZE = 1280
|
|
25
|
+
MIN_IMAGE_SIZE = 180
|
|
26
|
+
# Bounds on episode *duration*, not step count: trajectory_length / control_freq.
|
|
27
|
+
MIN_EPISODE_DURATION_S = 1
|
|
28
|
+
MAX_EPISODE_DURATION_S = 600
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_episode(data: EpisodeData, strict_annotation_check: bool = False) -> None:
|
|
32
|
+
"""Run all semantic checks on a loaded EpisodeData.
|
|
33
|
+
|
|
34
|
+
Raises EpisodeValidationError with a descriptive message on the first failure.
|
|
35
|
+
"""
|
|
36
|
+
_validate_metadata(data)
|
|
37
|
+
_validate_profile_consistency(data)
|
|
38
|
+
_validate_trajectory_lengths(data)
|
|
39
|
+
_validate_video_specs(data)
|
|
40
|
+
if strict_annotation_check:
|
|
41
|
+
if not data.annotations:
|
|
42
|
+
raise EpisodeValidationError("Annotations dict is empty, must be provided for upload")
|
|
43
|
+
_validate_annotations(data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Individual checks ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _validate_metadata(data: EpisodeData) -> None:
|
|
50
|
+
if not data.language_instruction:
|
|
51
|
+
raise EpisodeValidationError("language_instruction is empty")
|
|
52
|
+
if not data.episode_id:
|
|
53
|
+
raise EpisodeValidationError("episode_id is empty")
|
|
54
|
+
if not data.lab_id:
|
|
55
|
+
raise EpisodeValidationError("lab_id is empty")
|
|
56
|
+
if data.lab_id == "your_lab_id":
|
|
57
|
+
raise EpisodeValidationError("lab_id has not been changed from the placeholder value")
|
|
58
|
+
if not data.operator_name:
|
|
59
|
+
raise EpisodeValidationError("operator_name is empty")
|
|
60
|
+
if not (data.control_freq > 0):
|
|
61
|
+
raise EpisodeValidationError("control_freq must be > 0")
|
|
62
|
+
duration_s = data.trajectory_length / data.control_freq
|
|
63
|
+
if not (MIN_EPISODE_DURATION_S <= duration_s <= MAX_EPISODE_DURATION_S):
|
|
64
|
+
raise EpisodeValidationError(
|
|
65
|
+
f"episode duration {duration_s:.2f}s out of range "
|
|
66
|
+
f"[{MIN_EPISODE_DURATION_S}, {MAX_EPISODE_DURATION_S}]s "
|
|
67
|
+
f"({data.trajectory_length} steps at {data.control_freq:g} Hz)"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_profile_consistency(data: EpisodeData) -> None:
|
|
72
|
+
"""Check that observations, actions, and videos match the embedded robot profile."""
|
|
73
|
+
profile = data.robot_profile
|
|
74
|
+
|
|
75
|
+
for key in profile.robot_state_keys:
|
|
76
|
+
if key not in data.observations:
|
|
77
|
+
raise EpisodeValidationError(
|
|
78
|
+
f"Missing observations key required by profile: {key}. Got {list(data.observations.keys())}, required by profile.robot_state_keys={profile.robot_state_keys}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
for key in profile.action_space:
|
|
82
|
+
if key not in data.actions:
|
|
83
|
+
raise EpisodeValidationError(
|
|
84
|
+
f"Missing actions key required by profile.action_space: {key}. Got {list(data.actions.keys())}, required by profile.action_space={profile.action_space}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for cam in profile.camera_names:
|
|
88
|
+
if cam not in data.videos:
|
|
89
|
+
raise EpisodeValidationError(
|
|
90
|
+
f"Missing video for camera required by profile: {cam}. Got {list(data.videos.keys())}, required by profile.camera_names={profile.camera_names}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# The reverse direction. The profile is the episode's documentation, so anything present
|
|
94
|
+
# but undeclared has no joint names, no units and no DOF to check against, and cannot be
|
|
95
|
+
# interpreted by anyone downstream. EpisodeRecorder cannot produce this — it writes
|
|
96
|
+
# exactly profile.robot_state_keys — so this only ever catches hand-built or externally
|
|
97
|
+
# converted files.
|
|
98
|
+
_reject_undeclared(
|
|
99
|
+
"observations/robot_states", data.observations, profile.robot_state_keys,
|
|
100
|
+
"profile.robot_state_keys",
|
|
101
|
+
)
|
|
102
|
+
_reject_undeclared("actions", data.actions, profile.action_space, "profile.action_space")
|
|
103
|
+
|
|
104
|
+
_validate_cartesian_arm_count(data)
|
|
105
|
+
|
|
106
|
+
jp_obs = data.observations.get("joint_position")
|
|
107
|
+
if jp_obs is not None and jp_obs.ndim >= 2:
|
|
108
|
+
if len(profile.robot_state_joint_names) != jp_obs.shape[-1]:
|
|
109
|
+
raise EpisodeValidationError(
|
|
110
|
+
"robot_state_joint_names count does not match observations/joint_position DOF: "
|
|
111
|
+
f"the robot profile lists {len(profile.robot_state_joint_names)} joint name(s) in "
|
|
112
|
+
f"robot_state_joint_names, but the recorded observations/joint_position has "
|
|
113
|
+
f"{jp_obs.shape[-1]} DOF (last axis). Fix robot_state_joint_names in the robot "
|
|
114
|
+
"profile (or the recorded joint_position) so the two counts match."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if profile.action_joint_names:
|
|
118
|
+
for key in ("joint_position", "joint_velocity"):
|
|
119
|
+
arr = data.actions.get(key)
|
|
120
|
+
if arr is not None and arr.ndim >= 2:
|
|
121
|
+
if len(profile.action_joint_names) != arr.shape[-1]:
|
|
122
|
+
raise EpisodeValidationError(
|
|
123
|
+
f"action_joint_names count does not match actions/{key} DOF: "
|
|
124
|
+
f"the robot profile lists {len(profile.action_joint_names)} joint name(s) in "
|
|
125
|
+
f"action_joint_names, but the recorded actions/{key} has {arr.shape[-1]} DOF "
|
|
126
|
+
"(last axis). Fix action_joint_names in the robot profile (or the recorded "
|
|
127
|
+
"actions) so the two counts match."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _reject_undeclared(
|
|
132
|
+
group: str, present: dict[str, Any], declared: list[str], declared_by: str
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Every dataset in ``group`` must be declared by the profile."""
|
|
135
|
+
undeclared = sorted(set(present) - set(declared))
|
|
136
|
+
if undeclared:
|
|
137
|
+
raise EpisodeValidationError(
|
|
138
|
+
f"{group} contains {len(undeclared)} key(s) the robot profile does not declare: "
|
|
139
|
+
f"{undeclared}. The profile is what documents an episode, so undeclared data has "
|
|
140
|
+
f"no joint names, units or expected DOF, and nothing downstream can interpret it. "
|
|
141
|
+
f"Add the key(s) to {declared_by}, or stop recording them. "
|
|
142
|
+
f"{declared_by}={list(declared)}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _validate_cartesian_arm_count(data: EpisodeData) -> None:
|
|
147
|
+
"""A cartesian pose must carry one arm's worth of DOF per arm the profile declares.
|
|
148
|
+
|
|
149
|
+
``[x, y, z, qx, qy, qz, qw]`` is 7 values, so a bimanual robot records 14. Joint counts
|
|
150
|
+
cannot be constrained this way — two arms need not have the same DOF, and a 7+6 pair is
|
|
151
|
+
legitimate — but the end-effector pose is fixed by its representation.
|
|
152
|
+
"""
|
|
153
|
+
expected = 14 if data.robot_profile.is_biarm else 7
|
|
154
|
+
arms = "biarm" if data.robot_profile.is_biarm else "single-arm"
|
|
155
|
+
|
|
156
|
+
for group, arrays in (("observations", data.observations), ("actions", data.actions)):
|
|
157
|
+
arr = arrays.get("cartesian_position")
|
|
158
|
+
if arr is None or arr.ndim < 2:
|
|
159
|
+
continue
|
|
160
|
+
if arr.shape[-1] != expected:
|
|
161
|
+
raise EpisodeValidationError(
|
|
162
|
+
f"{group}/cartesian_position has {arr.shape[-1]} DOF, but the profile "
|
|
163
|
+
f"declares a {arms} robot (is_biarm={data.robot_profile.is_biarm}), which "
|
|
164
|
+
f"means {expected} — [x, y, z, qx, qy, qz, qw] per arm. Either the pose is "
|
|
165
|
+
f"missing an arm or is_biarm is wrong."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _validate_trajectory_lengths(data: EpisodeData) -> None:
|
|
170
|
+
"""All observation and action arrays must share the same trajectory length."""
|
|
171
|
+
lengths: dict[str, int] = {}
|
|
172
|
+
|
|
173
|
+
for key, arr in data.observations.items():
|
|
174
|
+
if arr.ndim > 0:
|
|
175
|
+
lengths[f"observations/{key}"] = arr.shape[0]
|
|
176
|
+
|
|
177
|
+
for key, arr in data.actions.items():
|
|
178
|
+
if arr.ndim > 0:
|
|
179
|
+
lengths[f"actions/{key}"] = arr.shape[0]
|
|
180
|
+
|
|
181
|
+
if not lengths:
|
|
182
|
+
raise EpisodeValidationError("No trajectory data found in observations or actions")
|
|
183
|
+
|
|
184
|
+
unique = set(lengths.values())
|
|
185
|
+
if len(unique) != 1:
|
|
186
|
+
raise EpisodeValidationError(f"Inconsistent trajectory lengths: {lengths}")
|
|
187
|
+
|
|
188
|
+
actual_T = unique.pop()
|
|
189
|
+
if actual_T != data.trajectory_length:
|
|
190
|
+
raise EpisodeValidationError(
|
|
191
|
+
f"trajectory_length field ({data.trajectory_length}) does not match "
|
|
192
|
+
f"array shapes ({actual_T})"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _validate_video_specs(data: EpisodeData) -> None:
|
|
197
|
+
"""Check per-camera resolution, frame count alignment, and duration alignment."""
|
|
198
|
+
if not data.videos:
|
|
199
|
+
raise EpisodeValidationError("No video entries found")
|
|
200
|
+
|
|
201
|
+
T = data.trajectory_length
|
|
202
|
+
frame_tolerance = max(5, int(0.1 * T))
|
|
203
|
+
expected_duration = T / data.control_freq
|
|
204
|
+
|
|
205
|
+
frame_counts: dict[str, int] = {}
|
|
206
|
+
for cam, info in data.videos.items():
|
|
207
|
+
if not (info.width >= MIN_IMAGE_SIZE and info.height >= MIN_IMAGE_SIZE):
|
|
208
|
+
raise EpisodeValidationError(
|
|
209
|
+
f"Video too small for camera {cam}: {info.width}x{info.height} "
|
|
210
|
+
f"(min {MIN_IMAGE_SIZE}px)"
|
|
211
|
+
)
|
|
212
|
+
if not (info.width <= MAX_IMAGE_SIZE and info.height <= MAX_IMAGE_SIZE):
|
|
213
|
+
raise EpisodeValidationError(
|
|
214
|
+
f"Video too large for camera {cam}: {info.width}x{info.height} "
|
|
215
|
+
f"(max {MAX_IMAGE_SIZE}px)"
|
|
216
|
+
)
|
|
217
|
+
if not (abs(info.frame_count - T) <= frame_tolerance):
|
|
218
|
+
raise EpisodeValidationError(
|
|
219
|
+
f"Frame count / trajectory mismatch for camera {cam}: "
|
|
220
|
+
f"frames={info.frame_count}, trajectory={T}"
|
|
221
|
+
)
|
|
222
|
+
duration = info.frame_count / info.fps
|
|
223
|
+
if not (abs(duration - expected_duration) <= 0.5):
|
|
224
|
+
raise EpisodeValidationError(
|
|
225
|
+
f"Video duration / control_freq mismatch for camera {cam}: "
|
|
226
|
+
f"duration={duration:.2f}s, expected={expected_duration:.2f}s"
|
|
227
|
+
)
|
|
228
|
+
frame_counts[cam] = info.frame_count
|
|
229
|
+
|
|
230
|
+
if len(frame_counts) > 1:
|
|
231
|
+
counts = list(frame_counts.values())
|
|
232
|
+
if not (max(counts) - min(counts) <= 1):
|
|
233
|
+
raise EpisodeValidationError(
|
|
234
|
+
f"Inconsistent frame counts across cameras: {frame_counts}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _annotation_attr_scalar_str(val: Any) -> str:
|
|
239
|
+
"""Normalize HDF5 attr scalars (bytes, numpy, str) to a trimmed string."""
|
|
240
|
+
if val is None:
|
|
241
|
+
return ""
|
|
242
|
+
if isinstance(val, bytes):
|
|
243
|
+
return val.decode("utf-8", errors="replace").strip()
|
|
244
|
+
if isinstance(val, str):
|
|
245
|
+
return val.strip()
|
|
246
|
+
if isinstance(val, np.generic):
|
|
247
|
+
return _annotation_attr_scalar_str(val.item())
|
|
248
|
+
if isinstance(val, np.ndarray) and val.shape == ():
|
|
249
|
+
return _annotation_attr_scalar_str(val.item())
|
|
250
|
+
return str(val).strip()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _validate_annotations(data: EpisodeData) -> None:
|
|
254
|
+
"""Every annotator subgroup must have a numeric success score in [0.0, 1.0].
|
|
255
|
+
|
|
256
|
+
Beyond that, the taxonomy fields are all optional: a partial annotation is valid, and
|
|
257
|
+
a failure with no taxonomy at all is valid. What is checked is only that what *is*
|
|
258
|
+
stored is readable and self-consistent — a malformed ``taxonomy`` blob, or an
|
|
259
|
+
``outcome`` that contradicts the ``success`` float, would make different readers reach
|
|
260
|
+
different conclusions about the same episode.
|
|
261
|
+
|
|
262
|
+
v1 files carry no ``outcome``, so they skip that check and stay valid unchanged.
|
|
263
|
+
"""
|
|
264
|
+
if not data.annotations:
|
|
265
|
+
raise EpisodeValidationError("annotations dict is empty")
|
|
266
|
+
|
|
267
|
+
for annotator, attrs in data.annotations.items():
|
|
268
|
+
if "success" not in attrs:
|
|
269
|
+
raise EpisodeValidationError(
|
|
270
|
+
f"episode_annotations/{annotator} is missing 'success' — "
|
|
271
|
+
"episode has not been fully annotated yet"
|
|
272
|
+
)
|
|
273
|
+
try:
|
|
274
|
+
success = float(attrs["success"])
|
|
275
|
+
except (TypeError, ValueError) as e:
|
|
276
|
+
raise EpisodeValidationError(
|
|
277
|
+
f"episode_annotations/{annotator}/success is not numeric: {attrs['success']!r}"
|
|
278
|
+
) from e
|
|
279
|
+
if np.isnan(success):
|
|
280
|
+
raise EpisodeValidationError(
|
|
281
|
+
f"episode_annotations/{annotator}/success is NaN — "
|
|
282
|
+
"episode has not been fully annotated yet"
|
|
283
|
+
)
|
|
284
|
+
if not (0.0 <= success <= 1.0):
|
|
285
|
+
raise EpisodeValidationError(
|
|
286
|
+
f"episode_annotations/{annotator}/success out of range [0.0, 1.0]: {success}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
taxonomy_raw = _annotation_attr_scalar_str(attrs.get("taxonomy", ""))
|
|
290
|
+
if taxonomy_raw:
|
|
291
|
+
try:
|
|
292
|
+
parsed = json.loads(taxonomy_raw)
|
|
293
|
+
except json.JSONDecodeError as e:
|
|
294
|
+
raise EpisodeValidationError(
|
|
295
|
+
f"episode_annotations/{annotator}/taxonomy is not valid JSON: "
|
|
296
|
+
f"{taxonomy_raw!r}"
|
|
297
|
+
) from e
|
|
298
|
+
if not isinstance(parsed, dict):
|
|
299
|
+
raise EpisodeValidationError(
|
|
300
|
+
f"episode_annotations/{annotator}/taxonomy must be a JSON object, "
|
|
301
|
+
f"got {type(parsed).__name__}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
outcome = _annotation_attr_scalar_str(parsed.get("outcome", "")).lower()
|
|
305
|
+
if outcome:
|
|
306
|
+
if outcome not in OUTCOME_SUCCESS:
|
|
307
|
+
raise EpisodeValidationError(
|
|
308
|
+
f"episode_annotations/{annotator}/taxonomy has unrecognized "
|
|
309
|
+
f"outcome {outcome!r}; expected one of {OUTCOMES}"
|
|
310
|
+
)
|
|
311
|
+
if (outcome == "failure") != (success < SUCCESS_THRESHOLD):
|
|
312
|
+
raise EpisodeValidationError(
|
|
313
|
+
f"episode_annotations/{annotator}: outcome {outcome!r} disagrees "
|
|
314
|
+
f"with success {success} (threshold {SUCCESS_THRESHOLD})"
|
|
315
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""The error every validation failure raises."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EpisodeValidationError(AssertionError):
|
|
7
|
+
"""An episode does not conform to the oopsiedata schema.
|
|
8
|
+
|
|
9
|
+
Subclasses :class:`AssertionError` deliberately. Validation used to be built entirely
|
|
10
|
+
out of bare ``assert`` statements, and ``except AssertionError`` is the documented way
|
|
11
|
+
to catch a validation failure — lab scripts will have copied it. Inheriting keeps every
|
|
12
|
+
one of those callers working while giving new code something specific to catch.
|
|
13
|
+
|
|
14
|
+
The reason for moving off ``assert`` at all: ``python -O`` strips assert statements, so
|
|
15
|
+
the entire schema check silently evaporated under optimised interpreters. This is the
|
|
16
|
+
gate in front of a public dataset; it should not depend on an interpreter flag.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Public validation API.
|
|
2
|
+
|
|
3
|
+
Composes episode_loader (file I/O, schema detection, video loading) and
|
|
4
|
+
episode_validator (semantic checks on loaded data) into the entry points
|
|
5
|
+
used by the CLI and tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from oopsie_data_tools.utils.h5 import find_episode_files
|
|
15
|
+
from oopsie_data_tools.utils.log import setup_logger
|
|
16
|
+
from oopsie_data_tools.utils.validation.episode_loader import load_episode_from_h5
|
|
17
|
+
from oopsie_data_tools.utils.validation.episode_validator import validate_episode
|
|
18
|
+
from oopsie_data_tools.utils.validation.errors import EpisodeValidationError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_h5_file(
|
|
24
|
+
h5_path: str,
|
|
25
|
+
strict_annotation_check: bool = False,
|
|
26
|
+
log_path: str | Path | None = None,
|
|
27
|
+
) -> bool:
|
|
28
|
+
"""Validate a single HDF5 episode file.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
h5_path: Path to the .h5 file.
|
|
32
|
+
strict_annotation_check: If True, require that annotations are present and non-empty.
|
|
33
|
+
log_path: Optional file to also write log output to (mirrors ``validate_session_dir``).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if all checks pass.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
EpisodeValidationError: On the first validation failure. It subclasses
|
|
40
|
+
``AssertionError``, so ``except AssertionError`` still catches it.
|
|
41
|
+
"""
|
|
42
|
+
if log_path is not None:
|
|
43
|
+
setup_logger(__name__, log_path)
|
|
44
|
+
data = load_episode_from_h5(h5_path)
|
|
45
|
+
validate_episode(data, strict_annotation_check=strict_annotation_check)
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_session_dir(session_dir: str, strict_annotation_check: bool = False, log_path: str | Path | None = None) -> int:
|
|
50
|
+
"""Validate every ``*.h5`` / ``*.hdf5`` file in a session directory.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
0 if all files passed, 1 if any failed or the directory is invalid.
|
|
54
|
+
"""
|
|
55
|
+
if log_path is not None:
|
|
56
|
+
setup_logger(__name__, log_path)
|
|
57
|
+
|
|
58
|
+
session_path = os.path.abspath(os.path.normpath(session_dir))
|
|
59
|
+
if not os.path.isdir(session_path):
|
|
60
|
+
logger.error("Not a directory: %s", session_path)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
h5_files = [str(p) for p in find_episode_files(session_path)]
|
|
64
|
+
|
|
65
|
+
if not h5_files:
|
|
66
|
+
logger.error("No .h5 or .hdf5 files found in %s", session_path)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
logger.info("Validating %d HDF5 file(s) in: %s", len(h5_files), session_path)
|
|
70
|
+
failures = 0
|
|
71
|
+
for i, path in enumerate(h5_files, 1):
|
|
72
|
+
name = os.path.basename(path)
|
|
73
|
+
logger.info("[%d/%d] %s", i, len(h5_files), name)
|
|
74
|
+
try:
|
|
75
|
+
validate_h5_file(path, strict_annotation_check=strict_annotation_check)
|
|
76
|
+
logger.info("%s passed", name)
|
|
77
|
+
except EpisodeValidationError as e:
|
|
78
|
+
failures += 1
|
|
79
|
+
logger.error("%s failed: %s", name, e)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# Anything else is a bug in the validator rather than a bad episode, so it is
|
|
82
|
+
# reported differently on purpose.
|
|
83
|
+
failures += 1
|
|
84
|
+
logger.error("%s unexpected error: %s", name, e)
|
|
85
|
+
|
|
86
|
+
passed = len(h5_files) - failures
|
|
87
|
+
logger.info("Summary: %d/%d passed", passed, len(h5_files))
|
|
88
|
+
return 1 if failures else 0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oopsie-data-tools
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Collect, annotate, validate and upload robotic manipulation rollout data.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: flask>=3.0.0
|
|
8
|
+
Requires-Dist: h5py>=3.7.0
|
|
9
|
+
Requires-Dist: huggingface-hub>=0.36.2
|
|
10
|
+
Requires-Dist: imageio-ffmpeg>=0.5.1
|
|
11
|
+
Requires-Dist: imageio>=2.35.1
|
|
12
|
+
Requires-Dist: numpy>=1.20.0
|
|
13
|
+
Requires-Dist: opencv-python>=4.11.0.86
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
15
|
+
Requires-Dist: scipy>=1.10.1
|
|
16
|
+
Provides-Extra: droid
|
|
17
|
+
Requires-Dist: pandas>=2.0.0; extra == 'droid'
|
|
18
|
+
Requires-Dist: pillow>=10.0.0; extra == 'droid'
|
|
19
|
+
Requires-Dist: tqdm>=4.66.0; extra == 'droid'
|
|
20
|
+
Requires-Dist: tyro>=0.9.0; extra == 'droid'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Oopsie Tools
|
|
24
|
+
|
|
25
|
+
Tools for collecting, annotating, inspecting, and converting robotic manipulation rollout data.
|
|
26
|
+
|
|
27
|
+
This repository currently provides around:
|
|
28
|
+
|
|
29
|
+
- HDF5 episode recording (`EpisodeRecorder`)
|
|
30
|
+
- Web annotation workflows
|
|
31
|
+
- In-the-loop annotation during policy rollout
|
|
32
|
+
|
|
33
|
+
## Command line tool
|
|
34
|
+
|
|
35
|
+
Installing the package (`uv sync`, or `pip install -e .`) provides the `oopsie-data` command:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
oopsie-data init # first-time setup (lab id + HF token)
|
|
39
|
+
oopsie-data show-config # where configs are read from, and what is in them
|
|
40
|
+
oopsie-data new-profile # starter robot profile to fill in
|
|
41
|
+
oopsie-data annotate --samples-dir ./samples # launch the web annotation UI
|
|
42
|
+
oopsie-data validate --path ./samples # check episodes against the schema
|
|
43
|
+
oopsie-data upload --path ./samples # validate, then upload to HuggingFace
|
|
44
|
+
oopsie-data submissions # what your lab has already uploaded
|
|
45
|
+
oopsie-data inspect episode.h5 # dump an episode's structure
|
|
46
|
+
oopsie-data restructure --source ./samples # split a folder that is too large to upload
|
|
47
|
+
oopsie-data upload --path ./samples --with-restructure # ...or do both in one step
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`init` prompts for your lab id and HuggingFace token and saves them to a location the other
|
|
51
|
+
commands find automatically. It checks the token against the HuggingFace API, but only warns
|
|
52
|
+
if that fails and saves it regardless — so a wrong token surfaces at `oopsie-data upload`, not
|
|
53
|
+
at `init`. `new-profile` writes a robot-profile skeleton you fill in; it will not load until
|
|
54
|
+
you do. `annotate` asks for your annotator name if `--annotator-name` is not passed.
|
|
55
|
+
`inspect` is a plain structure dump, so it also works on a file `validate` rejects.
|
|
56
|
+
`restructure` is only needed if `upload` stops you: HuggingFace enforces a per-directory
|
|
57
|
+
file limit, and it writes a copy of your session in which every directory over that limit
|
|
58
|
+
has been split into numbered subfolders. Directories already under it are copied through
|
|
59
|
+
unchanged, and the original is never modified.
|
|
60
|
+
|
|
61
|
+
Credentials and robot profiles are looked up separately, so the tool also works when
|
|
62
|
+
installed outside a checkout of this repository.
|
|
63
|
+
|
|
64
|
+
`contributor_config.yaml` belongs to you and is shared by every project:
|
|
65
|
+
|
|
66
|
+
1. `$OOPSIE_CONFIG_DIR`
|
|
67
|
+
2. `~/.config/oopsie-data` (or `$XDG_CONFIG_HOME/oopsie-data`)
|
|
68
|
+
3. this repository's `configs/` directory
|
|
69
|
+
|
|
70
|
+
Robot profiles belong to the robot code that uses them, and are never expected in your user
|
|
71
|
+
config directory:
|
|
72
|
+
|
|
73
|
+
1. `$OOPSIE_ROBOT_PROFILES_DIR`
|
|
74
|
+
2. `./robot_profiles` or `./configs/robot_profiles`, relative to where you run the command
|
|
75
|
+
3. this repository's `configs/robot_profiles`
|
|
76
|
+
|
|
77
|
+
In practice a profile is usually loaded by explicit path — `load_robot_profile("path/to.yaml")`.
|
|
78
|
+
Nothing is ever read out of the installed package: config that lives in `site-packages` is
|
|
79
|
+
read-only, invisible to you, and would silently shadow your own. Run `oopsie-data new-profile` to
|
|
80
|
+
write a starter profile into `./robot_profiles/` and fill it in from there. Pass
|
|
81
|
+
`oopsie-data --config-dir <dir> <command>` to override the credential location for a single run.
|
|
82
|
+
|
|
83
|
+
Run `oopsie-data show-config` to see which of these locations is actually being used, along with
|
|
84
|
+
the lab id and token in effect.
|
|
85
|
+
|
|
86
|
+
### Upgrading: `configs/contributor_config.yaml` is no longer tracked
|
|
87
|
+
|
|
88
|
+
That file holds your HuggingFace token, and it used to be committed to the repository — which
|
|
89
|
+
meant `.gitignore` did not apply to it and your token showed up in `git status`. It is now
|
|
90
|
+
untracked and ignored.
|
|
91
|
+
|
|
92
|
+
If you have an existing clone with your credentials filled in, `git pull` may refuse:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
error: Your local changes to the following files would be overwritten by merge:
|
|
96
|
+
configs/contributor_config.yaml
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Your credentials are safe; git is just protecting the file. Move them out of the working tree:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
oopsie-data init # writes to ~/.config/oopsie-data, verifies nothing is lost
|
|
103
|
+
rm configs/contributor_config.yaml
|
|
104
|
+
git pull
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Nothing else changes — that location is still searched, so an existing setup keeps working. You
|
|
108
|
+
will see a warning pointing here until you move it.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
For detailed explanations on how to use our tooling and contribute to the project, please visit [our website](https://oopsie-data.com/).
|
|
113
|
+
|
|
114
|
+
For an overview of the steps necessary to integrate the tooling into your workflow and to contribute data to the official Oopsie Data repositories, check out [our quickstart guide](https://oopsie-data.com/quickstart).
|
|
115
|
+
You can also use the information in AI_CONTEXT.md to guide a coding agent through the setup.
|
|
116
|
+
|
|
117
|
+
## Repository structure
|
|
118
|
+
|
|
119
|
+
The main tooling for data gathering and annotation is located in `oopsie-data-tools`.
|
|
120
|
+
|
|
121
|
+
We provide example scripts for automatically collecting and annotating evaluation data while running policy inference in examples. Currently we support the evaluation scripts supported by `openpi` and Trossen robotics `act_plus_plus` repository. If you want to run a different evaluation script, check out the detailed instructions on integrating our tools into standard robot evaluation pipelines.
|
|
122
|
+
|
|
123
|
+
## Issue/PR for support requests
|
|
124
|
+
|
|
125
|
+
We are very aware that changing eval code and recording data while doing experiments can be a big hassle and cause friction. We are therefore happy to help you integrate the recording tool into your setup. Please let us know what robot platform and policy you are evaluating and where to find a sample of your evaluation & inference code.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
You can use our toolset any time you like to record and annotate robot rollouts. To contribute your data to the official Oopsie Dataset, please follow the [sign-up instructions](https://oopsie-data.com/contributing/)!
|
|
130
|
+
|
|
131
|
+
If you run into any issues, please do not hesitate to contact the team via mail or raise an issue here.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
oopsie_data_tools/__init__.py,sha256=IXlKkl6pJVukFvSeMKnTArpNuJCyP5lN-SnRPHOqYgo,109
|
|
2
|
+
oopsie_data_tools/cli.py,sha256=RguBpkDzkulxe53rZ7HqLkQfPoGs83XEJNn6dXWSyAE,35300
|
|
3
|
+
oopsie_data_tools/init_wizard.py,sha256=e5VuMhmHhl0Y7Xqr19mZuK4MzAhgYJY3xELIRwZr-TE,12848
|
|
4
|
+
oopsie_data_tools/annotation_tool/__init__.py,sha256=yUz9RLGnPuazy4j2BdU-snnAlG702KMe07owJ7y-2No,146
|
|
5
|
+
oopsie_data_tools/annotation_tool/annotation_schema.py,sha256=esbDz8TyEX0OPj84CYaf2kz7ZtDovlILLODviK2gPkg,10676
|
|
6
|
+
oopsie_data_tools/annotation_tool/annotator_server.py,sha256=rs5iC1WFTLFlQGTkeBe0w0vVM-MHTiW6p27EoDuVKs4,31138
|
|
7
|
+
oopsie_data_tools/annotation_tool/episode_recorder.py,sha256=xVG3ynmgRl5V79cpXgljflBqjljpTJzlF-M8gg0DGDE,27101
|
|
8
|
+
oopsie_data_tools/annotation_tool/rollout_annotator.py,sha256=4Pe0qelJynJT-zisnYA3sZDRqZcLmzsaTUGnrnRSycQ,13422
|
|
9
|
+
oopsie_data_tools/annotation_tool/ui/annotator.html,sha256=uNrocdAe4HBDk46lMLhxa7yQuptPyFFcfeBVeGXiG98,98368
|
|
10
|
+
oopsie_data_tools/skill/SKILL.md,sha256=pa6jO0q1WDzD9NPuOaK1M0IKE14Rrq76ph5nrjrYfvI,5391
|
|
11
|
+
oopsie_data_tools/skill/reference/conversion.md,sha256=ok51uFUlhSTjU_ehzQ4ifCisUdAWFrUlyFCw-2mrgDk,11718
|
|
12
|
+
oopsie_data_tools/skill/reference/format.md,sha256=6ku3h-jhdeAbpf9Rj2Nb50q58n7hBMk2YdLnJMCWPEA,5882
|
|
13
|
+
oopsie_data_tools/skill/reference/robot-profile.md,sha256=znna_0wOe6WxV1Et_v06d0LrIx0bJnU2Mm6ZirR6who,5590
|
|
14
|
+
oopsie_data_tools/skill/reference/setup.md,sha256=_t7ap_YaSHdb_2G5zIlJIVcBw0PI9cdW1iABSFi0bBI,8910
|
|
15
|
+
oopsie_data_tools/skill/reference/troubleshooting.md,sha256=WkMnUUZypuQ5QMzAmFn-bURCBmq0LFnwWoIJKfgdWZ8,5522
|
|
16
|
+
oopsie_data_tools/test/__init__.py,sha256=S-_C9kt8ePKm3p912yXlBpSYn_fZC-DXkZqliQHWwBk,40
|
|
17
|
+
oopsie_data_tools/test/conftest.py,sha256=K12d_zD23PHz_pApQTJ95mUPPjjYWmDKqDqdd5jfrtk,6337
|
|
18
|
+
oopsie_data_tools/test/test_annotation_completeness.py,sha256=pLHmJjS5Xjdh5HiBsAoeCe4s5E0YxznIeQutRl3n_2g,6141
|
|
19
|
+
oopsie_data_tools/test/test_annotation_schema.py,sha256=2OxE9uES3EvYPKMKVrBWyeICg-uwcWI6026YOrPOKRg,6975
|
|
20
|
+
oopsie_data_tools/test/test_annotator_server.py,sha256=tlPY2HooVZvR_NhqPtHfXE7MZbujumUY-EuClg61CZ4,9188
|
|
21
|
+
oopsie_data_tools/test/test_annotator_server_guards.py,sha256=3apElV1CbKgbmKuk1WG8B-yJNIqapg_ytjuQMN8v5e0,5129
|
|
22
|
+
oopsie_data_tools/test/test_bulk_inference_end_to_end.py,sha256=ho5cATp_LY9bKDxUi35b2zP65ms4lWYzIdBaiBtG1Nc,4228
|
|
23
|
+
oopsie_data_tools/test/test_cli_config.py,sha256=X0b9biEgAndYlf2UB_MdimUPuWvSrdhLWYqaZin1nr4,3879
|
|
24
|
+
oopsie_data_tools/test/test_cli_tools.py,sha256=x8kAOXdzjrzWDbpL3RKA7buveE2_qLkDYzERXijDg0o,17748
|
|
25
|
+
oopsie_data_tools/test/test_contributor_config.py,sha256=QVRZjZG-GUXMK-Y7BhEKy8ZL0iLgO-KMq2Dy86eY4QY,1303
|
|
26
|
+
oopsie_data_tools/test/test_conversion_utils_annotations.py,sha256=UMdnrqzm6cUZPWhd54nZjetlu5SKntTTGFTFlDQVPY4,2751
|
|
27
|
+
oopsie_data_tools/test/test_credentials_location.py,sha256=gDSfnN79DI7Z1hox5lhEcS-mAsF0isUd9wSKOjaFJbc,4251
|
|
28
|
+
oopsie_data_tools/test/test_diversity.py,sha256=NGljPMlQL18ytz3oICpCKtlfSNXEPi8HzGSuws8DqkM,1197
|
|
29
|
+
oopsie_data_tools/test/test_episode_recorder.py,sha256=HO39ZGVatPckicSGT_iJH981PxdGhFXLVpYqKfokw7k,12772
|
|
30
|
+
oopsie_data_tools/test/test_episode_video_paths.py,sha256=wA4d7vp2WrAkdEncSjBuJceUS_8mp0o5BZuJXq1kOXg,7087
|
|
31
|
+
oopsie_data_tools/test/test_hf_upload.py,sha256=OjfqN7meP1j8GVOwVI8pw-G-IEmh1JIk-jnw601LTSY,8217
|
|
32
|
+
oopsie_data_tools/test/test_init_wizard.py,sha256=9lm9_3xeP1V5SkkJuUj2T19o1dZsXySmkkYPZy8l5CM,7181
|
|
33
|
+
oopsie_data_tools/test/test_install_skill.py,sha256=q-xqlB6GeHqInejV2bVXKeJen2E1KEVYsmv3Ms5Ub4o,4492
|
|
34
|
+
oopsie_data_tools/test/test_migrate_taxonomy_v2.py,sha256=a9Qhn0g7SoOd2Q0fJ8lLrab0eFXZwEKcEZAh_koNZU4,9047
|
|
35
|
+
oopsie_data_tools/test/test_new_profile.py,sha256=OHXdpu5nqCaO5DsZECRGxX4pi5pm8UoigjsdCji1_XI,3408
|
|
36
|
+
oopsie_data_tools/test/test_paths.py,sha256=0Cu3YyNCsShMLstULoh9JbyJIdD14kkvcSkTBcWAgH8,5652
|
|
37
|
+
oopsie_data_tools/test/test_python38_compat.py,sha256=TsQbF50ixPDmm0VvjaMHtXsmVjFcbWeFawm5IpIRNyk,3218
|
|
38
|
+
oopsie_data_tools/test/test_record_step_purity.py,sha256=Kq_S4Dlzb_hyuzjr2MDhH0cw55DiqgtHSav6H4Cngkg,4559
|
|
39
|
+
oopsie_data_tools/test/test_robot_setup.py,sha256=baeKEbk3NDZB0gaNqGAlj5gNJodPP3u58tNAHmdYy3A,10008
|
|
40
|
+
oopsie_data_tools/test/test_rollout_annotator.py,sha256=mESGTAaLoQKg6j9P_GM_uYm32Zk4Kd8n8Wg0Gu1kbyE,4526
|
|
41
|
+
oopsie_data_tools/test/test_rotation_utils.py,sha256=_AsVjOg7h_zHxCXOUQp2K-QiCa_O8fIfd3ZxNGvYEGM,4750
|
|
42
|
+
oopsie_data_tools/test/test_validate.py,sha256=3IRpbrUouGSJ4knmsQ_StF7ycVP0z7fQ29-3VAgR8z8,22989
|
|
43
|
+
oopsie_data_tools/test/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
|
+
oopsie_data_tools/test/fixtures/make_invalid.py,sha256=BXMDtoV8v0JwuxvsWQZ-oSJQvFDhOj-I3iQQCuRzFws,28648
|
|
45
|
+
oopsie_data_tools/test/fixtures/make_valid.py,sha256=E6nkzKbQ5f-sxkeS-poBmpgyXTTQqopj0_ZwJzihTkE,14298
|
|
46
|
+
oopsie_data_tools/utils/__init__.py,sha256=FJvt-LyfZ_71ePzCMkzDn1nkQo13nNuIB23eaJr0vdM,75
|
|
47
|
+
oopsie_data_tools/utils/claude_skill.py,sha256=jKCiPKCI9hUQjP6Ppaen-h7xwJ8LYwYuEuyHpQTzeV8,3562
|
|
48
|
+
oopsie_data_tools/utils/contributor_config.py,sha256=uHmE_gWMgPAJ-bgt7iLOfe64rW5oTu4Sl9TnpA0JHk8,3555
|
|
49
|
+
oopsie_data_tools/utils/conversion_utils.py,sha256=NZ2HoShlBUT8UkpUGWicY1zB5aZpHbN91GS--oGdqcI,8603
|
|
50
|
+
oopsie_data_tools/utils/h5.py,sha256=Uq-ZlIDTBFYeIZoZTQpLhIL2P8Qvirrmqnaf6ceGE1k,2066
|
|
51
|
+
oopsie_data_tools/utils/h5_inspect.py,sha256=w19JP6H3SPeRqPHjyrP-0Y8-gsNG0QD3cZv_TuANhOg,5137
|
|
52
|
+
oopsie_data_tools/utils/hf_limits.py,sha256=1Idk0rzPIIPYSMUUB35MjdzTcXMnD8aGtCRQHbDcfMk,1132
|
|
53
|
+
oopsie_data_tools/utils/hf_upload.py,sha256=3g3lrClBKwiNcgF8Icp-nLubKONMUxMPH67s3CupxKg,9565
|
|
54
|
+
oopsie_data_tools/utils/log.py,sha256=YHf9ItYdQH2Kdh3PabKI5wYbyz-2jXsRutlO98iwBWA,1286
|
|
55
|
+
oopsie_data_tools/utils/migrate_taxonomy_v2.py,sha256=VPlP_VecsyEJZgJXb1M-cOPEq1V98xhVVAeD9ywPfXY,8186
|
|
56
|
+
oopsie_data_tools/utils/paths.py,sha256=9INAcdyiEWTBg7og_Wa8HEnrAd9TKfe186_jvBbwplk,6107
|
|
57
|
+
oopsie_data_tools/utils/restructure.py,sha256=c-IlUbU6cvQ8Y4PkDpl-THbtiR4QBKpg0YOsRSz-0Mw,16673
|
|
58
|
+
oopsie_data_tools/utils/robot_profile/__init__.py,sha256=p7U8FPuC4_5MpwiO6G8kjYImmgWugplGix8TcqH0na4,68
|
|
59
|
+
oopsie_data_tools/utils/robot_profile/robot_profile.py,sha256=L742sR_JnU9MDHg2h3xawJ7PJvOU7szbAcUX5PqWYZM,8428
|
|
60
|
+
oopsie_data_tools/utils/robot_profile/rotation_utils.py,sha256=1wx8jDV7fNbVdCgl7QkScpOhfQTbpvDxxlejNK0vZs0,4394
|
|
61
|
+
oopsie_data_tools/utils/robot_profile/template.py,sha256=Ctiwb0IQ8MmDWw4JwdpRxOUilpxM7xvxvoW-8NzyQo4,4001
|
|
62
|
+
oopsie_data_tools/utils/validation/__init__.py,sha256=5VD5OuWEhbshVkX86lEa3kkvFdJIBRZ_dlZQfjnmos4,193
|
|
63
|
+
oopsie_data_tools/utils/validation/annotation_completeness.py,sha256=vrwn4_a0t_y80XoPFeKQfP9BnHrdR1IkzkI49SdvA28,2793
|
|
64
|
+
oopsie_data_tools/utils/validation/diversity.py,sha256=XVwSRf1at9NkUDA7HqCE__HavzY6Wjnc1PwwsEygjUc,3502
|
|
65
|
+
oopsie_data_tools/utils/validation/episode_data.py,sha256=hsag_Ai-_eZRO77b0P7_9JK7M5Mpm41T37pGCVS8iMc,1734
|
|
66
|
+
oopsie_data_tools/utils/validation/episode_loader.py,sha256=XYOxgZXzn_PMfs4orpf-BevkedpEvYh3d8Pq0M2NpBY,8064
|
|
67
|
+
oopsie_data_tools/utils/validation/episode_validator.py,sha256=BFRvCd1jFx2sT_Mm230rmAahMKj2pqPEVQlhkOqzcZk,14209
|
|
68
|
+
oopsie_data_tools/utils/validation/errors.py,sha256=wFqdNt1vLXgLTVYffNSzS9oztmNRXUBY5t1OIGQEdKQ,829
|
|
69
|
+
oopsie_data_tools/utils/validation/validation_utils.py,sha256=BbRv1BcYsLQG0i7jMzYEcmvCnU1IW9B0TlyM1lwUeVg,3150
|
|
70
|
+
oopsie_data_tools-0.2.0.dist-info/METADATA,sha256=MZQYyWOp84hCKusQRwTydXPec-DbvV-IX7xik3VuKnM,6651
|
|
71
|
+
oopsie_data_tools-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
72
|
+
oopsie_data_tools-0.2.0.dist-info/entry_points.txt,sha256=QaXQDGBYcp6HF8XOST7AQ5PAJVc-5F2vWMvdoOWkSu8,59
|
|
73
|
+
oopsie_data_tools-0.2.0.dist-info/licenses/LICENSE,sha256=2z6ApHhi7pIbotzuUQ6nki47YHsehL8fG_DV_rl85jA,1099
|
|
74
|
+
oopsie_data_tools-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oopsie Team (https://oopsie-data.com/data)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|