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,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
|