#!/usr/bin/env python3 """3D HAMSTER — 3D Trajectory Prediction demo (ZeroGPU). Depth-aware VLM planner: predicts metric 3D end-effector trajectories (and 2D trajectories / pointing / bbox / VQA) from a single RGB image + metric depth map + a language instruction. Adapted from the reference Gradio script in the official repo (scripts/trajectory_prediction_gradio.py) to run on ZeroGPU: the model is loaded once at module scope onto CUDA and inference is wrapped in @spaces.GPU. """ import os # DINOv2 geometry encoder uses xformers when available; force the pure-torch # fallback so we don't need an xformers CUDA build on Blackwell. os.environ.setdefault("XFORMERS_DISABLED", "1") import spaces # noqa: E402 MUST come before torch / transformers import json # noqa: E402 import re # noqa: E402 import tempfile # noqa: E402 from pathlib import Path # noqa: E402 from typing import Optional # noqa: E402 import cv2 # noqa: E402 import gradio as gr # noqa: E402 import numpy as np # noqa: E402 import plotly.graph_objects as go # noqa: E402 import torch # noqa: E402 from PIL import Image # noqa: E402 from huggingface_hub import snapshot_download # noqa: E402 # ── Constants (mirror the reference script) ────────────────────────────────── MODEL_ID = "DAVIAN-Robotics/3D_HAMSTER" HERE = Path(__file__).parent.resolve() EXAMPLES_DIR = str(HERE / "examples") TARGET_SIZE = 640 # training resolution: longest edge = 640 V5_SYSTEM_PROMPT = "" VQA_STYLE = "General VQA" BBOX_STYLE = "2D Bounding Box" V5_PROMPT_SUFFIXES = { "3D Trajectory": ( "Predict the full manipulation trajectory as point_3d waypoints " "with depth and gripper state in JSON." ), "2D Trajectory": ( "Predict the full manipulation trajectory as point_2d waypoints " "with gripper state in JSON." ), "3D Pointing": "Report the point_3d location in JSON.", "2D Pointing": "Report point_2d locations in JSON.", BBOX_STYLE: None, VQA_STYLE: None, } V5_PROMPT_STYLES = list(V5_PROMPT_SUFFIXES.keys()) V5_DEFAULT_STYLE = "3D Trajectory" POINTING_STYLES = {"2D Pointing", "3D Pointing"} def build_v5_human_message(instruction: str, prompt_style: str) -> str: instr = instruction.strip() if prompt_style == BBOX_STYLE: return ( f"I'm looking for {instr} in this image. Can you locate it? " "Report bbox coordinates in JSON format." ) if prompt_style not in V5_PROMPT_SUFFIXES: prompt_style = V5_DEFAULT_STYLE suffix = V5_PROMPT_SUFFIXES[prompt_style] if suffix is None: return instr return f"{instr}\n{suffix}" # ── Preprocessing (matches training pipeline) ──────────────────────────────── def resize_to_target(image, target_size=TARGET_SIZE, interp=cv2.INTER_LINEAR): h, w = image.shape[:2] scale = target_size / max(h, w) new_w, new_h = int(round(w * scale)), int(round(h * scale)) resized = cv2.resize(image, (new_w, new_h), interpolation=interp) return resized, scale def prepare_inputs_from_arrays(rgb, depth, tmp_dir): rgb_resized, scale = resize_to_target(rgb, TARGET_SIZE, cv2.INTER_LINEAR) depth_resized, _ = resize_to_target(depth, TARGET_SIZE, cv2.INTER_NEAREST) new_h, new_w = rgb_resized.shape[:2] mask = ((depth_resized > 0.01) & (depth_resized < 10.0)).astype(np.float32) pcd = np.zeros((new_h, new_w, 4), dtype=np.float32) pcd[:, :, 2] = depth_resized pcd[:, :, 3] = mask img_path = os.path.join(tmp_dir, "frame_0_640.png") npz_path = os.path.join(tmp_dir, "frame_0_640.npz") cv2.imwrite(img_path, cv2.cvtColor(rgb_resized, cv2.COLOR_RGB2BGR)) np.savez_compressed(npz_path, pcd=pcd.astype(np.float16)) return img_path, npz_path, rgb_resized, depth_resized, scale def prepare_inputs(rgb_pil, depth_npy_path, tmp_dir): rgb = np.array(rgb_pil.convert("RGB")) depth = np.load(depth_npy_path).astype(np.float32) img_path, npz_path, rgb_resized, depth_resized, _ = prepare_inputs_from_arrays( rgb, depth, tmp_dir ) return img_path, npz_path, rgb_resized, depth_resized # ── Parsing ────────────────────────────────────────────────────────────────── def parse_trajectory(output): """Legacy ... parser.""" waypoints, actions = [], [] ans_match = re.search(r"(.*?)", output, re.DOTALL) if not ans_match: ans_match = re.search(r"\[\[.*?\]\]", output, re.DOTALL) if not ans_match: return [], [] content = ans_match.group(0) else: content = ans_match.group(1) coord_pat = r"\[(\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)\]" action_pat = r"(.*?)" parts = re.split(action_pat, content) for i, part in enumerate(parts): if i % 2 == 0: for c in re.findall(coord_pat, part): waypoints.append([float(c[0]), float(c[1]), float(c[2])]) actions.append(None) else: if actions: actions[-1] = part.strip() return waypoints, actions def parse_gt_structured_json(gpt_value): m = re.search(r"```json\s*(.*?)\s*```", gpt_value, re.DOTALL) raw = m.group(1) if m else gpt_value.strip() if not raw.lstrip().startswith("["): arr = re.search(r"\[.*\]", raw, re.DOTALL) if not arr: return [], [] raw = arr.group(0) try: entries = json.loads(raw) except json.JSONDecodeError: return [], [] if not isinstance(entries, list): return [], [] key = "point_3d" if any( isinstance(e, dict) and "point_3d" in e for e in entries ) else "point_2d" waypoints, actions = [], [] for entry in entries: if not isinstance(entry, dict) or key not in entry: continue pt = entry[key] waypoints.append( [float(pt[0]), float(pt[1]), float(pt[2]) if len(pt) > 2 else 0.0] ) grip = entry.get("gripper", "none") if grip == "close": actions.append("Close Gripper") elif grip == "open": actions.append("Open Gripper") else: actions.append(None) return waypoints, actions def parse_bbox_2d(output): m = re.search(r"```json\s*(.*?)\s*```", output, re.DOTALL) raw = m.group(1) if m else output.strip() if not raw.lstrip().startswith("["): arr = re.search(r"\[.*\]", raw, re.DOTALL) if not arr: return [] raw = arr.group(0) try: entries = json.loads(raw) except json.JSONDecodeError: return [] if not isinstance(entries, list): return [] boxes = [] for e in entries: if not isinstance(e, dict) or "bbox_2d" not in e: continue b = e["bbox_2d"] if len(b) < 4: continue boxes.append( (float(b[0]), float(b[1]), float(b[2]), float(b[3]), str(e.get("label", ""))) ) return boxes # ── 2D visualization ───────────────────────────────────────────────────────── COLOR_WP = (0, 255, 0) COLOR_GRASP = (255, 0, 0) COLOR_RELEASE = (0, 0, 255) COLOR_LINE = (255, 255, 0) def visualize_2d(image, waypoints, actions): if not waypoints: return image img = image.copy() h, w = img.shape[:2] pixels = [(int(wp[0] / 1000 * w), int(wp[1] / 1000 * h)) for wp in waypoints] for i in range(len(pixels) - 1): cv2.line(img, pixels[i], pixels[i + 1], COLOR_LINE, 2, cv2.LINE_AA) for i, (px, py) in enumerate(pixels): act = actions[i] if i < len(actions) else None if act and "Close" in act: color, r = COLOR_GRASP, 12 elif act and "Open" in act: color, r = COLOR_RELEASE, 12 else: color, r = COLOR_WP, 8 cv2.circle(img, (px, py), r, color, -1) cv2.circle(img, (px, py), r, (255, 255, 255), 2) cv2.putText(img, str(i), (px - 5, py + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2) cv2.putText( img, f"d={waypoints[i][2]:.2f}m", (px + 15, py), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, ) y = 30 for label, color, xo in [ ("Waypoint", COLOR_WP, 10), ("Grasp", COLOR_GRASP, 110), ("Release", COLOR_RELEASE, 180) ]: cv2.circle(img, (xo, y - 5), 6, color, -1) cv2.putText(img, label, (xo + 10, y), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) return img def visualize_points(image, points): if not points: return image img = image.copy() h, w = img.shape[:2] for i, p in enumerate(points): px, py = int(p[0] / 1000 * w), int(p[1] / 1000 * h) cv2.circle(img, (px, py), 8, (0, 255, 0), -1) cv2.circle(img, (px, py), 8, (255, 255, 255), 2) cv2.putText( img, str(i + 1), (px + 11, py + 4), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, ) return img def visualize_bbox(image, boxes): if not boxes: return image img = image.copy() h, w = img.shape[:2] palette = [(0, 255, 0), (255, 80, 0), (0, 160, 255), (255, 0, 200), (255, 220, 0)] for i, (x1, y1, x2, y2, label) in enumerate(boxes): p1 = (int(x1 / 1000 * w), int(y1 / 1000 * h)) p2 = (int(x2 / 1000 * w), int(y2 / 1000 * h)) color = palette[i % len(palette)] cv2.rectangle(img, p1, p2, color, 2) tag = label or f"obj{i}" (tw, th), _ = cv2.getTextSize(tag, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) cv2.rectangle(img, (p1[0], p1[1] - th - 6), (p1[0] + tw + 4, p1[1]), color, -1) cv2.putText(img, tag, (p1[0] + 2, p1[1] - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) return img # ── 3D scene + trajectory visualization ────────────────────────────────────── def colorize_depth(depth, min_d=0.1, max_d=3.0): d = np.clip(depth, min_d, max_d) d = ((d - min_d) / (max_d - min_d) * 255).astype(np.uint8) colored = cv2.applyColorMap(d, cv2.COLORMAP_TURBO) colored = cv2.cvtColor(colored, cv2.COLOR_BGR2RGB) colored[depth <= 0] = [0, 0, 0] return colored def _create_sphere_points(center, radius=0.005, n=150): phi = np.random.uniform(0, 2 * np.pi, n) ct = np.random.uniform(-1, 1, n) theta = np.arccos(ct) return np.stack([ center[0] + radius * np.sin(theta) * np.cos(phi), center[1] + radius * np.sin(theta) * np.sin(phi), center[2] + radius * np.cos(theta), ], axis=1) def _create_tube_points(p1, p2, radius=0.002): d = p2 - p1 L = np.linalg.norm(d) if L < 1e-8: return np.empty((0, 3)) d = d / L perp1 = np.cross(d, [1, 0, 0]) if abs(d[0]) < 0.9 else np.cross(d, [0, 1, 0]) perp1 /= np.linalg.norm(perp1) pts = [] for ti in np.linspace(0, 1, max(int(L / 0.002), 10)): c = p1 + ti * (p2 - p1) for a in np.linspace(0, 2 * np.pi, 6, endpoint=False): pts.append(c + radius * (np.cos(a) * perp1 + np.sin(a) * np.cross(d, perp1))) return np.array(pts) def _uvd_to_xyz(coords, intrinsics_3x3, img_w, img_h, uvd_norm=1000.0): K = np.array(intrinsics_3x3, dtype=np.float64) Kinv = np.linalg.inv(K) u_px = (coords[:, 0] / uvd_norm) * img_w v_px = (coords[:, 1] / uvd_norm) * img_h pixels = np.stack([u_px, v_px, np.ones(len(u_px))], axis=1) return coords[:, 2:3] * (pixels @ Kinv.T) def default_intrinsics(img_h, img_w): f = float(max(img_h, img_w)) return [[f, 0.0, img_w / 2.0], [0.0, f, img_h / 2.0], [0.0, 0.0, 1.0]] def build_scene_pcd_simple(rgb, depth, intrinsics_3x3, depth_trunc=3.0, stride=2): H, W = depth.shape[:2] if rgb.shape[:2] != (H, W): rgb = cv2.resize(rgb, (W, H)) K = np.asarray(intrinsics_3x3, dtype=np.float64) fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] vs, us = np.mgrid[0:H:stride, 0:W:stride] z = depth[vs, us].astype(np.float32) valid = (z > 0.1) & (z < depth_trunc) z, us, vs = z[valid], us[valid], vs[valid] if z.size == 0: return None, None pts = np.stack([(us - cx) * z / fx, (vs - cy) * z / fy, z], axis=1) cols = rgb[vs, us].astype(np.float32) / 255.0 return pts, cols def build_3d_scene_figure(scene_pts, scene_cols, traj_dict, title="", subsample=2): fig = go.Figure() if scene_pts is not None and len(scene_pts) > 0: sp = scene_pts[::subsample] sr = (scene_cols[::subsample] * 255).astype(np.uint8) fig.add_trace(go.Scatter3d( x=sp[:, 0], y=sp[:, 1], z=sp[:, 2], mode="markers", marker=dict(size=1.5, color=[f"rgb({r},{g},{b})" for r, g, b in sr], opacity=0.6), name="Scene", hoverinfo="skip", )) for label, (xyz, color) in traj_dict.items(): if xyz is None or len(xyz) < 2: continue c = np.array(color) tpts, tcols = [], [] sp = _create_sphere_points(xyz[0], radius=0.008, n=400) tpts.append(sp) tcols.append(np.tile(np.clip(c * 1.3, 0, 1), (len(sp), 1))) ep = _create_sphere_points(xyz[-1], radius=0.008, n=400) tpts.append(ep) tcols.append(np.tile(c * 0.7, (len(ep), 1))) for i in range(len(xyz) - 1): tube = _create_tube_points(xyz[i], xyz[i + 1], radius=0.003) if len(tube) > 0: tpts.append(tube) tcols.append(np.tile(c, (len(tube), 1))) tpts = np.vstack(tpts) tcols = np.vstack(tcols) tr_rgb = (np.clip(tcols, 0, 1) * 255).astype(np.uint8) fig.add_trace(go.Scatter3d( x=tpts[:, 0], y=tpts[:, 1], z=tpts[:, 2], mode="markers", marker=dict(size=2.5, color=[f"rgb({r},{g},{b})" for r, g, b in tr_rgb], opacity=1.0), name=label, hoverinfo="skip", )) fig.update_layout( title=title, height=600, scene=dict( xaxis_title="X", yaxis_title="Y", zaxis_title="Z", aspectmode="data", bgcolor="white", camera=dict(eye=dict(x=0, y=0, z=-1.5), up=dict(x=0, y=-1, z=0)), ), paper_bgcolor="white", legend=dict(x=0.01, y=0.99, bgcolor="rgba(255,255,255,0.8)"), ) return fig def format_conversation(system, user, assistant): return ( "════════ SYSTEM ════════\n" f"{system.strip()}\n\n" "════════ USER ════════\n" f"{user.strip()}\n\n" "════════ ASSISTANT ════════\n" f"{assistant.strip()}" ) # ── Model (loaded once at module scope, eager .to("cuda")) ─────────────────── print(f"Downloading checkpoint {MODEL_ID} …") CKPT_DIR = snapshot_download(MODEL_ID) print(f"Checkpoint at {CKPT_DIR}") # Register the custom Qwen3-VL geometry model class with transformers Auto* . from hamster3d.model import register_qwen3_vl_geometry # noqa: E402 try: register_qwen3_vl_geometry() except Exception as e: # pragma: no cover print(f"register_qwen3_vl_geometry: {e!r}") from transformers import AutoModelForImageTextToText, AutoProcessor # noqa: E402 print("Loading processor …") PROCESSOR = AutoProcessor.from_pretrained(CKPT_DIR, trust_remote_code=True) print("Loading model …") MODEL = AutoModelForImageTextToText.from_pretrained( CKPT_DIR, dtype=torch.bfloat16, trust_remote_code=True, ).to("cuda") MODEL.eval() _PARAM_DTYPE = next(MODEL.parameters()).dtype print(f"Model loaded ({sum(p.numel() for p in MODEL.parameters()):,} params, dtype={_PARAM_DTYPE})") def _run_model(image_path, npz_path, query, system_prompt=V5_SYSTEM_PROMPT): """Greedy generation matching the reference ModelServer.predict.""" device = "cuda" rgb = np.array(Image.open(image_path).convert("RGB")) pcd = np.load(npz_path)["pcd"] # (H, W, 4) float16 depth = pcd[:, :, 2].astype(np.float32) rgb_tensor = torch.from_numpy(rgb).float().permute(2, 0, 1).unsqueeze(0) / 255.0 depth_tensor = torch.from_numpy(depth).float().unsqueeze(0) geometry_encoder_inputs = [rgb_tensor.to(device=device, dtype=_PARAM_DTYPE)] depth_maps = [depth_tensor.to(device=device, dtype=_PARAM_DTYPE)] messages = [] if system_prompt: messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]}) messages.append({ "role": "user", "content": [{"type": "image"}, {"type": "text", "text": query}], }) text = PROCESSOR.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) model_inputs = PROCESSOR( text=[text], images=[Image.open(image_path).convert("RGB")], return_tensors="pt", ).to(device) for _k, _v in list(model_inputs.items()): if torch.is_tensor(_v) and torch.is_floating_point(_v): model_inputs[_k] = _v.to(_PARAM_DTYPE) model_inputs["geometry_encoder_inputs"] = geometry_encoder_inputs model_inputs["depth_maps"] = depth_maps with torch.inference_mode(): output_ids = MODEL.generate( **model_inputs, max_new_tokens=1024, do_sample=False, temperature=None, top_p=None, ) input_len = model_inputs["input_ids"].shape[1] generated_ids = output_ids[:, input_len:] return PROCESSOR.batch_decode(generated_ids, skip_special_tokens=True)[0] # ── Inference handler (ZeroGPU) ────────────────────────────────────────────── @spaces.GPU(duration=120) def predict(rgb_image, depth_file, instruction, prompt_style): """Predict a robot manipulation trajectory / pointing / bbox / VQA answer. Args: rgb_image: RGB scene image (PIL). Auto-resized to longest edge 640. depth_file: metric depth map as a .npy file (float32, meters, aligned to RGB). instruction: free-form language instruction (e.g. "Pick up the red cup"). prompt_style: one of "3D Trajectory", "2D Trajectory", "3D Pointing", "2D Pointing", "2D Bounding Box", "General VQA". Returns: (overlay_image, raw_output_text, conversation_text, plotly_3d_figure) """ if rgb_image is None: return None, "Please provide an RGB image.", "", None if depth_file is None: return None, "Please provide a metric depth .npy file.", "", None if not instruction or not instruction.strip(): return None, "Please enter a task instruction.", "", None depth_path = depth_file if isinstance(depth_file, str) else depth_file.name tmp_dir = tempfile.mkdtemp(prefix="hamster3d_") img_path, npz_path, rgb_resized, depth_resized = prepare_inputs( rgb_image, depth_path, tmp_dir ) h, w = rgb_resized.shape[:2] human_msg = build_v5_human_message(instruction, prompt_style) raw = _run_model(img_path, npz_path, human_msg, system_prompt=V5_SYSTEM_PROMPT) conversation = format_conversation(V5_SYSTEM_PROMPT, f"{human_msg}", raw) K = default_intrinsics(h, w) scene_pts, scene_cols = build_scene_pcd_simple(rgb_resized, depth_resized, K) # General VQA → free-form answer, scene with no trajectory. if prompt_style == VQA_STYLE: fig = build_3d_scene_figure(scene_pts, scene_cols, {}, title=instruction.strip()) return rgb_resized, raw, conversation, fig # 2D bounding box. if prompt_style == BBOX_STYLE: boxes = parse_bbox_2d(raw) viz = visualize_bbox(rgb_resized, boxes) fig = build_3d_scene_figure(scene_pts, scene_cols, {}, title=instruction.strip()) return viz, raw, conversation, fig # Pointing → independent numbered points. if prompt_style in POINTING_STYLES: pts, _ = parse_gt_structured_json(raw) viz = visualize_points(rgb_resized, pts) if pts else rgb_resized fig = build_3d_scene_figure(scene_pts, scene_cols, {}, title=instruction.strip()) if pts: arr = np.array(pts, dtype=np.float32) if prompt_style == "2D Pointing" and depth_resized is not None: for i in range(len(arr)): up = int(np.clip(round(arr[i, 0] / 1000 * w), 0, w - 1)) vp = int(np.clip(round(arr[i, 1] / 1000 * h), 0, h - 1)) arr[i, 2] = float(depth_resized[vp, up]) xyz = _uvd_to_xyz(arr, K, w, h) fig.add_trace(go.Scatter3d( x=xyz[:, 0], y=xyz[:, 1], z=xyz[:, 2], mode="markers+text", marker=dict(size=8, color="lime", line=dict(width=2, color="white")), text=[str(i + 1) for i in range(len(xyz))], textposition="top center", name="Points", hoverinfo="skip", )) return viz, raw, conversation, fig # Trajectory (2D / 3D). waypoints, actions = parse_gt_structured_json(raw) if not waypoints: waypoints, actions = parse_trajectory(raw) viz_2d = visualize_2d(rgb_resized, waypoints, actions) if waypoints else rgb_resized traj_dict = {} if waypoints: pred_xyz = _uvd_to_xyz(np.array(waypoints, dtype=np.float32), K, w, h) traj_dict["Predicted trajectory"] = (pred_xyz, [1.0, 0.2, 0.0]) fig = build_3d_scene_figure(scene_pts, scene_cols, traj_dict, title=instruction.strip()) return viz_2d, raw, conversation, fig # ── Examples browser helpers ───────────────────────────────────────────────── def _load_example_assets(idx): """Return (rgb_pil, depth_npy_path, instruction) for bundled example idx.""" prefix = f"sample_{int(idx)}" rgb_path = os.path.join(EXAMPLES_DIR, f"{prefix}_rgb.png") depth_path = os.path.join(EXAMPLES_DIR, f"{prefix}_depth.npy") instr_path = os.path.join(EXAMPLES_DIR, f"{prefix}_instruction.txt") instruction = "" if os.path.isfile(instr_path): instruction = open(instr_path).read().strip() return rgb_path, depth_path, instruction def _example_rows(): rows = [] for i in range(6): rgb_path, depth_path, instruction = _load_example_assets(i) if os.path.isfile(rgb_path) and os.path.isfile(depth_path): rows.append([rgb_path, depth_path, instruction, V5_DEFAULT_STYLE]) return rows def on_depth_upload(depth_file): if depth_file is None: return None try: path = depth_file if isinstance(depth_file, str) else depth_file.name depth = np.load(path).astype(np.float32) return colorize_depth(depth) except Exception: return None # ── UI ─────────────────────────────────────────────────────────────────────── CSS = """ #col-container { max-width: 1200px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } .mono textarea { font-family: monospace; font-size: 13px; } """ with gr.Blocks(title="3D HAMSTER") as demo: with gr.Column(elem_id="col-container"): gr.Markdown( "# 🐹 3D HAMSTER — 3D Trajectory Prediction\n" "Depth-aware **Vision-Language-Action** planner (Qwen3-VL-8B + LingBot-Depth " "geometry encoder). From a single **RGB image + metric depth map + language " "instruction**, it predicts a metric **3D end-effector trajectory** " "(`[u, v, depth]` waypoints + gripper states), plus 2D trajectory / pointing / " "bounding-box / VQA modes.\n\n" "[Paper](https://huggingface.co/papers/2606.31329) · " "[Model](https://huggingface.co/DAVIAN-Robotics/3D_HAMSTER) · " "[Code](https://github.com/DAVIAN-Robotics/3D_HAMSTER) · " "[Project page](https://davian-robotics.github.io/3D_HAMSTER/)\n\n" "> ⚠️ Depth must be **metric (meters)** and aligned to the RGB frame. " "Disparity / normalized / millimeter depth will degrade the geometry." ) with gr.Row(): with gr.Column(scale=1): rgb_input = gr.Image(label="RGB Image", type="pil", height=320) depth_input = gr.File( label="Metric Depth (.npy, float32, meters)", file_types=[".npy"] ) depth_preview = gr.Image(label="Depth preview", type="numpy", height=180) instruction_input = gr.Textbox( label="Task instruction", placeholder="e.g. Pick up the red block and place it on the blue plate.", lines=2, ) prompt_style_input = gr.Radio( label="Prompt style", choices=V5_PROMPT_STYLES, value=V5_DEFAULT_STYLE, info="Trajectory: manipulation waypoints | Pointing: object points | " "2D Bounding Box: object box | General VQA: free-form answer", ) run_btn = gr.Button("Predict trajectory", variant="primary", size="lg") with gr.Column(scale=1): overlay_output = gr.Image(label="2D overlay", type="numpy", height=340) plot_3d_output = gr.Plot(label="3D scene + trajectory (rotate / zoom)") with gr.Accordion("Model output", open=False): raw_output = gr.Textbox( label="Raw model output", lines=4, interactive=False, elem_classes=["mono"], ) conversation_output = gr.Textbox( label="Full conversation", lines=10, interactive=False, elem_classes=["mono"], ) gr.Examples( examples=_example_rows(), inputs=[rgb_input, depth_input, instruction_input, prompt_style_input], outputs=[overlay_output, raw_output, conversation_output, plot_3d_output], fn=predict, cache_examples=False, run_on_click=True, label="Bundled examples (RGB + depth + instruction)", ) depth_input.change(fn=on_depth_upload, inputs=[depth_input], outputs=[depth_preview]) run_btn.click( fn=predict, inputs=[rgb_input, depth_input, instruction_input, prompt_style_input], outputs=[overlay_output, raw_output, conversation_output, plot_3d_output], api_name="predict", ) if __name__ == "__main__": demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)