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:
Greg
2026-02-23 11:56:01 -08:00
parent 7bb60125b9
commit 277f12c213
3 changed files with 145 additions and 6 deletions
+46 -2
View File
@@ -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.
![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom_Nodes-blue)
![License](https://img.shields.io/badge/license-MIT-green)
@@ -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
+73
View File
@@ -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
View File
@@ -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,