DynaTTT / scripts /extract_bridge_specialized.py
DanTim05's picture
Upload Zeva source and project assets
68eed61 verified
Raw History Blame Contribute Delete
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()