Expose latent upscale sampler controls
This commit is contained in:
@@ -1162,13 +1162,14 @@ What this really means:
|
||||
- the node renders the beat once
|
||||
- the sampled latent is upscaled in latent space to the target size
|
||||
- the conditioning is rebuilt at that target size
|
||||
- the node then runs a hard-coded 2-step refinement pass over the upscaled latent
|
||||
- the node then runs a short refinement pass over the upscaled latent with the sampler, scheduler, step count, denoise, and megapixel target you picked on the latent-upscale params node
|
||||
|
||||
Good starting point:
|
||||
|
||||
- use the `model` mode when you want the strongest latent detail recovery
|
||||
- use the interpolation mode when you want a cheaper resize-only path
|
||||
- keep the refinement denoise low to medium
|
||||
- start with `euler_ancestral`, `simple`, `2` steps, and `0.2` denoise
|
||||
- leave width and height at `0` unless you want an exact override; otherwise `megapixels` drives the target size
|
||||
|
||||
The important part is that this stage is still a latent pass, not a pixel-space resize:
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
- Per-shot directives now support `continuity:`, `ref_mode:`, `ref_noise_aug:`, `anchor_add:`, `soundscape:`, and `music:` in addition to the existing timing and wardrobe directives.
|
||||
|
||||
- `Dumas H3 Latent Upscale Params`
|
||||
- Inputs: `mode`, `model_name`, `method`, `width`, `height`, `device`, `precision`, `refine_denoise`
|
||||
- Inputs: `mode`, `model_name`, `method`, `width`, `height`, `device`, `precision`, `sampler_name`, `scheduler`, `steps`, `denoise`, `megapixels`
|
||||
- Output: `latent_upscale_param`
|
||||
- Bundles the optional latent-space upscaler settings used by `Dumas H3 Long Videos` before decode, so the main node can rebuild conditioning at the target size and run a short refinement pass.
|
||||
- Bundles the optional latent-space upscaler settings used by `Dumas H3 Long Videos` before decode, so the main node can rebuild conditioning at the target size and run a short refinement pass with your chosen sampler, scheduler, step count, and denoise.
|
||||
|
||||
- `Dumas H3 Beat Prompt`
|
||||
- Inputs: authored through the custom front-end beat editor
|
||||
|
||||
+58
-13
@@ -4,6 +4,7 @@ import math
|
||||
import os
|
||||
import re
|
||||
|
||||
import comfy.samplers
|
||||
import folder_paths
|
||||
import torch
|
||||
|
||||
@@ -33,6 +34,8 @@ LATENTS_STD = [
|
||||
]
|
||||
|
||||
_LATENT_UPSCALE_FOLDER = "latent_upscale_models"
|
||||
MP_UNIT = 1024 * 1024
|
||||
RES_MULTIPLE = 32
|
||||
|
||||
|
||||
def _models_dir():
|
||||
@@ -347,10 +350,29 @@ def _compute_upscale_target(width, height, h_in, w_in):
|
||||
return h_out, w_out, eff
|
||||
|
||||
|
||||
def _scale_to_megapixels(w, h, mp, multiple=RES_MULTIPLE):
|
||||
if not mp or mp <= 0 or w <= 0 or h <= 0:
|
||||
return int(h), int(w)
|
||||
multiple = max(1, int(multiple))
|
||||
scale = math.sqrt((float(mp) * MP_UNIT) / float(w * h))
|
||||
nw = max(multiple, int(round(w * scale / multiple)) * multiple)
|
||||
nh = max(multiple, int(round(h * scale / multiple)) * multiple)
|
||||
return nh, nw
|
||||
|
||||
|
||||
def _resolve_target_size(param, h_in, w_in):
|
||||
width = int(param.get("width", 0) or 0)
|
||||
height = int(param.get("height", 0) or 0)
|
||||
megapixels = float(param.get("megapixels", 0.0) or 0.0)
|
||||
if width > 0 and height > 0:
|
||||
return height, width
|
||||
if megapixels > 0:
|
||||
return _scale_to_megapixels(w_in, h_in, megapixels)
|
||||
return int(h_in), int(w_in)
|
||||
|
||||
|
||||
def upscale_video_model(video, param):
|
||||
model_name = param["model_name"]
|
||||
width = int(param["width"])
|
||||
height = int(param["height"])
|
||||
device = param.get("device", "cuda")
|
||||
precision = param.get("precision", "fp16")
|
||||
|
||||
@@ -359,7 +381,8 @@ def upscale_video_model(video, param):
|
||||
compute_dtype = {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}[precision]
|
||||
|
||||
_, c, t, h_in, w_in = video.shape
|
||||
h_out, w_out, eff = _compute_upscale_target(width, height, h_in, w_in)
|
||||
h_out, w_out = _resolve_target_size(param, h_in, w_in)
|
||||
eff = (w_out / float(w_in) + h_out / float(h_in)) / 2.0 if w_in and h_in else 1.0
|
||||
|
||||
if eff < 1.0 and (w_out < w_in or h_out < h_in):
|
||||
raise ValueError("This model only supports upscaling (effective scale >= 1.0).")
|
||||
@@ -386,10 +409,8 @@ def upscale_video_model(video, param):
|
||||
|
||||
def upscale_video_interp(video, param):
|
||||
method = str(param.get("method") or "bilinear")
|
||||
width = int(param.get("width", 0) or 0)
|
||||
height = int(param.get("height", 0) or 0)
|
||||
_, c, t, h_in, w_in = video.shape
|
||||
h_out, w_out, _ = _compute_upscale_target(width, height, h_in, w_in)
|
||||
h_out, w_out = _resolve_target_size(param, h_in, w_in)
|
||||
if h_out == h_in and w_out == w_in:
|
||||
return video, h_in, w_in
|
||||
video_bt = video.permute(0, 2, 1, 3, 4).reshape(-1, c, h_in, w_in)
|
||||
@@ -425,23 +446,42 @@ class H3LatentUpscaleParams:
|
||||
"method": (["nearest-exact", "bilinear", "area", "bicubic"], {"default": "bilinear",
|
||||
"tooltip": "Interpolation method used when mode = interp."}),
|
||||
"width": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 32,
|
||||
"tooltip": "Target upscaled pixel width for the latent refinement stage. 0 keeps the original width and effectively disables the resize."}),
|
||||
"tooltip": "Explicit target width for the latent refinement stage. Leave at 0 to let megapixels choose the size instead."}),
|
||||
"height": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 32,
|
||||
"tooltip": "Target upscaled pixel height for the latent refinement stage. 0 keeps the original height and effectively disables the resize."}),
|
||||
"tooltip": "Explicit target height for the latent refinement stage. Leave at 0 to let megapixels choose the size instead."}),
|
||||
"device": (["cuda", "cpu"], {"default": "cuda",
|
||||
"tooltip": "Device used by the H3 latent upscaler model when mode = model."}),
|
||||
"precision": (["fp16", "fp32", "bf16"], {"default": "fp16",
|
||||
"tooltip": "Computation precision used by the H3 latent upscaler model when mode = model."}),
|
||||
"refine_denoise": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "How much the 2-step refinement pass may rewrite the upscaled latent. Lower = safer, higher = freer."}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral",
|
||||
"tooltip": "Sampler used for the latent refinement pass. Default matches the current H3 preference."}),
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple",
|
||||
"tooltip": "Scheduler used for the latent refinement pass."}),
|
||||
"steps": ("INT", {"default": 2, "min": 1, "max": 50, "step": 1,
|
||||
"tooltip": "Number of refinement steps applied after the latent upscaler stage."}),
|
||||
"denoise": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "How much the refinement pass may rewrite the upscaled latent. Lower = safer, higher = freer."}),
|
||||
"megapixels": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 4.0, "step": 0.01,
|
||||
"tooltip": "Primary target size for the latent refinement stage. If width and height are both set, they win; otherwise the node scales the current shot to this pixel budget while preserving aspect ratio. 0 keeps the incoming latent size."}),
|
||||
}
|
||||
}
|
||||
|
||||
def build(self, mode, model_name, method, width, height, device, precision, refine_denoise):
|
||||
def build(self, mode, model_name, method, width, height, device, precision, sampler_name, scheduler, steps, denoise, megapixels):
|
||||
width = int(width)
|
||||
height = int(height)
|
||||
steps = int(steps)
|
||||
if mode == "off":
|
||||
return ({"mode": "off", "width": width, "height": height, "refine_denoise": float(refine_denoise)},)
|
||||
return ({
|
||||
"mode": "off",
|
||||
"width": width,
|
||||
"height": height,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"steps": steps,
|
||||
"denoise": float(denoise),
|
||||
"refine_denoise": float(denoise),
|
||||
"megapixels": float(megapixels),
|
||||
},)
|
||||
if width > 0:
|
||||
width = int(round(width / 32.0)) * 32
|
||||
if height > 0:
|
||||
@@ -454,7 +494,12 @@ class H3LatentUpscaleParams:
|
||||
"height": height,
|
||||
"device": device,
|
||||
"precision": precision,
|
||||
"refine_denoise": float(refine_denoise),
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"steps": steps,
|
||||
"denoise": float(denoise),
|
||||
"refine_denoise": float(denoise),
|
||||
"megapixels": float(megapixels),
|
||||
},)
|
||||
|
||||
|
||||
|
||||
+30
-13
@@ -33,9 +33,9 @@ and your VRAM, chains them, and returns the finished video + audio.
|
||||
Requirements: H3 is CFG-free (cfg 1) and needs no negative prompt -- the node
|
||||
makes an empty one internally. The main pass keeps denoise fixed at 1.0: a
|
||||
partial denoise desyncs the joint audio/video schedule. An optional latent
|
||||
upscale stage can rebuild the conditioning at a larger target size, run a short
|
||||
refinement pass, and keep the output video-only before the final pixel-space
|
||||
upscale options.
|
||||
upscale stage can rebuild the conditioning at a target size, run a short
|
||||
refinement pass with its own sampler controls, and keep the output video-only
|
||||
before the final pixel-space upscale options.
|
||||
|
||||
Verified against ComfyUI core (comfy_extras/nodes_minimax_h3.py, model_base.py,
|
||||
ldm/minimax/model.py, text_encoders/minimax.py, sd.py).
|
||||
@@ -4073,6 +4073,17 @@ def _video_only_refined_latent(base_latent, refined_latent):
|
||||
return refined_latent
|
||||
|
||||
|
||||
def _latent_upscale_target_size(base_w, base_h, param):
|
||||
width = int(param.get("width", 0) or 0)
|
||||
height = int(param.get("height", 0) or 0)
|
||||
if width > 0 and height > 0:
|
||||
return width, height
|
||||
megapixels = float(param.get("megapixels", 0.0) or 0.0)
|
||||
if megapixels > 0:
|
||||
return scale_to_megapixels(base_w, base_h, megapixels)
|
||||
return int(base_w), int(base_h)
|
||||
|
||||
|
||||
def _nested_tensor_parts(samples):
|
||||
if samples is None:
|
||||
return ()
|
||||
@@ -6107,8 +6118,9 @@ class H3LongVideos:
|
||||
"Try 256 on a tight card at 1344x768."}),
|
||||
"latent_upscale_param": ("DUMAS_H3_LATENT_UPSCALE_PARAM", {
|
||||
"tooltip": "Output of 'Dumas H3 Latent Upscale Params'. When connected, the first-pass "
|
||||
"latent is upscaled and run through a hard-coded 2-step refinement pass before "
|
||||
"decode. Leave unconnected to skip latent upscaling entirely."}),
|
||||
"latent is upscaled and run through a short refinement pass before decode, "
|
||||
"using the sampler, scheduler, steps, denoise, and megapixel target from that node. "
|
||||
"Leave unconnected to skip latent upscaling entirely."}),
|
||||
"upscale": (["off", "rtx", "model", "lanczos"], {"default": "off",
|
||||
"tooltip": "Optional post-pass on the finished frames. 'rtx' = NVIDIA RTX Video Super "
|
||||
"Resolution (Tensor Cores -- fastest and best for video; needs the "
|
||||
@@ -6440,8 +6452,6 @@ class H3LongVideos:
|
||||
if (
|
||||
isinstance(latent_upscale_param, dict)
|
||||
and str(latent_upscale_param.get("mode", "off")) != "off"
|
||||
and int(latent_upscale_param.get("width", 0) or 0) > 0
|
||||
and int(latent_upscale_param.get("height", 0) or 0) > 0
|
||||
):
|
||||
try:
|
||||
latent_start = time.perf_counter()
|
||||
@@ -6463,9 +6473,14 @@ class H3LongVideos:
|
||||
ref_noise_aug=ref_noise_aug, audio_vae=audio_vae, silent=silent)
|
||||
upscale_latent["samples"] = comfy.nested_tensor.NestedTensor(
|
||||
(upscaled_video, parts[1]))
|
||||
refine_steps = int(latent_upscale_param.get("steps", 2) or 2)
|
||||
refine_sampler = latent_upscale_param.get("sampler_name", sn)
|
||||
refine_scheduler = latent_upscale_param.get("scheduler", sch)
|
||||
refine_denoise_value = latent_upscale_param.get("denoise", latent_upscale_param.get("refine_denoise", 0.2))
|
||||
refine_denoise = 0.2 if refine_denoise_value is None else float(refine_denoise_value)
|
||||
(refined_out,) = nodes.common_ksampler(
|
||||
model, seed, 2, cfg, sn, sch, upscale_cond, negative, upscale_latent,
|
||||
denoise=float(latent_upscale_param.get("refine_denoise", 0.25) or 0.25))
|
||||
model, seed, refine_steps, cfg, refine_sampler, refine_scheduler, upscale_cond, negative, upscale_latent,
|
||||
denoise=refine_denoise)
|
||||
timing["latent_upscale_sample"] += time.perf_counter() - latent_start
|
||||
refined_out = _video_only_refined_latent(out, refined_out)
|
||||
except Exception as e:
|
||||
@@ -6591,18 +6606,20 @@ class H3LongVideos:
|
||||
model, ms_note = apply_h3_model_sampling(model, shift_video, shift_audio)
|
||||
latent_upscale_note = ""
|
||||
if isinstance(latent_upscale_param, dict) and str(latent_upscale_param.get("mode", "off")) != "off":
|
||||
target_w = int(latent_upscale_param.get("width", 0) or 0)
|
||||
target_h = int(latent_upscale_param.get("height", 0) or 0)
|
||||
if target_w > 0 and target_h > 0:
|
||||
target_w, target_h = _latent_upscale_target_size(w, h, latent_upscale_param)
|
||||
mode = str(latent_upscale_param.get("mode", "off"))
|
||||
detail = f" via {mode}"
|
||||
if mode == "model":
|
||||
detail += f"/{latent_upscale_param.get('model_name', 'none')}"
|
||||
else:
|
||||
detail += f"/{latent_upscale_param.get('method', 'bilinear')}"
|
||||
denoise_value = latent_upscale_param.get("denoise", latent_upscale_param.get("refine_denoise", 0.2))
|
||||
denoise = 0.2 if denoise_value is None else float(denoise_value)
|
||||
latent_upscale_note = (
|
||||
f" latent upscale: target {target_w}x{target_h}px{detail}; "
|
||||
f"2-step refinement denoise {float(latent_upscale_param.get('refine_denoise', 0.25) or 0.25):.2f}"
|
||||
f"{int(latent_upscale_param.get('steps', 2) or 2)}-step refinement "
|
||||
f"{latent_upscale_param.get('sampler_name', sn)}/{latent_upscale_param.get('scheduler', sch)} "
|
||||
f"denoise {denoise:.2f}"
|
||||
)
|
||||
|
||||
paras = split_paragraphs(prompt, "##")
|
||||
|
||||
@@ -306,18 +306,21 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
||||
latent_upscale_param={
|
||||
"mode": "model",
|
||||
"model_name": "upscale.safetensors",
|
||||
"width": 256,
|
||||
"height": 128,
|
||||
"device": "cpu",
|
||||
"precision": "fp16",
|
||||
"refine_denoise": 0.4,
|
||||
"sampler_name": "euler_ancestral",
|
||||
"scheduler": "simple",
|
||||
"steps": 2,
|
||||
"denoise": 0.4,
|
||||
"megapixels": 1.5,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(len(calls), 2)
|
||||
self.assertIsNot(calls[1][0][8], first_out)
|
||||
self.assertEqual(calls[1][0][8]["samples"].unbind()[0].name, "upv")
|
||||
self.assertEqual(calls[1][0][4], "res_multistep")
|
||||
self.assertEqual(calls[1][0][2], 2)
|
||||
self.assertEqual(calls[1][0][4], "euler_ancestral")
|
||||
self.assertEqual(calls[1][0][5], "simple")
|
||||
self.assertAlmostEqual(calls[1][1]["denoise"], 0.4)
|
||||
self.assertEqual(result[1], first_out)
|
||||
@@ -424,11 +427,13 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
||||
latent_upscale_param={
|
||||
"mode": "model",
|
||||
"model_name": "upscale.safetensors",
|
||||
"width": 256,
|
||||
"height": 128,
|
||||
"device": "cpu",
|
||||
"precision": "fp16",
|
||||
"refine_denoise": 0.4,
|
||||
"sampler_name": "euler_ancestral",
|
||||
"scheduler": "simple",
|
||||
"steps": 2,
|
||||
"denoise": 0.4,
|
||||
"megapixels": 1.5,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1096,6 +1101,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
||||
|
||||
def test_latent_upscale_params_node_is_exposed(self):
|
||||
latent = importlib.import_module("dumas_h3_latent_upscale")
|
||||
required = latent.H3LatentUpscaleParams.INPUT_TYPES()["required"]
|
||||
|
||||
self.assertEqual(
|
||||
latent.NODE_CLASS_MAPPINGS,
|
||||
@@ -1105,6 +1111,11 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
||||
latent.NODE_DISPLAY_NAME_MAPPINGS,
|
||||
{"DumasH3LatentUpscaleParams": "Dumas H3 Latent Upscale Params"},
|
||||
)
|
||||
self.assertEqual(required["sampler_name"][1]["default"], "euler_ancestral")
|
||||
self.assertEqual(required["scheduler"][1]["default"], "simple")
|
||||
self.assertEqual(required["steps"][1]["default"], 2)
|
||||
self.assertEqual(required["denoise"][1]["default"], 0.2)
|
||||
self.assertEqual(required["megapixels"][1]["default"], 1.0)
|
||||
|
||||
def test_compose_persistent_does_not_expand_ambiguous_plural_to_full_cast(self):
|
||||
active = self.module.parse_wardrobe(
|
||||
|
||||
Reference in New Issue
Block a user