| """ |
| RoboMind VLA — Task 1: rollout video + ground-truth label generation (MINIMAL). |
| |
| Runs ENTIRELY on Modal (CPU container, no GPU). Nothing runs locally. |
| |
| What it does |
| ------------ |
| 1. Downloads one Minari offline-RL dataset (`mujoco/humanoid/expert-v0`). |
| 2. For each of N episodes, reconstructs the simulator state from the stored |
| observations, sets it on a recovered MuJoCo env, and renders RGB frames |
| headlessly via OSMesa (software rendering -> no GPU needed). |
| 3. Writes an `.mp4` per episode plus a `metadata.jsonl` line carrying the |
| GROUND-TRUTH labels (episode return, fell flag, quality tier, #steps). |
| |
| Why reconstruct state instead of replaying actions open-loop: |
| The humanoid is chaotic; open-loop action replay from a fresh reset diverges |
| instantly and destroys the quality spectrum. Gymnasium's Humanoid observation |
| contains qpos[2:] and qvel, so we can rebuild (qpos, qvel) exactly and |
| set_state() each step -> the rendered video matches the dataset trajectory, |
| and the dataset's reward/termination labels stay valid for that video. |
| |
| Run it: |
| modal run data_gen_modal.py |
| # then inspect the volume: |
| modal volume ls robomind-data rollouts |
| |
| Done when: |
| `modal volume ls robomind-data rollouts` shows a few .mp4 files and a |
| metadata.jsonl with one line per rendered episode. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import modal |
|
|
| |
| DATASET_ID = "mujoco/humanoid/expert-v0" |
| N_EPISODES = 5 |
| RENDER_FPS = 30 |
|
|
| |
| image = ( |
| modal.Image.debian_slim(python_version="3.11") |
| .apt_install( |
| "libosmesa6", |
| "libosmesa6-dev", |
| "libgl1-mesa-glx", |
| "libglfw3", |
| "libglew2.2", |
| "patchelf", |
| "ffmpeg", |
| ) |
| .pip_install( |
| "minari[all]", |
| "gymnasium[mujoco]", |
| "mujoco>=3.1.0", |
| "imageio", |
| "imageio-ffmpeg", |
| "numpy", |
| ) |
| |
| .env({"MUJOCO_GL": "osmesa", "PYOPENGL_PLATFORM": "osmesa"}) |
| ) |
|
|
| app = modal.App("robomind-vla-data") |
|
|
| |
| volume = modal.Volume.from_name("robomind-data", create_if_missing=True) |
| OUT_DIR = "/data/rollouts" |
|
|
|
|
| @app.function(image=image, volumes={"/data": volume}, timeout=3600) |
| def generate(dataset_id: str = DATASET_ID, n_episodes: int = N_EPISODES) -> dict: |
| import json |
| import os |
|
|
| import imageio |
| import minari |
| import numpy as np |
|
|
| os.makedirs(OUT_DIR, exist_ok=True) |
|
|
| |
| tier = dataset_id.split("/")[-1].split("-v")[0] |
| env_name = dataset_id.split("/")[1] |
|
|
| print(f"[data] loading Minari dataset: {dataset_id}") |
| dataset = minari.load_dataset(dataset_id, download=True) |
| env = dataset.recover_environment(render_mode="rgb_array") |
|
|
| nq = int(env.unwrapped.model.nq) |
| nv = int(env.unwrapped.model.nv) |
| |
| |
| pos_len = nq - 2 |
| print(f"[data] env={env_name} nq={nq} nv={nv} (obs pos_len={pos_len})") |
|
|
| manifest = [] |
| written = 0 |
| for ep in dataset.iterate_episodes(): |
| if written >= n_episodes: |
| break |
|
|
| obs = np.asarray(ep.observations) |
| rewards = np.asarray(ep.rewards, dtype=float) |
| terminations = np.asarray(ep.terminations, dtype=bool) |
| truncations = np.asarray(ep.truncations, dtype=bool) |
|
|
| frames = [] |
| env.reset() |
| for t in range(obs.shape[0]): |
| o = obs[t] |
| qpos = np.concatenate([[0.0, 0.0], o[:pos_len]]) |
| qvel = o[pos_len:pos_len + nv] |
| env.unwrapped.set_state(qpos, qvel) |
| frame = env.render() |
| if frame is not None: |
| frames.append(np.asarray(frame)) |
|
|
| if not frames: |
| print(f"[data] WARNING: no frames rendered for episode {ep.id}, skipping") |
| continue |
|
|
| fell = bool(terminations.any() and not truncations.all()) |
| ep_return = float(rewards.sum()) |
| n_steps = int(rewards.shape[0]) |
|
|
| vid_name = f"{env_name}_{tier}_ep{ep.id}.mp4" |
| vid_path = os.path.join(OUT_DIR, vid_name) |
| imageio.mimwrite(vid_path, frames, fps=RENDER_FPS, macro_block_size=None) |
|
|
| record = { |
| "video": vid_name, |
| "env": env_name, |
| "tier": tier, |
| "episode_id": int(ep.id), |
| "num_steps": n_steps, |
| "return": ep_return, |
| "fell": fell, |
| } |
| manifest.append(record) |
| written += 1 |
| print(f"[data] wrote {vid_name} steps={n_steps} return={ep_return:.1f} fell={fell}") |
|
|
| |
| meta_path = os.path.join(OUT_DIR, "metadata.jsonl") |
| with open(meta_path, "a") as f: |
| for r in manifest: |
| f.write(json.dumps(r) + "\n") |
|
|
| volume.commit() |
| print(f"[data] done: {len(manifest)} episodes -> {OUT_DIR}") |
| return {"written": len(manifest), "dataset_id": dataset_id, "out_dir": OUT_DIR} |
|
|
|
|
| @app.local_entrypoint() |
| def main(): |
| result = generate.remote() |
| print("RESULT:", result) |
|
|