openbot-data 0.0.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.
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+
8
+ .venv/
9
+ venv/
10
+
11
+ build/
12
+ dist/
13
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenBot
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 ACCOUNTING CONTRACT OR TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ IN THE SOFTWARE.
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.4
2
+ Name: openbot-data
3
+ Version: 0.0.1
4
+ Summary: Clean, label, and organize robot data for training and evaluation.
5
+ Project-URL: Homepage, https://openbot.ai/data
6
+ Project-URL: Repository, https://github.com/openbotai/openbot-data
7
+ Project-URL: Issues, https://github.com/openbotai/openbot-data/issues
8
+ Author-email: OpenBot <hello@openbot.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: annotation,computer-vision,data-cleaning,dataset,egocentric-video,embodied-ai,failure-analysis,robot-data,robotics,wrist-camera
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: numpy>=1.24.0
24
+ Requires-Dist: opencv-python>=4.8.0
25
+ Requires-Dist: tqdm>=4.65.0
26
+ Requires-Dist: typer>=0.9.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.0.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
30
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # OpenBot Data
34
+
35
+ **Clean, label, and organize robot data for training and evaluation.**
36
+
37
+ OpenBot Data is a lightweight toolkit for cleaning, labeling, organizing, and preparing robot data for model training, evaluation, failure analysis, and deployment debugging.
38
+
39
+ > Robots do not just need more data.
40
+ > They need cleaner data, better labels, and harder failure cases.
41
+
42
+ ---
43
+
44
+ ## Current Status
45
+
46
+ **OpenBot Data is in early development.**
47
+
48
+ The current release (0.0.1) provides basic robot video inspection and dataset metadata generation:
49
+
50
+ - Scan robot video directories
51
+ - Extract video metadata (duration, fps, resolution, frame count)
52
+ - Extract preview frames for quick inspection
53
+ - Generate dataset manifest
54
+ - Generate basic quality reports
55
+
56
+ Planned features include dataset triage, quality-based filtering, annotation export, and failure case mining.
57
+
58
+ ---
59
+
60
+ ## Why OpenBot Data?
61
+
62
+ Robot data is messy.
63
+
64
+ Raw robot recordings often contain:
65
+
66
+ - Motion blur
67
+ - Camera shake
68
+ - Dark or overexposed frames
69
+ - Hand, gripper, or object occlusion
70
+ - Target objects moving out of view
71
+ - Repeated or invalid clips
72
+ - Failed actions mixed with successful ones
73
+ - Unclear task phases
74
+ - Noisy or incomplete annotations
75
+ - Long-tail failure cases
76
+ - Data that should not enter the training set
77
+
78
+ OpenBot Data helps robotics teams convert raw robot data into structured datasets that can be used for training, evaluation, benchmarking, and deployment iteration.
79
+
80
+ ---
81
+
82
+ ## First focus: ego-centric robot data
83
+
84
+ The first public version of OpenBot Data starts with ego-centric robot videos because they are one of the most common and messy data sources in embodied AI.
85
+
86
+ This includes:
87
+
88
+ - First-person robot videos
89
+ - Wrist-camera videos
90
+ - Close-range manipulation videos
91
+ - Robot vision clips
92
+ - Failure clips from real-world deployment
93
+ - Hard cases from evaluation runs
94
+
95
+ OpenBot Data helps turn these raw videos into clean, annotation-ready datasets and reusable failure cases.
96
+
97
+ ---
98
+
99
+ ## What it does
100
+
101
+ OpenBot Data focuses on four core workflows:
102
+
103
+ ### 1. Clean robot data
104
+
105
+ Detect and organize low-quality frames and clips, including:
106
+
107
+ - Blurry frames
108
+ - Dark scenes
109
+ - Duplicate frames
110
+ - Camera shake
111
+ - Invalid clips
112
+ - Target-out-of-view cases
113
+ - Heavy occlusion
114
+ - Low-quality training samples
115
+
116
+ ### 2. Prepare data for annotation
117
+
118
+ Convert raw robot data into annotation-ready datasets.
119
+
120
+ Planned annotation types include:
121
+
122
+ - Object labels
123
+ - Bounding boxes
124
+ - Task phases
125
+ - Success / failure labels
126
+ - Failure reasons
127
+ - Scene conditions
128
+ - Hand or gripper occlusion
129
+ - Object visibility
130
+ - Trainable / not-trainable flags
131
+
132
+ ### 3. Mine failure cases
133
+
134
+ Find hard cases that matter for real-world robot deployment:
135
+
136
+ - Motion blur failures
137
+ - Hand or gripper occlusion failures
138
+ - Target lost from view
139
+ - Dark-scene failures
140
+ - Reflective object failures
141
+ - Small-object failures
142
+ - Failed grasp or manipulation moments
143
+ - Long-tail deployment failures
144
+
145
+ ### 4. Export structured datasets
146
+
147
+ Export cleaned data for training, evaluation, and benchmarking.
148
+
149
+ Planned formats:
150
+
151
+ - OpenBot JSON
152
+ - COCO
153
+ - YOLO
154
+ - WebDataset
155
+ - Parquet
156
+ - LeRobot-compatible datasets
157
+ - RLDS-compatible datasets
158
+
159
+ ---
160
+
161
+ ## Installation
162
+
163
+ ```bash
164
+ pip install openbot-data
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Quickstart (v0.0.1)
170
+
171
+ ```bash
172
+ # Scan robot videos and inspect metadata
173
+ openbot-data scan ./robot_videos
174
+
175
+ # Full inspection: extract frames and generate reports
176
+ openbot-data inspect ./robot_videos --out ./openbot_dataset
177
+ ```
178
+
179
+ Example output:
180
+
181
+ ```text
182
+ openbot_dataset/
183
+ previews/ # Preview frames from videos
184
+ metadata/
185
+ manifest.json # Dataset manifest with video metadata
186
+ report.json # Quality metrics summary
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Usage
192
+
193
+ ### CLI
194
+
195
+ ```bash
196
+ openbot-data scan ./videos # Scan and show video metadata
197
+ openbot-data inspect ./videos --out ./dataset # Full inspection pipeline
198
+ ```
199
+
200
+ ### Python API
201
+
202
+ ```python
203
+ from openbot_data import scan_directory, inspect_dataset
204
+
205
+ # Scan videos
206
+ result = scan_directory("./robot_videos")
207
+ print(f"Found {result['valid_videos']} videos")
208
+
209
+ # Inspect and extract preview frames, manifest, and report
210
+ result = inspect_dataset(
211
+ video_dir="./robot_videos",
212
+ output_dir="./dataset",
213
+ )
214
+
215
+ print(result["manifest_path"])
216
+ print(result["report_path"])
217
+ ```
218
+
219
+ ### Development checks
220
+
221
+ ```bash
222
+ pip install -e ".[dev]"
223
+ pytest
224
+ python -m build
225
+ python -m twine check dist/*
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Example use cases
231
+
232
+ OpenBot Data can be used for:
233
+
234
+ * Robot vision dataset cleaning
235
+ * Ego-centric video preparation
236
+ * Wrist-camera data processing
237
+ * Embodied AI data preparation
238
+ * Failure case mining
239
+ * Deployment evaluation data preparation
240
+ * Synthetic data generation input curation
241
+ * Robot benchmark dataset construction
242
+ * Training / validation / test split management
243
+
244
+ ---
245
+
246
+ ## Roadmap
247
+
248
+ ### v0.0.1 — Installable Preview (Current)
249
+ * [x] Robot video scanner
250
+ * [x] Video metadata extraction (duration, fps, resolution, frame count)
251
+ * [x] Dataset manifest generation
252
+ * [x] Basic quality reports (JSON)
253
+ * [x] Preview frame extraction
254
+
255
+ ### v0.0.2 — Dataset Triage
256
+ * [ ] Blur score detection (Laplacian variance)
257
+ * [ ] Brightness score detection
258
+ * [ ] Black frame detection
259
+ * [ ] Basic duplicate-like frame detection
260
+ * [ ] keep / review / drop grouping
261
+ * [ ] HTML triage reports
262
+
263
+ ### v0.0.3 — Annotation Batch
264
+ * [ ] Quality-based sampling
265
+ * [ ] Annotation task generation
266
+ * [ ] Trainable/not-trainable flags
267
+
268
+ ### v0.0.4 — Export
269
+ * [ ] OpenBot JSON schema
270
+ * [ ] COCO export
271
+ * [ ] YOLO export
272
+
273
+ ### v0.1.0 — Robot Data Cleaning Pipeline
274
+ * [ ] Stable cleaning, triage, sampling, and export workflow
275
+ * [ ] Failure case tagging
276
+ * [ ] Multi-view robot data support
277
+ * [ ] LeRobot dataset compatibility
278
+ * [ ] RLDS compatibility
279
+
280
+ ---
281
+
282
+ ## OpenBot ecosystem
283
+
284
+ OpenBot Data is part of the OpenBot robotics intelligence infrastructure.
285
+
286
+ Planned modules:
287
+
288
+ * **OpenBot Data** — clean, label, and organize robot data
289
+ * **OpenBot Bench** — evaluate robot models before deployment
290
+ * **OpenBot Synth** — generate hard cases from failures
291
+ * **OpenBot API** — connect data, evaluation, and deployment workflows
292
+
293
+ ---
294
+
295
+ ## License
296
+
297
+ MIT License.
298
+
299
+ ---
300
+
301
+ ## Citation
302
+
303
+ No formal citation is required for the 0.0.1 preview. If you use OpenBot Data in research or production, link to the project repository.
304
+
305
+ If you use OpenBot Data in research or production, please cite the project repository.
306
+
307
+ ```bibtex
308
+ @software{openbot_data,
309
+ title = {OpenBot Data},
310
+ author = {OpenBot},
311
+ year = {2026},
312
+ url = {https://github.com/openbotai/openbot-data}
313
+ }
314
+ ```
@@ -0,0 +1,282 @@
1
+ # OpenBot Data
2
+
3
+ **Clean, label, and organize robot data for training and evaluation.**
4
+
5
+ OpenBot Data is a lightweight toolkit for cleaning, labeling, organizing, and preparing robot data for model training, evaluation, failure analysis, and deployment debugging.
6
+
7
+ > Robots do not just need more data.
8
+ > They need cleaner data, better labels, and harder failure cases.
9
+
10
+ ---
11
+
12
+ ## Current Status
13
+
14
+ **OpenBot Data is in early development.**
15
+
16
+ The current release (0.0.1) provides basic robot video inspection and dataset metadata generation:
17
+
18
+ - Scan robot video directories
19
+ - Extract video metadata (duration, fps, resolution, frame count)
20
+ - Extract preview frames for quick inspection
21
+ - Generate dataset manifest
22
+ - Generate basic quality reports
23
+
24
+ Planned features include dataset triage, quality-based filtering, annotation export, and failure case mining.
25
+
26
+ ---
27
+
28
+ ## Why OpenBot Data?
29
+
30
+ Robot data is messy.
31
+
32
+ Raw robot recordings often contain:
33
+
34
+ - Motion blur
35
+ - Camera shake
36
+ - Dark or overexposed frames
37
+ - Hand, gripper, or object occlusion
38
+ - Target objects moving out of view
39
+ - Repeated or invalid clips
40
+ - Failed actions mixed with successful ones
41
+ - Unclear task phases
42
+ - Noisy or incomplete annotations
43
+ - Long-tail failure cases
44
+ - Data that should not enter the training set
45
+
46
+ OpenBot Data helps robotics teams convert raw robot data into structured datasets that can be used for training, evaluation, benchmarking, and deployment iteration.
47
+
48
+ ---
49
+
50
+ ## First focus: ego-centric robot data
51
+
52
+ The first public version of OpenBot Data starts with ego-centric robot videos because they are one of the most common and messy data sources in embodied AI.
53
+
54
+ This includes:
55
+
56
+ - First-person robot videos
57
+ - Wrist-camera videos
58
+ - Close-range manipulation videos
59
+ - Robot vision clips
60
+ - Failure clips from real-world deployment
61
+ - Hard cases from evaluation runs
62
+
63
+ OpenBot Data helps turn these raw videos into clean, annotation-ready datasets and reusable failure cases.
64
+
65
+ ---
66
+
67
+ ## What it does
68
+
69
+ OpenBot Data focuses on four core workflows:
70
+
71
+ ### 1. Clean robot data
72
+
73
+ Detect and organize low-quality frames and clips, including:
74
+
75
+ - Blurry frames
76
+ - Dark scenes
77
+ - Duplicate frames
78
+ - Camera shake
79
+ - Invalid clips
80
+ - Target-out-of-view cases
81
+ - Heavy occlusion
82
+ - Low-quality training samples
83
+
84
+ ### 2. Prepare data for annotation
85
+
86
+ Convert raw robot data into annotation-ready datasets.
87
+
88
+ Planned annotation types include:
89
+
90
+ - Object labels
91
+ - Bounding boxes
92
+ - Task phases
93
+ - Success / failure labels
94
+ - Failure reasons
95
+ - Scene conditions
96
+ - Hand or gripper occlusion
97
+ - Object visibility
98
+ - Trainable / not-trainable flags
99
+
100
+ ### 3. Mine failure cases
101
+
102
+ Find hard cases that matter for real-world robot deployment:
103
+
104
+ - Motion blur failures
105
+ - Hand or gripper occlusion failures
106
+ - Target lost from view
107
+ - Dark-scene failures
108
+ - Reflective object failures
109
+ - Small-object failures
110
+ - Failed grasp or manipulation moments
111
+ - Long-tail deployment failures
112
+
113
+ ### 4. Export structured datasets
114
+
115
+ Export cleaned data for training, evaluation, and benchmarking.
116
+
117
+ Planned formats:
118
+
119
+ - OpenBot JSON
120
+ - COCO
121
+ - YOLO
122
+ - WebDataset
123
+ - Parquet
124
+ - LeRobot-compatible datasets
125
+ - RLDS-compatible datasets
126
+
127
+ ---
128
+
129
+ ## Installation
130
+
131
+ ```bash
132
+ pip install openbot-data
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Quickstart (v0.0.1)
138
+
139
+ ```bash
140
+ # Scan robot videos and inspect metadata
141
+ openbot-data scan ./robot_videos
142
+
143
+ # Full inspection: extract frames and generate reports
144
+ openbot-data inspect ./robot_videos --out ./openbot_dataset
145
+ ```
146
+
147
+ Example output:
148
+
149
+ ```text
150
+ openbot_dataset/
151
+ previews/ # Preview frames from videos
152
+ metadata/
153
+ manifest.json # Dataset manifest with video metadata
154
+ report.json # Quality metrics summary
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Usage
160
+
161
+ ### CLI
162
+
163
+ ```bash
164
+ openbot-data scan ./videos # Scan and show video metadata
165
+ openbot-data inspect ./videos --out ./dataset # Full inspection pipeline
166
+ ```
167
+
168
+ ### Python API
169
+
170
+ ```python
171
+ from openbot_data import scan_directory, inspect_dataset
172
+
173
+ # Scan videos
174
+ result = scan_directory("./robot_videos")
175
+ print(f"Found {result['valid_videos']} videos")
176
+
177
+ # Inspect and extract preview frames, manifest, and report
178
+ result = inspect_dataset(
179
+ video_dir="./robot_videos",
180
+ output_dir="./dataset",
181
+ )
182
+
183
+ print(result["manifest_path"])
184
+ print(result["report_path"])
185
+ ```
186
+
187
+ ### Development checks
188
+
189
+ ```bash
190
+ pip install -e ".[dev]"
191
+ pytest
192
+ python -m build
193
+ python -m twine check dist/*
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Example use cases
199
+
200
+ OpenBot Data can be used for:
201
+
202
+ * Robot vision dataset cleaning
203
+ * Ego-centric video preparation
204
+ * Wrist-camera data processing
205
+ * Embodied AI data preparation
206
+ * Failure case mining
207
+ * Deployment evaluation data preparation
208
+ * Synthetic data generation input curation
209
+ * Robot benchmark dataset construction
210
+ * Training / validation / test split management
211
+
212
+ ---
213
+
214
+ ## Roadmap
215
+
216
+ ### v0.0.1 — Installable Preview (Current)
217
+ * [x] Robot video scanner
218
+ * [x] Video metadata extraction (duration, fps, resolution, frame count)
219
+ * [x] Dataset manifest generation
220
+ * [x] Basic quality reports (JSON)
221
+ * [x] Preview frame extraction
222
+
223
+ ### v0.0.2 — Dataset Triage
224
+ * [ ] Blur score detection (Laplacian variance)
225
+ * [ ] Brightness score detection
226
+ * [ ] Black frame detection
227
+ * [ ] Basic duplicate-like frame detection
228
+ * [ ] keep / review / drop grouping
229
+ * [ ] HTML triage reports
230
+
231
+ ### v0.0.3 — Annotation Batch
232
+ * [ ] Quality-based sampling
233
+ * [ ] Annotation task generation
234
+ * [ ] Trainable/not-trainable flags
235
+
236
+ ### v0.0.4 — Export
237
+ * [ ] OpenBot JSON schema
238
+ * [ ] COCO export
239
+ * [ ] YOLO export
240
+
241
+ ### v0.1.0 — Robot Data Cleaning Pipeline
242
+ * [ ] Stable cleaning, triage, sampling, and export workflow
243
+ * [ ] Failure case tagging
244
+ * [ ] Multi-view robot data support
245
+ * [ ] LeRobot dataset compatibility
246
+ * [ ] RLDS compatibility
247
+
248
+ ---
249
+
250
+ ## OpenBot ecosystem
251
+
252
+ OpenBot Data is part of the OpenBot robotics intelligence infrastructure.
253
+
254
+ Planned modules:
255
+
256
+ * **OpenBot Data** — clean, label, and organize robot data
257
+ * **OpenBot Bench** — evaluate robot models before deployment
258
+ * **OpenBot Synth** — generate hard cases from failures
259
+ * **OpenBot API** — connect data, evaluation, and deployment workflows
260
+
261
+ ---
262
+
263
+ ## License
264
+
265
+ MIT License.
266
+
267
+ ---
268
+
269
+ ## Citation
270
+
271
+ No formal citation is required for the 0.0.1 preview. If you use OpenBot Data in research or production, link to the project repository.
272
+
273
+ If you use OpenBot Data in research or production, please cite the project repository.
274
+
275
+ ```bibtex
276
+ @software{openbot_data,
277
+ title = {OpenBot Data},
278
+ author = {OpenBot},
279
+ year = {2026},
280
+ url = {https://github.com/openbotai/openbot-data}
281
+ }
282
+ ```
@@ -0,0 +1,17 @@
1
+ """
2
+ OpenBot Data — Inspect robot video data before training.
3
+
4
+ OpenBot Data 0.0.1 provides basic robot video inspection and dataset metadata generation.
5
+ """
6
+
7
+ __version__ = "0.0.1"
8
+
9
+ from openbot_data.video import scan_directory, scan_video
10
+ from openbot_data.extract import extract_preview_frames, inspect_dataset
11
+
12
+ __all__ = [
13
+ "scan_directory",
14
+ "scan_video",
15
+ "extract_preview_frames",
16
+ "inspect_dataset",
17
+ ]
@@ -0,0 +1,85 @@
1
+ """
2
+ Command-line interface for OpenBot Data v0.0.1.
3
+ """
4
+
5
+ import typer
6
+ from typing import Optional
7
+ from pathlib import Path
8
+
9
+ from openbot_data.video import scan_directory
10
+ from openbot_data.extract import inspect_dataset
11
+
12
+ app = typer.Typer(
13
+ name="openbot-data",
14
+ help="Clean, label, and organize robot data for training and evaluation.",
15
+ add_completion=False
16
+ )
17
+
18
+
19
+ @app.command()
20
+ def scan(
21
+ path: str = typer.Argument(..., help="Path to robot video directory"),
22
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output JSON file path")
23
+ ):
24
+ """
25
+ Scan a directory for robot videos and show metadata.
26
+ """
27
+ import json
28
+
29
+ result = scan_directory(path)
30
+
31
+ if output:
32
+ output_path = Path(output)
33
+ output_path.parent.mkdir(parents=True, exist_ok=True)
34
+ with open(output_path, "w") as f:
35
+ json.dump(result, f, indent=2)
36
+ typer.echo(f"Scan results saved to {output_path}")
37
+ else:
38
+ typer.echo(json.dumps(result, indent=2))
39
+
40
+
41
+ @app.command()
42
+ def inspect(
43
+ video_dir: str = typer.Argument(..., help="Path to video directory"),
44
+ output: str = typer.Option(..., "--out", "-o", help="Output directory"),
45
+ ):
46
+ """
47
+ Inspect robot videos: extract preview frames and generate manifest.
48
+ """
49
+ typer.echo(f"Inspecting robot videos in {video_dir}...")
50
+
51
+ result = inspect_dataset(
52
+ video_dir=video_dir,
53
+ output_dir=output
54
+ )
55
+
56
+ if "error" in result:
57
+ typer.echo(f"Error: {result['error']}", err=True)
58
+ raise typer.Exit(1)
59
+
60
+ typer.echo("")
61
+ typer.echo("Inspection complete!")
62
+ typer.echo("")
63
+ typer.echo(f"Output directory: {output}")
64
+ typer.echo(f" Total videos: {result['total_videos']}")
65
+ typer.echo(f" Preview frames: {result['total_previews']}")
66
+ typer.echo("")
67
+ typer.echo("Generated files:")
68
+ typer.echo(f" {result['manifest_path']}")
69
+ typer.echo(f" {result['report_path']}")
70
+
71
+
72
+ @app.command()
73
+ def version():
74
+ """Show version information."""
75
+ from openbot_data import __version__
76
+ typer.echo(f"OpenBot Data v{__version__}")
77
+ typer.echo("https://github.com/openbotai/openbot-data")
78
+
79
+
80
+ def main():
81
+ app()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,214 @@
1
+ """
2
+ Frame extraction module for OpenBot Data v0.0.1.
3
+ Extracts preview frames from videos for inspection.
4
+ """
5
+
6
+ import hashlib
7
+ import cv2
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Dict, Any, List
11
+ from dataclasses import dataclass, asdict
12
+ from tqdm import tqdm
13
+
14
+
15
+ @dataclass
16
+ class PreviewFrame:
17
+ """Metadata for a preview frame."""
18
+ frame_id: str
19
+ video_path: str
20
+ frame_number: int
21
+ timestamp: float
22
+ path: str
23
+
24
+
25
+ def _video_output_id(video_path: Path) -> str:
26
+ digest = hashlib.sha1(str(video_path.resolve()).encode("utf-8")).hexdigest()[:8]
27
+ return f"{video_path.stem}_{digest}"
28
+
29
+
30
+ def extract_preview_frames(
31
+ video_path: str,
32
+ output_dir: str,
33
+ max_frames: int = 10
34
+ ) -> Dict[str, Any]:
35
+ """
36
+ Extract a few preview frames from a video.
37
+
38
+ Args:
39
+ video_path: Path to input video
40
+ output_dir: Path to output directory
41
+ max_frames: Maximum number of frames to extract
42
+
43
+ Returns:
44
+ Dictionary with extraction results
45
+ """
46
+ video_path = Path(video_path)
47
+ output_dir = Path(output_dir)
48
+
49
+ previews_dir = output_dir / "previews"
50
+ previews_dir.mkdir(parents=True, exist_ok=True)
51
+
52
+ cap = cv2.VideoCapture(str(video_path))
53
+
54
+ if not cap.isOpened():
55
+ return {"error": f"Cannot open video: {video_path}"}
56
+
57
+ if max_frames <= 0:
58
+ cap.release()
59
+ return {"error": "max_frames must be greater than 0"}
60
+
61
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
62
+ fps = cap.get(cv2.CAP_PROP_FPS)
63
+ if fps <= 0:
64
+ fps = 30.0
65
+
66
+ if total_frames <= 0:
67
+ cap.release()
68
+ return {"error": f"Video has no readable frames: {video_path}"}
69
+
70
+ video_name = video_path.stem
71
+ video_output_id = _video_output_id(video_path)
72
+
73
+ sample_count = min(max_frames, total_frames)
74
+ if sample_count == 1:
75
+ frame_indices = [0]
76
+ else:
77
+ frame_indices = [
78
+ int(round(i * (total_frames - 1) / (sample_count - 1)))
79
+ for i in range(sample_count)
80
+ ]
81
+
82
+ extracted_frames: List[PreviewFrame] = []
83
+
84
+ for frame_idx in frame_indices:
85
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
86
+ ret, frame = cap.read()
87
+
88
+ if not ret:
89
+ continue
90
+
91
+ timestamp = frame_idx / fps
92
+ frame_id = f"{video_output_id}_preview{len(extracted_frames):02d}.jpg"
93
+ frame_path = previews_dir / frame_id
94
+
95
+ cv2.imwrite(str(frame_path), frame)
96
+
97
+ extracted_frame = PreviewFrame(
98
+ frame_id=frame_id,
99
+ video_path=str(video_path),
100
+ frame_number=frame_idx,
101
+ timestamp=round(timestamp, 2),
102
+ path=str(frame_path)
103
+ )
104
+ extracted_frames.append(extracted_frame)
105
+
106
+ cap.release()
107
+
108
+ return {
109
+ "video_path": str(video_path),
110
+ "video_name": video_name,
111
+ "total_frames": total_frames,
112
+ "fps": fps,
113
+ "extracted_frames": len(extracted_frames),
114
+ "frames": [asdict(f) for f in extracted_frames]
115
+ }
116
+
117
+
118
+ def inspect_dataset(
119
+ video_dir: str,
120
+ output_dir: str
121
+ ) -> Dict[str, Any]:
122
+ """
123
+ Inspect a robot video dataset and generate manifest.
124
+
125
+ Args:
126
+ video_dir: Path to video directory
127
+ output_dir: Path to output directory
128
+
129
+ Returns:
130
+ Dictionary with inspection results
131
+ """
132
+ from openbot_data.video import scan_directory
133
+
134
+ scan_result = scan_directory(video_dir)
135
+
136
+ if "error" in scan_result:
137
+ return scan_result
138
+
139
+ output_dir = Path(output_dir)
140
+ output_dir.mkdir(parents=True, exist_ok=True)
141
+
142
+ metadata_dir = output_dir / "metadata"
143
+ metadata_dir.mkdir(parents=True, exist_ok=True)
144
+
145
+ all_previews = []
146
+ all_videos = []
147
+
148
+ for video_info in tqdm(scan_result["videos"], desc="Extracting previews"):
149
+ if not video_info["is_valid"]:
150
+ all_videos.append({
151
+ **video_info,
152
+ "previews": []
153
+ })
154
+ continue
155
+
156
+ video_path = video_info["path"]
157
+ video_name = Path(video_path).stem
158
+
159
+ preview_result = extract_preview_frames(
160
+ video_path,
161
+ str(output_dir),
162
+ max_frames=10
163
+ )
164
+
165
+ if "error" not in preview_result:
166
+ all_previews.extend(preview_result["frames"])
167
+
168
+ all_videos.append({
169
+ **video_info,
170
+ "previews": preview_result.get("frames", []) if "error" not in preview_result else []
171
+ })
172
+
173
+ # Generate manifest
174
+ manifest = {
175
+ "version": "0.0.1",
176
+ "source_dir": str(video_dir),
177
+ "output_dir": str(output_dir),
178
+ "total_videos": len(all_videos),
179
+ "valid_videos": sum(1 for v in all_videos if v["is_valid"]),
180
+ "total_previews": len(all_previews),
181
+ "videos": all_videos
182
+ }
183
+
184
+ manifest_path = metadata_dir / "manifest.json"
185
+ with open(manifest_path, "w") as f:
186
+ json.dump(manifest, f, indent=2)
187
+
188
+ # Generate basic report
189
+ report = {
190
+ "version": "0.0.1",
191
+ "source_dir": str(video_dir),
192
+ "output_dir": str(output_dir),
193
+ "total_videos": len(all_videos),
194
+ "valid_videos": sum(1 for v in all_videos if v["is_valid"]),
195
+ "invalid_videos": sum(1 for v in all_videos if not v["is_valid"]),
196
+ "total_duration": round(sum(v["duration"] for v in all_videos if v["is_valid"]), 2),
197
+ "total_size_mb": round(sum(v["size_mb"] for v in all_videos), 2),
198
+ "total_previews": len(all_previews),
199
+ "resolutions": [
200
+ list(resolution)
201
+ for resolution in sorted({(v["width"], v["height"]) for v in all_videos if v["is_valid"]})
202
+ ]
203
+ }
204
+
205
+ report_path = metadata_dir / "report.json"
206
+ with open(report_path, "w") as f:
207
+ json.dump(report, f, indent=2)
208
+
209
+ return {
210
+ "manifest_path": str(manifest_path),
211
+ "report_path": str(report_path),
212
+ "total_videos": len(all_videos),
213
+ "total_previews": len(all_previews)
214
+ }
@@ -0,0 +1,165 @@
1
+ """
2
+ Video processing module for OpenBot Data.
3
+ """
4
+
5
+ import cv2
6
+ from pathlib import Path
7
+ from typing import Dict, Any, Optional
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass
12
+ class VideoInfo:
13
+ """Video metadata."""
14
+ path: str
15
+ filename: str
16
+ width: int
17
+ height: int
18
+ fps: float
19
+ frame_count: int
20
+ duration: float
21
+ size_mb: float
22
+ is_valid: bool
23
+ error: Optional[str] = None
24
+
25
+
26
+ def scan_video(video_path: str) -> VideoInfo:
27
+ """
28
+ Scan a video file and extract metadata.
29
+
30
+ Args:
31
+ video_path: Path to video file
32
+
33
+ Returns:
34
+ VideoInfo object with metadata
35
+ """
36
+ path = Path(video_path)
37
+
38
+ if not path.exists():
39
+ return VideoInfo(
40
+ path=str(path),
41
+ filename=path.name,
42
+ width=0,
43
+ height=0,
44
+ fps=0,
45
+ frame_count=0,
46
+ duration=0,
47
+ size_mb=0,
48
+ is_valid=False,
49
+ error="File not found"
50
+ )
51
+
52
+ cap = None
53
+
54
+ try:
55
+ size_mb = path.stat().st_size / (1024 * 1024)
56
+ cap = cv2.VideoCapture(str(path))
57
+
58
+ if not cap.isOpened():
59
+ return VideoInfo(
60
+ path=str(path),
61
+ filename=path.name,
62
+ width=0,
63
+ height=0,
64
+ fps=0,
65
+ frame_count=0,
66
+ duration=0,
67
+ size_mb=size_mb,
68
+ is_valid=False,
69
+ error="Cannot open video file"
70
+ )
71
+
72
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
73
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
74
+ fps = cap.get(cv2.CAP_PROP_FPS)
75
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
76
+
77
+ if fps > 0:
78
+ duration = frame_count / fps
79
+ else:
80
+ duration = 0
81
+
82
+ is_valid = width > 0 and height > 0 and frame_count > 0
83
+
84
+ return VideoInfo(
85
+ path=str(path),
86
+ filename=path.name,
87
+ width=width,
88
+ height=height,
89
+ fps=fps,
90
+ frame_count=frame_count,
91
+ duration=duration,
92
+ size_mb=size_mb,
93
+ is_valid=is_valid,
94
+ error=None if is_valid else "Video has no readable frames"
95
+ )
96
+
97
+ except Exception as e:
98
+ return VideoInfo(
99
+ path=str(path),
100
+ filename=path.name,
101
+ width=0,
102
+ height=0,
103
+ fps=0,
104
+ frame_count=0,
105
+ duration=0,
106
+ size_mb=path.stat().st_size / (1024 * 1024) if path.exists() else 0,
107
+ is_valid=False,
108
+ error=str(e)
109
+ )
110
+ finally:
111
+ if cap is not None:
112
+ cap.release()
113
+
114
+
115
+ def scan_directory(directory: str) -> Dict[str, Any]:
116
+ """
117
+ Scan a directory for video files.
118
+
119
+ Args:
120
+ directory: Path to directory
121
+
122
+ Returns:
123
+ Dictionary with scan results
124
+ """
125
+ path = Path(directory)
126
+
127
+ if not path.exists() or not path.is_dir():
128
+ return {
129
+ "error": f"Directory not found: {directory}",
130
+ "videos": []
131
+ }
132
+
133
+ video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
134
+ video_files = []
135
+
136
+ for f in sorted(path.rglob("*")):
137
+ if f.is_file() and f.suffix.lower() in video_extensions:
138
+ info = scan_video(str(f))
139
+ video_files.append({
140
+ "path": str(f),
141
+ "filename": f.name,
142
+ "width": info.width,
143
+ "height": info.height,
144
+ "fps": info.fps,
145
+ "frame_count": info.frame_count,
146
+ "duration": round(info.duration, 2),
147
+ "size_mb": round(info.size_mb, 2),
148
+ "is_valid": info.is_valid,
149
+ "error": info.error
150
+ })
151
+
152
+ total_videos = len(video_files)
153
+ valid_videos = sum(1 for v in video_files if v["is_valid"])
154
+ total_size_mb = sum(v["size_mb"] for v in video_files)
155
+ total_duration = sum(v["duration"] for v in video_files if v["is_valid"])
156
+
157
+ return {
158
+ "directory": str(path),
159
+ "total_videos": total_videos,
160
+ "valid_videos": valid_videos,
161
+ "invalid_videos": total_videos - valid_videos,
162
+ "total_size_mb": round(total_size_mb, 2),
163
+ "total_duration": round(total_duration, 2),
164
+ "videos": video_files
165
+ }
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "openbot-data"
3
+ version = "0.0.1"
4
+ description = "Clean, label, and organize robot data for training and evaluation."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [
9
+ { name = "OpenBot", email = "hello@openbot.ai" }
10
+ ]
11
+ keywords = [
12
+ "robotics",
13
+ "embodied-ai",
14
+ "robot-data",
15
+ "egocentric-video",
16
+ "wrist-camera",
17
+ "data-cleaning",
18
+ "annotation",
19
+ "computer-vision",
20
+ "failure-analysis",
21
+ "dataset"
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 2 - Pre-Alpha",
25
+ "Intended Audience :: Developers",
26
+ "Intended Audience :: Science/Research",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
33
+ "Topic :: Scientific/Engineering :: Image Processing"
34
+ ]
35
+ dependencies = [
36
+ "opencv-python>=4.8.0",
37
+ "numpy>=1.24.0",
38
+ "tqdm>=4.65.0",
39
+ "typer>=0.9.0",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "build>=1.0.0",
45
+ "pytest>=7.0.0",
46
+ "twine>=5.0.0",
47
+ ]
48
+
49
+ [project.urls]
50
+ Homepage = "https://openbot.ai/data"
51
+ Repository = "https://github.com/openbotai/openbot-data"
52
+ Issues = "https://github.com/openbotai/openbot-data/issues"
53
+
54
+ [project.scripts]
55
+ openbot-data = "openbot_data.cli:main"
56
+
57
+ [build-system]
58
+ requires = ["hatchling"]
59
+ build-backend = "hatchling.build"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["openbot_data"]
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = [
66
+ "/LICENSE",
67
+ "/README.md",
68
+ "/openbot_data",
69
+ "/pyproject.toml",
70
+ "/tests",
71
+ ]
72
+ exclude = [
73
+ "**/__pycache__",
74
+ "**/*.pyc",
75
+ ]
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
@@ -0,0 +1,102 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import pytest
7
+ from typer.testing import CliRunner
8
+
9
+ import openbot_data
10
+ from openbot_data.cli import app
11
+ from openbot_data.extract import extract_preview_frames, inspect_dataset
12
+ from openbot_data.video import scan_directory
13
+
14
+
15
+ def make_video(path: Path, frames: int = 6) -> None:
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ writer = cv2.VideoWriter(
18
+ str(path),
19
+ cv2.VideoWriter_fourcc(*"MJPG"),
20
+ 5.0,
21
+ (16, 16),
22
+ )
23
+ if not writer.isOpened():
24
+ pytest.skip("OpenCV video writer is unavailable in this environment")
25
+
26
+ for frame_index in range(frames):
27
+ frame = np.full((16, 16, 3), frame_index * 20, dtype=np.uint8)
28
+ writer.write(frame)
29
+
30
+ writer.release()
31
+
32
+
33
+ def test_public_api_exports_expected_functions() -> None:
34
+ assert openbot_data.__version__ == "0.0.1"
35
+ assert set(openbot_data.__all__) == {
36
+ "extract_preview_frames",
37
+ "inspect_dataset",
38
+ "scan_directory",
39
+ "scan_video",
40
+ }
41
+
42
+
43
+ def test_scan_directory_reports_missing_directory(tmp_path: Path) -> None:
44
+ result = scan_directory(str(tmp_path / "missing"))
45
+
46
+ assert "error" in result
47
+ assert result["videos"] == []
48
+
49
+
50
+ def test_inspect_dataset_generates_manifest_report_and_previews(tmp_path: Path) -> None:
51
+ video_dir = tmp_path / "videos"
52
+ output_dir = tmp_path / "dataset"
53
+ make_video(video_dir / "clip.avi")
54
+
55
+ result = inspect_dataset(str(video_dir), str(output_dir))
56
+
57
+ manifest_path = Path(result["manifest_path"])
58
+ report_path = Path(result["report_path"])
59
+ assert manifest_path.exists()
60
+ assert report_path.exists()
61
+
62
+ manifest = json.loads(manifest_path.read_text())
63
+ report = json.loads(report_path.read_text())
64
+
65
+ assert manifest["valid_videos"] == 1
66
+ assert manifest["total_previews"] > 0
67
+ assert report["resolutions"] == [[16, 16]]
68
+ assert all(Path(frame["path"]).exists() for frame in manifest["videos"][0]["previews"])
69
+
70
+
71
+ def test_preview_frame_names_do_not_collide_for_same_basename(tmp_path: Path) -> None:
72
+ video_dir = tmp_path / "videos"
73
+ output_dir = tmp_path / "dataset"
74
+ make_video(video_dir / "left" / "clip.avi")
75
+ make_video(video_dir / "right" / "clip.avi")
76
+
77
+ result = inspect_dataset(str(video_dir), str(output_dir))
78
+ manifest = json.loads(Path(result["manifest_path"]).read_text())
79
+ preview_paths = [
80
+ frame["path"]
81
+ for video in manifest["videos"]
82
+ for frame in video["previews"]
83
+ ]
84
+
85
+ assert len(preview_paths) == len(set(preview_paths))
86
+
87
+
88
+ def test_cli_version_and_inspect_command(tmp_path: Path) -> None:
89
+ runner = CliRunner()
90
+ version_result = runner.invoke(app, ["version"])
91
+
92
+ assert version_result.exit_code == 0
93
+ assert "OpenBot Data v0.0.1" in version_result.output
94
+
95
+ video_dir = tmp_path / "videos"
96
+ output_dir = tmp_path / "dataset"
97
+ make_video(video_dir / "clip.avi")
98
+ inspect_result = runner.invoke(app, ["inspect", str(video_dir), "--out", str(output_dir)])
99
+
100
+ assert inspect_result.exit_code == 0
101
+ assert "Inspection complete!" in inspect_result.output
102
+ assert (output_dir / "metadata" / "manifest.json").exists()