Add H3 latent upscale refinement stage
This commit is contained in:
+194
-163
@@ -30,11 +30,12 @@ video; each later paragraph = a scene beat), a shot length, and a resolution fro
|
||||
the VRAM-appropriate list. It splits the beats into shots that fit H3's ceiling
|
||||
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 refinement
|
||||
pass can use its own denoise later, before any upscale, while keeping the output
|
||||
video-only.
|
||||
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.
|
||||
|
||||
Verified against ComfyUI core (comfy_extras/nodes_minimax_h3.py, model_base.py,
|
||||
ldm/minimax/model.py, text_encoders/minimax.py, sd.py).
|
||||
@@ -57,28 +58,36 @@ import comfy.nested_tensor
|
||||
import comfy.model_management as mm
|
||||
import node_helpers
|
||||
|
||||
try:
|
||||
from . import dumas_h3_overlay as _overlay
|
||||
from . import dumas_image_nodes as _image_nodes
|
||||
except ImportError: # loaded as a bare file (test_prompt_logic.py), not as a package
|
||||
import importlib.util as _ilu
|
||||
import os as _os
|
||||
import sys as _sys
|
||||
_spec = _ilu.spec_from_file_location(
|
||||
"dumas_h3_overlay",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_overlay.py"),
|
||||
)
|
||||
_overlay = _ilu.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_overlay)
|
||||
_image_nodes = _sys.modules.get("dumas_image_nodes")
|
||||
if _image_nodes is None:
|
||||
_img_spec = _ilu.spec_from_file_location(
|
||||
"dumas_image_nodes",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_image_nodes.py"),
|
||||
)
|
||||
_image_nodes = _ilu.module_from_spec(_img_spec)
|
||||
_sys.modules["dumas_image_nodes"] = _image_nodes
|
||||
_img_spec.loader.exec_module(_image_nodes)
|
||||
try:
|
||||
from . import dumas_h3_overlay as _overlay
|
||||
from .dumas_h3_latent_upscale import upscale_latent_video as _upscale_latent_video
|
||||
from . import dumas_image_nodes as _image_nodes
|
||||
except ImportError: # loaded as a bare file (test_prompt_logic.py), not as a package
|
||||
import importlib.util as _ilu
|
||||
import os as _os
|
||||
import sys as _sys
|
||||
_spec = _ilu.spec_from_file_location(
|
||||
"dumas_h3_overlay",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_overlay.py"),
|
||||
)
|
||||
_overlay = _ilu.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_overlay)
|
||||
_latent_spec = _ilu.spec_from_file_location(
|
||||
"dumas_h3_latent_upscale",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_latent_upscale.py"),
|
||||
)
|
||||
_latent_upscale = _ilu.module_from_spec(_latent_spec)
|
||||
_latent_spec.loader.exec_module(_latent_upscale)
|
||||
_upscale_latent_video = _latent_upscale.upscale_latent_video
|
||||
_image_nodes = _sys.modules.get("dumas_image_nodes")
|
||||
if _image_nodes is None:
|
||||
_img_spec = _ilu.spec_from_file_location(
|
||||
"dumas_image_nodes",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_image_nodes.py"),
|
||||
)
|
||||
_image_nodes = _ilu.module_from_spec(_img_spec)
|
||||
_sys.modules["dumas_image_nodes"] = _image_nodes
|
||||
_img_spec.loader.exec_module(_image_nodes)
|
||||
|
||||
AUDIO_LATENT_FPS = 40
|
||||
GB = 1024 ** 3
|
||||
@@ -272,16 +281,14 @@ def split_paragraphs(text, delimiter):
|
||||
# that carry no widget value can stay grouped in INPUT_TYPES without being listed.
|
||||
ADDED_WIDGETS = (
|
||||
"beat_split",
|
||||
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity",
|
||||
"watermark_margin", "intro_text", "intro_position", "intro_seconds",
|
||||
"intro_fade", "intro_size", "overlay_font", "overlay_stroke",
|
||||
"ref_mode", "ref_image_size", "ref_noise_aug", "auto_props", "prevent_nudity",
|
||||
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||
"motion_guard", "contact_guard",
|
||||
"auto_soundscape", "allow_nonspeech_vocals",
|
||||
"detail_pass", "detail_sampler_name", "detail_scheduler",
|
||||
"detail_steps", "detail_denoise",
|
||||
)
|
||||
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity",
|
||||
"watermark_margin", "intro_text", "intro_position", "intro_seconds",
|
||||
"intro_fade", "intro_size", "overlay_font", "overlay_stroke",
|
||||
"ref_mode", "ref_image_size", "ref_noise_aug", "auto_props", "prevent_nudity",
|
||||
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||
"motion_guard", "contact_guard",
|
||||
"auto_soundscape", "allow_nonspeech_vocals",
|
||||
)
|
||||
|
||||
NL = "\n"
|
||||
# Lines that CONFIGURE a beat rather than being one. They attach to the beat that
|
||||
@@ -4048,10 +4055,10 @@ def _latent_with_replaced_samples(template_latent, sampled_latent):
|
||||
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
|
||||
refined = refined_latent.get("samples") if isinstance(refined_latent, dict) else None
|
||||
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
|
||||
refined = refined_latent.get("samples") if isinstance(refined_latent, dict) else None
|
||||
if base is None or refined is None:
|
||||
return refined_latent
|
||||
if not getattr(base, "is_nested", False) or not getattr(refined, "is_nested", False):
|
||||
@@ -4062,8 +4069,22 @@ def _video_only_refined_latent(base_latent, refined_latent):
|
||||
if len(base_parts) >= 2 and len(refined_parts) >= 1:
|
||||
return {"samples": comfy.nested_tensor.NestedTensor((refined_parts[0], base_parts[-1]))}
|
||||
except Exception:
|
||||
return refined_latent
|
||||
return refined_latent
|
||||
return refined_latent
|
||||
return refined_latent
|
||||
|
||||
|
||||
def _nested_tensor_parts(samples):
|
||||
if samples is None:
|
||||
return ()
|
||||
parts = getattr(samples, "tensors", None)
|
||||
if parts is not None:
|
||||
return tuple(parts)
|
||||
if hasattr(samples, "unbind"):
|
||||
try:
|
||||
return tuple(samples.unbind())
|
||||
except Exception:
|
||||
return ()
|
||||
return ()
|
||||
|
||||
|
||||
def _coerce_bool_flag(value):
|
||||
@@ -4093,25 +4114,25 @@ def _format_elapsed_seconds(seconds):
|
||||
def _format_timing_note(shot_timings):
|
||||
if not shot_timings:
|
||||
return ""
|
||||
totals = {
|
||||
"total": 0.0,
|
||||
"retry_elapsed": 0.0,
|
||||
"sample": 0.0,
|
||||
"detail_sample": 0.0,
|
||||
"decode_video": 0.0,
|
||||
"decode_audio": 0.0,
|
||||
"cleanup": 0.0,
|
||||
totals = {
|
||||
"total": 0.0,
|
||||
"retry_elapsed": 0.0,
|
||||
"sample": 0.0,
|
||||
"latent_upscale_sample": 0.0,
|
||||
"decode_video": 0.0,
|
||||
"decode_audio": 0.0,
|
||||
"cleanup": 0.0,
|
||||
"retries": 0,
|
||||
}
|
||||
slowest = None
|
||||
for shot in shot_timings:
|
||||
totals["total"] += float(shot.get("total", 0.0) or 0.0)
|
||||
totals["retry_elapsed"] += float(shot.get("retry_elapsed", 0.0) or 0.0)
|
||||
totals["sample"] += float(shot.get("sample", 0.0) or 0.0)
|
||||
totals["detail_sample"] += float(shot.get("detail_sample", 0.0) or 0.0)
|
||||
totals["decode_video"] += float(shot.get("decode_video", 0.0) or 0.0)
|
||||
totals["decode_audio"] += float(shot.get("decode_audio", 0.0) or 0.0)
|
||||
totals["cleanup"] += float(shot.get("cleanup", 0.0) or 0.0)
|
||||
totals["retry_elapsed"] += float(shot.get("retry_elapsed", 0.0) or 0.0)
|
||||
totals["sample"] += float(shot.get("sample", 0.0) or 0.0)
|
||||
totals["latent_upscale_sample"] += float(shot.get("latent_upscale_sample", 0.0) or 0.0)
|
||||
totals["decode_video"] += float(shot.get("decode_video", 0.0) or 0.0)
|
||||
totals["decode_audio"] += float(shot.get("decode_audio", 0.0) or 0.0)
|
||||
totals["cleanup"] += float(shot.get("cleanup", 0.0) or 0.0)
|
||||
totals["retries"] += max(0, int(shot.get("attempts", 1) or 1) - 1)
|
||||
if slowest is None or float(shot.get("total", 0.0) or 0.0) > float(slowest.get("total", 0.0) or 0.0):
|
||||
slowest = shot
|
||||
@@ -4124,8 +4145,8 @@ def _format_timing_note(shot_timings):
|
||||
]
|
||||
if totals["retry_elapsed"]:
|
||||
pieces.append(f"retry elapsed {_format_elapsed_seconds(totals['retry_elapsed'])}")
|
||||
if totals["detail_sample"]:
|
||||
pieces.append(f"detail {_format_elapsed_seconds(totals['detail_sample'])}")
|
||||
if totals["latent_upscale_sample"]:
|
||||
pieces.append(f"latent upscale {_format_elapsed_seconds(totals['latent_upscale_sample'])}")
|
||||
if totals["retries"]:
|
||||
pieces.append(f"retries {totals['retries']}")
|
||||
if slowest is not None:
|
||||
@@ -6075,18 +6096,22 @@ class H3LongVideos:
|
||||
"doesn't cut to digital silence with a click. The silenced shots keep NO "
|
||||
"original audio at all -- fading the muted shot itself would leave this many "
|
||||
"ms of the gibberish audible at each end of every muted shot."}),
|
||||
"decode_tile_frames": ("INT", {"default": 0, "min": 0, "max": 128, "step": 1,
|
||||
"tooltip": "Temporal tiling for the VAE decode (tile_t). 0 = ComfyUI default, which "
|
||||
"expands the WHOLE clip at once -- the single largest allocation in a run, "
|
||||
"and the usual point where a big checkpoint tips into shared memory. Try 8-16 "
|
||||
"if you spill during decode rather than sampling. Lower = less peak VRAM, "
|
||||
"slightly slower."}),
|
||||
"decode_tile_size": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 32,
|
||||
"tooltip": "Spatial tile size for the VAE decode (tile_x/tile_y). 0 = ComfyUI default. "
|
||||
"Try 256 on a tight card at 1344x768."}),
|
||||
"decode_tile_frames": ("INT", {"default": 0, "min": 0, "max": 128, "step": 1,
|
||||
"tooltip": "Temporal tiling for the VAE decode (tile_t). 0 = ComfyUI default, which "
|
||||
"expands the WHOLE clip at once -- the single largest allocation in a run, "
|
||||
"and the usual point where a big checkpoint tips into shared memory. Try 8-16 "
|
||||
"if you spill during decode rather than sampling. Lower = less peak VRAM, "
|
||||
"slightly slower."}),
|
||||
"decode_tile_size": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 32,
|
||||
"tooltip": "Spatial tile size for the VAE decode (tile_x/tile_y). 0 = ComfyUI default. "
|
||||
"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."}),
|
||||
"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 "
|
||||
"tooltip": "Optional post-pass on the finished frames. 'rtx' = NVIDIA RTX Video Super "
|
||||
"Resolution (Tensor Cores -- fastest and best for video; needs the "
|
||||
"Nvidia_RTX_Nodes_ComfyUI pack, falls back automatically if absent). 'model' = "
|
||||
"a Real-ESRGAN/UltraSharp upscale model from upscale_model. 'lanczos' = plain "
|
||||
"resize. All of these ENHANCE/ENLARGE; for true detail reconstruction from a "
|
||||
@@ -6363,27 +6388,7 @@ class H3LongVideos:
|
||||
"the jacket, 'wardrobe: += sunglasses' adds one. TWO+ PEOPLE: name them -- "
|
||||
"'Maya = grey shorts, red jacket; Jon = navy overalls', then edit one at a "
|
||||
"time: 'wardrobe: Maya -= jacket' leaves Jon untouched."}),
|
||||
"detail_pass": ("BOOLEAN", {"default": False,
|
||||
"tooltip": "Run a second refinement sampler on each beat BEFORE any upscale. "
|
||||
"It reuses the same conditioning and keeps the output video-only by "
|
||||
"preserving the first pass's audio latent. Use it for detail cleanup, not "
|
||||
"for huge rewrites: too many steps or too much denoise can pull identity or "
|
||||
"continuity away from the main pass."}),
|
||||
"detail_sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler",
|
||||
"tooltip": "Sampler for the optional refinement pass. Euler is the maintained default "
|
||||
"direction for this lane."}),
|
||||
"detail_scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "beta",
|
||||
"tooltip": "Scheduler for the optional refinement pass. Beta is the maintained default "
|
||||
"direction for the H3 enhancement lane."}),
|
||||
"detail_steps": ("INT", {"default": 8, "min": 1, "max": 200,
|
||||
"tooltip": "Extra steps for the refinement pass only. Start around 4-8. More is not "
|
||||
"automatically better; once the pass starts rewriting instead of polishing, "
|
||||
"identity and continuity can drift."}),
|
||||
"detail_denoise": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "How hard the refinement pass is allowed to rewrite the beat latent. Start "
|
||||
"around 0.20-0.35 for gentle cleanup; 0.4+ is stronger and can noticeably "
|
||||
"change faces, motion or composition."}),
|
||||
},
|
||||
},
|
||||
# Read-only graph access, for SLA-LoRA detection: a LoRA's filename is
|
||||
# the only thing that identifies an SLA build, and the graph is the only
|
||||
# place it survives. Named 'graph'/'node_id' rather than the usual
|
||||
@@ -6404,15 +6409,14 @@ class H3LongVideos:
|
||||
opt[name] = opt.pop(name) # re-insert at the end, value unchanged
|
||||
return schema
|
||||
|
||||
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="beta",
|
||||
detail_steps=8, detail_denoise=0.4, timing_sink=None):
|
||||
timing = {"sample": 0.0, "detail_sample": 0.0, "decode_video": 0.0, "decode_audio": 0.0, "cleanup": 0.0}
|
||||
positive, latent = _build_shot_conditioning(clip, vae, prompt, w, h, ln, fps, handoff,
|
||||
ref_images=refs, ref_image_size=ref_image_size,
|
||||
ref_noise_aug=ref_noise_aug,
|
||||
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,
|
||||
latent_upscale_param=None, timing_sink=None):
|
||||
timing = {"sample": 0.0, "latent_upscale_sample": 0.0, "decode_video": 0.0, "decode_audio": 0.0, "cleanup": 0.0}
|
||||
positive, latent = _build_shot_conditioning(clip, vae, prompt, w, h, ln, fps, handoff,
|
||||
ref_images=refs, ref_image_size=ref_image_size,
|
||||
ref_noise_aug=ref_noise_aug,
|
||||
audio_vae=audio_vae, silent=silent)
|
||||
seed, steps, cfg, sn, sch, denoise = sa
|
||||
# Conditioning is built, so the text encoder and VAEs are dead weight for the
|
||||
@@ -6423,30 +6427,51 @@ class H3LongVideos:
|
||||
(out,) = nodes.common_ksampler(model, seed, steps, cfg, sn, sch, positive, negative,
|
||||
latent, denoise=denoise)
|
||||
timing["sample"] += time.perf_counter() - sample_start
|
||||
except Exception as e:
|
||||
# Mark WHERE this failed. `tiled` only affects the DECODE, so the caller's
|
||||
# OOM retry cannot help an OOM raised here -- it just re-runs the whole
|
||||
# sampling pass and fails the same way, which on a 362-frame shot is four
|
||||
# more minutes for nothing.
|
||||
if _is_oom(e):
|
||||
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)
|
||||
except Exception as e:
|
||||
# Mark WHERE this failed. `tiled` only affects the DECODE, so the caller's
|
||||
# OOM retry cannot help an OOM raised here -- it just re-runs the whole
|
||||
# sampling pass and fails the same way, which on a 362-frame shot is four
|
||||
# more minutes for nothing.
|
||||
if _is_oom(e):
|
||||
e._h3_stage = "sampling"
|
||||
raise
|
||||
refined_out = out
|
||||
latent_upscale_param = latent_upscale_param or None
|
||||
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:
|
||||
detail_start = time.perf_counter()
|
||||
latent_start = time.perf_counter()
|
||||
out_samples = out["samples"]
|
||||
parts = _nested_tensor_parts(out_samples)
|
||||
if not getattr(out_samples, "is_nested", False) or len(parts) < 2:
|
||||
raise RuntimeError("latent upscale expects a nested AV latent")
|
||||
upscaled_video, up_h, up_w = _upscale_latent_video(parts[0], latent_upscale_param)
|
||||
target_w = int(up_w) * 16
|
||||
target_h = int(up_h) * 16
|
||||
if target_w <= 0 or target_h <= 0:
|
||||
raise RuntimeError("latent upscale target size must be positive")
|
||||
if latent_upscale_param.get("mode") == "model" and str(latent_upscale_param.get("device", "cuda")) == "cuda" and hasattr(model, "clone_base_uuid"):
|
||||
mm.unload_model_and_clones(model, unload_additional_models=False)
|
||||
mm.soft_empty_cache()
|
||||
upscale_cond, upscale_latent = _build_shot_conditioning(
|
||||
clip, vae, prompt, target_w, target_h, ln, fps, handoff,
|
||||
ref_images=refs, ref_image_size=ref_image_size,
|
||||
ref_noise_aug=ref_noise_aug, audio_vae=audio_vae, silent=silent)
|
||||
upscale_latent["samples"] = comfy.nested_tensor.NestedTensor(
|
||||
(upscaled_video, parts[1]))
|
||||
(refined_out,) = nodes.common_ksampler(
|
||||
model, seed, int(detail_steps), cfg, detail_sampler_name, detail_scheduler,
|
||||
positive, negative, detail_latent, denoise=float(detail_denoise))
|
||||
timing["detail_sample"] += time.perf_counter() - detail_start
|
||||
model, seed, 2, cfg, sn, sch, upscale_cond, negative, upscale_latent,
|
||||
denoise=float(latent_upscale_param.get("refine_denoise", 0.25) or 0.25))
|
||||
timing["latent_upscale_sample"] += time.perf_counter() - latent_start
|
||||
refined_out = _video_only_refined_latent(out, refined_out)
|
||||
except Exception as e:
|
||||
if _is_oom(e):
|
||||
e._h3_stage = "sampling"
|
||||
raise
|
||||
refined_out = _video_only_refined_latent(out, refined_out)
|
||||
del detail_latent
|
||||
# Keep a CPU copy of the sampled latent BEFORE decoding, for the `latent`
|
||||
# output. Latents are ~1000x smaller than the frames they decode to (a
|
||||
# 1344x768 124f shot is ~1.5MB against ~1.5GB), so carrying one per shot for
|
||||
@@ -6458,8 +6483,8 @@ class H3LongVideos:
|
||||
audio = _decode_audio(audio_vae, out)
|
||||
timing["decode_audio"] += time.perf_counter() - decode_audio_start
|
||||
# Audio is much smaller than the video decode. Drop the first-pass
|
||||
# conditioning before the VAE work so the optional detail pass does not
|
||||
# keep both sampled latents resident across the heaviest allocation.
|
||||
# conditioning before the VAE work so the optional latent upscale pass does
|
||||
# not keep both sampled latents resident across the heaviest allocation.
|
||||
del out, positive, latent
|
||||
video = _decode_video(vae, refined_out, tiled, free_first=model,
|
||||
tile_t=decode_tile_frames, tile_xy=decode_tile_size)
|
||||
@@ -6489,19 +6514,18 @@ class H3LongVideos:
|
||||
auto_soundscape="fill if blank",
|
||||
auto_silence_nonspeech=True, allow_nonspeech_vocals=False,
|
||||
subject_count_guard="auto",
|
||||
upscale="off", upscale_model="none",
|
||||
upscale_target_short_edge=0, upscale_batch=4,
|
||||
mute_nonspeech_audio=True, mute_fade_ms=40,
|
||||
watermark_text="", watermark_position="bottom-right", watermark_size=4.0,
|
||||
watermark_opacity=0.75, watermark_margin=3.0,
|
||||
upscale="off", upscale_model="none",
|
||||
upscale_target_short_edge=0, upscale_batch=4,
|
||||
mute_nonspeech_audio=True, mute_fade_ms=40,
|
||||
watermark_text="", watermark_position="bottom-right", watermark_size=4.0,
|
||||
watermark_opacity=0.75, watermark_margin=3.0,
|
||||
intro_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
||||
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
||||
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_mode="auto ref2v", ref_image_size="match", ref_noise_aug=0.95,
|
||||
detail_pass=False, detail_sampler_name="euler", detail_scheduler="beta",
|
||||
detail_steps=8, detail_denoise=0.4,
|
||||
latent_upscale_param=None,
|
||||
graph=None, node_id=None):
|
||||
|
||||
# FIRST: detect a checkpoint swap since the previous execution and hard-flush.
|
||||
@@ -6562,17 +6586,26 @@ class H3LongVideos:
|
||||
# Patch the dual video/audio schedule onto the model here, so a missing
|
||||
# upstream ModelSamplingMiniMaxH3 can't silently produce gibberish audio.
|
||||
# Shifts come from the widgets (12/3 base default; MXFP8/turbo differ).
|
||||
ms_note = ""
|
||||
if apply_model_sampling:
|
||||
model, ms_note = apply_h3_model_sampling(model, shift_video, shift_audio)
|
||||
detail_note = ""
|
||||
if detail_pass:
|
||||
detail_note = (f" detail pass: {int(detail_steps)} step(s) via "
|
||||
f"{detail_sampler_name}/{detail_scheduler} at denoise "
|
||||
f"{float(detail_denoise):.2f}; video-only refinement keeps "
|
||||
f"audio from the first pass")
|
||||
|
||||
paras = split_paragraphs(prompt, "##")
|
||||
ms_note = ""
|
||||
if apply_model_sampling:
|
||||
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:
|
||||
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')}"
|
||||
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}"
|
||||
)
|
||||
|
||||
paras = split_paragraphs(prompt, "##")
|
||||
if anchor_override.strip():
|
||||
anchor, beat_paras = anchor_override.strip(), paras
|
||||
elif paras:
|
||||
@@ -6878,12 +6911,13 @@ class H3LongVideos:
|
||||
(f"PLAN (no render): {shape} = ~{total:g}s at {w}x{h}. "
|
||||
f"{len(beats) or 1} beat(s). decode {'tiled' if tiled else 'full'}. {vram_str}."
|
||||
+ (f" {beats_note}." if beats_note else "")
|
||||
+ (" ANCHOR: " + "; ".join(anchor_hazards) + "."
|
||||
if anchor_hazards else "")
|
||||
+ (f"{anatomy_note}." if anatomy_note else "")
|
||||
+ (f"{plan_audio}." if plan_audio else "")
|
||||
+ (" EXPOSURE -- " + "; ".join(wardrobe_notes) + "."
|
||||
if wardrobe_notes else "")
|
||||
+ (" ANCHOR: " + "; ".join(anchor_hazards) + "."
|
||||
if anchor_hazards else "")
|
||||
+ (f"{anatomy_note}." if anatomy_note else "")
|
||||
+ (f"{latent_upscale_note}." if latent_upscale_note else "")
|
||||
+ (f"{plan_audio}." if plan_audio else "")
|
||||
+ (" EXPOSURE -- " + "; ".join(wardrobe_notes) + "."
|
||||
if wardrobe_notes else "")
|
||||
+ (" OVERRIDES -- " + "; ".join(override_notes) + "."
|
||||
if override_notes else "")
|
||||
+ (f"{plan_ref}." if plan_ref else "")
|
||||
@@ -7025,12 +7059,11 @@ class H3LongVideos:
|
||||
shot_attempts += 1
|
||||
attempt_start = time.perf_counter()
|
||||
try:
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
detail_pass, detail_sampler_name, detail_scheduler,
|
||||
detail_steps, detail_denoise, timing_sink=shot_timing)
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
latent_upscale_param=latent_upscale_param, timing_sink=shot_timing)
|
||||
break
|
||||
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
||||
shot_retry_elapsed += time.perf_counter() - attempt_start
|
||||
@@ -7048,12 +7081,11 @@ class H3LongVideos:
|
||||
try:
|
||||
shot_attempts += 1
|
||||
attempt_start = time.perf_counter()
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
detail_pass, detail_sampler_name, detail_scheduler,
|
||||
detail_steps, detail_denoise, timing_sink=shot_timing)
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
latent_upscale_param=latent_upscale_param, timing_sink=shot_timing)
|
||||
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
||||
shot_retry_elapsed += time.perf_counter() - attempt_start
|
||||
if _is_oom(e) and getattr(e, "_h3_stage", "") == "sampling":
|
||||
@@ -7068,12 +7100,11 @@ class H3LongVideos:
|
||||
mm.soft_empty_cache(True); tiled = True; backoff.append(f"shot {i+1}: tiled")
|
||||
shot_attempts += 1
|
||||
attempt_start = time.perf_counter()
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
detail_pass, detail_sampler_name, detail_scheduler,
|
||||
detail_steps, detail_denoise, timing_sink=shot_timing)
|
||||
frames, audio, shot_latent = self._render(
|
||||
model, clip, vae, audio_vae, negative, gen_prompt, w, h, ln_i, fps,
|
||||
tiled, sa, shot_handoff, decode_tile_frames, decode_tile_size,
|
||||
shot_refs, ref_image_size, shot_aug, shot_silent,
|
||||
latent_upscale_param=latent_upscale_param, timing_sink=shot_timing)
|
||||
shot_retry_elapsed += time.perf_counter() - attempt_start
|
||||
|
||||
shot_total = time.perf_counter() - shot_total_start
|
||||
@@ -7343,7 +7374,7 @@ class H3LongVideos:
|
||||
f"shot before them ended on dialogue." if mouth_settled else "")
|
||||
+ (f"{anatomy_note}." if anatomy_note else "")
|
||||
+ (f"{latent_note}." if latent_note else "")
|
||||
+ (f"{detail_note}." if detail_note else "")
|
||||
+ (f"{latent_upscale_note}." if latent_upscale_note else "")
|
||||
+ (f" SLA LoRA '{os.path.basename(str(sla_name))}' paired with sparse attention."
|
||||
if sla_name and sparse_on else "")
|
||||
+ (f" {beats_note}." if beats_note else "")
|
||||
|
||||
Reference in New Issue
Block a user