"""DynamicVLA (hzxie/dynamic-vla-DOM) action-chunk demo.""" from __future__ import annotations import importlib.util import json import os import subprocess import sys import time from pathlib import Path os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") import spaces # noqa: E402 — must precede any CUDA-touching import if importlib.util.find_spec("lerobot") is None: subprocess.check_call( [sys.executable, "-m", "pip", "install", "--no-deps", "lerobot==0.3.3"] ) import numpy as np # noqa: E402 import pandas as pd # noqa: E402 import plotly.graph_objects as go # noqa: E402 import torch # noqa: E402 from huggingface_hub import snapshot_download # noqa: E402 from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature # noqa: E402 from PIL import Image # noqa: E402 from policies.dynamicvla.configuration_dynamicvla import DynamicVLAConfig # noqa: E402 from policies.dynamicvla.modeling_dynamicvla import ( # noqa: E402 DynamicVLAPolicy, load_dynamicvla, ) import gradio as gr # noqa: E402 MODEL_ID = "hzxie/dynamic-vla-DOM" IMG_H, IMG_W = 360, 480 N_OBS = 2 ACTION_COLS = ["x", "y", "z", "roll", "pitch", "yaw", "gripper"] ROOT = Path(__file__).resolve().parent EXAMPLES = ROOT / "examples" FEATURE_TYPES = { "STATE": FeatureType.STATE, "VISUAL": FeatureType.VISUAL, "ACTION": FeatureType.ACTION, } def _features(spec: dict) -> dict: return { key: PolicyFeature(type=FEATURE_TYPES[ft["type"]], shape=tuple(ft["shape"])) for key, ft in spec.items() } def _identity_stats(cfg: DynamicVLAConfig) -> dict[str, dict[str, torch.Tensor]]: """MEAN_STD with mean=0 / std=1 is a passthrough. Checkpoint strips norm buffers.""" stats: dict[str, dict[str, torch.Tensor]] = {} for name, feat in {**cfg.input_features, **cfg.output_features}.items(): if feat.type in (FeatureType.STATE, FeatureType.ACTION): shape = tuple(feat.shape) stats[name] = { "mean": torch.zeros(shape, dtype=torch.float32), "std": torch.ones(shape, dtype=torch.float32), "min": torch.full(shape, -1.0, dtype=torch.float32), "max": torch.ones(shape, dtype=torch.float32), } return stats def _build_config(ckpt_dir: str) -> DynamicVLAConfig: with open(os.path.join(ckpt_dir, "config.json"), encoding="utf-8") as fh: raw = json.load(fh) device = "cuda" if torch.cuda.is_available() else "cpu" cfg = DynamicVLAConfig( input_features=_features(raw["input_features"]), output_features=_features(raw["output_features"]), device=device, ) skip = {"type", "device", "input_features", "output_features"} for key, value in raw.items(): attr = key.lower() if attr in skip or value is None or not hasattr(cfg, attr): continue if attr == "normalization_mapping" and isinstance(value, dict): value = { k: (NormalizationMode[v] if isinstance(v, str) else v) for k, v in value.items() } setattr(cfg, attr, value) cfg.enable_streaming = False return cfg def load_policy() -> DynamicVLAPolicy: ckpt_dir = snapshot_download(MODEL_ID) cfg = _build_config(ckpt_dir) policy = DynamicVLAPolicy(cfg, dataset_stats=_identity_stats(cfg)) load_dynamicvla( policy, os.path.join(ckpt_dir, "model.safetensors"), device="cpu", checkpoint_keys_mapping="model._orig_mod.//model.", ) policy.eval() return policy.to("cuda") POLICY = load_policy() def _as_image(img) -> Image.Image | None: if img is None: return None if isinstance(img, Image.Image): return img.convert("RGB") if isinstance(img, np.ndarray): if img.ndim == 3 and img.shape[-1] == 4: img = img[..., :3] if img.dtype != np.uint8: img = np.clip(img, 0, 255).astype(np.uint8) if img.max() > 1.5 else ( np.clip(img * 255.0, 0, 255).astype(np.uint8) ) return Image.fromarray(img).convert("RGB") return Image.open(img).convert("RGB") def _to_nchw(img: Image.Image) -> torch.Tensor: resized = img.resize((IMG_W, IMG_H), Image.BILINEAR) arr = np.asarray(resized, dtype=np.float32) / 255.0 return torch.from_numpy(arr).permute(2, 0, 1) def _stack_obs(current: Image.Image, previous: Image.Image | None) -> torch.Tensor: cur = _to_nchw(current) prev = _to_nchw(previous) if previous is not None else cur return torch.stack([prev, cur], dim=0) # (n_obs, C, H, W) def _plot_path(actions: np.ndarray) -> go.Figure: xs, ys, zs = actions[:, 0], actions[:, 1], actions[:, 2] fig = go.Figure( data=[ go.Scatter3d( x=xs, y=ys, z=zs, mode="lines+markers", marker={"size": 4, "color": np.arange(len(xs)), "colorscale": "Viridis"}, line={"width": 5, "color": "#2ecc71"}, name="EE path", ), go.Scatter3d( x=[xs[0]], y=[ys[0]], z=[zs[0]], mode="markers", marker={"size": 8, "color": "#27ae60"}, name="start", ), go.Scatter3d( x=[xs[-1]], y=[ys[-1]], z=[zs[-1]], mode="markers", marker={"size": 8, "color": "#e74c3c"}, name="end", ), ] ) fig.update_layout( template="plotly_dark", height=420, margin={"l": 0, "r": 0, "t": 30, "b": 0}, scene={ "xaxis_title": "x (m)", "yaxis_title": "y (m)", "zaxis_title": "z (m)", "aspectmode": "data", }, title="Predicted 20-step end-effector chunk", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", legend={"orientation": "h"}, ) return fig def _summarize(actions: np.ndarray, elapsed: float, instruction: str) -> str: delta = actions[-1, :3] - actions[0, :3] grip = actions[:, -1] return ( f"**Instruction:** {instruction.strip()}\n\n" f"**Chunk:** {len(actions)} steps · **{elapsed:.2f}s** GPU\n\n" f"- Start xyz: `{actions[0, :3].round(4).tolist()}`\n" f"- End xyz: `{actions[-1, :3].round(4).tolist()}`\n" f"- Net Δxyz: `{delta.round(4).tolist()}`\n" f"- Gripper: min `{grip.min():.3f}` → max `{grip.max():.3f}` " f"(last `{grip[-1]:.3f}`)\n" f"- Rotation (last rpy): `{actions[-1, 3:6].round(4).tolist()}`" ) @spaces.GPU(duration=60) def predict_action_chunk( wrist: np.ndarray | Image.Image | None, opposite: np.ndarray | Image.Image | None, wrist_prev: np.ndarray | Image.Image | None, opposite_prev: np.ndarray | Image.Image | None, instruction: str, x: float, y: float, z: float, roll: float, pitch: float, yaw: float, apply_delta: bool, ) -> tuple[pd.DataFrame, go.Figure, str]: """Predict a 20-step DynamicVLA action chunk from dual-camera frames.""" if not instruction or not instruction.strip(): raise gr.Error("Provide a language instruction.") wrist_img = _as_image(wrist) opp_img = _as_image(opposite) if wrist_img is None or opp_img is None: raise gr.Error("Upload both wrist and opposite camera frames.") device = "cuda" wrist_t = _stack_obs(wrist_img, _as_image(wrist_prev)).unsqueeze(0).to(device) opp_t = _stack_obs(opp_img, _as_image(opposite_prev)).unsqueeze(0).to(device) state = torch.tensor( [[[x, y, z, roll, pitch, yaw]] * N_OBS], dtype=torch.float32, device=device ) batch = { "observation.images.wrist_cam": wrist_t, "observation.images.opst_cam": opp_t, "observation.state": state, "task": [instruction.strip()], } POLICY.reset() tick = time.perf_counter() with torch.inference_mode(): actions = POLICY.predict_action_chunk(batch) if apply_delta and getattr(POLICY.config, "use_delta_action", True): actions = actions.clone() actions[..., :6] = actions[..., :6] + state[:, -1:, :6] elapsed = time.perf_counter() - tick acts = actions[0].detach().float().cpu().numpy() table = pd.DataFrame(acts, columns=ACTION_COLS) table.insert(0, "step", np.arange(len(table))) return table, _plot_path(acts), _summarize(acts, elapsed, instruction) def _example_row(stem: str, instruction: str) -> list: img = str(EXAMPLES / f"{stem}.png") return [img, img, None, None, instruction, 0.40, 0.00, 0.30, 0.0, 0.0, 0.0, True] GALLERY_MD = """ ## DynamicVLA on DOM **DynamicVLA** (0.4B, SmolLM2-360M + FastViT) is a VLA for *moving* objects. It adds **Continuous Inference** and **Latent-aware Action Streaming** so the policy does not freeze between action chunks. This Space runs the official [`hzxie/dynamic-vla-DOM`](https://huggingface.co/hzxie/dynamic-vla-DOM) checkpoint and predicts a **20-step** 7-DoF end-effector chunk (`[x, y, z, roll, pitch, yaw, gripper]`). Closed-loop Isaac Lab eval is *not* hosted here — use [hzxie/DynamicVLA](https://github.com/hzxie/DynamicVLA) for that. | | | |---|---| | Paper | [arXiv:2601.22153](https://arxiv.org/abs/2601.22153) | | Dataset | [`hzxie/DOM`](https://huggingface.co/datasets/hzxie/DOM) — 200K episodes, 2.8K scenes, 206 objects | | Weights | [`hzxie/dynamic-vla-DOM`](https://huggingface.co/hzxie/dynamic-vla-DOM) | | Project | [infinitescript.com/project/dynamic-vla](https://www.infinitescript.com/project/dynamic-vla/) | | Spotlight | [YouTube](https://youtu.be/NmJnHcI04_Q) | """ def build_ui() -> gr.Blocks: with gr.Blocks(title="DynamicVLA · DOM") as demo: gr.Markdown( "# DynamicVLA — DOM action-chunk demo\n" "0.4B VLA for dynamic object manipulation. " "Upload wrist + scene cameras, write an instruction, get a 20-step EE chunk." ) with gr.Tabs(): with gr.Tab("Predict"): with gr.Row(): with gr.Column(): wrist = gr.Image(label="Wrist camera (current)", type="numpy") opposite = gr.Image( label="Opposite / scene camera (current)", type="numpy" ) with gr.Accordion("Previous frames (optional, n_obs=2)", open=False): wrist_prev = gr.Image( label="Wrist camera (t−1)", type="numpy" ) opposite_prev = gr.Image( label="Opposite camera (t−1)", type="numpy" ) instruction = gr.Textbox( label="Language instruction", placeholder="Pick up the rolling cylinder and place it onto the wooden block.", lines=2, ) with gr.Accordion("Current EE state (meters / rad)", open=False): with gr.Row(): x = gr.Number(value=0.40, label="x") y = gr.Number(value=0.00, label="y") z = gr.Number(value=0.30, label="z") with gr.Row(): roll = gr.Number(value=0.0, label="roll") pitch = gr.Number(value=0.0, label="pitch") yaw = gr.Number(value=0.0, label="yaw") apply_delta = gr.Checkbox( value=True, label="Add delta actions to current EE state", ) run = gr.Button("Predict action chunk", variant="primary") with gr.Column(): summary = gr.Markdown("Upload both views and run.") path = gr.Plot(label="EE trajectory") table = gr.Dataframe(label="Action chunk (20 × 7)") inputs = [ wrist, opposite, wrist_prev, opposite_prev, instruction, x, y, z, roll, pitch, yaw, apply_delta, ] run.click( fn=predict_action_chunk, inputs=inputs, outputs=[table, path, summary], ) gr.Examples( examples=[ _example_row( "franka-coffee", "Pick up the rolling cylinder and place it onto the wooden block.", ), _example_row( "piper-sesame", "Grasp the rolling roasted sesame container and place it onto the blue frisbee.", ), _example_row( "franka-tennis", "Get hold of the moving tennis ball and position it into the paper bowl.", ), ], inputs=inputs, outputs=[table, path, summary], fn=predict_action_chunk, label="DOM-style prompts (official comparison stills as both views)", cache_examples=True, cache_mode="lazy", ) with gr.Tab("About"): gr.Markdown(GALLERY_MD) if (EXAMPLES / "teaser.webp").exists(): gr.Image( value=str(EXAMPLES / "teaser.webp"), label="Official teaser", interactive=False, ) gr.Markdown( "```bibtex\n" "@article{xie2026dynamicvla,\n" " title = {DynamicVLA: A Vision-Language-Action Model for Dynamic Object Manipulation},\n" " author = {Xie, Haozhe and Wen, Beichen and Zheng, Jiarui and Chen, Zhaoxi\n" " and Hong, Fangzhou and Diao, Haiwen and Liu, Ziwei},\n" " journal = {arXiv preprint arXiv:2601.22153},\n" " year = {2026}\n" "}\n" "```" ) return demo demo = build_ui() if __name__ == "__main__": demo.launch( mcp_server=True, theme=gr.themes.Soft(primary_hue="green", neutral_hue="zinc"), )