Spaces:
Running on Zero
Running on Zero
File size: 23,590 Bytes
b890615 0bef3b6 b890615 0bef3b6 b890615 7abaefa cbeef84 0bef3b6 7abaefa c5e91c4 2a7d433 c5e91c4 b890615 c5e91c4 b890615 d41be86 7abaefa 2a7d433 0bef3b6 b890615 0bef3b6 b890615 0bef3b6 b890615 e689d1a b890615 0bef3b6 b890615 0bef3b6 b890615 0bef3b6 b890615 7abaefa 0bef3b6 7abaefa c5e91c4 7abaefa 0bef3b6 b890615 7abaefa 0bef3b6 cbeef84 b890615 0bef3b6 b890615 cbeef84 8df6536 0bef3b6 2a7d433 0bef3b6 2a7d433 0bef3b6 c5e91c4 d41be86 0bef3b6 c5e91c4 0bef3b6 c5e91c4 0bef3b6 d41be86 c5e91c4 2a7d433 0bef3b6 cbeef84 0bef3b6 cbeef84 2a7d433 cbeef84 0bef3b6 c5e91c4 b890615 0bef3b6 c5e91c4 b890615 0bef3b6 b890615 0bef3b6 e689d1a b39681e e689d1a b39681e 0bef3b6 e689d1a b39681e c5e91c4 b890615 c5e91c4 b890615 c5e91c4 0bef3b6 7abaefa b890615 7abaefa 0bef3b6 c5e91c4 0bef3b6 7abaefa 0bef3b6 7abaefa cbeef84 0bef3b6 cbeef84 0bef3b6 b890615 7abaefa e689d1a 0bef3b6 c5e91c4 0bef3b6 c5e91c4 0bef3b6 c5e91c4 0bef3b6 c5e91c4 cbeef84 d41be86 c5e91c4 0bef3b6 c5e91c4 b39681e c5e91c4 0bef3b6 b39681e 0bef3b6 c5e91c4 0bef3b6 c5e91c4 0bef3b6 cbeef84 0bef3b6 c5e91c4 0bef3b6 c5e91c4 cbeef84 c5e91c4 0bef3b6 c5e91c4 b890615 2a7d433 7a90059 c5e91c4 cbeef84 0bef3b6 c5e91c4 0bef3b6 c5e91c4 0bef3b6 b890615 c5e91c4 0bef3b6 c5e91c4 b890615 cbeef84 0bef3b6 cbeef84 0bef3b6 7a90059 0bef3b6 c5e91c4 0bef3b6 7abaefa c5e91c4 0bef3b6 7abaefa 0bef3b6 7abaefa 0bef3b6 b890615 0bef3b6 cbeef84 c5e91c4 7abaefa 0bef3b6 7abaefa d41be86 b890615 c5e91c4 b890615 7abaefa 0bef3b6 7abaefa c5e91c4 cbeef84 0bef3b6 b890615 0bef3b6 7abaefa c5e91c4 b890615 7abaefa cbeef84 7abaefa 0bef3b6 7abaefa cbeef84 7abaefa cbeef84 0bef3b6 b890615 0bef3b6 cbeef84 0bef3b6 b890615 0bef3b6 cbeef84 0bef3b6 7abaefa 0bef3b6 7abaefa 0bef3b6 b890615 0bef3b6 7abaefa 0bef3b6 b39681e c5e91c4 0bef3b6 7abaefa 0bef3b6 b39681e c5e91c4 0bef3b6 7abaefa 0bef3b6 b890615 0bef3b6 7abaefa b890615 0bef3b6 b890615 7abaefa c5e91c4 0bef3b6 c5e91c4 1f2b246 0bef3b6 b890615 1f2b246 0bef3b6 c5e91c4 0bef3b6 7abaefa 0bef3b6 7abaefa 0bef3b6 7abaefa 0bef3b6 7abaefa 0bef3b6 7abaefa c5e91c4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 | """RynnValue-8B — robotic value model demo.
Given a robot manipulation video and the task instruction, predicts how much
time is left until the task is finished at every point along the clip, plus a
short analysis block (description / instruction match / success).
Follows the official reference implementation
(https://github.com/alibaba-damo-academy/RynnValue, `rynn_infer/inference.py`):
prefix-uniform sampling — for evaluation step *i* the prefix `frames[0:i]` is
resampled to `num_frames` frames and the model's **last** prediction slot is
read out, so every score only conditions on frames seen so far.
"""
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # noqa: E402 (must precede torch / transformers)
import re # noqa: E402
import tempfile # noqa: E402
import time # noqa: E402
import gradio as gr # noqa: E402
import imageio.v2 as imageio # noqa: E402
import numpy as np # noqa: E402
import torch # noqa: E402
from PIL import Image # noqa: E402
from transformers import AutoConfig, AutoModel, AutoProcessor # noqa: E402
from plot_utils import save_video_with_trend # noqa: E402
MODEL_ID = "Alibaba-DAMO-Academy/RynnValue-8B"
# Defaults — each is also the default of its UI component, so clicking an
# example and pressing "Analyze" behave identically.
DEFAULT_ROBOT = "a Franka single-arm robot"
DEFAULT_CAMERA = "the main camera"
DEFAULT_NUM_STEPS = 16
DEFAULT_NUM_FRAMES = 32
DEFAULT_MAX_SIDE = 384
DEFAULT_MAX_NEW_TOKENS = 128
# Rendering budget: the input clip is temporally subsampled to at most this many
# frames and the playback fps is scaled to match, so wall-clock duration and the
# ground-truth "remaining time" reference curve are unchanged.
MAX_RENDER_FRAMES = 320
DISPLAY_MAX_SIDE = 640
# Longest ZeroGPU slot this demo will ever request.
GPU_BUDGET_S = 240
ROBOT_CHOICES = [
"a Franka single-arm robot",
"an SO-101 single-arm robot",
"a WidowX single-arm robot",
"a Jaco single-arm robot",
"a Koch dual-arm robot",
"an xArm single-arm robot",
"an Trossen dual-arm robot",
]
CAMERA_CHOICES = [
"the main camera",
"the side camera",
"the top-down camera",
"the wrist-mounted camera",
"the main left camera",
"the main right camera",
]
# --------------------------------------------------------------------------- #
# Model
# --------------------------------------------------------------------------- #
print(f"Loading {MODEL_ID} ...", flush=True)
_config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
# The exported config predates the attn-impl field, so force the custom
# prediction-slot isolation attention (mirrors rynn_infer/inference.py).
_config._attn_implementation = "pred_slot_isolated_eager"
model = AutoModel.from_pretrained(
MODEL_ID,
config=_config,
trust_remote_code=True,
torch_dtype=torch.bfloat16,
)
# `torch_dtype=` leaves the value heads in fp32 (they are constructed with an
# explicit dtype), so the trailing `dtype=` cast is required — the reference
# script does the same `model.to(device=..., dtype=...)`.
model = model.to(device="cuda", dtype=torch.bfloat16).eval()
processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
tokenizer = processor.tokenizer
EOS_TOKEN_ID = tokenizer.convert_tokens_to_ids("<|im_end|>")
print("Model ready.", flush=True)
# --------------------------------------------------------------------------- #
# Video / sampling helpers (ported from rynn_infer/inference.py)
# --------------------------------------------------------------------------- #
def _resize_long_side(img: Image.Image, max_side: int) -> Image.Image:
w, h = img.size
if max_side <= 0 or max(w, h) <= max_side:
return img
scale = max_side / float(max(w, h))
return img.resize((max(1, int(round(w * scale))), max(1, int(round(h * scale)))), Image.BICUBIC)
def load_video(video_path: str):
"""Decode a video into ``(frames, fps)``.
Frames are downscaled to ``DISPLAY_MAX_SIDE`` on the fly and strided so that
at most ``MAX_RENDER_FRAMES`` are kept; ``fps`` is scaled accordingly so the
rendered clip keeps real-time playback speed.
"""
if not video_path or not os.path.isfile(video_path):
raise gr.Error("Please upload a video first.")
reader = imageio.get_reader(video_path)
try:
meta = reader.get_meta_data()
src_fps = float(meta.get("fps") or 30.0)
duration = float(meta.get("duration") or 0.0)
est_total = int(duration * src_fps) if duration > 0 else 0
stride = 1
if est_total > MAX_RENDER_FRAMES:
stride = int(np.ceil(est_total / float(MAX_RENDER_FRAMES)))
frames = []
for i, frame in enumerate(reader):
if i % stride:
continue
frames.append(_resize_long_side(Image.fromarray(frame).convert("RGB"), DISPLAY_MAX_SIDE))
if len(frames) >= MAX_RENDER_FRAMES + 8:
break
finally:
reader.close()
if not frames:
raise gr.Error("Could not decode any frame from this video.")
return frames, src_fps / float(stride)
def sample_frame_indices(total: int, num_frames: int):
"""Uniformly pick ``num_frames`` indices out of ``total`` frames."""
if num_frames <= 0 or num_frames >= total:
return list(range(total))
if num_frames == 1:
return [total - 1]
step = (total - 1) / (num_frames - 1)
return [int(round(j * step)) for j in range(num_frames)]
_DESCRIPTION_RE = re.compile(r"-\s*Video Description:\s*(.+)", re.IGNORECASE)
_MATCH_RE = re.compile(r"-\s*Match:\s*(Yes|No)", re.IGNORECASE)
_SUCCESS_RE = re.compile(r"-\s*Success:\s*(Yes|No)", re.IGNORECASE)
def parse_analysis(text: str) -> dict:
"""Extract description / match / success from the generated Analysis block."""
def _first(pattern):
m = pattern.search(text)
return m.group(1).strip() if m else None
return {
"description": _first(_DESCRIPTION_RE),
"match": _first(_MATCH_RE),
"success": _first(_SUCCESS_RE),
}
def _estimate_duration(
video_path=None,
instruction="",
robot_description=DEFAULT_ROBOT,
camera_description=DEFAULT_CAMERA,
num_steps=DEFAULT_NUM_STEPS,
num_frames=DEFAULT_NUM_FRAMES,
max_image_side=DEFAULT_MAX_SIDE,
max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
*args,
**kwargs,
):
"""ZeroGPU duration estimate.
The cost is driven by the prefix sequence length, which depends on the
video's aspect ratio as well as the sliders — so the clip is probed for its
resolution and the visual-token count is computed exactly. The linear and
quadratic coefficients were fitted on the live Space (measured: 24.6 s for
240x240 @ 32 frames, 31.8 s for 640x360, 41.8 s for 640x480, all 16 steps).
"""
steps = int(num_steps or DEFAULT_NUM_STEPS)
frames = int(num_frames or DEFAULT_NUM_FRAMES)
side = int(max_image_side or DEFAULT_MAX_SIDE)
width, height, n_src = 640.0, 480.0, float(MAX_RENDER_FRAMES)
try:
reader = imageio.get_reader(video_path)
meta = reader.get_meta_data()
reader.close()
width, height = (float(v) for v in meta["size"])
n_src = min(
float(MAX_RENDER_FRAMES),
float(meta.get("duration") or 0.0) * float(meta.get("fps") or 30.0),
)
except Exception: # unreadable metadata — fall back to the worst case
pass
scale = min(1.0, DISPLAY_MAX_SIDE / max(width, height), side / max(width, height))
# 16 px patches merged 2x2 -> one visual token per 32x32 px, plus the 8+8
# <value>/<relative_value> tokens the processor emits per frame.
tokens_per_frame = np.ceil(width * scale / 32.0) * np.ceil(height * scale / 32.0) + 16
seq_len = frames * tokens_per_frame + 64
per_step = 4.6e-4 * seq_len + 1.0e-8 * seq_len**2
if _pick_batch_size(frames, side) < 4:
per_step *= 1.4
overhead = 8.0 + 0.05 * int(max_new_tokens or 128) + min(9.0, n_src / 40.0)
raw = steps * per_step + overhead
return int(max(30.0, raw)) if raw <= GPU_BUDGET_S else GPU_BUDGET_S
def _pick_batch_size(num_frames: int, max_image_side: int) -> int:
"""Prefix sub-samples per forward pass.
The checkpoint's `pred_slot_isolated_eager` attention materialises the full
(batch, heads, L, L) fp32 score matrix, so the batch has to shrink as the
per-prefix sequence grows or the GPU runs out of memory.
"""
cost = (num_frames / 32.0) * (max_image_side / 384.0) ** 2
if cost <= 1.05:
return 4
if cost <= 1.6:
return 2
return 1
# --------------------------------------------------------------------------- #
# Inference
# --------------------------------------------------------------------------- #
@spaces.GPU(duration=_estimate_duration)
def analyze_video(
video_path: str,
instruction: str,
robot_description: str = DEFAULT_ROBOT,
camera_description: str = DEFAULT_CAMERA,
num_steps: int = DEFAULT_NUM_STEPS,
num_frames: int = DEFAULT_NUM_FRAMES,
max_image_side: int = DEFAULT_MAX_SIDE,
max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
progress=gr.Progress(),
):
"""Predict how much time is left before a robot finishes a task.
Runs RynnValue-8B over a robot manipulation video: for each evaluated step
the prefix of the video seen so far is uniformly resampled and the model
predicts the remaining time to task completion in seconds. It also generates
an Analysis block (what happens in the video, whether the video matches the
instruction, whether the task succeeded).
Args:
video_path: Path to the robot trajectory video (mp4).
instruction: Natural-language task the robot is supposed to accomplish.
robot_description: Embodiment description, e.g. "a Franka single-arm robot".
camera_description: Camera viewpoint description, e.g. "the main camera".
num_steps: Number of prefix steps evaluated along the video.
num_frames: Frames uniformly resampled from each prefix.
max_image_side: Longer side each frame is resized to before the model sees it.
max_new_tokens: Token budget for the generated Analysis block.
Returns:
A video with the synchronized remaining-time curve, and a markdown report.
"""
if not instruction or not instruction.strip():
raise gr.Error("Please give the task instruction the robot is supposed to follow.")
instruction = instruction.strip()
robot_description = (robot_description or "").strip() or None
camera_description = (camera_description or "").strip() or None
if robot_description is None and camera_description is None:
raise gr.Error(
"This checkpoint was trained with meta information — fill in the "
"embodiment and/or viewpoint description."
)
num_steps = int(num_steps)
num_frames = int(num_frames)
max_image_side = int(max_image_side)
max_new_tokens = int(max_new_tokens)
if _estimate_duration(
video_path, instruction, robot_description, camera_description,
num_steps, num_frames, max_image_side, max_new_tokens,
) >= GPU_BUDGET_S:
raise gr.Error(
f"These settings need more than the {GPU_BUDGET_S}s GPU slot this demo "
"requests — lower *Prefix steps*, *Frames per step* or *Max image side* "
"in Advanced settings (or use a shorter clip)."
)
t0 = time.perf_counter()
progress(0.05, desc="Decoding video…")
frames, fps = load_video(video_path)
total = len(frames)
model_frames = [_resize_long_side(f, max_image_side) for f in frames]
eval_indices = sample_frame_indices(total, num_steps)
t_decode = time.perf_counter() - t0
def build_prefix_sample(end_idx):
frame_idx = np.linspace(0, end_idx, num_frames, dtype=int)
return processor.process_episode(
instruction=instruction,
images=[model_frames[j] for j in frame_idx],
robot_description=robot_description,
camera_description=camera_description,
)
def run_batch(samples):
batch_kwargs = dict(
input_ids=torch.cat([s["input_ids"] for s in samples], dim=0).to("cuda").long(),
attention_mask=torch.cat([s["attention_mask"] for s in samples], dim=0).to("cuda").long(),
pixel_values=torch.cat([s["pixel_values"].flatten(0, 1) for s in samples], dim=0).to("cuda"),
image_grid_thw=torch.cat(
[s["image_grid_thw"].flatten(0, 1) for s in samples], dim=0
).to("cuda").long(),
)
try:
with torch.inference_mode():
outputs = model(**batch_kwargs)
except (torch.cuda.OutOfMemoryError, RuntimeError) as exc:
torch.cuda.empty_cache()
raise gr.Error(
"Ran out of GPU memory for these settings — the checkpoint's custom "
"attention keeps the full attention matrix in memory, so lower "
"*Frames per step* or *Max image side* in Advanced settings. "
f"({type(exc).__name__})"
) from exc
pred = outputs.value.pred_value
if pred.dim() == 2 and pred.shape[0] == 1:
pred = pred.reshape(len(samples), -1)
if pred.dim() == 3:
pred = pred.mean(dim=0)
if pred.dim() == 2 and pred.shape[-1] > 1:
pred = pred[:, -1]
elif pred.dim() == 2:
pred = pred[:, 0]
return pred.float().reshape(-1).tolist()
t1 = time.perf_counter()
batch_size = _pick_batch_size(num_frames, max_image_side)
pred_value, batch, final_sample = [], [], None
for step, end_idx in enumerate(eval_indices):
sample = build_prefix_sample(end_idx)
if step == len(eval_indices) - 1:
final_sample = sample
batch.append(sample)
if len(batch) >= batch_size or step == len(eval_indices) - 1:
pred_value.extend(run_batch(batch))
batch = []
progress(
0.05 + 0.65 * len(pred_value) / max(1, len(eval_indices)),
desc=f"Value prediction {len(pred_value)}/{len(eval_indices)}",
)
t_value = time.perf_counter() - t1
# ---- Analysis pass on the full-video prefix ---------------------------- #
progress(0.72, desc="Generating analysis…")
t2 = time.perf_counter()
input_ids = final_sample["input_ids"].to("cuda").long()
with torch.inference_mode():
gen_out = model.generate(
input_ids=input_ids,
attention_mask=final_sample["attention_mask"].to("cuda").long(),
pixel_values=final_sample["pixel_values"].flatten(0, 1).to("cuda"),
image_grid_thw=final_sample["image_grid_thw"].flatten(0, 1).to("cuda").long(),
max_new_tokens=max_new_tokens,
do_sample=False,
num_beams=1,
eos_token_id=EOS_TOKEN_ID,
pad_token_id=EOS_TOKEN_ID,
use_cache=True,
)
analysis_text = tokenizer.decode(gen_out[0, input_ids.shape[1]:], skip_special_tokens=True)
analysis = parse_analysis(analysis_text)
t_gen = time.perf_counter() - t2
# ---- Render ------------------------------------------------------------ #
progress(0.85, desc="Rendering trend video…")
t3 = time.perf_counter()
out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
save_video_with_trend(
images=frames,
value=pred_value,
output_path=out_path,
fps=max(1.0, float(fps)),
title="Remaining Time (s)",
task_title=instruction,
sampled_indices=eval_indices,
)
t_render = time.perf_counter() - t3
total_time = time.perf_counter() - t0
clip_len = (total - 1) / float(fps)
first_pred, last_pred = float(pred_value[0]), float(pred_value[-1])
def _badge(v):
if v is None:
return "—"
return "✅ Yes" if v.lower() == "yes" else "❌ No"
report = f"""### Analysis
- **Video description:** {analysis['description'] or '—'}
- **Matches the instruction:** {_badge(analysis['match'])}
- **Task succeeded:** {_badge(analysis['success'])}
### Predicted remaining time
| | predicted | actual (clip) |
|---|---|---|
| at the first frame | **{first_pred:.2f} s** | {clip_len:.2f} s |
| at the last frame | **{last_pred:.2f} s** | 0.00 s |
<sub>{len(eval_indices)} prefix steps · {num_frames} frames/step · {max_image_side} px ·
decode {t_decode:.1f}s · value {t_value:.1f}s · analysis {t_gen:.1f}s · render {t_render:.1f}s ·
total {total_time:.1f}s</sub>
<details><summary>Raw generation</summary>
```
{analysis_text.strip()}
```
</details>"""
print(
f"[timing] decode={t_decode:.2f}s value={t_value:.2f}s gen={t_gen:.2f}s "
f"render={t_render:.2f}s total={total_time:.2f}s steps={len(eval_indices)} "
f"frames={num_frames} side={max_image_side}",
flush=True,
)
return out_path, report
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
CSS = """
#col-container { max-width: 1200px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="RynnValue-8B") as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
"""# RynnValue-8B — how much longer will the robot take?
A general-purpose value model for robot manipulation from Alibaba DAMO Academy. Give it a
trajectory video plus the task instruction: it predicts the **remaining time to completion
(seconds)** at every step along the clip, and says whether the video actually matches the
instruction and whether the task succeeded.
[Model](https://huggingface.co/Alibaba-DAMO-Academy/RynnValue-8B) ·
[Code](https://github.com/alibaba-damo-academy/RynnValue) ·
[Project page](https://alibaba-damo-academy.github.io/RynnValue.github.io/)
"""
)
with gr.Row():
with gr.Column(scale=1):
video_in = gr.Video(label="Robot trajectory video", sources=["upload"])
instruction_in = gr.Textbox(
label="Task instruction",
placeholder="Put the box in the drawer and close it",
)
with gr.Row():
robot_in = gr.Dropdown(
ROBOT_CHOICES,
value=DEFAULT_ROBOT,
label="Embodiment",
allow_custom_value=True,
scale=1,
)
camera_in = gr.Dropdown(
CAMERA_CHOICES,
value=DEFAULT_CAMERA,
label="Viewpoint",
allow_custom_value=True,
scale=1,
)
run_btn = gr.Button("Analyze trajectory", variant="primary")
with gr.Column(scale=1):
video_out = gr.Video(label="Remaining-time curve", autoplay=True)
report_out = gr.Markdown()
with gr.Accordion("Advanced settings", open=False):
with gr.Row():
num_steps_in = gr.Slider(
4, 48, value=DEFAULT_NUM_STEPS, step=1,
label="Prefix steps",
info="How many points along the video are evaluated",
)
num_frames_in = gr.Slider(
8, 48, value=DEFAULT_NUM_FRAMES, step=8,
label="Frames per step",
info="Frames uniformly resampled from each prefix",
)
with gr.Row():
image_side_in = gr.Slider(
224, 512, value=DEFAULT_MAX_SIDE, step=32,
label="Max image side (px)",
info="Frames are downscaled to this before the model sees them",
)
tokens_in = gr.Slider(
32, 256, value=DEFAULT_MAX_NEW_TOKENS, step=16,
label="Analysis max new tokens",
)
gr.Markdown(
"<sub>The reference implementation evaluates one prefix step per frame with 64 "
"frames at 640 px; the defaults here are trimmed so a run fits comfortably in a "
"ZeroGPU slot.</sub>"
)
gr.Examples(
examples=[
[
"examples/franka_box_into_drawer.mp4",
"Put the box in the drawer and close it",
"a Franka single-arm robot",
"the main camera",
],
[
"examples/soar_put_green_stick_in_brown_bowl.mp4",
"Put green stick in brown bowl",
"a WidowX single-arm robot",
"the main camera",
],
[
"examples/berkeley_rpt_stack_cup.mp4",
"Pick up the yellow cup and stack it on the other cup",
"a Franka single-arm robot",
"the wrist-mounted camera",
],
[
"examples/jaco_play_pick_up_green_cup.mp4",
"Pick up the green cup",
"a Jaco single-arm robot",
"the main camera",
],
[
"examples/so101_lego_into_box.mp4",
"Put the pink lego brick into the transparent box",
"an SO-101 single-arm robot",
"the side camera",
],
[
"examples/franka_box_into_drawer.mp4",
"Fold the towel and put it in the basket",
"a Franka single-arm robot",
"the main camera",
],
],
inputs=[video_in, instruction_in, robot_in, camera_in],
outputs=[video_out, report_out],
fn=analyze_video,
cache_examples=True,
cache_mode="lazy",
examples_per_page=6,
label="Examples (the last row deliberately mismatches video and instruction)",
)
gr.Markdown(
"""<sub>Example clips — `franka_box_into_drawer` is the demo clip bundled with the
[RynnValue repo](https://github.com/alibaba-damo-academy/RynnValue) (Apache-2.0);
`soar_*`, `berkeley_rpt_*` and `jaco_play_*` are the RoboMeter benchmark clips bundled in the same
repository (MIT), originating from [Open X-Embodiment](https://robotics-transformer-x.github.io/)
(CC BY 4.0); `so101_lego_into_box` is episode 1 of
[lerobot/svla_so101_pickplace](https://huggingface.co/datasets/lerobot/svla_so101_pickplace)
(Apache-2.0).</sub>"""
)
run_btn.click(
fn=analyze_video,
inputs=[
video_in,
instruction_in,
robot_in,
camera_in,
num_steps_in,
num_frames_in,
image_side_in,
tokens_in,
],
outputs=[video_out, report_out],
api_name="analyze_video",
)
if __name__ == "__main__":
demo.launch(mcp_server=True)
|