Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be05ad7d2 | ||
|
|
244f8c656f | ||
|
|
5655dc0d5e | ||
|
|
e0e48d12c8 | ||
|
|
950b55df5f | ||
|
|
352ac56295 | ||
|
|
c526ec9353 | ||
|
|
d4ff852491 |
@@ -0,0 +1,160 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report unexpected NRS behavior or a crash
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug. Please fill out as much detail as you can — NRS's guidance behavior is sensitive to model type and node settings, so precise details help a lot.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform / UI
|
||||||
|
description: Which UI/front-end are you running NRS through?
|
||||||
|
options:
|
||||||
|
- ComfyUI
|
||||||
|
- AUTOMATIC1111
|
||||||
|
- Forge
|
||||||
|
- reForge
|
||||||
|
- Forge Neo
|
||||||
|
- Stability Matrix
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-other
|
||||||
|
attributes:
|
||||||
|
label: Platform / UI (if "Other")
|
||||||
|
description: If you selected "Other" above, name the platform/UI here.
|
||||||
|
placeholder: e.g. SD.Next, a custom fork, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-version
|
||||||
|
attributes:
|
||||||
|
label: Platform version
|
||||||
|
description: >
|
||||||
|
The version of whichever platform you selected above. Found in that
|
||||||
|
platform's UI (e.g. Help/About, Settings) or its startup console
|
||||||
|
output.
|
||||||
|
placeholder: e.g. 0.3.30 (ComfyUI) or v1.10.1 (AUTOMATIC1111/Forge)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: model-family
|
||||||
|
attributes:
|
||||||
|
label: Model / sampler family
|
||||||
|
description: Which model or sampler family were you using when the issue occurred?
|
||||||
|
options:
|
||||||
|
- MiniMax H3
|
||||||
|
- Flux
|
||||||
|
- Chroma
|
||||||
|
- WAN
|
||||||
|
- SDXL
|
||||||
|
- SD 1.5
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: model-family-other
|
||||||
|
attributes:
|
||||||
|
label: Model / sampler family (if "Other")
|
||||||
|
description: If you selected "Other" above, name the model/sampler family here.
|
||||||
|
placeholder: e.g. custom checkpoint, HunyuanVideo, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: nrs-version
|
||||||
|
attributes:
|
||||||
|
label: NRS version
|
||||||
|
description: >
|
||||||
|
Look for the NRS log line in your console/terminal:
|
||||||
|
`NRS v<version>: prediction type detected -> <TYPE>` (printed when
|
||||||
|
NRS runs, or check the extension's about/version info, depending on
|
||||||
|
your platform). Copy the version number from that line.
|
||||||
|
placeholder: e.g. 0.7.4
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: pred-type
|
||||||
|
attributes:
|
||||||
|
label: Detected prediction type
|
||||||
|
description: >
|
||||||
|
From the same NRS log line as above
|
||||||
|
(`NRS v<version>: prediction type detected -> <TYPE>`), copy the
|
||||||
|
detected type (e.g. EPS, V, FLOW, UNKNOWN).
|
||||||
|
placeholder: e.g. FLOW
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: skew
|
||||||
|
attributes:
|
||||||
|
label: Skew value
|
||||||
|
placeholder: e.g. 2.00
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: stretch
|
||||||
|
attributes:
|
||||||
|
label: Stretch value
|
||||||
|
placeholder: e.g. 5.00
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: squash
|
||||||
|
attributes:
|
||||||
|
label: Squash value
|
||||||
|
placeholder: e.g. 0.75
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: What actually happened? Include screenshots if relevant.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: repro
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimal steps (or an attached workflow JSON) to reproduce the issue.
|
||||||
|
placeholder: |
|
||||||
|
1. Load workflow...
|
||||||
|
2. Set Skew/Stretch/Squash to...
|
||||||
|
3. Queue prompt...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: console-log
|
||||||
|
attributes:
|
||||||
|
label: Console log output
|
||||||
|
description: >
|
||||||
|
Paste the relevant console output, including the
|
||||||
|
`NRS v<version>: prediction type detected -> <TYPE>` line and any
|
||||||
|
errors/warnings/tracebacks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: NRS Discussions
|
||||||
|
url: https://github.com/Reithan/negative_rejection_steering/discussions
|
||||||
|
about: Ask questions or discuss ideas that aren't a bug report.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test suite with branch coverage gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
|
- name: Install real CPU torch
|
||||||
|
run: pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
- name: Install test tooling
|
||||||
|
run: pip install pytest pytest-cov diff-cover
|
||||||
|
|
||||||
|
- name: Run tests with branch coverage
|
||||||
|
run: pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing
|
||||||
|
|
||||||
|
- name: Determine base branch for diff-cover
|
||||||
|
id: base
|
||||||
|
run: |
|
||||||
|
base_ref="origin/main"
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ] && git rev-parse --verify "origin/${{ github.base_ref }}" >/dev/null 2>&1; then
|
||||||
|
base_ref="origin/${{ github.base_ref }}"
|
||||||
|
fi
|
||||||
|
echo "ref=${base_ref}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Enforce 90% branch coverage on changed code
|
||||||
|
run: diff-cover coverage.xml --compare-branch=${{ steps.base.outputs.ref }} --branch-coverage --fail-under=90
|
||||||
@@ -36,8 +36,8 @@ repos:
|
|||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: run-tests
|
- id: run-tests
|
||||||
name: Run pytest tests
|
name: Run pytest with branch-coverage gate
|
||||||
entry: bash -c 'if command -v uv > /dev/null 2>&1; then uv run pytest tests/ || exit 1; else echo "WARNING - uv not found, skipping tests"; fi'
|
entry: bash -c 'if ! command -v uv > /dev/null 2>&1; then echo "WARNING - uv not found, skipping tests and coverage gate"; exit 0; fi; if ! git rev-parse --verify --quiet origin/main > /dev/null 2>&1; then echo "ERROR - origin/main not resolvable locally; fetch origin main and retry"; exit 1; fi; uvx --with torch --with pytest-cov --with diff-cover pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing tests/; status=$?; if [ $status -ne 0 ]; then rm -f coverage.xml .coverage; exit $status; fi; uvx --with diff-cover diff-cover coverage.xml --compare-branch=origin/main --branch-coverage --fail-under=90; cov_status=$?; rm -f coverage.xml .coverage; exit $cov_status'
|
||||||
language: system
|
language: system
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [1.1.0] - Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **X0 (sample) prediction support** (#44): x0-prediction models are now handled through the shared v-prediction-space path, alongside EPS and v-pred.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Correct v-space conversion for all variance-preserving parameterizations** (#44). The sampler hook delivers `cond`/`uncond` as `x - x0` for EPS, v-pred, and x0 alike, so NRS now recovers the true velocity `v = (cond - A)/factor` and runs its geometry in v-prediction space, then inverts exactly on return. This replaces the prior EPS-only affine, which operated on an incorrect input-space assumption. Flow-matching (FLOW/CONST) models remain operated natively — their prediction is already a pure scalar multiple of the velocity, so no conversion is applied.
|
||||||
|
|
||||||
|
### Changed / Upgrade notes
|
||||||
|
|
||||||
|
- **Default parameters changed** 2/5/0.75 → **2/4/0.5** (Skew/Stretch/Squash) in both the ComfyUI node and the A1111-family (Forge/reForge/Forge Neo) script.
|
||||||
|
- **v-prediction models now run the v-space conversion** instead of operating on the raw guidance. For typical config ranges the output change is expected to be minimal (verified on EPS; v-pred/x0 are math-validated but **not yet image-validated** — spot-check and retune if needed).
|
||||||
|
- **Reproducibility note:** the same seed + config may produce a slightly different image than 1.0.0 because of the corrected v-space handling and the new defaults.
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-08-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Flow-family (flow-matching) models now use a dedicated FLOW prediction/operation space (#37) so NRS applies the correct guidance geometry to them. Previously these models were misclassified, causing NRS to operate on an incorrect prediction-type assumption and underperform. This is the headline fix in 1.0.0.
|
||||||
|
- Pack-aware per-stream NRS routing with a degeneracy tripwire (#36): NRS now unpacks multi-stream packed latents (e.g. MiniMax H3 audio+video) and applies the geometry per stream on the real channel axis, instead of collapsing to a silent no-op on the flat packed latent.
|
||||||
|
- Prediction-type detection for WAN / RES4LYF samplers (#30).
|
||||||
|
- Removed a mangled guard and dead operation-space code paths; added prediction-type detection tests (#34).
|
||||||
|
- Resolved Node.js 20 deprecation warnings in GitHub Actions.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `__version__` string plus a patch-time log line announcing the version and detected prediction type; platform-agnostic GitHub issue template (#39).
|
||||||
|
- CI: full test suite with a >90% branch-coverage gate on diffs (#38); version-increment check in the publish workflow (#33); git hooks and development infrastructure (#32).
|
||||||
|
- Declared `requires-python` (>=3.10) so dependency locking is deterministic across environments.
|
||||||
|
|
||||||
|
### Changed / Upgrade notes
|
||||||
|
|
||||||
|
- Because flow-family models now use the correct FLOW space, NRS output for these models changes (for the better). Existing users of flow-matching models should retune Skew/Stretch/Squash. The `pre-flow` git tag preserves the prior behavior if a rollback is needed.
|
||||||
@@ -54,6 +54,12 @@ Thank you for your interest in contributing! This document provides guidelines f
|
|||||||
pre-commit install --hook-type pre-push
|
pre-commit install --hook-type pre-push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The pre-push hook runs the full test suite with branch coverage and blocks
|
||||||
|
the push if changed code drops below 90% branch coverage (via pytest-cov +
|
||||||
|
diff-cover, mirroring CI). It needs `uv` installed — if `uv` isn't found,
|
||||||
|
the check is skipped with a warning — and `origin/main` fetched locally so
|
||||||
|
there's something to diff against.
|
||||||
|
|
||||||
6. **Verify setup**:
|
6. **Verify setup**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""NRS package init.
|
||||||
|
|
||||||
|
Re-exports `__version__` from nodes_NRS.py so `NRS.__version__` is
|
||||||
|
importable. This value must match the `version` field in pyproject.toml —
|
||||||
|
bump both together at release time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .nodes_NRS import __version__
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
+171
-84
@@ -1,25 +1,66 @@
|
|||||||
|
# ruff: noqa: N999 -- filename predates NRS/__init__.py; mixed-case "nodes_NRS"
|
||||||
|
# only became checkable once NRS became a regular (non-namespace) package here.
|
||||||
|
# Renaming it is out of scope (would break existing imports); pyproject.toml's
|
||||||
|
# per-file-ignores already carve out N802/N804 for this same file.
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
try:
|
||||||
|
import comfy.utils as _comfy_utils
|
||||||
|
except Exception:
|
||||||
|
_comfy_utils = None
|
||||||
|
|
||||||
|
# Must be bumped together with the `version` field in pyproject.toml at release time.
|
||||||
|
__version__ = "1.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_latents(combined, latent_shapes):
|
||||||
|
"""Split a flat packed latent [B, 1, N] back into its per-stream tensors.
|
||||||
|
|
||||||
|
Mirrors comfy.utils.unpack_latents: for each shape in latent_shapes, take
|
||||||
|
math.prod(shape[1:]) elements off the last dim and reshape that [B, 1, n]
|
||||||
|
slice back to `shape`.
|
||||||
|
"""
|
||||||
|
streams = []
|
||||||
|
offset = 0
|
||||||
|
for shape in latent_shapes:
|
||||||
|
n = math.prod(shape[1:])
|
||||||
|
chunk = combined[:, :, offset : offset + n]
|
||||||
|
streams.append(chunk.reshape(shape))
|
||||||
|
offset += n
|
||||||
|
return streams
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_latents(streams):
|
||||||
|
"""Pack a list of per-stream tensors [B, C, ...] into a flat [B, 1, N] tensor.
|
||||||
|
|
||||||
|
Mirrors comfy.utils.pack_latents: each stream is reshaped to (B, 1, -1)
|
||||||
|
and concatenated on the last dim.
|
||||||
|
"""
|
||||||
|
flat = [s.reshape(s.shape[0], 1, -1) for s in streams]
|
||||||
|
return torch.cat(flat, dim=-1)
|
||||||
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
class PredictionType(Enum):
|
class PredictionType(Enum):
|
||||||
EPS = auto() # ε-prediction
|
EPS = auto() # ε-prediction
|
||||||
V = auto() # v-prediction
|
V = auto() # v-prediction
|
||||||
X0 = auto() # x₀-prediction
|
X0 = auto() # x₀-prediction
|
||||||
|
FLOW = auto() # flow-matching / velocity — operated natively, no VP conversion
|
||||||
UNKNOWN = auto() # couldn’t detect / new scheduler
|
UNKNOWN = auto() # couldn’t detect / new scheduler
|
||||||
|
|
||||||
|
|
||||||
_RAW_TO_ENUM = {
|
_RAW_TO_ENUM = {
|
||||||
"eps": PredictionType.EPS,
|
"eps": PredictionType.EPS,
|
||||||
"epsilon": PredictionType.EPS,
|
"epsilon": PredictionType.EPS,
|
||||||
"flux": PredictionType.EPS,
|
"flux": PredictionType.FLOW,
|
||||||
"chroma": PredictionType.EPS,
|
"chroma": PredictionType.FLOW,
|
||||||
"flow": PredictionType.EPS, # FLOW models (WAN, etc.) are EPS-compatible
|
"flow": PredictionType.FLOW, # FLOW models (WAN, etc.) operated natively
|
||||||
"wan": PredictionType.EPS, # WAN21 is FLOW-based
|
"wan": PredictionType.FLOW, # WAN21 is FLOW-based
|
||||||
"const": PredictionType.EPS, # CONST prediction class used in FLOW models
|
"const": PredictionType.FLOW, # CONST prediction class used in FLOW models
|
||||||
"v": PredictionType.V,
|
"v": PredictionType.V,
|
||||||
"v_prediction": PredictionType.V,
|
"v_prediction": PredictionType.V,
|
||||||
"x0": PredictionType.X0,
|
"x0": PredictionType.X0,
|
||||||
@@ -47,7 +88,7 @@ class NRS:
|
|||||||
"stretch": (
|
"stretch": (
|
||||||
"FLOAT",
|
"FLOAT",
|
||||||
{
|
{
|
||||||
"default": 5.00,
|
"default": 4.00,
|
||||||
"min": -30.0,
|
"min": -30.0,
|
||||||
"max": 30.0,
|
"max": 30.0,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
@@ -57,7 +98,7 @@ class NRS:
|
|||||||
"squash": (
|
"squash": (
|
||||||
"FLOAT",
|
"FLOAT",
|
||||||
{
|
{
|
||||||
"default": 0.75,
|
"default": 0.50,
|
||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 1.0,
|
"max": 1.0,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
@@ -125,8 +166,8 @@ class NRS:
|
|||||||
|
|
||||||
# CONST class is used by FLOW models (WAN21, Flux, etc.)
|
# CONST class is used by FLOW models (WAN21, Flux, etc.)
|
||||||
if "const" in sampling_class_name:
|
if "const" in sampling_class_name:
|
||||||
logging.debug("NRS._get_pred_type: Detected FLOW model via CONST sampling class -> EPS")
|
logging.debug("NRS._get_pred_type: Detected FLOW model via CONST sampling class -> FLOW")
|
||||||
return PredictionType.EPS
|
return PredictionType.FLOW
|
||||||
elif "v_prediction" in sampling_class_name:
|
elif "v_prediction" in sampling_class_name:
|
||||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
|
logging.debug("NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
|
||||||
return PredictionType.V
|
return PredictionType.V
|
||||||
@@ -140,8 +181,8 @@ class NRS:
|
|||||||
logging.debug(f"NRS._get_pred_type: Found model.model.model_type: {model_type_str}")
|
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:
|
if "flow" in model_type_str or "flux" in model_type_str:
|
||||||
logging.debug("NRS._get_pred_type: Detected FLOW/Flux model via model_type -> EPS")
|
logging.debug("NRS._get_pred_type: Detected FLOW/Flux model via model_type -> FLOW")
|
||||||
return PredictionType.EPS
|
return PredictionType.FLOW
|
||||||
elif "v_prediction" in model_type_str:
|
elif "v_prediction" in model_type_str:
|
||||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via model_type -> V")
|
logging.debug("NRS._get_pred_type: Detected V-prediction model via model_type -> V")
|
||||||
return PredictionType.V
|
return PredictionType.V
|
||||||
@@ -159,98 +200,144 @@ class NRS:
|
|||||||
)
|
)
|
||||||
return PredictionType.EPS
|
return PredictionType.EPS
|
||||||
|
|
||||||
|
def _is_vp(self, pred_type):
|
||||||
|
"""VP (variance-preserving) parameterizations converted to v-space: EPS, V, X0.
|
||||||
|
|
||||||
|
UNKNOWN (and any unhandled type) falls back to VP/v-space. FLOW/CONST is the only
|
||||||
|
parameterization operated natively (see _convert_to_v_space).
|
||||||
|
"""
|
||||||
|
if pred_type in (PredictionType.EPS, PredictionType.V, PredictionType.X0):
|
||||||
|
return True
|
||||||
|
if pred_type == PredictionType.FLOW:
|
||||||
|
return False
|
||||||
|
logging.warning(f"NRS: unknown prediction type {pred_type}, treating as VP (v-space)")
|
||||||
|
return True
|
||||||
|
|
||||||
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond, pred_type):
|
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond, pred_type):
|
||||||
x_div = None
|
"""Convert the (x - x0) guidance vectors into v-prediction space before the NRS geometry.
|
||||||
v_cond = cond
|
|
||||||
v_uncond = uncond
|
|
||||||
if pred_type == PredictionType.V:
|
|
||||||
logging.debug("NRS._convert_to_v_space: already in v, no pre-scale needed")
|
|
||||||
pass # already in v space
|
|
||||||
elif pred_type == PredictionType.EPS:
|
|
||||||
# ε → v conversion
|
|
||||||
logging.debug("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)
|
The sampler hook delivers cond/uncond as `x - x0` for every parameterization (the
|
||||||
v_uncond = x_orig - (x_div - uncond * factor)
|
model's raw output is converted to a denoised x0 before NRS sees it), so the true
|
||||||
elif pred_type == PredictionType.X0:
|
velocity is recovered the same way regardless of EPS/V/X0:
|
||||||
raise NotImplementedError("NRS._convert_to_v_space: x0-prediction not supported yet.")
|
v = (cond - A)/factor = (x/(sigma^2+1) - x0) * sig_root/sigma
|
||||||
else:
|
with A = x*sigma^2/(sigma^2+1), factor = sigma/sqrt(sigma^2+1).
|
||||||
# Fallback: treat UNKNOWN as EPS and convert to V-space
|
|
||||||
logging.warning(f"NRS._convert_to_v_space: Unknown prediction type {pred_type}, treating as EPS")
|
|
||||||
logging.debug("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
|
FLOW/CONST is operated natively: `x - x0 = sigma*out` is a pure scalar multiple of
|
||||||
|
the model's velocity (no additive offset), and the NRS geometry is scale-invariant,
|
||||||
|
so identity already runs on the native prediction. There is no VP v-space for
|
||||||
|
flow-matching (its sigma is a [0,1] flow time, not a VP karras sigma).
|
||||||
|
"""
|
||||||
|
if not self._is_vp(pred_type):
|
||||||
|
logging.debug("NRS._convert_to_v_space: flow/const operated natively (identity)")
|
||||||
|
return cond, uncond
|
||||||
|
|
||||||
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma, pred_type):
|
logging.debug("NRS._convert_to_v_space: converting VP prediction to v-space")
|
||||||
nrs_result = x_final
|
factor = sigma / sig_root
|
||||||
if pred_type == PredictionType.V:
|
a_off = x_orig - x_orig / (sigma**2 + 1) # A = x*sigma^2/(sigma^2+1)
|
||||||
# already in v space
|
return (cond - a_off) / factor, (uncond - a_off) / factor
|
||||||
logging.debug("NRS._finalize_from_v_space: already in v, no post-scale needed")
|
|
||||||
pass
|
def _finalize_from_v_space(self, x_orig, x_final, sig_root, sigma, pred_type):
|
||||||
elif pred_type == PredictionType.EPS:
|
"""Invert _convert_to_v_space so the hook returns `x - x0_final`. Round-trips exactly."""
|
||||||
# v → ε conversion
|
if not self._is_vp(pred_type):
|
||||||
logging.debug("NRS._finalize_from_v_space: generating cfg_result for eps")
|
logging.debug("NRS._finalize_from_v_space: flow/const operated natively (identity)")
|
||||||
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
return x_final
|
||||||
elif pred_type == PredictionType.X0:
|
|
||||||
raise NotImplementedError("NRS._finalize_from_v_space: x0-prediction not supported yet.")
|
factor = sigma / sig_root
|
||||||
else:
|
a_off = x_orig - x_orig / (sigma**2 + 1)
|
||||||
# Fallback: treat UNKNOWN as EPS and convert from V-space
|
return a_off + x_final * factor
|
||||||
logging.warning(f"NRS._finalize_from_v_space: Unknown prediction type {pred_type}, treating as EPS")
|
|
||||||
logging.debug("NRS._finalize_from_v_space: generating cfg_result for eps (fallback)")
|
def _apply_guidance(self, x_orig, cond, uncond, sigma, skew, stretch, squash, pred_type):
|
||||||
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
"""Run the NRS geometry pipeline on a single (already-unpacked, channels-first) stream."""
|
||||||
return nrs_result
|
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
||||||
|
sig_root = (sigma**2 + 1).sqrt()
|
||||||
|
|
||||||
|
# Convert (x - x0) guidance into v-space for all VP parameterizations (EPS/V/X0);
|
||||||
|
# FLOW/CONST runs natively.
|
||||||
|
nrs_cond, nrs_uncond = self._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond, pred_type)
|
||||||
|
|
||||||
|
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,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)
|
||||||
|
|
||||||
|
# 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 = nrs_cond.norm(dim=1, keepdim=True)
|
||||||
|
nrs_len = skewed.norm(dim=1, keepdim=True) + eps
|
||||||
|
|
||||||
|
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
|
||||||
|
x_final = skewed * squash_scale
|
||||||
|
|
||||||
|
return self._finalize_from_v_space(x_orig, x_final, sig_root, sigma, pred_type)
|
||||||
|
|
||||||
def patch(self, model, skew, stretch, squash):
|
def patch(self, model, skew, stretch, squash):
|
||||||
pred_type = self._get_pred_type(model)
|
pred_type = self._get_pred_type(model)
|
||||||
|
logging.info(f"NRS v{__version__}: prediction type detected -> {pred_type.name}")
|
||||||
|
warned = {"done": False}
|
||||||
|
|
||||||
def nrs(args):
|
def nrs(args):
|
||||||
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
|
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
|
||||||
cond = args["cond"]
|
cond = args["cond"]
|
||||||
uncond = args["uncond"]
|
uncond = args["uncond"]
|
||||||
x_orig = args["input"]
|
x_orig = args["input"]
|
||||||
|
|
||||||
sigma = args["sigma"]
|
sigma = args["sigma"]
|
||||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
|
||||||
sig_root = (sigma**2 + 1).sqrt()
|
|
||||||
|
|
||||||
# Operation space is hardcoded to V for now; FLOW is added in a later PR.
|
shapes = getattr(args["model"], "latent_shapes", None)
|
||||||
x_div, nrs_cond, nrs_uncond = self._convert_to_v_space(
|
if shapes and len(shapes) > 1:
|
||||||
x_orig, sig_root, sigma, cond, uncond, pred_type
|
if _comfy_utils is not None and hasattr(_comfy_utils, "unpack_latents"):
|
||||||
)
|
cond_streams = _comfy_utils.unpack_latents(cond, shapes)
|
||||||
|
uncond_streams = _comfy_utils.unpack_latents(uncond, shapes)
|
||||||
|
x_streams = _comfy_utils.unpack_latents(x_orig, shapes)
|
||||||
|
else:
|
||||||
|
cond_streams = _unpack_latents(cond, shapes)
|
||||||
|
uncond_streams = _unpack_latents(uncond, shapes)
|
||||||
|
x_streams = _unpack_latents(x_orig, shapes)
|
||||||
|
else:
|
||||||
|
cond_streams, uncond_streams, x_streams = [cond], [uncond], [x_orig]
|
||||||
|
|
||||||
def _dot(a, b):
|
if not warned["done"]:
|
||||||
return (a * b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
|
for stream in x_streams:
|
||||||
|
if stream.shape[1] == 1:
|
||||||
|
logging.warning(
|
||||||
|
f"NRS.nrs: routed stream has a singleton reduction axis {tuple(stream.shape)}; "
|
||||||
|
"NRS geometry (dot/proj/skew) will degenerate to a no-op on this stream."
|
||||||
|
)
|
||||||
|
warned["done"] = True
|
||||||
|
break
|
||||||
|
|
||||||
def _nrm2(v):
|
results = [
|
||||||
return _dot(v, v)
|
self._apply_guidance(
|
||||||
|
x_streams[i],
|
||||||
|
cond_streams[i],
|
||||||
|
uncond_streams[i],
|
||||||
|
sigma,
|
||||||
|
skew,
|
||||||
|
stretch,
|
||||||
|
squash,
|
||||||
|
pred_type,
|
||||||
|
)
|
||||||
|
for i in range(len(cond_streams))
|
||||||
|
]
|
||||||
|
|
||||||
eps = torch.finfo(nrs_cond.dtype).eps
|
if len(results) == 1:
|
||||||
c_dot_c = _nrm2(nrs_cond) + eps # [B,1,W,H]
|
return results[0]
|
||||||
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
|
if _comfy_utils is not None and hasattr(_comfy_utils, "pack_latents"):
|
||||||
proj_diff = nrs_cond - u_on_c
|
return _comfy_utils.pack_latents(results)[0]
|
||||||
stretched = nrs_cond + (stretch * proj_diff)
|
return _pack_latents(results)
|
||||||
|
|
||||||
# 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 = nrs_cond.norm(dim=1, keepdim=True)
|
|
||||||
nrs_len = skewed.norm(dim=1, keepdim=True) + eps
|
|
||||||
|
|
||||||
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
|
|
||||||
x_final = skewed * squash_scale
|
|
||||||
|
|
||||||
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma, pred_type)
|
|
||||||
|
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
m.set_model_sampler_cfg_function(nrs, True)
|
m.set_model_sampler_cfg_function(nrs, True)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
|
|||||||
<img align="right" src="Examples/NRS_graph.png" alt="Graph of NRS vs CFG" style="width: 40%; float: right;">
|
<img align="right" src="Examples/NRS_graph.png" alt="Graph of NRS vs CFG" style="width: 40%; float: right;">
|
||||||
|
|
||||||
### NRS is Applied in Three Steps:
|
### 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.
|
0. ***V-Space**: pre-NRS step* The sampler hands NRS its `cond`/`uncond` as `x - x0` for every variance-preserving parameterization (EPS, v-prediction, and x0-prediction alike), so NRS recovers the true velocity `v` from them and runs its geometry in v-prediction space, then inverts the transform before returning. This one v-space path handles EPS, v-pred, and x0 models identically. Flow-matching models (flux, chroma, wan, and other flow/CONST families) are operated natively — their prediction is already a pure scalar multiple of the velocity, so no v-space conversion is applied.
|
||||||
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]
|
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]
|
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]
|
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]
|
||||||
@@ -63,7 +63,7 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
|
|||||||
|
|
||||||
## Beginner How-To
|
## Beginner How-To
|
||||||
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>
|
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.*
|
*Alternatively, try starting with the default of 2/4/0.5, or at 1/1/1 to get a baseline.*
|
||||||
2. Test some outputs. Results should be similar in quality to CFG.
|
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.
|
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.
|
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.
|
||||||
@@ -119,6 +119,15 @@ Once installed and enabled, the NRS settings panel will appear in your generatio
|
|||||||
### StabilityMatrix Integration
|
### 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.
|
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.
|
||||||
|
|
||||||
|
### NRS for Video
|
||||||
|
When using NRS with **video** models (e.g. MiniMax H3), two things need to be turned off or output quality suffers:
|
||||||
|
- **Caching accelerators** (EasyCache, TeaCache, etc.) — their change-thresholded caching skips model evaluations that NRS relies on. With NRS active, this causes motion stutter and audio artifacts.
|
||||||
|
- **Multistep samplers** (`res_multistep`, `dpmpp_2m`, `dpmpp_3m_sde`, and other history/"m" samplers) — they extrapolate NRS's guidance across steps, compounding instability over the clip. Use a memoryless sampler instead; **`euler_ancestral` is recommended** (`euler` and `heun` also work well).
|
||||||
|
|
||||||
|
NRS adds a second inference pass per step, like CFG, so video generation time increases accordingly. Consider reserving NRS for final generations or prompts that need extra adherence.
|
||||||
|
|
||||||
|
These caveats are video-specific — 2D image generation is unaffected.
|
||||||
|
|
||||||
## Submitted User Examples
|
## Submitted User Examples
|
||||||
| User | CFG | NRS |
|
| User | CFG | NRS |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
from .NRS.nodes_NRS import *
|
from .NRS.nodes_NRS import *
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {"NRS": NRS}
|
NODE_CLASS_MAPPINGS = {"NRS": NRS}
|
||||||
NODE_DISPLAY_NAME_MAPPINS = {"NRS": "Negative Rejection Steering"}
|
NODE_DISPLAY_NAME_MAPPINGS = {"NRS": "Negative Rejection Steering"}
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "negative_rejection_steering"
|
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."
|
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"}]
|
authors = [{name = "Bryan O'Malley", email = "bo122081@hotmail.com"}]
|
||||||
version = "0.7.4"
|
version = "1.1.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ class NRSScript(scripts.Script):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
self.skew = 2.00
|
self.skew = 2.00
|
||||||
self.stretch = 5.00
|
self.stretch = 4.00
|
||||||
self.squash = 0.75
|
self.squash = 0.50
|
||||||
|
|
||||||
sorting_priority = 5
|
sorting_priority = 5
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class NRSScript(scripts.Script):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"NRS: Enabled: {self.enabled}, Squash: {self.skew}, Stretch: {self.stretch}, Squash: {self.squash}"
|
f"NRS: Enabled: {self.enabled}, Skew: {self.skew}, Stretch: {self.stretch}, Squash: {self.squash}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""Tests for pack-aware per-stream routing in NRS.nodes_NRS.
|
||||||
|
|
||||||
|
These tests need real torch (tensor math), but tests/conftest.py installs a
|
||||||
|
MagicMock in sys.modules["torch"] for the whole session so other test modules
|
||||||
|
can import without the heavy dependency. We swap the real torch module in for
|
||||||
|
the duration of this module only, then restore the mock so the rest of the
|
||||||
|
suite is unaffected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_saved_torch = None
|
||||||
|
_saved_nodes_nrs = None
|
||||||
|
torch = None
|
||||||
|
nrs_module = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module(module):
|
||||||
|
# NOTE: we deliberately avoid importlib.reload() here. reload() mutates
|
||||||
|
# the *existing* NRS.nodes_NRS module dict in place, and other test
|
||||||
|
# modules (e.g. test_pred_type.py) import PredictionType/NRS at
|
||||||
|
# collection time and keep those references for the whole session. Their
|
||||||
|
# methods' __globals__ point at that same dict, so an in-place reload
|
||||||
|
# would silently swap PredictionType out from under them (new class
|
||||||
|
# object, same name -> broken identity-based Enum equality). Instead we
|
||||||
|
# unregister the module from sys.modules and import it fresh: this
|
||||||
|
# creates an independent module object, leaving the original (still
|
||||||
|
# cached in other modules' namespaces) untouched. We restore the exact
|
||||||
|
# original module object on teardown.
|
||||||
|
global _saved_torch, _saved_nodes_nrs, torch, nrs_module
|
||||||
|
_saved_torch = sys.modules.get("torch")
|
||||||
|
sys.modules.pop("torch", None)
|
||||||
|
try:
|
||||||
|
import torch as real_torch
|
||||||
|
except ImportError:
|
||||||
|
pytest.skip("real torch unavailable", allow_module_level=True)
|
||||||
|
torch = real_torch
|
||||||
|
|
||||||
|
_saved_nodes_nrs = sys.modules.get("NRS.nodes_NRS")
|
||||||
|
sys.modules.pop("NRS.nodes_NRS", None)
|
||||||
|
|
||||||
|
import NRS.nodes_NRS as m
|
||||||
|
|
||||||
|
nrs_module = m
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module(module):
|
||||||
|
if _saved_torch is not None:
|
||||||
|
sys.modules["torch"] = _saved_torch
|
||||||
|
else:
|
||||||
|
sys.modules.pop("torch", None)
|
||||||
|
|
||||||
|
if _saved_nodes_nrs is not None:
|
||||||
|
sys.modules["NRS.nodes_NRS"] = _saved_nodes_nrs
|
||||||
|
else:
|
||||||
|
sys.modules.pop("NRS.nodes_NRS", None)
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModelSampling:
|
||||||
|
"""Minimal stand-in that makes _get_pred_type fall back to EPS quickly."""
|
||||||
|
|
||||||
|
|
||||||
|
class _StubInnerModel:
|
||||||
|
def __init__(self, latent_shapes=None):
|
||||||
|
self.model_sampling = _StubModelSampling()
|
||||||
|
if latent_shapes is not None:
|
||||||
|
self.latent_shapes = latent_shapes
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModel:
|
||||||
|
"""Stub for the outer ComfyUI ModelPatcher passed to NRS.patch()."""
|
||||||
|
|
||||||
|
def __init__(self, latent_shapes=None):
|
||||||
|
self.model = _StubInnerModel(latent_shapes)
|
||||||
|
self._captured_fn = None
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_model_sampler_cfg_function(self, fn, flag):
|
||||||
|
self._captured_fn = fn
|
||||||
|
|
||||||
|
|
||||||
|
def _make_args(model, cond, uncond, x_orig, sigma):
|
||||||
|
return {
|
||||||
|
"model": model.model, # args["model"] is the inner model carrying latent_shapes
|
||||||
|
"cond": cond,
|
||||||
|
"uncond": uncond,
|
||||||
|
"input": x_orig,
|
||||||
|
"sigma": sigma,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 1: round-trip pack/unpack correctness
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtrip_unpack_repack_two_streams():
|
||||||
|
video = torch.randn(1, 4, 3, 2)
|
||||||
|
audio = torch.randn(1, 6, 5)
|
||||||
|
shapes = [video.shape, audio.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([video, audio])
|
||||||
|
assert packed.shape == (1, 1, video.numel() + audio.numel())
|
||||||
|
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
assert len(unpacked) == 2
|
||||||
|
assert torch.allclose(unpacked[0], video)
|
||||||
|
assert torch.allclose(unpacked[1], audio)
|
||||||
|
|
||||||
|
repacked = nrs_module._pack_latents(unpacked)
|
||||||
|
assert torch.allclose(repacked, packed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtrip_single_stream():
|
||||||
|
x = torch.randn(1, 4, 8, 8)
|
||||||
|
shapes = [x.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([x])
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
assert len(unpacked) == 1
|
||||||
|
assert torch.allclose(unpacked[0], x)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 2: fallback path (no latent_shapes) is a byte-for-byte regression no-op
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _run_nrs(model, cond, uncond, x_orig, sigma, skew=2.0, stretch=5.0, squash=0.75):
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
(patched_model,) = node.patch(model, skew, stretch, squash)
|
||||||
|
fn = patched_model._captured_fn
|
||||||
|
args = _make_args(model, cond, uncond, x_orig, sigma)
|
||||||
|
return fn(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_no_latent_shapes_matches_single_stream_shape():
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(2, 4, 8, 8)
|
||||||
|
uncond = torch.randn(2, 4, 8, 8)
|
||||||
|
x_orig = torch.randn(2, 4, 8, 8)
|
||||||
|
sigma = torch.rand(2) + 0.1
|
||||||
|
|
||||||
|
result = _run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
assert result.shape == x_orig.shape
|
||||||
|
|
||||||
|
# Regression check: manually compute the single-stream result the same
|
||||||
|
# way the pre-split code path did, and confirm equality.
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
expected = node._apply_guidance(x_orig, cond, uncond, sigma, 2.0, 5.0, 0.75, nrs_module.PredictionType.EPS)
|
||||||
|
assert torch.allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_stream_latent_shapes_also_matches():
|
||||||
|
"""A model.latent_shapes list of length 1 must take the same code path."""
|
||||||
|
cond = torch.randn(1, 4, 5, 5)
|
||||||
|
uncond = torch.randn(1, 4, 5, 5)
|
||||||
|
x_orig = torch.randn(1, 4, 5, 5)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
model = _StubModel(latent_shapes=[cond.shape])
|
||||||
|
result = _run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
expected = node._apply_guidance(x_orig, cond, uncond, sigma, 2.0, 5.0, 0.75, nrs_module.PredictionType.EPS)
|
||||||
|
assert torch.allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 3: degeneracy tripwire
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_tripwire_fires_on_flat_pack_without_latent_shapes(caplog):
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(1, 1, 100)
|
||||||
|
uncond = torch.randn(1, 1, 100)
|
||||||
|
x_orig = torch.randn(1, 1, 100)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
_run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
assert any("singleton reduction axis" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tripwire_does_not_fire_for_normal_single_stream(caplog):
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(1, 4, 8, 8)
|
||||||
|
uncond = torch.randn(1, 4, 8, 8)
|
||||||
|
x_orig = torch.randn(1, 4, 8, 8)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
_run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
assert not any("singleton reduction axis" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 4: per-stream reduced shapes after unpack (H3-like video + audio)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_per_stream_reduced_shapes_after_unpack():
|
||||||
|
video = torch.randn(1, 24, 4, 3, 2)
|
||||||
|
audio = torch.randn(1, 32, 2, 5)
|
||||||
|
shapes = [video.shape, audio.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([video, audio])
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
|
||||||
|
video_u, audio_u = unpacked
|
||||||
|
assert video_u.shape == video.shape
|
||||||
|
assert audio_u.shape == audio.shape
|
||||||
|
|
||||||
|
# Channels sit at dim 1 for both streams.
|
||||||
|
assert video_u.shape[1] == 24
|
||||||
|
assert audio_u.shape[1] == 32
|
||||||
|
|
||||||
|
video_reduced = video_u.sum(dim=1, keepdim=True)
|
||||||
|
audio_reduced = audio_u.sum(dim=1, keepdim=True)
|
||||||
|
assert video_reduced.shape == (1, 1, 4, 3, 2)
|
||||||
|
assert audio_reduced.shape == (1, 1, 2, 5)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 5: split restores non-degenerate rejection (proves Skew is alive)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_restores_nondegenerate_rejection():
|
||||||
|
"""On a real multi-channel stream, uncond's rejection on cond must not
|
||||||
|
collapse to ~0 -- this is the geometry that was silently dead on the flat
|
||||||
|
[B,1,N] pack before the unpack/repack fix.
|
||||||
|
"""
|
||||||
|
torch.manual_seed(0)
|
||||||
|
cond = torch.randn(1, 8, 4, 4)
|
||||||
|
# Make uncond non-parallel to cond so the rejection component is nonzero.
|
||||||
|
uncond = torch.randn(1, 8, 4, 4)
|
||||||
|
|
||||||
|
def _dot(a, b):
|
||||||
|
return (a * b).sum(dim=1, keepdim=True)
|
||||||
|
|
||||||
|
eps = torch.finfo(cond.dtype).eps
|
||||||
|
c_dot_c = _dot(cond, cond) + eps
|
||||||
|
u_dot_c = _dot(uncond, cond)
|
||||||
|
u_on_c = (u_dot_c / c_dot_c) * cond
|
||||||
|
u_rej_c = uncond - u_on_c
|
||||||
|
|
||||||
|
assert u_rej_c.abs().max().item() > 1e-4
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_pack_rejection_is_degenerate_without_split():
|
||||||
|
"""Sanity check for the bug this PR fixes: reducing over the flat pack's
|
||||||
|
singleton dim=1 axis collapses the rejection to exactly zero (up to
|
||||||
|
floating point noise from the eps regularization term).
|
||||||
|
"""
|
||||||
|
# float64 keeps the residual from the eps regularizer near the true
|
||||||
|
# machine epsilon instead of float32 accumulation noise, so the
|
||||||
|
# collapse-to-zero identity is exact enough to assert tightly.
|
||||||
|
packed_cond = torch.randn(1, 1, 100, dtype=torch.float64)
|
||||||
|
packed_uncond = torch.randn(1, 1, 100, dtype=torch.float64)
|
||||||
|
|
||||||
|
def _dot(a, b):
|
||||||
|
return (a * b).sum(dim=1, keepdim=True)
|
||||||
|
|
||||||
|
eps = torch.finfo(packed_cond.dtype).eps
|
||||||
|
c_dot_c = _dot(packed_cond, packed_cond) + eps
|
||||||
|
u_dot_c = _dot(packed_uncond, packed_cond)
|
||||||
|
u_on_c = (u_dot_c / c_dot_c) * packed_cond
|
||||||
|
u_rej_c = packed_uncond - u_on_c
|
||||||
|
|
||||||
|
assert u_rej_c.abs().max().item() < 1e-8
|
||||||
+113
-19
@@ -1,14 +1,22 @@
|
|||||||
"""Regression tests for NRS._get_pred_type and _RAW_TO_ENUM mappings.
|
"""Regression tests for NRS._get_pred_type, _RAW_TO_ENUM mappings, and the
|
||||||
|
V/FLOW/EPS operation-space conversion helpers.
|
||||||
|
|
||||||
These tests pin CURRENT behavior (flow-family names resolve to EPS) as a
|
PR-3 reclassified the flow-matching family (flux, chroma, flow, wan, const)
|
||||||
safety net ahead of the FLOW reclassification planned for a later PR. If
|
from PredictionType.EPS onto a new PredictionType.FLOW, which is operated
|
||||||
this file needs updating because flow-family names now map to
|
natively (identity conversion, no VP ε<->v algebra). These tests pin that
|
||||||
PredictionType.FLOW, that is expected -- it means the reclassification
|
post-reclassification behavior at both detection sites (the _RAW_TO_ENUM
|
||||||
landed and this net did its job.
|
dict and the enhanced-detection fallback in _get_pred_type).
|
||||||
|
|
||||||
|
FLOW is the sole native path; every VP parameterization (EPS, V, X0, and the
|
||||||
|
UNKNOWN fallback) shares one ε/v/x0 -> v-space conversion through
|
||||||
|
_convert_to_v_space / _finalize_from_v_space. These tests cover the FLOW
|
||||||
|
identity round-trip and confirm the VP branches actually transform their inputs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import enum
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -23,6 +31,14 @@ def _make_model_sampling(class_name):
|
|||||||
return type(class_name, (object,), {})()
|
return type(class_name, (object,), {})()
|
||||||
|
|
||||||
|
|
||||||
|
class _ModelType(enum.Enum):
|
||||||
|
"""Mirrors ComfyUI's real model_type.ModelType Enum, as exposed by MiniMax H3's
|
||||||
|
BaseModel.model_type -- a genuine Enum member, not a raw string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLOW = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class _StubModel:
|
class _StubModel:
|
||||||
"""Minimal stand-in for a model object walked by _get_pred_type."""
|
"""Minimal stand-in for a model object walked by _get_pred_type."""
|
||||||
|
|
||||||
@@ -41,11 +57,11 @@ class _StubModel:
|
|||||||
[
|
[
|
||||||
("eps", PredictionType.EPS),
|
("eps", PredictionType.EPS),
|
||||||
("epsilon", PredictionType.EPS),
|
("epsilon", PredictionType.EPS),
|
||||||
("flux", PredictionType.EPS),
|
("flux", PredictionType.FLOW),
|
||||||
("chroma", PredictionType.EPS),
|
("chroma", PredictionType.FLOW),
|
||||||
("flow", PredictionType.EPS),
|
("flow", PredictionType.FLOW),
|
||||||
("wan", PredictionType.EPS),
|
("wan", PredictionType.FLOW),
|
||||||
("const", PredictionType.EPS),
|
("const", PredictionType.FLOW),
|
||||||
("v", PredictionType.V),
|
("v", PredictionType.V),
|
||||||
("v_prediction", PredictionType.V),
|
("v_prediction", PredictionType.V),
|
||||||
("x0", PredictionType.X0),
|
("x0", PredictionType.X0),
|
||||||
@@ -80,22 +96,39 @@ class TestGetPredTypeDirectAttribute:
|
|||||||
model = _StubModel(model_type="x0")
|
model = _StubModel(model_type="x0")
|
||||||
assert node._get_pred_type(model) == PredictionType.X0
|
assert node._get_pred_type(model) == PredictionType.X0
|
||||||
|
|
||||||
def test_model_type_flow_family_is_currently_eps(self):
|
def test_model_type_flow_is_flow(self):
|
||||||
"""Flow-family models currently resolve to EPS (pre-reclassification)."""
|
"""Flow-family models resolve to FLOW (native operation, no VP conversion)."""
|
||||||
model = _StubModel(model_type="flow")
|
model = _StubModel(model_type="flow")
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_model_type_wan_is_currently_eps(self):
|
def test_model_type_wan_is_flow(self):
|
||||||
model = _StubModel(model_type="wan")
|
model = _StubModel(model_type="wan")
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
def test_h3_enum_model_type_resolves_to_flow(self):
|
||||||
|
"""MiniMax H3 exposes model.model.model_type as a real Enum member
|
||||||
|
(ModelType.FLOW), not a raw string. _canon's `isinstance(p, Enum)`
|
||||||
|
branch reduces it to `p.name` ("FLOW" -> "flow") before the
|
||||||
|
_RAW_TO_ENUM dict lookup, so this pins that Enum path -- as taken by
|
||||||
|
H3's real model_type attribute -- resolves at the direct-hit site.
|
||||||
|
"""
|
||||||
|
model = _StubModel(inner_model_type=_ModelType.FLOW)
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
|
||||||
class TestGetPredTypeEnhancedDetectionFallback:
|
class TestGetPredTypeEnhancedDetectionFallback:
|
||||||
"""The model_sampling class-name and model.model.model_type fallback paths."""
|
"""The model_sampling class-name and model.model.model_type fallback paths.
|
||||||
|
|
||||||
def test_model_sampling_const_class_is_eps(self):
|
Each stub below is deliberately built so the only detectable signal lives
|
||||||
|
in the fallback (section 3) logic -- not an exact _RAW_TO_ENUM key hit
|
||||||
|
during the BFS walk -- so these tests genuinely exercise the fallback
|
||||||
|
branches rather than just re-testing the dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_model_sampling_const_class_is_flow(self):
|
||||||
|
"""A CONST-like model_sampling class name is the only flow signal here."""
|
||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingContinuousEDMConst"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingContinuousEDMConst"))
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_model_sampling_v_prediction_class_is_v(self):
|
def test_model_sampling_v_prediction_class_is_v(self):
|
||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingV_Prediction"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingV_Prediction"))
|
||||||
@@ -105,7 +138,68 @@ class TestGetPredTypeEnhancedDetectionFallback:
|
|||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingEps"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingEps"))
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||||
|
|
||||||
|
def test_inner_model_type_flow_string_is_flow(self):
|
||||||
|
"""A model.model.model_type whose str() merely *contains* 'flow' (e.g. an
|
||||||
|
Enum repr like 'ModelType.FLOW') isn't an exact _RAW_TO_ENUM key, so the
|
||||||
|
BFS direct-hit path can't resolve it -- only the model.model.model_type
|
||||||
|
substring fallback can.
|
||||||
|
"""
|
||||||
|
model = _StubModel(inner_model_type="ModelType.FLOW")
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
def test_inner_model_type_flux_string_is_flow(self):
|
||||||
|
model = _StubModel(inner_model_type="ModelType.FLUX")
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_unrecognized_model_defaults_to_eps(self):
|
def test_unrecognized_model_defaults_to_eps(self):
|
||||||
"""Fully-unrecognized models fall back to EPS (documented default)."""
|
"""Fully-unrecognized models fall back to EPS (documented default)."""
|
||||||
model = _StubModel()
|
model = _StubModel()
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertToVSpaceBranches:
|
||||||
|
"""FLOW is the only native (identity) parameterization; every VP type
|
||||||
|
(EPS, V, X0, and the UNKNOWN fallback) now runs the shared ε/v/x0 -> v-space
|
||||||
|
algebra. FLOW identity needs no tensor math, so sentinel objects prove it;
|
||||||
|
the VP branches use MagicMock to confirm the algebra actually transforms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_flow_convert_is_identity(self):
|
||||||
|
node = NRS()
|
||||||
|
cond, uncond = object(), object()
|
||||||
|
v_cond, v_uncond = node._convert_to_v_space(object(), object(), object(), cond, uncond, PredictionType.FLOW)
|
||||||
|
assert v_cond is cond
|
||||||
|
assert v_uncond is uncond
|
||||||
|
|
||||||
|
def test_flow_finalize_is_identity(self):
|
||||||
|
node = NRS()
|
||||||
|
x_final = object()
|
||||||
|
result = node._finalize_from_v_space(object(), x_final, object(), object(), PredictionType.FLOW)
|
||||||
|
assert result is x_final
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pred_type", [PredictionType.EPS, PredictionType.V, PredictionType.X0])
|
||||||
|
def test_vp_convert_performs_algebra(self, pred_type):
|
||||||
|
"""EPS/V/X0 all run the ε->v conversion (cond/uncond are transformed,
|
||||||
|
not passed through)."""
|
||||||
|
node = NRS()
|
||||||
|
x_orig, sig_root, sigma = MagicMock(), MagicMock(), MagicMock()
|
||||||
|
cond, uncond = MagicMock(), MagicMock()
|
||||||
|
v_cond, v_uncond = node._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond, pred_type)
|
||||||
|
assert v_cond is not cond
|
||||||
|
assert v_uncond is not uncond
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pred_type", [PredictionType.EPS, PredictionType.V, PredictionType.X0])
|
||||||
|
def test_vp_finalize_performs_algebra(self, pred_type):
|
||||||
|
node = NRS()
|
||||||
|
x_orig, x_final, sig_root, sigma = MagicMock(), MagicMock(), MagicMock(), MagicMock()
|
||||||
|
result = node._finalize_from_v_space(x_orig, x_final, sig_root, sigma, pred_type)
|
||||||
|
assert result is not x_final
|
||||||
|
|
||||||
|
def test_unknown_convert_falls_back_to_vp(self):
|
||||||
|
"""UNKNOWN (and any unhandled type) is treated as VP -> runs the algebra."""
|
||||||
|
node = NRS()
|
||||||
|
x_orig, sig_root, sigma = MagicMock(), MagicMock(), MagicMock()
|
||||||
|
cond, uncond = MagicMock(), MagicMock()
|
||||||
|
v_cond, v_uncond = node._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond, PredictionType.UNKNOWN)
|
||||||
|
assert v_cond is not cond
|
||||||
|
assert v_uncond is not uncond
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for NRS package version metadata and the patch()-time version/pred-type log line."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path for imports
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
import NRS # noqa: E402
|
||||||
|
import NRS.nodes_NRS as nodes_NRS # noqa: E402, N812
|
||||||
|
|
||||||
|
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_is_nonempty_semver_string():
|
||||||
|
"""NRS.__version__ must be importable and look like a X.Y.Z version."""
|
||||||
|
assert isinstance(NRS.__version__, str)
|
||||||
|
assert NRS.__version__
|
||||||
|
assert _SEMVER_RE.match(NRS.__version__), f"__version__ {NRS.__version__!r} is not X.Y.Z"
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_matches_pyproject():
|
||||||
|
"""__version__ must be kept in lock-step with pyproject.toml's version field."""
|
||||||
|
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
||||||
|
text = pyproject_path.read_text()
|
||||||
|
match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text)
|
||||||
|
assert match, "Could not find version in pyproject.toml"
|
||||||
|
assert NRS.__version__ == match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModel:
|
||||||
|
"""Minimal stand-in that resolves to PredictionType.EPS via the direct-hit path."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.model_type = "eps"
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_model_sampler_cfg_function(self, fn, flag):
|
||||||
|
self._captured_fn = fn
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_logs_version_and_pred_type(caplog):
|
||||||
|
"""patch() must announce the NRS version and detected prediction type."""
|
||||||
|
node = nodes_NRS.NRS()
|
||||||
|
model = _StubModel()
|
||||||
|
|
||||||
|
with caplog.at_level("INFO"):
|
||||||
|
node.patch(model, skew=2.0, stretch=5.0, squash=0.75)
|
||||||
|
|
||||||
|
expected = f"NRS v{NRS.__version__}: prediction type detected -> {nodes_NRS.PredictionType.EPS.name}"
|
||||||
|
assert any(expected in rec.message for rec in caplog.records)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfgv"
|
name = "cfgv"
|
||||||
@@ -29,6 +29,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.29.0"
|
version = "3.29.0"
|
||||||
@@ -58,7 +70,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "negative-rejection-steering"
|
name = "negative-rejection-steering"
|
||||||
version = "0.7.4"
|
version = "1.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -143,10 +155,12 @@ version = "9.0.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "iniconfig" },
|
{ name = "iniconfig" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
{ name = "pluggy" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -172,6 +186,15 @@ version = "6.0.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
@@ -246,6 +269,69 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "21.3.3"
|
version = "21.3.3"
|
||||||
@@ -255,6 +341,7 @@ dependencies = [
|
|||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "python-discovery" },
|
{ name = "python-discovery" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|||||||
Reference in New Issue
Block a user