Files
AlexandClaude Opus 4.5 08c6702b36 LTXVImgToVideoInplaceAtIndex node
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>
2026-02-01 18:43:47 +02:00

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},)