# !/usr/bin/env python # -*- coding: UTF-8 -*- import os import torch import gc from PIL import Image import numpy as np import cv2 import comfy.model_management as mm import torch.nn.functional as F from comfy.utils import common_upscale,ProgressBar import folder_paths from diffusers import AutoencoderKLWan from .FlashVSR.vae import WanVAE from .FlashVSR.diffsynth.pipelines.flashvsr_full import TorchColorCorrectorWavelet from .FlashVSR.examples.WanVSR.infer_flashvsr_full import upscale_lq_video_bilinear,dup_first_frame_1cthw_simple from safetensors.torch import load_file cur_path = os.path.dirname(os.path.abspath(__file__)) def load_vae_model(vae,tcd_encoder,lite_vae, device="cuda"): vae_path=folder_paths.get_full_path("vae", vae) if vae != "none" else None tcd_encoder_path=folder_paths.get_full_path("FlashVSR", tcd_encoder) if tcd_encoder != "none" else None lite_vae_path=folder_paths.get_full_path("vae", lite_vae) if lite_vae != "none" else "none" if vae_path is not None: if vae_path.endswith(".safetensors"): config=AutoencoderKLWan.load_config(os.path.join(cur_path,"FlashVSR/config.json")) vae=AutoencoderKLWan.from_config(config).to(device,dtype=torch.bfloat16) vae_dict=load_file(vae_path,device="cpu") vae.load_state_dict(vae_dict,strict=False) del vae_dict vae.vae_mode="diffusers" else: from .FlashVSR.diffsynth import ModelManager,FlashVSRFullPipeline mm = ModelManager(torch_dtype=torch.bfloat16, device="cpu") mm.load_models([vae_path,]) pipe = FlashVSRFullPipeline.from_model_manager(mm, device="cuda") pipe.vae.model.encoder = None pipe.vae.model.conv1 = None pipe.enable_vram_management(num_persistent_param_in_dit=None,vae_only=True) pipe.load_models_to_device(["vae"]) vae=pipe.vae vae.vae_mode="normal" del pipe elif tcd_encoder_path is not None: from .FlashVSR.examples.WanVSR.utils.TCDecoder import build_tcdecoder vae = build_tcdecoder(new_channels=[512, 256, 128, 128], new_latent_channels=16+768) tcd_state=torch.load(tcd_encoder_path,weights_only=False,) if isinstance(tcd_state,dict) and "decoder" in tcd_state: tcd_state=tcd_state["decoder"] # 兼容 FlashVSR+ 的 TCDecoder.ckpt 包装结构 load_report=vae.load_state_dict(tcd_state, strict=False) total=len(vae.state_dict()) miss=len(load_report.missing_keys); unexp=len(load_report.unexpected_keys) print(f"[FlashVSR] TCDecoder load: matched {total-miss}/{total}, missing={miss}, unexpected={unexp}") if miss > total*0.05: print("[FlashVSR][WARN] TCDecoder 权重覆盖率过低,解码质量会严重异常!" "请确认 tcd_encoder 选择的是 FlashVSR+ 发布的 TCDecoder.ckpt") vae.vae_mode="tcd" elif lite_vae_path is not None: if "light" in lite_vae_path.lower() or "tae" in lite_vae_path.lower(): if os.path.basename(lite_vae_path).split(".")[0]=="lightvaew2_1": print("use lightvae decoder") vae = WanVAE(vae_path=lite_vae_path,dtype=torch.bfloat16,device=device,use_lightvae=True) vae.vae_mode="lightvae" elif os.path.basename(lite_vae_path).split(".")[0]=="taew2_1": from .FlashVSR.vae_tiny import WanVAE_tiny print("use vae_tiny decoder") vae = WanVAE_tiny(vae_path=lite_vae_path,dtype=torch.bfloat16,device=device,need_scaled=False) vae.vae_mode="tae" elif os.path.basename(lite_vae_path).split(".")[0]=="lighttaew2_1": from .FlashVSR.vae_tiny import WanVAE_tiny print("use vae_tiny light decoder") vae = WanVAE_tiny(vae_path=lite_vae_path,dtype=torch.bfloat16,device=device,need_scaled=True) vae.vae_mode="lighttae" else: raise ValueError(f"Unknown vae_name: {lite_vae_path},only support lightvae,tae,tae_tiny,lighttae_tiny") else: print("use upscale2x decoder") config=AutoencoderKLWan.load_config(os.path.join(cur_path,"FlashVSR/examples/config.json")) vae=AutoencoderKLWan.from_config(config).to(device,dtype=torch.bfloat16) vae_dict=load_file(lite_vae_path,device="cpu") vae.load_state_dict(vae_dict,strict=False) del vae_dict vae.vae_mode="upscale2x" else: raise ValueError("vae_path,tcd_encoder_path,lite_vae_path is None") if not vae.vae_mode=="lightvae": vae.eval() vae.requires_grad_(False) return vae def decode_latents( vae,latent,color_fix,fix_method,full_tiled, device): vae_mode=getattr(vae,"vae_mode","none") samples=latent["samples"] LQ_cur_idx=latent.get("LQ_cur_idx",0) pad_frames=latent.get("pad_frames",0) LQ=latent.get("LQ",None) if color_fix: ColorCorrector=TorchColorCorrectorWavelet(levels=5) pad_first_frame = True if "wavelet"== fix_method and color_fix else False if isinstance(samples,list) and isinstance(LQ_cur_idx,list) and vae_mode!="tcd": raise ValueError("when use long mode only support tcd_encoder") #print(samples.shape) #torch.Size([1, 16, 22, 160, 96]) if vae_mode=="none": samples=add_mean(samples) images=vae.decode(samples) #print(f"decode complete,images shape: {images.shape}") # torch.Size([1, 85, 1024, 768, 3]) images=images.permute(0,4,1,2,3) # torch.Size([1, 3, 85, 1024, 768]) else: if not vae_mode in ["normal","lightvae"]: vae.to(device) with torch.no_grad(): if isinstance(vae,AutoencoderKLWan): samples=add_mean(samples) if full_tiled: vae.enable_tiling() else: vae.disable_tiling() images=vae.decode(samples,return_dict=False)[0] images = map_neg1_1_to_0_1(images.cpu().float()) if vae_mode=="upscale2x": ch = images.shape[1] if ch == 3: upscale = 1 else: if ch % 3 == 0: upscale = round((ch // 3) ** 0.5) else: upscale = 1 images = F.pixel_shuffle(images.movedim(2, 1), upscale_factor=int(upscale)).movedim(1, 2) # pixel shuffle needs [..., C, H, W] format #torch.Size([1, 3, 77, 2048, 1536]) else: if vae_mode=="tcd": vae.clean_mem() if isinstance(samples,list): print("use tcd decoder to infer long mode") frames_total = [] for i,(cur,pre) in zip(samples,LQ_cur_idx): cur_LQ_frame = LQ[:,:,pre:cur,:,:] cur_frames = vae.decode_video(i.transpose(1, 2),parallel=False, show_progress_bar=False, cond=LQ[:,:,pre:cur,:,:].to(device)).transpose(1, 2).mul_(2).sub_(1) cur_frames = map_neg1_1_to_0_1(cur_frames.cpu().float()) #[21,8,8,8,8,8] #torch.Size([1, 3, 21, 1280, 768]) cur_LQ_frame = map_neg1_1_to_0_1(cur_LQ_frame.cpu().float()).to(device) if color_fix: try: if pad_first_frame: # 加帧 cur_frames = dup_first_frame_1cthw_simple(cur_frames) cur_LQ_frame=dup_first_frame_1cthw_simple(cur_LQ_frame) cur_frames = ColorCorrector( cur_frames.to(device=device), cur_LQ_frame, clip_range=(-1, 1), chunk_size=None, method=fix_method, ) if pad_first_frame: #减帧 cur_frames = cur_frames[:, :, 1:, :, :] # remove first frame except: print("color correction failed, return uncorrected images") frames_total.append(cur_frames.to('cpu')) images = torch.cat(frames_total, dim=2) if pad_frames>0: print(f"Remove{images.shape[2]} 's pad frames {pad_frames} --> original frames: {images.shape[2]-pad_frames}") images = images[:, :, :-pad_frames, :, :] #torch.Size([1, 3, 85, 1024, 768]) -> torch.Size([1, 3, 81, 1024, 768]) if 81 output -4 input + 8 return images.squeeze(0).permute(1,2,3,0).cpu() else: images = vae.decode_video(samples.transpose(1, 2),parallel=False, show_progress_bar=False, cond=LQ[:,:,:LQ_cur_idx,:,:]).transpose(1, 2).mul_(2).sub_(1) elif vae_mode=="normal": vae.clear_cache() images = vae.decode(samples, device=device, tiled=full_tiled, tile_size=(60, 104), tile_stride=(30, 52)) else: if isinstance(vae,WanVAE): if full_tiled: vae.use_tiling=True else: vae.use_tiling=False images=vae.decode(samples.squeeze(0)) images = map_neg1_1_to_0_1(images.cpu().float()) if not vae_mode=="lightvae": vae.to("cpu") torch.cuda.empty_cache() #print(f"decode complete,images shape: {images.shape}") if color_fix: LQ = map_neg1_1_to_0_1(LQ.cpu().float()) try: if pad_first_frame: images = dup_first_frame_1cthw_simple(images) LQ=dup_first_frame_1cthw_simple(LQ) if vae_mode=="upscale2x" and LQ.shape[-1]!=images.shape[-1]: scale_=int(images.shape[-1]/LQ.shape[-1]) LQ=upscale_lq_video_bilinear(LQ,scale_) images = ColorCorrector( images.to(device=device), LQ[:, :, :images.shape[2], :, :], clip_range=(-1, 1), chunk_size=16, method=fix_method ) if pad_first_frame: images = images[:, :, 1:, :, :] # remove first frame torch.Size([1, 3, 78, 1024, 768]) --> torch.Size([1, 3, 77, 1024, 768]) except: print("color correction failed, return uncorrected images") if pad_frames>0: print(f"Remove{images.shape[2]} 's pad frames {pad_frames} --> original frames: {images.shape[2]-pad_frames}") images = images[:, :, :-pad_frames, :, :] #torch.Size([1, 3, 85, 1024, 768]) -> torch.Size([1, 3, 81, 1024, 768]) if 81 output -4 input + 8 return images.squeeze(0).permute(1,2,3,0).cpu() def clear_comfyui_cache(): cf_models=mm.loaded_models() try: for pipe in cf_models: pipe.unpatch_model(device_to=torch.device("cpu")) print(f"Unpatching models.{pipe}") except: pass mm.soft_empty_cache() torch.cuda.empty_cache() max_gpu_memory = torch.cuda.max_memory_allocated() print(f"After Max GPU memory allocated: {max_gpu_memory / 1000 ** 3:.2f} GB") def add_mean(latents): vae_config={"latents_mean": [ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508, 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921 ], "latents_std": [ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743, 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.916 ],} latents_mean = (torch.tensor(vae_config["latents_mean"]).view(1, 16, 1, 1, 1).to(latents.device, latents.dtype)) latents_std = 1.0 / torch.tensor(vae_config["latents_std"]).view(1, 16, 1, 1, 1).to(latents.device, latents.dtype) latents = latents / latents_std + latents_mean return latents def gc_cleanup(): gc.collect() torch.cuda.empty_cache() def tensor2cv(tensor_image): if len(tensor_image.shape)==4:# b hwc to hwc tensor_image=tensor_image.squeeze(0) if tensor_image.is_cuda: tensor_image = tensor_image.cpu() tensor_image=tensor_image.numpy() #反归一化 maxValue=tensor_image.max() tensor_image=tensor_image*255/maxValue img_cv2=np.uint8(tensor_image)#32 to uint8 img_cv2=cv2.cvtColor(img_cv2,cv2.COLOR_RGB2BGR) return img_cv2 def phi2narry(img): img = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) return img def tensor2image(tensor): tensor = tensor.cpu() image_np = tensor.squeeze().mul(255).clamp(0, 255).byte().numpy() image = Image.fromarray(image_np, mode='RGB') return image def tensor2pillist(tensor_in): d1, _, _, _ = tensor_in.size() if d1 == 1: img_list = [tensor2image(tensor_in)] else: tensor_list = torch.chunk(tensor_in, chunks=d1) img_list=[tensor2image(i) for i in tensor_list] return img_list def tensor2pillist_upscale(tensor_in,width,height): d1, _, _, _ = tensor_in.size() if d1 == 1: img_list = [nomarl_upscale(tensor_in,width,height)] else: tensor_list = torch.chunk(tensor_in, chunks=d1) img_list=[nomarl_upscale(i,width,height) for i in tensor_list] return img_list def tensor2list(tensor_in,width,height): if tensor_in is None: return None d1, _, _, _ = tensor_in.size() if d1 == 1: tensor_list = [tensor_upscale(tensor_in,width,height)] else: tensor_list_ = torch.chunk(tensor_in, chunks=d1) tensor_list=[tensor_upscale(i,width,height) for i in tensor_list_] return tensor_list def tensor_upscale(tensor, width, height): samples = tensor.movedim(-1, 1) samples = common_upscale(samples, width, height, "nearest-exact", "center") samples = samples.movedim(1, -1) return samples def nomarl_upscale(img, width, height): samples = img.movedim(-1, 1) img = common_upscale(samples, width, height, "nearest-exact", "center") samples = img.movedim(1, -1) img = tensor2image(samples) return img def cv2tensor(img,bgr2rgb=True): assert type(img) == np.ndarray, 'the img type is {}, but ndarry expected'.format(type(img)) if bgr2rgb: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = torch.from_numpy(img.transpose((2, 0, 1))) return img.float().div(255).permute(1, 2, 0).unsqueeze(0) def images_generator(img_list: list, ): # get img size sizes = {} for image_ in img_list: if isinstance(image_, Image.Image): count = sizes.get(image_.size, 0) sizes[image_.size] = count + 1 elif isinstance(image_, np.ndarray): count = sizes.get(image_.shape[:2][::-1], 0) sizes[image_.shape[:2][::-1]] = count + 1 else: raise "unsupport image list,must be pil or cv2!!!" size = max(sizes.items(), key=lambda x: x[1])[0] yield size[0], size[1] # any to tensor def load_image(img_in): if isinstance(img_in, Image.Image): img_in = img_in.convert("RGB") i = np.array(img_in, dtype=np.float32) i = torch.from_numpy(i).div_(255) if i.shape[0] != size[1] or i.shape[1] != size[0]: i = torch.from_numpy(i).movedim(-1, 0).unsqueeze(0) i = common_upscale(i, size[0], size[1], "lanczos", "center") i = i.squeeze(0).movedim(0, -1).numpy() return i elif isinstance(img_in, np.ndarray): i = cv2.cvtColor(img_in, cv2.COLOR_BGR2RGB).astype(np.float32) i = torch.from_numpy(i).div_(255) print(i.shape) return i else: raise "unsupport image list,must be pil,cv2 or tensor!!!" total_images = len(img_list) processed_images = 0 pbar = ProgressBar(total_images) images = map(load_image, img_list) try: prev_image = next(images) while True: next_image = next(images) yield prev_image processed_images += 1 pbar.update_absolute(processed_images, total_images) prev_image = next_image except StopIteration: pass if prev_image is not None: yield prev_image def map_neg1_1_to_0_1(t): """ 接受 torch.Tensor 或可转 torch.Tensor 的输入。 可处理形状: H,W,C 或 B,H,W,C 或 B,T,H,W,C(会按元素处理)。 返回 float tensor,范围 0..1。 """ if not torch.is_tensor(t): t = torch.tensor(t) t = t.float() # map -1..1 -> 0..1 t = (t + 1.0) * 0.5 # 限幅到 [0,1] t = t.clamp(0.0, 1.0) # 保持在 cpu 端,调用方可决定是否转 device/dtype return t.cpu() def load_images_list(img_list: list, ): gen = images_generator(img_list) (width, height) = next(gen) images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (height, width, 3))))) if len(images) == 0: raise FileNotFoundError(f"No images could be loaded .") return images def get_video_files(directory, extensions=None): if extensions is None: extensions = ['webm', 'mp4', 'mkv', 'gif', 'mov'] extensions = [ext.lower() for ext in extensions] video_files = [] for root, dirs, files in os.walk(directory): for file in files: _, ext = os.path.splitext(file) ext = ext.lower()[1:] if ext in extensions: full_path = os.path.join(root, file) video_files.append(full_path) return video_files