Restore long videos release snapshot
This commit is contained in:
+1205
-1383
File diff suppressed because it is too large
Load Diff
@@ -160,41 +160,6 @@ 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):
|
||||||
@@ -226,7 +191,6 @@ 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):
|
||||||
@@ -244,7 +208,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=sentinel_model,
|
model=object(),
|
||||||
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,
|
||||||
@@ -290,95 +254,6 @@ 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
|
||||||
@@ -513,26 +388,24 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(dropped, [4])
|
self.assertEqual(dropped, [4])
|
||||||
|
|
||||||
def test_resolve_shot_references_uses_named_characters_without_picture_tags(self):
|
def test_resolve_prompt_refs_keeps_named_character_images_alongside_tagged_location(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, shot_tag_driven, mode_eff = self.module.resolve_shot_references(
|
text, references, dropped = self.module.resolve_prompt_refs(
|
||||||
"[Generation 1] Mara crosses the hangar.",
|
"Mara and Jon argue inside <Picture 3>.",
|
||||||
refs,
|
refs,
|
||||||
"auto ref2v",
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(text, "[Generation 1] Mara crosses the hangar.")
|
self.assertEqual(text, "Mara and Jon argue inside <Picture 1>.")
|
||||||
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img1"])
|
self.assertEqual(
|
||||||
|
[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 = [
|
||||||
@@ -559,24 +432,12 @@ 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"]
|
||||||
@@ -636,32 +497,6 @@ 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
|
||||||
@@ -773,36 +608,6 @@ 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. "
|
||||||
|
|||||||
Reference in New Issue
Block a user