diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index bc0494c..988eeb3 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -5731,10 +5731,19 @@ class H3LongVideos: "ref_7": ("REFERENCE", {"tooltip": "Reference object for ."}), "ref_8": ("REFERENCE", {"tooltip": "Reference object for ."}), "ref_9": ("REFERENCE", {"tooltip": "Reference object for ."}), + "ref_image_1": ("*", {"tooltip": "Legacy alias for ref_1. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_2": ("*", {"tooltip": "Legacy alias for ref_2. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_3": ("*", {"tooltip": "Legacy alias for ref_3. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_4": ("*", {"tooltip": "Legacy alias for ref_4. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_5": ("*", {"tooltip": "Legacy alias for ref_5. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_6": ("*", {"tooltip": "Legacy alias for ref_6. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_7": ("*", {"tooltip": "Legacy alias for ref_7. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_8": ("*", {"tooltip": "Legacy alias for ref_8. Accepts old IMAGE wiring or a REFERENCE payload."}), + "ref_image_9": ("*", {"tooltip": "Legacy alias for ref_9. Accepts old IMAGE wiring or a REFERENCE payload."}), "plan_only": ("BOOLEAN", {"default": False, "tooltip": "Preview the shot split WITHOUT rendering. Uses THIS node's own settings (no " "second node, no duplicate entry): returns the plan in 'info' and the " - "shots/frames/seconds outputs near-instantly. Turn off to render for real."}), + "shots/frames/seconds outputs near-instantly. Turn off to render for real."}), "fps": ("INT", {"default": 24, "min": 1, "max": 60, "tooltip": "DISPLAY ONLY -- H3 always renders 24 fps. The model's frame grid and its " "audio latent are both defined against 24, so this node computes every " @@ -6245,6 +6254,9 @@ class H3LongVideos: ref_1=None, ref_2=None, ref_3=None, ref_4=None, ref_5=None, ref_6=None, ref_7=None, ref_8=None, ref_9=None, + ref_image_1=None, ref_image_2=None, ref_image_3=None, ref_image_4=None, + ref_image_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None, + ref_image_9=None, ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999, detail_pass=False, detail_sampler_name="euler", detail_scheduler="karras", detail_steps=8, detail_denoise=0.4, @@ -6267,9 +6279,16 @@ class H3LongVideos: f"computed at {H3_FPS}; set your video-save node to {H3_FPS} too") fps = H3_FPS w, h = parse_resolution(resolution) - direct_ref_slots = ( + legacy_ref_slots = ( + ref_image_1, ref_image_2, ref_image_3, ref_image_4, ref_image_5, + ref_image_6, ref_image_7, ref_image_8, ref_image_9, + ) + direct_ref_slots = tuple( + current if current is not None else legacy + for current, legacy in zip(( ref_1, ref_2, ref_3, ref_4, ref_5, ref_6, ref_7, ref_8, ref_9, + ), legacy_ref_slots) ) ref_slots = direct_ref_slots direct_ref_count = len(_connected_refs(direct_ref_slots)) diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index c8a93d8..23245b4 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -369,6 +369,12 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): for index in range(1, 10): self.assertIn(f"ref_{index}", optional) + def test_input_types_keep_legacy_ref_image_aliases(self): + optional = self.module.H3LongVideos.INPUT_TYPES()["optional"] + + for index in range(1, 10): + self.assertIn(f"ref_image_{index}", optional) + def test_node_appends_per_beat_list_outputs_without_reordering_existing_slots(self): self.assertEqual( self.module.H3LongVideos.RETURN_NAMES[-2:], @@ -418,7 +424,104 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): self.assertIn("Persistent appearance for Mara: silver hair.", 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("Location context for Hangar: wet concrete floor.", context) + + def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self): + calls = {} + original_parse_resolution = self.module.parse_resolution + original_connected_refs = self.module._connected_refs + original_reference_character_memory = self.module._reference_character_memory + original_vram_gb = self.module.vram_gb + original_dit_resident_gb = self.module.dit_resident_gb + original_lora_overhead_gb = self.module.lora_overhead_gb + original_resolve_shot_frames = self.module.resolve_shot_frames + original_lora_active = self.module.lora_active + original_sla_pairing = self.module.sla_pairing + original_apply_h3_model_sampling = self.module.apply_h3_model_sampling + original_split_paragraphs = self.module.split_paragraphs + original_expand_beats = self.module.expand_beats + original_anchor_warnings = self.module.anchor_warnings + original_anchor_contributes_nothing = self.module.anchor_contributes_nothing + original_anchor_is_action_beat = self.module.anchor_is_action_beat + original_distribute_generations = self.module.distribute_generations + original_continuity_warnings = self.module.continuity_warnings + original_speech_flags = self.module.speech_flags + original_annotate_script_debug = self.module.annotate_script_debug + original_empty_av_latent = self.module._empty_av_latent + original_torch_zeros = getattr(self.module.torch, "zeros", None) + try: + self.module.torch.zeros = lambda shape: shape + self.module.parse_resolution = lambda _resolution: (640, 360) + self.module._connected_refs = lambda refs: [ref for ref in refs if ref is not None] + self.module._reference_character_memory = lambda refs: (calls.setdefault("refs", tuple(refs)), "")[1] + self.module.vram_gb = lambda: (0, 0) + self.module.dit_resident_gb = lambda _model: 0 + self.module.lora_overhead_gb = lambda _model: 0 + self.module.resolve_shot_frames = lambda *args, **kwargs: (53, "") + self.module.lora_active = lambda _model: False + self.module.sla_pairing = lambda *_args, **_kwargs: ("", False, "") + self.module.apply_h3_model_sampling = lambda model, *_args: (model, "") + self.module.split_paragraphs = lambda _prompt, _sep: ["Anchor.", "Beat."] + self.module.expand_beats = lambda beat_paras, _split: (list(beat_paras), "") + self.module.anchor_warnings = lambda _anchor: [] + self.module.anchor_contributes_nothing = lambda *_args, **_kwargs: False + self.module.anchor_is_action_beat = lambda *_args, **_kwargs: False + self.module.distribute_generations = lambda _anchor, beats, *_args, **_kwargs: list(beats) + self.module.continuity_warnings = lambda _gens: [] + self.module.speech_flags = lambda _beats: [] + self.module.annotate_script_debug = lambda *_args, **_kwargs: "script" + self.module._empty_av_latent = lambda *_args, **_kwargs: ({"samples": "latent"}, 5) + + clip = types.SimpleNamespace( + tokenize=lambda text, **kwargs: text, + encode_from_tokens_scheduled=lambda tokens: tokens, + ) + + result = self.module.H3LongVideos().run( + model=object(), + clip=clip, + vae=object(), + audio_vae=object(), + prompt="Anchor only.", + resolution="16:9", + steps=6, + cfg=1, + sampler_name="res_multistep", + scheduler="simple", + seed=1, + plan_only=True, + ref_image_1={"image": "legacy-1"}, + ref_image_3={"image": "legacy-3"}, + ) + + self.assertEqual(calls["refs"][0]["image"], "legacy-1") + self.assertIsNone(calls["refs"][1]) + self.assertEqual(calls["refs"][2]["image"], "legacy-3") + self.assertEqual(result[2].count("ref2va: 2 reference image(s)"), 1) + finally: + self.module.parse_resolution = original_parse_resolution + self.module._connected_refs = original_connected_refs + self.module._reference_character_memory = original_reference_character_memory + self.module.vram_gb = original_vram_gb + self.module.dit_resident_gb = original_dit_resident_gb + self.module.lora_overhead_gb = original_lora_overhead_gb + self.module.resolve_shot_frames = original_resolve_shot_frames + self.module.lora_active = original_lora_active + self.module.sla_pairing = original_sla_pairing + self.module.apply_h3_model_sampling = original_apply_h3_model_sampling + self.module.split_paragraphs = original_split_paragraphs + self.module.expand_beats = original_expand_beats + self.module.anchor_warnings = original_anchor_warnings + self.module.anchor_contributes_nothing = original_anchor_contributes_nothing + self.module.anchor_is_action_beat = original_anchor_is_action_beat + self.module.distribute_generations = original_distribute_generations + self.module.continuity_warnings = original_continuity_warnings + self.module.speech_flags = original_speech_flags + self.module.annotate_script_debug = original_annotate_script_debug + self.module._empty_av_latent = original_empty_av_latent + if original_torch_zeros is None: + delattr(self.module.torch, "zeros") + else: + self.module.torch.zeros = original_torch_zeros def test_reference_context_matches_tagged_character_without_name_in_text(self): refs = [