diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index 15201ac..dbb3526 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,5 @@ cython_debug/ # PyPI configuration file .pypirc +examples/desktop.ini +examples/1.mkv diff --git a/README.md b/README.md index 9f5a0bf..270bcd3 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ -# ComfyUI_Simple_String_Repository \ No newline at end of file +# ComfyUI_Simple_String_Repository + +A small utility node (in multiple variations) which assists prompt building by allowing you to use "quick and dirty" collections of strings, separated via new-line and indexed inside the string itself, directly in ComfyUI - as opposed to loading from massive collections of external files. + +The nodes also support basic "randomness" syntax, which is explained in the [sd dynamic prompts repo](https://github.com/adieyal/sd-dynamic-prompts). + +However, unlike the dynamic prompts in the above repo, this node aims at helping you build prompts faster using many small collections of strings already relevant to your specific project / workflow, as opposed to exploring different combinations of random elements from massive lists. +This is better illustrated below. + + +Install +----------------------------------------------------------------------------- + +**Option 1** + +Install via [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager). + +**Option 2** + +Clone the repository into your ComfyUI custom_nodes directory. +```text +git clone https://github.com/IgalOgonov/ComfyUI_Simple_String_Repository +``` + +The Node(s) +----------------------------------------------------------------------------- + +The nodes are all called "Simple String Repository". +The there are three **variations** based on the number of potentially selected strings (`Small` for 3, no suffix for 5, and `Large` for 10), and each node has a "compact" version which is worse for automated workflow but more comfortable if you intend to set all selection parameters (1 required, 2 optional) manually. + +![basic usage](examples/1.gif) + +Each node has the `string` input, which corresponds to the collection of strings you wish to use. +By design, each collection should represent alternative tags belonging to the same "group", e.g. art styles, type of atmosphere, color scheme, artist, etc. + +Below is the `delimiter`, used to separate multiple outputs in case you select more than one strings from the collection. + +Afterwards is where the `Compact` and `Regular` nodes differ. +In regular node, each `target` (the index of the sub-string you wish to select) has three relevant widgets: +* The index itself (`target`) +* Its strength (`strength`) +* In case of a collection of randomly interchangeable strings, as explained in the [sd dynamic prompts repo](https://github.com/adieyal/sd-dynamic-prompts), you may explicitly select one of the options via `random` (index starts at 0), or leave it at `-1` for a random output. + +The `Compact` nodes simply shove all three parameters into a single `,`-separated string. All optional parameters can be omitted. + +The **indexes** are highly **recommended**, but **optional**. +Any string that does not have an index will get assigned its line number as an index during runtime. +When duplicate indexes exist (typically, this would only happen because of a mess caused by mixing indexed and unindexed strings), only the first one can be selected. +The **front-end functionality** helps avoid that: + +1. `Add Missing Indexes` - This button will add unique numerical indexes to any strings missing them. Will also fix duplicates, but replacing any duplicate identifier with a new one. + The new indexes are always the lowest available unique index. + Good for when you already have specific automated workflows reliant on specific numbers, but overall, this is considered a quick and dirty fix. +2. `Regenerate All Indexes` - Strips all strings from their existing indexes, then assigns new ones, based on the line number. +3. `Remove All Indexes` - Strips all strings from their indexes. +4. `Reset Target Parameters` Resets the selection parameters, making the output blank. + +Intended Usage +----------------------------------------------------------------------------- +![alt text](2.png) + +Those nodes don't really have much functionality by themselves - they are meant to supplement existing prompt builder nodes, such as [String Function Tree](https://github.com/wolfden/ComfyUi_String_Function_Tree), that are meant for prompt building (as opposed to prompt generation / random exploration). + +The collections themselves can be created manually (e.g. explore articles about art styles relevant to your project, and copy-paste relevant styles into an "Art Styles" node), or you can take subsets from existing collections [like the ones over here](https://github.com/adieyal/sd-dynamic-prompts/tree/main/collections). +As you may have noticed from the earlier preview, the node does support the basic randomness syntax (e.g. `{something|like|this}`), although it's not optimized for it. + +![alt text](3-1.png) + +Note that you don't have to use short strings - you may also use large collections as a single string, as long as you're fine with giving up precision for such strings. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..2856509 --- /dev/null +++ b/__init__.py @@ -0,0 +1,24 @@ +from .py.simple_string_repository import SimpleStringRepositorySmall,SimpleStringRepositorySmallCompact, SimpleStringRepository, SimpleStringRepositoryCompact, SimpleStringRepositoryLarge, SimpleStringRepositoryLargeCompact + + +NODE_CLASS_MAPPINGS = { + "SimpleStringRepositorySmall": SimpleStringRepositorySmall, + "SimpleStringRepositorySmallCompact": SimpleStringRepositorySmallCompact, + "SimpleStringRepository": SimpleStringRepository, + "SimpleStringRepositoryCompact": SimpleStringRepositoryCompact, + "SimpleStringRepositoryLarge": SimpleStringRepositoryLarge, + "SimpleStringRepositoryLargeCompact": SimpleStringRepositoryLargeCompact, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SimpleStringRepositorySmall": "Simple String Repository Small (IO)", + "SimpleStringRepositorySmallCompact": "Simple String Repository Small Compact (IO)", + "SimpleStringRepository": "Simple String Repository (IO)", + "SimpleStringRepositoryCompact": "Simple String Repository Compact (IO)", + "SimpleStringRepositoryLarge": "Simple String Repository Large (IO)", + "SimpleStringRepositoryLargeCompact": "Simple String Repository Large Compact (IO)", +} + + +WEB_DIRECTORY = "./web" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/examples/1.gif b/examples/1.gif new file mode 100644 index 0000000..c59b5b9 Binary files /dev/null and b/examples/1.gif differ diff --git a/examples/1.webm b/examples/1.webm new file mode 100644 index 0000000..3a2214a Binary files /dev/null and b/examples/1.webm differ diff --git a/examples/2.png b/examples/2.png new file mode 100644 index 0000000..1632db3 Binary files /dev/null and b/examples/2.png differ diff --git a/examples/3.png b/examples/3.png new file mode 100644 index 0000000..14c5a91 Binary files /dev/null and b/examples/3.png differ diff --git a/py/simple_string_repository.py b/py/simple_string_repository.py new file mode 100644 index 0000000..fec6717 --- /dev/null +++ b/py/simple_string_repository.py @@ -0,0 +1,296 @@ +from .util import Util + +class SimpleStringRepositorySmall: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength1": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random1": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target2": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength2": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random2": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target3": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength3": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random3": ("INT", {"default":-1,"min": -1,"max": 1000}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None): + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3} + ] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(parsedInputs)] + + #Main Function + def exec(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None): + + #Parse inputs for clarity + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3} + ] + return Util.common_string_repo_function(string,delimiter,parsedInputs) + +class SimpleStringRepositorySmallCompact: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("STRING", {"default":'0,1,-1'}), + "target2": ("STRING", {"default":'0,1,-1'}), + "target3": ("STRING", {"default":'0,1,-1'}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, target2=None, target3=None): + targets = [target1,target2,target3] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(Util.parseCompactInputs(targets))] + + #Main Function + def exec(self, string, delimiter, target1=None, target2=None, target3=None): + targets = [target1,target2,target3] + return Util.common_string_repo_function(string,delimiter,Util.parseCompactInputs(targets)) + +class SimpleStringRepository: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength1": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random1": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target2": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength2": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random2": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target3": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength3": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random3": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target4": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength4": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random4": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target5": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength5": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random5": ("INT", {"default":-1,"min": -1,"max": 1000}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None, target4=None, strength4=None, random4=None, target5=None, strength5=None, random5=None): + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3}, + {"t":target4,"s":strength4,"r":random4}, + {"t":target5,"s":strength5,"r":random5} + ] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(parsedInputs)] + + #Main Function + def exec(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None, target4=None, strength4=None, random4=None, target5=None, strength5=None, random5=None): + + #Parse inputs for clarity + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3}, + {"t":target4,"s":strength4,"r":random4}, + {"t":target5,"s":strength5,"r":random5} + ] + return Util.common_string_repo_function(string,delimiter,parsedInputs) + +class SimpleStringRepositoryCompact: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("STRING", {"default":'0,1,-1'}), + "target2": ("STRING", {"default":'0,1,-1'}), + "target3": ("STRING", {"default":'0,1,-1'}), + "target4": ("STRING", {"default":'0,1,-1'}), + "target5": ("STRING", {"default":'0,1,-1'}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, target2=None, target3=None, target4=None, target5=None): + targets = [target1,target2,target3,target4,target5] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(Util.parseCompactInputs(targets))] + + #Main Function + def exec(self, string, delimiter, target1=None, target2=None, target3=None, target4=None, target5=None): + targets = [target1,target2,target3,target4,target5] + return Util.common_string_repo_function(string,delimiter,Util.parseCompactInputs(targets)) + +class SimpleStringRepositoryLarge: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength1": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random1": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target2": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength2": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random2": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target3": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength3": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random3": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target4": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength4": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random4": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target5": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength5": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random5": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target6": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength6": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random6": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target7": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength7": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random7": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target8": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength8": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random8": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target9": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength9": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random9": ("INT", {"default":-1,"min": -1,"max": 1000}), + "target10": ("INT", {"default":None,"min": 0,"max": 100000000}), + "strength10": ("FLOAT", {"default":1.0,"min": 0,"max": 10}), + "random10": ("INT", {"default":-1,"min": -1,"max": 1000}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None, target4=None, strength4=None, random4=None, target5=None, strength5=None, random5=None, + target6=None, strength6=None, random6=None, target7=None, strength7=None, random7=None, + target8=None, strength8=None, random8=None, target9=None, strength9=None, random9=None, target10=None, strength10=None, random10=None): + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3}, + {"t":target4,"s":strength4,"r":random4}, + {"t":target5,"s":strength5,"r":random5}, + {"t":target6,"s":strength6,"r":random6}, + {"t":target7,"s":strength7,"r":random7}, + {"t":target8,"s":strength8,"r":random8}, + {"t":target9,"s":strength9,"r":random9}, + {"t":target10,"s":strength10,"r":random10} + ] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(parsedInputs)] + + + #Main Function + def exec(self, string, delimiter, target1=None, strength1=None, random1=None, target2=None, strength2=None, random2=None, + target3=None, strength3=None, random3=None, target4=None, strength4=None, random4=None, target5=None, strength5=None, random5=None, + target6=None, strength6=None, random6=None, target7=None, strength7=None, random7=None, + target8=None, strength8=None, random8=None, target9=None, strength9=None, random9=None, target10=None, strength10=None, random10=None): + + #Parse inputs for clarity + parsedInputs = [ + {"t":target1,"s":strength1,"r":random1}, + {"t":target2,"s":strength2,"r":random2}, + {"t":target3,"s":strength3,"r":random3}, + {"t":target4,"s":strength4,"r":random4}, + {"t":target5,"s":strength5,"r":random5}, + {"t":target6,"s":strength6,"r":random6}, + {"t":target7,"s":strength7,"r":random7}, + {"t":target8,"s":strength8,"r":random8}, + {"t":target9,"s":strength9,"r":random9}, + {"t":target10,"s":strength10,"r":random10} + ] + return Util.common_string_repo_function(string,delimiter,parsedInputs) + +class SimpleStringRepositoryLargeCompact: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"multiline": True}), + "delimiter": ([",", " "], {}), + }, + "optional": { + "target1": ("STRING", {"default":'0,1,-1'}), + "target2": ("STRING", {"default":'0,1,-1'}), + "target3": ("STRING", {"default":'0,1,-1'}), + "target4": ("STRING", {"default":'0,1,-1'}), + "target5": ("STRING", {"default":'0,1,-1'}), + "target6": ("STRING", {"default":'0,1,-1'}), + "target7": ("STRING", {"default":'0,1,-1'}), + "target8": ("STRING", {"default":'0,1,-1'}), + "target9": ("STRING", {"default":'0,1,-1'}), + "target10": ("STRING", {"default":'0,1,-1'}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("parsed_string",) + CATEGORY = "utils" + FUNCTION = "exec" + OUTPUT_NODE = True + + def IS_CHANGED(self, string, delimiter, target1=None, target2=None, target3=None, target4=None, target5=None, + target6=None, target7=None, target8=None, target9=None, target10=None): + targets = [target1,target2,target3,target4,target5,target6,target7,target8,target9,target10] + return (True, float("NaN"))[Util.areThereTargetsWithRandomness(Util.parseCompactInputs(targets))] + + #Main Function + def exec(self, string, delimiter, target1=None, target2=None, target3=None, target4=None, target5=None, + target6=None, target7=None, target8=None, target9=None, target10=None): + targets = [target1,target2,target3,target4,target5,target6,target7,target8,target9,target10] + return Util.common_string_repo_function(string,delimiter,Util.parseCompactInputs(targets)) \ No newline at end of file diff --git a/py/util.py b/py/util.py new file mode 100644 index 0000000..988814b --- /dev/null +++ b/py/util.py @@ -0,0 +1,149 @@ +import re +import random + +class Util: + @staticmethod + def var_dump(var, prefix=''): + """ + You know you're a php developer when the first thing you ask for + when learning a new language is 'Where's var_dump?????' + """ + my_type = '[' + var.__class__.__name__ + '(' + str(len(var)) + ')]:' + print(prefix, my_type, sep='') + prefix += ' ' + for i in var: + if type(i) in (list, tuple, dict, set): + Util.var_dump(i, prefix) + else: + if isinstance(var, dict): + print(prefix, i, ': (', var[i].__class__.__name__, ') ', var[i], sep='') + else: + print(prefix, '(', i.__class__.__name__, ') ', i, sep='') + + @staticmethod + def removing_leading_whitespaces(text): + """ + Remove leading whitespaces + """ + return re.sub(r"^\s+","",text) + + @staticmethod + def areThereTargetsWithRandomness(inputs): + for tuple in inputs: + if(tuple["r"] == -1 and tuple["t"] != None): + return True + return False + + @staticmethod + def parseCompactInputs(inputs): + newInputs = [] + for string in inputs: + parsedString = string.split(',') + newTuple = { + "t":int(parsedString[0]) + } + + if(len(parsedString) >= 2): + newTuple["s"] = float(parsedString[1]) + else: + newTuple["s"] = 1.0 + + if(len(parsedString) >= 3): + newTuple["r"] = int(parsedString[2]) + else: + newTuple["r"] = -1 + + newInputs.append(newTuple) + return newInputs + + @staticmethod + def common_string_repo_function(string, delimiter, inputs): + """ + Common function which is actually the string repo main function. + The classes are just wrappers around it with a different number of similar inputs. + """ + orphanStrings = [] + newDict = {} + parsingIndex = 0 + parsedString = string.splitlines() + + # Parse all strings, separated by newline. + # A properly formatted string should either have ALL strings with unique numeric identifiers, or none at all. + # Strings without a leading number will be assigned one based on their line number. + # In case of a mix of both, unexpected behavior may occur - strings without an identifier will be assigned one, and subsequent duplicate numeric identifiers are ignored. + for line in parsedString: + + existingId = None + currentString = None + parsingIndex += 1 + + parsedLine = line.split(".") + if(len(parsedLine) != 1): + existingId = parsedLine[0] + currentString = parsedLine[1] + else: + currentString = parsedLine[0] + + Util.removing_leading_whitespaces(currentString) + + if(not existingId): + existingId = parsingIndex + + existingId = int(existingId) + + if existingId in newDict: + orphanStrings.extend((currentString,)) + else: + newDict[existingId] = currentString + + + # Debug: + # print("Dict:") + # for k,v in newDict.items(): + # print(k, "corresponds to", v) + # print("Orphans:") + # for v in orphanStrings: + # print(v) + + + #Parse all inputs + finalString = '' + for tuple in inputs: + + #Target does not exist! + if(tuple["t"] not in newDict): + # Debug: + #print("target", tuple["t"], "does not exist!") + continue + + #Target exists! + baseStr = newDict[tuple["t"]] + + #Handle randomness, if applicable + if(re.fullmatch("^{.+}$",baseStr)): + baseStr = baseStr[1:][:-1] + baseStr = baseStr.split("|") + + if(tuple["r"] == -1): + tuple["r"] = random.randint(0, len(baseStr)-1) + if(tuple["r"] >= len(baseStr)): + # Debug: + #print("Random string ", tuple["r"], "in target",tuple["t"],baseStr, "does not exist!") + continue + baseStr = baseStr[tuple["r"]] + + #Handle Strength + if(not tuple["s"]): + # Debug: + #print("Target",tuple["t"], "Has strength 0") + continue + elif(tuple["s"] != 1): + tuple["s"] = round(tuple["s"],2) + baseStr = f'({baseStr}:{tuple["s"]})' + + finalString += baseStr + delimiter + + finalString = finalString[:-1] + # Debug: + #print(finalString) + return {"ui": {"text": (finalString,)}, "result": (finalString,)} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cbab053 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "comfyui_simple_string_repository" +description = "Custom node that allows storing and accessing strings, meant to be parts of a prompt, in a simplified manner. Partially supports dynamic prompt syntax." +version = "1.0.0" +license = "LICENSE" + +[project.urls] +Repository = "https://github.com/IgalOgonov/ComfyUI_Simple_String_Repository" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "io" +DisplayName = "Simple String Repository" +Icon = "" diff --git a/web/js/simpleStringRepository.js b/web/js/simpleStringRepository.js new file mode 100644 index 0000000..771db8d --- /dev/null +++ b/web/js/simpleStringRepository.js @@ -0,0 +1,94 @@ +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; + +// Displays input text on a node + +app.registerExtension({ + name: "io.SimpleStringRepository", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeType?.comfyClass.startsWith('SimpleStringRepository')){ + //TODO Perhaps add stuff + } + }, + async nodeCreated(node, app) { + let nodeClass= node?.comfyClass; + if(nodeClass.startsWith('SimpleStringRepository')){ + let isCompact = node?.comfyClass.endsWith('Compact'); + + let extractParsedStrings = ()=>{ + let str_widget = node.widgets[0]; + let str = str_widget.value; + return str.split('\n'); + } + + //Adds numerical indexes to strings, does not destroy or change existing ones. + node.addWidget("button", "✍ Add Missing Indexes", null, () => { + let currentStrings = extractParsedStrings(); + let map = {} + let orphans = {}; + for(let i in currentStrings){ + let index = null; + let str = currentStrings[i]; + if(str.includes('.')){ + index = str.split('.')[0]; + str = str.split('.')[1]; + } + if(!index || map[index]) + orphans[i] = str; + else + map[index] = str; + } + let newIndex = 1; + for(let i in orphans){ + while(map[newIndex] !== undefined) + newIndex++; + currentStrings[i] = newIndex + '.' + orphans[i]; + map[newIndex] = orphans[i]; + } + node.widgets[0].value = currentStrings.join('\n'); + }, { serialize: false }); + + //Regenerates all string indexes, potentially changing existing ones. + node.addWidget("button", "⚠ Regenerate All Indexes", null, () => { + let currentStrings = extractParsedStrings(); + for(let i in currentStrings){ + if(currentStrings[i].includes('.')) + currentStrings[i] = currentStrings[i].split('.')[1]; + currentStrings[i] = (i-0+1) +'.'+ currentStrings[i]; + } + node.widgets[0].value = currentStrings.join('\n'); + }, { serialize: false }); + + //Strips all strings of their indexes + node.addWidget("button", "❌ Remove All Indexes", null, () => { + let currentStrings = extractParsedStrings(); + for(let i in currentStrings){ + if(currentStrings[i].includes('.')) + currentStrings[i] = currentStrings[i].split('.')[1]; + } + node.widgets[0].value = currentStrings.join('\n'); + }, { serialize: false }); + + //Resets Parameters (Target/Strength/Randomness) + node.addWidget("button", "↺ Reset Target Parameters", null, () => { + for(let i in node.widgets){ + let widget = node.widgets[i]; + switch(isCompact){ + case true: + if(widget.name.match(/^target\d+$/)) + widget.value = '0,1,-1'; + break; + case false: + if(widget.name.match(/^target\d+$/)) + widget.value = 0; + if(widget.name.match(/^strength\d+$/)) + widget.value = 1; + if(widget.name.match(/^random\d+$/)) + widget.value = -1; + break; + } + } + }, { serialize: false }); + } + }, +});