From af226987d769d2590f77b1d6cd82e4abd28578c5 Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Fri, 28 Aug 2026 09:37:22 +0000 Subject: [PATCH] Harden H3 detail pass conditioning --- dumas_h3_longvideos.py | 35 +++++++++++++++++++--- tests/test_dumas_h3_longvideos.py | 49 +++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index d635a97..8b2a65f 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -4031,6 +4031,21 @@ def _copy_sample_latent(out_latent): return None +def _latent_with_replaced_samples(template_latent, sampled_latent): + """Reuse the original latent payload, but swap in freshly sampled tensors.""" + if not isinstance(template_latent, dict): + return sampled_latent + out = dict(template_latent) + if isinstance(sampled_latent, dict): + for key, value in sampled_latent.items(): + if key != "samples" and key not in out: + out[key] = value + if "samples" in sampled_latent: + out["samples"] = sampled_latent["samples"] + return out + return sampled_latent + + def _video_only_refined_latent(base_latent, refined_latent): """Keep the refined video latent, but preserve the original audio latent.""" base = base_latent.get("samples") if isinstance(base_latent, dict) else None @@ -4047,6 +4062,16 @@ def _video_only_refined_latent(base_latent, refined_latent): except Exception: return refined_latent return refined_latent + + +def _coerce_bool_flag(value): + if isinstance(value, str): + text = value.strip().lower() + if text in ("", "0", "false", "no", "off", "none", "null"): + return False + if text in ("1", "true", "yes", "on"): + return True + return bool(value) # --- ref2va reference conditioning ---------------------------------------- @@ -6148,7 +6173,7 @@ class H3LongVideos: "building a separate graph."}), "detail_sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler", "tooltip": "Sampler used for the optional refinement pass."}), - "detail_scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "karras", + "detail_scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "beta", "tooltip": "Scheduler used for the optional refinement pass."}), "detail_steps": ("INT", {"default": 8, "min": 1, "max": 200, "tooltip": "Steps for the optional refinement pass."}), @@ -6178,7 +6203,7 @@ class H3LongVideos: def _render(self, model, clip, vae, audio_vae, negative, prompt, w, h, ln, fps, tiled, sa, handoff, decode_tile_frames=0, decode_tile_size=0, refs=None, ref_image_size="match", ref_noise_aug=None, silent=False, - detail_pass=False, detail_sampler_name="euler", detail_scheduler="karras", + detail_pass=False, detail_sampler_name="euler", detail_scheduler="beta", detail_steps=8, detail_denoise=0.4): positive, latent = _build_shot_conditioning(clip, vae, prompt, w, h, ln, fps, handoff, ref_images=refs, ref_image_size=ref_image_size, @@ -6200,11 +6225,13 @@ class H3LongVideos: e._h3_stage = "sampling" raise refined_out = out + detail_pass = _coerce_bool_flag(detail_pass) if detail_pass: + detail_latent = _latent_with_replaced_samples(latent, out) try: (refined_out,) = nodes.common_ksampler( model, seed, int(detail_steps), cfg, detail_sampler_name, detail_scheduler, - positive, negative, out, denoise=float(detail_denoise)) + positive, negative, detail_latent, denoise=float(detail_denoise)) except Exception as e: if _is_oom(e): e._h3_stage = "sampling" @@ -6254,7 +6281,7 @@ class H3LongVideos: ref_image_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None, ref_image_9=None, ref_mode="auto ref2v", ref_image_size="match", ref_noise_aug=0.95, - detail_pass=False, detail_sampler_name="euler", detail_scheduler="karras", + detail_pass=False, detail_sampler_name="euler", detail_scheduler="beta", detail_steps=8, detail_denoise=0.4, graph=None, node_id=None): diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index 5cf0a49..eb2fbe5 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -226,15 +226,16 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): handoff=None, detail_pass=True, detail_sampler_name="euler", - detail_scheduler="karras", + detail_scheduler="beta", detail_steps=5, detail_denoise=0.4, ) self.assertEqual(len(calls), 2) - self.assertIs(calls[1][0][8], first_out) + self.assertIsNot(calls[1][0][8], first_out) + self.assertIs(calls[1][0][8]["samples"], first_out["samples"]) self.assertEqual(calls[1][0][4], "euler") - self.assertEqual(calls[1][0][5], "karras") + self.assertEqual(calls[1][0][5], "beta") self.assertAlmostEqual(calls[1][1]["denoise"], 0.4) self.assertEqual(result[1], first_out) self.assertEqual(result[2][0].name, "v2") @@ -253,6 +254,48 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): else: self.module.comfy.nested_tensor.NestedTensor = original_nested + def test_detail_pass_treats_falsey_strings_as_disabled(self): + calls = [] + 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 + try: + self.module.nodes.common_ksampler = lambda *args, **kwargs: (calls.append((args, kwargs)) or {"samples": "latent"},) + self.module._build_shot_conditioning = lambda *_args, **_kwargs: ("cond", {"samples": "base"}) + self.module._evict_all_but = lambda *_args, **_kwargs: None + self.module._decode_video = lambda _vae, out_latent, *_args, **_kwargs: out_latent + self.module._decode_audio = lambda _vae, out_latent: out_latent + self.module._deep_cleanup = lambda: None + + self.module.H3LongVideos()._render( + model=object(), + clip=object(), + 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, + detail_pass="false", + ) + + self.assertEqual(len(calls), 1) + 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 + def test_distribute_generations_canonicalizes_per_shot_audio_and_anchor_directives(self): generations = self.module.distribute_generations( "",