Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38207e93e5 | ||
|
|
8bca327a07 | ||
|
|
23294fa8df | ||
|
|
f0cc7aee26 | ||
|
|
ba9e2f8d78 | ||
|
|
1b159d81f9 | ||
|
|
e6040e1bad | ||
|
|
4d98a77639 | ||
|
|
c1c4557096 | ||
|
|
df0a760c70 | ||
|
|
1cce96f64f | ||
|
|
b31d511a27 | ||
|
|
32a8bfa511 | ||
|
|
0604d7fb0e | ||
|
|
0f90ddd5da | ||
|
|
04b51c81f2 | ||
|
|
9f2807826e |
34
README.md
34
README.md
@@ -8,6 +8,10 @@ All of my nodes are created with the help of AI, so there may or may not be redu
|
|||||||
|
|
||||||
## ▶️ YouTube Tutorial Videos
|
## ▶️ YouTube Tutorial Videos
|
||||||
|
|
||||||
|
<a href="https://youtu.be/j28z5PZXkKk?si=TPeWKm2BW-uA3BAx">
|
||||||
|
<img width="642" height="718" alt="Capture d'écran 2026-06-17 234136" src="https://github.com/user-attachments/assets/89433110-8b78-4971-8098-3a9e9226f460" />
|
||||||
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -28,19 +32,33 @@ All of my nodes are created with the help of AI, so there may or may not be redu
|
|||||||
## ❓ How to install nodes
|
## ❓ How to install nodes
|
||||||
|
|
||||||
- Navigate to your `/ComfyUI/custom_nodes/ folder`
|
- Navigate to your `/ComfyUI/custom_nodes/ folder`
|
||||||
- Run `git clone https://github.com/WhatDreamscost/WhatDreamsCost-ComfyUI`
|
- Delete your old "WhatDreamsCost-ComfyUI" Folder
|
||||||
- Or download through the ComfyUI Manager.
|
- Run `git clone -b main_cs https://github.com/CGlide/WhatDreamsCost-ComfyUI.git`
|
||||||
|
|
||||||
|
|
||||||
**❗❗IMPORTANT❗❗**
|
**❗❗IMPORTANT❗❗**
|
||||||
|
|
||||||
If you don't see the latest version (v1.3.9) yet in the manager then just downloaded the nightly version (or fetch the updates to update the list to see the latest version).
|
|
||||||
Also you will need to update ComfyUI-LTXVideo and ComfyUI-KJNodes to the latest version as well. You cannot use this node without updating ComfyUI-LTXVideo!
|
This is a Modded LTX director node with some extra options to help you create videos with references sheets
|
||||||
|
|
||||||
|
This node uses Ollama Locally : you will need to install it (very lightweight) and
|
||||||
|
|
||||||
|
- In your Ollama model folder run this command to install qwen 3.5 2b q4 (1.9gb) "ollama run huihui_ai/qwen3.5-abliterated:2B"
|
||||||
|
|
||||||
|
- The model won't eat memory while generating since there is an auto clear VRAM when you hit Run or after 5min.
|
||||||
|
|
||||||
|
- You can still enter your description manually if you don't want to install it but it works very well!
|
||||||
|
|
||||||
|
- Both references mode work (fixed), Licon MSR and Ghost Mask.
|
||||||
|
|
||||||
|
- Use their Lora, that is an important step : https://huggingface.co/LiconStudio/LTX-2.3-Multiple-Subject-Reference/tree/main
|
||||||
|
|
||||||
|
- Enjoy!
|
||||||
|
|
||||||
|
<img width="596" height="668" alt="Capture d'écran 2026-06-13 234036" src="https://github.com/user-attachments/assets/2dfe7992-a72a-4eda-a103-41b3a8f77714" />
|
||||||
|
|
||||||
|
|
||||||
# 🔄 Recent Updates
|
# 🔄 Recent Updates
|
||||||
**v1.3.9**
|
|
||||||
* **Fixed recent updates not showing in the manager**
|
|
||||||
|
|
||||||
It took like 5 tries but I finally got it working 🤦♂️
|
|
||||||
|
|
||||||
**v1.3.3**
|
**v1.3.3**
|
||||||
* **LTX Director Hotfix 2**
|
* **LTX Director Hotfix 2**
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from .ltx_director import LTXDirector
|
|||||||
from .ltx_director_guide import LTXDirectorGuide
|
from .ltx_director_guide import LTXDirectorGuide
|
||||||
from comfy_api.latest import ComfyExtension, io
|
from comfy_api.latest import ComfyExtension, io
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
from .latent_slice import CleanLatentSlice
|
||||||
|
|
||||||
class PromptRelay(ComfyExtension):
|
class PromptRelay(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
@@ -29,6 +30,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"LoadVideoUI": LoadVideoUI,
|
"LoadVideoUI": LoadVideoUI,
|
||||||
"LTXDirector": LTXDirector,
|
"LTXDirector": LTXDirector,
|
||||||
"LTXDirectorGuide": LTXDirectorGuide,
|
"LTXDirectorGuide": LTXDirectorGuide,
|
||||||
|
"CleanLatentSlice": CleanLatentSlice,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -40,6 +42,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"LoadVideoUI": "Load Video UI",
|
"LoadVideoUI": "Load Video UI",
|
||||||
"LTXDirector": "LTX Director",
|
"LTXDirector": "LTX Director",
|
||||||
"LTXDirectorGuide": "LTX Director Guide",
|
"LTXDirectorGuide": "LTX Director Guide",
|
||||||
|
"CleanLatentSlice": "Clean Latent Slice",
|
||||||
}
|
}
|
||||||
|
|
||||||
WEB_DIRECTORY = "./js"
|
WEB_DIRECTORY = "./js"
|
||||||
|
|||||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/latent_slice.cpython-312.pyc
Normal file
BIN
__pycache__/latent_slice.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/load_audio_ui.cpython-312.pyc
Normal file
BIN
__pycache__/load_audio_ui.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/load_video_ui.cpython-312.pyc
Normal file
BIN
__pycache__/load_video_ui.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/ltx_director.cpython-312.pyc
Normal file
BIN
__pycache__/ltx_director.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/ltx_director_guide.cpython-312.pyc
Normal file
BIN
__pycache__/ltx_director_guide.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/ltx_keyframer.cpython-312.pyc
Normal file
BIN
__pycache__/ltx_keyframer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/ltx_sequencer.cpython-312.pyc
Normal file
BIN
__pycache__/ltx_sequencer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/multi_image_loader.cpython-312.pyc
Normal file
BIN
__pycache__/multi_image_loader.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/patches.cpython-312.pyc
Normal file
BIN
__pycache__/patches.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/prompt_relay.cpython-312.pyc
Normal file
BIN
__pycache__/prompt_relay.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/speech_length_calculator.cpython-312.pyc
Normal file
BIN
__pycache__/speech_length_calculator.cpython-312.pyc
Normal file
Binary file not shown.
1
example_workflows/LTX Director MSR_Ghost_V5.json
Normal file
1
example_workflows/LTX Director MSR_Ghost_V5.json
Normal file
File diff suppressed because one or more lines are too long
1
example_workflows/LTX Director Workflow_cs.json
Normal file
1
example_workflows/LTX Director Workflow_cs.json
Normal file
File diff suppressed because one or more lines are too long
1
example_workflows/LTX Director Workflow_v2.json
Normal file
1
example_workflows/LTX Director Workflow_v2.json
Normal file
File diff suppressed because one or more lines are too long
1
example_workflows/LTX Director Workflow_v4.json
Normal file
1
example_workflows/LTX Director Workflow_v4.json
Normal file
File diff suppressed because one or more lines are too long
1452
js/ltx_director.js
1452
js/ltx_director.js
File diff suppressed because it is too large
Load Diff
75
latent_slice.py
Normal file
75
latent_slice.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# --- START OF FILE latent_slice.py ---
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
class CleanLatentSlice:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"latent": ("LATENT",),
|
||||||
|
"start": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1, "tooltip": "The starting frame index to slice from (plug 'latent_start_index' here)."}),
|
||||||
|
"length": ("INT", {"default": 1, "min": 1, "max": 100000, "step": 1, "tooltip": "The number of frames to keep (plug 'clean_latent_frames' here)."}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("LATENT",)
|
||||||
|
RETURN_NAMES = ("latent",)
|
||||||
|
FUNCTION = "slice_latent"
|
||||||
|
CATEGORY = "WhatDreamsCost"
|
||||||
|
DESCRIPTION = "Safely slices a video latent starting from an offset index for a specific length. Uses torch.narrow to bypass PyTorch NestedTensor slicing bugs."
|
||||||
|
|
||||||
|
def slice_latent(self, latent, start, length):
|
||||||
|
new_latent = latent.copy()
|
||||||
|
|
||||||
|
def safe_slice(tensor, target_start, target_len):
|
||||||
|
dims = tensor.ndim if hasattr(tensor, "ndim") else len(tensor.shape)
|
||||||
|
|
||||||
|
# Constrain starting index and length to the actual size of the tensor
|
||||||
|
max_size = tensor.size(2) if dims == 5 else tensor.size(0)
|
||||||
|
actual_start = min(target_start, max_size - 1) if max_size > 0 else 0
|
||||||
|
actual_len = min(target_len, max_size - actual_start)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# torch.narrow is the safest low-level C++ slice method to bypass NestedTensor bugs
|
||||||
|
if dims == 5:
|
||||||
|
# [Batch, Channels, Frames, Height, Width] -> Slice dimension 2
|
||||||
|
return torch.narrow(tensor, 2, actual_start, actual_len)
|
||||||
|
elif dims == 4:
|
||||||
|
# [Frames, Channels, Height, Width] -> Slice dimension 0
|
||||||
|
return torch.narrow(tensor, 0, actual_start, actual_len)
|
||||||
|
elif dims == 3:
|
||||||
|
# [Frames, Height, Width] -> Slice dimension 0
|
||||||
|
return torch.narrow(tensor, 0, actual_start, actual_len)
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback if narrow fails
|
||||||
|
if dims == 5:
|
||||||
|
return tensor[:, :, actual_start : actual_start + actual_len]
|
||||||
|
elif dims == 4:
|
||||||
|
return tensor[actual_start : actual_start + actual_len]
|
||||||
|
elif dims == 3:
|
||||||
|
return tensor[actual_start : actual_start + actual_len]
|
||||||
|
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
# Safely slice video samples
|
||||||
|
if "samples" in new_latent:
|
||||||
|
new_latent["samples"] = safe_slice(new_latent["samples"], start, length)
|
||||||
|
|
||||||
|
# Safely slice video noise mask (if it exists)
|
||||||
|
if "noise_mask" in new_latent:
|
||||||
|
new_latent["noise_mask"] = safe_slice(new_latent["noise_mask"], start, length)
|
||||||
|
|
||||||
|
return (new_latent,)
|
||||||
|
|
||||||
|
|
||||||
|
# Register the node with ComfyUI
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"CleanLatentSlice": CleanLatentSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"CleanLatentSlice": "Clean Latent Slice"
|
||||||
|
}
|
||||||
|
|
||||||
|
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||||
1187
ltx_director.py
1187
ltx_director.py
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ class LTXDirectorGuide(LTXVAddGuide):
|
|||||||
GuideData.Input("guide_data", tooltip="Guide data produced by Prompt Relay Encode (Timeline)."),
|
GuideData.Input("guide_data", tooltip="Guide data produced by Prompt Relay Encode (Timeline)."),
|
||||||
io.Float.Input("scale_by", default=1.0, min=0.01, max=8.0, step=0.01, tooltip="Scale the latent by this factor."),
|
io.Float.Input("scale_by", default=1.0, min=0.01, max=8.0, step=0.01, tooltip="Scale the latent by this factor."),
|
||||||
io.Combo.Input("upscale_method", options=["nearest-exact", "bilinear", "area", "bicubic", "bislerp"], default="bicubic", tooltip="Method used to upscale/downscale the latent."),
|
io.Combo.Input("upscale_method", options=["nearest-exact", "bilinear", "area", "bicubic", "bislerp"], default="bicubic", tooltip="Method used to upscale/downscale the latent."),
|
||||||
|
io.Float.Input("msr_strength", default=0.0, min=0.0, max=1.0, step=0.05, tooltip="Licon MSR only: per-stage reference strength override. 0 = use the Director's value (full pull, right for stage 1). On a refinement/upscale stage set ~0.4 to hold detail without the references repainting the opening (fixes stage-2 mist/ghosting)."),
|
||||||
],
|
],
|
||||||
outputs=[
|
outputs=[
|
||||||
io.Conditioning.Output(display_name="positive"),
|
io.Conditioning.Output(display_name="positive"),
|
||||||
@@ -33,7 +34,7 @@ class LTXDirectorGuide(LTXVAddGuide):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, positive, negative, vae, latent, guide_data, scale_by=1.0, upscale_method="bicubic") -> io.NodeOutput:
|
def execute(cls, positive, negative, vae, latent, guide_data, scale_by=1.0, upscale_method="bicubic", msr_strength=0.0) -> io.NodeOutput:
|
||||||
scale_factors = vae.downscale_index_formula
|
scale_factors = vae.downscale_index_formula
|
||||||
|
|
||||||
# Clone latents to avoid mutating upstream nodes
|
# Clone latents to avoid mutating upstream nodes
|
||||||
@@ -68,6 +69,12 @@ class LTXDirectorGuide(LTXVAddGuide):
|
|||||||
|
|
||||||
_, _, latent_length, latent_height, latent_width = latent_image.shape
|
_, _, latent_length, latent_height, latent_width = latent_image.shape
|
||||||
|
|
||||||
|
# MSR mode: the Director handed us the raw references; inject them at THIS node's
|
||||||
|
# resolution (so a half-res Stage 1 and a full-res Stage 2 each stay self-consistent).
|
||||||
|
msr = guide_data.get("msr")
|
||||||
|
if msr is not None:
|
||||||
|
return cls._inject_msr(positive, negative, vae, latent_image, noise_mask, msr, msr_strength)
|
||||||
|
|
||||||
images = guide_data.get("images", [])
|
images = guide_data.get("images", [])
|
||||||
insert_frames = guide_data.get("insert_frames", [])
|
insert_frames = guide_data.get("insert_frames", [])
|
||||||
strengths = guide_data.get("strengths", [])
|
strengths = guide_data.get("strengths", [])
|
||||||
@@ -88,3 +95,60 @@ class LTXDirectorGuide(LTXVAddGuide):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask})
|
return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _inject_msr(cls, positive, negative, vae, latent_image, noise_mask, msr, msr_strength=0.0):
|
||||||
|
"""Inject the Licon MSR references at this node's current resolution."""
|
||||||
|
from nodes import NODE_CLASS_MAPPINGS
|
||||||
|
from .ltx_director import _execute_comfy_node, _unpack
|
||||||
|
|
||||||
|
IC = NODE_CLASS_MAPPINGS.get("LTXAddVideoICLoRAGuide")
|
||||||
|
AG = NODE_CLASS_MAPPINGS.get("LTXVAddGuide")
|
||||||
|
if IC is None or AG is None:
|
||||||
|
raise ValueError("MSR mode needs LTXAddVideoICLoRAGuide (ComfyUI-LTXVideo) and LTXVAddGuide (core LTXV nodes).")
|
||||||
|
|
||||||
|
def clamp01(v):
|
||||||
|
return max(0.0, min(1.0, float(v)))
|
||||||
|
|
||||||
|
slideshow = msr["slideshow"]
|
||||||
|
keyframes = msr["keyframes"]
|
||||||
|
prefix_latents = int(msr["prefix_latents"])
|
||||||
|
# Per-stage override: msr_strength > 0 wins (set it low on a refinement stage so the
|
||||||
|
# references hold detail without repainting the opening); 0 = use the Director's value.
|
||||||
|
strength = clamp01(msr_strength) if float(msr_strength) > 0.0 else clamp01(msr["strength"])
|
||||||
|
downscale = float(msr["downscale"])
|
||||||
|
|
||||||
|
# Pad the generated region so (pad + keyframes) == prefix_latents, so the downstream crop
|
||||||
|
# (which trims the slideshow's temporal footprint) lands on the true clean length.
|
||||||
|
pad_latents = max(0, prefix_latents - len(keyframes))
|
||||||
|
if pad_latents > 0:
|
||||||
|
B, C, F, H, W = latent_image.shape
|
||||||
|
latent_image = torch.cat(
|
||||||
|
[latent_image, torch.zeros((B, C, pad_latents, H, W), dtype=latent_image.dtype, device=latent_image.device)],
|
||||||
|
dim=2,
|
||||||
|
)
|
||||||
|
mb, mc, mf, mh, mw = noise_mask.shape
|
||||||
|
noise_mask = torch.cat(
|
||||||
|
[noise_mask, torch.ones((mb, mc, pad_latents, mh, mw), dtype=noise_mask.dtype, device=noise_mask.device)],
|
||||||
|
dim=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
latent = {"samples": latent_image, "noise_mask": noise_mask}
|
||||||
|
|
||||||
|
# CONDITIONING PATH: slideshow -> IC-LoRA -> keep conditioning, discard latent.
|
||||||
|
cp, cn, _ = _unpack(_execute_comfy_node(
|
||||||
|
IC, positive=positive, negative=negative, vae=vae, latent=latent, image=slideshow,
|
||||||
|
frame_idx=0, strength=strength, latent_downscale_factor=downscale,
|
||||||
|
crop="center", use_tiled_encode=False, tile_size=256, tile_overlap=64,
|
||||||
|
))
|
||||||
|
|
||||||
|
positive, negative = cp, cn
|
||||||
|
|
||||||
|
# LATENT PATH: one frozen keyframe per reference at frame 0 -> keep latent, drop cond.
|
||||||
|
for kf in keyframes:
|
||||||
|
_p, _n, latent = _unpack(_execute_comfy_node(
|
||||||
|
AG, positive=positive, negative=negative, vae=vae, latent=latent,
|
||||||
|
image=kf, frame_idx=0, strength=strength,
|
||||||
|
))
|
||||||
|
|
||||||
|
return io.NodeOutput(positive, negative, latent)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from comfy_extras.nodes_lt import get_noise_mask, LTXVAddGuide
|
from comfy_extras.nodes_lt import get_noise_mask, LTXVAddGuide
|
||||||
import torch
|
import torch
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
@@ -55,12 +56,14 @@ class LTXSequencer(LTXVAddGuide):
|
|||||||
node_id="LTXSequencer",
|
node_id="LTXSequencer",
|
||||||
display_name="LTX Sequencer",
|
display_name="LTX Sequencer",
|
||||||
category="WhatDreamsCost",
|
category="WhatDreamsCost",
|
||||||
description="Add multiple guide images at specified frame indices or seconds with strengths. Number of widgets is dynamically configured.",
|
description="Add multiple guide images at specified frame indices or seconds. Auto-syncs to Prompt Relay invisibly via the positive wire.",
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
outputs=[
|
outputs=[
|
||||||
io.Conditioning.Output(display_name="positive"),
|
io.Conditioning.Output(display_name="positive"),
|
||||||
io.Conditioning.Output(display_name="negative"),
|
io.Conditioning.Output(display_name="negative"),
|
||||||
io.Latent.Output(display_name="latent", tooltip="Video latent with added guides"),
|
io.Latent.Output(display_name="latent", tooltip="Video latent with added guides"),
|
||||||
|
# ---> ADDED SYNC LOG OUTPUT <---
|
||||||
|
io.String.Output(display_name="sync_log", tooltip="Outputs a readable text block showing the actual times used for each image"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,30 +92,64 @@ class LTXSequencer(LTXVAddGuide):
|
|||||||
insert_mode = kwargs.get("insert_mode", "frames")
|
insert_mode = kwargs.get("insert_mode", "frames")
|
||||||
frame_rate = kwargs.get("frame_rate", 24)
|
frame_rate = kwargs.get("frame_rate", 24)
|
||||||
|
|
||||||
|
# ---> EXTRACT GHOST SYNC DATA <---
|
||||||
|
timeline_data = None
|
||||||
|
if positive is not None and len(positive) > 0:
|
||||||
|
if "prompt_relay_timeline" in positive[0][1]:
|
||||||
|
try:
|
||||||
|
timeline_data = json.loads(positive[0][1]["prompt_relay_timeline"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Prepare the log text
|
||||||
|
sync_log_lines = []
|
||||||
|
if timeline_data:
|
||||||
|
sync_log_lines.append("=== TIMELINE SYNC ENABLED ===")
|
||||||
|
sync_log_lines.append(f"Invisibly Synced via Positive Wire! Using {insert_mode.upper()}:")
|
||||||
|
else:
|
||||||
|
sync_log_lines.append("=== MANUAL MODE ===")
|
||||||
|
sync_log_lines.append(f"No timeline detected, or Sync turned OFF. Using manual UI inputs ({insert_mode}):")
|
||||||
|
|
||||||
# Process inputs up to num_images, extracting dynamic frame/strength values from kwargs
|
# Process inputs up to num_images, extracting dynamic frame/strength values from kwargs
|
||||||
for i in range(1, num_images + 1):
|
for i in range(1, num_images + 1):
|
||||||
# Skip if this image index exceeds the batch
|
# Skip if this image index exceeds the batch
|
||||||
if i > batch_size:
|
if i > batch_size:
|
||||||
|
sync_log_lines.append(f"Image #{i}: Skipped (No image loaded in batch)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
img = multi_input[i-1:i] # Extract the single image frame from the batch
|
img = multi_input[i-1:i] # Extract the single image frame from the batch
|
||||||
if img is None:
|
if img is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Calculate the final frame index based on the chosen mode
|
|
||||||
f_idx = None
|
f_idx = None
|
||||||
if insert_mode == "frames":
|
strength = kwargs.get(f"strength_{i}", 1.0)
|
||||||
f_idx = kwargs.get(f"insert_frame_{i}")
|
|
||||||
elif insert_mode == "seconds":
|
# 1. AUTO-SYNC
|
||||||
sec = kwargs.get(f"insert_second_{i}")
|
if timeline_data and "starts_frames" in timeline_data and (i - 1) < len(timeline_data["starts_frames"]):
|
||||||
if sec is not None:
|
display_frame = timeline_data["starts_frames"][i - 1]
|
||||||
f_idx = int(sec * frame_rate)
|
display_sec = timeline_data["starts_seconds"][i - 1]
|
||||||
|
|
||||||
|
if insert_mode == "frames":
|
||||||
|
f_idx = display_frame
|
||||||
|
sync_log_lines.append(f"-> Image #{i} (Strength: {strength}): Synced to Segment #{i} start @ {display_frame} frames")
|
||||||
|
elif insert_mode == "seconds":
|
||||||
|
f_idx = int(display_sec * frame_rate)
|
||||||
|
sync_log_lines.append(f"-> Image #{i} (Strength: {strength}): Synced to Segment #{i} start @ {display_sec:.2f} seconds (Frame {f_idx})")
|
||||||
|
|
||||||
|
# 2. MANUAL FALLBACK
|
||||||
|
else:
|
||||||
|
if insert_mode == "frames":
|
||||||
|
f_idx = kwargs.get(f"insert_frame_{i}")
|
||||||
|
sync_log_lines.append(f"-> Image #{i} (Strength: {strength}): Manual input @ {f_idx} frames")
|
||||||
|
elif insert_mode == "seconds":
|
||||||
|
sec = kwargs.get(f"insert_second_{i}")
|
||||||
|
if sec is not None:
|
||||||
|
f_idx = int(sec * frame_rate)
|
||||||
|
sync_log_lines.append(f"-> Image #{i} (Strength: {strength}): Manual input @ {sec:.2f} seconds (Frame {f_idx})")
|
||||||
|
|
||||||
if f_idx is None:
|
if f_idx is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
strength = kwargs.get(f"strength_{i}", 1.0)
|
|
||||||
|
|
||||||
# Execution logic mirrored from LTXVAddGuideMulti
|
# Execution logic mirrored from LTXVAddGuideMulti
|
||||||
image_1, t = cls.encode(vae, latent_width, latent_height, img, scale_factors)
|
image_1, t = cls.encode(vae, latent_width, latent_height, img, scale_factors)
|
||||||
|
|
||||||
@@ -130,4 +167,8 @@ class LTXSequencer(LTXVAddGuide):
|
|||||||
scale_factors,
|
scale_factors,
|
||||||
)
|
)
|
||||||
|
|
||||||
return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask})
|
sync_log_str = "\n".join(sync_log_lines)
|
||||||
|
print(f"\n[LTX Sequencer]\n{sync_log_str}\n")
|
||||||
|
|
||||||
|
# Returned the newly formatted sync log string at the end of NodeOutput
|
||||||
|
return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask}, sync_log_str)
|
||||||
Reference in New Issue
Block a user