feat: Per-chunk text conditioning via ChainTextEmbeds node
- New ChainTextEmbeds node: chains up to 8 WanVideoTextEncode outputs into an ordered TEXT_EMBED_SEQUENCE for per-chunk prompting - WanChunkedI2VSampler: new optional text_embed_sequence input overrides single text_embeds when connected - Chunk N uses embed N from the sequence; last embed repeats for any extra chunks beyond the sequence length - Console output shows which embed index is used per chunk - README updated with per-chunk prompting workflow diagram
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
# ComfyUI VideoChunkTools
|
||||
|
||||
**9 utility nodes for generating long videos in ComfyUI by splitting them into overlapping chunks with rolling reference frames.**
|
||||
**10 utility nodes for generating long videos in ComfyUI by splitting them into overlapping chunks with rolling reference frames.**
|
||||
|
||||
Designed to solve the **"context window reversal" problem** — where video generation models (Wan 2.1/2.2, FantasyPortrait, etc.) revert to the starting state after ~135 frames because the reference image embedding pulls the generation back.
|
||||
|
||||
Now with **per-chunk text prompting** — change the narrative as your video progresses.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -44,9 +46,15 @@ Final: Concatenate all chunks → 240+ frame seamless video
|
||||
|
||||
| Node | Purpose |
|
||||
|------|---------|
|
||||
| **Wan Chunked I2V Sampler ♾️** | All-in-one node: encode→sample→decode→extract ref→repeat. Handles everything internally. |
|
||||
| **Wan Chunked I2V Sampler ♾️** | All-in-one node: encode→sample→decode→extract ref→repeat. Supports per-chunk text prompting. |
|
||||
| **Wan Chunk Calculator 🧮** | Calculate exact total_frames for N chunks with 4n+1 normalization |
|
||||
|
||||
### Text Conditioning Node (1 node — for per-chunk prompting)
|
||||
|
||||
| Node | Purpose |
|
||||
|------|---------|
|
||||
| **Chain Text Embeds 🔗** | Chain multiple WanVideoTextEncode outputs into an ordered sequence for per-chunk text conditioning |
|
||||
|
||||
> The Wan nodes gracefully degrade — if WanVideoWrapper isn't installed, the 7 core nodes still load and work fine.
|
||||
|
||||
---
|
||||
@@ -101,6 +109,27 @@ The **Wan Chunked I2V Sampler** handles everything in a single node:
|
||||
- **Multi-keyframe FLF** — provide a batch of end images to distribute across chunks
|
||||
- **Crossfade overlap** — set `end_blend_chunks` for smooth FLF transitions
|
||||
- **Auto 4n+1 normalization** — chunk sizes are automatically adjusted for Wan's requirements
|
||||
- **Per-chunk text prompts** — connect a `ChainTextEmbeds` node to change the text conditioning per-chunk
|
||||
|
||||
### Per-Chunk Text Prompting (Wan Models)
|
||||
|
||||
Change the narrative as your video progresses — each chunk can have its own text prompt:
|
||||
|
||||
1. Add multiple **WanVideoTextEncode** nodes, each with a different prompt
|
||||
2. Connect them to a **Chain Text Embeds 🔗** node (`embed_1`, `embed_2`, `embed_3`, ...)
|
||||
3. Connect the `embed_sequence` output to the sampler's `text_embed_sequence` input
|
||||
4. Chunk 1 uses embed_1, chunk 2 uses embed_2, etc.
|
||||
5. If you have fewer prompts than chunks, the last prompt repeats for remaining chunks
|
||||
|
||||
```
|
||||
[WanVideoTextEncode: "A cat sleeps on a sofa"]──┐
|
||||
[WanVideoTextEncode: "The cat wakes up"]─────────┼──▶ [Chain Text Embeds 🔗]──▶ text_embed_sequence
|
||||
[WanVideoTextEncode: "The cat jumps off"]────────┘
|
||||
│
|
||||
[Wan Model + VAE + Image]──────────────────────────▶ [Wan Chunked I2V Sampler ♾️]
|
||||
```
|
||||
|
||||
> **Tip**: You can still use the single `text_embeds` input if you want the same prompt for all chunks. The sequence input takes priority when connected.
|
||||
|
||||
---
|
||||
|
||||
@@ -180,6 +209,21 @@ All-in-one node for Wan I2V models. See the [Workflows](#all-in-one-wan-models)
|
||||
|
||||
Simple math: calculates `total_frames = chunk_frames + (num_chunks - 1) * (chunk_frames - 1)` with 4n+1 normalization.
|
||||
|
||||
### Chain Text Embeds 🔗
|
||||
|
||||
Chains up to 8 pre-encoded text embeddings into an ordered sequence for per-chunk text conditioning.
|
||||
|
||||
| Input | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| embed_1 | WANVIDEOTEXTEMBEDS | Yes | Text embedding for chunk 1 |
|
||||
| embed_2–embed_8 | WANVIDEOTEXTEMBEDS | No | Text embeddings for chunks 2–8 |
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| embed_sequence | TEXT_EMBED_SEQUENCE | Ordered list of embeddings — connect to sampler's `text_embed_sequence` input |
|
||||
|
||||
If you have fewer embeds than chunks, the last embed repeats for all remaining chunks. Non-connected slots are skipped (embed_1 + embed_3 = 2-entry sequence).
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -612,6 +612,77 @@ class ConcatVideoChunks:
|
||||
return (result, result.shape[0],)
|
||||
|
||||
|
||||
class ChainTextEmbeds:
|
||||
"""
|
||||
Chains multiple pre-encoded text embeddings into an ordered sequence
|
||||
for per-chunk text conditioning in WanChunkedI2VSampler.
|
||||
|
||||
Wire one WanVideoTextEncode output into each embed slot.
|
||||
Chunk 1 uses embed_1, chunk 2 uses embed_2, and so on.
|
||||
If you have fewer embeds than chunks, the last embed repeats
|
||||
for all remaining chunks.
|
||||
|
||||
Example (3 chunks, 2 prompts):
|
||||
embed_1 = encode("A cat sleeps on a sofa")
|
||||
embed_2 = encode("The cat wakes up and stretches")
|
||||
→ Chunk 1 gets embed_1, chunks 2-3 get embed_2
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"embed_1": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 1 (required)."
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"embed_2": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 2."
|
||||
}),
|
||||
"embed_3": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 3."
|
||||
}),
|
||||
"embed_4": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 4."
|
||||
}),
|
||||
"embed_5": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 5."
|
||||
}),
|
||||
"embed_6": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 6."
|
||||
}),
|
||||
"embed_7": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 7."
|
||||
}),
|
||||
"embed_8": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text embedding for chunk 8."
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TEXT_EMBED_SEQUENCE",)
|
||||
RETURN_NAMES = ("embed_sequence",)
|
||||
FUNCTION = "chain"
|
||||
CATEGORY = "VideoChunkTools"
|
||||
DESCRIPTION = (
|
||||
"Chains multiple WanVideoTextEncode outputs into an ordered "
|
||||
"sequence for per-chunk text conditioning.\n\n"
|
||||
"Connect the output to the 'text_embed_sequence' input of "
|
||||
"WanChunkedI2VSampler. If fewer embeds than chunks, the last "
|
||||
"embed repeats for all remaining chunks."
|
||||
)
|
||||
|
||||
def chain(self, embed_1, embed_2=None, embed_3=None, embed_4=None,
|
||||
embed_5=None, embed_6=None, embed_7=None, embed_8=None):
|
||||
sequence = [embed_1]
|
||||
for e in [embed_2, embed_3, embed_4, embed_5,
|
||||
embed_6, embed_7, embed_8]:
|
||||
if e is not None:
|
||||
sequence.append(e)
|
||||
return (sequence,)
|
||||
|
||||
|
||||
# ---------- Registration ----------
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -622,6 +693,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"GetFrameByIndex": GetFrameByIndex,
|
||||
"GetFrameRange": GetFrameRange,
|
||||
"VideoChunkPlanner": VideoChunkPlanner,
|
||||
"ChainTextEmbeds": ChainTextEmbeds,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -632,4 +704,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GetFrameByIndex": "Get Frame By Index",
|
||||
"GetFrameRange": "Get Frame Range",
|
||||
"VideoChunkPlanner": "Video Chunk Planner",
|
||||
"ChainTextEmbeds": "Chain Text Embeds 🔗",
|
||||
}
|
||||
|
||||
+26
-4
@@ -282,7 +282,13 @@ class WanChunkedI2VSampler:
|
||||
"tooltip": "Two-pass only: CFG for the second pass (model_b). Ignored in single-pass."
|
||||
}),
|
||||
"text_embeds": ("WANVIDEOTEXTEMBEDS", {
|
||||
"tooltip": "Text conditioning from WanVideoTextEncode."
|
||||
"tooltip": "Text conditioning from WanVideoTextEncode. Applied to ALL chunks uniformly."
|
||||
}),
|
||||
"text_embed_sequence": ("TEXT_EMBED_SEQUENCE", {
|
||||
"tooltip": "Per-chunk text conditioning from ChainTextEmbeds. "
|
||||
"Chunk 1 gets embed_1, chunk 2 gets embed_2, etc. "
|
||||
"Last embed repeats for extra chunks. "
|
||||
"Overrides text_embeds when connected."
|
||||
}),
|
||||
"clip_embeds": ("WANVIDIMAGE_CLIPEMBEDS", {
|
||||
"tooltip": "Optional CLIP vision embeddings from WanVideoClipVisionEncode."
|
||||
@@ -374,6 +380,7 @@ class WanChunkedI2VSampler:
|
||||
split_step=4,
|
||||
cfg_b=1.0,
|
||||
text_embeds=None,
|
||||
text_embed_sequence=None,
|
||||
clip_embeds=None,
|
||||
feta_args=None,
|
||||
context_options=None,
|
||||
@@ -468,6 +475,12 @@ class WanChunkedI2VSampler:
|
||||
print(f" End Lat Strength : {end_latent_strength}")
|
||||
else:
|
||||
print(f" FLF Mode : OFF")
|
||||
if text_embed_sequence is not None:
|
||||
print(f" Text Embeds : PER-CHUNK sequence ({len(text_embed_sequence)} embeds for {num_chunks} chunks)")
|
||||
elif text_embeds is not None:
|
||||
print(f" Text Embeds : Single (same for all chunks)")
|
||||
else:
|
||||
print(f" Text Embeds : None")
|
||||
print(f"{'=' * 65}\n")
|
||||
|
||||
# ── Progress bar (chunk-level) ──
|
||||
@@ -486,6 +499,15 @@ class WanChunkedI2VSampler:
|
||||
chunk_label = f"Chunk {chunk_idx + 1}/{num_chunks}"
|
||||
print(f"\n ── {chunk_label} ──")
|
||||
|
||||
# ── Resolve per-chunk text conditioning ──
|
||||
if text_embed_sequence is not None:
|
||||
embed_idx = min(chunk_idx, len(text_embed_sequence) - 1)
|
||||
chunk_text_embeds = text_embed_sequence[embed_idx]
|
||||
if len(text_embed_sequence) > 1:
|
||||
print(f" Text embed : #{embed_idx + 1} of {len(text_embed_sequence)}")
|
||||
else:
|
||||
chunk_text_embeds = text_embeds
|
||||
|
||||
# ============================================================
|
||||
# Step 1 — ENCODE reference image → image_embeds
|
||||
# ============================================================
|
||||
@@ -542,7 +564,7 @@ class WanChunkedI2VSampler:
|
||||
force_offload=force_offload,
|
||||
scheduler=scheduler,
|
||||
riflex_freq_index=riflex_freq_index,
|
||||
text_embeds=text_embeds,
|
||||
text_embeds=chunk_text_embeds,
|
||||
feta_args=feta_args,
|
||||
context_options=context_options,
|
||||
start_step=0,
|
||||
@@ -561,7 +583,7 @@ class WanChunkedI2VSampler:
|
||||
force_offload=force_offload,
|
||||
scheduler=scheduler,
|
||||
riflex_freq_index=riflex_freq_index,
|
||||
text_embeds=text_embeds,
|
||||
text_embeds=chunk_text_embeds,
|
||||
samples=high_latent,
|
||||
feta_args=feta_args,
|
||||
context_options=None,
|
||||
@@ -584,7 +606,7 @@ class WanChunkedI2VSampler:
|
||||
force_offload=force_offload,
|
||||
scheduler=scheduler,
|
||||
riflex_freq_index=riflex_freq_index,
|
||||
text_embeds=text_embeds,
|
||||
text_embeds=chunk_text_embeds,
|
||||
feta_args=feta_args,
|
||||
context_options=context_options,
|
||||
add_noise_to_samples=False,
|
||||
|
||||
Reference in New Issue
Block a user