Files
WJLUOXIAO-XB_ToolBox/nodes_wan.py
T
2026-05-28 22:48:50 +08:00

411 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
XB-ToolBox Wan 视频节点 (ROCm 优化版)
=====================================
完全独立:XB_WanAnimateToVideo / CompileSettings / BlockSwap 零依赖。
ModelLoader / Sampler / VAELoader / Decode / TextEncode 需要 ComfyUI-WanVideoWrapper。
"""
import torch
import os
import sys
import gc
import importlib.util
import comfy.model_management as mm
import folder_paths
# ROCm 全局补丁(纯 torch,零依赖)
if not hasattr(torch.backends.cuda, "matmul"):
class _FM: allow_fp16_accumulation = False
torch.backends.cuda.matmul = _FM()
elif not hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"):
torch.backends.cuda.matmul.allow_fp16_accumulation = False
if not hasattr(torch.cuda, "get_device_capability"):
torch.cuda.get_device_capability = lambda d=None: (9, 0)
if not hasattr(torch.cuda, "reset_peak_memory_stats"):
torch.cuda.reset_peak_memory_stats = lambda d=None: None
_PKG_NAME = "_xb_wanwrapper"
_wan_loaded = False
def _find_wan_root():
for base in folder_paths.get_folder_paths("custom_nodes"):
for n in os.listdir(base):
if n.startswith("ComfyUI-WanVideoWrapper"):
p = os.path.join(base, n)
if os.path.exists(os.path.join(p, "__init__.py")):
return p
raise RuntimeError(
"ComfyUI-WanVideoWrapper 未安装。\n"
"此节点依赖它作为模型架构库。\n"
"XB_WanAnimateToVideo/XB_WanCompileSettings/XB_WanBlockSwap 不需要。"
)
def _wan_setup():
global _wan_loaded
if _wan_loaded: return
root = _find_wan_root()
if _PKG_NAME not in sys.modules:
s = importlib.util.spec_from_file_location(_PKG_NAME, os.path.join(root, "__init__.py"), submodule_search_locations=[root])
m = importlib.util.module_from_spec(s)
sys.modules[_PKG_NAME] = m
s.loader.exec_module(m)
_wan_loaded = True
def _wan_import(rel_path: str):
_wan_setup()
full = _PKG_NAME + "." + rel_path
if full in sys.modules: return sys.modules[full]
root = _find_wan_root()
parts = rel_path.split(".")
fp = os.path.join(root, *parts[:-1], parts[-1] + ".py")
if not os.path.exists(fp):
fp = os.path.join(root, *parts, "__init__.py")
s = importlib.util.spec_from_file_location(full, fp)
m = importlib.util.module_from_spec(s)
sys.modules[full] = m
s.loader.exec_module(m)
return m
# ============================================================
# 节点 1: XB_WanCompileSettings(独立)
# ============================================================
class XB_WanCompileSettings:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"backend": (["inductor"], {"default": "inductor"}),
"fullgraph": ("BOOLEAN", {"default": False}),
"mode": (["default", "reduce-overhead", "max-autotune"], {"default": "default"}),
"dynamic": ("BOOLEAN", {"default": False}),
"dynamo_cache_size_limit": ("INT", {"default": 64, "min": 0, "max": 1024}),
"compile_transformer_blocks_only": ("BOOLEAN", {"default": True}),
}}
RETURN_TYPES = ("WANCOMPILEARGS",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, **kw):
kw.setdefault("force_parameter_static_shapes", False)
kw.setdefault("allow_unmerged_lora_compile", False)
return (kw,)
# ============================================================
# 节点 2: XB_WanModelLoader(需 WanVideoWrapper)
# ============================================================
class XB_WanModelLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": (folder_paths.get_filename_list("diffusion_models"),),
"base_precision": (["bf16", "fp16", "fp32"], {"default": "bf16"}),
"quantization": (["disabled", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"], {"default": "disabled"}),
"load_device": (["main_device", "offload_device"], {"default": "main_device"}),
"attention_mode": (["sdpa", "sageattn"], {"default": "sdpa"}),
"rms_norm_function": (["default", "pytorch"], {"default": "default"}),
},
"optional": {
"block_swap_args": ("BLOCKSWAPARGS",),
"compile_args": ("WANCOMPILEARGS",),
"lora": ("WANVIDLORA",),
},
}
RETURN_TYPES = ("WANVIDEOMODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, model, base_precision, quantization, load_device, attention_mode,
rms_norm_function, block_swap_args=None, compile_args=None, lora=None):
if attention_mode is not None and "flash" in str(attention_mode):
attention_mode = "sdpa"
if base_precision == "fp16_fast":
base_precision = "bf16"
if compile_args is not None and hasattr(torch.version, 'hip') and torch.version.hip:
compile_args = None
kw = {"model": model, "base_precision": base_precision, "quantization": quantization,
"load_device": load_device, "attention_mode": attention_mode, "rms_norm_function": rms_norm_function}
if block_swap_args is not None: kw["block_swap_args"] = block_swap_args
if compile_args is not None: kw["compile_args"] = compile_args
if lora is not None: kw["lora"] = lora
return _wan_import("nodes_model_loading").WanVideoModelLoader().loadmodel(**kw)
# ============================================================
# 节点 3: XB_WanBlockSwap(独立)
# ============================================================
class XB_WanBlockSwap:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"blocks_to_swap": ("INT", {"default": 0, "min": 0, "max": 48}),
"offload_img_emb": ("BOOLEAN", {"default": False}),
"offload_txt_emb": ("BOOLEAN", {"default": False}),
"use_non_blocking": ("BOOLEAN", {"default": True}),
"prefetch_blocks": ("INT", {"default": 0, "min": 0, "max": 40}),
"vace_blocks_to_swap": ("INT", {"default": 0, "min": 0, "max": 15}),
}}
RETURN_TYPES = ("BLOCKSWAPARGS",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, **kw): return (kw,)
# ============================================================
# 节点 4: XB_WanSampler(需 WanVideoWrapper)
# ============================================================
class XB_WanSampler:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("WANVIDEOMODEL",),
"image_embeds": ("WANVIDIMAGE_EMBEDS",),
"steps": ("INT", {"default": 20, "min": 1, "max": 1000}),
"cfg": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 30.0, "step": 0.5}),
"shift": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.5}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
"force_offload": ("BOOLEAN", {"default": True}),
"scheduler": (["unipc", "dpmpp_2m", "dpmpp_sde", "euler", "heun"], {"default": "unipc"}),
"riflex_freq_index": ("INT", {"default": 0, "min": 0, "max": 1000}),
"cleanup": (["不做任何清理", "单次缓存清理", "双次缓存清理"], {"default": "单次缓存清理"}),
},
"optional": {
"text_embeds": ("WANVIDEOTEXTEMBEDS",),
"samples": ("LATENT",),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"batched_cfg": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("LATENT", "LATENT")
RETURN_NAMES = ("samples", "denoised_samples")
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, model, image_embeds, steps, cfg, shift, seed, force_offload, scheduler,
riflex_freq_index, cleanup="单次缓存清理", text_embeds=None,
samples=None, denoise=1.0, batched_cfg=False, **extra):
if hasattr(torch.backends.cuda, 'enable_mem_efficient_sdp'):
torch.backends.cuda.enable_mem_efficient_sdp(True)
if hasattr(torch.backends.cuda, 'enable_flash_sdp'):
torch.backends.cuda.enable_flash_sdp(False)
kw = {"model": model, "image_embeds": image_embeds, "shift": shift,
"steps": steps, "cfg": cfg, "seed": seed, "force_offload": force_offload,
"scheduler": scheduler, "riflex_freq_index": riflex_freq_index}
if text_embeds is not None: kw["text_embeds"] = text_embeds
if samples is not None: kw["samples"] = samples
if denoise < 1.0: kw["denoise_strength"] = denoise
if batched_cfg: kw["batched_cfg"] = batched_cfg
kw.update(extra)
r = _wan_import("nodes_sampler").WanVideoSampler().process(**kw)
if cleanup == "单次缓存清理":
torch.cuda.synchronize(); torch.cuda.empty_cache()
elif cleanup == "双次缓存清理":
torch.cuda.synchronize(); mm.soft_empty_cache(); torch.cuda.empty_cache(); gc.collect()
return r
# ============================================================
# 节点 5: XB_WanTextEncode(需 WanVideoWrapper)
# ============================================================
class XB_WanTextEncode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"t5_model": ("WANTEXTENCODER",),
"positive_prompt": ("STRING", {"multiline": True, "default": ""}),
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
"use_disk_cache": ("BOOLEAN", {"default": True}),
"device": (["gpu", "cpu"], {"default": "gpu"}),
}}
RETURN_TYPES = ("WANVIDEOTEXTEMBEDS",)
RETURN_NAMES = ("text_embeds",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, t5_model, positive_prompt, negative_prompt, use_disk_cache, device):
return _wan_import("nodes").WanVideoTextEncode().process(
positive_prompt=positive_prompt, negative_prompt=negative_prompt,
t5=t5_model, use_disk_cache=use_disk_cache, device=device)
# ============================================================
# 节点 6: XB_WanVAELoader(需 WanVideoWrapper)
# ============================================================
class XB_WanVAELoader:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model_name": (folder_paths.get_filename_list("vae"),),
"precision": (["bf16", "fp16", "fp32"], {"default": "bf16"}),
}, "optional": {
"compile_args": ("WANCOMPILEARGS",),
"use_cpu_cache": ("BOOLEAN", {"default": False}),
}}
RETURN_TYPES = ("WANVAE",)
RETURN_NAMES = ("vae",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, model_name, precision, compile_args=None, use_cpu_cache=False):
import comfy.utils as cu
dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[precision]
sd = cu.load_torch_file(folder_paths.get_full_path_or_raise("vae", model_name), safe_load=True)
if not any(k.startswith("model.") for k in sd):
sd = {f"model.{k}": v for k, v in sd.items()}
dim = sd["model.decoder.conv1.bias"].shape[0]
pruning_rate = 0.75 if dim == 96 else 0.0
mod = _wan_import("wanvideo.wan_video_vae")
cls = mod.WanVideoVAE38 if sd["model.conv2.weight"].shape[0] == 48 else mod.WanVideoVAE
vae = cls(dtype=dtype, pruning_rate=pruning_rate, cpu_cache=use_cpu_cache)
vae.load_state_dict(sd); del sd
vae.eval(); vae.to(dtype=dtype)
print(f" [XB_WanVAELoader] {model_name} | {precision} | pruning={pruning_rate}")
return (vae,)
# ============================================================
# 节点 7: XB_WanDecode(需 WanVideoWrapper)
# ============================================================
class XB_WanDecode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"vae": ("WANVAE",), "samples": ("LATENT",),
"enable_vae_tiling": ("BOOLEAN", {"default": True}),
"tile_x": ("INT", {"default": 256, "min": 64, "max": 1024, "step": 64}),
"tile_y": ("INT", {"default": 256, "min": 64, "max": 1024, "step": 64}),
"tile_stride_x": ("INT", {"default": 192, "min": 32, "max": 1024, "step": 32}),
"tile_stride_y": ("INT", {"default": 192, "min": 32, "max": 1024, "step": 32}),
"cleanup": (["不做任何清理", "单次缓存清理", "双次缓存清理", "卸载显存模型"], {"default": "单次缓存清理"}),
}}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, vae, samples, enable_vae_tiling, tile_x, tile_y,
tile_stride_x, tile_stride_y, cleanup="单次缓存清理"):
r = _wan_import("nodes").WanVideoDecode().decode(
vae=vae, samples=samples, enable_vae_tiling=enable_vae_tiling,
tile_x=tile_x, tile_y=tile_y, tile_stride_x=tile_stride_x, tile_stride_y=tile_stride_y)
if cleanup == "单次缓存清理":
torch.cuda.synchronize(); torch.cuda.empty_cache()
elif cleanup == "双次缓存清理":
torch.cuda.synchronize(); mm.soft_empty_cache(); torch.cuda.empty_cache(); gc.collect()
elif cleanup == "卸载显存模型":
mm.unload_all_models(); mm.soft_empty_cache()
torch.cuda.synchronize(); torch.cuda.empty_cache(); gc.collect()
return r
# ============================================================
# 节点 8: XB_WanAnimateToVideo(独立,对标官方 WanAnimateToVideo)
# ============================================================
class XB_WanAnimateToVideo:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"positive": ("CONDITIONING",), "negative": ("CONDITIONING",), "vae": ("VAE",),
"width": ("INT", {"default": 832, "min": 16, "max": 16384, "step": 16}),
"height": ("INT", {"default": 480, "min": 16, "max": 16384, "step": 16}),
"length": ("INT", {"default": 77, "min": 1, "max": 16384, "step": 4}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
"continue_motion_max_frames": ("INT", {"default": 5, "min": 1, "max": 16384, "step": 4}),
"video_frame_offset": ("INT", {"default": 0, "min": 0, "max": 16384, "step": 1}),
"vae_tile_size": ("INT", {"default": 256, "min": 0, "max": 2048, "step": 64}),
},
"optional": {
"clip_vision_output": ("CLIP_VISION_OUTPUT",),
"reference_image": ("IMAGE",), "face_video": ("IMAGE",),
"pose_video": ("IMAGE",), "background_video": ("IMAGE",),
"character_mask": ("MASK",), "continue_motion": ("IMAGE",),
},
}
RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT", "INT", "INT", "INT")
RETURN_NAMES = ("positive", "negative", "latent", "trim_latent", "trim_image", "video_frame_offset")
FUNCTION = "go"
CATEGORY = "XB_ToolBox/Wan"
def go(self, positive, negative, vae, width, height, length, batch_size,
continue_motion_max_frames, video_frame_offset, vae_tile_size=256,
reference_image=None, clip_vision_output=None, face_video=None,
pose_video=None, continue_motion=None, background_video=None, character_mask=None):
import comfy.utils
import node_helpers
def _enc(pixels):
if pixels.dim() == 3: pixels = pixels.unsqueeze(0)
p = pixels[:, :, :, :3]
if vae_tile_size and vae_tile_size > 0:
try: return vae.encode_tiled(p, tile_x=vae_tile_size, tile_y=vae_tile_size, overlap=32, tile_t=256, overlap_t=8)
except (AttributeError, TypeError): pass
return vae.encode(p)
if hasattr(torch.backends.cuda, 'enable_mem_efficient_sdp'): torch.backends.cuda.enable_mem_efficient_sdp(True)
trim_latent, ref_motion_latent_length = 0, 0
latent_length = ((length - 1) // 4) + 1
if reference_image is None: reference_image = torch.zeros((1, height, width, 3))
image = comfy.utils.common_upscale(reference_image[:length].movedim(-1, 1), width, height, "area", "center").movedim(1, -1)
concat_latent_image = _enc(image)
mask = torch.zeros((1, 4, concat_latent_image.shape[-3], concat_latent_image.shape[-2], concat_latent_image.shape[-1]), device=concat_latent_image.device, dtype=concat_latent_image.dtype)
trim_latent += concat_latent_image.shape[2]
if continue_motion is None:
image = torch.ones((length, height, width, 3)) * 0.5
else:
continue_motion = continue_motion[-continue_motion_max_frames:]
video_frame_offset = max(0, video_frame_offset - continue_motion.shape[0])
continue_motion = comfy.utils.common_upscale(continue_motion[-length:].movedim(-1, 1), width, height, "area", "center").movedim(1, -1)
image = torch.ones((length, height, width, continue_motion.shape[-1]), device=continue_motion.device, dtype=continue_motion.dtype) * 0.5
image[:continue_motion.shape[0]] = continue_motion
ref_motion_latent_length += ((continue_motion.shape[0] - 1) // 4) + 1
if clip_vision_output is not None:
positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output})
negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output})
if pose_video is not None:
if pose_video.shape[0] <= video_frame_offset: pose_video = None
else: pose_video = pose_video[video_frame_offset:]
if pose_video is not None:
pose_video = comfy.utils.common_upscale(pose_video[:length].movedim(-1, 1), width, height, "area", "center").movedim(1, -1)
if pose_video.shape[0] < length: pose_video = torch.cat((pose_video,) + (pose_video[-1:],) * (length - pose_video.shape[0]), dim=0)
pvl = _enc(pose_video)
positive = node_helpers.conditioning_set_values(positive, {"pose_video_latent": pvl})
negative = node_helpers.conditioning_set_values(negative, {"pose_video_latent": pvl})
if face_video is not None:
if face_video.shape[0] <= video_frame_offset: face_video = None
else: face_video = face_video[video_frame_offset:]
if face_video is not None:
fv = comfy.utils.common_upscale(face_video[:length].movedim(-1, 1), 512, 512, "area", "center") * 2.0 - 1.0
fv = fv.movedim(0, 1).unsqueeze(0)
positive = node_helpers.conditioning_set_values(positive, {"face_video_pixels": fv})
negative = node_helpers.conditioning_set_values(negative, {"face_video_pixels": fv * 0.0 - 1.0})
ref_images_num = max(0, ref_motion_latent_length * 4 - 3)
if background_video is not None and background_video.shape[0] > video_frame_offset:
bg = comfy.utils.common_upscale(background_video[video_frame_offset:][:length].movedim(-1, 1), width, height, "area", "center").movedim(1, -1)
if bg.shape[0] > ref_images_num: image[ref_images_num:bg.shape[0]] = bg[ref_images_num:]
mask_ref = torch.ones((1, 1, latent_length * 4, concat_latent_image.shape[-2], concat_latent_image.shape[-1]), device=mask.device, dtype=mask.dtype)
if continue_motion is not None: mask_ref[:, :, :ref_motion_latent_length * 4] = 0.0
if character_mask is not None:
if character_mask.shape[0] > video_frame_offset or character_mask.shape[0] == 1:
cm = character_mask.repeat((length,) + (1,) * (character_mask.ndim - 1)) if character_mask.shape[0] == 1 else character_mask[video_frame_offset:]
# cm: [T, H, W] (3D) → 保持4D传入 common_upscale
if cm.ndim == 3:
cm = cm.unsqueeze(1) # [T, 1, H, W]
cm = comfy.utils.common_upscale(cm[:length], concat_latent_image.shape[-1], concat_latent_image.shape[-2], "nearest-exact", "center")
# cm: [length, 1, H_lat, W_lat] → 转为 [1, 1, length, H_lat, W_lat]
cm = cm.permute(1, 0, 2, 3).unsqueeze(0)
if cm.shape[2] > ref_images_num: mask_ref[:, :, ref_images_num:cm.shape[2]] = cm[:, :, ref_images_num:]
concat_latent_image = torch.cat((concat_latent_image, _enc(image)), dim=2)
mask_ref = mask_ref.view(1, mask_ref.shape[2] // 4, 4, mask_ref.shape[3], mask_ref.shape[4]).transpose(1, 2)
mask = torch.cat((mask, mask_ref), dim=2)
positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
latent = torch.zeros([batch_size, 16, latent_length + trim_latent, height // 8, width // 8], device=comfy.model_management.intermediate_device())
return (positive, negative, {"samples": latent}, trim_latent, max(0, ref_motion_latent_length * 4 - 3), video_frame_offset + length)
__all__ = [
"XB_WanCompileSettings", "XB_WanModelLoader", "XB_WanBlockSwap",
"XB_WanSampler", "XB_WanTextEncode", "XB_WanVAELoader", "XB_WanDecode",
"XB_WanAnimateToVideo",
]