Files
2025-04-24 10:27:12 +08:00

708 lines
35 KiB
Python

import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
import torch
import traceback
import einops
import safetensors.torch as sf
import numpy as np
import argparse
import math
from PIL import Image
from diffusers import AutoencoderKLHunyuanVideo
from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer
from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake
from diffusers_helper.utils import save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw, resize_and_center_crop, state_dict_weighted_merge, state_dict_offset_merge, generate_timestamp
from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked
from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan
from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation, offload_model_from_device_for_memory_preservation, fake_diffusers_current_device, DynamicSwapInstaller, unload_complete_models, load_model_as_complete
from transformers import SiglipImageProcessor, SiglipVisionModel
from diffusers_helper.clip_vision import hf_clip_vision_encode
from diffusers_helper.bucket_tools import find_nearest_bucket
import hashlib
import random
import string
import torchvision
from torchvision.transforms.functional import to_pil_image
import comfy.utils
from PIL import Image
import folder_paths
class SE_FramePack:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"ref_image": ("IMAGE", ),
"prompt": ("STRING", {"multiline": True}),
"total_second_length": ("INT", {"default": 5, "min": 1, "max": 120, "step": 1}),
"seed": ("INT", {"default": 3407, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 25, "min": 1, "max": 100, "step": 1}),
"use_teacache": ("BOOLEAN", {"default": True}),
"resolution": (["360p", "480p", "540p", "720p"], {"default": "480p"}),
"padding_mode": (["optimized", "default (test)", "constant (test)", "waterfall (test)", "center_focus (test)"], {"default": "optimized"}),
"end_condition_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
"enable_feature_fusion": ("BOOLEAN", {"default": True}),
"history_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"history_decay": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 0.9, "step": 0.01}),
"history_weight_min": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 0.8, "step": 0.01}),
"gpu_memory_preservation": ("FLOAT", {"default": 6, "min": 6, "max": 128, "step": 0.1, "description": "GPU内存保留(GB),设置更大的值可避免OOM但会降低速度"}),
"use_flash_attention": ("BOOLEAN", {"default": False}),
"use_sage_attention": ("BOOLEAN", {"default": False}),
"overlap_frames": ("INT", {"default": 33, "min": 1, "max": 100, "step": 1, "description": "section之间的混合帧数,默认值33 (latent_window_size * 4 - 3)"}),
"blend_mode": (["linear", "cosine", "sigmoid"], {"default": "linear", "description": "混合曲线类型:linear(线性)、cosine(余弦)、sigmoid(S型)"}),
},
"optional": {
"end_image": ("IMAGE", ),
},
}
RETURN_TYPES = ("IMAGE", "FLOAT")
RETURN_NAMES = ("frames", "fps")
CATEGORY = "TTPlanet/FramePack"
FUNCTION = "run"
TITLE = 'TTPlanet FramePack'
OUTPUT_NODE = True
def __init__(self):
self.high_vram = False
self.frames = None
self.fps = None
hunyuan_root = os.path.join(folder_paths.models_dir, 'HunyuanVideo')
flux_redux_bfl_root = os.path.join(folder_paths.models_dir, 'flux_redux_bfl')
framePackI2V_root = os.path.join(folder_paths.models_dir, 'FramePackI2V_HY')
self.text_encoder = LlamaModel.from_pretrained(hunyuan_root, subfolder='text_encoder', torch_dtype=torch.float16).cpu()
self.text_encoder_2 = CLIPTextModel.from_pretrained(hunyuan_root, subfolder='text_encoder_2', torch_dtype=torch.float16).cpu()
self.tokenizer = LlamaTokenizerFast.from_pretrained(hunyuan_root, subfolder='tokenizer')
self.tokenizer_2 = CLIPTokenizer.from_pretrained(hunyuan_root, subfolder='tokenizer_2')
self.vae = AutoencoderKLHunyuanVideo.from_pretrained(hunyuan_root, subfolder='vae', torch_dtype=torch.float16).cpu()
self.feature_extractor = SiglipImageProcessor.from_pretrained(flux_redux_bfl_root, subfolder='feature_extractor')
self.image_encoder = SiglipVisionModel.from_pretrained(flux_redux_bfl_root, subfolder='image_encoder', torch_dtype=torch.float16).cpu()
self.transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained(framePackI2V_root, torch_dtype=torch.bfloat16).cpu()
self.vae.eval()
self.text_encoder.eval()
self.text_encoder_2.eval()
self.image_encoder.eval()
self.transformer.eval()
if not self.high_vram:
self.vae.enable_slicing()
self.vae.enable_tiling()
self.transformer.high_quality_fp32_output_for_inference = True
print('transformer.high_quality_fp32_output_for_inference = True')
self.transformer.to(dtype=torch.bfloat16)
self.vae.to(dtype=torch.float16)
self.image_encoder.to(dtype=torch.float16)
self.text_encoder.to(dtype=torch.float16)
self.text_encoder_2.to(dtype=torch.float16)
self.vae.requires_grad_(False)
self.text_encoder.requires_grad_(False)
self.text_encoder_2.requires_grad_(False)
self.image_encoder.requires_grad_(False)
self.transformer.requires_grad_(False)
if not self.high_vram:
# DynamicSwapInstaller is same as huggingface's enable_sequential_offload but 3x faster
DynamicSwapInstaller.install_model(self.transformer, device=gpu)
DynamicSwapInstaller.install_model(self.text_encoder, device=gpu)
def strict_align(self, h, w):
"""确保高度和宽度是64的倍数,且潜在空间是8的倍数"""
aligned_h = int(round(h / 64)) * 64
aligned_w = int(round(w / 64)) * 64
assert (aligned_h % 64 == 0) and (aligned_w % 64 == 0), "尺寸必须是64的倍数"
assert (aligned_h//8) % 8 == 0 and (aligned_w//8) % 8 == 0, "潜在空间需要8的倍数"
return aligned_h, aligned_w
def preprocess_image(self, image):
if image is None:
return None
image_np = 255. * image[0].cpu().numpy()
image = Image.fromarray(np.clip(image_np, 0, 255).astype(np.uint8)).convert("RGB")
input_image = np.array(image)
return input_image
def run(self, **kwargs):
try:
image = kwargs['ref_image']
end_image = kwargs.get('end_image', None) # Use get with None as default
image_np = self.preprocess_image(image)
end_image_np = self.preprocess_image(end_image) if end_image is not None else None
prompt = kwargs['prompt']
seed = kwargs['seed']
total_second_length = kwargs['total_second_length']
steps = kwargs['steps']
use_teacache = kwargs['use_teacache']
resolution = kwargs['resolution']
padding_mode = kwargs['padding_mode']
end_condition_strength = kwargs['end_condition_strength']
enable_feature_fusion = kwargs['enable_feature_fusion']
history_weight = kwargs['history_weight']
history_decay = kwargs['history_decay']
history_weight_min = kwargs['history_weight_min']
gpu_memory_preservation = kwargs['gpu_memory_preservation']
use_flash_attention = kwargs['use_flash_attention']
use_sage_attention = kwargs['use_sage_attention']
overlap_frames = kwargs['overlap_frames']
blend_mode = kwargs['blend_mode']
# Remove "(test)" suffix from padding_mode
real_padding_mode = padding_mode.split(" ")[0]
random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
video_path = os.path.join(folder_paths.get_output_directory(), f'{random_str}.mp4')
self.pbar = comfy.utils.ProgressBar(steps * total_second_length)
self.exec(
input_image=image_np,
end_image=end_image_np,
prompt=prompt,
seed=seed,
total_second_length=total_second_length,
video_path=video_path,
steps=steps,
use_teacache=use_teacache,
resolution=resolution,
padding_mode=real_padding_mode,
end_condition_strength=end_condition_strength,
enable_feature_fusion=enable_feature_fusion,
history_weight=history_weight,
history_decay=history_decay,
history_weight_min=history_weight_min,
gpu_memory_preservation=gpu_memory_preservation,
use_flash_attention=use_flash_attention,
use_sage_attention=use_sage_attention,
overlap_frames=overlap_frames,
blend_mode=blend_mode
)
if os.path.exists(video_path):
self.fps = self.get_fps_with_torchvision(video_path)
self.frames = self.extract_frames_as_pil(video_path)
print(f'{video_path}:{self.fps} {len(self.frames)}')
else:
self.frames = []
self.fps = 0.0
except Exception as e:
print(f"Error in run: {str(e)}")
traceback.print_exc()
self.frames = []
self.fps = 0.0
return (self.frames, self.fps)
@torch.no_grad()
def exec(self, input_image, video_path,
end_image=None,
prompt="The girl dances gracefully, with clear movements, full of charm.",
n_prompt="",
seed=31337,
total_second_length=5,
latent_window_size=9,
steps=25,
cfg=1,
gs=32,
rs=0,
gpu_memory_preservation=6,
use_teacache=True,
resolution="480p",
padding_mode="optimized",
end_condition_strength=1.0,
enable_feature_fusion=True,
history_weight=1.0,
history_decay=0.0,
history_weight_min=0.0,
use_flash_attention=False,
use_sage_attention=False,
overlap_frames=33,
blend_mode="linear"):
total_latent_sections = (total_second_length * 30) / (latent_window_size * 4)
total_latent_sections = int(max(round(total_latent_sections), 1))
# 启用Progressive Decay(按需求默认开启且不需要展示给用户)
progressive_decay = True
try:
# 应用Flash Attention或Sage Attention
if use_flash_attention:
# 尝试导入并应用Flash Attention
try:
from flash_attn import flash_attn_func
print("Flash Attention enabled")
# 在此处应用Flash Attention的设置(如果需要的话)
except ImportError:
print("Flash Attention module not found, please install it first")
if use_sage_attention:
# 尝试导入并应用Sage Attention
try:
print("Sage Attention enabled")
# 在此处应用Sage Attention的设置(如果需要的话)
except ImportError:
print("Sage Attention module not found or not applicable")
# Clean GPU
if not self.high_vram:
unload_complete_models(
self.text_encoder, self.text_encoder_2, self.image_encoder, self.vae, self.transformer
)
# Text encoding
print('Text encoding')
if not self.high_vram:
fake_diffusers_current_device(self.text_encoder, gpu)
load_model_as_complete(self.text_encoder_2, target_device=gpu)
llama_vec, clip_l_pooler = encode_prompt_conds(prompt, self.text_encoder, self.text_encoder_2, self.tokenizer, self.tokenizer_2)
if cfg == 1:
llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler)
else:
llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, self.text_encoder, self.text_encoder_2, self.tokenizer, self.tokenizer_2)
llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512)
llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512)
# Processing input image (start frame)
print('Processing start frame ...')
H, W, C = input_image.shape
# 根据所选分辨率确定目标高度
if resolution == '360p':
target_height = 360
elif resolution == '540p':
target_height = 540
elif resolution == '720p':
target_height = 720
else: # 默认480p
target_height = 480
# 保持宽高比计算宽度
target_ratio = W / H
target_width = int(target_height * target_ratio)
if target_width % 2 != 0: # 确保是偶数
target_width += 1
# 找到最接近的桶分辨率,但保持宽高比
height, width = find_nearest_bucket(target_height, target_width, resolution=target_height)
print(f"Target resolution: {width}x{height}, Aspect ratio: {width/height:.3f}")
input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height)
input_image_pt = torch.from_numpy(input_image_np).float() / 127.5 - 1
input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None]
# Processing end image if provided
has_end_image = end_image is not None
end_image_np = None
end_image_pt = None
if has_end_image:
print('Processing end frame ...')
end_image_np = resize_and_center_crop(end_image, target_width=width, target_height=height)
end_image_pt = torch.from_numpy(end_image_np).float() / 127.5 - 1
end_image_pt = end_image_pt.permute(2, 0, 1)[None, :, None]
# VAE encoding
print('VAE encoding ...')
if not self.high_vram:
load_model_as_complete(self.vae, target_device=gpu)
start_latent = vae_encode(input_image_pt, self.vae)
end_latent = None
if has_end_image:
end_latent = vae_encode(end_image_pt, self.vae)
# CLIP Vision
print('CLIP Vision encoding ...')
if not self.high_vram:
load_model_as_complete(self.image_encoder, target_device=gpu)
# Start image encoding
image_encoder_output = hf_clip_vision_encode(input_image_np, self.feature_extractor, self.image_encoder)
image_encoder_last_hidden_state = image_encoder_output.last_hidden_state
# End image encoding if available
if has_end_image:
end_image_encoder_output = hf_clip_vision_encode(end_image_np, self.feature_extractor, self.image_encoder)
end_image_encoder_last_hidden_state = end_image_encoder_output.last_hidden_state
# 根据end_condition_strength和enable_feature_fusion参数处理
if end_condition_strength <= 0.0:
# 不使用结束特征
pass # 保持image_encoder_last_hidden_state不变
elif end_condition_strength >= 1.0:
# 完全按照原始方式融合
if enable_feature_fusion:
image_encoder_last_hidden_state = (image_encoder_last_hidden_state + end_image_encoder_last_hidden_state) / 2
else:
# 按照强度比例混合
if enable_feature_fusion:
image_encoder_last_hidden_state = (1.0 - end_condition_strength) * image_encoder_last_hidden_state + end_condition_strength * end_image_encoder_last_hidden_state
# Dtype
llama_vec = llama_vec.to(self.transformer.dtype)
llama_vec_n = llama_vec_n.to(self.transformer.dtype)
clip_l_pooler = clip_l_pooler.to(self.transformer.dtype)
clip_l_pooler_n = clip_l_pooler_n.to(self.transformer.dtype)
image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(self.transformer.dtype)
print('Start Sample')
rnd = torch.Generator("cpu").manual_seed(seed)
num_frames = latent_window_size * 4 - 3
history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu()
history_pixels = None
total_generated_latent_frames = 0
# 根据padding_mode选择不同的padding策略
if padding_mode == "default":
# 原始模式: 直接使用倒序数列
latent_paddings = list(reversed(range(total_latent_sections)))
elif padding_mode == "constant":
# 常量模式: 保持固定值2,最后两块特殊处理
if total_latent_sections <= 2:
# 对于短视频,使用[1, 0]或[0]
latent_paddings = list(reversed(range(total_latent_sections)))
else:
# 对于长视频,除了最后两块外都使用常量2
latent_paddings = [2] * (total_latent_sections - 2) + [1, 0]
elif padding_mode == "optimized":
# 优化模式: 对于长视频使用自定义分布
if total_latent_sections > 4:
latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0]
else:
latent_paddings = list(reversed(range(total_latent_sections)))
elif padding_mode == "waterfall":
# 瀑布模式
if total_latent_sections <= 2:
latent_paddings = list(reversed(range(total_latent_sections)))
else:
# 最大padding值
max_padding = 4
# 头部增长阶段的长度
head_size = min(4, total_latent_sections // 3)
# 尾部(值为1和0)的长度固定为2
tail_size = 2
# 中间部分长度
middle_size = total_latent_sections - head_size - tail_size
# 创建头部:线性增加到最大值
head_paddings = []
for i in range(head_size):
# 线性插值从3到max_padding
ratio = i / (head_size - 1) if head_size > 1 else 1
padding = 3 + (max_padding - 3) * ratio
head_paddings.append(round(padding))
# 中间部分全部为2
middle_paddings = [2] * middle_size
# 尾部固定为[1, 0]
tail_paddings = [1, 0]
# 组合所有部分
latent_paddings = head_paddings + middle_paddings + tail_paddings
elif padding_mode == "center_focus":
# 中心聚焦模式
if total_latent_sections <= 3:
latent_paddings = list(reversed(range(total_latent_sections)))
else:
# 更平滑的padding过渡
max_padding = 3
min_padding = 1
if total_latent_sections % 2 == 0: # 偶数个sections
half_size = total_latent_sections // 2
# 创建半边,然后镜像
first_half = []
for i in range(half_size):
position_ratio = i / (half_size - 1) if half_size > 1 else 0
padding = min_padding + (max_padding - min_padding) * position_ratio ** 2
first_half.append(round(padding))
# 镜像创建
second_half = first_half.copy()
second_half.reverse()
latent_paddings = first_half + second_half
else: # 奇数个sections
mid_idx = total_latent_sections // 2
latent_paddings = []
for i in range(total_latent_sections):
distance_ratio = abs(i - mid_idx) / mid_idx if mid_idx > 0 else 0
padding = max_padding - (max_padding - min_padding) * distance_ratio ** 2
latent_paddings.append(round(padding))
# 确保第一个元素不要太大
latent_paddings[0] = min(latent_paddings[0], 2)
# 确保最后几个section的padding有平滑过渡
if len(latent_paddings) >= 4:
latent_paddings[-4] = min(latent_paddings[-4], 3)
latent_paddings[-3] = 2
latent_paddings[-2] = 1
latent_paddings[-1] = 0
elif len(latent_paddings) == 3:
latent_paddings[0] = 2
latent_paddings[1] = 1
latent_paddings[2] = 0
else:
# 默认回退到优化模式
if total_latent_sections > 4:
latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0]
else:
latent_paddings = list(reversed(range(total_latent_sections)))
# 确保最后一个padding值为0
if len(latent_paddings) > 0:
latent_paddings[-1] = 0
print(f"Selected padding mode: {padding_mode}")
print(f"Total sections: {total_latent_sections}")
print(f"Latent paddings: {latent_paddings}")
# 计算每个section的history weight,实现衰减效果
section_weights = []
current_weight = history_weight
# 找出最大padding值,用于归一化padding影响
max_padding = max(latent_paddings) if latent_paddings else 3
if max_padding == 0: # 防止除以零错误
max_padding = 1
for i, padding in enumerate(latent_paddings):
section_weights.append(current_weight)
# 根据当前section的padding值调整衰减率
padding_factor = padding / max_padding # 归一化到0-1范围
# 调整衰减率:padding_factor越大,实际衰减越小
adjusted_decay = history_decay * (1.0 - padding_factor * 0.8) # 0.8是补偿系数
# 应用调整后的衰减率,并确保不低于最小值
current_weight = max(history_weight_min, current_weight * (1.0 - adjusted_decay))
print(f"Section {i+1} padding: {padding}, padding_factor: {padding_factor:.2f}, " +
f"adjusted_decay: {adjusted_decay:.4f}, weight: {current_weight:.4f}")
print(f"Section history weights: {section_weights}")
for i, latent_padding in enumerate(latent_paddings):
# 获取当前section的history weight
current_history_weight = section_weights[i]
print(f"Section {i+1} history weight: {current_history_weight:.4f}")
is_last_section = latent_padding == 0
is_first_section = latent_padding == latent_paddings[0]
latent_padding_size = latent_padding * latent_window_size
print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}, is_first_section = {is_first_section}')
indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0)
clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1)
clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1)
# 确保所有latent都是5维的 [1, 16, T, h, w]
if len(start_latent.shape) == 4: # [1, 16, h, w]
clean_latents_pre = start_latent.unsqueeze(2).to(history_latents) # 变为 [1, 16, 1, h, w]
else:
clean_latents_pre = start_latent.to(history_latents) # 已经是5维
# 从history_latents中提取
clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2)
# 应用当前section的history_weight,调整历史潜变量的影响力
if current_history_weight < 1.0:
# 如果启用渐进式衰减,对latent_indices涉及到的帧应用不同的权重
if progressive_decay and latent_window_size > 1 and not is_first_section:
# 只对clean_latents_post进行操作,它是历史信息
weighted_clean_latents_post = clean_latents_post.clone()
# 获取下一个section的权重
next_section_idx = min(i + 1, len(section_weights) - 1)
next_weight = section_weights[next_section_idx] if next_section_idx < len(section_weights) else max(history_weight_min, current_history_weight * 0.5)
# 在前一个section的后部分应用过渡衰减
# 这创建了一个从当前权重到下一个section权重的平滑过渡
middle_weight = (current_history_weight + next_weight) / 2
weighted_clean_latents_post = weighted_clean_latents_post * middle_weight
print(f"Progressive decay from {current_history_weight:.4f} to {middle_weight:.4f} for section {i+1}")
else:
weighted_clean_latents_post = clean_latents_post * current_history_weight
# 默认合并clean_latents
clean_latents = torch.cat([clean_latents_pre, weighted_clean_latents_post], dim=2)
else:
# 默认合并clean_latents
clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2)
# 使用结束帧latent
if has_end_image and is_first_section:
# 确保end_latent也是5维的
if len(end_latent.shape) == 4: # [1, 16, h, w]
end_latent_to_use = end_latent.unsqueeze(2) # 变为 [1, 16, 1, h, w]
else:
end_latent_to_use = end_latent
# 根据强度参数决定如何应用end_latent
if end_condition_strength <= 0.0:
# 强度为0,不使用end_latent,保持clean_latents不变
print(f"End condition strength is 0, not using end reference")
elif end_condition_strength >= 1.0:
# 强度为1,完全使用end_latent
print(f"End condition strength is 1, fully using end reference")
clean_latents = torch.cat([clean_latents_pre, end_latent_to_use.to(history_latents)], dim=2)
else:
# 混合start_latent和end_latent
print(f"Mixing start and end with strength {end_condition_strength}")
# 进行插值混合
mixed_latent = (1.0 - end_condition_strength) * clean_latents_post + end_condition_strength * end_latent_to_use.to(history_latents)
clean_latents = torch.cat([clean_latents_pre, mixed_latent], dim=2)
if not self.high_vram:
unload_complete_models()
move_model_to_device_with_memory_preservation(self.transformer, target_device=gpu, preserved_memory_gb=gpu_memory_preservation)
if use_teacache:
self.transformer.initialize_teacache(enable_teacache=True, num_steps=steps)
else:
self.transformer.initialize_teacache(enable_teacache=False)
def callback(d):
self.update(1)
return
generated_latents = sample_hunyuan(
transformer=self.transformer,
sampler='unipc',
width=width,
height=height,
frames=num_frames,
real_guidance_scale=cfg,
distilled_guidance_scale=gs,
guidance_rescale=rs,
num_inference_steps=steps,
generator=rnd,
prompt_embeds=llama_vec,
prompt_embeds_mask=llama_attention_mask,
prompt_poolers=clip_l_pooler,
negative_prompt_embeds=llama_vec_n,
negative_prompt_embeds_mask=llama_attention_mask_n,
negative_prompt_poolers=clip_l_pooler_n,
device=gpu,
dtype=torch.bfloat16,
image_embeddings=image_encoder_last_hidden_state,
latent_indices=latent_indices,
clean_latents=clean_latents,
clean_latent_indices=clean_latent_indices,
clean_latents_2x=clean_latents_2x,
clean_latent_2x_indices=clean_latent_2x_indices,
clean_latents_4x=clean_latents_4x,
clean_latent_4x_indices=clean_latent_4x_indices,
callback=callback,
)
if is_last_section:
# 确保start_latent是5维的,与generated_latents维度匹配
if len(start_latent.shape) == 4: # [1, 16, h, w]
start_latent_5d = start_latent.unsqueeze(2) # 变为 [1, 16, 1, h, w]
else:
start_latent_5d = start_latent
generated_latents = torch.cat([start_latent_5d.to(generated_latents), generated_latents], dim=2)
total_generated_latent_frames += int(generated_latents.shape[2])
history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2)
if not self.high_vram:
offload_model_from_device_for_memory_preservation(self.transformer, target_device=gpu, preserved_memory_gb=8)
load_model_as_complete(self.vae, target_device=gpu)
real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :]
if history_pixels is None:
history_pixels = vae_decode(real_history_latents, self.vae).cpu()
else:
section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2)
# 使用用户指定的overlap_frames替代固定值
overlapped_frames = overlap_frames
current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], self.vae).cpu()
history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames, blend_mode)
if not self.high_vram:
unload_complete_models()
# 如果是最后一个section,保存视频
if is_last_section:
save_bcthw_as_mp4(history_pixels, video_path, fps=30)
break
except Exception as e:
print(f"Error in exec: {str(e)}")
traceback.print_exc()
finally:
unload_complete_models()
def update(self, in_progress):
self.pbar.update(in_progress)
def extract_frames_as_pil(self, video_path):
video, _, _ = torchvision.io.read_video(video_path, pts_unit='sec') # (T, H, W, C)
frames = [to_pil_image(frame.permute(2, 0, 1)) for frame in video]
frames = [torch.from_numpy(np.array(frame).astype(np.float32) / 255.0) for frame in frames]
return frames
def get_fps_with_torchvision(self, video_path):
_, _, info = torchvision.io.read_video(video_path, pts_unit='sec')
return info['video_fps']
NODE_CLASS_MAPPINGS = {
"TTPlanet_FramePack": SE_FramePack,
}
def soft_append_bcthw(history, current, overlap=0, blend_mode='linear'):
if overlap <= 0:
return torch.cat([history, current], dim=2)
assert history.shape[2] >= overlap, f"History length ({history.shape[2]}) must be >= overlap ({overlap})"
assert current.shape[2] >= overlap, f"Current length ({current.shape[2]}) must be >= overlap ({overlap})"
# 根据不同的混合模式选择权重曲线
if blend_mode == 'cosine':
# 使用余弦曲线,提供更平滑的过渡
x = torch.linspace(0, math.pi, overlap, dtype=history.dtype, device=history.device)
weights = (torch.cos(x) + 1) / 2
elif blend_mode == 'sigmoid':
# 使用sigmoid函数,在中间区域提供更平缓的过渡
x = torch.linspace(-6, 6, overlap, dtype=history.dtype, device=history.device)
weights = torch.sigmoid(x)
else: # 'linear'
# 默认线性
weights = torch.linspace(1, 0, overlap, dtype=history.dtype, device=history.device)
weights = weights.view(1, 1, -1, 1, 1)
blended = weights * history[:, :, -overlap:] + (1 - weights) * current[:, :, :overlap]
output = torch.cat([history[:, :, :-overlap], blended, current[:, :, overlap:]], dim=2)
return output.to(history)