Files
smthemex-ComfyUI_FlashVSR/FlashVSR/utils.py
T
2025-10-31 16:55:24 +08:00

486 lines
16 KiB
Python

import os
import random
import subprocess
from typing import Optional
import imageio
import imageio_ffmpeg as ffmpeg
import numpy as np
import safetensors
import torch
import torch.distributed as dist
import torchvision
from einops import rearrange
from loguru import logger
def seed_all(seed):
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
def save_videos_grid(videos: torch.Tensor, path: str, rescale=False, n_rows=1, fps=24):
"""save videos by video tensor
copy from https://github.com/guoyww/AnimateDiff/blob/e92bd5671ba62c0d774a32951453e328018b7c5b/animatediff/utils/util.py#L61
Args:
videos (torch.Tensor): video tensor predicted by the model
path (str): path to save video
rescale (bool, optional): rescale the video tensor from [-1, 1] to . Defaults to False.
n_rows (int, optional): Defaults to 1.
fps (int, optional): video save fps. Defaults to 8.
"""
videos = rearrange(videos, "b c t h w -> t b c h w")
outputs = []
for x in videos:
x = torchvision.utils.make_grid(x, nrow=n_rows)
x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
if rescale:
x = (x + 1.0) / 2.0 # -1,1 -> 0,1
x = torch.clamp(x, 0, 1)
x = (x * 255).numpy().astype(np.uint8)
outputs.append(x)
os.makedirs(os.path.dirname(path), exist_ok=True)
imageio.mimsave(path, outputs, fps=fps)
def cache_video(
tensor,
save_file: str,
fps=30,
suffix=".mp4",
nrow=8,
normalize=True,
value_range=(-1, 1),
retry=5,
):
save_dir = os.path.dirname(save_file)
try:
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
except Exception as e:
logger.error(f"Failed to create directory: {save_dir}, error: {e}")
return None
cache_file = save_file
# save to cache
error = None
for _ in range(retry):
try:
# preprocess
tensor = tensor.clamp(min(value_range), max(value_range)) # type: ignore
tensor = torch.stack(
[torchvision.utils.make_grid(u, nrow=nrow, normalize=normalize, value_range=value_range) for u in tensor.unbind(2)],
dim=1,
).permute(1, 2, 3, 0)
tensor = (tensor * 255).type(torch.uint8).cpu()
# write video
writer = imageio.get_writer(cache_file, fps=fps, codec="libx264", quality=8)
for frame in tensor.numpy():
writer.append_data(frame)
writer.close()
del tensor
torch.cuda.empty_cache()
return cache_file
except Exception as e:
error = e
continue
else:
logger.info(f"cache_video failed, error: {error}", flush=True)
return None
def vae_to_comfyui_image(vae_output: torch.Tensor) -> torch.Tensor:
"""
Convert VAE decoder output to ComfyUI Image format
Args:
vae_output: VAE decoder output tensor, typically in range [-1, 1]
Shape: [B, C, T, H, W] or [B, C, H, W]
Returns:
ComfyUI Image tensor in range [0, 1]
Shape: [B, H, W, C] for single frame or [B*T, H, W, C] for video
"""
# Handle video tensor (5D) vs image tensor (4D)
if vae_output.dim() == 5:
# Video tensor: [B, C, T, H, W]
B, C, T, H, W = vae_output.shape
# Reshape to [B*T, C, H, W] for processing
vae_output = vae_output.permute(0, 2, 1, 3, 4).reshape(B * T, C, H, W)
# Normalize from [-1, 1] to [0, 1]
images = (vae_output + 1) / 2
# Clamp values to [0, 1]
images = torch.clamp(images, 0, 1)
# Convert from [B, C, H, W] to [B, H, W, C]
images = images.permute(0, 2, 3, 1).cpu()
return images
def vae_to_comfyui_image_inplace(vae_output: torch.Tensor) -> torch.Tensor:
"""
Convert VAE decoder output to ComfyUI Image format (inplace operation)
Args:
vae_output: VAE decoder output tensor, typically in range [-1, 1]
Shape: [B, C, T, H, W] or [B, C, H, W]
WARNING: This tensor will be modified in-place!
Returns:
ComfyUI Image tensor in range [0, 1]
Shape: [B, H, W, C] for single frame or [B*T, H, W, C] for video
Note: The returned tensor is the same object as input (modified in-place)
"""
# Handle video tensor (5D) vs image tensor (4D)
if vae_output.dim() == 5:
# Video tensor: [B, C, T, H, W]
B, C, T, H, W = vae_output.shape
# Reshape to [B*T, C, H, W] for processing (inplace view)
vae_output = vae_output.permute(0, 2, 1, 3, 4).contiguous().view(B * T, C, H, W)
# Normalize from [-1, 1] to [0, 1] (inplace)
vae_output.add_(1).div_(2)
# Clamp values to [0, 1] (inplace)
vae_output.clamp_(0, 1)
# Convert from [B, C, H, W] to [B, H, W, C] and move to CPU
vae_output = vae_output.permute(0, 2, 3, 1).cpu()
return vae_output
def save_to_video(
images: torch.Tensor,
output_path: str,
fps: float = 24.0,
method: str = "imageio",
lossless: bool = False,
output_pix_fmt: Optional[str] = "yuv420p",
) -> None:
"""
Save ComfyUI Image tensor to video file
Args:
images: ComfyUI Image tensor [N, H, W, C] in range [0, 1]
output_path: Path to save the video
fps: Frames per second
method: Save method - "imageio" or "ffmpeg"
lossless: Whether to use lossless encoding (ffmpeg method only)
output_pix_fmt: Pixel format for output (ffmpeg method only)
"""
assert images.dim() == 4 and images.shape[-1] == 3, "Input must be [N, H, W, C] with C=3"
# Ensure output directory exists
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
if method == "imageio":
# Convert to uint8
# frames = (images * 255).cpu().numpy().astype(np.uint8)
frames = (images * 255).to(torch.uint8).cpu().numpy()
imageio.mimsave(output_path, frames, fps=fps) # type: ignore
elif method == "ffmpeg":
# Convert to numpy and scale to [0, 255]
# frames = (images * 255).cpu().numpy().clip(0, 255).astype(np.uint8)
frames = (images * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
# Convert RGB to BGR for OpenCV/FFmpeg
frames = frames[..., ::-1].copy()
N, height, width, _ = frames.shape
# Ensure even dimensions for x264
width += width % 2
height += height % 2
# Get ffmpeg executable from imageio_ffmpeg
ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
if lossless:
command = [
ffmpeg_exe,
"-y", # Overwrite output file if it exists
"-f",
"rawvideo",
"-s",
f"{int(width)}x{int(height)}",
"-pix_fmt",
"bgr24",
"-r",
f"{fps}",
"-loglevel",
"error",
"-threads",
"4",
"-i",
"-", # Input from pipe
"-vcodec",
"libx264rgb",
"-crf",
"0",
"-an", # No audio
output_path,
]
else:
command = [
ffmpeg_exe,
"-y", # Overwrite output file if it exists
"-f",
"rawvideo",
"-s",
f"{int(width)}x{int(height)}",
"-pix_fmt",
"bgr24",
"-r",
f"{fps}",
"-loglevel",
"error",
"-threads",
"4",
"-i",
"-", # Input from pipe
"-vcodec",
"libx264",
"-pix_fmt",
output_pix_fmt,
"-an", # No audio
output_path,
]
# Run FFmpeg
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if process.stdin is None:
raise BrokenPipeError("No stdin buffer received.")
# Write frames to FFmpeg
for frame in frames:
# Pad frame if needed
if frame.shape[0] < height or frame.shape[1] < width:
padded = np.zeros((height, width, 3), dtype=np.uint8)
padded[: frame.shape[0], : frame.shape[1]] = frame
frame = padded
process.stdin.write(frame.tobytes())
process.stdin.close()
process.wait()
if process.returncode != 0:
error_output = process.stderr.read().decode() if process.stderr else "Unknown error"
raise RuntimeError(f"FFmpeg failed with error: {error_output}")
else:
raise ValueError(f"Unknown save method: {method}")
def remove_substrings_from_keys(original_dict, substr):
new_dict = {}
for key, value in original_dict.items():
new_dict[key.replace(substr, "")] = value
return new_dict
def find_torch_model_path(config, ckpt_config_key=None, filename=None, subdir=["original", "fp8", "int8", "distill_models", "distill_fp8", "distill_int8"]):
if ckpt_config_key and config.get(ckpt_config_key, None) is not None:
return config.get(ckpt_config_key)
paths_to_check = [
os.path.join(config["model_path"], filename),
]
if isinstance(subdir, list):
for sub in subdir:
paths_to_check.insert(0, os.path.join(config["model_path"], sub, filename))
else:
paths_to_check.insert(0, os.path.join(config["model_path"], subdir, filename))
for path in paths_to_check:
if os.path.exists(path):
return path
raise FileNotFoundError(f"PyTorch model file '{filename}' not found.\nPlease download the model from https://huggingface.co/lightx2v/ or specify the model path in the configuration file.")
def load_safetensors(in_path, remove_key=None, include_keys=None):
"""加载safetensors文件或目录,支持按key包含筛选或排除"""
include_keys = include_keys or []
if os.path.isdir(in_path):
return load_safetensors_from_dir(in_path, remove_key, include_keys)
elif os.path.isfile(in_path):
return load_safetensors_from_path(in_path, remove_key, include_keys)
else:
raise ValueError(f"{in_path} does not exist")
def load_safetensors_from_path(in_path, remove_key=None, include_keys=None):
"""从单个safetensors文件加载权重,支持按key筛选"""
include_keys = include_keys or []
tensors = {}
with safetensors.safe_open(in_path, framework="pt", device="cpu") as f:
for key in f.keys():
# 优先处理include_keys:如果非空,只保留包含任意指定key的条目
if include_keys:
if any(inc_key in key for inc_key in include_keys):
tensors[key] = f.get_tensor(key)
# 否则使用remove_key排除
else:
if not (remove_key and remove_key in key):
tensors[key] = f.get_tensor(key)
return tensors
def load_safetensors_from_dir(in_dir, remove_key=None, include_keys=None):
"""从目录加载所有safetensors文件,支持按key筛选"""
include_keys = include_keys or []
tensors = {}
safetensors_files = os.listdir(in_dir)
safetensors_files = [f for f in safetensors_files if f.endswith(".safetensors")]
for f in safetensors_files:
tensors.update(load_safetensors_from_path(os.path.join(in_dir, f), remove_key, include_keys))
return tensors
def load_pt_safetensors(in_path, remove_key=None, include_keys=None):
"""加载pt/pth或safetensors权重,支持按key筛选"""
include_keys = include_keys or []
ext = os.path.splitext(in_path)[-1]
if ext in (".pt", ".pth", ".tar"):
state_dict = torch.load(in_path, map_location="cpu", weights_only=True)
# 处理筛选逻辑
keys_to_keep = []
for key in state_dict.keys():
if include_keys:
if any(inc_key in key for inc_key in include_keys):
keys_to_keep.append(key)
else:
if not (remove_key and remove_key in key):
keys_to_keep.append(key)
# 只保留符合条件的key
state_dict = {k: state_dict[k] for k in keys_to_keep}
else:
state_dict = load_safetensors(in_path, remove_key, include_keys)
return state_dict
def load_weights(checkpoint_path, cpu_offload=False, remove_key=None, load_from_rank0=False, include_keys=None):
if not dist.is_initialized() or not load_from_rank0:
# Single GPU mode
logger.info(f"Loading weights from {checkpoint_path}")
cpu_weight_dict = load_pt_safetensors(checkpoint_path, remove_key, include_keys)
return cpu_weight_dict
# Multi-GPU mode
is_weight_loader = False
current_rank = dist.get_rank()
if current_rank == 0:
is_weight_loader = True
cpu_weight_dict = {}
if is_weight_loader:
logger.info(f"Loading weights from {checkpoint_path}")
cpu_weight_dict = load_pt_safetensors(checkpoint_path, remove_key)
meta_dict = {}
if is_weight_loader:
for key, tensor in cpu_weight_dict.items():
meta_dict[key] = {"shape": tensor.shape, "dtype": tensor.dtype}
obj_list = [meta_dict] if is_weight_loader else [None]
src_global_rank = 0
dist.broadcast_object_list(obj_list, src=src_global_rank)
synced_meta_dict = obj_list[0]
if cpu_offload:
target_device = "cpu"
distributed_weight_dict = {key: torch.empty(meta["shape"], dtype=meta["dtype"], device=target_device) for key, meta in synced_meta_dict.items()}
dist.barrier()
else:
target_device = torch.device(f"cuda:{current_rank}")
distributed_weight_dict = {key: torch.empty(meta["shape"], dtype=meta["dtype"], device=target_device) for key, meta in synced_meta_dict.items()}
dist.barrier(device_ids=[torch.cuda.current_device()])
for key in sorted(synced_meta_dict.keys()):
tensor_to_broadcast = distributed_weight_dict[key]
if is_weight_loader:
tensor_to_broadcast.copy_(cpu_weight_dict[key], non_blocking=True)
if cpu_offload:
if is_weight_loader:
gpu_tensor = tensor_to_broadcast.cuda()
dist.broadcast(gpu_tensor, src=src_global_rank)
tensor_to_broadcast.copy_(gpu_tensor.cpu(), non_blocking=True)
del gpu_tensor
torch.cuda.empty_cache()
else:
gpu_tensor = torch.empty_like(tensor_to_broadcast, device="cuda")
dist.broadcast(gpu_tensor, src=src_global_rank)
tensor_to_broadcast.copy_(gpu_tensor.cpu(), non_blocking=True)
del gpu_tensor
torch.cuda.empty_cache()
else:
dist.broadcast(tensor_to_broadcast, src=src_global_rank)
if is_weight_loader:
del cpu_weight_dict
if cpu_offload:
torch.cuda.empty_cache()
logger.info(f"Weights distributed across {dist.get_world_size()} devices on {target_device}")
return distributed_weight_dict
def masks_like(tensor, zero=False, generator=None, p=0.2, prev_len=1):
assert isinstance(tensor, torch.Tensor)
out = torch.ones_like(tensor)
if zero:
if generator is not None:
random_num = torch.rand(1, generator=generator, device=generator.device).item()
if random_num < p:
out[:, :prev_len] = torch.zeros_like(out[:, :prev_len])
else:
out[:, :prev_len] = torch.zeros_like(out[:, :prev_len])
return out
def best_output_size(w, h, dw, dh, expected_area):
# float output size
ratio = w / h
ow = (expected_area * ratio) ** 0.5
oh = expected_area / ow
# process width first
ow1 = int(ow // dw * dw)
oh1 = int(expected_area / ow1 // dh * dh)
assert ow1 % dw == 0 and oh1 % dh == 0 and ow1 * oh1 <= expected_area
ratio1 = ow1 / oh1
# process height first
oh2 = int(oh // dh * dh)
ow2 = int(expected_area / oh2 // dw * dw)
assert oh2 % dh == 0 and ow2 % dw == 0 and ow2 * oh2 <= expected_area
ratio2 = ow2 / oh2
# compare ratios
if max(ratio / ratio1, ratio1 / ratio) < max(ratio / ratio2, ratio2 / ratio):
return ow1, oh1
else:
return ow2, oh2