""" 推理工具:供 run_replay_loop_two_chunk 及评估脚本使用。 提供:load_pipeline_and_ckpt、load_prompt_for_video、sample_trajectory_samples_from_dataset。 VWM-style 简化版:使用 DiTBlock_w_Action + MLP_CamPose(block 内 action_mlp), 去除 CameraEncoder / camera_encoder_shallow 等冗余路径。 """ import os import re import sys import csv import random _script_dir = os.path.dirname(os.path.abspath(__file__)) _repo_root = os.path.dirname(os.path.dirname(_script_dir)) if _repo_root not in sys.path: sys.path.insert(0, _repo_root) import torch import torch.nn as nn from safetensors.torch import load_file as safe_load_file from diffsynth.pipelines.wan_video_new import WanVideoPipeline, ModelConfig from diffsynth.models.wan_video_dit import SelfAttention, CrossAttention, GateModule, modulate from diffsynth.models.memory.block_wise_ssm import BlockWiseStateSpaceMemory from diffsynth.models.memory.videossm_hybrid import HybridStateSpaceMemory DEFAULT_NEGATIVE_PROMPT = "oversaturated colors, overexposed, static, blurry details" # ── MLP_CamPose + DiTBlock_w_Action(与训练侧 train.py 完全一致)────────── class MLP_CamPose(nn.Module): def __init__(self, out_dim, pose_dim=12): super().__init__() self.proj = nn.Linear(pose_dim, out_dim) nn.init.zeros_(self.proj.weight) nn.init.zeros_(self.proj.bias) def forward(self, x): return self.proj(x) class DiTBlock_w_Action(nn.Module): def __init__(self, has_image_input, dim, num_heads, ffn_dim, eps=1e-6, add_action_attn=False, action_use_temporal_attention=True, use_cam_pose=False, use_block_wise_ssm=False, use_videossm_hybrid=False, videossm_kernel_size=3, videossm_expand=2): super().__init__() self.dim = dim self.num_heads = num_heads self.ffn_dim = ffn_dim if add_action_attn: self.self_attn_with_action = SelfAttention(dim, num_heads, eps) nn.init.zeros_(self.self_attn_with_action.o.weight) nn.init.zeros_(self.self_attn_with_action.o.bias) if use_cam_pose: self.action_mlp = MLP_CamPose(dim) else: self.action_mlp = MLP_CamPose(dim) self.self_attn = SelfAttention(dim, num_heads, eps) self.cross_attn = CrossAttention(dim, num_heads, eps, has_image_input=has_image_input) self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False) self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False) self.norm3 = nn.LayerNorm(dim, eps=eps) self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(approximate='tanh'), nn.Linear(ffn_dim, dim)) self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5) self.gate = GateModule() self.action_use_temporal_attention = action_use_temporal_attention self.use_block_wise_ssm = bool(use_block_wise_ssm) self.use_videossm_hybrid = bool(use_videossm_hybrid) if use_block_wise_ssm: self.block_wise_ssm = BlockWiseStateSpaceMemory(dim) if use_videossm_hybrid: self.videossm_hybrid = HybridStateSpaceMemory( dim, kernel_size=videossm_kernel_size, expand=videossm_expand ) def forward(self, x, context, t_mod, freqs, actions=None): has_seq = len(t_mod.shape) == 4 chunk_dim = 2 if has_seq else 1 shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ( self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=chunk_dim) if has_seq: shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ( shift_msa.squeeze(2), scale_msa.squeeze(2), gate_msa.squeeze(2), shift_mlp.squeeze(2), scale_mlp.squeeze(2), gate_mlp.squeeze(2), ) num_frames = None if actions is not None: original_x = x actions = self.action_mlp(actions.to(x.dtype)).to(x.dtype) bs, num_frames, dim = actions.shape actions = actions.reshape(bs, num_frames, 1, dim) x = x.reshape(bs, num_frames, -1, dim) x = x + actions if hasattr(self, "self_attn_with_action"): if not self.action_use_temporal_attention: x = x.reshape(bs, -1, dim) x = original_x + self.self_attn_with_action(x, freqs) else: from einops import rearrange x = rearrange(x, "b f p d -> (b p) f d") attn_out = self.self_attn_with_action(x) attn_out = rearrange(attn_out, "(b p) f d -> b f p d", b=bs) x = original_x + attn_out.reshape(bs, -1, dim) else: x = x.reshape(bs, -1, dim) input_x = modulate(self.norm1(x), shift_msa, scale_msa) x = self.gate(x, gate_msa, self.self_attn(input_x, freqs)) if num_frames is not None: if hasattr(self, "block_wise_ssm"): x = self.block_wise_ssm(x, f=num_frames) if hasattr(self, "videossm_hybrid"): spatial = x.shape[1] // int(num_frames) if int(num_frames) > 0 else 0 x = self.videossm_hybrid(x, f=num_frames, h=1, w=spatial) x = x + self.cross_attn(self.norm3(x), context) input_x = modulate(self.norm2(x), shift_mlp, scale_mlp) x = self.gate(x, gate_mlp, self.ffn(input_x)) return x # ── Utility functions ───────────────────────────────────────────────────── def load_pose_rt(json_file, frame_idx): """从数据集 camera json 读取单帧 12 维 RT。""" from src.model_training.fov_retrieval import load_camera_pose, pose_to_rt pose = load_camera_pose(json_file, int(frame_idx)) if pose is None: return None return pose_to_rt(pose, constrain_to_xy=True) def get_relative_rt(rt, ref_rt): """单帧相对位姿。""" from src.model_training.fov_retrieval import convert_rt_to_relative if rt is None or ref_rt is None or len(rt) < 12 or len(ref_rt) < 12: return None out = convert_rt_to_relative([rt], ref_rt) return out[0] if out else None def load_prompt_for_video(dataset_base, video_name): """从 dataset 目录下的 metadata CSV 读取该视频的 prompt。""" if not dataset_base or not video_name: return None vn = str(video_name).replace(".mp4", "").replace(".avi", "").strip() for name in ("metadata_full.csv", "metadata.csv", "prompts.csv"): path = os.path.join(dataset_base, name) if not os.path.isfile(path): continue try: with open(path, "r", encoding="utf-8") as f: for row in csv.DictReader(f): if row.get("video_name", "").strip() == vn: p = row.get("prompt", "").strip() if p: return p except Exception: pass return None def sample_trajectory_samples_from_dataset(dataset_base, num_samples=4, num_frames=81, seed=42): """从 dataset 枚举 (video_name, start_frame)。""" frames_dir = os.path.join(dataset_base, "frames") if not os.path.isdir(frames_dir): return [] candidates = [] for vn in sorted(os.listdir(frames_dir)): vd = os.path.join(frames_dir, vn) if not os.path.isdir(vd): continue try: names = [f for f in os.listdir(vd) if f.endswith(".png")] indices = sorted({int(os.path.splitext(n)[0]) for n in names if n[:-4].isdigit()}) if not indices: continue max_idx = max(indices) for start in indices: if start + num_frames - 1 <= max_idx: candidates.append((vn, start)) except Exception: continue if not candidates: return [] rng = random.Random(seed) if len(candidates) <= num_samples: return candidates return [candidates[i] for i in rng.sample(range(len(candidates)), num_samples)] # ── Pipeline loading (VWM-style) ────────────────────────────────────────── def _build_action_blocks( pipe, add_action_attn=False, action_use_temporal_attention=True, block_wise_block_ids=None, videossm_block_ids=None, ): """Replace DiT blocks with DiTBlock_w_Action (VWM cam_infer.py style).""" dit = pipe.dit old_blocks = dit.blocks has_image_input = getattr(dit, "has_image_input", False) dim = dit.dim num_heads = getattr(dit, "num_heads", None) or getattr(old_blocks[0], "num_heads", None) ffn_dim = getattr(dit, "ffn_dim", None) or getattr(old_blocks[0], "ffn_dim", None) eps = getattr(dit, "eps", 1e-6) block_dtype = next(old_blocks[0].parameters()).dtype block_device = next(old_blocks[0].parameters()).device block_wise_block_ids = set(block_wise_block_ids or []) videossm_block_ids = set(videossm_block_ids or []) new_blocks = torch.nn.ModuleList() for block_id, old_block in enumerate(old_blocks): new_block = DiTBlock_w_Action( has_image_input=has_image_input, dim=dim, num_heads=num_heads, ffn_dim=ffn_dim, eps=eps, add_action_attn=add_action_attn, action_use_temporal_attention=action_use_temporal_attention, use_cam_pose=True, use_block_wise_ssm=block_id in block_wise_block_ids, use_videossm_hybrid=block_id in videossm_block_ids, ) new_block = new_block.to(dtype=block_dtype, device=block_device) for attr in ("self_attn", "cross_attn", "norm1", "norm2", "norm3", "ffn"): if hasattr(old_block, attr) and hasattr(new_block, attr): getattr(new_block, attr).load_state_dict(getattr(old_block, attr).state_dict()) if hasattr(old_block, "modulation") and hasattr(new_block, "modulation"): with torch.no_grad(): new_block.modulation.copy_(old_block.modulation.to(dtype=block_dtype)) new_blocks.append(new_block) dit.blocks = new_blocks print(f"[loop_utils] Replaced {len(new_blocks)} blocks with DiTBlock_w_Action (MLP_CamPose)") if block_wise_block_ids: print(f"[loop_utils] Loaded Block-wise SSM slots on blocks: {sorted(block_wise_block_ids)[:8]}{'...' if len(block_wise_block_ids) > 8 else ''}") if videossm_block_ids: print(f"[loop_utils] Loaded VideoSSM hybrid slots on blocks: {sorted(videossm_block_ids)[:8]}{'...' if len(videossm_block_ids) > 8 else ''}") def load_pipeline_and_ckpt( ckpt_path, dit_path, text_encoder_path, vae_path, device="cuda", add_action_attn=False, action_use_temporal_attention=True, tokenizer_path=None, # Legacy kwargs accepted but ignored (CameraEncoder removed) **kwargs, ): """Load WanVideoPipeline, replace blocks with DiTBlock_w_Action, load ckpt (strict=False). VWM-style: no CameraEncoder, no complex inference logic. Action is injected via MLP_CamPose (nn.Linear(12, dim), zero-init) inside each DiTBlock_w_Action. """ print(f"[loop_utils] Loading pipeline (DiT -> {device})") if not tokenizer_path: import os as _os _base = _os.path.dirname(dit_path) _cand = _os.path.join(_base, "google", "umt5-xxl") if _os.path.isdir(_cand): tokenizer_path = _cand print(f"[loop_utils] Auto-detected tokenizer at {tokenizer_path}") model_configs = [ ModelConfig(path=dit_path, offload_device=device), ModelConfig(path=text_encoder_path, offload_device="cpu"), ModelConfig(path=vae_path, offload_device="cpu"), ] pipe = WanVideoPipeline.from_pretrained( torch_dtype=torch.bfloat16, device=device, model_configs=model_configs, tokenizer_config=ModelConfig(path=tokenizer_path) if tokenizer_path else None, ) ckpt = None block_wise_block_ids = set() videossm_block_ids = set() action_attn_block_ids = set() if ckpt_path and os.path.isfile(ckpt_path): ckpt = safe_load_file(ckpt_path) for key in ckpt.keys(): m = re.match(r"blocks\.(\d+)\.block_wise_ssm\.", key) if m: block_wise_block_ids.add(int(m.group(1))) m = re.match(r"blocks\.(\d+)\.videossm_hybrid\.", key) if m: videossm_block_ids.add(int(m.group(1))) m = re.match(r"blocks\.(\d+)\.self_attn_with_action\.", key) if m: action_attn_block_ids.add(int(m.group(1))) if action_attn_block_ids and not add_action_attn: add_action_attn = True print("[loop_utils] Detected action-attention weights in checkpoint; enabling self_attn_with_action") # Replace blocks with DiTBlock_w_Action, including memory slots implied by ckpt keys. _build_action_blocks( pipe, add_action_attn=add_action_attn, action_use_temporal_attention=action_use_temporal_attention, block_wise_block_ids=block_wise_block_ids, videossm_block_ids=videossm_block_ids, ) # Load ckpt (strict=False: base model keys match, action_mlp keys are extra) if ckpt_path and not os.path.isfile(ckpt_path): print(f"[loop_utils] WARNING: ckpt not found: {ckpt_path} — running with base model weights only!") if ckpt_path and os.path.isfile(ckpt_path): if ckpt is None: ckpt = safe_load_file(ckpt_path) missing, unexpected = pipe.dit.load_state_dict(ckpt, strict=False) action_keys = [k for k in ckpt if "action_mlp" in k] if not missing and not unexpected: print(f"[loop_utils] Ckpt loaded: {len(ckpt)} keys, perfect match") else: print(f"[loop_utils] Ckpt loaded: {len(ckpt)} keys, " f"missing={len(missing)}, unexpected={len(unexpected)}, " f"action_mlp_keys={len(action_keys)}") if missing: for k in sorted(missing)[:5]: print(f" missing: {k}") if unexpected: for k in sorted(unexpected)[:5]: print(f" unexpected: {k}") # Optional: load SpatialGridMemory if present in ckpt _smsd = { k.replace("spatial_memory_module.", "", 1): v for k, v in ckpt.items() if k.startswith("spatial_memory_module.") } if _smsd: try: from diffsynth.models.memory.spatial_grid_memory import SpatialGridMemory except ImportError: SpatialGridMemory = None if SpatialGridMemory is not None: dim = pipe.dit.dim w = _smsd.get("spatial_to_tokens") if w is not None: g2, num_tok = int(w.shape[0]), int(w.shape[1]) gsz = int(round(g2 ** 0.5)) if gsz * gsz != g2: gsz = 8 sm = SpatialGridMemory(dim, grid_size=gsz, num_tokens=num_tok) sm.load_state_dict(_smsd, strict=False) sm = sm.to(dtype=next(pipe.dit.parameters()).dtype, device=next(pipe.dit.parameters()).device) pipe.spatial_memory_module = sm pipe.use_spatial_memory_legacy = False print(f"[loop_utils] Loaded spatial_memory_module (grid={gsz}, tokens={num_tok})") _srmsd = { k.replace("spatial_memory_readout_module.", "", 1): v for k, v in ckpt.items() if k.startswith("spatial_memory_readout_module.") } if _srmsd: try: from diffsynth.models.memory.spatial_grid_memory import SpatialCrossAttnReadout except ImportError: SpatialCrossAttnReadout = None if SpatialCrossAttnReadout is not None: dim = pipe.dit.dim readout = SpatialCrossAttnReadout(dim=dim, num_heads=8) readout.load_state_dict(_srmsd, strict=False) readout = readout.to(dtype=next(pipe.dit.parameters()).dtype, device=next(pipe.dit.parameters()).device) pipe.spatial_memory_readout_module = readout print("[loop_utils] Loaded spatial_memory_readout_module") if getattr(pipe, "enable_vram_management", None): pipe.enable_vram_management() return pipe