rudraksh Claude Opus 4.7 commited on
Commit
9468cdd
·
1 Parent(s): 49aaa1d

Add OpenX Rerun Viewer: 6 robot types, 18 episodes

Browse files

Interactive 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 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>