Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated with flex feature convert nodes, as well as fade between batc… #19

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,40 @@
from .src.ak_scheduled_binary_comparison import AK_ScheduledBinaryComparison
from .src.ak_brightness_to_float_list import AK_BrightnessToFloatList
from .src.ak_float_list_to_dilate_mask_schedule import AK_FloatListToDilateMaskSchedule
from .src.ak_fade_between_batches import AK_FadeBetweenBatches
from .src.ak_split_image_batch import AK_SplitImageBatch
from .src.ak_convert_flex_feature_to_float_list import AK_FlexFeatureToFloatList
from .src.ak_convert_float_list_to_flex_feature import AK_FloatListToFlexFeature

NAME_POSTFIX = " | Akatz"

NODE_CONFIG = {
"AK_AnimatedDilationMaskLinear": {"class": AK_AnimatedDilationMaskLinear, "name": "AK Dilate Mask Linear"},
"AK_IPAdapterCustomWeights": {"class": AK_IPAdapterCustomWeights, "name": "AK IPAdapter Custom Weights"},
"AK_NormalizeMaskImage": {"class": AK_NormalizeImageColor, "name": "AK Normalize Image Color"},
"AK_AudioreactiveDilationMask": {"class": AK_AudioreactiveDilationMask, "name": "AK Audioreactive Dilate Mask"},
"AK_AudioreactiveDynamicDilationMask": {"class": AK_AudioreactiveDynamicDilationMask, "name": "AK Audioreactive Dynamic Dilate Mask"},
"AK_ConvertAudioToSaltAudio": {"class": AK_ConvertAudioToSaltAudio, "name": "AK Convert Audio To Salt Audio"},
"AK_ConvertSaltAudioToAudio": {"class": AK_ConvertSaltAudioToAudio, "name": "AK Convert Salt Audio To Audio"},
"AK_RescaleFloatList": {"class": AK_RescaleFloatList, "name": "AK Rescale Float List"},
"AK_ListToNumpyFloatArray": {"class": AK_ListToNumpyFloatArray, "name": "AK List To Numpy Float Array"},
"AK_LagChop": {"class": AK_LagChop, "name": "AK Lag Chop"},
"AK_BinaryAmplitudeGate": {"class": AK_BinaryAmplitudeGate, "name": "AK Binary Amplitude Gate"},
"AK_AdjustListSize": {"class": AK_AdjustListSize, "name": "AK Adjust List Size"},
"AK_VideoSpeedAdjust": {"class": AK_VideoSpeedAdjust, "name": "AK Video Speed Adjust"},
"AK_ConvertListToFloatList": {"class": AK_ConvertListToFloatList, "name": "AK Convert List To Float List"},
"AK_ShrinkNumSequence": {"class": AK_ShrinkNumSequence, "name": "AK Shrink Num Sequence"},
"AK_DilateMaskLinearInfinite": {"class": AK_DilateMaskLinearInfinite, "name": "AK Dilate Mask Linear Infinite"},
"AK_AudioFramesyncSchedule": {"class": AK_AudioFramesyncSchedule, "name": "AK Schedule Audio Framesync"},
"AK_AudioreactiveDilateMaskInfinite": {"class": AK_AudioreactiveDilateMaskInfinite, "name": "AK Audioreactive Dilate Mask Infinite"},
"AK_KeyframeScheduler": {"class": AK_KeyframeScheduler, "name": "AK Keyframe Scheduler"},
"AK_ScheduledBinaryComparison": {"class": AK_ScheduledBinaryComparison, "name": "AK Scheduled Binary Comparison"},
"AK_BrightnessToFloatList": {"class": AK_BrightnessToFloatList, "name": "AK Brightness To Float List"},
"AK_FloatListToDilateMaskSchedule": {"class": AK_FloatListToDilateMaskSchedule, "name": "AK Float List To Dilate Mask Schedule"},
"AK_AnimatedDilationMaskLinear": {"class": AK_AnimatedDilationMaskLinear, "name": "Dilate Mask Linear"},
"AK_IPAdapterCustomWeights": {"class": AK_IPAdapterCustomWeights, "name": "IPAdapter Custom Weights"},
"AK_NormalizeMaskImage": {"class": AK_NormalizeImageColor, "name": "Normalize Image Color"},
"AK_AudioreactiveDilationMask": {"class": AK_AudioreactiveDilationMask, "name": "Audioreactive Dilate Mask"},
"AK_AudioreactiveDynamicDilationMask": {"class": AK_AudioreactiveDynamicDilationMask, "name": "Audioreactive Dynamic Dilate Mask"},
"AK_ConvertAudioToSaltAudio": {"class": AK_ConvertAudioToSaltAudio, "name": "Convert Audio To Salt Audio"},
"AK_ConvertSaltAudioToAudio": {"class": AK_ConvertSaltAudioToAudio, "name": "Convert Salt Audio To Audio"},
"AK_RescaleFloatList": {"class": AK_RescaleFloatList, "name": "Rescale Float List"},
"AK_ListToNumpyFloatArray": {"class": AK_ListToNumpyFloatArray, "name": "List To Numpy Float Array"},
"AK_LagChop": {"class": AK_LagChop, "name": "Lag Chop"},
"AK_BinaryAmplitudeGate": {"class": AK_BinaryAmplitudeGate, "name": "Binary Amplitude Gate"},
"AK_AdjustListSize": {"class": AK_AdjustListSize, "name": "Adjust List Size"},
"AK_VideoSpeedAdjust": {"class": AK_VideoSpeedAdjust, "name": "Video Speed Adjust"},
"AK_ConvertListToFloatList": {"class": AK_ConvertListToFloatList, "name": "Convert List To Float List"},
"AK_ShrinkNumSequence": {"class": AK_ShrinkNumSequence, "name": "Shrink Num Sequence"},
"AK_DilateMaskLinearInfinite": {"class": AK_DilateMaskLinearInfinite, "name": "Dilate Mask Linear Infinite"},
"AK_AudioFramesyncSchedule": {"class": AK_AudioFramesyncSchedule, "name": "Schedule Audio Framesync"},
"AK_AudioreactiveDilateMaskInfinite": {"class": AK_AudioreactiveDilateMaskInfinite, "name": "Audioreactive Dilate Mask Infinite"},
"AK_KeyframeScheduler": {"class": AK_KeyframeScheduler, "name": "Keyframe Scheduler"},
"AK_ScheduledBinaryComparison": {"class": AK_ScheduledBinaryComparison, "name": "Scheduled Binary Comparison"},
"AK_BrightnessToFloatList": {"class": AK_BrightnessToFloatList, "name": "Brightness To Float List"},
"AK_FloatListToDilateMaskSchedule": {"class": AK_FloatListToDilateMaskSchedule, "name": "Float List To Dilate Mask Schedule"},
"AK_FadeBetweenBatches": {"class": AK_FadeBetweenBatches, "name": "Fade Between Batches"},
"AK_SplitImageBatch": {"class": AK_SplitImageBatch, "name": "Split Image Batch"},
"AK_FlexFeatureToFloatList": {"class": AK_FlexFeatureToFloatList, "name": "Flex Feature To Float List"},
"AK_FloatListToFlexFeature": {"class": AK_FloatListToFlexFeature, "name": "Float List To Flex Feature"},
}

