Initial commit

This commit is contained in:
Luis Quesada
2024-04-24 17:03:53 +02:00
parent c47538f2e5
commit 9560a61ccb
5 changed files with 921 additions and 2 deletions
+125 -2
View File
@@ -1,2 +1,125 @@
# ComfyUI-Prompt-Combinator
A ComfYUI node that generates all possible combinations of prompts from several lists of strings.
ComfyUI-Prompt-Combinator
Copyright (c) 2024, Luis Quesada Torres - https://github.com/lquesada | www.luisquesada.com
Check ComfyUI here: https://github.com/comfyanonymous/ComfyUI
# Overview
"🔢 Prompt Combinator" is a node that generates all possible combinations of prompts from several lists of strings.
Download the following example workflow from [here](prompt-combinator_example_workflow.json) or drag and drop the screenshot into ComfyUI.
![Workflow](prompt-combinator_example_workflow.png)
# Installation Instructions
Install via ComfyUI-Manager or go to the custom_nodes/ directory and run ```$ git clone https://github.com/lquesada/ComfyUI-Prompt-Combinator.git```
# Detailed Use Case
You want to produce all possible combinations of prompts from several lists of strings, e.g.
**Input list 1:**
```
a cat
a dog
```
**Input list 2:**
```
with pointy ears
with fluffy tail
with two heads
```
**Input list 3:**
```
cute
scary
```
**Expected output:** a list of strings that contain
```
a cat
with pointy ears
cute
a cat
with pointy ears
scary
a cat
with fluffy tail
cute
a cat
with fluffy tail
scary
a cat
with two heads
cute
a cat
with two heads
scary
a dog
with pointy ears
cute
a dog
with pointy ears
scary
a dog
with fluffy tail
cute
a dog
with fluffy tail
scary
a dog
with two heads
cute
a dog
with two heads
scary
```
Additionally, you may want to be able to identify what inputs were used in order to craft the filename_prefix, e.g.:
cat, pointy-ears, scary_00001_.png
dog, two-heads, cute_00001_.png
This custom node serves this purpose.
# License
Creative Commons License Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)
This is a human-readable summary of the Legal Code.
You are free:
* to Share — to copy, distribute and transmit the work
* to Remix — to adapt the work
Under the following conditions:
* Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
* Noncommercial — You may not use this work for commercial purposes.
* Share Alike — If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
With the understanding that:
* Waiver — Any of the above conditions can be waived if you get permission from the copyright holder.
* Public Domain — Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license.
* Other Rights — In no way are any of the following rights affected by the license:
Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations;
The author's moral rights;
Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights.
* Notice — For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.
For details and the full license text, see http://creativecommons.org/licenses/by-nc-sa/3.0/
Executable
+11
View File
@@ -0,0 +1,11 @@
from .prompt_combinator import PromptCombinator
NODE_CLASS_MAPPINGS = {
"PromptCombinator": PromptCombinator
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptCombinator": "🔢 Prompt Combinator"
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+705
View File
@@ -0,0 +1,705 @@
{
"last_node_id": 17,
"last_link_id": 20,
"nodes": [
{
"id": 1,
"type": "PromptCombinator",
"pos": [
280,
370
],
"size": {
"0": 390,
"1": 730
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "combinations",
"type": "STRING",
"links": [
8,
10
],
"shape": 6,
"slot_index": 0
},
{
"name": "id_1",
"type": "STRING",
"links": [
12
],
"shape": 6,
"slot_index": 1
},
{
"name": "id_2",
"type": "STRING",
"links": [
13
],
"shape": 6,
"slot_index": 2
},
{
"name": "id_3",
"type": "STRING",
"links": null,
"shape": 6,
"slot_index": 3
},
{
"name": "id_4",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "PromptCombinator"
},
"widgets_values": [
"@",
"#",
"# Note: format is ID@description\n# - ID is optional\n# - Comments/empty lines are ignored)\n\n# Animals\ncat@a cat\ndog@a dog\n\n#People\nwoman@a woman \n",
"car@driving a car\n\nbike@riding a bike",
"# IDs are optional\ncarrying an umbrella\n",
""
]
},
{
"id": 7,
"type": "String Literal",
"pos": [
280,
1160
],
"size": {
"0": 390,
"1": 100
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
9
],
"shape": 3,
"slot_index": 0
}
],
"title": "Negative Prompt",
"properties": {
"Node name for S&R": "String Literal"
},
"widgets_values": [
"ugly"
]
},
{
"id": 8,
"type": "ShowText|pysssss",
"pos": [
850,
190
],
"size": {
"0": 210,
"1": 360
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 10,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowText|pysssss"
},
"widgets_values": [
"",
"a cat\ndriving a car\ncarrying an umbrella\n",
"a cat\nriding a bike\ncarrying an umbrella\n",
"a dog\ndriving a car\ncarrying an umbrella\n",
"a dog\nriding a bike\ncarrying an umbrella\n",
"a woman\ndriving a car\ncarrying an umbrella\n",
"a woman\nriding a bike\ncarrying an umbrella\n"
]
},
{
"id": 4,
"type": "Efficient Loader",
"pos": [
770,
870
],
"size": {
"0": 400,
"1": 460
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "lora_stack",
"type": "LORA_STACK",
"link": null
},
{
"name": "cnet_stack",
"type": "CONTROL_NET_STACK",
"link": null
},
{
"name": "positive",
"type": "STRING",
"link": 8,
"widget": {
"name": "positive"
}
},
{
"name": "negative",
"type": "STRING",
"link": 9,
"widget": {
"name": "negative"
}
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
2
],
"shape": 3,
"slot_index": 0
},
{
"name": "CONDITIONING+",
"type": "CONDITIONING",
"links": [
3
],
"shape": 3,
"slot_index": 1
},
{
"name": "CONDITIONING-",
"type": "CONDITIONING",
"links": [
4
],
"shape": 3,
"slot_index": 2
},
{
"name": "LATENT",
"type": "LATENT",
"links": [
5
],
"shape": 3,
"slot_index": 3
},
{
"name": "VAE",
"type": "VAE",
"links": [
6
],
"shape": 3,
"slot_index": 4
},
{
"name": "CLIP",
"type": "CLIP",
"links": null,
"shape": 3
},
{
"name": "DEPENDENCIES",
"type": "DEPENDENCIES",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "Efficient Loader"
},
"widgets_values": [
"epicphotogasm_ultimateFidelity.safetensors",
"Baked VAE",
-1,
"None",
1,
1,
"CLIP_POSITIVE",
"CLIP_NEGATIVE",
"none",
"comfy",
512,
512,
1
],
"color": "#322",
"bgcolor": "#533",
"shape": 1
},
{
"id": 11,
"type": "ShowText|pysssss",
"pos": [
1080,
190
],
"size": {
"0": 210,
"1": 360
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 15,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowText|pysssss"
},
"widgets_values": [
"",
"cat, car",
"cat, bike",
"dog, car",
"dog, bike",
"woman, car",
"woman, bike"
]
},
{
"id": 17,
"type": "Note",
"pos": [
280,
80
],
"size": [
520,
60
],
"flags": {},
"order": 2,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"https://github.com/lquesada/ComfyUI-Prompt-Combinator - ComfyUI-Prompt-Combinator"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 10,
"type": "StringFunction|pysssss",
"pos": [
850,
610
],
"size": {
"0": 290,
"1": 214
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text_a",
"type": "STRING",
"link": 12,
"widget": {
"name": "text_a"
}
},
{
"name": "text_b",
"type": "STRING",
"link": 13,
"widget": {
"name": "text_b"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
14,
15
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "StringFunction|pysssss"
},
"widgets_values": [
"append",
"yes",
"",
"",
""
]
},
{
"id": 5,
"type": "KSampler (Efficient)",
"pos": [
1200,
870
],
"size": [
330,
560
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 2
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 3
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 4
},
{
"name": "latent_image",
"type": "LATENT",
"link": 5
},
{
"name": "optional_vae",
"type": "VAE",
"link": 6
},
{
"name": "script",
"type": "SCRIPT",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": null,
"shape": 3
},
{
"name": "CONDITIONING+",
"type": "CONDITIONING",
"links": null,
"shape": 3
},
{
"name": "CONDITIONING-",
"type": "CONDITIONING",
"links": null,
"shape": 3
},
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3
},
{
"name": "VAE",
"type": "VAE",
"links": null,
"shape": 3
},
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
7,
11
],
"shape": 3,
"slot_index": 5
}
],
"properties": {
"Node name for S&R": "KSampler (Efficient)"
},
"widgets_values": [
76,
null,
20,
7,
"euler",
"normal",
1,
"auto",
"true"
],
"color": "#223322",
"bgcolor": "#335533",
"shape": 1
},
{
"id": 6,
"type": "PreviewImage",
"pos": [
1560,
870
],
"size": [
430,
630
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 7
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1640,
490
],
"size": {
"0": 250,
"1": 270
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 11
},
{
"name": "filename_prefix",
"type": "STRING",
"link": 14,
"widget": {
"name": "filename_prefix"
}
}
],
"properties": {},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 16,
"type": "Note",
"pos": [
850,
80
],
"size": {
"0": 450,
"1": 60
},
"flags": {},
"order": 2,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"Note:\n- The different prompts generated (below this box, left)\n- The vs filename_prefixes generated from IDs (below this box, right)"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
2,
4,
0,
5,
0,
"MODEL"
],
[
3,
4,
1,
5,
1,
"CONDITIONING"
],
[
4,
4,
2,
5,
2,
"CONDITIONING"
],
[
5,
4,
3,
5,
3,
"LATENT"
],
[
6,
4,
4,
5,
4,
"VAE"
],
[
7,
5,
5,
6,
0,
"IMAGE"
],
[
8,
1,
0,
4,
2,
"STRING"
],
[
9,
7,
0,
4,
3,
"STRING"
],
[
10,
1,
0,
8,
0,
"STRING"
],
[
11,
5,
5,
9,
0,
"IMAGE"
],
[
12,
1,
1,
10,
0,
"STRING"
],
[
13,
1,
2,
10,
1,
"STRING"
],
[
14,
10,
0,
9,
1,
"STRING"
],
[
15,
10,
0,
11,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+80
View File
@@ -0,0 +1,80 @@
from itertools import product
class PromptCombinator:
"""
A example node
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"id_separator": ("STRING", {"default": '@'}),
"comment_prefix": ("STRING", {"default": '#'}),
"input_list_1": ("STRING", {"default": '', "multiline": True}),
"input_list_2": ("STRING", {"default": '', "multiline": True}),
"input_list_3": ("STRING", {"default": '', "multiline": True}),
"input_list_4": ("STRING", {"default": '', "multiline": True}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("combinations", "id_1", "id_2", "id_3", "id_4")
OUTPUT_IS_LIST = (True, True, True, True, True)
FUNCTION = "execute"
CATEGORY = "batch/🔢 PromptCombinator"
def parse_input(self, input_list, id_separator, comment_prefix):
# Split by newlines and ensure at least one empty element if list is empty
entries = input_list.strip().split('\n') if input_list.strip() else ['']
ids = []
descriptions = []
for entry in entries:
# Skip processing for lines that are empty or start with comment prefix
if entry.strip() == '' or entry.strip().startswith(comment_prefix):
continue
# Split entry by the separator, default to empty ID if separator not present
parts = entry.split(id_separator, 1)
if len(parts) == 1:
ids.append('')
descriptions.append(parts[0].strip())
else:
ids.append(parts[0].strip())
descriptions.append(parts[1].strip())
# If empty, guarantee that it returns at least an empty id and description
if not ids:
ids.append('')
if not descriptions:
descriptions.append('')
return ids, descriptions
def combine_descriptions_and_ids(self, *inputs, id_separator, comment_prefix):
list_of_ids = []
list_of_descriptions = []
# Parse all inputs
for input_list in inputs:
ids, descriptions = self.parse_input(input_list, id_separator, comment_prefix)
list_of_ids.append(ids)
list_of_descriptions.append(descriptions)
# Generate all combinations of descriptions and corresponding IDs
all_description_combinations = list(product(*list_of_descriptions))
all_id_combinations = list(product(*list_of_ids))
# Flatten the list of combinations
outputs = ['\n'.join(comb) for comb in all_description_combinations]
id_lists = list(zip(*all_id_combinations))
return outputs, id_lists
def execute(self, id_separator, comment_prefix, input_list_1, input_list_2, input_list_3, input_list_4):
combinations, (ids1, ids2, ids3, ids4) = self.combine_descriptions_and_ids(input_list_1, input_list_2, input_list_3, input_list_4, id_separator=id_separator, comment_prefix=comment_prefix)
return (combinations, ids1, ids2, ids3, ids4)