Automated PR - 2026-01-15
This commit is contained in:
@@ -5,6 +5,7 @@ from .easy_samplers import (
|
||||
LTXVBaseSampler,
|
||||
LTXVExtendSampler,
|
||||
LTXVInContextSampler,
|
||||
LTXVNormalizingSampler,
|
||||
)
|
||||
from .gemma_encoder import LTXVGemmaCLIPModelLoader, LTXVGemmaEnhancePrompt
|
||||
from .guide import LTXVAddGuideAdvanced
|
||||
@@ -61,6 +62,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"LTXVBaseSampler": LTXVBaseSampler,
|
||||
"LTXVInContextSampler": LTXVInContextSampler,
|
||||
"LTXVExtendSampler": LTXVExtendSampler,
|
||||
"LTXVNormalizingSampler": LTXVNormalizingSampler,
|
||||
"LTXVPreprocessMasks": LTXVPreprocessMasks,
|
||||
"LTXVPatcherVAE": LTXVPatcherVAE,
|
||||
"LTXVPromptEnhancer": LTXVPromptEnhancer,
|
||||
|
||||
@@ -4,6 +4,9 @@ import comfy
|
||||
import comfy_extras
|
||||
import nodes
|
||||
import torch
|
||||
from comfy.ldm.lightricks.av_model import LTXAVModel
|
||||
from comfy.nested_tensor import NestedTensor
|
||||
from comfy_api.latest import io
|
||||
from comfy_extras.nodes_custom_sampler import SamplerCustomAdvanced, SplitSigmas
|
||||
from comfy_extras.nodes_lt import EmptyLTXVLatentVideo, LTXVAddGuide, LTXVCropGuides
|
||||
|
||||
@@ -907,3 +910,142 @@ class LinearOverlapLatentTransition:
|
||||
"batch_index": combined_batch_index,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@comfy_node(description="LTXV Normalizing Sampler")
|
||||
class LTXVNormalizingSampler(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="LTXVNormalizingSampler",
|
||||
category="utility",
|
||||
inputs=[
|
||||
io.Noise.Input("noise"),
|
||||
io.Guider.Input("guider"),
|
||||
io.Sampler.Input("sampler"),
|
||||
io.Sigmas.Input("sigmas"),
|
||||
io.Latent.Input("latent_image"),
|
||||
io.String.Input(
|
||||
"video_normalization_factors", default="1,1,1,1,1,1,1,1"
|
||||
),
|
||||
io.String.Input(
|
||||
"audio_normalization_factors", default="1,1,0.25,1,1,0.25,1,1"
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Latent.Output(display_name="denoised_output"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(
|
||||
cls,
|
||||
noise,
|
||||
guider,
|
||||
sampler,
|
||||
sigmas,
|
||||
latent_image,
|
||||
video_normalization_factors,
|
||||
audio_normalization_factors,
|
||||
) -> io.NodeOutput:
|
||||
|
||||
if (
|
||||
guider.model_patcher.model.diffusion_model.__class__.__name__
|
||||
!= "LTXAVModel"
|
||||
):
|
||||
raise ValueError()
|
||||
|
||||
ltxav: LTXAVModel = guider.model_patcher.model.diffusion_model
|
||||
|
||||
video_normalization_factors = video_normalization_factors.split(",")
|
||||
audio_normalization_factors = audio_normalization_factors.split(",")
|
||||
video_normalization_factors = [
|
||||
float(factor) for factor in video_normalization_factors
|
||||
]
|
||||
audio_normalization_factors = [
|
||||
float(factor) for factor in audio_normalization_factors
|
||||
]
|
||||
|
||||
# Extend normalization factors to match the length of sigmas
|
||||
sigmas_len = len(sigmas) - 1
|
||||
if len(video_normalization_factors) < sigmas_len:
|
||||
if len(video_normalization_factors) > 0:
|
||||
video_normalization_factors.extend(
|
||||
[video_normalization_factors[-1]]
|
||||
* (sigmas_len - len(video_normalization_factors))
|
||||
)
|
||||
if len(audio_normalization_factors) < sigmas_len:
|
||||
if len(audio_normalization_factors) > 0:
|
||||
audio_normalization_factors.extend(
|
||||
[audio_normalization_factors[-1]]
|
||||
* (sigmas_len - len(audio_normalization_factors))
|
||||
)
|
||||
|
||||
# Calculate indices where both normalization factors are not 1.0
|
||||
sampling_split_indices = [
|
||||
i + 1
|
||||
for i, (v, a) in enumerate(
|
||||
zip(video_normalization_factors, audio_normalization_factors)
|
||||
)
|
||||
if v != 1.0 or a != 1.0
|
||||
]
|
||||
print("Sampling split indices: %s" % sampling_split_indices, flush=True)
|
||||
|
||||
# Split sigmas according to sampling_split_indices
|
||||
def split_by_indices(arr, indices):
|
||||
"""
|
||||
Splits arr into chunks according to indices (split points).
|
||||
Indices are treated as starting a new chunk at each index in the list.
|
||||
"""
|
||||
if not indices:
|
||||
return [arr]
|
||||
split_points = sorted(set(indices))
|
||||
chunks = []
|
||||
prev = 0
|
||||
for idx in split_points:
|
||||
if prev < idx:
|
||||
chunks.append(arr[prev : idx + 1])
|
||||
prev = idx
|
||||
if prev < len(arr):
|
||||
chunks.append(arr[prev:])
|
||||
return chunks
|
||||
|
||||
sigmas_chunks = split_by_indices(sigmas, sampling_split_indices)
|
||||
print("Sigmas chunks: %s" % sigmas_chunks, flush=True)
|
||||
|
||||
i = 0
|
||||
for sigmas_chunk in sigmas_chunks:
|
||||
i += len(sigmas_chunk) - 1
|
||||
print("Sampling with sigmas %s" % (sigmas_chunk), flush=True)
|
||||
(_, latent_image) = SamplerCustomAdvanced().execute(
|
||||
noise=noise,
|
||||
guider=guider,
|
||||
sampler=sampler,
|
||||
sigmas=sigmas_chunk,
|
||||
latent_image=latent_image,
|
||||
)
|
||||
video_samples, audio_samples = ltxav.separate_audio_and_video_latents(
|
||||
latent_image["samples"].tensors,
|
||||
None,
|
||||
)
|
||||
if i - 1 < len(video_normalization_factors) and i - 1 < len(
|
||||
audio_normalization_factors
|
||||
):
|
||||
video_samples = video_samples * video_normalization_factors[i - 1]
|
||||
audio_samples = audio_samples * audio_normalization_factors[i - 1]
|
||||
latent_image["samples"] = NestedTensor(
|
||||
ltxav.recombine_audio_and_video_latents(
|
||||
video_samples, audio_samples
|
||||
)
|
||||
)
|
||||
print(
|
||||
"After %d steps, the latent image was normalized by %f and %f"
|
||||
% (
|
||||
i,
|
||||
video_normalization_factors[i - 1],
|
||||
audio_normalization_factors[i - 1],
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
return io.NodeOutput(latent_image)
|
||||
|
||||
@@ -565,7 +565,7 @@
|
||||
"title": "Video Size",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Width & height settings must be divisible by 32 + 1. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
"Width & height settings must be divisible by 64. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -743,7 +743,7 @@
|
||||
"title": "Video Size",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Width & height settings must be divisible by 32 + 1. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
"Width & height settings must be divisible by 64. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"title": "Video Size",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Width & height settings must be divisible by 32 + 1. \n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
"Width & height settings must be divisible by 64. \n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"title": "Video Size",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Width & height settings must be divisible by 32 + 1. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
"Width & height settings must be divisible by 64. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -520,7 +520,7 @@
|
||||
"title": "Video Size",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Width & height settings must be divisible by 32 + 1. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
"Width & height settings must be divisible by 64. \nFrame count must be divisible by 8 + 1. \n\n\nRunning with invalid parameters **will not cause errors**. Instead, the flow will silently choose the closest valid parameters. "
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
+6
-2
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -528,8 +529,11 @@ def find_matching_dir(root_path: str, pattern: str) -> str:
|
||||
"""
|
||||
Recursively search for files matching a glob pattern and return the parent directory of the first match.
|
||||
"""
|
||||
|
||||
matches = list(Path(root_path).rglob(pattern))
|
||||
matches = [
|
||||
Path(p)
|
||||
for p in glob(f"{root_path}/**", recursive=True)
|
||||
if Path(p).match(pattern)
|
||||
]
|
||||
if not matches:
|
||||
raise FileNotFoundError(
|
||||
f"No files matching pattern '{pattern}' found under {root_path}"
|
||||
|
||||
@@ -85,6 +85,20 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
|
||||
return list(stg_indexes)
|
||||
|
||||
def unpack_latents(self, x: torch.Tensor):
|
||||
latent_shapes = (
|
||||
self.conds.get("positive", {})[0]
|
||||
.get("model_conds", {})
|
||||
.get("latent_shapes", None)
|
||||
.cond
|
||||
)
|
||||
vx, ax = comfy.utils.unpack_latents(x, latent_shapes)
|
||||
return vx, ax
|
||||
|
||||
def pack_latents(self, vx: torch.Tensor, ax: torch.Tensor):
|
||||
x, latent_shapes = comfy.utils.pack_latents([vx, ax])
|
||||
return x, latent_shapes
|
||||
|
||||
def predict_noise(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
@@ -92,7 +106,6 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
model_options: dict = {},
|
||||
seed=None,
|
||||
):
|
||||
x_shape = x.shape
|
||||
# in CFGGuider.predict_noise, we call sampling_function(), which uses cfg_function() to compute pos & neg
|
||||
# but we'd rather do a single batch of sampling pos, neg, and perturbed, so we call calc_cond_batch([perturbed,pos,neg]) directly
|
||||
current_step = self.current_step
|
||||
@@ -115,16 +128,13 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
run_a2v = video_params.do_cross_attn(current_step)
|
||||
run_v2a = audio_params.do_cross_attn(current_step)
|
||||
|
||||
vx, ax = self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
x, audio_length
|
||||
)
|
||||
vx, ax = self.unpack_latents(x)
|
||||
if not run_vx:
|
||||
vx = self.last_denoised_v
|
||||
if not run_ax:
|
||||
ax = self.last_denoised_a
|
||||
x = self.inner_model.diffusion_model.recombine_audio_and_video_latents(
|
||||
vx, ax, x_shape
|
||||
)
|
||||
x, _ = self.pack_latents(vx, ax)
|
||||
|
||||
if not run_vx and not run_ax:
|
||||
return x
|
||||
|
||||
@@ -145,11 +155,7 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
del model_options["transformer_options"]["run_ax"]
|
||||
del model_options["transformer_options"]["a2v_cross_attn"]
|
||||
del model_options["transformer_options"]["v2a_cross_attn"]
|
||||
v_noise_pred_pos, a_noise_pred_pos = (
|
||||
self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
noise_pred_pos, audio_length
|
||||
)
|
||||
)
|
||||
v_noise_pred_pos, a_noise_pred_pos = self.unpack_latents(noise_pred_pos)
|
||||
|
||||
a_noise_pred_neg, v_noise_pred_neg = 0, 0
|
||||
a_noise_pred_perturbed, v_noise_pred_perturbed = 0, 0
|
||||
@@ -168,11 +174,7 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
timestep,
|
||||
model_options,
|
||||
)[0]
|
||||
v_noise_pred_neg, a_noise_pred_neg = (
|
||||
self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
noise_pred_neg, audio_length
|
||||
)
|
||||
)
|
||||
v_noise_pred_neg, a_noise_pred_neg = self.unpack_latents(noise_pred_neg)
|
||||
finally:
|
||||
del model_options["transformer_options"]["run_vx"]
|
||||
del model_options["transformer_options"]["run_ax"]
|
||||
@@ -202,10 +204,8 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
timestep,
|
||||
model_options,
|
||||
)[0]
|
||||
v_noise_pred_perturbed, a_noise_pred_perturbed = (
|
||||
self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
noise_pred_perturbed, audio_length
|
||||
)
|
||||
v_noise_pred_perturbed, a_noise_pred_perturbed = self.unpack_latents(
|
||||
noise_pred_perturbed
|
||||
)
|
||||
finally:
|
||||
self.stg_flag.do_skip = False
|
||||
@@ -229,10 +229,8 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
timestep,
|
||||
model_options,
|
||||
)[0]
|
||||
v_noise_pred_modality, a_noise_pred_modality = (
|
||||
self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
noise_pred_modality, audio_length
|
||||
)
|
||||
v_noise_pred_modality, a_noise_pred_modality = self.unpack_latents(
|
||||
noise_pred_modality
|
||||
)
|
||||
finally:
|
||||
del model_options["transformer_options"]["a2v_cross_attn"]
|
||||
@@ -260,9 +258,7 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
else:
|
||||
ax = self.last_denoised_a
|
||||
|
||||
x = self.inner_model.diffusion_model.recombine_audio_and_video_latents(
|
||||
vx, ax, x_shape
|
||||
)
|
||||
x, _ = self.pack_latents(vx, ax)
|
||||
|
||||
# normally this would be done in cfg_function, but we skipped
|
||||
# that for efficiency: we can compute the noise predictions in
|
||||
@@ -285,11 +281,7 @@ class MultimodalGuider(comfy.samplers.CFGGuider):
|
||||
}
|
||||
x = fn(args)
|
||||
|
||||
self.last_denoised_v, self.last_denoised_a = (
|
||||
self.inner_model.diffusion_model.separate_audio_and_video_latents(
|
||||
x, audio_length
|
||||
)
|
||||
)
|
||||
self.last_denoised_v, self.last_denoised_a = self.unpack_latents(x)
|
||||
return x
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user