Compare commits

..
2 Commits
Author SHA1 Message Date
Reithan a70d1d09bb Fix prediction type detection for WAN/RES4LYF samplers (#30)
## Summary

Fixes issue #20 where NRS would fail with `RuntimeError: "Could not
determine prediction type for this model"` when using certain samplers
like WanKSamplerAdvanced and RES4LYF ClownsharKsampler.

## Changes Made

- **Enhanced prediction type mapping**: Added support for FLOW models
(`"flow"`, `"wan"`, `"const"` → `PredictionType.EPS`)
- **Improved model introspection**: Added `model_sampling` class
inspection and `model.model.model_type` enum detection
- **Graceful fallback**: Replaced RuntimeErrors with safe EPS defaults
when prediction type cannot be determined
- **Better debugging**: Added warning logs when using fallback
prediction types
- **Documentation**: Updated README with sampler compatibility
information
- **Version bump**: 0.7.3 → 0.7.4

## Technical Details

The issue occurred because WAN and similar samplers use **FLOW model
types** (ModelType.FLOW) that implement the CONST prediction class,
which are fundamentally different from traditional EPS/V-prediction
models. The original code only checked for string attributes and failed
to recognize these newer model architectures.

This fix follows the established pattern from commits fc38b5c (flux
support) and ba145c4 (chroma support) while adding robust fallback
mechanisms.

## Testing

- ✅ Manual integration testing planned with WanKSamplerAdvanced
- ✅ Manual integration testing planned with RES4LYF ClownsharKsampler  
- ✅ Backwards compatibility maintained for existing samplers
- ✅ Enhanced logging for debugging unrecognized models

## Risk Assessment

**Low Risk**: Only enhances existing detection logic without changing
core mathematical operations. Adds fallback instead of removing
functionality.

Closes #20

---------

Signed-off-by: Reithan <bo122081@hotmail.com>
2026-05-15 13:57:06 -07:00
Reithan b034c3f09f Improve setup documentation and ComfyUI UX (#29)
## Summary
Addresses documentation gaps identified in issue #25 by improving setup
instructions and user experience across all supported platforms.

## Changes Made

### 📚 README Enhancements
- **ComfyUI Setup Section**: Added collapsible setup instructions with:
  - Clear workflow explanation (Model → NRS Node → KSampler)
  - Prominent note that CFG setting on KSampler is ignored
  - Pro tip for validating NRS is working
  - Reference to the workflow image from issue #25

- **A1111/Forge/reForge Setup Section**: Added setup instructions
explaining:
  - Extension installation and enabling process
  - CFG Scale is ignored when NRS is active
  - Reference to Beginner How-To for parameter guidance

- **StabilityMatrix Integration**: Added section highlighting native
support with link to https://lykos.ai/

### 🖥️ ComfyUI Node UX Improvements
- **Node Description**: Added clear explanation that NRS replaces CFG
and KSampler CFG will be ignored
- **Parameter Tooltips**: Added helpful guidance directly in the
interface:
  - **Skew**: Explains direction steering, suggests starting with CFG/2
- **Stretch**: Explains positive intensification, suggests normal CFG
value
- **Squash**: Explains effect softening, recommends keeping low
initially

## Problem Solved
This addresses the confusion reported in issue #25 where users struggled
with:
- How to configure CFG values in ComfyUI workflows
- Understanding the relationship between NRS and CFG
- Lack of example workflows and clear setup guidance

## Test Plan
- [x] Verify README renders correctly with collapsible sections
- [ ] Test ComfyUI node shows tooltips when hovering over parameters
- [ ] Confirm node description appears in ComfyUI interface
- [x] Validate links work correctly (StabilityMatrix)

Fixes #25
2026-05-15 11:59:20 -07:00
4 changed files with 131 additions and 16 deletions
+5
View File
@@ -172,3 +172,8 @@ cython_debug/
# PyPI configuration file
.pypirc
# AI agents
.claude/settings.local.json
.claude/docs/**
+81 -15
View File
@@ -15,6 +15,9 @@ _RAW_TO_ENUM = {
"epsilon": PredictionType.EPS,
"flux": PredictionType.EPS,
"chroma": PredictionType.EPS,
"flow": PredictionType.EPS, # FLOW models (WAN, etc.) are EPS-compatible
"wan": PredictionType.EPS, # WAN21 is FLOW-based
"const": PredictionType.EPS, # CONST prediction class used in FLOW models
"v": PredictionType.V,
"v_prediction": PredictionType.V,
"x0": PredictionType.X0,
@@ -24,16 +27,21 @@ _RAW_TO_ENUM = {
class NRS:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"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 {"required": { "model": ("MODEL", {"tooltip": "Input model to apply NRS to"}),
"skew": ("FLOAT", {"default": 2.00, "min": -30.0, "max": 30.0, "step": 0.01,
"tooltip": "Changes the 'direction' of generation, steering away from negative prompt elements. Start with CFG/2."}),
"stretch": ("FLOAT", {"default": 5.00, "min": -30.0, "max": 30.0, "step": 0.01,
"tooltip": "Intensifies positive prompt elements. Start with your normal CFG value."}),
"squash": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "Softens Skew/Stretch effects, adding micro-detailing. Keep low initially."}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "advanced/model"
DESCRIPTION = "Negative Rejection Steering (NRS) replaces CFG with more nuanced guidance. IMPORTANT: Set your KSampler CFG to any value (it will be ignored). Connect your model through this node before sampling."
def _get_pred_type(self, model) -> PredictionType:
"""
In order to support Comfy, Forge, and possibly other models
@@ -61,7 +69,10 @@ class NRS:
for attr in ("model_type", "prediction_type", "parameterization"):
p = _canon(getattr(obj, attr, None))
if p:
return _RAW_TO_ENUM.get(p, PredictionType.UNKNOWN)
pred_type = _RAW_TO_ENUM.get(p, PredictionType.UNKNOWN)
if pred_type != PredictionType.UNKNOWN:
logging.debug(f"NRS._get_pred_type: Found prediction type '{p}' from attribute '{attr}' -> {pred_type}")
return pred_type
# 2) enqueue child containers we care about -------------------
for attr in ("model", "diffusion_model", "config", "scheduler", "inner_model", "model_sampling"):
@@ -70,8 +81,46 @@ class NRS:
seen.add(id(child))
queue.append(child)
# 3) default ------------------------------------------------------
return PredictionType.UNKNOWN
# 3) enhanced detection for FLOW models (WAN, Flux, etc.) -------
try:
# Check model_sampling class type for FLOW models
if hasattr(model, "model_sampling") and model.model_sampling is not None:
sampling_class_name = type(model.model_sampling).__name__.lower()
logging.debug(f"NRS._get_pred_type: Found model_sampling class: {sampling_class_name}")
# CONST class is used by FLOW models (WAN21, Flux, etc.)
if "const" in sampling_class_name:
logging.debug(f"NRS._get_pred_type: Detected FLOW model via CONST sampling class -> EPS")
return PredictionType.EPS
elif "v_prediction" in sampling_class_name:
logging.debug(f"NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
return PredictionType.V
elif "eps" in sampling_class_name:
logging.debug(f"NRS._get_pred_type: Detected EPS model via sampling class -> EPS")
return PredictionType.EPS
# Check model.model.model_type enum for newer models
if hasattr(model, "model") and hasattr(model.model, "model_type"):
model_type_str = _canon(str(model.model.model_type))
logging.debug(f"NRS._get_pred_type: Found model.model.model_type: {model_type_str}")
if "flow" in model_type_str or "flux" in model_type_str:
logging.debug(f"NRS._get_pred_type: Detected FLOW/Flux model via model_type -> EPS")
return PredictionType.EPS
elif "v_prediction" in model_type_str:
logging.debug(f"NRS._get_pred_type: Detected V-prediction model via model_type -> V")
return PredictionType.V
elif "eps" in model_type_str:
logging.debug(f"NRS._get_pred_type: Detected EPS model via model_type -> EPS")
return PredictionType.EPS
except Exception as e:
logging.debug(f"NRS._get_pred_type: Exception during enhanced detection: {e}")
# 4) safe default (matches docstring promise) --------------------
logging.warning(f"NRS._get_pred_type: Could not determine prediction type for model. Using EPS as fallback.")
logging.debug(f"NRS._get_pred_type: Model structure: {[attr for attr in dir(model) if not attr.startswith('_')]}")
return PredictionType.EPS
def _convert_to_eps_space(self, x_orig, sig_root, sigma, cond, uncond):
x_div = None
@@ -90,7 +139,9 @@ class NRS:
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.")
# Fallback: treat UNKNOWN as EPS (should not happen with enhanced detection)
logging.warning(f"NRS._convert_to_eps_space: Unknown prediction type {self.__pred_type}, treating as EPS")
pass
return x_div, eps_cond, eps_uncond
@@ -107,7 +158,9 @@ class NRS:
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.")
# Fallback: treat UNKNOWN as EPS (should not happen with enhanced detection)
logging.warning(f"NRS._finalize_from_eps_space: Unknown prediction type {self.__pred_type}, treating as EPS")
pass
return nrs_result
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond):
@@ -128,7 +181,13 @@ class NRS:
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.")
# Fallback: treat UNKNOWN as EPS and convert to V-space
logging.warning(f"NRS._convert_to_v_space: Unknown prediction type {self.__pred_type}, treating as EPS")
logging.debug(f"NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps (fallback)")
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)
return x_div, v_cond, v_uncond
@@ -145,7 +204,10 @@ class NRS:
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.")
# Fallback: treat UNKNOWN as EPS and convert from V-space
logging.warning(f"NRS._finalize_from_v_space: Unknown prediction type {self.__pred_type}, treating as EPS")
logging.debug(f"NRS._finalize_from_v_space: generating cfg_result for eps (fallback)")
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
return nrs_result
def patch(self, model, skew, stretch, squash):
@@ -172,9 +234,11 @@ class NRS:
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.")
# Fallback: treat as EPS (should not happen with enhanced detection)
logging.warning(f"NRS.nrs: Unknown operation space, treating as EPS for conversion")
x_div, nrs_cond, nrs_uncond = self._convert_to_eps_space(x_orig, sig_root, sigma, cond, uncond)
case _:
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
raise RuntimeError(f"NRS.nrs: Invalid PredictionType used for operation space: {self.__OPERATION_SPACE}")
def _dot(a, b):
return (a*b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
@@ -210,9 +274,11 @@ class NRS:
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.")
# Fallback: treat as EPS (should not happen with enhanced detection)
logging.warning(f"NRS.nrs: Unknown operation space, treating as EPS for finalization")
return self._finalize_from_eps_space(x_orig, x_div, x_final, sig_root, sigma)
case _:
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
raise RuntimeError(f"NRS.nrs: Invalid PredictionType used for operation space: {self.__OPERATION_SPACE}")
m = model.clone()
m.set_model_sampler_cfg_function(nrs, True)
+44
View File
@@ -73,6 +73,50 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
> [!WARNING]
> Don't set NRS values to negatives if there are things in your negative prompt you **actually** don't want to see.
## Setup & Installation
### ComfyUI
<details>
<summary>ComfyUI Setup Instructions</summary>
#### Installation
Install via ComfyUI Manager or manually clone this repository into your `ComfyUI/custom_nodes/` directory.
#### Usage
1. **Important**: Ignore the CFG setting on your KSampler node - NRS replaces CFG entirely
2. Connect your model through the **Negative Rejection Steering** node before sampling
3. Configure NRS parameters (Skew/Stretch/Squash) instead of using CFG
#### Basic Workflow
```
Model → NRS Node → KSampler
```
**Pro tip**: To verify NRS is working correctly, set CFG to an extremely high value (like 30). If your output looks normal, NRS is functioning properly. If the output appears "turbo fried," check your node connections.
**Sampler Compatibility**: NRS now supports advanced samplers including WanKSamplerAdvanced, RES4LYF samplers, and FLOW models (WAN21, Flux) with enhanced prediction type detection.
![ComfyUI Workflow Example](https://github.com/user-attachments/assets/edaa36a4-9ad8-4a35-bad3-dda80138b996)
</details>
### Automatic1111 / Forge / reForge
<details>
<summary>WebUI Setup Instructions</summary>
#### Installation
1. Install the extension through the Extensions tab in your WebUI
2. Enable the extension and restart your WebUI
#### Usage
Once installed and enabled, the NRS settings panel will appear in your generation interface. When NRS is active:
- **CFG Scale is ignored** - the WebUI may still show the CFG setting, but it has no effect
- Use the NRS parameters (Skew/Stretch/Squash) to control generation instead
- Follow the same parameter guidelines from the [Beginner How-To](#beginner-how-to) section
</details>
### StabilityMatrix Integration
NRS is available as a **natively supported module** in [StabilityMatrix](https://lykos.ai/), providing an easy installation and management option for users of that platform.
## Submitted User Examples
| User | CFG | NRS |
| --- | --- | --- |
+1 -1
View File
@@ -2,7 +2,7 @@
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"
version = "0.7.4"
license = {file = "LICENSE"}
readme = "README.md"