Spaces:
Sleeping
Sleeping
rudraksh Claude Opus 4.7 commited on
Commit ·
9468cdd
1
Parent(s): 49aaa1d
Add OpenX Rerun Viewer: 6 robot types, 18 episodes
Browse filesInteractive browser for OpenX-Embodiment datasets with embedded
Rerun visualization. Covers Google Robot, Franka Panda, WidowX,
Hello Stretch, Kinova Jaco, and KUKA IIWA.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .dockerignore +3 -0
- .gitattributes +1 -0
- Dockerfile +15 -0
- app.py +125 -0
- data/bridge_orig/episode_000000.rrd +3 -0
- data/bridge_orig/episode_000001.rrd +3 -0
- data/bridge_orig/episode_000002.rrd +3 -0
- data/dobbe/episode_000000.rrd +3 -0
- data/dobbe/episode_000001.rrd +3 -0
- data/dobbe/episode_000002.rrd +3 -0
- data/droid/episode_000000.rrd +3 -0
- data/droid/episode_000001.rrd +3 -0
- data/droid/episode_000002.rrd +3 -0
- data/fractal20220817_data/episode_000000.rrd +3 -0
- data/fractal20220817_data/episode_000001.rrd +3 -0
- data/fractal20220817_data/episode_000002.rrd +3 -0
- data/jaco_play/episode_000000.rrd +3 -0
- data/jaco_play/episode_000001.rrd +3 -0
- data/jaco_play/episode_000002.rrd +3 -0
- data/kuka/episode_000000.rrd +3 -0
- data/kuka/episode_000001.rrd +3 -0
- data/kuka/episode_000002.rrd +3 -0
- data/manifest.json +206 -0
- prepare_episodes.py +318 -0
- requirements.txt +8 -0
- static/style.css +1 -0
- templates/index.html +188 -0
.dockerignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.git
|
.gitattributes
CHANGED
|
@@ -22,6 +22,7 @@
|
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.rrd filter=lfs diff=lfs merge=lfs -text
|
| 26 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 27 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
libavcodec-extra libavformat-dev libavdevice-dev \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI server for the OpenX Rerun Viewer HF Space.
|
| 3 |
+
Runs the selection UI and serves .rrd files.
|
| 4 |
+
When running locally, also starts a local Rerun web viewer to avoid mixed-content issues.
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from fastapi import FastAPI
|
| 14 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 15 |
+
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
|
| 18 |
+
app = FastAPI(title="OpenX Rerun Viewer")
|
| 19 |
+
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"],
|
| 23 |
+
allow_methods=["GET", "HEAD", "OPTIONS"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
expose_headers=[
|
| 26 |
+
"Content-Length", "Content-Type", "Accept-Ranges",
|
| 27 |
+
"Cross-Origin-Resource-Policy",
|
| 28 |
+
],
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
DATA_DIR = Path(__file__).parent / "data"
|
| 32 |
+
MANIFEST_PATH = DATA_DIR / "manifest.json"
|
| 33 |
+
|
| 34 |
+
IS_LOCAL = "SPACE_ID" not in os.environ and "HF_SPACE" not in os.environ
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_manifest():
|
| 38 |
+
if MANIFEST_PATH.exists():
|
| 39 |
+
return json.loads(MANIFEST_PATH.read_text())
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@app.get("/", response_class=HTMLResponse)
|
| 44 |
+
async def index():
|
| 45 |
+
html = (Path(__file__).parent / "templates" / "index.html").read_text()
|
| 46 |
+
# Inject the viewer base URL: use local viewer for dev, app.rerun.io for production
|
| 47 |
+
if IS_LOCAL:
|
| 48 |
+
viewer_base = "http://localhost:9090"
|
| 49 |
+
else:
|
| 50 |
+
viewer_base = "https://app.rerun.io/version/0.26.2"
|
| 51 |
+
html = html.replace("{{VIEWER_BASE}}", viewer_base)
|
| 52 |
+
return HTMLResponse(html)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@app.get("/api/datasets")
|
| 56 |
+
async def list_datasets():
|
| 57 |
+
manifest = load_manifest()
|
| 58 |
+
return [
|
| 59 |
+
{
|
| 60 |
+
"slug": m["slug"],
|
| 61 |
+
"robot_type": m["robot_type"],
|
| 62 |
+
"fps": m.get("fps", 0),
|
| 63 |
+
"camera_keys": m.get("camera_keys", []),
|
| 64 |
+
"camera_labels": m.get("camera_labels", []),
|
| 65 |
+
"num_episodes_total": m.get("num_episodes_total", 0),
|
| 66 |
+
"state_dim": m.get("state_dim", 0),
|
| 67 |
+
"action_dim": m.get("action_dim", 0),
|
| 68 |
+
"episodes_available": [
|
| 69 |
+
{"index": ep["index"], "frames": ep.get("frames", "?")}
|
| 70 |
+
for ep in m.get("episodes_available", [])
|
| 71 |
+
],
|
| 72 |
+
}
|
| 73 |
+
for m in manifest
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.get("/api/datasets/{slug}/episodes")
|
| 78 |
+
async def list_episodes(slug: str):
|
| 79 |
+
manifest = load_manifest()
|
| 80 |
+
for m in manifest:
|
| 81 |
+
if m["slug"] == slug:
|
| 82 |
+
return m["episodes_available"]
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@app.api_route("/data/{slug}/episode_{index:str}.rrd", methods=["GET", "HEAD"])
|
| 87 |
+
async def serve_rrd(slug: str, index: str):
|
| 88 |
+
filepath = DATA_DIR / slug / f"episode_{index}.rrd"
|
| 89 |
+
if not filepath.exists():
|
| 90 |
+
return HTMLResponse(status_code=404, content="Episode not found")
|
| 91 |
+
return FileResponse(
|
| 92 |
+
filepath,
|
| 93 |
+
media_type="application/octet-stream",
|
| 94 |
+
headers={
|
| 95 |
+
"Accept-Ranges": "bytes",
|
| 96 |
+
"Access-Control-Allow-Origin": "*",
|
| 97 |
+
"Cross-Origin-Resource-Policy": "cross-origin",
|
| 98 |
+
"Cache-Control": "public, max-age=3600",
|
| 99 |
+
},
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
static_dir = Path(__file__).parent / "static"
|
| 104 |
+
if static_dir.exists():
|
| 105 |
+
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def start_local_viewer():
|
| 109 |
+
"""Start Rerun web viewer on port 9090 for local development."""
|
| 110 |
+
import rerun as rr
|
| 111 |
+
rr.serve_web_viewer(web_port=9090, open_browser=False)
|
| 112 |
+
# serve_web_viewer returns immediately after starting the server in a thread
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
import uvicorn
|
| 117 |
+
|
| 118 |
+
port = int(os.environ.get("PORT", 8000))
|
| 119 |
+
|
| 120 |
+
if IS_LOCAL:
|
| 121 |
+
print("Local mode: starting Rerun web viewer on http://localhost:9090")
|
| 122 |
+
start_local_viewer()
|
| 123 |
+
time.sleep(0.5)
|
| 124 |
+
|
| 125 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
data/bridge_orig/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:51903460b310529ac41f0335f4dcdff64d946704ba28bf8260c37b8b3cea7309
|
| 3 |
+
size 14270099
|
data/bridge_orig/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:03c1d7099dd130d493237cefe9f281f22854d0e21e9426aa38d2792aca215f68
|
| 3 |
+
size 7806978
|
data/bridge_orig/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3dca45de55db434e5df5db863912516f560b6d56c06267d19e3bf792b9ac43f0
|
| 3 |
+
size 7312412
|
data/dobbe/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:89c62baf47a68f5570a7c3c0420d0eaa8c50ffd4c49cce523f76c06eba15c35b
|
| 3 |
+
size 20219543
|
data/dobbe/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:433d18d82f73031dacaff98da6af9dc08308d952ecbd524375512bf432bd8906
|
| 3 |
+
size 53905361
|
data/dobbe/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:09d5b8de3006c68bf5c4f27f747c75332e85bc928d3e4f65117afaf673c8b491
|
| 3 |
+
size 26818095
|
data/droid/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3248d21abc772ba2bde357fd922e3a866fd53601ac5e83e133b5111e55056c3b
|
| 3 |
+
size 95062810
|
data/droid/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b1600b232854183f30fa943318d4c867b41291a8074670aed2b83bef95252b52
|
| 3 |
+
size 73363811
|
data/droid/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d059258a723f936488f98da219f20477b92e558c1f3f55460bf87b6f2644e2ea
|
| 3 |
+
size 93381685
|
data/fractal20220817_data/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:534f14e125a1a9f019996794d241a5e20069266fb9790b81fabf58443b8fa1d1
|
| 3 |
+
size 16987673
|
data/fractal20220817_data/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:48b38f276cae71ca6032d350fc670db8f09b6de7e3e950e3991dde6ce38391fa
|
| 3 |
+
size 10698015
|
data/fractal20220817_data/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:937ed9829cd0827b7467c62abff810d7de4987d20a972989b30a07b84ff0d3e2
|
| 3 |
+
size 3967992
|
data/jaco_play/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c64c2007fa1cb857012e70bd2f12509d965f326f28a7f69ad1c6fba2aa48eb2d
|
| 3 |
+
size 17340014
|
data/jaco_play/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e4f12095aab2d0553f281f85052c64e810fba98a77ce854404154ff503b54cb4
|
| 3 |
+
size 10299399
|
data/jaco_play/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1a9eb5f38489fb277bd6b21a0245b9245a37525978a1805c0bdb4b92b94d5fad
|
| 3 |
+
size 20648516
|
data/kuka/episode_000000.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:50879f8c7047ca89e623d90590437688d5a7d64f1b644b2f5c69a1d60eaebe8d
|
| 3 |
+
size 5200888
|
data/kuka/episode_000001.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c908786af4f521a90dd5590766c766e9c44e9ef67af5ffb66d82ecb92b1dd2a
|
| 3 |
+
size 6013097
|
data/kuka/episode_000002.rrd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e06a42e5b45638eda39fd98196e14752fdbd94550137e4c1e8ab18641ff71a4d
|
| 3 |
+
size 7105434
|
data/manifest.json
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"slug": "fractal20220817_data",
|
| 4 |
+
"repo_id": "IPEC-COMMUNITY/fractal20220817_data_lerobot",
|
| 5 |
+
"robot_type": "Google Robot",
|
| 6 |
+
"fps": 3,
|
| 7 |
+
"camera_keys": [
|
| 8 |
+
"observation.images.image"
|
| 9 |
+
],
|
| 10 |
+
"camera_labels": [
|
| 11 |
+
"main"
|
| 12 |
+
],
|
| 13 |
+
"episodes_available": [
|
| 14 |
+
{
|
| 15 |
+
"index": 0,
|
| 16 |
+
"file": "data/fractal20220817_data/episode_000000.rrd",
|
| 17 |
+
"frames": 115
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"index": 1,
|
| 21 |
+
"file": "data/fractal20220817_data/episode_000001.rrd",
|
| 22 |
+
"frames": 66
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"index": 2,
|
| 26 |
+
"file": "data/fractal20220817_data/episode_000002.rrd",
|
| 27 |
+
"frames": 25
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"state_dim": 8,
|
| 31 |
+
"action_dim": 7,
|
| 32 |
+
"num_episodes_total": 87212
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"slug": "droid",
|
| 36 |
+
"repo_id": "IPEC-COMMUNITY/droid_lerobot",
|
| 37 |
+
"robot_type": "Franka Panda",
|
| 38 |
+
"fps": 15,
|
| 39 |
+
"camera_keys": [
|
| 40 |
+
"observation.images.exterior_image_1_left",
|
| 41 |
+
"observation.images.exterior_image_2_left",
|
| 42 |
+
"observation.images.wrist_image_left"
|
| 43 |
+
],
|
| 44 |
+
"camera_labels": [
|
| 45 |
+
"exterior_left",
|
| 46 |
+
"exterior_right",
|
| 47 |
+
"wrist"
|
| 48 |
+
],
|
| 49 |
+
"episodes_available": [
|
| 50 |
+
{
|
| 51 |
+
"index": 0,
|
| 52 |
+
"file": "data/droid/episode_000000.rrd",
|
| 53 |
+
"frames": 240
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"index": 1,
|
| 57 |
+
"file": "data/droid/episode_000001.rrd",
|
| 58 |
+
"frames": 193
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"index": 2,
|
| 62 |
+
"file": "data/droid/episode_000002.rrd",
|
| 63 |
+
"frames": 239
|
| 64 |
+
}
|
| 65 |
+
],
|
| 66 |
+
"state_dim": 8,
|
| 67 |
+
"action_dim": 7,
|
| 68 |
+
"num_episodes_total": 92233
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"slug": "bridge_orig",
|
| 72 |
+
"repo_id": "IPEC-COMMUNITY/bridge_orig_lerobot",
|
| 73 |
+
"robot_type": "WidowX",
|
| 74 |
+
"fps": 5,
|
| 75 |
+
"camera_keys": [
|
| 76 |
+
"observation.images.image_0",
|
| 77 |
+
"observation.images.image_1",
|
| 78 |
+
"observation.images.image_2",
|
| 79 |
+
"observation.images.image_3"
|
| 80 |
+
],
|
| 81 |
+
"camera_labels": [
|
| 82 |
+
"cam_0",
|
| 83 |
+
"cam_1",
|
| 84 |
+
"cam_2",
|
| 85 |
+
"cam_3"
|
| 86 |
+
],
|
| 87 |
+
"episodes_available": [
|
| 88 |
+
{
|
| 89 |
+
"index": 0,
|
| 90 |
+
"file": "data/bridge_orig/episode_000000.rrd",
|
| 91 |
+
"frames": 26
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"index": 1,
|
| 95 |
+
"file": "data/bridge_orig/episode_000001.rrd",
|
| 96 |
+
"frames": 45
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"index": 2,
|
| 100 |
+
"file": "data/bridge_orig/episode_000002.rrd",
|
| 101 |
+
"frames": 45
|
| 102 |
+
}
|
| 103 |
+
],
|
| 104 |
+
"state_dim": 8,
|
| 105 |
+
"action_dim": 7,
|
| 106 |
+
"num_episodes_total": 53192
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"slug": "dobbe",
|
| 110 |
+
"repo_id": "IPEC-COMMUNITY/dobbe_lerobot",
|
| 111 |
+
"robot_type": "Hello Stretch",
|
| 112 |
+
"fps": 3.75,
|
| 113 |
+
"camera_keys": [
|
| 114 |
+
"observation.images.wrist_image"
|
| 115 |
+
],
|
| 116 |
+
"camera_labels": [
|
| 117 |
+
"wrist"
|
| 118 |
+
],
|
| 119 |
+
"episodes_available": [
|
| 120 |
+
{
|
| 121 |
+
"index": 0,
|
| 122 |
+
"file": "data/dobbe/episode_000000.rrd",
|
| 123 |
+
"frames": 225
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"index": 1,
|
| 127 |
+
"file": "data/dobbe/episode_000001.rrd",
|
| 128 |
+
"frames": 354
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"index": 2,
|
| 132 |
+
"file": "data/dobbe/episode_000002.rrd",
|
| 133 |
+
"frames": 277
|
| 134 |
+
}
|
| 135 |
+
],
|
| 136 |
+
"state_dim": 8,
|
| 137 |
+
"action_dim": 7,
|
| 138 |
+
"num_episodes_total": 5208
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"slug": "jaco_play",
|
| 142 |
+
"repo_id": "IPEC-COMMUNITY/jaco_play_lerobot",
|
| 143 |
+
"robot_type": "Kinova Jaco",
|
| 144 |
+
"fps": 10,
|
| 145 |
+
"camera_keys": [
|
| 146 |
+
"observation.images.image",
|
| 147 |
+
"observation.images.image_wrist"
|
| 148 |
+
],
|
| 149 |
+
"camera_labels": [
|
| 150 |
+
"main",
|
| 151 |
+
"wrist"
|
| 152 |
+
],
|
| 153 |
+
"episodes_available": [
|
| 154 |
+
{
|
| 155 |
+
"index": 0,
|
| 156 |
+
"file": "data/jaco_play/episode_000000.rrd",
|
| 157 |
+
"frames": 78
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"index": 1,
|
| 161 |
+
"file": "data/jaco_play/episode_000001.rrd",
|
| 162 |
+
"frames": 49
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"index": 2,
|
| 166 |
+
"file": "data/jaco_play/episode_000002.rrd",
|
| 167 |
+
"frames": 94
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"state_dim": 8,
|
| 171 |
+
"action_dim": 7,
|
| 172 |
+
"num_episodes_total": 976
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"slug": "kuka",
|
| 176 |
+
"repo_id": "IPEC-COMMUNITY/kuka_lerobot",
|
| 177 |
+
"robot_type": "KUKA IIWA",
|
| 178 |
+
"fps": 10,
|
| 179 |
+
"camera_keys": [
|
| 180 |
+
"observation.images.image"
|
| 181 |
+
],
|
| 182 |
+
"camera_labels": [
|
| 183 |
+
"main"
|
| 184 |
+
],
|
| 185 |
+
"episodes_available": [
|
| 186 |
+
{
|
| 187 |
+
"index": 0,
|
| 188 |
+
"file": "data/kuka/episode_000000.rrd",
|
| 189 |
+
"frames": 9
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"index": 1,
|
| 193 |
+
"file": "data/kuka/episode_000001.rrd",
|
| 194 |
+
"frames": 10
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"index": 2,
|
| 198 |
+
"file": "data/kuka/episode_000002.rrd",
|
| 199 |
+
"frames": 11
|
| 200 |
+
}
|
| 201 |
+
],
|
| 202 |
+
"state_dim": 8,
|
| 203 |
+
"action_dim": 7,
|
| 204 |
+
"num_episodes_total": 209880
|
| 205 |
+
}
|
| 206 |
+
]
|
prepare_episodes.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Offline preprocessing: download LeRobot episodes and convert to .rrd files.
|
| 3 |
+
Downloads 3 episodes each from 6 OpenX datasets covering diverse robot types.
|
| 4 |
+
Reads parquet + mp4 files directly (no lerobot dependency needed).
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import numpy as np
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import rerun as rr
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import av
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
|
| 15 |
+
DATA_DIR = Path(__file__).parent / "data"
|
| 16 |
+
|
| 17 |
+
# ---- Dataset Configuration ----
|
| 18 |
+
# chunk_size is how many episodes per chunk directory (1000 for most v2.0 datasets)
|
| 19 |
+
DATASETS = [
|
| 20 |
+
{
|
| 21 |
+
"slug": "fractal20220817_data",
|
| 22 |
+
"repo_id": "IPEC-COMMUNITY/fractal20220817_data_lerobot",
|
| 23 |
+
"robot_type": "Google Robot",
|
| 24 |
+
"episodes": [0, 1, 2],
|
| 25 |
+
"camera_keys": ["observation.images.image"],
|
| 26 |
+
"camera_labels": ["main"],
|
| 27 |
+
"fps": 3,
|
| 28 |
+
"chunk_size": 1000,
|
| 29 |
+
"state_dim": 8,
|
| 30 |
+
"action_dim": 7,
|
| 31 |
+
"num_episodes_total": 87212,
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"slug": "droid",
|
| 35 |
+
"repo_id": "IPEC-COMMUNITY/droid_lerobot",
|
| 36 |
+
"robot_type": "Franka Panda",
|
| 37 |
+
"episodes": [0, 1, 2],
|
| 38 |
+
"camera_keys": [
|
| 39 |
+
"observation.images.exterior_image_1_left",
|
| 40 |
+
"observation.images.exterior_image_2_left",
|
| 41 |
+
"observation.images.wrist_image_left",
|
| 42 |
+
],
|
| 43 |
+
"camera_labels": ["exterior_left", "exterior_right", "wrist"],
|
| 44 |
+
"fps": 15,
|
| 45 |
+
"chunk_size": 1000,
|
| 46 |
+
"state_dim": 8,
|
| 47 |
+
"action_dim": 7,
|
| 48 |
+
"num_episodes_total": 92233,
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"slug": "bridge_orig",
|
| 52 |
+
"repo_id": "IPEC-COMMUNITY/bridge_orig_lerobot",
|
| 53 |
+
"robot_type": "WidowX",
|
| 54 |
+
"episodes": [0, 1, 2],
|
| 55 |
+
"camera_keys": [
|
| 56 |
+
"observation.images.image_0",
|
| 57 |
+
"observation.images.image_1",
|
| 58 |
+
"observation.images.image_2",
|
| 59 |
+
"observation.images.image_3",
|
| 60 |
+
],
|
| 61 |
+
"camera_labels": ["cam_0", "cam_1", "cam_2", "cam_3"],
|
| 62 |
+
"fps": 5,
|
| 63 |
+
"chunk_size": 1000,
|
| 64 |
+
"state_dim": 8,
|
| 65 |
+
"action_dim": 7,
|
| 66 |
+
"num_episodes_total": 53192,
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"slug": "dobbe",
|
| 70 |
+
"repo_id": "IPEC-COMMUNITY/dobbe_lerobot",
|
| 71 |
+
"robot_type": "Hello Stretch",
|
| 72 |
+
"episodes": [0, 1, 2],
|
| 73 |
+
"camera_keys": ["observation.images.wrist_image"],
|
| 74 |
+
"camera_labels": ["wrist"],
|
| 75 |
+
"fps": 3.75,
|
| 76 |
+
"chunk_size": 1000,
|
| 77 |
+
"state_dim": 8,
|
| 78 |
+
"action_dim": 7,
|
| 79 |
+
"num_episodes_total": 5208,
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"slug": "jaco_play",
|
| 83 |
+
"repo_id": "IPEC-COMMUNITY/jaco_play_lerobot",
|
| 84 |
+
"robot_type": "Kinova Jaco",
|
| 85 |
+
"episodes": [0, 1, 2],
|
| 86 |
+
"camera_keys": [
|
| 87 |
+
"observation.images.image",
|
| 88 |
+
"observation.images.image_wrist",
|
| 89 |
+
],
|
| 90 |
+
"camera_labels": ["main", "wrist"],
|
| 91 |
+
"fps": 10,
|
| 92 |
+
"chunk_size": 1000,
|
| 93 |
+
"state_dim": 8,
|
| 94 |
+
"action_dim": 7,
|
| 95 |
+
"num_episodes_total": 976,
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"slug": "kuka",
|
| 99 |
+
"repo_id": "IPEC-COMMUNITY/kuka_lerobot",
|
| 100 |
+
"robot_type": "KUKA IIWA",
|
| 101 |
+
"episodes": [0, 1, 2],
|
| 102 |
+
"camera_keys": ["observation.images.image"],
|
| 103 |
+
"camera_labels": ["main"],
|
| 104 |
+
"fps": 10,
|
| 105 |
+
"chunk_size": 1000,
|
| 106 |
+
"state_dim": 8,
|
| 107 |
+
"action_dim": 7,
|
| 108 |
+
"num_episodes_total": 209880,
|
| 109 |
+
},
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def estimate_intrinsics(h, w):
|
| 114 |
+
"""Estimate pinhole intrinsics from image resolution."""
|
| 115 |
+
fx = fy = max(h, w) * 1.2
|
| 116 |
+
cx, cy = w / 2, h / 2
|
| 117 |
+
return np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def download_file(repo_id, path, cache_dir=None):
|
| 121 |
+
"""Download a file from HF and return local path. Returns None if not found."""
|
| 122 |
+
try:
|
| 123 |
+
return hf_hub_download(repo_id, path, repo_type="dataset", cache_dir=cache_dir)
|
| 124 |
+
except Exception:
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def read_video_frames(video_path):
|
| 129 |
+
"""Decode all frames from an MP4 file. Returns list of (H, W, C) numpy arrays."""
|
| 130 |
+
container = av.open(video_path)
|
| 131 |
+
stream = container.streams.video[0]
|
| 132 |
+
frames = []
|
| 133 |
+
for frame in container.decode(stream):
|
| 134 |
+
img = frame.to_ndarray(format="rgb24") # (H, W, 3) uint8
|
| 135 |
+
frames.append(img)
|
| 136 |
+
container.close()
|
| 137 |
+
return frames
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def process_episode(ds_config, ep_idx):
|
| 141 |
+
"""Download and convert a single episode to .rrd format.
|
| 142 |
+
Returns (rrd_path, num_frames) or (None, None) on failure."""
|
| 143 |
+
slug = ds_config["slug"]
|
| 144 |
+
repo_id = ds_config["repo_id"]
|
| 145 |
+
chunk_size = ds_config["chunk_size"]
|
| 146 |
+
camera_keys = ds_config["camera_keys"]
|
| 147 |
+
camera_labels = ds_config["camera_labels"]
|
| 148 |
+
fps = ds_config["fps"]
|
| 149 |
+
|
| 150 |
+
out_dir = DATA_DIR / slug
|
| 151 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 152 |
+
|
| 153 |
+
rrd_path = out_dir / f"episode_{ep_idx:06d}.rrd"
|
| 154 |
+
if rrd_path.exists():
|
| 155 |
+
# Get frame count from cached parquet
|
| 156 |
+
chunk = ep_idx // chunk_size
|
| 157 |
+
ep_str = f"episode_{ep_idx:06d}"
|
| 158 |
+
pq_path = f"data/chunk-{chunk:03d}/{ep_str}.parquet"
|
| 159 |
+
local_pq = download_file(repo_id, pq_path)
|
| 160 |
+
num_frames = len(pd.read_parquet(local_pq)) if local_pq else "?"
|
| 161 |
+
print(f" Episode {ep_idx}: already exists ({rrd_path.stat().st_size / 1024:.0f} KB)")
|
| 162 |
+
return rrd_path, num_frames
|
| 163 |
+
|
| 164 |
+
chunk = ep_idx // chunk_size
|
| 165 |
+
ep_str = f"episode_{ep_idx:06d}"
|
| 166 |
+
|
| 167 |
+
# Download parquet file
|
| 168 |
+
pq_path = f"data/chunk-{chunk:03d}/{ep_str}.parquet"
|
| 169 |
+
local_pq = download_file(repo_id, pq_path)
|
| 170 |
+
if local_pq is None:
|
| 171 |
+
print(f" Episode {ep_idx}: parquet not found ({pq_path})")
|
| 172 |
+
return None, None
|
| 173 |
+
|
| 174 |
+
# Read parquet data
|
| 175 |
+
df = pd.read_parquet(local_pq)
|
| 176 |
+
num_frames = len(df)
|
| 177 |
+
print(f" Episode {ep_idx}: {num_frames} frames, parquet OK, downloading videos...")
|
| 178 |
+
|
| 179 |
+
# Download and decode video for each camera
|
| 180 |
+
camera_frames = {}
|
| 181 |
+
for ck, cl in zip(camera_keys, camera_labels):
|
| 182 |
+
video_path = f"videos/chunk-{chunk:03d}/{ck}/{ep_str}.mp4"
|
| 183 |
+
local_video = download_file(repo_id, video_path)
|
| 184 |
+
if local_video is None:
|
| 185 |
+
print(f" Camera '{cl}': video not found, skipping")
|
| 186 |
+
continue
|
| 187 |
+
frames = read_video_frames(local_video)
|
| 188 |
+
camera_frames[cl] = frames
|
| 189 |
+
print(f" Camera '{cl}': {len(frames)} frames decoded")
|
| 190 |
+
|
| 191 |
+
if not camera_frames:
|
| 192 |
+
print(f" Episode {ep_idx}: no video data found, skipping")
|
| 193 |
+
return None, None
|
| 194 |
+
|
| 195 |
+
# Create .rrd recording
|
| 196 |
+
rec_id = f"{slug}_ep{ep_idx:06d}"
|
| 197 |
+
rr.init("openx_viewer", recording_id=rec_id)
|
| 198 |
+
|
| 199 |
+
# Determine camera positions (spread around the scene)
|
| 200 |
+
positions = [
|
| 201 |
+
(0.8, 0.2, 0.6),
|
| 202 |
+
(-0.8, 0.2, 0.6),
|
| 203 |
+
(0.2, 0.8, 0.4),
|
| 204 |
+
(-0.2, -0.8, 0.4),
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
# Get image resolution from first frame of first camera
|
| 208 |
+
first_cam = list(camera_frames.keys())[0]
|
| 209 |
+
first_img = camera_frames[first_cam][0]
|
| 210 |
+
h, w = first_img.shape[:2]
|
| 211 |
+
|
| 212 |
+
# Log camera setups (static)
|
| 213 |
+
for i, (cl, frames) in enumerate(camera_frames.items()):
|
| 214 |
+
pos = positions[i % len(positions)]
|
| 215 |
+
cam_path = f"cameras/{cl}"
|
| 216 |
+
rr.log(cam_path, rr.Transform3D(translation=pos), static=True)
|
| 217 |
+
rr.log(
|
| 218 |
+
f"{cam_path}/image",
|
| 219 |
+
rr.Pinhole(
|
| 220 |
+
image_from_camera=estimate_intrinsics(h, w),
|
| 221 |
+
resolution=[w, h],
|
| 222 |
+
camera_xyz=rr.ViewCoordinates.RDF,
|
| 223 |
+
image_plane_distance=0.5,
|
| 224 |
+
),
|
| 225 |
+
static=True,
|
| 226 |
+
)
|
| 227 |
+
# Note: subsequent cameras might have different resolutions but we keep same intrinsics
|
| 228 |
+
# as they're estimates anyway
|
| 229 |
+
|
| 230 |
+
# State/action labels
|
| 231 |
+
state_cols = [c for c in df.columns if c == "observation.state"]
|
| 232 |
+
action_cols = [c for c in df.columns if c == "action"]
|
| 233 |
+
state_labels = ["x", "y", "z", "rx", "ry", "rz", "rw", "gripper"]
|
| 234 |
+
action_labels = ["ax", "ay", "az", "aroll", "apitch", "ayaw", "agripper"]
|
| 235 |
+
|
| 236 |
+
# Log frames
|
| 237 |
+
n_video_frames = min(len(frames) for frames in camera_frames.values())
|
| 238 |
+
for frame_i in range(n_video_frames):
|
| 239 |
+
rr.set_time_sequence("frame", frame_i)
|
| 240 |
+
rr.set_time_seconds("time", frame_i / fps)
|
| 241 |
+
|
| 242 |
+
# Log camera images
|
| 243 |
+
for cl, frames in camera_frames.items():
|
| 244 |
+
if frame_i < len(frames):
|
| 245 |
+
rr.log(f"cameras/{cl}/image", rr.Image(frames[frame_i], color_model="RGB"))
|
| 246 |
+
|
| 247 |
+
# Log state if available and within bounds
|
| 248 |
+
if state_cols and frame_i < num_frames:
|
| 249 |
+
state = np.asarray(df.iloc[frame_i][state_cols[0]]).flatten()
|
| 250 |
+
for j, label in enumerate(state_labels):
|
| 251 |
+
if j < len(state):
|
| 252 |
+
rr.log(f"state/{label}", rr.Scalars([float(state[j])]))
|
| 253 |
+
|
| 254 |
+
# Log action if available and within bounds
|
| 255 |
+
if action_cols and frame_i < num_frames:
|
| 256 |
+
action = np.asarray(df.iloc[frame_i][action_cols[0]]).flatten()
|
| 257 |
+
for j, label in enumerate(action_labels):
|
| 258 |
+
if j < len(action):
|
| 259 |
+
rr.log(f"action/{label}", rr.Scalars([float(action[j])]))
|
| 260 |
+
|
| 261 |
+
rr.save(str(rrd_path))
|
| 262 |
+
rr.disconnect()
|
| 263 |
+
|
| 264 |
+
size_kb = rrd_path.stat().st_size / 1024
|
| 265 |
+
print(f" Episode {ep_idx}: saved {size_kb:.0f} KB")
|
| 266 |
+
return rrd_path, num_frames
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def process_dataset(ds_config):
|
| 270 |
+
"""Process all episodes for a dataset."""
|
| 271 |
+
slug = ds_config["slug"]
|
| 272 |
+
print(f"\n{'='*60}")
|
| 273 |
+
print(f"{ds_config['robot_type']} — {ds_config['repo_id']}")
|
| 274 |
+
print(f" Cameras: {ds_config['camera_labels']}")
|
| 275 |
+
print(f" Episodes: {ds_config['episodes']}")
|
| 276 |
+
|
| 277 |
+
manifest_entry = {
|
| 278 |
+
"slug": slug,
|
| 279 |
+
"repo_id": ds_config["repo_id"],
|
| 280 |
+
"robot_type": ds_config["robot_type"],
|
| 281 |
+
"fps": ds_config["fps"],
|
| 282 |
+
"camera_keys": ds_config["camera_keys"],
|
| 283 |
+
"camera_labels": ds_config["camera_labels"],
|
| 284 |
+
"state_dim": ds_config.get("state_dim", 8),
|
| 285 |
+
"action_dim": ds_config.get("action_dim", 7),
|
| 286 |
+
"num_episodes_total": ds_config.get("num_episodes_total", 0),
|
| 287 |
+
"episodes_available": [],
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
for ep_idx in ds_config["episodes"]:
|
| 291 |
+
rrd_path, num_frames = process_episode(ds_config, ep_idx)
|
| 292 |
+
if rrd_path:
|
| 293 |
+
manifest_entry["episodes_available"].append({
|
| 294 |
+
"index": ep_idx,
|
| 295 |
+
"frames": num_frames,
|
| 296 |
+
"file": f"data/{slug}/episode_{ep_idx:06d}.rrd",
|
| 297 |
+
})
|
| 298 |
+
|
| 299 |
+
return manifest_entry
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def main():
|
| 303 |
+
manifests = []
|
| 304 |
+
for ds_config in DATASETS:
|
| 305 |
+
meta = process_dataset(ds_config)
|
| 306 |
+
manifests.append(meta)
|
| 307 |
+
|
| 308 |
+
manifest_path = DATA_DIR / "manifest.json"
|
| 309 |
+
manifest_path.write_text(json.dumps(manifests, indent=2))
|
| 310 |
+
print(f"\n{'='*60}")
|
| 311 |
+
print(f"Manifest written to {manifest_path}")
|
| 312 |
+
print(f"Total datasets: {len(manifests)}")
|
| 313 |
+
total_eps = sum(len(m["episodes_available"]) for m in manifests)
|
| 314 |
+
print(f"Total episodes: {total_eps}")
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
if __name__ == "__main__":
|
| 318 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.115.0
|
| 2 |
+
uvicorn>=0.30.0
|
| 3 |
+
rerun-sdk>=0.26.0
|
| 4 |
+
huggingface-hub
|
| 5 |
+
jinja2
|
| 6 |
+
av
|
| 7 |
+
pandas
|
| 8 |
+
pyarrow
|
static/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/* Minimal additional styling - everything else is inline in index.html */
|
templates/index.html
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>OpenX Rerun Viewer</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body {
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 11 |
+
background: #0d1117;
|
| 12 |
+
color: #c9d1d9;
|
| 13 |
+
height: 100vh;
|
| 14 |
+
display: flex;
|
| 15 |
+
flex-direction: column;
|
| 16 |
+
}
|
| 17 |
+
.header {
|
| 18 |
+
padding: 12px 20px;
|
| 19 |
+
background: #161b22;
|
| 20 |
+
border-bottom: 1px solid #30363d;
|
| 21 |
+
display: flex;
|
| 22 |
+
align-items: center;
|
| 23 |
+
gap: 16px;
|
| 24 |
+
flex-shrink: 0;
|
| 25 |
+
}
|
| 26 |
+
.header h1 {
|
| 27 |
+
font-size: 18px;
|
| 28 |
+
font-weight: 600;
|
| 29 |
+
color: #f0f6fc;
|
| 30 |
+
white-space: nowrap;
|
| 31 |
+
}
|
| 32 |
+
.controls {
|
| 33 |
+
display: flex;
|
| 34 |
+
align-items: center;
|
| 35 |
+
gap: 10px;
|
| 36 |
+
flex-wrap: wrap;
|
| 37 |
+
}
|
| 38 |
+
.controls select, .controls input, .controls button {
|
| 39 |
+
padding: 6px 12px;
|
| 40 |
+
border-radius: 6px;
|
| 41 |
+
border: 1px solid #30363d;
|
| 42 |
+
background: #21262d;
|
| 43 |
+
color: #c9d1d9;
|
| 44 |
+
font-size: 13px;
|
| 45 |
+
}
|
| 46 |
+
.controls select:focus, .controls button:focus {
|
| 47 |
+
outline: none;
|
| 48 |
+
border-color: #58a6ff;
|
| 49 |
+
}
|
| 50 |
+
.controls button {
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
background: #238636;
|
| 53 |
+
border-color: #2ea043;
|
| 54 |
+
font-weight: 600;
|
| 55 |
+
}
|
| 56 |
+
.controls button:hover { background: #2ea043; }
|
| 57 |
+
.meta {
|
| 58 |
+
font-size: 12px;
|
| 59 |
+
color: #8b949e;
|
| 60 |
+
display: flex;
|
| 61 |
+
gap: 12px;
|
| 62 |
+
flex-wrap: wrap;
|
| 63 |
+
}
|
| 64 |
+
.meta span { white-space: nowrap; }
|
| 65 |
+
.viewer-container {
|
| 66 |
+
flex: 1;
|
| 67 |
+
overflow: hidden;
|
| 68 |
+
position: relative;
|
| 69 |
+
}
|
| 70 |
+
.viewer-container iframe {
|
| 71 |
+
width: 100%;
|
| 72 |
+
height: 100%;
|
| 73 |
+
border: none;
|
| 74 |
+
}
|
| 75 |
+
.placeholder {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
justify-content: center;
|
| 79 |
+
height: 100%;
|
| 80 |
+
color: #484f58;
|
| 81 |
+
font-size: 16px;
|
| 82 |
+
}
|
| 83 |
+
.placeholder p { text-align: center; line-height: 1.6; }
|
| 84 |
+
</style>
|
| 85 |
+
</head>
|
| 86 |
+
<body>
|
| 87 |
+
<div class="header">
|
| 88 |
+
<h1>OpenX Rerun Viewer</h1>
|
| 89 |
+
<div class="controls">
|
| 90 |
+
<select id="dataset-select">
|
| 91 |
+
<option value="">-- Select Dataset --</option>
|
| 92 |
+
</select>
|
| 93 |
+
<select id="episode-select" disabled>
|
| 94 |
+
<option value="">-- Select Episode --</option>
|
| 95 |
+
</select>
|
| 96 |
+
<button id="view-btn" disabled>View in Rerun</button>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="meta" id="meta-display"></div>
|
| 99 |
+
</div>
|
| 100 |
+
<div class="viewer-container">
|
| 101 |
+
<div class="placeholder" id="placeholder">
|
| 102 |
+
<p>Select a dataset and episode above to visualize in Rerun.<br>
|
| 103 |
+
<small>6 robot types · 18 episodes · OpenX-Embodiment</small></p>
|
| 104 |
+
</div>
|
| 105 |
+
<iframe id="rerun-frame" style="display:none"
|
| 106 |
+
allow="cross-origin-isolated"
|
| 107 |
+
allowfullscreen>
|
| 108 |
+
</iframe>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<script>
|
| 112 |
+
const BASE = document.querySelector('meta[content]')?.content || window.location.origin;
|
| 113 |
+
let datasets = [];
|
| 114 |
+
let currentSlug = null;
|
| 115 |
+
|
| 116 |
+
async function init() {
|
| 117 |
+
try {
|
| 118 |
+
const resp = await fetch('/api/datasets');
|
| 119 |
+
datasets = await resp.json();
|
| 120 |
+
const sel = document.getElementById('dataset-select');
|
| 121 |
+
datasets.forEach(ds => {
|
| 122 |
+
const opt = document.createElement('option');
|
| 123 |
+
opt.value = ds.slug;
|
| 124 |
+
opt.textContent = `${ds.robot_type} (${ds.episodes_available.length} eps, ${ds.fps}fps, ${ds.camera_labels.length} cam)`;
|
| 125 |
+
sel.appendChild(opt);
|
| 126 |
+
});
|
| 127 |
+
} catch (e) {
|
| 128 |
+
console.error('Failed to load datasets:', e);
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
document.getElementById('dataset-select').addEventListener('change', async (e) => {
|
| 133 |
+
const slug = e.target.value;
|
| 134 |
+
currentSlug = slug;
|
| 135 |
+
const epSel = document.getElementById('episode-select');
|
| 136 |
+
const viewBtn = document.getElementById('view-btn');
|
| 137 |
+
epSel.innerHTML = '<option value="">-- Select Episode --</option>';
|
| 138 |
+
epSel.disabled = !slug;
|
| 139 |
+
viewBtn.disabled = true;
|
| 140 |
+
|
| 141 |
+
if (!slug) {
|
| 142 |
+
document.getElementById('meta-display').innerHTML = '';
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
const ds = datasets.find(d => d.slug === slug);
|
| 147 |
+
if (!ds) return;
|
| 148 |
+
|
| 149 |
+
document.getElementById('meta-display').innerHTML =
|
| 150 |
+
`<span>Robot: <strong>${ds.robot_type}</strong></span>` +
|
| 151 |
+
`<span>FPS: ${ds.fps}</span>` +
|
| 152 |
+
`<span>Cameras: ${ds.camera_labels.join(', ')}</span>` +
|
| 153 |
+
`<span>State dim: ${ds.state_dim}</span>` +
|
| 154 |
+
`<span>Action dim: ${ds.action_dim}</span>`;
|
| 155 |
+
|
| 156 |
+
ds.episodes_available.forEach(ep => {
|
| 157 |
+
const opt = document.createElement('option');
|
| 158 |
+
opt.value = ep.index;
|
| 159 |
+
opt.textContent = `Episode ${ep.index} (${ep.frames} frames)`;
|
| 160 |
+
epSel.appendChild(opt);
|
| 161 |
+
});
|
| 162 |
+
epSel.disabled = false;
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
document.getElementById('episode-select').addEventListener('change', () => {
|
| 166 |
+
document.getElementById('view-btn').disabled =
|
| 167 |
+
!document.getElementById('episode-select').value;
|
| 168 |
+
});
|
| 169 |
+
|
| 170 |
+
document.getElementById('view-btn').addEventListener('click', () => {
|
| 171 |
+
const slug = currentSlug;
|
| 172 |
+
const epIdx = document.getElementById('episode-select').value;
|
| 173 |
+
if (!slug || epIdx === '') return;
|
| 174 |
+
|
| 175 |
+
const rrdUrl = `${window.location.origin}/data/${slug}/episode_${String(epIdx).padStart(6, '0')}.rrd`;
|
| 176 |
+
const viewerBase = '{{VIEWER_BASE}}';
|
| 177 |
+
const viewerUrl = `${viewerBase}/index.html?url=${encodeURIComponent(rrdUrl)}`;
|
| 178 |
+
|
| 179 |
+
document.getElementById('placeholder').style.display = 'none';
|
| 180 |
+
const frame = document.getElementById('rerun-frame');
|
| 181 |
+
frame.src = viewerUrl;
|
| 182 |
+
frame.style.display = 'block';
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
init();
|
| 186 |
+
</script>
|
| 187 |
+
</body>
|
| 188 |
+
</html>
|