diff --git a/README.md b/README.md index 0e675bc..8c35d00 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/nodes.py b/nodes.py index 03aeacd..083b419 100644 --- a/nodes.py +++ b/nodes.py @@ -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 🔗", } diff --git a/wan_chunked_sampler.py b/wan_chunked_sampler.py index 47c4269..f5a4b08 100644 --- a/wan_chunked_sampler.py +++ b/wan_chunked_sampler.py @@ -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,