commit 0cc39784ca6d02ef2a9c58d3d974db14ff152393 Author: DigitalIO Date: Tue Oct 10 17:45:02 2023 -0400 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48fd780 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/* +__pycache__/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e53767 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +The MIT License (MIT) + +Copyright © 2023 Michael Wolfe (DigitalIO) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b49c12 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# ComfyUI Stable Wildcards +## Overview +ComfyUI has an amazing feature that saves the workflow to reproduce an image in the image itself. In theory, you can +import the workflow and reproduce the exact image. Unfortunately, this does not work with wildcards. The goal of this +node is to implement wildcard support using a seed to stabilize the output to allow greater reproducibility. + +## Use +### Wildcards +Wildcards are supported via brackets and pipes. + +Example: + +`A painting of a {boat|fish} in the {sea|lake}` + +The first pair of words will randomly select `boat` or `fish`, and the second will either be `sea` or `lake`. +The following is a list of possible random output using the above prompt: + +* A painting of a boat in the sea +* A painting of a boat in the lake +* A painting of a fish in the sea +* A painting of a fish in the lake + +_Note:_ Everything including the brackets are replaced with the final word. +Include spaces around your ` {} ` to prevent words from running together, or use it +to modify words. +Ex: `{cat|dog}fish` outputs `catfish` or `dogfish`. + +### Node +The node requires three inputs +1) a string - Your prompt +2) a seed - This is what makes your prompt reproducible +3) a version - Allows for future changes to not break backwards compatibility + +The node has one output +1) a string - Your final prompt with wildcards processed + +### Bonus Node +An additional node is included that disabled the default dynamicPrompt behavior of multiline inputs. + +## Opinions +Below are *my opinions* and as such will probably not be changed. + +### CLIP +Many other implementations combine this feature with the CLIP node. I chose not to do this to keep the wildcard code and +CLIP implementation separate and more re-usable. + +### Wildcard Files +Probably won't be implemented. Files are external to the workflow, so there is no guarantee that (1) the file exists on +another system and (2) the file content has not been modified. File wildcards are not an appropriate approach if you are +looking to create repeatable output. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..7b7a002 --- /dev/null +++ b/__init__.py @@ -0,0 +1,46 @@ +# The MIT License (MIT) +# +# Copyright © 2023 Michael Wolfe (DigitalIO) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from .swnodes.StableWildcard import StableWildcard +from .swnodes.NonDynamicString import NondynamicString + +# Nodes passed to ComfyUI. +# Node names must be unique +NODE_MAP = [ + { + "name" : "Stable Wildcards", + "class": StableWildcard, + "disp" : "Stable Wildcard Output" + }, + { + "name" : "NonDynamic String", + "class": NondynamicString, + "disp" : "NonDynamic String" + } +] + +# Create the expected node maps for ComfyUI +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +for x in NODE_MAP: + x['class'].CATEGORY = 'StableWildcards' + NODE_CLASS_MAPPINGS[x['name']] = x['class'] + NODE_DISPLAY_NAME_MAPPINGS[x['name']] = x['disp'] diff --git a/swnodes/NonDynamicString.py b/swnodes/NonDynamicString.py new file mode 100644 index 0000000..bafabab --- /dev/null +++ b/swnodes/NonDynamicString.py @@ -0,0 +1,47 @@ +# The MIT License (MIT) +# +# Copyright © 2023 Michael Wolfe (DigitalIO) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class NondynamicString: + """ + ComfyUI Custom Node + A helper node that disable dynamic prompts + """ + + def __init__(self): + pass + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("String",) + OUTPUT_NODE = False + FUNCTION = "execute" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ('STRING', {'default': '', 'multiline': True, 'dynamicPrompts': False}), + }, + "optional": {} + } + + def execute(self, string, **kwargs): + """ + Do nothing + """ + + return string, diff --git a/swnodes/StableWildcard.py b/swnodes/StableWildcard.py new file mode 100644 index 0000000..87ffb0f --- /dev/null +++ b/swnodes/StableWildcard.py @@ -0,0 +1,86 @@ +# The MIT License (MIT) +# +# Copyright © 2023 Michael Wolfe (DigitalIO) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import sys +from random import Random +import re + + +class StableWildcard: + """ + ComfyUI Custom Node + Implements wildcards using a stable seed to make workflows reproducible after creation + """ + + def __init__(self): + pass + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("Prompt String",) + OUTPUT_NODE = False + FUNCTION = "execute" + + # A reusable wildcard pattern matches any string with brackets {} + # but will not select over other wildcards. + WILDCARD_PATTERN = re.compile('{[^{}]+}') + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + # Without dynamicPrompts set to false ComfyUI will randomize our prompt before we get to it! + "prompt": ('STRING', {'default': '', 'multiline': True, 'dynamicPrompts': False}), + "seed" : ('INT', {'default': 0, 'min': 0, 'max': sys.maxsize}), + + # Currently unused, but if a breaking change must be introduced + # it can be used to provide backwards compatibility. + "version": ([1], {'default': 1}) + }, + "optional": {} + } + + def execute(self, prompt, seed, **kwargs): + """ + Process wildcards using a seed to produce stable output. + To achieve stable results a new random object is created + using the given seed. + """ + + rng = Random(int(seed)) + + # Search & replace matches + match = self.WILDCARD_PATTERN.search(prompt) + while match: + + # Get the options - Remove the {}, split by | character + # Because the search pattern requires at least one character, + # ops is guaranteed to have at least one option + ops = match.group()[1:-1].split('|') + + # Pick a random option. + pick = ops[rng.randint(0, len(ops)-1)] + + # Replace the match and update the string + # Limit one match incase there are repeated wildcards + prompt = prompt.replace(match.group(), pick, 1) + + # Search for more wildcards + match = self.WILDCARD_PATTERN.search(prompt) + + print('\033[96m Stable Wildcard: ({}) "{}"\033[0m'.format(seed, prompt)) + + return prompt,