Files
olivv-cs-ComfyUI-FunPack/tests/test_reference_conditioning_ownership.py
T
digital-garbageandClaude Opus 5 8e3c707c81 fix(sampler): a wired reference conditioning is not a second scene
The Chain Sampler reads one conditioning entry per SCENE
(scene_conditionings = positive[:max_scenes]). When Studio stopped discarding
the entries past the first of a wired CONDITIONING — the fix for a reference
surviving while the prompt did not — those entries reached the sampler
untagged, and the r2v reference block was counted as a scene of its own. The
run sampled the shot twice, which from outside is "the model ran a second pass
for no reason".

Studio tags every pass-through entry as a companion; the sampler excludes
companions when counting scenes and hands them to every scene instead, so they
still reach the model. Both halves of the original bug stay fixed: nothing is
dropped, and nothing is mistaken for a scene.

Also fixes the liked-conditioning blend, which was the last payload path still
doing a raw lerp: _v2_shape_compatible ignores sequence length by design, so a
direction learned at 586 positions met a 494-position prompt, threw, and took
the whole pull toward what the user rated well with it. Routed through
_v2_payload_like like every other payload.

Adds stage timers for the nine learning steps the first pass did not cover —
55% of a 26.9s run was accounted for, and the rest was unnamed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:06:52 +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 "the scene split is SKIPPED" in src
assert "Disconnect it to let Studio split and encode 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),
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