17 Commits
main ... 0.13

Author SHA1 Message Date
CGlide
38207e93e5 README.md 2026-06-19 13:05:05 +02:00
CGlide
8bca327a07 Bug Fix :Global prompt optional 2026-06-19 12:23:29 +02:00
CGlide
23294fa8df Update README.md 2026-06-18 16:40:33 +02:00
CGlide
f0cc7aee26 Add files via upload 2026-06-18 00:09:24 +02:00
CGlide
ba9e2f8d78 Update README.md 2026-06-14 13:49:18 +02:00
CGlide
1b159d81f9 Update README.md 2026-06-14 00:20:18 +02:00
CGlide
e6040e1bad Update README.md 2026-06-14 00:18:03 +02:00
CGlide
4d98a77639 Update README.md 2026-06-14 00:14:52 +02:00
CGlide
c1c4557096 Update README.md 2026-06-14 00:13:57 +02:00
CGlide
df0a760c70 Update README.md 2026-06-14 00:10:47 +02:00
CGlide
1cce96f64f Update README.md 2026-06-14 00:03:33 +02:00
CGlide
b31d511a27 Add files via upload 2026-06-13 23:48:26 +02:00
CGlide
32a8bfa511 Add files via upload 2026-06-02 12:22:23 +02:00
CGlide
0604d7fb0e Add files via upload 2026-05-24 16:03:13 +02:00
CGlide
0f90ddd5da Add files via upload 2026-05-24 16:02:32 +02:00
CGlide
04b51c81f2 Add files via upload 2026-05-23 18:44:58 +02:00
CGlide
9f2807826e Add files via upload 2026-05-22 16:29:52 +02:00
44 changed files with 40858 additions and 40250 deletions

View File

@@ -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&#39;é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&#39;é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**

View File

@@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

75
latent_slice.py Normal file
View 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']

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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)