multimodalart HF Staff commited on
Commit
0bef3b6
·
verified ·
1 Parent(s): b890615

RynnValue-8B demo: reference prefix-uniform pipeline, examples, gradio 5.50

Browse files
.gitattributes CHANGED
@@ -40,3 +40,5 @@ examples/franka_put_box_in_drawer.mp4 filter=lfs diff=lfs merge=lfs -text
40
  examples/so100_pick_up_the_cube_and_place_it_in_the_box.mp4 filter=lfs diff=lfs merge=lfs -text
41
  assets/franka_box_into_drawer.mp4 filter=lfs diff=lfs merge=lfs -text
42
  assets/so101_lego_into_box.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
40
  examples/so100_pick_up_the_cube_and_place_it_in_the_box.mp4 filter=lfs diff=lfs merge=lfs -text
41
  assets/franka_box_into_drawer.mp4 filter=lfs diff=lfs merge=lfs -text
42
  assets/so101_lego_into_box.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ examples/franka_box_into_drawer.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ examples/so101_lego_into_box.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,40 +1,62 @@
1
  ---
2
- title: RynnValue 8B
3
  emoji: 🦾
4
  colorFrom: yellow
5
  colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.24.0
8
  app_file: app.py
9
  short_description: How much longer will this robot take?
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  pinned: false
 
 
 
 
 
 
 
13
  ---
14
 
15
  # RynnValue-8B
16
 
