Initial commit

This commit is contained in:
unknown
2025-12-06 15:16:30 +01:00
commit 7faf9951ac
9 changed files with 575 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules/
dist/
vendor/
cache/
.*/
*.min.*
*.test.*
*.spec.*
*.bundle.*
*.bundle-min.*
*.log
package-lock.json
+14
View File
@@ -0,0 +1,14 @@
from .sequence_prompt_nodes import (
MultiClipTextScriptMain,
MultiClipTextScriptClipSelector,
)
NODE_CLASS_MAPPINGS = {
"MultiClipTextScriptMain": MultiClipTextScriptMain,
"MultiClipTextScriptClipSelector": MultiClipTextScriptClipSelector,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MultiClipTextScriptMain": "Multi-Clip Text Script: Main",
"MultiClipTextScriptClipSelector": "Multi-Clip Text Script: Clip Selector",
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

+232
View File
@@ -0,0 +1,232 @@
# ComfyUI Multi-Clip Text Script Nodes
<!-- Added a full-width overview image at the very top -->
<img src="./multiclip_main.PNG" alt="Multi-Clip Text Script - Main overview" width="100%" />
## How to use it (visual)
<table>
<tr>
<td><img src="./multiclip01.PNG" alt="Step 1 - Configure Main node" width="100%"></td>
<td><img src="./multiclip02.PNG" alt="Step 2 - Author script with clips" width="100%"></td>
</tr>
<tr>
<td><img src="./multiclip03.PNG" alt="Step 3 - Select clip and format" width="100%"></td>
<td><img src="./multiclip04.PNG" alt="Step 4 - Connect to encoder and render" width="100%"></td>
</tr>
</table>
This ComfyUI node was created while I was making Wan 2.2 videos with 81 frames and crossfading them together. I found myself dragging and clicking through different parts of the workflow to change clip text prompts, so I wanted a faster, centralized place to edit all prompts.
This node was generated with ChatGPT 5.1, and this README was also written by ChatGPT.
Two simple nodes that help you author sequence-friendly prompts from a single script that contains multiple clips. You write one text block with (clip01), (clip02), … sections, then select one clip at a time to produce a clean prompt (with shared prefix/suffix and negative prompt).
Nodes
- Multi-Clip Text Script: Main
- Multi-Clip Text Script: Clip Selector
Category
- Text / Multi-Clip
File
- sequence_prompt_nodes.py
## Why this exists
- Keep consistent prompt parts across many shots (prefix/suffix/negative) while varying per-clip action text.
- Author all clips once, then iterate and render per-clip without copy/paste.
- Optional auto formatting to output a classic “prefix, clip, suffix.” prompt line.
## Installation
- Go to your ComfyUI/custom_nodes folder.
Clona the repo (exempel):
cd ComfyUI/custom_nodes
git clone https://github.com/TuonoMindCode/ComfyUI-MultiClip-Text-Script.git
or
Copy sequence_prompt_nodes.py and __init__.py into your ComfyUI/custom_nodes/ComfyUI-MultiClip-Text-Script.
Restart ComfyUI.
Search for:
- “Multi-Clip Text Script: Main”
- “Multi-Clip Text Script: Clip Selector”
## Script format
The Main node parses a single multiline script using labeled sections:
- Label each section with (clipXX) where XX is a number (1-based).
- Case-insensitive. Any number of digits is supported.
- Text for a clip runs until the next (clipYY) or the end of the script.
Example script
(clip01) woman walks forward and waves to the camera
(clip02) woman walks forward and gets angry
(clip03) woman throws the phone
Clips are sorted numerically by their label: (clip1), (clip02), (clip10) → 1, 2, 10.
## Node: Multi-Clip Text Script: Main
Purpose
- Parse the multi-clip script, store prefix/suffix and per-clip texts in an internal string (MULTICLIP_PAIR_DATA).
- Provide total clip count and a shared negative prompt for downstream nodes.
Inputs (required)
- prefix_text (STRING, multiline): Common description always included (subject, style, tempo, etc.).
- multi_clip_script (STRING, multiline): Your script with (clipXX) sections.
- suffix_text (STRING, multiline): Appended after every clip (camera style, quality, etc.).
Inputs (optional)
- negative_text (STRING, multiline): One negative prompt used for every clip.
Outputs
- pair_data (MULTICLIP_PAIR_DATA): Internal encoded data with prefix, suffix, and all clips.
- num_clips (INT): Number of parsed clips.
- multi_clip_negative (MULTICLIP_NEGATIVE): The shared negative prompt.
Details
- If no clips are found, pair_data still contains prefix and suffix. num_clips is 0.
- Leading/trailing whitespace is trimmed from all pieces.
## Node: Multi-Clip Text Script: Clip Selector
Purpose
- Select one clip (1-based index) from pair_data and build the final prompt text.
Inputs (required)
- pair_data (MULTICLIP_PAIR_DATA): Output from Main.
- clip_number (INT): 1-based index of the clip to use.
Inputs (optional)
- auto_format (BOOLEAN):
- False: Join with blank lines: prefix + blank + clip + blank + suffix.
- True: Produce “prefix, clip, suffix.” and clean trailing commas/semicolons/spaces.
- log_to_console (BOOLEAN): Print the combined prompt for debugging.
- negative (MULTICLIP_NEGATIVE): Connect from Main’s multi_clip_negative to pass the shared negative forward.
Outputs
- clip_text (STRING): The final prompt for the selected clip.
- negative_text (STRING): The negative prompt passthrough (if provided).
Behavior notes
- Out-of-range clip_number is clamped to the valid range.
- If pair_data had no clips, the output becomes a combination of prefix/suffix only.
- Cleaning rules used by auto_format remove trailing commas, semicolons, and spaces on each piece before joining. The final text gets a period if it ends without punctuation.
## Quick example
Inputs to Main
- prefix_text:
“cinematic, natural light, handheld”
- multi_clip_script:
```
(clip01)
woman walks forward and waves to the camera
(clip02)
woman walks forward and gets angry
(clip03)
woman throws the phone
```
- suffix_text:
“soft bokeh, film grain”
- negative_text:
“low quality, blurry, artifacts”
Main outputs
- pair_data → connect to Clip Selector
- num_clips → e.g., 3
- multi_clip_negative → connect to Clip Selector’s negative
Clip Selector
- clip_number: 2
- auto_format: False → Output:
```
cinematic, natural light, handheld
woman walks forward and gets angry
soft bokeh, film grain
```
- auto_format: True → Output:
```
cinematic, natural light, handheld, woman walks forward and gets angry, soft bokeh, film grain.
```
- negative_text: “low quality, blurry, artifacts”
## Typical workflow
- Multi-Clip Text Script: Main
- pair_data → Multi-Clip Text Script: Clip Selector.pair_data
- multi_clip_negative → Multi-Clip Text Script: Clip Selector.negative
- Multi-Clip Text Script: Clip Selector
- clip_text → your text encoder’s positive prompt input
- negative_text → your text encoder’s negative prompt input
- Render per-clip by changing clip_number or automating it in a loop.
## Internal encoding (for reference)
- pair_data is a single string combining:
- prefix
- suffix
- all clip texts, in order
- Separators (do not type these yourself; they are inserted by the node):
- META_SEP = “\n<<<MULTICLIP_META_SEP>>>\n”
- CLIP_SEP = “\n<<<MULTICLIP_CLIP_SEP>>>\n”
- Layout:
- prefix + META_SEP + suffix + META_SEP + (clip1 + CLIP_SEP + clip2 + CLIP_SEP + …)
## Tips
- Use auto_format = True for a compact, comma-separated prompt line.
- Keep prefix/suffix relatively short to avoid overly long prompts when combined.
- Enable log_to_console to debug the exact text sent to your text encoder.
- If no (clipXX) sections are detected, num_clips = 0; you can still use prefix/suffix.
## Troubleshooting
- No clips detected
- Ensure each section starts with (clip01), (clip02), … on its own line or followed by whitespace.
- Labels are case-insensitive but must follow the (clip<number>) pattern.
- Wrong clip selected
- clip_number is 1-based. 1 = first clip.
- Values out of range are clamped to the nearest valid index.
- Extra punctuation
- Use auto_format = True to remove trailing commas/semicolons/spaces from each part before joining.
## Development notes
- Node classes:
- MultiClipTextScriptMain
- MultiClipTextScriptClipSelector
- Display names:
- “Multi-Clip Text Script: Main”
- “Multi-Clip Text Script: Clip Selector”
- Category: “Text / Multi-Clip”
- Regex used to parse clips:
- (clip(\d+)) followed by text until the next (clipN) or end of string.
- DOTALL and IGNORECASE enabled.
- Sorting:
- Clips are sorted numerically by the parsed number part (e.g., clip02 → 2).
+317
View File
@@ -0,0 +1,317 @@
import re
# Separatorer för intern kodning i pair_data
CLIP_SEP = "\n<<<MULTICLIP_CLIP_SEP>>>\n"
META_SEP = "\n<<<MULTICLIP_META_SEP>>>\n"
class MultiClipTextScriptMain:
"""
Multi-Clip Text Script: Main
Tar text från t.ex. Ollama eller någon annan källa + konstant prefix/suffix:
prefix_text:
Beskrivning som alltid ska vara med (personen, scenen, stil, etc).
multi_clip_script (exempel):
(clip01)
woman waves at the camera
(clip02)
woman gets angry
(clip03)
woman calls someone
suffix_text:
Text som läggs till efter varje klipp (t.ex. kamera-stil, kvalitet, osv).
negative_text:
Negativ prompt som är gemensam för alla klipp.
Scriptet parsas i formatet:
(clip01)
<text>
(clip02)
<text>
...
Och omvandlas till ett internt paket (MULTICLIP_PAIR_DATA) där vi sparar:
- prefix
- suffix
- alla clip-texter i ordning
Sedan kan "Multi-Clip Text Script: Clip Selector" plocka ut
en clip-text i taget och bygga en färdig prompt.
Output:
pair_data (MULTICLIP_PAIR_DATA) – intern sträng med all data
num_clips (INT) – hur många klipp som hittades
multi_clip_negative (MULTICLIP_NEGATIVE) – negativ prompt för alla klipp
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# 1. Prefix först
"prefix_text": (
"STRING",
{
"multiline": True,
"default": "",
},
),
# 2. Huvudfönstret för (clip01)(clip02)...-scriptet
"multi_clip_script": (
"STRING",
{
"multiline": True,
"default": "(clip01)\nwoman walks forward and waves to the camera\n\n"
"(clip02)\nwoman walks forward and gets angry\n\n"
"(clip03)\nwoman throws the phone",
},
),
# 3. Suffix sist
"suffix_text": (
"STRING",
{
"multiline": True,
"default": "",
},
),
},
"optional": {
"negative_text": (
"STRING",
{
"multiline": True,
"default": "",
},
),
},
}
RETURN_TYPES = ("MULTICLIP_PAIR_DATA", "INT", "MULTICLIP_NEGATIVE")
RETURN_NAMES = ("pair_data", "num_clips", "multi_clip_negative")
FUNCTION = "parse_script"
CATEGORY = "Text / Multi-Clip"
def parse_script(
self,
prefix_text: str,
multi_clip_script: str,
suffix_text: str,
negative_text: str = "",
):
# Hitta alla (clipXX) + text fram till nästa (clipYY) eller slutet
pattern = r"\(clip(\d+)\)\s*(.*?)(?=\n\(clip\d+\)|\Z)"
matches = re.findall(pattern, multi_clip_script, flags=re.DOTALL | re.IGNORECASE)
clips = []
for idx, text in matches:
text = text.strip()
if text:
clips.append((int(idx), text)) # "01" -> 1 osv, för sortering
# Sortera efter clip-nummer
clips.sort(key=lambda x: x[0])
prefix_clean = (prefix_text or "").strip()
suffix_clean = (suffix_text or "").strip()
negative_clean = (negative_text or "").strip()
if not clips:
# Inga klipp hittade – koda ändå prefix/suffix men utan clips
payload = META_SEP.join([prefix_clean, suffix_clean, ""])
return (payload, 0, negative_clean)
# Själva klipp-texterna i ordning
clip_texts = [t for _, t in clips]
clips_blob = CLIP_SEP.join(clip_texts)
# Kodning i pair_data:
# prefix <<META_SEP>> suffix <<META_SEP>> clip1 <<CLIP_SEP>> clip2 <<CLIP_SEP>> ...
payload = META_SEP.join([prefix_clean, suffix_clean, clips_blob])
num_clips = len(clip_texts)
return (payload, num_clips, negative_clean)
class MultiClipTextScriptClipSelector:
"""
Multi-Clip Text Script: Clip Selector
Tar emot pair_data från MultiClipTextScriptMain (typ MULTICLIP_PAIR_DATA)
och väljer EN specifik klipp-text baserat på clip_number (1-baserat).
clip_number (1-baserat i UI):
1 -> första klipp-texten (clip01)
2 -> andra (clip02)
3 -> tredje (clip03)
osv.
auto_format:
- False: prefix + tomrad + clip + tomrad + suffix
- True: "prefix, clip, suffix." (klassisk prompt-stil)
+ rensar bort extra kommatecken i slutet av varje del.
negative:
- Input av typen MULTICLIP_NEGATIVE (ingen egen text-ruta i denna nod).
Typisk kedja:
Multi-Clip Text Script: Main (multi_clip_negative)
→ Anything Everywhere
→ Multi-Clip Text Script: Clip Selector (negative)
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pair_data": ("MULTICLIP_PAIR_DATA", {}),
"clip_number": (
"INT",
{
"default": 1,
"min": 1,
"max": 999,
"step": 1,
},
),
},
"optional": {
"auto_format": (
"BOOLEAN",
{
"default": False,
},
),
"log_to_console": (
"BOOLEAN",
{
"default": False,
},
),
"negative": (
"MULTICLIP_NEGATIVE",
{},
),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("clip_text", "negative_text")
FUNCTION = "select_pair"
CATEGORY = "Text / Multi-Clip"
def select_pair(
self,
pair_data: str,
clip_number: int,
auto_format: bool = False,
log_to_console: bool = False,
negative: str = "",
):
if not pair_data:
return ("", negative)
# Förväntat format:
# prefix <<META_SEP>> suffix <<META_SEP>> clips_blob
parts = pair_data.split(META_SEP)
if len(parts) == 3:
prefix, suffix, clips_blob = parts
elif len(parts) == 2:
prefix, clips_blob = parts
suffix = ""
else:
prefix = ""
suffix = ""
clips_blob = parts[0] if parts else ""
clips_blob = clips_blob or ""
clips = clips_blob.split(CLIP_SEP) if clips_blob else []
internal_index = clip_number - 1
if not clips:
combined = self._combine(prefix, "", suffix, auto_format)
if log_to_console and combined:
print("[Multi-Clip Selector] (no clips) →", combined)
return (combined, negative)
if internal_index < 0:
internal_index = 0
if internal_index >= len(clips):
internal_index = len(clips) - 1
clip_raw = clips[internal_index]
combined = self._combine(prefix, clip_raw, suffix, auto_format)
if log_to_console and combined:
print(f"[Multi-Clip Selector] clip_number={clip_number} →", combined)
return (combined, negative)
@staticmethod
def _clean_piece(piece: str) -> str:
if not piece:
return ""
p = piece.strip()
while p and p[-1] in [",", ";", " "]:
p = p[:-1]
return p.strip()
@staticmethod
def _combine(prefix: str, clip: str, suffix: str, auto_format: bool) -> str:
prefix = prefix or ""
clip = clip or ""
suffix = suffix or ""
if not auto_format:
parts = []
if prefix.strip():
parts.append(prefix.strip())
if clip.strip():
parts.append(clip.strip())
if suffix.strip():
parts.append(suffix.strip())
return "\n\n".join(parts)
prefix_clean = MultiClipTextScriptClipSelector._clean_piece(prefix)
clip_clean = MultiClipTextScriptClipSelector._clean_piece(clip)
suffix_clean = MultiClipTextScriptClipSelector._clean_piece(suffix)
parts = []
if prefix_clean:
parts.append(prefix_clean)
if clip_clean:
parts.append(clip_clean)
if suffix_clean:
parts.append(suffix_clean)
if not parts:
return ""
text = ", ".join(parts)
text = text.strip()
if text and text[-1] not in [".", "!", "?"]:
text += "."
return text
# Registrering för ComfyUI
NODE_CLASS_MAPPINGS = {
"MultiClipTextScriptMain": MultiClipTextScriptMain,
"MultiClipTextScriptClipSelector": MultiClipTextScriptClipSelector,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MultiClipTextScriptMain": "Multi-Clip Text Script: Main",
"MultiClipTextScriptClipSelector": "Multi-Clip Text Script: Clip Selector",
}