Files
olivv-cs-ComfyUI-FunPack/modules/models/minimax_h3/prompt_layout.py
T
digital-garbageandClaude Opus 5.5 9d028ccc92 feat(v5): prompt markup for H3 -- (word:1.5), [phrase@t0-t1], [a|b]
Prompt Markup strips it before the encoder; Apply Prompt Markup puts weights
and timed windows on attention and blends on the conditioning. Where the words
sit is the H3 module's (text_tokenizer, prompt_rows, video_time_rows). Wired
into both H3 presets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:11:51 +03:00

87 lines
3.4 KiB
Python

"""Where H3 puts the prompt, and how its time axis maps to rows.
Three capabilities, so prompt markup never learns H3's layout:
* `text_tokenizer(clip)` -- the raw HF tokenizer behind an H3 CLIP, for exact
character -> token offsets. Tokenizing is free; re-encoding with Qwen3-VL-32B
to find a phrase would cost a 32B forward pass per phrase.
* `prompt_rows(meta, cond_len, prompt_tokens)` -- (start, end) of the prompt
inside a conditioning. The prompt is the LAST thing H3's tokenizer appends
(reference labels and vision blocks come first), and `minimax_token_tags`
marks text 1, vision 0, so it is the tail of the final run of 1s. Measured
from the END so labels in front of it (an audio reference's "<Audio n>: ",
which has no vision block) do not shift it.
* `video_time_rows(samples, t0, t1)` -- a window in seconds as rows of the
target video, which is the last segment of the packed sequence.
The conditioning block leads the packed sequence, so a row inside it is also a
row of the sequence the attention runs over.
"""
FPS = 24
# Latent frame k spans FRAME_PER_TOKEN[k % 5] pixel frames (H3's 5k+2 grid).
FRAME_PER_TOKEN = (1, 4, 4, 4, 4)
def text_tokenizer(clip):
try:
tok = getattr(getattr(getattr(clip, "tokenizer", None), "qwen3vl_32b", None),
"tokenizer", None)
if tok is None:
return None
tok("probe", add_special_tokens=False, return_offsets_mapping=True)
return tok
except Exception: # noqa: BLE001 -- not H3's, or no offsets
return None
def _tags(meta):
tags = (meta or {}).get("minimax_token_tags")
if tags is None:
return None
try:
return [int(t) for t in tags.reshape(-1).tolist()]
except AttributeError:
return [int(t) for t in tags] if isinstance(tags, (list, tuple)) else None
def prompt_rows(meta, cond_len, prompt_tokens):
"""(start, end) of the prompt in the conditioning, or None if it cannot be
proven. Without tags the prompt is the final `prompt_tokens` rows."""
if prompt_tokens <= 0 or cond_len <= 0:
return None
tags = _tags(meta)
if tags is None:
start = cond_len - prompt_tokens
return (start, cond_len) if start >= 0 else None
end = min(len(tags), cond_len)
while end > 0 and tags[end - 1] != 1:
end -= 1
start = end
while start > 0 and tags[start - 1] == 1:
start -= 1
if end - start < prompt_tokens:
return None # a different string was encoded
return end - prompt_tokens, end
def video_time_rows(samples, t0, t1):
"""(video_rows, row_start, row_end) for a window in seconds, rows counted
from the start of the target video segment. None if it misses the clip."""
video = samples.unbind()[0] if getattr(samples, "is_nested", False) else samples
if getattr(video, "ndim", 0) != 5:
return None
latent_t = int(video.shape[2])
frame_rows = ((int(video.shape[3]) + 1) // 2) * ((int(video.shape[4]) + 1) // 2)
cum = [0]
for k in range(latent_t):
cum.append(cum[-1] + FRAME_PER_TOKEN[k % 5])
f0, f1 = int(round(t0 * FPS)), int(round(t1 * FPS))
if f1 <= f0 or f0 >= cum[-1]:
return None
k0 = max(k for k in range(latent_t) if cum[k] <= f0)
k1 = min(latent_t, max(k for k in range(latent_t) if cum[k] < f1) + 1)
if k1 <= k0:
return None
return latent_t * frame_rows, k0 * frame_rows, k1 * frame_rows