multimodalart HF Staff commited on
Commit
3c8c4c9
·
verified ·
1 Parent(s): fca1ab8

SyncWorld zero-shot robot simulator (ZeroGPU)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +19 -0
  2. README.md +73 -7
  3. app.py +656 -0
  4. assets/scenes/libero-black-bowl/calibration/agentview_rgb/video.mp4 +3 -0
  5. assets/scenes/libero-black-bowl/calibration/pose.pkl +3 -0
  6. assets/scenes/libero-black-bowl/expert/agentview_rgb/video.mp4 +3 -0
  7. assets/scenes/libero-black-bowl/expert/pose.pkl +3 -0
  8. assets/scenes/libero-black-bowl/thumb.jpg +0 -0
  9. assets/scenes/libero-bowl-plate/calibration/agentview_rgb/video.mp4 +3 -0
  10. assets/scenes/libero-bowl-plate/calibration/pose.pkl +3 -0
  11. assets/scenes/libero-bowl-plate/expert/agentview_rgb/video.mp4 +0 -0
  12. assets/scenes/libero-bowl-plate/expert/pose.pkl +3 -0
  13. assets/scenes/libero-bowl-plate/thumb.jpg +0 -0
  14. assets/scenes/libero-drawer/calibration/agentview_rgb/video.mp4 +3 -0
  15. assets/scenes/libero-drawer/calibration/pose.pkl +3 -0
  16. assets/scenes/libero-drawer/expert/agentview_rgb/video.mp4 +3 -0
  17. assets/scenes/libero-drawer/expert/pose.pkl +3 -0
  18. assets/scenes/libero-drawer/thumb.jpg +0 -0
  19. assets/scenes/libero-soup/calibration/agentview_rgb/video.mp4 +3 -0
  20. assets/scenes/libero-soup/calibration/pose.pkl +3 -0
  21. assets/scenes/libero-soup/expert/agentview_rgb/video.mp4 +3 -0
  22. assets/scenes/libero-soup/expert/pose.pkl +3 -0
  23. assets/scenes/libero-soup/thumb.jpg +0 -0
  24. assets/scenes/libero-stove/calibration/agentview_rgb/video.mp4 +3 -0
  25. assets/scenes/libero-stove/calibration/pose.pkl +3 -0
  26. assets/scenes/libero-stove/expert/agentview_rgb/video.mp4 +3 -0
  27. assets/scenes/libero-stove/expert/pose.pkl +3 -0
  28. assets/scenes/libero-stove/thumb.jpg +0 -0
  29. assets/scenes/peginsertion/calibration/pose.pkl +3 -0
  30. assets/scenes/peginsertion/calibration/render_camera_rgb/video.mp4 +3 -0
  31. assets/scenes/peginsertion/expert/pose.pkl +3 -0
  32. assets/scenes/peginsertion/expert/render_camera_rgb/video.mp4 +3 -0
  33. assets/scenes/peginsertion/thumb.jpg +0 -0
  34. assets/scenes/plugcharger/calibration/pose.pkl +3 -0
  35. assets/scenes/plugcharger/calibration/render_camera_rgb/video.mp4 +3 -0
  36. assets/scenes/plugcharger/expert/pose.pkl +3 -0
  37. assets/scenes/plugcharger/expert/render_camera_rgb/video.mp4 +3 -0
  38. assets/scenes/plugcharger/thumb.jpg +0 -0
  39. assets/scenes/pushcube/calibration/pose.pkl +3 -0
  40. assets/scenes/pushcube/calibration/render_camera_rgb/video.mp4 +3 -0
  41. assets/scenes/pushcube/expert/pose.pkl +3 -0
  42. assets/scenes/pushcube/expert/render_camera_rgb/video.mp4 +3 -0
  43. assets/scenes/pushcube/thumb.jpg +0 -0
  44. assets/scenes/scenes.json +72 -0
  45. assets/scenes/stackcube/calibration/pose.pkl +3 -0
  46. assets/scenes/stackcube/calibration/render_camera_rgb/video.mp4 +3 -0
  47. assets/scenes/stackcube/expert/pose.pkl +3 -0
  48. assets/scenes/stackcube/expert/render_camera_rgb/video.mp4 +3 -0
  49. assets/scenes/stackcube/thumb.jpg +0 -0
  50. assets/scenes/stackpyramid/calibration/pose.pkl +3 -0
.gitattributes CHANGED
@@ -33,3 +33,22 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/scenes/libero-black-bowl/calibration/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ assets/scenes/libero-black-bowl/expert/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ assets/scenes/libero-bowl-plate/calibration/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ assets/scenes/libero-drawer/calibration/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ assets/scenes/libero-drawer/expert/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ assets/scenes/libero-soup/calibration/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ assets/scenes/libero-soup/expert/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ assets/scenes/libero-stove/calibration/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ assets/scenes/libero-stove/expert/agentview_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
45
+ assets/scenes/peginsertion/calibration/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
46
+ assets/scenes/peginsertion/expert/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
47
+ assets/scenes/plugcharger/calibration/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
48
+ assets/scenes/plugcharger/expert/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
49
+ assets/scenes/pushcube/calibration/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
50
+ assets/scenes/pushcube/expert/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
51
+ assets/scenes/stackcube/calibration/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
52
+ assets/scenes/stackcube/expert/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
53
+ assets/scenes/stackpyramid/calibration/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
54
+ assets/scenes/stackpyramid/expert/render_camera_rgb/video.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,79 @@
1
  ---
2
- title: Syncworld Simulator
3
- emoji:
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SyncWorld Simulator
3
+ emoji: 🦾
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.26.0
 
8
  app_file: app.py
9
+ short_description: Visually-calibrated zero-shot robot world simulator
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
+ models:
13
+ - yyuncong/SyncWorld
14
+ datasets:
15
+ - yyuncong/SyncWorld-Evaluation
16
  ---
17
 
