Spaces:
Paused
Paused
Download api/runs.py from chandrakiran06/rosdiff: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/spaces/chandrakiran06/rosdiff/resolve/main/api/runs.py
- Command line
-
hf download hf://spaces/chandrakiran06/rosdiff/api/runs.py
-
curl -L -o runs.py https://huggingface.co/spaces/chandrakiran06/rosdiff/resolve/main/api/runs.py
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 | |