Files
alisson-anjos-ComfyUI-BFSNodes/tests/test_bfs_shot_loop.py
T
alisson-anjosandClaude Opus 5.5 d01ca239ce Never leave the join's outputs empty in mp4-file mode
With save_to = mp4 file, chosen or forced by the RAM guard, the images and
comparison outputs were blocked, so the result nodes stayed empty and looked
broken. They now carry the video read back from the written files, scaled to
fit the free RAM (the files keep the full size), and the join shows the
files on its node. Bump BFSNodes version to 1.105.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:54:06 +00:00

1212 lines
64 KiB
Python

from __future__ import annotations
import importlib.util
import os
import sys
import tempfile
import types
import unittest
from pathlib import Path
from unittest.mock import patch
import torch
ROOT = Path(__file__).resolve().parents[1]
_TMP = tempfile.mkdtemp(prefix="bfs_shotloop_test_")
# The module only needs a few folder_paths calls; stub them so the tests run without ComfyUI.
_fp = types.ModuleType("folder_paths")
_fp.get_temp_directory = lambda: _TMP
_fp.get_input_directory = lambda: _TMP
_fp.get_annotated_filepath = lambda name: str(Path(_TMP) / name)
sys.modules.setdefault("folder_paths", _fp)
class _Blocker:
def __init__(self, message):
self.message = message
_ce = types.ModuleType("comfy_execution")
_gu = types.ModuleType("comfy_execution.graph_utils")
_gu.ExecutionBlocker = _Blocker
sys.modules.setdefault("comfy_execution", _ce)
sys.modules.setdefault("comfy_execution.graph_utils", _gu)
SPEC = importlib.util.spec_from_file_location("bfs_shot_loop", ROOT / "bfs_shot_loop.py")
assert SPEC is not None and SPEC.loader is not None
SL = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(SL)
H3 = "H3 (17n+5)"
def _shot(i, start, end, cut=False, gen=None, count=1, queue=False, run_id=""):
length = end - start
return {"index": i, "count": count, "start": start, "end": end, "length": length,
"gen_length": gen or SL.snap_up(length, H3), "fps": 24.0, "cut_before": cut,
"queue": queue, "run_id": run_id}
class GridTest(unittest.TestCase):
def test_h3_grid(self):
self.assertEqual([SL.snap_up(x, H3) for x in (1, 5, 6, 22, 23, 107, 108)], [5, 5, 22, 22, 39, 107, 124])
self.assertEqual([SL.snap_down(x, H3) for x in (4, 5, 21, 22, 106, 107, 110)], [5, 5, 5, 22, 90, 107, 107])
def test_other_grids(self):
self.assertEqual(SL.snap_up(50, "LTX / Wan (8n+1)"), 57)
self.assertEqual(SL.snap_down(50, "LTX / Wan (8n+1)"), 49)
self.assertEqual(SL.snap_up(6, "Wan (4n+1)"), 9)
self.assertEqual(SL.snap_up(37, "any"), 37)
def test_generation_size_is_on_the_multiple(self):
w, h = SL.generation_size(1280, 720, 0.15, 32)
self.assertEqual((w % 32, h % 32), (0, 0))
self.assertGreater(w, h)
def test_planner_defaults_to_half_megapixel(self):
self.assertEqual(SL.DEFAULT_PLAN["megapixels"], 0.5)
class TextGenerateCompatibilityTest(unittest.TestCase):
def _generate(self, execute):
calls = []
def wrapped(*args, **kwargs):
result = execute(*args, **kwargs)
calls.append(kwargs)
return result
textgen = types.ModuleType("comfy_extras.nodes_textgen")
textgen.TextGenerate = type("TextGenerate", (), {"execute": staticmethod(wrapped)})
comfy_extras = types.ModuleType("comfy_extras")
comfy_extras.__path__ = []
modules = {"comfy_extras": comfy_extras, "comfy_extras.nodes_textgen": textgen}
with patch.dict(sys.modules, modules):
result = SL.vlm_generate(object(), "describe this", torch.zeros(1, 2, 2, 3), 32)
self.assertEqual(result, "generated")
return calls[0]
def test_old_textgenerate_signature_does_not_receive_mtp(self):
def execute(clip, prompt, max_length, sampling_mode, image=None):
return types.SimpleNamespace(args=("generated",))
kwargs = self._generate(execute)
self.assertNotIn("mtp", kwargs)
def test_new_textgenerate_signature_keeps_mtp_disabled(self):
def execute(clip, prompt, max_length, sampling_mode, image=None, mtp="auto"):
return types.SimpleNamespace(args=("generated",))
kwargs = self._generate(execute)
self.assertEqual(kwargs["mtp"], "off")
class PlanTest(unittest.TestCase):
def test_fixed_mode_splits_evenly_under_the_limit(self):
segs = SL.plan_segments(300, [], "fixed", 107, 24)
self.assertEqual([s["end"] - s["start"] for s in segs], [100, 100, 100])
def test_camera_cut_mode_splits_a_continuous_range_at_max_length(self):
segs = SL.plan_segments(360, [], "shots", 107, 1)
self.assertEqual(len(segs), 4)
self.assertTrue(all(s["end"] - s["start"] <= 107 for s in segs))
self.assertEqual(segs[-1]["end"], 360)
def test_shots_split_at_cuts_and_long_shots_are_divided(self):
segs = SL.plan_segments(300, [50, 80], "shots", 107, 10)
bounds = [(s["start"], s["end"]) for s in segs]
self.assertEqual(bounds[:2], [(0, 50), (50, 80)])
self.assertTrue(all(e - s <= 107 for s, e in bounds))
self.assertEqual(bounds[-1][1], 300)
self.assertTrue(segs[1]["cut_before"])
def test_short_shots_merge_into_a_neighbour_that_still_fits(self):
segs = SL.plan_segments(200, [10, 100], "shots", 107, 24)
self.assertEqual([(s["start"], s["end"]) for s in segs], [(0, 100), (100, 200)])
def test_limits_and_manual_bounds(self):
self.assertEqual(len(SL.plan_segments(1000, [], "fixed", 100, 10, max_parts=3)), 3)
self.assertEqual(SL.plan_segments(1000, [], "fixed", 100, 10, max_total=250)[-1]["end"], 250)
manual = SL.plan_segments(100, [], "manual", 107, 10, manual=[30, 70])
self.assertEqual([(s["start"], s["end"]) for s in manual], [(0, 30), (30, 70), (70, 100)])
def test_builtin_detector_finds_spikes_and_respects_the_gap(self):
score = [1.0] * 100
raw = [0.01] * 100
for i in (20, 22, 60):
score[i], raw[i] = 50.0, 0.5
self.assertEqual(SL.detect_cuts(score, raw, 0.5, 24.0), [20, 60])
class JoinTest(unittest.TestCase):
def _frames(self, n, value):
return torch.full((n, 8, 8, 3), float(value))
def test_trims_to_true_lengths_in_order(self):
shots = [_shot(1, 30, 50, cut=True, gen=22), _shot(0, 0, 30, gen=39)]
imgs = [self._frames(22, 0.8), self._frames(39, 0.2)]
video, audio, fps = SL.BFSShotJoin()._join_all(imgs, shots, [0])
self.assertEqual(video.shape[0], 50)
self.assertAlmostEqual(float(video[0, 0, 0, 0]), 0.2)
self.assertAlmostEqual(float(video[-1, 0, 0, 0]), 0.8)
self.assertEqual(fps, 24.0)
def test_crossfade_only_across_soft_joins(self):
imgs = [self._frames(39, 0.0), self._frames(39, 1.0)]
soft = SL.BFSShotJoin()._join_all(imgs, [_shot(0, 0, 30, gen=39), _shot(1, 30, 60, gen=39)], [4])[0]
hard = SL.BFSShotJoin()._join_all(imgs, [_shot(0, 0, 30, gen=39), _shot(1, 30, 60, cut=True, gen=39)], [4])[0]
self.assertGreater(float(soft[31, 0, 0, 0]), 0.0)
self.assertLess(float(soft[31, 0, 0, 0]), 1.0)
self.assertEqual(float(hard[31, 0, 0, 0]), 1.0)
def test_audio_is_trimmed_to_the_video(self):
audio = {"waveform": torch.zeros(1, 2, 48000 * 10), "sample_rate": 48000}
imgs = [self._frames(39, 0.5)]
_, a, _ = SL.BFSShotJoin()._join_all(imgs, [_shot(0, 0, 24, gen=39)], [0], [audio])
self.assertEqual(a["waveform"].shape[-1], 48000)
def test_queue_mode_blocks_until_every_shot_is_stored(self):
join = SL.BFSShotJoin()
shots = [_shot(0, 0, 20, count=2, queue=True, run_id="t1"), _shot(1, 20, 45, cut=True, count=2, queue=True, run_id="t1")]
first = join.join([self._frames(22, 0.1)], [shots[0]], [0])
self.assertIsInstance(first[0], _Blocker)
video, _, _, _, _ = join.join([self._frames(39, 0.9)], [shots[1]], [0])
self.assertEqual(video.shape[0], 45)
self.assertAlmostEqual(float(video[0, 0, 0, 0]), 0.1, places=2)
class FilterTest(unittest.TestCase):
STATS = {"persons": 1, "person_area": 0.2, "person_frames": 6, "faces": 1, "brightness": 0.4, "motion": 0.02, "sampled": 6}
def test_no_filter_keeps_everything(self):
self.assertEqual(SL.skip_reason(self.STATS, 50, dict(SL.DEFAULT_FILTERS)), "")
self.assertFalse(SL.filters_active(dict(SL.DEFAULT_FILTERS)))
def test_each_filter(self):
f = dict(SL.DEFAULT_FILTERS)
self.assertEqual(SL.skip_reason(dict(self.STATS, persons=0), 50, dict(f, person=True)), "no person")
self.assertIn("smaller", SL.skip_reason(dict(self.STATS, person_area=0.01), 50, dict(f, person=True, min_person_area=0.03)))
self.assertIn("more than", SL.skip_reason(dict(self.STATS, persons=5), 50, dict(f, max_persons=2)))
self.assertEqual(SL.skip_reason(dict(self.STATS, faces=0), 50, dict(f, face=True)), "no face")
self.assertEqual(SL.skip_reason(dict(self.STATS, brightness=0.02), 50, dict(f, skip_dark=True)), "dark / fade")
self.assertEqual(SL.skip_reason(dict(self.STATS, motion=0.0), 50, dict(f, skip_static=True)), "static")
self.assertIn("shorter", SL.skip_reason(self.STATS, 5, dict(f, min_frames=10)))
class TimelineJoinTest(unittest.TestCase):
def _tl(self, fill):
return {"path": "", "fps": 24.0, "width": 8, "height": 8, "fill": fill, "audio": None, "n": 60,
"src": list(range(60)),
"segs": [{"start": 0, "end": 20, "run": True, "cut_before": False, "run_index": 0},
{"start": 20, "end": 40, "run": False, "cut_before": True, "run_index": -1},
{"start": 40, "end": 60, "run": True, "cut_before": True, "run_index": 1}]}
def test_drop_removes_skipped_shots(self):
shots = [_shot(0, 0, 20, gen=22), _shot(1, 40, 60, cut=True, gen=22)]
imgs = [torch.zeros(22, 8, 8, 3), torch.ones(22, 8, 8, 3)]
video = SL.BFSShotJoin()._join_all(imgs, shots, [0], None, self._tl("drop"))[0]
self.assertEqual(video.shape[0], 40)
self.assertEqual(float(video[25, 0, 0, 0]), 1.0)
def test_original_fill_reads_the_source_frames(self):
shots = [_shot(0, 0, 20, gen=22), _shot(1, 40, 60, cut=True, gen=22)]
imgs = [torch.zeros(22, 8, 8, 3), torch.ones(22, 8, 8, 3)]
orig = SL._read_frames
SL._read_frames = lambda path, idx, size: [__import__("numpy").full((8, 8, 3), 128, "uint8") for _ in idx]
try:
video = SL.BFSShotJoin()._join_all(imgs, shots, [0], None, self._tl("original"))[0]
finally:
SL._read_frames = orig
self.assertEqual(video.shape[0], 60)
self.assertAlmostEqual(float(video[30, 0, 0, 0]), 128 / 255, places=3)
def test_skipped_shot_keeps_its_sound(self):
# 60 frames at 24 fps, sample rate 240 -> 10 samples per frame; each sample holds its timeline frame number
sr = 240
timeline_audio = {"waveform": torch.arange(600, dtype=torch.float32).div(10).floor().view(1, 1, 600), "sample_rate": sr}
runs_only = {"waveform": torch.full((1, 1, 400), -1.0), "sample_rate": sr, "bfs_runs_only": True}
tl = dict(self._tl("original"), audio=timeline_audio)
shots = [_shot(0, 0, 20, gen=22), _shot(1, 40, 60, cut=True, gen=22)]
imgs = [torch.zeros(22, 8, 8, 3), torch.ones(22, 8, 8, 3)]
orig = SL._read_frames
SL._read_frames = lambda path, idx, size: [__import__("numpy").full((8, 8, 3), 128, "uint8") for _ in idx]
try:
_, a, _ = SL.BFSShotJoin()._join_all(imgs, shots, [0], [runs_only], tl)
# a soundtrack from elsewhere is still used as given
_, other, _ = SL.BFSShotJoin()._join_all(imgs, shots, [0], [dict(runs_only, bfs_runs_only=False)], tl)
finally:
SL._read_frames = orig
wf = a["waveform"][0, 0]
self.assertEqual(wf.shape[0], 600)
self.assertEqual([float(wf[i * 10]) for i in (0, 25, 30, 45, 59)], [0.0, 25.0, 30.0, 45.0, 59.0]) # in sync, skipped shot not silent
self.assertNotIn("bfs_runs_only", a)
self.assertEqual(float(other["waveform"][0, 0, 0]), -1.0)
class CastTest(unittest.TestCase):
# person 0 on frames 0-47 (big face), person 1 on 48-95, both small on 96-119
CAST = {"step": 6, "people": [{"id": 0}, {"id": 1}], "samples":
[{"f": f, "faces": [{"pid": 0, "area": 0.1}]} for f in range(0, 48, 6)]
+ [{"f": f, "faces": [{"pid": 1, "area": 0.1}]} for f in range(48, 96, 6)]
+ [{"f": f, "faces": []} for f in range(96, 120, 6)]}
def test_shot_people_and_main(self):
self.assertEqual(SL.shot_people(self.CAST, 0, 60), {"people": [0, 1], "main": 0})
self.assertEqual(SL.shot_people(self.CAST, 96, 120), {"people": [], "main": -1})
def test_change_points_need_a_long_enough_run(self):
self.assertEqual(SL.person_change_points(self.CAST, 0, 120, 24), [48])
self.assertEqual(SL.person_change_points(self.CAST, 0, 60, 24), [])
def test_plan_splits_assigns_and_skips_by_person(self):
path = str(Path(_TMP) / "cast.mp4"); Path(path).write_bytes(b"x")
a = {"fps": 24.0, "n": 120, "score": [0.0] * 120, "raw": [0.0] * 120}
SL._CAST_CACHE[SL._cast_key(path, a)] = self.CAST
orig = SL.find_cuts
SL.find_cuts = lambda *args, **kw: ([], "test")
try:
plan = dict(SL.DEFAULT_PLAN, max_s=10, min_s=0.5, cast={"0": {"ref": "a.png"}}, cast_split=True, cast_only=True)
segs = SL.apply_filters(plan, a, path, SL.resolve_plan(plan, a, path))
finally:
SL.find_cuts = orig
self.assertEqual([(s["start"], s["end"]) for s in segs], [(0, 48), (48, 120)])
self.assertEqual(segs[0]["ref"], "a.png")
self.assertTrue(segs[0]["run"])
self.assertEqual(segs[1]["skip_reason"], "no linked person")
class ComparisonTest(unittest.TestCase):
def test_join_returns_a_labelled_side_by_side(self):
shots = [dict(_shot(0, 0, 20, gen=22), frames=torch.zeros(22, 32, 48, 3), ref=torch.ones(1, 40, 30, 3),
ref2=None, prompt="a test prompt"),
dict(_shot(1, 20, 40, cut=True, gen=22), frames=torch.zeros(22, 32, 48, 3), ref=None, ref2=None,
prompt="")]
imgs = [torch.full((22, 32, 48, 3), 0.5), torch.full((22, 32, 48, 3), 0.7)]
video, _, _, comp, _ = SL.BFSShotJoin().join(imgs, shots, [0], comparison=[True], label=["steps 20"])
self.assertEqual(video.shape[0], 40)
self.assertEqual(comp.shape[0], 40)
self.assertEqual(comp.shape[2] % 16, 0)
self.assertEqual(comp.shape[1] % 16, 0)
self.assertGreaterEqual(comp.shape[2], 48 * 2 + 32)
self.assertGreater(comp.shape[1], 32)
off = SL.BFSShotJoin().join(imgs, shots, [0])[3]
self.assertEqual(off.shape[0], 1)
class ContinuityTest(unittest.TestCase):
def test_pick_frame_ignores_the_overlap(self):
frames = torch.arange(30, dtype=torch.float32).view(30, 1, 1, 1).expand(30, 2, 2, 3)
self.assertEqual(float(SL.pick_frame(frames, 20, "first")[0, 0, 0, 0]), 0.0)
self.assertEqual(float(SL.pick_frame(frames, 20, "middle")[0, 0, 0, 0]), 10.0)
self.assertEqual(float(SL.pick_frame(frames, 20, "last")[0, 0, 0, 0]), 19.0)
def test_chain_uses_the_previous_result_only_when_asked(self):
frames = torch.arange(30, dtype=torch.float32).view(30, 1, 1, 1).expand(30, 2, 2, 3)
SL.remember_result({"index": 0, "count": 2}, frames)
shot = {"index": 1, "count": 2, "chain": "reference", "chain_frame": "last", "prev_length": 20}
self.assertEqual(float(SL.chain_image(shot)[0, 0, 0, 0]), 19.0)
self.assertIsNone(SL.chain_image(dict(shot, chain="off")))
self.assertIsNone(SL.chain_image(dict(shot, index=0)))
self.assertIsNone(SL.chain_image(dict(shot, index=3, count=4))) # not the shot right after
stored = torch.ones(1, 2, 2, 3)
self.assertIs(SL.chain_image(dict(shot, chain_image=stored)), stored)
def test_plan_carries_the_per_shot_setting(self):
a = {"fps": 24.0, "n": 100, "score": [0.0] * 100, "raw": [0.0] * 100}
plan = dict(SL.DEFAULT_PLAN, mode="fixed", max_s=2, bounds=[50],
segs=[{}, {"chain": "first frame", "chain_frame": "middle"}])
segs = SL.resolve_plan(plan, a)
self.assertEqual((segs[0]["chain"], segs[1]["chain"], segs[1]["chain_frame"]), ("head", "first frame", "middle")) # head is the default
class MaskCropTest(unittest.TestCase):
def test_spec_takes_target_from_shot_and_settings_from_global(self):
spec = SL.mask_spec({"text": "person", "padding": 0.9}, {"padding": 0.3, "text": "ignored"})
self.assertEqual((spec["text"], spec["padding"], spec["fill_holes"]), ("person", 0.3, True))
def test_shape_fills_holes_and_holds_in_time(self):
m = torch.zeros(5, 9, 9, dtype=torch.uint8)
m[2, 2:7, 2:7] = 1
m[2, 4, 4] = 0 # a hole
out = SL.shape_mask(m, dict(SL.DEFAULT_MASK, temporal_expand=1))
self.assertEqual(int(out[2, 4, 4]), 1)
self.assertEqual(int(out[1, 3, 3]), 1) # held one frame back
self.assertEqual(int(out[0, 3, 3]), 0)
blocks = SL.shape_mask(m, dict(SL.DEFAULT_MASK, temporal_expand=0, blockify=4))
self.assertEqual(tuple(blocks.shape), (5, 9, 9))
def test_box_is_the_union_plus_padding(self):
m = torch.zeros(2, 10, 10, dtype=torch.uint8)
m[0, 2:4, 2:4] = 1
m[1, 6:8, 6:8] = 1
self.assertEqual(SL.crop_box(m, 0.0), [0.2, 0.2, 0.8, 0.8])
self.assertIsNone(SL.crop_box(torch.zeros(2, 10, 10), 0.1))
def test_uncrop_pastes_only_inside_the_mask(self):
full = torch.zeros(3, 20, 20, 3)
mask = torch.zeros(3, 20, 20)
mask[:, 5:15, 5:15] = 1
shot = {"full_frames": full, "crop": {"box": [0.25, 0.25, 0.75, 0.75], "mask": mask, "paste": "mask",
"expand": 0, "feather": 0}}
out = SL.uncrop(torch.ones(3, 8, 8, 3), shot)
self.assertEqual(tuple(out.shape), (3, 20, 20, 3))
self.assertEqual(float(out[0, 10, 10, 0]), 1.0)
self.assertEqual(float(out[0, 1, 1, 0]), 0.0)
class DuetPanelCropTest(unittest.TestCase):
def test_join_side_cuts_the_panel_off(self):
shot = {"width": 64, "height": 32, "panel": {"position": "left", "h": 2, "w": 4, "strip_h": 0, "strip_w": 4}}
canvas = torch.zeros(3, 32, 128, 3)
canvas[:, :, 64:] = 1.0
out = SL.crop_panel(canvas, shot)
self.assertEqual(tuple(out.shape), (3, 32, 64, 3))
self.assertEqual(float(out.min()), 1.0)
self.assertIs(SL.crop_panel(out, shot), out) # already the video size
self.assertIs(SL.crop_panel(canvas, {"width": 64}), canvas) # no panel
class SilentAudioTest(unittest.TestCase):
def test_join_without_audio_returns_a_silent_track(self):
imgs = [torch.full((39, 8, 8, 3), 0.5)]
video, a, fps = SL.BFSShotJoin()._join_all(imgs, [_shot(0, 0, 24, gen=39)], [0])
self.assertEqual(tuple(a["waveform"].shape[:2]), (1, 2))
self.assertEqual(a["waveform"].shape[-1], 44100) # 24 frames at 24 fps = 1 s
self.assertEqual(float(a["waveform"].abs().max()), 0.0)
self.assertFalse(SL._usable({"waveform": torch.zeros(1, 2, 0), "sample_rate": 44100}))
class RepackTest(unittest.TestCase):
def test_replaces_only_connected_pieces_and_fits_the_guide(self):
shot = dict(_shot(0, 0, 20, gen=22), width=8, height=8, frames=torch.zeros(22, 8, 8, 3),
ref=torch.zeros(1, 4, 4, 3), ref2=None, prompt="old", audio=None)
ref = torch.ones(1, 6, 6, 3)
out = SL.BFSShotRepack().repack(shot, ref_image=ref)[0]
self.assertTrue(torch.equal(out["ref"], ref))
self.assertEqual(out["prompt"], "old")
self.assertIs(out["frames"], shot["frames"])
out = SL.BFSShotRepack().repack(shot, guide_frames=torch.ones(30, 16, 16, 3), prompt="new")[0]
self.assertEqual(tuple(out["frames"].shape), (22, 8, 8, 3))
self.assertEqual(out["prompt"], "new")
self.assertEqual(out["length"], 20)
class ConditioningWriterTest(unittest.TestCase):
"""The optional prompt writer of BFS Shot H3 Conditioning (template path, no models)."""
def setUp(self):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import bfs_h3_side_panel as SP
self.SP, self.seen = SP, {}
seen = self.seen
class R2V:
@staticmethod
def execute(**kw):
seen["prompt"] = kw["prompt"]
return types.SimpleNamespace(args=("pos", "lat"))
mod = types.ModuleType("comfy_extras.nodes_minimax_h3")
mod.MiniMaxH3ReferenceToVideo, mod.MiniMaxH3AddGuide = R2V, R2V
self._old = {k: sys.modules.get(k) for k in ("comfy_extras", "comfy_extras.nodes_minimax_h3")}
sys.modules["comfy_extras"] = types.ModuleType("comfy_extras")
sys.modules["comfy_extras.nodes_minimax_h3"] = mod
self._apply = SP.BFSH3SidePanel.apply
SP.BFSH3SidePanel.apply = lambda self, pos, lat, *a, **k: (pos, lat, {"position": "left"}, None, None)
self._patch = SP.patch_model_rope
SP.patch_model_rope = lambda m, info, gap: "patched"
def tearDown(self):
self.SP.BFSH3SidePanel.apply, self.SP.patch_model_rope = self._apply, self._patch
for k, v in self._old.items():
if v is None:
sys.modules.pop(k, None)
else:
sys.modules[k] = v
def _shot(self):
return dict(prompt="my own {layout} prompt", width=448, height=800, gen_length=22,
frames=torch.zeros(22, 8, 8, 3), ref=torch.zeros(1, 4, 4, 3), ref2=None, audio=None)
def _run(self, **kw):
c = SL.BFSShotH3Conditioning()
return c.condition(self._shot(), "clip", "vae", "none", True, "none", "match", **kw)
def test_planner_prompt_is_default_and_layout_filled(self):
out = self._run()
self.assertEqual(out[3], "my own prompt")
out = self._run(duet="canvas")
self.assertIn("the left half shows the original performance", out[3])
self.assertEqual(self.seen["prompt"], out[3])
def test_task_template_follows_the_duet_mode(self):
out = self._run(duet="canvas", task="character swap")
self.assertIn("split screen", out[3])
self.assertIn("<Picture 1>", out[3])
out = self._run(duet="shifted RoPE", model="m", task="character swap")
self.assertNotIn("split screen", out[3])
self.assertEqual(out[2], "patched")
def test_setting_picture_is_the_last_reference_with_static(self):
seen = {}
orig = sys.modules["comfy_extras.nodes_minimax_h3"].MiniMaxH3ReferenceToVideo.execute
def capture(**kw):
seen.update(kw)
return orig(**kw)
sys.modules["comfy_extras.nodes_minimax_h3"].MiniMaxH3ReferenceToVideo.execute = staticmethod(capture)
shot = self._shot()
shot["frames"] = torch.full((22, 8, 8, 3), 0.5)
mask = torch.zeros(22, 8, 8)
mask[:, 2:4, 2:4] = 1
out = SL.BFSShotH3Conditioning().condition(shot, "clip", "vae", "none", True, "none", "match", duet="canvas",
task="character swap", setting_ref="on (generation size)",
setting_mask=mask)
refs = list(seen["ref_images"].values())
self.assertEqual(len(refs), 2)
pic = refs[-1][0]
self.assertTrue(((pic[3, 3] == 0) | (pic[3, 3] == 1)).all()) # static inside the (grown) mask
self.assertTrue(torch.allclose(pic[7, 7], torch.tensor(0.5))) # the place outside it
self.assertIn("<Picture 2> shows the setting", out[3])
self.assertEqual(SL.add_setting("a {setting} b", 3, True), "a <Picture 3> b")
def test_task_needs_duet(self):
with self.assertRaises(ValueError):
self._run(task="style", instruction="anime")
class SimpleConditioningTest(unittest.TestCase):
def test_exposes_only_the_common_controls(self):
inputs = SL.BFSShotH3ConditioningSimple.INPUT_TYPES()
names = set(inputs["required"]) | set(inputs["optional"])
self.assertIn("with_audio", names)
self.assertIn("inpaint", names)
self.assertNotIn("duet", names)
self.assertNotIn("model", names)
self.assertNotIn("rope_gap", names)
self.assertNotIn("task", names)
def test_forwards_safe_advanced_defaults_and_hides_model_output(self):
seen = {}
original = SL.BFSShotH3Conditioning.condition
def fake(_self, *args, **kwargs):
seen["args"], seen["kwargs"] = args, kwargs
return "positive", "latent", "unchanged model", "prompt"
SL.BFSShotH3Conditioning.condition = fake
try:
out = SL.BFSShotH3ConditioningSimple().condition(
"shot", "clip", "vae", SL.BFSShotH3ConditioningSimple.GUIDE_MODES[0], True, "match",
audio_vae="audio vae", with_audio=True)
finally:
SL.BFSShotH3Conditioning.condition = original
self.assertEqual(out, ("positive", "latent", "prompt"))
self.assertEqual(seen["args"][5], "none")
self.assertTrue(seen["kwargs"]["with_audio"])
self.assertEqual(seen["kwargs"]["inpaint"], SL.INPAINT_MODES[0])
class TargetTest(unittest.TestCase):
def test_target_placeholder(self):
self.assertEqual(SL.fill_target("Replace {target} with <Subject 1>", "the man in a red coat"),
"Replace the man in a red coat with <Subject 1>")
self.assertEqual(SL.fill_target("Replace {target} with <Subject 1>", ""), "Replace the person with <Subject 1>")
self.assertEqual(SL.fill_target("no placeholder", "x"), "no placeholder")
class MaskOverlayTest(unittest.TestCase):
def test_overlay_marks_mask_and_box(self):
o = torch.zeros(3, 32, 32, 3)
m = torch.zeros(3, 32, 32); m[:, 8:16, 8:16] = 1
out = SL._mask_overlay(o, {"mask": m, "box": [0.1, 0.1, 0.9, 0.9]}, 3)
self.assertGreater(float(out[0, 12, 12, 0]), 0.3) # red inside the mask
self.assertEqual(float(out[0, 20, 20].sum()), 0.0) # untouched outside mask and box
self.assertGreater(float(out[0, 3, 16, 1]), 0.5) # yellow box edge
class StitchFinishTest(unittest.TestCase):
def _shot(self):
full = torch.full((4, 64, 64, 3), 0.5)
mask = torch.zeros(4, 64, 64); mask[:, 20:44, 24:40] = 1
return {"full_frames": full, "crop": {"box": [0.125, 0.125, 0.875, 0.875], "mask": mask, "paste": "mask",
"expand": 0, "feather": 2}}
def test_ring_colour_match_removes_drift_and_keeps_subject_contrast(self):
shot = self._shot()
gen = torch.full((4, 48, 48, 3), 0.62) # the model brightened the whole crop (+0.12)
gen[:, 12:36, 16:32] = 0.2 # a darker new subject
plain = SL.uncrop(gen, shot)
fixed = SL.uncrop(gen, shot, match_colors=1.0)
# around the subject the drift is gone, inside it the new subject stays darker than the scene
self.assertGreater(abs(float(plain[0, 21, 23].mean()) - 0.5), abs(float(fixed[0, 21, 23].mean()) - 0.5))
self.assertLess(float(fixed[0, 30, 30].mean()), 0.45)
def test_edge_hardness(self):
a = torch.tensor([[[0.1, 0.5, 0.9]]])
h = SL._alpha_hardness(a, 1.0)
self.assertEqual([round(float(x), 2) for x in h[0, 0]], [0.0, 0.5, 1.0])
self.assertTrue(torch.equal(SL._alpha_hardness(a, 0.0), a))
class InpaintInCropTest(unittest.TestCase):
def test_latent_starts_from_the_shot_and_only_the_mask_is_generated(self):
sys.path.insert(0, str(ROOT))
nested = sys.modules.get("comfy.nested_tensor")
if nested is None:
class _N:
is_nested = True
def __init__(self, t):
self.tensors = list(t)
comfy = sys.modules.setdefault("comfy", types.ModuleType("comfy"))
nested = types.ModuleType("comfy.nested_tensor"); nested.NestedTensor = _N
comfy.nested_tensor = nested
sys.modules["comfy.nested_tensor"] = nested
sys.modules.setdefault("comfy.utils", types.ModuleType("comfy.utils"))
class VAE:
def encode(self, px):
t = ((px.shape[0] - 5) // 17) * 5 + 2
x = px.mean(-1)[None, None]
return torch.nn.functional.interpolate(x, size=(t, px.shape[1] // 16, px.shape[2] // 16)).repeat(1, 24, 1, 1, 1)
lat = {"samples": nested.NestedTensor((torch.zeros(1, 24, 7, 8, 8), torch.zeros(1, 32, 2, 37)))}
frames = torch.full((22, 128, 128, 3), 0.4)
mask = torch.zeros(22, 128, 128); mask[:, 48:80, 48:80] = 1
pos, out = SL.inpaint_latent(lat, [], VAE(), frames, mask)
video = out["samples"].tensors[0]
self.assertAlmostEqual(float(video.mean()), 0.4, places=4) # starts from the shot's own frames
vm, am = out["noise_mask"].tensors
self.assertEqual(tuple(vm.shape), (1, 1, 7, 8, 8))
self.assertEqual(float(vm[0, 0, 3, 4, 4]), 1.0) # the person: generated
self.assertEqual(float(vm[0, 0, 3, 0, 0]), 0.0) # the rest of the crop: kept
self.assertEqual(float(vm[0, 0, 3, 2, 4]), 1.0) # grown by one latent cell
self.assertTrue(bool((am == 1).all())) # audio fully generated
_, out2 = SL.inpaint_latent(lat, [], VAE(), frames, mask, strength=0.8)
vm2 = out2["noise_mask"].tensors[0]
self.assertAlmostEqual(float(vm2[0, 0, 3, 4, 4]), 0.8, places=5) # partly regenerated inside the mask
self.assertEqual(float(vm2[0, 0, 3, 0, 0]), 0.0) # still kept outside
def test_generation_mask_with_and_without_crop(self):
m = torch.zeros(5, 64, 64); m[:, 20:40, 20:40] = 1
cropped = SL.generation_mask({"crop": {"crop_mask": m, "expand": 4}, "frames": torch.zeros(5, 64, 64, 3)})
self.assertEqual(float(cropped[0, 17, 30]), 1.0) # grown by expand
self.assertEqual(float(cropped[0, 10, 30]), 0.0)
with self.assertRaises(ValueError):
SL.generation_mask({"crop": None, "mask_src": None, "frames": torch.zeros(5, 64, 64, 3)})
orig = SL.shot_mask
SL.shot_mask = lambda *a: {"masks": torch.nn.functional.interpolate(m[:, None], size=(32, 32))[:, 0].to(torch.uint8)}
try:
full = SL.generation_mask({"crop": None, "frames": torch.zeros(5, 64, 64, 3),
"mask_src": {"path": "", "analysis": {}, "start": 0, "length": 5,
"spec": {"expand": 0}}})
finally:
SL.shot_mask = orig
self.assertEqual(tuple(full.shape), (5, 64, 64))
self.assertTrue(torch.equal(full, m))
def test_comparison_overlay_for_mask_only_shots(self):
m = torch.zeros(5, 32, 32); m[:, 8:24, 8:24] = 1
ms = {"path": __file__, "analysis": {"fps": 24.0}, "start": 0, "length": 5, "spec": dict(SL.DEFAULT_MASK, expand=0)}
shot = {"crop": None, "frames": torch.zeros(5, 64, 64, 3), "mask_src": ms, "inpaint": True}
orig = SL.shot_mask
SL.shot_mask = lambda *a: {"masks": m.to(torch.uint8)}
try:
ov = SL.overlay_of(shot)
self.assertEqual(tuple(ov["mask"].shape), (5, 64, 64))
self.assertIsNone(SL.overlay_of(dict(shot, inpaint=False))) # not used and not segmented yet: no SAM 3
self.assertIsNone(SL.overlay_of({"crop": None, "frames": shot["frames"]}))
finally:
SL.shot_mask = orig
o = SL._mask_overlay(torch.full((5, 32, 32, 3), 0.5), ov, 5)
self.assertGreater(float(o[0, 16, 16, 0]), float(o[0, 16, 16, 1])) # red inside the mask
self.assertAlmostEqual(float(o[0, 2, 2, 0]), 0.5) # untouched outside
class ExternalMaskTest(unittest.TestCase):
def _video(self, name, n=48, fps=24.0, w=64, h=36, box=(10, 5, 30, 25)):
import cv2
import numpy as np
path = str(Path(_TMP) / name)
vw = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
for i in range(n):
f = np.zeros((h, w, 3), np.uint8)
x0, y0, x1, y1 = box
f[y0:y1, x0 + i % 4:x1 + i % 4] = 255
vw.write(f)
vw.release()
return path
def setUp(self):
self.an = {"fps": 24.0, "n": 48, "n_src": 48, "fps_src": 24.0, "width": 64, "height": 36}
def test_precedence(self):
p = {"mask_video": "global.mp4"}
self.assertEqual(SL.plan_spec(p, {"video": "shot.mp4", "text": "man"})["ext"]["scope"], "shot")
self.assertNotIn("ext", SL.plan_spec(p, {"text": "man"})) # the shot's own SAM 3 text wins
self.assertEqual(SL.plan_spec(p, {})["ext"], {"file": "global.mp4", "scope": "video"})
SL._EXT_MASK["v.mp4"] = torch.ones(48, 36, 64)
try:
self.assertEqual(SL.plan_spec(p, {}, "v.mp4")["ext"], {"tensor": "v.mp4"}) # the node input beats the panel file
finally:
SL._EXT_MASK.pop("v.mp4")
self.assertFalse(SL.spec_has_mask(SL.plan_spec({}, {})))
def test_file_and_tensor_masks_replace_sam(self):
path = self._video("roto_whole.mp4")
self._video("roto_shot.mp4", n=12)
orig = SL.segment_frames
SL.segment_frames = lambda *a, **k: self.fail("SAM 3 must not run with an external mask")
try:
r = SL.shot_mask(path, self.an, 24, 12, SL.plan_spec({"mask_video": "roto_whole.mp4"}, {}))
self.assertEqual(r["masks"].shape[0], 12)
self.assertTrue(r["box"] is not None)
cov = float(r["masks"].float().mean())
self.assertTrue(0.1 < cov < 0.4, cov)
r2 = SL.shot_mask(path, self.an, 24, 12, SL.plan_spec({}, {"video": "roto_shot.mp4"}))
self.assertEqual(r2["masks"].shape[0], 12)
t = torch.zeros(48, 36, 64); t[:, 0:18, :] = 1 # top half
SL._EXT_MASK[path] = t
r3 = SL.shot_mask(path, self.an, 0, 10, SL.plan_spec({}, {}, path))
m = r3["masks"].float()
self.assertGreater(float(m[:, :8].mean()), 0.9)
self.assertLess(float(m[:, -8:].mean()), 0.1)
finally:
SL.segment_frames = orig
SL._EXT_MASK.pop(path, None)
class FramePasteTest(unittest.TestCase):
def test_paste_back_keeps_the_background_and_takes_the_new_outline(self):
base = torch.full((5, 64, 64, 3), 0.2) # the original frames
result = torch.full((5, 64, 64, 3), 0.9) # generated: everything changed
old = torch.zeros(5, 64, 64); old[:, 20:44, 20:36] = 1 # the original person
new = torch.zeros(5, 64, 64); new[:, 16:48, 18:44] = 1 # the new one is bigger
ms = {"path": "", "analysis": {}, "start": 0, "length": 5, "spec": dict(SL.DEFAULT_MASK, expand=0, feather=0)}
shot = {"index": 0, "frames": base, "mask_src": ms, "paste": True}
o1, o2 = SL.shot_mask, SL.segment_frames
SL.shot_mask = lambda *a: {"masks": old.to(torch.uint8)}
SL.segment_frames = lambda imgs, sp: torch.nn.functional.interpolate(new[:, None], size=imgs.shape[1:3])[:, 0]
try:
out = SL.paste_back(result, shot, True)
only_old = SL.paste_back(result, shot, False)
finally:
SL.shot_mask, SL.segment_frames = o1, o2
self.assertAlmostEqual(float(out[0, 2, 2, 0]), 0.2, places=4) # background: the original
self.assertAlmostEqual(float(out[0, 30, 28, 0]), 0.9, places=4) # the person: generated
self.assertAlmostEqual(float(out[0, 30, 41, 0]), 0.9, places=4) # the bigger new outline is pasted too
self.assertAlmostEqual(float(only_old[0, 30, 41, 0]), 0.2, places=4)
class MaskGuideTest(unittest.TestCase):
def test_masked_only_greys_everything_outside_the_mask(self):
fr = torch.full((4, 32, 32, 3), 0.9)
m = torch.zeros(4, 16, 16); m[:, 4:12, 4:12] = 1
g = SL.masked_only(fr, m)
self.assertEqual(tuple(g.shape), (4, 32, 32, 3))
self.assertAlmostEqual(float(g[0, 16, 16, 0]), 0.9) # the masked region
self.assertAlmostEqual(float(g[0, 2, 2, 0]), 0.5) # grey elsewhere
self.assertEqual(SL.MASK_GUIDES[0], "off")
fr2 = torch.rand(3, 64, 64, 3)
m2 = torch.zeros(3, 64, 64); m2[:, 10:50, 10:50] = 1
old_det = dict(SL._DET); SL._DET["pose"] = None # no YOLO pose model: a clear error
try:
with self.assertRaisesRegex(ValueError, "YOLO pose model"):
SL.masked_only(fr2, m2, look="pose (people)")
finally:
SL._DET.clear(); SL._DET.update(old_det)
for look in [x for x in SL.MASK_GUIDE_LOOKS if not x.startswith("pose")]:
g2 = SL.masked_only(fr2, m2, look=look)
self.assertEqual(tuple(g2.shape), (3, 64, 64, 3), look)
self.assertAlmostEqual(float(g2[0, 2, 2, 0]), 0.5, msg=look) # grey outside, every look
if look != "colour": # no colour inside
self.assertLess(float((g2[..., 0] - g2[..., 1]).abs().max()), 1e-5, look)
t = SL.add_mask_video("x subject_definitions: <Subject 1> is ...", 2)
self.assertIn("<Video 2> shows only the region", t)
self.assertEqual(SL.add_mask_video("follow {mask_video}", 1), "follow <Video 1>")
class ConditionFlowTest(unittest.TestCase):
"""BFS Shot H3 Conditioning with the native H3 nodes replaced by recorders."""
def setUp(self):
sys.path.insert(0, str(ROOT))
calls = self.calls = {"r2v": [], "guides": []}
class Out:
def __init__(self, *a):
self.args = a
class R2V:
@staticmethod
def execute(**kw):
calls["r2v"].append(kw)
N = sys.modules["comfy.nested_tensor"].NestedTensor
return Out([[torch.zeros(1), {}]], {"samples": N((torch.zeros(1, 24, 2, 4, 4), torch.zeros(1, 32, 2, 37)))})
class Guide:
@staticmethod
def execute(positive, latent, frame_idx, vae, image=None, **kw):
calls["guides"].append(image)
return Out(positive)
mod = types.ModuleType("comfy_extras.nodes_minimax_h3")
mod.MiniMaxH3ReferenceToVideo, mod.MiniMaxH3AddGuide = R2V, Guide
sys.modules.setdefault("comfy_extras", types.ModuleType("comfy_extras"))
self._old = sys.modules.get("comfy_extras.nodes_minimax_h3")
sys.modules["comfy_extras.nodes_minimax_h3"] = mod
if "comfy.nested_tensor" not in sys.modules:
class _N:
is_nested = True
def __init__(self, t):
self.tensors = list(t)
comfy = sys.modules.setdefault("comfy", types.ModuleType("comfy"))
nt = types.ModuleType("comfy.nested_tensor"); nt.NestedTensor = _N
comfy.nested_tensor = nt; sys.modules["comfy.nested_tensor"] = nt
def tearDown(self):
if self._old is not None:
sys.modules["comfy_extras.nodes_minimax_h3"] = self._old
else:
sys.modules.pop("comfy_extras.nodes_minimax_h3", None)
def _shot(self, **kw):
m = torch.zeros(22, 64, 64); m[:, 16:48, 16:40] = 1
shot = {"index": 0, "count": 1, "frames": torch.full((22, 64, 64, 3), 0.7), "ref": torch.zeros(1, 32, 32, 3),
"ref2": None, "prompt": "subject_definitions: <Subject 1> is ...", "width": 64, "height": 64,
"gen_length": 22, "audio": None, "crop": {"crop_mask": m, "expand": 0} if kw.get("crop") else None,
"inpaint": kw.get("inpaint", True)}
if not kw.get("crop"):
shot["mask_src"] = {"path": __file__, "analysis": {"fps": 24.0}, "start": 0, "length": 22, "spec": dict(SL.DEFAULT_MASK, expand=0)}
return shot, m
def run_cond(self, shot, m, **kw):
class VAE:
def encode(self, px):
return torch.zeros(1, 24, 2, px.shape[1] // 16, px.shape[2] // 16)
orig = SL.shot_mask
SL.shot_mask = lambda *a: {"masks": m.to(torch.uint8)}
try:
return SL.BFSShotH3Conditioning().condition(shot, None, VAE(), SL.BFSShotH3Conditioning.GUIDE_MODES[0], False,
"none", "match", **kw)
finally:
SL.shot_mask = orig
def test_reference_video_of_the_masked_region(self):
shot, m = self._shot()
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[3], mask_ref_size="full",
mask_guide_look="colour")
kw = self.calls["r2v"][0]
v = kw["ref_videos"]["ref_video_1"]
self.assertAlmostEqual(float(v[0, 2, 2, 0]), 0.5) # grey outside the mask
self.assertAlmostEqual(float(v[0, 30, 30, 0]), 0.7) # the subject
self.assertIn("<Video 1> shows only the region", kw["prompt"])
self.assertEqual(len(self.calls["guides"]), 1) # the normal aligned guide is still there
self.calls["r2v"].clear()
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[3], mask_ref_size="1/2")
self.assertEqual(tuple(self.calls["r2v"][0]["ref_videos"]["ref_video_1"].shape[1:3]), (32, 32))
def test_extra_and_instead_guides(self):
shot, m = self._shot()
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[1])
self.assertEqual(len(self.calls["guides"]), 2)
self.calls["guides"].clear()
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[2])
self.assertEqual(len(self.calls["guides"]), 1)
self.assertAlmostEqual(float(self.calls["guides"][0][0, 2, 2, 0]), 0.5) # it is the masked one
def test_ignored_outside_mask_only(self):
shot, m = self._shot(inpaint=False)
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[3])
self.assertNotIn("ref_videos", self.calls["r2v"][0])
shot, m = self._shot(crop=True)
self.calls["r2v"].clear()
self.run_cond(shot, m, inpaint=SL.INPAINT_MODES[0], mask_guide=SL.MASK_GUIDES[3])
self.assertNotIn("ref_videos", self.calls["r2v"][0])
class DescribeShortTest(unittest.TestCase):
def test_short_details_fit_inside_the_prompt(self):
self.assertEqual(SL.tidy_details('"The green goblin in a torn leather vest."\nextra', {"describe_preset": "short"}),
"green goblin in a torn leather vest")
self.assertEqual(SL.tidy_details("A young woman.", {"describe_preset": "full body"}), "A young woman.")
self.assertIn("one English noun phrase", SL.describe_instruction({}))
def test_every_preset_removes_vlm_answer_labels(self):
for preset in SL.DESCRIBE_PRESETS:
with self.subTest(preset=preset):
self.assertEqual(SL.tidy_details("**Answer:** elderly woman with large glasses", {"describe_preset": preset}),
"elderly woman with large glasses")
def test_short_mode_honours_its_twenty_word_contract(self):
text = "Answer: The " + " ".join(f"word{i}" for i in range(30))
out = SL.tidy_details(text, {"describe_preset": "short"})
self.assertEqual(len(out.split()), 20)
self.assertFalse(out.lower().startswith(("answer", "the ")))
def test_long_modes_are_bounded_but_not_collapsed_to_short(self):
text = " ".join(f"word{i}" for i in range(170))
full = SL.tidy_details(text, {"describe_preset": "full body"})
face = SL.tidy_details(text, {"describe_preset": "head / face"})
self.assertEqual(len(full.split()), 150)
self.assertEqual(len(face.split()), 110)
self.assertTrue(full.endswith("."))
class FitPromptTest(unittest.TestCase):
P = ("body_swap: subject_definitions: <Subject 1> is the {details} shown in <Picture 1>, whose identity define the "
"character. The guide video provides the scene; only {target} is replaced: their face become <Subject 1>. "
"summary: Replace {target} in the guide video with <Subject 1>.")
def test_slot_is_the_sentence_with_a_blank(self):
d = SL.prompt_slot(self.P, "{details}")
self.assertIn("<Subject 1> is the ___ shown in <Picture 1>", d)
self.assertNotIn("{target}", d)
t = SL.prompt_slot(self.P, "{target}")
self.assertIn("only ___ is replaced", t)
self.assertEqual(SL.prompt_slot("no marker here", "{details}"), "")
def test_answer_matches_the_blank(self):
d = SL.prompt_slot(self.P, "{details}")
self.assertEqual(SL.tidy_slot("A golden retriever with a thick coat.", d), "golden retriever with a thick coat")
t = SL.prompt_slot(self.P, "{target}")
self.assertEqual(SL.tidy_slot("the young woman in a black corset.", t), "the young woman in a black corset")
self.assertIn("goes into the blank", SL.describe_instruction({"describe_preset": "short"}, d))
self.assertNotIn("goes into the blank", SL.describe_instruction({"describe_preset": "short", "fit_prompt": False}, d))
self.assertNotIn("goes into the blank", SL.describe_instruction({"describe_preset": "full body"}, d))
# the VLM echoing the sentence (the bug the user hit)
echo = ("humanoid robot is the ___ shown in <Picture 1>, whose identity define the character. The robot has a sleek "
"body with a metallic silver torso.")
self.assertEqual(SL.tidy_slot(echo, d), "humanoid robot")
self.assertEqual(SL.tidy_slot("sleek silver humanoid robot with black joints shown in <Picture 1>", d),
"sleek silver humanoid robot with black joints")
self.assertEqual(SL.tidy_slot("<Subject 1> is the green goblin in a vest", d), "green goblin in a vest")
def test_target_label_is_removed_even_without_a_prompt_slot(self):
old = SL.vlm_generate
SL.vlm_generate = lambda *args, **kwargs: "Answer: elderly woman with large tortoiseshell glasses"
try:
crop = __import__("numpy").zeros((8, 8, 3), dtype="uint8")
self.assertEqual(SL.describe_target(object(), crop),
"the elderly woman with large tortoiseshell glasses")
finally:
SL.vlm_generate = old
class RangeTest(unittest.TestCase):
def test_start_end_and_max_total_from_the_start(self):
self.assertEqual(SL.plan_range(1000, 24.0, 10, 20), (240, 480))
self.assertEqual(SL.plan_range(1000, 24.0, 10, 0), (240, 1000))
self.assertEqual(SL.plan_range(1000, 24.0, 10, 0, 5), (240, 360)) # max total counted from the start
self.assertEqual(SL.plan_range(1000, 24.0, 0, 0, 0, start_f=250, end_f=500), (250, 500)) # frames win
self.assertEqual(SL.plan_range(1000, 24.0, 0, 0, 0, start_f=250, end_f=0), (250, 1000))
segs = SL.plan_segments(480, [100, 300], "shots", 107, 10, start=240)
self.assertEqual(segs[0]["start"], 240)
self.assertEqual(segs[-1]["end"], 480)
self.assertTrue(all(240 <= s["start"] < s["end"] <= 480 for s in segs))
self.assertTrue(any(s["start"] == 300 for s in segs)) # the cut inside the range is kept
fixed = SL.plan_segments(480, [], "fixed", 107, 10, start=240)
self.assertEqual((fixed[0]["start"], fixed[-1]["end"]), (240, 480))
manual = SL.plan_segments(480, [], "manual", 107, 10, manual=[100, 350], start=240)
self.assertEqual([(s["start"], s["end"]) for s in manual], [(240, 350), (350, 480)])
class TrackModeTest(unittest.TestCase):
def test_track_is_used_by_shots_without_their_own_selection_and_sliced_per_shot(self):
p = {"track": {"points": [{"x": 0.5, "y": 0.5, "label": 1}], "frame": 120, "text": "woman"}, "fps": 24}
self.assertIn("track", SL.plan_spec(p, {}, "v.mp4")["ext"])
self.assertNotIn("ext", SL.plan_spec(p, {"text": "man"}, "v.mp4")) # the shot's own text wins
self.assertEqual(SL.plan_spec(p, {"video": "m.mp4"}, "v.mp4")["ext"]["scope"], "shot")
whole = torch.zeros(200, 36, 64, dtype=torch.uint8)
whole[100:150, 10:20, 10:20] = 1 # the subject only in frames 100-149
orig = SL.track_video
SL.track_video = lambda path, a, t: (whole, 50) # the track covers frames 50-249
try:
spec = SL.plan_spec(p, {}, "v.mp4")
an = {"n": 300, "fps": 24.0}
m = SL.ext_masks(spec["ext"], an, 140, 20, 64, 36) # shot 140-159
self.assertEqual(tuple(m.shape), (20, 36, 64))
self.assertFalse(bool(m[0].any())) # frame 140 = track index 90
self.assertTrue(bool(m[-1].any())) # frame 159 = index 109
finally:
SL.track_video = orig
class DuetPromptTest(unittest.TestCase):
def test_fields_are_found_and_the_vlm_fills_them(self):
tpl = ("subject_definitions:\n<Subject 1> is the woman from <Picture 1>: {skin, face}.\n<Subject 2> is the {room} "
"from {setting}.\n\nsummary:\nx {layout}")
self.assertEqual(SL.template_slots(tpl), ["skin, face", "room"])
asked = []
def gen(clip, q, images, max_tokens, temperature=0.0, seed=0):
asked.append(q)
if images is None:
return tpl.replace("{skin, face}", "pale skin, oval face").replace("{room}", "kitchen")
return '{"medium": "phone footage", "actions": "the person waves"}'
old = SL.vlm_generate, SL.vlm_describe
SL.vlm_generate, SL.vlm_describe = gen, lambda clip, ims, q, mt=320, temperature=0.0, seed=0: "the kitchen with white cabinets"
try:
fr = torch.rand(9, 32, 32, 3)
out = SL.duet_prompt(object(), tpl, fr, [torch.rand(1, 32, 32, 3)], "character swap", "", 512,
setting=torch.rand(1, 32, 32, 3))
self.assertIn("pale skin, oval face", out); self.assertIn("{setting}", out); self.assertIn("{layout}", out)
self.assertIn("the person waves", asked[-1])
self.assertEqual(SL.duet_prompt(None, tpl, fr, [], "character swap", ""), tpl.strip())
self.assertEqual(SL.duet_prompt(object(), "a full prompt", fr, [], "character swap", ""), "a full prompt")
w = SL.duet_prompt(object(), "", fr, [torch.rand(1, 32, 32, 3)], "character swap", "", 512,
setting=torch.rand(1, 32, 32, 3))
self.assertIn("<Subject 2> is the kitchen with white cabinets from {setting}.", w)
finally:
SL.vlm_generate, SL.vlm_describe = old
class ShotHistoryTest(unittest.TestCase):
def test_history_is_taken_out_of_the_shot(self):
h = torch.full((17, 8, 8, 3), 255, dtype=torch.uint8)
shot = {"chain": "history", "index": 1, "count": 2, "history": h, "history_src": h}
hist, src = SL.shot_history(shot)
self.assertEqual((tuple(hist.shape), float(hist.max()), "history" in shot), ((17, 8, 8, 3), 1.0, False))
self.assertEqual(SL.shot_history({"chain": "off", "index": 1}), (None, None))
def test_auto_loop_takes_the_previous_render(self):
SL._LAST_RESULT.clear()
SL._LAST_RESULT.update(index=0, count=2, frames=torch.rand(40, 8, 8, 3))
hist, _ = SL.shot_history({"chain": "history", "index": 1, "count": 2, "prev_length": 30, "chain_history": 34})
self.assertEqual(hist.shape[0], 22) # 30 real frames -> 22, the longest 17k+5 that fits
SL._LAST_RESULT.clear()
def test_history_lengths_are_whole_vae_clips(self):
# 17 frames encode to 2 tokens (the first 5 frames): only 17k+5 reaches the boundary
self.assertEqual([SL.history_frame_count(w, 200) for w in (5, 17, 22, 34, 39, 51, 56)], [5, 22, 22, 39, 39, 56, 56])
self.assertEqual([SL.history_frame_count(56, a) for a in (4, 5, 21, 22, 38, 40)], [0, 5, 5, 22, 22, 39])
class ShotHeadTest(unittest.TestCase):
"""Continuity 'head': the previous shot's end starts the next shot, pinned in the latent, and the join cuts it."""
@staticmethod
def _segs(chain="head", n=22, cut=False, gap=0):
return [{"start": 0, "end": 60, "chain": "off", "cut_before": False},
{"start": 60 + gap, "end": 120, "chain": chain, "chain_history": n, "cut_before": cut}]
def test_head_length(self):
self.assertEqual(SL.head_length(self._segs(), 1, H3), 22)
self.assertEqual(SL.head_length(self._segs(n=56), 1, H3), 39) # capped at 39
self.assertEqual(SL.head_length(self._segs(n=17), 1, H3), 22) # old plans: next valid length up
self.assertEqual(SL.head_length(self._segs(), 0, H3), 0) # first shot
self.assertEqual(SL.head_length(self._segs(cut=True), 1, H3), 0) # a camera cut
self.assertEqual(SL.head_length(self._segs(gap=5), 1, H3), 0) # a skipped shot in between
self.assertEqual(SL.head_length(self._segs(chain="history"), 1, H3), 0)
self.assertEqual(SL.head_length(self._segs(), 1, "LTX / Wan (8n+1)"), 0)
def test_join_cuts_the_head(self):
img = torch.arange(30, dtype=torch.float32).view(30, 1, 1, 1).expand(30, 2, 2, 3)
shot = {"head": 5, "gen_length": 30, "frames": img.clone(), "crop": {"mask": torch.zeros(30, 2, 2), "box": [0, 0, 1, 1]}}
out, sh = SL.drop_head(img, shot)
self.assertEqual((out.shape[0], float(out[0, 0, 0, 0]), sh["gen_length"], sh["frames"].shape[0],
sh["crop"]["mask"].shape[0], sh["head"], shot["frames"].shape[0]), (25, 5.0, 25, 25, 25, 0, 30))
self.assertIs(SL.drop_head(img, {"head": 0})[0], img)
def test_remember_result_skips_the_head(self):
SL._LAST_RESULT.clear()
SL.remember_result({"index": 0, "count": 2, "head": 5}, torch.zeros(30, 2, 2, 3))
self.assertEqual(SL._LAST_RESULT["frames"].shape[0], 25)
SL._LAST_RESULT.clear()
def test_pin_head_keeps_the_first_latent_steps(self):
class NT:
def __init__(self, t):
self.tensors, self.is_nested = list(t), True
nt = types.ModuleType("comfy.nested_tensor")
nt.NestedTensor = NT
old = {k: sys.modules.get(k) for k in ("comfy", "comfy.nested_tensor")}
comfy = types.ModuleType("comfy")
comfy.nested_tensor = nt
sys.modules["comfy"], sys.modules["comfy.nested_tensor"] = comfy, nt
try:
import bfs_h3_history as HH
class VAE:
def encode(self, px):
n = px.shape[0]
self.size = tuple(px.shape[1:3])
return torch.full((1, 24, 5 * -(-n // 17) - 3, px.shape[1] // 16, px.shape[2] // 16), 7.0)
vae = VAE()
latent = {"samples": NT((torch.zeros(1, 24, 12, 4, 4), torch.zeros(1, 8, 50)))}
out, n = HH.pin_head(latent, vae, torch.rand(30, 32, 32, 3)) # 30 frames -> the last 22 -> 7 steps
v, m = out["samples"].tensors[0], out["noise_mask"].tensors[0]
self.assertEqual((n, vae.size), (22, (64, 64)))
self.assertTrue(bool((v[:, :, :7] == 7).all()) and bool((v[:, :, 7:] == 0).all()))
self.assertTrue(bool((m[:, :, :7] == 0).all()) and bool((m[:, :, 7:] == 1).all()))
self.assertTrue(bool((out["noise_mask"].tensors[1] == 1).all()))
# an existing inpaint mask stays as it is after the head
inp = torch.full((1, 1, 12, 4, 4), 0.5)
out2, _ = HH.pin_head(dict(latent, noise_mask=NT((inp, torch.ones(1, 8, 50)))), vae, torch.rand(22, 64, 64, 3))
m2 = out2["noise_mask"].tensors[0]
self.assertTrue(bool((m2[:, :, :7] == 0).all()) and bool((m2[:, :, 7:] == 0.5).all()) and bool((inp == 0.5).all()))
finally:
for k, v in old.items():
if v is None:
sys.modules.pop(k, None)
else:
sys.modules[k] = v
class MemoryStatusTest(unittest.TestCase):
def test_shot_tokens(self):
# 107 H3 frames -> 7 clips -> 32 latent steps; 960x544 -> 60 x 34 cells
self.assertEqual(SL.shot_tokens(107, 960, 544), 32 * 34 * 60)
self.assertEqual(SL.shot_tokens(22, 32, 32), 7 * 2 * 2)
self.assertGreater(SL.shot_tokens(90, 960, 544), SL.shot_tokens(73, 960, 544)) # a head makes the shot bigger
self.assertEqual(SL.shot_tokens(81, 32, 32, "LTX / Wan (8n+1)"), 21 * 2 * 2)
def test_record_and_status(self):
import json as _json
path = Path(_TMP) / SL._VRAM_FILE
path.unlink(missing_ok=True)
cuda = types.SimpleNamespace(is_available=lambda: True, is_initialized=lambda: True, current_device=lambda: 0,
get_device_name=lambda d: "Test GPU", max_memory_allocated=lambda: 12 * 2 ** 30,
reset_peak_memory_stats=lambda: None, mem_get_info=lambda: (8 * 2 ** 30, 32 * 2 ** 30))
with patch.object(SL.torch, "cuda", cuda):
SL.vram_record(5000)
SL.vram_record(3000) # smaller: the biggest stays
st = SL.vram_status()
self.assertEqual((st["gpu"], st["max_tokens"], st["max_peak_gb"], st["runs"], st["total_gb"], st["free_gb"]),
("Test GPU", 5000, 12.0, 2, 32.0, 8.0))
self.assertIn("Test GPU", _json.loads(path.read_text()))
SL.vram_record(4000) # no CUDA in use: nothing recorded, nothing started
self.assertEqual(len(_json.loads(path.read_text())["Test GPU"]["samples"]), 2)
class ShotStoreTest(unittest.TestCase):
def test_storage_round_trip(self):
d = tempfile.mkdtemp(dir=_TMP)
y = torch.linspace(0, 1, 48).view(1, 48, 1, 1)
frames = (((torch.sin(torch.arange(12).view(12, 1, 1, 1) + 9 * y) + 1) / 2).expand(12, 48, 64, 3) * 255).round().byte()
for k, mode in enumerate(("lossless", "raw", "smaller")):
SL.store_shot(d, k, frames, {"index": k, "fps": 24.0}, mode)
back, meta = SL.load_shot(d, k)
self.assertEqual((tuple(back.shape), meta["index"]), ((12, 48, 64, 3), k))
diff = int((back.int() - frames.int()).abs().max())
if mode == "smaller":
self.assertLess(diff, 40)
else:
self.assertEqual(diff, 0, mode) # lossless and raw are exact
self.assertTrue(os.path.exists(os.path.join(d, "shot_0000.mkv")))
self.assertFalse(os.path.exists(os.path.join(d, "shot_0001.mkv"))) # raw: the frames are in the .pt
def test_status_cascades_through_continuity(self):
d = tempfile.mkdtemp(dir=_TMP)
for i in range(3):
open(os.path.join(d, f"shot_{i:04d}.pt"), "wb").close()
st = {"done": [0, 1, 2], "sig": {"0": "a", "1": "b", "2": "c"}, "gen": {"0": "g0", "1": "g1", "2": "g2"},
"from": {"1": "g0", "2": "g1"}, "cur": {"sigs": ["a", "b", "c"], "prev": [False, True, False]}}
self.assertEqual(SL.shot_status(st, d), [True, True, True])
st["gen"]["0"] = "new" # shot 1 was redone: shot 2 continues from it
self.assertEqual(SL.shot_status(st, d), [True, False, True]) # shot 3 does not continue (cut)
st["cur"]["sigs"][2] = "changed" # a setting of shot 3 changed
self.assertEqual(SL.shot_status(st, d), [True, False, False])
def test_join_can_write_an_mp4(self):
import cv2
d = tempfile.mkdtemp(dir=_TMP)
imgs = [torch.full((22, 32, 32, 3), 0.2), torch.full((39, 32, 32, 3), 0.8)]
shots = [_shot(0, 0, 20, gen=22), _shot(1, 20, 45, cut=True, gen=39)]
audio = {"waveform": torch.zeros(1, 2, 48000 * 3), "sample_rate": 48000}
with patch.object(SL, "_next_output_path", lambda prefix: os.path.join(d, prefix + "_00001.mp4")):
out = SL.BFSShotJoin().join(imgs, shots, [0], audio=[audio], save_to=[SL.SAVE_TO[1]],
filename_prefix=["test"])
res = out["result"]
self.assertEqual(res[0].shape[0], 45) # the output still carries the video (read back)
self.assertAlmostEqual(float(res[0][30].mean()), 0.8, places=1)
self.assertTrue(res[4].endswith("test_00001.mp4"))
self.assertEqual(out["ui"]["images"][0]["filename"], "test_00001.mp4") # shown on the node
cap = cv2.VideoCapture(res[4]); n = 0
while cap.read()[0]:
n += 1
self.assertEqual(n, 45)
class JoinRamGuardTest(unittest.TestCase):
def test_images_join_falls_back_to_mp4_when_ram_is_short(self):
d = tempfile.mkdtemp(dir=_TMP)
imgs = [torch.full((22, 32, 32, 3), 0.2), torch.full((39, 32, 32, 3), 0.8)]
shots = [_shot(0, 0, 20, gen=22), _shot(1, 20, 45, cut=True, gen=39)]
with patch.object(SL, "_ram_available", lambda: 1000), \
patch.object(SL, "_next_output_path", lambda prefix: os.path.join(d, prefix + "_00001.mp4")):
out = SL.BFSShotJoin().join(imgs, shots, [0], comparison=[True])
res = out["result"]
self.assertTrue(os.path.exists(res[4]) and os.path.exists(res[4][:-4] + "_comparison.mp4"))
self.assertEqual(res[0].shape[0], 45) # never an empty output node
self.assertTrue(res[0].shape[1] < 32 and res[3].shape[0] == 45) # scaled down to the (tiny) free RAM
normal = SL.BFSShotJoin().join(imgs, shots, [0]) # enough RAM: images as before
self.assertEqual((normal[0].shape[0], normal[4]), (45, ""))
def test_join_bytes(self):
self.assertEqual(SL._join_bytes(10, 100, 200, False, 360), 10 * 100 * 200 * 12)
self.assertGreater(SL._join_bytes(10, 100, 200, True, 1024), SL._join_bytes(10, 100, 200, True, 50))
class StoredComparisonTest(unittest.TestCase):
def test_originals_go_to_a_video_not_the_pt(self):
rid = "cmp_" + str(id(self))
y = torch.linspace(0, 1, 64).view(1, 64, 1, 1)
src = [((torch.sin(torch.arange(n).view(n, 1, 1, 1) + 7 * y) + 1) / 2).expand(n, 64, 64, 3).clone() for n in (22, 39)]
shots = [dict(_shot(0, 0, 20, count=2, queue=True, run_id=rid, gen=22), frames=src[0]),
dict(_shot(1, 20, 45, cut=True, count=2, queue=True, run_id=rid, gen=39), frames=src[1])]
SL.BFSShotJoin().join([src[0] * 0.5], [shots[0]], [0], comparison=[True], comparison_height=[64])
out = SL.BFSShotJoin().join([src[1] * 0.5], [shots[1]], [0], comparison=[True], comparison_height=[64])
d = SL.run_dir(rid)
self.assertLess(os.path.getsize(os.path.join(d, "shot_0000.pt")), 50_000) # info only
self.assertTrue(os.path.exists(os.path.join(d, "shot_0000_orig.mkv")))
video, comp = out[0], out[3]
self.assertEqual(video.shape[0], 45)
self.assertEqual(comp.shape[0], 45)
# the original column (left) shows the source, not the dark filler of a missing original
self.assertGreater(float(comp[:, comp.shape[1] // 2, 5:60].mean()), 0.2)
class ColorAnchorTest(unittest.TestCase):
def _shot(self, sat):
src = torch.full((12, 48, 48, 3), 0.5)
res = src.clone()
res[:, 12:36, 12:36] = torch.tensor([0.5 - sat, 0.5 + sat, 0.5 - sat]) # a green subject
res[:, 12:36, 12:36] += torch.linspace(-0.05, 0.05, 24).view(1, 24, 1, 1)
return res, src
def test_subject_chroma_goes_back_to_the_anchor(self):
r1, s1 = self._shot(0.15)
anchor = SL.color_stats(r1, s1)
r4, s4 = self._shot(0.30) # 4 shots later: twice as saturated
out, before = SL.anchor_colors(r4, s4, anchor)
after = SL.color_stats(out, s4)
self.assertGreater(abs(before["mu"][0] - anchor["mu"][0]), 5)
self.assertLess(abs(after["mu"][0] - anchor["mu"][0]), 1.5)
self.assertLess(abs(after["mu"][1] - anchor["mu"][1]), 1.5)
self.assertAlmostEqual(float(out[0, 2, 2, 0]), 0.5, places=2) # the background is untouched
self.assertIsNone(SL.color_stats(s1, s1)) # no subject: nothing to anchor
class FillDetailsTest(unittest.TestCase):
def test_render_node_fills_details_with_its_vlm(self):
old = SL.vlm_describe
SL.vlm_describe = lambda clip, ims, q, mt=320, temperature=0.0, seed=0: "The elderly man with white hair in a light blue t-shirt."
try:
out = SL.fill_details_now("<Subject 1> is the {details} shown in <Picture 1>.", object(), [torch.rand(1, 8, 8, 3)])
self.assertEqual(out, "<Subject 1> is the elderly man with white hair in a light blue t-shirt shown in <Picture 1>.")
self.assertIn("character", SL.fill_details_now("is the {details} shown", None, []))
self.assertEqual(SL.fill_details_now("no slot", None, []), "no slot")
finally:
SL.vlm_describe = old
if __name__ == "__main__":
unittest.main()