18
+ # 🦾 SyncWorld visual calibration turns a world model into a zero-shot simulator
19
+
20
+ Demo for **[SyncWorld: Visual Calibration Enables World Models as Zero-Shot Simulators](https://huggingface.co/papers/2609.09155)**
21
+ (UMass Embodied AGI) — [code](https://github.com/UMass-Embodied-AGI/SyncWorld) ·
22
+ [weights](https://huggingface.co/yyuncong/SyncWorld).
23
+
24
+ A pretrained action-conditioned video world model cannot be dropped into a new scene, because it
25
+ does not know how *this* camera sees *this* robot's action space. SyncWorld's fix is **visual
26
+ calibration**: prepend a short clip of the arm sweeping each degree of freedom, paired with the
27
+ actions that produced it, as extra fully-conditioning context. The model infers the unseen
28
+ camera↔robot mapping in-context and becomes a usable forward-dynamics simulator — with no
29
+ per-scene finetuning.
30
+
31
+ ## What this Space does
32
+
33
+ Pick one of the authors' calibrated evaluation episodes, then either
34
+
35
+ - **replay the robot's own actions** — re-simulate the expert trajectory and compare against ground
36
+ truth, or
37
+ - **drive the arm yourself** — command a constant per-step end-effector twist
38
+ (Δx/Δy/Δz in cm/step, Δroll/Δpitch/Δyaw in °/step, plus the gripper) and watch the model render
39
+ a counterfactual rollout.
40
+
41
+ Each press generates one or more autoregressive 16-frame windows at 512 px, closed-loop (each
42
+ window conditions on the previously *generated* frames).
43
+
44
+ ## Inference path
45
+
46
+ Ported 1:1 from the authors' reference script `examples/eval_gripperhead_fdm_rollout.py`, keeping
47
+ the trained recipe exactly:
48
+
49
+ | | |
50
+ |---|---|
51
+ | Conditioning items | `[6 per-DoF calibration segments × 5 frames] + [25 sparse history frames @ stride 3] + [current frame + 16 future frames]` |
52
+ | Actions | 7-D `[Δpos_cm(3), Δeuler_xyz_deg(3), gripper(1)]`, `backward_framewise` convention, translation scale 100.0, rotation scale 57.2958 |
53
+ | Resolution / fps | 512 px square, conditioning fps 15 |
54
+ | Sampler | rectified flow, UniPC, 20 steps (adjustable) |
55
+ | Caption | the FDM training constant `"A robot arm interacts with the scene."` |
56
+ | Backbone | 16B Cosmos-3 mixture-of-transformers (Qwen3-VL-8B understanding expert + diffusion expert), Wan2.2 4×16×16 video VAE |
57
+
58
+ Deviations forced by the target hardware: single-process inference (`parallel_dims=None`),
59
+ `torch.compile` disabled (ZeroGPU forks a fresh worker per call), the audio expert disabled
60
+ (unused by this checkpoint), and the VLM processor served from a local copy of the seven tokenizer
61
+ files in `nvidia/Cosmos3-Nano` instead of downloading that repo's 35 GB of weights.
62
+
63
+ ## Bundled scenes
64
+
65
+ Ten episodes (5 ManiSkill + 5 LIBERO) copied verbatim from the authors' own evaluation set,
66
+ [`yyuncong/SyncWorld-Evaluation`](https://huggingface.co/datasets/yyuncong/SyncWorld-Evaluation),
67
+ each with its paired calibration sweep and pose track. Both the code and the data are released
68
+ under **OpenMDW-1.1**; see `ATTRIBUTIONS.md` / `NOTICE` in the upstream repo.
69
+
70
+ ## Citation
71
+
72
+ ```bibtex
73
+ @article{syncworld2026,
74
+ title = {SyncWorld: Visual Calibration Enables World Models as Zero-Shot Simulators},
75
+ author = {UMass Embodied AGI},
76
+ year = {2026},
77
+ url = {https://huggingface.co/papers/2609.09155}
78
+ }
79
+ ```
app.py ADDED
@@ -0,0 +1,656 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SyncWorld — visual calibration turns a video world model into a zero-shot robot simulator.
2
+
3
+ Gradio / ZeroGPU demo for `yyuncong/SyncWorld` (paper 2609.09155, UMass Embodied AGI).
4
+
5
+ The inference path is ported 1:1 from the authors' reference script
6
+ `examples/eval_gripperhead_fdm_rollout.py` in
7
+ https://github.com/UMass-Embodied-AGI/SyncWorld — same config surgery, same
8
+ multi-item `[calib x 6] + [history] + [current+future]` batch, same sampler
9
+ call, same conditioning horizon (25 sparse history frames @ stride 3, 16
10
+ predicted frames, 512px, conditioning fps 15, 7-D `[dpos_cm, deuler_deg,
11
+ gripper]` actions in the `backward_framewise` convention).
12
+ """
13
+ import os
14
+ import sys
15
+
16
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
17
+ os.environ.setdefault("COSMOS_DEVICE", "cuda")
18
+
19
+ import spaces # noqa: F401 # MUST precede torch / any CUDA-touching import
20
+
21
+ from cosmos_framework.inference.common.init import init_script
22
+
23
+ init_script() # sets grad off, seeds, wires the cosmos logger (matches the reference script)
24
+ sys.excepthook = sys.__excepthook__ # undo init_script's distributed excepthook (Gradio needs the default)
25
+
26
+ import functools
27
+ import json
28
+ import math
29
+ import pickle
30
+ import shutil
31
+ import tempfile
32
+ import time
33
+ import types
34
+ from pathlib import Path
35
+
36
+ import gradio as gr
37
+ import imageio
38
+ import numpy as np
39
+ import torch
40
+ import torch.nn.functional as F
41
+ from huggingface_hub import hf_hub_download, snapshot_download
42
+
43
+ from cosmos_framework.configs.base.defaults.compile import CompileConfig
44
+ from cosmos_framework.data.vfm.action.calib_segments import build_calib_segment_indices
45
+ from cosmos_framework.data.vfm.action.datasets.gripperhead_fdm_dataset import NEUTRAL_CAPTION_SEED
46
+ from cosmos_framework.data.vfm.action.domain_utils import get_domain_id
47
+ from cosmos_framework.data.vfm.action.pose_utils import pose_abs_to_rel
48
+ from cosmos_framework.data.vfm.action.transforms import build_sequence_plan_from_mode
49
+ from cosmos_framework.inference.model import Cosmos3OmniConfig, Cosmos3OmniModel
50
+ from cosmos_framework.model.vfm.vlm.qwen3_vl.utils import tokenize_caption
51
+
52
+ HERE = Path(__file__).parent
53
+ DEVICE = "cuda"
54
+ OUT_DIR = Path(tempfile.gettempdir()) / "syncworld_out"
55
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
56
+
57
+ CKPT_REPO = "yyuncong/SyncWorld"
58
+ VAE_REPO = "Wan-AI/Wan2.2-TI2V-5B"
59
+ VAE_FILE = "Wan2.2_VAE.pth"
60
+ QWEN_REPO = "nvidia/Cosmos3-Nano"
61
+ # Only the processor / tokenizer artifacts at the repo root — NOT the 35GB of weights.
62
+ QWEN_FILES = [
63
+ "chat_template.json",
64
+ "merges.txt",
65
+ "preprocessor_config.json",
66
+ "tokenizer.json",
67
+ "tokenizer_config.json",
68
+ "video_preprocessor_config.json",
69
+ "vocab.json",
70
+ ]
71
+
72
+ # ---------------------------------------------------------------- shipped recipe
73
+ # These are the trained checkpoint's conditioning + sampling defaults, i.e. the
74
+ # argparse defaults of the authors' eval script. They MUST match the checkpoint.
75
+ def _recipe(**over):
76
+ a = types.SimpleNamespace(
77
+ num_history_frames=25,
78
+ num_pred_frames=16,
79
+ history_frame_stride=3,
80
+ resolution=512,
81
+ fps=15.0,
82
+ use_calibration=True,
83
+ calib_null=False,
84
+ calib_segments=6,
85
+ calib_seg_len=5,
86
+ calib_frame_interval=3,
87
+ calib_positive_actions=False,
88
+ action_convention="backward_framewise",
89
+ action_rot_format="euler_xyz",
90
+ action_trans_scale=100.0,
91
+ action_rot_scale=57.2958,
92
+ action_cfg_scale=1.0,
93
+ num_steps=20,
94
+ seed=0,
95
+ )
96
+ for k, v in over.items():
97
+ setattr(a, k, v)
98
+ return a
99
+
100
+
101
+ # ================================================================ model loading
102
+ # (ported from the reference script: _REWRITES / _config_from_dir /
103
+ # _patch_offline_vlm_processor / load_model)
104
+ _REWRITES = [
105
+ ("cosmos3._src.vfm.configs.base.", "cosmos_framework.configs.base."),
106
+ ("cosmos3._src.vfm.models.", "cosmos_framework.model.vfm."),
107
+ ("cosmos3._src.vfm.tokenizers.", "cosmos_framework.model.vfm.tokenizers."),
108
+ ("cosmos3._src.imaginaire.", "cosmos_framework."),
109
+ ]
110
+
111
+
112
+ def _config_from_dir(d: Path, vae_path: str = "") -> Cosmos3OmniConfig:
113
+ cfg = d / "config.json" if (d / "config.json").exists() else d / "model" / "config.json"
114
+ if not cfg.exists():
115
+ raise FileNotFoundError(f"no config.json under {d}")
116
+ text = cfg.read_text()
117
+ for a, b in _REWRITES:
118
+ text = text.replace(a, b)
119
+ model_cfg = json.loads(text)["model"]
120
+ # The shipped config resolves the Wan2.2 video VAE through an object store
121
+ # (vae_path="pretrained/..." + bucket_name="bucket"). Point it at the local
122
+ # .pth and clear the bucket so loading is self-contained.
123
+ wan = (vae_path or "").strip()
124
+ if wan:
125
+ def _override_vae(o):
126
+ if isinstance(o, dict):
127
+ if "vae_path" in o: # the VIDEO tokenizer dict ("avae_path" is the audio one)
128
+ o["vae_path"] = wan
129
+ o["bucket_name"] = ""
130
+ for v in o.values():
131
+ _override_vae(v)
132
+ elif isinstance(o, list):
133
+ for v in o:
134
+ _override_vae(v)
135
+ _override_vae(model_cfg)
136
+ # Sound is DISABLED in the gripperhead FDM recipe, so the checkpoint has no
137
+ # sound expert; leaving sound_gen=True would make the build fetch an audio
138
+ # tokenizer that does not exist for this release.
139
+ if isinstance(model_cfg.get("config"), dict):
140
+ model_cfg["config"]["sound_gen"] = False
141
+ model_cfg["config"]["sound_tokenizer"] = None
142
+ return Cosmos3OmniConfig(model=model_cfg)
143
+
144
+
145
+ def _patch_offline_vlm_processor(qwen_assets: str):
146
+ """Serve the VLM processor from a local dir instead of shelling out to `uvx hf download`
147
+ (which would pull the whole 35GB nvidia/Cosmos3-Nano repo)."""
148
+ import cosmos_framework.utils.checkpoint_db as _ckdb
149
+
150
+ _orig = _ckdb._hf_download
151
+
152
+ def _patched(cmd_args):
153
+ repo = str(cmd_args[0]) if cmd_args else ""
154
+ if "Cosmos3-Nano" in repo:
155
+ print(f"[boot] offline VLM processor: _hf_download({repo}) -> {qwen_assets}", flush=True)
156
+ return qwen_assets
157
+ return _orig(cmd_args)
158
+
159
+ _ckdb._hf_download = _patched
160
+
161
+
162
+ def _load_model(checkpoint: str, vae_path: str, qwen_assets: str):
163
+ _patch_offline_vlm_processor(qwen_assets)
164
+ ckpt = Path(checkpoint)
165
+ wrapper = Cosmos3OmniModel.from_pretrained_dcp(
166
+ ckpt,
167
+ config=_config_from_dir(ckpt, vae_path),
168
+ compile_config=CompileConfig(enabled=False), # torch.compile is off: ZeroGPU forks a fresh worker
169
+ )
170
+ m = wrapper.model
171
+ # Single-process inference: null parallel_dims so every collective site takes
172
+ # its local no-op branch (the reference does the same for single-GPU eval).
173
+ if getattr(m, "parallel_dims", None) is not None:
174
+ m.parallel_dims = None
175
+ m.eval()
176
+ return m
177
+
178
+
179
+ # ================================================================ episode reading
180
+ # (ported: _read_video / _center_square_crop_np / _load_pose / _build_action /
181
+ # read_episode / _to_u8 / build_fdm_batch_multiitem / build_calib_eval_items /
182
+ # _gen_window)
183
+ def _read_video(path: str) -> np.ndarray:
184
+ import imageio.v3 as iio
185
+
186
+ try:
187
+ return iio.imread(path, plugin="pyav") # (T,H,W,C) uint8
188
+ except Exception:
189
+ return iio.imread(path)
190
+
191
+
192
+ @functools.lru_cache(maxsize=4)
193
+ def _read_video_cached(path: str) -> np.ndarray:
194
+ return _read_video(path)
195
+
196
+
197
+ def _center_square_crop_np(a: np.ndarray) -> np.ndarray:
198
+ h, w = a.shape[1], a.shape[2]
199
+ if h == w:
200
+ return a
201
+ m = min(h, w)
202
+ top, left = (h - m) // 2, (w - m) // 2
203
+ return a[:, top:top + m, left:left + m]
204
+
205
+
206
+ def _load_pose(leaf: str, thr: float = 0.6):
207
+ with open(os.path.join(leaf, "pose.pkl"), "rb") as f:
208
+ d = pickle.load(f)
209
+ mats = np.asarray(d["gripper_matrix"], dtype=np.float32) # (T,4,4)
210
+ gopen = np.asarray(d.get("gripper_open", np.ones(len(mats))), dtype=np.float32)
211
+ return mats, (gopen > thr).astype(np.float32)
212
+
213
+
214
+ def _build_action(mats, gopen, convention="backward_framewise", rot_format="euler_xyz",
215
+ trans_scale=100.0, rot_scale=57.2958) -> torch.Tensor:
216
+ """abs poses -> per-step delta actions [dpos_cm(3), deuler_deg(3), gripper(1)]."""
217
+ poses_rel = pose_abs_to_rel(mats, rotation_format=rot_format, pose_convention=convention,
218
+ translation_scale=trans_scale, rotation_scale=rot_scale)
219
+ grip = gopen[1:].reshape(-1, 1)
220
+ return torch.from_numpy(np.concatenate([poses_rel, grip], axis=-1).astype(np.float32))
221
+
222
+
223
+ def _frames_to_pm1(frames: np.ndarray, res: int) -> torch.Tensor:
224
+ v = torch.from_numpy(_center_square_crop_np(frames)).float().permute(0, 3, 1, 2) / 255.0
225
+ _, _, hh, ww = v.shape
226
+ th = res
227
+ tw = max(16, int(round((res * ww / hh) / 16) * 16)) # aspect preserving, /16 for the VAE
228
+ if (hh, ww) != (th, tw):
229
+ v = F.interpolate(v, size=(th, tw), mode="bilinear", align_corners=False)
230
+ return v * 2 - 1
231
+
232
+
233
+ def read_episode(leaf: str, view: str, res: int):
234
+ frames = _read_video_cached(os.path.join(leaf, view, "video.mp4"))
235
+ v = _frames_to_pm1(frames, res)
236
+ mats, gopen = _load_pose(leaf)
237
+ n = min(len(v), len(mats))
238
+ # FDM training always emits NEUTRAL_CAPTION_SEED, so anything else is out of distribution.
239
+ return v[:n], mats[:n], gopen[:n], NEUTRAL_CAPTION_SEED
240
+
241
+
242
+ def _to_u8(x: torch.Tensor) -> torch.Tensor:
243
+ return ((x.clamp(-1, 1) + 1) / 2 * 255.0).round().clamp(0, 255).to(torch.uint8)
244
+
245
+
246
+ def build_fdm_batch_multiitem(model, video_u8_list, action_list, caption, device, fps=15.0,
247
+ mode: str = "forward_dynamics"):
248
+ """N-item FDM batch: `[calib x K] + [history] + [current+future]`. Every item but the
249
+ last is fully conditioning; the last conditions on its current-frame latent and
250
+ generates the P future latents."""
251
+ maxD = model.config.max_action_dim
252
+
253
+ def pad(a):
254
+ p = torch.zeros(a.shape[0], maxD, device=device)
255
+ p[:, : a.shape[1]] = a.to(device)
256
+ return p
257
+
258
+ vids = [v.to(device) for v in video_u8_list]
259
+ acts = [pad(a) for a in action_list]
260
+ dims = [torch.tensor(int(a.shape[1]), dtype=torch.long, device=device) for a in action_list]
261
+ sizes = [torch.tensor([[v.shape[-2], v.shape[-1], v.shape[-2], v.shape[-1]]],
262
+ dtype=torch.float32, device=device) for v in vids]
263
+ cf = vids[-1]
264
+ sp = build_sequence_plan_from_mode(mode, video_length=cf.shape[1],
265
+ action_length=action_list[-1].shape[0], has_text=True,
266
+ num_condition_latent_frames=1)
267
+ sp.share_vision_temporal_positions = False # distinct time states per item — MUST match training
268
+ ids = tokenize_caption(caption, model.vlm_tokenizer, is_video=False,
269
+ use_system_prompt=model.vlm_config.use_system_prompt)
270
+ return {
271
+ model.input_video_key: [vids],
272
+ "action": [acts],
273
+ "raw_action_dim": [dims],
274
+ "image_size": [sizes],
275
+ "mode": [mode],
276
+ model.input_caption_key: [caption],
277
+ "text_token_ids": [torch.tensor(ids, dtype=torch.long, device=device).unsqueeze(0)],
278
+ "fps": torch.tensor([float(fps)], device=device),
279
+ "conditioning_fps": torch.tensor([float(fps)], device=device),
280
+ "domain_id": [torch.tensor(get_domain_id("gripperhead"), dtype=torch.long, device=device)],
281
+ "sequence_plan": [sp],
282
+ # NO is_preprocessed -> the uint8 items get normalized + re-stacked for the VAE
283
+ }
284
+
285
+
286
+ def build_calib_items(calib_dir: str, args, view: str):
287
+ """Load + per-DoF segment the calibration sweep into (K uint8 video items, K action blocks)."""
288
+ frames = _read_video_cached(os.path.join(calib_dir, view, "video.mp4"))
289
+ v = _frames_to_pm1(frames, args.resolution)
290
+ mats, gopen = _load_pose(calib_dir)
291
+ n = min(len(mats), len(v))
292
+ move_order = None
293
+ mrp = os.path.join(calib_dir, "move_range.pkl")
294
+ if os.path.isfile(mrp):
295
+ try:
296
+ with open(mrp, "rb") as f:
297
+ move_order = pickle.load(f).get("movement_order")
298
+ except Exception:
299
+ move_order = None
300
+ sub = list(range(0, n, args.calib_frame_interval)) or [0]
301
+ efficient = (args.calib_segments == 6) # 6 -> one segment per DoF; 12 -> both signs
302
+ seg_lists = build_calib_segment_indices(mats[sub], args.calib_seg_len, efficient, move_order,
303
+ positive_body_actions=bool(args.calib_positive_actions))
304
+ vids, acts = [], []
305
+ for seg in seg_lists:
306
+ real = [sub[i] for i in seg]
307
+ vids.append(_to_u8(v[real].permute(1, 0, 2, 3))) # (C,seg_len,H,W) uint8
308
+ acts.append(_build_action(mats[real], gopen[real], args.action_convention,
309
+ args.action_rot_format, args.action_trans_scale, args.action_rot_scale))
310
+ return vids, acts
311
+
312
+
313
+ def _gen_window(model, ep_frames, gen_frames, mats, gopen, caption, calib_v, calib_a,
314
+ start, args, device, custom_act=None):
315
+ """Generate ONE 1+P frame window at frame `start`. Returns (pred_u8, gt_u8) as (P+1,H,W,C)."""
316
+ T = ep_frames.shape[0]
317
+ H, P, S = args.num_history_frames, args.num_pred_frames, args.history_frame_stride
318
+ clamp = lambda x: max(0, min(int(x), T - 1)) # noqa: E731
319
+ use_history, use_calib = H > 1, args.use_calibration
320
+ cf_idx = [clamp(start)] + [clamp(start + 1 + j) for j in range(P)]
321
+ cf_frames = torch.stack([gen_frames[clamp(start)]]
322
+ + [ep_frames[clamp(start + 1 + j)] for j in range(P)], dim=1)
323
+ # custom-action mode: drive the window with a synthesized cm/deg action. The HISTORY
324
+ # action block stays real — it is observed context, not the commanded motion.
325
+ cf_act = (custom_act.to(torch.float32) if custom_act is not None else
326
+ _build_action(mats[cf_idx], gopen[cf_idx], args.action_convention,
327
+ args.action_rot_format, args.action_trans_scale, args.action_rot_scale))
328
+ items_v = list(calib_v) if use_calib else []
329
+ items_a = list(calib_a) if use_calib else []
330
+ if use_history:
331
+ hist_idx = [clamp(start - S * (H - i)) for i in range(H)]
332
+ hist_frames = torch.stack([gen_frames[i] for i in hist_idx], dim=1)
333
+ hist_act = _build_action(mats[hist_idx], gopen[hist_idx], args.action_convention,
334
+ args.action_rot_format, args.action_trans_scale, args.action_rot_scale)
335
+ items_v.append(_to_u8(hist_frames))
336
+ items_a.append(hist_act)
337
+ items_v.append(_to_u8(cf_frames))
338
+ items_a.append(cf_act)
339
+ batch = build_fdm_batch_multiitem(model, items_v, items_a, caption, device, fps=args.fps)
340
+ with torch.no_grad():
341
+ outputs = model.generate_samples_from_batch(
342
+ batch, guidance=1.0, action_guidance=args.action_cfg_scale,
343
+ seed=[args.seed], num_steps=args.num_steps)
344
+ dec = model.decode(outputs["vision"][0])[0].clamp(-1, 1) # (C,P+1,h,w)
345
+ pred_u8 = ((dec.float().permute(1, 2, 3, 0) + 1) / 2 * 255).round().clamp(0, 255).byte().cpu().numpy()
346
+ gt_win = torch.stack([ep_frames[clamp(start + i)] for i in range(P + 1)], dim=0)
347
+ gt_u8 = ((gt_win.float().permute(0, 2, 3, 1) + 1) / 2 * 255).round().clamp(0, 255).byte().cpu().numpy()
348
+ return pred_u8, gt_u8
349
+
350
+
351
+ # ================================================================ boot
352
+ _t = time.perf_counter()
353
+ print("[boot] fetching processor assets ...", flush=True)
354
+ QWEN_DIR = None
355
+ for _f in QWEN_FILES:
356
+ QWEN_DIR = os.path.dirname(hf_hub_download(QWEN_REPO, _f))
357
+ print(f"[boot] processor assets at {QWEN_DIR}", flush=True)
358
+
359
+ print("[boot] fetching Wan2.2 video VAE ...", flush=True)
360
+ VAE_PATH = hf_hub_download(VAE_REPO, VAE_FILE)
361
+
362
+ print(f"[boot] fetching {CKPT_REPO} (~31 GB) ...", flush=True)
363
+ CKPT_DIR = snapshot_download(CKPT_REPO)
364
+ print(f"[boot] checkpoint at {CKPT_DIR} ({time.perf_counter() - _t:.0f}s)", flush=True)
365
+
366
+ print("[boot] building + loading SyncWorld ...", flush=True)
367
+ MODEL = _load_model(CKPT_DIR, VAE_PATH, QWEN_DIR)
368
+ print(f"[boot] model ready ({time.perf_counter() - _t:.0f}s)", flush=True)
369
+
370
+ if not os.environ.get("SYNCWORLD_KEEP_CKPT"):
371
+ # The weights now live in (fake-)CUDA tensors that ZeroGPU packs to its own
372
+ # on-disk store; the 31 GB download is dead weight and would blow the Space's
373
+ # ephemeral disk once the pack copy lands.
374
+ try:
375
+ repo_root = Path(CKPT_DIR).parent.parent
376
+ if repo_root.name.startswith("models--"):
377
+ shutil.rmtree(repo_root, ignore_errors=True)
378
+ print(f"[boot] freed {repo_root}", flush=True)
379
+ except Exception as e: # pragma: no cover
380
+ print(f"[boot] WARN could not free checkpoint dir: {e!r}", flush=True)
381
+
382
+ # ================================================================ scenes
383
+ with open(HERE / "assets" / "scenes" / "scenes.json") as f:
384
+ SCENE_META = json.load(f)
385
+ SCENES = {}
386
+ for _slug, _m in SCENE_META.items():
387
+ _base = HERE / "assets" / "scenes" / _slug
388
+ SCENES[_m["label"]] = dict(
389
+ slug=_slug,
390
+ leaf=str(_base / "expert"),
391
+ calib=str(_base / "calibration"),
392
+ view=_m["view"],
393
+ num_frames=int(_m["num_frames"]),
394
+ thumb=str(_base / "thumb.jpg"),
395
+ expert_video=str(_base / "expert" / _m["view"] / "video.mp4"),
396
+ calib_video=str(_base / "calibration" / _m["view"] / "video.mp4"),
397
+ suite="ManiSkill" if _m["suite"].endswith("maniskill") else "LIBERO",
398
+ task=_m["task"],
399
+ )
400
+ SCENE_NAMES = sorted(SCENES)
401
+ MAX_FRAMES = max(v["num_frames"] for v in SCENES.values())
402
+
403
+ MODE_REPLAY = "Replay the robot's own actions"
404
+ MODE_DRIVE = "Drive the arm myself"
405
+ GRIP_KEEP, GRIP_OPEN, GRIP_CLOSE = "keep current", "open", "close"
406
+
407
+
408
+ def _write_mp4(frames_u8: np.ndarray, fps: int = 15) -> str:
409
+ d = tempfile.mkdtemp(dir=str(OUT_DIR))
410
+ p = os.path.join(d, "rollout.mp4")
411
+ imageio.mimwrite(p, list(frames_u8), fps=fps, macro_block_size=1, quality=9)
412
+ return p
413
+
414
+
415
+ def _estimate_duration(*a, **k):
416
+ """ZeroGPU reservation: scales with rollout rounds x sampler steps."""
417
+ rounds = int(a[10]) if len(a) > 10 else 2
418
+ steps = int(a[11]) if len(a) > 11 else 20
419
+ return int(min(280, 45 + max(1, rounds) * (8 + max(1, steps) * 2.2)))
420
+
421
+
422
+ @spaces.GPU(duration=_estimate_duration)
423
+ def simulate(
424
+ scene: str,
425
+ action_mode: str = MODE_REPLAY,
426
+ start_frame: int = 0,
427
+ delta_x_cm: float = 0.0,
428
+ delta_y_cm: float = 0.0,
429
+ delta_z_cm: float = 0.0,
430
+ delta_rot_x_deg: float = 0.0,
431
+ delta_rot_y_deg: float = 0.0,
432
+ delta_rot_z_deg: float = 0.0,
433
+ gripper: str = GRIP_KEEP,
434
+ rollout_rounds: int = 2,
435
+ num_steps: int = 20,
436
+ action_guidance: float = 1.0,
437
+ seed: int = 0,
438
+ progress=gr.Progress(track_tqdm=True),
439
+ ):
440
+ """Roll out the SyncWorld world model on a visually-calibrated robot episode.
441
+
442
+ Args:
443
+ scene: name of a bundled calibrated episode (a ManiSkill or LIBERO tabletop scene).
444
+ action_mode: "Replay the robot's own actions" to re-simulate the expert trajectory,
445
+ or "Drive the arm myself" to command a constant per-step end-effector twist.
446
+ start_frame: index of the episode frame used as the current observation.
447
+ delta_x_cm: commanded per-step end-effector translation along body X, in centimetres.
448
+ delta_y_cm: commanded per-step end-effector translation along body Y, in centimetres.
449
+ delta_z_cm: commanded per-step end-effector translation along body Z, in centimetres.
450
+ delta_rot_x_deg: commanded per-step end-effector rotation about X, in degrees.
451
+ delta_rot_y_deg: commanded per-step end-effector rotation about Y, in degrees.
452
+ delta_rot_z_deg: commanded per-step end-effector rotation about Z, in degrees.
453
+ gripper: commanded gripper state ("keep current", "open" or "close").
454
+ rollout_rounds: number of autoregressive 16-frame windows to chain.
455
+ num_steps: rectified-flow sampler steps per window.
456
+ action_guidance: action classifier-free-guidance scale.
457
+ seed: sampler seed.
458
+
459
+ Returns:
460
+ A tuple of (predicted rollout mp4, ground-truth window mp4, markdown run report).
461
+ """
462
+ t0 = time.perf_counter()
463
+ sc = SCENES.get(scene) or SCENES[SCENE_NAMES[0]]
464
+ args = _recipe(num_steps=int(num_steps), action_cfg_scale=float(action_guidance), seed=int(seed))
465
+
466
+ ep_frames, mats, gopen, caption = read_episode(sc["leaf"], sc["view"], args.resolution)
467
+ calib_v, calib_a = build_calib_items(sc["calib"], args, sc["view"])
468
+ t_prep = time.perf_counter() - t0
469
+
470
+ T = int(ep_frames.shape[0])
471
+ P = args.num_pred_frames
472
+ start = max(0, min(int(start_frame), T - 1))
473
+ rounds = max(1, min(int(rollout_rounds), 3))
474
+ drive = str(action_mode) == MODE_DRIVE
475
+
476
+ custom_row = None
477
+ if drive:
478
+ if gripper == GRIP_OPEN:
479
+ g = 1.0
480
+ elif gripper == GRIP_CLOSE:
481
+ g = 0.0
482
+ else:
483
+ g = float(gopen[start])
484
+ custom_row = torch.tensor(
485
+ [float(delta_x_cm), float(delta_y_cm), float(delta_z_cm),
486
+ float(delta_rot_x_deg), float(delta_rot_y_deg), float(delta_rot_z_deg), g],
487
+ dtype=torch.float32)
488
+
489
+ gen_frames = ep_frames.clone()
490
+ pred_acc, gt_acc = [], []
491
+ for r in range(rounds):
492
+ s = start + r * P
493
+ cact = custom_row.unsqueeze(0).repeat(P, 1) if custom_row is not None else None
494
+ pred_u8, gt_u8 = _gen_window(MODEL, ep_frames, gen_frames, mats, gopen, caption,
495
+ calib_v, calib_a, s, args, DEVICE, custom_act=cact)
496
+ if rounds > 1: # closed-loop write-back: the next round conditions on generated frames
497
+ for i in range(min(pred_u8.shape[0], gen_frames.shape[0] - s)):
498
+ fr = pred_u8[i].astype(np.float32) / 255.0 * 2.0 - 1.0
499
+ gen_frames[s + i] = torch.from_numpy(fr).permute(2, 0, 1).to(gen_frames)
500
+ skip = 0 if r == 0 else 1 # drop the 1-frame overlap between rounds
501
+ m = min(pred_u8.shape[0], gt_u8.shape[0])
502
+ pred_acc.extend(pred_u8[i] for i in range(skip, m))
503
+ gt_acc.extend(gt_u8[i] for i in range(skip, m))
504
+
505
+ pred = np.stack(pred_acc, 0)
506
+ gt = np.stack(gt_acc, 0)
507
+ pred_path = _write_mp4(pred)
508
+ gt_path = _write_mp4(gt)
509
+ dt = time.perf_counter() - t0
510
+
511
+ if drive:
512
+ act_desc = (f"commanded twist  `Δpos = ({delta_x_cm:+.2f}, {delta_y_cm:+.2f}, "
513
+ f"{delta_z_cm:+.2f}) cm/step`,  `Δrot = ({delta_rot_x_deg:+.2f}, "
514
+ f"{delta_rot_y_deg:+.2f}, {delta_rot_z_deg:+.2f}) deg/step`,  gripper "
515
+ f"**{gripper}** — held for {rounds * P} steps")
516
+ else:
517
+ act_desc = f"the episode's own recorded actions for frames {start}…{min(start + rounds * P, T - 1)}"
518
+ report = (
519
+ f"**{sc['suite']} · `{sc['task']}`**  ·  camera `{sc['view']}`  ·  "
520
+ f"episode has {T} frames\n\n"
521
+ f"Conditioned on **6 per-DoF calibration segments** + **25 sparse history frames** "
522
+ f"(stride 3, from frame {max(0, start - 3 * 25)}) + current frame **{start}**.\n\n"
523
+ f"Action: {act_desc}.\n\n"
524
+ f"Generated **{len(pred)} frames** in {rounds} autoregressive window(s) × {args.num_steps} "
525
+ f"sampler steps  ·  **{dt:.1f}s** total (scene prep {t_prep:.1f}s)."
526
+ )
527
+ return pred_path, gt_path, report
528
+
529
+
530
+ # ================================================================ UI
531
+ def _scene_preview(scene: str, start_frame: int):
532
+ sc = SCENES.get(scene) or SCENES[SCENE_NAMES[0]]
533
+ T = sc["num_frames"]
534
+ s = max(0, min(int(start_frame), T - 1))
535
+ try:
536
+ frame = _read_video_cached(sc["expert_video"])[s]
537
+ except Exception:
538
+ frame = None
539
+ info = (f"`{sc['suite']}` · `{sc['task']}` · camera `{sc['view']}` · **{T} frames** "
540
+ f"· current frame **{s}**")
541
+ return frame, sc["calib_video"], sc["expert_video"], info
542
+
543
+
544
+ THEME = gr.themes.Citrus()
545
+
546
+ DESC = """\
547
+ # 🦾 SyncWorld — a zero-shot robot simulator
548
+
549
+ [**SyncWorld**](https://huggingface.co/papers/2609.09155) shows that a pretrained video world model
550
+ becomes a usable robot simulator once you *visually calibrate* it: prepend a short clip of the arm
551
+ sweeping each degree of freedom, and the model infers the unseen camera↔robot mapping on the fly —
552
+ no per-scene finetuning.
553
+
554
+ Pick a calibrated scene, then either **replay the robot's own actions** or **drive the arm yourself**
555
+ with a per-step end-effector twist, and watch the model render what happens next.
556
+
557
+ *Model: [`yyuncong/SyncWorld`](https://huggingface.co/yyuncong/SyncWorld) (16B Cosmos-3 mixture-of-transformers,
558
+ Wan2.2 video VAE). Scenes and calibration clips are the authors' own evaluation episodes from
559
+ [`yyuncong/SyncWorld-Evaluation`](https://huggingface.co/datasets/yyuncong/SyncWorld-Evaluation) (OpenMDW-1.1).*
560
+ """
561
+
562
+ with gr.Blocks(theme=THEME, title="SyncWorld — zero-shot robot simulator") as demo:
563
+ gr.Markdown(DESC)
564
+
565
+ with gr.Row():
566
+ with gr.Column(scale=5):
567
+ scene = gr.Dropdown(SCENE_NAMES, value=SCENE_NAMES[0], label="Calibrated scene")
568
+ info_md = gr.Markdown()
569
+ with gr.Row():
570
+ cur_img = gr.Image(label="Current frame (the model's last observation)",
571
+ height=232, interactive=False)
572
+ calib_vid = gr.Video(label="Calibration sweep (6 DoF)", height=232,
573
+ interactive=False, autoplay=True, loop=True)
574
+ start_frame = gr.Slider(0, MAX_FRAMES - 1, value=0, step=1,
575
+ label="Current frame index (clamped to the episode length)")
576
+ action_mode = gr.Radio([MODE_REPLAY, MODE_DRIVE], value=MODE_REPLAY, label="Actions")
577
+ with gr.Group():
578
+ gr.Markdown("**Commanded end-effector twist** — used in *Drive the arm myself* mode. "
579
+ "Held constant for every predicted step (the calibration sweeps cover "
580
+ "roughly ±3 cm/step and ±3 °/step).")
581
+ with gr.Row():
582
+ dx = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ X (cm/step)")
583
+ dy = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ Y (cm/step)")
584
+ dz = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ Z (cm/step)")
585
+ with gr.Row():
586
+ rx = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ rot X (°/step)")
587
+ ry = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ rot Y (°/step)")
588
+ rz = gr.Slider(-3.0, 3.0, value=0.0, step=0.05, label="Δ rot Z (°/step)")
589
+ gripper = gr.Radio([GRIP_KEEP, GRIP_OPEN, GRIP_CLOSE], value=GRIP_KEEP,
590
+ label="Gripper")
591
+ rollout_rounds = gr.Slider(1, 3, value=2, step=1,
592
+ label="Rollout windows (16 generated frames each)")
593
+ with gr.Accordion("Advanced", open=False):
594
+ num_steps = gr.Slider(4, 40, value=20, step=1, label="Sampler steps per window")
595
+ action_guidance = gr.Slider(1.0, 5.0, value=1.0, step=0.1,
596
+ label="Action guidance (CFG)")
597
+ seed = gr.Slider(0, 2**31 - 1, value=0, step=1, label="Seed")
598
+ run = gr.Button("Simulate", variant="primary")
599
+
600
+ with gr.Column(scale=5):
601
+ out_pred = gr.Video(label="SyncWorld rollout", autoplay=True, loop=True,
602
+ interactive=False, height=360)
603
+ out_gt = gr.Video(label="Ground truth for the same window (reference)",
604
+ autoplay=True, loop=True, interactive=False, height=240)
605
+ report_md = gr.Markdown()
606
+
607
+ with gr.Accordion("Full expert episode (reference)", open=False):
608
+ expert_vid = gr.Video(label="Expert demonstration", interactive=False, height=360)
609
+
610
+ INPUTS = [scene, action_mode, start_frame, dx, dy, dz, rx, ry, rz, gripper,
611
+ rollout_rounds, num_steps, action_guidance, seed]
612
+ OUTPUTS = [out_pred, out_gt, report_md]
613
+
614
+ # Every example row supplies the FULL input list. Gradio inserts its Progress
615
+ # object at the positional index of the `progress` parameter, so a short row
616
+ # would silently shift values into the wrong arguments.
617
+ EXAMPLES = [
618
+ # scene, mode, start, dx, dy, dz, rx, ry, rz, gripper, rounds, steps, cfg, seed
619
+ ["ManiSkill · PushCube", MODE_REPLAY, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, GRIP_KEEP, 2, 20, 1.0, 0],
620
+ ["LIBERO · put the bowl on the plate", MODE_REPLAY, 16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, GRIP_KEEP, 2, 20, 1.0, 0],
621
+ ["ManiSkill · StackCube", MODE_DRIVE, 32, 0.0, 1.2, 0.0, 0.0, 0.0, 0.0, GRIP_KEEP, 2, 20, 1.0, 0],
622
+ ["ManiSkill · PushCube", MODE_DRIVE, 0, 1.5, 0.0, 0.0, 0.0, 0.0, 0.0, GRIP_KEEP, 2, 20, 1.0, 0],
623
+ ["LIBERO · open the middle drawer", MODE_DRIVE, 24, 0.0, 0.0, -1.2, 0.0, 0.0, 0.0, GRIP_KEEP, 2, 20, 1.0, 0],
624
+ ["LIBERO · alphabet soup into basket", MODE_DRIVE, 48, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, GRIP_CLOSE, 2, 20, 1.0, 0],
625
+ ]
626
+ EXAMPLE_LABELS = [
627
+ "PushCube · replay the expert",
628
+ "LIBERO bowl→plate · replay the expert",
629
+ "StackCube · push +Y at 1.2 cm/step",
630
+ "PushCube · push +X at 1.5 cm/step",
631
+ "LIBERO drawer · pull −Z at 1.2 cm/step",
632
+ "LIBERO soup · close the gripper, hold still",
633
+ ]
634
+
635
+ run.click(fn=simulate, inputs=INPUTS, outputs=OUTPUTS)
636
+
637
+ for _ev in (scene.change, start_frame.release):
638
+ _ev(fn=_scene_preview, inputs=[scene, start_frame],
639
+ outputs=[cur_img, calib_vid, expert_vid, info_md], show_progress="minimal")
640
+
641
+ gr.Examples(
642
+ examples=EXAMPLES,
643
+ example_labels=EXAMPLE_LABELS,
644
+ inputs=INPUTS,
645
+ outputs=OUTPUTS,
646
+ fn=simulate,
647
+ cache_examples=True,
648
+ cache_mode="lazy",
649
+ label="Examples",
650
+ )
651
+
652
+ demo.load(fn=_scene_preview, inputs=[scene, start_frame],
653
+ outputs=[cur_img, calib_vid, expert_vid, info_md])
654
+
655
+ if __name__ == "__main__":
656
+ demo.queue(max_size=12).launch(mcp_server=True, allowed_paths=[str(OUT_DIR), str(HERE / "assets")])
assets/scenes/libero-black-bowl/calibration/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4df8106071d26afbad4430d65b4a4ad87f036d25a19b9acd7c033f6c9db94b9b
3
+ size 130801
assets/scenes/libero-black-bowl/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f36ffe48457db0d8e3c8a7f98164b86e6ff04ddd09566b323967fd09dcc4d38c
3
+ size 33351
assets/scenes/libero-black-bowl/expert/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1fd11adb011caf6ecd364f3908fbedde20fac0973a66470ffda335dd39830dd7
3
+ size 147392
assets/scenes/libero-black-bowl/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c099ddae68aff7e093b72f7a2c8f31eeeb1e3fda88a176d50b4e2d7e8b880321
3
+ size 20164
assets/scenes/libero-black-bowl/thumb.jpg ADDED
assets/scenes/libero-bowl-plate/calibration/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df76bf0a3a6ea9c66884e93059338a39697d01b0381ff203bcefd267e2325f33
3
+ size 171469
assets/scenes/libero-bowl-plate/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a68fc4ce6960d7fb65192626ac2be4ad9426bf14ee188806c19057a43133c502
3
+ size 42567
assets/scenes/libero-bowl-plate/expert/agentview_rgb/video.mp4 ADDED
Binary file (97.7 kB). View file
 
assets/scenes/libero-bowl-plate/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:031a8f596b385be63492ac350721145051067df2d797333062ee9bd1bfc4cb35
3
+ size 11844
assets/scenes/libero-bowl-plate/thumb.jpg ADDED
assets/scenes/libero-drawer/calibration/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c86ccd71e4cf1a410d7cd8e0defadc3b907ef64b42d60c4d40a7c3ca19a4756a
3
+ size 142168
assets/scenes/libero-drawer/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df632260a5b759cf03f74c7f9604d869fb42627aa3ae010ccb67ff5aaf6f7ca5
3
+ size 34887
assets/scenes/libero-drawer/expert/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4d192f2e9079070ee653110ea023b74f8ccb2817b2ec65b334191fdbe5ececa5
3
+ size 142231
assets/scenes/libero-drawer/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa11f6bdcceb3e83d6367738c1c08b123ffddccc9d1102fe5990ef08091af8b4
3
+ size 17988
assets/scenes/libero-drawer/thumb.jpg ADDED
assets/scenes/libero-soup/calibration/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:99056f78f6dd0237eac3a61928dc7cc7538c64d010376c6740b9543a5d175231
3
+ size 199757
assets/scenes/libero-soup/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5e082d106c2b953cf904dff7909e11d6637f3dfcb7d87892f3d0927baac527a6
3
+ size 37959
assets/scenes/libero-soup/expert/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6afa6a3984a1ecc4f2f409cc590d2e4b045f5bbc3f6f9c18602907eabc7d50e
3
+ size 158901
assets/scenes/libero-soup/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a29645a254feda994d340368327851da72e0db55725803bd1f8031ce7028503
3
+ size 19268
assets/scenes/libero-soup/thumb.jpg ADDED
assets/scenes/libero-stove/calibration/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e05506712a8c19a320c05d2b663e49a375f9f84bdeb35404130022b789cfd10b
3
+ size 178356
assets/scenes/libero-stove/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:551906131f26cb71feeeb75ac9caac4b0e5d6eb915228e61b65d6175dc41eb9b
3
+ size 35655
assets/scenes/libero-stove/expert/agentview_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ebd84ff43c9fd0a2c6968444e25dc1e6f21daf87d65a66ef6dc4fd446b90d041
3
+ size 299642
assets/scenes/libero-stove/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:432f2235bb96fc43306c83426be6d4a72fcce9a5273212f5a13044926104aea1
3
+ size 35143
assets/scenes/libero-stove/thumb.jpg ADDED
assets/scenes/peginsertion/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b821226f7d54ce35d9c65e34ee3733e3b558c6d0b33ad3d820a84441b44eccc2
3
+ size 40263
assets/scenes/peginsertion/calibration/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce685583a51a6ed10388767caa1194578ab91c8ba7564f59c66cd9f8a38643c7
3
+ size 401985
assets/scenes/peginsertion/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2f3dfb8b8fc36322257195fd800abd768cf954448ac9a961995cb3f87f3a3c35
3
+ size 23270
assets/scenes/peginsertion/expert/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:868e664dfdbb75df9d438c7eb9e0286c7e2b007b29224928b38a437f3c488d12
3
+ size 290856
assets/scenes/peginsertion/thumb.jpg ADDED
assets/scenes/plugcharger/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b9b7a6ac349bff83a695e315bc02e662747d3fd96e2f65085396a0d1342754be
3
+ size 34887
assets/scenes/plugcharger/calibration/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94cc01fd066e02b89376a7f71fc5fc25c5c09ce21116547610ed66740ab0967f
3
+ size 273092
assets/scenes/plugcharger/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d35f4694d2270210e30323191372f10c3df808c4f5454ae63d796e850122b23
3
+ size 20454
assets/scenes/plugcharger/expert/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94b64706f8606a3d31bcd7e34f55347a86c2a0e81c492d202224b8037bb7edf0
3
+ size 206705
assets/scenes/plugcharger/thumb.jpg ADDED
assets/scenes/pushcube/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e150c2689746def363d24924317313239b155528148c5d212d475535d7a93b82
3
+ size 37959
assets/scenes/pushcube/calibration/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74a428f128b46fadedd1327ea6c1882267f766520e7cdcdeb6fc00da0e9f9ce0
3
+ size 385927
assets/scenes/pushcube/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1feb82f1bf0301033fc5748f36a04023bea4119f4221165da87771dc11a19157
3
+ size 9574
assets/scenes/pushcube/expert/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:535f820fb0f05cc360e09bf46c8983868f28992521e4f4279ba7684f3aed88b7
3
+ size 168226
assets/scenes/pushcube/thumb.jpg ADDED
assets/scenes/scenes.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "pushcube": {
3
+ "label": "ManiSkill \u00b7 PushCube",
4
+ "view": "render_camera_rgb",
5
+ "suite": "evaluation_maniskill",
6
+ "task": "PushCube-v1",
7
+ "num_frames": 72
8
+ },
9
+ "stackcube": {
10
+ "label": "ManiSkill \u00b7 StackCube",
11
+ "view": "render_camera_rgb",
12
+ "suite": "evaluation_maniskill",
13
+ "task": "StackCube-v1",
14
+ "num_frames": 108
15
+ },
16
+ "stackpyramid": {
17
+ "label": "ManiSkill \u00b7 StackPyramid",
18
+ "view": "render_camera_rgb",
19
+ "suite": "evaluation_maniskill",
20
+ "task": "StackPyramid-v1",
21
+ "num_frames": 177
22
+ },
23
+ "peginsertion": {
24
+ "label": "ManiSkill \u00b7 PegInsertionSide",
25
+ "view": "render_camera_rgb",
26
+ "suite": "evaluation_maniskill",
27
+ "task": "PegInsertionSide-v1",
28
+ "num_frames": 179
29
+ },
30
+ "plugcharger": {
31
+ "label": "ManiSkill \u00b7 PlugCharger",
32
+ "view": "render_camera_rgb",
33
+ "suite": "evaluation_maniskill",
34
+ "task": "PlugCharger-v1",
35
+ "num_frames": 157
36
+ },
37
+ "libero-bowl-plate": {
38
+ "label": "LIBERO \u00b7 put the bowl on the plate",
39
+ "view": "agentview_rgb",
40
+ "suite": "evaluation_libero",
41
+ "task": "put_the_bowl_on_the_plate",
42
+ "num_frames": 90
43
+ },
44
+ "libero-drawer": {
45
+ "label": "LIBERO \u00b7 open the middle drawer",
46
+ "view": "agentview_rgb",
47
+ "suite": "evaluation_libero",
48
+ "task": "open_the_middle_drawer_of_the_cabinet",
49
+ "num_frames": 138
50
+ },
51
+ "libero-soup": {
52
+ "label": "LIBERO \u00b7 alphabet soup into basket",
53
+ "view": "agentview_rgb",
54
+ "suite": "evaluation_libero",
55
+ "task": "pick_up_the_alphabet_soup_and_place_it_in_the_basket",
56
+ "num_frames": 148
57
+ },
58
+ "libero-black-bowl": {
59
+ "label": "LIBERO \u00b7 black bowl onto plate",
60
+ "view": "agentview_rgb",
61
+ "suite": "evaluation_libero",
62
+ "task": "pick_up_the_black_bowl_on_the_stove_and_place_it_on_the_plate",
63
+ "num_frames": 155
64
+ },
65
+ "libero-stove": {
66
+ "label": "LIBERO \u00b7 stove + moka pot",
67
+ "view": "agentview_rgb",
68
+ "suite": "evaluation_libero",
69
+ "task": "KITCHEN_SCENE3_turn_on_the_stove_and_put_the_moka_pot_on_it",
70
+ "num_frames": 272
71
+ }
72
+ }
assets/scenes/stackcube/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7788b705707d9566bed30098bc22c7ae9c59301da54bce0a5270b35fb12a8864
3
+ size 46407
assets/scenes/stackcube/calibration/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dcabdaf8a0af444fc1dbca7ee60a435fcdb23202636962db976ca8ed92a42440
3
+ size 468760
assets/scenes/stackcube/expert/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0dc2b5f9e732b1f090e6a815f86582dcf7cba27feaa8c9124f0ac598cae69ab1
3
+ size 14182
assets/scenes/stackcube/expert/render_camera_rgb/video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ebf6d49490eb59a8d161be2f0c12f36202297cfee95b7f9f97da45e2ad19107
3
+ size 192343
assets/scenes/stackcube/thumb.jpg ADDED
assets/scenes/stackpyramid/calibration/pose.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:393a570213981f6377fe153917d39b3ee3db73b7208a4c5bdcdf23d527b34359
3
+ size 37959