-
Notifications
You must be signed in to change notification settings - Fork 2
feat(examples): reading-data stage + numbered examples taxonomy (phase 1) #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
everettVT
wants to merge
2
commits into
main
Choose a base branch
from
everettVT/phase-01-reading-data
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # 01 - Reading data | ||
|
|
||
| Get robot datasets into Daft. Daft's native readers do the heavy lifting; | ||
| these scripts show the minimal, copyable pattern per source. | ||
|
|
||
| - `droid_episode_index.py` - `daft.datasets.droid.raw()`: filter successful | ||
| episodes and project an operational episode index, lazily. | ||
| - `lerobot_episode_index.py` - `daft.datasets.lerobot` (Daft >= 0.7.17): | ||
| episode/task/frame views of a LeRobot v3 dataset, filtered without decoding | ||
| any video. | ||
| - `egodex_raw_hdf5_video.py` - raw EgoDex episodes from a locally extracted | ||
| release via `daft_physical_ai.datasets.egodex`: lazy `hdf5_file` / | ||
| `video_file` access, no conversion step. | ||
|
|
||
| Planned: `mcap_topics.py` - Daft reads MCAP natively as of 0.7.19; a robot-log | ||
| topic-extraction example is queued. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import daft | ||
| from daft.datasets import droid | ||
|
|
||
|
|
||
| def build_episode_index() -> daft.DataFrame: | ||
| """Build a lazy DROID episode index using released Daft APIs.""" | ||
| episodes = droid.raw() | ||
|
|
||
| successful_episodes = episodes.where(daft.col("success") == daft.lit(True)) | ||
|
|
||
| return successful_episodes.select( | ||
| "uuid", | ||
| "scene_id", | ||
| "building", | ||
| "current_task", | ||
| "success", | ||
| "trajectory_length", | ||
| "wrist_cam_video", | ||
| "ext1_cam_video", | ||
| "ext2_cam_video", | ||
| ) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| episode_index = build_episode_index() | ||
|
|
||
| # Inspect the lazy plan before materializing remote data. | ||
| episode_index.explain(show_all=True) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """Index a LeRobot v3 dataset without decoding a single video frame. | ||
|
|
||
| `daft.datasets.lerobot` (Daft >= 0.7.17) reads a LeRobot dataset lazily: | ||
| `read_episodes` gives one row per episode straight from the metadata, | ||
| `read_tasks` the task table, and `read` one row per frame with episode | ||
| metadata broadcast on - video stays undecoded until you ask for it with | ||
| ``load_video_frames``. The dataset here is a tiny EgoDex sample (3 episodes / | ||
| 632 frames) in LeRobot v3 format. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
|
|
||
| from daft import col | ||
| from daft.datasets import lerobot | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser(description="Episode/task/frame views of a LeRobot dataset.") | ||
| parser.add_argument("--dataset", default="pepijn223/egodex-test", help="HF repo id or path") | ||
| parser.add_argument("--min-length", type=int, default=100, help="episode-length filter to demo") | ||
| args = parser.parse_args() | ||
|
|
||
| episodes = lerobot.read_episodes(args.dataset) | ||
| index = episodes.select("episode_index", "tasks", "length").sort("episode_index").to_pydict() | ||
| print(f"{args.dataset}: {len(index['episode_index'])} episodes") | ||
| for episode_index, tasks, length in zip(index["episode_index"], index["tasks"], index["length"]): | ||
| print(f" episode {episode_index}: {length:4d} frames {tasks[0][:70]}") | ||
|
|
||
| tasks = lerobot.read_tasks(args.dataset).to_pydict() | ||
| print(f"\n{len(next(iter(tasks.values()), []))} distinct tasks in the task table") | ||
|
|
||
| long_episodes = episodes.where(col("length") >= args.min_length) | ||
| frames = lerobot.read(args.dataset).join( | ||
| long_episodes.select("episode_index"), on="episode_index", how="semi" | ||
| ) | ||
| print( | ||
| f"frames in episodes with >= {args.min_length} steps: {frames.count_rows()} " | ||
| f"(selected without touching any video)" | ||
| ) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # 02 - Episode data | ||
|
|
||
| Episode-level views over robot datasets. | ||
|
|
||
| - `merge_lerobot_datasets.py` - merge two LeRobot recording sessions into one | ||
| training table: re-index `episode_index` and the global frame `index`, then | ||
| concat - the collision-prone part of combining recordings, as one Daft query. | ||
|
|
||
| Planned: normalizing demonstrations into the canonical one-row-per-step | ||
| contract (lands with the episode-contract PR), `episode_stats.py`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| """Merge two LeRobot recording sessions into one training table. | ||
|
|
||
| Robot data arrives in batches - yesterday's teleop session and today's land as | ||
| two LeRobot datasets, and both number their episodes from zero. Merging them | ||
| is an index problem: ``episode_index`` and the global frame ``index`` collide, | ||
| so the second session must be re-indexed before the tables can stack. With the | ||
| frames as a lazy dataframe that is an offset and a concat. | ||
|
|
||
| This demo reads the same tiny v3 dataset twice as "session A" and "session B" | ||
| (a second public v3 dataset is not available yet); the mechanics are exactly | ||
| those of merging distinct recordings from one rig. Task strings ride along on | ||
| every frame, so task identity survives the merge without a task_index remap. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
|
|
||
| from daft import col, lit | ||
| from daft.datasets import lerobot | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser(description="Merge two LeRobot sessions with re-indexed episodes.") | ||
| parser.add_argument("--session-a", default="pepijn223/egodex-test") | ||
| parser.add_argument("--session-b", default="pepijn223/egodex-test") | ||
| args = parser.parse_args() | ||
|
|
||
| frames_a = lerobot.read(args.session_a) | ||
| frames_b = lerobot.read(args.session_b) | ||
|
|
||
| # Session A's extent decides session B's offsets. | ||
| extent = frames_a.agg( | ||
| (col("episode_index").max() + lit(1)).alias("n_episodes"), | ||
| (col("index").max() + lit(1)).alias("n_frames"), | ||
| ).to_pydict() | ||
| episode_offset, frame_offset = extent["n_episodes"][0], extent["n_frames"][0] | ||
|
|
||
| merged = frames_a.concat( | ||
| frames_b.with_column("episode_index", col("episode_index") + lit(episode_offset)).with_column( | ||
| "index", col("index") + lit(frame_offset) | ||
| ) | ||
| ) | ||
|
|
||
| lengths = ( | ||
| merged.groupby("episode_index") | ||
| .agg(col("frame_index").count().alias("frames")) | ||
| .sort("episode_index") | ||
| .to_pydict() | ||
| ) | ||
| print(f"merged: {len(lengths['episode_index'])} episodes / {sum(lengths['frames'])} frames") | ||
| for episode_index, frames in zip(lengths["episode_index"], lengths["frames"]): | ||
| source = "A" if episode_index < episode_offset else "B" | ||
| print(f" episode {episode_index} (session {source}): {frames} frames") | ||
|
|
||
| n_frames = merged.count_rows() | ||
| n_distinct = merged.select("index").distinct().count_rows() | ||
| assert n_frames == n_distinct, "global frame index must stay unique after the merge" | ||
| print(f"\nglobal frame index unique after re-indexing: {n_distinct}/{n_frames}") | ||
| print("write it back out: merged.write_parquet(...) - one table, ready for training prep.") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Hand tracking (EgoDex, MediaPipe) | ||
|
|
||
| A complete hand-tracking walkthrough, generated by the `daft-physical-ai` CLI: | ||
| read a LeRobot dataset, run `track_hands` (MediaPipe, CPU), draw the keypoints | ||
| against the EgoDex ground truth, and score them (detect% + PCK). | ||
|
|
||
|  | ||
|
|
||
| Three equivalent forms: | ||
|
|
||
| - **`demo.md`** - read it start to finish; code and outputs inline, nothing to run. | ||
| - **`demo.ipynb`** - the same, executed (outputs included); open in JupyterLab. | ||
| - **`demo.py`** - plain script. | ||
|
|
||
| Run them: | ||
|
|
||
| ```bash | ||
| pip install "daft-physical-ai[mediapipe]" matplotlib scipy | ||
| python examples/04_episode_operations/hand_tracking/demo.py | ||
| # or: jupyter lab examples/04_episode_operations/hand_tracking/demo.ipynb | ||
| ``` | ||
|
|
||
| Want a different setup (WiLoR, both methods, a Modal GPU runtime, with/without | ||
| eval)? Generate your own: | ||
|
|
||
| ```bash | ||
| daft-physical-ai # interactive | ||
| daft-physical-ai --method wilor --runtime modal --mano-path MANO_RIGHT.pkl --no-input | ||
| ``` | ||
|
|
||
| > The committed files here are *executed* (so outputs and the image show without | ||
| > running). The CLI generates the same structure as a fresh starting point. | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,22 @@ | ||
| # Examples | ||
|
|
||
| A complete hand-tracking walkthrough, generated by the `daft-physical-ai` CLI: | ||
| read a LeRobot dataset, run `track_hands` (MediaPipe, CPU), draw the keypoints | ||
| against the EgoDex ground truth, and score them (detect% + PCK). | ||
|
|
||
|  | ||
|
|
||
| Three equivalent forms: | ||
|
|
||
| - **`demo.md`** - read it start to finish; code and outputs inline, nothing to run. | ||
| - **`demo.ipynb`** - the same, executed (outputs included); open in JupyterLab. | ||
| - **`demo.py`** - plain script. | ||
|
|
||
| Run them: | ||
|
|
||
| ```bash | ||
| # deps fetched on the fly, nothing to install | ||
| uv run --with "daft-physical-ai[mediapipe]" --with matplotlib --with scipy examples/demo.py | ||
| # or in JupyterLab: | ||
| uvx --from jupyterlab --with "daft-physical-ai[mediapipe]" --with matplotlib --with scipy jupyter-lab examples/demo.ipynb | ||
| ``` | ||
|
|
||
| Want a different setup (WiLoR, both methods, a Modal GPU runtime, with/without | ||
| eval)? Generate your own: | ||
|
|
||
| ```bash | ||
| daft-physical-ai hands # interactive | ||
| daft-physical-ai hands --method wilor --runtime modal --mano-path MANO_RIGHT.pkl --no-input | ||
| ``` | ||
|
|
||
| > The committed files here are *executed* (so outputs and the image show without | ||
| > running). The CLI generates the same structure as a fresh starting point. | ||
| Runnable physical-AI data recipes on Daft, numbered as the workflow a | ||
| researcher actually runs: read datasets, inspect episode data, transform, | ||
| run episode operations, label with inference, write outputs, hand off to | ||
| training, analyze policy evals. Stages land incrementally; directories | ||
| marked *planned* are reserved, with their target scripts named here. | ||
|
|
||
| | # | Stage | What it covers | Status | | ||
| |---|---|---|---| | ||
| | 01 | [Reading data](01_reading_data/) | Robot datasets into Daft: DROID metadata, LeRobot v3 episode/task/frame views, raw EgoDex HDF5+video | `droid_episode_index.py` · `lerobot_episode_index.py` · `egodex_raw_hdf5_video.py` | | ||
| | 02 | [Episode data](02_episode_data/) | Episode-level views and dataset combination | `merge_lerobot_datasets.py`; normalization lands with the episode contract | | ||
| | 03 | Transforms | Deterministic NumPy features as episode passes and in-plan expressions | planned | | ||
| | 04 | [Episode operations](04_episode_operations/) | Packaged robotics ops over episodes | [`hand_tracking/`](04_episode_operations/hand_tracking/); motion trim and pose queries planned | | ||
| | 05 | Inference | Model-backed labeling with Daft AI functions | planned | | ||
| | 06 | Writing data | Curated training artifacts as views | planned | | ||
| | 07 | Training handoff | Curated dataframes into `to_torch_dataloader` | planned | | ||
| | 08 | Policy evals | Benchmark reproduction and failure mining over rollout parquet | planned | | ||
|
|
||
| Every landed example runs first-try on a clean environment against public | ||
| data (EgoDex raw reading expects a locally extracted release - see its | ||
| docstring). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a reader follows these commands to generate a custom demo, they won't run the generator: the top-level parser creates subparsers and returns after printing help if no command is provided (
daft_physical_ai/cli/__init__.py:21-31), while--method/--runtimeare registered only on thehandssubcommand (daft_physical_ai/cli/hands.py:36-42). Please keepdaft-physical-ai hands ...here so both the interactive and noninteractive examples actually scaffold the demo.Useful? React with 👍 / 👎.