diff --git a/__init__.py b/__init__.py index 863777d..fdea274 100644 --- a/__init__.py +++ b/__init__.py @@ -8,6 +8,7 @@ from .boolean_reverse_node import BooleanReverse from .get_image_size_ratio_node import GetImageSizeRatio from .noise_plus_blend_node import NoisePlusBlend from .integer_settings_node import IntegerSettings +from .integer_settings_advanced_node import IntegerSettingsAdvanced from .choose_upscale_model_node import ChooseUpscaleModel from .region_mask_generator_node import RegionMaskGenerator from .region_mask_validator_node import RegionMaskValidator @@ -16,6 +17,11 @@ from .region_mask_conditioning_node import RegionMaskConditioning from .flux_attention_control_node import FluxAttentionControl from .region_overlay_visualizer_node import RegionOverlayVisualizer from .flux_attention_cleanup_node import FluxAttentionCleanup +from .hidream_resolution_node import HiDreamResolutionNode +from .perturbation_texture_node import PerturbationTexture +from .text_bridge_node import TextBridge +from .two_way_switch_node import TwoWaySwitch +from .three_way_switch_node import ThreeWaySwitch NODE_CLASS_MAPPINGS = { "FluxResolutionNode": FluxResolutionNode, @@ -26,6 +32,7 @@ NODE_CLASS_MAPPINGS = { "GetImageSizeRatio": GetImageSizeRatio, "NoisePlusBlend": NoisePlusBlend, "IntegerSettings": IntegerSettings, + "IntegerSettingsAdvanced": IntegerSettingsAdvanced, "ChooseUpscaleModel": ChooseUpscaleModel, "RegionMaskGenerator": RegionMaskGenerator, "RegionMaskValidator": RegionMaskValidator, @@ -34,6 +41,11 @@ NODE_CLASS_MAPPINGS = { "FluxAttentionControl": FluxAttentionControl, "RegionOverlayVisualizer": RegionOverlayVisualizer, "FluxAttentionCleanup": FluxAttentionCleanup, + "HiDreamResolutionNode": HiDreamResolutionNode, + "PerturbationTexture": PerturbationTexture, + "TextBridge": TextBridge, + "TwoWaySwitch": TwoWaySwitch, + "ThreeWaySwitch": ThreeWaySwitch, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -45,6 +57,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "GetImageSizeRatio": "Get Image Size Ratio", "NoisePlusBlend": "Noise Plus Blend", "IntegerSettings": "Integer Settings", + "IntegerSettingsAdvanced": "Integer Settings Advanced", "ChooseUpscaleModel": "Choose Upscale Model", "RegionMaskGenerator": "Region Mask Generator", "RegionMaskValidator": "Region Mask Validator", @@ -53,4 +66,14 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FluxAttentionControl": "Flux Attention Control", "RegionOverlayVisualizer": "Region Overlay Visualizer", "FluxAttentionCleanup": "Flux Attention Cleanup", -} \ No newline at end of file + "HiDreamResolutionNode": "HiDream Resolution", + "PerturbationTexture": "Perturbation Texture", + "TextBridge": "Text Bridge", + "TwoWaySwitch": "Switch (Two Way)", + "ThreeWaySwitch": "Switch (Three Way)", +} + +# Tell ComfyUI where to find JavaScript files +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/integer_settings_advanced_node.py b/integer_settings_advanced_node.py new file mode 100644 index 0000000..384878d --- /dev/null +++ b/integer_settings_advanced_node.py @@ -0,0 +1,38 @@ +class IntegerSettingsAdvanced: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "setting_1": ("BOOLEAN", {"default": True, "label_on": "Enable", "label_off": "Disable"}), + "setting_2": ("BOOLEAN", {"default": False, "label_on": "Enable", "label_off": "Disable"}), + "setting_3": ("BOOLEAN", {"default": False, "label_on": "Enable", "label_off": "Disable"}), + }, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("setting_value",) + FUNCTION = "integer_settings_advanced" + + CATEGORY = "ControlAltAI Nodes/Logic" + + def integer_settings_advanced(self, setting_1, setting_2, setting_3): + """ + Returns integer based on which setting is enabled. + Due to mutual exclusion (handled by JS), only one should be True. + Priority order: setting_3 > setting_2 > setting_1 + """ + if setting_3: + return (3,) + elif setting_2: + return (2,) + else: + # Default to 1 (setting_1 or fallback) + return (1,) + +NODE_CLASS_MAPPINGS = { + "IntegerSettingsAdvanced": IntegerSettingsAdvanced, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "IntegerSettingsAdvanced": "Integer Settings Advanced", +} \ No newline at end of file diff --git a/perturbation_texture_node.py b/perturbation_texture_node.py new file mode 100644 index 0000000..d8874a6 --- /dev/null +++ b/perturbation_texture_node.py @@ -0,0 +1,229 @@ +import numpy as np +from PIL import Image, ImageChops +import torch + +class PerturbationTexture: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "noise_scale": ("FLOAT", {"default": 0.5, "min": 0.00, "max": 1.00, "step": 0.01}), + "texture_strength": ("INT", {"default": 50, "min": 0, "max": 100}), + "texture_type": (["Film Grain", "Skin Pore", "Natural", "Fine Detail"], {"default": "Skin Pore"}), + "frequency": ("FLOAT", {"default": 1.0, "min": 0.2, "max": 5.0, "step": 0.1}), + "perturbation_factor": ("FLOAT", {"default": 0.30, "min": 0.01, "max": 0.5, "step": 0.01}), + "use_mask": ("BOOLEAN", {"default": False}), + }, + "optional": { + "mask": ("MASK",), + "seed": ("INT", {"default": -1}), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE") + RETURN_NAMES = ("textured_image_output", "texture_layer") + FUNCTION = "apply_perturbation_texture" + + CATEGORY = "ControlAltAI Nodes/Image" + + def tensor_to_pil(self, tensor_image): + """Converts tensor to a PIL Image""" + tensor_image = tensor_image.squeeze(0) # Remove batch dimension if it exists + pil_image = Image.fromarray((tensor_image.cpu().numpy() * 255).astype(np.uint8)) + return pil_image + + def pil_to_tensor(self, pil_image): + """Converts a PIL image back to a tensor""" + return torch.from_numpy(np.array(pil_image).astype(np.float32) / 255).unsqueeze(0) + + def generate_adaptive_texture(self, base_image, noise_scale, texture_type, frequency, perturbation_factor, texture_strength, seed=None): + """Generate texture with adaptive color matching.""" + width, height = base_image.size + + # Set seed for reproducibility if provided + if seed is not None and seed >= 0: + np.random.seed(seed) + + # Convert base image to numpy array + base_np = np.array(base_image).astype(np.float32) / 255.0 + + # Generate noise patterns based on texture type + noise_patterns = self.generate_noise_patterns(width, height, noise_scale, texture_type, frequency) + + # Convert noise to -1 to 1 range for proper mixing + noise_normalized = (noise_patterns.astype(np.float32) - 128.0) / 128.0 + + # Apply perturbation with texture_strength controlling the final intensity + effective_perturbation = perturbation_factor * (texture_strength / 100.0) + + # Apply noise as color-matched variations around the base color + result = base_np + (noise_normalized * effective_perturbation) + + # Clamp to valid range + result = np.clip(result, 0, 1) + + # Create a more visible texture layer for preview/debugging + texture_layer = base_np + (noise_normalized * perturbation_factor * 2.0) + texture_layer = np.clip(texture_layer, 0, 1) + + final_image = Image.fromarray((result * 255).astype(np.uint8)) + texture_image = Image.fromarray((texture_layer * 255).astype(np.uint8)) + + return final_image, texture_image + + def generate_noise_patterns(self, width, height, noise_scale, texture_type, frequency): + """Generates noise patterns optimized for each texture type.""" + + # Safe resize function for noise scaling + def safe_resize(arr, target_height, target_width): + from PIL import Image + if arr.ndim == 2: + img = Image.fromarray((arr * 255 / arr.max()).astype(np.uint8)) + else: + img = Image.fromarray(arr.astype(np.uint8)) + img = img.resize((target_width, target_height), Image.LANCZOS) + return np.array(img).astype(np.float32) / 255.0 * arr.max() + + if texture_type == "Film Grain": + # Film grain - larger, more irregular pattern with RGB variation + base_noise_r = np.random.normal(128, 64 * noise_scale, (height, width)) + base_noise_g = np.random.normal(128, 64 * noise_scale, (height, width)) + base_noise_b = np.random.normal(128, 64 * noise_scale, (height, width)) + + # Add larger scale variation for film-like clustering + large_scale_h = max(4, int(height/(4*frequency))) + large_scale_w = max(4, int(width/(4*frequency))) + large_scale_r = np.random.normal(0, 30 * noise_scale, (large_scale_h, large_scale_w)) + large_scale_g = np.random.normal(0, 30 * noise_scale, (large_scale_h, large_scale_w)) + large_scale_b = np.random.normal(0, 30 * noise_scale, (large_scale_h, large_scale_w)) + + large_scale_r = safe_resize(large_scale_r, height, width) + large_scale_g = safe_resize(large_scale_g, height, width) + large_scale_b = safe_resize(large_scale_b, height, width) + + combined_r = np.clip(base_noise_r * 0.7 + large_scale_r * 0.3, 0, 255) + combined_g = np.clip(base_noise_g * 0.7 + large_scale_g * 0.3, 0, 255) + combined_b = np.clip(base_noise_b * 0.7 + large_scale_b * 0.3, 0, 255) + + elif texture_type == "Skin Pore": + # Fine, subtle texture optimized for skin with reduced intensity + base_scale = noise_scale * 0.6 # More subtle for natural skin texture + + # Create subtle RGB variations for realistic skin texture + base_noise_r = np.random.normal(128, 32 * base_scale, (height, width)) + base_noise_g = np.random.normal(128, 28 * base_scale, (height, width)) + base_noise_b = np.random.normal(128, 24 * base_scale, (height, width)) + + # Fine pore-like details at higher frequency + fine_h = max(4, int(height*frequency*1.5)) + fine_w = max(4, int(width*frequency*1.5)) + fine_noise_r = np.random.normal(0, 20 * base_scale, (fine_h, fine_w)) + fine_noise_g = np.random.normal(0, 18 * base_scale, (fine_h, fine_w)) + fine_noise_b = np.random.normal(0, 16 * base_scale, (fine_h, fine_w)) + + fine_noise_r = safe_resize(fine_noise_r, height, width) + fine_noise_g = safe_resize(fine_noise_g, height, width) + fine_noise_b = safe_resize(fine_noise_b, height, width) + + combined_r = np.clip(base_noise_r + fine_noise_r * 0.8, 0, 255) + combined_g = np.clip(base_noise_g + fine_noise_g * 0.8, 0, 255) + combined_b = np.clip(base_noise_b + fine_noise_b * 0.8, 0, 255) + + elif texture_type == "Natural": + # Multi-layered natural texture with organic frequency distribution + base_noise_r = np.random.normal(128, 48 * noise_scale, (height, width)) + base_noise_g = np.random.normal(128, 44 * noise_scale, (height, width)) + base_noise_b = np.random.normal(128, 40 * noise_scale, (height, width)) + + # Multiple frequency layers for natural complexity + frequencies = [frequency*2, frequency, frequency/3] + weights = [0.5, 0.3, 0.2] + + combined_r = base_noise_r.copy() + combined_g = base_noise_g.copy() + combined_b = base_noise_b.copy() + + for freq, weight in zip(frequencies, weights): + f_h = max(4, int(height*freq)) + f_w = max(4, int(width*freq)) + + layer_r = np.random.normal(0, 30 * noise_scale * weight, (f_h, f_w)) + layer_g = np.random.normal(0, 28 * noise_scale * weight, (f_h, f_w)) + layer_b = np.random.normal(0, 26 * noise_scale * weight, (f_h, f_w)) + + layer_r = safe_resize(layer_r, height, width) + layer_g = safe_resize(layer_g, height, width) + layer_b = safe_resize(layer_b, height, width) + + combined_r += layer_r * weight + combined_g += layer_g * weight + combined_b += layer_b * weight + + combined_r = np.clip(combined_r, 0, 255) + combined_g = np.clip(combined_g, 0, 255) + combined_b = np.clip(combined_b, 0, 255) + + else: # Fine Detail + # High-frequency detailed texture for micro-details + high_freq = frequency * 2.5 + + base_noise_r = np.random.normal(128, 40 * noise_scale, (height, width)) + base_noise_g = np.random.normal(128, 38 * noise_scale, (height, width)) + base_noise_b = np.random.normal(128, 36 * noise_scale, (height, width)) + + # High-frequency fine details + fine_h = max(4, int(height*high_freq)) + fine_w = max(4, int(width*high_freq)) + fine_detail_r = np.random.normal(0, 25 * noise_scale, (fine_h, fine_w)) + fine_detail_g = np.random.normal(0, 23 * noise_scale, (fine_h, fine_w)) + fine_detail_b = np.random.normal(0, 21 * noise_scale, (fine_h, fine_w)) + + fine_detail_r = safe_resize(fine_detail_r, height, width) + fine_detail_g = safe_resize(fine_detail_g, height, width) + fine_detail_b = safe_resize(fine_detail_b, height, width) + + combined_r = np.clip(base_noise_r + fine_detail_r * 0.7, 0, 255) + combined_g = np.clip(base_noise_g + fine_detail_g * 0.7, 0, 255) + combined_b = np.clip(base_noise_b + fine_detail_b * 0.7, 0, 255) + + # Stack RGB channels into final noise pattern + return np.stack([combined_r, combined_g, combined_b], axis=2) + + def apply_perturbation_texture(self, image, noise_scale=0.5, texture_strength=50, texture_type="Skin Pore", + frequency=1.0, perturbation_factor=0.15, use_mask=False, mask=None, seed=-1): + """Main function to apply adaptive color-matched texture.""" + # Convert tensor image to PIL + base_image = self.tensor_to_pil(image) + + # Use provided seed or generate random if -1 + seed_value = seed if seed >= 0 else None + + # Generate adaptive texture + textured_image, texture_layer = self.generate_adaptive_texture( + base_image, noise_scale, texture_type, frequency, + perturbation_factor, texture_strength, seed_value + ) + + # Apply mask if specified + if use_mask and mask is not None: + mask_pil = self.tensor_to_pil(mask).convert('L') + mask_resized = mask_pil.resize(base_image.size) + # Invert mask so white areas get texture, black areas are protected + inverted_mask = ImageChops.invert(mask_resized) + # Composite: base where mask is black, textured where mask is white + textured_image = Image.composite(base_image, textured_image, inverted_mask) + + # Convert results back to tensors + texture_tensor = self.pil_to_tensor(texture_layer) + textured_tensor = self.pil_to_tensor(textured_image) + + return textured_tensor, texture_tensor + +NODE_CLASS_MAPPINGS = { + "PerturbationTexture": PerturbationTexture, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PerturbationTexture": "Perturbation Texture", +} \ No newline at end of file diff --git a/text_bridge_node.py b/text_bridge_node.py new file mode 100644 index 0000000..e5bf5f7 --- /dev/null +++ b/text_bridge_node.py @@ -0,0 +1,40 @@ +class TextBridge: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_input": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + "passthrough_text": ("STRING", {"forceInput": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text_output",) + FUNCTION = "bridge_text" + + CATEGORY = "ControlAltAI Nodes/Utility" + + def bridge_text(self, text_input="", passthrough_text=""): + """ + Bridge function that allows editing of input text and passes it through as output. + If passthrough_text is connected, it uses that as the base text. + The text_input field allows manual editing/override. + """ + # If passthrough_text is provided and text_input is empty or default, use passthrough + if passthrough_text and (not text_input or text_input == ""): + output_text = passthrough_text + else: + # Use the manually entered/edited text + output_text = text_input + + return (output_text,) + +NODE_CLASS_MAPPINGS = { + "TextBridge": TextBridge, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "TextBridge": "Text Bridge", +} \ No newline at end of file diff --git a/three_way_switch_node.py b/three_way_switch_node.py new file mode 100644 index 0000000..b25428b --- /dev/null +++ b/three_way_switch_node.py @@ -0,0 +1,62 @@ +class AnyType(str): + """A special string subclass that equals any other type for ComfyUI type checking.""" + def __ne__(self, __value: object) -> bool: + return False + +# Create an instance to use as the any type +any_type = AnyType("*") + +class ThreeWaySwitch: + """Three-way switch that selects between three inputs based on selection setting.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "selection_setting": ("INT", {"default": 1, "min": 1, "max": 3}), + }, + "optional": { + "input_1": (any_type,), + "input_2": (any_type,), + "input_3": (any_type,), + } + } + + RETURN_TYPES = (any_type,) + RETURN_NAMES = ("output",) + FUNCTION = "switch_inputs" + + CATEGORY = "ControlAltAI Nodes/Logic" + + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + """Allow any input types.""" + return True + + def switch_inputs(self, selection_setting=1, input_1=None, input_2=None, input_3=None): + """ + Three-way switch that selects between three inputs based on the selection_setting. + Compatible with IntegerSettingsAdvanced node: + - selection_setting = 1: selects input_1 + - selection_setting = 2: selects input_2 + - selection_setting = 3: selects input_3 + """ + if selection_setting == 2: + # Second option - select input_2, fallback to input_1, then input_3 + selected_output = input_2 if input_2 is not None else (input_1 if input_1 is not None else input_3) + elif selection_setting == 3: + # Third option - select input_3, fallback to input_1, then input_2 + selected_output = input_3 if input_3 is not None else (input_1 if input_1 is not None else input_2) + else: + # Default/First option (1) - select input_1, fallback to input_2, then input_3 + selected_output = input_1 if input_1 is not None else (input_2 if input_2 is not None else input_3) + + return (selected_output,) + +NODE_CLASS_MAPPINGS = { + "ThreeWaySwitch": ThreeWaySwitch, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ThreeWaySwitch": "Switch (Three Way)", +} \ No newline at end of file diff --git a/two_way_switch_node.py b/two_way_switch_node.py new file mode 100644 index 0000000..9c3059b --- /dev/null +++ b/two_way_switch_node.py @@ -0,0 +1,57 @@ +class AnyType(str): + """A special string subclass that equals any other type for ComfyUI type checking.""" + def __ne__(self, __value: object) -> bool: + return False + +# Create an instance to use as the any type +any_type = AnyType("*") + +class TwoWaySwitch: + """Two-way switch that selects between two inputs based on selection setting.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "selection_setting": ("INT", {"default": 1, "min": 1, "max": 2}), + }, + "optional": { + "input_1": (any_type,), + "input_2": (any_type,), + } + } + + RETURN_TYPES = (any_type,) + RETURN_NAMES = ("output",) + FUNCTION = "switch_inputs" + + CATEGORY = "ControlAltAI Nodes/Logic" + + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + """Allow any input types.""" + return True + + def switch_inputs(self, selection_setting=1, input_1=None, input_2=None): + """ + Two-way switch that selects between two inputs based on the selection_setting. + Compatible with IntegerSettings node: + - selection_setting = 1 (Disable): selects input_1 + - selection_setting = 2 (Enable): selects input_2 + """ + if selection_setting == 2: + # Enable state - select second input + selected_output = input_2 if input_2 is not None else input_1 + else: + # Disable state (1) or any other value - select first input + selected_output = input_1 if input_1 is not None else input_2 + + return (selected_output,) + +NODE_CLASS_MAPPINGS = { + "TwoWaySwitch": TwoWaySwitch, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "TwoWaySwitch": "Switch (Two Way)", +} \ No newline at end of file diff --git a/web/js/integer_settings_advanced.js b/web/js/integer_settings_advanced.js new file mode 100644 index 0000000..206eda4 --- /dev/null +++ b/web/js/integer_settings_advanced.js @@ -0,0 +1,78 @@ +import { app } from "/scripts/app.js"; + +// Register the extension for IntegerSettingsAdvanced node +app.registerExtension({ + name: "ControlAltAI.IntegerSettingsAdvanced", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "IntegerSettingsAdvanced") { + console.log("Registering IntegerSettingsAdvanced mutual exclusion behavior"); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = onNodeCreated?.apply(this, arguments); + + // Store reference to the node + const node = this; + + // Function to enforce mutual exclusion + function enforceMutualExclusion(activeWidget) { + // Get all boolean widgets + const booleanWidgets = node.widgets.filter(w => + w.type === "toggle" && + (w.name === "setting_1" || w.name === "setting_2" || w.name === "setting_3") + ); + + // If a widget is being set to true, set others to false + if (activeWidget.value === true) { + booleanWidgets.forEach(widget => { + if (widget !== activeWidget) { + widget.value = false; + } + }); + } + + // Always ensure at least one is true (always one behavior) + const anyEnabled = booleanWidgets.some(w => w.value === true); + if (!anyEnabled) { + // If none are enabled, enable setting_1 as default + const setting1Widget = booleanWidgets.find(w => w.name === "setting_1"); + if (setting1Widget) { + setting1Widget.value = true; + } + } + + // Trigger canvas redraw + if (app.graph) { + app.graph.setDirtyCanvas(true, false); + } + } + + // Hook into widget callbacks after node is fully created + setTimeout(() => { + node.widgets.forEach(widget => { + if (widget.type === "toggle" && + (widget.name === "setting_1" || widget.name === "setting_2" || widget.name === "setting_3")) { + + // Store original callback + const originalCallback = widget.callback; + + // Override with mutual exclusion logic + widget.callback = function(value) { + // Call original callback first + if (originalCallback) { + originalCallback.call(this, value); + } + + // Apply mutual exclusion + enforceMutualExclusion(this); + }; + } + }); + }, 10); + + return result; + }; + } + } +}); \ No newline at end of file