import argparse import os import cv2 import numpy as np from PIL import Image from diffusers.models.attention_processor import XFormersAttnProcessor from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection import torch from diffusers import AutoencoderKLTemporalDecoder, EulerDiscreteScheduler from animation.modules.attention_processor import AnimationAttnProcessor from animation.modules.attention_processor_normalized import AnimationIDAttnNormalizedProcessor from animation.modules.face_model import FaceModel from animation.modules.id_encoder import FusionFaceId from animation.modules.pose_net import PoseNet from animation.modules.unet import UNetSpatioTemporalConditionModel from animation.pipelines.inference_pipeline_animation import InferenceAnimationPipeline import random import subprocess from pathlib import Path def seed_everything(seed): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed % (2**32)) random.seed(seed) def load_images_from_folder(folder, width, height): images = [] files = os.listdir(folder) png_files = [f for f in files if f.endswith('.png')] png_files.sort(key=lambda x: int(x.split('_')[1].split('.')[0])) for filename in png_files: img = Image.open(os.path.join(folder, filename)).convert('RGB') img = img.resize((width, height)) images.append(img) return images def save_frames_as_png(frames, output_path): pil_frames = [Image.fromarray(frame) if isinstance(frame, np.ndarray) else frame for frame in frames] num_frames = len(pil_frames) for i in range(num_frames): pil_frame = pil_frames[i] save_path = os.path.join(output_path, f'frame_{i}.png') pil_frame.save(save_path) def save_frames_as_mp4(frames, output_mp4_path, fps): print("Starting saving the frames as mp4") height, width, _ = frames[0].shape fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 'H264' for better quality out = cv2.VideoWriter(output_mp4_path, fourcc, fps, (width, height)) for frame in frames: frame_bgr = frame if frame.shape[2] == 3 else cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) out.write(frame_bgr) out.release() def export_to_gif(frames, output_gif_path, fps): """ Export a list of frames to a GIF. Args: - frames (list): List of frames (as numpy arrays or PIL Image objects). - output_gif_path (str): Path to save the output GIF. - duration_ms (int): Duration of each frame in milliseconds. """ # Convert numpy arrays to PIL Images if needed pil_frames = [Image.fromarray(frame) if isinstance( frame, np.ndarray) else frame for frame in frames] pil_frames[0].save(output_gif_path.replace('.mp4', '.gif'), format='GIF', append_images=pil_frames[1:], save_all=True, duration=125, loop=0) def parse_args(): parser = argparse.ArgumentParser( description="Inference script" ) # KEEP parser.add_argument( "--pretrained_model_name_or_path", type=str, default=None, required=True ) ## KEEP #parser.add_argument( # "--validation_image", # type=str, # default=None, # help=( # "A set of paths to the controlnext conditioning image be evaluated every `--validation_steps`" # " and logged to `--report_to`. Provide either a matching number of `--validation_prompt`s, a" # " a single `--validation_prompt` to be used with all `--validation_image`s, or a single" # " `--validation_image` that will be used with all `--validation_prompt`s." # ), #) parser.add_argument( "--start_image_path", type=str, default=None, help=( "A set of paths to the controlnext conditioning image be evaluated every `--validation_steps`" " and logged to `--report_to`. Provide either a matching number of `--validation_prompt`s, a" " a single `--validation_prompt` to be used with all `--validation_image`s, or a single" " `--validation_image` that will be used with all `--validation_prompt`s." ), ) # =================== # Pose Input # Supply either `pose_images_dir`, `pose_video_path`, or `pre_pose_video_path`. # - pose_images_dir has inputs already fully processed # - pose_video_path just needs to be converted to frames # - pre_pose_video_path has not been converted to pose data frames # =================== parser.add_argument( "--pose_images_dir", type=str, default=None, help=( "the validation control images" ), ) parser.add_argument( "--pose_video_path", type=str, default=None, help=( "the validation control video (optional)" ), ) parser.add_argument( "--pre_pose_video_path", type=str, default=None, help=( "video to convert to pose" ), ) parser.add_argument( "--frame_output_dir", type=str, default=None, required=True ) parser.add_argument( "--video_output_path", type=str, default=None, required=True ) # TODO FIX parser.add_argument( "--height", type=int, default=768, required=False ) # TODO FIX parser.add_argument( "--width", type=int, default=512, required=False ) # KEEP parser.add_argument( "--guidance_scale", type=float, default=2.0, required=False ) # KEEP parser.add_argument( "--num_inference_steps", type=int, default=25, required=False ) # KEEP parser.add_argument( "--posenet_model_name_or_path", type=str, default=None, help="Path to pretrained posenet model", ) # KEEP parser.add_argument( "--face_encoder_model_name_or_path", type=str, default=None, help="Path to pretrained face encoder model", ) # KEEP parser.add_argument( "--unet_model_name_or_path", type=str, default=None, help="Path to pretrained unet model", ) # KEEP parser.add_argument( "--tile_size", type=int, default=16, required=False ) # KEEP parser.add_argument( "--overlap", type=int, default=4, required=False ) # KEEP parser.add_argument( "--noise_aug_strength", type=float, default=0.0, # or set to 0.02 required=False ) # KEEP parser.add_argument( "--frames_overlap", type=int, default=4, required=False ) # KEEP parser.add_argument( "--gradient_checkpointing", action="store_true", help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", ) # KEEP parser.add_argument( "--revision", type=str, default=None, required=False, help="Revision of pretrained model identifier from huggingface.co/models.", ) # KEEP parser.add_argument( "--decode_chunk_size", type=int, default=None, required=False ) args = parser.parse_args() return args def split_video_to_frames(video_path, output_path): Path(output_path).mkdir(parents=True, exist_ok=True) filename_format = f"{output_path}/frame_%d.png" command = [ 'ffmpeg', '-i', video_path, '-q:v', '1', '-start_number', '0', filename_format, ] print(f"Command: {command}", flush=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) #video_info = json.loads(result.stdout) def merge_frames_to_mp4(frames_dir, output_path, framerate=30): filename_format = f"{frames_dir}/frame_%d.png" command = [ 'ffmpeg', '-start_number', '0', '-framerate', str(framerate), '-i', filename_format, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', output_path, '-y', ] print(f"Command: {command}", flush=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) #video_info = json.loads(result.stdout) def prepare_pose_frames(args): pose_images_dir = args.pose_images_dir if not pose_images_dir: raise Exception('pose_images_dir must be set, even when using other arguments') Path(pose_images_dir).mkdir(parents=True, exist_ok=True) if args.pre_pose_video_path: print("Preparing pose frames from pre-pose video.") pre_pose_frame_dir = Path(f"{pose_images_dir}/frames") pre_pose_frame_dir.mkdir(parents=True, exist_ok=True) split_video_to_frames(args.pre_pose_video_path, pre_pose_frame_dir) reference_image_path = pre_pose_frame_dir / "frame_0.png" command = [ "python", "DWPose/skeleton_extraction.py", "--target_image_folder_path", pre_pose_frame_dir, "--ref_image_path", reference_image_path, "--poses_folder_path", pose_images_dir, ] print(f"Command: {command}", flush=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) elif args.pose_video_path: print("Preparing pose frames from pose video.") split_video_to_frames(args.pose_video_path, pose_images_dir) else: print("Pose frames assumed to already exist.") # TODO: autodetect resolution pose_images = load_images_from_folder(pose_images_dir, width=args.width, height=args.height) return pose_images if __name__ == "__main__": args = parse_args() # torch.set_default_dtype(torch.float16) seed = 23123134 # seed = 42 # seed = 123 seed_everything(seed) generator = torch.Generator(device='cuda').manual_seed(seed) feature_extractor = CLIPImageProcessor.from_pretrained(args.pretrained_model_name_or_path, subfolder="feature_extractor", revision=args.revision) noise_scheduler = EulerDiscreteScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") image_encoder = CLIPVisionModelWithProjection.from_pretrained( args.pretrained_model_name_or_path, subfolder="image_encoder", revision=args.revision ) vae = AutoencoderKLTemporalDecoder.from_pretrained( args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) unet = UNetSpatioTemporalConditionModel.from_pretrained( args.pretrained_model_name_or_path, subfolder="unet", low_cpu_mem_usage=True, ) pose_net = PoseNet(noise_latent_channels=unet.config.block_out_channels[0]) face_encoder = FusionFaceId( cross_attention_dim=1024, id_embeddings_dim=512, # clip_embeddings_dim=image_encoder.config.hidden_size, clip_embeddings_dim=1024, num_tokens=4, ) face_model = FaceModel() lora_rank = 128 attn_procs = {} unet_svd = unet.state_dict() for name in unet.attn_processors.keys(): if "transformer_blocks" in name and "temporal_transformer_blocks" not in name: cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim if name.startswith("mid_block"): hidden_size = unet.config.block_out_channels[-1] elif name.startswith("up_blocks"): block_id = int(name[len("up_blocks.")]) hidden_size = list(reversed(unet.config.block_out_channels))[block_id] elif name.startswith("down_blocks"): block_id = int(name[len("down_blocks.")]) hidden_size = unet.config.block_out_channels[block_id] if cross_attention_dim is None: # print(f"This is AnimationAttnProcessor: {name}") attn_procs[name] = AnimationAttnProcessor(hidden_size=hidden_size, cross_attention_dim=cross_attention_dim, rank=lora_rank) else: # print(f"This is AnimationIDAttnProcessor: {name}") layer_name = name.split(".processor")[0] weights = { "to_k_ip.weight": unet_svd[layer_name + ".to_k.weight"], "to_v_ip.weight": unet_svd[layer_name + ".to_v.weight"], } attn_procs[name] = AnimationIDAttnNormalizedProcessor(hidden_size=hidden_size, cross_attention_dim=cross_attention_dim, rank=lora_rank) attn_procs[name].load_state_dict(weights, strict=False) elif "temporal_transformer_blocks" in name: cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim if name.startswith("mid_block"): hidden_size = unet.config.block_out_channels[-1] elif name.startswith("up_blocks"): block_id = int(name[len("up_blocks.")]) hidden_size = list(reversed(unet.config.block_out_channels))[block_id] elif name.startswith("down_blocks"): block_id = int(name[len("down_blocks.")]) hidden_size = unet.config.block_out_channels[block_id] if cross_attention_dim is None: attn_procs[name] = XFormersAttnProcessor() else: attn_procs[name] = XFormersAttnProcessor() unet.set_attn_processor(attn_procs) # resume the previous checkpoint if args.posenet_model_name_or_path is not None and args.face_encoder_model_name_or_path is not None and args.unet_model_name_or_path is not None: print("Loading existing posenet weights, face_encoder weights and unet weights.") if args.posenet_model_name_or_path.endswith(".pth"): pose_net_state_dict = torch.load(args.posenet_model_name_or_path, map_location="cpu") pose_net.load_state_dict(pose_net_state_dict, strict=True) else: print("posenet weights loading fail") print(1/0) if args.face_encoder_model_name_or_path.endswith(".pth"): face_encoder_state_dict = torch.load(args.face_encoder_model_name_or_path, map_location="cpu") face_encoder.load_state_dict(face_encoder_state_dict, strict=True) else: print("face_encoder weights loading fail") print(1/0) if args.unet_model_name_or_path.endswith(".pth"): unet_state_dict = torch.load(args.unet_model_name_or_path, map_location="cpu") unet.load_state_dict(unet_state_dict, strict=True) else: print("unet weights loading fail") print(1/0) torch.cuda.empty_cache() vae.requires_grad_(False) image_encoder.requires_grad_(False) unet.requires_grad_(False) pose_net.requires_grad_(False) face_encoder.requires_grad_(False) if args.gradient_checkpointing: unet.enable_gradient_checkpointing() weight_dtype = torch.float16 # weight_dtype = torch.float32 # weight_dtype = torch.bfloat16 pipeline = InferenceAnimationPipeline( vae=vae, image_encoder=image_encoder, unet=unet, scheduler=noise_scheduler, feature_extractor=feature_extractor, pose_net=pose_net, face_encoder=face_encoder, ).to(device='cuda', dtype=weight_dtype) os.makedirs(args.frame_output_dir, exist_ok=True) pose_images = prepare_pose_frames(args) num_frames = len(pose_images) start_image_path = args.start_image_path start_image = Image.open(args.start_image_path).convert('RGB') face_model.face_helper.clean_all() start_image_face = cv2.imread(start_image_path) start_image_bgr = cv2.cvtColor(start_image_face, cv2.COLOR_RGB2BGR) start_image_face_info = face_model.app.get(start_image_bgr) if len(start_image_face_info) > 0: start_image_face_info = sorted(start_image_face_info, key=lambda x: (x['bbox'][2] - x['bbox'][0]) * (x['bbox'][3] - x['bbox'][1]))[-1] start_image_id_ante_embedding = start_image_face_info['embedding'] else: start_image_id_ante_embedding = None if start_image_id_ante_embedding is None: face_model.face_helper.read_image(start_image_bgr) face_model.face_helper.get_face_landmarks_5(only_center_face=True) face_model.face_helper.align_warp_face() if len(face_model.face_helper.cropped_faces) == 0: start_image_id_ante_embedding = np.zeros((512,)) else: start_image_align_face = face_model.face_helper.cropped_faces[0] print('fail to detect face using insightface, extract embedding on align face') start_image_id_ante_embedding = face_model.handler_ante.get_feat(start_image_align_face) # generator = torch.Generator(device=accelerator.device).manual_seed(23123134) decode_chunk_size = args.decode_chunk_size video_frames = pipeline( image=start_image, image_pose=pose_images, height=args.height, width=args.width, num_frames=num_frames, tile_size=args.tile_size, tile_overlap=args.frames_overlap, decode_chunk_size=decode_chunk_size, motion_bucket_id=127., fps=7, min_guidance_scale=args.guidance_scale, max_guidance_scale=args.guidance_scale, noise_aug_strength=args.noise_aug_strength, num_inference_steps=args.num_inference_steps, generator=generator, output_type="pil", validation_image_id_ante_embedding=start_image_id_ante_embedding, ).frames[0] for i in range(num_frames): img = video_frames[i] video_frames[i] = np.array(img) os.makedirs(args.frame_output_dir, exist_ok=True) save_frames_as_png(video_frames, args.frame_output_dir) merge_frames_to_mp4(args.frame_output_dir, args.video_output_path) # bash command_basic_infer.sh