oopsie-data-tools 0.2.1__tar.gz → 0.3__tar.gz

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 (103) hide show
  1. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.gitignore +2 -1
  2. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/PKG-INFO +7 -3
  3. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/README.md +5 -2
  4. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/annotator_server.py +91 -118
  5. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/episode_recorder.py +9 -42
  6. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/rollout_annotator.py +47 -63
  7. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/ui/annotator.html +183 -309
  8. oopsie_data_tools-0.3/oopsie_data_tools/cli.py +851 -0
  9. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/init_wizard.py +23 -54
  10. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/SKILL.md +53 -2
  11. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/reference/format.md +37 -8
  12. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/reference/robot-profile.md +4 -1
  13. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/reference/setup.md +6 -6
  14. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/reference/troubleshooting.md +9 -2
  15. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/conftest.py +2 -22
  16. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/fixtures/make_invalid.py +24 -96
  17. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_annotation_schema.py +0 -7
  18. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_annotator_server.py +48 -0
  19. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_annotator_server_guards.py +0 -7
  20. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_cli_config.py +10 -33
  21. oopsie_data_tools-0.3/oopsie_data_tools/test/test_cli_json.py +200 -0
  22. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_cli_tools.py +13 -10
  23. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_credentials_location.py +3 -3
  24. oopsie_data_tools-0.3/oopsie_data_tools/test/test_episode_recorder.py +201 -0
  25. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_hf_upload.py +1 -9
  26. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_init_wizard.py +3 -3
  27. oopsie_data_tools-0.3/oopsie_data_tools/test/test_install_skill.py +158 -0
  28. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_paths.py +0 -2
  29. oopsie_data_tools-0.3/oopsie_data_tools/test/test_robot_setup.py +150 -0
  30. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_validate.py +23 -131
  31. oopsie_data_tools-0.3/oopsie_data_tools/utils/claude_skill.py +198 -0
  32. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/contributor_config.py +15 -0
  33. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/conversion_utils.py +3 -29
  34. oopsie_data_tools-0.3/oopsie_data_tools/utils/h5_inspect.py +230 -0
  35. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/hf_upload.py +10 -13
  36. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/migrate_taxonomy_v2.py +0 -38
  37. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/paths.py +0 -17
  38. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/restructure.py +20 -62
  39. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/episode_loader.py +8 -0
  40. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/episode_validator.py +13 -6
  41. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/validation_utils.py +35 -0
  42. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/pyproject.toml +2 -1
  43. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/uv.lock +4 -1
  44. oopsie_data_tools-0.2.1/oopsie_data_tools/cli.py +0 -897
  45. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_episode_recorder.py +0 -335
  46. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_install_skill.py +0 -136
  47. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_python38_compat.py +0 -79
  48. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_robot_setup.py +0 -244
  49. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_rollout_annotator.py +0 -134
  50. oopsie_data_tools-0.2.1/oopsie_data_tools/utils/claude_skill.py +0 -128
  51. oopsie_data_tools-0.2.1/oopsie_data_tools/utils/h5_inspect.py +0 -167
  52. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  53. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/ISSUE_TEMPLATE/help-request--data-conversion.md +0 -0
  54. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/ISSUE_TEMPLATE/help-request--robot-profile.md +0 -0
  55. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/ISSUE_TEMPLATE/improvement-suggestion.md +0 -0
  56. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/workflows/ci.yml +0 -0
  57. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.github/workflows/publish.yml +0 -0
  58. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/.python-version +0 -0
  59. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/LICENSE +0 -0
  60. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/configs/robot_profiles/act_plus_plus_robot_profile.yaml +0 -0
  61. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/configs/robot_profiles/openpi_example_robot_profile.yaml +0 -0
  62. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/configs/robot_profiles/template.yaml +0 -0
  63. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/conversion_script_examples/_common.py +0 -0
  64. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/conversion_script_examples/convert_ar_aloha.py +0 -0
  65. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/conversion_script_examples/convert_rlds.py +0 -0
  66. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/conversion_script_examples/convert_soar.py +0 -0
  67. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/act_plus_plus/run_policy.py +0 -0
  68. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/act_plus_plus/run_policy_browser.py +0 -0
  69. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/act_plus_plus/run_policy_bulk.py +0 -0
  70. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/act_plus_plus/run_policy_inloop.py +0 -0
  71. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/openpi/run_policy_bulk.py +0 -0
  72. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/examples/inference_examples/openpi/run_policy_inloop.py +0 -0
  73. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/__init__.py +0 -0
  74. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/__init__.py +0 -0
  75. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/annotation_tool/annotation_schema.py +0 -0
  76. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/skill/reference/conversion.md +0 -0
  77. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/__init__.py +0 -0
  78. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/fixtures/__init__.py +0 -0
  79. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/fixtures/make_valid.py +0 -0
  80. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_annotation_completeness.py +0 -0
  81. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_bulk_inference_end_to_end.py +0 -0
  82. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_contributor_config.py +0 -0
  83. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_conversion_utils_annotations.py +0 -0
  84. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_conversion_utils_writers.py +0 -0
  85. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_diversity.py +0 -0
  86. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_episode_video_paths.py +0 -0
  87. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_migrate_taxonomy_v2.py +0 -0
  88. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_new_profile.py +0 -0
  89. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_record_step_purity.py +0 -0
  90. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/test/test_rotation_utils.py +0 -0
  91. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/__init__.py +0 -0
  92. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/h5.py +0 -0
  93. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/hf_limits.py +0 -0
  94. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/log.py +0 -0
  95. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/robot_profile/__init__.py +0 -0
  96. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/robot_profile/robot_profile.py +0 -0
  97. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/robot_profile/rotation_utils.py +0 -0
  98. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/robot_profile/template.py +0 -0
  99. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/__init__.py +0 -0
  100. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/annotation_completeness.py +0 -0
  101. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/diversity.py +0 -0
  102. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/episode_data.py +0 -0
  103. {oopsie_data_tools-0.2.1 → oopsie_data_tools-0.3}/oopsie_data_tools/utils/validation/errors.py +0 -0
