Files
olivv-cs-ComfyUI-FunPack/tests/test_reference_conditioning_ownership.py
T
digital-garbage 0597603980 feat(log): one line per feature, state then reason
[FunPack] K/V conditioning patch: Inactive | Non-LTX model loaded
    [FunPack] Chunk cache: Active | 22 chunks, 4 steps each, consistency

`feature(tag, name, active, reason)` is the shape, and `skipped` renders
identically — a deliberate skip and an inactive feature are the same fact and
should not have two spellings. `failed` becomes `Name: Failed | <effect>`,
keeping the effect rather than the exception text: "that explains what I am
looking at" is the half worth the line, and an exception message rarely says it.

Everything funpack_log prints is now cut at the first sentence end, so the call
sites keep their reasoning without spending it on every run. SENTENCE ENDS ONLY
— an em-dash introduces the reason as often as the essay, and a renderer that
guesses wrong eats the one part of the line worth reading. A single long
sentence is hard-trimmed instead of left to run.

FUNPACK_LOG=verbose (or set_verbose) prints the full text, exception included.

Converted to the new shape: the chunk cache, region locks, interior pins, the
taste push, the token-refiner edit, AdaLN gains, rating-learned gains, phrase
emphasis, the K/V conditioning patch, the scene split, Bounded Attention,
per-block LoRA weights, LTX enhancements. The rest of the log gets the trim for
free and keeps its own phrasing until converted.

Tests: 1815 python (+9), 68 JS.
2026-08-26 23:10:27 +03:00

199 lines
9.2 KiB
Python

