rosdiff / api /runs.py
Chandrakiran-08
Deploy to HF Spaces
8501103
Raw History Blame Contribute Delete
21.8 kB
"""Report pages for jobs (simulation runs and the jobs built on them), and their signed links."""
from __future__ import annotations
import hashlib
import hmac
import html
import json
import secrets
import uuid
from .sim_backend import TERMINAL
from .storage import Storage
# Official embedding: @foxglove/embed (vendored in api/static) drives an embed.foxglove.dev iframe.
# The iframe fetches the MCAP itself, so the R2 bucket must allow CORS from https://embed.foxglove.dev.
_FOXGLOVE_SCRIPT = """
<script type="module">
import { FoxgloveViewer } from "/static/foxglove-embed/index.js";
const el = document.getElementById("foxglove");
const status = document.getElementById("foxglove-status");
const viewer = new FoxgloveViewer({
parent: el,
orgSlug: el.dataset.org || undefined,
initialDataSource: { type: "remote-file", urls: JSON.parse(el.dataset.urls) },
});
viewer.addEventListener("ready", () => {
status.textContent = "Add a 3D panel for /scene + /tf, or a Plot of /state.root_height.";
});
viewer.addEventListener("error", (e) => { status.textContent = "Foxglove viewer error: " + e.detail; });
</script>"""
_FALLBACK_SECRET = secrets.token_hex(32)
def report_token(secret: str, run_id: str) -> str:
key = (secret or _FALLBACK_SECRET).encode()
return hmac.new(key, run_id.encode(), hashlib.sha256).hexdigest()[:32]
def check_token(secret: str, run_id: str, token: str | None) -> bool:
return bool(token) and hmac.compare_digest(report_token(secret, run_id), token)
def new_run_id() -> str:
return uuid.uuid4().hex[:16]
# --------------------------------------------------------------------------- report page
_PAGE = """<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>RosDiff {title_short} {run_id}</title>
<link rel="icon" href="data:,">
{refresh}
<style>
:root {{ --fg:#1b1f24; --muted:#5b6470; --bg:#ffffff; --card:#f5f6f8; --ok:#1a7f37; --bad:#cf222e; --warn:#9a6700; }}
@media (prefers-color-scheme: dark) {{ :root {{ --fg:#e6e8eb; --muted:#9aa4af; --bg:#0f1115; --card:#191c22; }} }}
body {{ font: 15px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; color: var(--fg); background: var(--bg);
max-width: 1200px; margin: 0 auto; padding: 16px; }}
h1 {{ font-size: 20px; margin: 0 0 4px; }} .muted {{ color: var(--muted); }}
.card {{ background: var(--card); border-radius: 10px; padding: 14px 16px; margin: 14px 0; }}
.status {{ font-weight: 600; }} .COMPLETED {{ color: var(--ok); }}
.FAILED, .TIMED_OUT, .CANCELLED {{ color: var(--bad); }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 8px 16px; }}
.grid div span {{ display: block; color: var(--muted); font-size: 12px; }}
video {{ width: 100%; border-radius: 8px; background: #000; }}
.viewer {{ width: 100%; height: 640px; border-radius: 8px; overflow: hidden; margin-top: 8px; }}
a {{ color: #0969da; }} pre {{ white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }}
</style></head><body>
<h1>{title} <code>{run_id}</code></h1>
<div class="muted">{meta}</div>
<div class="card"><span class="status {status}">{status_label}</span> {status_note}</div>
{body}
</body></html>"""
def _fmt(v) -> str:
if v is None:
return "–"
if isinstance(v, bool):
return "yes" if v else "no"
return html.escape(str(v))
def render_report(
run: dict,
storage: Storage | None,
foxglove_org: str | None = None,
children: list[dict] | None = None,
link=None,
) -> str:
"""HTML report for any job. `children`: jobs made from this one; `link(job_id)`: its signed report URL."""
from .jobs import KINDS
status = run["status"]
kind = run.get("kind") or "sim"
running = status not in TERMINAL
notes = {
"SUBMITTING": "Submitting to the GPU queue…",
"IN_QUEUE": "Waiting for a GPU worker (a cold start can take a minute).",
"IN_PROGRESS": "Simulating and rendering…",
"COMPLETED": "",
"FAILED": "The run failed.",
"TIMED_OUT": "The run hit the execution time limit.",
"CANCELLED": "The run was cancelled.",
}
body = []
output = run.get("output") or {}
summary = output.get("summary") or {}
files = output.get("files") or {}
if run.get("error"):
body.append(f'<div class="card"><b>Error</b><pre>{html.escape(run["error"])}</pre></div>')
if kind != "sim":
body.extend(_kind_body(kind, run, storage))
if kind == "sim" and status == "COMPLETED" and summary:
cells = [
("Simulated", f"{summary.get('simulated_seconds')} s"),
("Robot fell", summary.get("fell")),
("Fell at", f"{summary['fell_at']} s" if summary.get("fell_at") is not None else None),
(
"Root height min / final",
f"{summary.get('root_min_height')} / {summary.get('root_final_height')} m"
if "root_min_height" in summary
else None,
),
("Unstable at", summary.get("unstable_at")),
("Max contacts", summary.get("max_contacts")),
("Real-time factor", summary.get("realtime_factor")),
("GPU time", f"{run['execution_ms'] / 1000:.1f} s" if run.get("execution_ms") else None),
]
grid = "".join(f"<div><span>{html.escape(k)}</span>{_fmt(v)}</div>" for k, v in cells)
warn = summary.get("warnings") or []
extra = f'<p class="muted">MuJoCo warnings: {html.escape(", ".join(warn))}</p>' if warn else ""
body.append(f'<div class="card"><div class="grid">{grid}</div>{extra}</div>')
if kind == "sim" and status == "COMPLETED" and storage is not None:
videos = [(files[k], mime) for k, mime in (("video", "video/mp4"), ("video_webm", "video/webm")) if k in files]
if videos:
sources = "".join(f'<source src="{html.escape(storage.url(key))}" type="{mime}">' for key, mime in videos)
body.append(
f'<div class="card"><b>Video</b><video controls autoplay muted loop playsinline>{sources}</video></div>'
)
elif summary.get("render_error"):
body.append(f'<div class="card muted">No video: {html.escape(summary["render_error"])}</div>')
if "mcap" in files:
mcap_url = storage.url(files["mcap"])
body.append(
'<div class="card"><b>3D replay (Foxglove)</b> '
f'<span class="muted">— topics /scene, /tf, /state · <a href="{html.escape(mcap_url)}">download '
".mcap</a> (opens in any Foxglove app)</span>"
f'<div id="foxglove" class="viewer" data-urls="{html.escape(json.dumps([mcap_url]))}" '
f'data-org="{html.escape(foxglove_org or "")}"></div>'
'<p id="foxglove-status" class="muted">Loading the Foxglove viewer… it asks you to sign in to a '
"(free) Foxglove account the first time.</p></div>" + _FOXGLOVE_SCRIPT
)
links = [f'<a href="{html.escape(storage.url(files[k]))}">{k}</a>' for k in ("scene", "summary") if k in files]
if links:
body.append(f'<div class="card muted">Files: {" · ".join(links)} (links valid for 7 days)</div>')
if children:
rows = []
for c in children:
label = KINDS[c["kind"]].label if c.get("kind") in KINDS else (c.get("kind") or "unknown")
href = link(c["id"]) if link else None
name = f'<a href="{html.escape(href)}">{html.escape(label)}</a>' if href else html.escape(label)
verdict = _child_note(c)
rows.append(
f'<div>{name} <code>{html.escape(c["id"])}</code> · <span class="status {html.escape(c.get("status", "UNKNOWN"))}">'
f"{html.escape((c.get('status') or 'UNKNOWN').replace('_', ' ').title())}</span>{verdict}</div>"
)
body.append('<div class="card"><b>Follow-up jobs</b>' + "".join(rows) + "</div>")
if kind == "sim":
meta = (
f"{_fmt(run.get('robot') or 'no robot')} · {run.get('duration_s') or 0:g} s · controller: "
f"{_fmt(run.get('controller'))} · started {_fmt(run['created_at'][:19])} UTC"
)
else:
parent = f"from run <code>{html.escape(run['parent_id'])}</code> · " if run.get("parent_id") else ""
meta = f"{parent}started {_fmt(run['created_at'][:19])} UTC"
if run.get("cost_usd") is not None:
meta += f" · est. cost ${run['cost_usd']:.3f}"
title = KINDS[kind].label if kind in KINDS else kind
return _PAGE.format(
run_id=html.escape(run["id"]),
title=html.escape(title),
title_short=html.escape(kind),
meta=meta,
refresh='<meta http-equiv="refresh" content="10">' if running else "",
status=html.escape(status),
status_label=html.escape(status.replace("_", " ").title()),
status_note=html.escape(notes.get(status, "")) + (" This page refreshes every 10 s." if running else ""),
body="\n".join(body),
)
# --------------------------------------------------------------------------- other job kinds
# kind -> function(job, storage) -> list of HTML cards; registered by the modules that add each kind.
KIND_BODIES: dict = {}
def _kind_body(kind: str, job: dict, storage: Storage | None) -> list[str]:
if job["status"] != "COMPLETED":
return []
if kind in KIND_BODIES:
return KIND_BODIES[kind](job, storage)
output = job.get("output") or {}
cards = []
summary = output.get("summary") or {}
if summary:
cells = "".join(
f"<div><span>{html.escape(str(k))}</span>{_fmt(v)}</div>"
for k, v in summary.items()
if not isinstance(v, (dict, list))
)
cards.append(f'<div class="card"><div class="grid">{cells}</div></div>')
cards.extend(file_links(output.get("files") or {}, storage))
return cards
def file_links(files: dict, storage: Storage | None) -> list[str]:
if not files or storage is None:
return []
links = [f'<a href="{html.escape(storage.url(key))}">{html.escape(name)}</a>' for name, key in files.items()]
return [f'<div class="card muted">Files: {" · ".join(links)} (links valid for 7 days)</div>']
def _child_note(job: dict) -> str:
"""One-line result shown next to a follow-up job on its parent's page."""
note = ((job.get("output") or {}).get("summary") or {}).get("headline")
return f" — {html.escape(str(note))}" if note else ""
def _badge(ok: bool | None, yes: str, no: str) -> str:
if ok is None:
return '<span class="muted">not run</span>'
return f'<span class="status {"COMPLETED" if ok else "FAILED"}">{html.escape(yes if ok else no)}</span>'
def _list(items, limit: int = 30) -> str:
items = list(items or [])
if not items:
return '<span class="muted">none</span>'
more = f" … and {len(items) - limit} more" if len(items) > limit else ""
return "<code>" + "</code>, <code>".join(html.escape(str(i)) for i in items[:limit]) + "</code>" + more
def _ros_check_body(job: dict, storage: Storage | None) -> list[str]:
output = job.get("output") or {}
r = output.get("summary") or {}
files = output.get("files") or {}
b, la, te, li = r.get("build") or {}, r.get("launch"), r.get("test"), r.get("lint") or {}
cards = [f'<div class="card"><b>{html.escape(r.get("headline", ""))}</b></div>']
cells = [
("ROS 2", r.get("distro")),
("Packages", ", ".join(r.get("packages") or [])),
("colcon build", _badge(b.get("ok"), "builds", "fails")),
("Launch (10 s)", _badge(la.get("ok") if la else None, "runs", "fails")),
(
"Tests",
_badge(te.get("ok"), f"{te.get('tests')} passed", f"{te.get('failures', 0) + te.get('errors', 0)} failing")
if te and te.get("ran")
else '<span class="muted">none in package</span>',
),
("Lint issues", li.get("total", 0)),
]
grid = "".join(
f"<div><span>{html.escape(k)}</span>{v if isinstance(v, str) and v.startswith('<') else _fmt(v)}</div>"
for k, v in cells
)
cards.append(f'<div class="card"><div class="grid">{grid}</div></div>')
problems = []
if b.get("errors"):
problems.append("<b>Build errors</b><pre>" + html.escape("\n".join(b["errors"])) + "</pre>")
missing = (b.get("missing_dependencies") or []) + ((la or {}).get("missing_dependencies") or [])
if missing:
problems.append(
f"<p><b>Missing dependencies:</b> {_list(dict.fromkeys(missing))} — not installed in the sandbox, or "
"not declared in package.xml.</p>"
)
if la and la.get("crashed"):
problems.append(f"<p><b>Crashed:</b> {_list(la['crashed'])}</p>")
if la and la.get("errors"):
problems.append("<b>Runtime errors</b><pre>" + html.escape("\n".join(la["errors"])) + "</pre>")
if li.get("issues"):
issues = ", ".join(f"{k}: {v}" for k, v in li["issues"].items() if v)
if issues:
problems.append(f'<p class="muted">Lint: {html.escape(issues)} (see lint.log)</p>')
if problems:
cards.append('<div class="card">' + "".join(problems) + "</div>")
if la:
cards.append(
f'<div class="card"><b>While running</b><p>Started: {_list(la.get("started"))}</p>'
f"<p>Nodes: {_list(la.get('nodes'))}</p><p>Topics: {_list(la.get('topics'))}</p></div>"
)
if storage is not None and "bag.mcap" in files:
url = storage.url(files["bag.mcap"])
cards.append(
'<div class="card"><b>Recorded topics (Foxglove)</b> <span class="muted">— every topic during the '
f'launch, recorded with ros2 bag · <a href="{html.escape(url)}">download .mcap</a></span>'
f'<div id="foxglove" class="viewer" data-urls="{html.escape(json.dumps([url]))}" data-org=""></div>'
'<p id="foxglove-status" class="muted">Loading the Foxglove viewer…</p></div>' + _FOXGLOVE_SCRIPT
)
cards.extend(file_links({k: v for k, v in files.items() if k != "bag.mcap"}, storage))
return cards
KIND_BODIES["ros_check"] = _ros_check_body
def _sweep_body(job: dict, storage: Storage | None) -> list[str]:
output = job.get("output") or {}
s = output.get("summary") or {}
files = output.get("files") or {}
cards = [f'<div class="card"><b>{html.escape(s.get("headline", ""))}</b></div>']
r = s.get("ranges") or {}
cells = [
("Worlds", s.get("worlds")),
("Engine", s.get("engine")),
("Simulated each", f"{s.get('simulated_seconds_each')} s"),
("Controller", s.get("controller")),
("Fall rate", f"{100 * s['fall_rate']:.0f}%" if "fall_rate" in s else None),
("Largest push survived", f"{s['max_push_n_survived']} N" if "max_push_n_survived" in s else None),
("Friction scale", "–".join(map(str, r.get("friction_scale", [])))),
("Object mass scale", "–".join(map(str, r.get("object_mass_scale", [])))),
(
"Push",
f"0–{(r.get('push_newtons') or [0, 0])[1]} N for {r.get('push_duration_s')} s at {r.get('push_at_s')} s",
),
("Compute time", f"{s.get('wall_seconds')} s"),
]
grid = "".join(f"<div><span>{html.escape(k)}</span>{_fmt(v)}</div>" for k, v in cells)
note = f'<p class="muted">{html.escape(s["engine_note"])}</p>' if s.get("engine_note") else ""
cards.append(f'<div class="card"><div class="grid">{grid}</div>{note}</div>')
worlds = []
if storage is not None and "sweep.json" in files:
try:
worlds = json.loads(storage.read_text(files["sweep.json"]) or "{}").get("worlds", [])
except (ValueError, AttributeError):
worlds = []
if worlds and "fall_rate" in s:
cards.append(
'<div class="card"><b>Push vs. friction</b> <span class="muted">— each dot is one world; '
"red fell, green stayed up</span>" + _scatter(worlds) + "</div>"
)
cards.extend(file_links(files, storage))
return cards
def _scatter(worlds: list[dict], w: int = 640, h: int = 280) -> str:
pad = 40
pushes = [x["push_n"] for x in worlds] or [1]
fr = [x["friction"] for x in worlds] or [1]
px_max, f_min, f_max = max(max(pushes), 1e-6), min(fr), max(max(fr), min(fr) + 1e-6)
dots = []
for x in worlds:
cx = pad + (w - 2 * pad) * x["push_n"] / px_max
cy = h - pad - (h - 2 * pad) * (x["friction"] - f_min) / (f_max - f_min)
color = "var(--bad)" if x.get("fell") else "var(--ok)"
tip = f"push {x['push_n']} N, friction x{x['friction']}, mass x{x['object_mass']}"
dots.append(
f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="4" fill="{color}"><title>{html.escape(tip)}</title></circle>'
)
axes = (
f'<line x1="{pad}" y1="{h - pad}" x2="{w - pad}" y2="{h - pad}" stroke="currentColor" opacity=".4"/>'
f'<line x1="{pad}" y1="{pad}" x2="{pad}" y2="{h - pad}" stroke="currentColor" opacity=".4"/>'
f'<text x="{w / 2}" y="{h - 8}" text-anchor="middle" font-size="12" fill="currentColor">'
f"push (N), 0–{px_max:g}</text>"
f'<text x="12" y="{h / 2}" font-size="12" fill="currentColor" transform="rotate(-90 12 {h / 2})" '
f'text-anchor="middle">friction ×{f_min:.2f}–{f_max:.2f}</text>'
)
return f'<svg viewBox="0 0 {w} {h}" style="width:100%;max-width:{w}px;height:auto">{axes}{"".join(dots)}</svg>'
KIND_BODIES["sweep"] = _sweep_body
def _review_body(job: dict, storage: Storage | None) -> list[str]:
r = (job.get("output") or {}).get("summary") or {}
cards = [
f'<div class="card"><b>{html.escape(r.get("headline", ""))}</b>'
+ (f"<p>{html.escape(r['summary'])}</p>" if r.get("summary") else "")
+ "</div>"
]
cells = [
("Matches the description", r.get("matches_scenario")),
("Physics plausible", r.get("physics_plausible")),
("Robot fell", r.get("robot_fell")),
("Frames watched", r.get("frames")),
("Model", r.get("model")),
]
grid = "".join(f"<div><span>{html.escape(k)}</span>{_fmt(v)}</div>" for k, v in cells)
cards.append(f'<div class="card"><div class="grid">{grid}</div></div>')
if r.get("issues"):
items = "".join(f"<li>{html.escape(i)}</li>" for i in r["issues"])
cards.append(f'<div class="card"><b>Issues it saw</b><ul>{items}</ul></div>')
if r.get("reasoning"):
cards.append(
f'<div class="card"><details><summary>Model reasoning</summary><pre>{html.escape(r["reasoning"])}</pre>'
"</details></div>"
)
if r.get("raw_answer"):
cards.append(f'<div class="card"><b>Raw answer</b><pre>{html.escape(r["raw_answer"])}</pre></div>')
cards.append(
'<div class="card muted">An automatic first pass by NVIDIA Cosmos Reason, to point reviewers at runs worth a '
"look. It can be wrong: the video and the numbers on the run's own report are what count.</div>"
)
return cards
KIND_BODIES["review"] = _review_body
def _export_body(job: dict, storage: Storage | None) -> list[str]:
output = job.get("output") or {}
r = output.get("summary") or {}
files = output.get("files") or {}
link = ""
if r.get("hub_url"):
link = f' · <a href="{html.escape(r["hub_url"])}">open on Hugging Face</a>'
cards = [f'<div class="card"><b>{html.escape(r.get("headline", ""))}</b>{link}</div>']
cells = [
("Dataset", r.get("repo_id")),
("Episodes", r.get("episodes")),
("Frames", r.get("frames")),
("FPS", r.get("fps")),
("Runs", ", ".join(r.get("runs") or [])),
]
grid = "".join(f"<div><span>{html.escape(k)}</span>{_fmt(v)}</div>" for k, v in cells)
cards.append(f'<div class="card"><div class="grid">{grid}</div></div>')
repo = html.escape(r.get("repo_id", "<repo_id>"))
load = f'LeRobotDataset("{repo}")' if r.get("hub_url") else f'LeRobotDataset("{repo}", root="path/to/unzipped")'
cards.append(
'<div class="card"><b>Use it</b><pre>pip install lerobot\n'
f"from lerobot.datasets.lerobot_dataset import LeRobotDataset\nds = {load}\n"
"ds[0].keys() # observation.state, observation.velocity, action, observation.images.main, task</pre></div>"
)
cards.extend(file_links(files, storage))
return cards
KIND_BODIES["export"] = _export_body
def _isaac_body(job: dict, storage: Storage | None) -> list[str]:
output = job.get("output") or {}
r = output.get("summary") or {}
files = output.get("files") or {}
cards = [f'<div class="card"><b>{html.escape(r.get("headline", ""))}</b></div>']
if storage is not None and "isaac.mp4" in files:
url = html.escape(storage.url(files["isaac.mp4"]))
cards.append(
f'<div class="card"><b>Isaac Sim render</b><video controls autoplay muted loop playsinline>'
f'<source src="{url}" type="video/mp4"></video></div>'
)
cells = [("Renderer", r.get("renderer")), ("Frames", r.get("frames")), ("Length", f"{r.get('seconds')} s")]
grid = "".join(f"<div><span>{html.escape(k)}</span>{_fmt(v)}</div>" for k, v in cells)
cards.append(f'<div class="card"><div class="grid">{grid}</div></div>')
cards.append(
'<div class="card muted">A replay: MuJoCo simulated the run, Isaac Sim draws the same motion with RTX '
"lighting (no second physics engine), so the two videos show the same thing.</div>"
)
return cards
KIND_BODIES["isaac"] = _isaac_body