Compare commits

..
36 Commits
Author SHA1 Message Date
Reithan 3d8827f132 Cleanup old math versions and fix variables (#16) 2025-07-26 04:34:34 -07:00
Reithan 62bef2e275 Update README.md 2025-07-21 04:01:10 -07:00
Reithan fecdfe01df Update README.md 2025-07-21 04:00:01 -07:00
Reithan c5610837e4 Update README.md 2025-07-21 03:59:41 -07:00
Reithan 21ac7cf0cb Update pyproject.toml (#14) 2025-07-21 03:56:18 -07:00
Reithan 4930d862d5 Update publish.yml 2025-07-21 03:50:11 -07:00
e8b727f914 Add pyproject.toml for Custom Node Registry (#7)
Hey! My name is Robin and I'm from [comfy-org](https://comfy.org/)! We
would love to have you join the Comfy Registry, a public collection of
custom nodes which lets authors publish nodes by version and automate
testing against existing workflows.

The registry is already integrated with ComfyUI-Manager, and we want it
to be the default place users install nodes from eventually. We do a
security-scan of every node to improve safety. Feel free to read up more
on the registry
[here](https://docs.comfy.org/registry/overview#introduction)

Action Required:

- [ ] Go to the [registry](https://registry.comfy.org). Login and create
a publisher id (everything after the `@` sign on your registry profile).
- [ ] Add the publisher id into the pyproject.toml file.
- [ ] Merge the separate Github Actions PR, then merge this PR.

If you want to publish the node manually, [install the
cli](https://docs.comfy.org/comfy-cli/getting-started#install-cli) by
running `pip install comfy-cli`, then run `comfy node publish`

Otherwise, if you have any questions, please message me on discord at
robinken or join our [server](https://discord.com/invite/comfyorg)!

---------

Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
Co-authored-by: Reithan <bo122081@hotmail.com>
2025-07-21 03:46:38 -07:00
Reithan 389aedfd17 upload icon 2025-07-21 03:43:44 -07:00
99824b2ee5 Add Github Action for Publishing to Comfy Registry (#6)
This PR adds a Github Action (publish-node-action) that will publish an
updated version of your custom node to the
[registry](https://registry.comfy.org/) whenever the `pyproject.toml`
file changes. The pyproject.toml defines the custom node version you
want to publish (added in another PR). Make sure you update the version
number in `pyproject.toml` when you make a change that should be
published to everyone!

Action Required:

- [ ] Make sure the trigger branch (`master` or `main`) in
`publish.yaml` matches the branch you want to use as the publishing
branch. It will only trigger when the pyproject.toml gets updated on
that branch.
- [ ] Create an api key on the Registry for publishing from Github.
[Instructions](https://docs.comfy.org/registry/publishing#create-an-api-key-for-publishing).
- [ ] Add it to your Github Repository Secrets as
`REGISTRY_ACCESS_TOKEN`.

Please message me on Discord at robinken or join our
[server](https://discord.com/invite/comfyorg) server if you have any
questions!

---------

Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
Co-authored-by: Reithan <bo122081@hotmail.com>
2025-07-21 01:50:39 -07:00
Reithan 4bb226aabb Update README.md (#13) 2025-07-21 01:18:32 -07:00
Reithan 60b5127cf4 Update to Math v0.6.0 (#12)
- [X] add math 0.6
- [X] update readme
- [X] add example images
- [X] upload permanent copy of graph image
2025-07-21 01:14:06 -07:00
Reithan e72afd4189 update note 2025-07-20 07:20:35 -07:00
Reithan 98a9b6d656 minor optimizations 2025-07-20 07:13:07 -07:00
Reithan e0988d3b24 Fix detection of model type (#11)
- fix some scaling issues
2025-07-20 04:04:54 -07:00
Reithan 932c7b2136 fix batch size error when applying scale as broadcast 2025-07-19 20:24:28 -07:00
Reithan 50ddd2ac47 Update math to 0.5.0 (#10)
- [X] detects v-pred/eps and uses appropriate pre/post scaling
- [X] supports detection in Forge, Comfy and various loaders/models
2025-07-19 17:59:27 -07:00
Reithan 3768f4a768 Update ComfyUI compatibility (#5) 2025-04-13 22:13:50 -07:00
Reithan e82622cefe Update negative_rejection_steering_script.py 2025-03-29 05:13:16 -07:00
Reithan 793914ced3 Update README.md (#4)
Correctd steps
2025-03-28 15:52:13 -07:00
Reithan 46540aa7bc Update README.md
Add example images
2025-03-24 00:58:59 -07:00
Reithan 873034b095 add init file for ComfyUI 2025-03-24 00:53:57 -07:00
Reithan 7d399643dd remove unneeded import 2025-03-24 00:53:44 -07:00
Reithan 50033c2622 add user examples 2025-03-24 00:52:15 -07:00
Reithan 36eb9d592b Update README.md
fix typo
2025-03-22 19:02:55 -07:00
Reithan bc50983954 Update README.md
tl;dr added
2025-03-22 19:01:27 -07:00
Reithan 0c37c6b124 Update README.md
Hide math stuff to help overwhelm
2025-03-22 18:56:38 -07:00
Reithan 8d7d9281f7 Update README.md
update tip verbiage
2025-03-22 18:51:30 -07:00
Reithan e55881afe2 Update README.md
"it's" to "its"
2025-03-22 18:40:25 -07:00
Reithan 0e8e508213 Update README.md
Turn params to bullet points
2025-03-22 18:36:09 -07:00
Reithan ff69ac386d Update README.md
Move image
2025-03-22 18:32:58 -07:00
Reithan 5be3c4c4f1 Update README.md
Update wording and instructions for clarity.
2025-03-22 18:31:19 -07:00
Reithan 81b836d2f1 Update README.md
Add Interactive Graph to Readme
2025-03-22 18:18:59 -07:00
Reithan a57c16a624 Update README.md 2025-03-22 05:06:20 -07:00
Reithan e6cdf03189 Update module UI style (#2)
Update module UI style
2025-03-22 01:47:42 -07:00
Reithan 47e4b5073b Update README.md
wording
2025-03-21 22:24:43 -07:00
Reithan 0778fa4b03 Update README.md
Update CFG comparison
2025-03-21 22:08:25 -07:00
14 changed files with 323 additions and 178 deletions
+27
View File
@@ -0,0 +1,27 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'Reithan' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
personal_access_token: ${{ secrets.COMFY_REGISTRY_KEY }}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

+186 -149
View File
@@ -1,179 +1,216 @@
import ldm_patched.modules.model_base
import logging
import torch
from enum import Enum, auto
from typing import Any
class PredictionType(Enum):
EPS = auto() # ε-prediction
V = auto() # v-prediction
X0 = auto() # x₀-prediction
UNKNOWN = auto() # couldn’t detect / new scheduler
_RAW_TO_ENUM = {
"eps": PredictionType.EPS,
"epsilon": PredictionType.EPS,
"v": PredictionType.V,
"v_prediction": PredictionType.V,
"x0": PredictionType.X0,
"sample": PredictionType.X0,
}
class NRS:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"skew": ("FLOAT", {"default": 2.0, "min": -30.0, "max": 30.0, "step": 0.01}),
"skew": ("FLOAT", {"default": 4.0, "min": -30.0, "max": 30.0, "step": 0.01}),
"stretch": ("FLOAT", {"default": 2.0, "min": -30.0, "max": 30.0, "step": 0.01}),
"squash": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"squash": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "advanced/model"
def _get_pred_type(self, model) -> PredictionType:
"""
In order to support Comfy, Forge, and possibly other models
and various loaders.
Walk common wrappers until we find something that looks like a
prediction-type flag, then map it to the enum.
Defaults to EPS if all else fails.
"""
def _canon(p):
if p is None:
return ""
if isinstance(p, bytes):
p = p.decode(errors="ignore")
if isinstance(p, Enum):
p = p.name
return str(p).strip().lower()
# Breadth-first search through a few well-known wrappers.
queue, seen = [model], set()
while queue:
obj = queue.pop(0)
# 1) direct hit on this object ---------------------------------
for attr in ("model_type", "prediction_type", "parameterization"):
p = _canon(getattr(obj, attr, None))
if p:
return _RAW_TO_ENUM.get(p, PredictionType.UNKNOWN)
# 2) enqueue child containers we care about -------------------
for attr in ("model", "diffusion_model", "config", "scheduler", "inner_model", "model_sampling"):
child = getattr(obj, attr, None)
if child is not None and id(child) not in seen:
seen.add(id(child))
queue.append(child)
# 3) default ------------------------------------------------------
return PredictionType.UNKNOWN
def _convert_to_eps_space(self, x_orig, sig_root, sigma, cond, uncond):
x_div = None
eps_cond = cond
eps_uncond = uncond
if self.__pred_type == PredictionType.V:
# v → ε conversion
logging.debug(f"NRS._convert_to_eps_space: generating x_div, eps_cond, and eps_uncond for v-pred")
x_div = x_orig / (sigma ** 2 + 1)
eps_cond = ((x_div - (x_orig - cond)) * sig_root) / (sigma)
eps_uncond = ((x_div - (x_orig - uncond)) * sig_root) / (sigma)
elif self.__pred_type == PredictionType.EPS:
logging.debug(f"NRS._convert_to_eps_space: already in eps, no pre-scale needed")
pass # already in ε space
elif self.__pred_type == PredictionType.X0:
raise NotImplementedError("NRS._convert_to_eps_space: x0-prediction not supported yet.")
else:
raise RuntimeError("NRS._convert_to_eps_space: Could not determine prediction type for this model.")
return x_div, eps_cond, eps_uncond
def _finalize_from_eps_space(self, x_orig, x_div, x_final, sig_root, sigma):
nrs_result = x_final
if self.__pred_type == PredictionType.V:
# ε → v conversion
logging.debug(f"NRS._finalize_from_eps_space: generating cfg_result for v-pred")
nrs_result = x_orig - (x_div - x_final * sigma / sig_root)
elif self.__pred_type == PredictionType.EPS:
# already in ε space
logging.debug(f"NRS._finalize_from_eps_space: already in eps, no post-scale needed")
pass
elif self.__pred_type == PredictionType.X0:
raise NotImplementedError("NRS._finalize_from_eps_space: x0-prediction not supported yet.")
else:
raise RuntimeError("NRS._finalize_from_eps_space: Could not determine prediction type for this model.")
return nrs_result
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond):
x_div = None
v_cond = cond
v_uncond = uncond
if self.__pred_type == PredictionType.V:
logging.debug(f"NRS._convert_to_v_space: already in v, no pre-scale needed")
pass # already in v space
elif self.__pred_type == PredictionType.EPS:
# ε → v conversion
logging.debug(f"NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps")
x_div = x_orig / (sigma ** 2 + 1)
factor = sigma / sig_root
v_cond = x_orig - (x_div - cond * factor)
v_uncond = x_orig - (x_div - uncond * factor)
elif self.__pred_type == PredictionType.X0:
raise NotImplementedError("NRS._convert_to_v_space: x0-prediction not supported yet.")
else:
raise RuntimeError("NRS._convert_to_v_space: Could not determine prediction type for this model.")
return x_div, v_cond, v_uncond
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma):
nrs_result = x_final
if self.__pred_type == PredictionType.V:
# already in v space
logging.debug(f"NRS._finalize_from_v_space: already in v, no post-scale needed")
pass
elif self.__pred_type == PredictionType.EPS:
# v → ε conversion
logging.debug(f"NRS._finalize_from_v_space: generating cfg_result for eps")
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
elif self.__pred_type == PredictionType.X0:
raise NotImplementedError("NRS._finalize_from_v_space: x0-prediction not supported yet.")
else:
raise RuntimeError("NRS._finalize_from_v_space: Could not determine prediction type for this model.")
return nrs_result
def patch(self, model, skew, stretch, squash):
self.__pred_type = self._get_pred_type(model) if not hasattr(self, "__pred_type") else self.__pred_type
self.__OPERATION_SPACE = PredictionType.V
def nrs(args):
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
# self.__pred_type = self.__pred_type if self.__pred_type is not None else self._get_pred_type(model)
cond = args["cond"]
uncond = args["uncond"]
x_orig = args["input"]
sigma = args["sigma"]
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
x_orig = args["input"]
sig_root = (sigma ** 2 + 1).sqrt()
x_div, nrs_cond, nrs_uncond = None, None, None
match self.__OPERATION_SPACE:
case PredictionType.V:
x_div, nrs_cond, nrs_uncond = self._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond)
case PredictionType.EPS:
x_div, nrs_cond, nrs_uncond = self._convert_to_eps_space(x_orig, sig_root, sigma, cond, uncond)
case PredictionType.X0:
raise RuntimeError("NRS.nrs: x0-prediction not supported yet.")
case PredictionType.UNKNOWN:
raise RuntimeError("NRS.nrs: Could not determine prediction type for this operation.")
case _:
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
def _dot(a, b):
return (a*b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
#rescale cfg has to be done on v-pred model output
x = x_orig / (sigma * sigma + 1.0)
cond = ((x - (x_orig - cond)) * (sigma ** 2 + 1.0) ** 0.5) / (sigma)
uncond = ((x - (x_orig - uncond)) * (sigma ** 2 + 1.0) ** 0.5) / (sigma)
logging.debug(f"NRS.nrs: generated cond and uncond")
def _nrm2(v):
return _dot(v, v)
x_final = None
match "v0.4.5":
case "v1":
# displace cond by rejection of uncond on cond
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c = (u_dot_c / c_dot_c) * cond
u_rej_c = uncond - u_on_c
displaced = (cond - skew * u_rej_c)
logging.debug(f"NRS.nrs: displaced")
eps = torch.finfo(nrs_cond.dtype).eps
c_dot_c = _nrm2(nrs_cond) + eps # [B,1,W,H]
u_dot_c = _dot(nrs_uncond, nrs_cond) # [B,1,W,H]
u_on_c = (u_dot_c / c_dot_c) * nrs_cond # [B,1,W,H] * [B,C,H,W]
# Amplify Cond based on length compared to projection of uncond
proj_diff = nrs_cond - u_on_c
stretched = nrs_cond + (stretch * proj_diff)
# squash displaced vector towards len(cond) based on squash scale
d_len_sq = torch.sum(displaced * displaced, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * ((c_dot_c/d_len_sq) ** 0.5)
squashed = displaced * squash_scale
logging.debug(f"NRS.nrs: squashed")
# Skew/Steer Conf based on rejection of uncond on cond
u_rej_c = nrs_uncond - u_on_c
skewed = stretched - (skew * u_rej_c)
# stretch turned vector towards cond based on stretch scale
sq_dot_c = torch.sum(squashed * cond, dim=-1, keepdim=True)
sq_on_c = (sq_dot_c / c_dot_c) * cond
x_final = squashed + sq_on_c * stretch
logging.debug(f"NRS.nrs: final")
case "v2":
# displace cond by rejection of uncond on cond
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
displaced = cond + stretch * (cond - torch.clamp(u_dot_c / c_dot_c, min=0, max=1) * cond) - skew * u_rej_c
logging.debug(f"NRS.nrs: displaced & stretched")
# Squash final length back down to original length of cond
cond_len = nrs_cond.norm(dim=1, keepdim=True)
nrs_len = skewed.norm(dim=1, keepdim=True) + eps
# squash displaced vector towards len(cond) based on squash scale
d_len_sq = torch.sum(displaced * displaced, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * ((c_dot_c/d_len_sq) ** 0.5)
x_final = displaced * squash_scale
logging.debug(f"NRS.nrs: final")
case "v3":
# displace cond by rejection of uncond on cond
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
displaced = (cond - skew * u_rej_c)
logging.debug(f"NRS.nrs: displaced")
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
x_final = skewed * squash_scale
# squash displaced vector towards len(cond) based on squash scale
d_len_sq = torch.sum(displaced * displaced, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * ((c_dot_c/d_len_sq) ** 0.5)
# stretch vector towards 2*len(cond) - len(u_on_c)
c_len = c_dot_c ** 0.5
stretch_scale = (1 - stretch) + stretch * (2 * c_len - u_on_c_mag)/c_len
x_final = displaced * squash_scale * stretch_scale
logging.debug(f"NRS.nrs: final")
case "v4":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
rej_dor_rej = torch.sum(u_rej_c * u_rej_c, dim=-1, keepdim=True)
x_final = (cond - squash * u_rej_c + stretch * cond * ((rej_dor_rej/c_dot_c) ** 0.5))
logging.debug(f"NRS.nrs: displaced")
case "v0.4.1":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
rej_dor_rej = torch.sum(u_rej_c * u_rej_c, dim=-1, keepdim=True)
stretched = cond + stretch * cond * ((rej_dor_rej/c_dot_c) ** 0.5)
skewed = stretched - skew * u_rej_c
sk_dot_sk = torch.sum(skewed * skewed, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * ((c_dot_c/sk_dot_sk) ** 0.5)
x_final = skewed * squash_scale
logging.debug(f"NRS.nrs: displaced")
case "v0.4.2":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
proj_len = torch.sum(u_on_c * u_on_c, dim=-1, keepdim=True) ** 0.5
cond_len = c_dot_c ** 0.5
stretched = cond * (1 + stretch * torch.abs(cond_len - proj_len) / cond_len)
skewed = stretched - skew * u_rej_c
sk_dot_sk = torch.sum(skewed * skewed, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * cond_len / (sk_dot_sk ** 0.5)
x_final = skewed * squash_scale
logging.debug(f"NRS.nrs: displaced")
case "v0.4.3":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
proj_len = torch.sum(u_on_c * u_on_c, dim=-1, keepdim=True) ** 0.5
cond_len = c_dot_c ** 0.5
stretched = cond * (1 + stretch * (cond_len - proj_len) / cond_len)
skewed = stretched - skew * u_rej_c
sk_dot_sk = torch.sum(skewed * skewed, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * cond_len / (sk_dot_sk ** 0.5)
x_final = skewed * squash_scale
logging.debug(f"NRS.nrs: displaced")
case "v0.4.4":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
cond_len = c_dot_c ** 0.5
proj_diff = cond - u_on_c
proj_diff_len = torch.sum(proj_diff * proj_diff, dim=-1, keepdim=True) ** 0.5
stretched = cond * (1 + stretch * proj_diff_len / cond_len)
skewed = stretched - skew * u_rej_c
sk_dot_sk = torch.sum(skewed * skewed, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * cond_len / (sk_dot_sk ** 0.5)
x_final = skewed * squash_scale
logging.debug(f"NRS.nrs: displaced")
case "v0.4.5":
u_dot_c = torch.sum(uncond * cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(cond * cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * cond
u_rej_c = uncond - u_on_c
cond_len = c_dot_c ** 0.5
proj_diff = cond - u_on_c
# Amplify Cond based on length compared to projection of uncond
stretched = cond + (stretch * proj_diff)
# Skew/Steer Conf based on rejection of uncond on cond
skewed = stretched - skew * u_rej_c
# Squash final length back down to original length of cond
sk_dot_sk = torch.sum(skewed * skewed, dim=-1, keepdim=True)
squash_scale = (1 - squash) + squash * cond_len / (sk_dot_sk ** 0.5)
x_final = skewed * squash_scale
return x_orig - (x - x_final * sigma / (sigma * sigma + 1.0) ** 0.5)
match self.__OPERATION_SPACE:
case PredictionType.V:
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma)
case PredictionType.EPS:
return self._finalize_from_eps_space(x_orig, x_div, x_final, sig_root, sigma)
case PredictionType.X0:
raise RuntimeError("NRS.nrs: x0-prediction not supported yet.")
case PredictionType.UNKNOWN:
raise RuntimeError("NRS.nrs: Could not determine prediction type for this operation.")
case _:
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
m = model.clone()
m.set_model_sampler_cfg_function(nrs, True)
+71 -19
View File
@@ -1,27 +1,79 @@
[![CodeQL](https://github.com/Reithan/negative_rejection_steering/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/Reithan/negative_rejection_steering/actions/workflows/github-code-scanning/codeql)
[![ComfyUI Registry](https://github.com/Reithan/negative_rejection_steering/actions/workflows/publish.yml/badge.svg)](https://registry.comfy.org/nodes/negative_rejection_steering)
# Negative Rejection Steering
NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidance with a more nuanced steering of the generation process.
NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidance with a more nuanced and composable steering of the generation process with better mathematical basis.
This is accomplised in 3 steps:
1. **Displacement**: The conditioned output tensor is displaced in the direction of the rejection of the unconditioned tensor on the conditioned tensor. This lengthens the tensor in a direction perpendicular to it's direction without affecting the positive guidance. The tensor is displaced by the rejection x the Displacement parameter.
2. **Squashing**: The displaced tensor is rescaled towards the original length of the conditioned tensor. This means for high displacement scaling values the tensor 'turns' away from the unconditioned direction, which for very negative displacements, it turns towards the unconditioned tensor. 0 displacement outputs the original conditioned tensor.
3. **Stretching**: The post-squash 'steered' tensor is stretched towards the direction of the original conditioned tensor. The more sharp the steering the less pronounced the stretch is, with fully aligned tensors being stretched the full stretch scale parameter. 1x stretch adds 100% length to the tensor.
#### _**TL;DR**_:
1. CFG is a bad 'knob'
2. NRS replaces CFG with 3 new knobs.
3. NRS lets you to create cooler outputs than CFG.
# Alpha Release
Implements NRS with Skew, Stretch, and Squash parameters.
> [!TIP]
> Skip to the [Beginner How-To](#beginner-how-to) if you want to just get started.
## Parameters
Skew and Stretch are roughly similar to CFG, but decomposed, with `Stretch + Skew = CFG`, roughly.
### Math Demonstration
<details>
<summary>Expand for explanation of algorithm</summary>
<img align="right" src="Examples/NRS_graph.png" alt="Graph of NRS vs CFG" style="width: 40%; float: right;">
**Skew** changes the 'direction' of generation, which should result in changes to the content and composition of the image.
**Stretch** changes to 'amplification' of generation, which should result in stronger prompt representation.
**Squash** 'normalizes' the resulting guidance back towards the original amplitude with 1.0 being the same amplitude, while 0.0 is the unmodified amplitude resulting from the Squash and Stretch functions.
### NRS is Applied in Three Steps:
0. ***V-Space**: Optional pre-NRS step* If the model is not using v-prediction, we transform the EPS `cond` and `uncond` into v-prediction space before continuing, then revert to eps-space before return.
1. **Skewing**: The conditioned output tensor is skewed away from the direction of the rejection of the unconditioned tensor on the conditioned tensor. This lengthens the tensor in a direction perpendicular to its direction without affecting the positive guidance. The tensor is displaced by the rejection multiplied by the Skew parameter.[^1]
2. **Stretching**: The skewed tensor is stretched towards the direction of the original conditioned tensor based on its difference from the projection of uncond on cond. The stretch is multiplied by the Stretch parameter.[^1]
3. **Squashing**: The skewed and stretched tensor is rescaled towards the original length of the conditioned tensor. 100% squashing outputs the original length of the conditioned tensor simply 'steered' towards the skewed & squashed version's direction.[^1]
[^1]: All operations are done per feature across the step's batch, width, and height.
[Interactive Graph on Math3D.org](https://www.math3d.org/aTJW4UZtCh)
</details>
## Examples of NRS Effects
**Skew**
![Skew Example](Examples/skew_array.png)
**Stretch**
![Stretch Example](Examples/stretch_array.png)
**Squash**
![Squash Example](Examples/squash_matrix.png)
<details>
<summary><small>Generation details for reproduction</small></summary>
| Prompt | |
| ---------- | --- |
| Tool | [Stable Diffusion WebUI reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge) |
| Sampler | DPM++ 2M |
| Scheduler | Align Your Steps |
| Steps | 25 |
| Dimensions | 912 x 624 |
| Seed | `1334103348` |
| Model | [Lobotomized Mix v1.5](https://civitai.com/models/1144932) |
| Embeddings | [Lazy Embeddings for ALL illustrious NoobAI...](https://civitai.com/models/1302719), [Smooth Embeddings](https://civitai.com/models/1065154) |
| Positive | lazypos, [Smooth_Quality\|SmoothNoob_Quality], BREAK<br>very awa, masterpiece, best quality, year 2024, newest, highres, absurdres,<br>1girl, samurai archer, cyberpunk cityscape, rain-soaked rooftop, neon reflection puddles, volumetric mist,<br>photorealistic, digital art,<br>dramatic rim lighting, shallow depth of field, low angle viewpoint |
| Negative | lazyloli, lazynsfw, BREAK<br>lazyhand, SmoothNegative_Hands-neg, BREAK<br>[Smooth_Negative-neg\|SmoothNoob_Negative-neg], BREAK<br>lowres, worst quality, worst aesthetic, bad quality, jpeg artifacts, scan artifacts,<br>blurry, deformed anatomy, bad hands, extra fingers, missing fingers, mutated hands,<br>watermark, logo, text, nsfw |
</details>
### Explanation of Effects
#### Skew
**Skew** changes the 'direction' of your generation, altering the image generation to 'steer' away from negative prompt elements as they conflict with your positive prompt. Increasing Skew will change scene composition, geometry, and scene elements to ensure that the final image aligns with the intention of your prompt pair.
#### Stretch
**Stretch** changes the intensity of generated elements that align more with your positive prompt than the negative. This 'hits the gas' on any elements that are more strongly aligned with your positive prompt than your negative, and 'hit the brakes' on the opposite.
#### Squash
**Squash** is the speed limit. At 0.0 Squash, each diffusion step receives the full intensity you set from Skew and Stretch, while 1.0 Squash ensures each step has only the original step size output by the model. This setting will only remove intensity unless you have a non-zero Skew value. Squash will 'soften' the effects of Skew and Stretch as it's raised, but the 'removed' Skew and Stretch intensity is replaced by enhanced micro-detailing and 'burn'. Squash should generally be left low and used as a 'finishing' step after dialing in a decent Skew and Stretch value.
## Beginner How-To
1. Set Squash to 0.0
2. Set Skew & Stretch each to 1/2 your normal CFG Scale setting
3. Test some generation. Results should be 'similar' in quality to CFG
4. Adjust Skew up/down to change content and composition
5. Adjust Stretch up/down to change strength of image aspects and colors
6. Adjust Squash up to remove artifacts and color burn (these will tend to be replaced by additional or extraneous details and elements)
1. Set Skew to your normal CFG Scale setting and Stretch to 1/2 your normal CFG Scale. Set Squash to 0.0.<br>
*Alternatively, try starting at 1/1/0.0 to get a baseline.*
2. Test some outputs. Results should be similar in quality to CFG.
3. Adjust Skew to change the intensity of your outputs adherence to your positive and negative prompts. This primarily effects composition of the output.
4. Adjust Stretch to intensify your positive prompt's aspects and colors where they differ from the negative prompt. This primarily effects color and texture.
5. Adjust Squash to soften Skew and Stretch's effects. The intensity removed from Skew and Stretch will generally become additional micro-detailing and elements.
**Tip**: You can experiment with negative values for Skew and Stretch as well to see what the model 'believes' your negative prompt 'means'.
> [!TIP]
> You can experiment with negative values for each setting as well. This can be useful to understand how the model interpreting your negative prompt.
> [!WARNING]
> Don't set NRS values to negatives if there are things in your negative prompt you **actually** don't want to see.
## Submitted User Examples
| User | CFG | NRS |
| --- | --- | --- |
| Mohnjiles from StabilityMatrix | ![CFG Example](Examples/mohnjiles_cfg.png) | ![NRS Example](Examples/mohnjiles_nrs.png) |
+5
View File
@@ -0,0 +1,5 @@
from .NRS.nodes_NRS import *
NODE_CLASS_MAPPINGS = {"NRS": NRS}
NODE_DISPLAY_NAME_MAPPINS = {"NRS": "Negative Rejection Steering"}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

+11
View File
@@ -0,0 +1,11 @@
# The 'purpose' or 'intention' behind our 3 knobs
## SKEW
This is the primary 'steering' knob. This will 'turn' the 'direction' the current denoising step is traveling in the latent space. If we define the 'default' (cond) direction as 'prior step -> cond' then we're just applying a 'lateral' skew to that direction to 'turn' it 'away' from the 'unintended' direction (uncond)
## STRETCH
This is the sister knob to Skew. This is the accelerator. We want to go 'faster' into the intended direction (cond) the less aligned it is with the unintended direction (uncond). Think of this like a combination of brakes + gas. If we're headed directly for a brick wall (uncond is in the same direction as cond), we want to apply no acceleration, or negative acceleration. If we're traveling directly away from danger (uncond is in the opposite direction of cond) then we want to stomp the gas and get as far away as we can. There's only 1 problem with this BASIC-level description: as we get further into generation, regardless of pos/neg promp, cond & uncon will naturally align to be the same vector[^1]. In the last stop of inference, cond and uncond will be basically identical if nothing has fucked up. So whatever math we apply here needs to take the progressive alignment of cond & uncond into account. That's why were/are scaling only on the projection difference right now, rather than the full projection.
[^1]: This is more true in eps than v-pred. Stretch is inherently more powerful in v-pred based models than eps models.
## SQUASH
This is out 'safety' knob. Think of this like a 'limiter' in a car. This sets the 'top speed' we can go to some multiple of the 'default' speed the model would 'like to' go. i.e. whatever length of directional vector the model produces prior to any skewing or stretching is treated as the 'default' length with Squash=1.0 ensuring we only every go that 'speed' and no more, while Squash=0.0 lets us go any speed we want based on the other 2 knobs. GENERALLY we'll be leaving Squash at 0.0 unless we need it for specific generations.
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "negative_rejection_steering"
description = "NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidance with a more nuanced and composable steering of the generation process with better mathematical basis."
authors = [{name = "Bryan O'Malley", email = "bo122081@hotmail.com"}]
version = "0.7.0"
license = {file = "LICENSE"}
readme = "README.md"
[project.urls]
Repository = "https://github.com/Reithan/negative_rejection_steering"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "reithan"
DisplayName = "Negative Rejection Steering"
Icon = "https://raw.githubusercontent.com/Reithan/negative_rejection_steering/main/icon.png"
+7 -10
View File
@@ -11,28 +11,25 @@ class NRSScript(scripts.Script):
def __init__(self):
super().__init__()
self.enabled = False
self.skew = 2.0
self.skew = 4.0
self.stretch = 2.0
self.squash = 1.0
self.squash = 0.0
sorting_priority = 5
def title(self):
return "Negative Rejection Steering for reForge"
return "Negative Rejection Steering"
def show(self, is_img2img):
return scripts.AlwaysVisible
def ui(self, *args, **kwargs):
with gr.Accordion(open=False, label=self.title()):
gr.HTML("<p><i>Adjust the settings for Negative Rejection Steering.</i></p>")
enabled = gr.Checkbox(label="Enable NRS", value=self.enabled)
gr.HTML("<p><i>Adjust the amount guidance is steered.</i></p>")
skew = gr.Slider(label="NRS Skew Scale", minimum=-30.0, maximum=30.0, step=0.01, value=self.skew)
gr.HTML("<p><i>Adjust the amount guidance is amplified.</i></p>")
stretch = gr.Slider(label="NRS Stretch Scale", minimum=-30.0, maximum=30.0, step=0.01, value=self.stretch)
gr.HTML("<p><i>Adjust the amount final guidance is normalized.</i></p>")
squash = gr.Slider(label="NRS Squash Multiplier", minimum=0.0, maximum=1.0, step=0.01, value=self.squash)
gr.HTML("<p><i>Adjust the settings for Negative Rejection Steering.</i></p>")
skew = gr.Slider(label="NRS Skew Scale", info="Adjusts the amount guidance is steered.", minimum=-30.0, maximum=30.0, step=0.01, value=self.skew)
stretch = gr.Slider(label="NRS Stretch Scale", info="Adjusts the amount guidance is amplified.", minimum=-30.0, maximum=30.0, step=0.01, value=self.stretch)
squash = gr.Slider(label="NRS Squash Multiplier", info="Adjusts the amount final guidance is normalized.", minimum=0.0, maximum=1.0, step=0.01, value=self.squash)
enabled.change(
lambda x: self.update_enabled(x),