oopsie-data-tools 0.2.0__tar.gz → 0.2.1__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 (100) hide show
  1. oopsie_data_tools-0.2.1/.github/workflows/publish.yml +67 -0
  2. oopsie_data_tools-0.2.1/PKG-INFO +54 -0
  3. oopsie_data_tools-0.2.1/README.md +32 -0
  4. oopsie_data_tools-0.2.1/examples/conversion_script_examples/_common.py +172 -0
  5. oopsie_data_tools-0.2.1/examples/conversion_script_examples/convert_ar_aloha.py +275 -0
  6. oopsie_data_tools-0.2.1/examples/conversion_script_examples/convert_rlds.py +279 -0
  7. oopsie_data_tools-0.2.1/examples/conversion_script_examples/convert_soar.py +250 -0
  8. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/annotation_schema.py +8 -6
  9. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/annotator_server.py +6 -6
  10. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/ui/annotator.html +23 -14
  11. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/cli.py +13 -10
  12. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/init_wizard.py +23 -12
  13. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/reference/conversion.md +29 -10
  14. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/reference/format.md +1 -1
  15. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/reference/setup.md +15 -7
  16. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/fixtures/make_invalid.py +1 -1
  17. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/fixtures/make_valid.py +3 -3
  18. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_annotation_completeness.py +4 -4
  19. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_annotation_schema.py +6 -6
  20. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_annotator_server.py +5 -5
  21. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_cli_config.py +11 -11
  22. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_cli_tools.py +1 -1
  23. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_conversion_utils_annotations.py +2 -2
  24. oopsie_data_tools-0.2.1/oopsie_data_tools/test/test_conversion_utils_writers.py +76 -0
  25. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_credentials_location.py +47 -17
  26. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_hf_upload.py +10 -12
  27. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_install_skill.py +20 -4
  28. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_migrate_taxonomy_v2.py +5 -5
  29. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_new_profile.py +7 -6
  30. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_paths.py +32 -11
  31. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_robot_setup.py +3 -3
  32. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_validate.py +3 -3
  33. oopsie_data_tools-0.2.1/oopsie_data_tools/utils/claude_skill.py +128 -0
  34. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/contributor_config.py +26 -16
  35. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/conversion_utils.py +71 -3
  36. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/hf_upload.py +24 -7
  37. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/migrate_taxonomy_v2.py +5 -5
  38. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/paths.py +31 -30
  39. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/annotation_completeness.py +4 -4
  40. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/pyproject.toml +1 -1
  41. oopsie_data_tools-0.2.0/AI_CONTEXT.md +0 -269
  42. oopsie_data_tools-0.2.0/PKG-INFO +0 -131
  43. oopsie_data_tools-0.2.0/README.md +0 -109
  44. oopsie_data_tools-0.2.0/oopsie_data_tools/utils/claude_skill.py +0 -96
  45. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  46. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.github/ISSUE_TEMPLATE/help-request--data-conversion.md +0 -0
  47. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.github/ISSUE_TEMPLATE/help-request--robot-profile.md +0 -0
  48. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.github/ISSUE_TEMPLATE/improvement-suggestion.md +0 -0
  49. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.github/workflows/ci.yml +0 -0
  50. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.gitignore +0 -0
  51. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/.python-version +0 -0
  52. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/LICENSE +0 -0
  53. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/configs/robot_profiles/act_plus_plus_robot_profile.yaml +0 -0
  54. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/configs/robot_profiles/openpi_example_robot_profile.yaml +0 -0
  55. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/configs/robot_profiles/template.yaml +0 -0
  56. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/act_plus_plus/run_policy.py +0 -0
  57. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/act_plus_plus/run_policy_browser.py +0 -0
  58. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/act_plus_plus/run_policy_bulk.py +0 -0
  59. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/act_plus_plus/run_policy_inloop.py +0 -0
  60. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/openpi/run_policy_bulk.py +0 -0
  61. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/examples/inference_examples/openpi/run_policy_inloop.py +0 -0
  62. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/__init__.py +0 -0
  63. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/__init__.py +0 -0
  64. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/episode_recorder.py +0 -0
  65. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/annotation_tool/rollout_annotator.py +0 -0
  66. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/SKILL.md +0 -0
  67. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/reference/robot-profile.md +0 -0
  68. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/skill/reference/troubleshooting.md +0 -0
  69. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/__init__.py +0 -0
  70. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/conftest.py +0 -0
  71. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/fixtures/__init__.py +0 -0
  72. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_annotator_server_guards.py +0 -0
  73. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_bulk_inference_end_to_end.py +0 -0
  74. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_contributor_config.py +0 -0
  75. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_diversity.py +0 -0
  76. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_episode_recorder.py +0 -0
  77. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_episode_video_paths.py +0 -0
  78. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_init_wizard.py +0 -0
  79. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_python38_compat.py +0 -0
  80. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_record_step_purity.py +0 -0
  81. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_rollout_annotator.py +0 -0
  82. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/test/test_rotation_utils.py +0 -0
  83. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/__init__.py +0 -0
  84. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/h5.py +0 -0
  85. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/h5_inspect.py +0 -0
  86. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/hf_limits.py +0 -0
  87. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/log.py +0 -0
  88. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/restructure.py +0 -0
  89. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/robot_profile/__init__.py +0 -0
  90. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/robot_profile/robot_profile.py +0 -0
  91. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/robot_profile/rotation_utils.py +0 -0
  92. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/robot_profile/template.py +0 -0
  93. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/__init__.py +0 -0
  94. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/diversity.py +0 -0
  95. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/episode_data.py +0 -0
  96. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/episode_loader.py +0 -0
  97. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/episode_validator.py +0 -0
  98. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/errors.py +0 -0
  99. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/oopsie_data_tools/utils/validation/validation_utils.py +0 -0
  100. {oopsie_data_tools-0.2.0 → oopsie_data_tools-0.2.1}/uv.lock +0 -0
