Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8b335ed
feat: add modal marimo demo site
everettVT Jul 2, 2026
293b973
feat: add episode failure analysis primitives
everettVT Jul 3, 2026
f8f1eb8
feat: reframe failure_modes as evals and number the examples taxonomy
everettVT Jul 5, 2026
48ccb5d
feat: ship real LIBERO-Spatial rollouts in-repo with benchmark-analys…
everettVT Jul 5, 2026
7d0d34c
feat(evals): label the real rollout failures from per-step signals
everettVT Jul 6, 2026
3c4ae8d
feat(ingest): port the robomimic/LIBERO HDF5 demo adapter from the ha…
everettVT Jul 6, 2026
a79b783
feat: land the full LIBERO-Spatial demonstration suite as canonical s…
everettVT Jul 6, 2026
e264aba
feat(operations): motion_trim - the no-noops audit as one Daft groupby
everettVT Jul 6, 2026
140c175
feat(curation): the eval->training bridge, with the loop's last three…
everettVT Jul 6, 2026
49e06db
docs: the loop is operational up to the training step
everettVT Jul 6, 2026
b39642d
feat: adopt daft 0.7.17 - the LeRobot reader ships, the nightly goes …
everettVT Jul 7, 2026
45e1edf
feat(pose): port the EgoDex pose core - geometry, feature tracks, sce…
everettVT Jul 7, 2026
3cbb0d8
docs(site): the gallery tells today's story - the measured loop, not …
everettVT Jul 7, 2026
6b427b0
feat(pose): sync the upstream in-DAG temporal pattern - rates as wind…
everettVT Jul 7, 2026
fbb5a0e
fix(pose): type the @daft.func call sites for CI's cold-cache mypy
everettVT Jul 7, 2026
6f33c34
feat(examples): add Daft-native LeRobot and DROID reading scripts
everettVT Aug 31, 2026
37944ef
merge: bring main into the slimmed dataset-scripts branch
everettVT Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ Three complete walkthroughs, generated by the `daft-physical-ai` CLI. Hand
tracking lives in [hands/](hands/); reward scoring in [rewards/](rewards/);
motion trimming in [trim/](trim/).

Daft already reads robot data. These scripts are the copyable pattern per
format - no extra package surface:

- [`lerobot_episode_index.py`](lerobot_episode_index.py) - episode / task /
frame views of a LeRobot v3 dataset, filtered without decoding video
- [`merge_lerobot_datasets.py`](merge_lerobot_datasets.py) - re-index
`episode_index` and the global frame `index` before concatenating two
recording sessions
- [`droid_episode_index.py`](droid_episode_index.py) - lazy DROID episode
index: successful episodes, projected columns, plan only
- [`egodex_raw_hdf5_video.py`](egodex_raw_hdf5_video.py) - raw EgoDex HDF5 +
video through `daft_physical_ai.datasets.egodex`

```bash
uv run python examples/lerobot_episode_index.py
uv run python examples/merge_lerobot_datasets.py
uv run python examples/droid_episode_index.py
```

## Hand tracking

A complete hand-tracking walkthrough, generated by the `daft-physical-ai` CLI:
Expand Down
34 changes: 34 additions & 0 deletions examples/droid_episode_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Build a lazy DROID episode index with Daft's native reader.

`daft.datasets.droid.raw()` catalogs episodes without decoding video. Filter
and project first; materialize later. This script only prints the plan.
"""

from __future__ import annotations

import daft
from daft.datasets import droid


def build_episode_index() -> daft.DataFrame:
episodes = droid.raw()
successful = episodes.where(daft.col("success") == daft.lit(True))
return successful.select(
"uuid",
"scene_id",
"building",
"current_task",
"success",
"trajectory_length",
"wrist_cam_video",
"ext1_cam_video",
"ext2_cam_video",
)


def main() -> None:
build_episode_index().explain(show_all=True)


if __name__ == "__main__":
main()
45 changes: 45 additions & 0 deletions examples/lerobot_episode_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Index a LeRobot v3 dataset without decoding a single video frame.

`daft.datasets.lerobot` reads a LeRobot dataset lazily: `read_episodes` gives
one row per episode 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 default dataset 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())
61 changes: 61 additions & 0 deletions examples/merge_lerobot_datasets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Merge two LeRobot recording sessions into one table.

Yesterday's teleop and today's land as two LeRobot datasets, both numbering
episodes from zero. `episode_index` and the global frame `index` collide, so
the second session is re-indexed before 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 those of
merging distinct recordings from one rig. Task strings ride on every frame, so
task identity survives 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)

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)
)
Comment on lines +36 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remap task indices before concatenating sessions

When independently recorded sessions assign the same local task_index to different task strings—commonly both start at zero—this concatenation leaves those indices unchanged while making the other identifiers global. The merged table then maps one task index to multiple tasks, so downstream grouping, training, or export through the LeRobot task table can silently conflate supervision; retaining the strings does not make the conflicting integer column safe. Build a unified task mapping and rewrite task_index, or drop that column.

Useful? React with 👍 / 👎.

Comment on lines +37 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Offset the episode range metadata with frame indices

For session B, lerobot.read() broadcasts episode metadata such as dataset_from_index and dataset_to_index, but this chain increments only the row-level index. Those rows consequently no longer satisfy the canonical relationship index == dataset_from_index + frame_index used by examples/trim/demo.py:41, so consumers relying on the ranges can discard or seek the wrong frames from session B. Offset both range columns by frame_offset, or remove and recompute them during the merge.

Useful? React with 👍 / 👎.

)

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}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading