Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
"""Prompt markup, with H3's REAL tokenizer (offline) and the tiny real H3 model.
|
|
|
|
The encoder is a stand-in -- one fixed vector per token id -- because Qwen3-VL-32B
|
|
is not a unit-test dependency; placement depends on the tokenizer and the tags,
|
|
which are real here.
|
|
"""
|
|
|
|
import math
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _needs_comfy(comfyui):
|
|
"""Imports comfy."""
|
|
|
|
|
|
class FakeH3Clip:
|
|
def __init__(self):
|
|
from comfy.text_encoders.minimax import MiniMaxH3Tokenizer
|
|
self.tokenizer = MiniMaxH3Tokenizer(embedding_directory=None, tokenizer_data={})
|
|
|
|
def tokenize(self, text):
|
|
return self.tokenizer.tokenize_with_weights(text)
|
|
|
|
def encode_from_tokens_scheduled(self, tokens):
|
|
ids = [t[0] for t in tokens["qwen3vl_32b"][0]]
|
|
rows = torch.stack([torch.randn(48, generator=torch.Generator().manual_seed(int(i)))
|
|
for i in ids]).unsqueeze(0)
|
|
return [[rows, {"minimax_token_tags": torch.ones(len(ids), dtype=torch.long)}]]
|
|
|
|
|
|
def _encode(clip, text):
|
|
return clip.encode_from_tokens_scheduled(clip.tokenize(text))
|
|
|
|
|
|
def _apply(tiny, raw):
|
|
from modules.conditioning.prompt_markup.nodes import (FunPackApplyPromptMarkup,
|
|
FunPackPromptMarkup)
|
|
clean, markup, _ = FunPackPromptMarkup.execute(raw).result
|
|
clip = FakeH3Clip()
|
|
positive = _encode(clip, clean)
|
|
tiny.context = positive[0][0]
|
|
model, pos, status = FunPackApplyPromptMarkup.execute(
|
|
tiny.patcher, clip, positive, {"samples": tiny.video}, markup).result
|
|
return model, pos, status, positive
|
|
|
|
|
|
def _spy_masks(tiny, model):
|
|
"""Every (q rows, mask) the backend receives, via an override UNDER ours."""
|
|
from core import dit_hooks
|
|
seen = []
|
|
|
|
def spy(func, q, k, v, *a, mask=None, **kw):
|
|
seen.append((q.shape[2], mask))
|
|
return func(q, k, v, *a, mask=mask, **kw)
|
|
|
|
under = tiny.patcher.clone()
|
|
dit_hooks.add_attention_override(under, "spy", spy)
|
|
ours = model.model_options["transformer_options"]["optimized_attention_override"]
|
|
# Put ours on top of the spy.
|
|
under.model_options["transformer_options"]["optimized_attention_override"] = (
|
|
lambda func, *a, **kw: ours(lambda *x, **y: under_spy(func, *x, **y), *a, **kw))
|
|
under_spy = lambda func, *a, **kw: spy(func, *a, **kw) # noqa: E731
|
|
tiny.run(under)
|
|
return seen
|
|
|
|
|
|
def test_the_node_strips_markup_and_reports_it():
|
|
from modules.conditioning.prompt_markup.nodes import FunPackPromptMarkup
|
|
clean, markup, status = FunPackPromptMarkup.execute("a (red:1.5) car [sits|runs]").result
|
|
assert clean == "a red car sits"
|
|
assert status == "1 weighted, 1 blended"
|
|
|
|
|
|
def test_a_weight_biases_exactly_that_words_keys(tiny_h3):
|
|
model, _p, status, _pos = _apply(tiny_h3, "a (red:1.5) car drives")
|
|
assert status == "1 weight(s)"
|
|
masks = [m for n, m in _spy_masks(tiny_h3, model) if m is not None]
|
|
assert masks, "the bias never reached attention"
|
|
row = masks[0].reshape(-1)
|
|
# tokens: "a" " red" " car" " drives" -> "red" is token 1, prompt at row 0
|
|
assert row[1].item() == pytest.approx(math.log(1.5), rel=1e-4)
|
|
assert torch.count_nonzero(row).item() == 1
|
|
|
|
|
|
def test_it_changes_the_output_and_weight_one_does_not(tiny_h3):
|
|
base_model, _p, _s, pos = _apply(tiny_h3, "a red car drives")
|
|
base = tiny_h3.run(base_model)
|
|
weighted, *_ = _apply(tiny_h3, "a (red:3) car drives")
|
|
assert not torch.allclose(tiny_h3.run(weighted)[0], base[0])
|
|
|
|
|
|
def test_a_timed_phrase_is_masked_from_video_outside_its_window(tiny_h3):
|
|
# Latent frame 0 = pixel frame 0, frame 1 = pixel frames 1-4 at 24 fps;
|
|
# 0.05-0.2 s touches only latent frame 1 = video rows 4..8 of 8.
|
|
model, _p, status, _pos = _apply(tiny_h3, "a red [car@0.05-0.2] drives")
|
|
assert status == "1 timed phrase(s)"
|
|
calls = _spy_masks(tiny_h3, model)
|
|
# The text-only token refiner runs first on exactly the prompt: untouched.
|
|
n, m = calls[0]
|
|
assert n == int(tiny_h3.context.shape[1]) and m is None, "the refiner saw the window"
|
|
per_block = [(n, m) for n, m in calls if m is not None][:3] # block 0's chunks
|
|
assert [n for n, _m in per_block] == [10, 4, 4] # text+audio | video out | in
|
|
assert sum(n for n, _m in per_block) == tiny_h3_seq(tiny_h3)
|
|
key = [m.reshape(-1)[2].item() for _n, m in per_block] # "car" is token 2
|
|
assert key[0] == 0.0, "text/audio rows lost the phrase"
|
|
assert key[1] == pytest.approx(-30.0), "video outside the window still sees it"
|
|
assert key[2] == 0.0, "video inside the window lost it"
|
|
|
|
|
|
def tiny_h3_seq(tiny):
|
|
video_rows = 2 * 2 * 2
|
|
return int(tiny.context.shape[1]) + video_rows + 2 * 3
|
|
|
|
|
|
def test_a_blend_moves_only_the_prompt_rows_and_zero_strength_moves_nothing(tiny_h3):
|
|
_m, pos, status, original = _apply(tiny_h3, "a red car [drives|flies]")
|
|
assert status == "1 blend(s)"
|
|
assert not torch.allclose(pos[0][0], original[0][0])
|
|
_m, pos0, _s, original0 = _apply(tiny_h3, "a red car [drives|flies:0]")
|
|
assert torch.allclose(pos0[0][0], original0[0][0])
|
|
|
|
|
|
def test_an_encoder_no_model_module_knows_is_refused_out_loud(tiny_h3):
|
|
from modules.conditioning.prompt_markup.nodes import (FunPackApplyPromptMarkup,
|
|
FunPackPromptMarkup)
|
|
_clean, markup, _ = FunPackPromptMarkup.execute("a (red:2) car").result
|
|
|
|
class OtherClip:
|
|
tokenizer = object()
|
|
|
|
positive = [[torch.zeros(1, 4, 48), {}]]
|
|
model, pos, status = FunPackApplyPromptMarkup.execute(
|
|
tiny_h3.patcher, OtherClip(), positive, {"samples": tiny_h3.video}, markup).result
|
|
assert status.startswith("not applied") and pos is positive
|
|
|
|
|
|
def test_the_phrases_it_placed_are_published_for_anything_that_wants_to_look_at_them(tiny_h3):
|
|
from modules.conditioning.prompt_markup.nodes import PHRASES
|
|
model, _p, _s, pos = _apply(tiny_h3, "a (red:1.5) car [drives@0.05-0.2]")
|
|
published = model.model_options["transformer_options"][PHRASES]
|
|
assert published["cond_len"] == int(pos[0][0].shape[1])
|
|
assert [b - a for a, b in published["spans"]] == [1, 1] # "red" and "drives": one token each
|
|
plain, *_ = _apply(tiny_h3, "a red car drives")
|
|
assert PHRASES not in plain.model_options.get("transformer_options", {})
|