multimodalart HF Staff commited on
Commit
1feaaac
·
verified ·
1 Parent(s): 986a3f6

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +11 -14
  2. app.py +197 -233
  3. astronaut.jpg +0 -0
  4. hot_air_balloon.jpg +0 -0
  5. husky_dog.jpg +0 -0
  6. requirements.txt +10 -12
README.md CHANGED
@@ -4,25 +4,22 @@ emoji: 🌊
4
  colorFrom: pink
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.15.1
8
  app_file: app.py
9
- short_description: Flow-based world model for robot video prediction
10
- python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  ---
13
 
14
  # FlowWAM
15
 
16
- **FlowWAM** generates future RGB and optical-flow video from a scene image and a robot task instruction.
 
17
 
18
- Given an initial scene observation and a natural-language instruction, the dual-stream video diffusion model (based on Wan2.2-TI2V-5B) jointly predicts:
 
 
19
 
20
- - **Future RGB frames** — the predicted visual trajectory
21
- - **Optical flow frames** — per-pixel motion representing the action signal
22
-
23
- ## Model
24
-
25
- - **Backbone**: Wan2.2-TI2V-5B (dual-stream video DiT)
26
- - **Checkpoint**: `YixiangChen/FlowWAM` (flowwam_robotwin)
27
- - **Paper**: [FlowWAM: Optical Flow as a Unified Action Representation for World Action Models](https://arxiv.org/abs/2607.13017)
28
- - **Project Page**: [flow-wam.github.io](https://flow-wam.github.io/)
 
4
  colorFrom: pink
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.20.0
8
  app_file: app.py
9
+ short_description: Predict future RGB video + optical flow from an image
10
+ python_version: "3.10"
11
  startup_duration_timeout: 1h
12
  ---
13
 
14
  # FlowWAM
15
 
16
+ Interactive demo of **FlowWAM: Optical Flow as a Unified Action Representation
17
+ for World Action Models**.
18
 
19
+ A single dual-stream video diffusion model (built on Wan2.2-TI2V-5B) jointly
20
+ predicts a **future RGB video** and its **optical-flow field** from one input
21
+ image and a short text instruction.
22
 
23
+ - Paper: https://huggingface.co/papers/2607.13017
24
+ - Code: https://github.com/YixiangChen515/FlowWAM
25
+ - Weights: https://huggingface.co/YixiangChen/FlowWAM
 
 
 
 
 
 
app.py CHANGED
@@ -1,84 +1,81 @@
1
  import os
2
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3
 
4
- import spaces
5
  import sys
6
- import tempfile
7
  import time
8
- import numpy as np
 
 
9
  import torch
 
10
  import gradio as gr
11
  from PIL import Image
12
- from tqdm import tqdm
13
 
14
- # ── Make the diffsynth library importable ──────────────────────────────
15
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
 
16
 
17
- from diffsynth.models.wan_video_dit_dual_stream import FlowStreamModule, init_flow_stream
 
18
  from diffsynth.pipelines.wan_video_new import WanVideoPipeline, ModelConfig
19
  from diffsynth.pipelines.wan_video_dual_stream import model_fn_wan_video_dual_stream
20
- from diffsynth.schedulers.flow_match import FlowMatchScheduler
21
  from diffsynth.data.video import save_video
22
- from diffsynth.models.utils import load_state_dict
23
 
24
- # ── Constants ───────────────────────────────────────────────────────────
25
- CHECKPOINT_ID = "YixiangChen/FlowWAM"
26
- CHECKPOINT_FILE = "flowwam_robotwin.safetensors"
27
- CAMERA_PREFIX = (
28
- "A multi-view video of an aloha robot in T-shape layout: "
29
- "the top row shows the full-size rear camera view, "
30
- "the bottom-left shows the half-size left arm camera view, "
31
- "and the bottom-right shows the half-size right arm camera view. "
32
- "The robot is performing the following task: "
33
- )
34
- SIZE_W, SIZE_H = 320, 256
35
- NUM_VIDEO_FRAMES = 9
36
- VIDEO_INFERENCE_STEPS = 25
37
- SIGMA_SHIFT = 5.0
38
-
39
- # ── Model loading (module scope) ────────────────────────────────────────
40
- log_msgs = []
41
- def _log(msg):
42
- log_msgs.append(msg)
43
- print(msg, flush=True)
44
-
45
- _log("Loading Wan2.2-TI2V-5B pipeline (VAE + T5 + DiT) ...")
 
 
 
 
46
  pipe = WanVideoPipeline.from_pretrained(
47
- torch_dtype=torch.bfloat16,
48
- device="cuda",
49
  model_configs=[
50
- ModelConfig(
51
- model_id="Wan-AI/Wan2.2-TI2V-5B",
52
- origin_file_pattern="models_t5_umt5-xxl-enc-bf16.pth",
53
- offload_device="cpu",
54
- ),
55
- ModelConfig(
56
- model_id="Wan-AI/Wan2.2-TI2V-5B",
57
- origin_file_pattern="diffusion_pytorch_model*.safetensors",
58
- offload_device="cpu",
59
- ),
60
- ModelConfig(
61
- model_id="Wan-AI/Wan2.2-TI2V-5B",
62
- origin_file_pattern="Wan2.2_VAE.pth",
63
- offload_device="cpu",
64
- ),
65
  ],
66
  tokenizer_config=ModelConfig(
67
- model_id="Wan-AI/Wan2.1-T2V-1.3B",
68
  origin_file_pattern="google/*",
 
 
69
  ),
 
70
  )
71
 
72
- _log("Initialising FlowStreamModule ...")
73
  flow_stream = init_flow_stream(pipe.dit)
74
 
75
- _log("Downloading and loading FlowWAM checkpoint ...")
76
- from huggingface_hub import hf_hub_download
77
- ckpt_path = hf_hub_download(CHECKPOINT_ID, CHECKPOINT_FILE, repo_type="model")
 
78
  state_dict = load_state_dict(ckpt_path)
79
 
80
- dit_keys = {}
81
- flow_keys = {}
82
  for k, v in state_dict.items():
83
  if k.startswith("action_expert."):
84
  continue
@@ -87,160 +84,137 @@ for k, v in state_dict.items():
87
  else:
88
  dit_keys[k] = v
89
 
 
90
  fp32_dit_values = {k: v.clone() for k, v in dit_keys.items() if v.dtype == torch.float32}
91
 
92
  missing, unexpected = pipe.dit.load_state_dict(dit_keys, strict=False)
93
- _log(f"DiT (full): loaded {len(dit_keys) - len(unexpected)} keys, {len(missing)} missing, {len(unexpected)} unexpected")
94
-
95
- if flow_keys:
96
- missing_f, unexpected_f = flow_stream.load_state_dict(flow_keys, strict=False)
97
- _log(f"FlowStream (full): loaded {len(flow_keys) - len(unexpected_f)} keys, {len(missing_f)} missing, {len(unexpected_f)} unexpected")
98
-
99
- # Apply fp32 modulation restoration
100
- from diffsynth.vram_management.layers import AutoWrappedLinear, WanAutoCastLayerNorm
101
- param_map = dict(pipe.dit.named_parameters())
102
- restored = 0
103
- for key, fp32_value in fp32_dit_values.items():
104
- if key in param_map:
105
- param_map[key].data = fp32_value.to(device=param_map[key].device)
106
- restored += param_map[key].numel()
107
-
108
- for seq_module in [pipe.dit.time_embedding, pipe.dit.time_projection]:
109
- for sub in seq_module.modules():
110
- if isinstance(sub, AutoWrappedLinear):
111
- sub.offload_dtype = torch.float32
112
- sub.onload_dtype = torch.float32
113
- sub.computation_dtype = torch.float32
114
-
115
- def _pre_hook(_mod, args):
116
- return tuple(a.float() if isinstance(a, torch.Tensor) else a for a in args)
117
-
118
- def _post_hook(_mod, _args, output):
119
- return output.bfloat16() if isinstance(output, torch.Tensor) else output
120
-
121
- for seq_module in [pipe.dit.time_embedding, pipe.dit.time_projection]:
122
- seq_module.register_forward_pre_hook(_pre_hook)
123
- seq_module.register_forward_hook(_post_hook)
124
-
125
- for module in pipe.dit.modules():
126
- if isinstance(module, WanAutoCastLayerNorm):
127
- module.offload_dtype = torch.float32
128
- module.onload_dtype = torch.float32
129
-
130
- _log(f"[FP32Modulation] Restored {restored:,} fp32 params")
131
-
132
- # Enable VRAM management (needed for 48 GB ZeroGPU large tier)
133
  pipe.enable_vram_management()
134
- flow_stream = flow_stream.to(device="cuda", dtype=torch.bfloat16).eval()
135
- _log("Model loaded successfully.")
136
-
137
-
138
- # ── T-shape tiling helper ───────────────────────────────────────────────
139
- def tshape_tile(head_img, left_img, right_img):
140
- """T-shape spatial concatenation: head on top, left+right half-res below."""
141
- orig_h, orig_w = head_img.shape[:2]
142
- half_h, half_w = orig_h // 2, orig_w // 2
143
- import cv2
144
- left_half = cv2.resize(left_img, (half_w, half_h))
145
- right_half = cv2.resize(right_img, (half_w, half_h))
146
- bottom = np.hstack([left_half, right_half])
147
- return np.vstack([head_img, bottom])
148
-
149
-
150
- # ── Inference ──────────────────────────────────────────────────────────
151
- @spaces.GPU(duration=300)
152
- def generate_video(
153
- input_image,
154
- instruction: str,
155
- seed: int = 1,
156
- num_video_frames: int = NUM_VIDEO_FRAMES,
157
- video_inference_steps: int = VIDEO_INFERENCE_STEPS,
158
- progress=gr.Progress(track_tqdm=True),
159
- ):
160
- """Generate future RGB and optical-flow video frames from a single input image.
161
-
162
- Given a scene image and a language instruction describing the robot task,
163
- FlowWAM's dual-stream video diffusion jointly predicts the future RGB
164
- trajectory and the corresponding optical-flow video.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  Args:
167
- input_image: The initial scene image (any resolution; will be resized).
168
- instruction: Natural-language description of the robot task.
169
- seed: Random seed for reproducibility.
170
- num_video_frames: Number of future video frames to generate (default 9).
171
- video_inference_steps: Number of diffusion denoising steps (default 25).
 
 
 
 
 
172
  """
173
- t0 = time.perf_counter()
174
-
175
- if input_image is None:
176
  raise gr.Error("Please provide an input image.")
 
177
 
178
- w, h = SIZE_W, SIZE_H
179
  device = pipe.device
180
  dtype = pipe.torch_dtype
181
  vae_z_dim = getattr(pipe.vae, "z_dim", 16)
182
-
183
- # ── Prepare the T-shape tiled image ────────────────────────────────
184
- img_np = np.array(input_image.convert("RGB"))
185
- head = np.array(Image.fromarray(img_np).resize((w, h), Image.BICUBIC))
186
- # For single-image input, create left/right views by cropping the image
187
- half_h = h // 2
188
- half_w = w // 2
189
- left_crop = img_np[:, :img_np.shape[1]//2]
190
- right_crop = img_np[:, img_np.shape[1]//2:]
191
- if left_crop.size == 0 or right_crop.size == 0:
192
- left_crop = img_np
193
- right_crop = img_np
194
- left = np.array(Image.fromarray(left_crop).resize((w, h), Image.BICUBIC))
195
- right = np.array(Image.fromarray(right_crop).resize((w, h), Image.BICUBIC))
196
- tiled = tshape_tile(head, left, right)
197
-
198
- tiled_h, tiled_w = tiled.shape[:2]
199
  tiled_h, tiled_w, video_frames = pipe.check_resize_height_width(
200
- tiled_h, tiled_w, num_video_frames)
201
- tiled_pil = Image.fromarray(tiled).resize((tiled_w, tiled_h), Image.BICUBIC)
202
- flow_h, flow_w = tiled_h, tiled_w
203
 
204
- # ── Text encoding ──────────────────────────────────────────────────
205
- video_prompt = CAMERA_PREFIX + instruction
206
  pipe.load_models_to_device(["text_encoder"])
207
- context = pipe.prompter.encode_prompt(video_prompt, positive=True, device=device)
208
 
209
- # ── VAE encode the single conditioning frame ───────────────────────
210
  pipe.load_models_to_device(["vae"])
211
  upscale = pipe.vae.upsampling_factor
212
  T_lat = (video_frames - 1) // 4 + 1
213
  rgb_H_lat = tiled_h // upscale
214
  rgb_W_lat = tiled_w // upscale
215
- flow_H_lat = flow_h // upscale
216
- flow_W_lat = flow_w // upscale
217
 
218
- rgb_vid = pipe.preprocess_video([tiled_pil])
219
  rgb_prefix = pipe.vae.encode(rgb_vid, device=device).to(dtype=dtype, device=device)
220
 
221
- zero_flow_pil = Image.new("RGB", (flow_w, flow_h), (255, 255, 255))
222
  flow_vid = pipe.preprocess_video([zero_flow_pil])
223
  flow_prefix = pipe.vae.encode(flow_vid, device=device).to(dtype=dtype, device=device)
224
 
225
  rgb_noise_shape = (1, vae_z_dim, T_lat, rgb_H_lat, rgb_W_lat)
226
- flow_noise_shape = (1, vae_z_dim, T_lat, flow_H_lat, flow_W_lat)
227
  rgb_noise = pipe.generate_noise(rgb_noise_shape, seed=seed, rand_device="cpu").to(dtype=dtype, device=device)
228
  rgb_noise[:, :, :1] = rgb_prefix
229
- flow_noise = pipe.generate_noise(
230
- flow_noise_shape, seed=(seed + 1) if seed is not None else None, rand_device="cpu"
231
- ).to(dtype=dtype, device=device)
232
  flow_noise[:, :, :1] = flow_prefix
233
 
234
  rgb_latents = rgb_noise.clone()
235
  flow_latents = flow_noise.clone()
236
 
237
- # ── Stage 1: dual-stream video denoising ────────────────────────────
238
- pipe.scheduler.set_timesteps(video_inference_steps, shift=SIGMA_SHIFT)
239
  pipe.load_models_to_device(pipe.in_iteration_models)
240
-
241
- for progress_id, timestep in enumerate(
242
- tqdm(pipe.scheduler.timesteps, desc="Video DiT Denoising")
243
- ):
244
  t_tensor = timestep.unsqueeze(0).to(dtype=dtype, device=device)
245
  rgb_pred, flow_pred = model_fn_wan_video_dual_stream(
246
  dit=pipe.dit,
@@ -257,86 +231,76 @@ def generate_video(
257
  rgb_latents[:, :, :1] = rgb_prefix
258
  flow_latents[:, :, :1] = flow_prefix
259
 
260
- # ── Decode both streams ─────────────────────────────────────────────
261
  pipe.load_models_to_device(["vae"])
262
- rgb_video = pipe.vae_output_to_video(pipe.vae.decode(rgb_latents, device=device))
263
- flow_video = pipe.vae_output_to_video(pipe.vae.decode(flow_latents, device=device))
264
  pipe.load_models_to_device([])
265
 
266
- # ── Save videos ─────────────────────────────────────────────────────
267
- rgb_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
268
- flow_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
269
- save_video(rgb_video, rgb_path, fps=12)
270
- save_video(flow_video, flow_path, fps=12)
271
-
272
- elapsed = time.perf_counter() - t0
273
- _log(f"Generation complete in {elapsed:.1f}s")
274
 
275
- return rgb_path, flow_path, f"{elapsed:.1f}s"
276
 
277
-
278
- # ── Gradio UI ───────────────────────────────────────────────────────────
 
279
  CSS = """
280
  #col-container { max-width: 1100px; margin: 0 auto; }
281
  .dark .gradio-container { color: var(--body-text-color); }
282
  """
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
285
  with gr.Column(elem_id="col-container"):
286
- gr.Markdown(
287
- "# 🌊 FlowWAM: Optical Flow as a Unified Action Representation for World Action Models\n"
288
- "Generate future RGB and optical-flow video from a scene image and a robot task instruction.\n\n"
289
- "[Paper](https://arxiv.org/abs/2607.13017) | "
290
- "[Project Page](https://flow-wam.github.io/) | "
291
- "[Model](https://huggingface.co/YixiangChen/FlowWAM)"
292
- )
293
-
294
  with gr.Row():
295
- with gr.Column(scale=1):
296
- input_image = gr.Image(
297
- label="Input Scene Image",
298
- type="pil",
299
- height=256,
300
- )
301
  instruction = gr.Textbox(
302
- label="Task Instruction",
303
- placeholder="e.g., pick up the block and place it on the right",
304
- lines=2,
305
  )
306
- run_btn = gr.Button("Generate Video", variant="primary")
307
-
308
- with gr.Column(scale=1):
309
- rgb_video_out = gr.Video(label="Predicted Future RGB")
310
- flow_video_out = gr.Video(label="Predicted Optical Flow")
311
- time_out = gr.Textbox(label="Inference Time", interactive=False)
312
-
313
- with gr.Accordion("Advanced Settings", open=False):
314
- seed_input = gr.Number(label="Seed", value=1, precision=0)
315
- num_frames_input = gr.Number(
316
- label="Number of Video Frames", value=NUM_VIDEO_FRAMES,
317
- precision=0, minimum=5, maximum=49,
318
- )
319
- num_steps_input = gr.Slider(
320
- label="Denoising Steps", value=VIDEO_INFERENCE_STEPS,
321
- minimum=5, maximum=50, step=1,
322
- )
323
 
324
  gr.Examples(
325
  examples=[
326
- ["example_input.jpg", "pick up the block and place it in the basket"],
327
- ["example_input.jpg", "grab the bottle and move it to the right side"],
 
328
  ],
329
- inputs=[input_image, instruction],
330
- outputs=[rgb_video_out, flow_video_out, time_out],
331
- fn=generate_video,
332
  cache_examples=True,
333
  cache_mode="lazy",
334
  )
335
 
336
- run_btn.click(
337
- fn=generate_video,
338
- inputs=[input_image, instruction, seed_input, num_frames_input, num_steps_input],
339
- outputs=[rgb_video_out, flow_video_out, time_out],
340
- )
341
-
342
- demo.launch(mcp_server=True)
 
1
  import os
2
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3
 
 
4
  import sys
 
5
  import time
6
+ import tempfile
7
+
8
+ import spaces # noqa: E402 (must precede torch / CUDA imports)
9
  import torch
10
+ import numpy as np
11
  import gradio as gr
12
  from PIL import Image
13
+ from huggingface_hub import hf_hub_download
14
 
15
+ # Make the vendored diffsynth package importable.
16
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17
+ if SCRIPT_DIR not in sys.path:
18
+ sys.path.insert(0, SCRIPT_DIR)
19
 
20
+ from diffsynth.models.utils import load_state_dict
21
+ from diffsynth.models.wan_video_dit_dual_stream import init_flow_stream
22
  from diffsynth.pipelines.wan_video_new import WanVideoPipeline, ModelConfig
23
  from diffsynth.pipelines.wan_video_dual_stream import model_fn_wan_video_dual_stream
 
24
  from diffsynth.data.video import save_video
 
25
 
26
+ # ----------------------------------------------------------------------------
27
+ # Model setup (module scope — ZeroGPU packs weights to disk at startup).
28
+ # ----------------------------------------------------------------------------
29
+ BASE_MODEL = "Wan-AI/Wan2.2-TI2V-5B"
30
+ TOKENIZER_MODEL = "Wan-AI/Wan2.1-T2V-1.3B"
31
+ FLOWWAM_REPO = "YixiangChen/FlowWAM"
32
+ FLOWWAM_CKPT = "flowwam_worldarena_stage1.safetensors"
33
+
34
+ MODELS_DIR = os.path.join(SCRIPT_DIR, "models")
35
+ os.makedirs(MODELS_DIR, exist_ok=True)
36
+
37
+ DTYPE = torch.bfloat16
38
+ DEVICE = "cuda"
39
+
40
+
41
+ def _mc(pattern, offload="cpu"):
42
+ return ModelConfig(
43
+ model_id=BASE_MODEL,
44
+ origin_file_pattern=pattern,
45
+ offload_device=offload,
46
+ local_model_path=MODELS_DIR,
47
+ download_resource="huggingface",
48
+ )
49
+
50
+
51
+ print("Loading Wan2.2-TI2V-5B dual-stream pipeline (VAE + T5 + DiT) ...", flush=True)
52
  pipe = WanVideoPipeline.from_pretrained(
53
+ torch_dtype=DTYPE,
54
+ device=DEVICE,
55
  model_configs=[
56
+ _mc("models_t5_umt5-xxl-enc-bf16.pth"),
57
+ _mc("diffusion_pytorch_model*.safetensors"),
58
+ _mc("Wan2.2_VAE.pth"),
 
 
 
 
 
 
 
 
 
 
 
 
59
  ],
60
  tokenizer_config=ModelConfig(
61
+ model_id=TOKENIZER_MODEL,
62
  origin_file_pattern="google/*",
63
+ local_model_path=MODELS_DIR,
64
+ download_resource="huggingface",
65
  ),
66
+ redirect_common_files=False,
67
  )
68
 
69
+ # Flow stream: deep-copied patch-embed + head from the DiT.
70
  flow_stream = init_flow_stream(pipe.dit)
71
 
72
+ # Load the FlowWAM checkpoint: DiT + flow_stream keys (no action_expert in
73
+ # the world-model stage-1 checkpoint).
74
+ print(f"Downloading FlowWAM checkpoint {FLOWWAM_CKPT} ...", flush=True)
75
+ ckpt_path = hf_hub_download(FLOWWAM_REPO, FLOWWAM_CKPT)
76
  state_dict = load_state_dict(ckpt_path)
77
 
78
+ dit_keys, flow_keys = {}, {}
 
79
  for k, v in state_dict.items():
80
  if k.startswith("action_expert."):
81
  continue
 
84
  else:
85
  dit_keys[k] = v
86
 
87
+ # Params trained in fp32 (modulation / time-MLP / LayerNorm) — restore later.
88
  fp32_dit_values = {k: v.clone() for k, v in dit_keys.items() if v.dtype == torch.float32}
89
 
90
  missing, unexpected = pipe.dit.load_state_dict(dit_keys, strict=False)
91
+ print(f"DiT (full): loaded {len(dit_keys) - len(unexpected)} keys, "
92
+ f"{len(missing)} missing, {len(unexpected)} unexpected", flush=True)
93
+ missing, unexpected = flow_stream.load_state_dict(flow_keys, strict=False)
94
+ print(f"FlowStream (full): loaded {len(flow_keys) - len(unexpected)} keys, "
95
+ f"{len(missing)} missing, {len(unexpected)} unexpected", flush=True)
96
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  pipe.enable_vram_management()
98
+
99
+
100
+ def _apply_fp32_modulation(dit, fp32_state_values):
101
+ """Restore fp32 precision for modulation / time-MLP / LayerNorm params."""
102
+ from diffsynth.vram_management.layers import AutoWrappedLinear, WanAutoCastLayerNorm
103
+ param_map = dict(dit.named_parameters())
104
+ for key, fp32_value in fp32_state_values.items():
105
+ if key in param_map:
106
+ param_map[key].data = fp32_value.to(device=param_map[key].device)
107
+ for seq_module in [dit.time_embedding, dit.time_projection]:
108
+ for sub in seq_module.modules():
109
+ if isinstance(sub, AutoWrappedLinear):
110
+ sub.offload_dtype = torch.float32
111
+ sub.onload_dtype = torch.float32
112
+ sub.computation_dtype = torch.float32
113
+
114
+ def _pre_hook(_mod, args):
115
+ return tuple(a.float() if isinstance(a, torch.Tensor) else a for a in args)
116
+
117
+ def _post_hook(_mod, _args, output):
118
+ return output.bfloat16() if isinstance(output, torch.Tensor) else output
119
+
120
+ for seq_module in [dit.time_embedding, dit.time_projection]:
121
+ seq_module.register_forward_pre_hook(_pre_hook)
122
+ seq_module.register_forward_hook(_post_hook)
123
+ for module in dit.modules():
124
+ if isinstance(module, WanAutoCastLayerNorm):
125
+ module.offload_dtype = torch.float32
126
+ module.onload_dtype = torch.float32
127
+
128
+
129
+ if fp32_dit_values:
130
+ _apply_fp32_modulation(pipe.dit, fp32_dit_values)
131
+
132
+ flow_stream = flow_stream.to(device=DEVICE, dtype=DTYPE).eval()
133
+ print("FlowWAM pipeline ready.", flush=True)
134
+
135
+
136
+ # ----------------------------------------------------------------------------
137
+ # Inference — dual-stream world-model rollout (stage 1 only).
138
+ # ----------------------------------------------------------------------------
139
+ def _estimate(image, instruction, num_frames=49, num_inference_steps=25,
140
+ sigma_shift=5.0, seed=1, *args, **kwargs):
141
+ steps = int(num_inference_steps)
142
+ return min(160, 45 + int(steps * 4.0))
143
+
144
+
145
+ @spaces.GPU(duration=_estimate)
146
+ @torch.no_grad()
147
+ def generate(image, instruction, num_frames=49, num_inference_steps=25,
148
+ sigma_shift=5.0, seed=1,
149
+ progress=gr.Progress(track_tqdm=True)):
150
+ """Predict a future RGB video and its optical-flow field from one image + instruction.
151
 
152
  Args:
153
+ image: the conditioning first frame (PIL image).
154
+ instruction: text describing the action / motion to imagine.
155
+ num_frames: number of frames to generate (4k+1).
156
+ num_inference_steps: dual-stream denoising steps.
157
+ sigma_shift: flow-match scheduler sigma shift.
158
+ seed: RNG seed.
159
+
160
+ Returns:
161
+ (rgb_video_path, flow_video_path): mp4 files for the predicted future
162
+ RGB frames and the predicted optical-flow field.
163
  """
164
+ if image is None:
 
 
165
  raise gr.Error("Please provide an input image.")
166
+ instruction = (instruction or "").strip()
167
 
 
168
  device = pipe.device
169
  dtype = pipe.torch_dtype
170
  vae_z_dim = getattr(pipe.vae, "z_dim", 16)
171
+ seed = int(seed)
172
+ num_frames = int(num_frames)
173
+
174
+ # ---- Resize conditioning frame to a valid grid ----
175
+ if isinstance(image, np.ndarray):
176
+ image = Image.fromarray(image)
177
+ image = image.convert("RGB")
178
+ w, h = image.size
179
+ # Keep a compact aspect-preserving size (~320x256 like the reference).
180
+ target_w = 320
181
+ target_h = max(1, round(h * target_w / w))
 
 
 
 
 
 
182
  tiled_h, tiled_w, video_frames = pipe.check_resize_height_width(
183
+ target_h, target_w, num_frames)
184
+ cond_pil = image.resize((tiled_w, tiled_h), Image.BICUBIC)
 
185
 
186
+ # ---- Text encoding ----
 
187
  pipe.load_models_to_device(["text_encoder"])
188
+ context = pipe.prompter.encode_prompt(instruction, positive=True, device=device)
189
 
190
+ # ---- VAE encode the single conditioning frame (RGB + zero-flow prefix) ----
191
  pipe.load_models_to_device(["vae"])
192
  upscale = pipe.vae.upsampling_factor
193
  T_lat = (video_frames - 1) // 4 + 1
194
  rgb_H_lat = tiled_h // upscale
195
  rgb_W_lat = tiled_w // upscale
 
 
196
 
197
+ rgb_vid = pipe.preprocess_video([cond_pil])
198
  rgb_prefix = pipe.vae.encode(rgb_vid, device=device).to(dtype=dtype, device=device)
199
 
200
+ zero_flow_pil = Image.new("RGB", (tiled_w, tiled_h), (255, 255, 255))
201
  flow_vid = pipe.preprocess_video([zero_flow_pil])
202
  flow_prefix = pipe.vae.encode(flow_vid, device=device).to(dtype=dtype, device=device)
203
 
204
  rgb_noise_shape = (1, vae_z_dim, T_lat, rgb_H_lat, rgb_W_lat)
205
+ flow_noise_shape = (1, vae_z_dim, T_lat, rgb_H_lat, rgb_W_lat)
206
  rgb_noise = pipe.generate_noise(rgb_noise_shape, seed=seed, rand_device="cpu").to(dtype=dtype, device=device)
207
  rgb_noise[:, :, :1] = rgb_prefix
208
+ flow_noise = pipe.generate_noise(flow_noise_shape, seed=seed + 1, rand_device="cpu").to(dtype=dtype, device=device)
 
 
209
  flow_noise[:, :, :1] = flow_prefix
210
 
211
  rgb_latents = rgb_noise.clone()
212
  flow_latents = flow_noise.clone()
213
 
214
+ # ---- Dual-stream video denoising ----
215
+ pipe.scheduler.set_timesteps(int(num_inference_steps), shift=float(sigma_shift))
216
  pipe.load_models_to_device(pipe.in_iteration_models)
217
+ for progress_id, timestep in enumerate(pipe.scheduler.timesteps):
 
 
 
218
  t_tensor = timestep.unsqueeze(0).to(dtype=dtype, device=device)
219
  rgb_pred, flow_pred = model_fn_wan_video_dual_stream(
220
  dit=pipe.dit,
 
231
  rgb_latents[:, :, :1] = rgb_prefix
232
  flow_latents[:, :, :1] = flow_prefix
233
 
234
+ # ---- Decode both streams ----
235
  pipe.load_models_to_device(["vae"])
236
+ rgb_frames = pipe.vae_output_to_video(pipe.vae.decode(rgb_latents, device=device))
237
+ flow_frames = pipe.vae_output_to_video(pipe.vae.decode(flow_latents, device=device))
238
  pipe.load_models_to_device([])
239
 
240
+ rgb_path = tempfile.NamedTemporaryFile(suffix="_rgb.mp4", delete=False).name
241
+ flow_path = tempfile.NamedTemporaryFile(suffix="_flow.mp4", delete=False).name
242
+ save_video(rgb_frames, rgb_path, fps=12)
243
+ save_video(flow_frames, flow_path, fps=12)
244
+ return rgb_path, flow_path
 
 
 
245
 
 
246
 
247
+ # ----------------------------------------------------------------------------
248
+ # UI
249
+ # ----------------------------------------------------------------------------
250
  CSS = """
251
  #col-container { max-width: 1100px; margin: 0 auto; }
252
  .dark .gradio-container { color: var(--body-text-color); }
253
  """
254
 
255
+ DESCRIPTION = """
256
+ # FlowWAM — Optical Flow as a Unified Action Representation
257
+
258
+ A single dual-stream video diffusion model (built on **Wan2.2-TI2V-5B**) that
259
+ jointly predicts a **future RGB video** and its **optical-flow field** from one
260
+ image and a short text instruction. From the paper
261
+ *FlowWAM: Optical Flow as a Unified Action Representation for World Action Models*.
262
+
263
+ Give it a starting frame and describe the motion — it imagines how the scene
264
+ evolves and the dense per-pixel motion (flow) that drives it.
265
+
266
+ [Paper](https://huggingface.co/papers/2607.13017) · [Code](https://github.com/YixiangChen515/FlowWAM) · [Weights](https://huggingface.co/YixiangChen/FlowWAM)
267
+ """
268
+
269
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
270
  with gr.Column(elem_id="col-container"):
271
+ gr.Markdown(DESCRIPTION)
 
 
 
 
 
 
 
272
  with gr.Row():
273
+ with gr.Column():
274
+ image = gr.Image(label="Input image (first frame)", type="pil")
 
 
 
 
275
  instruction = gr.Textbox(
276
+ label="Instruction",
277
+ placeholder="describe the motion, e.g. 'the astronaut walks forward'",
 
278
  )
279
+ run = gr.Button("Generate", variant="primary")
280
+ with gr.Column():
281
+ rgb_out = gr.Video(label="Predicted future RGB")
282
+ flow_out = gr.Video(label="Predicted optical flow")
283
+ with gr.Accordion("Advanced settings", open=False):
284
+ num_frames = gr.Slider(13, 49, value=49, step=4, label="Frames (4k+1)")
285
+ num_inference_steps = gr.Slider(10, 40, value=25, step=1, label="Denoising steps")
286
+ sigma_shift = gr.Slider(1.0, 8.0, value=5.0, step=0.5, label="Sigma shift")
287
+ seed = gr.Number(value=1, precision=0, label="Seed")
288
+
289
+ inputs = [image, instruction, num_frames, num_inference_steps, sigma_shift, seed]
290
+ run.click(generate, inputs=inputs, outputs=[rgb_out, flow_out], api_name="generate")
 
 
 
 
 
291
 
292
  gr.Examples(
293
  examples=[
294
+ ["astronaut.jpg", "the astronaut walks across the surface"],
295
+ ["husky_dog.jpg", "the dog runs forward"],
296
+ ["hot_air_balloon.jpg", "the balloon drifts upward across the sky"],
297
  ],
298
+ inputs=[image, instruction],
299
+ outputs=[rgb_out, flow_out],
300
+ fn=generate,
301
  cache_examples=True,
302
  cache_mode="lazy",
303
  )
304
 
305
+ if __name__ == "__main__":
306
+ demo.launch(mcp_server=True)
 
 
 
 
 
astronaut.jpg ADDED
hot_air_balloon.jpg ADDED
husky_dog.jpg ADDED
requirements.txt CHANGED
@@ -1,18 +1,16 @@
1
- transformers
2
  torchvision
3
- imageio
4
- imageio-ffmpeg
5
- pillow
6
- regex
7
  safetensors
8
  einops
 
 
 
 
9
  sentencepiece
10
  protobuf
11
- modelscope
12
  ftfy
13
- pynvml
14
- accelerate
15
- opencv-python
16
- pyyaml
17
- tqdm
18
- numpy<2
 
1
+ torch
2
  torchvision
3
+ transformers
4
+ accelerate
 
 
5
  safetensors
6
  einops
7
+ imageio
8
+ imageio[ffmpeg]
9
+ opencv-python-headless
10
+ pillow
11
  sentencepiece
12
  protobuf
 
13
  ftfy
14
+ regex
15
+ modelscope
16
+ numpy<2