Compare commits

...
Author SHA1 Message Date
Alisson AnjosandClaude Opus 5.5 6df4276ae9 TARA: accept Add Guide keyframes (history frames) in native temporal mode
Add Guide keyframes pack as 'cond' rows right after the text, before the
references, at the target's own timeline positions and never denoised. The
layout now locates the reference rows after them instead of refusing them, so a
shot of a long take can lock the previous shot's last frames at its start while
TARA follows <Video 1>. The keyframe rows get no temporal bias; reference and
target alignment is unchanged (new test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:22:59 +00:00
2 changed files with 23 additions and 4 deletions
+5 -3
View File
@@ -48,8 +48,9 @@ def dense_backend_selection(override):
def temporal_layout(native, refs, keyframes, video_index=1):
if keyframes:
raise ValueError('Native temporal mode cannot be combined with Add Guide, first-frame guides or history. Remove those guides.')
# Add Guide keyframes (e.g. the previous shot's last frames as history) pack as 'cond' rows right after the text,
# before the references, at the target's own timeline positions and never denoised. They stay as they are: no
# temporal bias touches them, and the reference rows are located after them.
required = ('position_ids', 'segments', 'seq_len', 'signature')
if any(not hasattr(native, x) for x in required):
raise RuntimeError('Unsupported H3 PackedLayout API; update ComfyUI.')
@@ -59,8 +60,9 @@ def temporal_layout(native, refs, keyframes, video_index=1):
target = torch.zeros_like(source)
ta, tb, _ = next(s for s in native.segments if s[2] == 'video')
target[ta:tb] = True
# Native packing is text, each reference's optional audio then video, targets.
# Native packing is text, keyframe cond rows, each reference's optional audio then video, targets.
cursor = next(s[1] for s in native.segments if s[2] == 'text')
cursor = max([cursor] + [s[1] for s in native.segments if s[2] in ('cond', 'cond_audio')])
videos = 0
for ref in refs:
kind = ref['kind']
+18 -1
View File
@@ -36,7 +36,24 @@ def test_wrong_video_index_and_temporal_mismatch_rejected():
with pytest.raises(ValueError,match='Video 2'):node.temporal_layout(l,r,None,2)
l,r=example(6)
with pytest.raises(ValueError,match='identical temporal'):node.temporal_layout(l,r,None)
with pytest.raises(ValueError,match='Add Guide'):node.temporal_layout(l,r,[{}])
def test_add_guide_history_keyframes_keep_reference_alignment():
# The previous shot's last frames as an Add Guide history (cond rows after the text) must not shift the reference.
l,refs=example();plain,src0,tgt0=node.temporal_layout(l,refs,None)
kfs=[dict(resolved_frame_index=0,latent=torch.zeros(1,4,2,8,8))]
k=CORE.PackedLayout(3,7,8,8,2,keyframes=kfs,refs=refs)
aligned,src,tgt=node.temporal_layout(k,refs,kfs)
cond=[s for s in k.segments if s[2]=='cond'];assert cond
a,b,_=cond[0];n=b-a
assert src.sum()==src0.sum() and tgt.sum()==tgt0.sum()
assert not src[a:b].any() and not tgt[a:b].any()
# Same rows as without history, shifted by the cond rows; same aligned positions for the reference and the target.
assert torch.equal(src[b:],src0[a:]) and torch.equal(tgt[b:],tgt0[a:])
torch.testing.assert_close(aligned.position_ids[src],plain.position_ids[src0],rtol=0,atol=1e-12)
torch.testing.assert_close(aligned.position_ids[tgt],plain.position_ids[tgt0],rtol=0,atol=1e-12)
# History rows sit on the target's first frames and are left untouched.
torch.testing.assert_close(aligned.position_ids[a:b],k.position_ids[a:b],rtol=0,atol=0)
assert aligned.position_ids[a,0]==aligned.position_ids[tgt][0,0]
def context():
l,r=example();a,s,t=node.temporal_layout(l,r,None)