def generate_node_mappings(node_config):
Expand All @@ -60,7 +70,7 @@ def generate_node_mappings(node_config):

for node_name, node_info in node_config.items():
node_class_mappings[node_name] = node_info["class"]
node_display_name_mappings[node_name] = node_info.get("name", node_info["class"].__name__)
node_display_name_mappings[node_name] = node_info.get("name", node_info["class"].__name__) + NAME_POSTFIX

return node_class_mappings, node_display_name_mappings

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "comfyui-akatz-nodes"
description = "Simple custom node pack for nodes I use in my workflows. Includes Dilate Mask Linear for animating masks."
version = "1.7.0"
version = "1.8.0"
license = {file = "LICENSE"}
dependencies = ["numpy", "torch", "opencv-python", "pydub"]

Expand Down
48 changes: 48 additions & 0 deletions src/ak_convert_flex_feature_to_float_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

class AK_FlexFeatureToFloatList:
def __init__(self):
pass

@classmethod
def INPUT_TYPES(s):
return {
"required": {
"feature": ("FEATURE", {"forceInput": True}), # 'forceInput' used for custom types like FEATURE
},
}

RETURN_TYPES = ("FLOAT",) # Define the output as a list of floats
FUNCTION = "convert_feature_to_float_list"
CATEGORY = "💜Akatz Nodes/Utils"

DESCRIPTION = """
AK_FlexFeatureToFloatList:
This node converts a FEATURE type input into a list of float values, one per frame.

- feature: A custom Feature object with attributes:
- feature.get_value_at_frame(i): Method that returns a float value for frame `i`.
- feature.frame_count: The total number of frames in the feature.
"""

def convert_feature_to_float_list(self, feature):
"""
Convert the values from a Feature object to a list of floats.

Args:
- feature: The Feature object which contains frame values.

Returns:
- tuple: A tuple containing a list of floats, where each float is the value
extracted from the feature for each frame.
"""
# Initialize an empty list to store float values
float_list = []

# Iterate over each frame in the feature and extract the normalized value
for i in range(feature.frame_count):
value = feature.get_value_at_frame(i)
float_list.append(value)

# Return the list as a tuple (since ComfyUI expects tuples)
return (float_list,)

52 changes: 52 additions & 0 deletions src/ak_convert_float_list_to_flex_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import numpy as np

class AK_FloatListToFlexFeature:
def __init__(self):
pass

@classmethod
def INPUT_TYPES(s):
return {
"required": {
"float_list": ("FLOAT", {"forceInput": True}), # 'forceInput' used for custom types like FEATURE
"original_feature": ("FEATURE", {"forceInput": True}), # The original FEATURE object to combine with the float list for the output
},
}

RETURN_TYPES = ("FEATURE",) # Define the output as a FEATURE object
FUNCTION = "convert_float_list_to_feature"
CATEGORY = "💜Akatz Nodes/Utils"

