This commit is contained in:
Fill
2025-08-18 22:16:44 -05:00
parent ba6fbe4d2d
commit 2bcc24aba0
2 changed files with 73 additions and 105 deletions
+24
View File
@@ -80,6 +80,30 @@ class FL_ImageCaptionLayoutPDF:
rows_per_page = math.floor((height - padding_pt) / item_height)
items_per_page = items_per_row * rows_per_page
# Validate that at least one item can fit on the page
if items_per_page == 0:
# Calculate page dimensions for error message
page_width_usable = width - padding_pt
page_height_usable = height - padding_pt
error_msg = (
f"Layout Error: Items are too large to fit on the page.\n"
f"Current settings:\n"
f" - Page size: {width:.0f} x {height:.0f} pts ({orientation} orientation)\n"
f" - Usable area: {page_width_usable:.0f} x {page_height_usable:.0f} pts (after padding)\n"
f" - Item size needed: {item_width:.0f} x {item_height:.0f} pts\n"
f" - Display size: {display_size_pt} pts\n"
f" - Caption height: {caption_height_pt} pts\n"
f" - Padding: {padding_pt} pts\n\n"
f"Solutions:\n"
f" 1. Reduce 'display_size' (currently {display_size_pt})\n"
f" 2. Reduce 'caption_height' (currently {caption_height_pt})\n"
f" 3. Reduce 'padding' (currently {padding_pt})\n"
f" 4. Try switching orientation (currently '{orientation}')\n"
f" 5. Use fewer 'images_per_row' (currently {images_per_row})"
)
raise ValueError(error_msg)
for i in range(0, len(image_files), items_per_page):
page_images = image_files[i:i + items_per_page]
+49 -105
View File
@@ -11,11 +11,6 @@ import comfy.utils
import comfy.latent_formats
import comfy.clip_vision
# Import original WAN functionality
import sys
sys.path.append(str(ROOT_COMFY / "comfy_extras"))
from nodes_wan import WanFirstLastFrameToVideo
class FL_WanFirstLastFrameToVideo:
@classmethod
def INPUT_TYPES(s):
@@ -57,7 +52,7 @@ class FL_WanFirstLastFrameToVideo:
CATEGORY = "🏵️Fill Nodes/video"
def __init__(self):
self.original_wan = WanFirstLastFrameToVideo()
pass
def generate_interpolation_curve(self, curve_type: str, length: int) -> torch.Tensor:
"""Generate normalized timing curve [0,1] for frame interpolation"""
@@ -163,99 +158,16 @@ class FL_WanFirstLastFrameToVideo:
return g2d / g2d.sum()
def generate_interpolation_curve(self, curve_type, length):
"""Generate interpolation curve based on curve type"""
t = torch.linspace(0, 1, length)
if curve_type == "linear":
return t
elif curve_type == "ease_in":
return t * t
elif curve_type == "ease_out":
return 1 - (1 - t) * (1 - t)
elif curve_type == "ease_in_out":
return torch.where(t < 0.5, 2 * t * t, 1 - 2 * (1 - t) * (1 - t))
elif curve_type == "bounce":
# Simple bounce effect
return torch.abs(torch.sin(t * math.pi * 2)) * (1 - t) + t
else:
return t # Default to linear
def calculate_keyframe_positions(self, start_pos, end_pos, length, buffer):
"""Calculate frame positions based on timing parameters"""
start_frame = int(start_pos * (length - 1))
end_frame = int(end_pos * (length - 1))
return {
'start_frame': start_frame,
'end_frame': end_frame,
'buffer_start': max(0, start_frame - buffer),
'buffer_end': min(length - 1, end_frame + buffer)
}
def process_custom_mask(self, mask_input, feather, invert):
"""Process custom interpolation mask with feathering and inversion options"""
mask = mask_input.clone()
if invert:
mask = 1.0 - mask
if feather > 0:
# Apply Gaussian blur for feathering
kernel_size = int(feather * 10) * 2 + 1 # Ensure odd kernel size
sigma = feather * 3
# Create Gaussian kernel
x = torch.arange(kernel_size, dtype=torch.float32, device=mask.device)
x = x - kernel_size // 2
gauss = torch.exp(-(x ** 2) / (2 * sigma ** 2))
gauss = gauss / gauss.sum()
# Apply separable Gaussian blur if mask is 2D
if len(mask.shape) >= 2:
kernel_h = gauss.view(1, 1, -1, 1)
kernel_w = gauss.view(1, 1, 1, -1)
# Ensure mask has batch and channel dimensions
original_shape = mask.shape
if len(mask.shape) == 2:
mask = mask.unsqueeze(0).unsqueeze(0)
elif len(mask.shape) == 3:
mask = mask.unsqueeze(0)
# Apply blur
mask = F.conv2d(mask, kernel_h, padding=(kernel_size//2, 0))
mask = F.conv2d(mask, kernel_w, padding=(0, kernel_size//2))
# Restore original shape
mask = mask.view(original_shape)
return mask
def integrate_custom_mask(self, wan_mask, custom_mask, width, height):
"""Integrate custom mask with WAN's guidance mask"""
# Resize custom mask to match WAN mask dimensions
mask_h, mask_w = wan_mask.shape[-2], wan_mask.shape[-1]
custom_resized = F.interpolate(custom_mask.unsqueeze(0).unsqueeze(0),
size=(mask_h, mask_w),
mode='bilinear', align_corners=False).squeeze()
# Combine masks: WAN mask controls temporal placement, custom mask controls spatial regions
combined_mask = wan_mask.clone()
for i in range(combined_mask.shape[2]):
combined_mask[:, :, i] = combined_mask[:, :, i] * custom_resized
return combined_mask
def encode(self, positive, negative, vae, width, height, length, batch_size,
keyframe_start_position, keyframe_end_position, interpolation_curve, temporal_buffer,
start_image=None, end_image=None, clip_vision_start_image=None, clip_vision_end_image=None,
interpolation_mask=None, mask_feather=0.1, mask_invert=False, regional_blending=True):
# If all parameters are default, use original WAN
# If all parameters are default, use original WAN logic
if (keyframe_start_position == 0.0 and keyframe_end_position == 1.0 and
interpolation_curve == "linear" and interpolation_mask is None and temporal_buffer == 3):
return self.original_wan.encode(
return self.original_wan_encode(
positive, negative, vae, width, height, length, batch_size,
start_image, end_image, clip_vision_start_image, clip_vision_end_image
)
@@ -268,6 +180,52 @@ class FL_WanFirstLastFrameToVideo:
interpolation_mask, mask_feather, mask_invert, regional_blending
)
def original_wan_encode(self, positive, negative, vae, width, height, length, batch_size,
start_image=None, end_image=None, clip_vision_start_image=None, clip_vision_end_image=None):
"""Original WAN functionality integrated natively"""
latent = torch.zeros([batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8],
device=comfy.model_management.intermediate_device())
if start_image is not None:
start_image = comfy.utils.common_upscale(start_image[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1)
if end_image is not None:
end_image = comfy.utils.common_upscale(end_image[-length:].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1)
image = torch.ones((length, height, width, 3)) * 0.5
mask = torch.ones((1, 1, latent.shape[2] * 4, latent.shape[-2], latent.shape[-1]))
if start_image is not None:
image[:start_image.shape[0]] = start_image
mask[:, :, :start_image.shape[0] + 3] = 0.0
if end_image is not None:
image[-end_image.shape[0]:] = end_image
mask[:, :, -end_image.shape[0]:] = 0.0
concat_latent_image = vae.encode(image[:, :, :, :3])
mask = mask.view(1, mask.shape[2] // 4, 4, mask.shape[3], mask.shape[4]).transpose(1, 2)
positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
clip_vision_output = None
if clip_vision_start_image is not None:
clip_vision_output = clip_vision_start_image
if clip_vision_end_image is not None:
if clip_vision_output is not None:
states = torch.cat([clip_vision_output.penultimate_hidden_states, clip_vision_end_image.penultimate_hidden_states], dim=-2)
clip_vision_output = comfy.clip_vision.Output()
clip_vision_output.penultimate_hidden_states = states
else:
clip_vision_output = clip_vision_end_image
if clip_vision_output is not None:
positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output})
negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output})
out_latent = {"samples": latent}
return (positive, negative, out_latent)
def enhanced_wan_encode(self, positive, negative, vae, width, height, length, batch_size,
keyframe_start_position, keyframe_end_position, interpolation_curve, temporal_buffer,
start_image=None, end_image=None, clip_vision_start_image=None, clip_vision_end_image=None,
@@ -375,20 +333,6 @@ class FL_WanFirstLastFrameToVideo:
out_latent = {"samples": latent}
return (positive, negative, out_latent)
def integrate_custom_mask(self, wan_mask, custom_mask, width, height):
"""Integrate custom mask with WAN's guidance mask"""
# Resize custom mask to match WAN mask dimensions
mask_h, mask_w = wan_mask.shape[-2], wan_mask.shape[-1]
custom_resized = F.interpolate(custom_mask.unsqueeze(0).unsqueeze(0),
size=(mask_h, mask_w),
mode='bilinear', align_corners=False).squeeze()
# Combine masks: WAN mask controls temporal placement, custom mask controls spatial regions
combined_mask = wan_mask.clone()
for i in range(combined_mask.shape[2]):
combined_mask[:, :, i] = combined_mask[:, :, i] * custom_resized
return combined_mask
# Node registration info
NODE_CLASS_MAPPINGS = {