Author SHA1 Message Date
blepping 742e071364 Update changelog 2024-05-21 07:03:00 -06:00
blepping 0f748bd1f8 Update documentation and examples 2024-05-21 06:47:08 -06:00
blepping a7da42c305 Adjust FrUX input names 2024-05-21 03:10:56 -06:00
blepping e64606c672 Refactoring, cleanups 2024-05-19 03:41:46 -06:00
blepping fb667e08c8 Add more filter preview sizes, use configured oversample 2024-05-18 15:53:04 -06:00
blepping 4a01af10e7 Allow setting size and gain in filter preview node 2024-05-18 09:25:02 -06:00
blepping ec7c9a4632 Add py/external.py - derp!
Accelerate FRUX by caching the filters when possible

Allow doing FFT on CPU in FRUX for GPUs that won't work otherwise

Allow disabling normalization in SamplerConfigOverride node

Fix base power and pink noise types.

Other cleanups
2024-05-18 07:33:42 -06:00
blepping c235bc18b2 Allow using brownian noise in NoisyLatentLike node when sigmas are attached 2024-05-16 18:46:13 -06:00
blepping b47f3f3291 Add FreeUExtreme node and associated config node 2024-05-16 16:24:07 -06:00
blepping 9dc16c5402 Add SonarPreviewFilter node
Better filter normalization (maybe)

Add a scale parameter to filters
2024-05-16 16:22:50 -06:00
blepping 40726e5d84 GuidedNoise fixes 2024-05-14 18:00:30 -06:00
blepping 9cad01df09 Add SonarPowerFilter node, improve RepeatedNoise, other stuff 2024-05-14 16:41:39 -06:00
blepping ab2f08268f Allow showing custom noise preview in SonarPowerFilterNoise node 2024-05-13 10:06:49 -06:00
blepping 9a6ee9ac33 Improve channel filter (not written by me obviously, thanks Gaeros!) 2024-05-12 18:35:13 -06:00
blepping 144c7ba43a Fix channel correlation construction in PowerNoise 2024-05-12 12:47:16 -06:00
blepping 0ef1bd5bbd Add the ability to set channel correlations in SonarPowerNoise and SonarPowerFilterNoise 2024-05-12 12:35:49 -06:00
blepping e4f53e9594 Add SonarPowerFilterNoise, SonarRandomNoise and SonarBlendFilterNoise nodes 2024-05-12 07:59:47 -06:00
blepping 202a371337 Documentation updates 2024-05-11 11:01:46 -06:00
blepping c455599e9e More cleanups and fixes 2024-05-11 08:08:35 -06:00
blepping b950e1b051 Cleanups and fixes 2024-05-11 07:32:11 -06:00
blepping 4e87817908 Refactor, add scheduled, guided and composite noise types 2024-05-09 23:05:25 -06:00
15 changed files with 1215 additions and 5019 deletions
+3 -50
View File
@@ -64,47 +64,6 @@ You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#g
Very abbreviated section. The init type can make a big difference. If you use `RANDOM` you can get away with setting `direction` to high values (like up to `2.25` or so) and absurdly low values (like `-30.0`). It's also possible to set `momentum` and `momentum_hist` to negative values, although whether it's a good idea...
<details>
<summary>Click to expand advanced parameters info</summary>
There are some extra advanced parameters that may be passed by YAML/JSON using `SamplerConfigOVerride`'s `yaml_parameters`. Defaults:
```yaml
sonar_params:
# One of: classic, new, denoised
# classic: Should be the same as the way it works in the A1111 extension.
# new: Possibly improved version that doesn't blend in the history again.
# denoised: Instead of using the noise prediction, we do momentum on denoised instead.
momentum_mode: new
# The following two parameters may be used to control when
# momentum sampling is active. Steps are 0-based with 0 being the first step.
momentum_start_step: 0
momentum_end_step: 9999
# Controls whether history always gets updated, whether or not within the
# start/end step range or only in that range. Can be used to affect the initial
# history value.
always_update_history: true
# Only applies when the init type is RAND.
rand_init_noise_multiplier: 1.0
# If you have ComfyUI-bleh installed, you can use any blend mode it provides.
# Otherwise you can have your blend mode in any color you want as long as it's lerp.
blend_mode: lerp
# Defaultss to blend_mode if unset.
momentum_blend_mode: null
# Defaults to blend_mode if unset. Only applies to linear guidance mode.
guidance_blend_mode: null
```
Additionally, it's possible to override the normal Sonar parameters here as well. If they exist in the `sonar_params` block, they will overwrite the values in the node.
</details>
## Guidance
You can try the `SamplerSonarNaive` sampler which has an optional latent input. The guidance _probably_ isn't working correctly and the implementation definitely isn't exactly the same as the original A1111 version but it still might be fun to play with. The `linear` guidance type is a lot more sensitive to the `guidance_factor` than the `euler` type. For `euler`, reasonable values are around `0.01` to `0.1`, for `linear` reasonable values are more like `0.001` to `0.02`. It is also possible to set guidance factor to a negative value, I've found this results in high contrast and very vivid colors.
@@ -148,17 +107,11 @@ Original Sonar Sampler implementation (for A1111): https://github.com/Kahsolt/st
My version was initially based on this Sonar sampler implementation for Diffusers: https://github.com/alexblattner/modified-euler-samplers-for-sonar-diffusers/
* Many noise generation functions copied from https://github.com/Clybius/ComfyUI-Extra-Samplers with only minor modifications. I may have broken some of them in the process _or_ they may not have been suitable for use and I took them anyway. If they don't work it is not a reflection on the original source.
* Noise spectral modulation modified from https://github.com/Clybius/ComfyUI-Extra-Samplers
* New pyramid noise based on implementation in [Jonathan Whitaker](https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2)'s article on multi-resolution noise.
* Original `SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Additionally, he provided a lot of guidance with refactoring it to allow separate filtering and other enhancements and answered a multitude of dumb questions. To say those changes are only co-authored is probably giving myself too much credit. Thank you! Your patience and help is very much appreciated.
* New 1/f (onef) and power law (white, grey, violet, velvet) noise types referenced from https://github.com/WASasquatch/PowerNoiseSuite
* Wavelet noise idea (and some of the default settings) from https://github.com/ClownsharkBatwing/RES4LYF
Many noise generation functions copied from https://github.com/Clybius/ComfyUI-Extra-Samplers with only minor modifications. I may have broken some of them in the process _or_ they may not have been suitable for use and I took them anyway. If they don't work it is not a reflection on the original source.
## Errata
New pyramid noise based on implementation in [Jonathan Whitaker](https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2)'s article on multi-resolution noise.
* The noise types might not actually do what they claim. In that, I mean something I called "pink" noise might not be what is technically known as "pink noise". My implementations are best-effort. Bug reports and contributions to improve this repo are always welcome!
* Whether noise gets generated on GPU or CPU is probably inconsistent. This means changing GPU types may change seeds, also when this eventually gets fixed it will probably also change seeds.
Original `SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Additionally, he provided a lot of guidance with refactoring it to allow separate filtering and other enhancements and answered a multitude of dumb questions. To say those changes are only co-authored is probably giving myself too much credit. Thank you! Your patience and help is very much appreciated.
## Sonar Examples
Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