DESCRIPTION = """
AK_FloatListToFlexFeature:
This node converts a list of float values into a FEATURE type input.

- original_feature: A custom Feature object with attributes:
- original_feature.get_value_at_frame(i): Method that returns a float value for frame `i`.
- original_feature.frame_count: The total number of frames in the feature.
"""

def convert_float_list_to_feature(self, float_list, original_feature):
"""
Convert the values from a list of floats to a FEATURE object.

Args:
- float_list: A list of floats.
- original_feature: The Feature object which contains frame values.

Returns:
- original_feature: A FEATURE object with the same frame values as the float list.
"""

# Convert the float list to a numpy array
array = np.array(float_list)

# Get frame count from the number of elements in the array
frame_count = array.shape[0]

original_feature.frame_count = frame_count
original_feature.data = array

# Return the feature as a tuple (since ComfyUI expects tuples)
return (original_feature,)

75 changes: 75 additions & 0 deletions src/ak_fade_between_batches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import torch
import torch.nn.functional as F

class AK_FadeBetweenBatches:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE",), # First image batch
"image2": ("IMAGE",), # Second image batch
"overlap_frames": ("INT", {"default": 10, "min": 1}), # Number of frames to overlap/fade
}
}

RETURN_TYPES = ("IMAGE",)
FUNCTION = "fade_batches"
CATEGORY = "💜Akatz Nodes/Utils"
DESCRIPTION = """
# AK Fade Between Batches
This node takes two image batches and blends them together by transitioning
with overlapping frames. The output is a single image batch where:
- image1 starts,
- a number of overlap frames fade between image1 and image2,
- image2 finishes.
"""

def fade_batches(self, image1, image2, overlap_frames):
# Check if image1 or image2 is None or empty
if image1 is None or image1.numel() == 0:
if image2 is None or image2.numel() == 0:
raise ValueError("Both image1 and image2 are None or empty.")
return (image2,)

if image2 is None or image2.numel() == 0:
return (image1,)

# Ensure image2 has the same height, width, and channels as image1
if image1.shape[1:] != image2.shape[1:]:
# Resize image2 to match image1 dimensions (using bilinear interpolation)
image2 = F.interpolate(image2.permute(0, 3, 1, 2), size=image1.shape[1:3], mode='bilinear', align_corners=False)
image2 = image2.permute(0, 2, 3, 1) # Re-permute to get back to [B, H, W, C]

batch_size1 = image1.shape[0]
batch_size2 = image2.shape[0]

# Calculate the number of frames outside of the overlap
non_overlap1 = batch_size1 - overlap_frames
non_overlap2 = batch_size2 - overlap_frames

if non_overlap1 < 0 or non_overlap2 < 0:
raise ValueError("Overlap frames exceed the batch sizes of the input images.")

# Create the transition frames by fading between image1 and image2
transition_frames = []
for i in range(overlap_frames):
alpha_step = 1 / ((overlap_frames + 2) - 1) # Alpha will go from 0 to 1
alpha = alpha_step * (i + 1)
fade_frame = (1 - alpha) * image1[non_overlap1 + i] + alpha * image2[i]
transition_frames.append(fade_frame)

# Stack the frames into a single tensor (batch)
result_batch = torch.cat(
(image1[:non_overlap1], # First part from image1
torch.stack(transition_frames), # Transition frames
image2[overlap_frames:]), # Remaining frames from image2
dim=0
)
return (result_batch,)

# image1 = inputs['0_image']
# image2 = inputs['1_image']
# overlap_frames = inputs['2_int']

# instance = AK_FadeBetweenBatches()
# outputs[0] = instance.fade_batches(image1, image2, overlap_frames)[0]
40 changes: 40 additions & 0 deletions src/ak_split_image_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import torch

class AK_SplitImageBatch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_batch": ("IMAGE",), # The input image batch to be split
"split_index": ("INT", {"default": 1, "min": 1}), # The index at which to split the batch
"split_batch_index": ("INT", {"default": 0, "min": 0, "max": 1}), # Whether to return the first or second batch
}
}

RETURN_TYPES = ("IMAGE",)
FUNCTION = "split_image_batch"
CATEGORY = "💜Akatz Nodes/Utils"
DESCRIPTION = """
# AK Split Image Batch
This node splits an input image batch into two at a given index, and returns one of the partitions.
- split_index: The index at which to split the image batch.
- split_batch_index: 0 to return the first partition, 1 to return the second partition.
"""

def split_image_batch(self, image_batch, split_index, split_batch_index):
# Validate the split index to be within range
batch_size = image_batch.shape[0]
if split_index < 1 or split_index >= batch_size:
raise ValueError(f"split_index must be between 1 and {batch_size - 1}.")

# Split the image batch into two parts
batch1 = image_batch[:split_index] # First part up to split_index
batch2 = image_batch[split_index:] # Second part from split_index to the end

# Select which batch to return based on split_batch_index (0 or 1)
if split_batch_index == 0:
return (batch1,)
elif split_batch_index == 1:
return (batch2,)
else:
raise ValueError("split_batch_index must be either 0 or 1.")