Enhanced LTXVImgToVideoInplace with: - Arbitrary frame_idx (negative values for last frame) - Chainable noise masks via get_noise_mask() - Preprocessing: CRF, blur, interpolation, crop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
import torch
|
|
import comfy.utils
|
|
import comfy_extras.nodes_lt as nodes_lt
|
|
import comfy_extras.nodes_post_processing as post_processing
|
|
|
|
|
|
class LTXVImgToVideoInplaceAtIndex:
|
|
"""
|
|
Places an encoded image at any frame position in an existing video latent.
|
|
|
|
Improvements over the built-in LTXVImgToVideoInplace:
|
|
- Arbitrary frame_idx (not just frame 0). Negative values count from the end.
|
|
- Noise mask chaining: uses get_noise_mask() so multiple instances can be
|
|
chained without overwriting each other's masks.
|
|
- Preprocessing options (CRF, blur, interpolation, crop) matching
|
|
LTXVAddGuideAdvanced.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"vae": ("VAE",),
|
|
"image": ("IMAGE",),
|
|
"latent": ("LATENT",),
|
|
"frame_idx": (
|
|
"INT",
|
|
{
|
|
"default": 0,
|
|
"min": -9999,
|
|
"max": 9999,
|
|
"step": 1,
|
|
"tooltip": (
|
|
"Pixel-space frame index. 0 = first frame, -1 = last frame. "
|
|
"Converted to latent index using VAE temporal downscale factor."
|
|
),
|
|
},
|
|
),
|
|
"strength": (
|
|
"FLOAT",
|
|
{
|
|
"default": 1.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"tooltip": "Conditioning strength. 1.0 = fully conditioned, 0.0 = no conditioning.",
|
|
},
|
|
),
|
|
"crf": (
|
|
"INT",
|
|
{
|
|
"default": 29,
|
|
"min": 0,
|
|
"max": 51,
|
|
"step": 1,
|
|
"tooltip": "CRF compression. 0 = disabled. Higher values add more compression artifacts.",
|
|
},
|
|
),
|
|
"blur_radius": (
|
|
"INT",
|
|
{
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 7,
|
|
"step": 1,
|
|
"tooltip": "Gaussian blur kernel radius. 0 = disabled.",
|
|
},
|
|
),
|
|
"interpolation": (
|
|
[
|
|
"lanczos",
|
|
"bislerp",
|
|
"nearest",
|
|
"bilinear",
|
|
"bicubic",
|
|
"area",
|
|
"nearest-exact",
|
|
],
|
|
{"default": "lanczos"},
|
|
),
|
|
"crop": (["disabled", "center"], {"default": "disabled"}),
|
|
"bypass": (
|
|
"BOOLEAN",
|
|
{"default": False, "tooltip": "Bypass the conditioning."},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("latent",)
|
|
FUNCTION = "generate"
|
|
CATEGORY = "conditioning/video_models"
|
|
DESCRIPTION = (
|
|
"Places an image at an arbitrary frame position in a video latent. "
|
|
"Supports chaining: noise masks from previous calls are preserved."
|
|
)
|
|
|
|
def generate(
|
|
self, vae, image, latent, frame_idx, strength, crf, blur_radius,
|
|
interpolation, crop, bypass=False,
|
|
):
|
|
if bypass:
|
|
return (latent,)
|
|
|
|
# VAE scale factors
|
|
time_scale, height_scale, width_scale = vae.downscale_index_formula
|
|
|
|
# Compute target pixel dimensions from latent shape
|
|
samples = latent["samples"]
|
|
batch, channels, latent_frames, latent_h, latent_w = samples.shape
|
|
width = latent_w * width_scale
|
|
height = latent_h * height_scale
|
|
|
|
# Preprocessing: resize, CRF, blur (matching LTXVAddGuideAdvanced)
|
|
image = (
|
|
comfy.utils.common_upscale(
|
|
image.movedim(-1, 1), width, height, interpolation, crop=crop
|
|
)
|
|
.movedim(1, -1)
|
|
.clamp(0, 1)
|
|
)
|
|
if crf > 0:
|
|
image = nodes_lt.LTXVPreprocess.execute(image, crf)[0]
|
|
if blur_radius > 0:
|
|
sigma = 0.3 * blur_radius
|
|
image = post_processing.Blur.execute(image, blur_radius, sigma)[0]
|
|
|
|
# VAE encode (RGB channels only)
|
|
encode_pixels = image[:, :, :, :3]
|
|
t = vae.encode(encode_pixels)
|
|
cond_length = t.shape[2]
|
|
|
|
# Convert pixel frame_idx to latent index
|
|
total_pixel_frames = (latent_frames - 1) * time_scale + 1
|
|
|
|
if frame_idx < 0:
|
|
frame_idx = max(0, total_pixel_frames + frame_idx)
|
|
frame_idx = min(frame_idx, total_pixel_frames - 1)
|
|
|
|
if frame_idx == 0:
|
|
latent_idx = 0
|
|
else:
|
|
latent_idx = ((frame_idx - 1) // time_scale) + 1
|
|
|
|
# Validate bounds
|
|
assert latent_idx + cond_length <= latent_frames, (
|
|
f"Conditioning frames exceed latent length. "
|
|
f"latent_idx={latent_idx}, cond_length={cond_length}, latent_frames={latent_frames}"
|
|
)
|
|
|
|
# Chain noise mask properly (preserves masks from previous calls)
|
|
noise_mask = nodes_lt.get_noise_mask(latent)
|
|
samples = samples.clone()
|
|
|
|
# Replace at the target index
|
|
samples[:, :, latent_idx:latent_idx + cond_length] = t
|
|
noise_mask[:, :, latent_idx:latent_idx + cond_length] = 1.0 - strength
|
|
|
|
return ({"samples": samples, "noise_mask": noise_mask},)
|