├── .gitattributes ├── examples └── image_edit_workflow.png ├── requirements.txt ├── pyproject.toml ├── README.md ├── CHANGELOG.md ├── install.py └── __init__.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /examples/image_edit_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThereforeGames/ComfyUI-Unprompted/HEAD/examples/image_edit_workflow.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | unprompted@git+https://github.com/ThereforeGames/unprompted==11.2.0 2 | opencv-python 3 | nltk 4 | pattern@git+https://github.com/NicolasBizzozzero/pattern -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-unprompted" 3 | description = "A node that processes input text with the [a/Unprompted templating language](https://github.com/ThereforeGames/unprompted).\n" 4 | version = "0.2.2" 5 | license = "LICENSE" 6 | dependencies = ["unprompted@git+https://github.com/ThereforeGames/unprompted", "opencv-python", "nltk", "pattern@git+https://github.com/NicolasBizzozzero/pattern"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/ThereforeGames/ComfyUI-Unprompted" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "thereforegames" 14 | DisplayName = "ComfyUI-Unprompted" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unprompted Node for ComfyUI 2 | 3 | https://github.com/ThereforeGames/ComfyUI-Unprompted/assets/95403634/613c3c0f-5066-41c5-8aea-042afd1c7d47 4 | 5 | This is a ComfyUI node that processes your input text with the Unprompted templating language. Early alpha release. 6 | 7 | Please refer to the [main Unprompted repository](https://github.com/ThereforeGames/unprompted) for more information. Detailed documentation is available there. Thank you! 8 | 9 | ### Installation 10 | 11 | Download the repository (or `git clone`) and move the files to your `comfyui/custom_nodes/ComfyUI-Unprompted` folder. Restart ComfyUI and you should now have access to the Unprompted node. 12 | 13 | > ⚠️ **Note:** Some shortcodes were specifically designed for use with the Automatic1111 WebUI and are not compatible with this ComfyUI node. This node is primarily intended for text-based operations such as wildcards. 14 | 15 | ## Frequently Asked Questions 16 | 17 |
Where are my Unprompted templates located? 18 | 19 | Your Unprompted templates are located in the `ComfyUI/venv/Lib/site-packages/unprompted/templates` folder. You can `[call]` files from this location using the node, e.g. `[call common/examples/human/main]`. 20 | 21 |
22 | 23 |
What are `string_prefix` and `string_affix`? 24 | 25 | The prefix is added to the beginning of the main `string_field` box, and the affix is added to the end. These are simply for convenience. 26 | 27 |
28 | 29 |
How do I edit images with the Unprompted node? 30 | 31 | As of v0.2.0, you can use the `anything` input to pass an image to the Unprompted node. To test this, please try the following workflow: 32 | 33 | 34 | 35 | Note the use of the `anything` input and the `set_anything_to` widget. We set the image to the `comfy_var` variable, which is then accessed via the `[image_edit]` shortcode: 36 | 37 | ``` 38 | [image_edit input="comfy_var" add_noise=500] 39 | ``` 40 | 41 | We also ensure that the `return_image_var` widget refers to `comfy_var` so that we can see our changes to the image in the Preview Image node. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 |
0.3.0 - 13 November 2024 5 | 6 | ### Added 7 | - New node `Set Variable Rack`: Allows you to feed many inputs into custom Unprompted variables 8 | - New setting `goodbye_routines`: Runs Unprompted's memory-freeing methods, recommend enabling this on the final Unprompted node in your workflow 9 | 10 | ### Fixed 11 | - Error related to the `anything` input in newer versions of ComfyUI 12 | 13 |
14 | 15 |
0.2.2 - 29 July 2024 16 | 17 | ### Added 18 | - Support Unprompted v11.2.0 19 | 20 |
21 | 22 |
0.2.1 - 23 June 2024 23 | 24 | ### Fixed 25 | - Resolved an issue with `install.py` package upgrade logic 26 | 27 |
28 | 29 |
0.2.0 - 23 June 2024 30 | 31 | ### Added 32 | - New input `anything`: connect any node to this input (for example, an image) and it will be made accessible in your Unprompted string as a variable 33 | - New widget `set_anything_to`: the variable name to use for the `anything` input (defaults to `comfy_var`) 34 | - New output `IMAGE`: Unprompted can now return an image contained in a variable of your choosing (in the future, I may extend this to additional data types - but for now I think `STRING`and `IMAGE` are the most useful) 35 | - New widget `return_image_var`: the variable that contains the output image (defaults to `comfy_var`) 36 | - The Unprompted object's `webui` variable is now `comfy`, making it easier for shortcodes to implement ComfyUI support 37 | 38 | ### Changed 39 | - The Node version is now shown in the header instead of the Unprompted language version 40 | 41 |
42 | 43 |
0.1.1 - 7 June 2024 44 | 45 | ### Fixed 46 | - Speculative fixes for ComfyUI Manager support 47 | 48 |
49 | 50 |
0.1.0 - 5 June 2024 51 | 52 | ### Added 53 | - New setting `always_rerun` to force the node to re-run even when the input data hasn't changed 54 | 55 |
56 | 57 |
0.0.1 - 4 June 2024 58 | 59 | ### Added 60 | - Initial release 61 | 62 |
-------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | print("### ComfyUI-Unprompted: Check dependencies") 2 | import os, re, threading, locale, subprocess, sys, pkg_resources 3 | from packaging import version 4 | 5 | this_path = os.path.dirname(os.path.realpath(__file__)) 6 | debug = False 7 | 8 | # Code for pip_install and is_installed comes from ComfyUI-Impact-Pack (install.py) 9 | # (Why doesn't ComfyUI provide these functions natively?) 10 | 11 | if "python_embeded" in sys.executable or "python_embedded" in sys.executable: 12 | pip_install = [sys.executable, "-s", "-m", "pip", "install", "--upgrade"] 13 | else: 14 | pip_install = [sys.executable, "-m", "pip", "install", "--upgrade"] 15 | 16 | 17 | def handle_stream(stream, is_stdout): 18 | stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') 19 | 20 | for msg in stream: 21 | if is_stdout: 22 | print(msg, end="", file=sys.stdout) 23 | else: 24 | print(msg, end="", file=sys.stderr) 25 | 26 | 27 | def process_wrap(cmd_str, cwd=None, handler=None): 28 | print(f"[Impact Pack] EXECUTE: {cmd_str} in '{cwd}'") 29 | process = subprocess.Popen(cmd_str, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1) 30 | 31 | if handler is None: 32 | handler = handle_stream 33 | 34 | stdout_thread = threading.Thread(target=handler, args=(process.stdout, True)) 35 | stderr_thread = threading.Thread(target=handler, args=(process.stderr, False)) 36 | 37 | stdout_thread.start() 38 | stderr_thread.start() 39 | 40 | stdout_thread.join() 41 | stderr_thread.join() 42 | 43 | return process.wait() 44 | 45 | 46 | pip_list = None 47 | 48 | 49 | def get_installed_packages(): 50 | global pip_list 51 | 52 | if pip_list is None: 53 | try: 54 | result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True) 55 | pip_list = set([line.split()[0].lower() for line in result.split('\n') if line.strip()]) 56 | except subprocess.CalledProcessError as e: 57 | print(f"[ComfyUI-Unprompted] Failed to retrieve the information of installed pip packages.") 58 | return set() 59 | 60 | return pip_list 61 | 62 | 63 | def is_installed(name): 64 | name = name.strip().split("@")[0] # Split the name at '@' and take the first part 65 | pattern = r'([^<>!=]+)([<>!=]=?)' 66 | match = re.search(pattern, name) 67 | 68 | if match: 69 | name = match.group(1) 70 | 71 | result = name.lower() in get_installed_packages() 72 | return result 73 | 74 | 75 | requirements = os.path.join(this_path, "requirements.txt") 76 | with open(requirements) as file: 77 | for package in file: 78 | try: 79 | # package_with_comment = package.split("#", 1) 80 | # package = package_with_comment[0].strip() 81 | # reason = package_with_comment[1].strip() 82 | 83 | if "==" in package: 84 | package_name, package_version = package.split("==") 85 | if "@" in package_name: 86 | package = package_name 87 | package_name = package_name.strip().split("@")[0] 88 | try: 89 | installed_version = pkg_resources.get_distribution(package_name).version 90 | if version.parse(installed_version) < version.parse(package_version): 91 | print(f"Upgrading `{package_name}` from {installed_version} to {package_version}") 92 | process_wrap(pip_install + [package]) 93 | except pkg_resources.DistributionNotFound: 94 | # Package is not installed, install it 95 | process_wrap(pip_install + [package]) 96 | elif not is_installed(package): 97 | process_wrap(pip_install + [package]) 98 | except Exception as e: 99 | print(e) 100 | print(f"(ERROR) Failed to install {package} dependency for Unprompted.") 101 | pass 102 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | def do_install(): 2 | import importlib, os 3 | spec = importlib.util.spec_from_file_location("unprompted_install", os.path.join(os.path.dirname(__file__), "install.py")) 4 | unprompted_install = importlib.util.module_from_spec(spec) 5 | spec.loader.exec_module(unprompted_install) 6 | 7 | 8 | do_install() 9 | 10 | # Prep main object 11 | import inspect, os, sys, time 12 | import unprompted.lib_unprompted.shared 13 | 14 | module_path = os.path.dirname(os.path.dirname(inspect.getfile(unprompted.lib_unprompted.shared.Unprompted))) 15 | 16 | # Add the directory to your system's path 17 | sys.path.insert(0, f"{module_path}") 18 | 19 | Unprompted = unprompted.lib_unprompted.shared.Unprompted(module_path) 20 | Unprompted.webui = "comfy" 21 | Unprompted.NODE_VERSION = "0.3.0" 22 | 23 | 24 | class UnpromptedNode: 25 | """ 26 | A example node 27 | 28 | Class methods 29 | ------------- 30 | INPUT_TYPES (dict): 31 | Tell the main program input parameters of nodes. 32 | IS_CHANGED: 33 | optional method to control when the node is re executed. 34 | 35 | Attributes 36 | ---------- 37 | RETURN_TYPES (`tuple`): 38 | The type of each element in the output tulple. 39 | RETURN_NAMES (`tuple`): 40 | Optional: The name of each output in the output tulple. 41 | FUNCTION (`str`): 42 | The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() 43 | OUTPUT_NODE ([`bool`]): 44 | If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. 45 | The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. 46 | Assumed to be False if not present. 47 | CATEGORY (`str`): 48 | The category the node should appear in the UI. 49 | execute(s) -> tuple || None: 50 | The entry point method. The name of this method must be the same as the value of property `FUNCTION`. 51 | For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. 52 | """ 53 | 54 | def __init__(self): 55 | pass 56 | 57 | @classmethod 58 | def INPUT_TYPES(s): 59 | 60 | # wildcard trick is taken from pythongossss's 61 | class AnyType(str): 62 | 63 | def __ne__(self, __value: object) -> bool: 64 | return False 65 | 66 | """ 67 | Return a dictionary which contains config for all input fields. 68 | """ 69 | return { 70 | "required": { 71 | "string_field": ( 72 | "STRING", 73 | { 74 | "multiline": True, # True if you want the field to look like the one on the ClipTextEncode node 75 | }), 76 | }, 77 | "optional": { 78 | "anything": (AnyType("*"), { 79 | "default": None 80 | }), 81 | "set_anything_to": ("STRING", { 82 | "default": "comfy_var" 83 | }), 84 | "return_image_var": ("STRING", { 85 | "default": "comfy_var", 86 | }), 87 | "always_rerun": ("BOOLEAN", { 88 | "default": False 89 | }), 90 | "goodbye_routines": ("BOOLEAN", { 91 | "default": False 92 | }), 93 | "string_prefix": ("STRING", { 94 | "forceInput": True, 95 | "default": "" 96 | }), 97 | "string_suffix": ("STRING", { 98 | "forceInput": True, 99 | "default": "" 100 | }), 101 | } 102 | } 103 | 104 | RETURN_TYPES = ( 105 | "STRING", 106 | "IMAGE", 107 | ) 108 | #RETURN_NAMES = ("image_output_name",) 109 | 110 | FUNCTION = "do_unprompted" 111 | 112 | #OUTPUT_NODE = False 113 | 114 | CATEGORY = "unprompted" 115 | 116 | def do_unprompted(self, **kwargs): 117 | 118 | if kwargs.get("anything") is not None: 119 | Unprompted.shortcode_user_vars[kwargs.get("set_anything_to")] = kwargs.get("anything") 120 | 121 | result = Unprompted.start(kwargs.get("string_prefix", "") + kwargs.get("string_field", "") + kwargs.get("string_suffix", "")) 122 | 123 | image_result = None 124 | if kwargs.get("return_image_var"): 125 | image_var = kwargs.get("return_image_var") 126 | if image_var in Unprompted.shortcode_user_vars: 127 | image_result = Unprompted.shortcode_user_vars[image_var] 128 | 129 | if kwargs.get("goodbye_routines"): 130 | # Cleanup routines 131 | Unprompted.shortcode_user_vars = {} 132 | Unprompted.cleanup() 133 | Unprompted.goodbye() 134 | 135 | return ( 136 | result, 137 | image_result, 138 | ) 139 | 140 | """ 141 | The node will always be re executed if any of the inputs change but 142 | this method can be used to force the node to execute again even when the inputs don't change. 143 | You can make this node return a number or a string. This value will be compared to the one returned the last time the node was 144 | executed, if it is different the node will be executed again. 145 | This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash 146 | changes between executions the LoadImage node is executed again. 147 | """ 148 | 149 | #@classmethod 150 | def IS_CHANGED(**kwargs): 151 | if kwargs.get("always_rerun") is True: 152 | return str(time.time()) 153 | 154 | 155 | class UnpromptedSetRack: 156 | 157 | def __init__(self): 158 | pass 159 | 160 | @classmethod 161 | def INPUT_TYPES(s): 162 | 163 | # wildcard trick is taken from pythongossss's 164 | class AnyType(str): 165 | 166 | def __ne__(self, __value: object) -> bool: 167 | return False 168 | 169 | """ 170 | Return a dictionary which contains config for all input fields. 171 | """ 172 | return { 173 | "optional": { 174 | "input_a": (AnyType("*"), { 175 | "default": None 176 | }), 177 | "input_b": (AnyType("*"), { 178 | "default": None 179 | }), 180 | "input_c": (AnyType("*"), { 181 | "default": None 182 | }), 183 | "input_d": (AnyType("*"), { 184 | "default": None 185 | }), 186 | "input_e": (AnyType("*"), { 187 | "default": None 188 | }), 189 | "input_f": (AnyType("*"), { 190 | "default": None 191 | }), 192 | "input_g": (AnyType("*"), { 193 | "default": None 194 | }), 195 | "input_h": (AnyType("*"), { 196 | "default": None 197 | }), 198 | "input_i": (AnyType("*"), { 199 | "default": None 200 | }), 201 | "input_j": (AnyType("*"), { 202 | "default": None 203 | }), 204 | "input_k": (AnyType("*"), { 205 | "default": None 206 | }), 207 | "input_l": (AnyType("*"), { 208 | "default": None 209 | }), 210 | "input_m": (AnyType("*"), { 211 | "default": None 212 | }), 213 | "input_n": (AnyType("*"), { 214 | "default": None 215 | }), 216 | "input_o": (AnyType("*"), { 217 | "default": None 218 | }), 219 | "input_p": (AnyType("*"), { 220 | "default": None 221 | }), 222 | "input_q": (AnyType("*"), { 223 | "default": None 224 | }), 225 | "input_r": (AnyType("*"), { 226 | "default": None 227 | }), 228 | "input_s": (AnyType("*"), { 229 | "default": None 230 | }), 231 | "input_t": (AnyType("*"), { 232 | "default": None 233 | }), 234 | "input_u": (AnyType("*"), { 235 | "default": None 236 | }), 237 | "input_v": (AnyType("*"), { 238 | "default": None 239 | }), 240 | "input_w": (AnyType("*"), { 241 | "default": None 242 | }), 243 | "input_x": (AnyType("*"), { 244 | "default": None 245 | }), 246 | "input_y": (AnyType("*"), { 247 | "default": None 248 | }), 249 | "input_z": (AnyType("*"), { 250 | "default": None 251 | }), 252 | "var_a": ("STRING", { 253 | "default": "var_a" 254 | }), 255 | "var_b": ("STRING", { 256 | "default": "var_b" 257 | }), 258 | "var_c": ("STRING", { 259 | "default": "var_c" 260 | }), 261 | "var_d": ("STRING", { 262 | "default": "var_d" 263 | }), 264 | "var_e": ("STRING", { 265 | "default": "var_e" 266 | }), 267 | "var_f": ("STRING", { 268 | "default": "var_f" 269 | }), 270 | "var_g": ("STRING", { 271 | "default": "var_g" 272 | }), 273 | "var_h": ("STRING", { 274 | "default": "var_h" 275 | }), 276 | "var_i": ("STRING", { 277 | "default": "var_i" 278 | }), 279 | "var_j": ("STRING", { 280 | "default": "var_j" 281 | }), 282 | "var_k": ("STRING", { 283 | "default": "var_k" 284 | }), 285 | "var_l": ("STRING", { 286 | "default": "var_l" 287 | }), 288 | "var_m": ("STRING", { 289 | "default": "var_m" 290 | }), 291 | "var_n": ("STRING", { 292 | "default": "var_n" 293 | }), 294 | "var_o": ("STRING", { 295 | "default": "var_o" 296 | }), 297 | "var_p": ("STRING", { 298 | "default": "var_p" 299 | }), 300 | "var_q": ("STRING", { 301 | "default": "var_q" 302 | }), 303 | "var_r": ("STRING", { 304 | "default": "var_r" 305 | }), 306 | "var_s": ("STRING", { 307 | "default": "var_s" 308 | }), 309 | "var_t": ("STRING", { 310 | "default": "var_t" 311 | }), 312 | "var_u": ("STRING", { 313 | "default": "var_u" 314 | }), 315 | "var_v": ("STRING", { 316 | "default": "var_v" 317 | }), 318 | "var_w": ("STRING", { 319 | "default": "var_w" 320 | }), 321 | "var_x": ("STRING", { 322 | "default": "var_x" 323 | }), 324 | "var_y": ("STRING", { 325 | "default": "var_y" 326 | }), 327 | "var_z": ("STRING", { 328 | "default": "var_z" 329 | }), 330 | } 331 | } 332 | 333 | CATEGORY = "unprompted" 334 | RETURN_TYPES = ("STRING", ) 335 | #RETURN_NAMES = ("image_output_name",) 336 | 337 | FUNCTION = "set_unprompted" 338 | 339 | def set_unprompted(self, **kwargs): 340 | 341 | for key in ["input_a", "input_b", "input_c", "input_d", "input_e", "input_f", "input_g", "input_h", "input_i", "input_j", "input_k", "input_l", "input_m", "input_n", "input_o", "input_p", "input_q", "input_r", "input_s", "input_t", "input_u", "input_v", "input_w", "input_x", "input_y", "input_z"]: 342 | # Match the input to the var 343 | if kwargs.get(key) is not None: 344 | Unprompted.shortcode_user_vars[kwargs.get(f"var_{key[-1]}")] = kwargs.get(key) 345 | 346 | image_result = None 347 | if kwargs.get("return_image_var"): 348 | image_var = kwargs.get("return_image_var") 349 | if image_var in Unprompted.shortcode_user_vars: 350 | image_result = Unprompted.shortcode_user_vars[image_var] 351 | 352 | return ("") 353 | 354 | 355 | # Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension 356 | # WEB_DIRECTORY = "./somejs" 357 | 358 | # A dictionary that contains all nodes you want to export with their names 359 | # NOTE: names should be globally unique 360 | NODE_CLASS_MAPPINGS = { 361 | "Unprompted": UnpromptedNode, 362 | "UnpromptedSetRack": UnpromptedSetRack, 363 | } 364 | 365 | # A dictionary that contains the friendly/humanly readable titles for the nodes 366 | NODE_DISPLAY_NAME_MAPPINGS = {"Unprompted": f"Unprompted v{Unprompted.NODE_VERSION}", "UnpromptedSetRack": "Set Variable Rack"} 367 | --------------------------------------------------------------------------------