phys-ai-dataconverter 0.1.0__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 (25) hide show
  1. phys_ai_dataconverter-0.1.0/LICENSE +21 -0
  2. phys_ai_dataconverter-0.1.0/PKG-INFO +138 -0
  3. phys_ai_dataconverter-0.1.0/README.md +102 -0
  4. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/__init__.py +8 -0
  5. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/__main__.py +5 -0
  6. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/align.py +45 -0
  7. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/cli.py +117 -0
  8. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/convert.py +227 -0
  9. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/episode.py +264 -0
  10. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/images.py +40 -0
  11. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/model.py +67 -0
  12. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/readers/__init__.py +33 -0
  13. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/readers/mcap_ros2.py +90 -0
  14. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/verify.py +65 -0
  15. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/writers/__init__.py +8 -0
  16. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter/writers/lerobot_v3.py +264 -0
  17. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/PKG-INFO +138 -0
  18. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/SOURCES.txt +23 -0
  19. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/dependency_links.txt +1 -0
  20. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/entry_points.txt +2 -0
  21. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/requires.txt +9 -0
  22. phys_ai_dataconverter-0.1.0/phys_ai_dataconverter.egg-info/top_level.txt +1 -0
  23. phys_ai_dataconverter-0.1.0/pyproject.toml +53 -0
  24. phys_ai_dataconverter-0.1.0/setup.cfg +4 -0
  25. phys_ai_dataconverter-0.1.0/tests/test_convert.py +390 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anand Krishniyer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: phys-ai-dataconverter
