├── .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 |
--------------------------------------------------------------------------------