Files
olivv-cs-ComfyUI-FunPack/tests/test_negative_erase.py
T
digital-garbageandClaude Opus 5 7a2526a8b7 feat(studio): use the negative prompt at CFG 1
Erase the negative's pooled direction from the positive conditioning
(projection by default, never the vision span). Plus an option to let a
wired positive CONDITIONING own the prompt while CLIP keeps its jobs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 03:33:17 +03:00

158 lines
6.3 KiB
Python

"""Using the negative prompt at CFG 1 by erasing its direction from the positive.
MiniMax H3 never evaluates the negative branch, so the negative prompt is dead weight. These
tests pin what the replacement actually does — and, more importantly, what it refuses to do:
touch the vision span, change a token that has nothing to do with the negative, or fail
silently when there is nothing to work with.
"""
import sys
from pathlib import Path
import pytest
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import negative_erase as ne # noqa: E402
def _cond(t, meta=None):
return [(t, dict(meta or {}))]
# --- the direction ---------------------------------------------------------------------
def test_direction_is_the_pooled_unit_vector():
t = torch.tensor([[[2.0, 0.0], [4.0, 0.0]]])
d = ne.direction(t)
assert torch.allclose(d, torch.tensor([1.0, 0.0]))
def test_direction_pools_only_the_negatives_text_positions():
"""H3 packs [text | vision | audio] into one sequence. Pooling the vision span into the
'negative prompt direction' would make an image the model was shown part of what gets
removed from the prompt."""
t = torch.tensor([[[1.0, 0.0], [0.0, 9.0]]])
meta = {"minimax_token_tags": torch.tensor([1, 0])}
assert torch.allclose(ne.direction(t, meta), torch.tensor([1.0, 0.0]))
def test_a_zero_negative_has_no_direction():
assert ne.direction(torch.zeros(1, 3, 4)) is None
def test_mismatched_tags_are_ignored_rather_than_trusted():
"""Tags shorter than the conditioning mean the two disagree; selecting on them would pick
the wrong rows. Fall back to pooling everything."""
t = torch.tensor([[[1.0, 0.0], [0.0, 1.0]]])
d = ne.direction(t, {"minimax_token_tags": torch.tensor([1])})
assert d is not None and float(d[1]) > 0
# --- projection ------------------------------------------------------------------------
def test_project_removes_the_component_and_leaves_the_rest():
"""The whole reason projection is the default: a token orthogonal to the negative is not
moved at all, while a token aligned with it loses that alignment."""
u = torch.tensor([1.0, 0.0])
pos = torch.tensor([[[3.0, 4.0], [0.0, 5.0]]])
out = ne.erase(pos, u, 1.0, renorm=False)
assert float(out[0, 0, 0]) == pytest.approx(0.0) # the aligned part is gone
assert float(out[0, 0, 1]) == pytest.approx(4.0) # the rest is untouched
assert torch.allclose(out[0, 1], pos[0, 1]) # orthogonal token: unmoved
def test_strength_scales_the_removal():
u = torch.tensor([1.0, 0.0])
out = ne.erase(torch.tensor([[[2.0, 1.0]]]), u, 0.5, renorm=False)
assert float(out[0, 0, 0]) == pytest.approx(1.0)
def test_above_one_pushes_past_orthogonal_into_the_opposite():
u = torch.tensor([1.0, 0.0])
out = ne.erase(torch.tensor([[[2.0, 1.0]]]), u, 2.0, renorm=False)
assert float(out[0, 0, 0]) == pytest.approx(-2.0)
def test_renorm_keeps_each_tokens_strength():
"""Projection always shrinks. Without this, a big strength reads partly as a quieter
prompt rather than purely as less of the unwanted thing."""
u = torch.tensor([1.0, 0.0])
pos = torch.tensor([[[3.0, 4.0]]])
out = ne.erase(pos, u, 1.0, renorm=True)
assert float(out.norm()) == pytest.approx(5.0, abs=1e-4)
assert float(out[0, 0, 0]) == pytest.approx(0.0, abs=1e-4)
def test_subtract_moves_every_token_by_the_same_relative_amount():
"""The blunt mode, kept for comparison: it moves a token that had nothing to do with the
negative just as much as one that did."""
u = torch.tensor([1.0, 0.0])
out = ne.erase(torch.tensor([[[0.0, 1.0]]]), u, 0.5, mode="subtract", renorm=False)
assert float(out[0, 0, 0]) == pytest.approx(-0.5) # an orthogonal token IS moved
def test_the_vision_span_of_the_positive_is_never_touched():
"""Erasing a text direction out of the reference image's conditioning is not what a
negative PROMPT was asked to do."""
u = torch.tensor([1.0, 0.0])
pos = torch.tensor([[[1.0, 0.0], [1.0, 0.0]]])
meta = {"minimax_token_tags": torch.tensor([1, 0])}
out = ne.erase(pos, u, 1.0, meta=meta, renorm=False)
assert float(out[0, 0, 0]) == pytest.approx(0.0) # text position: erased
assert float(out[0, 1, 0]) == pytest.approx(1.0) # vision position: untouched
def test_dtype_and_device_survive():
u = torch.tensor([1.0, 0.0])
pos = torch.tensor([[[3.0, 4.0]]], dtype=torch.float16)
assert ne.erase(pos, u, 1.0).dtype == torch.float16
# --- the list-level entry point --------------------------------------------------------
def test_apply_returns_a_note_saying_what_it_did():
pos = _cond(torch.tensor([[[3.0, 4.0]]]))
neg = _cond(torch.tensor([[[1.0, 0.0]]]))
out, note = ne.apply(pos, neg, 1.0)
assert "negative_erase" in note and "project" in note
assert float(out[0][0][0, 0, 0]) == pytest.approx(0.0, abs=1e-4)
def test_strength_zero_is_a_true_no_op():
pos = _cond(torch.tensor([[[3.0, 4.0]]]))
out, note = ne.apply(pos, _cond(torch.tensor([[[1.0, 0.0]]])), 0.0)
assert out is pos and note == ""
def test_an_empty_negative_says_so_instead_of_going_quiet():
"""A switch that is on and doing nothing has to say which. Silence reads as 'it worked'."""
pos = _cond(torch.tensor([[[3.0, 4.0]]]))
out, note = ne.apply(pos, [], 1.0)
assert out is pos and "no negative conditioning" in note
def test_a_useless_negative_says_so_too():
pos = _cond(torch.tensor([[[3.0, 4.0]]]))
out, note = ne.apply(pos, _cond(torch.zeros(1, 2, 2)), 1.0)
assert out is pos and "no usable direction" in note
def test_every_scene_entry_is_treated():
"""A multi-scene prompt is a list of conditionings; erasing from only the first would
make the negative apply to scene 1 alone."""
pos = [(torch.tensor([[[3.0, 4.0]]]), {}), (torch.tensor([[[5.0, 12.0]]]), {})]
out, note = ne.apply(pos, _cond(torch.tensor([[[1.0, 0.0]]])), 1.0)
assert "2 conditioning entries" in note
assert all(float(e[0][0, 0, 0]) == pytest.approx(0.0, abs=1e-3) for e in out)
def test_a_dimension_mismatch_leaves_the_conditioning_alone():
pos = _cond(torch.tensor([[[3.0, 4.0, 5.0]]]))
out, note = ne.apply(pos, _cond(torch.tensor([[[1.0, 0.0]]])), 1.0)
assert "no conditioning entry could be modified" in note
assert out is pos