"""A wired positive CONDITIONING owns the prompt; Studio does not re-encode it.
CLIP used to win whenever both were connected, so Studio replaced the wired conditioning
with its own text-only encode. That is not an approximation of the same tensor. The node
that built the conditioning may have shown Qwen a reference image — and Qwen3-VL is causal,
so the vision block precedes the prompt and every prompt row is a hidden state that has
already read the picture. Studio cannot reproduce that: nothing in the editor pipeline wires
`h3_references` or `source_image`, so its encode never sees an image at all.
"""
import inspect
import sys
import types
import pytest
import torch
sys.path.insert(0, ".")
sys.modules.setdefault("folder_paths", types.SimpleNamespace(models_dir=""))
@pytest.fixture
def studio(monkeypatch):
import conditioning
node = conditioning.FunPackVideoRefinerV2.__new__(conditioning.FunPackVideoRefinerV2)
monkeypatch.setattr(node, "_v2_encode_prompt",
lambda *a, **k: (torch.ones(1, 4, 8), {}, "encoded from text"),
raising=False)
monkeypatch.setattr(node, "_v2_extract_conditioning",
lambda c: (c[0][0], dict(c[0][1])), raising=False)
monkeypatch.setattr(node, "_v2_text_tokenizer_status", lambda: "tokenizer ok",
raising=False)
return node
def _wired(seq=6, dim=8, **meta):
return [[torch.zeros(1, seq, dim), meta]]
def test_a_reference_carrying_conditioning_survives(studio):
"""The case that motivated the change: an r2v node's conditioning reaching the sampler
intact instead of being replaced by a text-only re-encode."""
wired = _wired(minimax_token_tags=torch.tensor([1, 1, 0, 0, 1, 1]))
cond, _meta, _note, owner = studio._v2_conditioning_source(object(), "a cat", wired)
assert owner == "CONDITIONING-owned"
assert torch.equal(cond, wired[0][0])
def test_a_text_only_conditioning_survives_too(studio):
"""Not gated on detecting a vision block. An i2v checkpoint pins its anchor as a keyframe
rather than tokenizing it, so its conditioning carries no vision block and a detector
would have missed it — while the node still prepared something Studio cannot rebuild."""
wired = _wired(minimax_token_tags=torch.tensor([1, 1, 1, 1, 1, 1]))
cond, _meta, _note, owner = studio._v2_conditioning_source(object(), "a cat", wired)
assert owner == "CONDITIONING-owned"
assert torch.equal(cond, wired[0][0])
def test_clip_keeps_every_other_job(studio):
"""Both stay connected: CLIP is still what encodes the negative and the references."""
_cond, _meta, note, _owner = studio._v2_conditioning_source(object(), "a cat", _wired())
assert "CLIP still encodes the negative and references" in note
def test_no_setting_selects_between_them(studio):
"""The wire IS the instruction. Wanting CLIP to own the prompt means not wiring a
conditioning, so there is nothing here to configure."""
import inspect
params = inspect.signature(studio._v2_conditioning_source).parameters
assert "clip_owns_prompt" not in params
assert "prefer_wired" not in params
def test_no_conditioning_wired_still_uses_clip(studio):
cond, _meta, _note, owner = studio._v2_conditioning_source(object(), "a cat", None)
assert owner == "CLIP-owned"
assert torch.equal(cond, torch.ones(1, 4, 8))
def test_an_invalid_wired_conditioning_falls_back_to_clip(studio, monkeypatch, capsys):
"""Falling through in silence would look like the wire did nothing."""
monkeypatch.setattr(studio, "_v2_extract_conditioning",
lambda c: ("not a tensor", {}), raising=False)
cond, _meta, _note, owner = studio._v2_conditioning_source(object(), "a cat", _wired())
assert owner == "CLIP-owned"
assert torch.equal(cond, torch.ones(1, 4, 8))
assert "invalid" in capsys.readouterr().out
# --- the scene split cannot rebuild what it did not encode -----------------
def test_the_split_is_skipped_when_the_conditioning_is_wired(monkeypatch, capsys):
"""The split re-encodes every scene from text and its entries replace the output
WHOLESALE. It re-establishes H3's visual conditioning from source_image /
h3_references — neither of which the editor pipeline ever sets — so a wired reference
conditioning was replaced by text-only encodes and the character became someone else.
With CLIP disconnected the split returns None and the wired conditioning survived, which
is exactly why disconnecting CLIP 'fixed' it.
"""
import conditioning
node = conditioning.FunPackVideoRefinerV2()
called = []
monkeypatch.setattr(node, "_v2_transition_scene_conditionings",
lambda *a, **k: called.append(a) or None, raising=False)
src = inspect.getsource(conditioning.FunPackVideoRefinerV2.refine_v2)
# The guard is on ownership, not on CLIP being absent.
assert 'split_by_transitions and conditioning_owner == "CONDITIONING-owned"' in src
assert "elif split_by_transitions:" in src
def test_the_skip_is_reported_not_silent():
"""A silently unsplit multi-scene run looks like the scene texts were ignored."""
import conditioning
src = inspect.getsource(conditioning.FunPackVideoRefinerV2.refine_v2)
assert '"Scene split", False' in src
assert "Disconnect it to split per scene." in src
# --- a multi-entry wired conditioning keeps all of its entries --------------
def test_entries_past_the_first_are_not_discarded():
"""An r2v node emits the reference block and the encoded prompt as SEPARATE entries.
`_v2_extract_conditioning` reads entry 0 and Studio rebuilt a single-entry list around
it, so the character survived and the prompt did not — a tensor holding a vision block
and nothing to say. Symptom: the right face speaking invented syllables."""
import conditioning
src = inspect.getsource(conditioning.FunPackVideoRefinerV2.refine_v2)
assert 'conditioning_owner == "CONDITIONING-owned" and isinstance(positive_conditioning, list)' in src
# Kept AND tagged: the Chain Sampler counts one entry per scene, so an untagged extra
# entry becomes a second scene instead of riding with the first.
assert 'meta_copy["funpack_companion_conditioning"] = True' in src
assert "output_conditioning + companions" in src
def test_the_extra_entries_are_reported():
import conditioning
src = inspect.getsource(conditioning.FunPackVideoRefinerV2.refine_v2)
assert "pass through unchanged" in src
def test_the_tag_surplus_message_names_the_prompt_as_the_missing_half():
"""The first version of this message said the REFERENCE was missing. It is the opposite:
a leading run that is almost all image marks IS the reference, and what was trimmed is
the prompt."""
import conditioning
src = inspect.getsource(conditioning)
assert "are the encoded PROMPT" in src
assert "The prompt has been cut, not the tags." in src
# --- nothing re-encodes a conditioning it did not build --------------------
def test_a_wired_entry_is_marked_so_later_steps_can_tell(studio):
_cond, meta, _note, _owner = studio._v2_conditioning_source(object(), "a cat", _wired())
assert meta["funpack_conditioning_owner"] == "wired"
def test_bounded_attention_skips_a_wired_entry(studio, monkeypatch):
"""It replaces the tensor with a re-encode of the text — which has no reference in it,
because this node was never given one. 512 positions became 197: the whole conditioning
swapped for a text-only encode."""
import conditioning
monkeypatch.setattr(conditioning, "_log",
types.SimpleNamespace(failed=lambda *a, **k: None,
note_on_change=lambda *a, **k: None, feature=lambda *a, **k: None),
raising=False)
monkeypatch.setattr(studio, "_v2_bounded_attention_split_encode",
lambda *a, **k: (torch.ones(1, 197, 8), 40), raising=False)
wired = [[torch.zeros(1, 512, 8),
{"funpack_conditioning_owner": "wired", "funpack_scene_text": "One. Two."}]]
out = studio._v2_apply_bounded_attention(wired, object())
assert int(out[0][0].shape[1]) == 512
assert "funpack_bound_split_tokens" not in out[0][1]
def test_bounded_attention_still_runs_on_studios_own_encode(studio, monkeypatch):
"""Skipping it everywhere would neuter the feature for the graphs it was built for."""
monkeypatch.setattr(studio, "_v2_bounded_attention_split_encode",
lambda *a, **k: (torch.ones(1, 197, 8), 40), raising=False)
own = [[torch.zeros(1, 512, 8), {"funpack_scene_text": "One. Two."}]]
out = studio._v2_apply_bounded_attention(own, object())
assert out[0][1]["funpack_bound_split_tokens"] == 40
def test_every_finalize_stage_is_length_checked():
"""A stage that rebuilds the tensor showed up only as a tag mismatch three functions
away, which read like housekeeping."""
import conditioning
src = inspect.getsource(conditioning.FunPackVideoRefinerV2._v2_finalize_conditioning)
for name in ("pulse temporal", "rapid temporal", "auto temporal", "bounded attention",
"relative steering", "absolute steering"):
assert f'_step("{name}"' in src
assert "changed the conditioning length" in src