From 085ca7b749ca8e47f91f39bb717244a7e8c8a8f5 Mon Sep 17 00:00:00 2001 From: Everett Kleven Date: Fri, 10 Jul 2026 17:03:41 -0700 Subject: [PATCH 1/2] feat(examples): the reading-data stage and the numbered examples taxonomy Phase 1 of splitting #13 into reviewable pieces: pure examples, zero new package surface. - examples/README.md indexes the researcher workflow as numbered stages (01 reading data ... 08 policy evals); unlanded stages are marked planned with their target scripts named - 01_reading_data: droid_episode_index.py (daft.datasets.droid; column names updated for the 0.7.18 *_cam_video schema), lerobot_episode_index.py (read_episodes/read_tasks/read without decoding video), and the existing egodex_raw_hdf5_video.py relocated into the stage - 02_episode_data: merge_lerobot_datasets.py - two recording sessions into one table by re-indexing episode_index and the global frame index - the generated hand-tracking demo moves to 04_episode_operations/hand_tracking/ (renames; regen default + README links updated) Every landed script runs on a clean env against daft 0.7.18; suite and pre-commit (ruff + ty) green. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 5 +- README.md | 8 +-- examples/01_reading_data/README.md | 13 ++++ .../01_reading_data/droid_episode_index.py | 34 +++++++++ .../egodex_raw_hdf5_video.py | 0 .../01_reading_data/lerobot_episode_index.py | 46 +++++++++++++ examples/02_episode_data/README.md | 10 +++ .../02_episode_data/merge_lerobot_datasets.py | 65 ++++++++++++++++++ .../hand_tracking/README.md | 32 +++++++++ .../hand_tracking}/demo.ipynb | 0 .../hand_tracking}/demo.md | 0 .../hand_tracking}/demo.py | 0 .../hand_tracking}/demo_keypoints.png | Bin examples/README.md | 51 ++++++-------- scripts/regen_demo.py | 6 +- 15 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 examples/01_reading_data/README.md create mode 100644 examples/01_reading_data/droid_episode_index.py rename examples/{ => 01_reading_data}/egodex_raw_hdf5_video.py (100%) create mode 100644 examples/01_reading_data/lerobot_episode_index.py create mode 100644 examples/02_episode_data/README.md create mode 100644 examples/02_episode_data/merge_lerobot_datasets.py create mode 100644 examples/04_episode_operations/hand_tracking/README.md rename examples/{ => 04_episode_operations/hand_tracking}/demo.ipynb (100%) rename examples/{ => 04_episode_operations/hand_tracking}/demo.md (100%) rename examples/{ => 04_episode_operations/hand_tracking}/demo.py (100%) rename examples/{ => 04_episode_operations/hand_tracking}/demo_keypoints.png (100%) diff --git a/AGENTS.md b/AGENTS.md index 5d11a58..cdc38c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,9 +43,10 @@ Working implementations to port from: multibase `src/post7_hand_tracking/egodex_ batched video decode) - tracked with full steps in [#17](https://github.com/Eventual-Inc/daft-physical-ai/issues/17). -# Regenerating the examples demo +# Regenerating the hand-tracking demo -`examples/{demo.py,demo.ipynb,demo.md,demo_keypoints.png}` are **generated** - +`examples/04_episode_operations/hand_tracking/{demo.py,demo.ipynb,demo.md,demo_keypoints.png}` +are **generated** - don't hand-edit them. They all render from one shared cell list in `daft_physical_ai/_render.py`, so editing the source keeps the three formats in sync. To rebuild them: diff --git a/README.md b/README.md index 3c60736..68d7737 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,13 @@ list[list[float32]], kp3d: list[list[float32]] }]`, defined as `HANDS_DTYPE` in A complete walkthrough - read a dataset, run `track_hands` (MediaPipe), draw the keypoints, and score against EgoDex ground truth: -![track_hands keypoints](examples/demo_keypoints.png) +![track_hands keypoints](examples/04_episode_operations/hand_tracking/demo_keypoints.png) Available in three equivalent forms: -- **[examples/demo.md](examples/demo.md)** - read it start to finish; code and outputs inline. -- **[examples/demo.ipynb](examples/demo.ipynb)** - runnable notebook (outputs included). -- **[examples/demo.py](examples/demo.py)** - plain script. +- **[examples/04_episode_operations/hand_tracking/demo.md](examples/04_episode_operations/hand_tracking/demo.md)** - read it start to finish; code and outputs inline. +- **[examples/04_episode_operations/hand_tracking/demo.ipynb](examples/04_episode_operations/hand_tracking/demo.ipynb)** - runnable notebook (outputs included). +- **[examples/04_episode_operations/hand_tracking/demo.py](examples/04_episode_operations/hand_tracking/demo.py)** - plain script. Generate your own (other methods, a Modal GPU runtime, with/without eval) with the `daft-physical-ai hands` command - run it with no flags for an interactive diff --git a/examples/01_reading_data/README.md b/examples/01_reading_data/README.md new file mode 100644 index 0000000..1a94b69 --- /dev/null +++ b/examples/01_reading_data/README.md @@ -0,0 +1,13 @@ +# 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. diff --git a/examples/01_reading_data/droid_episode_index.py b/examples/01_reading_data/droid_episode_index.py new file mode 100644 index 0000000..9a04893 --- /dev/null +++ b/examples/01_reading_data/droid_episode_index.py @@ -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() diff --git a/examples/egodex_raw_hdf5_video.py b/examples/01_reading_data/egodex_raw_hdf5_video.py similarity index 100% rename from examples/egodex_raw_hdf5_video.py rename to examples/01_reading_data/egodex_raw_hdf5_video.py diff --git a/examples/01_reading_data/lerobot_episode_index.py b/examples/01_reading_data/lerobot_episode_index.py new file mode 100644 index 0000000..bfdaac1 --- /dev/null +++ b/examples/01_reading_data/lerobot_episode_index.py @@ -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()) diff --git a/examples/02_episode_data/README.md b/examples/02_episode_data/README.md new file mode 100644 index 0000000..279ad55 --- /dev/null +++ b/examples/02_episode_data/README.md @@ -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`. diff --git a/examples/02_episode_data/merge_lerobot_datasets.py b/examples/02_episode_data/merge_lerobot_datasets.py new file mode 100644 index 0000000..84ceed8 --- /dev/null +++ b/examples/02_episode_data/merge_lerobot_datasets.py @@ -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()) diff --git a/examples/04_episode_operations/hand_tracking/README.md b/examples/04_episode_operations/hand_tracking/README.md new file mode 100644 index 0000000..472ae5f --- /dev/null +++ b/examples/04_episode_operations/hand_tracking/README.md @@ -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). + +![ground truth vs predictions](demo_keypoints.png) + +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. diff --git a/examples/demo.ipynb b/examples/04_episode_operations/hand_tracking/demo.ipynb similarity index 100% rename from examples/demo.ipynb rename to examples/04_episode_operations/hand_tracking/demo.ipynb diff --git a/examples/demo.md b/examples/04_episode_operations/hand_tracking/demo.md similarity index 100% rename from examples/demo.md rename to examples/04_episode_operations/hand_tracking/demo.md diff --git a/examples/demo.py b/examples/04_episode_operations/hand_tracking/demo.py similarity index 100% rename from examples/demo.py rename to examples/04_episode_operations/hand_tracking/demo.py diff --git a/examples/demo_keypoints.png b/examples/04_episode_operations/hand_tracking/demo_keypoints.png similarity index 100% rename from examples/demo_keypoints.png rename to examples/04_episode_operations/hand_tracking/demo_keypoints.png diff --git a/examples/README.md b/examples/README.md index 12d4b9a..593cd10 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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). - -![ground truth vs predictions](demo_keypoints.png) - -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). diff --git a/scripts/regen_demo.py b/scripts/regen_demo.py index 044bdd7..0b0e13c 100755 --- a/scripts/regen_demo.py +++ b/scripts/regen_demo.py @@ -137,7 +137,11 @@ def _execute(nb_path: Path) -> None: def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description="Regenerate the examples/ demo programmatically.") - p.add_argument("--output-dir", default="examples", help="where to write the demo (default: examples)") + p.add_argument( + "--output-dir", + default="examples/04_episode_operations/hand_tracking", + help="where to write the demo (default: the committed hand-tracking example)", + ) p.add_argument("--skip-exec", action="store_true", help="reuse --source instead of executing a fresh notebook") p.add_argument("--source", help="executed notebook to reuse with --skip-exec (default: /demo.ipynb)") args = p.parse_args(argv) From e1d15dd4cac7d33ba56b2a160a70ed0d8459d058 Mon Sep 17 00:00:00 2001 From: Everett Kleven Date: Fri, 10 Jul 2026 17:14:55 -0700 Subject: [PATCH 2/2] chore: bump daft floor to 0.7.19 0.7.19 adds native MCAP reading (queued as a stage-01 example), HuggingFace path-handling fixes, and single_file parquet writes. All stage-01/02 examples and the suite revalidated against it. Co-Authored-By: Claude Fable 5 --- examples/01_reading_data/README.md | 3 +++ pyproject.toml | 2 +- uv.lock | 15 +++++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/01_reading_data/README.md b/examples/01_reading_data/README.md index 1a94b69..a26b408 100644 --- a/examples/01_reading_data/README.md +++ b/examples/01_reading_data/README.md @@ -11,3 +11,6 @@ these scripts show the minimal, copyable pattern per source. - `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. diff --git a/pyproject.toml b/pyproject.toml index 6813045..82f0844 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Physical-AI data processing on Daft, starting with hand tracking. # (Daft#7184) the demo depends on for reasonable remote-read performance. The # [video] brings av + pillow for video decode; [hdf5] supports raw EgoDex # metadata and trajectory files. Both are part of the normal package surface. -dependencies = ["daft[hdf5,video]>=0.7.18", "numpy"] +dependencies = ["daft[hdf5,video]>=0.7.19", "numpy"] dynamic = ["version"] license = "Apache-2.0" authors = [{name = "Eventual"}] diff --git a/uv.lock b/uv.lock index 5d270a9..1c7745a 100644 --- a/uv.lock +++ b/uv.lock @@ -662,7 +662,7 @@ wheels = [ [[package]] name = "daft" -version = "0.7.18" +version = "0.7.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fsspec" }, @@ -671,13 +671,12 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/36/85ffdc882972a88964056ea6856d51cba207443b4d143d54d986d1588017/daft-0.7.18.tar.gz", hash = "sha256:6dd53a16aaca9c16fccec7763e259b942ff00d2a96b9a382985cb56d57416ef3", size = 3359615, upload-time = "2026-07-09T01:01:23.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/e0/457e3e2285fc4d97d49838e2b377958a4f112062ce31e9a47af43b185222/daft-0.7.18-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd83c9f284bb62a09912628ed4d7273ad8aa16fd8e047311312efc6026ce1c10", size = 55648464, upload-time = "2026-07-09T01:00:58.024Z" }, - { url = "https://files.pythonhosted.org/packages/81/58/18da1c58d606d15a09487019601933cbc69c4dde70e698a073ff6fae7e05/daft-0.7.18-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:511e4078c2655a931974340d1f90b6892a804558cfc97f2c85adf9d9e2e95226", size = 51486766, upload-time = "2026-07-09T01:01:01.307Z" }, - { url = "https://files.pythonhosted.org/packages/f9/85/72676e760e8387c03b78e4eb27172810ec00ebb47aceef153ab835c22319/daft-0.7.18-cp310-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:0538578b7ff0c345f25310fb942eb9feafe5be07332d8d393fdd0a57b6d809d1", size = 53820711, upload-time = "2026-07-09T01:01:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/86/72/d38076455f832a40a9bf5d8eeae8e18794a255a9015ca8b6b59d7729ade7/daft-0.7.18-cp310-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9f4d1591776804b0c9db32ed13d6dd1c9220c27a1603692519db5d1d669b38e9", size = 56017539, upload-time = "2026-07-09T01:01:07.304Z" }, - { url = "https://files.pythonhosted.org/packages/a3/26/13973f5591d1dea4497ac860e22f38f711c4fffd6129278a2935ee1e0953/daft-0.7.18-cp310-abi3-win_amd64.whl", hash = "sha256:8733c4ae21e0a10075bf6e29d1bafd40b7b0a629f3e105fe5d7202e90c06396a", size = 55171843, upload-time = "2026-07-09T01:01:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/4165cee0dcb8110c37064abfe67d14bf503eb3378ad3b98e059a4e920fa6/daft-0.7.19-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7862b017a33c7d7259ece90daf4b2f16e46677774c974bf3c491477476783aab", size = 55673370, upload-time = "2026-07-10T02:35:42.985Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/6699fceee675e292ba7a3e3be18ec7dd6e813623660011082e486e586b6a/daft-0.7.19-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3b928b7af90b02fd460d96e3b524fe8bee3aaafa53c8ad0c54706f20808819c0", size = 51508723, upload-time = "2026-07-10T02:35:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7e/d2723e52fee17d7ad73af3ebda9867aa5fd89e5f2ebf422d53523752ada1/daft-0.7.19-cp310-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:0e5af435aca282c6a54a2ff9f3ac452be219a9abd2572888f595aa5eb4dcccaa", size = 53833418, upload-time = "2026-07-10T02:35:50.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0e/ce9d740e25fe592a7ac87f4704499229e7356e1bdaa4597ac28d5caab75a/daft-0.7.19-cp310-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:6f1d343b231814158061ab108546513c3314fcdb2a3d4530289ea1b781822d0a", size = 56020515, upload-time = "2026-07-10T02:35:54.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ce/f22368e38de6fa757e9598a63838723f28bd1ceaada7a5661268df8319da/daft-0.7.19-cp310-abi3-win_amd64.whl", hash = "sha256:d3e5267bf437321f36858ab30ebf2e86974b1ee1cdb4506fbc783e171119e09f", size = 55181842, upload-time = "2026-07-10T02:35:58.308Z" }, ] [package.optional-dependencies] @@ -758,7 +757,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "daft", extras = ["hdf5", "video"], specifier = ">=0.7.18" }, + { name = "daft", extras = ["hdf5", "video"], specifier = ">=0.7.19" }, { name = "einops", marker = "extra == 'all'" }, { name = "einops", marker = "extra == 'wilor'" }, { name = "hydra-colorlog", marker = "extra == 'all'" },