@@ -0,0 +1,67 @@
1
+ name: Publish to PyPI
2
+
3
+ # Release flow: bump `version` in pyproject.toml, merge to main, then
4
+ # git tag v0.2.1 && git push origin v0.2.1
5
+ # The tag must sit on a commit that is on main, and its version must match
6
+ # pyproject.toml — both are enforced below before anything is uploaded.
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ # The ancestry check below needs main, which a tag checkout does not fetch.
18
+ fetch-depth: 0
19
+ - uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Tag must be on main
24
+ run: |
25
+ git fetch --no-tags origin main
26
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
27
+ echo "::error::${GITHUB_REF_NAME} points at a commit that is not on main."
28
+ exit 1
29
+ fi
30
+
31
+ # A tag whose version disagrees with pyproject.toml publishes a release under a
32
+ # name nobody expects, and PyPI uploads cannot be replaced. Fail before building.
33
+ - name: Tag must match the project version
34
+ run: |
35
+ project_version="$(uv version --short)"
36
+ tag_version="${GITHUB_REF_NAME#v}"
37
+ if [ "$project_version" != "$tag_version" ]; then
38
+ echo "::error::tag ${GITHUB_REF_NAME} != pyproject.toml version ${project_version}."
39
+ exit 1
40
+ fi
41
+
42
+ - run: uv build
43
+ - name: Check metadata renders on PyPI
44
+ run: uvx twine check --strict dist/*
45
+
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ publish:
52
+ needs: build
53
+ runs-on: ubuntu-latest
54
+ # Trusted Publishing: PyPI verifies this workflow's OIDC token instead of an API
55
+ # token, so there is no long-lived credential in the repo. The environment name
56
+ # must match the one configured on the PyPI publisher.
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/p/oopsie-data-tools
60
+ permissions:
61
+ id-token: write
62
+ steps:
63
+ - uses: actions/download-artifact@v4
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: oopsie-data-tools
3
+ Version: 0.2.1
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
+ ---
34
+
35
+ For detailed explanations on how to use our tooling and contribute to the project, please visit [our website](https://oopsie-data.com/).
36
+
37
+ 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
+
40
+ ## Repository structure
41
+
42
+ The main tooling for data gathering and annotation is located in `oopsie-data-tools`.
43
+
44
+ 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.
45
+
46
+ ## Issue/PR for support requests
47
+
48
+ 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.
49
+
50
+ ## Contributing
51
+
52
+ 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/)!
53
+
54
+ If you run into any issues, please do not hesitate to contact the team via mail or raise an issue here.
@@ -0,0 +1,32 @@
1
+ # Oopsie Tools
2
+
3
+ Tools for collecting, annotating, inspecting, and converting robotic manipulation rollout data.
4
+
5
+ This repository currently provides around:
6
+
7
+ - HDF5 episode recording (`EpisodeRecorder`)
8
+ - Web annotation workflows
9
+ - In-the-loop annotation during policy rollout
10
+
11
+ ---
12
+
13
+ For detailed explanations on how to use our tooling and contribute to the project, please visit [our website](https://oopsie-data.com/).
14
+
15
+ 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
+
18
+ ## Repository structure
19
+
20
+ The main tooling for data gathering and annotation is located in `oopsie-data-tools`.
21
+
22
+ 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.
23
+
24
+ ## Issue/PR for support requests
25
+
26
+ 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.
27
+
28
+ ## Contributing
29
+
30
+ 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/)!
31
+
32
+ If you run into any issues, please do not hesitate to contact the team via mail or raise an issue here.
@@ -0,0 +1,172 @@
1
+ """Scaffolding shared by the converter examples in this directory.
2
+
3
+ Everything schema-shaped — root attrs, robot states, actions, video paths, annotations —
4
+ lives in :mod:`oopsie_data_tools.utils.conversion_utils` and is used from there. What is
5
+ left over is the part that is the same in every converter but is not about the format:
6
+ the CLI shape, the output directory layout, and a batch loop that keeps going when one
7
+ episode fails.
8
+
9
+ A converter in this directory is then three things:
10
+
11
+ 1. ``build_profile(...)`` — the :class:`RobotProfile` describing the source robot;
12
+ 2. ``convert_one(item, episode_id, out)`` — read one source episode, write one HDF5;
13
+ 3. a ``main`` that discovers items and hands them to :func:`run_batch`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import shutil
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Callable, Iterable, Sequence, TypeVar
23
+
24
+ from tqdm import tqdm
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ class Skip(Exception):
30
+ """Raised by a converter to drop one source episode without counting it as a failure.
31
+
32
+ Use it for episodes that are legitimately out of scope — too short for the validator's
33
+ duration bounds, missing a camera — and let real errors propagate as themselves.
34
+ """
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ConversionOutput:
39
+ """The ``{output_dir}/{episode_id}.h5`` + ``{output_dir}/videos/`` layout.
40
+
41
+ Videos are *copied* rather than symlinked: a symlink survives local validation and then
42
+ uploads as a dangling path.
43
+ """
44
+
45
+ output_dir: Path
46
+ videos_dir: Path
47
+
48
+ @classmethod
49
+ def create(cls, output_dir: Path | str) -> "ConversionOutput":
50
+ output_dir = Path(output_dir).resolve()
51
+ videos_dir = output_dir / "videos"
52
+ videos_dir.mkdir(parents=True, exist_ok=True)
53
+ return cls(output_dir=output_dir, videos_dir=videos_dir)
54
+
55
+ def episode_h5(self, episode_id: str) -> Path:
56
+ return self.output_dir / f"{episode_id}.h5"
57
+
58
+ def video_path(self, episode_id: str, camera: str) -> Path:
59
+ """Absolute destination for one camera's MP4.
60
+
61
+ Pass it straight to ``write_video_paths``, which relativizes against the .h5 file.
62
+ """
63
+ return self.videos_dir / f"{episode_id}_{camera}.mp4"
64
+
65
+ def copy_video(self, source: Path, episode_id: str, camera: str) -> Path:
66
+ if not Path(source).is_file():
67
+ raise Skip(f"missing source video for camera {camera!r}")
68
+ destination = self.video_path(episode_id, camera)
69
+ shutil.copy2(source, destination)
70
+ return destination
71
+
72
+
73
+ def add_common_args(parser: argparse.ArgumentParser, *, source_help: str) -> None:
74
+ """Add the arguments every converter in this directory takes."""
75
+ parser.add_argument("--source", "-s", type=Path, required=True, help=source_help)
76
+ parser.add_argument(
77
+ "--output-dir",
78
+ "-o",
79
+ type=Path,
80
+ required=True,
81
+ help="Output directory for the HDF5 files and their videos/ subdirectory.",
82
+ )
83
+ parser.add_argument(
84
+ "--lab-id",
85
+ required=True,
86
+ help="Your registered lab id. Written as a root attr; 'your_lab_id' is rejected.",
87
+ )
88
+ parser.add_argument(
89
+ "--operator-name",
90
+ required=True,
91
+ help="Who ran the episodes. Written as a root attr.",
92
+ )
93
+ parser.add_argument(
94
+ "--annotator-name",
95
+ required=True,
96
+ help="Name of the episode_annotations/<annotator>/ subgroup the labels land in.",
97
+ )
98
+ parser.add_argument(
99
+ "--start-id",
100
+ type=int,
101
+ default=0,
102
+ help="Starting numeric counter for episode IDs (default: 0).",
103
+ )
104
+ parser.add_argument(
105
+ "--max-episodes",
106
+ type=int,
107
+ default=None,
108
+ help="Stop after writing this many episodes.",
109
+ )
110
+ parser.add_argument(
111
+ "--overwrite",
112
+ action="store_true",
113
+ help="Rewrite output files that already exist (default: leave them alone).",
114
+ )
115
+
116
+
117
+ def default_episode_id(_item: object, counter: int) -> str:
118
+ return f"episode_{counter:06d}"
119
+
120
+
121
+ def run_batch(
122
+ items: Sequence[T] | Iterable[T],
123
+ convert_one: Callable[[T, str, ConversionOutput], None],
124
+ *,
125
+ out: ConversionOutput,
126
+ episode_id_for: Callable[[T, int], str] = default_episode_id,
127
+ label: Callable[[T], str] = str,
128
+ start_id: int = 0,
129
+ desc: str = "Converting",
130
+ overwrite: bool = False,
131
+ max_episodes: int | None = None,
132
+ ) -> dict[str, int]:
133
+ """Convert every item, counting outcomes instead of stopping at the first failure.
134
+
135
+ ``convert_one`` writes ``out.episode_h5(episode_id)``; raising :class:`Skip` drops the
136
+ item. The counter behind ``episode_id_for`` advances for every item, skipped ones
137
+ included, so an id stays tied to its source episode across reruns with different
138
+ filters.
139
+ """
140
+ items = list(items)
141
+ counts = {"written": 0, "exists": 0, "skipped": 0, "failed": 0}
142
+
143
+ with tqdm(items, desc=desc, unit="ep") as progress:
144
+ for offset, item in enumerate(progress):
145
+ if max_episodes is not None and counts["written"] >= max_episodes:
146
+ break
147
+ episode_id = episode_id_for(item, start_id + offset)
148
+ progress.set_postfix_str(label(item)[:48])
149
+
150
+ if out.episode_h5(episode_id).exists() and not overwrite:
151
+ counts["exists"] += 1
152
+ continue
153
+ try:
154
+ convert_one(item, episode_id, out)
155
+ except Skip as skip:
156
+ tqdm.write(f" – {episode_id}: {skip}")
157
+ counts["skipped"] += 1
158
+ except Exception as exc: # noqa: BLE001 — one bad episode must not end the run
159
+ tqdm.write(f" ✗ {episode_id}: {exc}")
160
+ counts["failed"] += 1
161
+ else:
162
+ counts["written"] += 1
163
+
164
+ return counts
165
+
166
+
167
+ def report(counts: dict[str, int]) -> None:
168
+ tqdm.write(
169
+ f"\nDone: {counts['written']} written, {counts['exists']} already existed, "
170
+ f"{counts['skipped']} skipped, {counts['failed']} failed.\n"
171
+ "Next: oopsie-data validate --path <output-dir>"
172
+ )
@@ -0,0 +1,275 @@
1
+ """Convert ACT / ALOHA episode HDF5 files to ``oopsiedata_format_v1`` HDF5.
2
+
3
+ Source layout (one file per episode, as written by the ACT / ACT++ recording stack):
4
+
5
+ /action (T, 14) target joint positions, both arms
6
+ /observations/qpos (T, 14) measured joint positions, both arms
7
+ /observations/qvel, /observations/effort (T, 14) optional, unused here
8
+ /success (T,) per-timestep success flag
9
+ /compress_len (3, T) JPEG byte lengths, one row per camera
10
+ /observations/images/camera_high (T, ...) frames, JPEG-compressed or raw RGB
11
+ /observations/images/camera_wrist_left (T, ...)
12
+ /observations/images/camera_wrist_right (T, ...)
13
+
14
+ Both 14-vectors are ``[6 arm joints + gripper]`` per arm. They are written out whole as
15
+ ``joint_position``, matching the 14 entries in the profile's joint-name lists, with the two
16
+ gripper columns additionally written as ``gripper_position`` — the layout of the bundled
17
+ ``configs/robot_profiles/act_plus_plus_robot_profile.yaml``.
18
+
19
+ There are no cartesian data in the source and the profile does not claim any: a joint action
20
+ space requires ``joint_position`` state, not ``cartesian_position``. Earlier versions of this
21
+ script wrote zero-filled cartesian arrays, which the validator now rejects as undeclared.
22
+
23
+ The images are decoded and re-encoded to one MP4 per camera at ``control_freq`` fps, and
24
+ downscaled if a side exceeds the 1280 px the validator allows.
25
+
26
+ ``language_instruction`` is not stored in the source, so it is passed on the command line and
27
+ applies to every episode in the run.
28
+
29
+ Output — one file per source episode:
30
+
31
+ {output_dir}/{episode_id}.h5
32
+ {output_dir}/videos/{episode_id}_{camera}.mp4
33
+
34
+ Usage:
35
+ # A single episode
36
+ python convert_ar_aloha.py -s /data/aloha/episode_0.hdf5 -o ./converted \\
37
+ --lab-id my_lab --operator-name alice --annotator-name act_eval \\
38
+ --language "Pick up the ball"
39
+
40
+ # Every episode in a directory
41
+ python convert_ar_aloha.py -s /data/aloha/rollouts -o ./converted \\
42
+ --lab-id my_lab --operator-name alice --annotator-name act_eval \\
43
+ --language "Pick up the ball"
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ from functools import partial
50
+ from pathlib import Path
51
+
52
+ import cv2
53
+ import h5py
54
+ import numpy as np
55
+ from _common import ConversionOutput, Skip, add_common_args, report, run_batch
56
+
57
+ from oopsie_data_tools.annotation_tool.episode_recorder import write_mp4
58
+ from oopsie_data_tools.utils.conversion_utils import (
59
+ resize_frames,
60
+ write_actions,
61
+ write_episode_annotations,
62
+ write_robot_states,
63
+ write_root_attrs,
64
+ write_video_paths,
65
+ )
66
+ from oopsie_data_tools.utils.robot_profile.robot_profile import RobotProfile
67
+
68
+ DEFAULT_CONTROL_FREQ = 50 # Hz — ALOHA's teleop rate, and the video fps
69
+
70
+ # Column indices within the 14D qpos / action vectors.
71
+ GRIPPER_IDX = [6, 13]
72
+
73
+ # Source dataset → output camera name. The order is also the row order of /compress_len.
74
+ CAMERA_MAP = {
75
+ "camera_high": "top",
76
+ "camera_wrist_left": "left_wrist",
77
+ "camera_wrist_right": "right_wrist",
78
+ }
79
+
80
+ JOINT_NAMES = [
81
+ f"{joint}_{side}"
82
+ for side in ("left", "right")
83
+ for joint in (
84
+ "waist",
85
+ "shoulder",
86
+ "elbow",
87
+ "forearm_roll",
88
+ "wrist_angle",
89
+ "wrist_rotate",
90
+ "gripper",
91
+ )
92
+ ]
93
+ ACTION_SPACE = ["joint_position", "gripper_position"]
94
+
95
+
96
+ def build_profile(policy_name: str, control_freq: int) -> RobotProfile:
97
+ return RobotProfile(
98
+ policy_name=policy_name,
99
+ robot_name="viperx_300",
100
+ gripper_name="viperx_300_gripper",
101
+ is_biarm=True,
102
+ uses_mobile_base=False,
103
+ control_freq=control_freq,
104
+ camera_names=list(CAMERA_MAP.values()),
105
+ robot_state_keys=["joint_position", "gripper_position"],
106
+ robot_state_joint_names=list(JOINT_NAMES),
107
+ action_space=list(ACTION_SPACE),
108
+ action_joint_names=list(JOINT_NAMES),
109
+ )
110
+
111
+
112
+ def _decode_frames(raw: np.ndarray, lengths: np.ndarray | None) -> np.ndarray:
113
+ """Return ``(T, H, W, 3)`` RGB frames from a camera dataset.
114
+
115
+ ``lengths`` is the camera's ``/compress_len`` row; without it the dataset is assumed to
116
+ hold raw RGB frames already, which is how uncompressed ALOHA recordings are stored.
117
+ """
118
+ if lengths is None:
119
+ if raw.ndim != 4 or raw.shape[-1] != 3:
120
+ raise ValueError(
121
+ f"Camera dataset has shape {raw.shape} and the file has no /compress_len, "
122
+ "so it is neither raw RGB nor decodable JPEG."
123
+ )
124
+ return np.asarray(raw, dtype=np.uint8)
125
+
126
+ frames = []
127
+ for t in range(raw.shape[0]):
128
+ buffer = np.frombuffer(raw[t, : int(lengths[t])], dtype=np.uint8)
129
+ bgr = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
130
+ if bgr is None:
131
+ raise ValueError(f"Failed to decode JPEG frame {t}")
132
+ frames.append(bgr[:, :, ::-1]) # BGR → RGB
133
+ return np.stack(frames, axis=0)
134
+
135
+
136
+ def convert_one(
137
+ source_h5: Path,
138
+ episode_id: str,
139
+ out: ConversionOutput,
140
+ *,
141
+ lab_id: str,
142
+ operator_name: str,
143
+ annotator_name: str,
144
+ language_instruction: str,
145
+ policy_name: str,
146
+ control_freq: int,
147
+ success_aggregation: str,
148
+ ) -> None:
149
+ profile = build_profile(policy_name, control_freq)
150
+
151
+ with h5py.File(source_h5, "r") as src:
152
+ action = src["action"][()]
153
+ qpos = src["observations/qpos"][()]
154
+
155
+ success_per_step = src["success"][()]
156
+ if success_per_step.size == 0:
157
+ raise Skip("no /success values to aggregate")
158
+ success = float(
159
+ success_per_step[-1] if success_aggregation == "last" else success_per_step.max()
160
+ )
161
+
162
+ compress_len = src["compress_len"][()] if "compress_len" in src else None
163
+ frames = {
164
+ camera: _decode_frames(
165
+ src[f"observations/images/{key}"][()],
166
+ None if compress_len is None else compress_len[row],
167
+ )
168
+ for row, (key, camera) in enumerate(CAMERA_MAP.items())
169
+ }
170
+
171
+ video_paths = {}
172
+ for camera, camera_frames in frames.items():
173
+ destination = out.video_path(episode_id, camera)
174
+ write_mp4(destination, resize_frames(camera_frames), float(control_freq))
175
+ video_paths[camera] = str(destination)
176
+
177
+ h5_path = out.episode_h5(episode_id)
178
+ with h5py.File(h5_path, "w") as f:
179
+ write_root_attrs(
180
+ f,
181
+ episode_id=episode_id,
182
+ language_instruction=language_instruction,
183
+ lab_id=lab_id,
184
+ operator_name=operator_name,
185
+ robot_profile=profile,
186
+ )
187
+ write_video_paths(f, video_paths, h5_path)
188
+ write_robot_states(
189
+ f,
190
+ {
191
+ "joint_position": qpos,
192
+ "gripper_position": qpos[:, GRIPPER_IDX],
193
+ },
194
+ profile.robot_state_keys,
195
+ )
196
+ write_actions(
197
+ f,
198
+ {
199
+ "joint_position": action,
200
+ "gripper_position": action[:, GRIPPER_IDX],
201
+ },
202
+ profile.action_space,
203
+ )
204
+ write_episode_annotations(f, annotator_name=annotator_name, success=success)
205
+
206
+
207
+ def discover(source: Path) -> list[Path]:
208
+ if source.is_file():
209
+ return [source]
210
+ files = sorted(p for p in source.iterdir() if p.suffix in {".h5", ".hdf5"})
211
+ if not files:
212
+ raise SystemExit(f"No .h5 or .hdf5 files found in {source}")
213
+ return files
214
+
215
+
216
+ def main() -> None:
217
+ parser = argparse.ArgumentParser(
218
+ description="Convert ACT/ALOHA HDF5 episodes to oopsiedata_format_v1 HDF5",
219
+ formatter_class=argparse.RawDescriptionHelpFormatter,
220
+ epilog=__doc__,
221
+ )
222
+ add_common_args(
223
+ parser,
224
+ source_help="A single ACT/ALOHA .hdf5 episode, or a directory of them.",
225
+ )
226
+ parser.add_argument(
227
+ "--language",
228
+ "-l",
229
+ required=True,
230
+ help="Language instruction for every episode (the source files do not store one).",
231
+ )
232
+ parser.add_argument(
233
+ "--policy-name",
234
+ default="act_plus_plus",
235
+ help="Value for robot_profile.policy_name (default: act_plus_plus).",
236
+ )
237
+ parser.add_argument(
238
+ "--control-freq",
239
+ type=int,
240
+ default=DEFAULT_CONTROL_FREQ,
241
+ help=f"Control frequency in Hz, also the video fps (default: {DEFAULT_CONTROL_FREQ}).",
242
+ )
243
+ parser.add_argument(
244
+ "--success-aggregation",
245
+ choices=["max", "last"],
246
+ default="max",
247
+ help="How to collapse the per-timestep /success flag to a scalar (default: max).",
248
+ )
249
+ args = parser.parse_args()
250
+
251
+ out = ConversionOutput.create(args.output_dir)
252
+ counts = run_batch(
253
+ discover(args.source.resolve()),
254
+ partial(
255
+ convert_one,
256
+ lab_id=args.lab_id,
257
+ operator_name=args.operator_name,
258
+ annotator_name=args.annotator_name,
259
+ language_instruction=args.language,
260
+ policy_name=args.policy_name,
261
+ control_freq=args.control_freq,
262
+ success_aggregation=args.success_aggregation,
263
+ ),
264
+ out=out,
265
+ label=lambda path: path.name,
266
+ start_id=args.start_id,
267
+ desc="ACT/ALOHA",
268
+ overwrite=args.overwrite,
269
+ max_episodes=args.max_episodes,
270
+ )
271
+ report(counts)
272
+
273
+
274
+ if __name__ == "__main__":
275
+ main()