draft accumulator

This commit is contained in:
RyanOnTheInside
2025-04-28 19:10:22 -04:00
parent 4932f62522
commit 5703bb78ca
2 changed files with 438 additions and 0 deletions
@@ -0,0 +1,436 @@
import torch
import numpy as np
from ....src.realtimenodes.control_base import ControlNodeBase
from ....src.utils.general import AlwaysEqualProxy
class BaseAccumulatorNode(ControlNodeBase):
"""
Base class for accumulator nodes that collect inputs over multiple workflow runs.
Subclasses should override the process_input and get_output methods to handle specific data types.
"""
CATEGORY = "Realtime Nodes/control/accumulators"
FUNCTION = "update"
DESCRIPTION = "Base class for accumulators that collect inputs over multiple workflow runs"
# These should be overridden by subclasses
NODE_TYPE_NAME = "Base"
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
inputs["required"].update({
# Input type will be defined by subclasses
"batch_size": ("INT", {
"default": 5,
"min": 1,
"max": 1000,
"step": 1,
"tooltip": "Number of inputs to accumulate before outputting"
}),
"pad_incomplete_batch": ("BOOLEAN", {
"default": False,
"tooltip": "If True, duplicate values to reach batch_size when not enough values are accumulated"
}),
"reset_after_batch": ("BOOLEAN", {
"default": False,
"tooltip": "If True, clears accumulated values after outputting a complete batch"
}),
"add_to_batch": ("BOOLEAN", {
"default": True,
"tooltip": "If False, the current input will not be added to the batch"
}),
"reset_batch": ("BOOLEAN", {
"default": False,
"tooltip": "If True, clears all accumulated values"
})
})
return inputs
class AnyAccumulatorNode(BaseAccumulatorNode):
"""
Node that accumulates inputs of any type over multiple workflow runs.
"""
RETURN_TYPES = (AlwaysEqualProxy("*"),)
RETURN_NAMES = ("accumulated_values",)
NODE_TYPE_NAME = "Any"
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
inputs["required"]["input_value"] = (AlwaysEqualProxy("*"), {"tooltip": "Any value to accumulate"})
return inputs
def update(self, input_value, batch_size, pad_incomplete_batch,
reset_after_batch, add_to_batch, reset_batch, always_execute=True, unique_id=None):
"""
For general objects, we still use a list-based approach
"""
# Use the unique_id directly for state management
state_key = f"__accumulator_{self.NODE_TYPE_NAME}__"
# Get current state or initialize empty
state = self.get_state({state_key: []}, unique_id)
accumulated = state[state_key]
# Handle reset if requested
if reset_batch:
accumulated = []
# Add current input to batch if requested
elif add_to_batch:
# When batch is already full, remove oldest item first (FIFO queue)
if len(accumulated) >= batch_size:
accumulated.pop(0) # Remove oldest item
# Add new item
accumulated.append(input_value)
# Check if batch is complete
batch_complete = len(accumulated) >= batch_size
# Process accumulated values for output
if batch_complete:
# Only take the requested batch size
output_values = accumulated[:batch_size]
elif pad_incomplete_batch and accumulated:
# Pad to reach batch_size by duplicating elements
output_values = []
idx = 0
while len(output_values) < batch_size:
output_values.append(accumulated[idx % len(accumulated)])
idx += 1
else:
# Just use whatever we have
output_values = accumulated.copy()
# Reset if batch is complete and reset_after_batch is True
if batch_complete and reset_after_batch:
accumulated = []
# Update state
state[state_key] = accumulated
self.set_state(state, unique_id)
return (output_values,)
class ImageAccumulatorNode(BaseAccumulatorNode):
"""
Node that accumulates image inputs (BHWC format) over multiple workflow runs.
"""
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image_batch",)
NODE_TYPE_NAME = "Image"
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
inputs["required"]["input_value"] = ("IMAGE", {"tooltip": "Image to accumulate (BHWC format)"})
return inputs
def update(self, input_value, batch_size, pad_incomplete_batch,
reset_after_batch, add_to_batch, reset_batch, always_execute=True, unique_id=None):
"""
Directly batch tensors instead of storing a list
"""
# Use the unique_id directly for state management
state_key = f"__accumulator_{self.NODE_TYPE_NAME}__"
# Get current state or initialize empty tensor
state = self.get_state({
state_key: None,
f"{state_key}_frames": []
}, unique_id)
# For tensor types, we keep both the batched tensor and a list of individual frames
# This allows us to easily implement queue-like behavior
accumulated_tensor = state[state_key]
frames = state[f"{state_key}_frames"]
# Add batch dimension if needed (HWC -> BHWC)
if len(input_value.shape) == 3:
current_input = input_value.unsqueeze(0)
else:
current_input = input_value
# Handle reset if requested
if reset_batch:
accumulated_tensor = None
frames = []
# Auto-reset on size mismatch
elif add_to_batch and frames and current_input.shape[1:] != frames[0].shape[1:]:
print(f"[ImageAccumulatorNode] Size mismatch, resetting batch: {frames[0].shape[1:]} vs {current_input.shape[1:]}")
accumulated_tensor = None
frames = []
# Add current input to batch if requested
if add_to_batch:
# When batch is already full, remove oldest frame (FIFO queue)
if len(frames) >= batch_size:
frames.pop(0) # Remove oldest frame
# Add new frame(s)
# Handle multi-batch inputs by adding them one by one
for i in range(current_input.shape[0]):
frame = current_input[i:i+1] # Get single frame with batch dimension
frames.append(frame)
# Rebuild accumulated tensor from frames
if frames:
accumulated_tensor = torch.cat(frames, dim=0)
else:
accumulated_tensor = None
# Prepare output
batch_complete = accumulated_tensor is not None and accumulated_tensor.shape[0] >= batch_size
if batch_complete:
# Only take the requested batch size
output_tensor = accumulated_tensor[:batch_size]
elif pad_incomplete_batch and accumulated_tensor is not None and accumulated_tensor.shape[0] > 0:
# Pad to reach batch_size by duplicating elements
tensors_to_cat = [accumulated_tensor]
remaining = batch_size - accumulated_tensor.shape[0]
while remaining > 0:
# Add a slice of the tensor that's not bigger than what we need
slice_size = min(accumulated_tensor.shape[0], remaining)
tensors_to_cat.append(accumulated_tensor[:slice_size])
remaining -= slice_size
# Concatenate all tensors
output_tensor = torch.cat(tensors_to_cat, dim=0)
else:
# Return what we have
output_tensor = accumulated_tensor if accumulated_tensor is not None else torch.zeros((0, 0, 0, 3))
# Reset if batch is complete and reset_after_batch is True
if batch_complete and reset_after_batch:
accumulated_tensor = None
frames = []
# Update state
state[state_key] = accumulated_tensor
state[f"{state_key}_frames"] = frames
self.set_state(state, unique_id)
return (output_tensor,)
class MaskAccumulatorNode(BaseAccumulatorNode):
"""
Node that accumulates mask inputs over multiple workflow runs.
"""
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask_batch",)
NODE_TYPE_NAME = "Mask"
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
inputs["required"]["input_value"] = ("MASK", {"tooltip": "Mask to accumulate"})
return inputs
def update(self, input_value, batch_size, pad_incomplete_batch,
reset_after_batch, add_to_batch, reset_batch, always_execute=True, unique_id=None):
"""
Directly batch tensors instead of storing a list
"""
# Use the unique_id directly for state management
state_key = f"__accumulator_{self.NODE_TYPE_NAME}__"
# Get current state or initialize empty tensor
state = self.get_state({
state_key: None,
f"{state_key}_frames": []
}, unique_id)
# For tensor types, we keep both the batched tensor and a list of individual frames
accumulated_tensor = state[state_key]
frames = state[f"{state_key}_frames"]
# Add batch dimension if needed (HW -> BHW)
if len(input_value.shape) == 2:
current_input = input_value.unsqueeze(0)
else:
current_input = input_value
# Handle reset if requested
if reset_batch:
accumulated_tensor = None
frames = []
# Auto-reset on size mismatch
elif add_to_batch and frames and current_input.shape[1:] != frames[0].shape[1:]:
print(f"[MaskAccumulatorNode] Size mismatch, resetting batch: {frames[0].shape[1:]} vs {current_input.shape[1:]}")
accumulated_tensor = None
frames = []
# Add current input to batch if requested
if add_to_batch:
# When batch is already full, remove oldest frame (FIFO queue)
if len(frames) >= batch_size:
frames.pop(0) # Remove oldest frame
# Add new frame(s)
# Handle multi-batch inputs by adding them one by one
for i in range(current_input.shape[0]):
frame = current_input[i:i+1] # Get single frame with batch dimension
frames.append(frame)
# Rebuild accumulated tensor from frames
if frames:
accumulated_tensor = torch.cat(frames, dim=0)
else:
accumulated_tensor = None
# Prepare output
batch_complete = accumulated_tensor is not None and accumulated_tensor.shape[0] >= batch_size
if batch_complete:
# Only take the requested batch size
output_tensor = accumulated_tensor[:batch_size]
elif pad_incomplete_batch and accumulated_tensor is not None and accumulated_tensor.shape[0] > 0:
# Pad to reach batch_size by duplicating elements
tensors_to_cat = [accumulated_tensor]
remaining = batch_size - accumulated_tensor.shape[0]
while remaining > 0:
# Add a slice of the tensor that's not bigger than what we need
slice_size = min(accumulated_tensor.shape[0], remaining)
tensors_to_cat.append(accumulated_tensor[:slice_size])
remaining -= slice_size
# Concatenate all tensors
output_tensor = torch.cat(tensors_to_cat, dim=0)
else:
# Return what we have
output_tensor = accumulated_tensor if accumulated_tensor is not None else torch.zeros((0, 0))
# Reset if batch is complete and reset_after_batch is True
if batch_complete and reset_after_batch:
accumulated_tensor = None
frames = []
# Update state
state[state_key] = accumulated_tensor
state[f"{state_key}_frames"] = frames
self.set_state(state, unique_id)
return (output_tensor,)
class LatentAccumulatorNode(BaseAccumulatorNode):
"""
Node that accumulates latent inputs over multiple workflow runs.
"""
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("latent_batch",)
NODE_TYPE_NAME = "Latent"
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
inputs["required"]["input_value"] = ("LATENT", {"tooltip": "Latent to accumulate"})
return inputs
def update(self, input_value, batch_size, pad_incomplete_batch,
reset_after_batch, add_to_batch, reset_batch, always_execute=True, unique_id=None):
"""
Directly batch latent tensors instead of storing a list
"""
# Use the unique_id directly for state management
state_key = f"__accumulator_{self.NODE_TYPE_NAME}__"
# Get current state or initialize empty
state = self.get_state({
f"{state_key}_samples": None,
f"{state_key}_frames": [],
f"{state_key}_metadata": {}
}, unique_id)
accumulated_samples = state[f"{state_key}_samples"]
frames = state[f"{state_key}_frames"]
metadata = state[f"{state_key}_metadata"]
# Get samples tensor from input latent
current_samples = input_value['samples']
# Collect metadata from the first latent
if not metadata and add_to_batch:
for key, value in input_value.items():
if key != 'samples':
metadata[key] = value
# Handle reset if requested
if reset_batch:
accumulated_samples = None
frames = []
# Auto-reset on size mismatch
elif add_to_batch and frames and current_samples.shape[1:] != frames[0].shape[1:]:
print(f"[LatentAccumulatorNode] Size mismatch, resetting batch: {frames[0].shape[1:]} vs {current_samples.shape[1:]}")
accumulated_samples = None
frames = []
# Add current input to batch if requested
if add_to_batch:
# When batch is already full, remove oldest frame (FIFO queue)
if len(frames) >= batch_size:
frames.pop(0) # Remove oldest frame
# Add new frame(s)
# Handle multi-batch inputs by adding them one by one
for i in range(current_samples.shape[0]):
frame = current_samples[i:i+1] # Get single frame with batch dimension
frames.append(frame)
# Rebuild accumulated tensor from frames
if frames:
accumulated_samples = torch.cat(frames, dim=0)
else:
accumulated_samples = None
# Prepare output
batch_complete = accumulated_samples is not None and accumulated_samples.shape[0] >= batch_size
if batch_complete:
# Only take the requested batch size
output_samples = accumulated_samples[:batch_size]
elif pad_incomplete_batch and accumulated_samples is not None and accumulated_samples.shape[0] > 0:
# Pad to reach batch_size by duplicating elements
tensors_to_cat = [accumulated_samples]
remaining = batch_size - accumulated_samples.shape[0]
while remaining > 0:
# Add a slice of the tensor that's not bigger than what we need
slice_size = min(accumulated_samples.shape[0], remaining)
tensors_to_cat.append(accumulated_samples[:slice_size])
remaining -= slice_size
# Concatenate all tensors
output_samples = torch.cat(tensors_to_cat, dim=0)
else:
# Return what we have
output_samples = accumulated_samples if accumulated_samples is not None else torch.zeros((0, 0, 0, 0))
# Reset if batch is complete and reset_after_batch is True
if batch_complete and reset_after_batch:
accumulated_samples = None
frames = []
# Update state
state[f"{state_key}_samples"] = accumulated_samples
state[f"{state_key}_frames"] = frames
state[f"{state_key}_metadata"] = metadata
self.set_state(state, unique_id)
# Return latent dict with samples
return ({"samples": output_samples, **metadata},)
@@ -156,3 +156,5 @@ class SetStateNode(ControlNodeBase):
print(f"[State Node] Error storing value: {str(e)}")
return (value,)