This commit is contained in:
blepping
2025-07-23 05:17:07 -06:00
parent 4a97ad3468
commit e61fc24ebe
21 changed files with 3255 additions and 2549 deletions
+3 -11
View File
@@ -1,7 +1,7 @@
import sys
from . import py # noqa: F401
from .py import freeu_extreme, nodes, powernoise, sonar
from .py import nodes, sonar
def blep_init():
@@ -15,17 +15,9 @@ def blep_init():
sonar.add_samplers()
blep_init()
NODE_CLASS_MAPPINGS = (
nodes.NODE_CLASS_MAPPINGS
| powernoise.NODE_CLASS_MAPPINGS
| freeu_extreme.NODE_CLASS_MAPPINGS
)
NODE_CLASS_MAPPINGS = nodes.NODE_CLASS_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS = nodes.NODE_DISPLAY_NAME_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS = (
getattr(nodes, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(powernoise, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(freeu_extreme, "NODE_DISPLAY_NAME_MAPPINGS", {})
)
NODE_DISPLAY_NAME_MAPPINGS = getattr(nodes, "NODE_DISPLAY_NAME_MAPPINGS", {})
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+7
View File
@@ -2,6 +2,13 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20250723
Once again, large set of changes/internal reorganization which may break stuff. If you run into problems or experience anything weird, please create an issue.
* Added a `SonarResizedNoiseAdv` node that allows more control (and is more useful for models like ACE-Steps where you might want to deal with absolute sizes).
* Added a `SonarWaveletCFG` node which allows you use different CFG values for different frequencies.
## 20250705
This is a large set of changes. Please let me know anything doesn't seem to be working properly.
+5 -1
View File
@@ -120,7 +120,11 @@ class IntegratedNode(type):
def __new__(cls: type, name: str, bases: tuple, attrs: dict) -> object:
obj = type.__new__(cls, name, bases, attrs)
if hasattr(obj, "INPUT_TYPES"):
if hasattr(obj, "INPUT_TYPES") and not getattr(
obj.INPUT_TYPES,
"_NO_REPLACE",
False,
):
obj.INPUT_TYPES = partial(cls.wrap_INPUT_TYPES, obj.INPUT_TYPES)
return obj
+30 -13
View File
@@ -2,13 +2,18 @@ from __future__ import annotations
import math
import random
from typing import TYPE_CHECKING
import torch
from . import utils
if TYPE_CHECKING:
from types import Sequence
class SonarLatentOperation:
EXTENDED_LATENT_OPERATION = True
SKIP_ARGS = frozenset(("sigma", "t2", "cond", "uncond", "cond_scale", "raw_args"))
def __init__(
@@ -36,7 +41,7 @@ class SonarLatentOperation:
op = self.op
if op is None:
return t
if not isinstance(op, SonarLatentOperation):
if not getattr(op, "EXTENDED_LATENT_OPERATION", False):
kwargs = {k: v for k, v in kwargs.items() if k not in self.SKIP_ARGS}
return op(t, *args, **kwargs)
@@ -61,6 +66,7 @@ class SonarLatentOperationAdvanced(SonarLatentOperation):
input_multiplier: float,
output_multiplier: float,
difference_multiplier: float,
ops: Sequence,
op_alt=None,
**kwargs: dict,
) -> None:
@@ -71,6 +77,7 @@ class SonarLatentOperationAdvanced(SonarLatentOperation):
self.output_multiplier = output_multiplier
self.difference_multiplier = difference_multiplier
self.op_alt = op_alt
self.ops = ops
def __call__(
self,
@@ -87,14 +94,12 @@ class SonarLatentOperationAdvanced(SonarLatentOperation):
if self.op_alt is None
else self.call_op(t, sigma=sigma, op=self.op_alt, **kwargs)
)
output = self.call_op(
t if self.input_multiplier == 1.0 else t * self.input_multiplier,
sigma=sigma,
**kwargs,
)
if self.output_multiplier != 1.0:
output = output * self.output_multiplier # noqa: PLR6104
diff = output - t
output = t * self.input_multiplier if self.input_multiplier != 1.0 else t
for op in self.ops:
t = self.call_op(output, sigma=sigma, op=op, **kwargs)
diff = (
output * self.output_multiplier if self.output_multiplier == 1.0 else output
) - t
if self.difference_multiplier != 1.0:
diff *= self.difference_multiplier
return self.blend_function(t, diff, self.blend_strength)
@@ -181,11 +186,23 @@ class SonarLatentOperationNoise(SonarLatentOperation):
class SonarLatentOperationSetSeed(SonarLatentOperation):
def __init__(self, *args: list, seed: int, **kwargs: dict):
def __init__(self, *args: list, seed: int, restore_rng_state: bool, **kwargs: dict):
super().__init__(*args, **kwargs)
self.seed = seed
self.restore_rng_state = restore_rng_state
def __call__(self, *args: list, **kwargs: dict) -> torch.Tensor:
torch.manual_seed(self.seed)
random.seed(self.seed)
return super().__call__(*args, **kwargs)
if self.restore_rng_state:
pyrandst = random.getstate()
torchrandst = torch.random.get_rng_state()
else:
pyrandst = torchrandst = None
try:
torch.manual_seed(self.seed)
random.seed(self.seed)
result = super().__call__(*args, **kwargs)
finally:
if self.restore_rng_state:
torch.random.set_rng_state(torchrandst)
random.setstate(pyrandst)
return result
+18 -17
View File
@@ -1,31 +1,32 @@
from . import (
base,
freeu_extreme,
integrations,
latent_operations,
misc,
momentum_samplers,
noise_filters,
noise_types,
powernoise,
wavelet_cfg,
)
NODE_CLASS_MAPPINGS = {
"SonarCustomNoise": base.SonarCustomNoiseNode,
"SonarCustomNoiseAdv": base.SonarCustomNoiseAdvNode,
} | (
integrations.NODE_CLASS_MAPPINGS
| latent_operations.NODE_CLASS_MAPPINGS
| misc.NODE_CLASS_MAPPINGS
| momentum_samplers.NODE_CLASS_MAPPINGS
| noise_filters.NODE_CLASS_MAPPINGS
| noise_types.NODE_CLASS_MAPPINGS
)
}
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = (
getattr(integrations, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(latent_operations, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(misc, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(momentum_samplers, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(noise_filters, "NODE_DISPLAY_NAME_MAPPINGS", {})
| getattr(noise_types, "NODE_DISPLAY_NAME_MAPPINGS", {})
)
for nm in (
freeu_extreme,
integrations,
latent_operations,
misc,
momentum_samplers,
noise_filters,
noise_types,
powernoise,
wavelet_cfg,
):
NODE_CLASS_MAPPINGS |= getattr(nm, "NODE_CLASS_MAPPINGS", {})
NODE_DISPLAY_NAME_MAPPINGS |= getattr(nm, "NODE_DISPLAY_NAME_MAPPINGS", {})
+194 -72
View File
@@ -1,11 +1,11 @@
# ruff: noqa: TID252
from __future__ import annotations
import abc
from typing import Any
from .. import noise
from ..external import IntegratedNode
from .. import noise, utils
from ..external import MODULES, IntegratedNode
from .base_inputtypes import InputCollection, InputTypes, LazyInputTypes
try:
from comfy_execution import validation as comfy_validation
@@ -47,6 +47,147 @@ NOISE_INPUT_TYPES_HINT = (
)
class SonarInputCollection(InputCollection):
def __init__(self, *args: list, **kwargs: dict):
super().__init__(*args, **kwargs)
self._DELEGATE_KEYS = self._DELEGATE_KEYS | frozenset(( # noqa: PLR6104
"customnoise",
"normalizetristate",
"selectblend",
"selectnoise",
"selectscalemode",
"yaml",
))
def yaml(
self,
name: str = "yaml_parameters",
*,
tooltip="Allows specifying custom parameters via YAML. Note: When specifying paramaters this way, there is generally not much error checking.",
placeholder="# YAML or JSON here",
dynamicPrompts=False, # noqa: N803
multiline=True,
**kwargs: dict,
):
return self.field(
name,
"STRING",
tooltip=tooltip,
placeholder=placeholder,
dynamicPrompts=dynamicPrompts,
multiline=multiline,
**kwargs,
)
def selectblend(
self,
name: str = "blend_mode",
*,
default="lerp",
insert_modes=(),
tooltip="Mode used for blending. If you have ComfyUI-bleh then you will have access to many more blend modes.",
**kwargs: dict,
) -> InputCollection:
if not MODULES.initialized:
raise RuntimeError(
"Attempt to get blending modes before integrations were initialized",
)
return self.field(
name,
(*insert_modes, *utils.BLENDING_MODES.keys()),
default=default,
tooltip=tooltip,
**kwargs,
)
def selectscalemode(
self,
name: str,
*,
default="nearest-exact",
insert_modes=(),
tooltip="Mode used for scaling. If you have ComfyUI-bleh then you will have access to many more scale modes.",
**kwargs: dict,
) -> InputCollection:
if not MODULES.initialized:
raise RuntimeError(
"Attempt to get scale modes before integrations were initialized",
)
return self.field(
name,
(*insert_modes, *utils.UPSCALE_METHODS),
default=default,
tooltip=tooltip,
**kwargs,
)
def selectnoise(
self,
name: str,
*,
default="gaussian",
insert_types=(),
tooltip="Sets the type of noise.",
**kwargs: dict,
) -> InputCollection:
return self.field(
name,
(*insert_types, *noise.NoiseType.get_names()),
default=default,
tooltip=tooltip,
**kwargs,
)
def customnoise(
self,
name: str,
add_hint: bool = True, # noqa: FBT001
tooltip="Allows connecting a custom noise chain.",
**kwargs: dict,
) -> InputCollection:
if add_hint:
tooltip = f"{tooltip}\n{NOISE_INPUT_TYPES_HINT}"
return self.field(name, WILDCARD_NOISE, tooltip=tooltip, **kwargs)
def normalizetristate(
self,
name: str,
*,
default="default",
tooltip="Controls whether noise is normalized to 1.0 strength.",
**kwargs: dict,
):
return self.field(
name,
("default", "forced", "disabled"),
default=default,
tooltip=tooltip,
**kwargs,
)
class SonarInputTypes(InputTypes):
_NO_REPLACE = True
def __init__(self, *args: list, **kwargs: dict):
super().__init__(
*args,
collection_class=SonarInputCollection,
**kwargs,
)
class SonarLazyInputTypes(LazyInputTypes):
_NO_REPLACE = True
def __init__(self, *args: list, initializers=(), **kwargs: dict):
super().__init__(
*args,
initializers=(MODULES.initialize, *initializers),
**kwargs,
)
class SonarCustomNoiseNodeBase(metaclass=IntegratedNode):
DESCRIPTION = "A custom noise item."
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
@@ -58,48 +199,23 @@ class SonarCustomNoiseNodeBase(metaclass=IntegratedNode):
def get_item_class(self):
raise NotImplementedError
@classmethod
def INPUT_TYPES(cls, *, include_rescale=True, include_chain=True):
result = {
"required": {
"factor": (
"FLOAT",
{
"default": 1.0,
"min": -10000.0,
"max": 10000.0,
"step": 0.001,
"round": False,
"tooltip": "Scaling factor for the generated noise of this type.",
},
),
},
"optional": {},
}
if include_rescale:
result["required"] |= {
"rescale": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 10000.0,
"step": 0.001,
"round": False,
"tooltip": "When non-zero, this custom noise item and other custom noise items items connected to it will have their factor scaled to add up to the specified rescale value. When set to 0, rescaling is disabled.",
},
),
}
if include_chain:
result["optional"] |= {
"sonar_custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Optional input for more custom noise items.\n{NOISE_INPUT_TYPES_HINT}",
},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda *, include_rescale=True, include_chain=True: SonarInputTypes()
.req_float_factor(
default=1.0,
tooltip="Scaling factor for the generated noise of this type.",
)
.req_float_rescale(
_skip=not include_rescale,
default=0.0,
min=0.0,
tooltip="When non-zero, this custom noise item and other custom noise items items connected to it will have their factor scaled to add up to the specified rescale value. When set to 0, rescaling is disabled.",
)
.opt_customnoise_sonar_custom_noise_opt(
_skip=not include_chain,
tooltip="Optional input for more custom noise items.",
),
)
def go(
self,
@@ -118,19 +234,35 @@ class SonarCustomNoiseNodeBase(metaclass=IntegratedNode):
return (nis if rescale == 0 else nis.rescaled(rescale),)
class NoiseChainInputTypes(SonarInputTypes):
def __init__(self, *, parent=SonarCustomNoiseNodeBase, **kwargs: dict):
super().__init__(parent=parent, **kwargs)
class NoiseNoChainInputTypes(SonarInputTypes):
def __init__(
self,
*,
parent=SonarCustomNoiseNodeBase,
parent_args=(),
parent_kwargs=None,
**kwargs: dict,
):
super().__init__(
parent=parent,
parent_args=parent_args,
parent_kwargs={"include_chain": False, "include_rescale": False}
| (parent_kwargs if parent_kwargs is not None else {}),
**kwargs,
)
class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"noise_type": (
tuple(noise.NoiseType.get_names()),
{
"tooltip": "Sets the type of noise to generate.",
},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes().req_selectnoise_noise_type(
tooltip="Sets the type of noise to generate.",
),
)
@classmethod
def get_item_class(cls):
@@ -140,21 +272,11 @@ class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
class SonarCustomNoiseAdvNode(SonarCustomNoiseNode):
DESCRIPTION = "A custom noise item allowing advanced YAML parameter input."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["optional"] |= {
"yaml_parameters": (
"STRING",
{
"tooltip": "Allows specifying custom parameters via YAML. Note: When specifying paramaters this way, there is no error checking.",
"placeholder": "# YAML or JSON here",
"dynamicPrompts": False,
"multiline": True,
},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes(parent=SonarCustomNoiseNode).opt_yaml(
tooltip="Allows specifying custom parameters via YAML. Note: When specifying paramaters this way, there is generally little to no error checking.",
),
)
class SonarNormalizeNoiseNodeMixin:
+263
View File
@@ -0,0 +1,263 @@
# ruff: noqa: A002
from __future__ import annotations
from copy import deepcopy
from functools import partial
from typing import Callable, TypeVar
class InputCollection:
_DELEGATE_KEYS = frozenset((
"bool",
"boolean",
"clip",
"conditioning",
"field",
"float",
"image",
"int",
"latent",
"model",
"sampler",
"seed",
"sigmas",
"string",
"vae",
))
def __init__(self, **kwargs: dict):
self.fields = kwargs
def __getattr__(self, key: str):
splitkey = key.split("_", 1)
if len(splitkey) == 1 or splitkey[0] not in self._DELEGATE_KEYS:
errstr = f"Unknown attribute {key} for InputCollection"
raise AttributeError(errstr)
meth = getattr(self, splitkey[0])
return partial(meth, splitkey[1]) if len(splitkey) == 2 else meth
def to_dict(self):
return deepcopy(self.fields)
def clone(self):
return InputCollection(**self.to_dict())
def __len__(self) -> int:
return len(self.fields)
def __contains__(self, key: str) -> bool:
return key in self.fields
def field(
self,
name: str,
type: str | tuple,
*,
_skip: bool = False,
**kwargs: dict,
) -> InputCollection:
if not _skip:
self.fields[name] = (type,) if not kwargs else (type, kwargs)
return self
def string(
self,
name: str,
**kwargs: dict,
) -> InputCollection:
return self.field(name, "STRING", **kwargs)
def float(
self,
name: str,
*,
step: float = 0.001,
min: float = -10000.0,
max: float = 10000.0,
round: bool = False,
**kwargs: dict,
) -> InputCollection:
return self.field(
name,
"FLOAT",
step=step,
min=min,
max=max,
round=round,
**kwargs,
)
def int(
self,
name: str,
*,
min: float = -10000,
max: float = 10000,
**kwargs: dict,
) -> InputCollection:
return self.field(
name,
"INT",
min=min,
max=max,
**kwargs,
)
def bool(
self,
name: str,
default: bool = False,
**kwargs: dict,
) -> InputCollection:
return self.field(name, "BOOLEAN", default=default, **kwargs)
boolean = bool
def seed(
self,
name: str = "seed",
*,
default: int = 0,
min: int = 0,
max: int = 0xFFFFFFFFFFFFFFFF,
tooltip="Seed to use for generated noise",
**kwargs: dict,
) -> InputCollection:
return self.int(
name,
default=default,
min=min,
max=max,
tooltip=tooltip,
**kwargs,
)
def image(self, name: str = "image", **kwargs: dict) -> InputCollection:
return self.field(name, "IMAGE", **kwargs)
def latent(self, name: str = "latent", **kwargs: dict) -> InputCollection:
return self.field(name, "LATENT", **kwargs)
def conditioning(
self,
name: str = "conditioning",
**kwargs: dict,
) -> InputCollection:
return self.field(name, "CONDITIONING", **kwargs)
def model(self, name: str = "model", **kwargs: dict) -> InputCollection:
return self.field(name, "MODEL", **kwargs)
def sigmas(self, name: str = "sigmas", **kwargs: dict) -> InputCollection:
return self.field(name, "SIGMAS", **kwargs)
def sampler(self, name: str = "sampler", **kwargs: dict) -> InputCollection:
return self.field(name, "SAMPLER", **kwargs)
def clip(self, name: str = "clip", **kwargs: dict) -> InputCollection:
return self.field(name, "CLIP", **kwargs)
def vae(self, name: str = "vae", **kwargs: dict) -> InputCollection:
return self.field(name, "VAE", **kwargs)
class InputTypes:
C = TypeVar("C", bound=type)
def __init__(
self,
*,
parent=None,
parent_field: str | None = "INPUT_TYPES",
parent_args=(),
parent_kwargs=None,
required: dict | C | None = None,
optional: dict | C | None = None,
collection_class: C = InputCollection,
):
if parent is not None and parent_field is not None:
parent = getattr(parent, parent_field)
if isinstance(parent, LazyInputTypes):
parent = parent.get_input_types(
*parent_args,
**({} if parent_kwargs is None else parent_kwargs),
)
if isinstance(parent, LazyInputTypes):
raise TypeError("Unexpected multi-level LazyInputTypes parent!")
if required is None:
required = {}
elif isinstance(required, collection_class):
required = required.to_dict()
elif not isinstance(required, dict):
raise TypeError("Bad type for 'required' parameter.")
if optional is None:
optional = {}
elif isinstance(optional, collection_class):
optional = optional.to_dict()
elif not isinstance(optional, dict):
raise TypeError("Bad type for 'optional' parameter.")
if parent is not None:
required = parent.required.to_dict() | required
optional = parent.optional.to_dict() | optional
self.required = collection_class(**required)
self.optional = collection_class(**optional)
def __len__(self) -> int:
return len(self.required) + len(self.optional)
def clone(self) -> InputTypes:
return InputTypes(required=self.required, optional=self.optional)
def to_dict(self) -> dict:
return {
"required": self.required.to_dict(),
"optional": self.optional.to_dict(),
}
def __call__(self) -> dict:
return self.to_dict()
def __getattr__(self, key: str):
if key.startswith("req_"):
meth = getattr(self.required, key[4:])
elif key.startswith("opt_"):
meth = getattr(self.optional, key[4:])
else:
errstr = f"Unknown attribute {key} for InputTypes"
raise AttributeError(errstr)
def wrapper(*args: list, **kwargs: dict):
meth(*args, **kwargs)
return self
return wrapper
class LazyInputTypes:
def __init__(self, builder: Callable, initializers=()):
self._input_types_params = {}
self._input_types = None
self.builder = builder
self.initializers = initializers
def get_input_types(self, *args: list, **kwargs: dict):
if args or kwargs:
args = tuple(args)
cache_key = (args, tuple(kwargs.items()))
cached = self._input_types_params.get(cache_key)
else:
cache_key = None
cached = self._input_types
if cached:
return cached
for fun in self.initializers:
fun()
result = self.builder(*args, **kwargs)
if not cache_key:
self._input_types = result
else:
self._input_types_params[cache_key] = result
return result
def __call__(self, *args: list, **kwargs: dict) -> dict:
return self.get_input_types(*args, **kwargs)()
@@ -2,8 +2,8 @@ from __future__ import annotations
import torch
from . import utils
from .external import IntegratedNode
from .. import utils
from ..external import IntegratedNode
from .powernoise import PowerFilter
-2
View File
@@ -1,5 +1,3 @@
# ruff: noqa: TID252
from __future__ import annotations
from comfy import samplers
+195 -302
View File
@@ -1,5 +1,3 @@
# ruff: noqa: TID252
from __future__ import annotations
import functools
@@ -14,7 +12,7 @@ from ..latent_ops import (
SonarLatentOperationNoise,
SonarLatentOperationSetSeed,
)
from .base import NOISE_INPUT_TYPES_HINT, WILDCARD_NOISE
from .base import SonarInputTypes, SonarLazyInputTypes
from .noise_filters import SonarQuantileFilteredNoiseNode
if TYPE_CHECKING:
@@ -28,157 +26,96 @@ class SonarApplyLatentOperationCFG(metaclass=IntegratedNode):
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"mode": (
(
"cond_sub_uncond",
"denoised_sub_uncond",
"uncond_sub_cond",
"denoised",
"cond",
"uncond",
"model_input",
),
{
"default": "cond_sub_uncond",
"tooltip": "cond_sub_uncond is what ComfyUI's latent operations use. The non-sub_uncond modes likely won't work with pred_flip mode enabled. If you have anything but the denoised options selected, this will use pre-CFG, otherwise it will use post-CFG (unless you are using model_input).",
},
),
"pred_flip_mode": (
"BOOLEAN",
{
"default": False,
"tooltip": "Lets you try to apply the latent operation to the noise prediction rather than the image prediction. Doesn't work properly with the non-sub_uncond modes. No real reason it should be better, just something you can try. Note: The noise prediction gets scaled by the sigma first, in case that's useful information.",
},
),
"require_uncond": (
"BOOLEAN",
{
"default": False,
"tooltip": "When enabled, the operation will be skipped if uncond is unavailable. This will also happen if you choose a mode that requires uncond.",
},
),
"start_sigma": (
"FLOAT",
{
"default": -1.0,
"min": -1.0,
"max": 9999.0,
"tooltip": "Sigma when the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.",
},
),
"end_sigma": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 9999.0,
},
),
"blend_mode": (
tuple(utils.BLENDING_MODES.keys()),
{
"default": "lerp",
"tooltip": "Controls how the output of the latent operation is blended with the original result.",
},
),
"blend_strength": (
"FLOAT",
{
"default": 0.5,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Strength of the blend. For a normal blend mode like LERP, 1.0 means use 100% of the output from the latent operation, 0.0 means use none of it and only the original value. Note: Blending is applied to the final result of the operations, in other words operation_2 sees a full unblended result from operation_1.",
},
),
"blend_scale_mode": (
(
"none",
"reverse_sampling",
"sampling",
"reverse_enabled_range",
"enabled_range",
"sampling_sin",
"enabled_range_sin",
),
{
"default": "reverse_sampling",
"tooltip": "Can be used to scale the blend strength over time. Basically works like blend_strength * scale_factor (see below)\nnone: Just uses the blend_strength you have set.\nreverse_sampling: The opposite of the model sampling percent, so if you're making a new generation, the beginning of sampling will be 1.0 and the end will be 0.0. The recommended option as applying these operations usually works better toward the beginning of sampling.\nsampling: Same as reverse_sampling, except the beginning will be 0.0 and the end will be 1.0.\nreverse_enabled_range: Flipped percentage of the range between start_sigma and end_sigma.\nenabled_range: Percentage of the range between start_sigma and end_sigma.\nsampling_sin: Uses the sampling percentage with the sine function such that blend_strength will hit the peak value in the middle of the range.\nenabled_range_sin: Similar to sampling_sin except it applies to the percentage of the enabled range.",
},
),
"blend_scale_offset": (
"FLOAT",
{
"default": 0.0,
"min": -1.0,
"max": 1.0,
"tooltip": "Only applies when blend_scale_mode is not none. Adds the offset to the calculated percentage and then clamps it to be between blend_scale_min and blend_scale_max.",
},
),
"blend_scale_min": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 1.0,
"tooltip": "Only applies when blend_scale_mode is not none. Minimum value for the blend scale percentage.",
},
),
"blend_scale_max": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 1.0,
"tooltip": "Only applies when blend_scale_mode is not none. Maximum value for the blend scale percentage.",
},
),
"immediate_blend": (
"BOOLEAN",
{
"default": False,
},
),
},
"optional": {
"operation_1": (
"LATENT_OPERATION",
{
"tooltip": "Optional LATENT_OPERATION. The operations will be applied in sequence.",
},
),
"operation_2": (
"LATENT_OPERATION",
{
"tooltip": "Optional LATENT_OPERATION. The operations will be applied in sequence.",
},
),
"operation_3": (
"LATENT_OPERATION",
{
"tooltip": "Optional LATENT_OPERATION. The operations will be applied in sequence.",
},
),
"operation_4": (
"LATENT_OPERATION",
{
"tooltip": "Optional LATENT_OPERATION. The operations will be applied in sequence.",
},
),
"operation_5": (
"LATENT_OPERATION",
{
"tooltip": "Optional LATENT_OPERATION. The operations will be applied in sequence.",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_model()
.req_field_mode(
(
"cond_sub_uncond",
"denoised_sub_uncond",
"uncond_sub_cond",
"denoised",
"cond",
"uncond",
"model_input",
),
default="cond_sub_uncond",
tooltip="cond_sub_uncond is what ComfyUI's latent operations use. The non-sub_uncond modes likely won't work with pred_flip mode enabled. If you have anything but the denoised options selected, this will use pre-CFG, otherwise it will use post-CFG (unless you are using model_input).",
)
.req_bool_pred_flip_mode(
tooltip="Lets you try to apply the latent operation to the noise prediction rather than the image prediction. Doesn't work properly with the non-sub_uncond modes. No real reason it should be better, just something you can try. Note: The noise prediction gets scaled by the sigma first, in case that's useful information.",
)
.req_bool_require_uncond(
tooltip="When enabled, the operation will be skipped if uncond is unavailable. This will also happen if you choose a mode that requires uncond.",
)
.req_float_start_sigma(
default=-1.0,
min=-1.0,
tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.",
)
.req_float_end_sigma(
default=0.0,
min=0.0,
tooltip="Last sigma the effect is active.",
)
.req_selectblend_blend_mode(
tooltip="Controls how the output of the latent operation is blended with the original result.",
)
.req_float_blend_strength(
default=0.5,
tooltip="Strength of the blend. For a normal blend mode like LERP, 1.0 means use 100% of the output from the latent operation, 0.0 means use none of it and only the original value. Note: Blending is applied to the final result of the operations unless you enable immediate_blend, in other words operation_2 sees a full unblended result from operation_1.",
)
.req_field_blend_scale_mode(
(
"none",
"reverse_sampling",
"sampling",
"reverse_enabled_range",
"enabled_range",
"sampling_sin",
"enabled_range_sin",
),
default="reverse_sampling",
tooltip="Can be used to scale the blend strength over time. Basically works like blend_strength * scale_factor (see below)\nnone: Just uses the blend_strength you have set.\nreverse_sampling: The opposite of the model sampling percent, so if you're making a new generation, the beginning of sampling will be 1.0 and the end will be 0.0. The recommended option as applying these operations usually works better toward the beginning of sampling.\nsampling: Same as reverse_sampling, except the beginning will be 0.0 and the end will be 1.0.\nreverse_enabled_range: Flipped percentage of the range between start_sigma and end_sigma.\nenabled_range: Percentage of the range between start_sigma and end_sigma.\nsampling_sin: Uses the sampling percentage with the sine function such that blend_strength will hit the peak value in the middle of the range.\nenabled_range_sin: Similar to sampling_sin except it applies to the percentage of the enabled range.",
)
.req_float_blend_scale_offset(
default=0.0,
min=-1.0,
max=1.0,
tooltip="Only applies when blend_scale_mode is not none. Adds the offset to the calculated percentage and then clamps it to be between blend_scale_min and blend_scale_max.",
)
.req_float_blend_scale_min(
default=0.0,
tooltip="Only applies when blend_scale_mode is not none. Minimum value for the blend scale percentage. Many blend modes don't tolerate negative values here.",
)
.req_float_blend_scale_max(
default=1.0,
tooltip="Only applies when blend_scale_mode is not none. Maximum value for the blend scale percentage. Many blend modes don't tolerate values over 1.0 here.",
)
.req_bool_immediate_blend(
tooltip="You can enable this to do blending immediately after each latent operation is called. Mainly affects the case where you have multiple latent operations connected.",
)
.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.",
),
)
@staticmethod
def get_blend_scaling(
@@ -274,7 +211,7 @@ class SonarApplyLatentOperationCFG(metaclass=IntegratedNode):
blend_scale_mode = "none"
orig_mode = mode
def patch(args: dict) -> torch.Tensor: # noqa: PLR0914
def patch(args: dict) -> torch.Tensor:
nonlocal mode
x = args["input"]
@@ -418,100 +355,69 @@ class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode):
class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode):
DESCRIPTION = "Allows scheduling and other advanced features for latent operations."
DESCRIPTION = "Allows scheduling and other advanced features for latent operations. If you attach the optional extra LATENT_OPERATIONS, they will be called in sequence _before_ blending or output scaling."
RETURN_TYPES = ("LATENT_OPERATION",)
CATEGORY = "latent/advanced/operations"
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"operation": (
"LATENT_OPERATION",
{
"tooltip": "Latent operation to apply.",
},
),
"start_sigma": (
"FLOAT",
{
"default": -1.0,
"min": -1.0,
"max": 9999.0,
"tooltip": "Sigma when the effect becomes active. You can use -1.0 here for no limit.",
},
),
"end_sigma": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 9999.0,
},
),
"input_multiplier": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Flat multiplier on the input to the latent operation. The multiplied input is *not* used when calculating the difference, it is only passed to the operation.",
},
),
"output_multiplier": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Flat multiplier on the output from the latent operation. Occurs before blending or calculating the difference.",
},
),
"difference_multiplier": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Flat multiplier on the difference or change from the original that the operation performed. Occurs after output_multiplier and before blending applies.",
},
),
"blend_mode": (
tuple(utils.BLENDING_MODES.keys()),
{
"default": "inject",
"tooltip": "Controls how the change from the operation is combined with the input. The default of inject just adds it scaled by the blend strength. With 1.0 blend strength, this is just using the output from the operation with no change.",
},
),
"blend_strength": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Strength of the blend.",
},
),
},
"optional": {
"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).",
},
),
},
}
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,
tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.",
)
.req_float_end_sigma(
default=0.0,
min=0.0,
tooltip="Last sigma the effect is active.",
)
.req_float_input_multiplier(
default=1.0,
tooltip="Flat multiplier on the input to the latent operation. The multiplied input is *not* used when calculating the difference, it is only passed to the operation.",
)
.req_float_output_multiplier(
default=1.0,
tooltip="Flat multiplier on the output from the latent operation. Occurs before blending or calculating the difference.",
)
.req_float_difference_multiplier(
default=1.0,
tooltip="Flat multiplier on the difference or change from the original that the operation performed. Occurs after output_multiplier and before blending applies.",
)
.req_selectblend_blend_mode(
default="inject",
tooltip="Controls how the change from the operation is combined with the input. The default of inject just adds it scaled by the blend strength. With 1.0 blend strength, this is just using the output from the operation with no change.",
)
.req_float_blend_strength(
default=0.5,
tooltip="Strength of the blend.",
)
.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).",
)
.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.",
),
)
@classmethod
def go(
@@ -526,9 +432,16 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode):
blend_mode: str,
blend_strength: float,
operation_alt=None,
operation_2=None,
operation_3=None,
operation_4=None,
operation_5=None,
) -> tuple[SonarLatentOperationAdvanced]:
if not isinstance(operation, SonarLatentOperation):
operation = SonarLatentOperation(op=operation)
operations = tuple(
o if isinstance(o, SonarLatentOperation) else SonarLatentOperation(op=o)
for o in (operation, operation_2, operation_3, operation_4, operation_5)
if o is not None
)
if operation_alt is not None and not isinstance(
operation_alt,
SonarLatentOperation,
@@ -536,7 +449,7 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode):
operation_alt = SonarLatentOperation(op=operation_alt)
return (
SonarLatentOperationAdvanced(
op=operation,
ops=operations,
op_alt=operation_alt,
start_sigma=start_sigma,
end_sigma=end_sigma,
@@ -556,44 +469,22 @@ class SonarLatentOperationNoiseNode(metaclass=IntegratedNode):
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"custom_noise": (
WILDCARD_NOISE,
{"tooltip": f"Custom noise. \n{NOISE_INPUT_TYPES_HINT}"},
),
"scale_to_sigma": (
"BOOLEAN",
{
"default": False,
"tooltip": "Scales the noise to the current sigma.",
},
),
"cpu_noise": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether noise is generated on the CPU or GPU. GPU is usually faster but may change seeds for different models of GPU.",
},
),
"normalize": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether the generated noise is normalized.",
},
),
"lazy_noise_sampler": (
"BOOLEAN",
{
"default": True,
"tooltip": "When enabled, the latent operation will attempt to cache the noise sampler between calls and only recreate it when necessary. However, there isn't a 100% reliable way for a latent operation to know when sampling starts/ends so if we get it wrong this will lead to non-deterministic generations. I believe the heuristic I'm using to detect this should be reliable but you can disable it if you notice weird results.",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_customnoise_custom_noise()
.req_bool_scale_to_sigma(tooltip="Scales the noise to the current sigma.")
.req_bool_cpu_noise(
tooltip="Controls whether noise is generated on the CPU or GPU. GPU is usually faster but may change seeds for different models of GPU.",
)
.req_bool_normalize(
default=True,
tooltip="Controls whether the generated noise is normalized.",
)
.req_bool_lazy_noise_sampler(
default=True,
tooltip="When enabled, the latent operation will attempt to cache the noise sampler between calls and only recreate it when necessary. However, there isn't a 100% reliable way for a latent operation to know when sampling starts/ends so if we get it wrong this will lead to non-deterministic generations. I believe the heuristic I'm using to detect this should be reliable but you can disable it if you notice weird results.",
),
)
@classmethod
def go(
@@ -623,22 +514,17 @@ class SonarLatentOperationSetSeedNode(metaclass=IntegratedNode):
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"operation": ("LATENT_OPERATION",),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "Seed to set. Note that this is called _every time_ before the operation.",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_field_operation("LATENT_OPERATION")
.req_seed(
tooltip="Seed to set. Note that this is called _every time_ before the operation.",
)
.req_bool_restore_rng_state(
default=False,
tooltip="When enabled, the current RNG state is saved just before calling the operation and restored afterwards. In other words, only the latent operation will see the seed you set. Note: This only handles the PyTorch and Python random module states.",
),
)
@classmethod
def go(
@@ -646,8 +532,15 @@ class SonarLatentOperationSetSeedNode(metaclass=IntegratedNode):
*,
operation,
seed: int,
restore_rng_state: bool,
) -> tuple[SonarLatentOperationSetSeed]:
return (SonarLatentOperationSetSeed(op=operation, seed=seed),)
return (
SonarLatentOperationSetSeed(
op=operation,
seed=seed,
restore_rng_state=restore_rng_state,
),
)
NODE_CLASS_MAPPINGS = {
+184 -403
View File
@@ -1,5 +1,3 @@
# ruff: noqa: TID252
from __future__ import annotations
import functools
@@ -17,9 +15,10 @@ from .. import noise, utils
from ..external import IntegratedNode
from ..noise import NoiseType
from .base import (
NOISE_INPUT_TYPES_HINT,
WILDCARD_NOISE,
NoiseChainInputTypes,
SonarCustomNoiseNodeBase,
SonarInputTypes,
SonarLazyInputTypes,
SonarNormalizeNoiseNodeMixin,
)
@@ -32,96 +31,44 @@ class NoisyLatentLikeNode(metaclass=IntegratedNode):
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"noise_type": (
tuple(noise.NoiseType.get_names()),
{
"default": "gaussian",
"tooltip": "Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.",
},
),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "Seed to use for generated noise.",
},
),
"latent": (
"LATENT",
{
"tooltip": "Latent used as a reference for generating noise.",
},
),
"multiplier": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -10000.0,
"max": 10000.0,
"round": False,
"tooltip": "Multiplier for the strength of the generated noise. Performed after mul_by_sigmas_opt.",
},
),
"add_to_latent": (
"BOOLEAN",
{
"default": False,
"tooltip": "Add the generated noise to the reference latent rather than adding it to an empty latent. Generally should be enabled for img2img workflows.",
},
),
"repeat_batch": (
"INT",
{
"default": 1,
"tooltip": "Repeats the noise generation the specified number of times. For example, if set to two and your reference latent is also batch two you will get a batch of four as output.",
},
),
"cpu_noise": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether noise will be generated on GPU or CPU. Only affects noise types that support GPU generation (maybe only Brownian).",
},
),
"normalize": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.",
},
),
},
"optional": {
"custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Allows connecting a custom noise chain. When connected, noise_type has no effect.\n{NOISE_INPUT_TYPES_HINT}",
},
),
"mul_by_sigmas_opt": (
"SIGMAS",
{
"tooltip": "When connected, will scale the generated noise by the first sigma. Must also connect model_opt to enable.",
},
),
"model_opt": (
"MODEL",
{
"tooltip": "Used when mul_by_sigmas_opt is connected, no effect otherwise.",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_selectnoise_noise_type(
tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.",
)
.req_seed()
.req_latent(tooltip="Latent used as a reference for generating noise.")
.req_float_multiplier(
default=1.0,
tooltip="Multiplier for the strength of the generated noise. Performed after mul_by_sigmas_opt.",
)
.req_bool_add_to_latent(
tooltip="Add the generated noise to the reference latent rather than adding it to an empty latent. Generally should be enabled for img2img workflows.",
)
.req_int_repeat_batch(
default=1,
min=1,
tooltip="Repeats the noise generation the specified number of times. For example, if set to two and your reference latent is also batch two you will get a batch of four as output.",
)
.req_bool_cpu_noise(
default=True,
tooltip="Controls whether noise will be generated on GPU or CPU. Only affects noise types that support GPU generation (maybe only Brownian).",
)
.req_bool_normalize(
default=True,
tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.",
)
.opt_customnoise_custom_noise_opt()
.opt_sigmas_mul_by_sigmas_opt(
tooltip="When connected, will scale the generated noise by the first sigma. Must also connect model_opt to enable.",
)
.opt_model_model_opt(
tooltip="Used when mul_by_sigmas_opt is connected, no effect otherwise.",
),
)
@classmethod
def go( # noqa: PLR0914
def go(
cls,
*,
noise_type: str,
@@ -213,161 +160,86 @@ class SonarNoiseImageNode(metaclass=IntegratedNode):
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"noise_type": (
tuple(NoiseType.get_names()),
{
"default": "gaussian",
"tooltip": "Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.",
},
),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "Seed to use for generated noise.",
},
),
"image": (
"IMAGE",
{
"tooltip": "Image noise will be added to.",
},
),
"noise_min": (
"FLOAT",
{
"default": 0.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.",
},
),
"noise_max": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.",
},
),
"noise_multiplier": (
"FLOAT",
{
"default": 0.5,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Multiplier for the strength of the generated noise. This is performed after noise_min/max scaling.",
},
),
"channel_mode": (
(
"RGB",
"RGBA",
"R",
"G",
"B",
"A",
"RA",
"GA",
"BA",
"RG",
"RB",
"GB",
"RGA",
"RBA",
"GBA",
),
{
"default": "RGB",
"tooltip": "RGBA will also add noise to the alpha channel as well if it exists. Only used for 3 or 4 channel images, for other numbers of channels (i.e. one channel) then all channels will be targeted.",
},
),
"blend_mode": (
("simple_add", *utils.BLENDING_MODES.keys()),
{
"default": "simple_add",
"tooltip": "Controls how the generated noise is combined with the image. simple_add just adds it and blend_strength is ignored in that case.",
},
),
"blend_strength": (
"FLOAT",
{
"default": 0.5,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Multiplier for the strength of the generated noise.",
},
),
"overflow_mode": (
("clamp", "rescale"),
{
"default": "clamp",
"tooltip": "When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.",
},
),
"greyscale_mode": (
"BOOLEAN",
{
"default": False,
"tooltip": "When enabled, generated noise will be averaged so the same amount value is added to all specified channels.",
},
),
"pure_noise_mode": (
"BOOLEAN",
{
"default": False,
"tooltip": "When enabled, the original image is only used for its shape and you will be adding noise to an image full of zeros (black), suitable for creating pure noise images.",
},
),
"dtype": (
("default", "float32", "float64", "float16", "bfloat16"),
{
"default": "default",
"tooltip": "When set to default it will use the same type as the input tensor (probably float32). You can manually set the dtype if you want, though it likely isn't going to matter. Using dtypes with limited range (float16, bfloat16) isn't recommended.",
},
),
"cpu_noise": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether noise will be generated on GPU or CPU.",
},
),
"normalize": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.",
},
),
},
"optional": {
"custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Allows connecting a custom noise chain. When connected, noise_type has no effect.\n{NOISE_INPUT_TYPES_HINT}",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_selectnoise_noise_type(
tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.",
)
.req_seed()
.req_image(tooltip="Image noise will be added to.")
.req_float_noise_min(
default=0.0,
tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.",
)
.req_float_noise_max(
default=1.0,
tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.",
)
.req_float_noise_multiplier(
default=0.5,
tooltip="Multiplier for the strength of the generated noise. This is performed after noise_min/max scaling.",
)
.req_field_channel_mode(
(
"RGB",
"RGBA",
"R",
"G",
"B",
"A",
"RA",
"GA",
"BA",
"RG",
"RB",
"GB",
"RGA",
"RBA",
"GBA",
),
default="RGB",
tooltip="RGBA will also add noise to the alpha channel as well if it exists. Only used for 3 or 4 channel images, for other numbers of channels (i.e. one channel) then all channels will be targeted.",
)
.req_selectblend(
insert_modes=("simple_add",),
default="simple_add",
tooltip="Controls how the generated noise is combined with the image. simple_add just adds it and blend_strength is ignored in that case.",
)
.req_float_blend_strength(
default=0.5,
tooltip="Multiplier for the strength of the generated noise.",
)
.req_field_overflow_mode(
("clamp", "rescale"),
default="clamp",
tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.",
)
.req_bool_greyscale_mode(
tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.",
)
.req_bool_pure_noise_mode(
tooltip="When enabled, the original image is only used for its shape and you will be adding noise to an image full of zeros (black), suitable for creating pure noise images.",
)
.req_field_dtype(
("default", "float32", "float64", "float16", "bfloat16"),
default="default",
tooltip="When set to default it will use the same type as the input tensor (probably float32). You can manually set the dtype if you want, though it likely isn't going to matter. Using dtypes with limited range (float16, bfloat16) isn't recommended.",
)
.req_bool_cpu_noise(
default=True,
tooltip="Controls whether noise will be generated on GPU or CPU.",
)
.req_bool_normalize(
default=True,
tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.",
)
.opt_customnoise_custom_noise_opt(
tooltip="Allows connecting a custom noise chain. When connected, noise_type has no effect.",
),
)
@classmethod
def go( # noqa: PLR0914
def go(
cls,
*,
noise_type: str,
@@ -551,52 +423,25 @@ class SonarToComfyNOISENode(metaclass=IntegratedNode):
CATEGORY = "sampling/custom_sampling/noise"
FUNCTION = "go"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"custom_noise": (
WILDCARD_NOISE,
{
"tooltip": f"Custom noise type to convert.\n{NOISE_INPUT_TYPES_HINT}",
},
),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "Seed to use for generated noise.",
},
),
"cpu_noise": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether noise is generated on CPU or GPU.",
},
),
"normalize": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether generated noise is normalized to 1.0 strength.",
},
),
"multiplier": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Simple multiplier applied to noise after all other scaling and normalization effects. If set to 0, no noise will be generated (same as disabling noise).",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_customnoise_custom_noise(
tooltip="Custom noise type to convert.",
)
.req_seed(tooltip="Seed to use for generated noise.")
.req_bool_cpu_noise(
default=True,
tooltip="Controls whether noise is generated on CPU or GPU.",
)
.req_bool_normalize(
default=True,
tooltip="Controls whether generated noise is normalized to 1.0 strength.",
)
.req_float_multiplier(
default=1.0,
tooltip="Simple multiplier applied to noise after all other scaling and normalization effects. If set to 0, no noise will be generated (same as disabling noise).",
),
)
@classmethod
def go(cls, *, custom_noise, seed, cpu_noise=True, normalize=True, multiplier=1.0):
@@ -614,100 +459,47 @@ class SonarToComfyNOISENode(metaclass=IntegratedNode):
class SamplerNodeConfigOverride(metaclass=IntegratedNode):
DESCRIPTION = "Allows overriding paramaters for a SAMPLER. Only parameters that particular sampler supports will be applied, so for example setting ETA will have no effect for non-ancestral Euler."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sampler": ("SAMPLER",),
"eta": (
"FLOAT",
{
"default": 1.0,
"step": 0.01,
"max": 1000.0,
"round": False,
"tooltip": "Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.",
},
),
"s_noise": (
"FLOAT",
{
"default": 1.0,
"step": 0.01,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Multiplier for noise added during ancestral or SDE sampling.",
},
),
"s_churn": (
"FLOAT",
{
"default": 0.0,
"step": 0.01,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Churn was the predececessor of ETA. Only used by a few types of samplers (notably Euler non-ancestral). Not used by any ancestral or SDE samplers.",
},
),
"r": (
"FLOAT",
{
"default": 0.5,
"step": 0.01,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Used by dpmpp_sde.",
},
),
"sde_solver": (
("midpoint", "heun"),
{
"tooltip": "Solver used by dpmpp_2m_sde.",
},
),
"cpu_noise": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether noise is generated on CPU or GPU.",
},
),
"normalize": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether generated noise is normalized to 1.0 strength.",
},
),
},
"optional": {
"noise_type": (
("DEFAULT", *NoiseType.get_names()),
{
"default": "DEFAULT",
"tooltip": "Noise type used during ancestral or SDE sampling. Leave blank to use the default for the attached sampler. Only used when the custom noise input is not connected.",
},
),
"custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.\n{NOISE_INPUT_TYPES_HINT}",
},
),
"yaml_parameters": (
"STRING",
{
"tooltip": "Allows specifying custom parameters via YAML. Note: When specifying paramaters this way, there is no error checking.",
"placeholder": "# YAML or JSON here",
"dynamicPrompts": False,
"multiline": True,
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_sampler()
.req_float_eta(
default=1.0,
tooltip="Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.",
)
.req_float_s_noise(
default=1.0,
tooltip="Multiplier for noise added during ancestral or SDE sampling.",
)
.req_float_s_churn(
default=0.0,
tooltip="Churn was the predececessor of ETA. Only used by a few types of samplers (notably Euler non-ancestral). Not used by any ancestral or SDE samplers.",
)
.req_float_r(
default=0.5,
tooltip="Used by dpmpp_sde (and perhaps a few other SDE samplers).",
)
.req_field_sde_solver(
("midpoint", "heun"),
tooltip="Solver used by dpmpp_2m_sde.",
)
.req_bool_cpu_noise(
default=True,
tooltip="Controls whether noise is generated on CPU or GPU.",
)
.req_bool_normalize(
default=True,
tooltip="Controls whether generated noise is normalized to 1.0 strength.",
)
.opt_selectnoise_noise_type(
insert_types=("DEFAULT",),
default="DEFAULT",
tooltip="Noise type used during ancestral or SDE sampling. DEFAULT will use the default for the attached sampler. Only used when the custom noise input is not connected.",
)
.opt_customnoise_custom_noise_opt(
tooltip="Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.",
)
.opt_yaml(),
)
RETURN_TYPES = ("SAMPLER",)
CATEGORY = "sampling/custom_sampling/samplers"
@@ -834,24 +626,13 @@ class SamplerNodeConfigOverride(metaclass=IntegratedNode):
class SonarSplitNoiseChainNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin):
DESCRIPTION = "Custom noise type that allows splitting off a new chain. This can be useful if you want a link in the chain to be a blended type."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"normalize": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength.",
},
),
}
result["optional"] |= {
"custom_noise": (
WILDCARD_NOISE,
{"tooltip": f"Custom noise. \n{NOISE_INPUT_TYPES_HINT}"},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_normalizetristate_normalize(
tooltip="Controls whether the generated noise is normalized to 1.0 strength.",
)
.opt_customnoise_custom_noise(),
)
@classmethod
def get_item_class(cls):
+84 -198
View File
@@ -1,5 +1,3 @@
# ruff: noqa: TID252
from __future__ import annotations
from comfy import samplers
@@ -15,55 +13,37 @@ from ..sonar import (
SonarEuler,
SonarEulerAncestral,
)
from .base import NOISE_INPUT_TYPES_HINT, WILDCARD_NOISE
from .base import SonarInputTypes, SonarLazyInputTypes
class GuidanceConfigNode:
class GuidanceConfigNode(metaclass=IntegratedNode):
DESCRIPTION = "Allows specifying extended guidance parameters for Sonar samplers."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"factor": (
"FLOAT",
{
"default": 0.01,
"min": -2.0,
"max": 2.0,
"step": 0.001,
"round": False,
"tooltip": "Controls the strength of the guidance. You'll generally want to use fairly low values here.",
},
),
"guidance_type": (
tuple(t.name.lower() for t in GuidanceType),
{
"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.",
},
),
"start_step": (
"INT",
{
"default": 0,
"min": 0,
"tooltip": "First zero-based step the guidance is active.",
},
),
"end_step": (
"INT",
{
"default": 9999,
"min": 0,
"tooltip": "Last zero-based step the guidance is active.",
},
),
"latent": (
"LATENT",
{"tooltip": "Latent to use as a reference for guidance."},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_float_factor(
default=0.01,
min=-2.0,
max=2.0,
tooltip="Controls the strength of the guidance. You'll generally want to use fairly low values here.",
)
.req_field_guidance_type(
tuple(t.name.lower() for t in GuidanceType),
default="linear",
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_int_start_step(
default=0,
min=0,
tooltip="First zero-based step the guidance is active.",
)
.req_int_end_step(
default=9999,
min=0,
tooltip="Last zero-based step the guidance is active.",
)
.req_latent(tooltip="Latent to use as a reference for guidance."),
)
RETURN_TYPES = ("SONAR_GUIDANCE_CFG",)
CATEGORY = "sampling/custom_sampling/samplers"
@@ -90,68 +70,44 @@ class GuidanceConfigNode:
)
class SamplerNodeSonarBase(metaclass=IntegratedNode):
class SamplerNodeSonarBase:
DESCRIPTION = "Sonar - momentum based sampler node."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"momentum": (
"FLOAT",
{
"default": 0.95,
"min": -0.5,
"max": 2.5,
"step": 0.01,
"round": False,
"tooltip": "How much of the normal result to keep during sampling. 0.95 means 95% normal, 5% from history. When set to 1.0 effectively disables momentum.",
},
),
"momentum_hist": (
"FLOAT",
{
"default": 0.75,
"min": -1.5,
"max": 1.5,
"step": 0.01,
"round": False,
"tooltip": "How much of the existing history to leave at each update. 0.75 means keep 75%, mix in 25% of the new result.",
},
),
"momentum_init": (
tuple(t.name for t in HistoryType),
{
"tooltip": "Initial value used for momentum history. ZERO - history starts zeroed out. RAND - History is initialized with a random value. SAMPLE - History is initialized from the latent at the start of sampling.",
},
),
"direction": (
"FLOAT",
{
"default": 1.0,
"min": -30.0,
"max": 15.0,
"step": 0.01,
"round": False,
"tooltip": "Multiplier applied to the result of normal sampling.",
},
),
"rand_init_noise_type": (
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
{
"tooltip": "Noise type to use when momentum_init is set to RANDOM.",
},
),
},
"optional": {
"guidance_cfg_opt": (
"SONAR_GUIDANCE_CFG",
{
"tooltip": "Optional input for extended guidance parameters.",
},
),
},
}
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes()
.req_float_momentum(
default=0.95,
min=-0.5,
max=2.5,
tooltip="How much of the normal result to keep during sampling. 0.95 means 95% normal, 5% from history. When set to 1.0 effectively disables momentum.",
)
.req_float_momentum_hist(
default=0.75,
min=-1.5,
max=1.5,
tooltip="How much of the existing history to leave at each update. 0.75 means keep 75%, mix in 25% of the new result.",
)
.req_field_momentum_init(
tuple(t.name for t in HistoryType),
default="ZERO",
tooltip="Initial value used for momentum history. ZERO - history starts zeroed out. RAND - History is initialized with a random value. SAMPLE - History is initialized from the latent at the start of sampling.",
)
.req_float_direction(
default=1.0,
min=-30.0,
max=15.0,
tooltip="Multiplier applied to the result of normal sampling.",
)
.req_field_init_noise_type(
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
default="gaussian",
tooltip="Noise type to use when momentum_init is set to RANDOM.",
)
.opt_field_guidance_cfg_opt(
"SONAR_GUIDANCE_CFG",
tooltip="Optional input for extended guidance parameters.",
),
)
RETURN_TYPES = ("SAMPLER",)
CATEGORY = "sampling/custom_sampling/samplers"
@@ -186,52 +142,23 @@ class SamplerNodeSonarEuler(SamplerNodeSonarBase):
class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"].update(
{
"s_noise": (
"FLOAT",
{
"default": 1.0,
"min": -1000.0,
"max": 1000.0,
"step": 0.01,
"round": False,
"tooltip": "Multiplier for noise added during ancestral or SDE sampling.",
},
),
"eta": (
"FLOAT",
{
"default": 1.0,
"min": -1000.0,
"max": 1000.0,
"step": 0.01,
"round": False,
"tooltip": "Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.",
},
),
"noise_type": (
tuple(NoiseType.get_names()),
{
"tooltip": "Noise type used during ancestral or SDE sampling. Only used when the custom noise input is not connected.",
},
),
},
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes(parent=SamplerNodeSonarEuler)
.req_float_s_noise(
default=1.0,
tooltip="Multiplier for noise added during ancestral or SDE sampling.",
)
result["optional"].update(
{
"custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.\n{NOISE_INPUT_TYPES_HINT}",
},
),
},
.req_float_eta(
default=1.0,
tooltip="Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.",
)
return result
.req_selectnoise_noise_type(
tooltip="Noise type used during ancestral or SDE sampling. Only used when the custom noise input is not connected.",
)
.opt_customnoise_custom_noise_opt(
tooltip="Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.",
),
)
@classmethod
def get_sampler(
@@ -270,53 +197,12 @@ class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
)
class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"].update(
{
"s_noise": (
"FLOAT",
{
"default": 1.0,
"min": -1000.0,
"max": 1000.0,
"step": 0.01,
"round": False,
"tooltip": "Multiplier for noise added during ancestral or SDE sampling.",
},
),
"eta": (
"FLOAT",
{
"default": 1.0,
"min": -1000.0,
"max": 1000.0,
"step": 0.01,
"round": False,
"tooltip": "Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.",
},
),
"noise_type": (
tuple(NoiseType.get_names(default=NoiseType.BROWNIAN)),
{
"tooltip": "Noise type used during ancestral or SDE sampling. Only used when the custom noise input is not connected.",
},
),
},
)
result["optional"].update(
{
"custom_noise_opt": (
WILDCARD_NOISE,
{
"tooltip": f"Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.\n{NOISE_INPUT_TYPES_HINT}",
},
),
},
)
return result
class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEulerAncestral):
INPUT_TYPES = SonarLazyInputTypes(
lambda: SonarInputTypes(
parent=SamplerNodeSonarEulerAncestral,
).req_selectnoise_noise_type(default="brownian"),
)
@classmethod
def get_sampler(
+730 -1027
View File
File diff suppressed because it is too large Load Diff
+254 -471
View File
@@ -1,5 +1,3 @@
# ruff: noqa: TID252
from __future__ import annotations
import torch
@@ -7,9 +5,9 @@ import torch
from .. import noise, utils
from ..noise_generation import DistroNoiseGenerator
from .base import (
NOISE_INPUT_TYPES_HINT,
WILDCARD_NOISE,
NoiseChainInputTypes,
SonarCustomNoiseNodeBase,
SonarLazyInputTypes,
SonarNormalizeNoiseNodeMixin,
)
@@ -19,50 +17,33 @@ class SonarAdvancedPyramidNoiseNode(SonarCustomNoiseNodeBase):
"Custom noise type that allows specifying parameters for Pyramid variants."
)
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"variant": (
(
"highres_pyramid",
"pyramid",
"pyramid_old",
),
{
"tooltip": "Sets the Pyramid noise variant to generate.",
"default": "highres_pyramid",
},
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_field_variant(
(
"highres_pyramid",
"pyramid",
"pyramid_old",
),
"iterations": (
"INT",
{
"default": -1,
"min": -1,
"max": 8,
"tooltip": "When set to -1 will use the variant default.",
},
),
"discount": (
"FLOAT",
{
"default": 0.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "When set to 0 will use the variant default.",
},
),
"upscale_mode": (
("default", *utils.UPSCALE_METHODS),
{
"tooltip": "Allows setting the scaling mode for Pyramid noise. Leave on default to use the variant default.",
"default": "default",
},
),
}
return result
default="highres_pyramid",
tooltip="Sets the Pyramid noise variant to generate.",
)
.req_int_iterations(
default=-1,
min=-1,
max=8,
tooltip="When set to -1 will use the variant default.",
)
.req_float_discount(
default=0.0,
tooltip="When set to 0 will use the variant default.",
)
.req_selectscalemode_upscale_mode(
insert_modes=("default",),
default="default",
tooltip="Allows setting the scaling mode for Pyramid noise. Leave on default to use the variant default.",
),
)
@classmethod
def get_item_class(cls):
@@ -93,63 +74,29 @@ class SonarAdvancedPyramidNoiseNode(SonarCustomNoiseNodeBase):
class SonarAdvanced1fNoiseNode(SonarCustomNoiseNodeBase):
DESCRIPTION = "Custom noise type that allows specifying parameters for 1f (pink, green, etc) variants."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"alpha": (
"FLOAT",
{
"default": 0.25,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.",
},
),
"k": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Currently no description of exactly what it does, it's just another knob you can try turning for a different effect.",
},
),
"vertical_factor": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Vertical frequency scaling factor.",
},
),
"horizontal_factor": (
"FLOAT",
{
"default": 1.0,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Horizontal frequency scaling factor.",
},
),
"use_sqrt": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether to sqrt when dividing the FFT. Negative hfac/wfac won't work when enabled. Turning it off seems to make the parameters have a much stronger effect.",
},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_float_alpha(
default=0.25,
tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.",
)
.req_float_k(
default=1.0,
tooltip="Currently no description of exactly what it does, it's just another knob you can try turning for a different effect.",
)
.req_float_vertical_factor(
default=1.0,
tooltip="Vertical frequency scaling factor.",
)
.req_float_horizontal_factor(
default=1.0,
tooltip="Horizontal frequency scaling factor.",
)
.req_bool_use_sqrt(
default=True,
tooltip="Controls whether to sqrt when dividing the FFT. Negative hfac/wfac won't work when enabled. Turning it off seems to make the parameters have a much stronger effect.",
),
)
@classmethod
def get_item_class(cls):
@@ -180,55 +127,36 @@ class SonarAdvanced1fNoiseNode(SonarCustomNoiseNodeBase):
class SonarAdvancedPowerLawNoiseNode(SonarCustomNoiseNodeBase):
DESCRIPTION = "Custom noise type that allows specifying parameters for power law (grey, violet, etc) variants. "
DESCRIPTION = "Custom noise type that allows specifying parameters for power law (grey, violet, etc) variants."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"alpha": (
"FLOAT",
{
"default": 0.5,
"step": 0.001,
"min": -1000.0,
"max": 1000.0,
"round": False,
"tooltip": "Alpha parameter of the generated noise. Positive values (low frequency noise) tend to produce colorful results.",
},
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_float_alpha(
default=0.5,
tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.",
)
.req_field_div_max_dims(
(
"none",
"non-batch",
"spatial",
"all",
"batch",
"channel",
"height",
"width",
),
"div_max_dims": (
(
"none",
"non-batch",
"spatial",
"all",
"batch",
"channel",
"height",
"width",
),
{
"default": "non-batch",
"tooltip": "If non-none, the noise gets divide by the maxmimu over this dimension.",
},
),
"use_div_max_abs": (
"BOOLEAN",
{
"default": True,
"tooltip": "Only has an effect when div_max_dims is not none. Controls whether maximization is done with the absolute values or raw values.",
},
),
"use_sign": (
"BOOLEAN",
{
"default": False,
"tooltip": "When set, only the sign of the initial noise is used, so -0.5, -0.2 all turn into -1, 0.5, 2, etc all turn into 1.",
},
),
}
return result
default="non-batch",
tooltip="If non-none, the noise gets divide by the maximum over this dimension.",
)
.req_bool_use_div_max_abs(
default=True,
tooltip="Only has an effect when div_max_dims is not none. Controls whether maximization is done with the absolute values or raw values.",
)
.req_bool_use_sign(
tooltip="When set, only the sign of the initial noise is used, so -0.5, -0.2 all turn into -1, 0.5, 2, etc all turn into 1.",
),
)
@classmethod
def get_item_class(cls):
@@ -270,200 +198,117 @@ class SonarAdvancedPowerLawNoiseNode(SonarCustomNoiseNodeBase):
class SonarAdvancedCollatzNoiseNode(SonarCustomNoiseNodeBase):
DESCRIPTION = "Custom noise type that allows specifying parameters for Collatz noise. Very experimental, also very slow. It might just about work as initial noise with non-ancestral sampling but if you get weird results I recommend mixing it with other noise types or possibly using ancestral/SDE sampling."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"adjust_scale": (
"BOOLEAN",
{
"default": False,
"tooltip": "When enabled, the output will be normalized to values between -1 and 1 using the last two dimensions (if there are four or more), otherwise dimensions after the first.",
},
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_bool_adjust_scale(
default=False,
tooltip="When enabled, the output will be normalized to values between -1 and 1 using the last two dimensions (if there are four or more), otherwise dimensions after the first.",
)
.req_string_chain_length(
default="1, 1, 2, 2, 3, 3",
tooltip="Comma-separated list of chain lengths. Cannot be empty. Iterations will cycle through the list and wrap. Controls the length of Collatz chains. Note: Using a high chain length may be very slow, especially if combined with many iterations.",
)
.req_int_chain_offset(
default=5,
min=0,
max=10000,
tooltip="Uses values starting at the specified offset. Note: This entails generating chains of length chain_length + chain_offset, which may be quite slow if you use high values.",
)
.req_int_iterations(
default=10,
min=1,
max=10000,
tooltip="Number of iterations to run. Warning: Collatz noise (my implementation, anyway) is EXTREMELY slow.",
)
.req_bool_iteration_sign_flipping(
default=True,
tooltip="Controls whether we cycle between flipping the sign on the output from each iteration. May average out weirdness... Or make stuff weirder.",
)
.req_float_rmin(
default=-8000.0,
tooltip="Minimum value a chain can start with. Going as low as -9500 should be safe with float32.",
)
.req_float_rmax(
default=8000.0,
tooltip="Maximum value a chain can start with. I don't recommend going over 9500 if you are using the float32 dtype here as that is where the Collatz chain starts to reach values that can't be accurately represented.",
)
.req_string_dims(
default="-1, -1, -2, -2",
tooltip="Comma-separated list of dimensions. Cannot be empty. May be negative to count from the end of the list. Iterations will cycle through the list and wrap.",
)
.req_bool_flatten(
tooltip="Controls whether dimensions past the current one selected from the dims parameter will get flattened.",
)
.req_field_output_mode(
(
"values",
"ratios",
"mults",
"adds",
"seed_x_mults",
"seed_x_adds",
"noise_x_ratios",
"noise_x_mults",
"noise_x_adds",
),
"chain_length": (
"STRING",
{
"default": "1, 1, 2, 2, 3, 3",
"tooltip": "Comma-separated list of chain lengths. Cannot be empty. Iterations will cycle through the list and wrap. Controls the length of Collatz chains. Note: Using a high chain length may be very slow, especially if combined with many iterations.",
},
),
"chain_offset": (
"INT",
{
"default": 5,
"min": 0,
"max": 10000,
"tooltip": "Uses values starting at the specified offset. Note: This entails generating chains of length chain_length + chain_offset, which may be quite slow if you use high values.",
},
),
"iterations": (
"INT",
{
"default": 10,
"min": 1,
"max": 10000,
"tooltip": "Number of iterations to run. Warning: Collatz noise (my implementation, anyway) is EXTREMELY slow.",
},
),
"iteration_sign_flipping": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether we cycle between flipping the sign on the output from each iteration. May average out weirdness... Or make stuff weirder.",
},
),
"rmin": (
"FLOAT",
{
"default": -8000.0,
"min": -100000.0,
"max": 100000.0,
"tooltip": "Minimum value a chain can start with. Going as low as -9500 should be safe with float32.",
},
),
"rmax": (
"FLOAT",
{
"default": 8000.0,
"min": -100000.0,
"max": 100000.0,
"tooltip": "Maximum value a chain can start with. I don't recommend going over 9500 if you are using the float32 dtype here as that is where the Collatz chain starts to reach values that can't be accurately represented.",
},
),
"dims": (
"STRING",
{
"default": "-1, -1, -2, -2",
"tooltip": "Comma-separated list of dimensions. Cannot be empty. May be negative to count from the end of the list. Iterations will cycle through the list and wrap.",
},
),
"flatten": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether dimensions past the current one selected from the dims parameter will get flattened.",
},
),
"output_mode": (
(
"values",
"ratios",
"mults",
"adds",
"seed_x_mults",
"seed_x_adds",
"noise_x_ratios",
"noise_x_mults",
"noise_x_adds",
),
{
"default": "values",
},
),
"quantile": (
"FLOAT",
{
"default": 0.5,
"min": 0.0,
"max": 1.0,
"tooltip": "The initial output of each iteration will be run through quantile normalization. Setting the parameter to 0 or 1 will disable quantile normalization.",
},
),
"quantile_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.",
},
),
"noise_dtype": (
("float32", "float64", "float16", "bfloat16"),
{
"default": "float32",
"tooltip": "Generally should be left at the default. Only float32 and float64 will work if you have quantile normalization enabled.",
},
),
"even_multiplier": (
"FLOAT",
{
"default": 0.5,
"min": -10000.0,
"max": 1000.0,
"tooltip": "Multiplier to use when the previous link in the chain is even. Collatz uses 0.5 (divides by two) here.",
},
),
"even_addition": (
"FLOAT",
{
"default": 0.0,
"min": -10000.0,
"max": 1000.0,
"tooltip": "Value to add when the previous link in the chain is even. Collatz uses 0 here.",
},
),
"odd_multiplier": (
"FLOAT",
{
"default": 3.0,
"min": -10000.0,
"max": 1000.0,
"tooltip": "Multiplier to use when the previous link in the chain is odd. Collatz uses 3 here.",
},
),
"odd_addition": (
"FLOAT",
{
"default": 1.0,
"min": -10000.0,
"max": 1000.0,
"tooltip": "Value to add when the previous link in the chain is odd. Collatz uses 1 here.",
},
),
"integer_math": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether the results during chain generation get truncated to an integer value or not. Should be enabled if you actually want to generate accurate Collatz chains.",
},
),
"add_preserves_sign": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether additions use the same sign as the item they're being added to.",
},
),
"break_loops": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether the chain resets back to the seed value once it reaches 1 or 0. Generally should be left enabled, otherwise the chain will oscillate between only a few values for the rest of the length (at least with the Collatz rules).",
},
),
"seed_mode": (
("default", "force_odd", "force_even"),
{
"default": "default",
"tooltip": "Default mode just uses whatever the original seed value was. force_odd/force_even will force it to the specified parity by adding one if it doesn't match. Starting from odd seeds might result in longer chains. Enabling the force modes may cause the initial seeds to exceed rmax by one.",
},
),
}
result["optional"] |= {
"seed_custom_noise": (
WILDCARD_NOISE,
{
"tooltip": f"Optional custom noise to use for initial values for Collatz chains. May be slow as it will generate noise according to the original input size and then crop it. Does this noise type have enough warnings about it being slow? Yeah. Connecting something here will probably make it even slower!\n{NOISE_INPUT_TYPES_HINT}",
},
),
"mix_custom_noise": (
WILDCARD_NOISE,
{
"tooltip": f"Optional custom noise to use with the output modes starting with 'noise'.\n{NOISE_INPUT_TYPES_HINT}",
},
),
}
return result
default="values",
)
.req_float_quantile(
default=0.5,
min=0.0,
max=1.0,
tooltip="The initial output of each iteration will be run through quantile normalization. Setting the parameter to 0 or 1 will disable quantile normalization.",
)
.req_field_quantile_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.",
)
.req_field_noise_dtype(
("float32", "float64", "float16", "bfloat16"),
default="float32",
tooltip="Generally should be left at the default. Only float32 and float64 will work if you have quantile normalization enabled.",
)
.req_float_even_multiplier(
default=0.5,
tooltip="Multiplier to use when the previous link in the chain is even. Collatz uses 0.5 (divides by two) here.",
)
.req_float_even_addition(
default=0.0,
tooltip="Value to add when the previous link in the chain is even. Collatz uses 0 here.",
)
.req_float_odd_multiplier(
default=3.0,
tooltip="Multiplier to use when the previous link in the chain is odd. Collatz uses 3 here.",
)
.req_float_odd_addition(
default=1.0,
tooltip="Value to add when the previous link in the chain is odd. Collatz uses 1 here.",
)
.req_bool_integer_math(
default=True,
tooltip="Controls whether the results during chain generation get truncated to an integer value or not. Should be enabled if you actually want to generate accurate Collatz chains.",
)
.req_bool_add_preserves_sign(
default=True,
tooltip="Controls whether additions use the same sign as the item they're being added to.",
)
.req_bool_break_loops(
default=True,
tooltip="Controls whether the chain resets back to the seed value once it reaches 1 or 0. Generally should be left enabled, otherwise the chain will oscillate between only a few values for the rest of the length (at least with the Collatz rules).",
)
.req_field_seed_mode(
("default", "force_odd", "force_even"),
default="default",
tooltip="Default mode just uses whatever the original seed value was. force_odd/force_even will force it to the specified parity by adding one if it doesn't match. Starting from odd seeds might result in longer chains. Enabling the force modes may cause the initial seeds to exceed rmax by one.",
)
.opt_customnoise_seed_custom_noise(
tooltip="Optional custom noise to use for initial values for Collatz chains. May be slow as it will generate noise according to the original input size and then crop it. Does this noise type have enough warnings about it being slow? Yeah. Connecting something here will probably make it even slower!",
)
.opt_customnoise_mix_custom_noise(
tooltip="Optional custom noise to use with the output modes starting with 'noise'.",
),
)
@classmethod
def get_item_class(cls):
@@ -640,133 +485,71 @@ class SonarWaveletNoiseNode(
):
DESCRIPTION = "Custom noise type that allows generating wavelet noise. Very simple explanation of how a single octave works:\n1) Generate some noise.\n2) Scale it down 50%.\n3) Scale it back up to the original size.\n4) Subtract the scaled noise from the original noise.\nScaling the noise down and then back up blurs it, so this is essentially sharpening the noise."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES()
result["required"] |= {
"octaves": (
"INT",
{
"default": 4,
"min": -100,
"max": 100,
"tooltip": "Number of octaves to generate. You can use a negative number here to run the octaves in reverse order though it may produce weird results/not work very well.",
},
),
"octave_height_factor": (
"FLOAT",
{
"default": 0.5,
"min": 0.001,
"max": 10000.0,
"tooltip": "Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.",
},
),
"octave_width_factor": (
"FLOAT",
{
"default": 0.5,
"min": 0.001,
"max": 10000.0,
"tooltip": "Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.",
},
),
"octave_scale_mode": (
utils.UPSCALE_METHODS,
{
"tooltip": "Scaling mode used within each octave to produce the scaled noise. By default this will be scaling down that octave's noise.",
"default": "adaptive_avg_pool2d",
},
),
"octave_rescale_mode": (
utils.UPSCALE_METHODS,
{
"tooltip": "Scaling mode used within each octave to scale the noise back up to that octave's original size.",
"default": "bilinear",
},
),
"post_octave_rescale_mode": (
utils.UPSCALE_METHODS,
{
"tooltip": "Scaling mode used to scale the output of an octave back up to the actual latent size.",
"default": "bilinear",
},
),
"initial_amplitude": (
"FLOAT",
{
"default": 1.0,
"min": -10000.0,
"max": 10000.0,
"tooltip": "Basically the strength an octave gets added to the total. This will be scaled by persistance after each octave.",
},
),
"persistence": (
"FLOAT",
{
"default": 0.5,
"min": -10000.0,
"max": 10000.0,
"tooltip": "Multiplier applied to amplitude after each octave. 0.5 means the first octave uses initial_amplitude, the second uses half of that and so on.",
},
),
"height_factor": (
"FLOAT",
{
"default": 2.0,
"min": 0.001,
"max": 10000.0,
"tooltip": "Scaling factor for height, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.",
},
),
"width_factor": (
"FLOAT",
{
"tooltip": "Scaling factor for width, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.",
"default": 2.0,
"min": 0.001,
"max": 10000.0,
},
),
"update_blend": (
"FLOAT",
{
"tooltip": "Controls how original_noise - scaled_noise is blended with original_noise. The default is to use 100% original_noise - scaled_noise.",
"default": 1.0,
"min": -10000.0,
"max": 10000.0,
},
),
"update_blend_mode": (
("simple_add", *utils.BLENDING_MODES.keys()),
{
"default": "lerp",
"tooltip": "Controls how the enhanced noise from each octave is blended with that octave's raw noise. With normal wavelet noise there's no blending and you use 100% enhanced noise.",
},
),
"normalize_noise": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether the noise source is normalized before wavelet filtering occurs.",
},
),
"normalize": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength. For weird blend modes, you may want to set this to forced.",
},
),
}
result["optional"] |= {
"custom_noise": (
WILDCARD_NOISE,
{
"tooltip": f"Optional: Custom noise input. If unconnected will default to Gaussian noise. Note: When connected, the noise for all octaves will be generated at the maximum scale and then cropped which may be slow.\n{NOISE_INPUT_TYPES_HINT}",
},
),
}
return result
INPUT_TYPES = SonarLazyInputTypes(
lambda: NoiseChainInputTypes()
.req_int_octaves(
default=4,
min=-100,
max=100,
tooltip="Number of octaves to generate. You can use a negative number here to run the octaves in reverse order though it may produce weird results/not work very well.",
)
.req_float_octave_height_factor(
default=0.5,
min=0.001,
tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.",
)
.req_float_octave_width_factor(
default=0.5,
min=0.001,
tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.",
)
.req_selectscalemode_octave_scale_mode(
default="adaptive_avg_pool2d",
tooltip="Scaling mode used within each octave to produce the scaled noise. By default this will be scaling down that octave's noise.",
)
.req_selectscalemode_octave_rescale_mode(
default="bilinear",
tooltip="Scaling mode used within each octave to scale the noise back up to that octave's original size.",
)
.req_selectscalemode_post_octave_rescale_mode(
default="bilinear",
tooltip="Scaling mode used to scale the output of an octave back up to the actual latent size.",
)
.req_float_initial_amplitude(
default=1.0,
tooltip="Basically the strength an octave gets added to the total. This will be scaled by persistance after each octave.",
)
.req_float_persistence(
default=0.5,
tooltip="Multiplier applied to amplitude after each octave. 0.5 means the first octave uses initial_amplitude, the second uses half of that and so on.",
)
.req_float_height_factor(
default=2.0,
min=0.001,
tooltip="Scaling factor for height, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.",
)
.req_float_width_factor(
tooltip="Scaling factor for width, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.",
default=2.0,
min=0.001,
)
.req_float_update_blend(
tooltip="Controls how original_noise - scaled_noise is blended with original_noise. The default is to use 100% original_noise - scaled_noise.",
default=1.0,
)
.req_selectblend_update_blend_mode(
insert_modes=("simple_add",),
default="lerp",
tooltip="Controls how the enhanced noise from each octave is blended with that octave's raw noise. With normal wavelet noise there's no blending and you use 100% enhanced noise.",
)
.req_bool_normalize_noise(
tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.",
)
.req_normalizetristate_normalize()
.opt_customnoise_custom_noise(
tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise. Note: When connected, the noise for all octaves will be generated at the maximum scale and then cropped which may be slow.",
),
)
@classmethod
def get_item_class(cls):
+3 -3
View File
@@ -16,14 +16,14 @@ from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
from PIL import Image
from torch import Tensor
from .nodes.base import (
from ..noise import CustomNoiseItemBase
from ..utils import scale_noise
from .base import (
NOISE_INPUT_TYPES_HINT,
WILDCARD_NOISE,
SonarCustomNoiseNodeBase,
SonarNormalizeNoiseNodeMixin,
)
from .noise import CustomNoiseItemBase
from .utils import scale_noise
PREVIEW_FORMAT = comfy.latent_formats.SD15()
File diff suppressed because it is too large Load Diff
+39 -9
View File
@@ -40,7 +40,15 @@ class CustomNoiseItemBase(abc.ABC):
self.factor = factor
self.keys = set(kwargs.keys())
for k, v in kwargs.items():
setattr(self, k, v)
do_clone = k in {
"custom_noise",
"custom_noise_opt",
"noise",
"noise_opt",
"sonar_custom_noise",
"sonar_custom_noise_opt",
} and hasattr(v, "clone")
setattr(self, k, v.clone() if do_clone else v)
def clone_key(self, k):
return getattr(self, k)
@@ -1179,9 +1187,7 @@ class NormalizeToScaleNoise(CustomNoiseItemBase):
min_positive_value: float,
max_positive_value: float,
mode: str,
dims: tuple,
normalize_noise: float,
normalize,
**kwargs,
):
if mode == "simple":
if min_negative_value >= max_positive_value:
@@ -1207,9 +1213,7 @@ class NormalizeToScaleNoise(CustomNoiseItemBase):
min_positive_value=min_positive_value,
max_positive_value=max_positive_value,
mode=mode,
dims=dims,
normalize_noise=normalize_noise,
normalize=normalize,
**kwargs,
)
def clone_key(self, k):
@@ -1218,6 +1222,8 @@ class NormalizeToScaleNoise(CustomNoiseItemBase):
return super().clone_key(k)
def make_noise_sampler(self, x, *args, normalized=True, **kwargs):
std_dims, std_multiplier = self.std_dims, self.std_multiplier
mean_dims, mean_multiplier = self.mean_dims, self.mean_multiplier
factor = self.factor
mode = self.mode
if mode == "simple":
@@ -1252,6 +1258,16 @@ class NormalizeToScaleNoise(CustomNoiseItemBase):
else:
for bidx in range(noise.shape[0]):
noise[bidx] = noise_filter(noise[bidx])
if mean_multiplier != 0:
noise -= noise.mean(dim=mean_dims, keepdim=True).mul_(mean_multiplier)
if std_multiplier != 0:
noise_std = (
noise.std(dim=std_dims, keepdim=True)
.sub_(1.0)
.mul_(std_multiplier)
.add_(1.0)
)
noise /= torch.where(noise_std == 0, 1e-07, noise_std)
return scale_noise(noise, factor, normalized=normalize)
return noise_sampler
@@ -1353,9 +1369,23 @@ class ResizedNoise(CustomNoiseItemBase):
raise ValueError("ResizedNoise can only handle 3+ dimensional latents")
factor = self.factor
normalize = self.get_normalize("normalize", normalized)
spatial_compression = self.spatial_compression
spatial_mode = self.spatial_mode
width, height = self.width, self.height
xh, xw = x.shape[-2:]
nh, nw = self.height // 8, self.width // 8
offsh, offsw = self.crop_offset_vertical // 8, self.crop_offset_horizontal // 8
if spatial_mode != "percentage":
height //= spatial_compression
width //= spatial_compression
if spatial_mode == "absolute":
nh, nw = int(height), int(width)
elif spatial_mode == "relative":
nh, nw = int(xh + height), int(xw + width)
elif spatial_mode == "percentage":
nh, nw = max(1, int(xh * height)), max(1, int(xw * width))
else:
raise ValueError("Bad spatial_mode")
offsh = self.crop_offset_vertical // spatial_compression
offsw = self.crop_offset_horizontal // spatial_compression
if xh == nh and xw == nw:
ns = self.custom_noise.make_noise_sampler(
x,
+24 -4
View File
@@ -1424,10 +1424,12 @@ class ScatternetFilteredNoiseGenerator(FramesToChannelsNoiseGenerator):
)
super().__init__(*args, **kwargs)
if self.output_mode not in {
"channels_adjusted",
"channels",
"channels_adjusted",
"channels_scaled",
"flat",
"flat_adjusted",
"flat_scaled",
}:
raise ValueError("Bad output mode")
@@ -1462,6 +1464,8 @@ class ScatternetFilteredNoiseGenerator(FramesToChannelsNoiseGenerator):
"scatternet_order": 1,
"per_channel_scatternet": False,
"output_mode": "channels_adjusted",
# If None, uses probselect when available, otherwise bilinear.
"upscale_mode": None,
"noise_sampler": None,
}
@@ -1479,12 +1483,14 @@ class ScatternetFilteredNoiseGenerator(FramesToChannelsNoiseGenerator):
def generate(self, *args):
adjusted_shape = self.get_adjusted_shape()
adjusted = self.output_mode.endswith("_adjusted")
scaled = self.output_mode.endswith("_scaled")
adjusted = scaled or self.output_mode.endswith("_adjusted")
order = abs(self.scatternet_order)
order_spatial_compensation = 2**order
output_mode = (
self.output_mode.split("_", 1)[0] if adjusted else self.output_mode
)
spatial_compensation = 1 if adjusted else 2 ** abs(self.scatternet_order)
spatial_compensation = 1 if adjusted else order_spatial_compensation
if self.noise_sampler is None:
temp_shape = (
(
@@ -1498,6 +1504,20 @@ class ScatternetFilteredNoiseGenerator(FramesToChannelsNoiseGenerator):
noise = self.rand_like(shape=temp_shape)
else:
noise = self.noise_sampler(*args)
if scaled:
upscale_mode = self.upscale_mode
if upscale_mode is None:
upscale_mode = (
"probselect"
if "probselect" in utils.UPSCALE_METHODS
else "bilinear"
)
noise = utils.scale_samples(
noise,
adjusted_shape[-1] * order_spatial_compensation,
adjusted_shape[-2] * order_spatial_compensation,
mode=upscale_mode,
)
if self.scatternet_order == 0:
return self.fix_output_frames(noise)
self.scatternet = self.scatternet.to(device=self.device, dtype=self.dtype)
@@ -1729,7 +1749,7 @@ class CollatzNoiseGenerator(NoiseGenerator):
result[dim] = slice(idx, None, stride)
return result
def _generate_iteration( # noqa: PLR0914
def _generate_iteration(
self,
*args,
dim: int,
+67
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import math
from functools import partial
from typing import TYPE_CHECKING
import torch
from comfy.model_management import device_supports_non_blocking
@@ -9,6 +10,9 @@ from comfy.utils import common_upscale
from .external import MODULES as EXT
if TYPE_CHECKING:
from collections.abc import Sequence
BLENDING_MODES = {
"lerp": torch.lerp,
"inject": lambda a, b, t: (b * t).add_(a),
@@ -484,3 +488,66 @@ def trunc_decimals(x: torch.Tensor, decimals: int = 3) -> torch.Tensor:
def maybe_apply(val, cond, fun):
return fun(val) if cond else val
def maybe_apply_kwargs(d: dict | None, cond, fun, *, default=None):
return default if d is None or not cond else fun(**d)
def tensor_item(val: torch.Tensor | float, *, collapse_function=torch.max) -> float:
if isinstance(val, torch.Tensor):
return float(collapse_function(val).detach().cpu().item())
return float(val)
# Does not handle out of order or duplicated sigmas.
def step_from_sigmas(
sigma: float | torch.Tensor,
sigmas: torch.Tensor,
*,
decimals: int | None = 4,
output_decimals: int = 2,
) -> float | None:
sigma = tensor_item(sigma)
sigmas = sigmas.detach().cpu()
if sigmas.ndim == 2:
sigmas = sigmas.max(dim=0).values
elif sigmas.ndim != 1:
errstr = f"Unexpected number of dimensions in sigmas, should be 1 or 2 but got shape {sigmas.shape}"
raise ValueError(errstr)
sigmas = sigmas[:-1]
if not len(sigmas) or torch.any(sigmas <= 0):
return None
if decimals is not None:
sigmas = sigmas.round(decimals=decimals)
sigma = round(sigma, decimals)
sigma_min, sigma_max = sigmas.aminmax()
if not sigma_min <= sigma <= sigma_max:
return None
max_idx = len(sigmas) - 1
idx = int(tensor_item((sigmas - sigma).abs().argmin()))
idx_sigma = tensor_item(sigmas[idx])
if decimals is not None:
idx_sigma = round(idx_sigma, decimals)
if sigma == idx_sigma:
return float(idx)
# Between sigmas, but guaranteed to be in range here.
idx_low, idx_high = (idx, idx - 1) if sigma > idx_sigma else (idx + 1, idx)
if idx_low < 0 or idx_high < 0 or idx_low > max_idx or idx_high > max_idx:
return None
sigma_low, sigma_high = tensor_item(sigmas[idx_low]), tensor_item(sigmas[idx_high])
step_diff = sigma_high - sigma_low
pct = 1.0 - ((sigma - sigma_low) / step_diff)
return round(idx_high + pct, output_decimals)
def clamp_float(val: float, minval=0.0, maxval=1.0) -> float:
return max(minval, min(val, maxval))
def filter_dict(d: dict, keep: set | Sequence, *, recursive: bool = False) -> dict:
return {
k: v if not (recursive and isinstance(v, dict)) else filter_dict(v, keep)
for k, v in d.items()
if k in keep
}
+94 -14
View File
@@ -11,17 +11,19 @@ if TYPE_CHECKING:
try:
import pytorch_wavelets as ptwav
import pywt
HAVE_WAVELETS = True
except ImportError:
ptwav = None
pywt = None
HAVE_WAVELETS = False
class Wavelet:
DEFAULT_MODE = "periodization"
DEFAULT_MODE = "symmetric"
DEFAULT_LEVEL = 3
DEFAULT_WAVE = "haar"
DEFAULT_WAVE = "db4"
DEFAULT_USE_1D_DWT = False
DEFAULT_USE_DTCWT = False
DEFAULT_QSHIFT = "qshift_a"
@@ -102,16 +104,97 @@ class Wavelet:
))
return result
def to(self, *args: list, **kwargs: dict) -> None:
self._wavelet_forward = self._wavelet_forward.to(*args, **kwargs)
self._wavelet_inverse = self._wavelet_inverse.to(*args, **kwargs)
def to(self, *args: list, copy: bool = False, **kwargs: dict) -> Wavelet:
o = Wavelet.__new__(Wavelet) if copy else self
o._wavelet_forward = self._wavelet_forward.to(*args, **kwargs) # noqa: SLF001
o._wavelet_inverse = self._wavelet_inverse.to(*args, **kwargs) # noqa: SLF001
return o
@staticmethod
def wavelist() -> tuple:
return tuple(pywt.wavelist()) if HAVE_WAVELETS else ()
@staticmethod
def biortlist() -> tuple:
return (
("near_sym_a", "near_sym_b", "antonini", "legall") if HAVE_WAVELETS else ()
)
@staticmethod
def qshiftlist() -> tuple:
return (
("qshift_a", "qshift_b", "qshift_c", "qshift_d", "qshift_06")
if HAVE_WAVELETS
else ()
)
@staticmethod
def modelist() -> tuple:
return (
(
"symmetric",
"zero",
"reflect",
"replicate",
"periodization",
"periodic",
"constant",
)
if HAVE_WAVELETS
else ()
)
def expand_yh_scales(
yh: Sequence,
*,
yh_scales: float | Sequence = 1.0,
) -> float | tuple:
yhlen = len(yh)
yh_shape = yh[0].shape
# Doesn't make sense to target orientations for 1D DWD (3D here).
olen = yh_shape[2] if len(yh_shape) > 3 else 1
# print(f"\nSIZES: yhlen={yhlen}, olen={olen}, yh_shape={yh[0].shape}")
if isinstance(yh_scales, (float, int)):
return ((float(yh_scales),) * olen,) * yhlen
otemplate = (1.0,) * olen
yh_scales = tuple(
(float(band),) * olen
if isinstance(band, (float, int))
else (
(
*(float(i) for i in band[:olen]),
*otemplate[: olen - len(band[:olen])],
)
if isinstance(band, (tuple, list))
else band
)
for band in yh_scales
)
if "fill" in yh_scales:
fillidx = yh_scales.index("fill")
if "fill" in yh_scales[fillidx + 1 :]:
raise ValueError("Only one fill allowed.")
if fillidx == 0 or len(yh_scales) < 2:
raise ValueError(
"Invalid fill value, cannot be in the first position or the only item.",
)
yhslen = len(yh_scales)
if yhslen - 1 < yhlen:
# Need to pad.
fill = (yh_scales[fillidx - 1],) * (yhlen - (len(yh_scales) - 1))
yh_scales = (*yh_scales[:fillidx], *fill, *yh_scales[fillidx + 1 :])
else:
# Just remove the "fill".
yh_scales = (*yh_scales[:fillidx], *yh_scales[fillidx + 1 :])
return yh_scales[:yhlen]
def wavelet_scaling(
yl: torch.Tensor,
yh: Sequence,
yl_scale: float | torch.Tensor,
yh_scales: float | torch.Tensor | None,
yh_scales: float | Sequence | None,
*,
in_place: bool = False,
) -> tuple:
@@ -120,19 +203,16 @@ def wavelet_scaling(
yh = tuple(yhband.clone() for yhband in yh)
if yl_scale != 1.0:
yl *= yl_scale
if yh_scales is None or yh_scales == 1.0:
return (yl, yh)
if isinstance(yh_scales, (int, float)):
yh_scales = (yh_scales,) * len(yh)
# print("SCALES", self.yl_scale, yh_scales)
yh_scales = expand_yh_scales(
yh,
yh_scales=yh_scales if yh_scales is not None else 1.0,
)
for hscale, ht in zip(yh_scales, yh):
# print(">> SCALING", hscale)
if isinstance(hscale, (int, float)):
ht *= hscale # noqa: PLW2901
continue
for lidx in range(min(ht.shape[2], len(hscale))):
# print(">> SCALE IDX", lidx)
ht[:, :, lidx, :, :] *= hscale[lidx]
ht[:, :, lidx] *= hscale[lidx]
return (yl, yh)
+2
View File
@@ -29,10 +29,12 @@ ignore = [
"FBT002",
"PLR0912",
"PLR0913",
"PLR0914",
"PLR0915",
"PLR0917",
"PLR2004",
"T201",
"TID252",
"TRY003",
"N802",
"N999",