diff --git a/py/latent_ops.py b/py/latent_ops.py index 53553ed..5061d56 100644 --- a/py/latent_ops.py +++ b/py/latent_ops.py @@ -64,6 +64,7 @@ class SonarLatentOperationAdvanced(SonarLatentOperation): *, blend_mode: str, blend_strength: float, + blend_strategy: str, input_multiplier: float, output_multiplier: float, difference_multiplier: float, @@ -74,6 +75,7 @@ class SonarLatentOperationAdvanced(SonarLatentOperation): super().__init__(**kwargs) self.blend_function = utils.BLENDING_MODES[blend_mode] self.blend_strength = blend_strength + self.blend_strategy = blend_strategy self.input_multiplier = input_multiplier self.output_multiplier = output_multiplier self.difference_multiplier = difference_multiplier @@ -103,7 +105,11 @@ class SonarLatentOperationAdvanced(SonarLatentOperation): ) - t if self.difference_multiplier != 1.0: diff *= self.difference_multiplier - return self.blend_function(t, diff, self.blend_strength) + if self.blend_strategy == "difference": + return self.blend_function(t, diff, self.blend_strength) + if self.blend_strategy == "result": + return self.blend_function(t, t + diff, self.blend_strength) + raise ValueError(f"Unknown blend strategy: {self.blend_strategy}") class SonarLatentOperationNoise(SonarLatentOperation): diff --git a/py/nodes/latent_operations.py b/py/nodes/latent_operations.py index ffc7662..029b6b5 100644 --- a/py/nodes/latent_operations.py +++ b/py/nodes/latent_operations.py @@ -361,10 +361,6 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): INPUT_TYPES = SonarLazyInputTypes( lambda: SonarInputTypes() - .req_field_operation( - "LATENT_OPERATION", - tooltip="Latent operation to apply.", - ) .req_float_start_sigma( default=-1.0, min=-1.0, @@ -395,6 +391,15 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): default=0.5, tooltip="Strength of the blend.", ) + .req_field_blend_strategy( + ("difference", "result"), + default="difference", + tooltip="Controls whether blending occurs with the difference or changed result after the latent operation.", + ) + .opt_field_operation( + "LATENT_OPERATION", + tooltip="Latent operation to apply.", + ) .opt_field_operation_alt( "LATENT_OPERATION", tooltip="Optional alternative operation that will be used when the primary one isn't enabled. May be useful in a case when you want one operation between sigma 1.0 and 0.5 and then a difference operation for lower sigmas which is kind of annoying to specify manually (you'd need to do something like configure another operation to start at 0.499999 or something).", @@ -421,7 +426,6 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): def go( cls, *, - operation, start_sigma: float, end_sigma: float, input_multiplier: float, @@ -429,6 +433,8 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): difference_multiplier: float, blend_mode: str, blend_strength: float, + blend_strategy: str, + operation=None, operation_alt=None, operation_2=None, operation_3=None, @@ -456,6 +462,7 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): difference_multiplier=difference_multiplier, blend_mode=blend_mode, blend_strength=blend_strength, + blend_strategy=blend_strategy, ), ) diff --git a/py/nodes/misc.py b/py/nodes/misc.py index d05f040..0d65544 100644 --- a/py/nodes/misc.py +++ b/py/nodes/misc.py @@ -4,12 +4,13 @@ import functools import inspect import math import random -from typing import Any, Callable +from typing import TYPE_CHECKING, Any import numpy as np import torch import yaml from comfy import model_management, samplers +from comfy import utils as comfy_utils from tqdm import tqdm from .. import noise, utils @@ -24,6 +25,14 @@ from .base import ( SonarNormalizeNoiseNodeMixin, ) +if TYPE_CHECKING: + from collections.abc import Callable + +try: + from comfy import nested_tensor +except (ModuleNotFoundError, ImportError): + nested_tensor = None + class NoisyLatentLikeNode(metaclass=IntegratedNode): DESCRIPTION = "Allows generating noise (and optionally adding it) based on a reference latent. Note: For img2img workflows, you will generally want to enable add_to_latent as well as connecting the model and sigmas inputs." @@ -390,10 +399,29 @@ class CustomNOISE: return result.to_sparse() errstr = f"Cannot handle latent layout {type(latent_image.layout).__name__}" raise NotImplementedError(errstr) - return result if self.multiplier == 1.0 else result.mul_(self.multiplier) + if self.multiplier != 1.0: + result *= self.multiplier + return result def generate_noise(self, input_latent): - latent_image = input_latent["samples"] + latent_image = orig_latent_image = input_latent["samples"] + orig_type = type(latent_image) + if nested_tensor is not None and latent_image.is_nested: + latent_image, latent_shapes = comfy_utils.pack_latents( + latent_image.unbind(), + ) + else: + latent_shapes = None + + def result_out(result, latent_shapes): + if latent_shapes is None: + return result + tensors = comfy_utils.unpack_latents(result, latent_shapes) + if isinstance(orig_type, torch.Tensor): + # Should be an actual PyTorch nested tensor, not ComfyUI's custom class + return orig_type(tensors, layout=orig_latent_image.layout) + return orig_type(tensors) + # TODO: Test this stuff. batch_inds = input_latent.get("batch_index") torch.manual_seed(self.seed) random.seed(self.seed) @@ -405,18 +433,33 @@ class CustomNOISE: device="cpu", ) if batch_inds is None: - return self._sample_noise(latent_image, self.seed) - unique_inds, inverse_inds = np.unique(batch_inds, return_inverse=True) - result = [] - batch_size = latent_image.shape[0] - for idx in range(unique_inds[-1] + 1): - noise = self._sample_noise( - latent_image[idx % batch_size].unsqueeze(0), - self.seed + idx, + return result_out( + self._sample_noise(latent_image, self.seed), + latent_shapes, ) - if idx in unique_inds: - result.append(noise) - return torch.cat(tuple(result[i] for i in inverse_inds), axis=0) + batch_size = latent_image.shape[0] + unique_inds, inverse_inds = np.unique(batch_inds, return_inverse=True) + use_idxs = { + out_idx: idx % batch_size for out_idx, idx in enumerate(unique_inds) + } + use_idxs = (idx for idx in range(unique_inds[-1] + 1) if idx in unique_inds) + use_idxs = {idx: inverse_inds[uidx] for uidx, idx in enumerate(use_idxs)} + result = torch.empty( + (len(use_idxs), *latent_image.shape[1:]), + dtype=latent_image.dtype, + device=latent_image.device, + ) + for idx in range(unique_inds[-1] + 1): + sample_idx = idx % batch_size + sample = latent_image[sample_idx].unsqueeze(0) + print( + f"\nNOISE: idx {idx}, sample_idx {sample_idx}, shape {latent_image[sample_idx].shape}, nested={sample.is_nested}", + ) + noise = self._sample_noise(sample, self.seed + idx) + batch_out_idx = use_idxs.get(idx) + if batch_out_idx is not None: + result[batch_out_idx : batch_out_idx + 1] = noise[:1] + return result_out(result, latent_shapes) class SonarToComfyNOISENode(metaclass=IntegratedNode): diff --git a/py/nodes/noise_filters.py b/py/nodes/noise_filters.py index 4eeccac..57cd6fc 100644 --- a/py/nodes/noise_filters.py +++ b/py/nodes/noise_filters.py @@ -19,40 +19,44 @@ class SonarModulatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeM DESCRIPTION = "Custom noise type that allows modulating the output of another custom noise generator." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_sonar_custom_noise(tooltip="Custom noise type to modulate.") - .req_field_modulation_type( - ( - "intensity", - "frequency", - "spectral_signum", - "none", - ), - tooltip="Type of modulation to use.", - ) - .req_int_dims( - default=3, - min=1, - max=3, - tooltip="Dimensions to modulate over. 1 - channels only, 2 - height and width, 3 - both", - ) - .req_float_strength( - default=2.0, - min=-100.0, - max=100.0, - tooltip="Controls the strength of the modulation effect.", - ) - .req_normalizetristate_normalize_result( - tooltip="Controls whether the final result is normalized to 1.0 strength.", - ) - .req_normalizetristate_normalize_noise( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .req_bool_normalize_ref( - default=True, - tooltip="Controls whether the reference latent (when present) is normalized to 1.0 strength.", - ) - .opt_latent_ref_latent_opt(), + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_sonar_custom_noise( + tooltip="Custom noise type to modulate." + ) + .req_field_modulation_type( + ( + "intensity", + "frequency", + "spectral_signum", + "none", + ), + tooltip="Type of modulation to use.", + ) + .req_int_dims( + default=3, + min=1, + max=3, + tooltip="Dimensions to modulate over. 1 - channels only, 2 - height and width, 3 - both", + ) + .req_float_strength( + default=2.0, + min=-100.0, + max=100.0, + tooltip="Controls the strength of the modulation effect.", + ) + .req_normalizetristate_normalize_result( + tooltip="Controls whether the final result is normalized to 1.0 strength.", + ) + .req_normalizetristate_normalize_noise( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .req_bool_normalize_ref( + default=True, + tooltip="Controls whether the reference latent (when present) is normalized to 1.0 strength.", + ) + .opt_latent_ref_latent_opt() + ), ) @classmethod @@ -91,27 +95,31 @@ class SonarRepeatedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMi DESCRIPTION = "Custom noise type that allows caching the output of other custom noise generators." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_sonar_custom_noise(tooltip="Custom noise type to modulate.") - .req_int_repeat_length( - default=8, - min=1, - max=100, - tooltip="Number of items to cache.", - ) - .req_int_max_recycle( - default=1000, - min=1, - max=1000, - tooltip="Number of times an individual item will be used before it is replaced with fresh noise.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .req_field_permute( - ("enabled", "disabled", "always"), - default="enabled", - tooltip="When enabled, recycled noise will be permuted by randomly flipping it, rolling the channels, etc. If set to always, the noise will be permuted the first time it's used as well.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_sonar_custom_noise( + tooltip="Custom noise type to modulate." + ) + .req_int_repeat_length( + default=8, + min=1, + max=100, + tooltip="Number of items to cache.", + ) + .req_int_max_recycle( + default=1000, + min=1, + max=1000, + tooltip="Number of times an individual item will be used before it is replaced with fresh noise.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .req_field_permute( + ("enabled", "disabled", "always"), + default="enabled", + tooltip="When enabled, recycled noise will be permuted by randomly flipping it, rolling the channels, etc. If set to always, the noise will be permuted the first time it's used as well.", + ) ), ) @@ -143,30 +151,32 @@ class SonarScheduledNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeM DESCRIPTION = "Custom noise type that allows scheduling the output of other custom noise generators. NOTE: If you don't connect the fallback custom noise input, no noise will be generated outside of the start_percent, end_percent range. I recommend connecting a 1.0 strength Gaussian custom noise node as the fallback." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_model( - tooltip="The model input is required to calculate sampling percentages.", - ) - .req_customnoise_sonar_custom_noise( - tooltip="Custom noise to use when start_percent and end_percent matches.", - ) - .req_float_start_percent( - default=0.0, - min=0.0, - max=1.0, - tooltip="Time the custom noise becomes active. Note: Sampling percentage where 1.0 indicates 100%, not based on steps.", - ) - .req_float_end_percent( - default=1.0, - min=0.0, - max=1.0, - tooltip="Time the custom noise effect ends - inclusive, so only sampling percentages greater than this will be excluded. Note: Sampling percentage where 1.0 indicates 100%, not based on steps.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .opt_customnoise_fallback_sonar_custom_noise( - tooltip="Optional input for noise to use when outside of the start_percent, end_percent range. NOTE: When not connected, defaults to NO NOISE which is probably not what you want.", + lambda: ( + NoiseNoChainInputTypes() + .req_model( + tooltip="The model input is required to calculate sampling percentages.", + ) + .req_customnoise_sonar_custom_noise( + tooltip="Custom noise to use when start_percent and end_percent matches.", + ) + .req_float_start_percent( + default=0.0, + min=0.0, + max=1.0, + tooltip="Time the custom noise becomes active. Note: Sampling percentage where 1.0 indicates 100%, not based on steps.", + ) + .req_float_end_percent( + default=1.0, + min=0.0, + max=1.0, + tooltip="Time the custom noise effect ends - inclusive, so only sampling percentages greater than this will be excluded. Note: Sampling percentage where 1.0 indicates 100%, not based on steps.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_customnoise_fallback_sonar_custom_noise( + tooltip="Optional input for noise to use when outside of the start_percent, end_percent range. NOTE: When not connected, defaults to NO NOISE which is probably not what you want.", + ) ), ) @@ -202,25 +212,27 @@ class SonarCompositeNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeM DESCRIPTION = "Custom noise type that allows compositing two other custom noise generators based on a mask." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_sonar_custom_noise_dst( - tooltip="Custom noise input for noise where the mask is not set.", - ) - .req_customnoise_sonar_custom_noise_src( - tooltip="Custom noise input for noise where the mask is set.", - ) - .req_normalizetristate_normalize_dst( - tooltip="Controls whether noise generated for dst is normalized to 1.0 strength.", - ) - .req_normalizetristate_normalize_src( - tooltip="Controls whether noise generated for src is normalized to 1.0 strength.", - ) - .req_normalizetristate_normalize_result( - tooltip="Controls whether the final result after composition is normalized to 1.0 strength.", - ) - .req_field_mask( - "MASK", - tooltip="Mask to use when compositing noise. Where the mask is 1.0, you will get 100% src, where it is 0.75 you will get 75% src and 25% dst. The mask will be rescaled to match the latent size if necessary.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_sonar_custom_noise_dst( + tooltip="Custom noise input for noise where the mask is not set.", + ) + .req_customnoise_sonar_custom_noise_src( + tooltip="Custom noise input for noise where the mask is set.", + ) + .req_normalizetristate_normalize_dst( + tooltip="Controls whether noise generated for dst is normalized to 1.0 strength.", + ) + .req_normalizetristate_normalize_src( + tooltip="Controls whether noise generated for src is normalized to 1.0 strength.", + ) + .req_normalizetristate_normalize_result( + tooltip="Controls whether the final result after composition is normalized to 1.0 strength.", + ) + .req_field_mask( + "MASK", + tooltip="Mask to use when compositing noise. Where the mask is 1.0, you will get 100% src, where it is 0.75 you will get 75% src and 25% dst. The mask will be rescaled to match the latent size if necessary.", + ) ), ) @@ -254,33 +266,35 @@ class SonarGuidedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixi DESCRIPTION = "Custom noise type that mixes a references with another custom noise generator to guide the generation." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_latent( - tooltip="Latent to use for guidance.", - ) - .req_field_method( - ("euler", "linear"), - default="euler", - tooltip="Method to use when calculating guidance. When set to linear, will simply LERP the guidance at the specified strength. When set to Euler, will do a Euler step toward the guidance instead.", - ) - .req_float_guidance_factor( - default=0.0125, - min=-100.0, - max=100.0, - tooltip="Strength of the guidance to apply. Generally should be a relatively slow value to avoid overpowering the generation.", - ) - .req_normalizetristate_normalize_noise( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .req_normalizetristate_normalize_result( - tooltip="Controls whether the final result is normalized to 1.0 strength.", - ) - .req_bool_normalize_ref( - default=True, - tooltip="Controls whether the reference latent (when present) is normalized to 1.0 strength.", - ) - .opt_customnoise_sonar_custom_noise( - tooltip="Optional custom noise input to combine with the guidance. If you don't attach something here your reference will be combined with zeros.", + lambda: ( + NoiseNoChainInputTypes() + .req_latent( + tooltip="Latent to use for guidance.", + ) + .req_field_method( + ("euler", "linear"), + default="euler", + tooltip="Method to use when calculating guidance. When set to linear, will simply LERP the guidance at the specified strength. When set to Euler, will do a Euler step toward the guidance instead.", + ) + .req_float_guidance_factor( + default=0.0125, + min=-100.0, + max=100.0, + tooltip="Strength of the guidance to apply. Generally should be a relatively slow value to avoid overpowering the generation.", + ) + .req_normalizetristate_normalize_noise( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .req_normalizetristate_normalize_result( + tooltip="Controls whether the final result is normalized to 1.0 strength.", + ) + .req_bool_normalize_ref( + default=True, + tooltip="Controls whether the reference latent (when present) is normalized to 1.0 strength.", + ) + .opt_customnoise_sonar_custom_noise( + tooltip="Optional custom noise input to combine with the guidance. If you don't attach something here your reference will be combined with zeros.", + ) ), ) @@ -320,18 +334,20 @@ class SonarRandomNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixi DESCRIPTION = "Custom noise type that randomly selects between other custom noise items connected to it." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_sonar_custom_noise( - tooltip="Custom noise input for noise items to randomize. Note: Unlike most other custom noise nodes, this is treated like a list.", - ) - .req_int_mix_count( - default=1, - min=1, - max=100, - tooltip="Number of items to select each time noise is generated.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_sonar_custom_noise( + tooltip="Custom noise input for noise items to randomize. Note: Unlike most other custom noise nodes, this is treated like a list.", + ) + .req_int_mix_count( + default=1, + min=1, + max=100, + tooltip="Number of items to select each time noise is generated.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) ), ) @@ -358,23 +374,25 @@ class SonarChannelNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMix DESCRIPTION = "Custom noise type that uses a different noise generator for each channel. Note: The connected noise items are treated as a list. If you want to blend noise types, you can use something like a SonarBlendedNoise node." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_sonar_custom_noise( - tooltip="Custom noise input for noise items corresponding to each channel. SD1/2x and SDXL use 4 channels, Flux and SD3 use 16. Note: Unlike most other custom noise nodes, this is treated like a list where the noise item furthest from the node corresponds to channel 0.", - ) - .req_field_insufficient_channels_mode( - ("wrap", "repeat", "zero"), - default="wrap", - tooltip="Controls behavior for when there are less noise items connected than channels in the latent. wrap - wraps back to the first noise item, repeat - repeats the last item, zero - fills the channel with zeros (generally not recommended).", - ) - .req_int_mix_count( - default=1, - min=1, - max=100, - tooltip="Number of items to select each time noise is generated.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_sonar_custom_noise( + tooltip="Custom noise input for noise items corresponding to each channel. SD1/2x and SDXL use 4 channels, Flux and SD3 use 16. Note: Unlike most other custom noise nodes, this is treated like a list where the noise item furthest from the node corresponds to channel 0.", + ) + .req_field_insufficient_channels_mode( + ("wrap", "repeat", "zero"), + default="wrap", + tooltip="Controls behavior for when there are less noise items connected than channels in the latent. wrap - wraps back to the first noise item, repeat - repeats the last item, zero - fills the channel with zeros (generally not recommended).", + ) + .req_int_mix_count( + default=1, + min=1, + max=100, + tooltip="Number of items to select each time noise is generated.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) ), ) @@ -402,25 +420,27 @@ class SonarBlendedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMix DESCRIPTION = "Custom noise type that allows blending two other noise items." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_float_noise_2_percent( - default=0.5, - tooltip="Blend strength for custom_noise_2. Note that if set to 0 then custom_noise_2 is optional (and will not be called to generate noise) and if set to 1 then custom_noise_1 will not be called to generate noise. This only applies when custom_noise_mask is not connected. This is worth mentioning since going from a strength of 0.000000001 to 0 could make a big difference. Important: When custom_noise_mask is connected, this value will be added to the mask and then the mask will be clamped to 0 through 1. In other words, you could use this to ensure the mask ranges between 0.5 and 1.0 by setting it to 0.5 or ensure it ranges between 0 and 0.5 by setting it to -0.5.", - ) - .req_selectblend( - tooltip="Mode used for blending the two noise types. More modes will be available if ComfyUI-bleh is installed.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength. For weird blend modes, you may want to set this to forced.", - ) - .opt_customnoise_custom_noise_1( - tooltip="Custom noise. Optional if noise_2_percent is 1 and custom_noise_mask is not connected..", - ) - .opt_customnoise_custom_noise_2( - tooltip="Custom noise. Optional if noise_2_percent is 0 and custom_noise_mask is not connected..", - ) - .opt_customnoise_custom_noise_mask( - tooltip="Custom noise. If connected, this will be used instead of noise_2_percent to determine the blend ratio. Noise generated by this will be normalized to a 0 through 1 scale. When connected, both custom noise inputs are mandatory.", + lambda: ( + NoiseChainInputTypes() + .req_float_noise_2_percent( + default=0.5, + tooltip="Blend strength for custom_noise_2. Note that if set to 0 then custom_noise_2 is optional (and will not be called to generate noise) and if set to 1 then custom_noise_1 will not be called to generate noise. This only applies when custom_noise_mask is not connected. This is worth mentioning since going from a strength of 0.000000001 to 0 could make a big difference. Important: When custom_noise_mask is connected, this value will be added to the mask and then the mask will be clamped to 0 through 1. In other words, you could use this to ensure the mask ranges between 0.5 and 1.0 by setting it to 0.5 or ensure it ranges between 0 and 0.5 by setting it to -0.5.", + ) + .req_selectblend( + tooltip="Mode used for blending the two noise types. More modes will be available if ComfyUI-bleh is installed.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength. For weird blend modes, you may want to set this to forced.", + ) + .opt_customnoise_custom_noise_1( + tooltip="Custom noise. Optional if noise_2_percent is 1 and custom_noise_mask is not connected..", + ) + .opt_customnoise_custom_noise_2( + tooltip="Custom noise. Optional if noise_2_percent is 0 and custom_noise_mask is not connected..", + ) + .opt_customnoise_custom_noise_mask( + tooltip="Custom noise. If connected, this will be used instead of noise_2_percent to determine the blend ratio. Noise generated by this will be normalized to a 0 through 1 scale. When connected, both custom noise inputs are mandatory.", + ) ), ) @@ -461,72 +481,74 @@ class SonarResizedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMix DESCRIPTION = "Custom noise type that allows resizing another noise item." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_int_width( - default=1152, - min=16, - max=1024 * 1024 * 1024, - step=8, - tooltip="Note: This should almost always be set to a higher value than the image you're actually sampling.", - ) - .req_int_height( - default=1152, - min=16, - max=1024 * 1024 * 1024, - step=8, - tooltip="Note: This should almost always be set to a higher value than the image you're actually sampling.", - ) - .req_field_downscale_strategy( - ("crop", "scale"), - default="crop", - tooltip="Scaling noise is something you'd pretty much only use to create weird effects. For normal workflows, leave this on crop.", - ) - .req_field_initial_reference( - ("prefer_crop", "prefer_scale"), - default="prefer_crop", - tooltip="The initial latent the noise sampler uses as a reference may not match the requested width/height. This setting controls whether to crop or scale. Note: Cropping can only occur when the initial reference is larger than width/height in both dimensions which is unlikely (and not recommended).", - ) - .req_field_crop_mode( - ( - "center", - "top_left", - "top_center", - "top_right", - "center_left", - "center_right", - "bottom_left", - "bottom_center", - "bottom_right", - ), - default="center", - tooltip="Note: Crops will have a bias toward the lower number when the size isn't divisible by two. For example, a center crop of size 3 from (0, 1, 2, 3, 4, 5) will result in (1, 2, 3).", - ) - .req_int_crop_offset_horizontal( - default=0, - step=8, - min=-8000, - max=8000, - tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the right, negative values will move it toward the left. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to top_right then setting a positive offset isn't going to do anything: it's already as far right as it can go.", - ) - .req_int_crop_offset_vertical( - default=0, - step=8, - min=-8000, - max=8000, - tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the bottom, negative values will move it toward the top. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to bottom_right then setting a positive offset isn't going to do anything: it's already as far down as it can go.", - ) - .req_selectscalemode_upscale_mode( - tooltip="Allows setting the scaling mode when width/height is smaller than the requested size.", - default="nearest-exact", - ) - .req_selectscalemode_downscale_mode( - tooltip="Allows setting the scaling mode when width/height is larger than the requested size and downscale_strategy is set to 'scale'.", - default="nearest-exact", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .req_customnoise_custom_noise(), + lambda: ( + NoiseNoChainInputTypes() + .req_int_width( + default=1152, + min=16, + max=1024 * 1024 * 1024, + step=8, + tooltip="Note: This should almost always be set to a higher value than the image you're actually sampling.", + ) + .req_int_height( + default=1152, + min=16, + max=1024 * 1024 * 1024, + step=8, + tooltip="Note: This should almost always be set to a higher value than the image you're actually sampling.", + ) + .req_field_downscale_strategy( + ("crop", "scale"), + default="crop", + tooltip="Scaling noise is something you'd pretty much only use to create weird effects. For normal workflows, leave this on crop.", + ) + .req_field_initial_reference( + ("prefer_crop", "prefer_scale"), + default="prefer_crop", + tooltip="The initial latent the noise sampler uses as a reference may not match the requested width/height. This setting controls whether to crop or scale. Note: Cropping can only occur when the initial reference is larger than width/height in both dimensions which is unlikely (and not recommended).", + ) + .req_field_crop_mode( + ( + "center", + "top_left", + "top_center", + "top_right", + "center_left", + "center_right", + "bottom_left", + "bottom_center", + "bottom_right", + ), + default="center", + tooltip="Note: Crops will have a bias toward the lower number when the size isn't divisible by two. For example, a center crop of size 3 from (0, 1, 2, 3, 4, 5) will result in (1, 2, 3).", + ) + .req_int_crop_offset_horizontal( + default=0, + step=8, + min=-8000, + max=8000, + tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the right, negative values will move it toward the left. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to top_right then setting a positive offset isn't going to do anything: it's already as far right as it can go.", + ) + .req_int_crop_offset_vertical( + default=0, + step=8, + min=-8000, + max=8000, + tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the bottom, negative values will move it toward the top. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to bottom_right then setting a positive offset isn't going to do anything: it's already as far down as it can go.", + ) + .req_selectscalemode_upscale_mode( + tooltip="Allows setting the scaling mode when width/height is smaller than the requested size.", + default="nearest-exact", + ) + .req_selectscalemode_downscale_mode( + tooltip="Allows setting the scaling mode when width/height is larger than the requested size and downscale_strategy is set to 'scale'.", + default="nearest-exact", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .req_customnoise_custom_noise() + ), ) @classmethod @@ -571,72 +593,74 @@ class SonarResizedNoiseAdvNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNode DESCRIPTION = "Custom noise type that allows resizing another noise item. Advanced version of the SonarResizedNoise node." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_float_width( - default=32.0, - min=0.0, - tooltip="Note: In absolute mode, this should almost always be set to a higher value than the image you're actually sampling.", - ) - .req_float_height( - default=32.0, - min=0.0, - tooltip="Note: In absolute mode, this should almost always be set to a higher value than the image you're actually sampling.", - ) - .req_field_spatial_mode( - ("relative", "percentage", "absolute"), - default="relative", - tooltip="In relative mode, the sizes control padding. In percentage mode, the values will be interpreted as percentages of the origal size where 1.0 would be 100%, 0.5 would be 50% and so on. In absolute mode, this controls the absolute size.", - ) - .req_int_spatial_compression( - min=1, - default=8, - tooltip="Most image models use 8x spatial compression. When spatial mode is absolute, the sizes will be multiplied by this value. It is ignored in percentage mode.", - ) - .req_field_downscale_strategy( - ("crop", "scale"), - default="crop", - tooltip="Scaling noise is something you'd pretty much only use to create weird effects. For normal workflows, leave this on crop.", - ) - .req_field_initial_reference( - ("prefer_crop", "prefer_scale"), - default="prefer_crop", - tooltip="The initial latent the noise sampler uses as a reference may not match the requested width/height. This setting controls whether to crop or scale. Note: Cropping can only occur when the initial reference is larger than width/height in both dimensions which is unlikely (and not recommended).", - ) - .req_field_crop_mode( - ( - "center", - "top_left", - "top_center", - "top_right", - "center_left", - "center_right", - "bottom_left", - "bottom_center", - "bottom_right", - ), - default="center", - tooltip="Note: Crops will have a bias toward the lower number when the size isn't divisible by two. For example, a center crop of size 3 from (0, 1, 2, 3, 4, 5) will result in (1, 2, 3).", - ) - .req_int_crop_offset_horizontal( - default=0, - tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the right, negative values will move it toward the left. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to top_right then setting a positive offset isn't going to do anything: it's already as far right as it can go.", - ) - .req_int_crop_offset_vertical( - default=0, - tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the bottom, negative values will move it toward the top. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to bottom_right then setting a positive offset isn't going to do anything: it's already as far down as it can go.", - ) - .req_selectscalemode_upscale_mode( - tooltip="Allows setting the scaling mode when width/height is smaller than the requested size.", - default="nearest-exact", - ) - .req_selectscalemode_downscale_mode( - tooltip="Allows setting the scaling mode when width/height is larger than the requested size and downscale_strategy is set to 'scale'.", - default="nearest-exact", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .req_customnoise_custom_noise(), + lambda: ( + NoiseNoChainInputTypes() + .req_float_width( + default=32.0, + min=0.0, + tooltip="Note: In absolute mode, this should almost always be set to a higher value than the image you're actually sampling.", + ) + .req_float_height( + default=32.0, + min=0.0, + tooltip="Note: In absolute mode, this should almost always be set to a higher value than the image you're actually sampling.", + ) + .req_field_spatial_mode( + ("relative", "percentage", "absolute"), + default="relative", + tooltip="In relative mode, the sizes control padding. In percentage mode, the values will be interpreted as percentages of the origal size where 1.0 would be 100%, 0.5 would be 50% and so on. In absolute mode, this controls the absolute size.", + ) + .req_int_spatial_compression( + min=1, + default=8, + tooltip="Most image models use 8x spatial compression. When spatial mode is absolute, the sizes will be multiplied by this value. It is ignored in percentage mode.", + ) + .req_field_downscale_strategy( + ("crop", "scale"), + default="crop", + tooltip="Scaling noise is something you'd pretty much only use to create weird effects. For normal workflows, leave this on crop.", + ) + .req_field_initial_reference( + ("prefer_crop", "prefer_scale"), + default="prefer_crop", + tooltip="The initial latent the noise sampler uses as a reference may not match the requested width/height. This setting controls whether to crop or scale. Note: Cropping can only occur when the initial reference is larger than width/height in both dimensions which is unlikely (and not recommended).", + ) + .req_field_crop_mode( + ( + "center", + "top_left", + "top_center", + "top_right", + "center_left", + "center_right", + "bottom_left", + "bottom_center", + "bottom_right", + ), + default="center", + tooltip="Note: Crops will have a bias toward the lower number when the size isn't divisible by two. For example, a center crop of size 3 from (0, 1, 2, 3, 4, 5) will result in (1, 2, 3).", + ) + .req_int_crop_offset_horizontal( + default=0, + tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the right, negative values will move it toward the left. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to top_right then setting a positive offset isn't going to do anything: it's already as far right as it can go.", + ) + .req_int_crop_offset_vertical( + default=0, + tooltip="This offsets the cropped view by the specified size. Positive values will move it toward the bottom, negative values will move it toward the top. The offsets will be adjusted to to fit in the available space. For example, if you have crop_mode set to bottom_right then setting a positive offset isn't going to do anything: it's already as far down as it can go.", + ) + .req_selectscalemode_upscale_mode( + tooltip="Allows setting the scaling mode when width/height is smaller than the requested size.", + default="nearest-exact", + ) + .req_selectscalemode_downscale_mode( + tooltip="Allows setting the scaling mode when width/height is larger than the requested size and downscale_strategy is set to 'scale'.", + default="nearest-exact", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .req_customnoise_custom_noise() + ), ) @classmethod @@ -683,53 +707,55 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "Custom noise type that allows filtering noise based on the quantile" INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_custom_noise( - tooltip="Custom noise type to filter.", - ) - .req_float_quantile( - default=0.85, - min=-1.0, - max=1.0, - step=0.001, - round=False, - tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme. (Experimental) You can also use a negative quantile to consider values closest to 0 to be 'extreme'.", - ) - .req_field_dim( - ("global", "0", "1", "2", "3", "4"), - default="1", - tooltip="Controls what dimensions quantile normalization uses. Dimensions start from 0. Image latents have dimensions: batch, channel, row, column. Video latents have dimensions: batch, channel, frame, row, column.", - ) - .req_bool_flatten( - default=True, - tooltip="Controls whether the noise is flattened before quantile normalization. You can try disabling it but they may have a very strong row/column influence.", - ) - .req_float_norm_factor( - default=1.0, - min=0.00001, - max=10000.0, - step=0.001, - tooltip="Multiplier on the input noise just before it is clipped to the quantile min/max. Generally should be left at the default.", - ) - .req_float_norm_power( - default=0.5, - min=-10000.0, - max=10000.0, - step=0.001, - tooltip="The absolute value of the noise is raised to this power after it is clipped to the quantile min/max. You can use negative values here, but anything below -0.3 will probably produce pretty strange effects. Generally should be left at the default.", - ) - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized before quantile filtering occurs.", - ) - .req_normalizetristate_normalize( - default="disabled", - tooltip="Controls whether the generated noise is normalized to 1.0 strength after quantile filtering.", - ) - .req_field_strategy( - tuple(utils.quantile_handlers.keys()), - default="clamp", - tooltip="Determines how to treat outliers. zero and reverse_zero modes are only useful if you're going to do something like add the result to some other noise. zero will return zero for anything outside the quantile range, reverse_zero only _keeps_ the outliers and zeros everything else.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_custom_noise( + tooltip="Custom noise type to filter.", + ) + .req_float_quantile( + default=0.85, + min=-1.0, + max=1.0, + step=0.001, + round=False, + tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme. (Experimental) You can also use a negative quantile to consider values closest to 0 to be 'extreme'.", + ) + .req_field_dim( + ("global", "0", "1", "2", "3", "4"), + default="1", + tooltip="Controls what dimensions quantile normalization uses. Dimensions start from 0. Image latents have dimensions: batch, channel, row, column. Video latents have dimensions: batch, channel, frame, row, column.", + ) + .req_bool_flatten( + default=True, + tooltip="Controls whether the noise is flattened before quantile normalization. You can try disabling it but they may have a very strong row/column influence.", + ) + .req_float_norm_factor( + default=1.0, + min=0.00001, + max=10000.0, + step=0.001, + tooltip="Multiplier on the input noise just before it is clipped to the quantile min/max. Generally should be left at the default.", + ) + .req_float_norm_power( + default=0.5, + min=-10000.0, + max=10000.0, + step=0.001, + tooltip="The absolute value of the noise is raised to this power after it is clipped to the quantile min/max. You can use negative values here, but anything below -0.3 will probably produce pretty strange effects. Generally should be left at the default.", + ) + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized before quantile filtering occurs.", + ) + .req_normalizetristate_normalize( + default="disabled", + tooltip="Controls whether the generated noise is normalized to 1.0 strength after quantile filtering.", + ) + .req_field_strategy( + tuple(utils.quantile_handlers.keys()), + default="clamp", + tooltip="Determines how to treat outliers. zero and reverse_zero modes are only useful if you're going to do something like add the result to some other noise. zero will return zero for anything outside the quantile range, reverse_zero only _keeps_ the outliers and zeros everything else.", + ) ), ) @@ -771,23 +797,25 @@ class SonarShuffledNoiseNode(SonarCustomNoiseNodeBase): ) INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_custom_noise(tooltip="Custom noise type to filter.") - .req_string_dims( - default="1,-2,-1", - tooltip="Comma separated list of dimensions to shuffle. May be negative to count from the end.", - ) - .req_string_percentages( - default="1.0,0.25,0.25", - tooltip="Comma separated list of percentages (0.0 to 1.0 for 100%) of elements to shuffle. Paired with the list of dimensions and wrap if it's shorter. For example, if you specified three dimensions and two percentages, the third dimension in the list would use the first percentage again.", - ) - .req_bool_fork_rng( - default=True, - tooltip="When enabled, the RNG state will be forked to generate the shuffle values.", - ) - .req_bool_no_identity( - default=True, - tooltip="When enabled, ensures shuffle never ends up selecting the original element.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_custom_noise(tooltip="Custom noise type to filter.") + .req_string_dims( + default="1,-2,-1", + tooltip="Comma separated list of dimensions to shuffle. May be negative to count from the end.", + ) + .req_string_percentages( + default="1.0,0.25,0.25", + tooltip="Comma separated list of percentages (0.0 to 1.0 for 100%) of elements to shuffle. Paired with the list of dimensions and wrap if it's shorter. For example, if you specified three dimensions and two percentages, the third dimension in the list would use the first percentage again.", + ) + .req_bool_fork_rng( + default=True, + tooltip="When enabled, the RNG state will be forked to generate the shuffle values.", + ) + .req_bool_no_identity( + default=True, + tooltip="When enabled, ensures shuffle never ends up selecting the original element.", + ) ), ) @@ -825,24 +853,26 @@ class SonarPatternBreakNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "Custom noise type that allows breaking patterns in the noise with configurable strength" INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_custom_noise(tooltip="Custom noise type to filter.") - .req_float_detail_level( - default=0.0, - tooltip="Controls the detail level of the noise when break_pattern is non-zero. No effect when strength is 0.", - ) - .req_selectblend( - tooltip="Function to use for blending original noise with pattern broken noise. If you have ComfyUI-bleh then you will have access to many more blend modes.", - ) - .req_float_percentage( - default=1.0, - min=0.0, - max=1.0, - tooltip="Percentage of elements to shuffle in the specified dimensions.", - ) - .req_bool_restore_scale( - default=True, - tooltip="Controls whether the original min/max values get preserved. Not sure which is better, it is slightly slower to do this though.", + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_custom_noise(tooltip="Custom noise type to filter.") + .req_float_detail_level( + default=0.0, + tooltip="Controls the detail level of the noise when break_pattern is non-zero. No effect when strength is 0.", + ) + .req_selectblend( + tooltip="Function to use for blending original noise with pattern broken noise. If you have ComfyUI-bleh then you will have access to many more blend modes.", + ) + .req_float_percentage( + default=1.0, + min=0.0, + max=1.0, + tooltip="Percentage of elements to shuffle in the specified dimensions.", + ) + .req_bool_restore_scale( + default=True, + tooltip="Controls whether the original min/max values get preserved. Not sure which is better, it is slightly slower to do this though.", + ) ), ) @@ -909,21 +939,23 @@ yh_scales: 1.0 """ INPUT_TYPES = SonarLazyInputTypes( - lambda _yaml_placeholder=_yaml_placeholder: NoiseChainInputTypes() - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .opt_customnoise_custom_noise( - tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise.", - ) - .opt_customnoise_custom_noise_high( - tooltip="Optional: Custom noise input. If unconnected will use the same noise generator as custom_noise. However, if you do connect it this noise will be used for the high-frequency side of the wavelet.", - ) - .opt_yaml(placeholder=_yaml_placeholder), + lambda _yaml_placeholder=_yaml_placeholder: ( + NoiseChainInputTypes() + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_customnoise_custom_noise( + tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise.", + ) + .opt_customnoise_custom_noise_high( + tooltip="Optional: Custom noise input. If unconnected will use the same noise generator as custom_noise. However, if you do connect it this noise will be used for the high-frequency side of the wavelet.", + ) + .opt_yaml(placeholder=_yaml_placeholder) + ), ) @classmethod @@ -963,58 +995,60 @@ class SonarScatternetFilteredNoiseNode( DESCRIPTION = "Custom noise type that allows filtering noise using a scatternet (basically wavelets). Requires the pytorch_wavelets package to be installed in your Python environment. Can be used to do stuff like take the higher frequency components of a very low-frequency noise type such as Pyramid. Currently only works with 4D latents." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_string_padding_mode( - default="symmetric", - tooltip="This is just passed to the pytorch_wavelets scatternet constructor. Valid padding modes that I know of (second order only supports symmetric and zero): symmetric, reflect, zero, periodization, constant, replicate, periodic", - ) - .req_bool_use_symmetric_filter( - default=False, - tooltip="Slower, but possibly higher quality.", - ) - .req_float_magbias( - default=1e-02, - min=-1000.0, - max=1000.0, - tooltip="Magnitude bias. Changing it doesn't seem to affect anything, but you can try.", - ) - .req_float_output_offset( - default=0.0, - min=-100000.0, - max=100000.0, - tooltip="Controls where the output starts. The beginning is the low frequency bands, the end is high frequencies. If less than 1 (positive or negative) it will be treated as a percentage into the dimension. Negative values count from the end.", - ) - .req_field_output_mode( - ( - "channels_adjusted", - "flat_adjusted", - "channels", - "flat", - "channels_scaled", - "flat_scaled", - ), - default="channels_adjusted", - tooltip="The normal scatternet reduces the spatial dimensions 2x, the second order one 4x. The adjusted modes will generate larger noise (in the spatial dimensions) to compensate, this is slower but gives you a lot more room to work with. The scaled modes will just scale the noise to compensate (likely doesn't work well). Modes that start with channels will index along the channel dimension, otherwise the indexing will be flat (after the batch dimension). Note: I recommend channels_adjusted mode, it's very possible the offset indexing math is wrong for other modes.", - ) - .req_int_scatternet_order( - default=1, - min=-3, - max=3, - tooltip="Each order increases the number of channels exponentially. You can use a primitive node to bypass the limit of 3 here if you're a crazy person, the code will handle any value but you're very likely to die of old age or run out of VRAM or both if you go above 3 (and even that is stretching it). You can set this to 0 to disable scatternet filtering quickly. Negative values are the same as positive ones here with one exception: there's a specialized 2nd order scatternet which will be used by default for order 2, however it may not support the normal parameters (like padding modes). Use -2 here if you just want to stack two normal scatternet layers instead.", - ) - .req_bool_per_channel_scatternet( - default=False, - tooltip="Runs the scatternet on each channel separately. May be very slow. Models like SDXL use 4 channels, models like Flux have 16. Enabling this may help with non-adjusted output modes.", - ) - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized before scatternet filtering occurs.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .opt_customnoise_custom_noise( - tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise.", + lambda: ( + NoiseChainInputTypes() + .req_string_padding_mode( + default="symmetric", + tooltip="This is just passed to the pytorch_wavelets scatternet constructor. Valid padding modes that I know of (second order only supports symmetric and zero): symmetric, reflect, zero, periodization, constant, replicate, periodic", + ) + .req_bool_use_symmetric_filter( + default=False, + tooltip="Slower, but possibly higher quality.", + ) + .req_float_magbias( + default=1e-02, + min=-1000.0, + max=1000.0, + tooltip="Magnitude bias. Changing it doesn't seem to affect anything, but you can try.", + ) + .req_float_output_offset( + default=0.0, + min=-100000.0, + max=100000.0, + tooltip="Controls where the output starts. The beginning is the low frequency bands, the end is high frequencies. If less than 1 (positive or negative) it will be treated as a percentage into the dimension. Negative values count from the end.", + ) + .req_field_output_mode( + ( + "channels_adjusted", + "flat_adjusted", + "channels", + "flat", + "channels_scaled", + "flat_scaled", + ), + default="channels_adjusted", + tooltip="The normal scatternet reduces the spatial dimensions 2x, the second order one 4x. The adjusted modes will generate larger noise (in the spatial dimensions) to compensate, this is slower but gives you a lot more room to work with. The scaled modes will just scale the noise to compensate (likely doesn't work well). Modes that start with channels will index along the channel dimension, otherwise the indexing will be flat (after the batch dimension). Note: I recommend channels_adjusted mode, it's very possible the offset indexing math is wrong for other modes.", + ) + .req_int_scatternet_order( + default=1, + min=-3, + max=3, + tooltip="Each order increases the number of channels exponentially. You can use a primitive node to bypass the limit of 3 here if you're a crazy person, the code will handle any value but you're very likely to die of old age or run out of VRAM or both if you go above 3 (and even that is stretching it). You can set this to 0 to disable scatternet filtering quickly. Negative values are the same as positive ones here with one exception: there's a specialized 2nd order scatternet which will be used by default for order 2, however it may not support the normal parameters (like padding modes). Use -2 here if you just want to stack two normal scatternet layers instead.", + ) + .req_bool_per_channel_scatternet( + default=False, + tooltip="Runs the scatternet on each channel separately. May be very slow. Models like SDXL use 4 channels, models like Flux have 16. Enabling this may help with non-adjusted output modes.", + ) + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized before scatternet filtering occurs.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_customnoise_custom_noise( + tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise.", + ) ), ) @@ -1054,6 +1088,86 @@ class SonarScatternetFilteredNoiseNode( ) +class SonarNoveltyFilteredNoiseNode( + SonarCustomNoiseNodeBase, + SonarNormalizeNoiseNodeMixin, +): + DESCRIPTION = "TBD" + + INPUT_TYPES = SonarLazyInputTypes( + lambda: ( + NoiseChainInputTypes() + .req_int_skip_initial( + default=1, + min=0, + max=9999, + ) + .req_int_iters_per_call( + default=1, + min=1, + max=256, + ) + .req_float_blend_ratio( + default=1.0, + min=-100000.0, + max=100000.0, + tooltip="TBD", + ) + .req_selectblend_blend_mode(default="ortho") + .req_float_update_blend_ratio( + default=1.0, + min=-100000.0, + max=100000.0, + tooltip="TBD", + ) + .req_selectblend_update_blend_mode(default="weighted_average") + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized before scatternet filtering occurs.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_customnoise_custom_noise( + tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise.", + ) + ), + ) + + @classmethod + def get_item_class(cls): + return noise.NoveltyFilteredNoise + + def go( + self, + *, + factor: float, + rescale: float, + skip_initial: int, + iters_per_call: int, + blend_ratio: float, + blend_mode: str, + update_blend_ratio: float, + update_blend_mode: str, + normalize: str, + normalize_noise: bool, + custom_noise: object | None = None, + ): + return super().go( + factor, + rescale=rescale, + noise=custom_noise, + skip_initial=skip_initial, + iters_per_call=iters_per_call, + blend_mode=blend_mode, + blend_ratio=blend_ratio, + update_blend_mode=update_blend_mode, + update_blend_ratio=update_blend_ratio, + normalize_noise=normalize_noise, + normalize=normalize, + ) + + class SonarRippleFilteredNoiseNode( SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin, @@ -1063,59 +1177,61 @@ class SonarRippleFilteredNoiseNode( ) INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_customnoise_custom_noise() - .req_field_mode( - ("sin", "cos", "sin_copysign", "cos_copysign"), - default="cos", - tooltip="Function to use for rippling. The copysign variations are not recommended, they will force the noise to the sign of the wave (whether it's above or below the midline) which has an extremely strong effect. If you want to try it, use something like a 1:16 ratio or higher with normal noise.", - ) - .req_int_dim( - default=-1, - min=-100, - max=100, - tooltip="Dimension to use for the ripple effect. Negative dimensions count from the end where -1 is the last dimension.", - ) - .req_bool_flatten( - default=False, - tooltip="When enabled, the noise will be flattened starting from (and including) the specified dimension.", - ) - .req_float_offset( - default=0.0, - min=-10000, - max=10000.0, - tooltip="Simple addition to the base value used for the wave.", - ) - .req_float_roll( - default=0.0, - min=-10000, - max=10000.0, - tooltip="Rolls the wave by this many elements each time the noise generator is called. Negative values roll backward.", - ) - .req_float_amplitude_high( - default=0.25, - min=-10000, - max=10000.0, - tooltip="Scale for noise at the highest point of the wave. This adds to the base value (respecting sign). For example, if set to 0.25 you will get noise * 1.25 at that point. It's also possible to use negative values, -0.25 will result in noise * -1.25.", - ) - .req_float_amplitude_low( - default=0.15, - min=-10000, - max=10000.0, - tooltip="Scale for noise at the lowest point of the wave. This subtracts from the base value (respecting sign). For example, if set to 0.25 you will get noise * 0.75 at that point. It's also possible to use negative values, -0.25 will result in noise * -0.75.", - ) - .req_float_period( - default=3.0, - min=-10000, - max=10000.0, - tooltip="Number of oscillations along the specified dimension.", - ) - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", - ) - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + lambda: ( + NoiseChainInputTypes() + .req_customnoise_custom_noise() + .req_field_mode( + ("sin", "cos", "sin_copysign", "cos_copysign"), + default="cos", + tooltip="Function to use for rippling. The copysign variations are not recommended, they will force the noise to the sign of the wave (whether it's above or below the midline) which has an extremely strong effect. If you want to try it, use something like a 1:16 ratio or higher with normal noise.", + ) + .req_int_dim( + default=-1, + min=-100, + max=100, + tooltip="Dimension to use for the ripple effect. Negative dimensions count from the end where -1 is the last dimension.", + ) + .req_bool_flatten( + default=False, + tooltip="When enabled, the noise will be flattened starting from (and including) the specified dimension.", + ) + .req_float_offset( + default=0.0, + min=-10000, + max=10000.0, + tooltip="Simple addition to the base value used for the wave.", + ) + .req_float_roll( + default=0.0, + min=-10000, + max=10000.0, + tooltip="Rolls the wave by this many elements each time the noise generator is called. Negative values roll backward.", + ) + .req_float_amplitude_high( + default=0.25, + min=-10000, + max=10000.0, + tooltip="Scale for noise at the highest point of the wave. This adds to the base value (respecting sign). For example, if set to 0.25 you will get noise * 1.25 at that point. It's also possible to use negative values, -0.25 will result in noise * -1.25.", + ) + .req_float_amplitude_low( + default=0.15, + min=-10000, + max=10000.0, + tooltip="Scale for noise at the lowest point of the wave. This subtracts from the base value (respecting sign). For example, if set to 0.25 you will get noise * 0.75 at that point. It's also possible to use negative values, -0.25 will result in noise * -0.75.", + ) + .req_float_period( + default=3.0, + min=-10000, + max=10000.0, + tooltip="Number of oscillations along the specified dimension.", + ) + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", + ) + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) ), ) @@ -1166,68 +1282,70 @@ class SonarNormalizeNoiseToScaleNode( DESCRIPTION = "Custom noise type that allows precisely controling noise normalization. The default range of -4.5 to 4.5 is roughly what you'd get from 10,000 items of Gaussian noise." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_customnoise_custom_noise() - .req_float_min_negative_value( - default=-4.5, - min=-10000.0, - max=10000.0, - tooltip="In simple mode, this is just the lowest value in the range (and can be positive, despite the name). In advanced mode, this controls the minimum negative value. If you set it to 0 or higher then normalization will leave negative values alone.", - ) - .req_float_max_negative_value( - default=0.0, - min=-10000.0, - max=10000.0, - tooltip="Not used in simple mode. In advanced mode, this controls the maximum negative value. If you set it to 0 or higher, a maximum negative value will be automatically determined from negative value closest (but not equal to) zero.", - ) - .req_float_min_positive_value( - default=0.0, - min=-10000.0, - max=10000.0, - tooltip="Not used in simple mode. In advanced mode, this controls the minmum positive value. If you set it to 0 or lower, a minimum positive value will be automatically determined from positive value closest (but not equal to) zero.", - ) - .req_float_max_positive_value( - default=4.5, - min=-10000.0, - max=10000.0, - tooltip="In simple mode, this is just the highest value in the range (and can be negative, despite the name). In advanced mode, this controls the maximum positive value. If you set it to 0 or lower then normalization will leave positive values alone.", - ) - .req_field_mode( - ("simple", "advanced"), - default="simple", - tooltip="There are several modes:\nsimple: The noise will be rebalanced to be in between min_negative_value and max_positive_value. Though it sounds weird, you don't need to respect the positive/negative in the names. It is just treated as a simple range.\nadvanced: Positive and negative values in the noise are separately rebalanced to be between the specified ranges. If you set max_negative_value to something positive or min_positive_value to something negative this will automatically determine whatever the closest value to zero is for each sign. Additionally, if you set max_positive_value to something negative or min_negative_value to something positive then values for that sign will be left alone.", - ) - .req_string_dims( - default="-3, -2, -1", - tooltip="A comma separated list of dimensions which can be negative to count from the end. This behaves differently in advanced mode: If left blank, normalization will be global. If set to anything, normalization will be over each batch item separately. The actual values of the dimensions are ignored in advanced mode currently.", - ) - .req_string_std_dims( - default="-3, -2, -1", - tooltip="A comma separated list of dimensions which can be negative to count from the end.", - ) - .req_float_std_multiplier( - default=1.0, - min=-10000.0, - max=10000.0, - tooltip="Multiplier on the distance of the std from 1.0. The noise will be divided by this. You can set it to 1.0 to skip the division. When enabled, the division occurs before the final normalize and scaling and after the min/max value parameters are applied.", - ) - .req_string_mean_dims( - default="-3, -2, -1", - tooltip="A comma separated list of dimensions which can be negative to count from the end.", - ) - .req_float_mean_multiplier( - default=1.0, - min=-10000.0, - max=10000.0, - tooltip="Multiplier on the mean of the noise. The mean will be subtracted from the noise if it's not 0. This occurs before the final normalize and scaling and after the min/max value parameters are applied.", - ) - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized immediately after generation.", - ) - .req_normalizetristate_normalize( - default="disabled", - tooltip="Controls whether the generated noise is normalized to 1.0 strength. Enabling this does the same thing as the default mean/std settings.", + lambda: ( + NoiseChainInputTypes() + .req_customnoise_custom_noise() + .req_float_min_negative_value( + default=-4.5, + min=-10000.0, + max=10000.0, + tooltip="In simple mode, this is just the lowest value in the range (and can be positive, despite the name). In advanced mode, this controls the minimum negative value. If you set it to 0 or higher then normalization will leave negative values alone.", + ) + .req_float_max_negative_value( + default=0.0, + min=-10000.0, + max=10000.0, + tooltip="Not used in simple mode. In advanced mode, this controls the maximum negative value. If you set it to 0 or higher, a maximum negative value will be automatically determined from negative value closest (but not equal to) zero.", + ) + .req_float_min_positive_value( + default=0.0, + min=-10000.0, + max=10000.0, + tooltip="Not used in simple mode. In advanced mode, this controls the minmum positive value. If you set it to 0 or lower, a minimum positive value will be automatically determined from positive value closest (but not equal to) zero.", + ) + .req_float_max_positive_value( + default=4.5, + min=-10000.0, + max=10000.0, + tooltip="In simple mode, this is just the highest value in the range (and can be negative, despite the name). In advanced mode, this controls the maximum positive value. If you set it to 0 or lower then normalization will leave positive values alone.", + ) + .req_field_mode( + ("simple", "advanced"), + default="simple", + tooltip="There are several modes:\nsimple: The noise will be rebalanced to be in between min_negative_value and max_positive_value. Though it sounds weird, you don't need to respect the positive/negative in the names. It is just treated as a simple range.\nadvanced: Positive and negative values in the noise are separately rebalanced to be between the specified ranges. If you set max_negative_value to something positive or min_positive_value to something negative this will automatically determine whatever the closest value to zero is for each sign. Additionally, if you set max_positive_value to something negative or min_negative_value to something positive then values for that sign will be left alone.", + ) + .req_string_dims( + default="-3, -2, -1", + tooltip="A comma separated list of dimensions which can be negative to count from the end. This behaves differently in advanced mode: If left blank, normalization will be global. If set to anything, normalization will be over each batch item separately. The actual values of the dimensions are ignored in advanced mode currently.", + ) + .req_string_std_dims( + default="-3, -2, -1", + tooltip="A comma separated list of dimensions which can be negative to count from the end.", + ) + .req_float_std_multiplier( + default=1.0, + min=-10000.0, + max=10000.0, + tooltip="Multiplier on the distance of the std from 1.0. The noise will be divided by this. You can set it to 1.0 to skip the division. When enabled, the division occurs before the final normalize and scaling and after the min/max value parameters are applied.", + ) + .req_string_mean_dims( + default="-3, -2, -1", + tooltip="A comma separated list of dimensions which can be negative to count from the end.", + ) + .req_float_mean_multiplier( + default=1.0, + min=-10000.0, + max=10000.0, + tooltip="Multiplier on the mean of the noise. The mean will be subtracted from the noise if it's not 0. This occurs before the final normalize and scaling and after the min/max value parameters are applied.", + ) + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized immediately after generation.", + ) + .req_normalizetristate_normalize( + default="disabled", + tooltip="Controls whether the generated noise is normalized to 1.0 strength. Enabling this does the same thing as the default mean/std settings.", + ) ), ) @@ -1286,31 +1404,33 @@ class SonarPerDimNoiseNode( DESCRIPTION = "Custom noise type that allows calling the noise sampler multiple times along a dimension. Can be useful for stuff like moving slices of 3D Perlin noise into the batch dimension." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_customnoise_custom_noise() - .req_int_dim( - default=0, - min=-100, - max=100, - tooltip="Dimension to use. The default usually corresponds to the batch. Be careful using dimensions above 1 as those tend to be spatial and you might end up calling a slow noise sampler hundreds of times.", - ) - .req_bool_shrink_dim( - default=False, - tooltip="When enabled, the reference latent will be chunk_size in the specified dimension. When disabled, noise will be generated according to the initial latent size and then sliced along the specified dimension. Enabling it should be considerably faster/more memory efficient but may not work well for some noise types.", - ) - .req_int_chunk_size( - default=1, - min=1, - max=10000, - tooltip="Can be used to control how many times the noise sampler is called. For example, if you have dim=0, chunk_size=2 and are dealing with a batch of 4, this will call the noise sampler twice, taking the first two items from the first call and the last two items from the second call.", - ) - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized initially.", - ) - .req_normalizetristate_normalize( - default="disabled", - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + lambda: ( + NoiseChainInputTypes() + .req_customnoise_custom_noise() + .req_int_dim( + default=0, + min=-100, + max=100, + tooltip="Dimension to use. The default usually corresponds to the batch. Be careful using dimensions above 1 as those tend to be spatial and you might end up calling a slow noise sampler hundreds of times.", + ) + .req_bool_shrink_dim( + default=True, + tooltip="When enabled, the reference latent will be chunk_size in the specified dimension. When disabled, noise will be generated according to the initial latent size and then sliced along the specified dimension. Enabling it should be considerably faster/more memory efficient but may not work well for some noise types.", + ) + .req_int_chunk_size( + default=1, + min=1, + max=10000, + tooltip="Can be used to control how many times the noise sampler is called. For example, if you have dim=0, chunk_size=2 and are dealing with a batch of 4, this will call the noise sampler twice, taking the first two items from the first call and the last two items from the second call.", + ) + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized initially.", + ) + .req_normalizetristate_normalize( + default="disabled", + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) ), ) @@ -1353,35 +1473,37 @@ class SonarLatentOperationFilteredNoiseNode( DESCRIPTION = "Custom noise type that allows filtering noise with a LATENT_OPERATION. If you connect more than one, the operations will be run in sequence." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_customnoise_custom_noise() - .req_bool_normalize_noise( - default=False, - tooltip="Controls whether the noise source is normalized initially.", - ) - .req_normalizetristate_normalize( - default="disabled", - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .opt_field_operation_1( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_2( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_3( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_4( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_5( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + lambda: ( + NoiseChainInputTypes() + .req_customnoise_custom_noise() + .req_bool_normalize_noise( + default=False, + tooltip="Controls whether the noise source is normalized initially.", + ) + .req_normalizetristate_normalize( + default="disabled", + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_field_operation_1( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_2( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_3( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_4( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_5( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) ), ) @@ -1427,58 +1549,60 @@ class SonarCustomNoiseParametersNode( DESCRIPTION = "Custom noise type that allows setting parameters like dtype or forking the RNG." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseNoChainInputTypes() - .req_customnoise_custom_noise() - .req_int_rng_state_offset( - default=0, - min=0, - tooltip="In other words, seed. Avoiding using the word seed here to suppress ComfyUI's annoying default behavior. If you want stuff like auto-increment you can connect an INT primitive node.", - ) - .req_field_rng_offset_mode( - ("disabled", "override", "add"), - default="disabled", - tooltip="Controls the seed passed to the noise sampler and also seeding when rng_mode is set to separate. Most noise samplers don't care about the seed so this generally will only have an effect in when rng_mode is set to separate.", - ) - .req_field_rng_mode( - ("default", "separate", "fork"), - default="default", - tooltip="default mode doesn't do anything special. separate mode creates a generator and saves/restores the state when generating noise (also includes the Python random module). fork uses the existing RNG state (for both Torch and Python random module) but restores it to whatever it was before the custom noise was called.", - ) - .req_bool_frames_to_channels( - tooltip="Only applicable for 5D latents (video models). Will move the frame dimension into channels, may be necessary if a noise type can't deal with 5D latents directly. It's safe to enable this for all models.", - ) - .req_bool_ensure_square_aspect_ratio( - tooltip="Will rearrange the height/width sizes to be square, padding with zeros if necessary. May help some noise types work better with extreme aspect ratios, can also deal with 3D (1 spatial dimension) latents.", - ) - .req_bool_fix_invalid( - tooltip="Replaces any NaNs or infinite values with 0.", - ) - .req_field_override_dtype( - ( - "default", - "float64", - "float32", - "float16", - "bfloat16", - "float8_e4m3fn", - "float8_e4m3fnuz", - "float8_e5m2", - "float8_e5m2fnuz", - "float8_e8m0fnu", - "int64", - "int32", - "int16", - "int8", - ), - default="default", - tooltip="Can be used to override the dtype the noise is generated with. Not all noise generators support all types. I don't recommend using the int or float8 types. Probably the most useful override is float64.", - ) - .req_field_override_device( - ("default", "cpu", "gpu"), - default="default", - tooltip="default just uses whatever device normally would be used. gpu will use ComfyUI's default GPU device and also toggle the cpu_noise flag off. cpu will use the CPU device and toggle the cpu_noise flag on.", - ) - .req_normalizetristate_normalize(), + lambda: ( + NoiseNoChainInputTypes() + .req_customnoise_custom_noise() + .req_int_rng_state_offset( + default=0, + min=0, + tooltip="In other words, seed. Avoiding using the word seed here to suppress ComfyUI's annoying default behavior. If you want stuff like auto-increment you can connect an INT primitive node.", + ) + .req_field_rng_offset_mode( + ("disabled", "override", "add"), + default="disabled", + tooltip="Controls the seed passed to the noise sampler and also seeding when rng_mode is set to separate. Most noise samplers don't care about the seed so this generally will only have an effect in when rng_mode is set to separate.", + ) + .req_field_rng_mode( + ("default", "separate", "fork"), + default="default", + tooltip="default mode doesn't do anything special. separate mode creates a generator and saves/restores the state when generating noise (also includes the Python random module). fork uses the existing RNG state (for both Torch and Python random module) but restores it to whatever it was before the custom noise was called.", + ) + .req_bool_frames_to_channels( + tooltip="Only applicable for 5D latents (video models). Will move the frame dimension into channels, may be necessary if a noise type can't deal with 5D latents directly. It's safe to enable this for all models.", + ) + .req_bool_ensure_square_aspect_ratio( + tooltip="Will rearrange the height/width sizes to be square, padding with zeros if necessary. May help some noise types work better with extreme aspect ratios, can also deal with 3D (1 spatial dimension) latents.", + ) + .req_bool_fix_invalid( + tooltip="Replaces any NaNs or infinite values with 0.", + ) + .req_field_override_dtype( + ( + "default", + "float64", + "float32", + "float16", + "bfloat16", + "float8_e4m3fn", + "float8_e4m3fnuz", + "float8_e5m2", + "float8_e5m2fnuz", + "float8_e8m0fnu", + "int64", + "int32", + "int16", + "int8", + ), + default="default", + tooltip="Can be used to override the dtype the noise is generated with. Not all noise generators support all types. I don't recommend using the int or float8 types. Probably the most useful override is float64.", + ) + .req_field_override_device( + ("default", "cpu", "gpu"), + default="default", + tooltip="default just uses whatever device normally would be used. gpu will use ComfyUI's default GPU device and also toggle the cpu_noise flag off. cpu will use the CPU device and toggle the cpu_noise flag on.", + ) + .req_normalizetristate_normalize() + ), ) @classmethod @@ -1551,6 +1675,7 @@ NODE_CLASS_MAPPINGS = { "SonarLatentOperationFilteredNoise": SonarLatentOperationFilteredNoiseNode, "SonarModulatedNoise": SonarModulatedNoiseNode, "SonarNormalizeNoiseToScale": SonarNormalizeNoiseToScaleNode, + "SonarNoveltyFilteredNoise": SonarNoveltyFilteredNoiseNode, "SonarPatternBreakNoise": SonarPatternBreakNoiseNode, "SonarPerDimNoise": SonarPerDimNoiseNode, "SonarQuantileFilteredNoise": SonarQuantileFilteredNoiseNode, diff --git a/py/nodes/noise_types.py b/py/nodes/noise_types.py index e808e9c..bd5d9ce 100644 --- a/py/nodes/noise_types.py +++ b/py/nodes/noise_types.py @@ -755,12 +755,13 @@ class SonarAdvancedSimulationNoiseNode(SonarCustomNoiseNodeBase): .req_field_band_shape( ("log_gaussian", "raised_cosine"), default="log_gaussian", - tooltip="TBD", + tooltip="No effect in power_law spectral mode or in multi_octave spectral mode when octaves is set to 0.", ) .req_field_channel_mode( ( "stacked", "over_depth", + "flat", "over_depth_alt", "over_depth_avg", "over_depth_h", @@ -811,8 +812,8 @@ class SonarAdvancedSimulationNoiseNode(SonarCustomNoiseNodeBase): ) .req_int_octaves( default=3, - min=1, - tooltip="Number of octaves of noise to generate. Only has an effect in multi_octave spectral mode.", + min=0, + tooltip="Number of octaves of noise to generate. Only has an effect in multi_octave spectral mode. You can also set octaves to 0 to disable octaves.", ) .req_float_lacunarity(default=2.0) .req_float_gain(default=0.75) diff --git a/py/noise.py b/py/noise.py index ab12947..95cb47c 100644 --- a/py/noise.py +++ b/py/noise.py @@ -10,6 +10,7 @@ import comfy import torch import yaml from comfy.k_diffusion import sampling +from comfy.model_management import throw_exception_if_processing_interrupted from torch import Tensor from . import external, utils @@ -64,7 +65,16 @@ class CustomNoiseItemBase(abc.ABC): def get_normalize(self, k, default=None): val = getattr(self, k, None) - return default if val is None else val + if val in {None, "default"}: + return default + if val == "disabled": + return False + if val == "forced": + return True + return default + # if isinstance(val, bool): + # return val + # return default if val is None else val @abc.abstractmethod def make_noise_sampler( @@ -1691,6 +1701,62 @@ class ScatternetFilteredNoise(CustomNoiseItemBase): return noise_sampler +class NoveltyFilteredNoise(CustomNoiseItemBase): + def clone_key(self, k): + if k == "noise" and self.noise is not None: + return self.noise.clone() + return super().clone_key(k) + + def make_noise_sampler( + self, + x, + sigma_min, + sigma_max, + *args, + normalized=True, + **kwargs, + ): + factor = self.factor + normalize = self.get_normalize("normalize", normalized) + if self.noise is not None: + internal_ns = self.noise.make_noise_sampler( + x, + *args, + sigma_min=sigma_min, + sigma_max=sigma_max, + normalized=self.normalize_noise, + **kwargs, + ) + else: + internal_ns = None + ns_kwargs = getattr(self, "ns_kwargs", {}).copy() + kwargs |= ns_kwargs + ns = NoveltyFilteredNoiseGenerator( + x, + *args, + sigma_min=sigma_min, + sigma_max=sigma_max, + normalized=False, + noise_sampler=internal_ns, + skip_initial=self.skip_initial, + iters_per_call=self.iters_per_call, + blend_ratio=self.blend_ratio, + blend_function=utils.BLENDING_MODES[self.blend_mode], + update_blend_ratio=self.update_blend_ratio, + update_blend_function=utils.BLENDING_MODES[self.update_blend_mode], + **kwargs, + ) + + def noise_sampler(sigma, sigma_next): + return scale_noise( + ns(sigma, sigma_next), + factor, + normalized=normalize, + ) + + return noise_sampler + + class LatentOperationFilteredNoise(CustomNoiseItemBase): def clone_key(self, k): if k == "noise" and self.noise is not None: @@ -1895,29 +1961,32 @@ class PerDimNoise(CustomNoiseItemBase): slice(-dim_size, None) if d == dim else slice(None, None) for d in range(x.ndim) ) + n_chunks = math.ceil(dim_size / chunk_size) + if self.shrink_dim: def noise_sampler(sigma, sigma_next) -> torch.Tensor: - noise = torch.cat( - tuple(ns(sigma, sigma_next) for _ in range(dim_size)), - dim=dim, - )[trim_slice] + chunks = [] + for _ in range(n_chunks): + throw_exception_if_processing_interrupted() + chunks.append(ns(sigma, sigma_next)) + noise = torch.cat(chunks, dim=dim)[trim_slice] return scale_noise(noise, factor, normalized=normalize) - else: - select_dim = [slice(None, None) for d in range(x.ndim)] - n_chunks = math.ceil(dim_size / chunk_size) - temp_shape = list(x.shape) - temp_shape[dim] = int(n_chunks * chunk_size) + return noise_sampler - def noise_sampler(sigma, sigma_next) -> torch.Tensor: - nonlocal select_dim - result = x.new_zeros(temp_shape) - # result = torch.zeros_like(x) - for idx in range(0, dim_size, chunk_size): - select_dim[dim] = slice(idx, idx + chunk_size) - result[select_dim] = ns(sigma, sigma_next)[select_dim] - return scale_noise(result[trim_slice], factor, normalized=normalize) + select_dim = [slice(None, None) for d in range(x.ndim)] + temp_shape = list(x.shape) + temp_shape[dim] = int(n_chunks * chunk_size) + + def noise_sampler(sigma, sigma_next) -> torch.Tensor: + nonlocal select_dim + result = x.new_zeros(temp_shape) + for idx in range(0, dim_size, chunk_size): + throw_exception_if_processing_interrupted() + select_dim[dim] = slice(idx, idx + chunk_size) + result[select_dim] = ns(sigma, sigma_next)[select_dim] + return scale_noise(result[trim_slice], factor, normalized=normalize) return noise_sampler @@ -2123,6 +2192,9 @@ class CustomNoiseParametersNoise(CustomNoiseItemBase): ): factor = self.factor normalize = self.get_normalize("normalize", normalized) + print( + f"\n****** NS: normalized={normalized}, normalize={normalize}, self.normalize={self.normalize}" + ) orig_shape = x.shape orig_dtype = x.dtype orig_device = x.device diff --git a/py/noise_generation/__init__.py b/py/noise_generation/__init__.py index c8aee49..6c4b224 100644 --- a/py/noise_generation/__init__.py +++ b/py/noise_generation/__init__.py @@ -1,6 +1,7 @@ from .base import MixedNoiseGenerator, NoiseError, NoiseType from .collatz_noise_generator import CollatzNoiseGenerator from .distro_noise_generator import DistroNoiseGenerator +from .novelty_filtered_noise import NoveltyFilteredNoiseGenerator from .scatternet_filtered_noise_generator import ScatternetFilteredNoiseGenerator from .simple_noise_generators import ( BrownianNoiseGenerator, @@ -34,6 +35,7 @@ __all__ = ( "MixedNoiseGenerator", "NoiseError", "NoiseType", + "NoveltyFilteredNoiseGenerator", "OneFNoiseGenerator", "PerlinOldNoiseGenerator", "PinkOldNoiseGenerator", diff --git a/py/noise_generation/collatz_noise_generator.py b/py/noise_generation/collatz_noise_generator.py index b39ae9c..a42752d 100644 --- a/py/noise_generation/collatz_noise_generator.py +++ b/py/noise_generation/collatz_noise_generator.py @@ -20,8 +20,6 @@ F = torch.nn.functional class CollatzNoiseGenerator(NoiseGenerator): name = "collatz" - chain_cache: ClassVar[dict] = {} - @classmethod def ng_params(cls): return super().ng_params() | { @@ -54,10 +52,10 @@ class CollatzNoiseGenerator(NoiseGenerator): } @staticmethod - def _get_iter_slices(n_dims, dim, idx, stride) -> list: + def _get_iter_slices(n_dims, dim, idx, stride) -> tuple: result = [slice(None)] * n_dims result[dim] = slice(idx, None, stride) - return result + return tuple(result) def _generate_iteration( self, diff --git a/py/noise_generation/novelty_filtered_noise.py b/py/noise_generation/novelty_filtered_noise.py new file mode 100644 index 0000000..9f0cb6b --- /dev/null +++ b/py/noise_generation/novelty_filtered_noise.py @@ -0,0 +1,186 @@ +# ruff: noqa: ANN002, ANN003 +from __future__ import annotations + +import math +from functools import partial +from typing import TYPE_CHECKING + +import torch + +from .base import NoiseGenerator + +if TYPE_CHECKING: + from collections.abc import Callable + +F = torch.nn.functional + + +def sum_rms_blend( + a: torch.Tensor, + b: torch.Tensor, + t: torch.Tensor | float = 1.0, + *, + orig_shape: torch.Size | tuple[int, ...], + dims_a: tuple[int, ...] = (1,), + dims_b: tuple[int, ...] = (-1, -2), +) -> torch.Tensor: + rms_a = a / math.prod(orig_shape[d] for d in dims_a) ** 0.5 + rms_b = b / math.prod(orig_shape[d] for d in dims_b) ** 0.5 + variance_a = rms_a.pow_(2.0) + variance_b = rms_b.pow_(2.0) + result = variance_a + result += variance_b * t + result /= 1.0 + t + result **= 0.5 + return result + + +def metrics_blend( + a: torch.Tensor, + b: torch.Tensor, + t: torch.Tensor | float = 1.0, + *, + orig_shape: torch.Size | tuple[int, ...], + dims_a: tuple[int, ...] = (-1, -2), + dims_b: tuple[int, ...] = (1,), + use_rms: bool = True, + rms_power: float = 2.0, +) -> torch.Tensor: + count_a = math.prod(orig_shape[d] for d in dims_a) + count_b = math.prod(orig_shape[d] for d in dims_b) + denom_a = count_a ** (1 / rms_power) if use_rms else count_a + denom_b = count_b ** (1 / rms_power) if use_rms else count_b + curr_a = a / denom_a + curr_b = b / denom_b + if use_rms: + curr_a **= rms_power + curr_b = curr_b.pow_(rms_power) * t + result = curr_b.add_(curr_a) + result /= 1.0 + t + return result.pow_(1.0 / rms_power) if use_rms else result + + +class NoveltyFilteredNoiseGenerator(NoiseGenerator): + name = "novelty" + + initial_noise_state: torch.Tensor | None = None + noise_state: torch.Tensor | None = None + blend_function: Callable | None = None + + @classmethod + def ng_params(cls): + return super().ng_params() | { + "skip_initial": 1, + "iters_per_call": 1, + "dim_groups": ((1,), (-1, -2)), + "blend_ratio": 1.0, + "blend_function": None, + "update_blend_ratio": 1.0, + "update_blend_function": None, + "noise_sampler": None, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.blend_function is None: + raise ValueError("Missing blend function!") + + def generate(self, *args) -> torch.Tensor: + ng = ( + partial(self.noise_sampler, *args) if self.noise_sampler else self.rand_like + ) + noise_state = self.noise_state + had_state = self.noise_state is not None + it_counter = 0 if had_state else 0 - self.skip_initial + its_call = max(1, self.iters_per_call) + bf = self.blend_function + blend_ratio = self.blend_ratio + update_blend_ratio = self.update_blend_ratio + ubf = self.update_blend_function + if ubf is None: + # Linear weighted average + def ubf(a: torch.Tensor, b: torch.Tensor, t: float) -> torch.Tensor: + return (b * t).add_(a).div_(1.0 + abs(t)) + + call_initial_noise = None + curr_noise = None + call_noise_state = None + while it_counter < its_call: + if noise_state is None: + noise_state = ng() + self.initial_noise_state = noise_state.clone() + self.noise_state = noise_state.clone() + continue + curr_noise = ng() + if call_initial_noise is None: + call_initial_noise = curr_noise.clone() + seen = {id(curr_noise)} + for ortho_target in ( + self.initial_noise_state, + call_initial_noise if it_counter > 0 else None, + noise_state, + ): + tid = id(ortho_target) + if ortho_target is None or tid in seen: + continue + seen.add(tid) + curr_noise = bf(ortho_target, curr_noise, blend_ratio) + curr_noise -= ortho_target + it_counter += 1 + if it_counter < 1: + self.noise_state = curr_noise.clone() + noise_state = curr_noise + continue + if call_noise_state is None: + call_noise_state = curr_noise + else: + call_noise_state = ubf(call_noise_state, curr_noise, update_blend_ratio) + if call_noise_state is None: + raise RuntimeError("Unexpected unpopulated call_noise_state!") + # self.noise_state = call_noise_state.clone() + self.noise_state = ubf(noise_state, call_noise_state, update_blend_ratio) + return call_noise_state + + # def generate(self, *args) -> torch.Tensor: + # ng = ( + # partial(self.noise_sampler, *args) if self.noise_sampler else self.rand_like + # ) + # noise_state = self.noise_state + # had_state = self.noise_state is not None + # it_counter = 0 if had_state else 0 - self.skip_initial + # its_call = self.iters_per_call + # bf = self.blend_function + # blend_ratio = self.blend_ratio + # update_blend_ratio = self.update_blend_ratio + # ubf = self.update_blend_function + # if ubf is None or True: + # # Linear weighted average + # def ubf(a: torch.Tensor, b: torch.Tensor, t: float) -> torch.Tensor: + # return (b * t).add_(a).div_(1.0 + abs(t)) + + # call_initial_noise = None + # while it_counter < its_call: + # if noise_state is None: + # noise_state = ng() + # self.initial_noise_state = noise_state.clone() + # continue + # curr_noise = ng() + # it_counter += 1 + # if it_counter < 1: + # noise_state = curr_noise + # continue + # if call_initial_noise is None and self.iters_per_call > 1: + # call_initial_noise = curr_noise.clone() + # for ortho_target in ( + # self.initial_noise_state, + # call_initial_noise if it_counter > 0 else None, + # noise_state, + # ): + # if ortho_target is None: + # continue + # curr_noise = bf(ortho_target, curr_noise, blend_ratio) + # curr_noise -= ortho_target + # # curr_noise = bf(noise_state, ng(), blend_ratio).sub_(noise_state) + # noise_state = ubf(noise_state, curr_noise, update_blend_ratio) + # self.noise_state = noise_state.clone() + # return noise_state diff --git a/py/noise_generation/simulation_noise_generator.py b/py/noise_generation/simulation_noise_generator.py index 1b40c6a..58495b5 100644 --- a/py/noise_generation/simulation_noise_generator.py +++ b/py/noise_generation/simulation_noise_generator.py @@ -3,6 +3,7 @@ from __future__ import annotations import itertools import math +from typing import Any import torch @@ -76,16 +77,20 @@ class SimulationNoiseGenerator(NoiseGenerator): torch.complex128 if self.dtype == torch.float64 else torch.complex64 ) self.eff_batch = ( - self.batch if cm != "stacked" else self.batch * math.ceil(self.channels / 3) + self.batch + if cm not in {"stacked", "flat"} + else self.batch * math.ceil(self.channels / 3) + ) + ns_shape = torch.Size( + ( + self.eff_batch, + self.depth * self.depth_increment, + self.height, + self.width, + ) ) - ns_shape = torch.Size(( - self.eff_batch, - self.depth * self.depth_increment, - self.height, - self.width, - )) - def gaussian_noise_sampler(*_args: list) -> torch.Tensor: + def gaussian_noise_sampler(*_args: Any) -> torch.Tensor: return torch.randn(ns_shape, dtype=self.cdtype, device=self.gen_device).to( device=self.device, ) @@ -121,7 +126,7 @@ class SimulationNoiseGenerator(NoiseGenerator): ) @staticmethod - def _radial_k(*ks: list) -> torch.Tensor: + def _radial_k(*ks: torch.Tensor) -> torch.Tensor: """Calculates the radial distance in k-space.""" return sum(kt**2 for kt in ks).sqrt_() @@ -187,6 +192,9 @@ class SimulationNoiseGenerator(NoiseGenerator): return wk_out( self._handle_band_shape(k_rad, self.band_pass_low, self.band_pass_high), ) + if self.octaves == 0: + # Ones where k_rad is non-zero, otherwise zero. + return (k_rad != 0).to(k_rad) base_k = 2 * math.pi / max(1, min(sizes)) if self.base_k == 0 else self.base_k wk = torch.zeros_like(k_rad) for o in range(self.octaves): @@ -205,9 +213,17 @@ class SimulationNoiseGenerator(NoiseGenerator): ns_args: tuple | list, **_kwargs, ): + n_dims = len(k_grids_orig) + n_samplers = len(self.noise_samplers) f_fs = tuple( - ns(*ns_args).to(device=self.device).mul_(wk) for ns in self.noise_samplers + self.noise_samplers[ns_idx % n_samplers](*ns_args) + .to( + device=self.device, + ) + .mul_(wk) + for ns_idx in range(n_dims) ) + # --- Perform the Helmholtz projection using the UN SCALED grids --- k_sq_proj = self._radial_k(*k_grids_orig) ** 2 k_dot_f = sum(k_p * f_f for k_p, f_f in zip(k_grids_orig, f_fs)) @@ -218,35 +234,6 @@ class SimulationNoiseGenerator(NoiseGenerator): f_f - k_grid * k_grid_scale for f_f, k_grid in zip(f_fs, k_grids_orig) ) - # def _handle_field_curl( - # self, - # *, - # k_rad: torch.Tensor, - # k_grids_orig: tuple, - # wk: torch.Tensor, - # ns_args: tuple | list, - # **_kwargs, - # ): - # if len(k_grids_orig) != 3: - # raise ValueError("Can only handle 3 dimensions currently") - # # inv_k_rad = torch.where(k_rad == 0, 0.0, 1.0 / k_rad) - # # wk_potential = wk * inv_k_rad - # wk_potential = wk / k_rad - # f_fs = tuple( - # ns(*ns_args).to(device=self.device).mul_(wk_potential) - # for ns in self.noise_samplers - # ) - # i_k_grids = tuple( - # (1j * k_grid).to(dtype=self.cdtype) for k_grid in k_grids_orig - # ) - # fz, fy, fx = f_fs - # ikz, iky, ikx = i_k_grids - # return ( - # ikx * fy - iky * fx, # z - # ikz * fx - ikx * fz, # y - # iky * fz - ikz * fy, # x - # ) - def _handle_field_curl( self, *, @@ -324,70 +311,6 @@ class SimulationNoiseGenerator(NoiseGenerator): _handle_field_curl_ndim = _handle_field_curl - def _handle_field_basis_orig( - self, - *, - k_rad: torch.Tensor, - k_grids_orig: tuple, - wk: torch.Tensor, - ns_args: tuple | list, - **_kwargs, - ) -> tuple: - if len(k_grids_orig) != 3: - raise ValueError("Can only handle 3 dimensions currently") - k_norm_z, k_norm_y, k_norm_x = ( - torch.where(k_rad == 0, 0.0, k / k_rad) for k in k_grids_orig - ) - - # Pick a fixed vector `ez` to take a cross product with. - # Handle the singularity where k is parallel to ez. - ez = torch.tensor((0.0, 0.0, 1.0), device=self.device, dtype=self.dtype) - is_parallel = (k_norm_x.abs() < 1e-6) & (k_norm_y.abs() < 1e-6) - - # First basis vector u = k x ez (or k x ey for the singularity) - ux = torch.where( - is_parallel, - k_norm_y * 0 - k_norm_z * 1, - k_norm_y * ez[2] - k_norm_z * ez[1], - ) - uy = torch.where( - is_parallel, - k_norm_z * 0 - k_norm_x * 0, - k_norm_z * ez[0] - k_norm_x * ez[2], - ) - uz = torch.where( - is_parallel, - k_norm_x * 1 - k_norm_y * 0, - k_norm_x * ez[1] - k_norm_y * ez[0], - ) - - u_mag = torch.sqrt(sum((ux**2, uy**2, uz**2))) - inv_u_mag = torch.where(u_mag == 0, 0.0, 1.0 / u_mag) - ux *= inv_u_mag - uy *= inv_u_mag - uz *= inv_u_mag - ux[k_rad == 0], uy[k_rad == 0], uz[k_rad == 0] = 0, 0, 0 - - # Second basis vector v = k x u - vx = k_norm_y * uz - k_norm_z * uy - vy = k_norm_z * ux - k_norm_x * uz - vz = k_norm_x * uy - k_norm_y * ux - - # 3. Generate two independent random complex scalar fields - a_f = self.noise_samplers[0](*ns_args).to(device=self.device) - b_f = self.noise_samplers[1](*ns_args).to(device=self.device) - - # 4. Modulate the random fields by the spectral envelope - a_f *= wk - b_f *= wk - - # 5. Project the random fields onto the basis vectors to form the final field - return ( - a_f * uz + b_f * vz, # z - a_f * uy + b_f * vy, # y - a_f * ux + b_f * vx, # x - ) - def _handle_field_basis( self, *, @@ -407,9 +330,6 @@ class SimulationNoiseGenerator(NoiseGenerator): # --- Case 1: 2D (simple and fast) --- if n_dims * nd_fixup == 2: - if len(self.noise_samplers) < 1: - raise ValueError("2D basis mode requires at least 1 noise sampler.") - # The basis is a single vector perpendicular to k: u = (-ky, kx) kn_y, kn_x = k_norm_components basis_vectors = [ @@ -420,6 +340,8 @@ class SimulationNoiseGenerator(NoiseGenerator): # --- Case 2: 3D (fast cross-product method) --- elif n_dims * nd_fixup == 3: + num_random_fields = 2 + kn_z, kn_y, kn_x = k_norm_components ez = torch.tensor([0.0, 0.0, 1.0], device=self.device, dtype=self.dtype) is_parallel = (kn_x.abs() < 1e-6) & (kn_y.abs() < 1e-6) @@ -441,7 +363,6 @@ class SimulationNoiseGenerator(NoiseGenerator): v = (vz, vy, vx) basis_vectors = [u, v] - num_random_fields = 2 # --- Case 3: N-D (General Gram-Schmidt process) --- else: @@ -505,7 +426,7 @@ class SimulationNoiseGenerator(NoiseGenerator): _handle_field_basis_ndim = _handle_field_basis - def generate_octaves( + def generate_field( self, batch: int, height: int, @@ -567,7 +488,7 @@ class SimulationNoiseGenerator(NoiseGenerator): def generate(self, *args) -> torch.Tensor: cm = self.channel_mode if self.noise_chunk is None: - self.noise_chunk = self.generate_octaves( + self.noise_chunk = self.generate_field( self.eff_batch, self.height, self.width, @@ -585,6 +506,9 @@ class SimulationNoiseGenerator(NoiseGenerator): ), dim=2, ) + elif cm == "flat": + noise = self.noise_chunk[:, :, self.current_depth] + noise = noise.flatten()[: math.prod(self.shape)] elif cm in {"over_depth", "over_depth_alt"}: noise = self.noise_chunk[:, :, depth_from:depth_to] if cm == "over_depth": diff --git a/py/utils.py b/py/utils.py index d51a85e..5ffa36c 100644 --- a/py/utils.py +++ b/py/utils.py @@ -2,8 +2,9 @@ from __future__ import annotations import math import random -from functools import partial -from typing import TYPE_CHECKING, Callable +from enum import Enum, auto +from functools import lru_cache, partial +from typing import TYPE_CHECKING, NamedTuple import torch from comfy.model_management import device_supports_non_blocking, get_torch_device @@ -12,12 +13,15 @@ from comfy.utils import common_upscale from .external import MODULES as EXT if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence + +F = torch.nn.functional BLENDING_MODES = { "lerp": torch.lerp, "inject": lambda a, b, t: (b * t).add_(a), "subtract_b": lambda a, b, t: a - b * t, + "weighted_average": lambda a, b, t: (b * t).add_(a) / (1.0 + abs(t)), } UPSCALE_METHODS = ( "bilinear", @@ -95,7 +99,7 @@ def scale_noise( return noise.mul_(factor) if factor != 1 else noise if normalize_dims is not None: std = noise.std(dim=normalize_dims, keepdim=True) - noise = noise / std # noqa: PLR6104 + noise = (noise / std).nan_to_num_() return noise.sub_(noise.mean(dim=normalize_dims, keepdim=True)).mul_(factor) mean, std = noise.mean().item(), noise.std().item() threshold = threshold_std_devs / math.sqrt(numel) @@ -121,6 +125,47 @@ def tensor_to( return tensor.to(dest, non_blocking=non_blocking) +def range_wrap( + x: torch.Tensor, + min_val: float | torch.Tensor, + max_val: float | torch.Tensor, +) -> torch.Tensor: + return min_val + (x - min_val).remainder_(max_val - min_val) + + +def softplus_soft_clamp( + t: torch.Tensor, + min_val: torch.Tensor | float = 0.0, + max_val: torch.Tensor | float = 1.0, + *, + # We define stiffness as a multiplier (beta) for the softplus function. + # Higher stiffness = sharper transition. + stiffness: float = 1.0, + safe: bool = True, +) -> torch.Tensor: + if isinstance(min_val, (float, int)): + min_val = t.new_tensor(min_val) + if isinstance(max_val, (float, int)): + max_val = t.new_tensor(max_val) + + if stiffness < 1e-04: + return t.clamp(min_val, max_val) + + # Calculate how much we are exceeding the Max + # softplus(beta * x) / beta + upper_overshoot = F.softplus((t - max_val).mul_(stiffness)).div_(-stiffness) + + # Calculate how much we are falling short of the Min + lower_undershoot = F.softplus((min_val - t).mul_(stiffness)).div_(stiffness) + + # Apply corrections: + # Original - (Amount over max) + (Amount under min) + t = upper_overshoot.add_(t).add_(lower_undershoot) + if safe: + t = t.clamp(min_val, max_val) + return t + + def _quantile_norm_scaledown( noise: torch.Tensor, nq: torch.Tensor, @@ -360,10 +405,51 @@ quantile_handlers = { count_flipping=True, avoid_sign=True, ), + "wrap": lambda noise, nq, **_kwargs: range_wrap(noise, -nq, nq), + "wrap_keepsign": lambda noise, nq, **_kwargs: torch.where( + noise.abs() > nq, + range_wrap(noise, -nq, nq).copysign_(noise), + noise, + ), + "wrap_avoidsign": lambda noise, nq, **_kwargs: torch.where( + noise.abs() > nq, + range_wrap(noise, -nq, nq).copysign_(noise.neg()), + noise, + ), + "softplus_clamp_s01": lambda noise, nq, **_kwargs: softplus_soft_clamp( + noise, + -nq, + nq, + stiffness=0.1, + ), + "softplus_clamp_s05": lambda noise, nq, **_kwargs: softplus_soft_clamp( + noise, + -nq, + nq, + stiffness=0.5, + ), + "softplus_clamp_s1": lambda noise, nq, **_kwargs: softplus_soft_clamp( + noise, + -nq, + nq, + stiffness=1.0, + ), + "softplus_clamp_s2": lambda noise, nq, **_kwargs: softplus_soft_clamp( + noise, + -nq, + nq, + stiffness=1.0, + ), + "softplus_clamp_s5": lambda noise, nq, **_kwargs: softplus_soft_clamp( + noise, + -nq, + nq, + stiffness=5.0, + ), } -# Initial version based on Studentt distribution normalizatino from https://github.com/Clybius/ComfyUI-Extra-Samplers/ +# Initial version based on StudentT distribution normalization from https://github.com/Clybius/ComfyUI-Extra-Samplers/ def quantile_normalize( noise: torch.Tensor, *, @@ -449,6 +535,239 @@ def quantile_normalize( return noise if noise.shape == orig_shape else noise.reshape(orig_shape) +# class QuantileNormMode(Enum): +# # Quantile is applied to absolute values. +# SYMMETRIC = auto() +# # Quantile is applied to signed values. +# SEPERATE = auto() + + +class QuantileNormQuantileMode(Enum): + QUANTILE = auto() + # User supplied value to use as the quantile value. + USER_HIGH = auto() + # Like setting a negative quantile. + USER_LOW = auto() + + +class QuantileNormSignMode(Enum): + DEFAULT = auto() + KEEP = auto() + AVOID = auto() + + +class QuantileNormTargetMode(Enum): + BOTH = auto() + POSITIVE = auto() + NEGATIVE = auto() + + +class QuantileNorm(NamedTuple): + # mode: QuantileNormMode = QuantileNormMode.SYMMETRIC + quantile_mode: QuantileNormQuantileMode = QuantileNormQuantileMode.QUANTILE + sign_mode: QuantileNormSignMode = QuantileNormSignMode.DEFAULT + target_mode: QuantileNormTargetMode = QuantileNormTargetMode.BOTH + strategy: str = "clamp" + # Overrides strategy. + strategy_handler: Callable | None = None + start_end_dim: tuple[int, int] | None = (1, -1) + dims: tuple[int, ...] = () + quantile: float | torch.Tensor = 0.75 + # If none, we use absmax when quantile is negative, otherwise + # abs value at this quantile. + low_extreme_quantile: float | None = 0.99 + nq_scale: float = 1.0 + power: float = 0.0 + use_abs_quantile: bool = True + # When setting a target other than BOTH, apply the mask to the quantile calculation as well. + use_quantile_mask: bool = True + use_float64: bool = True + fix_invalid: bool = True + + @staticmethod + def fix_dim(dim: int, ndim: int) -> int | None: + if dim < 0: + dim = ndim + dim + return dim if 0 <= dim < ndim else None + + @classmethod + def fix_dims(cls, dims: tuple[int, ...], ndim: int) -> tuple[int, ...]: + dims = tuple(d for d in (cls.fix_dim(d_, ndim) for d_ in dims) if d is not None) + return tuple({dims}) + + def get_dims(self, ndim: int) -> tuple[int, ...]: + dims = self.fix_dims(self.dims) + if self.start_end_dim is None: + return dims + start_end_dim = self.fix_dims(*self.start_end_dim, ndim) + if len(start_end_dim) != 2: + return dims + sd, ed = start_end_dim + if sd > ed: + sd, ed = ed, sd + return tuple({range(sd, ed + 1), *dims}) + + @classmethod + @lru_cache(maxsize=128) + def get_perms( + cls, + # Must be deduped and sanitized. + dims: tuple[int, ...], + ndim: int, + ) -> tuple[tuple[int, ...], tuple[int, ...]]: + other_dims = tuple(d for d in range(ndim) if d not in dims) + perms = (*other_dims, *dims) + inv_perms_t = torch.nn.utils.rnn.invert_permutation( + torch.tensor(perms, device="cpu"), + ) + if inv_perms_t is None: + errstr = f"torch.nn.utils.rnn.invert_permutation returned None for input {perms}!" + raise RuntimeError(errstr) + inv_perms = tuple(inv_perms_t.tolist()) + return (perms, inv_perms) + + def __call__(self, t: torch.Tensor) -> torch.Tensor: + ndim = t.ndim + dims = self.get_dims(ndim) + handler = ( + quantile_handlers.get(self.strategy) + if self.strategy_handler is None + else self.strategy_handler + ) + if handler is None: + raise ValueError("No strategy handler") + if not dims or self.quantile == 0 or t.numel() < 2: + return t + orig_dtype = t.dtype + orig_t = t + eff_dtype = torch.float64 if self.use_float64 else torch.float32 + dlen = len(dims) + olen = ndim - dlen + perms, inv_perms = self.get_perms(dims, ndim) + t = t.to(dtype=eff_dtype) + # Move the dims we're working with to the end. + t = t.permute(perms) + permuted_shape = t.shape + # And flatten them. + t = t.flatten(start_dim=olen) + use_low_extreme = ( + isinstance(self.quantile, float) and self.quantile < 0 + ) or self.quantile_mode == QuantileNormQuantileMode.USER_LOW + if use_low_extreme: + raise RuntimeError("NYI") + if self.target_mode == QuantileNormTargetMode.NEGATIVE: + mask = t.sign() < 0 + elif self.target_mode == QuantileNormTargetMode.POSITIVE: + mask = t.sign() > 0 + else: + mask = None + masked_quantile = self.use_quantile_mask and mask is not None + if self.quantile_mode == QuantileNormQuantileMode.QUANTILE: + nq_input = t.abs() if self.use_abs_quantile else t + if masked_quantile: + nq_input = nq_input[mask] + if not torch.any(nq_input): + return orig_t + nq = torch.quantile(nq_input, dim=-1, keepdim=True) + else: + raise RuntimeError("NYI") + if self.nq_scale != 1.0: + nq *= self.nq_scale + # ... + if self.power not in {0.0, 1.0}: + t = t.abs().pow_(self.power).copysign_(t) + if self.fix_invalid: + t = t.nan_to_num_() + t = t.to(dtype=orig_dtype) + # Back to the original shape. + return t.reshape(permuted_shape).permute(inv_perms).contiguous() + + +def quantile_normalize_adv( + noise: torch.Tensor, + *, + quantile: float | tuple | list = 0.75, + dim: int | None = 1, + flatten: bool = True, + nq_fac: float = 1.0, + pow_fac: float = 0.5, + strategy: str = "clamp", + strategy_handler=None, + eps=1e-08, +) -> torch.Tensor: + if noise.numel() == 0: + return noise + if isinstance(quantile, (tuple, list)): + for q in quantile: + noise = quantile_normalize( + noise=noise, + quantile=q, + dim=dim, + flatten=flatten, + nq_fac=nq_fac, + pow_fac=pow_fac, + strategy=strategy, + strategy_handler=strategy_handler, + ) + return noise + if quantile is None or quantile >= 1 or quantile <= -1: + return noise + centered = quantile < 0 + absquantile = abs(quantile) + orig_shape = noise.shape + if noise.ndim > 1 and flatten: + flatnoise = noise.flatten(start_dim=dim) + else: + flatten = False + flatnoise = noise + handler = ( + quantile_handlers.get(strategy) + if strategy_handler is None + else strategy_handler + ) + if handler is None: + raise ValueError("Unknown strategy") + if not centered: + nq = torch.quantile( + flatnoise.abs(), + quantile, + dim=-1 if flatten else dim, + keepdim=True, + ) + nq = nq.mul_(nq_fac).add_(eps) + # print(f"\nNQ: {nq}") + noise = handler( + flatnoise, + nq, + orig_noise=noise, + dim=dim, + flatten=flatten, + ) + else: + absnoise = flatnoise.abs() + maxabs = absnoise.amax(dim=-1 if flatten else dim, keepdim=True) + proxy = flatnoise.sign().mul_(maxabs - absnoise) + nq_proxy = torch.quantile( + proxy.abs(), + absquantile, + dim=-1 if flatten else dim, + keepdim=True, + ) + nq_proxy = nq_proxy.mul_(nq_fac).add_(eps) + # print(f"\nNQ proxy: {nq_proxy}") + out_proxy = handler( + proxy, + nq_proxy, + orig_noise=noise, + dim=dim, + flatten=flatten, + ) + noise = out_proxy.sign().mul_(maxabs - out_proxy.abs()) + if pow_fac not in {0.0, 1.0}: + noise = noise.abs().pow_(pow_fac).copysign(noise) + return noise if noise.shape == orig_shape else noise.reshape(orig_shape) + + def normalize_to_scale( latent: torch.Tensor, target_min: float, diff --git a/py/wavelet_cfg.py b/py/wavelet_cfg.py index da854bb..61dc439 100644 --- a/py/wavelet_cfg.py +++ b/py/wavelet_cfg.py @@ -172,6 +172,7 @@ class WCFGPercentages(NamedTuple): else: pct_enabled_sigmas = (start_sigma - sigma) / (start_sigma - end_sigma) steps = len(sigmas) - 1 + have_steps = False if steps > 1: step = utils.step_from_sigmas(sigma, sigmas) pct_steps = step / (steps - 1) if step is not None else None @@ -179,10 +180,11 @@ class WCFGPercentages(NamedTuple): (sigmas <= start_sigma) & (sigmas >= end_sigma) ] if len(enabled_steps) > 1: + have_steps = True step_first = enabled_steps[0].item() step_last = enabled_steps[-1].item() pct_enabled_steps = (step - step_first) / (step_last - step_first) - else: + if not have_steps: step = 0.0 pct_steps = 1.0 step_first = step_last = None @@ -247,6 +249,8 @@ class WCFGScales(NamedTuple): target = self.yh_scales if isinstance(target, float): return f"{target:.4f}" + if not isinstance(target, (list, tuple)): + return str(target) result = ", ".join( self.pretty_yh_scales(target=val) if isinstance(val, (list, tuple))