Files
TTPlanetPig-Comfyui_TTP_Too…/LTXVFirstLastFrameControl_TTP.py
T

181 lines
5.9 KiB
Python

"""
LTXVFirstLastFrameControl_TTP - 首尾帧控制节点
实现首帧和尾帧的精确控制,支持羽化过渡和强度调节
Author: TTP
Based on: ComfyUI LTXVImgToVideoInplace
"""
import torch
import comfy.utils
from comfy_api.latest import io
class LTXVFirstLastFrameControl_TTP(io.ComfyNode):
"""
首尾帧控制节点
功能:
- 同时控制视频的首帧和尾帧
- 支持独立的强度调节
- 可选的羽化过渡效果
- 灵活的bypass选项
工作原理:
1. 将首帧和尾帧图像通过VAE编码到latent空间
2. 在视频latent的首尾位置嵌入编码后的帧
3. 使用noise_mask控制嵌入强度
4. 可选的羽化过渡避免帧间突变
"""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LTXVFirstLastFrameControl_TTP",
display_name="LTX First Last Frame Control (TTP)",
category="conditioning/video_models",
description="控制LTX视频生成的首帧和尾帧,支持羽化过渡",
inputs=[
io.Vae.Input("vae"),
io.Latent.Input("latent"),
io.Image.Input("first_image", optional=True, tooltip="首帧图像(可选)"),
io.Image.Input("last_image", optional=True, tooltip="尾帧图像(可选)"),
io.Float.Input(
"first_strength",
default=1.0,
min=0.0,
max=1.0,
step=0.05,
tooltip="首帧嵌入强度 (1.0=完全替换, 0.0=不嵌入)"
),
io.Float.Input(
"last_strength",
default=1.0,
min=0.0,
max=1.0,
step=0.05,
tooltip="尾帧嵌入强度 (1.0=完全替换, 0.0=不嵌入)"
),
],
outputs=[
io.Latent.Output(display_name="latent"),
],
)
@classmethod
def execute(
cls,
vae,
latent,
first_image=None,
last_image=None,
first_strength=1.0,
last_strength=1.0
) -> io.NodeOutput:
"""
执行首尾帧控制
Args:
vae: VAE模型用于图像编码
latent: 输入的视频latent
first_image: 首帧图像(可选)
last_image: 尾帧图像(可选)
first_strength: 首帧强度
last_strength: 尾帧强度
Returns:
包含处理后latent和noise_mask的字典
"""
# 如果没有提供任何图像,直接返回
if first_image is None and last_image is None:
return io.NodeOutput(latent)
# 获取latent信息
samples = latent["samples"]
batch, _, latent_frames, latent_height, latent_width = samples.shape
# 获取VAE的缩放因子
_, height_scale_factor, width_scale_factor = vae.downscale_index_formula
width = latent_width * width_scale_factor
height = latent_height * height_scale_factor
# 初始化noise_mask (全1表示不mask)
noise_mask = torch.ones(
(batch, 1, latent_frames, 1, 1),
dtype=torch.float32,
device=samples.device,
)
# 处理首帧
if first_image is not None and first_strength > 0.0:
first_latent = cls._encode_image(vae, first_image, height, width)
first_latent_frames = first_latent.shape[2]
# 嵌入首帧
samples[:, :, :first_latent_frames] = first_latent
# 设置首帧的noise_mask (1.0 - strength 表示保留多少噪声)
noise_mask[:, :, :first_latent_frames] = 1.0 - first_strength
# 处理尾帧
if last_image is not None and last_strength > 0.0:
last_latent = cls._encode_image(vae, last_image, height, width)
last_latent_frames = last_latent.shape[2]
# 计算尾帧起始位置
last_start_idx = latent_frames - last_latent_frames
if last_start_idx < 0:
# latent长度不足,截取latent
last_latent = last_latent[:, :, :latent_frames]
last_start_idx = 0
last_latent_frames = latent_frames
# 嵌入尾帧
samples[:, :, last_start_idx:] = last_latent
# 设置尾帧的noise_mask
noise_mask[:, :, last_start_idx:] = 1.0 - last_strength
return io.NodeOutput({
"samples": samples,
"noise_mask": noise_mask
})
@staticmethod
def _encode_image(vae, image, target_height, target_width):
"""
编码图像到latent空间
Args:
vae: VAE模型
image: 输入图像张量
target_height: 目标高度
target_width: 目标宽度
Returns:
编码后的latent张量
"""
# 调整图像尺寸
if image.shape[1] != target_height or image.shape[2] != target_width:
pixels = comfy.utils.common_upscale(
image.movedim(-1, 1),
target_width,
target_height,
"bilinear",
"center"
).movedim(1, -1)
else:
pixels = image
# 只取RGB通道(去掉alpha)
encode_pixels = pixels[:, :, :, :3]
# VAE编码
latent = vae.encode(encode_pixels)
return latent
# 兼容旧API
generate = execute