Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c4ae67e32 | ||
|
|
cf90ae74e1 | ||
|
|
4a97ad3468 | ||
|
|
c2a93d55cb | ||
|
|
2b2a76bcbe | ||
|
|
29ed97230e | ||
|
|
c3d1149aff | ||
|
|
83460f3b8f | ||
|
|
9dedbeb0b0 | ||
|
|
d25d01542e | ||
|
|
1295521583 | ||
|
|
607868c5c1 | ||
|
|
543f39ebf2 | ||
|
|
8097f26863 | ||
|
|
a4fed311a8 | ||
|
|
2988afa34a | ||
|
|
68fc7418d1 | ||
|
|
6d15c0bbca | ||
|
|
f7cbbfcbda | ||
|
|
6369627e99 | ||
|
|
4f1934345a | ||
|
|
d2dc34e9cf | ||
|
|
12ff37d8e3 | ||
|
|
55d9346713 | ||
|
|
28b9d9c9c2 | ||
|
|
1fa6b44c47 | ||
|
|
5eacd52bbf | ||
|
|
408686b9b8 | ||
|
|
4844b7109e | ||
|
|
78e8451324 |
@@ -1,13 +1,34 @@
|
||||
# ComfyUI-sonar
|
||||
|
||||
A janky implementation of Sonar sampling (momentum-based sampling) for [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It may or may not be working _properly_ but it does produce pretty reasonable results. I am using it personally. At this point, I would say it's suitable for general use with the caveat that it's very likely stuff like implementation and inputs to nodes will still be changing fairly frequently. In other words, don't depend on reproduceable generations with this unless you're willing to keep track of the git revision something was generated with.
|
||||
A janky implementation of Sonar sampling (momentum-based sampling) for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) as well as an assortment of advanced noise tools.
|
||||
|
||||
Currently supports Euler, Euler Ancestral, and DPM++ SDE sampling.
|
||||
Disclaimer: It's very likely stuff like implementation and inputs to nodes will still be changing fairly frequently. In other words, don't depend on reproduceable generations with this unless you're willing to keep track of the git revision something was generated with.
|
||||
|
||||
Momentum based sampling currently supports Euler, Euler Ancestral, and DPM++ SDE sampling.
|
||||
|
||||
See the [ChangeLog](changelog.md) for recent user-visible changes.
|
||||
|
||||
## Description
|
||||
|
||||
This started out as an implementation of Sonar sampling and has evolved into something more like a noise toybox.
|
||||
|
||||
Please note that while a lot of the nodes in here have a `Sonar` prefix, that doesn't indicate a relation with
|
||||
the original Sonar sampling implementation. Why is there random noise stuff in this repo? Mainly because it gets
|
||||
very awkward having node collections depending on other node collections.
|
||||
|
||||
Keep reading below this section for information on Sonar sampling and associated nodes.
|
||||
|
||||
For information on the advanced noise tools which include many different noise types, nodes to schedule,
|
||||
composite and otherwise manipulate noise see:
|
||||
|
||||
* [Base Noise Types](docs/base_noise_types.md) - examples and descriptions of the base noise types.
|
||||
* [Advanced Power Noise](docs/advanced_power_noise.md) - examples and descriptions of the advanced power noise node.
|
||||
* [Advanced Noise Nodes](docs/advanced_noise_nodes.md) - examples and descriptions of advanced noise nodes (schedule, composite, etc).
|
||||
* [FreeU Extreme](docs/frux.md) - a build your own FreeU kit that allows advanced filtering, blending, scheduling of effects as well as targetting input and middle blocks.
|
||||
* [Wavelet CFG](docs/waveletcfg.md) - replacement CFG function that lets you set different CFG scales for high/low frequency parts of the latent. You can even do stuff like use a different CFG scale for horizontal versus vertical.
|
||||
|
||||
## Sonar Description
|
||||
|
||||
See https://github.com/Kahsolt/stable-diffusion-webui-sonar for a more in-depth explanation.
|
||||
|
||||
The `direction` parameter should (unless I screwed it up) work like setting sign to positive or negative: `1.0` is positive, `-1.0` is negative. You can also potentially play with fractional values.
|
||||
@@ -40,75 +61,51 @@ Attempt to add momentum and guidance to the DPM++ SDE sampler. It may not work c
|
||||
|
||||
You can optionally plug this into the Sonar sampler nodes. See the [Guidance](#guidance) section below.
|
||||
|
||||
### `NoisyLatentLike`
|
||||
|
||||
This node takes a reference latent and generates noise of the same shape. The one required input is `latent`.
|
||||
|
||||
You can connect a `SonarCustomNoise` or `SonerPowerNoise` node to the `custom_noise_opt` input: if that is attached, the built in noise type selector is ignored. The generated noise will be multiplied by the `multiplier` value. Note that you cannot use `brownian` noise whether specified directly or via custom noise nodes.
|
||||
|
||||
The node has two main modes: simply generate and scale the noise by the multiplier and return or add it to the input latent. In this mode, you don't connect anything to the `mul_by_sigmas_opt` or `model_opt` inputs and you would use other nodes to calculate the correct strength.
|
||||
|
||||
In the second mode you must connect sigmas (for example from a `BasicScheduler` node) to the `mul_by_sigmas_opt` input and connect a model to the `model_opt` input. It will calculate the strength based on the first item in the list of sigmas (so you could use something like a `SplitSigmas` node to slice them as needed). Note that `multiplier` still applies: the calculated strength will be scaled by it. This second mode is generally this is the most convenient way to use the node since the two main uses cases are: making a latent with initial noise or adding noise to a latent (for img2img type stuff).
|
||||
|
||||
If you want to create noise for initial sampling, connect model and sigmas to the node, connect an empty latent (or one of the appropriate size) to it and that is basically all you need to do (aside from configuring the noise types). For img2img (upscaling, etc), either slice the sigmas at the appropriate or set a denoise in something like the `BasicScheduler` node. **Note**: You also need to turn on the `add_to_latent` toggle. Turning this on doesn't matter for initial noise since an empty latent is all zeros.
|
||||
|
||||
|
||||
### `SamplerConfigOverride`
|
||||
|
||||
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.
|
||||
|
||||
### `SonarCustomNoise`
|
||||
|
||||
See the [Noise](#noise) section below for information on noise types.
|
||||
|
||||
### `SonarPowerNoise`
|
||||
|
||||
This node generates [fractional Brownian motion (fBm) noise](https://en.wikipedia.org/wiki/Fractional_Brownian_motion#Frequency-domain_interpretation). It offers versatility in producing various types of noise including gaussian, pink, 2D brownian noise, and all intermediates.
|
||||
|
||||
By default, the node generates normal gaussian noise.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Expand detailed explanation</summary>
|
||||
|
||||
|
||||
Here's an overview of its parameters:
|
||||
|
||||
- `factor` and `rescale` operate similarly to `SonarCustomNoise`, enabling the addition of multiple sources of noises.
|
||||
- `time_brownian` introduces correlation across sampler timesteps for SDE solvers.
|
||||
- `alpha` is the main parameter. `alpha > 0` amplifies low frequencies; `alpha = 1` yields pink noise, and `alpha = 2` produces brownian noise. Conversely, for `alpha < 0`, it amplifies high frequencies.
|
||||
- `min_freq` and `max_freq` determine the range of frequencies allowed through. Setting `max_freq = `$\sqrt{1/2} \simeq 0.7071$ enables the passage of the highest frequencies. In cases where `alpha < 0`, setting `max_freq = 0.5` is advisable to diminish the power of diagonally oriented frequencies.
|
||||
- `stretch`, `rotate`, and `pnorm` alter the filter's shape by stretching, rotating, or cushioning the band-pass region.
|
||||
- Lowering `mix` moderates the filter's effect by blending back unfiltered gaussian noise from the same sample.
|
||||
- `common_mode` is an attempt to desaturate the latent by injecting the average across channels into every latent channel. However, this may result in a specific color due to the encoding of the unit vector by the latent space. Note that this is done _after_ the `mix`ing of unfiltered gaussian noise.
|
||||
- Enabling `preview` provides a visual representation of the filter. `no_mix` sets `mix = 1` for the preview. The preview includes, from left to right:
|
||||
- Fourier domain visualization: Low frequencies at the center, with black indicating filtered-out frequencies.
|
||||
- Spatial visualization of the 2D kernel: The filtering can be interpreted as convolution with the displayed kernel.
|
||||
- Sample: Gaussian sample with shaped frequency spectrum. A single latent channel will look like this.
|
||||
|
||||
**Frequency-domain Interpretation**: The Fourier transform decomposes a 2D latent into sinusoids covering all spatial orientations and frequencies. For an independent and identically distributed gaussian sample, energy is evenly distributed across all frequencies and orientations. Scaling the power spectrum by $1 / f^\alpha$, where $\alpha>0$, boosts low frequencies, introducing spatial correlations.
|
||||
|
||||
**Spatial Domain Interpretation**: A gaussian latent sample comprises independently sampled pixels, exhibiting no spatial correlations. Conversely, a requirement that each pixel value differs from its neighbors by a $\epsilon \sim \mathcal{N}(0, 1)$ results in 2D brownian noise ($\alpha=2$).
|
||||
|
||||
**Seed Considerations**: While the node defaults to outputting gaussian noise, a given seed produce a different sample than the one produced by other gaussian noise sources. This stems from sampling the noise directly in the frequency domain to avoid the cost of a FFT. When `time_brownian = true`, noise sampling occurs in the spatial domain, ensuring that default parameters yield output equivalent to `SonarCustomNoise` set to `brownian`.
|
||||
|
||||
</details>
|
||||
|
||||
From a usage perspective, using positive alpha will tend to create a colorful effect, using negative alpha will create line/streak like artifacts sort of like an oil painting canvas. Start with small values at first (`-0.1`, `0.1`) and adjust as necessary. `time_brownian` makes the effect of power noise (and alpha) stronger - also note that it can only be used when sampling and not for `NoisyLatentLike`. Setting `common_mode` also generally seems to intensify these effects. Different types of models (normal EPS models, v-prediction models, SDXL) generally react differently to these exotic noise types so my advice is to experiment! Lowering `mix` uses normal gaussian noise for part of the generated noise. For example, `mix=1.0` means 100% power noise, `mix=0.5` means 50/50 power noise and normal gaussian noise. This also is about the same as setting factor to `0.5` and plugging in a `SonarCustomNoise` node with factor at `0.5` also and the type set to `guassian`.
|
||||
|
||||
Noise from the `SonarCustomNoise` node and `SonarPowerNoise` can be freely mixed.
|
||||
|
||||
### `KRestartSamplerCustomNoise`
|
||||
|
||||
If you have a recent enough version of [ComfyUI_restart_sampling](https://github.com/ssitu/ComfyUI_restart_sampling/)
|
||||
installed, you'll also get the `KRestartSamplerCustomNoise` node which is exactly the same as `KRestartSamplerCustom`
|
||||
except for adding an optional custom noise input.
|
||||
See the restart sampling repo for more information: https://github.com/ssitu/ComfyUI_restart_sampling
|
||||
|
||||
## Sonar Sampler Parameters
|
||||
|
||||
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.
|
||||
@@ -121,27 +118,26 @@ Without guidance it should basically work the same as the ancestral Euler versio
|
||||
|
||||
## Noise
|
||||
|
||||
I basically just copied a bunch of noise functions without really knowing what they do. The main thing I can say is they produce a semi-reasonable result and it's different from the other noise samplers. See [Credits](#credits) below.
|
||||
See [Base Noise Types](docs/base_noise_types.md) for examples.
|
||||
|
||||
1. `gaussian`: This is the default noise type.
|
||||
2. `uniform`: Might enhance background details?
|
||||
3. `brownian`: This is the noise type SDE samplers use.
|
||||
4. `perlin`
|
||||
5. `studentt`: There's a comment that says it may enhance subject details. It seemed to produce a fairly dark result.
|
||||
6. `pink`
|
||||
7. `highres_pyramid`: Not extensively tested, but it is slower than the other noise types. I would guess it does something like enhance details.
|
||||
8. `laplacian`
|
||||
9. `power`
|
||||
10. `rainbow_mild` and `rainbow_intense`: A combination of green (-ish, the implementation may be broken) noise plus perlin noise. Very colorful results.
|
||||
11. `green_test`: Even more rainbow-y than the rainbow noise types. It _probably_ isn't working correctly, but the results are very interesting and colorful. Depending on the model, it may not work well for an initial generation but may be worth trying with img2img type workflows.
|
||||
|
||||
You can scroll down to the the [Examples](#examples) section near the bottom to see some example generations with different noise types.
|
||||
|
||||
The sampler and `NoisyLatentLike` nodes now take an optional `SonarCustomNoise` input. You can chain `SonarCustomNoise` nodes together to mix different types of noise, similar to how some of the built in ones. It shouldn't matter what order the noise types are chained. If `rescale` is set to `0.0` no rescaling will occur. `factor` is the proportion of that type of noise you want. If you want to use `rescale` it should be on the node that you are plugging into a sampler. Just for example if you had two `SonarCustomNoise` nodes both with `factor=0.7` and `rescale=1.0` on the last one, it would be effectively the same as if you'd used `factor=0.5` and `rescale=1.0` doesn't actually do anything. You can also rescale to values above `1.0` — the result is more noise, similar to increasing `s_noise` above `1.0` on a sampler. The simple explanation is `rescale` means you don't have to make sure the `factor`s add up to the scale you want (which normally would be `1.0`).
|
||||
The sampler and `NoisyLatentLike` nodes now take an optional `SonarCustomNoise` input.
|
||||
|
||||
**Note**: If you connect the optional `SonarCustomNoise` node to a Sonar sampler, the `NoisyLatentLike` node or the `SamplerConfigOverride` node, it will override the noise type selected in the node.
|
||||
|
||||
## Integrations
|
||||
|
||||
You'll get some bonus features if you have some other node collections installed:
|
||||
|
||||
### `KRestartSamplerCustomNoise`
|
||||
|
||||
If you have a recent enough version of [ComfyUI_restart_sampling](https://github.com/ssitu/ComfyUI_restart_sampling/)
|
||||
installed, you'll also get the `KRestartSamplerCustomNoise` node which is exactly the same as `KRestartSamplerCustom`
|
||||
except for adding an optional custom noise input.
|
||||
See the restart sampling repo for more information: https://github.com/ssitu/ComfyUI_restart_sampling
|
||||
|
||||
### `RestartSamplerCustomNoise`
|
||||
|
||||
As above, except this is the custom sampler version.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -153,11 +149,20 @@ 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.
|
||||
* 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
|
||||
* Pattern break algorithm adapted from https://github.com/Extraltodeus/noise_latent_perlinpinpin
|
||||
|
||||
`SonarPowerNoise` contributed by [elias-gaeros](https://github.com/elias-gaeros/). Thanks!
|
||||
## Errata
|
||||
|
||||
## Examples
|
||||
* 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.
|
||||
|
||||
## Sonar Examples
|
||||
|
||||
Unfortunately, right now these examples are somewhat incomplete and out of date. I hope to update them when I get the time.
|
||||
|
||||
@@ -180,149 +185,10 @@ Using the `linear` guidance type and `guidance_factor=-0.015`. The reference ima
|
||||
|
||||
</details>
|
||||
|
||||
### Noise Types
|
||||
|
||||
### Noise Types (img2img)
|
||||
See:
|
||||
|
||||
These were generated with `s_noise=1.05` to make the noise effect more pronounced, 30 steps at `0.66` denoise, sonar settings increased slightly to enhance the effect (`momentum=0.9, momentum_hist=0.85, direction=1.0, momentum_init=ZERO`). It is probably easier to compare using these as the image _mostly_ stays the same as the sonar sampler settings change.
|
||||
|
||||
<details>
|
||||
<summary>Expand renoise example images</summary>
|
||||
|
||||
#### Base
|
||||
|
||||
Base image - no Sonar Sampler steps.
|
||||
|
||||

|
||||
|
||||
#### Euler A
|
||||
|
||||
Normal (non-sonar) Eular A. Not really a comparison with noise (think it would use gaussian) but with the difference in effect from momentum.
|
||||
|
||||

|
||||
|
||||
|
||||
#### Gaussian
|
||||
|
||||

|
||||
|
||||
#### Brownian
|
||||
|
||||

|
||||
|
||||
#### Perlin
|
||||
|
||||

|
||||
|
||||
#### Uniform
|
||||
|
||||

|
||||
|
||||
#### Highres Pyramid
|
||||
|
||||

|
||||
|
||||
#### Pink
|
||||
|
||||

|
||||
|
||||
#### StudentT
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
|
||||
#### StudentT_test
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### Laplacian
|
||||
|
||||

|
||||
|
||||
#### Power
|
||||
|
||||

|
||||
|
||||
#### Rainbow Mild
|
||||
|
||||

|
||||
|
||||
#### Rainbow Intense
|
||||
|
||||

|
||||
|
||||
#### Green_test
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
### Noise Types (Initial Generations)
|
||||
|
||||
These were generated with `s_noise=1.1` to make the noise effect more pronounced, default sonar settings (`momentum=0.95, momentum_hist=0.75, direction=1.0, momentum_init=ZERO`). It may be harder to see the noise effects since the composition can change a lot in initial generations.
|
||||
|
||||
<details>
|
||||
<summary>Expand initial generation example images</summary>
|
||||
|
||||
#### Gaussian
|
||||
|
||||

|
||||
|
||||
#### Brownian
|
||||
|
||||

|
||||
|
||||
#### Perlin
|
||||
|
||||

|
||||
|
||||
#### Uniform
|
||||
|
||||

|
||||
|
||||
#### Highres Pyramid
|
||||
|
||||

|
||||
|
||||
#### Pink
|
||||
|
||||

|
||||
|
||||
#### StudentT
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### StudentT_test
|
||||
|
||||
**outdated**
|
||||
|
||||

|
||||
|
||||
#### Laplacian
|
||||
|
||||

|
||||
|
||||
#### Power
|
||||
|
||||

|
||||
|
||||
#### Rainbow Mild
|
||||
|
||||

|
||||
|
||||
#### Rainbow Intense
|
||||
|
||||

|
||||
|
||||
#### Green_test
|
||||
|
||||
This might seem too crazy for actual use, but you can actually get decent results using the DPMPP Sonar sampler and a relatively high step count.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
* [Base Noise Types](docs/base_noise_types.md)
|
||||
* [Advanced Power Noise](docs/advanced_power_noise.md)
|
||||
* [Advanced Noise Nodes](docs/advanced_noise_nodes.md)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
from .py import nodes, powernoise, sonar
|
||||
import sys
|
||||
|
||||
from . import py # noqa: F401
|
||||
from .py import nodes, sonar
|
||||
|
||||
|
||||
def blep_init():
|
||||
bi = sys.modules.get("_blepping_integrations", {})
|
||||
if "sonar" in bi:
|
||||
return
|
||||
bi["sonar"] = sys.modules[__name__]
|
||||
sys.modules["_blepping_integrations"] = bi
|
||||
|
||||
|
||||
sonar.add_samplers()
|
||||
blep_init()
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SamplerSonarEuler": nodes.SamplerNodeSonarEuler,
|
||||
"SamplerSonarEulerA": nodes.SamplerNodeSonarEulerAncestral,
|
||||
"SamplerSonarDPMPPSDE": nodes.SamplerNodeSonarDPMPPSDE,
|
||||
"SamplerConfigOverride": nodes.SamplerNodeConfigOverride,
|
||||
"NoisyLatentLike": nodes.NoisyLatentLikeNode,
|
||||
"SonarCustomNoise": nodes.SonarCustomNoiseNode,
|
||||
"SonarPowerNoise": powernoise.SonarPowerNoiseNode,
|
||||
"SonarGuidanceConfig": nodes.GuidanceConfigNode,
|
||||
}
|
||||
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", {})
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
if hasattr(nodes, "KRestartSamplerCustomNoise"):
|
||||
NODE_CLASS_MAPPINGS["KRestartSamplerCustomNoise"] = nodes.KRestartSamplerCustomNoise
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 435 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 434 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 479 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 451 KiB |
@@ -2,6 +2,135 @@
|
||||
|
||||
Note, only relatively significant changes to user-visible functionality will be included here. Most recent changes at the top.
|
||||
|
||||
## 20250808
|
||||
|
||||
Aside from the `sigmoid` quantile mode change and `SonarShuffledNoise`, these changes should not break workflows. Let me know if you experience anything unusual.
|
||||
|
||||
* The `sigmoid` quantile norm mode was renamed to `sigmoid_keepsign` since that's what it was doing. There is a replacement `sigmoid` quantile norm mode that doesn't care about sign.
|
||||
* Quantile normalization can now take a negative quantile to consider values closest to zero the "extremes". Note: Experimental feature that may not be implemented correctly/subject to change.
|
||||
* `SonarShuffledNoise` node reworked. Unfortunately, this will break workflows. If anyone has a burning need for the old version, let me know and I can bring it back as a separate node. The new approach should be better in general though.
|
||||
* Fixed momentum sampler init parameter passing.
|
||||
* Added more Voronoi noise octave modes.
|
||||
* Added replace_2pt/3pt (and variants) quantile norm result modes that use multiple replacement values.
|
||||
|
||||
## 20250805
|
||||
|
||||
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.
|
||||
* Added a `SonarCustomNoiseParameters` node that lets you set some parameters as well as override seed/device/dtype.
|
||||
* Added `replace`, `replace_keepsign` and `replace_avoidsign` quantile norm modes.
|
||||
* `SonarBlendedNoise` now has a `custom_noise_mask` input. When connected, it will generate noise with that, put it on a 0-1 scale and use that to control the blend.
|
||||
* Added a `SonarAdvancedVoronoiNoise` node.
|
||||
|
||||
## 20250705
|
||||
|
||||
This is a large set of changes. Please let me know anything doesn't seem to be working properly.
|
||||
|
||||
* Reorganized the node structure. This is an internal change and shouldn't affect users but please let me know if you notice anything weird.
|
||||
* Added a `SonarLatentOperationAdvanced` node which allows more control over when individual latent operations are active and their effects get blended.
|
||||
* Added a `SonarSplitNoiseChain` node. Can be useful if you want to have an item in the chain be a blended.
|
||||
* Added a `SonarLatentOperationNoise` node that can be used to inject noise. You can also use the guided noise node to turn a reference into "noise".
|
||||
* Expanded the functionality of the `SonarWaveletFilteredNoise` node. You can now attach two custom noise inputs to use for the high/low frequency parts of the wavelet as well as blend the wavelets.
|
||||
* Added a `SonarNormalizeNoiseToScale` node that lets you normalize noise to specific value ranges.
|
||||
* Added a `SonarPerDimNoise` node that lets you do stuff like call a noise sampler once per batch item (can be useful for 3D Perlin noise).
|
||||
* Fixed an issue where the normalization parameter wasn't respected. This may change seeds.
|
||||
* Added a `SonarLatentOperationFilteredNoise` node that allows you to run noise through a `LATENT_OPERATION`.
|
||||
* Added a `SonarLatentOperationSetSeed` node that can be used to set the seed (mainly useful for running latent operations that add noise outside of sampling).
|
||||
* Added a `SonarScatternetFilteredNoise` node that uses a scatternet to filter noise. Similar to wavelet filtering. Note: Very experimental, way not work properly.
|
||||
* Fixed an issue with pattern break noise, this may change seeds for workflows using that noise type.
|
||||
|
||||
## 20250627
|
||||
|
||||
* Added `SonarRippleFilteredNoise` node.
|
||||
* Added `SonarApplyLatentOperationCFG` node, similar to the built-in `ApplyLatentOperationCFG` node with scheduling and a lot of different application modes.
|
||||
* Added a `SonarLatentOperationQuantileFilter` node that can be used to apply the quantile normalization functioen to the latent during sampling.
|
||||
* A bunch more quantile normalization modes.
|
||||
* Fixed broken quantile normalization dimension handling. Unfortunately this will likely change seeds.
|
||||
|
||||
## 20250612
|
||||
|
||||
* Reimplemented Collatz noise with many new features. Unfortunately this breaks existing workflows. If anyone misses the old version, let me know and I can add it back in (might do that anyway).
|
||||
* Added actual wavelet noise based on https://en.wikipedia.org/wiki/Wavelet_noise .
|
||||
* Added `reverse_zero`, `scale_down`, `tanh`, `tanh_outliers`, `sigmoid` and `sigmoid_outliers` quantile normalization limit modes.
|
||||
|
||||
## 20250602
|
||||
|
||||
* Fixed broken calculation for Collatz noise.
|
||||
* Added `SonarPatternBreakNoise` node that allows breaking patterns in the noise.
|
||||
* Added `SonarShuffledNoise` node that allows shuffling elements along user-specified dimensions.
|
||||
* Added a strategy option to the `SonarQuantileFilteredNoise` node.
|
||||
* Added variants to Collatz noise. Variant one is maybe similar to the original iteration.
|
||||
* Added `SonarNoiseImage` node that allows generating noisy images or adding noise to existing images.
|
||||
|
||||
## 20250528
|
||||
|
||||
* Added `override_sigma`, `override_sigma_next`, `override_sigma_min` and `override_sigma_max` options that can be set in the `SonarCustomNoiseAdv` node YAML options. This enables using noise generators that require a sigma in stuff like initial noise (for example, Brownian). You will need to manually find and set the correct values yourself.
|
||||
* Added Collatz noise based on the Collatz conjecture. Very experimental, very slow, likely to change and quite possibly just plain bad. But you can try it.
|
||||
|
||||
## 20250505
|
||||
|
||||
* Added `SonarQuantileFilteredNoise` node.
|
||||
* Better compatibility with older Python versions.
|
||||
|
||||
## 20250227
|
||||
|
||||
* Add 5D latent (video models) support for most custom noise types.
|
||||
|
||||
## 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.
|
||||
|
||||
* Add `SonarScheduledNoise`, `SonarCompositeNoise`, `SonarGuidedNoise`, `SonarRandomNoise` nodes. See [Advanced Noise Nodes](docs/advanced_noise_nodes.md).
|
||||
* Add `SonarPowerFilterNoise`, `SonarPowerFilter`, `SonarPreviewFilter` nodes. See [Advanced Power Noise](docs/advanced_power_noise.md).
|
||||
* Add `FreeUExtreme`, `FreeUExtremeConfig` nodes. See [FreeU Extreme](docs/frux.md).
|
||||
* Replace `pyramid` noise type with a (hopefully) more correct implementation. You can use `pyramid_old` for the previous behavior.
|
||||
* Add more noise types and variations.
|
||||
* The `NoisyLatentLike` node now allows using brownian noise if you connect a model and sigmas.
|
||||
|
||||
## 20240506
|
||||
|
||||
* Add `SonarModulatedNoise` and `SonarRepeatedNoise` nodes.
|
||||
|
||||
## 20240327
|
||||
|
||||
* Fixed issue when using Sonar samplers in normal sampling nodes/via stuff like `KSamplerSelect`.
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
# Advanced Nodes
|
||||
|
||||
## Normalization
|
||||
|
||||
Normalization essentially rebalances the noise (or mixture of noise) to 1.0 strength and then scales based
|
||||
on the factor of the node. Most nodes will allow you to set three values:
|
||||
|
||||
* `default`: By default, noise will be normalized only just before it's used. So you could consider this setting to be false except for where it is connected to an actual noise consumer (i.e. a `SamplerConfigOverride` node).
|
||||
* `forced`: Will always normalize.
|
||||
* `disabled`: Will never normalize.
|
||||
|
||||
## `SONAR_CUSTOM_NOISE`
|
||||
|
||||
This node output type actually constitutes a chain of noise items. For most nodes, when you use it as input,
|
||||
they will add an item to the chain. There are some exceptions that treat the `SONAR_CUSTOM_NOISE` input as a list:
|
||||
|
||||
* `SonarRepeatedNoise`
|
||||
* `SonarRandomNoise`
|
||||
|
||||
There are also some exceptions that will consume the list rather than adding an item to it:
|
||||
|
||||
* `SonarModulatedNoise`
|
||||
* `SonarCompositeNoise`
|
||||
* `SonarScheduledNoise`
|
||||
* `SonarGuidedNoise`
|
||||
|
||||
The distinction is mainly only important when setting `rescale`. Visual example:
|
||||
|
||||

|
||||
|
||||
It may be counter intuitive that there are actually two separate chains here.
|
||||
|
||||
## Examples
|
||||
|
||||
Note on the examples included for some of these nodes:
|
||||
|
||||
The example images included for some of these nodes all have metadata and can be loaded in ComfyUI.
|
||||
Generated using `dpmpp_2s_ancestral`, Karras scheduler and starting out with gaussian noise then switching
|
||||
to the custom noise type at the 35% mark.
|
||||
|
||||
***
|
||||
|
||||
### `SonarCustomNoise`
|
||||
|
||||
You can chain `SonarCustomNoise` nodes together to mix different types of noise. The order of `SonarCustomNoise` nodes is not important.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `factor` controls the strength of the noise.
|
||||
- `rescale` controls rebalancing `factor` for nodes in the chain. When `rescale` is set to `0.0`, no rebalancing will occur. Otherwise the current node as well as the nodes connect to it will have their `factor` adjusted to add up to the rescale value. For example, if you have three nodes with `factor` 1.0 and the last with `rescale` 1.0, then the `factor` value will be adjusted to `1/3 = 0.3333...`. *Note*: Rescaling uses the `factor` absolute value.
|
||||
- `noise_type` allows you to select the built-in noise type.
|
||||
|
||||
***
|
||||
|
||||
### `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`.
|
||||
|
||||
You can connect a `SonarCustomNoise` or `SonerPowerNoise` node to the `custom_noise_opt` input: if that is attached, the built in noise type selector is ignored. The generated noise will be multiplied by the `multiplier` value. **Note**: If you select `brownian` noise (either through the dropdown or by connecting custom noise nodes) you must connect a model and sigmas.
|
||||
|
||||
The node has two main modes: simply generate and scale the noise by the multiplier and return or add it to the input latent. In this mode, you don't connect anything to the `mul_by_sigmas_opt` or `model_opt` inputs and you would use other nodes to calculate the correct strength.
|
||||
|
||||
In the second mode you must connect sigmas (for example from a `BasicScheduler` node) to the `mul_by_sigmas_opt` input and connect a model to the `model_opt` input. It will calculate the strength based on the first item in the list of sigmas (so you could use something like a `SplitSigmas` node to slice them as needed). Note that `multiplier` still applies: the calculated strength will be scaled by it. This second mode is generally this is the most convenient way to use the node since the two main uses cases are: making a latent with initial noise or adding noise to a latent (for img2img type stuff).
|
||||
|
||||
If you want to create noise for initial sampling, connect model and sigmas to the node, connect an empty latent (or one of the appropriate size) to it and that is basically all you need to do (aside from configuring the noise types). For img2img (upscaling, etc), either slice the sigmas at the appropriate or set a denoise in something like the `BasicScheduler` node. *Note*: For img2img, you also need to turn on the `add_to_latent` toggle. Turning this on doesn't matter for initial noise since an empty latent is all zeros.
|
||||
|
||||
**Note**: This node does not currently respect the latent noise mask.
|
||||
|
||||
***
|
||||
|
||||
### `SamplerConfigOverride`
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## `SonarQuantileFilteredNoise`
|
||||
|
||||
Allows quantile normalizing of arbitrary noise generators, works like the `SonarAdvancedDistroNoise` node (see below).
|
||||
|
||||
***
|
||||
|
||||
### `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`
|
||||
|
||||
Experimental noise modulation based on code stolen from
|
||||
[ComfyUI-Extra-Samplers](https://github.com/Clybius/ComfyUI-Extra-Samplers). `intensity` and `frequency` modulation
|
||||
types _probably_ do not work correctly for normal sampling — I expect the modulation will be based on the tensor
|
||||
where the noise sampler was created rather than each step. However it may be useful for something like restart sampling
|
||||
noise (see `KRestartSamplerCustomNoise` below). You can also pass it a reference latent to modulate based on
|
||||
instead (only used for `intensity` and `frequency` modulation types).
|
||||
|
||||
*Note*: It's likely this node will be changed in the future.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐ Expand Example Images ⭐</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
These examples all use the `spectral_signum` modulation type as it doesn't depend on a reference.
|
||||
|
||||
#### Positive Strength
|
||||
|
||||
Dims 3:
|
||||
|
||||

|
||||
|
||||
Dims 3 (with studentt noise):
|
||||
|
||||

|
||||
|
||||
Dims 2:
|
||||
|
||||

|
||||
|
||||
Dims 1:
|
||||
|
||||

|
||||
|
||||
#### Negative Strength
|
||||
|
||||
Dims 3:
|
||||
|
||||

|
||||
|
||||
Dims 3 (with studentt noise):
|
||||
|
||||

|
||||
|
||||
Dims 2:
|
||||
|
||||

|
||||
|
||||
Dims 1:
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
### `SonarRepeatedNoise`
|
||||
|
||||
Experimental node to cache noise sampler results. Why would you want to do this? Some noise samplers are
|
||||
relatively slow (`pyramid` for example) or it may be slow to generate noise if you are mixing many types
|
||||
of noise. When `permute` is enabled, a random effect like flipping the noise or rolling it in some dimension
|
||||
will be chosen each time the noise sampler is called. I recommend leaving `permute` on. Note that repeated
|
||||
noise (especially with `permute` disabled) can be stronger than normal noise, so you may need to rescale to
|
||||
a value lower than `1.0` or decrease `s_noise` for the sampler. You may also set the maximum number of
|
||||
times noise is reused by setting `max_recycle`.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐ Expand Example Images ⭐</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
Repeated noise is very strong (especially when permute is disabled). You generally won't get good
|
||||
results using 1.0 strength:
|
||||
|
||||

|
||||
|
||||
I recommend considerably decreasing the strength (example here is using 0.75 which is still a bit too much):
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
### `SonarCompositeNoise`
|
||||
|
||||
Allows compositing noise types based on a mask. Noise is mixed based on the strength of the mask at a location.
|
||||
For example, where the mask is 1.0 (max strength) you will get 100% `noise_src` and 0% `noise_dst`. Where the
|
||||
mask is 0.75 you will get 75% `noise_src` and 25% `noise_dst`.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐ Expand Example Images ⭐</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
These examples use a base noise type of gaussian and composite in an area with a different type
|
||||
near middle. The custom noise is also set to a higher strength than normal to highlight the effect.
|
||||
|
||||
**No Composite (for comparison)**
|
||||
|
||||

|
||||
|
||||
**Brownian**
|
||||
|
||||

|
||||
|
||||
**Pyramid**
|
||||
|
||||

|
||||
|
||||
**Pyramid negative factor**
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
### `SonarScheduledNoise`
|
||||
|
||||
Allows switching between noise types based on percentage of sampling (note: not percentage of steps).
|
||||
|
||||
**Note**: You don't have to connect the fallback noise type but the default is to generate _no_ noise, which
|
||||
is most likely not what you want. The majority of the time, it is recommend to connect something like gaussian
|
||||
noise at 1.0 strength.
|
||||
|
||||
All the example images here use the `SonarScheduledNoise` node so you can pick any one of them to see it
|
||||
in action!
|
||||
|
||||
***
|
||||
|
||||
### `SonarGuidedNoise`
|
||||
|
||||
Works similarly as described in the [Guidance](../README.md#guidance) section of the main README, however the guidance is applied
|
||||
to the raw noise. You can use `SonarScheduledNoise` to only apply guidance at certain times. Using `euler`
|
||||
mode seems considerably stronger than `linear`. The default value should be reasonable for `euler`, may need to be
|
||||
increased somewhat for `linear`.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐ Expand Example Images ⭐</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
#### Pattern
|
||||
|
||||
These examples use a half circle pattern as the reference: 
|
||||
|
||||
|
||||
##### Euler
|
||||
|
||||
Positive strength:
|
||||
|
||||

|
||||
|
||||
Negative strength:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
##### Linear
|
||||
|
||||
Normal positive strength:
|
||||
|
||||

|
||||
|
||||
Normal negative strength:
|
||||
|
||||

|
||||
|
||||
Strong positive strength:
|
||||
|
||||

|
||||
|
||||
Strong negative strength:
|
||||
|
||||

|
||||
|
||||
|
||||
***
|
||||
|
||||
#### Gradient
|
||||
|
||||
These examples use a vertical gradient as the reference: 
|
||||
|
||||
That is dark to light. Light to dark examples just flip the gradient vertically.
|
||||
|
||||
##### Euler
|
||||
|
||||
Dark to light:
|
||||
|
||||

|
||||
|
||||
Light to dark:
|
||||
|
||||

|
||||
|
||||
Dark to light (negative strength):
|
||||
|
||||

|
||||
|
||||
Light to dark (negative strength):
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
##### Linear
|
||||
|
||||
Dark to light:
|
||||
|
||||

|
||||
|
||||
Light to dark:
|
||||
|
||||

|
||||
|
||||
Dark to light (negative strength):
|
||||
|
||||

|
||||
|
||||
Light to dark (negative strength):
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
### `SonarRandomNoise`
|
||||
|
||||
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%.
|
||||
|
||||
### `SonarAdvancedVoronoiNoise`
|
||||
|
||||
This node can create multi-octave 3D Voronoi noise (also known as Worley noise). See: https://en.wikipedia.org/wiki/Worley_noise
|
||||
|
||||
Similar to Pyramid and other weird noise types, this noise generally will require mixing with something more normal. The default settings actually just about work with SDXL.
|
||||
|
||||
The node has many options for calculating the distance between the feature points and for processing the output. The modes are entered as a string, you can hover over the widget to get a brief list of possible modes. Both distance and result modes support some common features:
|
||||
|
||||
* You can enter a comma-separated list of modes. This allows using a different mode per octave. If there are more octaves than you have modes defined, the mode will wrap. In other words, if you're generating three octaves and you define two modes then the third octave will use the first mode you defined.
|
||||
* You can enter a `+` (plus symbol) separated list of modes. The modes will be calculated and the result will be the average. Distance modes all have the common parameter `dscale` which defaults to 1 and can be overridden. Result modes use `rscale`. See below for a description on passing parameters.
|
||||
* It's possible to pass parameters to distance and result modes. Example with a result mode: `diff:idx1=0:idx2=1:rscale=0.5`
|
||||
|
||||
Some modes act as wrappers to other modes. All modes will just ignore parameters they don't understand. Each time a submode is called, one level of "_" at the beginning of parameter names is stripped off. It's not user-friendly but this does allow passing parameters to submodes. Since unknown parameters are ignored, you only need to bother with this if the mode that's calling the submode will use that parameter. Dumb example: `gradient_magnitude:name1=diff:name2=gradient_magnitude:_name1=f4:_name2=f4`. `gradient_magnitude` takes two submodes that it calls (specified with `name1` and `name2`). The top-level `gradient_magnitude` will consume the `name1` and `name2` parameters, strip one level of underscores off the parameter names and call the submodes.
|
||||
|
||||
#### Distance Modes
|
||||
|
||||
Modes listed with the defaults for parameters they support. These modes also support `dscale` which defaults to 1 and can be used to manually adjust the scale of the mode result.
|
||||
|
||||
* `euclidean`
|
||||
* `manhatten`
|
||||
* `chebyshev`
|
||||
* `minkowsi:p=3.0`
|
||||
* `quadratic`
|
||||
* `angle:idx=2` - idxs here range from 0 to 2.
|
||||
* `angle_tanh:idx=2` - Same as `angle` but scales the result with tanh.
|
||||
* `angle_sigmoid:idx=2` - Same as `angle` but scales the result with the sigmoid function.
|
||||
* `fuzz:name=euclidean:fuzz=0.25` - Acts as a wrapper for another mode (specified with `name`). Will perturb the result by `fuzz` percent of the absolute maximum value. Or more simply, randomizes values by +/- `fuzz` percentage so if you set `fuzz=1` you will essentially get pure noise.
|
||||
* `fractal_norm:scale=0.1:multiplier=10.0:mode=sin:name=euclidean` - This acts as a wrapper for another mode. `mode` may be one of `sin`, `cos`. It will adjust the input to the mode it wraps by `scale * sin(input * multiplier)` (assuming `mode=sin`).
|
||||
* `weight:h=1.0:w=1.0:z=0.25:name=euclidean` - This acts as a wrapper for another mode and allows you to scale height/width/z (depth) before calling it.
|
||||
|
||||
#### Result Modes
|
||||
|
||||
Modes listed with the defaults for parameters they support. These modes also support `rscale` which defaults to 1 and can be used to manually adjust the scale of the mode result.
|
||||
|
||||
* `f1` - distance to the closest cell.
|
||||
* `f2`, `f3`, `f4` - Same as `f1` but for the second closest, third closest, etc. Goes up to `f4`.
|
||||
* `f:idx=0` - Allows specifying `f` modes over `f4`. Note that `idx` is zero-based so 0 corresponds to `f1`.
|
||||
* `inv_f1` (through `inv_f4`). For `inv_f1`, the result is `1 / f1` (with a tiny value added to avoid divide by zero).
|
||||
* `inv_f:idx=0` - Like the `f` mode using the formula described above.
|
||||
* `diff:idx1=0:idx2=1` - Zero based indexes where 0 corresponds to `f1`, etc. For the default (`f1` and `f2`) the result is `f2 - f1`.
|
||||
* `diff2:idx1=0:idx2=1` - Similar to `diff` described above, however the result is divided by the two `f` results (plus a tiny addition to avoid divide by zero). For example with the defaults this works out to `(f2 - f1) / (f2 + f1)`.
|
||||
* `cellid` - Returns a discrete value for the area of each cell (diffusion models hate this). You will need to dilute the Voronoi noise a lot to actually use this. It could also possibly be used for masking.
|
||||
* `median_distance`
|
||||
* `fuzz:name=f1:fuzz=0.25` - Works the same as `fuzz` in distance modes. See the description there.
|
||||
* `fractal_norm:scale=0.1:multiplier=10.0:mode=sin:name=diff` - This acts as a wrapper for another mode. `mode` may be one of `sin`, `cos`. It will adjust the input to the mode it wraps by `scale * sin(input * multiplier)` (assuming `mode=sin`).
|
||||
* `ridge:name=diff:exp=-1.0` - Wraps another mode and may enhance cell borders (doesn't seem super useful).
|
||||
* `softmin:temperature=50.0` - Passes the result through softmin and can be used to smooth the output from distance modes like `angle` that may experience abrupt changes as you move through `z`. It also supports a `use_sorted` parameter that will apply this adjustment to the sorted values as well if it's present and set to anything. Higher temperatures will result in less of a smoothing effect.
|
||||
* `gradient_magnitude:name1=f4:name2=f4:padding_mode=replicate` - Wraps two other modes. Seems pretty nice for adding detail when using low numbers of feature points. `padding_mode` can be set to modes that PyTorch's `pad` function supports.
|
||||
|
||||
#### Depth
|
||||
|
||||
|
||||
The node has several `z`-related parameters. `z` here refers to the depth dimension and will (currently) only apply if you're using the same noise sampler more than once. So generally not for initial noise, unless you're doing something unusual.
|
||||
|
||||
When `z_max` is set to 0 the feature points will be reset each time the noise sampler is called. Otherwise it will track the current `z` (depth) and increment it by whatever you specify each time the noise sampler is called. `SonarPerDimNoise` can be useful here if you want depth over dimensions like batch or channels.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Advanced Power Noise
|
||||
|
||||
## `SonarPowerNoise`
|
||||
|
||||
This node generates [fractional Brownian motion (fBm) noise](https://en.wikipedia.org/wiki/Fractional_Brownian_motion#Frequency-domain_interpretation). It offers versatility in producing various types of noise including gaussian, pink, 2D brownian noise, and all intermediates.
|
||||
|
||||
By default, the node generates normal gaussian noise.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>⭐⭐ Expand advanced parameter explanation ⭐⭐</summary>
|
||||
|
||||
Here's an overview of its parameters:
|
||||
|
||||
- `factor` and `rescale` operate similarly to `SonarCustomNoise`, enabling the addition of multiple sources of noises.
|
||||
- `time_brownian` introduces correlation across sampler timesteps for SDE solvers.
|
||||
- `alpha` is the main parameter. `alpha > 0` amplifies low frequencies; `alpha = 1` yields pink noise, and `alpha = 2` produces brownian noise. Conversely, for `alpha < 0`, it amplifies high frequencies.
|
||||
- `min_freq` and `max_freq` determine the range of frequencies allowed through. Setting `max_freq = `$\sqrt{1/2} \simeq 0.7071$ enables the passage of the highest frequencies. In cases where `alpha < 0`, setting `max_freq = 0.5` is advisable to diminish the power of diagonally oriented frequencies.
|
||||
- `stretch`, `rotate`, and `pnorm` alter the filter's shape by stretching, rotating, or cushioning the band-pass region.
|
||||
- Lowering `mix` moderates the filter's effect by blending back unfiltered gaussian noise from the same sample.
|
||||
- `common_mode` is an attempt to desaturate the latent by injecting the average across channels into every latent channel. **FIXME: it's not all channels anymore** However, this may result in a specific color due to the encoding of the unit vector by the latent space. Note that this is done _after_ the `mix`ing of unfiltered gaussian noise.
|
||||
- `channel_correlation` **FIXME**: TBD
|
||||
- Enabling `preview` provides a visual representation of the filter. `no_mix` sets `mix = 1` for the preview. The preview includes, from left to right:
|
||||
- Fourier domain visualization: Low frequencies at the center, with black indicating filtered-out frequencies.
|
||||
- Spatial visualization of the 2D kernel: The filtering can be interpreted as convolution with the displayed kernel.
|
||||
- Sample: Gaussian sample with shaped frequency spectrum. A single latent channel will look like this.
|
||||
|
||||
**Frequency-domain Interpretation**: The Fourier transform decomposes a 2D latent into sinusoids covering all spatial orientations and frequencies. For an independent and identically distributed gaussian sample, energy is evenly distributed across all frequencies and orientations. Scaling the power spectrum by $1 / f^\alpha$, where $\alpha>0$, boosts low frequencies, introducing spatial correlations.
|
||||
|
||||
**Spatial Domain Interpretation**: A gaussian latent sample comprises independently sampled pixels, exhibiting no spatial correlations. Conversely, a requirement that each pixel value differs from its neighbors by a $\epsilon \sim \mathcal{N}(0, 1)$ results in 2D brownian noise ($\alpha=2$).
|
||||
|
||||
**Seed Considerations**: While the node defaults to outputting gaussian noise, a given seed produce a different sample than the one produced by other gaussian noise sources. This stems from sampling the noise directly in the frequency domain to avoid the cost of a FFT. When `time_brownian = true`, noise sampling occurs in the spatial domain, ensuring that default parameters yield output equivalent to `SonarCustomNoise` set to `brownian`.
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
From a usage perspective, using positive alpha will tend to create a colorful effect, using negative alpha will create line/streak like artifacts sort of like an oil painting canvas. Start with small values at first (`-0.1`, `0.1`) and adjust as necessary. `time_brownian` makes the effect of power noise (and alpha) stronger - also note that it can only be used when sampling and not for `NoisyLatentLike`. Setting `common_mode` also generally seems to intensify these effects. Different types of models (normal EPS models, v-prediction models, SDXL) generally react differently to these exotic noise types so my advice is to experiment! Lowering `mix` uses normal gaussian noise for part of the generated noise. For example, `mix=1.0` means 100% power noise, `mix=0.5` means 50/50 power noise and normal gaussian noise. This also is about the same as setting factor to `0.5` and plugging in a `SonarCustomNoise` node with factor at `0.5` also and the type set to `guassian`.
|
||||
|
||||
Noise from the `SonarCustomNoise` node and `SonarPowerNoise` can be freely mixed.
|
||||
|
||||
## `SonarPowerFilterNoise`
|
||||
|
||||
This node lets you connect a filter (see below) and a custom noise chain. It basically lets you run any type of noise through the power noise filter.
|
||||
|
||||
New parameters:
|
||||
|
||||
* `filter_norm_factor` controls how much normalization is applied to the filter. `1.0` means fully normalized, `0.0` means no normalization.
|
||||
* You may set the preview type to `custom` to see a color preview of the filtered noise. Note that this uses whatever preview type you have configured in ComfyUI (for example, TAESD). The preview is based on SD 1.5's interpretation of the noise.
|
||||
|
||||
## `SonarPowerFilter`
|
||||
|
||||
Most of the parameters here are similar to the `SonarPowerNoise` node. New parameters:
|
||||
|
||||
* `scale` allows you to scale the filter (you could consider this to be set to `1.0` in the `SonarPowerNoise` node).
|
||||
* `compose_mode` allows you to compose multiple filters. Note that composition occurs like `current_filter OPERATION connected_filter`. So if you set `compose_mode` to `sub`, you will get `current_filter - connected_filter`. Scaling occurs before composition.
|
||||
|
||||
## `SonarPreviewFilter`
|
||||
|
||||
Allows you to preview a filter. It does not modify the input filter.
|
||||
|
||||
***
|
||||
|
||||
## Examples
|
||||
|
||||
The example images are all workflow-included. Generated using `dpmpp_2s_ancestral`, Karras scheduler and
|
||||
starting out with gaussian noise then switching to power noise at the 35% mark. `filter_norm_factor` is set to
|
||||
1.0 in these examples.
|
||||
|
||||
### Node Defaults
|
||||
|
||||
This should be the same as normal gaussian noise.
|
||||
|
||||

|
||||
|
||||
### Positive Alpha
|
||||
|
||||
Positive alpha generally produces a colorful effect. Start with relatively low values and increase
|
||||
until you achieve the desired result. Note that these examples use _relatively_ extreme settings.
|
||||
|
||||
With alpha 0.25:
|
||||
|
||||

|
||||
|
||||
With alpha 0.25, common mode 0.25:
|
||||
|
||||

|
||||
|
||||
With alpha 0.35:
|
||||
|
||||

|
||||
|
||||
With alpha 0.35, common mode 0.35:
|
||||
|
||||

|
||||
|
||||
With alpha 0.5:
|
||||
|
||||

|
||||
|
||||
With alpha 0.5, common mode 0.5:
|
||||
|
||||

|
||||
|
||||
### Negative Alpha
|
||||
|
||||
With alpha -0.5:
|
||||
|
||||

|
||||
|
||||
With alpha -1.5:
|
||||
|
||||

|
||||
|
||||
### Time Brownian Mode
|
||||
|
||||

|
||||
|
||||
With alpha 0.5:
|
||||
|
||||

|
||||
|
||||
With alpha -0.5:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Base Noise Examples
|
||||
|
||||
The example images are all workflow-included. Generated using `dpmpp_2s_ancestral`, Karras scheduler and
|
||||
starting out with gaussian noise then switching to the custom noise type at the 35% mark.
|
||||
|
||||
Some of these noise types are too extreme to be used for initial generations or even with pure
|
||||
noise of that type. However you can either schedule the noise type to kick in at a certain percentage
|
||||
(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`
|
||||
* `voronoi_mix` - A mix of Voronoi (60%) and Gaussian noise types.
|
||||
* `voronoi_fuzz` - Voronoi noise with distance mode `fuzz:name=angle_tanh:fuzz=0.1`.
|
||||
* `white`
|
||||
|
||||
## Brownian
|
||||
|
||||
This is the default noise type for SDE samplers.
|
||||
|
||||

|
||||
|
||||
***
|
||||
## Gaussian
|
||||
|
||||
This is the default noise type for non-SDE samplers.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Green Test
|
||||
|
||||
This is _probably_ not actually green noise. It produces a very colorful effect, however
|
||||
it's very strong and not really suitable for initial generation.
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Highres Pyramid
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Laplacian
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Perlin
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pink Old
|
||||
|
||||
Previously known as `pink`. The implementation isn't correct, though in terms of results it's fine.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Power Old
|
||||
|
||||
Previously known as `power`. The implementation isn't correct, though in terms of results it's fine.
|
||||
|
||||

|
||||
|
||||
Also see the [Advanced Power Noise](advanced_power_noise.md) examples.
|
||||
|
||||
***
|
||||
|
||||
## Pyramid
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pyramid Discount5
|
||||
|
||||
Pyramid noise, generated with a discount of 0.5. (Generally less extreme effect.)
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pyramid Mix
|
||||
|
||||
Pyramid mix is a combination of positive and negative pyramid noise. The effect on
|
||||
the generation is mild compared to raw pyramid noise.
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
You can also use a negative multiplier to achieve a different effect:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Pyramid Old
|
||||
|
||||
This may not actually be pyramid noise at all. Also note that it is quite slow to generate as it
|
||||
effectively generates noise ~60x the latent size.
|
||||
|
||||

|
||||
|
||||
Variation using area scaling:
|
||||
|
||||

|
||||
|
||||
Variation using bislerp scaling:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Rainbow
|
||||
|
||||
Rainbow is a mix of Perlin and Green noise types.
|
||||
|
||||
The "mild" variation uses a relatively low proportion of green noise:
|
||||
|
||||

|
||||
|
||||
The "intense" variation uses a higher proportion of green noise for a more extreme effect.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Studentt
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
## Uniform
|
||||
|
||||

|
||||
@@ -0,0 +1,40 @@
|
||||
# FreeU Extreme
|
||||
|
||||
I admit it's a really dumb name. This is basically a build-your-own FreeU kit.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
Workflow image is also workflow-embedded.
|
||||
|
||||

|
||||
|
||||
## Nodes
|
||||
|
||||
### `FreeUExtreme`
|
||||
|
||||
Allows you to apply a FreeU (v1 or v2) effect to input, output or middle blocks.
|
||||
|
||||
**Note**: ComfyUI by default does not allow patching the middle in the required way. You will need to have
|
||||
[FreeU Advanced](https://github.com/WASasquatch/FreeU_Advanced) installed and enabled, otherwise connecting
|
||||
configs to the `middle` input will have no effect.
|
||||
|
||||
Also note that input and middle do not have a `skip` target so configs targetting that will never match.
|
||||
|
||||
### `FreeUExtremeConfig`
|
||||
|
||||
Better documentation coming soon hopefully. For now, see the workflow example above to get started.
|
||||
|
||||
Also see documentation on filters [here](./advanced_power_noise.md#sonarpowerfilter).
|
||||
|
||||
## Examples
|
||||
|
||||
ComfyUI built-in FreeU V2 for reference:
|
||||
|
||||

|
||||
|
||||
FreeU Extreme example:
|
||||
|
||||

|
||||
|
||||
Note that this is just for example purposes - no attempt was made to get a pretty generation. You
|
||||
may get better results enabling `hidden_mean` even for the skip connections.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Wavelet CFG
|
||||
|
||||
A CFG function that lets you use different CFG values for different frequencies.
|
||||
|
||||
Node: `SonarWaveletCFG`
|
||||
|
||||
## Requirements
|
||||
|
||||
You will need to have the `pytorch_wavelets` package installed in your Python environment to use this.
|
||||
|
||||
Link: https://github.com/fbcotter/pytorch_wavelets
|
||||
|
||||
## YAML crash course
|
||||
|
||||
You can skip past this if you already know YAML. Since wavelet CFG definitions are defined with YAML rules,
|
||||
I am putting this section near the beginning.
|
||||
|
||||
First, JSON is valid YAML, so if you know JSON you can use that if you prefer. Since JSON is valid YAML, this also means the structure of YAML documents is the same as a JSON document.
|
||||
|
||||
YAML looks like this:
|
||||
|
||||
```yaml
|
||||
# Comments start with the hash symbol.
|
||||
# YAML will guess the type if you don't do stuff like quote strings, so the item below
|
||||
# will be "value".
|
||||
key1_name: value
|
||||
# The value of a key can also be a set of keys (usually called an object)
|
||||
# YAML uses indentation to control block grouping.
|
||||
key2_name:
|
||||
# A comment
|
||||
subkey1_name: 123
|
||||
# A list of three items, two integers and a string.
|
||||
some_list: [1, 2, "hello"]
|
||||
# You can also specify lists like this:
|
||||
some_other_list:
|
||||
- 1
|
||||
- 2
|
||||
- hello
|
||||
# It's legal to specify the same key multiple times. This just overwrites
|
||||
# whatever the previous value was.
|
||||
subkey1_name: 345
|
||||
```
|
||||
|
||||
YAML item types:
|
||||
|
||||
* String: `"hello there"` or `hello there` (you may need to quote when there are special characters).
|
||||
* Integer: `123`
|
||||
* Floating point value: `1.23`
|
||||
* Object, may be specified in-line like JSON: `{ key: value, key: value }`. Be careful to separate the value from the colon after the key or it may be interpreted incorrectly. It may be a good idea to quote string values if you're using this syntax (and it's necessary if they have special characters or spaces).
|
||||
* List, may be specified in-line like JSON: `[1, 2, "hello there"]`
|
||||
* Null: `null`. If you want the string "null" then you'd need to quote it.
|
||||
* Boolean: `true` and `false`.
|
||||
|
||||
#### Advanced YAML
|
||||
|
||||
YAML also has a number of advanced features like references. You can use this to avoid repeating the same information multiple times. For example:
|
||||
|
||||
```yaml
|
||||
single_value: &single_ref_name [1, 2, 3]
|
||||
# This is the same as other_value: [1, 2, 3]
|
||||
other_value: *single_ref_name
|
||||
|
||||
# You can also do this with objects.
|
||||
reference_block: &ref_name
|
||||
key: value
|
||||
other_key: other_value
|
||||
whatever:
|
||||
# This sets the "whatever" object to be the same as "reference_block"
|
||||
# Note that you can't just do "whatever: *ref_name" to get that effect here.
|
||||
<<: *ref_name
|
||||
# And you can just overwrite the keys you want to change:
|
||||
key: 123
|
||||
# At this point, "whatever" is { key: 123, other_key: other_value }
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Unfortunately, this isn't very user-friendly and needs to be configured with YAML. This is a relatively basic description of
|
||||
usage. To see all possible options, look at the default configuration definition in the node.
|
||||
|
||||
General information on wavelets from the library I'm using to do wavelet transforms: https://pytorch-wavelets.readthedocs.io/en/latest/index.html
|
||||
|
||||
Trimmed down, the default config looks like this:
|
||||
|
||||
```yaml
|
||||
# This block is used to set the CFG scales.
|
||||
diff:
|
||||
# Scale for the low-frequency band.
|
||||
yl_scale: 5.0
|
||||
|
||||
# Scale for the high-frequency bands.
|
||||
yh_scales: 3.0
|
||||
|
||||
# Sets the wavelet type. DB4 is a good general-purpose wavelet to use.
|
||||
wave: db4
|
||||
|
||||
# Sets the wavelet level.
|
||||
level: 5
|
||||
|
||||
# Set to true if you want to get detailed information dumped to your console.
|
||||
verbose: false
|
||||
```
|
||||
|
||||
Wavelets decompose the value into a low frequency value and a set of high-frequency bands. The number of high-frequency
|
||||
will be equal to the wavelet level, so in this example you will have one low-frequency band and five high-frequency
|
||||
bands to work with. The high-frequency bands are further decomposed into three parts which can also be targeted
|
||||
individually: horizontal, vertical, diagonal.
|
||||
|
||||
The `diff` (or `difference`, whichever you prefer) block is where you set the CFG scales. It's called `difference`
|
||||
because CFG is defined as `uncond + (cond - uncond) * cfg_scale` (`cond` is the positive prompt, `uncond` is
|
||||
negative). So CFG is just the difference between `cond` and `uncond`, multiplied by the CFG scale.
|
||||
|
||||
The example configuration here is using CFG 5 for the low frequency band and CFG 3 for the high frequency bands. It is
|
||||
possible to get even more specific that that. Since we're using `level: 5` here, that means there are five frequency
|
||||
bands that can all be set individually. The high-frequency bands are ordered from fine to coarse detail levels. Example:
|
||||
|
||||
```yaml
|
||||
diff:
|
||||
yl_scale: 5.0
|
||||
# Can also be written: yh_scales: [5.0, 3.0, fill]
|
||||
yh_scales:
|
||||
# Highest/finest band.
|
||||
- 5.0
|
||||
# Decreasing order of detail/frequency.
|
||||
- 3.0
|
||||
- fill
|
||||
```
|
||||
|
||||
The special value `fill` will just repeat the value before it to fill the rest of the bands. Note that if
|
||||
you don't specify the bands or fill then the bands you don't set will use `1.0`. For example with five
|
||||
bands, `[5.0, 3.0, fill]` is the same as `[5.0, 3.0, 3.0, 3.0, 3.0]` while `[5.0, 3.0]` is the same
|
||||
as `[5.0, 3.0, 1.0, 1.0, 1.0]`. You can only use one `fill` per `yh_scales` definition.
|
||||
|
||||
As mentioned, it's also possible to target horizontal, vertical and diagonal bands. You can do this
|
||||
by using a list instead of numeric value for a band definition. **Note**: You need to specify all
|
||||
three bands, `fill` isn't valid here. Example:
|
||||
|
||||
```yaml
|
||||
diff:
|
||||
yl_scale: 5.0
|
||||
# Can also be written: yh_scales: [5.0, 3.0, fill]
|
||||
yh_scales:
|
||||
- [3.0, 3.0, 5.0]
|
||||
- 3.0
|
||||
- fill
|
||||
```
|
||||
|
||||
This example uses CFG 3.0 for horizontal and vertical in finest high-frequency band and CFG 5.0 for
|
||||
diagonal. The remaining high-frequency bands use CFG 3.0.
|
||||
|
||||
## Scheduling CFG
|
||||
|
||||
It's also possible to transition from one set of CFG scales to another over time. The wavelet scales
|
||||
block has an alternative definition format:
|
||||
|
||||
```yaml
|
||||
diff:
|
||||
# One of linear, logarithmic, exponential, half_cosine, sine
|
||||
# Sine mode will hit the peak scales_after values in the middle of the range.
|
||||
schedule: linear
|
||||
|
||||
# One of: sampling, enabled_sampling, sigmas, enabled_sigmas, step, enabled_steps
|
||||
schedule_mode: enabled_sampling
|
||||
|
||||
# When enabled, flips the schedule percentage. This happens before the schedule is applied
|
||||
# or any offset/multiplier stuff. If you want to flip the final result you can do something like
|
||||
# schedule_offset_after: -1.0 and schedule_multiplier_after: -1.0
|
||||
reverse_schedule: false
|
||||
|
||||
scales_start:
|
||||
yl_scale: 5.0
|
||||
yh_scales: 3.0
|
||||
scales_end:
|
||||
yl_scale: 2.0
|
||||
yh_scales: 5.0
|
||||
```
|
||||
|
||||
The way interpolating scales works is we determine a value between 0.0 and 1.0 based on `schedule` and
|
||||
`schedule_mode` and then do linear interpolation (LERP) between the values in `scales_start` and `scales_end`.
|
||||
LERP is just `value_1 * (1.0 - ratio) + value_2 * ratio` so when `ratio` is 1 you get 100% `value_2`,
|
||||
when it's 0.5 you get half of each and when it's 0 you get 100% of `value_1`.
|
||||
|
||||
**Note**: If you have both `scales_start` and toplevel `yl_scale`/`yh_scales` definitions, the
|
||||
scales in `scales_start` will take precedence.
|
||||
|
||||
#### `schedule`
|
||||
|
||||
Current schedule types: `linear`, `logarithmic`, `exponential`, `half_cosine`, `sine`
|
||||
|
||||
Linear just changes by the same amount over time, with the exception
|
||||
of `sine`, the other possible values are similar except the change forms a curve (where it may be slow at first and then
|
||||
accelerate or vice versa). Experiment with them to see what you prefer. `sine` has a somewhat different effect, the
|
||||
percentage of `scales_end` will increase and peak in the middle of the range, then decrease.
|
||||
|
||||
#### `schedule_mode`
|
||||
|
||||
Current modes: `sampling`, `enabled_sampling`, `sigmas`, `enabled_sigmas`, `step`, `enabled_steps`
|
||||
|
||||
* `sampling`: Sampling is a percentage that starts at 0.0 and ends at 1.0 (assuming you're doing txt2img). This isn't really related
|
||||
to the schedule or steps.
|
||||
* `sigmas`: This calculates the difference between the starting sigma and ending sigma as a percentage.
|
||||
* `step`: Not very well tested and may not work (especially with multi-step samplers). The percentage in this case is the percentage
|
||||
steps
|
||||
|
||||
The `_enabled` variants calculate the percentage based on the range that wavelet CFG is enabled for (in other words,
|
||||
the range betwmeen `start_sigma` and `end_sigma`). I'd suggest not using them as it's a lot easier to predict what values
|
||||
will be used when the schedule start/end points aren't also changing.
|
||||
|
||||
***
|
||||
|
||||
In addition to the values described here, there are number of other advanced configuration options that can be used
|
||||
to add/subtract and offset to the calculated percentage value, multiply it, etc. These advanced parameters can be
|
||||
used to do stuff like speed up the transition between config values, keep them within a certain range with minimum/maximum
|
||||
thresholds, etc. See the default YAML config definition in the node to see what is possible.
|
||||
|
||||
|
||||
## Scheduling rules
|
||||
|
||||
Rules may be scheduled using this syntax:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- start_sigma: -1.0
|
||||
end_sigma: 5.0
|
||||
diff:
|
||||
yl_scale: 5.0
|
||||
yh_scales: 3.0
|
||||
- start_sigma: -1.0
|
||||
end_sigma: 0.0
|
||||
diff:
|
||||
yl_scale: 2.0
|
||||
yh_scales: 5.0
|
||||
```
|
||||
|
||||
Values from the top-level are valid within a rule. The definitions from the node are added as the first rule,
|
||||
so if you want to only configure stuff in a `rules` block you can just set the start sigma in the node to `0.0` (
|
||||
which will never match). Rules are checked in order and the first matching one is used.
|
||||
|
||||
An alternative method of scheduling rules is to just chain multiple `SonarWaveletCFG` nodes. If you set the fallback
|
||||
mode to `existing` it is also possible to blend the current result with the next matching one (or normal CFG as the
|
||||
case may be).
|
||||
@@ -0,0 +1,132 @@
|
||||
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
|
||||
|
||||
|
||||
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(module_name: str, key: str) -> ModuleType | None:
|
||||
bi_module = sys.modules.get("_blepping_integrations", {}).get(key)
|
||||
if bi_module is not None:
|
||||
return bi_module
|
||||
module_key = f"custom_nodes.{module_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, ih.key)
|
||||
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") and not getattr(
|
||||
obj.INPUT_TYPES,
|
||||
"_NO_REPLACE",
|
||||
False,
|
||||
):
|
||||
obj.INPUT_TYPES = partial(cls.wrap_INPUT_TYPES, obj.INPUT_TYPES)
|
||||
return obj
|
||||
|
||||
|
||||
__all__ = ("MODULES",)
|
||||
@@ -0,0 +1,209 @@
|
||||
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
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
start_sigma: float = math.inf,
|
||||
end_sigma: float = 0.0,
|
||||
op=None,
|
||||
):
|
||||
self.start_sigma = start_sigma if start_sigma >= 0 else math.inf
|
||||
self.end_sigma = end_sigma
|
||||
self.op = op
|
||||
|
||||
def enabled(self, sigma: torch.Tensor | float | None = None) -> bool:
|
||||
if isinstance(sigma, torch.Tensor):
|
||||
sigma = sigma.detach().max().cpu().item()
|
||||
return sigma is None or self.end_sigma <= sigma <= self.start_sigma
|
||||
|
||||
def call_op(
|
||||
self,
|
||||
t: torch.Tensor,
|
||||
*args: list,
|
||||
op=None,
|
||||
**kwargs: dict,
|
||||
) -> torch.Tensor:
|
||||
if op is None:
|
||||
op = self.op
|
||||
if op is None:
|
||||
return t
|
||||
if not getattr(op, "EXTENDED_LATENT_OPERATION", False):
|
||||
return op(latent=t)
|
||||
return op(*args, latent=t, **kwargs)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
latent: torch.Tensor,
|
||||
*,
|
||||
sigma: torch.Tensor | float | None = None,
|
||||
**kwargs: dict,
|
||||
) -> torch.Tensor:
|
||||
if not self.enabled(sigma=sigma):
|
||||
return latent
|
||||
return self.call_op(latent, sigma=sigma, **kwargs)
|
||||
|
||||
|
||||
class SonarLatentOperationAdvanced(SonarLatentOperation):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
blend_mode: str,
|
||||
blend_strength: float,
|
||||
input_multiplier: float,
|
||||
output_multiplier: float,
|
||||
difference_multiplier: float,
|
||||
ops: Sequence,
|
||||
op_alt=None,
|
||||
**kwargs: dict,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.blend_function = utils.BLENDING_MODES[blend_mode]
|
||||
self.blend_strength = blend_strength
|
||||
self.input_multiplier = input_multiplier
|
||||
self.output_multiplier = output_multiplier
|
||||
self.difference_multiplier = difference_multiplier
|
||||
self.op_alt = op_alt
|
||||
self.ops = ops
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
latent: torch.Tensor,
|
||||
*,
|
||||
sigma: torch.Tensor | float | None = None,
|
||||
**kwargs: dict,
|
||||
) -> torch.Tensor:
|
||||
t = latent
|
||||
enabled = self.enabled(sigma)
|
||||
if not enabled:
|
||||
return (
|
||||
t
|
||||
if self.op_alt is None
|
||||
else self.call_op(t, sigma=sigma, op=self.op_alt, **kwargs)
|
||||
)
|
||||
output = t * self.input_multiplier if self.input_multiplier != 1.0 else t
|
||||
for op in self.ops:
|
||||
output = 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)
|
||||
|
||||
|
||||
class SonarLatentOperationNoise(SonarLatentOperation):
|
||||
def __init__(
|
||||
self,
|
||||
*args: list,
|
||||
custom_noise,
|
||||
scale_to_sigma: bool = False,
|
||||
cpu_noise: bool = False,
|
||||
normalize: bool = True,
|
||||
lazy_noise_sampler: bool = False,
|
||||
**kwargs: dict,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.custom_noise = custom_noise
|
||||
self.normalize = normalize
|
||||
self.scale_to_sigma = scale_to_sigma
|
||||
self.cpu_noise = cpu_noise
|
||||
self.lazy_noise_sampler = lazy_noise_sampler
|
||||
self.noise_sampler = None
|
||||
self.cache_id = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
latent: torch.Tensor,
|
||||
*,
|
||||
sigma: torch.Tensor | float | None = None,
|
||||
**kwargs: dict,
|
||||
) -> torch.Tensor:
|
||||
t = latent
|
||||
enabled = self.enabled(sigma)
|
||||
if not enabled:
|
||||
return t
|
||||
if isinstance(sigma, float):
|
||||
sigma = t.new_full((1,), sigma)
|
||||
make_ns = not self.lazy_noise_sampler or self.noise_sampler is None
|
||||
sigma_min = sigma_max = sigma_next = None
|
||||
sample_sigmas = (
|
||||
kwargs.get("raw_args", {})
|
||||
.get("model_options", {})
|
||||
.get("transformer_options", {})
|
||||
.get("sample_sigmas")
|
||||
)
|
||||
if sample_sigmas is not None and sigma is not None:
|
||||
guessed_step = (sample_sigmas - sigma).abs().argmin().detach().item()
|
||||
guessed_sigma = sample_sigmas[guessed_step].max().detach().item()
|
||||
if guessed_sigma == sigma and guessed_step + 1 < len(sample_sigmas):
|
||||
sigma_next = sample_sigmas[guessed_step + 1]
|
||||
if self.lazy_noise_sampler and not make_ns:
|
||||
cache_id = (
|
||||
id(sample_sigmas) if isinstance(sample_sigmas, torch.Tensor) else None
|
||||
)
|
||||
make_ns = cache_id is None or cache_id != self.cache_id
|
||||
self.cache_id = cache_id
|
||||
if make_ns and sample_sigmas is not None:
|
||||
sigmas_min = sample_sigmas[sample_sigmas > 0]
|
||||
sigma_min = (
|
||||
sigmas_min.min().detach().item() if torch.any(sigmas_min) else 0.0
|
||||
)
|
||||
del sigmas_min
|
||||
sigma_max = sample_sigmas.max().detach().item()
|
||||
ns = (
|
||||
self.custom_noise.make_noise_sampler(
|
||||
t,
|
||||
sigma_min=sigma_min,
|
||||
sigma_max=sigma_max,
|
||||
normalized=self.normalize,
|
||||
seed=torch.randint(1, 1 << 31, (), device="cpu").item(),
|
||||
cpu=self.cpu_noise,
|
||||
)
|
||||
if make_ns
|
||||
else self.noise_sampler
|
||||
)
|
||||
if make_ns and self.lazy_noise_sampler:
|
||||
self.noise_sampler = ns
|
||||
noise = ns(sigma, sigma if sigma_next is None else sigma_next)
|
||||
if self.scale_to_sigma and sigma is not None:
|
||||
noise *= sigma
|
||||
noise += t
|
||||
return noise
|
||||
|
||||
|
||||
class SonarLatentOperationSetSeed(SonarLatentOperation):
|
||||
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:
|
||||
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
|
||||
@@ -1,680 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import inspect
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Callable
|
||||
|
||||
import torch
|
||||
from comfy import samplers
|
||||
|
||||
from . import noise
|
||||
from .noise import NoiseType
|
||||
from .sonar import (
|
||||
GuidanceConfig,
|
||||
GuidanceType,
|
||||
HistoryType,
|
||||
SonarConfig,
|
||||
SonarDPMPPSDE,
|
||||
SonarEuler,
|
||||
SonarEulerAncestral,
|
||||
)
|
||||
|
||||
|
||||
class NoisyLatentLikeNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"noise_type": (tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
"latent": ("LATENT",),
|
||||
"multiplier": ("FLOAT", {"default": 1.0}),
|
||||
"add_to_latent": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
"mul_by_sigmas_opt": ("SIGMAS",),
|
||||
"model_opt": ("MODEL",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
CATEGORY = "latent/noise"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
def go(
|
||||
self,
|
||||
noise_type: str,
|
||||
seed: None | int,
|
||||
latent: dict,
|
||||
multiplier: float = 1.0,
|
||||
add_to_latent=False,
|
||||
custom_noise_opt: object | None = None,
|
||||
mul_by_sigmas_opt: None | torch.Tensor = None,
|
||||
model_opt: object | None = None,
|
||||
):
|
||||
model, sigmas = model_opt, mul_by_sigmas_opt
|
||||
if sigmas is not None and len(sigmas) > 0:
|
||||
if model is None:
|
||||
raise ValueError(
|
||||
"NoisyLatentLike requires a model when sigmas are connected!",
|
||||
)
|
||||
while hasattr(model, "model"):
|
||||
model = model.model
|
||||
latent_scale_factor = model.latent_format.scale_factor
|
||||
max_denoise = samplers.Sampler().max_denoise(
|
||||
SimpleNamespace(inner_model=model),
|
||||
sigmas,
|
||||
)
|
||||
multiplier *= (
|
||||
float(
|
||||
torch.sqrt(1.0 + sigmas[0] ** 2.0) if max_denoise else sigmas[0],
|
||||
)
|
||||
/ latent_scale_factor
|
||||
)
|
||||
latent_samples = latent["samples"]
|
||||
if custom_noise_opt is not None:
|
||||
ns = custom_noise_opt.make_noise_sampler(latent_samples)
|
||||
else:
|
||||
ns = noise.get_noise_sampler(
|
||||
NoiseType[noise_type.upper()],
|
||||
latent_samples,
|
||||
None,
|
||||
None,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
)
|
||||
randst = torch.random.get_rng_state()
|
||||
try:
|
||||
torch.random.manual_seed(seed)
|
||||
result = ns(None, None)
|
||||
finally:
|
||||
torch.random.set_rng_state(randst)
|
||||
if multiplier != 1.0:
|
||||
result *= multiplier
|
||||
if add_to_latent:
|
||||
result += latent_samples.to(result.device)
|
||||
return ({"samples": result},)
|
||||
|
||||
|
||||
class SonarCustomNoiseNodeBase(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def get_item_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"rescale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"sonar_custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
rescale,
|
||||
sonar_custom_noise_opt=None,
|
||||
**kwargs: dict[str, Any],
|
||||
):
|
||||
nis = (
|
||||
sonar_custom_noise_opt.clone()
|
||||
if sonar_custom_noise_opt
|
||||
else noise.CustomNoiseChain()
|
||||
)
|
||||
if factor != 0:
|
||||
nis.add(self.get_item_class()(factor, **kwargs))
|
||||
return (nis if rescale == 0 else nis.rescaled(rescale),)
|
||||
|
||||
|
||||
class SonarCustomNoiseNode(SonarCustomNoiseNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"] |= {
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return noise.CustomNoiseItem
|
||||
|
||||
|
||||
class GuidanceConfigNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.01,
|
||||
"min": -2.0,
|
||||
"max": 2.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"guidance_type": (tuple(t.name.lower() for t in GuidanceType),),
|
||||
"start_step": ("INT", {"default": 1, "min": 1}),
|
||||
"end_step": ("INT", {"default": 9999, "min": 1}),
|
||||
"latent": ("LATENT",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SONAR_GUIDANCE_CFG",)
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
FUNCTION = "make_guidance_cfg"
|
||||
|
||||
def make_guidance_cfg(
|
||||
self,
|
||||
guidance_type,
|
||||
factor,
|
||||
start_step,
|
||||
end_step,
|
||||
latent,
|
||||
):
|
||||
return (
|
||||
GuidanceConfig(
|
||||
guidance_type=GuidanceType[guidance_type.upper()],
|
||||
factor=factor,
|
||||
start_step=start_step,
|
||||
end_step=end_step,
|
||||
latent=latent.get("samples"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeSonarBase:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"momentum": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.95,
|
||||
"min": -0.5,
|
||||
"max": 2.5,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"momentum_hist": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.75,
|
||||
"min": -1.5,
|
||||
"max": 1.5,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"momentum_init": (tuple(t.name for t in HistoryType),),
|
||||
"direction": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": -30.0,
|
||||
"max": 15.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"rand_init_noise_type": (
|
||||
tuple(NoiseType.get_names(skip=(NoiseType.BROWNIAN,))),
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"guidance_cfg_opt": ("SONAR_GUIDANCE_CFG",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
|
||||
class SamplerNodeSonarEuler(SamplerNodeSonarBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"].update(
|
||||
{
|
||||
"s_noise": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
FUNCTION = "get_sampler"
|
||||
|
||||
def get_sampler(
|
||||
self,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
s_noise,
|
||||
guidance_cfg_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
SonarEuler.sampler,
|
||||
{
|
||||
"s_noise": s_noise,
|
||||
"sonar_config": cfg,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"].update(
|
||||
{
|
||||
"eta": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
},
|
||||
)
|
||||
result["optional"].update(
|
||||
{
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
def get_sampler(
|
||||
self,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
noise_type,
|
||||
eta,
|
||||
s_noise,
|
||||
guidance_cfg_opt=None,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
SonarEulerAncestral.sampler,
|
||||
{
|
||||
"sonar_config": cfg,
|
||||
"eta": eta,
|
||||
"s_noise": s_noise,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEuler):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"].update(
|
||||
{
|
||||
"eta": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"noise_type": (tuple(NoiseType.get_names(default=NoiseType.BROWNIAN)),),
|
||||
},
|
||||
)
|
||||
result["optional"].update(
|
||||
{
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
def get_sampler(
|
||||
self,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
noise_type,
|
||||
eta,
|
||||
s_noise,
|
||||
guidance_cfg_opt=None,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
SonarDPMPPSDE.sampler,
|
||||
{
|
||||
"sonar_config": cfg,
|
||||
"eta": eta,
|
||||
"s_noise": s_noise,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeConfigOverride:
|
||||
KWARG_OVERRIDES = ("s_noise", "eta", "s_churn", "r", "solver_type")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sampler": ("SAMPLER",),
|
||||
"eta": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"s_noise": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"s_churn": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"r": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.5,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"sde_solver": (("midpoint", "heun"),),
|
||||
},
|
||||
"optional": {
|
||||
"noise_type": (tuple(NoiseType.get_names()),),
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
FUNCTION = "get_sampler"
|
||||
|
||||
def get_sampler(
|
||||
self,
|
||||
sampler,
|
||||
eta,
|
||||
s_noise,
|
||||
s_churn,
|
||||
r,
|
||||
sde_solver,
|
||||
noise_type=None,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
self.sampler_function,
|
||||
extra_options=sampler.extra_options
|
||||
| {
|
||||
"override_sampler_cfg": {
|
||||
"sampler": sampler,
|
||||
"noise_type": NoiseType[noise_type.upper()]
|
||||
if noise_type is not None
|
||||
else None,
|
||||
"custom_noise": custom_noise_opt,
|
||||
"s_noise": s_noise,
|
||||
"eta": eta,
|
||||
"s_churn": s_churn,
|
||||
"r": r,
|
||||
"solver_type": sde_solver,
|
||||
},
|
||||
},
|
||||
inpaint_options=sampler.inpaint_options | {},
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@torch.no_grad()
|
||||
def sampler_function(
|
||||
cls,
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
*args: list[Any],
|
||||
override_sampler_cfg: dict[str, Any] | None = None,
|
||||
noise_sampler: Callable | None = None,
|
||||
extra_args: dict[str, Any] | None = None,
|
||||
**kwargs: dict[str, Any],
|
||||
):
|
||||
if not override_sampler_cfg:
|
||||
raise ValueError("Override sampler config missing!")
|
||||
if extra_args is None:
|
||||
extra_args = {}
|
||||
cfg = override_sampler_cfg
|
||||
sampler, noise_type, custom_noise = (
|
||||
cfg["sampler"],
|
||||
cfg.get("noise_type"),
|
||||
cfg.get("custom_noise"),
|
||||
)
|
||||
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
|
||||
seed = extra_args.get("seed")
|
||||
if custom_noise is not None:
|
||||
noise_sampler = custom_noise.make_noise_sampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
)
|
||||
elif noise_type is not None:
|
||||
noise_sampler = noise.get_noise_sampler(
|
||||
noise_type,
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
)
|
||||
sig = inspect.signature(sampler.sampler_function)
|
||||
params = sig.parameters
|
||||
kwargs = kwargs | {}
|
||||
if "noise_sampler" in params:
|
||||
kwargs["noise_sampler"] = noise_sampler
|
||||
for k in cls.KWARG_OVERRIDES:
|
||||
if k not in params or cfg.get(k) is None:
|
||||
continue
|
||||
kwargs[k] = cfg[k]
|
||||
return sampler.sampler_function(
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
*args,
|
||||
extra_args=extra_args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
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 # noqa: TRY301
|
||||
|
||||
class KRestartSamplerCustomNoise:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"add_noise": (["enable", "disable"],),
|
||||
"noise_seed": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
|
||||
),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
||||
"sampler": ("SAMPLER",),
|
||||
"scheduler": (tuple(rs.restart_sampling.SCHEDULER_MAPPING.keys()),),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"latent_image": ("LATENT",),
|
||||
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
||||
"return_with_leftover_noise": (["disable", "enable"],),
|
||||
"segments": (
|
||||
"STRING",
|
||||
{
|
||||
"default": rs.restart_sampling.DEFAULT_SEGMENTS,
|
||||
"multiline": False,
|
||||
},
|
||||
),
|
||||
"restart_scheduler": (rs.nodes.get_supported_restart_schedulers(),),
|
||||
"chunked_mode": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_noise_opt": ("SONAR_CUSTOM_NOISE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT", "LATENT")
|
||||
RETURN_NAMES = ("output", "denoised_output")
|
||||
FUNCTION = "sample"
|
||||
CATEGORY = "sampling"
|
||||
|
||||
def sample(
|
||||
self,
|
||||
model,
|
||||
add_noise,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
start_at_step,
|
||||
end_at_step,
|
||||
return_with_leftover_noise,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
chunked_mode=False,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
return rs.restart_sampling.restart_sampling(
|
||||
model,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
disable_noise=add_noise == "disable",
|
||||
step_range=(start_at_step, end_at_step),
|
||||
force_full_denoise=return_with_leftover_noise != "enable",
|
||||
output_only=False,
|
||||
chunked_mode=chunked_mode,
|
||||
custom_noise=custom_noise_opt.make_noise_sampler
|
||||
if custom_noise_opt
|
||||
else None,
|
||||
)
|
||||
except (ImportError, NotImplementedError):
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
from . import (
|
||||
base,
|
||||
freeu_extreme,
|
||||
integrations,
|
||||
latent_operations,
|
||||
misc,
|
||||
momentum_samplers,
|
||||
noise_filters,
|
||||
noise_types,
|
||||
powernoise,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SonarCustomNoise": base.SonarCustomNoiseNode,
|
||||
"SonarCustomNoiseAdv": base.SonarCustomNoiseAdvNode,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
for nm in (
|
||||
freeu_extreme,
|
||||
integrations,
|
||||
latent_operations,
|
||||
misc,
|
||||
momentum_samplers,
|
||||
noise_filters,
|
||||
noise_types,
|
||||
powernoise,
|
||||
):
|
||||
NODE_CLASS_MAPPINGS |= getattr(nm, "NODE_CLASS_MAPPINGS", {})
|
||||
NODE_DISPLAY_NAME_MAPPINGS |= getattr(nm, "NODE_DISPLAY_NAME_MAPPINGS", {})
|
||||
@@ -0,0 +1,290 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
|
||||
if not hasattr(comfy_validation, "validate_node_input"):
|
||||
raise NotImplementedError # noqa: TRY301
|
||||
HAVE_COMFY_UNION_TYPE = comfy_validation.validate_node_input("B", "A,B")
|
||||
except (ImportError, NotImplementedError):
|
||||
HAVE_COMFY_UNION_TYPE = False
|
||||
except Exception as exc: # noqa: BLE001
|
||||
HAVE_COMFY_UNION_TYPE = False
|
||||
print(
|
||||
f"** ComfyUI-sonar: Warning, caught unexpected exception trying to detect ComfyUI union type support. Disabling. Exception: {exc}",
|
||||
)
|
||||
|
||||
NOISE_INPUT_TYPES = frozenset(("SONAR_CUSTOM_NOISE", "OCS_NOISE"))
|
||||
|
||||
if not HAVE_COMFY_UNION_TYPE:
|
||||
|
||||
class Wildcard(str): # noqa: FURB189
|
||||
__slots__ = ("whitelist",)
|
||||
|
||||
@classmethod
|
||||
def __new__(cls, s, *args: list, whitelist=None, **kwargs: dict):
|
||||
result = super().__new__(s, *args, **kwargs)
|
||||
result.whitelist = whitelist
|
||||
return result
|
||||
|
||||
def __ne__(self, other):
|
||||
return False if self.whitelist is None else other not in self.whitelist
|
||||
|
||||
WILDCARD_NOISE = Wildcard("*", whitelist=NOISE_INPUT_TYPES)
|
||||
else:
|
||||
WILDCARD_NOISE = ",".join(NOISE_INPUT_TYPES)
|
||||
|
||||
|
||||
NOISE_INPUT_TYPES_HINT = (
|
||||
f"The following input types are supported: {', '.join(NOISE_INPUT_TYPES)}"
|
||||
)
|
||||
|
||||
|
||||
class SonarInputCollection(InputCollection):
|
||||
def __init__(self, *args: list, **kwargs: dict):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._DELEGATE_KEYS = self._DELEGATE_KEYS | frozenset(( # noqa: PLR6104
|
||||
"customnoise",
|
||||
"floatpct",
|
||||
"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,
|
||||
)
|
||||
|
||||
def floatpct(self, name: str, *, min=0.0, max=1.0, **kwargs: dict): # noqa: A002
|
||||
return self.float(name=name, min=min, max=max, **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=(MODULES.initialize,), **kwargs: dict):
|
||||
super().__init__(
|
||||
*args,
|
||||
initializers=initializers,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class SonarCustomNoiseNodeBase(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "A custom noise item."
|
||||
RETURN_TYPES = ("SONAR_CUSTOM_NOISE",)
|
||||
OUTPUT_TOOLTIPS = ("A custom noise chain.",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_item_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
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.",
|
||||
),
|
||||
initializers=(),
|
||||
)
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor=1.0,
|
||||
rescale=0.0,
|
||||
sonar_custom_noise_opt=None,
|
||||
**kwargs: dict[str, Any],
|
||||
):
|
||||
nis = (
|
||||
sonar_custom_noise_opt.clone()
|
||||
if sonar_custom_noise_opt
|
||||
else noise.CustomNoiseChain()
|
||||
)
|
||||
if factor != 0:
|
||||
nis.add(self.get_item_class()(factor, **kwargs))
|
||||
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):
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: NoiseChainInputTypes().req_selectnoise_noise_type(
|
||||
tooltip="Sets the type of noise to generate.",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return noise.CustomNoiseItem
|
||||
|
||||
|
||||
class SonarCustomNoiseAdvNode(SonarCustomNoiseNode):
|
||||
DESCRIPTION = "A custom noise item allowing advanced YAML parameter input."
|
||||
|
||||
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:
|
||||
@staticmethod
|
||||
def get_normalize(val: str) -> bool | None:
|
||||
return None if val == "default" else val == "forced"
|
||||
@@ -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)()
|
||||
@@ -0,0 +1,340 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from .. import utils
|
||||
from .base import SonarInputTypes, SonarLazyInputTypes
|
||||
from .powernoise import PowerFilter
|
||||
|
||||
|
||||
def ffilter(x, pfilter, normalization_factor=1.0, cfg_idx=None, filter_cache=None):
|
||||
cache_key = None
|
||||
if filter_cache is not None and cfg_idx is not None:
|
||||
cache_key = (cfg_idx, x.shape[-2:])
|
||||
filter_rfft = filter_cache.get(cache_key)
|
||||
if filter_rfft is None:
|
||||
filter_rfft = PowerFilter.normalize(
|
||||
pfilter.build(x.shape),
|
||||
x.shape,
|
||||
normalization_factor=normalization_factor,
|
||||
).to(x.device, non_blocking=True)
|
||||
if cache_key:
|
||||
filter_cache[cache_key] = filter_rfft
|
||||
x_rfft = torch.fft.rfft2(x.to(torch.float32), norm="ortho")
|
||||
x_filt = torch.fft.irfft2(
|
||||
x_rfft.mul_(filter_rfft),
|
||||
s=x.shape[-2:],
|
||||
norm="ortho",
|
||||
)
|
||||
return x_filt.to(x.dtype, non_blocking=True)
|
||||
|
||||
|
||||
class FreeUExtremeConfigNode:
|
||||
DESCRIPTION = "Allows setting configuration for FreeU Extreme."
|
||||
RETURN_TYPES = ("FRUX_CONFIG",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "model_patches"
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: SonarInputTypes()
|
||||
.req_bool_stage_1(
|
||||
default=True,
|
||||
tooltip="Controls whether this configuration applies to stage 1.",
|
||||
)
|
||||
.req_bool_stage_2(
|
||||
default=False,
|
||||
tooltip="Controls whether this configuration applies to stage 2.",
|
||||
)
|
||||
.req_bool_stage_3(
|
||||
default=False,
|
||||
tooltip="Controls whether this configuration applies to stage 3.",
|
||||
)
|
||||
.req_field_target(
|
||||
("backbone", "skip", "both"),
|
||||
default="backbone",
|
||||
tooltip="Controls whether this filter applies to backbone or skip layers (or both).",
|
||||
)
|
||||
.req_floatpct_start(
|
||||
default=0.0,
|
||||
tooltip="Start time as percentage of sampling this configuration applies to. Inclusive.",
|
||||
)
|
||||
.req_floatpct_end(
|
||||
default=1.0,
|
||||
tooltip="End time as percentage of sampling this configuration applies to. Inclusive.",
|
||||
)
|
||||
.req_floatpct_slice(
|
||||
default=1.0,
|
||||
tooltip="Percentage of the layer the FreeU effect is applied to.",
|
||||
)
|
||||
.req_floatpct_slice_offset(
|
||||
default=0.0,
|
||||
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%.",
|
||||
)
|
||||
.req_float_filter_norm(
|
||||
default=0.0,
|
||||
min=-10.0,
|
||||
max=10.0,
|
||||
tooltip="Normalization factor applied to the filter. 1.0 means 100% normalized.",
|
||||
)
|
||||
.req_float_scale(
|
||||
default=1.0,
|
||||
tooltip="Strength of the effects applied by this configuration.",
|
||||
)
|
||||
.req_float_blend(
|
||||
default=1.0,
|
||||
tooltip="Blends the filtered result based on the specified strength where 1.0 means 100% filtered.",
|
||||
)
|
||||
.req_selectblend_blend_mode(
|
||||
tooltip="Mode used when blending. Generally only has an effect when blend is set to values other than 0 or 1",
|
||||
)
|
||||
.req_bool_hidden_mean(
|
||||
default=True,
|
||||
tooltip="You can think of this as FreeU V2 mode.",
|
||||
)
|
||||
.req_bool_final(
|
||||
default=True,
|
||||
tooltip="When enabled, other configurations won't be considered if this one matched. Otherwise, multiple configurations/filter effects can be stacked.",
|
||||
)
|
||||
.opt_field_sonar_power_filter_opt(
|
||||
"SONAR_POWER_FILTER",
|
||||
tooltip="Optionally attach a Power Filter here to set filtering parameters.",
|
||||
)
|
||||
.opt_field_frux_config_opt(
|
||||
"FRUX_CONFIG",
|
||||
tooltip="Optionally attach another configuration node here.",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def go(cls, **kwargs: dict):
|
||||
return (FreeUExtremeConfig(**kwargs),)
|
||||
|
||||
|
||||
class FreeUExtremeConfig:
|
||||
_keys = (
|
||||
"target",
|
||||
"stage_1",
|
||||
"stage_2",
|
||||
"stage_3",
|
||||
"start",
|
||||
"end",
|
||||
"slice",
|
||||
"slice_offset",
|
||||
"filter_norm",
|
||||
"scale",
|
||||
"blend",
|
||||
"blend_mode",
|
||||
"hidden_mean",
|
||||
"final",
|
||||
"sonar_power_filter",
|
||||
"frux_config",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
target,
|
||||
stage_1=False,
|
||||
stage_2=False,
|
||||
stage_3=False,
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
slice=1.0, # noqa: A002
|
||||
slice_offset=0.0,
|
||||
filter_norm=1.0,
|
||||
scale=1.0,
|
||||
blend=1.0,
|
||||
blend_mode=None,
|
||||
hidden_mean=True,
|
||||
final=True,
|
||||
sonar_power_filter_opt=None,
|
||||
frux_config_opt=None,
|
||||
):
|
||||
self.target = target
|
||||
self.stage_1 = stage_1
|
||||
self.stage_2 = stage_2
|
||||
self.stage_3 = stage_3
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.slice = slice
|
||||
self.slice_offset = slice_offset
|
||||
self.filter_norm = filter_norm
|
||||
self.scale = scale
|
||||
self.blend = blend
|
||||
self.blend_mode = blend_mode
|
||||
self.hidden_mean = hidden_mean
|
||||
self.final = final
|
||||
self.sonar_power_filter = sonar_power_filter_opt
|
||||
self.frux_config = frux_config_opt
|
||||
|
||||
def get_config_list(self):
|
||||
result = [self]
|
||||
curr = self
|
||||
while cfg := curr.frux_config:
|
||||
curr = cfg
|
||||
if (
|
||||
cfg.start >= 1
|
||||
or cfg.end <= 0
|
||||
or cfg.blend == 0
|
||||
or not (cfg.stage_1 or cfg.stage_2 or cfg.stage_3)
|
||||
):
|
||||
continue
|
||||
result.append(cfg)
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
# Hidden mean function modified from https://github.com/WASasquatch/FreeU_Advanced
|
||||
def get_scale(self, h: torch.Tensor) -> torch.Tensor:
|
||||
if not self.hidden_mean:
|
||||
return self.scale
|
||||
hmean = h.mean(1).unsqueeze(1)
|
||||
hmax, hmin = (
|
||||
op(hmean.view(hmean.shape[0], -1), dim=-1, keepdim=True)[0]
|
||||
for op in (torch.max, torch.min)
|
||||
)
|
||||
hmean -= hmin.unsqueeze(2).unsqueeze(3)
|
||||
hmean /= (hmax - hmin).unsqueeze(2).unsqueeze(3)
|
||||
return 1.0 + (self.scale - 1.0) * hmean
|
||||
|
||||
def check_match(self, pct, stage, is_skip=False):
|
||||
if pct < self.start or pct > self.end:
|
||||
return False
|
||||
if not getattr(self, f"stage_{stage}"):
|
||||
return False
|
||||
return not self.target not in {"skip" if is_skip else "backbone", "both"}
|
||||
|
||||
def apply(self, idx, x, filter_cache, cpu_fft=False):
|
||||
_batch, features, _height, _width = x.shape
|
||||
scale = self.get_scale(x)
|
||||
slice_size = int(features * self.slice)
|
||||
slice_offs = int(features * self.slice_offset)
|
||||
|
||||
xslice = (
|
||||
self.apply_filter(
|
||||
idx,
|
||||
x[:, slice_offs : slice_offs + slice_size],
|
||||
filter_cache,
|
||||
cpu_fft=cpu_fft,
|
||||
)
|
||||
* scale
|
||||
)
|
||||
x[:, slice_offs : slice_offs + slice_size] = (
|
||||
xslice
|
||||
if self.blend == 1.0
|
||||
else utils.BLENDING_MODES[self.blend_mode](
|
||||
x[:, slice_offs : slice_offs + slice_size],
|
||||
xslice,
|
||||
self.blend,
|
||||
)
|
||||
)
|
||||
return x
|
||||
|
||||
def apply_filter(self, idx, xslice, filter_cache, cpu_fft=False):
|
||||
filt = self.sonar_power_filter
|
||||
if filt is None:
|
||||
return xslice
|
||||
device = xslice.device
|
||||
if cpu_fft:
|
||||
xslice = xslice.to("cpu")
|
||||
xslice = ffilter(
|
||||
xslice,
|
||||
filt,
|
||||
normalization_factor=self.filter_norm,
|
||||
cfg_idx=idx,
|
||||
filter_cache=filter_cache,
|
||||
)
|
||||
if cpu_fft:
|
||||
xslice = xslice.to(device)
|
||||
return xslice
|
||||
|
||||
def clone(self):
|
||||
return self.__class__(**{k: getattr(self, k) for k in self._keys})
|
||||
|
||||
def __repr__(self):
|
||||
meh = {k: getattr(self, k) for k in self._keys}
|
||||
return f"<FRUXConfig: {meh}>"
|
||||
|
||||
|
||||
class FreeUExtremeNode:
|
||||
DESCRIPTION = "Main FreeU Extreme node. Allows patching a model with the FreeU (V2) effect with more control."
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "model_patches"
|
||||
|
||||
INPUT_TYPES = (
|
||||
SonarInputTypes()
|
||||
.req_model(tooltip="Model to patch.")
|
||||
.req_bool_cpu_fft(
|
||||
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.",
|
||||
)
|
||||
.opt_field_input_config(
|
||||
"FRUX_CONFIG",
|
||||
tooltip="Allows specifying configuration for input blocks.",
|
||||
)
|
||||
.opt_field_middle_config(
|
||||
"FRUX_CONFIG",
|
||||
tooltip="Allows specifying configuration for middle blocks.",
|
||||
)
|
||||
.opt_field_output_config(
|
||||
"FRUX_CONFIG",
|
||||
tooltip="Allows specifying configuration for output blocks.",
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
model,
|
||||
cpu_fft,
|
||||
input_config=None,
|
||||
middle_config=None,
|
||||
output_config=None,
|
||||
):
|
||||
model_channels = model.model.model_config.unet_config["model_channels"]
|
||||
stages = {model_channels * 4: 1, model_channels * 2: 2, model_channels: 3}
|
||||
icfg, mcfg, ocfg = (
|
||||
() if cfg is None else cfg.get_config_list()
|
||||
for cfg in (input_config, middle_config, output_config)
|
||||
)
|
||||
m = model.clone()
|
||||
ms = m.get_model_object("model_sampling")
|
||||
filter_cache = {}
|
||||
|
||||
def handler(_typ, h_shape, cfg, x, toptions, is_skip=False):
|
||||
stage = stages.get(h_shape[1])
|
||||
if stage is None:
|
||||
return x
|
||||
sigma = toptions["sigmas"].max().detach().cpu()
|
||||
pct = 1.0 - (ms.timestep(sigma) / 999.0)
|
||||
for idx, ci in enumerate(cfg):
|
||||
if not ci.check_match(pct, stage, is_skip):
|
||||
continue
|
||||
x = ci.apply(idx, x, filter_cache, cpu_fft=cpu_fft)
|
||||
if ci.final:
|
||||
break
|
||||
return x
|
||||
|
||||
def in_patch(h, toptions):
|
||||
return handler("input", h.shape, icfg, h, toptions)
|
||||
|
||||
def mid_patch(h, toptions):
|
||||
return handler("middle", h.shape, mcfg, h, toptions)
|
||||
|
||||
def out_patch(h, hsp, toptions):
|
||||
h = handler("output", h.shape, ocfg, h, toptions)
|
||||
hsp = handler("output", h.shape, ocfg, hsp, toptions, is_skip=True)
|
||||
return h, hsp
|
||||
|
||||
if icfg:
|
||||
m.set_model_input_block_patch(in_patch)
|
||||
if mcfg:
|
||||
m.set_model_patch(mid_patch, "middle_block_patch")
|
||||
if ocfg:
|
||||
m.set_model_output_block_patch(out_patch)
|
||||
return (m,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FreeUExtremeConfig": FreeUExtremeConfigNode,
|
||||
"FreeUExtreme": FreeUExtremeNode,
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comfy import samplers
|
||||
|
||||
from .. import external, noise
|
||||
from .base import (
|
||||
NoiseNoChainInputTypes,
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarInputTypes,
|
||||
SonarLazyInputTypes,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
|
||||
|
||||
bleh = None
|
||||
|
||||
|
||||
class SonarBlendFilterNoiseNode(
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
):
|
||||
DESCRIPTION = "Custom noise type that allows blending and filtering the output of another noise generator using ComfyUI-bleh."
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: NoiseNoChainInputTypes()
|
||||
.req_customnoise_sonar_custom_noise()
|
||||
.req_selectblend(insert_modes=("simple_add",), default="simple_add")
|
||||
.req_field_ffilter(
|
||||
() if bleh is None else tuple(bleh.py.latent_utils.FILTER_PRESETS.keys()),
|
||||
)
|
||||
.req_string_ffilter_custom(default="")
|
||||
.req_float_ffilter_scale(default=1.0)
|
||||
.req_float_ffilter_strength(default=0.0)
|
||||
.req_int_ffilter_threshold(default=1, min=1, max=32)
|
||||
.req_field_enhance_mode(
|
||||
("none",)
|
||||
if bleh is None
|
||||
else ("none", *bleh.py.latent_utils.ENHANCE_METHODS),
|
||||
default="none",
|
||||
)
|
||||
.req_float_enhance_strength(default=0.0)
|
||||
.req_field_affect(("result", "noise", "both"), default="result")
|
||||
.req_normalizetristate_normalize_result()
|
||||
.req_normalizetristate_normalize_noise(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return noise.BlendFilterNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
blend_mode,
|
||||
ffilter,
|
||||
ffilter_custom,
|
||||
ffilter_scale,
|
||||
ffilter_strength,
|
||||
ffilter_threshold,
|
||||
enhance_mode,
|
||||
enhance_strength,
|
||||
affect,
|
||||
normalize_result,
|
||||
normalize_noise,
|
||||
):
|
||||
if bleh is None:
|
||||
raise RuntimeError("bleh not available")
|
||||
import ast # noqa: PLC0415
|
||||
|
||||
ffilter_custom = ffilter_custom.strip()
|
||||
normalize_result = (
|
||||
None if normalize_result == "default" else normalize_result == "forced"
|
||||
)
|
||||
normalize_noise = (
|
||||
None if normalize_noise == "default" else normalize_noise == "forced"
|
||||
)
|
||||
if ffilter_custom:
|
||||
ffilter = ast.literal_eval(f"[{ffilter_custom}]")
|
||||
elif ffilter == "none":
|
||||
ffilter = None
|
||||
else:
|
||||
ffilter = bleh.py.latent_utils.FILTER_PRESETS[ffilter]
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise.clone(),
|
||||
blend_mode=blend_mode,
|
||||
ffilter=ffilter,
|
||||
ffilter_scale=ffilter_scale,
|
||||
ffilter_strength=ffilter_strength,
|
||||
ffilter_threshold=ffilter_threshold,
|
||||
enhance_mode=enhance_mode,
|
||||
enhance_strength=enhance_strength,
|
||||
affect=affect,
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
)
|
||||
|
||||
|
||||
class SonarBlehOpsNoiseNode(
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
):
|
||||
DESCRIPTION = (
|
||||
"Custom noise type that allows manipulating noise with ComfyUI-bleh ops."
|
||||
)
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: NoiseNoChainInputTypes()
|
||||
.req_customnoise_sonar_custom_noise()
|
||||
.req_normalizetristate_normalize()
|
||||
.req_yaml_rules(
|
||||
tooltip="Enter rules in the bleh block ops format here.",
|
||||
placeholder="# YAML ops here",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return noise.BlehOpsNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
rules,
|
||||
normalize,
|
||||
):
|
||||
if bleh is None:
|
||||
raise RuntimeError("bleh not available")
|
||||
return super().go(
|
||||
factor,
|
||||
noise=sonar_custom_noise.clone(),
|
||||
rules=bleh.py.nodes.ops.RuleGroup.from_yaml(rules),
|
||||
normalize=normalize,
|
||||
)
|
||||
|
||||
|
||||
restart = None
|
||||
|
||||
|
||||
def KRestartSamplerCustomNoise_INPUT_TYPES_BUILDER():
|
||||
if restart is not None:
|
||||
get_normal_schedulers = getattr(
|
||||
restart.nodes,
|
||||
"get_supported_normal_schedulers",
|
||||
restart.nodes.get_supported_restart_schedulers,
|
||||
)
|
||||
restart_normal_schedulers = get_normal_schedulers()
|
||||
restart_schedulers = restart.nodes.get_supported_restart_schedulers()
|
||||
restart_default_segments = restart.restart_sampling.DEFAULT_SEGMENTS
|
||||
else:
|
||||
restart_default_segments = ""
|
||||
restart_normal_schedulers = restart_schedulers = ()
|
||||
return (
|
||||
SonarInputTypes()
|
||||
.req_model()
|
||||
.req_field_add_noise(("enable", "disable"), default="enable")
|
||||
.req_seed_noise_seed()
|
||||
.req_int_steps(default=20, min=1)
|
||||
.req_float_cfg(default=8.0, min=0.0)
|
||||
.req_sampler()
|
||||
.req_field_scheduler(restart_normal_schedulers)
|
||||
.req_conditioning_positive()
|
||||
.req_conditioning_negative()
|
||||
.req_latent_latent_image()
|
||||
.req_int_start_at_step(default=0, min=0)
|
||||
.req_int_end_at_step(default=10000, min=0)
|
||||
.req_field_return_with_leftover_noise(
|
||||
("disable", "enable"),
|
||||
default="disable",
|
||||
)
|
||||
.req_string_segments(default=restart_default_segments)
|
||||
.req_field_restart_scheduler(restart_schedulers)
|
||||
.req_bool_chunked_mode(default=True)
|
||||
.opt_customnoise_custom_noise_opt(tooltip="Optional custom noise input.")
|
||||
)
|
||||
|
||||
|
||||
class KRestartSamplerCustomNoise:
|
||||
DESCRIPTION = "Restart sampler variant that allows specifying a custom noise type for noise added by restarts."
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(KRestartSamplerCustomNoise_INPUT_TYPES_BUILDER)
|
||||
|
||||
RETURN_TYPES = ("LATENT", "LATENT")
|
||||
RETURN_NAMES = ("output", "denoised_output")
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "sampling"
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
*,
|
||||
model,
|
||||
add_noise,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
start_at_step,
|
||||
end_at_step,
|
||||
return_with_leftover_noise,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
chunked_mode=False,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
if restart is None:
|
||||
raise RuntimeError("Restart not available")
|
||||
return restart.restart_sampling.restart_sampling(
|
||||
model,
|
||||
noise_seed,
|
||||
steps,
|
||||
cfg,
|
||||
sampler,
|
||||
scheduler,
|
||||
positive,
|
||||
negative,
|
||||
latent_image,
|
||||
segments,
|
||||
restart_scheduler,
|
||||
disable_noise=add_noise == "disable",
|
||||
step_range=(start_at_step, end_at_step),
|
||||
force_full_denoise=return_with_leftover_noise != "enable",
|
||||
output_only=False,
|
||||
chunked_mode=chunked_mode,
|
||||
custom_noise=custom_noise_opt.make_noise_sampler
|
||||
if custom_noise_opt
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
class RestartSamplerCustomNoise:
|
||||
DESCRIPTION = "Wrapper used to make another sampler Restart compatible. Allows specifying a custom type for noise added by restarts."
|
||||
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
FUNCTION = "go"
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: SonarInputTypes()
|
||||
.req_sampler()
|
||||
.req_bool_chunked_mode(default=True)
|
||||
.opt_customnoise_custom_noise_opt(tooltip="Optional custom noise input."),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def go(cls, sampler, chunked_mode, custom_noise_opt=None):
|
||||
if restart is None or not hasattr(restart.restart_sampling, "RestartSampler"):
|
||||
raise RuntimeError("Restart not available")
|
||||
restart_options = {
|
||||
"restart_chunked": chunked_mode,
|
||||
"restart_wrapped_sampler": sampler,
|
||||
"restart_custom_noise": None
|
||||
if custom_noise_opt is None
|
||||
else custom_noise_opt.make_noise_sampler,
|
||||
}
|
||||
restart_sampler = samplers.KSAMPLER(
|
||||
restart.restart_sampling.RestartSampler.sampler_function,
|
||||
extra_options=sampler.extra_options | restart_options,
|
||||
inpaint_options=sampler.inpaint_options,
|
||||
)
|
||||
return (restart_sampler,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS |= {
|
||||
"KRestartSamplerCustomNoise": KRestartSamplerCustomNoise,
|
||||
"RestartSamplerCustomNoise": RestartSamplerCustomNoise,
|
||||
"SonarBlendFilterNoise": SonarBlendFilterNoiseNode,
|
||||
"SonarBlehOpsNoise": SonarBlehOpsNoiseNode,
|
||||
}
|
||||
|
||||
|
||||
def init_integrations(integrations):
|
||||
global restart, bleh # noqa: PLW0603
|
||||
restart = integrations.restart
|
||||
bleh = integrations.bleh
|
||||
|
||||
|
||||
external.MODULES.register_init_handler(init_integrations)
|
||||
@@ -0,0 +1,550 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import math
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .. import utils
|
||||
from ..external import IntegratedNode
|
||||
from ..latent_ops import (
|
||||
SonarLatentOperation,
|
||||
SonarLatentOperationAdvanced,
|
||||
SonarLatentOperationNoise,
|
||||
SonarLatentOperationSetSeed,
|
||||
)
|
||||
from .base import SonarInputTypes, SonarLazyInputTypes
|
||||
from .noise_filters import SonarQuantileFilteredNoiseNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
|
||||
|
||||
class SonarApplyLatentOperationCFG(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Allows applying a LATENT_OPERATION during sampling. ComfyUI has a few that are builtin and this node pack also includes: SonarLatentOperationQuantileFilter."
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
CATEGORY = "latent/advanced/operations"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
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(
|
||||
*,
|
||||
model_sampling: object,
|
||||
scale_mode: str,
|
||||
sigma: float,
|
||||
sigma_t_max: torch.Tensor,
|
||||
start_sigma: float,
|
||||
end_sigma: float,
|
||||
offset: float,
|
||||
min_pct: float,
|
||||
max_pct: float,
|
||||
) -> float | torch.Tensor:
|
||||
if scale_mode == "none":
|
||||
return 1.0
|
||||
if scale_mode in {"sampling", "sampling_sin", "reverse_sampling"}:
|
||||
rev_sampling_pct = (
|
||||
(model_sampling.timestep(sigma_t_max) / 999).clamp(0, 1).detach().item()
|
||||
)
|
||||
result = (
|
||||
1.0 - rev_sampling_pct if scale_mode == "sampling" else rev_sampling_pct
|
||||
)
|
||||
elif scale_mode in {
|
||||
"enabled_range",
|
||||
"enabled_range_sin",
|
||||
"reverse_enabled_range",
|
||||
}:
|
||||
rev_range_pct = (sigma - end_sigma) / (start_sigma - end_sigma)
|
||||
result = (
|
||||
1.0 - rev_range_pct if scale_mode == "enabled_range" else rev_range_pct
|
||||
)
|
||||
else:
|
||||
raise ValueError("Bad blend_scale_mode")
|
||||
if scale_mode.endswith("_sin"):
|
||||
result = math.sin(result * math.pi)
|
||||
return max(min_pct, min(result + offset, max_pct))
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
*,
|
||||
model,
|
||||
mode: str,
|
||||
pred_flip_mode: bool,
|
||||
require_uncond: bool,
|
||||
start_sigma: float,
|
||||
end_sigma: float,
|
||||
blend_mode: str,
|
||||
blend_strength: float,
|
||||
blend_scale_mode: str,
|
||||
blend_scale_offset: float,
|
||||
blend_scale_min: float,
|
||||
blend_scale_max: float,
|
||||
immediate_blend: bool,
|
||||
operation_1=None,
|
||||
operation_2=None,
|
||||
operation_3=None,
|
||||
operation_4=None,
|
||||
operation_5=None,
|
||||
) -> tuple:
|
||||
if mode == "model_input":
|
||||
if require_uncond:
|
||||
raise ValueError(
|
||||
"require_uncond does not make sense for the model_input mode.",
|
||||
)
|
||||
if pred_flip_mode:
|
||||
raise ValueError(
|
||||
"pred_flip does not make sense for the model_input mode.",
|
||||
)
|
||||
model = model.clone()
|
||||
operations = tuple(
|
||||
SonarLatentOperation(op=o)
|
||||
for o in (operation_1, operation_2, operation_3, operation_4, operation_5)
|
||||
if o is not None
|
||||
)
|
||||
if not operations:
|
||||
return (model,)
|
||||
ms = model.get_model_object("model_sampling")
|
||||
post_cfg_mode = mode in {"denoised", "denoised_sub_uncond"}
|
||||
blend_function = utils.BLENDING_MODES[blend_mode]
|
||||
sigma_max, sigma_min = (
|
||||
ms.sigma_max.detach().item(),
|
||||
ms.sigma_min.detach().item(),
|
||||
)
|
||||
if start_sigma < 0:
|
||||
start_sigma = sigma_max
|
||||
start_sigma = max(sigma_min, min(sigma_max, start_sigma))
|
||||
end_sigma = max(sigma_min, min(sigma_max, end_sigma))
|
||||
if end_sigma > start_sigma:
|
||||
start_sigma, end_sigma = end_sigma, start_sigma
|
||||
if start_sigma == end_sigma:
|
||||
blend_scale_mode = "none"
|
||||
orig_mode = mode
|
||||
|
||||
def patch(args: dict) -> torch.Tensor:
|
||||
nonlocal mode
|
||||
|
||||
x = args["input"]
|
||||
cond_scale = args.get("cond_scale")
|
||||
sigma_t = args["sigma"]
|
||||
sigma_t_max = sigma_t.max()
|
||||
if sigma_t.numel() > 1:
|
||||
shape_pad = (1,) * (x.ndim - sigma_t.ndim)
|
||||
sigma_t = sigma_t.reshape(sigma_t.shape[0], *shape_pad)
|
||||
sigma = sigma_t_max.detach().item()
|
||||
enabled = end_sigma <= sigma <= start_sigma
|
||||
conds_out = args.get("conds_out", ())
|
||||
uncond = (
|
||||
args.get("uncond_denoised")
|
||||
if post_cfg_mode
|
||||
else (conds_out[1] if len(conds_out) > 1 else None)
|
||||
)
|
||||
if uncond is None and (
|
||||
require_uncond
|
||||
or mode in {"uncond", "uncond_sub_cond", "denoised_sub_uncond"}
|
||||
):
|
||||
enabled = False
|
||||
if not enabled:
|
||||
if mode == "model_input":
|
||||
return x
|
||||
return args["denoised"] if post_cfg_mode else conds_out
|
||||
cond = conds_out[0] if not post_cfg_mode and len(conds_out) else None
|
||||
if uncond is None and mode.endswith("_sub_uncond"):
|
||||
mode = orig_mode.split("_", 1)[0]
|
||||
else:
|
||||
mode = orig_mode
|
||||
if mode == "model_input":
|
||||
t1 = x
|
||||
t2 = None
|
||||
elif mode in {"cond", "cond_sub_uncond"}:
|
||||
t1 = cond
|
||||
t2 = uncond if mode == "cond_sub_uncond" else None
|
||||
elif mode in {"uncond", "uncond_sub_cond"}:
|
||||
t1 = uncond
|
||||
t2 = cond if mode == "uncond_sub_cond" else None
|
||||
else:
|
||||
t1 = args["denoised"]
|
||||
t2 = uncond if mode == "denoised_sub_uncond" else None
|
||||
t1_orig = t1
|
||||
if pred_flip_mode:
|
||||
t1 = (x - t1) / sigma_t
|
||||
if t2 is not None:
|
||||
t2 = (x - t2) / sigma_t
|
||||
curr_blend = blend_strength * cls.get_blend_scaling(
|
||||
scale_mode=blend_scale_mode,
|
||||
offset=blend_scale_offset,
|
||||
min_pct=blend_scale_min,
|
||||
max_pct=blend_scale_max,
|
||||
model_sampling=args["model"].model_sampling,
|
||||
start_sigma=start_sigma,
|
||||
end_sigma=end_sigma,
|
||||
sigma=max(sigma_min, min(sigma, sigma_max)),
|
||||
sigma_t_max=sigma_t_max.clamp(sigma_min, sigma_max),
|
||||
)
|
||||
result = t1 - t2 if t2 is not None else t1.clone()
|
||||
for operation in operations:
|
||||
curr_result = operation(
|
||||
result,
|
||||
sigma=sigma,
|
||||
t2=t2,
|
||||
cond=cond,
|
||||
uncond=uncond,
|
||||
cond_scale=cond_scale,
|
||||
raw_args=args,
|
||||
)
|
||||
result = (
|
||||
blend_function(result, curr_result, curr_blend)
|
||||
if immediate_blend
|
||||
else curr_result
|
||||
)
|
||||
if t2 is not None:
|
||||
result += t2
|
||||
if pred_flip_mode:
|
||||
result = x - sigma_t * result
|
||||
if not immediate_blend:
|
||||
result = blend_function(t1_orig, result, curr_blend)
|
||||
if post_cfg_mode or mode == "model_input":
|
||||
return result
|
||||
conds_out = conds_out.copy()
|
||||
conds_out[0 if mode.startswith("cond") else 1] = result
|
||||
return conds_out
|
||||
|
||||
if post_cfg_mode:
|
||||
model.set_model_sampler_post_cfg_function(patch)
|
||||
elif mode == "model_input":
|
||||
|
||||
def patch_wrapper(apply_model, args: dict) -> torch.Tensor:
|
||||
timestep = args["timestep"]
|
||||
patch_args = args | {"sigma": timestep, "model": model.model}
|
||||
return apply_model(patch(patch_args), timestep, **args["c"])
|
||||
|
||||
model.set_model_unet_function_wrapper(patch_wrapper)
|
||||
else:
|
||||
model.set_model_sampler_pre_cfg_function(patch)
|
||||
return (model,)
|
||||
|
||||
|
||||
class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode):
|
||||
DESCRIPTION = "Allows applying a quantile normalization function to the latent during sampling. Can be used with Sonar SonarApplyLatentOperationCFG. The just copies most of the parameters from the other quantile normalization node where it talks to 'noise', this will apply to whatever you're applying the latent operation to (denoised, uncond, etc)."
|
||||
RETURN_TYPES = ("LATENT_OPERATION",)
|
||||
CATEGORY = "latent/advanced/operations"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result.pop("optional", None)
|
||||
reqparams = result["required"]
|
||||
for k in ("custom_noise", "normalize", "normalize_noise", "factor"):
|
||||
reqparams.pop(k, None)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
*,
|
||||
quantile: float,
|
||||
dim: str,
|
||||
flatten: bool,
|
||||
norm_power: float,
|
||||
norm_factor: float,
|
||||
strategy: str,
|
||||
):
|
||||
qnorm_filter = functools.partial(
|
||||
utils.quantile_normalize,
|
||||
quantile=quantile,
|
||||
dim=None if dim == "global" else int(dim),
|
||||
flatten=flatten,
|
||||
nq_fac=norm_factor,
|
||||
pow_fac=norm_power,
|
||||
strategy=strategy,
|
||||
)
|
||||
|
||||
return (SonarLatentOperation(op=lambda latent: qnorm_filter(latent)),) # noqa: PLW0108
|
||||
|
||||
|
||||
class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode):
|
||||
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"
|
||||
|
||||
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(
|
||||
cls,
|
||||
*,
|
||||
operation,
|
||||
start_sigma: float,
|
||||
end_sigma: float,
|
||||
input_multiplier: float,
|
||||
output_multiplier: float,
|
||||
difference_multiplier: float,
|
||||
blend_mode: str,
|
||||
blend_strength: float,
|
||||
operation_alt=None,
|
||||
operation_2=None,
|
||||
operation_3=None,
|
||||
operation_4=None,
|
||||
operation_5=None,
|
||||
) -> tuple[SonarLatentOperationAdvanced]:
|
||||
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,
|
||||
):
|
||||
operation_alt = SonarLatentOperation(op=operation_alt)
|
||||
return (
|
||||
SonarLatentOperationAdvanced(
|
||||
ops=operations,
|
||||
op_alt=operation_alt,
|
||||
start_sigma=start_sigma,
|
||||
end_sigma=end_sigma,
|
||||
input_multiplier=input_multiplier,
|
||||
output_multiplier=output_multiplier,
|
||||
difference_multiplier=difference_multiplier,
|
||||
blend_mode=blend_mode,
|
||||
blend_strength=blend_strength,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SonarLatentOperationNoiseNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Latent operation that allows injecting noise."
|
||||
RETURN_TYPES = ("LATENT_OPERATION",)
|
||||
CATEGORY = "latent/advanced/operations"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
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(
|
||||
cls,
|
||||
*,
|
||||
custom_noise,
|
||||
scale_to_sigma: bool,
|
||||
cpu_noise: bool,
|
||||
normalize: bool,
|
||||
lazy_noise_sampler: bool,
|
||||
) -> tuple[SonarLatentOperationNoise]:
|
||||
return (
|
||||
SonarLatentOperationNoise(
|
||||
custom_noise=custom_noise,
|
||||
scale_to_sigma=scale_to_sigma,
|
||||
cpu_noise=cpu_noise,
|
||||
normalize=normalize,
|
||||
lazy_noise_sampler=lazy_noise_sampler,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SonarLatentOperationSetSeedNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Latent operation that allows setting a seed. Can be useful for running latent operations that generate noise outside of a normal sampling context (i.e. operations on the initial latent before sampling)."
|
||||
RETURN_TYPES = ("LATENT_OPERATION",)
|
||||
CATEGORY = "latent/advanced/operations"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
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(
|
||||
cls,
|
||||
*,
|
||||
operation,
|
||||
seed: int,
|
||||
restore_rng_state: bool,
|
||||
) -> tuple[SonarLatentOperationSetSeed]:
|
||||
return (
|
||||
SonarLatentOperationSetSeed(
|
||||
op=operation,
|
||||
seed=seed,
|
||||
restore_rng_state=restore_rng_state,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SonarApplyLatentOperationCFG": SonarApplyLatentOperationCFG,
|
||||
"SonarLatentOperationQuantileFilter": SonarLatentOperationQuantileFilter,
|
||||
"SonarLatentOperationAdvanced": SonarLatentOperationAdvancedNode,
|
||||
"SonarLatentOperationNoise": SonarLatentOperationNoiseNode,
|
||||
"SonarLatentOperationSetSeed": SonarLatentOperationSetSeedNode,
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import math
|
||||
import random
|
||||
from typing import Any, Callable
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
from comfy import model_management, samplers
|
||||
from tqdm import tqdm
|
||||
|
||||
from .. import noise, utils
|
||||
from ..external import IntegratedNode
|
||||
from ..noise import NoiseType
|
||||
from ..wavelet_cfg import WaveletCFG, WCFGRules
|
||||
from .base import (
|
||||
NoiseChainInputTypes,
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarInputTypes,
|
||||
SonarLazyInputTypes,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
)
|
||||
|
||||
|
||||
class NoisyLatentLikeNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Allows generating noise (and optionally adding it) based on a reference latent. Note: For img2img workflows, you will generally want to enable add_to_latent as well as connecting the model and sigmas inputs."
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
OUTPUT_TOOLTIPS = ("The noisy latent image.",)
|
||||
CATEGORY = "latent/noise"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
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(
|
||||
cls,
|
||||
*,
|
||||
noise_type: str,
|
||||
seed: int | None,
|
||||
latent: dict,
|
||||
multiplier: float = 1.0,
|
||||
add_to_latent=False,
|
||||
repeat_batch=1,
|
||||
cpu_noise=True,
|
||||
normalize=True,
|
||||
custom_noise_opt: object | None = None,
|
||||
mul_by_sigmas_opt: torch.Tensor | None = None,
|
||||
model_opt: object | None = None,
|
||||
):
|
||||
model, sigmas = model_opt, mul_by_sigmas_opt
|
||||
if sigmas is not None and len(sigmas) > 0:
|
||||
if model is None:
|
||||
raise ValueError(
|
||||
"NoisyLatentLike requires a model when sigmas are connected!",
|
||||
)
|
||||
while hasattr(model, "model"):
|
||||
model = model.model
|
||||
latent_scale_factor = model.latent_format.scale_factor
|
||||
model_sigma_max = float(model.model_sampling.sigma_max)
|
||||
first_sigma = float(sigmas[0])
|
||||
max_denoise = (
|
||||
math.isclose(model_sigma_max, first_sigma, rel_tol=1e-05)
|
||||
or first_sigma > model_sigma_max
|
||||
)
|
||||
multiplier *= (
|
||||
float(
|
||||
torch.sqrt(1.0 + sigmas[0] ** 2.0) if max_denoise else sigmas[0],
|
||||
)
|
||||
/ latent_scale_factor
|
||||
)
|
||||
if sigmas is not None and sigmas.numel() > 1:
|
||||
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
|
||||
sigma, sigma_next = sigmas[0], sigmas[1]
|
||||
else:
|
||||
sigma_min, sigma_max, sigma, sigma_next = (None,) * 4
|
||||
latent_samples = latent["samples"]
|
||||
orig_device = latent_samples.device
|
||||
want_device = (
|
||||
torch.device("cpu") if cpu_noise else model_management.get_torch_device()
|
||||
)
|
||||
if latent_samples.device != want_device:
|
||||
latent_samples = latent_samples.detach().clone().to(want_device)
|
||||
if custom_noise_opt is not None:
|
||||
ns = custom_noise_opt.make_noise_sampler(
|
||||
latent_samples,
|
||||
sigma_min=sigma_min,
|
||||
sigma_max=sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu_noise,
|
||||
normalized=normalize,
|
||||
)
|
||||
else:
|
||||
ns = noise.get_noise_sampler(
|
||||
NoiseType[noise_type.upper()],
|
||||
latent_samples,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu_noise,
|
||||
normalized=normalize,
|
||||
)
|
||||
randst = torch.random.get_rng_state()
|
||||
try:
|
||||
torch.random.manual_seed(seed)
|
||||
result = torch.cat(
|
||||
tuple(ns(sigma, sigma_next) for _ in range(repeat_batch)),
|
||||
dim=0,
|
||||
)
|
||||
finally:
|
||||
torch.random.set_rng_state(randst)
|
||||
result = utils.scale_noise(result, multiplier, normalized=True)
|
||||
if add_to_latent:
|
||||
result += latent_samples.repeat(
|
||||
*(repeat_batch if i == 0 else 1 for i in range(latent_samples.ndim)),
|
||||
).to(result)
|
||||
result = result.to(orig_device)
|
||||
return ({"samples": result},)
|
||||
|
||||
|
||||
class SonarNoiseImageNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Allows adding noise to an image or generating images full of noise."
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
CATEGORY = "image"
|
||||
|
||||
FUNCTION = "go"
|
||||
|
||||
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(
|
||||
cls,
|
||||
*,
|
||||
noise_type: str,
|
||||
seed: int,
|
||||
image: torch.Tensor,
|
||||
noise_multiplier: float,
|
||||
noise_min: float,
|
||||
noise_max: float,
|
||||
channel_mode: str,
|
||||
blend_mode: str,
|
||||
blend_strength: float,
|
||||
overflow_mode: str,
|
||||
greyscale_mode: bool,
|
||||
dtype: str,
|
||||
pure_noise_mode: bool,
|
||||
cpu_noise: bool,
|
||||
normalize: bool,
|
||||
custom_noise_opt: object | None = None,
|
||||
):
|
||||
sigma_min, sigma_max, sigma, sigma_next = (None,) * 4
|
||||
orig_image = image = (
|
||||
torch.zeros_like(image) if pure_noise_mode else image.detach().clone()
|
||||
)
|
||||
if image.ndim == 3:
|
||||
image = image.unsqueeze(0)
|
||||
elif image.ndim != 4:
|
||||
errstr = (
|
||||
f"Expected image tensor with 3 or 4 dimensions, got {image.ndim}",
|
||||
)
|
||||
raise ValueError(errstr)
|
||||
blend_function = (
|
||||
utils.BLENDING_MODES[blend_mode]
|
||||
if blend_mode != "simple_add"
|
||||
else lambda a, b, _t: a + b
|
||||
)
|
||||
if noise_min > noise_max:
|
||||
noise_min, noise_max = noise_max, noise_min
|
||||
image = image.movedim(-1, 1)
|
||||
channels = image.shape[1]
|
||||
channel_map = {"R": 0, "B": 1, "G": 2, "A": 3}
|
||||
channel_mode = channel_mode.upper()
|
||||
if channels == 3 or channels == 4: # noqa: PLR1714
|
||||
channel_targets = tuple(
|
||||
channel_map[c]
|
||||
for c in "RGBA"
|
||||
if c in channel_mode and channel_map[c] < channels
|
||||
)
|
||||
else:
|
||||
channel_targets = tuple(range(channels))
|
||||
want_device = (
|
||||
torch.device("cpu") if cpu_noise else model_management.get_torch_device()
|
||||
)
|
||||
image = image.to(
|
||||
device=want_device,
|
||||
dtype={
|
||||
"float32": torch.float32,
|
||||
"float64": torch.float64,
|
||||
"bfloat16": torch.bfloat16,
|
||||
"float16": torch.float16,
|
||||
}.get(
|
||||
dtype,
|
||||
image.dtype,
|
||||
),
|
||||
)
|
||||
pyrandst = random.getstate()
|
||||
randst = torch.random.get_rng_state()
|
||||
try:
|
||||
random.seed(seed)
|
||||
torch.random.manual_seed(seed)
|
||||
if custom_noise_opt is not None:
|
||||
ns = custom_noise_opt.make_noise_sampler(
|
||||
image,
|
||||
sigma_min=sigma_min,
|
||||
sigma_max=sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu_noise,
|
||||
normalized=normalize,
|
||||
)
|
||||
else:
|
||||
ns = noise.get_noise_sampler(
|
||||
NoiseType[noise_type.upper()],
|
||||
image,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu_noise,
|
||||
normalized=normalize,
|
||||
)
|
||||
result = ns(sigma, sigma_next)
|
||||
finally:
|
||||
torch.random.set_rng_state(randst)
|
||||
random.setstate(pyrandst)
|
||||
del ns
|
||||
result = utils.scale_noise(result, normalized=True)
|
||||
if greyscale_mode:
|
||||
result = result.mean(dim=1, keepdim=True).expand(image.shape).contiguous()
|
||||
if noise_max != 0 and noise_min != noise_max: # noqa: PLR1714
|
||||
result = utils.normalize_to_scale(result, noise_min, noise_max)
|
||||
result *= noise_multiplier
|
||||
image[:, channel_targets, ...] = blend_function(
|
||||
image[:, channel_targets, ...],
|
||||
result[:, channel_targets, ...],
|
||||
blend_strength,
|
||||
)
|
||||
if overflow_mode == "rescale":
|
||||
image = utils.normalize_to_scale(image, 0.0, 1.0)
|
||||
else:
|
||||
image = image.clip_(0, 1)
|
||||
image = image.movedim(1, -1).to(
|
||||
device=orig_image.device,
|
||||
dtype=orig_image.dtype,
|
||||
)
|
||||
return (image,)
|
||||
|
||||
|
||||
class CustomNOISE:
|
||||
def __init__(
|
||||
self,
|
||||
custom_noise,
|
||||
seed,
|
||||
*,
|
||||
cpu_noise=True,
|
||||
normalize=True,
|
||||
multiplier=1.0,
|
||||
):
|
||||
self.custom_noise = custom_noise
|
||||
self.seed = seed
|
||||
self.cpu_noise = cpu_noise
|
||||
self.normalize = normalize
|
||||
self.multiplier = multiplier
|
||||
|
||||
def _sample_noise(self, latent_image, seed):
|
||||
result = self.custom_noise.make_noise_sampler(
|
||||
latent_image,
|
||||
None,
|
||||
None,
|
||||
seed=seed,
|
||||
cpu=self.cpu_noise,
|
||||
normalized=self.normalize,
|
||||
)(None, None).to(
|
||||
device="cpu",
|
||||
dtype=latent_image.dtype,
|
||||
)
|
||||
if result.layout != latent_image.layout:
|
||||
if latent_image.layout == torch.sparse_coo:
|
||||
return result.to_sparse()
|
||||
errstr = f"Cannot handle latent layout {type(latent_image.layout).__name__}"
|
||||
raise NotImplementedError(errstr)
|
||||
return result if self.multiplier == 1.0 else result.mul_(self.multiplier)
|
||||
|
||||
def generate_noise(self, input_latent):
|
||||
latent_image = input_latent["samples"]
|
||||
batch_inds = input_latent.get("batch_index")
|
||||
torch.manual_seed(self.seed)
|
||||
random.seed(self.seed)
|
||||
if self.multiplier == 0.0:
|
||||
return torch.zeros(
|
||||
latent_image.shape,
|
||||
dtype=latent_image.dtype,
|
||||
layout=latent_image.layout,
|
||||
device="cpu",
|
||||
)
|
||||
if batch_inds is None:
|
||||
return self._sample_noise(latent_image, self.seed)
|
||||
unique_inds, inverse_inds = np.unique(batch_inds, return_inverse=True)
|
||||
result = []
|
||||
batch_size = latent_image.shape[0]
|
||||
for idx in range(unique_inds[-1] + 1):
|
||||
noise = self._sample_noise(
|
||||
latent_image[idx % batch_size].unsqueeze(0),
|
||||
self.seed + idx,
|
||||
)
|
||||
if idx in unique_inds:
|
||||
result.append(noise)
|
||||
return torch.cat(tuple(result[i] for i in inverse_inds), axis=0)
|
||||
|
||||
|
||||
class SonarToComfyNOISENode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Allows converting SONAR_CUSTOM_NOISE to NOISE (used by SamplerCustomAdvanced and possibly other custom samplers). NOTE: Does not work with noise types that depend on sigma (Brownian, ScheduledNoise, etc)."
|
||||
RETURN_TYPES = ("NOISE",)
|
||||
CATEGORY = "sampling/custom_sampling/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
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):
|
||||
return (
|
||||
CustomNOISE(
|
||||
custom_noise,
|
||||
seed,
|
||||
cpu_noise=cpu_noise,
|
||||
normalize=normalize,
|
||||
multiplier=multiplier,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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."
|
||||
|
||||
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"
|
||||
|
||||
FUNCTION = "get_sampler"
|
||||
|
||||
def get_sampler(
|
||||
self,
|
||||
*,
|
||||
sampler,
|
||||
eta,
|
||||
s_noise,
|
||||
s_churn,
|
||||
r,
|
||||
sde_solver,
|
||||
cpu_noise=True,
|
||||
noise_type=None,
|
||||
custom_noise_opt=None,
|
||||
normalize=True,
|
||||
yaml_parameters="",
|
||||
):
|
||||
sampler_kwargs = {
|
||||
"s_noise": s_noise,
|
||||
"eta": eta,
|
||||
"s_churn": s_churn,
|
||||
"r": r,
|
||||
"solver_type": sde_solver,
|
||||
}
|
||||
if yaml_parameters:
|
||||
extra_params = yaml.safe_load(yaml_parameters)
|
||||
if extra_params is None:
|
||||
pass
|
||||
elif not isinstance(extra_params, dict):
|
||||
raise ValueError(
|
||||
"SamplerConfigOverride: yaml_parameters must either be null or an object",
|
||||
)
|
||||
else:
|
||||
sampler_kwargs |= extra_params
|
||||
sampler_function = functools.update_wrapper(
|
||||
functools.partial(
|
||||
self.sampler_function,
|
||||
override_sampler_cfg={
|
||||
"sampler": sampler,
|
||||
"noise_type": NoiseType[noise_type.upper()]
|
||||
if noise_type not in {None, "DEFAULT"}
|
||||
else None,
|
||||
"custom_noise": custom_noise_opt,
|
||||
"sampler_kwargs": sampler_kwargs,
|
||||
"cpu_noise": cpu_noise,
|
||||
"normalize": normalize,
|
||||
},
|
||||
),
|
||||
sampler.sampler_function,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
sampler_function,
|
||||
extra_options=sampler.extra_options.copy(),
|
||||
inpaint_options=sampler.inpaint_options.copy(),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sampler_function(
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
*args: list[Any],
|
||||
override_sampler_cfg: dict[str, Any] | None = None,
|
||||
noise_sampler: Callable | None = None,
|
||||
extra_args: dict[str, Any] | None = None,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> torch.Tensor:
|
||||
if not override_sampler_cfg:
|
||||
raise ValueError("Override sampler config missing!")
|
||||
if extra_args is None:
|
||||
extra_args = {}
|
||||
cfg = override_sampler_cfg
|
||||
sampler, sampler_kwargs, noise_type, custom_noise, cpu, normalize = (
|
||||
cfg["sampler"],
|
||||
cfg["sampler_kwargs"],
|
||||
cfg.get("noise_type"),
|
||||
cfg.get("custom_noise"),
|
||||
cfg.get("cpu_noise", True),
|
||||
cfg.get("normalize", True),
|
||||
)
|
||||
sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max()
|
||||
sig = inspect.signature(sampler.sampler_function)
|
||||
params = sig.parameters
|
||||
if "noise_sampler" in params:
|
||||
seed = extra_args.get("seed")
|
||||
if custom_noise is not None:
|
||||
noise_sampler = custom_noise.make_noise_sampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
normalized=normalize,
|
||||
)
|
||||
elif noise_type is not None:
|
||||
noise_sampler = noise.get_noise_sampler(
|
||||
noise_type,
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
normalized=normalize,
|
||||
)
|
||||
kwargs |= {k: v for k, v in sampler_kwargs.items() if k in params}
|
||||
if "noise_sampler" in params:
|
||||
kwargs["noise_sampler"] = noise_sampler
|
||||
return sampler.sampler_function(
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
*args,
|
||||
extra_args=extra_args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
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."
|
||||
|
||||
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):
|
||||
return noise.BlendedNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
sonar_custom_noise_opt=None,
|
||||
normalize,
|
||||
custom_noise=None,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
blend_function=lambda a, _b, _t: a,
|
||||
normalize=self.get_normalize(normalize),
|
||||
custom_noise_1=custom_noise,
|
||||
custom_noise_2=None,
|
||||
noise_2_percent=0.0,
|
||||
)
|
||||
|
||||
|
||||
class SonarWaveletCFGNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Wavelet CFG function that allows you to apply different CFG strength to different frequencies."
|
||||
CATEGORY = "model_patches"
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "go"
|
||||
|
||||
_yaml_placeholder = """# YAML or JSON here.
|
||||
# I recommend reading the documentation at https://github.com/blepping/ComfyUI-sonar/docs/waveletcfg.md
|
||||
# For wavelet information, see: https://pytorch-wavelets.readthedocs.io/en/latest/index.html
|
||||
|
||||
# You may override the fields from the node like start_sigma here.
|
||||
|
||||
# This section is basically the CFG scale. (All scales sections use the same format.)
|
||||
difference:
|
||||
# Scale for the low frequency components.
|
||||
yl_scale: 5.0
|
||||
|
||||
# Scale (or scales) for high frequency components.
|
||||
# This can be scalar or a list or list of lists.
|
||||
# List example:
|
||||
# yh_scales:
|
||||
# - [1, 2, 3]
|
||||
# - fill
|
||||
# - 5
|
||||
# You can separately apply a scale to items equal to the wavelet level. Levels go from fine to coarse.
|
||||
# If the item is a list, the three items correspond to horizontal, vertical, diagonal for DWT. (DTCWT has 6.)
|
||||
# You can have one "fill" item, this will replicate the item before it however many times is necessary to
|
||||
# match the wavelet level.
|
||||
yh_scales: 3.0
|
||||
|
||||
# You can optionally include a scales_end block with yl_scale/yh_scales.
|
||||
# to interpolate from the toplevel scales (can also be in a scales_start blockx if you prefer).
|
||||
|
||||
# scales_end:
|
||||
# yl_scale: 1.0
|
||||
# yh_scales: 1.0
|
||||
|
||||
# The following scheduling parameters only apply if scales_end exists.
|
||||
|
||||
# One of linear, logarithmic, exponential, half_cosine, sine
|
||||
# Sine mode will hit the peak scales_after values in the middle of the range.
|
||||
schedule: linear
|
||||
|
||||
# One of: sampling, enabled_sampling, sigmas, enabled_sigmas, step, enabled_steps
|
||||
schedule_mode: sampling
|
||||
|
||||
# When enabled, flips the schedule percentage. This happens before the schedule is applied
|
||||
# or any offset/multiplier stuff. If you want to flip the final result you can do something like
|
||||
# schedule_offset_after: -1.0 and schedule_multiplier_after: -1.0
|
||||
reverse_schedule: false
|
||||
|
||||
# Added to the percentage before the schedule function is applied.
|
||||
schedule_offset: 0.0
|
||||
|
||||
# Applied to the percentage before the schedule function (but after the offset).
|
||||
schedule_multiplier: 1.0
|
||||
|
||||
# Added to the percentage after the schedule function is applied.
|
||||
schedule_offset_after: 0.0
|
||||
|
||||
# Applied to the percentage after the schedule function (but after the offset).
|
||||
schedule_multiplier_after: 1.0
|
||||
|
||||
# Min/max for the final calculated percent. Must be between 0 and 1.
|
||||
schedule_min: 0.0
|
||||
schedule_max: 1.0
|
||||
|
||||
# If you're a crazy person, you can use non-standard blend modes for interpolating
|
||||
# the scales. Not recommended.
|
||||
blend_mode: lerp
|
||||
|
||||
|
||||
# Wavelet type
|
||||
wave: db4
|
||||
|
||||
# Wavelet level
|
||||
level: 5
|
||||
|
||||
### Start of advanced options
|
||||
|
||||
# Mode used for padding
|
||||
padding_mode: symmetric
|
||||
|
||||
# Mutually exclusive with DTCWT mode.
|
||||
use_1d_dwt: false
|
||||
|
||||
# Enables DTCWT mode.
|
||||
use_dtcwt: false
|
||||
|
||||
# Configuration for DTCWT, only relevant when enabled.
|
||||
biort: near_sym_a
|
||||
qshift: qshift_a
|
||||
|
||||
# It's also possible to set these wavelet options with an "inv_"
|
||||
# prefix: mode, biort, qshift, wave, padding_mode
|
||||
|
||||
# One of: noise_norm, noise, denoised
|
||||
# Normal CFG uses denoised mode. noise_norm divides by the current sigma, noise just uses the raw noise prediction.
|
||||
target_mode: denoised
|
||||
|
||||
# Can be used to scale cond before the difference is calculated.
|
||||
cond:
|
||||
yl_scale: 1.0
|
||||
yh_scales: 1.0
|
||||
|
||||
# Can be used to scale uncond before the difference is calculated.
|
||||
uncond:
|
||||
yl_scale: 1.0
|
||||
yh_scales: 1.0
|
||||
|
||||
# Can be used to scale the final result after blending.
|
||||
final:
|
||||
yl_scale: 1.0
|
||||
yh_scales: 1.0
|
||||
|
||||
# Uses float64 for the wavelets/scaling/blending operations.
|
||||
# It doesn't seem to hurt performance much, but you can disable it if you want.
|
||||
high_precision_mode: true
|
||||
|
||||
# Inject is just addition which is usually what you want. The normal CFG function is:
|
||||
# uncond + (cond - uncond) * cfg_scale
|
||||
difference_blend_mode: inject
|
||||
difference_blend_strength: 1.0
|
||||
|
||||
# Per-rule value, can be enabled to spam your console with information when
|
||||
# rules activate, dump exactly what high/low scales are used, etc.
|
||||
verbose: false
|
||||
|
||||
# You may include a rules block which is a list of these configuration definitions.
|
||||
# Include start_sigma/end_sigma parameters. The first matching definition will be used.
|
||||
# rules:
|
||||
# - start_sigma: -1.0
|
||||
"""
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda _yaml_placeholder=_yaml_placeholder: SonarInputTypes()
|
||||
.req_model()
|
||||
.req_float_start_sigma(
|
||||
default=-1.0,
|
||||
min=-1.0,
|
||||
tooltip="First sigma wavelet CFG will be used.",
|
||||
)
|
||||
.req_float_end_sigma(
|
||||
default=0.0,
|
||||
min=0.0,
|
||||
tooltip="Last sigma wavelet CFG will be used.",
|
||||
)
|
||||
.req_field_fallback_mode(
|
||||
("existing", "own"),
|
||||
default="existing",
|
||||
tooltip="Existing mode uses whatever CFG function existed set when this model patch was applied. Own mode does the CFG calculation on its own. The scale will be whatever you set in your guider or sampler.",
|
||||
)
|
||||
.req_selectblend_blend_mode(
|
||||
tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.",
|
||||
)
|
||||
.req_float_blend_strength(
|
||||
default=1.0,
|
||||
tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.",
|
||||
)
|
||||
.req_yaml(default=_yaml_placeholder)
|
||||
.opt_field_operation_cond(
|
||||
"LATENT_OPERATION",
|
||||
tooltip="Optional latent operation that will be applied to cond. Note: Latent operations only apply if a rule matches.",
|
||||
)
|
||||
.opt_field_operation_uncond(
|
||||
"LATENT_OPERATION",
|
||||
tooltip="Optional latent operation that will be applied to uncond. Note: Latent operations only apply if a rule matches.",
|
||||
)
|
||||
.opt_field_operation_fallback_cfg(
|
||||
"LATENT_OPERATION",
|
||||
tooltip="Optional latent operation that will be applied to the fallback (non-wavelet) CFG result. Note: Latent operations only apply if a rule matches.",
|
||||
)
|
||||
.opt_field_operation_wavelet_cfg(
|
||||
"LATENT_OPERATION",
|
||||
tooltip="Optional latent operation that will be applied to wavelet CFG result. Note: Latent operations only apply if a rule matches.",
|
||||
)
|
||||
.opt_field_operation_result(
|
||||
"LATENT_OPERATION",
|
||||
tooltip="Optional latent operation that will be applied to the final result, after wavelet and normal CFG are potentially blended. Note: Latent operations only apply if a rule matches.",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
*,
|
||||
model: object,
|
||||
start_sigma: float,
|
||||
end_sigma: float,
|
||||
fallback_mode: str,
|
||||
blend_mode: str,
|
||||
blend_strength: float,
|
||||
yaml_parameters: str,
|
||||
operation_cond: Callable | None = None,
|
||||
operation_uncond: Callable | None = None,
|
||||
operation_fallback_cfg: Callable | None = None,
|
||||
operation_wavelet_cfg: Callable | None = None,
|
||||
operation_result: Callable | None = None,
|
||||
_override_rules_dict: dict | None = None,
|
||||
) -> tuple[object]:
|
||||
if start_sigma < 0:
|
||||
start_sigma = math.inf
|
||||
if _override_rules_dict is not None:
|
||||
wavelet_params = _override_rules_dict.copy()
|
||||
else:
|
||||
wavelet_params = yaml.safe_load(yaml_parameters)
|
||||
rules = WCFGRules.build(
|
||||
**(
|
||||
{
|
||||
"start_sigma": start_sigma,
|
||||
"end_sigma": end_sigma,
|
||||
"fallback_existing": fallback_mode == "existing",
|
||||
"blend_mode": blend_mode,
|
||||
"blend_strength": blend_strength,
|
||||
}
|
||||
| wavelet_params
|
||||
),
|
||||
)
|
||||
if len(rules) and rules[0].verbose:
|
||||
tqdm.write(f"\nWCFG: Using rules: {rules}\n")
|
||||
model = model.clone()
|
||||
model.set_model_sampler_cfg_function(
|
||||
WaveletCFG(
|
||||
existing_cfg=model.model_options.get("sampler_cfg_function"),
|
||||
rules=rules,
|
||||
operation_cond=operation_cond,
|
||||
operation_uncond=operation_uncond,
|
||||
operation_fallback_cfg=operation_fallback_cfg,
|
||||
operation_wavelet_cfg=operation_wavelet_cfg,
|
||||
operation_result=operation_result,
|
||||
),
|
||||
)
|
||||
return (model,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"NoisyLatentLike": NoisyLatentLikeNode,
|
||||
"SamplerConfigOverride": SamplerNodeConfigOverride,
|
||||
"SONAR_CUSTOM_NOISE to NOISE": SonarToComfyNOISENode,
|
||||
"SonarNoiseImage": SonarNoiseImageNode,
|
||||
"SonarSplitNoiseChain": SonarSplitNoiseChainNode,
|
||||
"SonarWaveletCFG": SonarWaveletCFGNode,
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comfy import samplers
|
||||
|
||||
from ..external import IntegratedNode
|
||||
from ..noise import NoiseType
|
||||
from ..sonar import (
|
||||
GuidanceConfig,
|
||||
GuidanceType,
|
||||
HistoryType,
|
||||
SonarConfig,
|
||||
SonarDPMPPSDE,
|
||||
SonarEuler,
|
||||
SonarEulerAncestral,
|
||||
)
|
||||
from .base import SonarInputTypes, SonarLazyInputTypes
|
||||
|
||||
|
||||
class GuidanceConfigNode(metaclass=IntegratedNode):
|
||||
DESCRIPTION = "Allows specifying extended guidance parameters for Sonar samplers."
|
||||
|
||||
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"
|
||||
|
||||
FUNCTION = "make_guidance_cfg"
|
||||
|
||||
@classmethod
|
||||
def make_guidance_cfg(
|
||||
cls,
|
||||
guidance_type,
|
||||
factor,
|
||||
start_step,
|
||||
end_step,
|
||||
latent,
|
||||
):
|
||||
return (
|
||||
GuidanceConfig(
|
||||
guidance_type=GuidanceType[guidance_type.upper()],
|
||||
factor=factor,
|
||||
start_step=start_step,
|
||||
end_step=end_step,
|
||||
latent=latent.get("samples"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeSonarBase:
|
||||
DESCRIPTION = "Sonar - momentum based sampler node."
|
||||
|
||||
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_rand_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"
|
||||
|
||||
|
||||
class SamplerNodeSonarEuler(SamplerNodeSonarBase):
|
||||
RETURN_TYPES = ("SAMPLER",)
|
||||
CATEGORY = "sampling/custom_sampling/samplers"
|
||||
|
||||
FUNCTION = "get_sampler"
|
||||
|
||||
@classmethod
|
||||
def get_sampler(
|
||||
cls,
|
||||
*,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
guidance_cfg_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (samplers.KSAMPLER(SonarEuler.sampler, {"sonar_config": cfg}),)
|
||||
|
||||
|
||||
class SamplerNodeSonarEulerAncestral(SamplerNodeSonarEuler):
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: SonarInputTypes(parent=SamplerNodeSonarEuler)
|
||||
.req_float_s_noise(
|
||||
default=1.0,
|
||||
tooltip="Multiplier for noise added during ancestral or SDE sampling.",
|
||||
)
|
||||
.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_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(
|
||||
cls,
|
||||
*,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
noise_type,
|
||||
eta,
|
||||
s_noise,
|
||||
guidance_cfg_opt=None,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
SonarEulerAncestral.sampler,
|
||||
{
|
||||
"sonar_config": cfg,
|
||||
"eta": eta,
|
||||
"s_noise": s_noise,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SamplerNodeSonarDPMPPSDE(SamplerNodeSonarEulerAncestral):
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: SonarInputTypes(
|
||||
parent=SamplerNodeSonarEulerAncestral,
|
||||
).req_selectnoise_noise_type(default="brownian"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_sampler(
|
||||
cls,
|
||||
*,
|
||||
momentum,
|
||||
momentum_hist,
|
||||
momentum_init,
|
||||
direction,
|
||||
rand_init_noise_type,
|
||||
noise_type,
|
||||
eta,
|
||||
s_noise,
|
||||
guidance_cfg_opt=None,
|
||||
custom_noise_opt=None,
|
||||
):
|
||||
cfg = SonarConfig(
|
||||
momentum=momentum,
|
||||
init=HistoryType[momentum_init.upper()],
|
||||
momentum_hist=momentum_hist,
|
||||
direction=direction,
|
||||
rand_init_noise_type=NoiseType[rand_init_noise_type.upper()],
|
||||
noise_type=NoiseType[noise_type.upper()],
|
||||
custom_noise=custom_noise_opt.clone() if custom_noise_opt else None,
|
||||
guidance=guidance_cfg_opt,
|
||||
)
|
||||
return (
|
||||
samplers.KSAMPLER(
|
||||
SonarDPMPPSDE.sampler,
|
||||
{
|
||||
"sonar_config": cfg,
|
||||
"eta": eta,
|
||||
"s_noise": s_noise,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SamplerSonarEuler": SamplerNodeSonarEuler,
|
||||
"SamplerSonarEulerA": SamplerNodeSonarEulerAncestral,
|
||||
"SamplerSonarDPMPPSDE": SamplerNodeSonarDPMPPSDE,
|
||||
"SonarGuidanceConfig": GuidanceConfigNode,
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from .. import noise, utils
|
||||
from ..noise_generation import DistroNoiseGenerator, VoronoiNoiseGenerator
|
||||
from .base import (
|
||||
NoiseChainInputTypes,
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarLazyInputTypes,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
)
|
||||
|
||||
|
||||
class SonarAdvancedPyramidNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = (
|
||||
"Custom noise type that allows specifying parameters for Pyramid variants."
|
||||
)
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda: NoiseChainInputTypes()
|
||||
.req_field_variant(
|
||||
(
|
||||
"highres_pyramid",
|
||||
"pyramid",
|
||||
"pyramid_old",
|
||||
),
|
||||
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):
|
||||
return noise.AdvancedPyramidNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
variant,
|
||||
iterations,
|
||||
discount,
|
||||
upscale_mode,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
variant=variant,
|
||||
iterations=iterations if iterations != -1 else None,
|
||||
discount=discount if discount != 0 else None,
|
||||
upscale_mode=upscale_mode if upscale_mode != "default" else None,
|
||||
)
|
||||
|
||||
|
||||
class SonarAdvanced1fNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = "Custom noise type that allows specifying parameters for 1f (pink, green, etc) variants."
|
||||
|
||||
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):
|
||||
return noise.Advanced1fNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
alpha,
|
||||
k,
|
||||
vertical_factor,
|
||||
horizontal_factor,
|
||||
use_sqrt,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
alpha=alpha,
|
||||
k=k,
|
||||
hfac=vertical_factor,
|
||||
wfac=horizontal_factor,
|
||||
use_sqrt=use_sqrt,
|
||||
)
|
||||
|
||||
|
||||
class SonarAdvancedPowerLawNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = "Custom noise type that allows specifying parameters for power law (grey, violet, etc) variants."
|
||||
|
||||
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",
|
||||
),
|
||||
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):
|
||||
return noise.AdvancedPowerLawNoise
|
||||
|
||||
MAX_DIMS_MAP = { # noqa: RUF012
|
||||
"none": None,
|
||||
"non-batch": (-3, -2, -1),
|
||||
"spatial": (-2, -1),
|
||||
"all": (),
|
||||
"batch": 0,
|
||||
"channel": 1,
|
||||
"height": 2,
|
||||
"width": 3,
|
||||
}
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
alpha,
|
||||
div_max_dims,
|
||||
use_sign,
|
||||
use_div_max_abs,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
alpha=alpha,
|
||||
div_max_dims=self.MAX_DIMS_MAP.get(div_max_dims),
|
||||
use_sign=use_sign,
|
||||
use_div_max_abs=use_div_max_abs,
|
||||
)
|
||||
|
||||
|
||||
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."
|
||||
|
||||
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",
|
||||
),
|
||||
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):
|
||||
return noise.AdvancedCollatzNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor: float,
|
||||
rescale: float,
|
||||
adjust_scale: bool,
|
||||
iteration_sign_flipping: bool,
|
||||
chain_length: int,
|
||||
iterations: int,
|
||||
rmin: float,
|
||||
rmax: float,
|
||||
flatten: bool,
|
||||
dims: str,
|
||||
output_mode: str,
|
||||
noise_dtype: str,
|
||||
quantile: float,
|
||||
quantile_strategy: str,
|
||||
integer_math: bool,
|
||||
add_preserves_sign: bool,
|
||||
even_multiplier: float,
|
||||
even_addition: float,
|
||||
odd_multiplier: float,
|
||||
odd_addition: float,
|
||||
chain_offset: int,
|
||||
seed_mode: str,
|
||||
break_loops: bool,
|
||||
seed_custom_noise: object | None = None,
|
||||
mix_custom_noise: object | None = None,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
if rmin > rmax:
|
||||
rmin, rmax = rmax, rmin
|
||||
dims = tuple(int(i) for i in dims.split(","))
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
adjust_scale=adjust_scale,
|
||||
iteration_sign_flipping=iteration_sign_flipping,
|
||||
chain_length=tuple(int(i) for i in chain_length.split(",")),
|
||||
iterations=iterations,
|
||||
rmin=rmin,
|
||||
rmax=rmax,
|
||||
flatten=flatten,
|
||||
dims=dims,
|
||||
output_mode=output_mode,
|
||||
quantile=quantile,
|
||||
quantile_strategy=quantile_strategy,
|
||||
integer_math=integer_math,
|
||||
add_preserves_sign=add_preserves_sign,
|
||||
even_multiplier=even_multiplier,
|
||||
even_addition=even_addition,
|
||||
odd_multiplier=odd_multiplier,
|
||||
odd_addition=odd_addition,
|
||||
chain_offset=chain_offset,
|
||||
break_loops=break_loops,
|
||||
seed_mode=seed_mode,
|
||||
noise_dtype={
|
||||
"float32": torch.float32,
|
||||
"float64": torch.float64,
|
||||
"float16": torch.float16,
|
||||
"bfloat16": torch.bfloat16,
|
||||
}.get(noise_dtype, torch.float32),
|
||||
seed_custom_noise=seed_custom_noise,
|
||||
mix_custom_noise=mix_custom_noise,
|
||||
)
|
||||
|
||||
|
||||
class SonarAdvancedDistroNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = "Custom noise type that allows specifying parameters for Distro variants. See: https://pytorch.org/docs/stable/distributions.html"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
distro_params = DistroNoiseGenerator.distro_params()
|
||||
variants = tuple(sorted(distro_params.keys()))
|
||||
combined_params = DistroNoiseGenerator.build_params()
|
||||
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"] |= {
|
||||
"distribution": (
|
||||
variants,
|
||||
{
|
||||
"tooltip": "Sets the distribution used for noise generation. See: https://pytorch.org/docs/stable/distributions.html",
|
||||
"default": "uniform",
|
||||
},
|
||||
),
|
||||
"quantile_norm": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.85,
|
||||
"min": -1.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
"tooltip": "When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the distribution and how many of the values are extreme. (Experimental) You can use a negative quantile to consider the values closest to zero as extreme.",
|
||||
},
|
||||
),
|
||||
"quantile_norm_mode": (
|
||||
(
|
||||
"global",
|
||||
"batch",
|
||||
"channel",
|
||||
"batch_row",
|
||||
"batch_col",
|
||||
"nonflat_row",
|
||||
"nonflat_col",
|
||||
),
|
||||
{
|
||||
"default": "batch",
|
||||
"tooltip": "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": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "-1",
|
||||
"tooltip": "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.\nExample: 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",
|
||||
},
|
||||
),
|
||||
} | {
|
||||
k: ("STRING" if isinstance(v["default"], str) else v.get("_ty", "FLOAT"), v)
|
||||
for k, v in combined_params.items()
|
||||
}
|
||||
# print("RESULT:", result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return noise.AdvancedDistroNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
distribution,
|
||||
quantile_norm,
|
||||
quantile_norm_mode,
|
||||
result_index,
|
||||
sonar_custom_noise_opt=None,
|
||||
**kwargs: dict[str],
|
||||
):
|
||||
normdim, normflat = {
|
||||
"global": (None, True),
|
||||
"batch": (0, True),
|
||||
"channel": (1, True),
|
||||
"batch_row": (2, True),
|
||||
"batch_col": (3, True),
|
||||
"nonflat_row": (2, False),
|
||||
"nonflat_col": (3, False),
|
||||
}.get(quantile_norm_mode, (1, True))
|
||||
result_index = tuple(int(v) for v in result_index.split(None))
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
distro=distribution,
|
||||
quantile_norm=quantile_norm,
|
||||
quantile_norm_dim=normdim,
|
||||
quantile_norm_flatten=normflat,
|
||||
result_index=result_index,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class SonarWaveletNoiseNode(
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
):
|
||||
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."
|
||||
|
||||
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):
|
||||
return noise.AdvancedWaveletNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor,
|
||||
rescale,
|
||||
normalize,
|
||||
octaves: int,
|
||||
octave_height_factor: float,
|
||||
octave_width_factor: float,
|
||||
octave_scale_mode: str,
|
||||
octave_rescale_mode: str,
|
||||
post_octave_rescale_mode: str,
|
||||
initial_amplitude: float,
|
||||
persistence: float,
|
||||
height_factor: float,
|
||||
width_factor: float,
|
||||
update_blend: float,
|
||||
update_blend_mode: str,
|
||||
normalize_noise: bool,
|
||||
custom_noise=None,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
if persistence == 0 or initial_amplitude == 0 or octaves == 0:
|
||||
raise ValueError(
|
||||
"Persistence, initial amplitude and octaves must be non-zero",
|
||||
)
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
octaves=octaves,
|
||||
octave_height_factor=octave_height_factor,
|
||||
octave_width_factor=octave_width_factor,
|
||||
octave_scale_mode=octave_scale_mode,
|
||||
octave_rescale_mode=octave_rescale_mode,
|
||||
post_octave_rescale_mode=post_octave_rescale_mode,
|
||||
initial_amplitude=initial_amplitude,
|
||||
persistence=persistence,
|
||||
height_factor=height_factor,
|
||||
width_factor=width_factor,
|
||||
update_blend=update_blend,
|
||||
update_blend_function=utils.BLENDING_MODES[update_blend_mode],
|
||||
normalize=self.get_normalize(normalize),
|
||||
normalize_noise=normalize_noise,
|
||||
custom_noise=custom_noise,
|
||||
)
|
||||
|
||||
|
||||
class SonarAdvancedVoronoiNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = "Voronoi noise is a very weird noise type. The default settings are just borderline usable with SDXL at a 20% ratio with normal Gaussian noise. I recommend reading the section on this noise type in the project documentation (under advanced noise types) as there are too many features to describe in the node itself."
|
||||
|
||||
INPUT_TYPES = SonarLazyInputTypes(
|
||||
lambda _pretty_distance_modes=", ".join( # noqa: B008
|
||||
sorted(VoronoiNoiseGenerator.voronoi_distance_modes), # noqa: B008
|
||||
),
|
||||
_pretty_result_modes=", ".join( # noqa: B008
|
||||
sorted(VoronoiNoiseGenerator.voronoi_result_modes), # noqa: B008
|
||||
): NoiseChainInputTypes()
|
||||
.req_string_n_points(
|
||||
default="256",
|
||||
tooltip="Controls the number of features points in the generated noise. Higher generally results in more detail/better results but is slower. May be a comma separated list for each octave (only applicable when octave mode is set to new_features). 2 is the minimum value.",
|
||||
)
|
||||
.req_string_distance_mode(
|
||||
default="euclidean",
|
||||
placeholder=f"One of: {_pretty_distance_modes}",
|
||||
tooltip="Distance modes. You can specify a comma-separated list of items which will be used for each octave.\n"
|
||||
"You can specify an average of multiple distance modes by separating the names with +.\n"
|
||||
"Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n"
|
||||
"All modes support scaling their output with dscale (which defaults to 1).\n"
|
||||
f"Possible distance modes: {_pretty_distance_modes}",
|
||||
)
|
||||
.req_float_z_initial(
|
||||
default=0.0,
|
||||
tooltip="Initial value for z (depth).",
|
||||
)
|
||||
.req_float_z_increment(
|
||||
default=1.0,
|
||||
tooltip="Amount z (depth) is incremented when applicable.",
|
||||
)
|
||||
.req_float_z_max(
|
||||
default=9999.0,
|
||||
tooltip="Maximum difference from the intial value. At that point, z_max_mode will apply. When set to 0, z_increment has no effect and you will get different noise each time you call the noise sampler.",
|
||||
)
|
||||
.req_field_z_max_mode(
|
||||
(
|
||||
"reset",
|
||||
"wrap",
|
||||
"bounce",
|
||||
),
|
||||
default="reset",
|
||||
tooltip="Controls what happens when the z_max limit is hit (see tooltip for z_max). Reset will reset the feature points and z to the initial values. Wrap will reset z to the initial value. Bounce will flip the sign on the increment and do an increment.",
|
||||
)
|
||||
.req_string_result_mode(
|
||||
default="diff2",
|
||||
placeholder=f"One of: {_pretty_result_modes}",
|
||||
tooltip="Result modes. You can specify a comma-separated list of items which will be used for each octave.\n"
|
||||
"You can specify an average of multiple result modes by separating the names with +.\n"
|
||||
"Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n"
|
||||
"All modes support scaling their output with rscale (which defaults to 1).\n"
|
||||
f"Possible result modes: {_pretty_result_modes}",
|
||||
)
|
||||
.req_field_octave_mode(
|
||||
(
|
||||
"same_features",
|
||||
"new_features",
|
||||
"same_invert_odd",
|
||||
"same_invert_even",
|
||||
"same_roll_chan_up",
|
||||
"same_roll_chan_down",
|
||||
"same_roll_dir_up",
|
||||
"same_roll_dir_down",
|
||||
),
|
||||
default="new_features",
|
||||
tooltip="Only relevant when generating multiple octaves. Controls whether octaves share a set of feature points or if they are different for each octave (note that this is slower). Modes starting with 'same' will use the same feature points per octave but may transform them.",
|
||||
)
|
||||
.req_int_octaves(
|
||||
default=3,
|
||||
min=1,
|
||||
tooltip="Number of octaves of noise to generate.",
|
||||
)
|
||||
.req_float_gain(default=0.75)
|
||||
.req_float_lacunarity(default=2.0)
|
||||
.req_float_initial_amplitude(default=1.0)
|
||||
.req_float_initial_scale(default=1.0)
|
||||
.req_normalizetristate_normalize()
|
||||
.opt_customnoise(
|
||||
"custom_noise",
|
||||
tooltip="Optional input if you want to use some other noise type for the initial feature points. Won't work well with noise types that care about the content of the latent (I think only spectral modulation) or manage their own seed (I believe this only applies to Brownian or if you're using the custom noise parameters node to override seeds/fork the RNG).",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return noise.AdvancedVoronoiNoise
|
||||
|
||||
def go(
|
||||
self,
|
||||
*,
|
||||
factor: float,
|
||||
rescale: float,
|
||||
n_points: str,
|
||||
distance_mode: str,
|
||||
z_initial: float,
|
||||
z_increment: float,
|
||||
z_max: float,
|
||||
z_max_mode: str,
|
||||
result_mode: str,
|
||||
octave_mode: str,
|
||||
octaves: int,
|
||||
gain: float,
|
||||
lacunarity: float,
|
||||
initial_amplitude: float,
|
||||
initial_scale: float,
|
||||
normalize: str,
|
||||
custom_noise=None,
|
||||
sonar_custom_noise_opt=None,
|
||||
):
|
||||
n_points = tuple(int(v) for v in n_points.split(","))
|
||||
distance_mode = tuple(v.strip() for v in distance_mode.split(","))
|
||||
result_mode = tuple(v.strip() for v in result_mode.split(","))
|
||||
return super().go(
|
||||
factor,
|
||||
rescale=rescale,
|
||||
sonar_custom_noise_opt=sonar_custom_noise_opt,
|
||||
n_points=n_points,
|
||||
distance_mode=distance_mode,
|
||||
z_initial=z_initial,
|
||||
z_increment=z_increment,
|
||||
z_max=z_max,
|
||||
z_max_mode=z_max_mode,
|
||||
result_mode=result_mode,
|
||||
octave_mode=octave_mode,
|
||||
octaves=octaves,
|
||||
gain=gain,
|
||||
lacunarity=lacunarity,
|
||||
initial_amplitude=initial_amplitude,
|
||||
initial_scale=initial_scale,
|
||||
custom_noise=custom_noise,
|
||||
normalize=normalize,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SonarAdvancedPyramidNoise": SonarAdvancedPyramidNoiseNode,
|
||||
"SonarAdvanced1fNoise": SonarAdvanced1fNoiseNode,
|
||||
"SonarAdvancedPowerLawNoise": SonarAdvancedPowerLawNoiseNode,
|
||||
"SonarAdvancedCollatzNoise": SonarAdvancedCollatzNoiseNode,
|
||||
"SonarAdvancedDistroNoise": SonarAdvancedDistroNoiseNode,
|
||||
"SonarAdvancedVoronoiNoise": SonarAdvancedVoronoiNoiseNode,
|
||||
"SonarWaveletNoise": SonarWaveletNoiseNode,
|
||||
}
|
||||
@@ -0,0 +1,893 @@
|
||||
# Initial implementation by https://github.com/elias-gaeros/
|
||||
# He also provided a lot of help with refactoring and other improvements. Thanks!
|
||||
# (But if anything is broken in here, I'm almost certainly the one to blame.)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import comfy
|
||||
import folder_paths
|
||||
import latent_preview
|
||||
import torch
|
||||
from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
|
||||
from PIL import Image
|
||||
from torch import Tensor
|
||||
|
||||
from ..noise import CustomNoiseItemBase
|
||||
from ..utils import scale_noise
|
||||
from .base import (
|
||||
NOISE_INPUT_TYPES_HINT,
|
||||
WILDCARD_NOISE,
|
||||
NoiseChainInputTypes,
|
||||
SonarCustomNoiseNodeBase,
|
||||
SonarInputTypes,
|
||||
SonarNormalizeNoiseNodeMixin,
|
||||
)
|
||||
|
||||
PREVIEW_FORMAT = comfy.latent_formats.SD15()
|
||||
|
||||
|
||||
def make_preview_result(img, result, prefix="sonar_temp"):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
prefix_append = f"{prefix}_" + "".join(
|
||||
random.choice("abcdefghijklmnopqrstupvxyz") # noqa: S311
|
||||
for x in range(5)
|
||||
)
|
||||
full_output_folder, filename, counter, subfolder, _ = (
|
||||
folder_paths.get_save_image_path(prefix_append, output_dir)
|
||||
)
|
||||
filename = f"{filename}_{counter:05}_.png"
|
||||
file_path = os.path.join(full_output_folder, filename) # noqa: PTH118
|
||||
img.save(file_path, compress_level=1)
|
||||
|
||||
return {
|
||||
"ui": {
|
||||
"images": [
|
||||
{"filename": filename, "subfolder": subfolder, "type": "temp"},
|
||||
],
|
||||
},
|
||||
"result": result,
|
||||
}
|
||||
|
||||
|
||||
class ChannelMixer:
|
||||
def __init__(self, channel_count, common_mode, channel_correlation):
|
||||
self.channel_count = channel_count
|
||||
self.common_mode = common_mode
|
||||
self.channel_correlation = channel_correlation
|
||||
self.mixer = self.build() if common_mode is not None else None
|
||||
|
||||
def build(self):
|
||||
c = self.channel_count
|
||||
common_mode = self.common_mode
|
||||
correlation_count = c * (c - 1) // 2
|
||||
channel_correlation = self.channel_correlation[:correlation_count]
|
||||
channel_correlation = torch.cat(
|
||||
(
|
||||
channel_correlation * common_mode,
|
||||
torch.full(
|
||||
(correlation_count - channel_correlation.numel(),),
|
||||
common_mode,
|
||||
),
|
||||
),
|
||||
)
|
||||
channel_mixer = torch.eye(c).index_put_(
|
||||
tuple(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)
|
||||
torch.diagonal(channel_mixer)[:] = 1.0
|
||||
channel_mixer *= dc.clamp_min(0).sqrt().unsqueeze(0)
|
||||
channel_mixer /= channel_mixer.norm(dim=1, keepdim=True)
|
||||
return channel_mixer
|
||||
|
||||
def to(self, *args: list, **kwargs: dict):
|
||||
if self.mixer is not None:
|
||||
self.mixer = self.mixer.to(*args, **kwargs)
|
||||
return self
|
||||
|
||||
def apply(self, noise, shape, copy=False):
|
||||
if self.mixer is None:
|
||||
return noise if not copy else noise.clone()
|
||||
b, c, h, w = shape
|
||||
if c != self.channel_count:
|
||||
raise ValueError("Channel count mismatch")
|
||||
noise = self.mixer @ noise.swapaxes(0, 1).reshape(c, -1)
|
||||
return noise.reshape(c, b, h, w).swapaxes(1, 0)
|
||||
|
||||
def __call__(self, *args: list, **kwargs: dict):
|
||||
return self.apply(*args, **kwargs)
|
||||
|
||||
|
||||
class PowerFilter:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
min_freq=0.0,
|
||||
max_freq=0.7071,
|
||||
stretch=1.0,
|
||||
rotate=0.0,
|
||||
pnorm=2.0,
|
||||
alpha=0.0,
|
||||
scale=1.0,
|
||||
rel_bw=0.125,
|
||||
oversample=4,
|
||||
compose_with: PowerFilter | None = None,
|
||||
compose_mode="max",
|
||||
):
|
||||
self.min_freq = min_freq
|
||||
self.max_freq = max(max_freq, min_freq)
|
||||
self.stretch = stretch
|
||||
self.rotate = rotate
|
||||
self.pnorm = pnorm
|
||||
self.alpha = alpha
|
||||
self.scale = scale
|
||||
self.rel_bw = rel_bw
|
||||
self.oversample = oversample
|
||||
self.compose_with = compose_with
|
||||
self.compose_mode = compose_mode
|
||||
|
||||
def clone(self):
|
||||
fargs = {
|
||||
k: getattr(self, k)
|
||||
for k in (
|
||||
"min_freq",
|
||||
"max_freq",
|
||||
"stretch",
|
||||
"rotate",
|
||||
"pnorm",
|
||||
"alpha",
|
||||
"scale",
|
||||
"rel_bw",
|
||||
"oversample",
|
||||
"compose_mode",
|
||||
)
|
||||
}
|
||||
fargs["compose_with"] = (
|
||||
self.compose_with.clone() if self.compose_with is not None else None
|
||||
)
|
||||
return self.__class__(**fargs)
|
||||
|
||||
@classmethod
|
||||
def compose(cls, a, b, compose_mode="max"):
|
||||
if a.shape != b.shape:
|
||||
raise ValueError("Filter compose size mismatch!")
|
||||
cf = {
|
||||
"max": torch.max,
|
||||
"min": torch.min,
|
||||
"add": torch.add,
|
||||
"sub": torch.sub,
|
||||
"mul": torch.mul,
|
||||
}.get(compose_mode, torch.max)
|
||||
return cf(a, b).clamp_(min=0.0)
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, op, shape, mix=1.0, normalization_factor=1.0):
|
||||
height, width = shape[-2:]
|
||||
hfreq_bins = width // 2 + 1
|
||||
|
||||
# Flat unit gain frequency response
|
||||
if mix < 1.0:
|
||||
flat = torch.ones(1, 1, height, hfreq_bins)
|
||||
if mix <= 0.0:
|
||||
return flat
|
||||
if normalization_factor != 0:
|
||||
op *= torch.lerp(
|
||||
torch.scalar_tensor(1.0),
|
||||
1.0 / op.square().mean().sqrt(),
|
||||
normalization_factor,
|
||||
)
|
||||
if mix < 1.0:
|
||||
op = torch.lerp(flat, op, mix, out=op)
|
||||
return op
|
||||
|
||||
def build(self, shape, override_oversample=None, composed=True):
|
||||
"""Construct a band-pass * 1/f^alpha filter in rfft space."""
|
||||
oversample = (
|
||||
override_oversample if override_oversample is not None else self.oversample
|
||||
)
|
||||
rel_bw = self.rel_bw
|
||||
height, width = shape[-2:]
|
||||
hfreq_bins = width // 2 + 1
|
||||
|
||||
# Start with an over-sampled fftshift(rfft2freq()) grid. uses complex
|
||||
# numbers for convenient 2d rotation (unrelated to the fft complex phase
|
||||
# space)
|
||||
fc = torch.complex(
|
||||
# real-fftfreq
|
||||
torch.linspace(0, 0.5, oversample * hfreq_bins),
|
||||
# normal fftfreq
|
||||
torch.linspace(
|
||||
-(height // 2) / height,
|
||||
((height - 1) // 2) / height,
|
||||
oversample * height,
|
||||
).unsqueeze(1),
|
||||
)
|
||||
# Rotate, stretch and p-norm
|
||||
if abs(self.rotate) >= 1e-3:
|
||||
fc *= torch.exp(1.0j * torch.deg2rad(torch.scalar_tensor(self.rotate)))
|
||||
if self.stretch > 1.0:
|
||||
fc.real *= self.stretch
|
||||
else:
|
||||
fc.imag *= 1.0 / self.stretch
|
||||
if abs(self.pnorm - 2.0) < 1e-3:
|
||||
d = fc.abs()
|
||||
else:
|
||||
d = (
|
||||
torch.view_as_real(fc)
|
||||
.abs()
|
||||
.pow(self.pnorm)
|
||||
.sum(-1)
|
||||
.pow(1.0 / self.pnorm)
|
||||
)
|
||||
|
||||
# filter gain function
|
||||
op = torch.empty_like(d)
|
||||
m_highpass = d >= self.min_freq
|
||||
m_lowpass = d < self.max_freq
|
||||
m_band = m_highpass & m_lowpass
|
||||
# 1 / f^alpha for the band-pass region
|
||||
op[m_band] = d[m_band].pow(-self.alpha)
|
||||
# easing gaussian (TODO: try cosine windows)
|
||||
m_lowpass = ~m_lowpass
|
||||
op[m_lowpass] = math.pow(self.max_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_lowpass] - self.max_freq).square() / (rel_bw * self.max_freq) ** 2,
|
||||
)
|
||||
if self.min_freq > 0.0:
|
||||
m_highpass = ~m_highpass
|
||||
op[m_highpass] = math.pow(self.min_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_highpass] - self.min_freq).square()
|
||||
/ (rel_bw * self.min_freq) ** 2,
|
||||
)
|
||||
op = torch.nn.functional.interpolate(
|
||||
op[None, None, ...],
|
||||
(height, hfreq_bins),
|
||||
mode="bilinear",
|
||||
align_corners=True,
|
||||
)
|
||||
op = op.roll(-(height // 2), -2) # ifftshift
|
||||
if self.alpha > 0:
|
||||
# In general, the mean offset should be kept as is, sampled from
|
||||
# N(0, 1 / sqrt(H*W) ). However, gain goes to inf when alpha>0.
|
||||
op[..., 0, 0] = 0
|
||||
if self.scale != 1.0:
|
||||
op *= self.scale
|
||||
if composed and self.compose_with is not None:
|
||||
return self.compose(
|
||||
op,
|
||||
self.compose_with.build(shape, override_oversample=override_oversample),
|
||||
self.compose_mode,
|
||||
)
|
||||
return op
|
||||
|
||||
def preview(
|
||||
self,
|
||||
size=(128, 128),
|
||||
mix=1.0,
|
||||
normalization_factor=1.0,
|
||||
raw=False,
|
||||
kernel_gain=1 / 3,
|
||||
filter_gain=1 / 3,
|
||||
):
|
||||
shape = (1, 4, *size)
|
||||
filter_rfft = self.__class__.normalize(
|
||||
self.build(size),
|
||||
shape,
|
||||
mix=mix,
|
||||
normalization_factor=normalization_factor,
|
||||
)
|
||||
filter_fft = rfft2_to_fft2(filter_rfft)
|
||||
kernel = torch.fft.irfft2(filter_rfft, s=size, norm="ortho")
|
||||
kernel = kernel.roll((size[0] // 2, size[1] // 2), (-2, -1))
|
||||
img = (
|
||||
filter_fft.mul_(filter_gain).tanh_().mul_(256.0),
|
||||
kernel.mul_(kernel_gain).tanh_().add_(1.0).mul_(128.0),
|
||||
)
|
||||
if raw:
|
||||
return img
|
||||
img = torch.cat(img, dim=-1).clamp(0, 255).to(torch.uint8)
|
||||
return Image.fromarray(img[0, 0].numpy())
|
||||
|
||||
|
||||
class PowerNoiseItem(CustomNoiseItemBase):
|
||||
def __init__(
|
||||
self,
|
||||
factor,
|
||||
*,
|
||||
channel_correlation,
|
||||
power_filter=None,
|
||||
**kwargs: dict,
|
||||
):
|
||||
if isinstance(channel_correlation, str):
|
||||
channel_correlation = torch.tensor(
|
||||
tuple(
|
||||
float(val)
|
||||
for val in (val.strip() for val in channel_correlation.split(","))
|
||||
if val
|
||||
),
|
||||
device="cpu",
|
||||
dtype=torch.float,
|
||||
)
|
||||
if power_filter is None:
|
||||
fargs = {
|
||||
k: kwargs.pop(k)
|
||||
for k in ("min_freq", "max_freq", "stretch", "rotate", "pnorm", "alpha")
|
||||
if k in kwargs
|
||||
}
|
||||
power_filter = PowerFilter(**fargs)
|
||||
super().__init__(
|
||||
factor,
|
||||
power_filter=power_filter,
|
||||
channel_correlation=channel_correlation,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def make_filter(self, shape, oversample=None):
|
||||
return PowerFilter.normalize(
|
||||
self.power_filter.build(shape, override_oversample=oversample),
|
||||
shape,
|
||||
mix=self.mix,
|
||||
normalization_factor=getattr(self, "filter_norm_factor", 1.0),
|
||||
)
|
||||
|
||||
def make_noise_sampler_internal(
|
||||
self,
|
||||
x: Tensor,
|
||||
noise_sampler,
|
||||
filter_rfft,
|
||||
normalized=True,
|
||||
):
|
||||
shape = x.shape
|
||||
device = x.device
|
||||
time_brownian = self.time_brownian
|
||||
|
||||
channel_mixer = ChannelMixer(
|
||||
shape[1],
|
||||
self.common_mode,
|
||||
self.channel_correlation,
|
||||
).to(device, non_blocking=True)
|
||||
|
||||
def sampler(sigma, sigma_next):
|
||||
noise = noise_sampler(sigma, sigma_next).to(device)
|
||||
noise_rfft = (
|
||||
torch.fft.rfft2(noise, norm="ortho") if time_brownian else noise
|
||||
)
|
||||
noise = torch.fft.irfft2(
|
||||
noise_rfft.mul_(filter_rfft),
|
||||
s=shape[-2:],
|
||||
norm="ortho",
|
||||
)
|
||||
noise = channel_mixer(noise, shape)
|
||||
return scale_noise(noise, self.factor, normalized=normalized)
|
||||
|
||||
return sampler
|
||||
|
||||
def make_noise_sampler(
|
||||
self,
|
||||
x: Tensor,
|
||||
sigma_min: float | None,
|
||||
sigma_max: float | None,
|
||||
*,
|
||||
seed: int | None,
|
||||
cpu: bool = True,
|
||||
normalized=True,
|
||||
):
|
||||
shape, device = x.shape, x.device
|
||||
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
|
||||
if self.time_brownian:
|
||||
if sigma_min is None:
|
||||
raise ValueError(
|
||||
"time correlated brownian mode is valid only for stochastic samplers",
|
||||
)
|
||||
noise_sampler = BrownianTreeNoiseSampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
)
|
||||
else:
|
||||
|
||||
def noise_sampler(_s, _sn):
|
||||
return torch.randn(
|
||||
(*shape[:-1], filter_rfft.shape[-1]),
|
||||
dtype=torch.complex64,
|
||||
device=device,
|
||||
)
|
||||
|
||||
return self.make_noise_sampler_internal(
|
||||
x,
|
||||
noise_sampler,
|
||||
filter_rfft,
|
||||
normalized=normalized,
|
||||
)
|
||||
|
||||
def preview(
|
||||
self,
|
||||
size=(128, 128),
|
||||
noise=None,
|
||||
kernel_gain=1 / 3,
|
||||
filter_gain=1 / 3,
|
||||
):
|
||||
filter_rfft = self.make_filter(size, oversample=1)
|
||||
if noise is None:
|
||||
noise = torch.fft.irfft2(
|
||||
filter_rfft
|
||||
* torch.randn(
|
||||
filter_rfft.shape,
|
||||
dtype=torch.complex64,
|
||||
generator=torch.Generator().manual_seed(0),
|
||||
),
|
||||
s=size,
|
||||
norm="ortho",
|
||||
)
|
||||
else:
|
||||
noise_rfft = torch.fft.rfft2(noise, norm="ortho")
|
||||
noise = torch.fft.irfft2(
|
||||
noise_rfft.mul_(filter_rfft),
|
||||
s=noise.shape[-2:],
|
||||
norm="ortho",
|
||||
)
|
||||
filter_preview = self.power_filter.preview(
|
||||
size=size,
|
||||
normalization_factor=getattr(self, "filter_norm_factor", 1.0),
|
||||
filter_gain=filter_gain,
|
||||
kernel_gain=kernel_gain,
|
||||
raw=True,
|
||||
)
|
||||
img = (
|
||||
torch.cat(
|
||||
(
|
||||
*filter_preview,
|
||||
noise.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
.clamp(0, 255)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
return Image.fromarray(img[0, 0].numpy())
|
||||
|
||||
|
||||
def rfft2_to_fft2(x):
|
||||
"""Apply hermitian-summetry to reconstruct the second half of a fft.
|
||||
|
||||
Only for previews.
|
||||
"""
|
||||
height, width = x.shape[-2:]
|
||||
x_r = x.roll(height // 2, -2) # torch.fft.fftshift(x, -2)
|
||||
x_l = x_r[..., 1 : -1 if width & 1 else None]
|
||||
x_l = torch.flip(x_l.conj(), dims=(-2, -1))
|
||||
if height & 1 == 0:
|
||||
x_l = x_l.roll(1, -2)
|
||||
return torch.cat((x_l, x_r), dim=-1)
|
||||
|
||||
|
||||
class PowerFilterNoiseItem(PowerNoiseItem):
|
||||
def __init__(
|
||||
self,
|
||||
factor,
|
||||
*,
|
||||
noise,
|
||||
normalize_noise,
|
||||
normalize_result,
|
||||
**kwargs: dict,
|
||||
):
|
||||
super().__init__(
|
||||
factor,
|
||||
noise=noise.clone(),
|
||||
normalize_noise=normalize_noise,
|
||||
normalize_result=normalize_result,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def clone_key(self, k):
|
||||
if k == "noise":
|
||||
return self.noise.clone()
|
||||
return super().clone_key(k)
|
||||
|
||||
def make_noise_sampler(
|
||||
self,
|
||||
x: Tensor,
|
||||
sigma_min: float | None,
|
||||
sigma_max: float | None,
|
||||
*,
|
||||
seed: int | None,
|
||||
cpu: bool = True,
|
||||
normalized=True,
|
||||
):
|
||||
shape, device = x.shape, x.device
|
||||
normalize_noise = self.get_normalize("normalize_noise", False) # noqa: FBT003
|
||||
normalize_result = self.get_normalize("normalize_result", normalized)
|
||||
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
|
||||
noise_sampler = self.noise.make_noise_sampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed,
|
||||
cpu,
|
||||
normalized=normalize_noise,
|
||||
)
|
||||
|
||||
return self.make_noise_sampler_internal(
|
||||
x,
|
||||
noise_sampler,
|
||||
filter_rfft,
|
||||
normalized=normalize_result,
|
||||
)
|
||||
|
||||
def preview(self, size=(128, 128)):
|
||||
if getattr(self, "preview_type", None) != "custom":
|
||||
return super().preview(size=size)
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn((1, 4, *size), dtype=torch.float, device="cpu")
|
||||
ns = self.noise.make_noise_sampler(
|
||||
x,
|
||||
torch.scalar_tensor(0.0),
|
||||
torch.scalar_tensor(14.0),
|
||||
0,
|
||||
True, # noqa: FBT003
|
||||
normalized=self.normalize_noise is True,
|
||||
)
|
||||
filtered_ns = self.make_noise_sampler_internal(
|
||||
x,
|
||||
ns,
|
||||
self.make_filter(x.shape),
|
||||
self.normalize_result in {True, None},
|
||||
)
|
||||
filtered_noise = filtered_ns(
|
||||
torch.scalar_tensor(14.0),
|
||||
torch.scalar_tensor(10.0),
|
||||
)
|
||||
previewer = latent_preview.get_previewer(None, PREVIEW_FORMAT)
|
||||
default_preview = super().preview(size=size).convert("RGB")
|
||||
preview = previewer.decode_latent_to_preview(filtered_noise.cpu())
|
||||
default_preview.paste(
|
||||
preview.resize((size[-1], size[-2])),
|
||||
box=(size[-1] * 2, 0),
|
||||
)
|
||||
return default_preview
|
||||
|
||||
|
||||
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
|
||||
DESCRIPTION = "Custom noise type that applies a filter to generated noise."
|
||||
|
||||
INPUT_TYPES = (
|
||||
NoiseChainInputTypes()
|
||||
.req_bool_time_brownian(
|
||||
tooltip="Controls whether brownian noise is used when mix isn't 1.0.",
|
||||
)
|
||||
.req_float_alpha(
|
||||
default=0.0,
|
||||
min=-5.0,
|
||||
max=5.0,
|
||||
tooltip="Values above 0 will amplify low frequencies, negative values will amplify high frequencies.",
|
||||
)
|
||||
.req_float_max_freq(
|
||||
default=0.7071,
|
||||
min=0.0,
|
||||
max=0.7071,
|
||||
tooltip="Maximum frequency to pass through the filter.",
|
||||
)
|
||||
.req_float_min_freq(
|
||||
default=0.0,
|
||||
min=0.0,
|
||||
max=0.7071,
|
||||
tooltip="Minimum frequency to pass through the filter.",
|
||||
)
|
||||
.req_float_stretch(
|
||||
default=1.0,
|
||||
min=0.01,
|
||||
max=100.0,
|
||||
tooltip="Stretches the filter's shape by the specified factor.",
|
||||
)
|
||||
.req_float_rotate(
|
||||
default=0.0,
|
||||
min=-90.0,
|
||||
max=90.0,
|
||||
step=5.0,
|
||||
tooltip="Rotates the filter.",
|
||||
)
|
||||
.req_float_pnorm(
|
||||
default=2.0,
|
||||
min=0.125,
|
||||
max=100.0,
|
||||
step=0.1,
|
||||
tooltip="Factor used for cushioning the band-pass region.",
|
||||
)
|
||||
.req_floatpct_mix(
|
||||
default=1.0,
|
||||
tooltip="Controls the ratio of filtered noise. For example, 0.75 means 75% noise with the filter effects applied, 25% raw noise.",
|
||||
)
|
||||
.req_float_common_mode(
|
||||
default=0.0,
|
||||
min=-100.0,
|
||||
max=100.0,
|
||||
tooltip="Attempts to desaturate the latent by injecting the average across channels (controlled by channel_correction). Applied after mix.",
|
||||
)
|
||||
.req_string_channel_correlation(
|
||||
default="1, 1, 1, 1, 1, 1",
|
||||
tooltip="Comma-separated list of channel correlation strengths.",
|
||||
)
|
||||
.req_field_preview(
|
||||
("none", "no_mix", "mix"),
|
||||
default="none",
|
||||
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.",
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return PowerNoiseItem
|
||||
|
||||
def go(
|
||||
self,
|
||||
preview="none",
|
||||
**kwargs: dict,
|
||||
):
|
||||
result = super().go(**kwargs)
|
||||
if preview == "none":
|
||||
return result
|
||||
if preview == "no_mix":
|
||||
kwargs["mix"] = 1.0
|
||||
img = self.get_item_class()(preview_type=preview, **kwargs).preview()
|
||||
return make_preview_result(img, result)
|
||||
|
||||
|
||||
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()
|
||||
for k in (
|
||||
"min_freq",
|
||||
"max_freq",
|
||||
"stretch",
|
||||
"rotate",
|
||||
"pnorm",
|
||||
"alpha",
|
||||
"time_brownian",
|
||||
):
|
||||
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.",
|
||||
},
|
||||
),
|
||||
"filter_norm_factor": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"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.",
|
||||
},
|
||||
),
|
||||
}
|
||||
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.",
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_item_class(cls):
|
||||
return PowerFilterNoiseItem
|
||||
|
||||
def go(
|
||||
self,
|
||||
factor,
|
||||
sonar_custom_noise,
|
||||
sonar_power_filter,
|
||||
filter_norm_factor,
|
||||
normalize_noise,
|
||||
normalize_result,
|
||||
preview="none",
|
||||
**kwargs: dict,
|
||||
):
|
||||
return super().go(
|
||||
factor=factor,
|
||||
noise=sonar_custom_noise,
|
||||
normalize_noise=self.get_normalize(normalize_noise),
|
||||
normalize_result=self.get_normalize(normalize_result),
|
||||
preview=preview,
|
||||
time_brownian=True,
|
||||
power_filter=sonar_power_filter,
|
||||
filter_norm_factor=filter_norm_factor,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class SonarPowerFilterNode:
|
||||
RETURN_TYPES = ("SONAR_POWER_FILTER",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
|
||||
@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",
|
||||
{
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 128,
|
||||
"tooltip": "Oversampling factor used for the filter size.",
|
||||
},
|
||||
),
|
||||
"blur": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.125,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.01,
|
||||
"round": False,
|
||||
"tooltip": "Slightly blurs the filter to reduce artifacts.",
|
||||
},
|
||||
),
|
||||
"scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": -100.0,
|
||||
"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.",
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"power_filter_opt": ("SONAR_POWER_FILTER",),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
min_freq=0.0,
|
||||
max_freq=0.7071,
|
||||
stretch=1.0,
|
||||
rotate=0.0,
|
||||
pnorm=2.0,
|
||||
alpha=0.0,
|
||||
blur=0.125,
|
||||
oversample=4,
|
||||
scale=1.0,
|
||||
compose_mode="max",
|
||||
power_filter_opt=None,
|
||||
):
|
||||
return (
|
||||
PowerFilter(
|
||||
min_freq=min_freq,
|
||||
max_freq=max_freq,
|
||||
stretch=stretch,
|
||||
rotate=rotate,
|
||||
pnorm=pnorm,
|
||||
alpha=alpha,
|
||||
scale=scale,
|
||||
rel_bw=blur,
|
||||
oversample=oversample,
|
||||
compose_mode=compose_mode,
|
||||
compose_with=power_filter_opt,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SonarPreviewFilterNode:
|
||||
DESCRIPTION = "Allows previewing a Power Filter."
|
||||
RETURN_TYPES = ("SONAR_POWER_FILTER",)
|
||||
CATEGORY = "advanced/noise"
|
||||
FUNCTION = "go"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
INPUT_TYPES = (
|
||||
SonarInputTypes()
|
||||
.req_field_sonar_power_filter(
|
||||
"SONAR_POWER_FILTER",
|
||||
tooltip="Power Filter to preview.",
|
||||
)
|
||||
.req_float_filter_gain(
|
||||
default=1 / 3,
|
||||
min=0.0,
|
||||
tooltip="Gain factor applied to the filter part of the preview.",
|
||||
)
|
||||
.req_float_kernel_gain(
|
||||
default=1 / 3,
|
||||
min=0.0,
|
||||
tooltip="Gain factor applied to the kernel part of the preview.",
|
||||
)
|
||||
.req_floatpct_norm_factor(
|
||||
default=1.0,
|
||||
tooltip="Normalization factor applied to the filter before previewing. 1.0 means 100% normalized.",
|
||||
)
|
||||
.req_field_preview_size(
|
||||
(
|
||||
"128x128",
|
||||
"256x256",
|
||||
"384x256",
|
||||
"256x384",
|
||||
"768x512",
|
||||
"512x768",
|
||||
"768x768",
|
||||
"128x127",
|
||||
"127x128",
|
||||
),
|
||||
default="128x128",
|
||||
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,
|
||||
sonar_power_filter,
|
||||
filter_gain=1 / 3,
|
||||
kernel_gain=1 / 3,
|
||||
norm_factor=1.0,
|
||||
preview_size="256x256",
|
||||
):
|
||||
filt = sonar_power_filter.clone()
|
||||
filt.preview_type = "custom"
|
||||
preview_size = tuple(int(val) for val in preview_size.split("x", 1))
|
||||
return make_preview_result(
|
||||
filt.preview(
|
||||
size=(preview_size[1], preview_size[0]),
|
||||
filter_gain=filter_gain,
|
||||
kernel_gain=kernel_gain,
|
||||
normalization_factor=norm_factor,
|
||||
),
|
||||
(filt,),
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SonarPowerNoise": SonarPowerNoiseNode,
|
||||
"SonarPowerFilterNoise": SonarPowerFilterNoiseNode,
|
||||
"SonarPowerFilter": SonarPowerFilterNode,
|
||||
"SonarPreviewFilter": SonarPreviewFilterNode,
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import folder_paths
|
||||
import torch
|
||||
from comfy.k_diffusion.sampling import BrownianTreeNoiseSampler
|
||||
from PIL import Image
|
||||
from torch import Tensor
|
||||
|
||||
from .nodes import SonarCustomNoiseNodeBase
|
||||
from .noise import CustomNoiseItemBase
|
||||
|
||||
# ruff: noqa: ANN003, FBT001, FBT002
|
||||
|
||||
|
||||
class PowerNoiseItem(CustomNoiseItemBase):
|
||||
def __init__(self, factor, **kwargs):
|
||||
super().__init__(factor, **kwargs)
|
||||
self.max_freq = max(self.max_freq, self.min_freq)
|
||||
|
||||
def make_filter(self, shape, oversample=4, rel_bw=0.125):
|
||||
"""Construct a band-pass * 1/f^alpha filter in rfft space."""
|
||||
height, width = shape[-2:]
|
||||
hfreq_bins = width // 2 + 1
|
||||
|
||||
# Flat unit gain frequency response
|
||||
if self.mix < 1.0:
|
||||
flat = torch.ones(1, 1, height, hfreq_bins)
|
||||
if self.mix <= 0.0:
|
||||
return flat
|
||||
|
||||
# Start with an over-sampled fftshift(rfft2freq()) grid. uses complex
|
||||
# numbers for convenient 2d rotation (unrelated to the fft complex phase
|
||||
# space)
|
||||
fc = torch.complex(
|
||||
# real-fftfreq
|
||||
torch.linspace(0, 0.5, oversample * hfreq_bins),
|
||||
# normal fftfreq
|
||||
torch.linspace(
|
||||
-(height // 2) / height,
|
||||
((height - 1) // 2) / height,
|
||||
oversample * height,
|
||||
).unsqueeze(1),
|
||||
)
|
||||
# Rotate, stretch and p-norm
|
||||
if abs(self.rotate) >= 1e-3:
|
||||
fc *= torch.exp(1.0j * torch.deg2rad(torch.scalar_tensor(self.rotate)))
|
||||
if self.stretch > 1.0:
|
||||
fc.real *= self.stretch
|
||||
else:
|
||||
fc.imag *= 1.0 / self.stretch
|
||||
if abs(self.pnorm - 2.0) < 1e-3:
|
||||
d = fc.abs()
|
||||
else:
|
||||
d = (
|
||||
torch.view_as_real(fc)
|
||||
.abs()
|
||||
.pow(self.pnorm)
|
||||
.sum(-1)
|
||||
.pow(1.0 / self.pnorm)
|
||||
)
|
||||
|
||||
# filter gain function
|
||||
op = torch.empty_like(d)
|
||||
m_highpass = d >= self.min_freq
|
||||
m_lowpass = d < self.max_freq
|
||||
m_band = m_highpass & m_lowpass
|
||||
# 1 / f^alpha for the band-pass region
|
||||
op[m_band] = d[m_band].pow(-self.alpha)
|
||||
# easing gaussian (TODO: try cosine windows)
|
||||
m_lowpass = ~m_lowpass
|
||||
op[m_lowpass] = math.pow(self.max_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_lowpass] - self.max_freq).square() / (rel_bw * self.max_freq) ** 2,
|
||||
)
|
||||
if self.min_freq > 0.0:
|
||||
m_highpass = ~m_highpass
|
||||
op[m_highpass] = math.pow(self.min_freq, -self.alpha) * torch.exp(
|
||||
-(d[m_highpass] - self.min_freq).square()
|
||||
/ (rel_bw * self.min_freq) ** 2,
|
||||
)
|
||||
op = torch.nn.functional.interpolate(
|
||||
op[None, None, ...],
|
||||
(height, hfreq_bins),
|
||||
mode="bilinear",
|
||||
align_corners=True,
|
||||
)
|
||||
op = op.roll(-(height // 2), -2) # ifftshift
|
||||
if self.alpha > 0:
|
||||
# In general, the mean offset should be kept as is, sampled from
|
||||
# N(0, 1 / sqrt(H*W) ). However, gain goes to inf when alpha>0.
|
||||
op[..., 0, 0] = 0
|
||||
|
||||
# Scale to unit power gain, then mix flat filter
|
||||
mean_pow_gain = op.mean()
|
||||
if mean_pow_gain <= 0.0:
|
||||
# don't fail catastrophically when something broke
|
||||
return flat
|
||||
op *= 1.0 / mean_pow_gain
|
||||
if self.mix < 1.0:
|
||||
op = torch.lerp(flat, op, self.mix, out=op)
|
||||
return op.sqrt_()
|
||||
|
||||
def make_noise_sampler(
|
||||
self,
|
||||
x: Tensor,
|
||||
sigma_min: float | None,
|
||||
sigma_max: float | None,
|
||||
seed: int | None,
|
||||
cpu: bool = True,
|
||||
):
|
||||
shape = x.shape
|
||||
device = x.device
|
||||
time_brownian = self.time_brownian
|
||||
if self.time_brownian:
|
||||
if sigma_min is None:
|
||||
raise ValueError(
|
||||
"time correlated brownian mode is valid only for stochastic samplers",
|
||||
)
|
||||
brownian_tree = BrownianTreeNoiseSampler(
|
||||
x,
|
||||
sigma_min,
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=cpu,
|
||||
)
|
||||
|
||||
common_mode = self.common_mode
|
||||
if common_mode > 0.0:
|
||||
b, c, h, w = shape
|
||||
torch.eye(c, c)
|
||||
channel_mixer = torch.lerp(
|
||||
torch.eye(c, c),
|
||||
torch.ones(c, c) / c,
|
||||
common_mode,
|
||||
)
|
||||
channel_mixer = channel_mixer.sqrt().to(device, non_blocking=True)
|
||||
|
||||
filter_rfft = self.make_filter(shape).to(device, non_blocking=True)
|
||||
|
||||
def sampler(sigma, sigma_next):
|
||||
if time_brownian:
|
||||
noise = brownian_tree(sigma, sigma_next).to(device)
|
||||
noise_rfft = torch.fft.rfft2(noise, norm="ortho")
|
||||
else:
|
||||
noise_rfft = torch.randn(
|
||||
(*shape[:-1], filter_rfft.shape[-1]),
|
||||
dtype=torch.complex64,
|
||||
device=device,
|
||||
)
|
||||
noise = torch.fft.irfft2(
|
||||
noise_rfft.mul_(filter_rfft),
|
||||
s=shape[-2:],
|
||||
norm="ortho",
|
||||
)
|
||||
|
||||
if common_mode > 0.0:
|
||||
noise = channel_mixer @ noise.swapaxes(0, 1).reshape(c, -1)
|
||||
noise = noise.reshape(c, b, h, w).swapaxes(1, 0)
|
||||
return noise.mul_(self.factor)
|
||||
|
||||
return sampler
|
||||
|
||||
def preview(self, size=(128, 128)):
|
||||
filter_rfft = self.make_filter(size, oversample=1)
|
||||
filter_fft = rfft2_to_fft2(filter_rfft)
|
||||
noise = torch.fft.irfft2(
|
||||
filter_rfft
|
||||
* torch.randn(
|
||||
filter_rfft.shape,
|
||||
dtype=torch.complex64,
|
||||
generator=torch.Generator().manual_seed(0),
|
||||
),
|
||||
s=size,
|
||||
norm="ortho",
|
||||
)
|
||||
kernel = torch.fft.irfft2(filter_rfft, s=size, norm="ortho")
|
||||
kernel = kernel.roll((size[0] // 2, size[1] // 2), (-2, -1))
|
||||
img = (
|
||||
torch.cat(
|
||||
[
|
||||
filter_fft.mul_(1 / 3).tanh_().mul_(256.0),
|
||||
kernel.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
|
||||
noise.mul_(1 / 3).tanh_().add_(1.0).mul_(128.0),
|
||||
],
|
||||
dim=-1,
|
||||
)
|
||||
.clamp(0, 255)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
return Image.fromarray(img[0, 0].numpy())
|
||||
|
||||
|
||||
def rfft2_to_fft2(x):
|
||||
"""Apply hermitian-summetry to reconstruct the second half of a fft.
|
||||
|
||||
Only for previews.
|
||||
"""
|
||||
height, width = x.shape[-2:]
|
||||
x_r = x.roll(height // 2, -2) # torch.fft.fftshift(x, -2)
|
||||
x_l = x_r[..., 1 : -1 if width & 1 else None]
|
||||
x_l = torch.flip(x_l.conj(), dims=(-2, -1))
|
||||
if height & 1 == 0:
|
||||
x_l = x_l.roll(1, -2)
|
||||
return torch.cat([x_l, x_r], dim=-1)
|
||||
|
||||
|
||||
class SonarPowerNoiseNode(SonarCustomNoiseNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
result = super().INPUT_TYPES()
|
||||
result["required"] |= {
|
||||
"time_brownian": ("BOOLEAN", {"default": False}),
|
||||
"alpha": (
|
||||
"FLOAT",
|
||||
{
|
||||
"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,
|
||||
},
|
||||
),
|
||||
"mix": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"common_mode": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"round": False,
|
||||
},
|
||||
),
|
||||
"preview": (["none", "no_mix", "mix"],),
|
||||
}
|
||||
return result
|
||||
|
||||
def get_item_class(self):
|
||||
return PowerNoiseItem
|
||||
|
||||
def go(
|
||||
self,
|
||||
preview="none",
|
||||
**kwargs,
|
||||
):
|
||||
result = super().go(**kwargs)
|
||||
if preview == "none":
|
||||
return result
|
||||
if preview == "no_mix":
|
||||
kwargs["mix"] = 1.0
|
||||
img = PowerNoiseItem(**kwargs).preview()
|
||||
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
prefix_append = "sonar_temp_" + "".join(
|
||||
random.choice("abcdefghijklmnopqrstupvxyz") # noqa: S311
|
||||
for x in range(5)
|
||||
)
|
||||
full_output_folder, filename, counter, subfolder, _ = (
|
||||
folder_paths.get_save_image_path(prefix_append, output_dir)
|
||||
)
|
||||
filename = f"{filename}_{counter:05}_.png"
|
||||
file_path = os.path.join(full_output_folder, filename) # noqa: PTH118
|
||||
img.save(file_path, compress_level=1)
|
||||
|
||||
return {
|
||||
"ui": {
|
||||
"images": [
|
||||
{"filename": filename, "subfolder": subfolder, "type": "temp"},
|
||||
],
|
||||
},
|
||||
"result": result,
|
||||
}
|
||||
@@ -2,22 +2,26 @@
|
||||
|
||||
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 import sampling
|
||||
from comfy.k_diffusion.sampling import get_ancestral_step, to_d
|
||||
from comfy.samplers import KSampler, k_diffusion_sampling
|
||||
from torch import Tensor
|
||||
from tqdm.auto import trange
|
||||
|
||||
from . import noise
|
||||
from . import noise, utils
|
||||
|
||||
|
||||
class HistoryType(Enum):
|
||||
ZERO = auto()
|
||||
RAND = auto()
|
||||
SAMPLE = auto()
|
||||
SAMPLE_NORM = auto()
|
||||
|
||||
|
||||
class GuidanceType(Enum):
|
||||
@@ -33,15 +37,34 @@ 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:
|
||||
@@ -51,19 +74,74 @@ 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,
|
||||
sigmas: Tensor,
|
||||
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,
|
||||
@@ -83,53 +161,163 @@ class SonarBase:
|
||||
sigma_max,
|
||||
seed=seed,
|
||||
cpu=True,
|
||||
normalized=True,
|
||||
)
|
||||
self.noise_sampler = noise_sampler
|
||||
return noise_sampler
|
||||
|
||||
def init_hist_d(self, x: Tensor) -> None:
|
||||
if self.history_d is not None:
|
||||
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):
|
||||
return
|
||||
cfg = self.cfg
|
||||
init = cfg.init
|
||||
# memorize delta momentum
|
||||
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:
|
||||
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:
|
||||
ns = noise.get_noise_sampler(
|
||||
self.cfg.rand_init_noise_type,
|
||||
cfg.rand_init_noise_type,
|
||||
x,
|
||||
None,
|
||||
None,
|
||||
seed=self.extra_args.get("seed"),
|
||||
cpu=True,
|
||||
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")
|
||||
|
||||
def update_hist(self, momentum_d):
|
||||
q = 1.0 - self.cfg.momentum_hist
|
||||
@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:
|
||||
hd = self.history_d
|
||||
if isinstance(hd, int) and hd == 0:
|
||||
self.history_d = momentum_d
|
||||
else:
|
||||
self.history_d = (1.0 - q) * hd + q * momentum_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
|
||||
|
||||
def momentum_step(self, x: Tensor, d: Tensor, dt: Tensor):
|
||||
if self.cfg.momentum == 1.0:
|
||||
return x + d * dt
|
||||
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:
|
||||
hd = self.history_d
|
||||
# correct current `d` with momentum
|
||||
p = (1.0 - self.cfg.momentum) * self.cfg.direction
|
||||
momentum_d = (1.0 - p) * d + p * hd
|
||||
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
|
||||
|
||||
# Euler method with momentum
|
||||
x = x + momentum_d * dt
|
||||
|
||||
self.update_hist(momentum_d)
|
||||
|
||||
return x
|
||||
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)
|
||||
|
||||
|
||||
class SonarGuidanceMixin:
|
||||
@@ -148,46 +336,79 @@ class SonarGuidanceMixin:
|
||||
def prepare_ref_latent(latent: Tensor | None) -> Tensor:
|
||||
if latent is None:
|
||||
return None
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
):
|
||||
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)
|
||||
return self.guidance_linear(
|
||||
x,
|
||||
self.ref_latent,
|
||||
self.guidance.factor,
|
||||
blend=self.guidance_blend,
|
||||
)
|
||||
if self.guidance.guidance_type == GuidanceType.EULER:
|
||||
return self.guidance_euler(step_index, x, denoised)
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
return self.guidance_euler(
|
||||
sigma,
|
||||
sigma_next,
|
||||
x,
|
||||
denoised,
|
||||
self.ref_latent,
|
||||
self.guidance.factor,
|
||||
)
|
||||
raise ValueError("Sonar: Guidance: Unknown guidance type")
|
||||
|
||||
@classmethod
|
||||
def guidance_shift(cls, t: Tensor, ref_latent: Tensor, *, dim=None):
|
||||
if dim is None:
|
||||
dim = tuple(range(-(t.ndim - 1), 0))
|
||||
avg_t = t.mean(dim=dim, keepdim=True)
|
||||
std_t = t.std(dim=dim, keepdim=True)
|
||||
return (ref_latent * std_t).add_(avg_t)
|
||||
|
||||
@classmethod
|
||||
def guidance_euler(
|
||||
self,
|
||||
step_index: int,
|
||||
cls,
|
||||
sigma: Tensor,
|
||||
sigma_next: Tensor,
|
||||
x: Tensor,
|
||||
denoised: Tensor,
|
||||
):
|
||||
avg_t = denoised.mean(dim=[1, 2, 3], keepdim=True)
|
||||
std_t = denoised.std(dim=[1, 2, 3], keepdim=True)
|
||||
ref_img_shift = self.ref_latent * std_t + avg_t
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
|
||||
d = sampling.to_d(x, sigma, ref_img_shift)
|
||||
dt = (sigma_next - sigma) * self.guidance.factor
|
||||
return x + d * dt
|
||||
ref_latent: Tensor,
|
||||
factor: float = 0.2,
|
||||
*,
|
||||
do_shift: bool = True,
|
||||
) -> Tensor:
|
||||
if torch.equal(sigma, sigma_next):
|
||||
return cls.guidance_linear(x, ref_latent, factor=factor, do_shift=do_shift)
|
||||
ref_img_shift = (
|
||||
cls.guidance_shift(denoised, ref_latent) if do_shift else ref_latent
|
||||
)
|
||||
d = to_d(x, sigma, ref_img_shift)
|
||||
dt = (sigma_next - sigma) * factor
|
||||
return (d * dt).add_(x)
|
||||
|
||||
@classmethod
|
||||
def guidance_linear(
|
||||
self,
|
||||
cls,
|
||||
x: Tensor,
|
||||
):
|
||||
avg_t = x.mean(dim=[1, 2, 3], keepdim=True)
|
||||
std_t = x.std(dim=[1, 2, 3], keepdim=True)
|
||||
ref_img_shift = self.ref_latent * std_t + avg_t
|
||||
return (1.0 - self.guidance.factor) * x + self.guidance.factor * ref_img_shift
|
||||
ref_latent: Tensor,
|
||||
factor: float = 0.2,
|
||||
*,
|
||||
blend=torch.lerp,
|
||||
do_shift: bool = True,
|
||||
) -> Tensor:
|
||||
ref_img_shift = cls.guidance_shift(x, ref_latent) if do_shift else ref_latent
|
||||
return blend(x, ref_img_shift, factor)
|
||||
|
||||
|
||||
class SonarWithGuidance(SonarBase, SonarGuidanceMixin):
|
||||
@@ -200,9 +421,9 @@ class SonarSampler(SonarWithGuidance):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
sigmas,
|
||||
s_in,
|
||||
extra_args,
|
||||
sigmas: Tensor,
|
||||
s_in: Tensor,
|
||||
extra_args: dict[str, Any],
|
||||
*args: list[Any],
|
||||
**kwargs: dict[str, Any],
|
||||
):
|
||||
@@ -212,90 +433,68 @@ 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,
|
||||
):
|
||||
self.init_hist_d(sample)
|
||||
def step(self, step_index: int, sample: torch.FloatTensor):
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
|
||||
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
|
||||
denoised = self.call_model(sample, sigma)
|
||||
result_sample = self.momentum_step(
|
||||
step_index,
|
||||
sample,
|
||||
denoised,
|
||||
sigma,
|
||||
sigma_next,
|
||||
)
|
||||
|
||||
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:
|
||||
if sigma_next > 0:
|
||||
result_sample = self.guidance_step(step_index, result_sample, denoised)
|
||||
|
||||
return (
|
||||
result_sample,
|
||||
sigma,
|
||||
sigma_hat,
|
||||
sigma,
|
||||
denoised,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@torch.no_grad()
|
||||
def sampler(
|
||||
cls,
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
extra_args=None,
|
||||
x: Tensor,
|
||||
sigmas: Tensor,
|
||||
extra_args: dict | None = None,
|
||||
callback=None,
|
||||
disable=None,
|
||||
disable: bool | None = None, # noqa: FBT001
|
||||
noise_sampler: Callable | None = None,
|
||||
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_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 = cls(
|
||||
s_churn,
|
||||
s_tmin,
|
||||
s_tmax,
|
||||
s_noise,
|
||||
model,
|
||||
sigmas,
|
||||
s_in,
|
||||
@@ -319,7 +518,7 @@ class SonarEuler(SonarSampler):
|
||||
{
|
||||
"x": x,
|
||||
"i": i,
|
||||
"sigma": sigmas[i],
|
||||
"sigma": sigma,
|
||||
"sigma_hat": sigma_hat,
|
||||
"denoised": denoised,
|
||||
},
|
||||
@@ -344,36 +543,36 @@ class SonarEulerAncestral(SonarSampler):
|
||||
step_index: int,
|
||||
sample: torch.FloatTensor,
|
||||
):
|
||||
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,
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
sigma_down, sigma_up = get_ancestral_step(
|
||||
sigma,
|
||||
sigma_next,
|
||||
eta=self.eta,
|
||||
)
|
||||
|
||||
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:
|
||||
denoised = self.call_model(sample, sigma)
|
||||
result_sample = self.momentum_step(
|
||||
step_index,
|
||||
sample,
|
||||
denoised,
|
||||
sigma,
|
||||
sigma_down,
|
||||
)
|
||||
if sigma_next > 0:
|
||||
result_sample = self.guidance_step(step_index, result_sample, denoised)
|
||||
result_sample = (
|
||||
result_sample = ( # noqa: PLR6104
|
||||
result_sample
|
||||
+ self.noise_sampler(sigma_from, sigma_to) * self.s_noise * sigma_up
|
||||
+ self.noise_sampler(sigma, sigma_next) * (self.s_noise * sigma_up)
|
||||
)
|
||||
|
||||
return (
|
||||
result_sample,
|
||||
sigma_from,
|
||||
sigma_from,
|
||||
sigma,
|
||||
sigma,
|
||||
denoised,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@torch.no_grad()
|
||||
def sampler(
|
||||
cls,
|
||||
model,
|
||||
@@ -382,14 +581,14 @@ class SonarEulerAncestral(SonarSampler):
|
||||
extra_args=None,
|
||||
callback=None,
|
||||
disable=None,
|
||||
sonar_config=None,
|
||||
sonar_config: SonarConfig | None = None,
|
||||
sonar_params: dict | None = None,
|
||||
eta=1.0,
|
||||
s_noise=1.0,
|
||||
noise_sampler: Callable | None = None,
|
||||
):
|
||||
if sonar_config is None:
|
||||
sonar_config = SonarConfig()
|
||||
s_in = x.new_ones([x.shape[0]])
|
||||
sonar_config = cls.get_config(sonar_config, sonar_params)
|
||||
s_in = x.new_ones((x.shape[0],))
|
||||
sonar = cls(
|
||||
eta,
|
||||
s_noise,
|
||||
@@ -407,7 +606,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,
|
||||
)
|
||||
@@ -439,125 +638,154 @@ class SonarDPMPPSDE(SonarSampler):
|
||||
self.s_noise = s_noise
|
||||
|
||||
@staticmethod
|
||||
def sigma_fn(t) -> float:
|
||||
def sigma_fn(t: Tensor) -> float:
|
||||
return t.neg().exp()
|
||||
|
||||
@staticmethod
|
||||
def t_fn(sigma) -> float:
|
||||
return sigma.log.neg()
|
||||
def t_fn(sigma: Tensor) -> float:
|
||||
return sigma.log().neg()
|
||||
|
||||
# DPM++ solver algorithm copied from ComfyUI source.
|
||||
def momentum_step(
|
||||
def momentum_step( # noqa: PLR0914
|
||||
self,
|
||||
step_index,
|
||||
step_index: int,
|
||||
x: Tensor,
|
||||
denoised: Tensor,
|
||||
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)
|
||||
sigma: Tensor,
|
||||
sigma_next: Tensor,
|
||||
sigma_down: Tensor,
|
||||
) -> Tensor:
|
||||
if sigma_next == 0:
|
||||
return super().momentum_step(step_index, x, denoised, sigma, sigma_down)
|
||||
|
||||
def sigma_fn(t):
|
||||
return t.neg().exp()
|
||||
|
||||
def t_fn(sigma):
|
||||
return sigma.log().neg()
|
||||
|
||||
hd = self.history_d
|
||||
p = (1.0 - self.cfg.momentum) * self.cfg.direction
|
||||
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 = t_fn(sigma_from), t_fn(sigma_to)
|
||||
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
|
||||
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),
|
||||
s_t, s_s = self.sigma_fn(t), self.sigma_fn(s)
|
||||
sd, su = get_ancestral_step(
|
||||
s_t,
|
||||
s_s,
|
||||
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
|
||||
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)
|
||||
return x + self.noise_sampler(sigma_fn(t), sigma_fn(t_next)) * self.s_noise * su
|
||||
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()
|
||||
|
||||
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,
|
||||
sigma, sigma_next = self.sigmas[step_index], self.sigmas[step_index + 1]
|
||||
sigma_down, _sigma_up = get_ancestral_step(
|
||||
sigma,
|
||||
sigma_next,
|
||||
eta=self.eta,
|
||||
)
|
||||
|
||||
denoised = self.model(sample, sigma_from * self.s_in, **self.extra_args)
|
||||
denoised = self.call_model(sample, sigma)
|
||||
result_sample = self.momentum_step(
|
||||
step_index,
|
||||
sample,
|
||||
denoised,
|
||||
sigma_from,
|
||||
sigma_to,
|
||||
sigma,
|
||||
sigma_next,
|
||||
sigma_down,
|
||||
)
|
||||
|
||||
return (
|
||||
result_sample,
|
||||
sigma_from,
|
||||
sigma_from,
|
||||
sigma,
|
||||
sigma,
|
||||
denoised,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@torch.no_grad()
|
||||
def sampler(
|
||||
cls,
|
||||
model,
|
||||
x,
|
||||
sigmas,
|
||||
extra_args=None,
|
||||
x: Tensor,
|
||||
sigmas: Tensor,
|
||||
extra_args: dict | None = None,
|
||||
callback=None,
|
||||
disable=None,
|
||||
sonar_config=None,
|
||||
disable: bool | None = None, # noqa: FBT001
|
||||
sonar_config: SonarConfig | None = None,
|
||||
sonar_params: dict | None = None,
|
||||
eta=1.0,
|
||||
s_noise=1.0,
|
||||
noise_sampler=None,
|
||||
):
|
||||
if sonar_config is None:
|
||||
sonar_config = SonarConfig()
|
||||
s_in = x.new_ones([x.shape[0]])
|
||||
) -> Tensor:
|
||||
sonar_config = cls.get_config(sonar_config, sonar_params)
|
||||
s_in = x.new_ones((x.shape[0],))
|
||||
sonar = cls(
|
||||
eta,
|
||||
s_noise,
|
||||
@@ -575,7 +803,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,
|
||||
)
|
||||
@@ -592,11 +820,7 @@ class SonarDPMPPSDE(SonarSampler):
|
||||
return x
|
||||
|
||||
|
||||
def add_samplers():
|
||||
import importlib
|
||||
|
||||
from comfy.samplers import KSampler, k_diffusion_sampling
|
||||
|
||||
def add_samplers() -> None:
|
||||
extra_samplers = {
|
||||
"sonar_euler": SonarEuler.sampler,
|
||||
"sonar_euler_ancestral": SonarEulerAncestral.sampler,
|
||||
|
||||