601 lines
22 KiB
Python
601 lines
22 KiB
Python
# ComfyUI-VS
|
|
# VS Image Compare Deluxe
|
|
# A retro transition/image comparison node for ComfyUI.
|
|
# Made for before/after comparisons, video previews
|
|
|
|
import math
|
|
import torch
|
|
import torch.nn.functional as F
|
|
|
|
|
|
def _single_frame(img: torch.Tensor) -> torch.Tensor:
|
|
# ComfyUI IMAGE tensors are usually [B, H, W, C]
|
|
if img.ndim == 4:
|
|
return img[0]
|
|
return img
|
|
|
|
|
|
def _resize_image(img: torch.Tensor, target_h: int, target_w: int) -> torch.Tensor:
|
|
if img.shape[0] == target_h and img.shape[1] == target_w:
|
|
return img
|
|
|
|
x = img.permute(2, 0, 1).unsqueeze(0) # HWC -> NCHW
|
|
x = F.interpolate(x, size=(target_h, target_w), mode="bilinear", align_corners=False)
|
|
return x.squeeze(0).permute(1, 2, 0) # NCHW -> HWC
|
|
|
|
|
|
def _smoothstep01(x: torch.Tensor) -> torch.Tensor:
|
|
x = torch.clamp(x, 0.0, 1.0)
|
|
return x * x * (3.0 - 2.0 * x)
|
|
|
|
|
|
def _soft_mask(score: torch.Tensor, t: float, feather: float = 0.03) -> torch.Tensor:
|
|
"""
|
|
score: H x W tensor in [0,1], lower values reveal earlier.
|
|
returns: H x W x 1 mask in [0,1]
|
|
"""
|
|
feather = max(float(feather), 1e-6)
|
|
x = (t - score + feather) / (2.0 * feather)
|
|
return _smoothstep01(x).unsqueeze(-1)
|
|
|
|
|
|
def _normalize_score(score: torch.Tensor, eps: float = 1e-4) -> torch.Tensor:
|
|
"""
|
|
Normalize score map to [0, 1-eps] so feathered masks finish cleanly.
|
|
This prevents tiny seams on the final frame.
|
|
"""
|
|
score = score - torch.min(score)
|
|
score = score / torch.clamp(torch.max(score), min=1e-6)
|
|
return torch.clamp(score, 0.0, 1.0 - eps)
|
|
|
|
|
|
def _seeded_rand(shape, seed: int, device):
|
|
# Generate on CPU for compatibility, then move to target device
|
|
g = torch.Generator(device="cpu")
|
|
g.manual_seed(int(seed) & 0x7FFFFFFFFFFFFFFF)
|
|
return torch.rand(shape, generator=g, device="cpu").to(device)
|
|
|
|
|
|
def _normalized_grid(h: int, w: int, device):
|
|
ys = torch.linspace(-1.0, 1.0, h, device=device).view(h, 1).expand(h, w)
|
|
xs = torch.linspace(-1.0, 1.0, w, device=device).view(1, w).expand(h, w)
|
|
return xs, ys
|
|
|
|
|
|
def _ease_value(t: float, easing: str) -> float:
|
|
t = max(0.0, min(1.0, float(t)))
|
|
|
|
if easing == "linear":
|
|
return t
|
|
if easing == "ease_in":
|
|
return t * t
|
|
if easing == "ease_out":
|
|
return 1.0 - ((1.0 - t) * (1.0 - t))
|
|
if easing == "ease_in_out":
|
|
if t < 0.5:
|
|
return 2.0 * t * t
|
|
return 1.0 - ((-2.0 * t + 2.0) ** 2) / 2.0
|
|
if easing == "smoothstep":
|
|
return t * t * (3.0 - 2.0 * t)
|
|
|
|
return t
|
|
|
|
|
|
class VSImageCompareDeluxe:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"source_image": ("IMAGE",),
|
|
"generated_image": ("IMAGE",),
|
|
"effect_type": (
|
|
[
|
|
"Fade",
|
|
"Dissolve",
|
|
"Circle Wipe",
|
|
"Diamond Wipe",
|
|
"Star Wipe",
|
|
"Clock Wipe",
|
|
"Door Wipe Open",
|
|
"Door Wipe Close",
|
|
"Iris Open",
|
|
"Iris Close",
|
|
"Heart Wipe",
|
|
"Spiral Wipe",
|
|
"Venetian Blinds",
|
|
"Checkerboard",
|
|
"Falling Blocks",
|
|
"Random Blocks",
|
|
"Pixelate Reveal",
|
|
"Luma Wipe",
|
|
"Push Left",
|
|
"Push Right",
|
|
"Toaster Color Shift",
|
|
],
|
|
{"default": "Fade"},
|
|
),
|
|
"frames": ("INT", {"default": 48, "min": 2, "max": 600, "step": 1}),
|
|
"fps": ("INT", {"default": 12, "min": 1, "max": 120, "step": 1}),
|
|
"loop_count": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}),
|
|
"loop_style": (["restart", "ping_pong"], {"default": "restart"}),
|
|
"hold_frames": ("INT", {"default": 0, "min": 0, "max": 120, "step": 1}),
|
|
"easing": (
|
|
["linear", "ease_in", "ease_out", "ease_in_out", "smoothstep"],
|
|
{"default": "smoothstep"},
|
|
),
|
|
"feather": ("FLOAT", {"default": 0.025, "min": 0.001, "max": 0.25, "step": 0.001}),
|
|
"block_size": ("INT", {"default": 48, "min": 8, "max": 256, "step": 8}),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0x7FFFFFFF, "step": 1}),
|
|
"invert_pattern": ("BOOLEAN", {"default": False}),
|
|
"toaster_tint_strength": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"video_flavor": (
|
|
["none", "scanlines", "vhs_wobble", "crt_soft", "full_cheese"],
|
|
{"default": "none"},
|
|
),
|
|
"video_flavor_strength": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
|
RETURN_NAMES = ("frames_out", "frame_count", "fps")
|
|
FUNCTION = "build_compare"
|
|
CATEGORY = "ComfyUI-VS"
|
|
|
|
def _build_transition_state(self, effect_type: str, h: int, w: int, device, seed: int, block_size: int):
|
|
state = {"effect_type": effect_type}
|
|
xs, ys = _normalized_grid(h, w, device)
|
|
|
|
radius = torch.sqrt(xs * xs + ys * ys)
|
|
radius = _normalize_score(radius)
|
|
|
|
theta = torch.atan2(ys, xs)
|
|
|
|
if effect_type == "Dissolve":
|
|
state["noise"] = _seeded_rand((h, w), seed, device)
|
|
|
|
elif effect_type in ("Circle Wipe", "Iris Open"):
|
|
state["score"] = radius
|
|
|
|
elif effect_type == "Iris Close":
|
|
state["score"] = 1.0 - radius
|
|
|
|
elif effect_type == "Diamond Wipe":
|
|
score = torch.abs(xs) + torch.abs(ys)
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Star Wipe":
|
|
# 5-point-ish star burst
|
|
star_shape = 0.5 + 0.5 * torch.pow(torch.abs(torch.cos(theta * 5.0)), 0.65)
|
|
score = radius / torch.clamp(star_shape, min=1e-6)
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Clock Wipe":
|
|
# Starts at top and moves clockwise
|
|
angle = torch.atan2(xs, -ys)
|
|
angle = torch.remainder(angle + (2.0 * math.pi), 2.0 * math.pi) / (2.0 * math.pi)
|
|
state["score"] = _normalize_score(angle)
|
|
|
|
elif effect_type == "Door Wipe Open":
|
|
# Center reveals first, moving outward.
|
|
score = torch.abs(xs)
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Door Wipe Close":
|
|
# Edges reveal first, finishing with both sides meeting at the center seam.
|
|
score = 1.0 - torch.abs(xs)
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Heart Wipe":
|
|
# Heart-shaped implicit curve. Negative/low values reveal first from the heart center outward.
|
|
# Coordinates adjusted so the heart fits the image.
|
|
x = xs * 1.25
|
|
y = -ys * 1.25 + 0.25
|
|
heart = (x * x + y * y - 1.0) ** 3 - (x * x) * (y ** 3)
|
|
|
|
# Inside heart is <= 0. Convert to a reveal score where heart appears first.
|
|
score = heart
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Spiral Wipe":
|
|
# Spiraling score field.
|
|
angle = torch.remainder(theta + (2.0 * math.pi), 2.0 * math.pi) / (2.0 * math.pi)
|
|
spiral = radius + angle * 0.35
|
|
state["score"] = _normalize_score(spiral)
|
|
|
|
elif effect_type == "Venetian Blinds":
|
|
stripe_h = max(4, block_size // 2)
|
|
row_idx = torch.arange(h, device=device)
|
|
stripe_idx = row_idx // stripe_h
|
|
stripe_dir = (stripe_idx % 2).float().view(h, 1).expand(h, w)
|
|
|
|
x01 = (torch.arange(w, device=device).float() / max(w - 1, 1)).view(1, w).expand(h, w)
|
|
score = torch.where(stripe_dir < 0.5, x01, 1.0 - x01)
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type == "Checkerboard":
|
|
tile = max(8, block_size)
|
|
gy = (torch.arange(h, device=device) // tile).view(h, 1).expand(h, w)
|
|
gx = (torch.arange(w, device=device) // tile).view(1, w).expand(h, w)
|
|
|
|
parity = ((gx + gy) % 2).float()
|
|
|
|
local_x = ((torch.arange(w, device=device) % tile).float() / max(tile - 1, 1)).view(1, w).expand(h, w)
|
|
local_y = ((torch.arange(h, device=device) % tile).float() / max(tile - 1, 1)).view(h, 1).expand(h, w)
|
|
local = (local_x + local_y) * 0.5
|
|
|
|
# Two-phase reveal
|
|
score = parity * 0.5 + local * 0.5
|
|
state["score"] = _normalize_score(score)
|
|
|
|
elif effect_type in ("Falling Blocks", "Random Blocks", "Pixelate Reveal"):
|
|
grid_h = math.ceil(h / block_size)
|
|
grid_w = math.ceil(w / block_size)
|
|
|
|
delays = _seeded_rand((grid_h, grid_w), seed, device) * 0.75
|
|
|
|
if effect_type == "Falling Blocks":
|
|
row_bias = torch.linspace(0.0, 0.25, grid_h, device=device).view(grid_h, 1)
|
|
delays = torch.clamp(delays * 0.65 + row_bias, 0.0, 0.95)
|
|
|
|
by = torch.arange(h, device=device) // block_size
|
|
bx = torch.arange(w, device=device) // block_size
|
|
delay_map = delays[by[:, None], bx[None, :]]
|
|
|
|
local_y = ((torch.arange(h, device=device) % block_size).float() / max(block_size - 1, 1)).view(h, 1).expand(h, w)
|
|
local_x = ((torch.arange(w, device=device) % block_size).float() / max(block_size - 1, 1)).view(1, w).expand(h, w)
|
|
|
|
detail_map = (local_y * 0.14) + (local_x * 0.06)
|
|
|
|
state["delay_map"] = delay_map
|
|
state["detail_map"] = detail_map
|
|
|
|
elif effect_type == "Luma Wipe":
|
|
# Luma based on generated image is built later because it needs dst.
|
|
pass
|
|
|
|
return state
|
|
|
|
def _blend_from_mask(self, src: torch.Tensor, dst: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
return torch.clamp(src * (1.0 - mask) + dst * mask, 0.0, 1.0)
|
|
|
|
def _push_frame(self, src: torch.Tensor, dst: torch.Tensor, t: float, direction: str) -> torch.Tensor:
|
|
h, w, c = src.shape
|
|
out = torch.zeros_like(src)
|
|
|
|
offset = int(round(w * t))
|
|
offset = max(0, min(w, offset))
|
|
|
|
if direction == "left":
|
|
# src moves left out, dst comes in from right
|
|
if offset < w:
|
|
out[:, : w - offset, :] = src[:, offset:, :]
|
|
if offset > 0:
|
|
out[:, w - offset :, :] = dst[:, :offset, :]
|
|
|
|
elif direction == "right":
|
|
# src moves right out, dst comes in from left
|
|
if offset < w:
|
|
out[:, offset:, :] = src[:, : w - offset, :]
|
|
if offset > 0:
|
|
out[:, :offset, :] = dst[:, w - offset :, :]
|
|
|
|
return out.clamp(0.0, 1.0)
|
|
|
|
def _toaster_color_shift(self, src: torch.Tensor, dst: torch.Tensor, t: float, tint_strength: float) -> torch.Tensor:
|
|
# Slight retro-blue/purple-ish broadcast cheese.
|
|
s = max(0.0, min(1.0, float(tint_strength)))
|
|
tint = torch.tensor(
|
|
[1.0 + 0.15 * s, 1.0 + 0.05 * s, 1.0 + 0.45 * s],
|
|
device=src.device,
|
|
dtype=src.dtype,
|
|
).view(1, 1, 3)
|
|
|
|
tinted_dst = torch.clamp(dst * tint, 0.0, 1.0)
|
|
|
|
# Subtle scanline-style modulation.
|
|
h, w, _ = src.shape
|
|
y = torch.arange(h, device=src.device, dtype=src.dtype).view(h, 1, 1)
|
|
scan = 1.0 - 0.03 * s * ((y % 2.0) == 0).to(src.dtype)
|
|
tinted_dst = torch.clamp(tinted_dst * scan, 0.0, 1.0)
|
|
|
|
return torch.clamp(src * (1.0 - t) + tinted_dst * t, 0.0, 1.0)
|
|
|
|
def _pixelate_reveal(
|
|
self,
|
|
src: torch.Tensor,
|
|
dst: torch.Tensor,
|
|
t: float,
|
|
state: dict,
|
|
block_size: int,
|
|
feather: float,
|
|
invert_pattern: bool,
|
|
) -> torch.Tensor:
|
|
h, w, c = src.shape
|
|
device = src.device
|
|
|
|
delay_map = state["delay_map"]
|
|
detail_map = state["detail_map"]
|
|
score = torch.clamp(delay_map + detail_map, 0.0, 0.999)
|
|
|
|
if invert_pattern:
|
|
score = 1.0 - score
|
|
|
|
mask = _soft_mask(score, t, feather=max(feather, 0.01))
|
|
|
|
# Pixelated generated image. As t increases, pixel blocks get smaller.
|
|
max_px = max(2, int(block_size))
|
|
min_px = 1
|
|
px = int(round(max_px * (1.0 - t) + min_px * t))
|
|
px = max(1, px)
|
|
|
|
small_h = max(1, h // px)
|
|
small_w = max(1, w // px)
|
|
|
|
x = dst.permute(2, 0, 1).unsqueeze(0)
|
|
small = F.interpolate(x, size=(small_h, small_w), mode="nearest")
|
|
pix = F.interpolate(small, size=(h, w), mode="nearest")
|
|
pix = pix.squeeze(0).permute(1, 2, 0)
|
|
|
|
# Blend from source into pixelated destination, which sharpens by the end.
|
|
return self._blend_from_mask(src, pix, mask)
|
|
|
|
def _video_flavor(
|
|
self,
|
|
frame: torch.Tensor,
|
|
frame_index: int,
|
|
total_frames: int,
|
|
flavor: str,
|
|
strength: float,
|
|
seed: int,
|
|
) -> torch.Tensor:
|
|
if flavor == "none" or strength <= 0.0:
|
|
return frame
|
|
|
|
s = max(0.0, min(1.0, float(strength)))
|
|
h, w, c = frame.shape
|
|
device = frame.device
|
|
out = frame
|
|
|
|
# Common scanline modulation.
|
|
if flavor in ("scanlines", "crt_soft", "full_cheese"):
|
|
y = torch.arange(h, device=device, dtype=frame.dtype).view(h, 1, 1)
|
|
scan = 1.0 - (0.04 * s) * ((y % 2.0) == 0).to(frame.dtype)
|
|
out = torch.clamp(out * scan, 0.0, 1.0)
|
|
|
|
# VHS-ish horizontal wobble.
|
|
if flavor in ("vhs_wobble", "full_cheese"):
|
|
rows = torch.arange(h, device=device, dtype=frame.dtype)
|
|
phase = (frame_index / max(total_frames - 1, 1)) * 2.0 * math.pi
|
|
wobble = torch.sin(rows * 0.075 + phase * 3.0) * (2.5 * s)
|
|
wobble = wobble.round().to(torch.int64)
|
|
|
|
shifted = torch.empty_like(out)
|
|
for yy in range(h):
|
|
shifted[yy] = torch.roll(out[yy], shifts=int(wobble[yy].item()), dims=0)
|
|
out = shifted
|
|
|
|
# CRT softening / mild bloom-ish blur.
|
|
if flavor in ("crt_soft", "full_cheese"):
|
|
x = out.permute(2, 0, 1).unsqueeze(0)
|
|
blurred = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
|
|
blurred = blurred.squeeze(0).permute(1, 2, 0)
|
|
out = torch.clamp(out * (1.0 - 0.25 * s) + blurred * (0.25 * s), 0.0, 1.0)
|
|
|
|
# Tiny noise and chroma-ish shift for full cheese.
|
|
if flavor == "full_cheese":
|
|
noise = _seeded_rand((h, w, 1), seed + frame_index * 7919, device) - 0.5
|
|
out = torch.clamp(out + noise * (0.035 * s), 0.0, 1.0)
|
|
|
|
# Slight red/blue channel horizontal split.
|
|
shift = max(1, int(round(2 * s)))
|
|
r = torch.roll(out[:, :, 0], shifts=shift, dims=1)
|
|
g = out[:, :, 1]
|
|
b = torch.roll(out[:, :, 2], shifts=-shift, dims=1)
|
|
out = torch.stack([r, g, b], dim=2).clamp(0.0, 1.0)
|
|
|
|
return out.clamp(0.0, 1.0)
|
|
|
|
def _mask_for_effect(
|
|
self,
|
|
t: float,
|
|
effect_type: str,
|
|
state: dict,
|
|
src: torch.Tensor,
|
|
dst: torch.Tensor,
|
|
h: int,
|
|
w: int,
|
|
device,
|
|
feather: float,
|
|
invert_pattern: bool,
|
|
) -> torch.Tensor:
|
|
if effect_type == "Fade":
|
|
return torch.full((h, w, 1), float(t), device=device)
|
|
|
|
if effect_type == "Dissolve":
|
|
score = state["noise"]
|
|
if invert_pattern:
|
|
score = 1.0 - score
|
|
return _soft_mask(score, t, feather=max(feather, 0.01))
|
|
|
|
if effect_type == "Luma Wipe":
|
|
# Use generated image brightness as the transition map.
|
|
# Dark areas reveal earlier; invert_pattern flips that.
|
|
luma = dst[:, :, 0] * 0.2126 + dst[:, :, 1] * 0.7152 + dst[:, :, 2] * 0.0722
|
|
score = _normalize_score(luma)
|
|
if invert_pattern:
|
|
score = 1.0 - score
|
|
return _soft_mask(score, t, feather=max(feather, 0.01))
|
|
|
|
if effect_type in (
|
|
"Circle Wipe",
|
|
"Diamond Wipe",
|
|
"Star Wipe",
|
|
"Clock Wipe",
|
|
"Door Wipe Open",
|
|
"Door Wipe Close",
|
|
"Iris Open",
|
|
"Iris Close",
|
|
"Heart Wipe",
|
|
"Spiral Wipe",
|
|
"Venetian Blinds",
|
|
"Checkerboard",
|
|
):
|
|
score = state["score"]
|
|
if invert_pattern:
|
|
score = 1.0 - score
|
|
return _soft_mask(score, t, feather=feather)
|
|
|
|
if effect_type in ("Falling Blocks", "Random Blocks"):
|
|
delay_map = state["delay_map"]
|
|
detail_map = state["detail_map"]
|
|
score = torch.clamp(delay_map + detail_map, 0.0, 0.999)
|
|
if invert_pattern:
|
|
score = 1.0 - score
|
|
return _soft_mask(score, t, feather=max(feather, 0.01))
|
|
|
|
return torch.full((h, w, 1), float(t), device=device)
|
|
|
|
def _render_frame(
|
|
self,
|
|
src: torch.Tensor,
|
|
dst: torch.Tensor,
|
|
effect_type: str,
|
|
t: float,
|
|
state: dict,
|
|
feather: float,
|
|
block_size: int,
|
|
invert_pattern: bool,
|
|
toaster_tint_strength: float,
|
|
) -> torch.Tensor:
|
|
h, w, _ = src.shape
|
|
device = src.device
|
|
|
|
if effect_type == "Push Left":
|
|
return self._push_frame(src, dst, t, direction="left")
|
|
|
|
if effect_type == "Push Right":
|
|
return self._push_frame(src, dst, t, direction="right")
|
|
|
|
if effect_type == "Toaster Color Shift":
|
|
return self._toaster_color_shift(src, dst, t, toaster_tint_strength)
|
|
|
|
if effect_type == "Pixelate Reveal":
|
|
return self._pixelate_reveal(
|
|
src=src,
|
|
dst=dst,
|
|
t=t,
|
|
state=state,
|
|
block_size=block_size,
|
|
feather=feather,
|
|
invert_pattern=invert_pattern,
|
|
)
|
|
|
|
mask = self._mask_for_effect(
|
|
t=t,
|
|
effect_type=effect_type,
|
|
state=state,
|
|
src=src,
|
|
dst=dst,
|
|
h=h,
|
|
w=w,
|
|
device=device,
|
|
feather=feather,
|
|
invert_pattern=invert_pattern,
|
|
)
|
|
return self._blend_from_mask(src, dst, mask)
|
|
|
|
def build_compare(
|
|
self,
|
|
source_image,
|
|
generated_image,
|
|
effect_type,
|
|
frames,
|
|
fps,
|
|
loop_count,
|
|
loop_style,
|
|
hold_frames,
|
|
easing,
|
|
feather,
|
|
block_size,
|
|
seed,
|
|
invert_pattern,
|
|
toaster_tint_strength,
|
|
video_flavor,
|
|
video_flavor_strength,
|
|
):
|
|
src = _single_frame(source_image).to(torch.float32).clamp(0.0, 1.0)
|
|
dst = _single_frame(generated_image).to(torch.float32).clamp(0.0, 1.0)
|
|
|
|
# Match generated image to source image size.
|
|
target_h, target_w = src.shape[0], src.shape[1]
|
|
dst = _resize_image(dst, target_h, target_w).to(src.device)
|
|
|
|
h, w, _ = src.shape
|
|
device = src.device
|
|
|
|
state = self._build_transition_state(
|
|
effect_type=effect_type,
|
|
h=h,
|
|
w=w,
|
|
device=device,
|
|
seed=seed,
|
|
block_size=block_size,
|
|
)
|
|
|
|
cycle_frames = []
|
|
for i in range(frames):
|
|
raw_t = i / max(frames - 1, 1)
|
|
t = _ease_value(raw_t, easing)
|
|
|
|
frame = self._render_frame(
|
|
src=src,
|
|
dst=dst,
|
|
effect_type=effect_type,
|
|
t=t,
|
|
state=state,
|
|
feather=feather,
|
|
block_size=block_size,
|
|
invert_pattern=bool(invert_pattern),
|
|
toaster_tint_strength=toaster_tint_strength,
|
|
)
|
|
cycle_frames.append(frame)
|
|
|
|
output_frames = []
|
|
|
|
for _ in range(loop_count):
|
|
output_frames.extend(cycle_frames)
|
|
|
|
for _hf in range(hold_frames):
|
|
output_frames.append(dst.clone())
|
|
|
|
if loop_style == "ping_pong" and len(cycle_frames) > 2:
|
|
reverse_frames = list(reversed(cycle_frames[1:-1]))
|
|
output_frames.extend(reverse_frames)
|
|
|
|
for _hf in range(hold_frames):
|
|
output_frames.append(src.clone())
|
|
|
|
flavored_frames = []
|
|
total = len(output_frames)
|
|
for idx, frame in enumerate(output_frames):
|
|
flavored = self._video_flavor(
|
|
frame=frame,
|
|
frame_index=idx,
|
|
total_frames=total,
|
|
flavor=video_flavor,
|
|
strength=video_flavor_strength,
|
|
seed=seed,
|
|
)
|
|
flavored_frames.append(flavored)
|
|
|
|
batch = torch.stack(flavored_frames, dim=0)
|
|
return (batch, int(batch.shape[0]), int(fps))
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"VS Image Compare Deluxe": VSImageCompareDeluxe,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"VS Image Compare Deluxe": "VS Image Compare Deluxe",
|
|
} |