17
  Demo of [`Alibaba-DAMO-Academy/RynnValue-8B`](https://huggingface.co/Alibaba-DAMO-Academy/RynnValue-8B),
18
  a ~9.6B robotic value model built on Qwen3-VL. Given a robot manipulation video and the task
19
- instruction, it predicts the **remaining time to completion** at every point in the clip and writes
20
- a short analysis block (video description, instruction match, success).
 
21
 
22
  The app follows the official reference implementation
23
  ([`rynn_infer/inference.py`](https://github.com/alibaba-damo-academy/RynnValue/blob/main/rynn_infer/inference.py)):
24
  prefix-uniform sampling, where evaluation step *i* resamples `frames[0:i]` to `num_frames` frames
25
- and reads the model's **last** prediction slot, so each score only conditions on frames seen so far.
26
- `plot_utils.py` is vendored from the same repo (Apache-2.0) with the trend plot memoised per
27
- prediction step for speed.
 
 
28
 
29
  Deviations from the reference, forced by the ZeroGPU time budget: fewer evaluation steps
30
- (16 vs one-per-frame), fewer frames per step (16 vs 64), smaller frames (448 px vs 640 px), and the
31
- rendered video is temporally subsampled to ≤420 frames (playback fps scaled to match, so durations
32
  and the ground-truth reference curve are unchanged). All are adjustable in *Advanced settings*.
33
 
34
- ## Example assets
 
35
 
36
- - `assets/franka_box_into_drawer.mp4` the sample clip shipped with the official RynnValue repo
37
- (Apache-2.0), transcoded to 640×360.
38
- - `assets/so101_lego_into_box.mp4` — episode 1 of
 
 
 
 
 
 
 
 
39
  [`lerobot/svla_so101_pickplace`](https://huggingface.co/datasets/lerobot/svla_so101_pickplace)
40
  (Apache-2.0), side camera.
 
 
 
 
1
  ---
2
+ title: RynnValue-8B
3
  emoji: 🦾
4
  colorFrom: yellow
5
  colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 5.50.0
8
  app_file: app.py
9
  short_description: How much longer will this robot take?
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  pinned: false
13
+ license: apache-2.0
14
+ models:
15
+ - Alibaba-DAMO-Academy/RynnValue-8B
16
+ tags:
17
+ - robotics
18
+ - value-model
19
+ - video
20
  ---
21
 
22
  # RynnValue-8B
23
 
24
  Demo of [`Alibaba-DAMO-Academy/RynnValue-8B`](https://huggingface.co/Alibaba-DAMO-Academy/RynnValue-8B),
25
  a ~9.6B robotic value model built on Qwen3-VL. Given a robot manipulation video and the task
26
+ instruction, it predicts the **remaining time to task completion (seconds)** at every point along
27
+ the clip — rendered as a curve synchronised with the video plus a short **Analysis** block
28
+ (video description, instruction `Match`, task `Success`).
29
 
30
  The app follows the official reference implementation
31
  ([`rynn_infer/inference.py`](https://github.com/alibaba-damo-academy/RynnValue/blob/main/rynn_infer/inference.py)):
32
  prefix-uniform sampling, where evaluation step *i* resamples `frames[0:i]` to `num_frames` frames
33
+ and reads the model's **last** prediction slot, so each score only conditions on frames seen so
34
+ far; the custom `pred_slot_isolated_eager` attention implementation; and a greedy Analysis
35
+ generation pass over the full-video prefix. `plot_utils.py` is vendored from the same repo
36
+ (Apache-2.0) with the trend plot memoised per prediction step (it is otherwise re-rendered once
37
+ per output video frame).
38
 
39
  Deviations from the reference, forced by the ZeroGPU time budget: fewer evaluation steps
40
+ (16 vs one-per-frame), fewer frames per step (32 vs 64), smaller frames (384 px vs 640 px), and the
41
+ rendered video is temporally subsampled to ≤320 frames (playback fps scaled to match, so durations
42
  and the ground-truth reference curve are unchanged). All are adjustable in *Advanced settings*.
43
 
44
+ `transformers` is pinned to 4.57.x (what the checkpoint's remote code targets), which requires
45
+ `huggingface-hub<1.0` — hence the Gradio 5.x SDK version.
46
 
47
+ ## Example assets & attribution
48
+
49
+ - `examples/franka_box_into_drawer.mp4` — the demo clip bundled with
50
+ [alibaba-damo-academy/RynnValue](https://github.com/alibaba-damo-academy/RynnValue)
51
+ (Apache-2.0), re-encoded to 640 px.
52
+ - `examples/soar_put_green_stick_in_brown_bowl.mp4`, `examples/berkeley_rpt_stack_cup.mp4`,
53
+ `examples/jaco_play_pick_up_green_cup.mp4` — RoboMeter benchmark clips bundled in the same
54
+ repository (MIT), originating from
55
+ [Open X-Embodiment](https://robotics-transformer-x.github.io/) (SOAR / Berkeley RPT /
56
+ Jaco Play), CC BY 4.0. The task strings are the ones used in the RoboMeter README.
57
+ - `examples/so101_lego_into_box.mp4` — episode 1 of
58
  [`lerobot/svla_so101_pickplace`](https://huggingface.co/datasets/lerobot/svla_so101_pickplace)
59
  (Apache-2.0), side camera.
60
+
61
+ The last example row pairs the Franka video with an unrelated instruction to show the
62
+ video/instruction matching behaviour (`Match: No`).
app.py CHANGED
@@ -1,10 +1,10 @@
1
  """RynnValue-8B — robotic value model demo.
2
 
3
- Predicts, for every step of a robot manipulation video, how much time is left
4
- until the task is finished, and writes a short analysis (description / does the
5
- video match the instruction / did it succeed).
6
 
7
- Faithful to the official reference implementation
8
  (https://github.com/alibaba-damo-academy/RynnValue, `rynn_infer/inference.py`):
9
  prefix-uniform sampling — for evaluation step *i* the prefix `frames[0:i]` is
10
  resampled to `num_frames` frames and the model's **last** prediction slot is
@@ -15,7 +15,7 @@ import os
15
 
16
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
17
 
18
- import spaces # noqa: E402 (must come before torch)
19
 
20
  import re # noqa: E402
21
  import tempfile # noqa: E402
@@ -32,25 +32,26 @@ from plot_utils import save_video_with_trend # noqa: E402
32
 
33
  MODEL_ID = "Alibaba-DAMO-Academy/RynnValue-8B"
34
 
35
- # Defaults — every one of these is also the default of its UI component so that
36
- # clicking an example and pressing "Analyze" behave identically.
37
  DEFAULT_ROBOT = "a Franka single-arm robot"
38
  DEFAULT_CAMERA = "the main camera"
39
  DEFAULT_NUM_STEPS = 16
40
- DEFAULT_NUM_FRAMES = 16
41
- DEFAULT_MAX_SIDE = 448
42
  DEFAULT_MAX_NEW_TOKENS = 128
43
- BATCH_SIZE = 2
44
 
45
- # Rendering budget: the input video is temporally subsampled to at most this
46
- # many frames (playback fps is scaled to match, so wall-clock duration and the
47
- # ground-truth "remaining time" reference curve stay correct).
48
- MAX_RENDER_FRAMES = 420
49
  DISPLAY_MAX_SIDE = 640
50
 
51
  ROBOT_CHOICES = [
52
  "a Franka single-arm robot",
53
  "an SO-101 single-arm robot",
 
 
54
  "a Koch dual-arm robot",
55
  "an xArm single-arm robot",
56
  "an Trossen dual-arm robot",
@@ -67,78 +68,78 @@ CAMERA_CHOICES = [
67
  # --------------------------------------------------------------------------- #
68
  # Model
69
  # --------------------------------------------------------------------------- #
70
- config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
 
71
  # The exported config predates the attn-impl field, so force the custom
72
- # prediction-slot isolation attention exactly like the reference script does.
73
- config._attn_implementation = "pred_slot_isolated_eager"
74
 
75
  model = AutoModel.from_pretrained(
76
  MODEL_ID,
77
- config=config,
78
  trust_remote_code=True,
79
  torch_dtype=torch.bfloat16,
80
  )
81
- model = model.to("cuda").eval()
 
 
 
82
 
83
  processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
84
  tokenizer = processor.tokenizer
85
  EOS_TOKEN_ID = tokenizer.convert_tokens_to_ids("<|im_end|>")
 
86
 
87
  # --------------------------------------------------------------------------- #
88
- # Video / sampling helpers (mirrors rynn_infer/inference.py)
89
  # --------------------------------------------------------------------------- #
90
 
91
 
92
- def _resize_to_max_side(img, max_side):
93
- if max_side <= 0:
94
- return img
95
  w, h = img.size
96
- if max(w, h) <= max_side:
97
  return img
98
- scale = max_side / max(w, h)
99
  return img.resize((max(1, int(round(w * scale))), max(1, int(round(h * scale)))), Image.BICUBIC)
100
 
101
 
102
- def load_video_frames(video_path):
103
- """Decode a video into (frames, fps).
104
 
105
- Frames are temporally subsampled to at most ``MAX_RENDER_FRAMES`` and the
106
- returned fps is scaled accordingly, so the clip keeps its real duration.
 
107
  """
108
  if not video_path or not os.path.isfile(video_path):
109
- raise gr.Error("Please provide a video file.")
110
 
111
  reader = imageio.get_reader(video_path)
112
- meta = reader.get_meta_data()
113
- fps = float(meta.get("fps") or 30.0)
114
- duration = meta.get("duration")
115
- est_total = int(duration * fps) if duration else 0
116
-
117
- stride = max(1, int(np.ceil(est_total / MAX_RENDER_FRAMES))) if est_total else 1
118
-
119
- frames = []
120
  try:
 
 
 
 
 
 
 
 
 
121
  for i, frame in enumerate(reader):
122
  if i % stride:
123
  continue
124
- img = Image.fromarray(frame).convert("RGB")
125
- frames.append(_resize_to_max_side(img, DISPLAY_MAX_SIDE))
 
126
  finally:
127
  reader.close()
128
 
129
  if not frames:
130
- raise gr.Error("No frames could be decoded from that video.")
131
-
132
- # Guard against a bad duration estimate.
133
- while len(frames) > MAX_RENDER_FRAMES:
134
- frames = frames[::2]
135
- stride *= 2
136
 
137
- return frames, fps / stride
138
 
139
-
140
- def sample_frame_indices(total, num_frames):
141
- """Uniformly pick ``num_frames`` indices from ``total`` frames."""
142
  if num_frames <= 0 or num_frames >= total:
143
  return list(range(total))
144
  if num_frames == 1:
@@ -152,7 +153,9 @@ _MATCH_RE = re.compile(r"-\s*Match:\s*(Yes|No)", re.IGNORECASE)
152
  _SUCCESS_RE = re.compile(r"-\s*Success:\s*(Yes|No)", re.IGNORECASE)
153
 
154
 
155
- def parse_analysis(text):
 
 
156
  def _first(pattern):
157
  m = pattern.search(text)
158
  return m.group(1).strip() if m else None
@@ -164,22 +167,32 @@ def parse_analysis(text):
164
  }
165
 
166
 
167
- def _estimate_duration(*args, **kwargs):
168
- """ZeroGPU duration estimate — scales with the prefill work requested."""
169
- try:
170
- num_steps = int(args[4]) if len(args) > 4 else DEFAULT_NUM_STEPS
171
- num_frames = int(args[5]) if len(args) > 5 else DEFAULT_NUM_FRAMES
172
- except (TypeError, ValueError):
173
- num_steps, num_frames = DEFAULT_NUM_STEPS, DEFAULT_NUM_FRAMES
174
- return int(min(300, 45 + 0.09 * num_steps * num_frames))
 
 
 
 
 
 
 
 
 
 
175
 
176
 
177
  # --------------------------------------------------------------------------- #
178
  # Inference
179
  # --------------------------------------------------------------------------- #
180
  @spaces.GPU(duration=_estimate_duration)
181
- def analyze(
182
- video: str,
183
  instruction: str,
184
  robot_description: str = DEFAULT_ROBOT,
185
  camera_description: str = DEFAULT_CAMERA,
@@ -189,46 +202,56 @@ def analyze(
189
  max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
190
  progress=gr.Progress(),
191
  ):
192
- """Score a robot manipulation video with RynnValue-8B.
193
 
194
- Returns an annotated video (frames + predicted remaining-time curve) and a
195
- markdown report with the model's description / instruction-match / success
196
- judgement.
 
 
197
 
198
  Args:
199
- video: path to a robot manipulation video (mp4).
200
- instruction: the task the robot was asked to do, e.g. "Put the box in the drawer and close it".
201
- robot_description: embodiment phrase for the meta block, e.g. "a Franka single-arm robot".
202
- camera_description: viewpoint phrase for the meta block, e.g. "the main camera".
203
- num_steps: number of evaluation points along the video (one forward pass each).
204
- num_frames: frames fed to the model per evaluation point.
205
- max_image_side: frames are downscaled to this longer side before the vision tower.
206
- max_new_tokens: token budget for the generated analysis block.
 
 
 
207
  """
208
  if not instruction or not instruction.strip():
209
- raise gr.Error("Please give the task instruction the robot was asked to follow.")
210
  instruction = instruction.strip()
211
- robot_description = (robot_description or DEFAULT_ROBOT).strip()
212
- camera_description = (camera_description or DEFAULT_CAMERA).strip()
 
 
 
 
 
 
213
  num_steps = int(num_steps)
214
  num_frames = int(num_frames)
215
  max_image_side = int(max_image_side)
216
  max_new_tokens = int(max_new_tokens)
217
 
218
- t_all = time.perf_counter()
219
- progress(0.0, desc="Decoding video…")
220
- frames, fps = load_video_frames(video)
221
  total = len(frames)
222
- model_images = [_resize_to_max_side(f, max_image_side) for f in frames]
223
-
224
  eval_indices = sample_frame_indices(total, num_steps)
 
225
 
226
  def build_prefix_sample(end_idx):
227
  frame_idx = np.linspace(0, end_idx, num_frames, dtype=int)
228
- prefix_images = [model_images[j] for j in frame_idx]
229
  return processor.process_episode(
230
  instruction=instruction,
231
- images=prefix_images,
232
  robot_description=robot_description,
233
  camera_description=camera_description,
234
  )
@@ -237,9 +260,7 @@ def analyze(
237
  batch_kwargs = dict(
238
  input_ids=torch.cat([s["input_ids"] for s in samples], dim=0).to("cuda").long(),
239
  attention_mask=torch.cat([s["attention_mask"] for s in samples], dim=0).to("cuda").long(),
240
- pixel_values=torch.cat(
241
- [s["pixel_values"].flatten(0, 1) for s in samples], dim=0
242
- ).to("cuda", dtype=torch.bfloat16),
243
  image_grid_thw=torch.cat(
244
  [s["image_grid_thw"].flatten(0, 1) for s in samples], dim=0
245
  ).to("cuda").long(),
@@ -257,30 +278,32 @@ def analyze(
257
  pred = pred[:, 0]
258
  return pred.float().reshape(-1).tolist()
259
 
260
- t_gpu = time.perf_counter()
261
- pred_value = []
262
- final_sample = None
263
- batch = []
264
  for step, end_idx in enumerate(eval_indices):
265
  sample = build_prefix_sample(end_idx)
266
  if step == len(eval_indices) - 1:
267
  final_sample = sample
268
  batch.append(sample)
269
- if len(batch) >= BATCH_SIZE or step == len(eval_indices) - 1:
270
  pred_value.extend(run_batch(batch))
271
  batch = []
272
  progress(
273
- 0.05 + 0.7 * len(pred_value) / len(eval_indices),
274
- desc=f"Value pass {len(pred_value)}/{len(eval_indices)}",
275
  )
 
276
 
277
- progress(0.78, desc="Writing analysis…")
 
 
278
  input_ids = final_sample["input_ids"].to("cuda").long()
279
  with torch.inference_mode():
280
  gen_out = model.generate(
281
  input_ids=input_ids,
282
  attention_mask=final_sample["attention_mask"].to("cuda").long(),
283
- pixel_values=final_sample["pixel_values"].flatten(0, 1).to("cuda", dtype=torch.bfloat16),
284
  image_grid_thw=final_sample["image_grid_thw"].flatten(0, 1).to("cuda").long(),
285
  max_new_tokens=max_new_tokens,
286
  do_sample=False,
@@ -291,73 +314,80 @@ def analyze(
291
  )
292
  analysis_text = tokenizer.decode(gen_out[0, input_ids.shape[1]:], skip_special_tokens=True)
293
  analysis = parse_analysis(analysis_text)
294
- gpu_seconds = time.perf_counter() - t_gpu
295
 
 
296
  progress(0.85, desc="Rendering trend video…")
297
- out_file = os.path.join(tempfile.mkdtemp(), "rynnvalue_trend.mp4")
 
298
  save_video_with_trend(
299
  images=frames,
300
  value=pred_value,
301
- output_path=out_file,
302
- fps=fps,
303
  title="Remaining Time (s)",
304
  task_title=instruction,
305
  sampled_indices=eval_indices,
306
  )
 
 
307
 
308
- total_seconds = time.perf_counter() - t_all
309
- first_v, last_v = pred_value[0], pred_value[-1]
310
- true_total = (eval_indices[-1] - eval_indices[0]) / float(fps)
311
 
312
- def _tick(v):
313
  if v is None:
314
  return "—"
315
- return {"yes": "✅ Yes", "no": "❌ No"}.get(v.lower(), v)
316
 
317
  report = f"""### Analysis
318
-
319
  - **Video description:** {analysis['description'] or '—'}
320
- - **Matches the instruction:** {_tick(analysis['match'])}
321
- - **Task succeeded:** {_tick(analysis['success'])}
322
-
323
- ### Predicted time-to-completion
324
 
325
- | | |
326
- |---|---|
327
- | At the first frame | **{first_v:.2f} s** remaining |
328
- | At the last frame | **{last_v:.2f} s** remaining |
329
- | Actual clip length | {true_total:.2f} s |
330
 
331
- <sub>{len(eval_indices)} evaluation steps · {num_frames} frames/step · {total} frames rendered @ {fps:.1f} fps ·
332
- {gpu_seconds:.1f} s model time · {total_seconds:.1f} s total</sub>
 
333
 
334
- <details><summary>Raw model output</summary>
335
 
336
  ```
337
  {analysis_text.strip()}
338
  ```
339
  </details>"""
340
 
341
- return out_file, report
 
 
 
 
 
 
342
 
343
 
344
  # --------------------------------------------------------------------------- #
345
  # UI
346
  # --------------------------------------------------------------------------- #
347
  CSS = """
348
- #col-container { max-width: 1180px; margin: 0 auto; }
349
  .dark .gradio-container { color: var(--body-text-color); }
350
  """
351
 
352
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="RynnValue-8B") as demo:
353
  with gr.Column(elem_id="col-container"):
354
  gr.Markdown(
355
- """# RynnValue-8B — how much longer will this robot take?
356
 
357
- A robotic value model from **Alibaba DAMO Academy**: given a manipulation video and the task
358
- instruction, it predicts the **remaining time to completion** at every point in the clip, and
359
- writes a short analysis (what it sees, whether the video matches the instruction, whether the
360
- task succeeded).
361
 
362
  [Model](https://huggingface.co/Alibaba-DAMO-Academy/RynnValue-8B) ·
363
  [Code](https://github.com/alibaba-damo-academy/RynnValue) ·
@@ -367,98 +397,131 @@ task succeeded).
367
 
368
  with gr.Row():
369
  with gr.Column(scale=1):
370
- video = gr.Video(label="Robot manipulation video", sources=["upload"])
371
- instruction = gr.Textbox(
372
  label="Task instruction",
373
  placeholder="Put the box in the drawer and close it",
374
  )
375
  with gr.Row():
376
- robot_description = gr.Dropdown(
377
  ROBOT_CHOICES,
378
  value=DEFAULT_ROBOT,
379
  label="Embodiment",
380
  allow_custom_value=True,
 
381
  )
382
- camera_description = gr.Dropdown(
383
  CAMERA_CHOICES,
384
  value=DEFAULT_CAMERA,
385
  label="Viewpoint",
386
  allow_custom_value=True,
 
387
  )
388
- run = gr.Button("Analyze", variant="primary")
389
  with gr.Column(scale=1):
390
- out_video = gr.Video(label="Predicted value trend", autoplay=True)
391
- report = gr.Markdown()
392
 
393
  with gr.Accordion("Advanced settings", open=False):
394
  with gr.Row():
395
- num_steps = gr.Slider(
396
  4, 48, value=DEFAULT_NUM_STEPS, step=1,
397
- label="Evaluation steps",
398
- info="Points on the curve. One forward pass each.",
399
  )
400
- num_frames = gr.Slider(
401
- 4, 32, value=DEFAULT_NUM_FRAMES, step=1,
402
  label="Frames per step",
403
- info="Frames resampled from the prefix seen so far.",
404
  )
405
  with gr.Row():
406
- max_image_side = gr.Slider(
407
- 256, 640, value=DEFAULT_MAX_SIDE, step=32,
408
  label="Max image side (px)",
 
409
  )
410
- max_new_tokens = gr.Slider(
411
  32, 256, value=DEFAULT_MAX_NEW_TOKENS, step=16,
412
- label="Max analysis tokens",
413
  )
414
  gr.Markdown(
415
- "The reference implementation uses 64 frames per step and one step per frame; "
416
- "the defaults here are trimmed so a run fits comfortably in a ZeroGPU slot."
 
417
  )
418
 
419
  gr.Examples(
420
  examples=[
421
  [
422
- "assets/franka_box_into_drawer.mp4",
423
  "Put the box in the drawer and close it",
424
  "a Franka single-arm robot",
425
  "the main camera",
426
  ],
427
  [
428
- "assets/so101_lego_into_box.mp4",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  "Put the pink lego brick into the transparent box",
430
  "an SO-101 single-arm robot",
431
  "the side camera",
432
  ],
433
  [
434
- "assets/franka_box_into_drawer.mp4",
435
- "Fold the blue towel and put it on the shelf",
436
  "a Franka single-arm robot",
437
  "the main camera",
438
  ],
439
  ],
440
- inputs=[video, instruction, robot_description, camera_description],
441
- outputs=[out_video, report],
442
- fn=analyze,
443
  cache_examples=True,
444
  cache_mode="lazy",
445
- label="Examples (the third one is deliberately mismatched)",
 
 
 
 
 
 
 
 
 
 
 
446
  )
447
 
448
- run.click(
449
- analyze,
450
  inputs=[
451
- video,
452
- instruction,
453
- robot_description,
454
- camera_description,
455
- num_steps,
456
- num_frames,
457
- max_image_side,
458
- max_new_tokens,
459
  ],
460
- outputs=[out_video, report],
461
- api_name="analyze",
462
  )
463
 
464
  if __name__ == "__main__":
 
1
  """RynnValue-8B — robotic value model demo.
2
 
3
+ Given a robot manipulation video and the task instruction, predicts how much
4
+ time is left until the task is finished at every point along the clip, plus a
5
+ short analysis block (description / instruction match / success).
6
 
7
+ Follows the official reference implementation
8
  (https://github.com/alibaba-damo-academy/RynnValue, `rynn_infer/inference.py`):
9
  prefix-uniform sampling — for evaluation step *i* the prefix `frames[0:i]` is
10
  resampled to `num_frames` frames and the model's **last** prediction slot is
 
15
 
16
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
17
 
18
+ import spaces # noqa: E402 (must precede torch / transformers)
19
 
20
  import re # noqa: E402
21
  import tempfile # noqa: E402
 
32
 
33
  MODEL_ID = "Alibaba-DAMO-Academy/RynnValue-8B"
34
 
35
+ # Defaults — each is also the default of its UI component, so clicking an
36
+ # example and pressing "Analyze" behave identically.
37
  DEFAULT_ROBOT = "a Franka single-arm robot"
38
  DEFAULT_CAMERA = "the main camera"
39
  DEFAULT_NUM_STEPS = 16
40
+ DEFAULT_NUM_FRAMES = 32
41
+ DEFAULT_MAX_SIDE = 384
42
  DEFAULT_MAX_NEW_TOKENS = 128
 
43
 
44
+ # Rendering budget: the input clip is temporally subsampled to at most this many
45
+ # frames and the playback fps is scaled to match, so wall-clock duration and the
46
+ # ground-truth "remaining time" reference curve are unchanged.
47
+ MAX_RENDER_FRAMES = 320
48
  DISPLAY_MAX_SIDE = 640
49
 
50
  ROBOT_CHOICES = [
51
  "a Franka single-arm robot",
52
  "an SO-101 single-arm robot",
53
+ "a WidowX single-arm robot",
54
+ "a Jaco single-arm robot",
55
  "a Koch dual-arm robot",
56
  "an xArm single-arm robot",
57
  "an Trossen dual-arm robot",
 
68
  # --------------------------------------------------------------------------- #
69
  # Model
70
  # --------------------------------------------------------------------------- #
71
+ print(f"Loading {MODEL_ID} ...", flush=True)
72
+ _config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
73
  # The exported config predates the attn-impl field, so force the custom
74
+ # prediction-slot isolation attention (mirrors rynn_infer/inference.py).
75
+ _config._attn_implementation = "pred_slot_isolated_eager"
76
 
77
  model = AutoModel.from_pretrained(
78
  MODEL_ID,
79
+ config=_config,
80
  trust_remote_code=True,
81
  torch_dtype=torch.bfloat16,
82
  )
83
+ # `torch_dtype=` leaves the value heads in fp32 (they are constructed with an
84
+ # explicit dtype), so the trailing `dtype=` cast is required — the reference
85
+ # script does the same `model.to(device=..., dtype=...)`.
86
+ model = model.to(device="cuda", dtype=torch.bfloat16).eval()
87
 
88
  processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
89
  tokenizer = processor.tokenizer
90
  EOS_TOKEN_ID = tokenizer.convert_tokens_to_ids("<|im_end|>")
91
+ print("Model ready.", flush=True)
92
 
93
  # --------------------------------------------------------------------------- #
94
+ # Video / sampling helpers (ported from rynn_infer/inference.py)
95
  # --------------------------------------------------------------------------- #
96
 
97
 
98
+ def _resize_long_side(img: Image.Image, max_side: int) -> Image.Image:
 
 
99
  w, h = img.size
100
+ if max_side <= 0 or max(w, h) <= max_side:
101
  return img
102
+ scale = max_side / float(max(w, h))
103
  return img.resize((max(1, int(round(w * scale))), max(1, int(round(h * scale)))), Image.BICUBIC)
104
 
105
 
106
+ def load_video(video_path: str):
107
+ """Decode a video into ``(frames, fps)``.
108
 
109
+ Frames are downscaled to ``DISPLAY_MAX_SIDE`` on the fly and strided so that
110
+ at most ``MAX_RENDER_FRAMES`` are kept; ``fps`` is scaled accordingly so the
111
+ rendered clip keeps real-time playback speed.
112
  """
113
  if not video_path or not os.path.isfile(video_path):
114
+ raise gr.Error("Please upload a video first.")
115
 
116
  reader = imageio.get_reader(video_path)
 
 
 
 
 
 
 
 
117
  try:
118
+ meta = reader.get_meta_data()
119
+ src_fps = float(meta.get("fps") or 30.0)
120
+ duration = float(meta.get("duration") or 0.0)
121
+ est_total = int(duration * src_fps) if duration > 0 else 0
122
+ stride = 1
123
+ if est_total > MAX_RENDER_FRAMES:
124
+ stride = int(np.ceil(est_total / float(MAX_RENDER_FRAMES)))
125
+
126
+ frames = []
127
  for i, frame in enumerate(reader):
128
  if i % stride:
129
  continue
130
+ frames.append(_resize_long_side(Image.fromarray(frame).convert("RGB"), DISPLAY_MAX_SIDE))
131
+ if len(frames) >= MAX_RENDER_FRAMES + 8:
132
+ break
133
  finally:
134
  reader.close()
135
 
136
  if not frames:
137
+ raise gr.Error("Could not decode any frame from this video.")
138
+ return frames, src_fps / float(stride)
 
 
 
 
139
 
 
140
 
141
+ def sample_frame_indices(total: int, num_frames: int):
142
+ """Uniformly pick ``num_frames`` indices out of ``total`` frames."""
 
143
  if num_frames <= 0 or num_frames >= total:
144
  return list(range(total))
145
  if num_frames == 1:
 
153
  _SUCCESS_RE = re.compile(r"-\s*Success:\s*(Yes|No)", re.IGNORECASE)
154
 
155
 
156
+ def parse_analysis(text: str) -> dict:
157
+ """Extract description / match / success from the generated Analysis block."""
158
+
159
  def _first(pattern):
160
  m = pattern.search(text)
161
  return m.group(1).strip() if m else None
 
167
  }
168
 
169
 
170
+ def _estimate_duration(
171
+ video_path=None,
172
+ instruction="",
173
+ robot_description=DEFAULT_ROBOT,
174
+ camera_description=DEFAULT_CAMERA,
175
+ num_steps=DEFAULT_NUM_STEPS,
176
+ num_frames=DEFAULT_NUM_FRAMES,
177
+ max_image_side=DEFAULT_MAX_SIDE,
178
+ max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
179
+ *args,
180
+ **kwargs,
181
+ ):
182
+ """ZeroGPU duration estimate; scales with the prefill work requested."""
183
+ steps = int(num_steps or DEFAULT_NUM_STEPS)
184
+ frames = int(num_frames or DEFAULT_NUM_FRAMES)
185
+ side = int(max_image_side or DEFAULT_MAX_SIDE)
186
+ per_step = 0.75 * (frames / 32.0) * (side / 384.0) ** 2
187
+ return int(min(240, 25 + steps * per_step + 0.05 * int(max_new_tokens or 128)))
188
 
189
 
190
  # --------------------------------------------------------------------------- #
191
  # Inference
192
  # --------------------------------------------------------------------------- #
193
  @spaces.GPU(duration=_estimate_duration)
194
+ def analyze_video(
195
+ video_path: str,
196
  instruction: str,
197
  robot_description: str = DEFAULT_ROBOT,
198
  camera_description: str = DEFAULT_CAMERA,
 
202
  max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
203
  progress=gr.Progress(),
204
  ):
205
+ """Predict how much time is left before a robot finishes a task.
206
 
207
+ Runs RynnValue-8B over a robot manipulation video: for each evaluated step
208
+ the prefix of the video seen so far is uniformly resampled and the model
209
+ predicts the remaining time to task completion in seconds. It also generates
210
+ an Analysis block (what happens in the video, whether the video matches the
211
+ instruction, whether the task succeeded).
212
 
213
  Args:
214
+ video_path: Path to the robot trajectory video (mp4).
215
+ instruction: Natural-language task the robot is supposed to accomplish.
216
+ robot_description: Embodiment description, e.g. "a Franka single-arm robot".
217
+ camera_description: Camera viewpoint description, e.g. "the main camera".
218
+ num_steps: Number of prefix steps evaluated along the video.
219
+ num_frames: Frames uniformly resampled from each prefix.
220
+ max_image_side: Longer side each frame is resized to before the model sees it.
221
+ max_new_tokens: Token budget for the generated Analysis block.
222
+
223
+ Returns:
224
+ A video with the synchronized remaining-time curve, and a markdown report.
225
  """
226
  if not instruction or not instruction.strip():
227
+ raise gr.Error("Please give the task instruction the robot is supposed to follow.")
228
  instruction = instruction.strip()
229
+ robot_description = (robot_description or "").strip() or None
230
+ camera_description = (camera_description or "").strip() or None
231
+ if robot_description is None and camera_description is None:
232
+ raise gr.Error(
233
+ "This checkpoint was trained with meta information — fill in the "
234
+ "embodiment and/or viewpoint description."
235
+ )
236
+
237
  num_steps = int(num_steps)
238
  num_frames = int(num_frames)
239
  max_image_side = int(max_image_side)
240
  max_new_tokens = int(max_new_tokens)
241
 
242
+ t0 = time.perf_counter()
243
+ progress(0.05, desc="Decoding video…")
244
+ frames, fps = load_video(video_path)
245
  total = len(frames)
246
+ model_frames = [_resize_long_side(f, max_image_side) for f in frames]
 
247
  eval_indices = sample_frame_indices(total, num_steps)
248
+ t_decode = time.perf_counter() - t0
249
 
250
  def build_prefix_sample(end_idx):
251
  frame_idx = np.linspace(0, end_idx, num_frames, dtype=int)
 
252
  return processor.process_episode(
253
  instruction=instruction,
254
+ images=[model_frames[j] for j in frame_idx],
255
  robot_description=robot_description,
256
  camera_description=camera_description,
257
  )
 
260
  batch_kwargs = dict(
261
  input_ids=torch.cat([s["input_ids"] for s in samples], dim=0).to("cuda").long(),
262
  attention_mask=torch.cat([s["attention_mask"] for s in samples], dim=0).to("cuda").long(),
263
+ pixel_values=torch.cat([s["pixel_values"].flatten(0, 1) for s in samples], dim=0).to("cuda"),
 
 
264
  image_grid_thw=torch.cat(
265
  [s["image_grid_thw"].flatten(0, 1) for s in samples], dim=0
266
  ).to("cuda").long(),
 
278
  pred = pred[:, 0]
279
  return pred.float().reshape(-1).tolist()
280
 
281
+ t1 = time.perf_counter()
282
+ batch_size = 4 if (num_frames <= 32 and max_image_side <= 448) else 2
283
+ pred_value, batch, final_sample = [], [], None
 
284
  for step, end_idx in enumerate(eval_indices):
285
  sample = build_prefix_sample(end_idx)
286
  if step == len(eval_indices) - 1:
287
  final_sample = sample
288
  batch.append(sample)
289
+ if len(batch) >= batch_size or step == len(eval_indices) - 1:
290
  pred_value.extend(run_batch(batch))
291
  batch = []
292
  progress(
293
+ 0.05 + 0.65 * len(pred_value) / max(1, len(eval_indices)),
294
+ desc=f"Value prediction {len(pred_value)}/{len(eval_indices)}",
295
  )
296
+ t_value = time.perf_counter() - t1
297
 
298
+ # ---- Analysis pass on the full-video prefix ---------------------------- #
299
+ progress(0.72, desc="Generating analysis…")
300
+ t2 = time.perf_counter()
301
  input_ids = final_sample["input_ids"].to("cuda").long()
302
  with torch.inference_mode():
303
  gen_out = model.generate(
304
  input_ids=input_ids,
305
  attention_mask=final_sample["attention_mask"].to("cuda").long(),
306
+ pixel_values=final_sample["pixel_values"].flatten(0, 1).to("cuda"),
307
  image_grid_thw=final_sample["image_grid_thw"].flatten(0, 1).to("cuda").long(),
308
  max_new_tokens=max_new_tokens,
309
  do_sample=False,
 
314
  )
315
  analysis_text = tokenizer.decode(gen_out[0, input_ids.shape[1]:], skip_special_tokens=True)
316
  analysis = parse_analysis(analysis_text)
317
+ t_gen = time.perf_counter() - t2
318
 
319
+ # ---- Render ------------------------------------------------------------ #
320
  progress(0.85, desc="Rendering trend video…")
321
+ t3 = time.perf_counter()
322
+ out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
323
  save_video_with_trend(
324
  images=frames,
325
  value=pred_value,
326
+ output_path=out_path,
327
+ fps=max(1.0, float(fps)),
328
  title="Remaining Time (s)",
329
  task_title=instruction,
330
  sampled_indices=eval_indices,
331
  )
332
+ t_render = time.perf_counter() - t3
333
+ total_time = time.perf_counter() - t0
334
 
335
+ clip_len = (total - 1) / float(fps)
336
+ first_pred, last_pred = float(pred_value[0]), float(pred_value[-1])
 
337
 
338
+ def _badge(v):
339
  if v is None:
340
  return "—"
341
+ return "✅ Yes" if v.lower() == "yes" else "❌ No"
342
 
343
  report = f"""### Analysis
 
344
  - **Video description:** {analysis['description'] or '—'}
345
+ - **Matches the instruction:** {_badge(analysis['match'])}
346
+ - **Task succeeded:** {_badge(analysis['success'])}
 
 
347
 
348
+ ### Predicted remaining time
349
+ | | predicted | actual (clip) |
350
+ |---|---|---|
351
+ | at the first frame | **{first_pred:.2f} s** | {clip_len:.2f} s |
352
+ | at the last frame | **{last_pred:.2f} s** | 0.00 s |
353
 
354
+ <sub>{len(eval_indices)} prefix steps · {num_frames} frames/step · {max_image_side} px ·
355
+ decode {t_decode:.1f}s · value {t_value:.1f}s · analysis {t_gen:.1f}s · render {t_render:.1f}s ·
356
+ total {total_time:.1f}s</sub>
357
 
358
+ <details><summary>Raw generation</summary>
359
 
360
  ```
361
  {analysis_text.strip()}
362
  ```
363
  </details>"""
364
 
365
+ print(
366
+ f"[timing] decode={t_decode:.2f}s value={t_value:.2f}s gen={t_gen:.2f}s "
367
+ f"render={t_render:.2f}s total={total_time:.2f}s steps={len(eval_indices)} "
368
+ f"frames={num_frames} side={max_image_side}",
369
+ flush=True,
370
+ )
371
+ return out_path, report
372
 
373
 
374
  # --------------------------------------------------------------------------- #
375
  # UI
376
  # --------------------------------------------------------------------------- #
377
  CSS = """
378
+ #col-container { max-width: 1200px; margin: 0 auto; }
379
  .dark .gradio-container { color: var(--body-text-color); }
380
  """
381
 
382
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="RynnValue-8B") as demo:
383
  with gr.Column(elem_id="col-container"):
384
  gr.Markdown(
385
+ """# RynnValue-8B — how much longer will the robot take?
386
 
387
+ A general-purpose value model for robot manipulation from Alibaba DAMO Academy. Give it a
388
+ trajectory video plus the task instruction: it predicts the **remaining time to completion
389
+ (seconds)** at every step along the clip, and says whether the video actually matches the
390
+ instruction and whether the task succeeded.
391
 
392
  [Model](https://huggingface.co/Alibaba-DAMO-Academy/RynnValue-8B) ·
393
  [Code](https://github.com/alibaba-damo-academy/RynnValue) ·
 
397
 
398
  with gr.Row():
399
  with gr.Column(scale=1):
400
+ video_in = gr.Video(label="Robot trajectory video", sources=["upload"])
401
+ instruction_in = gr.Textbox(
402
  label="Task instruction",
403
  placeholder="Put the box in the drawer and close it",
404
  )
405
  with gr.Row():
406
+ robot_in = gr.Dropdown(
407
  ROBOT_CHOICES,
408
  value=DEFAULT_ROBOT,
409
  label="Embodiment",
410
  allow_custom_value=True,
411
+ scale=1,
412
  )
413
+ camera_in = gr.Dropdown(
414
  CAMERA_CHOICES,
415
  value=DEFAULT_CAMERA,
416
  label="Viewpoint",
417
  allow_custom_value=True,
418
+ scale=1,
419
  )
420
+ run_btn = gr.Button("Analyze trajectory", variant="primary")
421
  with gr.Column(scale=1):
422
+ video_out = gr.Video(label="Remaining-time curve", autoplay=True)
423
+ report_out = gr.Markdown()
424
 
425
  with gr.Accordion("Advanced settings", open=False):
426
  with gr.Row():
427
+ num_steps_in = gr.Slider(
428
  4, 48, value=DEFAULT_NUM_STEPS, step=1,
429
+ label="Prefix steps",
430
+ info="How many points along the video are evaluated",
431
  )
432
+ num_frames_in = gr.Slider(
433
+ 8, 64, value=DEFAULT_NUM_FRAMES, step=8,
434
  label="Frames per step",
435
+ info="Frames uniformly resampled from each prefix",
436
  )
437
  with gr.Row():
438
+ image_side_in = gr.Slider(
439
+ 224, 640, value=DEFAULT_MAX_SIDE, step=32,
440
  label="Max image side (px)",
441
+ info="Frames are downscaled to this before the model sees them",
442
  )
443
+ tokens_in = gr.Slider(
444
  32, 256, value=DEFAULT_MAX_NEW_TOKENS, step=16,
445
+ label="Analysis max new tokens",
446
  )
447
  gr.Markdown(
448
+ "<sub>The reference implementation evaluates one prefix step per frame with 64 "
449
+ "frames at 640 px; the defaults here are trimmed so a run fits comfortably in a "
450
+ "ZeroGPU slot.</sub>"
451
  )
452
 
453
  gr.Examples(
454
  examples=[
455
  [
456
+ "examples/franka_box_into_drawer.mp4",
457
  "Put the box in the drawer and close it",
458
  "a Franka single-arm robot",
459
  "the main camera",
460
  ],
461
  [
462
+ "examples/soar_put_green_stick_in_brown_bowl.mp4",
463
+ "Put green stick in brown bowl",
464
+ "a WidowX single-arm robot",
465
+ "the main camera",
466
+ ],
467
+ [
468
+ "examples/berkeley_rpt_stack_cup.mp4",
469
+ "Pick up the yellow cup and stack it on the other cup",
470
+ "a Franka single-arm robot",
471
+ "the wrist-mounted camera",
472
+ ],
473
+ [
474
+ "examples/jaco_play_pick_up_green_cup.mp4",
475
+ "Pick up the green cup",
476
+ "a Jaco single-arm robot",
477
+ "the main camera",
478
+ ],
479
+ [
480
+ "examples/so101_lego_into_box.mp4",
481
  "Put the pink lego brick into the transparent box",
482
  "an SO-101 single-arm robot",
483
  "the side camera",
484
  ],
485
  [
486
+ "examples/franka_box_into_drawer.mp4",
487
+ "Fold the towel and put it in the basket",
488
  "a Franka single-arm robot",
489
  "the main camera",
490
  ],
491
  ],
492
+ inputs=[video_in, instruction_in, robot_in, camera_in],
493
+ outputs=[video_out, report_out],
494
+ fn=analyze_video,
495
  cache_examples=True,
496
  cache_mode="lazy",
497
+ examples_per_page=6,
498
+ label="Examples (the last row deliberately mismatches video and instruction)",
499
+ )
500
+
501
+ gr.Markdown(
502
+ """<sub>Example clips — `franka_box_into_drawer` is the demo clip bundled with the
503
+ [RynnValue repo](https://github.com/alibaba-damo-academy/RynnValue) (Apache-2.0);
504
+ `soar_*`, `berkeley_rpt_*` and `jaco_play_*` are the RoboMeter benchmark clips bundled in the same
505
+ repository (MIT), originating from [Open X-Embodiment](https://robotics-transformer-x.github.io/)
506
+ (CC BY 4.0); `so101_lego_into_box` is episode 1 of
507
+ [lerobot/svla_so101_pickplace](https://huggingface.co/datasets/lerobot/svla_so101_pickplace)
508
+ (Apache-2.0).</sub>"""
509
  )
510
 
511
+ run_btn.click(
512
+ fn=analyze_video,
513
  inputs=[
514
+ video_in,
515
+ instruction_in,
516
+ robot_in,
517
+ camera_in,
518
+ num_steps_in,
519
+ num_frames_in,
520
+ image_side_in,
521
+ tokens_in,
522
  ],
523
+ outputs=[video_out, report_out],
524
+ api_name="analyze_video",
525
  )
526
 
527
  if __name__ == "__main__":
{assets → examples}/franka_box_into_drawer.mp4 RENAMED
File without changes
examples/put_the_box_in_the_drawer_and_close_it.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:48590c921c75ba487706f86b24341209ce3eba354d75587b0be36ddad0d89951
3
- size 1135507
 
 
 
 
examples/so100_pick_up_the_cube_and_place_it_in_the_box.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:094306960c45bb1d81aa60bc618323ef0b0697e034ea28bcefe2401d1d37b271
3
- size 654381
 
 
 
 
{assets → examples}/so101_lego_into_box.mp4 RENAMED
File without changes
requirements.txt CHANGED
@@ -1,3 +1,5 @@
 
 
1
  transformers==4.57.6
2
  accelerate
3
  torchvision
 
1
+ # transformers 4.57.x is what the checkpoint's remote code targets, and it pins
2
+ # huggingface-hub<1.0 — hence the gradio 5.x SDK version in README.md.
3
  transformers==4.57.6
4
  accelerate
5
  torchvision