Files
olivv-cs-ComfyUI-FunPack/modules/conditioning/identity_transfer/tests/test_identity_transfer.py
T

134 lines
5.6 KiB
Python

import pytest
import torch
from core import patching
KEY = "funpack_identity"
def _run(tiny, patched):
patched.patch_model(device_to=torch.device("cpu"), load_weights=False)
try:
return tiny.run(patched)
finally:
patched.unpatch_model(unpatch_weights=False)
def _patched(tiny, seg=2.0, ref=None):
from modules.conditioning.identity_transfer import patch
p = tiny.patcher.clone()
torch.manual_seed(1)
patch.install(p, ref if ref is not None else torch.randn(1, 128, 1, 2, 2), seg, KEY)
return p
def test_the_reference_goes_in_and_is_cut_off_again_so_shapes_are_unchanged(tiny_ltx):
base_v, base_a = tiny_ltx.run()
v, a = _run(tiny_ltx, _patched(tiny_ltx))
assert v.shape == base_v.shape and a.shape == base_a.shape
assert not torch.allclose(v, base_v) # the reference tokens were attended to
def test_the_source_phase_tag_changes_the_result(tiny_ltx):
ref = torch.randn(1, 128, 1, 2, 2)
v2, _ = _run(tiny_ltx, _patched(tiny_ltx, seg=2.0, ref=ref))
v0, _ = _run(tiny_ltx, _patched(tiny_ltx, seg=0.0, ref=ref))
assert not torch.allclose(v2, v0)
def test_it_comes_off_again(tiny_ltx):
base_v, _ = tiny_ltx.run()
p = _patched(tiny_ltx)
assert patching.strip(p, KEY) == 4 and not p.object_patches
v, _ = _run(tiny_ltx, p)
assert torch.allclose(v, base_v)
def test_a_model_without_the_stages_is_refused_in_words(tiny_ltx):
from modules.conditioning.identity_transfer import patch
class NotLtx:
def get_model_object(self, name):
return None
with pytest.raises(RuntimeError, match="LTX audio\\+video"):
patch.install(NotLtx(), torch.zeros(1), 2.0, KEY)
def test_a_failing_stage_is_said_and_the_run_goes_on_without_the_reference(tiny_ltx, monkeypatch):
from modules.conditioning.identity_transfer import patch
said = []
monkeypatch.setattr(patch.log, "once", lambda key, level, source, message: said.append(message))
base_v, _ = tiny_ltx.run()
v, _ = _run(tiny_ltx, _patched(tiny_ltx, ref=torch.randn(1, 5, 1, 2, 2))) # wrong channel count
assert said and "appending the reference tokens" in said[0]
assert torch.allclose(v, base_v)
def test_the_node_passes_everything_through_with_no_image(tiny_ltx):
from modules.conditioning.identity_transfer.nodes import FunPackIdentityTransfer
pos, neg = [[torch.zeros(1, 2, 3), {}]], [[torch.zeros(1, 2, 3), {}]]
out = FunPackIdentityTransfer.execute(tiny_ltx.patcher, None, pos, neg, {"samples": tiny_ltx.video}).result
assert out[1] is pos and out[2] is neg and "untouched" in out[3] and not out[0].object_patches
def test_the_node_encodes_the_reference_at_the_latent_size_and_patches(tiny_ltx):
from modules.conditioning.identity_transfer.nodes import FunPackIdentityTransfer
seen = {}
class Vae:
downscale_index_formula = (8, 32, 32)
def encode(self, pixels):
seen["shape"] = tuple(pixels.shape)
return torch.randn(1, 128, 1, 2, 2)
image = torch.rand(1, 100, 80, 3)
pos, neg = [[torch.zeros(1, 2, 3), {}]], [[torch.zeros(1, 2, 3), {}]]
patched, p, n, status = FunPackIdentityTransfer.execute(
tiny_ltx.patcher, Vae(), pos, neg, {"samples": tiny_ltx.video}, image=image).result
assert seen["shape"] == (1, 64, 64, 3) # latent 2x2 at /32
assert "reference tokens appended (source phase 2)" in status and len(patched.object_patches) == 4
def test_the_arcface_tokens_are_appended_to_both_conditionings():
from modules.conditioning.identity_transfer.projector import append_context_tokens
cond = [[torch.zeros(1, 2, 6), {"attention_mask": torch.ones(1, 2)}]]
out = append_context_tokens(cond, torch.ones(1, 4, 8)) # wider tokens are cut to the context
assert out[0][0].shape == (1, 6, 6) and out[0][1]["attention_mask"].shape == (1, 6)
def test_the_rotation_composes_onto_both_rope_layouts():
from modules.conditioning.identity_transfer.rope import rotate_overlap_freqs
cos, sin = torch.ones(1, 6, 4), torch.zeros(1, 6, 4)
c2, s2, _ = rotate_overlap_freqs((cos, sin, "split"), 2, 1.0)
assert torch.equal(c2[:, :4], cos[:, :4]) and not torch.equal(c2[:, 4:], cos[:, 4:])
mat = torch.eye(2).reshape(1, 1, 1, 1, 2, 2).expand(1, 6, 1, 4, 2, 2).clone()
m2, _ = rotate_overlap_freqs((mat, "split"), 2, 1.0)
assert torch.equal(m2[:, :4], mat[:, :4]) and not torch.equal(m2[:, 4:], mat[:, 4:])
assert rotate_overlap_freqs((cos, sin, "split"), 0, 1.0)[0] is cos
def _guided(tiny, patched):
"""The real forward with a guide frame: a denoise_mask with a hard-pinned frame, keyframe_idxs, per-token timestep."""
patched.patch_model(device_to=torch.device("cpu"), load_weights=False)
try:
video, dmask = torch.randn(1, 128, 3, 2, 2), torch.ones(1, 1, 3, 2, 2)
dmask[:, :, 0] = 0.0
dmask[:, :, -1] = 0.0
kf = torch.zeros(1, 3, 4, 2)
kf[:, 0] = 5
dm = patched.model.diffusion_model
ts = dm.patchifier.patchify((dmask * 500.0)[:, :1])[0].reshape(1, -1)
opts = {"sample_sigmas": torch.tensor([1.0, .5, 0.0]), "sigmas": torch.tensor([.5])}
with torch.inference_mode():
return dm([video, tiny.audio], (ts, torch.full((1, 3), 500.0)), tiny.context, frame_rate=25,
transformer_options=opts, denoise_mask=dmask, keyframe_idxs=kf)
finally:
patched.unpatch_model(unpatch_weights=False)
def test_a_latent_with_guide_frames_still_runs(tiny_ltx):
base_v, _ = _guided(tiny_ltx, tiny_ltx.patcher.clone())
v, _ = _guided(tiny_ltx, _patched(tiny_ltx))
assert v.shape == base_v.shape and not torch.allclose(v, base_v)