Files
2026-07-13 13:05:14 +08:00

223 lines
9.3 KiB
Python

"""Register a few DROID episodes to a local Rerun catalog so the rest of the example has data to index.
Two sources, auto-selected (override with `--source`):
* **Bundled** — the `tests/assets/rrd/sample_5` episodes shipped in the Rerun repo (via git-LFS).
Used automatically in a monorepo checkout: no download, works offline.
* **Hugging Face** — a few episodes from the [`rerun/droid_sample`](https://huggingface.co/datasets/rerun/droid_sample)
dataset, downloaded into `./data`. Used when the bundled episodes aren't available
(e.g. a standalone sparse-checkout of just this example).
Either way the episodes are registered to the catalog as a dataset (default name `droid:sample`).
They carry H.264 `VideoStream`s but no pre-computed embeddings, so `ingest.py` will take its
(slower) compute path: decode frames and embed them with SigLIP-2.
Episodes are optimized first to derive keyframe markers, so the compute path can decode
frames (DROID doesn't log the markers the decoder needs). Pass `--no-optimize` to skip.
Run inside the rerun SDK venv, with a `rerun server` running in another terminal, e.g.:
uv run python prepare_dataset.py
"""
from __future__ import annotations
import argparse
from dataclasses import replace
from pathlib import Path
from tqdm import tqdm
import rerun as rr
from rerun.experimental import OptimizationProfile, RrdReader
# `tests/assets/rrd/sample_5`, relative to this file at `examples/python/droid_semantic_search/`.
BUNDLED_SAMPLE_DIR = Path(__file__).resolve().parents[3] / "tests" / "assets" / "rrd" / "sample_5"
DEFAULT_REPO_ID = "rerun/droid_sample"
DEFAULT_OUTPUT_DIR = Path(__file__).resolve().parent / "data"
DEFAULT_OPTIMIZED_DIR = Path(__file__).resolve().parent / "optimized"
DEFAULT_DATASET_NAME = "droid:sample"
DEFAULT_CATALOG_URL = "rerun+http://127.0.0.1:51234"
_LFS_POINTER_PREFIX = b"version https://git-lfs.github.com/spec/v1"
def _is_lfs_pointer(path: Path) -> bool:
"""True if *path* is an un-pulled git-LFS pointer file rather than the real RRD."""
with path.open("rb") as f:
return f.read(len(_LFS_POINTER_PREFIX)) == _LFS_POINTER_PREFIX
def bundled_episode_paths() -> list[Path]:
"""Return the bundled sample_5 RRDs (sorted), or an empty list if they're not available.
Raises if the directory exists but the files are un-pulled git-LFS pointers — that's a
recoverable monorepo setup issue worth surfacing rather than silently working around.
"""
if not BUNDLED_SAMPLE_DIR.is_dir():
return []
rrds = sorted(BUNDLED_SAMPLE_DIR.glob("*.rrd"))
if not rrds:
return []
if any(_is_lfs_pointer(p) for p in rrds):
raise SystemExit(
f"The bundled DROID episodes in {BUNDLED_SAMPLE_DIR} are un-pulled git-LFS pointers.\n"
"Fetch them with `git lfs install && git lfs pull`, or pass `--source huggingface` to download instead.",
)
return rrds
def download_episodes(repo_id: str, num_episodes: int, dest: Path) -> list[Path]:
"""Download the first *num_episodes* (0 for all) `.rrd` files from *repo_id* into *dest*.
`snapshot_download` renders its own per-file progress bars, so the user sees the download advance.
"""
from huggingface_hub import HfApi, snapshot_download
files = sorted(f for f in HfApi().list_repo_files(repo_id, repo_type="dataset") if f.endswith(".rrd"))
if not files:
raise SystemExit(f"No .rrd files found in '{repo_id}'.")
files = files if num_episodes == 0 else files[:num_episodes]
print(f"Downloading {len(files)} episode(s) from '{repo_id}' to {dest} …")
local_dir = snapshot_download(repo_id=repo_id, repo_type="dataset", allow_patterns=files, local_dir=dest)
return [Path(local_dir) / f for f in files]
def resolve_episodes(source: str, *, num_episodes: int, repo_id: str, output_dir: Path) -> list[Path]:
"""Pick the episode RRDs to register, per the requested *source* (`auto`/`bundled`/`huggingface`)."""
if source in ("auto", "bundled"):
bundled = bundled_episode_paths()
if bundled:
paths = bundled if num_episodes == 0 else bundled[:num_episodes]
print(f"Using {len(paths)} bundled episode(s) from {BUNDLED_SAMPLE_DIR}")
return paths
if source == "bundled":
raise SystemExit(f"No bundled episodes found at {BUNDLED_SAMPLE_DIR}; pass `--source huggingface`.")
print(f"Bundled episodes not found at {BUNDLED_SAMPLE_DIR}; downloading from the Hub instead.")
return download_episodes(repo_id, num_episodes, output_dir)
def optimize_episodes(rrd_paths: list[Path], dest_dir: Path) -> list[Path]:
"""Derive `VideoStream:is_keyframe` markers (DROID doesn't log them) so the decoder can seek.
Writes a fixed copy of each episode to *dest_dir* and returns the new paths. IDs are
preserved, so catalog segment IDs and `segment_url` links are unchanged.
"""
dest_dir.mkdir(parents=True, exist_ok=True)
profile = replace(OptimizationProfile.OBJECT_STORE, fix_keyframe=True)
optimized: list[Path] = []
for src in tqdm(rrd_paths, desc="Optimizing", unit="episode"):
reader = RrdReader(src)
recordings = reader.recordings()
if len(recordings) != 1:
raise SystemExit(f"Expected one recording in {src}, found {len(recordings)}.")
entry = recordings[0]
store = reader.stream(store=entry).collect(optimize=profile)
dst = dest_dir / src.name
store.write_rrd(dst, application_id=entry.application_id, recording_id=entry.recording_id)
optimized.append(dst)
return optimized
def register_to_catalog(rrd_paths: list[Path], *, catalog_url: str, dataset_name: str) -> None:
"""Register per-episode RRDs to a catalog server instance.
Uses absolute `file://` URIs so the catalog can read the RRDs directly from the local filesystem.
Streams `iter_results()` so a progress bar advances as each segment finishes, rather than blocking
silently on `wait()`.
"""
print(f"\nRegistering {len(rrd_paths)} episode(s) to {catalog_url} as dataset '{dataset_name}' …")
client = rr.catalog.CatalogClient(catalog_url)
dataset = client.create_dataset(dataset_name, exist_ok=True)
uris = [f"file://{p.resolve()}" for p in rrd_paths]
on_duplicate = rr.catalog.OnDuplicateSegmentLayer(rr.catalog.OnDuplicateSegmentLayer.REPLACE)
handle = dataset.register(uris, on_duplicate=on_duplicate)
failures: list[str] = []
for result in tqdm(handle.iter_results(), total=len(uris), desc="Registering", unit="segment"):
if result.is_error:
failures.append(f"{result.uri}: {result.error}")
if failures:
joined = "\n ".join(failures)
raise SystemExit(f"Failed to register {len(failures)} of {len(uris)} episode(s):\n {joined}")
print(" registration done")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--source",
choices=("auto", "bundled", "huggingface"),
default="auto",
help="Where to get episodes: 'bundled' (in-repo sample_5), 'huggingface' (download), "
"or 'auto' (bundled if available, else download). Default: auto.",
)
parser.add_argument(
"--repo-id",
default=DEFAULT_REPO_ID,
help=f"Hugging Face dataset repo id, for the download path (default: {DEFAULT_REPO_ID}).",
)
parser.add_argument(
"--num-episodes",
type=int,
default=5,
help="Number of episodes to register (0 for all). The full Hub dataset is ~3.3 GB.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help=f"Directory to download episode RRDs into, for the download path (default: {DEFAULT_OUTPUT_DIR}).",
)
parser.add_argument(
"--optimize",
default=True,
action=argparse.BooleanOptionalAction,
help="Derive keyframe markers before registering, so ingest.py can decode frames "
"(~100%% yield vs ~25%%). Use --no-optimize to register episodes as-is.",
)
parser.add_argument(
"--optimized-dir",
type=Path,
default=DEFAULT_OPTIMIZED_DIR,
help=f"Directory to write optimized episode RRDs into (default: {DEFAULT_OPTIMIZED_DIR}).",
)
parser.add_argument(
"--catalog-url",
default=DEFAULT_CATALOG_URL,
help="Rerun catalog URL to register episodes with. Pass an empty string to skip registration.",
)
parser.add_argument(
"--dataset-name",
default=DEFAULT_DATASET_NAME,
help=f"Name of the dataset to create/use in the catalog (default: {DEFAULT_DATASET_NAME}).",
)
args = parser.parse_args()
rrd_paths = resolve_episodes(
args.source,
num_episodes=args.num_episodes,
repo_id=args.repo_id,
output_dir=args.output_dir,
)
if args.optimize:
print(f"\nOptimizing {len(rrd_paths)} episode(s) into {args.optimized_dir} …")
rrd_paths = optimize_episodes(rrd_paths, args.optimized_dir)
if args.catalog_url:
register_to_catalog(rrd_paths, catalog_url=args.catalog_url, dataset_name=args.dataset_name)
else:
print(f"Skipping registration (empty --catalog-url). Episodes: {[str(p) for p in rrd_paths]}")
if __name__ == "__main__":
main()