Revert "Restore long videos release snapshot"

This reverts commit 4a0f5cfcd4.
This commit is contained in:
2026-08-30 21:36:50 +00:00
parent 4a0f5cfcd4
commit 58b1c77c19
2 changed files with 1587 additions and 1214 deletions
+1383 -1205
View File
File diff suppressed because it is too large Load Diff
+204 -9
View File
@@ -160,6 +160,41 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
"keyframe carry", "keyframe carry",
) )
def test_timing_summary_reports_retry_and_bucket_totals(self):
note = self.module._format_timing_note([
{
"shot": 1,
"total": 12.4,
"retry_elapsed": 1.2,
"attempts": 2,
"sample": 8.0,
"detail_sample": 0.5,
"decode_video": 2.1,
"decode_audio": 0.4,
"cleanup": 0.2,
},
{
"shot": 2,
"total": 7.6,
"retry_elapsed": 0.0,
"attempts": 1,
"sample": 6.5,
"decode_video": 0.5,
"decode_audio": 0.3,
"cleanup": 0.1,
},
])
self.assertIn("timing: 2 shot(s) total 20.0s", note)
self.assertIn("sample 14.5s", note)
self.assertIn("decode video 2.6s", note)
self.assertIn("decode audio 0.7s", note)
self.assertIn("cleanup 0.3s", note)
self.assertIn("retry elapsed 1.2s", note)
self.assertIn("detail 0.5s", note)
self.assertIn("retries 1", note)
self.assertIn("slowest shot 1 12.4s", note)
def test_detail_pass_refines_video_but_preserves_audio(self): def test_detail_pass_refines_video_but_preserves_audio(self):
class FakeTensor: class FakeTensor:
def __init__(self, name): def __init__(self, name):
@@ -191,6 +226,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
original_cleanup = self.module._deep_cleanup original_cleanup = self.module._deep_cleanup
original_nested = getattr(self.module.comfy.nested_tensor, "NestedTensor", None) original_nested = getattr(self.module.comfy.nested_tensor, "NestedTensor", None)
try: try:
sentinel_model = object()
self.module.comfy.nested_tensor.NestedTensor = FakeNestedTensor self.module.comfy.nested_tensor.NestedTensor = FakeNestedTensor
def common_ksampler(*args, **kwargs): def common_ksampler(*args, **kwargs):
@@ -208,7 +244,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
self.module._deep_cleanup = lambda: None self.module._deep_cleanup = lambda: None
result = self.module.H3LongVideos()._render( result = self.module.H3LongVideos()._render(
model=object(), model=sentinel_model,
clip=types.SimpleNamespace( clip=types.SimpleNamespace(
tokenize=lambda text, **kwargs: text, tokenize=lambda text, **kwargs: text,
encode_from_tokens_scheduled=lambda tokens: tokens, encode_from_tokens_scheduled=lambda tokens: tokens,
@@ -254,6 +290,95 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
else: else:
self.module.comfy.nested_tensor.NestedTensor = original_nested self.module.comfy.nested_tensor.NestedTensor = original_nested
def test_render_retries_decode_with_tiling_after_decode_oom(self):
class FakeTensor:
def __init__(self, name):
self.name = name
def detach(self):
return self
def to(self, *args, **kwargs):
return self
class FakeNestedTensor:
def __init__(self, parts):
self._parts = tuple(parts)
self.is_nested = True
def unbind(self):
return self._parts
decode_calls = []
cleanup_calls = []
first_out = {"samples": FakeNestedTensor((FakeTensor("v1"), FakeTensor("a1")))}
original_common_ksampler = self.module.nodes.common_ksampler
original_build = self.module._build_shot_conditioning
original_evict = self.module._evict_all_but
original_decode_video = self.module._decode_video
original_decode_audio = self.module._decode_audio
original_cleanup = self.module._deep_cleanup
original_soft_empty_cache = self.module.mm.soft_empty_cache
original_nested = getattr(self.module.comfy.nested_tensor, "NestedTensor", None)
try:
sentinel_model = object()
self.module.comfy.nested_tensor.NestedTensor = FakeNestedTensor
self.module.nodes.common_ksampler = lambda *args, **kwargs: (first_out,)
self.module._build_shot_conditioning = lambda *_args, **_kwargs: (
"cond",
{"samples": FakeNestedTensor((FakeTensor("basev"), FakeTensor("basea")))},
)
self.module._evict_all_but = lambda *_args, **_kwargs: None
def decode_video(_vae, out_latent, tiled, free_first=None, tile_t=None, tile_xy=None):
decode_calls.append((tiled, free_first, tile_t, tile_xy))
if len(decode_calls) == 1:
raise RuntimeError("CUDA out of memory during decode")
return out_latent
self.module._decode_video = decode_video
self.module._decode_audio = lambda _vae, out_latent: out_latent
self.module.mm.soft_empty_cache = lambda *args, **kwargs: cleanup_calls.append((args, kwargs))
self.module._deep_cleanup = lambda: None
result = self.module.H3LongVideos()._render(
model=sentinel_model,
clip=types.SimpleNamespace(
tokenize=lambda text, **kwargs: text,
encode_from_tokens_scheduled=lambda tokens: tokens,
),
vae=object(),
audio_vae=object(),
negative="negative",
prompt="beat",
w=128,
h=64,
ln=24,
fps=24,
tiled=False,
sa=(123, 20, 1.0, "res_multistep", "simple", 1.0),
handoff=None,
)
self.assertEqual(len(decode_calls), 2)
self.assertEqual(decode_calls[0], (False, sentinel_model, 0, 0))
self.assertEqual(decode_calls[1], (True, None, 16, 256))
self.assertTrue(cleanup_calls)
self.assertIs(result[1], first_out)
finally:
self.module.nodes.common_ksampler = original_common_ksampler
self.module._build_shot_conditioning = original_build
self.module._evict_all_but = original_evict
self.module._decode_video = original_decode_video
self.module._decode_audio = original_decode_audio
self.module._deep_cleanup = original_cleanup
self.module.mm.soft_empty_cache = original_soft_empty_cache
if original_nested is None:
delattr(self.module.comfy.nested_tensor, "NestedTensor")
else:
self.module.comfy.nested_tensor.NestedTensor = original_nested
def test_detail_pass_treats_falsey_strings_as_disabled(self): def test_detail_pass_treats_falsey_strings_as_disabled(self):
calls = [] calls = []
original_common_ksampler = self.module.nodes.common_ksampler original_common_ksampler = self.module.nodes.common_ksampler
@@ -388,24 +513,26 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
) )
self.assertEqual(dropped, [4]) self.assertEqual(dropped, [4])
def test_resolve_prompt_refs_keeps_named_character_images_alongside_tagged_location(self): def test_resolve_shot_references_uses_named_characters_without_picture_tags(self):
refs = [ refs = [
{"kind": "character", "image": "img1", "name": "Mara"}, {"kind": "character", "image": "img1", "name": "Mara"},
{"kind": "character", "image": "img2", "name": "Jon"}, {"kind": "character", "image": "img2", "name": "Jon"},
{"kind": "location", "image": "img3", "name": "Hangar"}, {"kind": "location", "image": "img3", "name": "Hangar"},
] ]
text, references, dropped = self.module.resolve_prompt_refs( text, references, dropped, shot_tag_driven, mode_eff = self.module.resolve_shot_references(
"Mara and Jon argue inside <Picture 3>.", "[Generation 1] Mara crosses the hangar.",
refs, refs,
"auto ref2v",
0,
None,
) )
self.assertEqual(text, "Mara and Jon argue inside <Picture 1>.") self.assertEqual(text, "[Generation 1] Mara crosses the hangar.")
self.assertEqual( self.assertEqual([self.module._reference_image(ref) for ref in references], ["img1"])
[self.module._reference_image(ref) for ref in references],
["img3", "img1", "img2"],
)
self.assertEqual(dropped, []) self.assertEqual(dropped, [])
self.assertFalse(shot_tag_driven)
self.assertEqual(mode_eff, "auto ref2v")
def test_shot_references_uses_all_connected_sparse_slots(self): def test_shot_references_uses_all_connected_sparse_slots(self):
refs = [ refs = [
@@ -432,12 +559,24 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
for index in range(1, 10): for index in range(1, 10):
self.assertIn(f"ref_{index}", optional) self.assertIn(f"ref_{index}", optional)
names = list(optional)
ref_positions = [names.index(f"ref_{index}") for index in range(1, 10)]
self.assertEqual(ref_positions, list(range(ref_positions[0], ref_positions[0] + 9)))
def test_input_types_keep_legacy_ref_image_aliases(self): def test_input_types_keep_legacy_ref_image_aliases(self):
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"] optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
for index in range(1, 10): for index in range(1, 10):
self.assertIn(f"ref_image_{index}", optional) self.assertIn(f"ref_image_{index}", optional)
def test_shot_seconds_tooltip_describes_ceiling_behavior(self):
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
tooltip = optional["shot_seconds"][1]["tooltip"]
self.assertIn("GLOBAL per-shot ceiling", tooltip)
self.assertIn("not 'force every beat to exactly this length'", tooltip)
self.assertIn("hard ~15.1s single-shot limit", tooltip)
def test_run_defaults_match_declared_ref_widget_defaults(self): def test_run_defaults_match_declared_ref_widget_defaults(self):
node = self.module.H3LongVideos() node = self.module.H3LongVideos()
optional = node.INPUT_TYPES()["optional"] optional = node.INPUT_TYPES()["optional"]
@@ -497,6 +636,32 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context) self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
self.assertIn("Character notes for Mara: wears a long grey coat.", context) self.assertIn("Character notes for Mara: wears a long grey coat.", context)
def test_reference_context_matches_location_names_without_picture_tag(self):
refs = [
{"kind": "location", "image": "img2", "name": "Hangar", "aliases": ["loading bay"], "description": "wet concrete floor", "general": "cold industrial lighting"},
]
context = self.module._reference_context_for_text(
"[Generation 1] They argue in the hangar near the loading bay.",
refs,
)
self.assertIn("Location context for Hangar: wet concrete floor.", context)
self.assertIn("Location notes for Hangar: cold industrial lighting.", context)
def test_reference_context_skips_ambiguous_name_matches(self):
refs = [
{"kind": "character", "image": "img1", "name": "Alex", "description": "short dark hair"},
{"kind": "character", "image": "img2", "name": "Alex", "description": "tall blond hair"},
]
context = self.module._reference_context_for_text(
"[Generation 1] Alex enters the room.",
refs,
)
self.assertEqual(context, "")
def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self): def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self):
calls = {} calls = {}
original_parse_resolution = self.module.parse_resolution original_parse_resolution = self.module.parse_resolution
@@ -608,6 +773,36 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
self.assertIn("Persistent appearance for Mara: silver hair.", context) self.assertIn("Persistent appearance for Mara: silver hair.", context)
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context) self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
def test_reference_context_can_skip_character_wardrobe_when_live_memory_is_explicit(self):
refs = [
{"kind": "character", "image": "img1", "name": "Mara", "description": "silver hair", "wardrobe": "red jacket"},
]
context = self.module._reference_context_for_text(
"[Generation 1] Mara walks into the room.",
refs,
include_character_wardrobe=False,
)
self.assertIn("Persistent appearance for Mara: silver hair.", context)
self.assertNotIn("Persistent wardrobe/style for Mara: red jacket.", context)
def test_resolve_tagged_refs_drops_reference_without_image(self):
refs = [
{"kind": "character", "image": None, "name": "Mara"},
{"kind": "character", "image": "img2", "name": "Jon"},
]
rewritten, matched, dropped = self.module.resolve_tagged_refs(
"[Generation 1] <Picture 1> faces <Picture 2>.",
refs,
)
self.assertEqual(rewritten, "[Generation 1] faces <Picture 1>.")
self.assertEqual(dropped, [1])
self.assertEqual(len(matched), 1)
self.assertEqual(matched[0]["name"], "Jon")
def test_reference_context_injects_immediately_after_generation_label(self): def test_reference_context_injects_immediately_after_generation_label(self):
block = ( block = (
"[Generation 1] Classic sitcom lighting and staging. " "[Generation 1] Classic sitcom lighting and staging. "