@@ -220,7 +220,8 @@ trajectory_*/
220
220
 
221
221
  # Claude Code memory file
222
222
  CLAUDE.md
223
- .claude
223
+ .claude/*
224
+ .claude/skills/*
224
225
 
225
226
  # Jekyll build output
226
227
  docs/_site/
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oopsie-data-tools
3
- Version: 0.2.1
3
+ Version: 0.3
4
4
  Summary: Collect, annotate, validate and upload robotic manipulation rollout data.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.8
7
+ Requires-Dist: click>=8.1.3
7
8
  Requires-Dist: flask>=3.0.0
8
9
  Requires-Dist: h5py>=3.7.0
9
10
  Requires-Dist: huggingface-hub>=0.36.2
@@ -24,18 +25,21 @@ Description-Content-Type: text/markdown
24
25
 
25
26
  Tools for collecting, annotating, inspecting, and converting robotic manipulation rollout data.
26
27
 
27
- This repository currently provides around:
28
+ This repository currently provides tools for:
28
29
 
29
30
  - HDF5 episode recording (`EpisodeRecorder`)
30
31
  - Web annotation workflows
31
32
  - In-the-loop annotation during policy rollout
32
33
 
34
+ as well as all the necessary utilities to validate, inspect, and upload Oopsie-Data to the official repositories.
35
+
36
+ [Sign up today](https://forms.gle/9arwZHAvRjvbozoT7) and start contributing!
37
+
33
38
  ---
34
39
 
35
40
  For detailed explanations on how to use our tooling and contribute to the project, please visit [our website](https://oopsie-data.com/).
36
41
 
37
42
  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).
38
- You can also use the information in AI_CONTEXT.md to guide a coding agent through the setup.
39
43
 
40
44
  ## Repository structure
41
45
 
@@ -2,18 +2,21 @@
2
2
 
3
3
  Tools for collecting, annotating, inspecting, and converting robotic manipulation rollout data.
4
4
 
5
- This repository currently provides around:
5
+ This repository currently provides tools for:
6
6
 
7
7
  - HDF5 episode recording (`EpisodeRecorder`)
8
8
  - Web annotation workflows
9
9
  - In-the-loop annotation during policy rollout
10
10
 
11
+ as well as all the necessary utilities to validate, inspect, and upload Oopsie-Data to the official repositories.
12
+
13
+ [Sign up today](https://forms.gle/9arwZHAvRjvbozoT7) and start contributing!
14
+
11
15
  ---
12
16
 
13
17
  For detailed explanations on how to use our tooling and contribute to the project, please visit [our website](https://oopsie-data.com/).
14
18
 
15
19
  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).
16
- You can also use the information in AI_CONTEXT.md to guide a coding agent through the setup.
17
20
 
18
21
  ## Repository structure
19
22
 
@@ -14,17 +14,16 @@ Tool can also be launched as a standalone module to browse and annotate existing
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import argparse
18
17
  import json
19
18
  import logging
20
- import os
19
+ import socket
21
20
  import threading
22
21
  import webbrowser
23
22
  from dataclasses import dataclass
24
23
  from datetime import datetime, timezone
25
24
  from pathlib import Path
26
25
  from typing import Any
27
- from urllib.parse import quote, unquote
26
+ from urllib.parse import quote
28
27
 
29
28
  import h5py
30
29
  from flask import Flask, abort, jsonify, request, send_file
@@ -257,8 +256,6 @@ def api_get_annotation(sample_id: str):
257
256
  def api_save_annotation_json() -> Any:
258
257
  """Save annotation with ``sample_id`` in the JSON body (avoids slashes in URL paths)."""
259
258
  payload = _json_payload()
260
- if not isinstance(payload, dict):
261
- return jsonify({"error": "invalid JSON body"}), 400
262
259
  sample_id = str(payload.get("sample_id", "")).strip()
263
260
  if not sample_id:
264
261
  return jsonify({"error": "sample_id is required"}), 400
@@ -348,15 +345,19 @@ class H5PathError(ValueError):
348
345
 
349
346
  def __init__(self, message: str, status: int = 400) -> None:
350
347
  super().__init__(message)
351
- self.message = message
352
348
  self.status = status
353
349
 
354
350
 
351
+ @app.errorhandler(H5PathError)
352
+ def _h5_path_error(e: H5PathError):
353
+ return jsonify({"error": str(e)}), e.status
354
+
355
+
355
356
  def _safe_h5_path_from_query(samples_root: Path) -> Path:
356
357
  raw = str(request.args.get("path", "")).strip()
357
358
  if not raw:
358
359
  raise H5PathError("path query parameter is required", 400)
359
- rel = Path(unquote(raw))
360
+ rel = Path(raw)
360
361
  target = (samples_root.resolve() / rel).resolve()
361
362
  try:
362
363
  target.relative_to(samples_root.resolve())
@@ -443,21 +444,9 @@ def _h5_annotation_tick_level(h5f: h5py.File, annotator_name: str) -> int:
443
444
  level = _annotation_tick_level(ann)
444
445
  if level > 0:
445
446
  return level
446
- raw = _read_h5_attr(ea, "failure_annotation", "")
447
- raw_s = str(raw or "").strip()
448
- if raw_s:
449
- try:
450
- parsed = json.loads(raw_s)
451
- if isinstance(parsed, dict):
452
- if len(parsed) > 0:
453
- return 1
454
- elif bool(parsed):
455
- return 1
456
- except Exception:
457
- return 1
458
- if annotator_name in ea.keys() and isinstance(ea[annotator_name], h5py.Group):
459
- if _annotator_subgroup_looks_annotated(ea[annotator_name]):
460
- return 1
447
+ sub = ea.get(annotator_name)
448
+ if isinstance(sub, h5py.Group) and _annotator_subgroup_looks_annotated(sub):
449
+ return 1
461
450
  return 0
462
451
 
463
452
 
@@ -539,6 +528,49 @@ def _has_other_human_annotation(h5f: h5py.File, annotator_name: str) -> bool:
539
528
  return False
540
529
 
541
530
 
531
+ _H5_META_CACHE: dict[tuple[str, str], tuple[tuple[float, int], dict[str, Any]]] = {}
532
+ _H5_META_LOCK = threading.Lock()
533
+ _H5_META_CACHE_MAX = 5000
534
+
535
+
536
+ def _h5_meta(path: Path, annotator_name: str) -> dict[str, Any]:
537
+ """Annotation summary of one episode, cached on (mtime, size).
538
+
539
+ The browser polls ``/api/h5/list`` every 1.5 s per open tab, so without this every
540
+ poll would reopen every episode in the tree.
541
+ """
542
+ try:
543
+ st = path.stat()
544
+ except OSError:
545
+ return {"tick_level": 0, "annotated_by_others": False, "annotation": None}
546
+ key = (str(path), annotator_name)
547
+ stamp = (st.st_mtime, st.st_size)
548
+ with _H5_META_LOCK:
549
+ hit = _H5_META_CACHE.get(key)
550
+ if hit is not None and hit[0] == stamp:
551
+ return hit[1]
552
+
553
+ meta: dict[str, Any] = {"tick_level": 0, "annotated_by_others": False, "annotation": None}
554
+ try:
555
+ with h5py.File(path, "r") as h5f:
556
+ meta["tick_level"] = _h5_annotation_tick_level(h5f, annotator_name)
557
+ meta["annotated_by_others"] = _has_other_human_annotation(h5f, annotator_name)
558
+ ea = h5f.get("episode_annotations")
559
+ # Only the current annotator's OWN subgroup — never the legacy
560
+ # episode-level failure_annotation fallback, which isn't annotator-scoped
561
+ # and could surface someone else's labels (#27).
562
+ if isinstance(ea, h5py.Group) and isinstance(ea.get(annotator_name), h5py.Group):
563
+ meta["annotation"] = _read_existing_annotation_dict(ea, annotator_name)
564
+ except Exception:
565
+ meta = {"tick_level": 0, "annotated_by_others": False, "annotation": None}
566
+
567
+ with _H5_META_LOCK:
568
+ if len(_H5_META_CACHE) >= _H5_META_CACHE_MAX:
569
+ _H5_META_CACHE.clear()
570
+ _H5_META_CACHE[key] = (stamp, meta)
571
+ return meta
572
+
573
+
542
574
  @app.get("/api/h5/list")
543
575
  def api_h5_list():
544
576
  rt = _get_runtime()
@@ -548,15 +580,9 @@ def api_h5_list():
548
580
  if not p.is_file():
549
581
  continue
550
582
  rel = p.resolve().relative_to(root).as_posix()
551
- tick_level = 0
552
- others = False
553
- try:
554
- with h5py.File(p, "r") as h5f:
555
- tick_level = _h5_annotation_tick_level(h5f, rt.cfg.annotator_name)
556
- others = _has_other_human_annotation(h5f, rt.cfg.annotator_name)
557
- except Exception:
558
- tick_level = 0
559
- others = False
583
+ meta = _h5_meta(p, rt.cfg.annotator_name)
584
+ tick_level = meta["tick_level"]
585
+ others = meta["annotated_by_others"]
560
586
  annotated = tick_level >= 1
561
587
  entries.append(
562
588
  {
@@ -593,18 +619,8 @@ def api_recent_annotations():
593
619
  for p in files:
594
620
  if len(recent) >= limit:
595
621
  break
596
- try:
597
- with h5py.File(p, "r") as h5f:
598
- ea = h5f.get("episode_annotations")
599
- if not isinstance(ea, h5py.Group):
600
- continue
601
- # Only the current annotator's OWN subgroup — never the legacy
602
- # episode-level failure_annotation fallback, which isn't
603
- # annotator-scoped and could surface someone else's labels (#27).
604
- if ann_name not in ea.keys() or not isinstance(ea[ann_name], h5py.Group):
605
- continue
606
- ann = _read_existing_annotation_dict(ea, ann_name)
607
- except Exception:
622
+ ann = _h5_meta(p, ann_name)["annotation"]
623
+ if ann is None:
608
624
  continue
609
625
  # Anything but a clean success has details worth copying — a suboptimal-execution
610
626
  # description is as reusable as a failure's.
@@ -632,10 +648,7 @@ def api_recent_annotations():
632
648
  def api_h5_sample():
633
649
  rt = _get_runtime()
634
650
  samples_root = rt.cfg.samples_dir.resolve()
635
- try:
636
- h5_path = _safe_h5_path_from_query(samples_root)
637
- except H5PathError as e:
638
- return jsonify({"error": e.message}), e.status
651
+ h5_path = _safe_h5_path_from_query(samples_root)
639
652
 
640
653
  video_urls: dict[str, str] = {}
641
654
  existing_annotation: dict[str, Any] = {}
@@ -705,14 +718,9 @@ def api_h5_sample():
705
718
  def api_h5_save_annotation():
706
719
  rt = _get_runtime()
707
720
  samples_root = rt.cfg.samples_dir.resolve()
708
- try:
709
- h5_path = _safe_h5_path_from_query(samples_root)
710
- except H5PathError as e:
711
- return jsonify({"error": e.message}), e.status
721
+ h5_path = _safe_h5_path_from_query(samples_root)
712
722
 
713
723
  payload = _json_payload()
714
- if not isinstance(payload, dict):
715
- return jsonify({"error": "invalid JSON body"}), 400
716
724
  error = validate_annotation_payload(payload)
717
725
  if error:
718
726
  return jsonify({"error": error}), 400
@@ -727,10 +735,8 @@ def api_h5_save_annotation():
727
735
  ea = f.require_group("episode_annotations")
728
736
  ag = ea.require_group(ann["annotator"])
729
737
  write_annotation_attrs(ag, ann)
730
- except OSError as e:
731
- return jsonify({"error": f"could not write HDF5: {e}"}), 500
732
738
  except Exception as e:
733
- return jsonify({"error": f"could not update annotation: {e}"}), 500
739
+ return jsonify({"error": f"could not write annotation: {e}"}), 500
734
740
 
735
741
  return jsonify({"status": "saved", "annotation": ann})
736
742
 
@@ -740,10 +746,7 @@ def api_h5_set_instruction():
740
746
  """Overwrite an episode's ``language_instruction`` root attr (issue #31)."""
741
747
  rt = _get_runtime()
742
748
  samples_root = rt.cfg.samples_dir.resolve()
743
- try:
744
- h5_path = _safe_h5_path_from_query(samples_root)
745
- except H5PathError as e:
746
- return jsonify({"error": e.message}), e.status
749
+ h5_path = _safe_h5_path_from_query(samples_root)
747
750
 
748
751
  instruction = str(_json_payload().get("instruction", "")).strip()
749
752
  if not instruction:
@@ -752,10 +755,8 @@ def api_h5_set_instruction():
752
755
  try:
753
756
  with h5py.File(h5_path, "r+") as f:
754
757
  f.attrs["language_instruction"] = instruction
755
- except OSError as e:
756
- return jsonify({"error": f"could not write HDF5: {e}"}), 500
757
758
  except Exception as e:
758
- return jsonify({"error": f"could not update instruction: {e}"}), 500
759
+ return jsonify({"error": f"could not write instruction: {e}"}), 500
759
760
 
760
761
  return jsonify({"status": "saved", "language_instruction": instruction})
761
762
 
@@ -770,21 +771,29 @@ def _load_template(filename: str) -> str:
770
771
  return path.read_text(encoding="utf-8")
771
772
 
772
773
 
774
+ def _port_in_use(port: int, host: str = "localhost") -> bool:
775
+ """Whether something is already listening on ``host:port``.
776
+
777
+ Connecting rather than test-binding: a bind probe also trips on sockets in TIME_WAIT,
778
+ which werkzeug's SO_REUSEADDR would have accepted anyway.
779
+ """
780
+ try:
781
+ with socket.create_connection((host, port), timeout=0.5):
782
+ return True
783
+ except OSError:
784
+ return False
785
+
786
+
773
787
  def run_server(
774
788
  samples_dir: Path,
775
789
  annotator_name: str,
776
790
  port: int = 5001,
777
791
  open_browser: bool = True,
778
792
  with_rollouts: bool = False,
779
- debug: bool = False,
780
793
  ) -> int:
781
794
  """Configure the runtime and serve the annotation UI (blocks until interrupted).
782
795
 
783
- Shared by this module's ``main()`` and the ``oopsie-data annotate`` CLI command.
784
-
785
- ``debug`` is off by default and should stay off outside development: it enables the
786
- Werkzeug debugger, whose interactive console executes arbitrary Python on request, and
787
- the reloader, which re-executes this process and so loses any state answered at startup.
796
+ Backs the ``oopsie-data annotate`` CLI command.
788
797
  """
789
798
  samples_dir = Path(samples_dir).resolve()
790
799
  samples_dir.mkdir(parents=True, exist_ok=True)
@@ -794,57 +803,21 @@ def run_server(
794
803
  browse_only=bool(not with_rollouts),
795
804
  )
796
805
 
797
- # WERKZEUG_RUN_MAIN marks the reloader's child process; only the parent should open a
798
- # browser. Only reachable with debug=True, but harmless and correct either way.
799
- if open_browser and os.environ.get("WERKZEUG_RUN_MAIN") != "true":
806
+ # Checked before the browser opens: werkzeug would otherwise fail its own bind a
807
+ # moment later, leaving a tab pointed at whatever already owns the port.
808
+ if _port_in_use(port):
809
+ logging.getLogger(__name__).error(
810
+ "Port %d is already in use — another annotator is probably still running "
811
+ "(open http://localhost:%d/ to check). Stop it, or pass --port with a free port.",
812
+ port,
813
+ port,
814
+ )
815
+ return 1
816
+
817
+ if open_browser:
800
818
  webbrowser.open(f"http://localhost:{port}/")
801
819
 
802
820
  logging.getLogger("werkzeug").setLevel(logging.WARNING)
803
821
 
804
- app.run(host="localhost", port=port, debug=debug)
822
+ app.run(host="localhost", port=port)
805
823
  return 0
806
-
807
-
808
- def main() -> int:
809
- parser = argparse.ArgumentParser(description="Annotator server")
810
- parser.add_argument(
811
- "--samples-dir",
812
- type=Path,
813
- default=Path("samples"),
814
- help="Directory containing saved MP4s (and HDF5 episodes) for this session",
815
- )
816
- parser.add_argument("--port", type=int, default=5001)
817
- parser.add_argument(
818
- "--annotator-name",
819
- type=str,
820
- required=True,
821
- help="Annotator name to stamp into saved annotations",
822
- )
823
- parser.add_argument("--no-browser", action="store_true")
824
- parser.add_argument(
825
- "--with-rollouts",
826
- action="store_true",
827
- help="HDF5 browser + annotation form + rollouts",
828
- )
829
- parser.add_argument(
830
- "--debug",
831
- action="store_true",
832
- help=(
833
- "Development only: enable the Werkzeug reloader and debugger. The debugger's "
834
- "console executes arbitrary Python on request — never use it on a shared machine."
835
- ),
836
- )
837
- args = parser.parse_args()
838
-
839
- return run_server(
840
- samples_dir=args.samples_dir,
841
- annotator_name=args.annotator_name,
842
- port=args.port,
843
- open_browser=not args.no_browser,
844
- with_rollouts=args.with_rollouts,
845
- debug=args.debug,
846
- )
847
-
848
-
849
- if __name__ == "__main__":
850
- raise SystemExit(main())
@@ -17,7 +17,7 @@ from oopsie_data_tools.annotation_tool.annotation_schema import (
17
17
  success_to_outcome,
18
18
  write_annotation_attrs,
19
19
  )
20
- from oopsie_data_tools.utils.contributor_config import read_contributor_config
20
+ from oopsie_data_tools.utils import contributor_config
21
21
  from oopsie_data_tools.utils.robot_profile.robot_profile import RobotProfile, robot_profile_to_json
22
22
  from oopsie_data_tools.utils.robot_profile.rotation_utils import ActionQuatConversion
23
23
  from oopsie_data_tools.utils.validation.episode_data import EpisodeData, VideoInfo
@@ -88,7 +88,7 @@ class EpisodeRecorder:
88
88
  robot_profile (RobotProfile): Robot profile.
89
89
  data_root_dir (str): Base output directory for saved artifacts.
90
90
  operator_name (str): Name of the operator recording the episode.
91
- session_name (str | None): Optional unique session name
91
+ resume_session_name (str | None): Optional unique session name
92
92
 
93
93
  Raises:
94
94
  ValueError: If ``data_root_dir`` is not a valid directory.
@@ -134,7 +134,7 @@ class EpisodeRecorder:
134
134
  # AttributeError on save_fname — an undocumented ordering requirement.
135
135
  self.reset_episode_recorder()
136
136
 
137
- self.lab_id, _ = read_contributor_config()
137
+ self.lab_id, _ = contributor_config.read_contributor_config()
138
138
 
139
139
  def reset_episode_recorder(self) -> None:
140
140
  """Reset the buffers and start a new episode."""
@@ -180,24 +180,13 @@ class EpisodeRecorder:
180
180
  # Buffer frames for each configured camera (if available)
181
181
  for cam in self.camera_names:
182
182
  frame = self._get_camera_frame(observation["image_observation"], cam)
183
- if frame is not None:
184
- self.frames[cam].append(np.asarray(frame, dtype=np.uint8))
183
+ self.frames[cam].append(np.asarray(frame, dtype=np.uint8))
185
184
 
186
185
  # Buffer timestep data
187
186
  step_data = {"robot_state": {}, "action_dict": {}}
188
187
  for key in self.robot_profile.robot_state_keys:
189
188
  step_data["robot_state"][key] = np.asarray(robot_state[key], dtype=np.float32)
190
- step_data["action_dict"] = {
191
- "cartesian_position": action.get("cartesian_position", None),
192
- "cartesian_velocity": action.get("cartesian_velocity", None),
193
- "joint_position": action.get("joint_position", None),
194
- "joint_velocity": action.get("joint_velocity", None),
195
- "base_position": action.get("base_position", None),
196
- "base_velocity": action.get("base_velocity", None),
197
- "gripper_velocity": action.get("gripper_velocity", None),
198
- "gripper_position": action.get("gripper_position", None),
199
- "gripper_binary": action.get("gripper_binary", None),
200
- }
189
+ step_data["action_dict"] = {k: action.get(k) for k in sorted(VALID_ACTION_KEYS)}
201
190
  self.timesteps.append(step_data)
202
191
 
203
192
  def _save_videos(self) -> dict[str, str]:
@@ -252,7 +241,7 @@ class EpisodeRecorder:
252
241
  """Persist the currently buffered episode to disk.
253
242
 
254
243
  Args:
255
- metadata (dict[str, Any]): Save metadata containing language and
244
+ data (dict[str, Any]): Save metadata containing language and
256
245
  annotation fields.
257
246
 
258
247
  Returns:
@@ -349,14 +338,6 @@ class EpisodeRecorder:
349
338
  f"action must not be empty. Valid keys: {VALID_ACTION_KEYS}. Please pass it in your record_step() call. Double check that the passed keys match the robot profile you initialized the recorder with."
350
339
  )
351
340
 
352
- # Make sure the action keys are valid
353
- invalid_action = set(action.keys()) - set(VALID_ACTION_KEYS)
354
- if invalid_action:
355
- raise ValueError(
356
- f"action contains unrecognized keys: {sorted(invalid_action)}. "
357
- f"Valid keys: {VALID_ACTION_KEYS}"
358
- )
359
-
360
341
  # Make sure the action keys agree between robot_profile and the action dict
361
342
  profile_action_keys = set(self.robot_profile.action_space)
362
343
  action_keys = set(action.keys())
@@ -429,7 +410,7 @@ class EpisodeRecorder:
429
410
 
430
411
  Args:
431
412
  path (Path): Target HDF5 file path.
432
- metadata (dict[str, Any]): Normalized metadata payload.
413
+ data (dict[str, Any]): Normalized metadata payload.
433
414
 
434
415
  Returns:
435
416
  None: This method only performs file I/O side effects.
@@ -562,22 +543,8 @@ class EpisodeRecorder:
562
543
  def _get_camera_frame(
563
544
  self, observation: dict[str, Any], cam_name: str
564
545
  ) -> np.ndarray | None:
565
- """Extract a camera frame from supported observation key patterns.
566
-
567
- Args:
568
- observation (dict[str, Any]): Observation dictionary for one
569
- timestep.
570
- cam_name (str): Canonical camera name to resolve.
571
-
572
- Returns:
573
- np.ndarray | None: Camera frame array when available, otherwise
574
- ``None``.
575
- """
576
- candidates = (cam_name, f"image_{cam_name}", f"{cam_name}_image")
577
- for key in candidates:
578
- if key in observation:
579
- return np.asarray(observation[key])
580
- return None
546
+ """The frame for *cam_name*; the key is guaranteed present by the step check."""
547
+ return np.asarray(observation[cam_name])
581
548
 
582
549
  def _validate_pre_save(self, data: dict[str, Any]) -> None:
583
550
  """Perform final validation checks before saving the episode.