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.
Files changed (74) hide show
  1. oopsie_data_tools/__init__.py +7 -0
  2. oopsie_data_tools/annotation_tool/__init__.py +5 -0
  3. oopsie_data_tools/annotation_tool/annotation_schema.py +266 -0
  4. oopsie_data_tools/annotation_tool/annotator_server.py +850 -0
  5. oopsie_data_tools/annotation_tool/episode_recorder.py +648 -0
  6. oopsie_data_tools/annotation_tool/rollout_annotator.py +333 -0
  7. oopsie_data_tools/annotation_tool/ui/annotator.html +2240 -0
  8. oopsie_data_tools/cli.py +894 -0
  9. oopsie_data_tools/init_wizard.py +329 -0
  10. oopsie_data_tools/skill/SKILL.md +98 -0
  11. oopsie_data_tools/skill/reference/conversion.md +257 -0
  12. oopsie_data_tools/skill/reference/format.md +112 -0
  13. oopsie_data_tools/skill/reference/robot-profile.md +104 -0
  14. oopsie_data_tools/skill/reference/setup.md +216 -0
  15. oopsie_data_tools/skill/reference/troubleshooting.md +97 -0
  16. oopsie_data_tools/test/__init__.py +1 -0
  17. oopsie_data_tools/test/conftest.py +174 -0
  18. oopsie_data_tools/test/fixtures/__init__.py +0 -0
  19. oopsie_data_tools/test/fixtures/make_invalid.py +633 -0
  20. oopsie_data_tools/test/fixtures/make_valid.py +406 -0
  21. oopsie_data_tools/test/test_annotation_completeness.py +154 -0
  22. oopsie_data_tools/test/test_annotation_schema.py +190 -0
  23. oopsie_data_tools/test/test_annotator_server.py +240 -0
  24. oopsie_data_tools/test/test_annotator_server_guards.py +146 -0
  25. oopsie_data_tools/test/test_bulk_inference_end_to_end.py +112 -0
  26. oopsie_data_tools/test/test_cli_config.py +111 -0
  27. oopsie_data_tools/test/test_cli_tools.py +438 -0
  28. oopsie_data_tools/test/test_contributor_config.py +38 -0
  29. oopsie_data_tools/test/test_conversion_utils_annotations.py +75 -0
  30. oopsie_data_tools/test/test_credentials_location.py +106 -0
  31. oopsie_data_tools/test/test_diversity.py +33 -0
  32. oopsie_data_tools/test/test_episode_recorder.py +335 -0
  33. oopsie_data_tools/test/test_episode_video_paths.py +173 -0
  34. oopsie_data_tools/test/test_hf_upload.py +234 -0
  35. oopsie_data_tools/test/test_init_wizard.py +193 -0
  36. oopsie_data_tools/test/test_install_skill.py +120 -0
  37. oopsie_data_tools/test/test_migrate_taxonomy_v2.py +255 -0
  38. oopsie_data_tools/test/test_new_profile.py +84 -0
  39. oopsie_data_tools/test/test_paths.py +137 -0
  40. oopsie_data_tools/test/test_python38_compat.py +79 -0
  41. oopsie_data_tools/test/test_record_step_purity.py +127 -0
  42. oopsie_data_tools/test/test_robot_setup.py +244 -0
  43. oopsie_data_tools/test/test_rollout_annotator.py +134 -0
  44. oopsie_data_tools/test/test_rotation_utils.py +126 -0
  45. oopsie_data_tools/test/test_validate.py +494 -0
  46. oopsie_data_tools/utils/__init__.py +1 -0
  47. oopsie_data_tools/utils/claude_skill.py +96 -0
  48. oopsie_data_tools/utils/contributor_config.py +91 -0
  49. oopsie_data_tools/utils/conversion_utils.py +220 -0
  50. oopsie_data_tools/utils/h5.py +60 -0
  51. oopsie_data_tools/utils/h5_inspect.py +167 -0
  52. oopsie_data_tools/utils/hf_limits.py +22 -0
  53. oopsie_data_tools/utils/hf_upload.py +235 -0
  54. oopsie_data_tools/utils/log.py +35 -0
  55. oopsie_data_tools/utils/migrate_taxonomy_v2.py +215 -0
  56. oopsie_data_tools/utils/paths.py +162 -0
  57. oopsie_data_tools/utils/restructure.py +402 -0
  58. oopsie_data_tools/utils/robot_profile/__init__.py +1 -0
  59. oopsie_data_tools/utils/robot_profile/robot_profile.py +240 -0
  60. oopsie_data_tools/utils/robot_profile/rotation_utils.py +119 -0
  61. oopsie_data_tools/utils/robot_profile/template.py +108 -0
  62. oopsie_data_tools/utils/validation/__init__.py +5 -0
  63. oopsie_data_tools/utils/validation/annotation_completeness.py +67 -0
  64. oopsie_data_tools/utils/validation/diversity.py +93 -0
  65. oopsie_data_tools/utils/validation/episode_data.py +54 -0
  66. oopsie_data_tools/utils/validation/episode_loader.py +201 -0
  67. oopsie_data_tools/utils/validation/episode_validator.py +315 -0
  68. oopsie_data_tools/utils/validation/errors.py +17 -0
  69. oopsie_data_tools/utils/validation/validation_utils.py +88 -0
  70. oopsie_data_tools-0.2.0.dist-info/METADATA +131 -0
  71. oopsie_data_tools-0.2.0.dist-info/RECORD +74 -0
  72. oopsie_data_tools-0.2.0.dist-info/WHEEL +4 -0
  73. oopsie_data_tools-0.2.0.dist-info/entry_points.txt +2 -0
  74. 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ oopsie-data = oopsie_data_tools.cli:main
@@ -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.