-38
View File
@@ -2,44 +2,6 @@
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
## 20250130
*Note*: May change seeds.
This set of changes includes some pretty major internal refactoring. Definitely possible that I broke something, so please create an issue if you run into problems.
* Noise generation should now respect whether generating on CPU vs GPU is selected. Previously it likely was defaulting to generating on GPU. This may change seeds.
* Refactored momentum samplers, this may change seeds especially if you were using weird parameters like negative direction.
* Added some new parameters for momentum samplers.
* Removed the `s_noise` and churn parameters from the normal Sonar Euler sampler. May break workflows. (Churn was the predecessor to ancestral samplers and is basically obsolete.)
* Added `wavelet` and `distro` noise types.
* Added `SonarCustomNoiseAdv` node that allows passing parameters via YAML.
* Added `SonarResizedNoise` node that allows you to generate noise at a fixed size and then crop/resize it to match the generation.
* Added `SonarAdvancedDistroNoise` node that allows generating noise with basically all the distributions PyTorch supports.
* Added `SonarWaveletFilteredNoise` node that lets you filter another noise generator using wavelets.
## 20241129
*Note*: Contains some potentially workflow-breaking changes.
* `pink` noise type renamed to `pink_old` - the implementation was incorrect.
* `power` noise type renamed to `power_old` - the implementation was incorrect.
* Added `onef_pinkish` (higher frequencye) and `onef_greenish` (lower frequency) noise types.
* Added `SonarAdvanced1fNoise` node and `onef_pinkish`, `onef_greenish`, `onef_pinkish_mix`, `onef_greenish_mix`, and `onef_pinkishgreenish` noise types.
* Added `SonarAdvancedPowerLawNoise` node and `grey`, `white`, `violet` and `velvet` noise types.
* The `SonarAdvancedPyramidNoise` node can now use upscale methods from my [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) node pack if it is available.
* Added the `SonarChannelNoise` and `SonarBlendedNoise` nodes.
* Added the `SonarBlehOpsNoise` node.
* Added advanced parameter input to the SampleConfigOverride node, you can now pass options directly to the wrapped sampler function.
* Custom noise inputs now are semi-wildcard and will accept `OCS_NOISE` or `SONAR_CUSTOM_NOISE` interchangeably.
## 20240823
* Added descriptions and tooltips for most nodes.
* Added `repeat_batch` parameter to `NoisyLatentLike` node.
* Added a `SONAR_CUSTOM_NOISE to NOISE` node to allow converting from Sonar's custom noise type to the built in ComfyUI `NOISE` (used by `SamplerCustomAdvanced` and possibly other nodes).
* Added a `SonarAdvancedPyramidNoise` node that allows setting parameters for the pyramid noise variants.
## 20240521
Mega update! Many new features, documentation reorganized.
-128
View File
@@ -52,21 +52,6 @@ Parameters:
***
### `SonarCustomNoiseAdv`
Same as the `SonarCustomNoise` except it also includes a text widget for passing parameters by YAML or JSON (JSON is valid YAML).
Just for example, instead of using the absurdly large `SonarAdvancedDistroNoise` node, you could do something like:
```yaml
distro: wishart
quantile_norm: 0.5
wishart_cov_size: 4
wishart_df: 3.5
```
***
### `NoisyLatentLike`
This node takes a reference latent and generates noise of the same shape. The one required input is `latent`.
@@ -87,93 +72,6 @@ If you want to create noise for initial sampling, connect model and sigmas to th
This node can be used to override configuration settings for other samplers, including the noise type. For example, you could force `euler_ancestral` to use a different noise type. It's also possible to override other settings like `s_noise`, etc. *Note*: The wrapper inspects the sampling function's arguments to see what it supports, so you should connect the sampler directly to this rather than having other nodes (like a different sampler wrapper) in between.
You can enter YAML parameters in the text input, these arguments are passed directly to the sampler function without any error checking. If the same key exists in the node itself (i.e. `s_noise`) the one in the text input will take precedence. Note that these are based on the internal sampler function so the names of the arguments won't necessarily be the same as the sampler node (but they often are). You may need to check the source code for the sampler.
***
### `SONAR_CUSTOM_NOISE to NOISE`
This node can be used to convert Sonar custom noise to the `NOISE` type used by the builtin `SamplerCustomAdvanced` (and any other nodes that take a `NOISE` input).
***
### `SonarAdvancedPyramidNoise`
Allows setting some parameters for the pyramid noise variants (`pyramid`, `highres_pyramid` and `pyramid_old`). `discount` further from zero generally results in a more extreme colorful effect (can also be set to negative values). Higher `iterations` also tends to make the effect more extreme - zero iterations will just return normal Gaussian noise. You can also experiment with the `upscale_mode` for different effects.
### `SonarAdvanced1fNoise`
More extensive documentation TBD (hopefully). For now, a few recipes:
These differ differ only in alpha. For the other parameters, use `k=1, vf=1, hf=1, use_sqrt=true` to start.
* `blue`: `alpha=1`
* `green`: `alpha=0.75`
* `pink`: `alpha=0.5`
*
### `SonarAdvancedPowerLawNoise`
More extensive documentation TBD (hopefully). For now, a few recipes:
* `white`: `alpha=0, use_sign=true, div_max_dims=none`
* `grey`: `alpha=0, use_sign=false, div_max_dims=none`
* `velvet`: `alpha=1, use_sign=true, div_max_dims=all, use_div_max_abs=true`
* `violet`: `alpha=0.5, use_sign=true, div_max_dims=all, use_div_max_abs=true`
***
## `SonarWaveletFilteredNoise`
You will need [pytorch_wavelets](https://github.com/fbcotter/pytorch_wavelets) installed in your Python environment to use this one.
Allows filtering another noise source using wavelets. Parameters are specified using YAML (or JSON) in the text widget. The defaults are:
```yaml
use_dtcwt: false
mode: periodization
level: 3
wave: haar
# Only used in DTCWT mode.
qshift: qshift_a
# Only used in DTCWT mode.
biort: near_sym_a
# Additional parameters for the inverse wavelet operation
# are null by default and will use whatever the
# forward parameter is set to:
# inv_mode, inv_wave, inv_biort, inv_qshift
# Note: Using different parameters for the inverse wavelet
# operation may not work well (or just fail entirely).
# Scale for the lowpass filter.
yl_scale: 1.0
# Scales for the highpass filter. Can be a single value (null is basically 1.0).
yh_scales: null
```
***
### `SonarAdvancedDistroNoise`
See: https://pytorch.org/docs/stable/distributions.html
For the most part, we just pass parameters directly to PyTorch's distribution classes. Some of them have specific requirements so it is possible to set invalid parameters.
It may be more convenient to specify parameters using the `SonarCustomNoiseAdv` node than this gigantic monstrosity of a node. **Note**: In that case, pass the distribution name using `distro`, i.e. `distro: laplacian`.
Common parameters:
* `quantile_norm`: When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the distribution and how many of the values are extreme. Some actually work better with quantile normalization disabled.
* `quantile_norm_mode`: Controls what dimensions quantile normalization uses. By default, the noise is flattened first. You can try the nonflat versions but they may have a very strong row/column influence. Only applies when quantile_norm is active.
* `result_index`: When noise generation returns a batch of items, it will select the specified index. Negative indexes count from the end. Values outside the valid range will be automatically adjusted. You may enter a space-separated list of values for the case where there might be multiple added batch dimensions. Excess batch dimensions are removed from the end, indexe from result_index are used in order so you may want to enter the indexes in reverse order. Example: If your noise has shape `(1, 4, 3, 3)` and two 2-sized batch dims are added resulting in `(1, 4, 3, 3, 2, 2)` and you wanted index 0 from the first additional batch dimension and 1 from the second you would use result_index: `1 0`
Individual distributions have parameters beginning with their name, i.e. `laplacian_loc`. Parameters that are string inputs usually allow entering multiple space-separated items. This will usually result in the output noise being a batch, which can be selected with the `result_index` parameter.
Suggestions for fun distributions to try: Wishart and VonMises can produce some interesting results.
***
### `SonarModulatedNoise`
@@ -416,29 +314,3 @@ Light to dark (negative strength):
Randomly chooses between the noise types in the chain connected to it each time the noise sampler is called.
You generally do not want to use `rescale` here. You can also set `mix_count` to choose and combine multiple
types.
### `SonarChannelNoise`
Allows using a different noise generator per channel. The custom noise items attached to this node are treated as a list where the furthest item from the node will correspond to channel 0. For example where CN is a custom noise node and SCN is the `SonarChannelNoise` node:
```plaintext
CN (channel 0) -> CN (channel 1) -> SCN
```
Don't enable `rescale` in the custom noise nodes attached to `SonarChannelNoise`. If you want a blend of noise types for a channel, you can use something like `SonarBlendedNoise`.
### `SonarBlendedNoise`
Allows blending two noise generators. If [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) is available, you will have access to many more blending modes.
### `SonarBlehOpsNoise`
Only provided if [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) is available. Allows transforming/manipulating noise with bleh blockops expressions. For instance, you can do something like:
```yaml
- ops:
- [multiply, -1]
- [roll, -2, 0.5]
```
to flip the sign on the noise and then roll dimension -2 (height) by 50%.
+2 -18
View File
@@ -8,18 +8,6 @@ noise of that type. However you can either schedule the noise type to kick in at
(as in these examples) and/or mix it with something a bit more run of the mill. See
[advanced_noise_nodes](advanced_noise_nodes.md).
## Documentation TBD
* `grey`
* `onef_greenish_mix` (50/50 mix of positive/negative noise.)
* `onef_greenish`
* `onef_pinkish_mix` (50/50 mix of positive/negative noise.)
* `onef_pinkish`
* `onef_pinkishgreenish` (50/50 mix of `onef_pinkish` and `onef_greenish`.)
* `velvet`
* `violet`
* `white`
## Brownian
This is the default noise type for SDE samplers.
@@ -74,17 +62,13 @@ Variation using bislerp scaling:
***
## Pink Old
Previously known as `pink`. The implementation isn't correct, though in terms of results it's fine.
## Pink
![Pink](../assets/example_images/noise_base_types/noise_pink.png)
***
## Power Old
Previously known as `power`. The implementation isn't correct, though in terms of results it's fine.
## Power Builtin
![PowerBuiltin](../assets/example_images/noise_base_types/noise_power_builtin.png)
+14 -117
View File
@@ -1,125 +1,22 @@
from __future__ import annotations
import contextlib
import importlib
import sys
from functools import partial
from typing import TYPE_CHECKING, Callable, NamedTuple
if TYPE_CHECKING:
from types import ModuleType
MODULES = {}
with contextlib.suppress(ImportError, NotImplementedError):
bleh = importlib.import_module("custom_nodes.ComfyUI-bleh")
bleh_version = getattr(bleh, "BLEH_VERSION", -1)
if bleh_version < 1:
raise NotImplementedError
MODULES["bleh"] = bleh
class Integrations:
class Integration(NamedTuple):
key: str
module_name: str
handler: Callable | None = None
def __init__(self):
self.initialized = False
self.modules = {}
self.init_handlers = []
self.handlers = []
def __getitem__(self, key):
return self.modules[key]
def __contains__(self, key):
return key in self.modules
def __getattr__(self, key):
return self.modules.get(key)
@staticmethod
def get_custom_node(name: str) -> ModuleType | None:
module_key = f"custom_nodes.{name}"
with contextlib.suppress(StopIteration):
spec = importlib.util.find_spec(module_key)
if spec is None:
return None
return next(
v
for v in sys.modules.copy().values()
if hasattr(v, "__spec__")
and v.__spec__ is not None
and v.__spec__.origin == spec.origin
)
return None
def register_init_handler(self, handler):
self.init_handlers.append(handler)
def register_integration(self, key: str, module_name: str, handler=None) -> None:
if self.initialized:
raise ValueError(
"Internal error: Cannot register integration after initialization",
)
if any(item[0] == key or item[1] == module_name for item in self.handlers):
errstr = (
f"Module {module_name} ({key}) already in integration handlers list!"
)
raise ValueError(errstr)
self.handlers.append(self.Integration(key, module_name, handler))
def initialize(self) -> None:
if self.initialized:
return
self.initialized = True
for ih in self.handlers:
module = self.get_custom_node(ih.module_name)
if module is None:
continue
if ih.handler is not None:
module = ih.handler(module)
if module is not None:
self.modules[ih.key] = module
for init_handler in self.init_handlers:
init_handler(self)
class SonarIntegrations(Integrations):
def __init__(self, *args: list, **kwargs: dict):
super().__init__(*args, **kwargs)
self.register_integration("bleh", "ComfyUI-bleh", self.bleh_integration)
self.register_integration(
"restart",
"ComfyUI_restart_sampling",
self.restart_integration,
)
@classmethod
def bleh_integration(cls, module: ModuleType) -> ModuleType | None:
bleh_version = getattr(module, "BLEH_VERSION", -1)
if bleh_version < 1:
return None
return module
@classmethod
def restart_integration(cls, module: ModuleType) -> ModuleType | None:
if hasattr(module, "restart_sampling") and hasattr(
module.restart_sampling,
"DEFAULT_SEGMENTS",
):
return module
return None
MODULES = SonarIntegrations()
class IntegratedNode(type):
@staticmethod
def wrap_INPUT_TYPES(orig_method: Callable, *args: list, **kwargs: dict) -> dict:
MODULES.initialize()
return orig_method(*args, **kwargs)
def __new__(cls: type, name: str, bases: tuple, attrs: dict) -> object:
obj = type.__new__(cls, name, bases, attrs)
if hasattr(obj, "INPUT_TYPES"):
obj.INPUT_TYPES = partial(cls.wrap_INPUT_TYPES, obj.INPUT_TYPES)
return obj
with contextlib.suppress(ImportError, NotImplementedError):
import custom_nodes.ComfyUI_restart_sampling as rs
if not hasattr(rs.restart_sampling, "DEFAULT_SEGMENTS"):
# Dumb test but this should only exist in restart sampling versions that
# support plugging in custom noise.
raise NotImplementedError
MODULES["restart"] = rs
__all__ = ("MODULES",)
+32 -111
View File
@@ -2,8 +2,7 @@ from __future__ import annotations
import torch
from . import utils
from .external import IntegratedNode
from .external import MODULES as EXTERNAL_MODULES
from .powernoise import PowerFilter
@@ -29,8 +28,14 @@ def ffilter(x, pfilter, normalization_factor=1.0, cfg_idx=None, filter_cache=Non
return x_filt.to(x.dtype, non_blocking=True)
class FreeUExtremeConfigNode(metaclass=IntegratedNode):
DESCRIPTION = "Allows setting configuration for FreeU Extreme."
BLEND_OPS = (
{"lerp": torch.lerp}
if "bleh" not in EXTERNAL_MODULES
else EXTERNAL_MODULES["bleh"].py.latent_utils.BLENDING_MODES
)
class FreeUExtremeConfigNode:
RETURN_TYPES = ("FRUX_CONFIG",)
FUNCTION = "go"
CATEGORY = "model_patches"
@@ -39,33 +44,10 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
def INPUT_TYPES(cls):
return {
"required": {
"stage_1": (
"BOOLEAN",
{
"default": True,
"tooltip": "Controls whether this configuration applies to stage 1.",
},
),
"stage_2": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether this configuration applies to stage 2.",
},
),
"stage_3": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether this configuration applies to stage 3.",
},
),
"target": (
("backbone", "skip", "both"),
{
"tooltip": "Controls whether this filter applies to backbone or skip layers (or both).",
},
),
"stage_1": ("BOOLEAN", {"default": True}),
"stage_2": ("BOOLEAN", {"default": False}),
"stage_3": ("BOOLEAN", {"default": False}),
"target": (("backbone", "skip", "both"),),
"start": (
"FLOAT",
{
@@ -74,7 +56,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Start time as percentage of sampling this configuration applies to. Inclusive.",
},
),
"end": (
@@ -85,7 +66,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "End time as percentage of sampling this configuration applies to. Inclusive.",
},
),
"slice": (
@@ -96,7 +76,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Percentage of the layer the FreeU effect is applied to.",
},
),
"slice_offset": (
@@ -107,7 +86,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Offset as a percentage the layer is applied to. For example if slice is 0.25 and slice_offset is 0.25 then the filter will apply to the range 25% through 50%.",
},
),
"filter_norm": (
@@ -118,7 +96,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 10.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the filter. 1.0 means 100% normalized.",
},
),
"scale": (
@@ -129,7 +106,6 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 100.0,
"step": 0.1,
"round": False,
"tooltip": "Strength of the effects applied by this configuration.",
},
),
"blend": (
@@ -140,48 +116,19 @@ class FreeUExtremeConfigNode(metaclass=IntegratedNode):
"max": 10.0,
"step": 0.1,
"round": False,
"tooltip": "Blends the filtered result based on the specified strength where 1.0 means 100% filtered.",
},
),
"blend_mode": (
tuple(utils.BLENDING_MODES.keys()),
{
"tooltip": "Mode used when blending. Generally only has an effect when blend is set to values other than 0 or 1",
},
),
"hidden_mean": (
"BOOLEAN",
{
"default": True,
"tooltip": "You can think of this as FreeU V2 mode.",
},
),
"final": (
"BOOLEAN",
{
"default": True,
"tooltip": "When enabled, other configurations won't be considered if this one matched. Otherwise, multiple configurations/filter effects can be stacked.",
},
),
"blend_mode": (tuple(BLEND_OPS.keys()),),
"hidden_mean": ("BOOLEAN", {"default": True}),
"final": ("BOOLEAN", {"default": True}),
},
"optional": {
"sonar_power_filter_opt": (
"SONAR_POWER_FILTER",
{
"tooltip": "Optionally attach a Power Filter here to set filtering parameters.",
},
),
"frux_config_opt": (
"FRUX_CONFIG",
{
"tooltip": "Optionally attach another configuration node here.",
},
),
"sonar_power_filter_opt": ("SONAR_POWER_FILTER",),
"frux_config_opt": ("FRUX_CONFIG",),
},
}
@classmethod
def go(cls, **kwargs: dict):
def go(self, **kwargs: dict):
return (FreeUExtremeConfig(**kwargs),)
@@ -276,10 +223,12 @@ class FreeUExtremeConfig:
return False
if not getattr(self, f"stage_{stage}"):
return False
return not self.target not in {"skip" if is_skip else "backbone", "both"}
if self.target not in ("skip" if is_skip else "backbone", "both"):
return False
return True
def apply(self, idx, x, filter_cache, cpu_fft=False):
_batch, features, _height, _width = x.shape
batch, features, height, width = x.shape
scale = self.get_scale(x)
slice_size = int(features * self.slice)
slice_offs = int(features * self.slice_offset)
@@ -296,7 +245,7 @@ class FreeUExtremeConfig:
x[:, slice_offs : slice_offs + slice_size] = (
xslice
if self.blend == 1.0
else utils.BLENDING_MODES[self.blend_mode](
else BLEND_OPS[self.blend_mode](
x[:, slice_offs : slice_offs + slice_size],
xslice,
self.blend,
@@ -325,13 +274,12 @@ class FreeUExtremeConfig:
def clone(self):
return self.__class__(**{k: getattr(self, k) for k in self._keys})
def __repr__(self):
def __repr__(self): # noqa: D105
meh = {k: getattr(self, k) for k in self._keys}
return f"<FRUXConfig: {meh}>"
class FreeUExtremeNode(metaclass=IntegratedNode):
DESCRIPTION = "Main FreeU Extreme node. Allows patching a model with the FreeU (V2) effect with more control."
class FreeUExtremeNode:
RETURN_TYPES = ("MODEL",)
FUNCTION = "go"
CATEGORY = "model_patches"
@@ -340,45 +288,18 @@ class FreeUExtremeNode(metaclass=IntegratedNode):
def INPUT_TYPES(cls):
return {
"required": {
"model": (
"MODEL",
{
"tooltip": "Model to patch.",
},
),
"cpu_fft": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether to perform FFT calculations on the CPU. May be necessary for some GPUs that don't have native support for FFT operations at the cost of performance.",
},
),
"model": ("MODEL",),
"cpu_fft": ("BOOLEAN", {"default": False}),
},
"optional": {
"input_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for input blocks.",
},
),
"middle_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for middle blocks.",
},
),
"output_config": (
"FRUX_CONFIG",
{
"tooltip": "Allows specifying configuration for output blocks.",
},
),
"input_config": ("FRUX_CONFIG",),
"middle_config": ("FRUX_CONFIG",),
"output_config": ("FRUX_CONFIG",),
},
}
@classmethod
def go(
cls,
self,
model,
cpu_fft,
input_config=None,
+367 -1805
View File
File diff suppressed because it is too large Load Diff
+149 -732
View File
File diff suppressed because it is too large Load Diff
+356 -1288
View File
File diff suppressed because it is too large Load Diff
+77 -111
View File
@@ -16,14 +16,9 @@ from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
from PIL import Image
from torch import Tensor
from .nodes import (
NOISE_INPUT_TYPES_HINT,
WILDCARD_NOISE,
SonarCustomNoiseNodeBase,
SonarNormalizeNoiseNodeMixin,
)
from .nodes import SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMixin
from .noise import CustomNoiseItemBase
from .utils import scale_noise
from .noise_generation import scale_noise
# ruff: noqa: ANN003, FBT001, FBT002
@@ -74,10 +69,8 @@ class ChannelMixer:
),
),
)
channel_mixer = torch.eye(c).index_put_(
tuple(torch.tril_indices(c, c, offset=-1)),
channel_correlation,
)
channel_mixer = torch.eye(c)
channel_mixer[*torch.tril_indices(c, c, offset=-1)] = channel_correlation
channel_mixer += channel_mixer.tril(-1).mT
channel_mixer = torch.linalg.ldl_factor(channel_mixer).LD
dc = torch.diagonal_copy(channel_mixer)
@@ -117,7 +110,7 @@ class PowerFilter:
scale=1.0,
rel_bw=0.125,
oversample=4,
compose_with: PowerFilter | None = None,
compose_with: None | PowerFilter = None,
compose_mode="max",
):
self.min_freq = min_freq
@@ -521,7 +514,7 @@ class PowerFilterNoiseItem(PowerNoiseItem):
x,
ns,
self.make_filter(x.shape),
self.normalize_result in {True, None},
self.normalize_result in (True, None),
)
filtered_noise = filtered_ns(
torch.scalar_tensor(14.0),
@@ -538,19 +531,11 @@ class PowerFilterNoiseItem(PowerNoiseItem):
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
DESCRIPTION = "Custom noise type that applies a filter to generated noise."
@classmethod
def INPUT_TYPES(cls, *args: list, **kwargs: dict):
result = super().INPUT_TYPES(*args, **kwargs)
result["required"] |= {
"time_brownian": (
"BOOLEAN",
{
"default": False,
"tooltip": "Controls whether brownian noise is used when mix isn't 1.0.",
},
),
"time_brownian": ("BOOLEAN", {"default": False}),
"alpha": (
"FLOAT",
{
@@ -559,7 +544,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 5.0,
"step": 0.001,
"round": False,
"tooltip": "Values above 0 will amplify low frequencies, negative values will amplify high frequencies.",
},
),
"max_freq": (
@@ -570,7 +554,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 0.7071,
"step": 0.001,
"round": False,
"tooltip": "Maximum frequency to pass through the filter.",
},
),
"min_freq": (
@@ -581,7 +564,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 0.7071,
"step": 0.001,
"round": False,
"tooltip": "Minimum frequency to pass through the filter.",
},
),
"stretch": (
@@ -592,7 +574,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 100,
"step": 0.1,
"round": False,
"tooltip": "Stretches the filter's shape by the specified factor.",
},
),
"rotate": (
@@ -603,7 +584,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 90,
"step": 5,
"round": False,
"tooltip": "Rotates the filter.",
},
),
"pnorm": (
@@ -614,7 +594,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 100,
"step": 0.1,
"round": False,
"tooltip": "Factor used for cushioning the band-pass region.",
},
),
"mix": (
@@ -625,7 +604,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 1.0,
"step": 0.001,
"round": False,
"tooltip": "Controls the ratio of filtered noise. For example, 0.75 means 75% noise with the filter effects applied, 25% raw noise.",
},
),
"common_mode": (
@@ -636,7 +614,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"max": 100.0,
"step": 0.001,
"round": False,
"tooltip": "Attempts to desaturate thelatent by injecting the average across channels (controlled by channel_correction). Applied after mix.",
},
),
"channel_correlation": (
@@ -645,20 +622,13 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
"default": "1, 1, 1, 1, 1, 1",
"multiline": False,
"dynamicPrompts": False,
"tooltip": "Comma-separated list of channel correlation strengths.",
},
),
"preview": (
("none", "no_mix", "mix"),
{
"tooltip": "When enabled, displays a preview of the filter shape and a sample of noise. Mix - previews noise after mix is applied. no_mix - only previews the filtered noise.",
},
),
"preview": (("none", "no_mix", "mix"),),
}
return result
@classmethod
def get_item_class(cls):
def get_item_class(self):
return PowerNoiseItem
def go(
@@ -676,8 +646,6 @@ class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
class SonarPowerFilterNoiseNode(SonarPowerNoiseNode, SonarNormalizeNoiseNodeMixin):
DESCRIPTION = "Custom noise type that allows applying a Power Filter to another custom noise generator."
@classmethod
def INPUT_TYPES(cls):
result = super().INPUT_TYPES(include_rescale=False, include_chain=False)
@@ -692,18 +660,8 @@ class SonarPowerFilterNoiseNode(SonarPowerNoiseNode, SonarNormalizeNoiseNodeMixi
):
del result["required"][k]
result["required"] |= {
"sonar_custom_noise": (
WILDCARD_NOISE,
{
"tooltip": f"Custom noise type to filter.\n{NOISE_INPUT_TYPES_HINT}",
},
),
"sonar_power_filter": (
"SONAR_POWER_FILTER",
{
"tooltip": "Filter to use.",
},
),
"sonar_custom_noise": ("SONAR_CUSTOM_NOISE",),
"sonar_power_filter": ("SONAR_POWER_FILTER",),
"filter_norm_factor": (
"FLOAT",
{
@@ -712,32 +670,15 @@ class SonarPowerFilterNoiseNode(SonarPowerNoiseNode, SonarNormalizeNoiseNodeMixi
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the specified filter. 1.0 means 100% normalized.",
},
),
"normalize_result": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the final result is normalized to 1.0 strength.",
},
),
"normalize_noise": (
("default", "forced", "disabled"),
{
"tooltip": "Controls whether the generated noise is normalized to 1.0 strength.",
},
),
"normalize_result": (("default", "forced", "disabled"),),
"normalize_noise": (("default", "forced", "disabled"),),
}
result["required"]["preview"] = (
(*result["required"]["preview"][0], "custom"),
{
"tooltip": "When enabled, displays a preview of the filter shape and a sample of noise. Mix - previews noise after mix is applied. no_mix - only previews the filtered noise. custom - Like no_mix, but will use a latent previewer to display a color preview of the generated noise. Works best when previewer is set to TAESD.",
},
)
result["required"]["preview"] = ((*result["required"]["preview"][0], "custom"),)
return result
@classmethod
def get_item_class(cls):
def get_item_class(self):
return PowerFilterNoiseItem
def go(
@@ -771,23 +712,69 @@ class SonarPowerFilterNode:
@classmethod
def INPUT_TYPES(cls):
include_keys = {"alpha", "max_freq", "min_freq", "stretch", "rotate", "pnorm"}
return {
"required": {
k: v
for k, v in SonarPowerNoiseNode.INPUT_TYPES()["required"].items()
if k in include_keys
}
| {
"oversample": (
"INT",
"alpha": (
"FLOAT",
{
"default": 4,
"min": 1,
"max": 128,
"tooltip": "Oversampling factor used for the filter size.",
"default": 0.0,
"min": -5.0,
"max": 5.0,
"step": 0.001,
"round": False,
},
),
"max_freq": (
"FLOAT",
{
"default": 0.7071,
"min": 0.0,
"max": 0.7071,
"step": 0.001,
"round": False,
},
),
"min_freq": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 0.7071,
"step": 0.001,
"round": False,
},
),
"stretch": (
"FLOAT",
{
"default": 1.0,
"min": 0.01,
"max": 100,
"step": 0.1,
"round": False,
},
),
"rotate": (
"FLOAT",
{
"default": 0,
"min": -90,
"max": 90,
"step": 5,
"round": False,
},
),
"pnorm": (
"FLOAT",
{
"default": 2,
"min": 0.125,
"max": 100,
"step": 0.1,
"round": False,
},
),
"oversample": ("INT", {"default": 4, "min": 1, "max": 128}),
"blur": (
"FLOAT",
{
@@ -796,7 +783,6 @@ class SonarPowerFilterNode:
"max": 10.0,
"step": 0.01,
"round": False,
"tooltip": "Slightly blurs the filter to reduce artifacts.",
},
),
"scale": (
@@ -807,24 +793,17 @@ class SonarPowerFilterNode:
"max": 100.0,
"step": 0.1,
"round": False,
"tooltip": "Scales the filter to the specified strength. May be negative.",
},
),
"compose_mode": (
("max", "min", "add", "sub", "mul"),
{
"tooltip": "Controls composition of the option attached filter. For example, when set to MUL the result will be this filter multiplied by the attached filter. No effect if the optional filter input is not attached.",
},
),
"compose_mode": (("max", "min", "add", "sub", "mul"),),
},
"optional": {
"power_filter_opt": ("SONAR_POWER_FILTER",),
},
}
@classmethod
def go(
cls,
self,
min_freq=0.0,
max_freq=0.7071,
stretch=1.0,
@@ -855,7 +834,6 @@ class SonarPowerFilterNode:
class SonarPreviewFilterNode:
DESCRIPTION = "Allows previewing a Power Filter."
RETURN_TYPES = ("SONAR_POWER_FILTER",)
CATEGORY = "advanced/noise"
FUNCTION = "go"
@@ -865,12 +843,7 @@ class SonarPreviewFilterNode:
def INPUT_TYPES(cls):
return {
"required": {
"sonar_power_filter": (
"SONAR_POWER_FILTER",
{
"tooltip": "Power Filter to preview.",
},
),
"sonar_power_filter": ("SONAR_POWER_FILTER",),
"filter_gain": (
"FLOAT",
{
@@ -879,7 +852,6 @@ class SonarPreviewFilterNode:
"max": 1000000.0,
"step": 0.1,
"round": False,
"tooltip": "Gain factor applied to the filter part of the preview.",
},
),
"kernel_gain": (
@@ -890,7 +862,6 @@ class SonarPreviewFilterNode:
"max": 1000000.0,
"step": 0.1,
"round": False,
"tooltip": "Gain factor applied to the kernel part of the preview.",
},
),
"norm_factor": (
@@ -901,7 +872,6 @@ class SonarPreviewFilterNode:
"max": 1.0,
"step": 0.1,
"round": False,
"tooltip": "Normalization factor applied to the filter before previewing. 1.0 means 100% normalized.",
},
),
"preview_size": (
@@ -916,16 +886,12 @@ class SonarPreviewFilterNode:
"128x127",
"127x128",
),
{
"tooltip": "Controls the size of the generated preview. Note: Sizes are in latent pixels. For most models, one latent pixel equals eight pixels",
},
),
},
}
@classmethod
def go(
cls,
self,
sonar_power_filter,
filter_gain=1 / 3,
kernel_gain=1 / 3,
+215 -420
View File
@@ -2,26 +2,22 @@
from __future__ import annotations
import importlib
from enum import Enum, auto
from functools import lru_cache
from sys import stderr
from typing import Any, Callable, NamedTuple
import torch
from comfy.k_diffusion.sampling import get_ancestral_step, to_d
from comfy.samplers import KSampler, k_diffusion_sampling
from comfy.k_diffusion import sampling
from torch import Tensor
from tqdm.auto import trange
from . import noise, utils
from . import noise
class HistoryType(Enum):
ZERO = auto()
RAND = auto()
SAMPLE = auto()
SAMPLE_NORM = auto()
class GuidanceType(Enum):
@@ -37,34 +33,15 @@ class GuidanceConfig(NamedTuple):
latent: Tensor | None = None
class MomentumMode(Enum):
CLASSIC = auto()
NEW = auto()
DENOISED = auto()
class SonarConfig(NamedTuple):
momentum: float = 0.95
momentum_hist: float = 0.75
direction: float = 1.0
momentum_start_step: int = 0
momentum_end_step: int = 9999
always_update_history: bool = True
momentum_mode: MomentumMode = MomentumMode.NEW
init: HistoryType = HistoryType.ZERO
noise_type: noise.NoiseType | None = None
custom_noise: noise.CustomNoise | None = None
rand_init_noise_type: noise.NoiseType | None = None
rand_init_noise_multiplier: float | int = 1.0
guidance: GuidanceConfig | None = None
blend_mode: str = "lerp"
momentum_blend_mode: str | None = None
history_blend_mode: str | None = None
guidance_blend_mode: str | None = None
def get_with_default(self, k: str, default: Any) -> Any: # noqa: ANN401
val = getattr(self, k)
return val if val is not None else default
class SonarBase:
@@ -74,74 +51,19 @@ class SonarBase:
self.history_d = None
self.cfg = cfg
self.noise_sampler = None
blend_mode = cfg.blend_mode
momentum_blend_mode = cfg.get_with_default("momentum_blend_mode", blend_mode)
history_blend_mode = cfg.get_with_default("history_blend_mode", blend_mode)
guidance_blend_mode = cfg.get_with_default("guidance_blend_mode", blend_mode)
bf = self.blend = utils.BLENDING_MODES[blend_mode]
self.momentum_blend = (
bf
if momentum_blend_mode == blend_mode
else utils.BLENDING_MODES[momentum_blend_mode]
)
self.history_blend = (
bf
if history_blend_mode == blend_mode
else utils.BLENDING_MODES[history_blend_mode]
)
self.guidance_blend = (
bf
if guidance_blend_mode == blend_mode
else utils.BLENDING_MODES[guidance_blend_mode]
)
_cfg_fixups = (
("momentum_mode", MomentumMode),
("init", HistoryType),
("noise_type", noise.NoiseType),
)
@classmethod
def get_config(
cls,
cfg: SonarConfig | None = None,
ext: dict | None = None,
) -> SonarConfig:
cfgdict = ext.copy() if ext is not None else {}
empty = object()
for k, enum_class in cls._cfg_fixups:
val = cfgdict.get(k, empty)
if val is empty:
continue
if isinstance(val, str):
val = getattr(enum_class, val.strip().upper(), empty)
if val is empty:
validstr = ", ".join(enum_class.__members__.keys())
errstr = f"Bad value for {k} of type enum {enum_class.__name__}, must be one of the following: {validstr}"
raise ValueError(errstr)
cfgdict[k] = val
continue
if not isinstance(val, enum_class):
errstr = f"Bad parameter type for {k}: Must be valid string or instance of {enum_class.__name__}"
raise TypeError(errstr)
if cfg is None:
return SonarConfig(**cfgdict)
cfgdict = cfg._asdict() | cfgdict
return SonarConfig(**cfgdict)
def set_noise_sampler(
self,
x: Tensor,
sigmas: Tensor,
sigmas,
noise_sampler: Callable | None,
seed: int | None = None,
) -> Callable:
):
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
if noise_sampler is not None and self.cfg.noise_type not in {
if noise_sampler is not None and self.cfg.noise_type not in (
None,
self.DEFAULT_NOISE_TYPE,
}:
):
print(
"Sonar: Warning: Noise sampler supplied, overriding noise type from settings",
file=stderr,
@@ -166,32 +88,17 @@ class SonarBase:
self.noise_sampler = noise_sampler
return noise_sampler
def init_hist_d(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
) -> None:
if self.history_d is not None or not self.check_step(step, is_history=True):
def init_hist_d(self, x: Tensor) -> None:
if self.history_d is not None:
return
cfg = self.cfg
init = cfg.init
# memorize delta momentum
if init == HistoryType.ZERO:
self.history_d = None
elif init == HistoryType.SAMPLE:
self.history_d = (
x if cfg.momentum_mode != MomentumMode.DENOISED else denoised
)
elif init == HistoryType.SAMPLE_NORM:
self.history_d = (
x if cfg.momentum_mode != MomentumMode.DENOISED else denoised
) / sigma
elif init == HistoryType.RAND:
if self.cfg.init == HistoryType.ZERO:
self.history_d = 0
elif self.cfg.init == HistoryType.SAMPLE:
self.history_d = x
elif self.cfg.init == HistoryType.RAND:
ns = noise.get_noise_sampler(
cfg.rand_init_noise_type,
self.cfg.rand_init_noise_type,
x,
None,
None,
@@ -200,124 +107,31 @@ class SonarBase:
normalized=True,
)
self.history_d = ns(None, None)
if cfg.rand_init_noise_multiplier != 1:
self.history_d *= cfg.rand_init_noise_multiplier
else:
raise ValueError("Sonar sampler: bad history type")
@property
@lru_cache(maxsize=1) # noqa: B019
def history_ratios(self):
direction = self.cfg.direction
momentum_hist = self.cfg.momentum_hist
return (
momentum_hist,
1.0 + abs(direction) * (1 - momentum_hist)
if direction < 0
else 2.0 - direction,
direction,
)
def check_step(self, step: int, *, is_history: bool = False):
cfg = self.cfg
if is_history and cfg.always_update_history:
return True
return cfg.momentum_start_step <= step <= cfg.momentum_end_step
def update_hist(self, momentum_d: torch.Tensor, step: int) -> None:
hd, cfg = self.history_d, self.cfg
if cfg.momentum_hist == 1 or not self.check_step(step, is_history=True):
return
hd_ratio, hd_scale, md_scale = self.history_ratios
self.history_d = (
momentum_d
if hd is None
else self.history_blend(momentum_d * md_scale, hd * hd_scale, hd_ratio)
)
def momentum_mix(
self,
history: Tensor | None,
item: Tensor,
sigma: Tensor,
*,
is_denoised: bool = False,
momentum=None,
) -> Tensor:
momentum = self.cfg.momentum if momentum is None else momentum
mode = self.cfg.momentum_mode
if (
momentum == 1 # noqa: PLR0916
or history is None
or (mode == MomentumMode.DENOISED and not is_denoised)
or (mode != MomentumMode.DENOISED and is_denoised)
):
return item
return self.momentum_blend(
history * sigma if is_denoised else history,
item,
momentum,
)
def get_momentum_denoised(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
momentum: float | None = None,
update_history=True,
) -> Tensor:
def update_hist(self, momentum_d):
q = 1.0 - self.cfg.momentum_hist
hd = self.history_d
momentum_denoised = self.momentum_mix(
hd,
denoised,
sigma,
is_denoised=True,
momentum=momentum,
)
if update_history:
self.init_hist_d(x, denoised, sigma, step=step)
self.update_hist(denoised / sigma, step=step)
return momentum_denoised if self.check_step(step) else denoised
if isinstance(hd, int) and hd == 0:
self.history_d = momentum_d
else:
self.history_d = (1.0 - q) * hd + q * momentum_d
def get_momentum_d(
self,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
*,
step: int,
momentum: float | None = None,
d: Tensor | None = None,
update_history=True,
) -> Tensor:
def momentum_step(self, x: Tensor, d: Tensor, dt: Tensor):
if self.cfg.momentum == 1.0:
return x + d * dt
hd = self.history_d
cfg = self.cfg
momentum = cfg.momentum if momentum is None else momentum
mode = cfg.momentum_mode
d = to_d(x, sigma, denoised) if d is None else d
if momentum == 1 or mode == MomentumMode.DENOISED:
return d
momentum_d = self.momentum_mix(hd, d, sigma)
if update_history:
self.init_hist_d(x, denoised, sigma, step=step)
self.update_hist(d if mode == MomentumMode.NEW else momentum_d, step=step)
return momentum_d if self.check_step(step) else d
# correct current `d` with momentum
p = (1.0 - self.cfg.momentum) * self.cfg.direction
momentum_d = (1.0 - p) * d + p * hd
def momentum_step(
self,
step: int,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
sigma_down: Tensor,
) -> Tensor:
dt = sigma_down - sigma
denoised = self.get_momentum_denoised(x, denoised, sigma, step=step)
momentum_d = self.get_momentum_d(x, denoised, sigma, step=step)
return (momentum_d * dt).add_(x)
# Euler method with momentum
x = x + momentum_d * dt
self.update_hist(momentum_d)
return x
class SonarGuidanceMixin:
@@ -336,26 +150,19 @@ class SonarGuidanceMixin:
def prepare_ref_latent(latent: Tensor | None) -> Tensor:
if latent is None:
return None
avg_s = latent.mean(dim=(-2, -1), keepdim=True)
std_s = latent.std(dim=(-2, -1), keepdim=True)
return (latent - avg_s).div_(std_s).to(latent.dtype)
avg_s = latent.mean(dim=[2, 3], keepdim=True)
std_s = latent.std(dim=[2, 3], keepdim=True)
return ((latent - avg_s) / std_s).to(latent.dtype)
def guidance_step(self, step_index: int, x: Tensor, denoised: Tensor) -> Tensor:
if (
self.guidance is None
or self.guidance.factor == 0.0
or not self.guidance.start_step <= step_index <= self.guidance.end_step
def guidance_step(self, step_index: int, x: Tensor, denoised: Tensor):
if (self.guidance is None or self.guidance.factor == 0.0) or not (
self.guidance.start_step <= (step_index + 1) <= self.guidance.end_step
):
return x
if self.ref_latent.device != x.device:
self.ref_latent = self.ref_latent.to(device=x.device)
if self.guidance.guidance_type == GuidanceType.LINEAR:
return self.guidance_linear(
x,
self.ref_latent,
self.guidance.factor,
blend=self.guidance_blend,
)
return self.guidance_linear(x, self.ref_latent, self.guidance.factor)
if self.guidance.guidance_type == GuidanceType.EULER:
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
return self.guidance_euler(
@@ -377,26 +184,20 @@ class SonarGuidanceMixin:
ref_latent: Tensor,
factor: float = 0.2,
) -> Tensor:
avg_t = denoised.mean(dim=(-3, -2, -1), keepdim=True)
std_t = denoised.std(dim=(-3, -2, -1), keepdim=True)
avg_t = denoised.mean(dim=[1, 2, 3], keepdim=True)
std_t = denoised.std(dim=[1, 2, 3], keepdim=True)
ref_img_shift = ref_latent * std_t + avg_t
d = to_d(x, sigma, ref_img_shift)
d = sampling.to_d(x, sigma, ref_img_shift)
dt = (sigma_next - sigma) * factor
return (d * dt).add_(x)
return x + d * dt
@staticmethod
def guidance_linear(
x: Tensor,
ref_latent: Tensor,
factor: float = 0.2,
*,
blend=torch.lerp,
) -> Tensor:
avg_t = x.mean(dim=(-3, -2, -1), keepdim=True)
std_t = x.std(dim=(-3, -2, -1), keepdim=True)
ref_img_shift = (ref_latent * std_t).add_(avg_t)
return blend(x, ref_img_shift, factor)
def guidance_linear(x: Tensor, ref_latent: Tensor, factor: float = 0.2) -> Tensor:
avg_t = x.mean(dim=[1, 2, 3], keepdim=True)
std_t = x.std(dim=[1, 2, 3], keepdim=True)
ref_img_shift = ref_latent * std_t + avg_t
return (1.0 - factor) * x + factor * ref_img_shift
class SonarWithGuidance(SonarBase, SonarGuidanceMixin):
@@ -409,9 +210,9 @@ class SonarSampler(SonarWithGuidance):
def __init__(
self,
model,
sigmas: Tensor,
s_in: Tensor,
extra_args: dict[str, Any],
sigmas,
s_in,
extra_args,
*args: list[Any],
**kwargs: dict[str, Any],
):
@@ -421,49 +222,62 @@ class SonarSampler(SonarWithGuidance):
self.s_in = s_in
self.extra_args = extra_args
def call_model(
self,
x: Tensor,
sigma: Tensor,
*args: list[Any],
s_in=None,
extra_args=None,
) -> Tensor:
if s_in is None:
s_in = self.s_in
extra_args = (
self.extra_args if extra_args is None else self.extra_args | extra_args
)
return self.model(x, sigma * s_in, *args, **extra_args)
class SonarEuler(SonarSampler):
def __init__(
self,
s_churn: float = 0.0,
s_tmin: float = 0.0,
s_tmax: float = float("inf"),
s_noise: float = 1.0,
*args: list[Any],
**kwargs: dict[str, Any],
):
super().__init__(*args, **kwargs)
self.s_churn = s_churn
self.s_tmin = s_tmin
self.s_tmax = s_tmax
self.s_noise = s_noise
def step(self, step_index: int, sample: torch.FloatTensor):
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
def step(
self,
step_index: int,
sample: torch.FloatTensor,
):
self.init_hist_d(sample)
denoised = self.call_model(sample, sigma)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma,
sigma_next,
sigma, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
gamma = (
min(self.s_churn / (len(self.sigmas) - 1), 2**0.5 - 1)
if self.s_tmin <= sigma <= self.s_tmax
else 0.0
)
if sigma_next > 0:
sigma_hat = sigma * (gamma + 1)
if gamma > 0:
noise = (
self.noise_sampler(sigma, sigma_to)
if self.noise_sampler
else torch.randn_like(sample)
)
eps = noise * self.s_noise
sample = sample + eps * (sigma_hat**2 - sigma**2) ** 0.5
denoised = self.model(sample, sigma_hat * self.s_in, **self.extra_args)
derivative = sampling.to_d(sample, sigma, denoised)
dt = self.sigmas[step_index + 1] - sigma_hat
result_sample = self.momentum_step(sample, derivative, dt)
if self.sigmas[step_index + 1] > 0:
result_sample = self.guidance_step(step_index, result_sample, denoised)
return (
result_sample,
sigma,
sigma,
sigma_hat,
denoised,
)
@@ -472,18 +286,26 @@ class SonarEuler(SonarSampler):
def sampler(
cls,
model,
x: Tensor,
sigmas: Tensor,
extra_args: dict | None = None,
x,
sigmas,
extra_args=None,
callback=None,
disable: bool | None = None, # noqa: FBT001
disable=None,
noise_sampler: Callable | None = None,
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
) -> Tensor:
sonar_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
sonar_config=None,
s_churn=0.0,
s_tmin=0.0,
s_tmax=float("inf"),
s_noise=1.0,
):
if sonar_config is None:
sonar_config = SonarConfig()
s_in = x.new_ones([x.shape[0]])
sonar = cls(
s_churn,
s_tmin,
s_tmax,
s_noise,
model,
sigmas,
s_in,
@@ -507,7 +329,7 @@ class SonarEuler(SonarSampler):
{
"x": x,
"i": i,
"sigma": sigma,
"sigma": sigmas[i],
"sigma_hat": sigma_hat,
"denoised": denoised,
},
@@ -532,32 +354,31 @@ class SonarEulerAncestral(SonarSampler):
step_index: int,
sample: torch.FloatTensor,
):
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = get_ancestral_step(
sigma,
sigma_next,
self.init_hist_d(sample)
sigma_from, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = sampling.get_ancestral_step(
sigma_from,
sigma_to,
eta=self.eta,
)
denoised = self.call_model(sample, sigma)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma,
sigma_down,
)
if sigma_next > 0:
denoised = self.model(sample, sigma_from * self.s_in, **self.extra_args)
derivative = sampling.to_d(sample, sigma_from, denoised)
dt = sigma_down - sigma_from
result_sample = self.momentum_step(sample, derivative, dt)
if sigma_to > 0:
result_sample = self.guidance_step(step_index, result_sample, denoised)
result_sample = ( # noqa: PLR6104
result_sample = (
result_sample
+ self.noise_sampler(sigma, sigma_next) * (self.s_noise * sigma_up)
+ self.noise_sampler(sigma_from, sigma_to) * self.s_noise * sigma_up
)
return (
result_sample,
sigma,
sigma,
sigma_from,
sigma_from,
denoised,
)
@@ -571,14 +392,14 @@ class SonarEulerAncestral(SonarSampler):
extra_args=None,
callback=None,
disable=None,
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
sonar_config=None,
eta=1.0,
s_noise=1.0,
noise_sampler: Callable | None = None,
):
sonar_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
if sonar_config is None:
sonar_config = SonarConfig()
s_in = x.new_ones([x.shape[0]])
sonar = cls(
eta,
s_noise,
@@ -596,7 +417,7 @@ class SonarEulerAncestral(SonarSampler):
)
for i in trange(len(sigmas) - 1, disable=disable):
x, _sigma, sigma_hat, denoised = sonar.step(
x, sigma, sigma_hat, denoised = sonar.step(
i,
x,
)
@@ -628,134 +449,104 @@ class SonarDPMPPSDE(SonarSampler):
self.s_noise = s_noise
@staticmethod
def sigma_fn(t: Tensor) -> float:
def sigma_fn(t) -> float:
return t.neg().exp()
@staticmethod
def t_fn(sigma: Tensor) -> float:
return sigma.log().neg()
def t_fn(sigma) -> float:
return sigma.log.neg()
# DPM++ solver algorithm copied from ComfyUI source.
def momentum_step( # noqa: PLR0914
def momentum_step(
self,
step_index: int,
step_index,
x: Tensor,
denoised: Tensor,
sigma: Tensor,
sigma_next: Tensor,
sigma_down: Tensor,
) -> Tensor:
if sigma_next == 0:
return super().momentum_step(step_index, x, denoised, sigma, sigma_down)
sigma_from,
sigma_to,
sigma_down,
):
if sigma_to == 0:
derivative = sampling.to_d(x, sigma_from, denoised)
dt = sigma_down - sigma_from
return super().momentum_step(x, derivative, dt)
cfg = self.cfg
# Halve the momentum proportion if there's history since we will use it twice.
adjusted_momentum = (
cfg.momentum + (1 - cfg.momentum) / 2
if self.history_d is not None
else cfg.momentum
)
r = 1 / 2
# DPM-Solver++
t, t_next = self.t_fn(sigma), self.t_fn(sigma_next)
h = t_next - t
s = t + h * r
fac = 1 / (2 * r)
# Step 1
s_t, s_s = self.sigma_fn(t), self.sigma_fn(s)
sd, su = get_ancestral_step(
s_t,
s_s,
self.eta,
)
s_ = self.t_fn(sd)
momentum_denoised = self.get_momentum_denoised(
x,
denoised,
sigma,
step=step_index,
)
diff_2 = (t - s_).expm1() * momentum_denoised
momentum_d = self.get_momentum_d(
x,
momentum_denoised,
sigma,
step=step_index,
momentum=adjusted_momentum,
d=diff_2,
)
x_2 = ((self.sigma_fn(s_) / s_t) * x).sub_(momentum_d)
x_2 += self.noise_sampler(s_t, s_s).mul_(
self.s_noise * su,
)
sigma_2 = s_s
denoised_2 = self.call_model(x_2, sigma_2)
momentum_denoised_2 = self.get_momentum_denoised(
x,
denoised_2,
sigma_2,
step=step_index,
)
# Step 2
s_t_next = self.sigma_fn(t_next)
sd, su = get_ancestral_step(
s_t,
s_t_next,
self.eta,
)
t_down = self.t_fn(sd)
denoised_d = (1 - fac) * momentum_denoised + fac * momentum_denoised_2
diff_1 = (t - t_down).expm1() * denoised_d
momentum_d = self.get_momentum_d(
x,
momentum_denoised_2,
sigma_2,
step=step_index,
momentum=adjusted_momentum,
d=diff_1,
)
x = ((self.sigma_fn(t_down) / s_t) * x).sub_(momentum_d)
x = self.guidance_step(step_index, x, denoised_d)
x += self.noise_sampler(s_t, s_t_next).mul_(
self.s_noise * su,
)
return x
def step(
self,
step_index: int,
sample: torch.FloatTensor,
) -> Tensor:
def sigma_fn(t):
return t.neg().exp()
def t_fn(sigma):
return sigma.log().neg()
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, _sigma_up = get_ancestral_step(
sigma,
sigma_next,
hd = self.history_d
p = (1.0 - self.cfg.momentum) * self.cfg.direction
r = 1 / 2
# DPM-Solver++
t, t_next = t_fn(sigma_from), t_fn(sigma_to)
h = t_next - t
s = t + h * r
fac = 1 / (2 * r)
# Step 1
sd, su = sampling.get_ancestral_step(sigma_fn(t), sigma_fn(s), self.eta)
s_ = t_fn(sd)
diff_2 = (t - s_).expm1() * denoised
momentum_d = (1.0 - p) * diff_2 + p * hd
self.update_hist(momentum_d)
hd = self.history_d
x_2 = (sigma_fn(s_) / sigma_fn(t)) * x - momentum_d
x_2 = x_2 + self.noise_sampler(sigma_fn(t), sigma_fn(s)) * self.s_noise * su
denoised_2 = self.model(x_2, sigma_fn(s) * self.s_in, **self.extra_args)
# Step 2
sd, su = sampling.get_ancestral_step(
sigma_fn(t),
sigma_fn(t_next),
self.eta,
)
t_next_ = t_fn(sd)
denoised_d = (1 - fac) * denoised + fac * denoised_2
diff_1 = (t - t_next_).expm1() * denoised_d
momentum_d = (1.0 - p) * diff_1 + p * hd
self.update_hist(momentum_d)
x = (sigma_fn(t_next_) / sigma_fn(t)) * x - momentum_d
x = self.guidance_step(step_index, x, denoised_d)
return x + self.noise_sampler(sigma_fn(t), sigma_fn(t_next)) * self.s_noise * su
def step(
self,
step_index: int,
sample: torch.FloatTensor,
):
def sigma_fn(t):
return t.neg().exp()
def t_fn(sigma):
return sigma.log().neg()
self.init_hist_d(sample)
sigma_from, sigma_to = self.sigmas[step_index], self.sigmas[step_index + 1]
sigma_down, sigma_up = sampling.get_ancestral_step(
sigma_from,
sigma_to,
eta=self.eta,
)
denoised = self.call_model(sample, sigma)
denoised = self.model(sample, sigma_from * self.s_in, **self.extra_args)
result_sample = self.momentum_step(
step_index,
sample,
denoised,
sigma,
sigma_next,
sigma_from,
sigma_to,
sigma_down,
)
return (
result_sample,
sigma,
sigma,
sigma_from,
sigma_from,
denoised,
)
@@ -764,19 +555,19 @@ class SonarDPMPPSDE(SonarSampler):
def sampler(
cls,
model,
x: Tensor,
sigmas: Tensor,
extra_args: dict | None = None,
x,
sigmas,
extra_args=None,
callback=None,
disable: bool | None = None, # noqa: FBT001
sonar_config: SonarConfig | None = None,
sonar_params: dict | None = None,
disable=None,
sonar_config=None,
eta=1.0,
s_noise=1.0,
noise_sampler=None,
) -> Tensor:
sonar_config = cls.get_config(sonar_config, sonar_params)
s_in = x.new_ones((x.shape[0],))
):
if sonar_config is None:
sonar_config = SonarConfig()
s_in = x.new_ones([x.shape[0]])
sonar = cls(
eta,
s_noise,
@@ -794,7 +585,7 @@ class SonarDPMPPSDE(SonarSampler):
)
for i in trange(len(sigmas) - 1, disable=disable):
x, _sigma, sigma_hat, denoised = sonar.step(
x, sigma, sigma_hat, denoised = sonar.step(
i,
x,
)
@@ -811,7 +602,11 @@ class SonarDPMPPSDE(SonarSampler):
return x
def add_samplers() -> None:
def add_samplers():
import importlib
from comfy.samplers import KSampler, k_diffusion_sampling
extra_samplers = {
"sonar_euler": SonarEuler.sampler,
"sonar_euler_ancestral": SonarEulerAncestral.sampler,
-196
View File
@@ -1,196 +0,0 @@
from __future__ import annotations
import math
import torch
from comfy.model_management import device_supports_non_blocking
from comfy.utils import common_upscale
from .external import MODULES as EXT
BLENDING_MODES = {"lerp": torch.lerp}
UPSCALE_METHODS = (
"bilinear",
"nearest-exact",
"nearest",
"area",
"bicubic",
"bislerp",
)
def scale_samples(
samples: torch.Tensor,
width: int,
height: int,
*,
mode: str = "bicubic",
) -> torch.Tensor:
return common_upscale(samples, width, height, mode, None)
def init_integrations(integrations) -> None:
global scale_samples, BLENDING_MODES, UPSCALE_METHODS # noqa: PLW0603
bleh = integrations.bleh
if bleh is None:
return
bleh_latentutils = bleh.py.latent_utils
BLENDING_MODES = bleh_latentutils.BLENDING_MODES
UPSCALE_METHODS = bleh_latentutils.UPSCALE_METHODS
scale_samples = bleh_latentutils.scale_samples
EXT.register_init_handler(init_integrations)
def scale_noise(
noise: torch.Tensor,
factor: float = 1.0,
*,
normalized: bool = True,
threshold_std_devs: float = 2.5,
normalize_dims: tuple | None = None,
) -> torch.Tensor:
numel = noise.numel()
if not normalized or numel == 0:
return noise.mul_(factor) if factor != 1 else noise
if normalize_dims is not None:
std = noise.std(dim=normalize_dims, keepdim=True)
noise = noise / std # noqa: PLR6104
return noise.sub_(noise.mean(dim=normalize_dims, keepdim=True)).mul_(factor)
mean, std = noise.mean().item(), noise.std().item()
threshold = threshold_std_devs / math.sqrt(numel)
if abs(mean) > threshold:
noise -= mean
if abs(1.0 - std) > threshold:
noise /= std
return noise.mul_(factor) if factor != 1 else noise
CAN_NONBLOCK = {}
def tensor_to(
tensor: torch.Tensor,
dest: torch.Tensor | torch.Device | str,
) -> torch.Tensor:
device = dest.device if isinstance(dest, torch.Tensor) else dest
non_blocking = CAN_NONBLOCK.get(device)
if non_blocking is None:
non_blocking = device_supports_non_blocking(device)
CAN_NONBLOCK[device] = non_blocking
return tensor.to(dest, non_blocking=non_blocking)
def quantile_normalize(
noise: torch.Tensor,
*,
quantile: float = 0.75,
dim: int | None = 1,
flatten: bool = True,
nq_fac: float = 1.0,
pow_fac: float = 0.5,
) -> torch.Tensor:
if quantile is None or quantile <= 0 or quantile >= 1:
return noise
orig_shape = noise.shape
if isinstance(quantile, (tuple, list)):
quantile = torch.tensor(
quantile,
device=noise.device,
dtype=noise.dtype,
)
qdim = dim
if noise.ndim > 1 and flatten:
if qdim is not None and qdim >= noise.ndim:
qdim = 1 if noise.ndim > 2 else None
if qdim is None:
flatdim = 0
elif qdim in {0, 1}:
flatdim = qdim + 1
elif qdim in {2, 3}:
noise = noise.movedim(qdim, 1)
tempshape = noise.shape
flatdim = 2
else:
raise ValueError(
"Cannot handling quantile normalization flattening dims > 3",
)
else:
flatdim = None
nq = torch.quantile(
(noise if flatdim is None else noise.flatten(start_dim=flatdim)).abs(),
quantile,
dim=-1,
)
nq_shape = tuple(nq.shape) + (1,) * (noise.ndim - nq.ndim)
nq = nq.mul_(nq_fac).reshape(*nq_shape)
noise = noise.clamp(-nq, nq)
noise = torch.copysign(
torch.pow(torch.abs(noise), pow_fac),
noise,
)
if flatdim is not None and qdim in {2, 3}:
return (
noise.reshape(tempshape).movedim(1, qdim).reshape(orig_shape).contiguous()
)
return noise
def adjust_slice(s: slice, size: int, offset: int) -> slice:
if offset == 0:
return s
# Input slice must have positive start/stop and be in bounds for the object that will be sliced here.
start = s.start if s.start is not None else 0
stop = s.stop if s.stop is not None else size
if offset < 0:
adj = min(start, abs(offset))
return slice(start - adj, stop - adj)
adj = min(size - stop, offset)
return slice(start + adj, stop + adj)
def crop_samples(
tensor: torch.Tensor,
width: int,
height: int,
*,
mode="center",
offset_width: int = 0,
offset_height: int = 0,
):
if tensor.ndim < 3:
raise ValueError("Can only handle >= 3 dimensional tensors")
th, tw = tensor.shape[-2:]
if (tw, th) == (width, height):
return tensor
if tw < width or th < height:
raise ValueError("Can't crop sample smaller than requested width or height")
if mode == "center":
hmode = wmode = "center"
else:
hmode, wmode, *splitextra = mode.split("_")
if splitextra:
raise ValueError("Bad composite mode")
if hmode == "top":
hslice = slice(0, height)
elif hmode == "center":
hoffs = (th - height) // 2
hslice = slice(hoffs, hoffs + height)
elif hmode == "bottom":
hslice = slice(th - height, th)
else:
raise ValueError("Bad height mode in composite mode")
if wmode == "left":
wslice = slice(0, width)
elif wmode == "center":
woffs = (tw - width) // 2
wslice = slice(woffs, woffs + width)
elif wmode == "right":
wslice = slice(tw - width, tw)
else:
raise ValueError("Bad width mode in composite mode")
wslice = adjust_slice(wslice, tw, offset_width)
hslice = adjust_slice(hslice, th, offset_height)
return tensor[..., hslice, wslice]
-5
View File
@@ -8,15 +8,11 @@ ignore = [
"ANN204",
"ANN206",
"C901",
"CPY001",
"DOC201",
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D211",
"D213",
@@ -30,7 +26,6 @@ ignore = [
"PLR0912",
"PLR0913",
"PLR0915",
"PLR0917",
"PLR2004",
"T201",
"TRY003",