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,7 @@
1
+ import logging
2
+
3
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
4
+
5
+
6
+ def get_hf_token():
7
+ pass
@@ -0,0 +1,5 @@
1
+ """Annotation tool for robotic failure samples with multi-user support."""
2
+
3
+ from pathlib import Path
4
+
5
+ ANNOTATION_TOOL_DIR = Path(__file__).parent
@@ -0,0 +1,266 @@
1
+ """Single definition of the oopsie human-annotation vocabulary and its HDF5 shape.
2
+
3
+ Everything about the taxonomy that more than one module has to agree on lives here: the
4
+ schema strings, the four outcome slugs and their numeric ``success`` mapping, the v1 -> v2
5
+ upcast tables, and the read/write pair for an ``episode_annotations/<annotator>`` subgroup.
6
+
7
+ Kept to json + h5py so the in-the-loop recorder, the converter helpers, the validator, the
8
+ web annotator and the offline migration can all share it without importing each other.
9
+
10
+ Stored values are stable slugs, not the prose the annotation form shows. That split is
11
+ deliberate: the UI wording changes far more often than the taxonomy does, and prose on disk
12
+ made every rewording a data migration.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any, Mapping
19
+
20
+ import h5py
21
+
22
+ # ── Schema identifiers ────────────────────────────────────────────────────────
23
+ # Written as attrs on episode_annotations/<annotator>. Distinct from the *file* schema
24
+ # (OOPSIE_DATA_SCHEMA_V1 in episode_loader.py), which versions the episode layout itself.
25
+ ANNOTATION_SCHEMA_V1 = "oopsie_failure_taxonomy_v1"
26
+ ANNOTATION_SCHEMA_V2 = "oopsie_failure_taxonomy_v2"
27
+ ANNOTATION_SCHEMA_CURRENT = ANNOTATION_SCHEMA_V2
28
+
29
+ TAXONOMY_SCHEMA_V1 = "oopsiedata_taxonomy_schema_v1"
30
+ TAXONOMY_SCHEMA_V2 = "oopsiedata_taxonomy_schema_v2"
31
+ TAXONOMY_SCHEMA_CURRENT = TAXONOMY_SCHEMA_V2
32
+
33
+ # ── Vocabulary ────────────────────────────────────────────────────────────────
34
+ SUCCESS_THRESHOLD = 0.5
35
+
36
+ # The one place the four outcomes and their numeric success value are defined. The float
37
+ # stays in the file so converters, uploaders and any external consumer that only knows
38
+ # "success" keep working unchanged.
39
+ OUTCOME_SUCCESS: dict[str, float] = {
40
+ "success": 1.0,
41
+ "success_suboptimal": 1.0,
42
+ "success_side_effect": 1.0,
43
+ "failure": 0.0,
44
+ }
45
+ OUTCOMES: tuple[str, ...] = tuple(OUTCOME_SUCCESS)
46
+
47
+ SIDE_EFFECT_CATEGORIES: tuple[str, ...] = (
48
+ "reaching",
49
+ "grasp",
50
+ "manipulation",
51
+ "sequencing_semantic",
52
+ "collision",
53
+ "hardware",
54
+ "not_attempted",
55
+ "other",
56
+ )
57
+
58
+ SEVERITIES: tuple[str, ...] = ("low", "medium", "catastrophic")
59
+
60
+
61
+ def outcome_to_success(outcome: Any) -> "float | None":
62
+ """Numeric ``success`` for an outcome slug, or ``None`` if unrecognized.
63
+
64
+ ``None`` means "leave the attr unwritten" rather than "0.0": an unrecognized outcome is
65
+ a bug upstream, and inventing a failure for it would silently mislabel the episode.
66
+ """
67
+ return OUTCOME_SUCCESS.get(str(outcome or "").strip().lower())
68
+
69
+
70
+ def success_to_outcome(success: Any) -> "str | None":
71
+ """Coarsest outcome consistent with a numeric ``success``.
72
+
73
+ Used to read v1 files and to give converters that only have a float a valid slug. It can
74
+ never return either qualified success -- that distinction does not exist in the float.
75
+ """
76
+ try:
77
+ value = float(success)
78
+ except (TypeError, ValueError):
79
+ return None
80
+ return "success" if value >= SUCCESS_THRESHOLD else "failure"
81
+
82
+
83
+ # ── v1 -> v2 upcast tables ────────────────────────────────────────────────────
84
+ # v1 stored the display prose on disk. These tables are the only description of the
85
+ # correspondence, shared by the read path below and by migrate_taxonomy_v2.
86
+ V1_SUCCESS_CATEGORY_TO_OUTCOME: dict[str, str] = {
87
+ "clean success": "success",
88
+ "success with side-effects": "success_side_effect",
89
+ "suboptimal execution": "success_suboptimal",
90
+ }
91
+
92
+ V1_CATEGORY_TO_SLUG: dict[str, str] = {
93
+ "reaching failure (pre contact)": "reaching",
94
+ "grasp failure (at contact)": "grasp",
95
+ "manipulation failure (post contact)": "manipulation",
96
+ "sequencing or semantic failure": "sequencing_semantic",
97
+ "collision failure": "collision",
98
+ "hardware/mechanical issue": "hardware",
99
+ "task not attempted": "not_attempted",
100
+ "other": "other",
101
+ }
102
+
103
+ # v1 severities were whole sentences ("Low severity - no damage, ..."), so they are matched
104
+ # by prefix rather than equality.
105
+ V1_SEVERITY_PREFIX_TO_SLUG: tuple[tuple[str, str], ...] = (
106
+ ("low severity", "low"),
107
+ ("medium severity", "medium"),
108
+ ("catastrophic", "catastrophic"),
109
+ )
110
+
111
+
112
+ def decode_attr(value: Any) -> str:
113
+ """Normalize an HDF5 attr scalar (bytes / numpy scalar / str) to a trimmed str."""
114
+ if value is None:
115
+ return ""
116
+ if isinstance(value, bytes):
117
+ return value.decode("utf-8", errors="replace").strip()
118
+ if isinstance(value, str):
119
+ return value.strip()
120
+ item = getattr(value, "item", None)
121
+ if callable(item):
122
+ try:
123
+ return decode_attr(item())
124
+ except (ValueError, TypeError):
125
+ pass
126
+ return str(value).strip()
127
+
128
+
129
+ def as_value_list(value: Any) -> list:
130
+ """Categories arrive as a list (the form) or a scalar (older hand-written records)."""
131
+ if value is None:
132
+ return []
133
+ if isinstance(value, (list, tuple)):
134
+ return [v for v in value if decode_attr(v)]
135
+ decoded = decode_attr(value)
136
+ return [decoded] if decoded else []
137
+
138
+
139
+ def normalize_category(value: Any) -> str:
140
+ """Map one category value to its slug, passing unknown values through verbatim.
141
+
142
+ Unknown values are preserved rather than folded into ``other`` so a stale or hand-rolled
143
+ label stays visible in the data. Silently relabelling human annotations is not reversible.
144
+ """
145
+ text = decode_attr(value)
146
+ if not text:
147
+ return ""
148
+ if text in SIDE_EFFECT_CATEGORIES:
149
+ return text
150
+ return V1_CATEGORY_TO_SLUG.get(text.lower(), text)
151
+
152
+
153
+ def normalize_severity(value: Any) -> str:
154
+ """Map one severity value to its slug, passing unknown values through verbatim."""
155
+ text = decode_attr(value)
156
+ if not text:
157
+ return ""
158
+ lowered = text.lower()
159
+ if lowered in SEVERITIES:
160
+ return lowered
161
+ for prefix, slug in V1_SEVERITY_PREFIX_TO_SLUG:
162
+ if lowered.startswith(prefix):
163
+ return slug
164
+ return text
165
+
166
+
167
+ def parse_taxonomy(raw: Any) -> dict:
168
+ """Decode a JSON-object attr, tolerating absence and malformed content.
169
+
170
+ Used for the ``taxonomy`` attr, and reused by the server for ``robot_profile`` -- both
171
+ are JSON objects stored as attrs with the same "missing is empty" semantics.
172
+ """
173
+ if isinstance(raw, dict):
174
+ return raw
175
+ text = decode_attr(raw)
176
+ if not text:
177
+ return {}
178
+ try:
179
+ parsed = json.loads(text)
180
+ except json.JSONDecodeError:
181
+ return {}
182
+ return parsed if isinstance(parsed, dict) else {}
183
+
184
+
185
+ # ── Read ──────────────────────────────────────────────────────────────────────
186
+
187
+
188
+ def read_annotation_attrs(attrs: Mapping[str, Any]) -> dict:
189
+ """Decode a v1 or v2 annotation attr set into the v2 annotation dict.
190
+
191
+ This is the only place that knows v1 exists. v1 files are upcast on read and never
192
+ rewritten, so a dataset can hold both versions indefinitely.
193
+
194
+ Keys absent from the file stay absent from the result, so callers can tell "not
195
+ annotated" from "annotated and left empty".
196
+ """
197
+ out: dict = {}
198
+ taxonomy = parse_taxonomy(attrs.get("taxonomy", ""))
199
+
200
+ outcome = decode_attr(taxonomy.get("outcome")).lower()
201
+ if outcome not in OUTCOME_SUCCESS:
202
+ # v1: rebuild from the success float, then refine with the qualified-success label.
203
+ outcome = success_to_outcome(attrs.get("success")) or ""
204
+ legacy_category = decode_attr(taxonomy.get("success_category")).lower()
205
+ if outcome == "success" and legacy_category:
206
+ outcome = V1_SUCCESS_CATEGORY_TO_OUTCOME.get(legacy_category, outcome)
207
+ if outcome:
208
+ out["outcome"] = outcome
209
+
210
+ if "episode_description" in attrs:
211
+ out["episode_description"] = decode_attr(attrs.get("episode_description"))
212
+ elif "failure_description" in attrs: # v1
213
+ out["episode_description"] = decode_attr(attrs.get("failure_description"))
214
+
215
+ raw_categories = taxonomy.get("side_effect_category", taxonomy.get("failure_category"))
216
+ if raw_categories is not None:
217
+ out["side_effect_category"] = [normalize_category(c) for c in as_value_list(raw_categories)]
218
+
219
+ if taxonomy.get("severity") is not None:
220
+ out["severity"] = normalize_severity(taxonomy.get("severity"))
221
+
222
+ if "additional_notes" in attrs:
223
+ out["additional_notes"] = decode_attr(attrs.get("additional_notes"))
224
+
225
+ return out
226
+
227
+
228
+ # ── Write ─────────────────────────────────────────────────────────────────────
229
+
230
+
231
+ def annotation_attrs_dict(annotation: Mapping[str, Any]) -> dict:
232
+ """Build the v2 attr mapping for one annotator subgroup.
233
+
234
+ Split out from :func:`write_annotation_attrs` so callers that assemble an
235
+ ``episode_annotations`` dict before opening the file (the recorder) produce the same attr
236
+ set as callers writing straight into a group.
237
+ """
238
+ attrs: dict = {
239
+ "schema": annotation.get("schema") or ANNOTATION_SCHEMA_CURRENT,
240
+ "source": annotation.get("source") or "human",
241
+ "timestamp": annotation.get("annotated_at", annotation.get("timestamp", "")),
242
+ "episode_description": annotation.get("episode_description", "") or "",
243
+ "taxonomy_schema": TAXONOMY_SCHEMA_CURRENT,
244
+ "additional_notes": annotation.get("additional_notes", "") or "",
245
+ }
246
+
247
+ outcome = decode_attr(annotation.get("outcome")).lower()
248
+ success = outcome_to_success(outcome)
249
+ if success is not None:
250
+ attrs["success"] = float(success)
251
+
252
+ taxonomy = {
253
+ "outcome": outcome,
254
+ "side_effect_category": [
255
+ normalize_category(c) for c in as_value_list(annotation.get("side_effect_category"))
256
+ ],
257
+ "severity": normalize_severity(annotation.get("severity", "")),
258
+ }
259
+ attrs["taxonomy"] = json.dumps(taxonomy, ensure_ascii=False)
260
+ return attrs
261
+
262
+
263
+ def write_annotation_attrs(group: h5py.Group, annotation: Mapping[str, Any]) -> None:
264
+ """Write the oopsie annotation attr set onto an ``episode_annotations`` subgroup."""
265
+ for key, value in annotation_attrs_dict(annotation).items():
266
+ group.attrs[key] = value