3
+ Version: 0.1.0
4
+ Summary: Convert robot recordings (ROS 2 MCAP) into training datasets (LeRobot v3): cameras, joint state and actions aligned by capture time
5
+ Author: Anand Krishniyer
6
+ Author-email: akrishniyer@gmail.com
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/akrishni/phys-ai-dataconverter
9
+ Project-URL: Documentation, https://github.com/akrishni/phys-ai-dataconverter#readme
10
+ Project-URL: Issues, https://github.com/akrishni/phys-ai-dataconverter/issues
11
+ Keywords: robotics,dataset,mcap,ros2,rosbag,lerobot,imitation-learning,conversion
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: phys-ai-dataprofiler>=0.4.0
28
+ Requires-Dist: mcap>=1.0
29
+ Requires-Dist: mcap-ros2-support>=0.5
30
+ Requires-Dist: av>=12
31
+ Requires-Dist: numpy
32
+ Requires-Dist: pyarrow>=14
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # phys-ai-dataconverter
38
+
39
+ Convert robot recordings into datasets you can train on. Version 0.1 turns ROS 2 teleoperation recordings
40
+ (MCAP) into [LeRobot](https://github.com/huggingface/lerobot) v3 datasets. It pairs camera frames, joint
41
+ state and the operator's commands by capture time, and refuses to fill gaps with stale values.
42
+
43
+ ```bash
44
+ pip install phys-ai-dataconverter
45
+ phys-ai-dataconverter recordings/ -o ./pick_bottles --task "pick up the bottles" --check preset:teleop-imitation
46
+ ```
47
+
48
+ Each `.mcap` file becomes one episode. `--check` then profiles the result's data table and videos with
49
+ [phys-ai-dataprofiler](https://github.com/akrishni/phys-ai-dataprofiler) and checks it against a
50
+ requirements file or profiler preset, saving the report in `meta/check.md`. The output works unchanged with LeRobot's `LeRobotDataset` and `lerobot-train`.
51
+
52
+ ## What goes in
53
+
54
+ The camera's frames are the timeline, one row per frame:
55
+
56
+ | Column | From | Default |
57
+ |---|---|---|
58
+ | `observation.images.<camera>` | `sensor_msgs/CompressedImage` (JPEG, PNG) or raw `sensor_msgs/Image`, encoded to AV1 | the highest-resolution camera; `--camera` |
59
+ | `observation.state` | `sensor_msgs/JointState` positions | the joints the action commands; `--state-joints` |
60
+ | `action` | `trajectory_msgs/JointTrajectory` commands (the teleop leader), concatenated | every such topic; `--action-topic`, `--no-action` |
61
+
62
+ The camera name comes from the topic: `/zed/zed_node/left/image_rect_color/compressed` becomes
63
+ `observation.images.zed_left`, or set it with `--camera-key`. The topics are chosen from the first file and
64
+ printed, and every later file must have the same ones.
65
+
66
+ ## Alignment
67
+
68
+ Each frame takes the joint reading and the command nearest to it in time, within half a frame interval
69
+ (`--tolerance-ms`). Leading or trailing frames with no match yet are dropped. A frame in the middle with no
70
+ match stops the conversion with the time and size of the gap. That's a gap in the recording, so the frame
71
+ isn't filled with a stale value. Raise `--tolerance-ms` to accept it.
72
+
73
+ **Clocks.** A ROS message has two times: when the sensor captured it (`header.stamp`) and when the
74
+ recorder wrote it (MCAP `log_time`). On real robots a camera frame reaches the recorder about 25 ms after
75
+ capture and a joint reading about 1 ms after, so pairing on write time matches each frame with the arm's
76
+ position a frame later. The converter pairs on capture time. Teleop commands usually carry no capture
77
+ time, so they're placed on the capture timeline using the joint topic's recorder delay. `--clock log`
78
+ pairs on write time; `--clock header` requires a capture time on every topic.
79
+
80
+ ## What comes out
81
+
82
+ LeRobot v3.0, as written by lerobot 0.4 and later:
83
+
84
+ ```
85
+ data/chunk-000/file-000.parquet observation.state, action, timestamp, frame_index, episode_index, index, task_index
86
+ videos/<camera>/chunk-000/file-000.mp4 all episodes back to back, AV1, a keyframe every 2 frames
87
+ meta/info.json stats.json tasks.parquet episodes/chunk-000/file-000.parquet
88
+ meta/conversion.json sources, topics, clock, worst match per episode, notes
89
+ ```
90
+
91
+ `meta/conversion.json` also records what the converter noticed: dropped camera frames, a camera rate
92
+ that differs from the dataset fps, or a robot clock that isn't synchronised with the recorder (two of
93
+ the public test recordings below are 73 days off). The output is built in `<output>.partial` and renamed
94
+ when complete, so a failed run leaves nothing behind. `--overwrite` only ever replaces a folder that is
95
+ already a LeRobot dataset.
96
+
97
+ ## Tested on
98
+
99
+ `ola31/readme_test_rosbag_1` and `Dongkkka/physical_ai_tools_test_rosbag` on the Hugging Face Hub, a
100
+ two-arm teleop robot: 3 recordings, 1,777 frames at 30 fps, 19 joints.
101
+ - **Values:** every state and action value equals the source message.
102
+ - **Timing:** each frame's joint reading is within 5 ms of the frame's capture time.
103
+ - **Video:** video frames are in source order.
104
+ - **LeRobot:** `LeRobotDataset` loads the result, and `lerobot-train` trains ACT on it.
105
+
106
+ ## Limits
107
+
108
+ Version 0.1 handles one camera per dataset, ROS 2 MCAP input, and joint state and commands from
109
+ `JointState` and `JointTrajectory`. Planned next: several cameras, ROS 1 `.bag` and ROS 2 `.db3`.
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ git clone https://github.com/akrishni/phys-ai-dataconverter
115
+ cd phys-ai-dataconverter
116
+ pip install -e ".[dev]"
117
+ python tests/run_all.py # offline: MCAP files with known answers are written by the tests
118
+ python tests/run_all.py --hub # also convert a public recording from the Hub
119
+ ```
120
+
121
+ ```
122
+ phys_ai_dataconverter/
123
+ model.py Stream, image and topic types shared by everything below
124
+ readers/ one module per input format (mcap_ros2)
125
+ episode.py one recording to one aligned episode: topic choice, clocks, alignment
126
+ align.py nearest-timestamp matching with a tolerance
127
+ writers/ one module per output format (lerobot_v3)
128
+ convert.py the pipeline: read, align, write to <output>.partial, rename
129
+ verify.py --check: profile the output with phys-ai-dataprofiler and evaluate requirements
130
+ cli.py the phys-ai-dataconverter command
131
+ ```
132
+
133
+ A new input format is a new module in `readers/` that produces `Stream`s. A new output format is a new
134
+ module in `writers/`. Nothing else changes.
135
+
136
+ ## License
137
+
138
+ MIT License, copyright (c) 2026 Anand Krishniyer. See [LICENSE](LICENSE).
@@ -0,0 +1,102 @@
1
+ # phys-ai-dataconverter
2
+
3
+ Convert robot recordings into datasets you can train on. Version 0.1 turns ROS 2 teleoperation recordings
4
+ (MCAP) into [LeRobot](https://github.com/huggingface/lerobot) v3 datasets. It pairs camera frames, joint
5
+ state and the operator's commands by capture time, and refuses to fill gaps with stale values.
6
+
7
+ ```bash
8
+ pip install phys-ai-dataconverter
9
+ phys-ai-dataconverter recordings/ -o ./pick_bottles --task "pick up the bottles" --check preset:teleop-imitation
10
+ ```
11
+
12
+ Each `.mcap` file becomes one episode. `--check` then profiles the result's data table and videos with
13
+ [phys-ai-dataprofiler](https://github.com/akrishni/phys-ai-dataprofiler) and checks it against a
14
+ requirements file or profiler preset, saving the report in `meta/check.md`. The output works unchanged with LeRobot's `LeRobotDataset` and `lerobot-train`.
15
+
16
+ ## What goes in
17
+
18
+ The camera's frames are the timeline, one row per frame:
19
+
20
+ | Column | From | Default |
21
+ |---|---|---|
22
+ | `observation.images.<camera>` | `sensor_msgs/CompressedImage` (JPEG, PNG) or raw `sensor_msgs/Image`, encoded to AV1 | the highest-resolution camera; `--camera` |
23
+ | `observation.state` | `sensor_msgs/JointState` positions | the joints the action commands; `--state-joints` |
24
+ | `action` | `trajectory_msgs/JointTrajectory` commands (the teleop leader), concatenated | every such topic; `--action-topic`, `--no-action` |
25
+
26
+ The camera name comes from the topic: `/zed/zed_node/left/image_rect_color/compressed` becomes
27
+ `observation.images.zed_left`, or set it with `--camera-key`. The topics are chosen from the first file and
28
+ printed, and every later file must have the same ones.
29
+
30
+ ## Alignment
31
+
32
+ Each frame takes the joint reading and the command nearest to it in time, within half a frame interval
33
+ (`--tolerance-ms`). Leading or trailing frames with no match yet are dropped. A frame in the middle with no
34
+ match stops the conversion with the time and size of the gap. That's a gap in the recording, so the frame
35
+ isn't filled with a stale value. Raise `--tolerance-ms` to accept it.
36
+
37
+ **Clocks.** A ROS message has two times: when the sensor captured it (`header.stamp`) and when the
38
+ recorder wrote it (MCAP `log_time`). On real robots a camera frame reaches the recorder about 25 ms after
39
+ capture and a joint reading about 1 ms after, so pairing on write time matches each frame with the arm's
40
+ position a frame later. The converter pairs on capture time. Teleop commands usually carry no capture
41
+ time, so they're placed on the capture timeline using the joint topic's recorder delay. `--clock log`
42
+ pairs on write time; `--clock header` requires a capture time on every topic.
43
+
44
+ ## What comes out
45
+
46
+ LeRobot v3.0, as written by lerobot 0.4 and later:
47
+
48
+ ```
49
+ data/chunk-000/file-000.parquet observation.state, action, timestamp, frame_index, episode_index, index, task_index
50
+ videos/<camera>/chunk-000/file-000.mp4 all episodes back to back, AV1, a keyframe every 2 frames
51
+ meta/info.json stats.json tasks.parquet episodes/chunk-000/file-000.parquet
52
+ meta/conversion.json sources, topics, clock, worst match per episode, notes
53
+ ```
54
+
55
+ `meta/conversion.json` also records what the converter noticed: dropped camera frames, a camera rate
56
+ that differs from the dataset fps, or a robot clock that isn't synchronised with the recorder (two of
57
+ the public test recordings below are 73 days off). The output is built in `<output>.partial` and renamed
58
+ when complete, so a failed run leaves nothing behind. `--overwrite` only ever replaces a folder that is
59
+ already a LeRobot dataset.
60
+
61
+ ## Tested on
62
+
63
+ `ola31/readme_test_rosbag_1` and `Dongkkka/physical_ai_tools_test_rosbag` on the Hugging Face Hub, a
64
+ two-arm teleop robot: 3 recordings, 1,777 frames at 30 fps, 19 joints.
65
+ - **Values:** every state and action value equals the source message.
66
+ - **Timing:** each frame's joint reading is within 5 ms of the frame's capture time.
67
+ - **Video:** video frames are in source order.
68
+ - **LeRobot:** `LeRobotDataset` loads the result, and `lerobot-train` trains ACT on it.
69
+
70
+ ## Limits
71
+
72
+ Version 0.1 handles one camera per dataset, ROS 2 MCAP input, and joint state and commands from
73
+ `JointState` and `JointTrajectory`. Planned next: several cameras, ROS 1 `.bag` and ROS 2 `.db3`.
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ git clone https://github.com/akrishni/phys-ai-dataconverter
79
+ cd phys-ai-dataconverter
80
+ pip install -e ".[dev]"
81
+ python tests/run_all.py # offline: MCAP files with known answers are written by the tests
82
+ python tests/run_all.py --hub # also convert a public recording from the Hub
83
+ ```
84
+
85
+ ```
86
+ phys_ai_dataconverter/
87
+ model.py Stream, image and topic types shared by everything below
88
+ readers/ one module per input format (mcap_ros2)
89
+ episode.py one recording to one aligned episode: topic choice, clocks, alignment
90
+ align.py nearest-timestamp matching with a tolerance
91
+ writers/ one module per output format (lerobot_v3)
92
+ convert.py the pipeline: read, align, write to <output>.partial, rename
93
+ verify.py --check: profile the output with phys-ai-dataprofiler and evaluate requirements
94
+ cli.py the phys-ai-dataconverter command
95
+ ```
96
+
97
+ A new input format is a new module in `readers/` that produces `Stream`s. A new output format is a new
98
+ module in `writers/`. Nothing else changes.
99
+
100
+ ## License
101
+
102
+ MIT License, copyright (c) 2026 Anand Krishniyer. See [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ """phys_ai_dataconverter: convert robot recordings (ROS 2 MCAP) into training datasets (LeRobot v3).
2
+
3
+ Copyright (c) 2026 Anand Krishniyer. MIT License."""
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from phys_ai_dataconverter.model import ConversionError # noqa: E402,F401
8
+ from phys_ai_dataconverter.convert import ConversionReport, ConvertOptions, convert # noqa: E402,F401
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from phys_ai_dataconverter.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,45 @@
1
+ """
2
+ Nearest-timestamp alignment of one stream onto the ticks of another.
3
+
4
+ Each reference tick (a camera frame) takes the source sample (a joint reading) closest to it in time.
5
+ The distance is kept, and a tick whose nearest sample is further than the tolerance gets no value
6
+ rather than a stale one: the caller decides whether to drop the frame or fail.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ import numpy as np
14
+
15
+
16
+ @dataclass
17
+ class Alignment:
18
+ index: np.ndarray # per tick: index of the nearest sample, -1 when outside the tolerance
19
+ age_ns: np.ndarray # per tick: |tick - sample time| in ns (of the nearest sample, even when rejected)
20
+ matched: np.ndarray # per tick: True when within the tolerance
21
+
22
+ @property
23
+ def n_unmatched(self) -> int:
24
+ return int((~self.matched).sum())
25
+
26
+
27
+ def nearest(ticks_ns: np.ndarray, sample_ns: np.ndarray, tolerance_ns: int) -> Alignment:
28
+ """Match every tick to the nearest sample. Both arrays must be sorted ascending; ties go to the
29
+ earlier sample, so a value is never taken from after it could have been known when both are
30
+ equally close."""
31
+ ticks_ns = np.asarray(ticks_ns, dtype=np.int64)
32
+ sample_ns = np.asarray(sample_ns, dtype=np.int64)
33
+ if len(sample_ns) == 0:
34
+ n = len(ticks_ns)
35
+ return Alignment(np.full(n, -1), np.full(n, np.iinfo(np.int64).max), np.zeros(n, dtype=bool))
36
+ if np.any(np.diff(sample_ns) < 0) or np.any(np.diff(ticks_ns) < 0):
37
+ raise ValueError("timestamps must be sorted")
38
+ right = np.searchsorted(sample_ns, ticks_ns, side="left").clip(0, len(sample_ns) - 1)
39
+ left = (right - 1).clip(0, len(sample_ns) - 1)
40
+ d_right = np.abs(sample_ns[right] - ticks_ns)
41
+ d_left = np.abs(ticks_ns - sample_ns[left])
42
+ idx = np.where(d_left <= d_right, left, right)
43
+ age = np.minimum(d_left, d_right)
44
+ ok = age <= tolerance_ns
45
+ return Alignment(np.where(ok, idx, -1), age, ok)
@@ -0,0 +1,117 @@
1
+ """
2
+ phys-ai-dataconverter: convert robot recordings into training datasets.
3
+
4
+ phys-ai-dataconverter recordings/ -o ./pick_bottles --task "pick up the bottles"
5
+ phys-ai-dataconverter rec1.mcap rec2.mcap -o ./ds --check preset:teleop-imitation
6
+
7
+ Exit codes: 0 converted (and, with --check, the check passed), 1 the --check failed,
8
+ 2 the conversion was refused (the message says why), 4 the --check was unknown.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from phys_ai_dataconverter import __version__
18
+
19
+ PROG = "phys-ai-dataconverter"
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ p = argparse.ArgumentParser(
24
+ prog=PROG,
25
+ description="Convert robot recordings (one episode per file) into a LeRobot v3 dataset: one camera, "
26
+ "observation.state from a joint state topic and action from joint command topics, aligned "
27
+ "to the camera frames by nearest capture time. Input: ROS 2 MCAP.",
28
+ epilog="Example: phys-ai-dataconverter recordings/ -o ./pick_bottles --task \"pick up the bottles\" "
29
+ "--camera /zed/zed_node/left/image_rect_color/compressed --check preset:teleop-imitation",
30
+ )
31
+ p.add_argument("--version", action="version", version=f"{PROG} {__version__}")
32
+ p.add_argument("inputs", nargs="+", help="Recording files, or folders searched for them (one episode per file)")
33
+ p.add_argument("--output", "-o", required=True, help="Folder for the new dataset")
34
+ p.add_argument("--task", default="teleoperation", help="Task description stored with every episode")
35
+ s = p.add_argument_group("streams (defaults are picked from the first file and printed)")
36
+ s.add_argument("--camera", help="Image topic (default: the highest-resolution one)")
37
+ s.add_argument("--camera-key", help="Feature name (default: observation.images.<from the topic>)")
38
+ s.add_argument("--state-topic", help="Joint state topic (default: the busiest one)")
39
+ s.add_argument("--state-joints", help="Comma-separated joint names for observation.state "
40
+ "(default: the joints the action topics command)")
41
+ s.add_argument("--action-topic", action="append", dest="action_topics",
42
+ help="Joint command topic; repeat for several, concatenated in order (default: all, sorted)")
43
+ s.add_argument("--no-action", action="store_true", help="Write no action column")
44
+ t = p.add_argument_group("timing")
45
+ t.add_argument("--fps", type=int, help="Dataset frame rate (default: the camera's measured rate, rounded)")
46
+ t.add_argument("--tolerance-ms", type=float,
47
+ help="Largest allowed distance from a frame to its matched state/command (default: half a frame)")
48
+ t.add_argument("--clock", choices=["auto", "header", "log"], default="auto",
49
+ help="auto (default): pair camera and state on capture time (header.stamp), placing unstamped "
50
+ "commands via the state topic's recorder delay; header: capture time for every topic; "
51
+ "log: recorder write time for every topic")
52
+ v = p.add_argument_group("output")
53
+ v.add_argument("--format", dest="output_format", default="lerobot-v3", choices=["lerobot-v3"])
54
+ v.add_argument("--vcodec", default="libsvtav1", help="libsvtav1 (LeRobot's default) or h264 (faster)")
55
+ v.add_argument("--crf", type=int, default=30, help="Video quality; lower is better and larger (default 30)")
56
+ v.add_argument("--robot-type", help="Stored in info.json")
57
+ v.add_argument("--overwrite", action="store_true", help="Replace an existing dataset at --output")
58
+ p.add_argument("--check", metavar="FILE|preset:NAME",
59
+ help="After converting, profile the output with phys-ai-dataprofiler and check it against "
60
+ "these requirements (a YAML file or a profiler preset); the report goes in meta/")
61
+ p.add_argument("--quiet", action="store_true")
62
+ return p
63
+
64
+
65
+ def _inputs(args: list[str]) -> list[Path]:
66
+ from phys_ai_dataconverter.readers import input_extensions
67
+ exts = input_extensions()
68
+ files: list[Path] = []
69
+ for i in args:
70
+ q = Path(i)
71
+ if q.is_dir():
72
+ found = sorted(f for f in q.rglob("*") if f.is_file() and f.suffix.lower() in exts)
73
+ if not found:
74
+ raise SystemExit(f"Error: no {' '.join(exts)} files under {q}")
75
+ files += found
76
+ else:
77
+ files.append(q)
78
+ return files
79
+
80
+
81
+ def main(argv: list[str] | None = None) -> int:
82
+ a = build_parser().parse_args(argv)
83
+ log = (lambda m: None) if a.quiet else (lambda m: print(m, file=sys.stderr))
84
+ from phys_ai_dataconverter.convert import ConvertOptions, convert, print_report
85
+ from phys_ai_dataconverter.model import ConversionError
86
+ try:
87
+ files = _inputs(a.inputs)
88
+ report = convert(files, a.output, ConvertOptions(
89
+ camera=a.camera, camera_key=a.camera_key, state_topic=a.state_topic,
90
+ state_joints=[j.strip() for j in a.state_joints.split(",")] if a.state_joints else None,
91
+ action_topics=a.action_topics, no_action=a.no_action, task=a.task, fps=a.fps,
92
+ tolerance_ms=a.tolerance_ms, clock=a.clock, output_format=a.output_format, vcodec=a.vcodec,
93
+ crf=a.crf, robot_type=a.robot_type, overwrite=a.overwrite), log=log)
94
+ except ConversionError as exc:
95
+ print(f"Error: {exc}", file=sys.stderr)
96
+ return 2
97
+ except SystemExit as exc:
98
+ if isinstance(exc.code, str):
99
+ print(exc.code, file=sys.stderr)
100
+ return 2
101
+ raise
102
+ print_report(report, log)
103
+ if not a.check:
104
+ log(f"Check it: {PROG} ... --check preset:teleop-imitation (or a requirements file)")
105
+ return 0
106
+ from phys_ai_dataconverter.verify import check
107
+ log("")
108
+ log(f"Checking {a.output} against {a.check} with phys-ai-dataprofiler")
109
+ try:
110
+ return check(a.output, a.check, log if not a.quiet else (lambda m: None))
111
+ except ConversionError as exc:
112
+ print(f"Error: {exc}", file=sys.stderr)
113
+ return 2
114
+
115
+
116
+ if __name__ == "__main__":
117
+ sys.exit(main())