Files
smthemex-ComfyUI_AniCrafter/AniCrafter_node.py
T
2025-07-09 14:00:15 +08:00

275 lines
11 KiB
Python

# !/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import torch
import gc
import numpy as np
from torchvision.transforms import v2
from .AniCrafter.diffsynth import ModelManager
from .node_utils import gc_cleanup,tensor2pil_upscale,tensor2pil_list,find_gaussian_files,load_images
#from .AniCrafter.run_pipeline_with_preprocess import prepare_models,predata_for_anicrafter,infer_anicrafter
from .AniCrafter.run_pipeline import predata_for_anicrafter_dispre,prepare_models,infer_anicrafter
import folder_paths
from .AniCrafter.diffsynth.prompters import WanPrompter
########
MAX_SEED = np.iinfo(np.int32).max
current_node_path = os.path.dirname(os.path.abspath(__file__))
device = torch.device(
"cuda:0") if torch.cuda.is_available() else torch.device(
"mps") if torch.backends.mps.is_available() else torch.device(
"cpu")
# add checkpoints dir
AniCrafter_weigths_path = os.path.join(folder_paths.models_dir, "AniCrafter")
if not os.path.exists(AniCrafter_weigths_path):
os.makedirs(AniCrafter_weigths_path)
folder_paths.add_model_folder_path("AniCrafter", AniCrafter_weigths_path)
######
class AniCrafterPreImage:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"role_image": ("IMAGE",),
"clip_vision": (["none"] + folder_paths.get_filename_list("clip_vision"),),
"clean_up": ("BOOLEAN", {"default": True},),
"width": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 16, "display": "number"}),
"height": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 16, "display": "number"}),
}}
RETURN_TYPES = ("AniCrafter_DATA",)
RETURN_NAMES = ( "image_data",)
FUNCTION = "sampler_main"
CATEGORY = "AniCrafter"
def sampler_main(self, role_image,clip_vision,clean_up,width,height ):
if clip_vision == "none":
raise ValueError("Please select a CLIP model")
else:
clip_vision_path=folder_paths.get_full_path("clip_vision", clip_vision)
model_manager = ModelManager(device="cpu")
model_manager.load_models([clip_vision_path],torch_dtype=torch.bfloat16,) # Image Encoder is loaded with float32) #
image_encoder = model_manager.fetch_model("wan_video_image_encoder")
character_image=tensor2pil_upscale(role_image, width, height)
image = torch.Tensor(np.array(character_image, dtype=np.float32) * (2 / 255) - 1).permute(2, 0, 1).unsqueeze(0).to(device)
image_encoder.to(device)
clip_context = image_encoder.encode_image([image])
image_encoder.to("cpu")
if clean_up:
model_manager.clear_model_memory()
gc_cleanup()
print(clip_context.shape)#torch.Size([1, 257, 1280]) is_cuda True
#print(clip_context.dtype)#torch.bfloat16
#clip_context=clip_context.to(device)
return ({"clip_context":clip_context,"width":width,"height":height,"character_image":character_image,},)
class AniCrafterPreText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"T5": (["none"] + folder_paths.get_filename_list("clip"),),
"cpu_infer": ("BOOLEAN", {"default": False},),
"clean_up": ("BOOLEAN", {"default": True},),
"prompt":("STRING", {"multiline": True,"default":"human in a scene"}),
"negative_prompt":("STRING", {"multiline": True,"default":"细节模糊不清,字幕,作品,画作,画面,静止,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,杂乱的背景,三条腿,背景人很多,倒着走"} ),
}}
RETURN_TYPES = ("AniCrafter_TEXT",)
RETURN_NAMES = ("text_emb",)
FUNCTION = "sampler_main"
CATEGORY = "AniCrafter"
def sampler_main(self, T5,cpu_infer,clean_up,prompt,negative_prompt,):
if T5 == "none":
raise ValueError("Please select a T5 model")
else:
T5_path=folder_paths.get_full_path("clip", T5)
model_manager = ModelManager(device="cpu")
tokenizer_path_=os.path.join(folder_paths.base_path,"custom_nodes/ComfyUI_AniCrafter/configs/Wan2.1-I2V-14B-720P/google/umt5-xxl")
prompter = WanPrompter(tokenizer_path=tokenizer_path_)
model_manager.load_models([T5_path,],torch_dtype=torch.bfloat16,) # You can set `torch_dtype=torch.float8_e4m3fn` to enable FP8 quantization.)
text_encoder_model_and_path = model_manager.fetch_model("wan_video_text_encoder", require_model_path=True)
#print(text_encoder_model_and_path)
if text_encoder_model_and_path is not None:
text_encoder, tokenizer_path = text_encoder_model_and_path
prompter.fetch_models(text_encoder)
prompter.fetch_tokenizer(tokenizer_path_)
if not cpu_infer:
prompter.to_cuda()
prompt_emb_posi = prompter.encode_prompt(prompt, positive=True,)
prompt_emb_nega = prompter.encode_prompt(negative_prompt, positive=False,)
prompter.offload()
else:
prompt_emb_posi = prompter.encode_prompt(prompt, positive=True,device="cpu")
prompt_emb_nega = prompter.encode_prompt(negative_prompt, positive=False,device="cpu")
prompt_emb_posi = prompt_emb_posi.to(device)
prompt_emb_nega = prompt_emb_nega.to(device)
if clean_up:
model_manager.clear_model_memory()
print(prompt_emb_posi.shape,prompt_emb_nega.shape,prompt_emb_posi.is_cuda,prompt_emb_posi.dtype) #torch.Size([1, 512, 4096]) torch.Size([1, 512, 4096]) # True torch.float32
return ({"prompt_emb_posi": {"context": prompt_emb_posi}, "prompt_emb_nega": {"context": prompt_emb_nega}, },)
class AniCrafterPreVideo:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
gaussian_files_list = ["none"] + find_gaussian_files(folder_paths.get_input_directory()) if find_gaussian_files(folder_paths.get_input_directory()) else [""]
return {
"required": {
"image_data":("AniCrafter_DATA",),
"video_image": ("IMAGE",),
"gaussian_files": (gaussian_files_list,),
"max_frames": ("INT", {"default": 80, "min": 8, "max": 2048, "step": 4, "display": "number"}),
"clean_up": ("BOOLEAN", {"default": True},),
}}
RETURN_TYPES = ("AniCrafter_PREDATA",)
RETURN_NAMES = ("data_dict", )
FUNCTION = "sampler_main"
CATEGORY = "AniCrafter"
def sampler_main(self, image_data,video_image,gaussian_files,max_frames,clean_up ):
width=image_data.get("width")
height=image_data.get("height")
character_image=image_data.get("character_image")
max_frames=max_frames + 1 # must be 1 (mod 4)
image_list=tensor2pil_list(video_image,width,height)
frame_process_norm = v2.Compose([
v2.Resize(size=(height, width), antialias=True),
v2.ToTensor(),
v2.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
ref_combine_blend_tensor,ref_combine_smplx_tensor,height_, width_=predata_for_anicrafter_dispre(frame_process_norm,
image_list,character_image,AniCrafter_weigths_path,gaussian_files,clean_up,smplx_mesh_pils_origin_=None,smplx_path_=None,bkgd_pils_origin_=None,max_frames= max_frames)
image_data["ref_combine_blend_tensor"]=ref_combine_blend_tensor
image_data["ref_combine_smplx_tensor"]=ref_combine_smplx_tensor
image_data["width"]=width_
image_data["height"]=height_
image_data["max_frames"]=max_frames
return (image_data,)
class AniCrafterLoader:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"dit": (["none"] + folder_paths.get_filename_list("diffusion_models"),),
"vae": (["none"] + folder_paths.get_filename_list("vae"),),
"lora_alpha":("FLOAT", {"default": 1.0, "min": 0.1, "max": 1.0, "step": 0.1}),
},
}
RETURN_TYPES = ("MODEL_AniCrafter",)
RETURN_NAMES = ("model",)
FUNCTION = "loader_main"
CATEGORY = "AniCrafter"
def loader_main(self,dit,vae,lora_alpha):
if dit == "none":
raise ValueError("Please select a DIT model")
else:
dit_path=folder_paths.get_full_path("diffusion_models", dit)
if vae == "none":
raise ValueError("Please select a VAE model")
else:
vae_path=folder_paths.get_full_path("vae", vae)
# load model
print("***********Load model ***********")
pipe = prepare_models(dit_path,vae_path, os.path.join(AniCrafter_weigths_path, "pretrained_models/anicrafter"),lora_alpha)
print("***********Load model done ***********")
gc_cleanup()
return (pipe ,)
class AniCrafterSampler:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text_emb": ("AniCrafter_TEXT",),
"data_dict": ("AniCrafter_PREDATA",), # {}
"model": ("MODEL_AniCrafter",),
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED}),
"num_inference_steps": ("INT", {"default": 50, "min": 1, "max": 2048, "step": 1, "display": "number"}),
"cfg_value": ("FLOAT", {"default": 1.5, "min": 0.1, "max": 20.0, "step": 0.1}),
"fps": ("FLOAT", {"default": 24.0, "min": 5.0, "max": 120.0, "step": 1.0}),
"use_teacache": ("BOOLEAN", {"default": True},),
"use_tiled": ("BOOLEAN", {"default": True},),
}}
RETURN_TYPES = ("IMAGE", "FLOAT")
RETURN_NAMES = ("image", "fps")
FUNCTION = "sampler_main"
CATEGORY = "AniCrafter"
def sampler_main(self,text_emb, data_dict, model, seed, num_inference_steps,cfg_value, fps,use_teacache,use_tiled):
print("***********Start infer ***********")
iamges = infer_anicrafter(model, data_dict.get("ref_combine_blend_tensor"),data_dict.get("ref_combine_smplx_tensor"),
data_dict.get("height"),data_dict.get("width"),
num_inference_steps,seed ,use_teacache,cfg_value,use_tiled,text_emb,data_dict, )
gc.collect()
torch.cuda.empty_cache()
return (load_images(iamges), fps)
NODE_CLASS_MAPPINGS = {
"AniCrafterPreImage": AniCrafterPreImage,
"AniCrafterPreText": AniCrafterPreText,
"AniCrafterPreVideo": AniCrafterPreVideo,
"AniCrafterLoader": AniCrafterLoader,
"AniCrafterSampler": AniCrafterSampler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AniCrafterPreImage": "AniCrafterPreImage",
"AniCrafterPreText": "AniCrafterPreText",
"AniCrafterPreVideo": "AniCrafterPreVideo",
"AniCrafterLoader": "AniCrafterLoader",
"AniCrafterSampler": "AniCrafterSampler",
}