Compare commits

..
17 Commits
Author SHA1 Message Date
Reithan 2583c237f2 Bump version from 0.7.2 to 0.7.3
Signed-off-by: Reithan <bo122081@hotmail.com>
2026-02-13 17:41:23 -08:00
Reithan c83958c457 Update default parameter values for NRS node and script (#26)
Updates the default parameter values for the Negative Rejection Steering
implementation to improve usability and user experience.

## Changes
- **Skew**: 4.0 → 2.00
- **Stretch**: 2.0 → 5.00  
- **Squash**: 0.0 → 0.75

These new defaults (2/5/0.75) provide a better starting point for users,
as documented in the updated README.

## Files Modified
- `NRS/nodes_NRS.py`: Updated default values in INPUT_TYPES
- `scripts/negative_rejection_steering_script.py`: Updated default
initialization values
- `README.md`: Updated beginner how-to guide to reference the new
defaults

---------

Signed-off-by: Reithan <bo122081@hotmail.com>
2026-02-13 17:36:43 -08:00
Reithan a0b2d99bc7 Bump version from 0.7.1 to 0.7.2 2025-12-04 20:44:46 -08:00
Reithan ba145c4722 Add 'chroma' to _RAW_TO_ENUM mapping (#23)
Ad support for chroma as EPS.
2025-12-04 18:29:03 -08:00
Reithan c21dbfe1e5 Update README.md (#21) 2025-09-11 20:27:24 -07:00
Alexander Brown fc38b5c998 Add flux to the Prediction Type inference (#18)
Testing it in Comfy, it seems like it works as `EPS`, though it also
worked with `V`.
2025-07-31 23:23:09 -07:00
Reithan d26fcf6fc8 Update pyproject.toml 2025-07-26 04:44:24 -07:00
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
6 changed files with 79 additions and 201 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 }}
+27 -195
View File
@@ -13,6 +13,8 @@ class PredictionType(Enum):
_RAW_TO_ENUM = {
"eps": PredictionType.EPS,
"epsilon": PredictionType.EPS,
"flux": PredictionType.EPS,
"chroma": PredictionType.EPS,
"v": PredictionType.V,
"v_prediction": PredictionType.V,
"x0": PredictionType.X0,
@@ -23,9 +25,9 @@ class NRS:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"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": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"skew": ("FLOAT", {"default": 2.00, "min": -30.0, "max": 30.0, "step": 0.01}),
"stretch": ("FLOAT", {"default": 5.00, "min": -30.0, "max": 30.0, "step": 0.01}),
"squash": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
@@ -161,7 +163,7 @@ class NRS:
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
sig_root = (sigma ** 2 + 1).sqrt()
nrs_cond, nrs_uncond = None, None
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)
@@ -174,201 +176,31 @@ class NRS:
case _:
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
x_final = None
match "v0.6.0":
case "v1":
# displace cond by rejection of uncond on cond
u_dot_c = torch.sum(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c = (u_dot_c / c_dot_c) * nrs_cond
u_rej_c = nrs_uncond - u_on_c
displaced = (nrs_cond - skew * u_rej_c)
logging.debug(f"NRS.nrs: displaced")
def _dot(a, b):
return (a*b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
# 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")
def _nrm2(v):
return _dot(v, v)
# stretch turned vector towards cond based on stretch scale
sq_dot_c = torch.sum(squashed * nrs_cond, dim=-1, keepdim=True)
sq_on_c = (sq_dot_c / c_dot_c) * nrs_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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
displaced = nrs_cond + stretch * (nrs_cond - torch.clamp(u_dot_c / c_dot_c, min=0, max=1) * nrs_cond) - skew * u_rej_c
logging.debug(f"NRS.nrs: displaced & stretched")
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)
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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
displaced = (nrs_cond - skew * u_rej_c)
logging.debug(f"NRS.nrs: displaced")
# Skew/Steer Conf based on rejection of uncond on cond
u_rej_c = nrs_uncond - u_on_c
skewed = stretched - (skew * u_rej_c)
# 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)
# 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
# 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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
rej_dor_rej = torch.sum(u_rej_c * u_rej_c, dim=-1, keepdim=True)
x_final = (nrs_cond - squash * u_rej_c + stretch * nrs_cond * ((rej_dor_rej/c_dot_c) ** 0.5))
logging.debug(f"NRS.nrs: displaced")
case "v0.4.1":
u_dot_c = torch.sum(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
rej_dor_rej = torch.sum(u_rej_c * u_rej_c, dim=-1, keepdim=True)
stretched = nrs_cond + stretch * nrs_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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_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 = nrs_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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_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 = nrs_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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
cond_len = c_dot_c ** 0.5
proj_diff = nrs_cond - u_on_c
proj_diff_len = torch.sum(proj_diff * proj_diff, dim=-1, keepdim=True) ** 0.5
stretched = nrs_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(nrs_uncond * nrs_cond, dim=-1, keepdim=True)
c_dot_c = torch.sum(nrs_cond * nrs_cond, dim=-1, keepdim=True)
u_on_c_mag = (u_dot_c / c_dot_c)
u_on_c = u_on_c_mag * nrs_cond
u_rej_c = nrs_uncond - u_on_c
cond_len = c_dot_c ** 0.5
proj_diff = nrs_cond - u_on_c
# Amplify Cond based on length compared to projection of uncond
stretched = nrs_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
case "v0.5.0":
def _dot(a, b):
return (a*b).flatten(2).sum(dim=2, keepdim=True) # [B,C,W,H] => [B,C,1]
def _nrm2(v):
return _dot(v, v)
eps = torch.finfo(nrs_cond.dtype).eps
c_dot_c = _nrm2(nrs_cond) + eps # [B,1]
u_dot_c = _dot(nrs_uncond, nrs_cond) # [B,1]
u_on_c = (u_dot_c / c_dot_c).unsqueeze(-1) * nrs_cond # [B,1,1,1] * [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)
# Skew/Steer Conf based on rejection of uncond on cond
u_rej_c = nrs_uncond - u_on_c
skewed = stretched - (skew * u_rej_c)
# Squash final length back down to original length of cond
cond_len = torch.sqrt(c_dot_c) # [B,1]
nrs_len = torch.sqrt(_nrm2(skewed)) + eps # [B,1]
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
x_final = skewed * squash_scale.unsqueeze(-1)
case "v0.6.0":
def _dot(a, b):
return (a*b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
def _nrm2(v):
return _dot(v, v)
eps = torch.finfo(nrs_cond.dtype).eps
c_dot_c = _nrm2(nrs_cond) + eps # [B,1]
u_dot_c = _dot(nrs_uncond, nrs_cond) # [B,1]
u_on_c = (u_dot_c / c_dot_c) * nrs_cond # [B,1,1,1] * [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)
# Skew/Steer Conf based on rejection of uncond on cond
u_rej_c = nrs_uncond - u_on_c
skewed = stretched - (skew * u_rej_c)
# Squash final length back down to original length of cond
cond_len = cond.norm(dim=1, keepdim=True)
nrs_len = skewed.norm(dim=1, keepdim=True)
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
x_final = skewed * squash_scale
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
x_final = skewed * squash_scale
match self.__OPERATION_SPACE:
case PredictionType.V:
@@ -388,4 +220,4 @@ class NRS:
NODE_CLASS_MAPPINGS = {
"NRS": NRS,
}
}
+6 -3
View File
@@ -1,3 +1,6 @@
[![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 and composable steering of the generation process with better mathematical basis.
@@ -54,11 +57,11 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
#### 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 has no effect 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.
**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 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.*
1. Set Skew to 1/2 of your normal CFG Scale setting and Stretch to your full normal CFG Scale. Set Squash to 0.0.<br>
*Alternatively, try starting with the default of 2/5/0.75, or at 1/1/1 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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

+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.3"
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"
@@ -11,9 +11,9 @@ class NRSScript(scripts.Script):
def __init__(self):
super().__init__()
self.enabled = False
self.skew = 4.0
self.stretch = 2.0
self.squash = 0.0
self.skew = 2.00
self.stretch = 5.00
self.squash = 0.75
sorting_priority = 5