openx-viewer / app.py
rudraksh
Add OpenX Rerun Viewer: 6 robot types, 18 episodes
9468cdd
"""
FastAPI server for the OpenX Rerun Viewer HF Space.
Runs the selection UI and serves .rrd files.
When running locally, also starts a local Rerun web viewer to avoid mixed-content issues.
"""
import json
import os
import subprocess
import sys
import time
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="OpenX Rerun Viewer")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "HEAD", "OPTIONS"],
allow_headers=["*"],
expose_headers=[
"Content-Length", "Content-Type", "Accept-Ranges",
"Cross-Origin-Resource-Policy",
],
)
DATA_DIR = Path(__file__).parent / "data"
MANIFEST_PATH = DATA_DIR / "manifest.json"
IS_LOCAL = "SPACE_ID" not in os.environ and "HF_SPACE" not in os.environ
def load_manifest():
if MANIFEST_PATH.exists():
return json.loads(MANIFEST_PATH.read_text())
return []
@app.get("/", response_class=HTMLResponse)
async def index():
html = (Path(__file__).parent / "templates" / "index.html").read_text()
# Inject the viewer base URL: use local viewer for dev, app.rerun.io for production
if IS_LOCAL:
viewer_base = "http://localhost:9090"
else:
viewer_base = "https://app.rerun.io/version/0.26.2"
html = html.replace("{{VIEWER_BASE}}", viewer_base)
return HTMLResponse(html)
@app.get("/api/datasets")
async def list_datasets():
manifest = load_manifest()
return [
{
"slug": m["slug"],
"robot_type": m["robot_type"],
"fps": m.get("fps", 0),
"camera_keys": m.get("camera_keys", []),
"camera_labels": m.get("camera_labels", []),
"num_episodes_total": m.get("num_episodes_total", 0),
"state_dim": m.get("state_dim", 0),
"action_dim": m.get("action_dim", 0),
"episodes_available": [
{"index": ep["index"], "frames": ep.get("frames", "?")}
for ep in m.get("episodes_available", [])
],
}
for m in manifest
]
@app.get("/api/datasets/{slug}/episodes")
async def list_episodes(slug: str):
manifest = load_manifest()
for m in manifest:
if m["slug"] == slug:
return m["episodes_available"]
return []
@app.api_route("/data/{slug}/episode_{index:str}.rrd", methods=["GET", "HEAD"])
async def serve_rrd(slug: str, index: str):
filepath = DATA_DIR / slug / f"episode_{index}.rrd"
if not filepath.exists():
return HTMLResponse(status_code=404, content="Episode not found")
return FileResponse(
filepath,
media_type="application/octet-stream",
headers={
"Accept-Ranges": "bytes",
"Access-Control-Allow-Origin": "*",
"Cross-Origin-Resource-Policy": "cross-origin",
"Cache-Control": "public, max-age=3600",
},
)
static_dir = Path(__file__).parent / "static"
if static_dir.exists():
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
def start_local_viewer():
"""Start Rerun web viewer on port 9090 for local development."""
import rerun as rr
rr.serve_web_viewer(web_port=9090, open_browser=False)
# serve_web_viewer returns immediately after starting the server in a thread
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8000))
if IS_LOCAL:
print("Local mode: starting Rerun web viewer on http://localhost:9090")
start_local_viewer()
time.sleep(0.5)
uvicorn.run(app, host="0.0.0.0", port=port)