Download scripts/extract_bridge_specialized.py from DanTim05/DynaTTT: direct link, hf CLI and curl.
- Browser
- Download file 5.24 kB
-
https://huggingface.co/spaces/DanTim05/DynaTTT/resolve/main/scripts/extract_bridge_specialized.py
- Command line
-
hf download hf://spaces/DanTim05/DynaTTT/scripts/extract_bridge_specialized.py
-
curl -L -o extract_bridge_specialized.py https://huggingface.co/spaces/DanTim05/DynaTTT/resolve/main/scripts/extract_bridge_specialized.py
5.24 kB
| #!/usr/bin/env python3 | |
| """Extract a low-variation Bridge/LeRobot subset by episode task text. | |
| The default selection is the plate-placement family (``put ... on plate``), | |
| which is close to the current carrot-on-plate evaluation while retaining | |
| multiple objects and camera scenes. Episodes are copied whole, so no video | |
| re-encoding or temporal duplication is introduced. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| from pathlib import Path | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--src", default="/opt/zhangchenyu/datasets/bridge_orig_lerobot_smoke") | |
| ap.add_argument("--dst", default=".artifacts/bridge_put_on_plate_259") | |
| ap.add_argument("--pattern", default=r"^put .+ on plate(?: sink)?$") | |
| ap.add_argument("--max-episodes", type=int, default=None) | |
| ap.add_argument("--min-length", type=int, default=None, | |
| help="keep only episodes with at least this many frames") | |
| ap.add_argument("--hardlink-videos", action="store_true", | |
| help="hardlink videos instead of duplicating them when possible") | |
| args = ap.parse_args() | |
| src, dst = Path(args.src), Path(args.dst) | |
| meta = src / "meta" | |
| episodes = [json.loads(x) for x in (meta / "episodes.jsonl").read_text().splitlines()] | |
| tasks = {int(x["task_index"]): x["task"] for x in | |
| (json.loads(x) for x in (meta / "tasks.jsonl").read_text().splitlines())} | |
| rx = re.compile(args.pattern, re.I) | |
| selected = [] | |
| for ep in episodes: | |
| text = " ".join(ep.get("tasks", [])) | |
| if rx.fullmatch(text.strip()): | |
| selected.append((ep, text.strip())) | |
| selected.sort(key=lambda x: int(x[0]["episode_index"])) | |
| if args.min_length is not None: | |
| selected = [x for x in selected if int(x[0]["length"]) >= args.min_length] | |
| if args.max_episodes is not None: | |
| selected = selected[:args.max_episodes] | |
| if not selected: | |
| raise SystemExit("no matching episodes") | |
| missing = [] | |
| for ep, _ in selected: | |
| i = int(ep["episode_index"]) | |
| chunk = i // 1000 | |
| dp = src / f"data/chunk-{chunk:03d}/episode_{i:06d}.parquet" | |
| vp = src / f"videos/chunk-{chunk:03d}/observation.images.image_0/episode_{i:06d}.mp4" | |
| if not dp.exists() or not vp.exists(): | |
| missing.extend(str(p) for p in (dp, vp) if not p.exists()) | |
| if missing: | |
| raise SystemExit("raw parquet/video files are missing; metadata-only source. First missing: " + missing[0]) | |
| # Import only after the source check: this script remains usable for dry inspection. | |
| import pyarrow.parquet as pq | |
| import pyarrow as pa | |
| (dst / "meta").mkdir(parents=True, exist_ok=True) | |
| out_eps, out_tasks = [], [] | |
| total_frames = 0 | |
| for new_i, (ep, text) in enumerate(selected): | |
| old_i = int(ep["episode_index"]) | |
| chunk = old_i // 1000 | |
| old_dp = src / f"data/chunk-{chunk:03d}/episode_{old_i:06d}.parquet" | |
| old_vp = src / f"videos/chunk-{chunk:03d}/observation.images.image_0/episode_{old_i:06d}.mp4" | |
| new_dp = dst / f"data/chunk-000/episode_{new_i:06d}.parquet" | |
| new_vp = dst / f"videos/chunk-000/observation.images.image_0/episode_{new_i:06d}.mp4" | |
| new_dp.parent.mkdir(parents=True, exist_ok=True) | |
| new_vp.parent.mkdir(parents=True, exist_ok=True) | |
| tab = pq.read_table(old_dp) | |
| # Keep all rows but rewrite episode/task ids to a compact standalone set. | |
| cols = {name: tab[name] for name in tab.column_names} | |
| if "episode_index" in cols: | |
| cols["episode_index"] = pa.array([new_i] * tab.num_rows, type=cols["episode_index"].type) | |
| task_col = pa.array([new_i] * tab.num_rows, type=cols["task_index"].type) | |
| cols["task_index"] = task_col | |
| pq.write_table(pa.table(cols), new_dp) | |
| if args.hardlink_videos: | |
| os.link(old_vp, new_vp) | |
| else: | |
| shutil.copy2(old_vp, new_vp) | |
| out_eps.append({"episode_index": new_i, "tasks": [text], "length": int(ep["length"])}) | |
| out_tasks.append({"task_index": new_i, "task": text}) | |
| total_frames += int(ep["length"]) | |
| info = json.loads((meta / "info.json").read_text()) | |
| info["total_episodes"] = len(out_eps) | |
| info["total_frames"] = total_frames | |
| info["total_tasks"] = len(out_tasks) | |
| info["total_videos"] = len(out_eps) | |
| info["total_chunks"] = 1 | |
| info["splits"] = {"train": f"0:{len(out_eps)}"} | |
| info["data_path"] = "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet" | |
| info["video_path"] = "videos/chunk-{episode_chunk:03d}/{video_key}/episode_{episode_index:06d}.mp4" | |
| (dst / "meta/info.json").write_text(json.dumps(info, indent=2) + "\n") | |
| (dst / "meta/episodes.jsonl").write_text("".join(json.dumps(x) + "\n" for x in out_eps)) | |
| (dst / "meta/tasks.jsonl").write_text("".join(json.dumps(x) + "\n" for x in out_tasks)) | |
| for name in ("modality.json", "stats.json", "stats_gr00t.json"): | |
| if (meta / name).exists(): shutil.copy2(meta / name, dst / "meta" / name) | |
| print(json.dumps({"episodes": len(out_eps), "frames": total_frames, "dst": str(dst)}, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |