From 8a8195738f15d6833aa29a62df25e679d073f413 Mon Sep 17 00:00:00 2001 From: noembryo Date: Wed, 14 Jan 2026 21:09:55 +0200 Subject: [PATCH] 1.3.0 --- README.md | 116 +++++++++++++++++++----------- __init__.py | 47 ++++++++++-- nodes.py | 152 +++++++++++++++++++++++++++++++++++---- pyproject.toml | 9 +-- web/js/noEmbryo_utils.js | 112 +++++++++++++++++++++++++++++ 5 files changed, 371 insertions(+), 65 deletions(-) create mode 100644 web/js/noEmbryo_utils.js diff --git a/README.md b/README.md index f7b85f4..e24bf05 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # noEmbryo Nodes A diverse set of nodes for ComfyUI. -- [PromptTermList (1-6)](#prompttermlist-1-6) +- [Json Prompt Loader](#json-prompt-loader) - [Resolution Scale](#resolution-scale) - [Regex Text Chopper](#regex-text-chopper) - [Auto Save Workflow](#auto-save-workflow) +- [PromptTermList (1-6)](#prompttermlist-1-6) You can access them through "Add node > noEmbryo" submenu. @@ -11,46 +12,41 @@ You can access them through "Add node > noEmbryo" submenu. [![License: MIT][MIT]](LICENSE) --- -## PromptTermList (1-6) -These are some nodes that help with the creation of Prompts inside [ComfyUI](https://github.com/comfyanonymous/ComfyUI). +## Json Prompt Loader +![Example](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/JsonLoader.png) +A node that can load a `.json` file with `item:prompt` pairs and outputs the selected item's prompt, while combining it with a custom prompt. +It can load `.json` files from any directory, not just the node's directory. +For the custom text integration, there is a variable (can be specified by the user), that can be used in the item's prompt text to insert the custom text anywhere in the body of the prompt. -![Demo workflow](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/Screen2.png) - - - -### Usage -Every one of the 6 nodes have a different `json` file that stores its Prompt Terms in "label"/"value" pairs. -The "label" part is what we see at the node's dropdown menu, and the "value" part is what it produces at its `Term` output when we run a generation job. - -These `json` files are located inside the `TermLists` directory, in the node's folder. -There are two ways to add a new term. -- From within ComfyUI: - - Connect a text box to the node's `text` input. - - Write the "label"/"value" part in the box using the following format: - ``` - label=Descriptive text - value=masterpiece, artful and cozy - ``` - - Enable the `store_input` switch. - - Run a generation job. - - Refresh the page. -- Manually: - - Just open the `json` file and add/remove/change entries. - - Ofcourse we must be very careful with this, to keep the `json` format of labels/values (with the appropriate commas), otherwise the file will not be parsed. - -This `text` input is also useful if we want to manually add something *after* our term, or as the *only* term if we select the `None` label of the dropdown. -The `strength` value is changing the impact of the term by using the parenthesis format like this: `(a great term:1.3)` - -We can delete a term by sending an empty value to the `text` input like this: -``` -label=The label to be deleted -value= -``` +- **UI controls** + - **json_path** is the path to the `.json` file. + It can be an absolute path of your hard drive, or a relative path to the ComfyUI's installation directory. + - **selected_item** selects one of the `.json` file's items (prompts). + - **variable** is the variable that will be used to insert the custom text into the item's prompt text. + - **custom_text** is the custom text that will be inserted into the item's prompt text. +- **Advanced Usage** + ![Example](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/JsonLoader2.png) + - **Adding new items** + To add a new item, we have to write it in the `custom_text` input, using the following format: + ``` + item=The prompt's title/keyword + value=The prompt's text, including our variable + ``` + - **Updating items** + To update an item, we use the same method, just write the specific item we want to update. + ``` + item=An existing title/keyword + value=The new prompt's text, including our variable + ``` + - **Deleting items** + To delete an item, we use the same format, but we just leave the `value` empty. + ``` + item=An existing title/keyword + value= + ``` + All of these methods will update the `.json` file, and the node will output a message about the success or failure of the operation. + We can also load an empty `.json`file and use this method to populate it. --- ## Resolution Scale ![Demo workflow](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/res_scale1.png) @@ -77,6 +73,46 @@ This node can save the current workflow to a `.json` file, every time a generati - **status** outputs a text with the status of the saving process (the path of the saved file, or an error message). - **✳️trigger** is a dummy output that's used to trigger the saving process, even if nothing is connected to the node. +--- +## PromptTermList 1-6 + +**The PromptTermList nodes are now obsolete, and can mostly be replaced by the [Json Prompt Loader](#json-prompt-loader) node. +I won't remove them for compatibility reasons, but I would recommend using the [JsonPromptLoader](#json-prompt-loader) node instead.** + +![Demo workflow](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/Screen2.png) +These are some nodes that help with the creation of Prompts inside [ComfyUI](https://github.com/comfyanonymous/ComfyUI). + +### Usage + +Every one of these 6 nodes have a different `json` file that stores its Prompt Terms in "label"/"value" pairs. +The "label" part is what we see at the node's dropdown menu, and the "value" part is what it produces at its `Term` output when we run a generation job. + +These `json` files are located inside the `TermLists` directory, in the node's folder. +There are two ways to add a new term. +- From within ComfyUI: + - Connect a text box to the node's `text` input. + - Write the "label"/"value" part in the box using the following format: + ``` + label=Descriptive text + value=masterpiece, artful and cozy + ``` + - Enable the `store_input` switch. + - Run a generation job. + - Refresh the page. +- Manually: + - Just open the `json` file and add/remove/change entries. + + Ofcourse we must be very careful with this, to keep the `json` format of labels/values (with the appropriate commas), otherwise the file will not be parsed. + +This `text` input is also useful if we want to manually add something *after* our term, or as the *only* term if we select the `None` label of the dropdown. +The `strength` value is changing the impact of the term by using the parenthesis format like this: `(a great term:1.3)` + +We can delete a term by sending an empty value to the `text` input like this: +``` +label=The label to be deleted +value= +``` + --- ## Installation * Use the ComfyUI Manager @@ -85,6 +121,6 @@ This node can save the current workflow to a `.json` file, every time a generati * git clone https://github.com/noembryo/ComfyUI-noEmbryo.git * Restart ComfyUI - +--- [Python]:https://img.shields.io/badge/Made%20with-Python-1f425f.svg [MIT]:https://img.shields.io/badge/License-MIT-green.svg diff --git a/__init__.py b/__init__.py index 1918d2c..f99e10a 100644 --- a/__init__.py +++ b/__init__.py @@ -10,18 +10,23 @@ import os import io from os.path import isfile, join, isdir import importlib -from .nodes import LISTS_PATH, __author__, __version__ +from aiohttp import web +from server import PromptServer + +from .nodes import LISTS_PATH, JsonPromptLoader, __author__, __version__ print(f"### Loading: {__author__} nodes v{__version__}") +WEB_DIRECTORY = "./web/js" + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + node_list = [ "nodes", # "load_image_from_dir", ] -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - for module_name in node_list: imported_module = importlib.import_module(".{}".format(module_name), __name__) @@ -35,9 +40,39 @@ for i in range(6): # create TermList1-6 if not exists file_name = join(LISTS_PATH, "TermList{}.json".format(i + 1)) if not isfile(file_name): with io.open(file_name, "w+", encoding="utf-8", newline="\n") as f: + # noinspection PyTypeChecker json.dump({"None": ""}, f, indent=4) -WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", - "WEB_DIRECTORY", + "WEB_DIRECTORY", "setup", ] + +# Add custom route for JSON loading +try: + @PromptServer.instance.routes.get('/noembryo/get_json') + async def get_json(request): + path = request.query.get('path', '') + if path: + try: + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + JsonPromptLoader.load_data(path) # Update Python data + return web.json_response(data) + except Exception as e: + return web.json_response({"error": str(e)}) + return web.json_response({}) +except ImportError: + pass # Server not available + +# def setup(): +# @PromptServer.instance.routes.get('/noembryo/get_json') +# async def get_json(request): +# path = request.query.get('path', '') +# if path: +# try: +# with open(path, 'r', encoding='utf-8') as f: +# data = json.load(f) +# return web.json_response(data) +# except Exception as e: +# return web.json_response({"error": str(e)}) +# return web.json_response({}) diff --git a/nodes.py b/nodes.py index 309a9c1..a2675d1 100644 --- a/nodes.py +++ b/nodes.py @@ -1,6 +1,6 @@ import os, re, io import json -from os.path import realpath, join, dirname, isabs +from os.path import realpath, join, dirname, isabs, splitext, basename from datetime import datetime import folder_paths @@ -12,12 +12,133 @@ MANIFEST = {"name": "noEmbryo Nodes", "license": "MIT", } __author__ = "noEmbryo" -__version__ = "1.2.0" +__version__ = "1.3.0" # LISTS_PATH = realpath("./custom_nodes/ComfyUI-noEmbryo/TermLists/") LISTS_PATH = join(dirname(realpath(__file__)), "TermLists") +class JsonPromptLoader: + data = {} + data_labels = ["None"] + json_path = "" + + def __init__(self): + super(JsonPromptLoader, self).__init__() + self.name = type(self).__name__ + + @classmethod + def load_data(cls, json_path): + cls.json_path = "" + if not splitext(json_path)[1].lower() == ".json": + return + if json_path: + try: + with io.open(json_path, mode="r", encoding="utf-8") as f: + cls.data.clear() + cls.data["None"] = "" + cls.data.update(json.load(f)) + cls.data_labels[:] = list(cls.data.keys()) + cls.json_path = json_path + except (FileNotFoundError, json.JSONDecodeError): + cls.data.clear() + cls.data.update({}) + cls.data_labels[:] = ["None"] + if os.stat(json_path).st_size == 0: # empty json files + cls.json_path = json_path + else: # no path given + cls.data.clear() + cls.data.update({}) + cls.data_labels[:] = ["None"] + + @classmethod + def INPUT_TYPES(cls): + return {"required": {"json_path": ("STRING", {"default": "", + "tooltip": "Path to a JSON file with " + "`item`:`prompt` pairs"}), + "selected_item": (cls.data_labels, cls.data), # Options will be updated by JS + "variable": ("STRING", {"default": "{subject}", + "tooltip": "If this variable exists in the selected item's prompt,\n" + "it will be replaced with the custom_prompt text"}), + "custom_prompt": ("STRING", {"multiline": True, "default": "", + "tooltip": "Text to replace the variable in the selected prompt.\n" + "You can also use it to save a new item or update an existing one.\n" + "To do that you should use the following format:\n" + "item=... ...\n" + "value=.... .... ...\n" + "To delete an existing item, use an empty value:\n" + "item=... ...\n" + "value="}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("Prompt",) + FUNCTION = "run" + CATEGORY = "noEmbryo" + + def run(self, json_path, selected_item, variable, custom_prompt): + self.load_data(json_path) + if custom_prompt: + message = self.edit_data(custom_prompt) + if message: # if the custom_prompt was saved successfully + return (message,) + if selected_item in self.data and selected_item != "None": + prompt = self.data[selected_item] + # if variable and "{" + variable + "}" in prompt: + if variable and variable in prompt: + prompt = prompt.replace(variable, custom_prompt) + # prompt = prompt.replace("{" + variable + "}", custom_prompt) + elif custom_prompt: + prompt += " " + custom_prompt + else: + prompt = custom_prompt + return (prompt,) + + def edit_data(self, text): + """ Parses the json values from the custom_prompt and changes the json file + + :type text: str + :param text: The custom_prompt text + """ + lines = text.splitlines() + if len(lines) >= 2: + if all((lines[0].startswith("item="), lines[1].startswith("value="))): + if not self.json_path: + return False + item = lines[0][5:] + lines_txt = "\n".join(lines[1:]) + value = lines_txt[6:] + filename = basename(self.json_path) + if item == "None": # cannot change None + msg = f'{filename}: The item "{item}" cannot be changed!' + return msg + if not value: # delete item + if item in self.data: + del self.data[item] + msg = f'{filename}: The item "{item}" was deleted!' + self.save_json_file() + else: + msg = f'{filename}: The item "{item}" does not exist!' + else: # save/update item + if item in self.data: + msg = f'{filename}: The item "{item}" is updated!' + else: + msg = f'{filename}: The item "{item}" is added!' + self.data[item] = value + self.save_json_file() + return msg + return False + + def save_json_file(self): + with io.open(self.json_path, mode="w", encoding="utf-8") as f: + data = self.data.copy() + if "None" in data: + del data["None"] + # noinspection PyTypeChecker + json.dump(data, f, ensure_ascii=False, indent=4) + + class PromptTermList: idx = 0 data = {"None": ""} @@ -50,7 +171,7 @@ class PromptTermList: cls.load_data_from_json(list_path) term_list = [i[0] for i in cls.data_labels] # noinspection SqlNoDataSourceInspection,SqlResolve - return {"required": {"terms": (term_list,{"tooltip": "Select a term from the " + return {"required": {"terms": (term_list,{"tooltip": "Choose a term from the " "TermList with the " "corresponding number"}), }, "optional": {"text": ("STRING", {"forceInput": True, @@ -115,7 +236,8 @@ class PromptTermList: self.data[label] = value with io.open(join(LISTS_PATH, "TermList{}.json".format(self.idx)), mode="w", encoding="utf-8") as f: - json.dump(self.data, f, indent=4) + # noinspection PyTypeChecker + json.dump(self.data, f, ensure_ascii=False, indent=4) RETURN_TYPES = ("STRING",) RETURN_NAMES = ("Term",) @@ -340,6 +462,7 @@ class AutoSaveWorkflow: # Save workflow to JSON with open(save_path, "w", encoding="utf-8") as f: + # noinspection PyTypeChecker json.dump(workflow_data, f, indent=4) status = f"Workflow saved to: {save_path}" @@ -349,27 +472,26 @@ class AutoSaveWorkflow: return (status,) -NODE_CLASS_MAPPINGS = {"PromptTermList1": PromptTermList1, +NODE_CLASS_MAPPINGS = {f"JsonPromptLoader -{__author__}": JsonPromptLoader, + f"Resolution Scale -{__author__}": ResolutionScale, + f"Regex Text Chopper -{__author__}": RegExTextChopper, + f"Auto Save Workflow -{__author__}": AutoSaveWorkflow, + "PromptTermList1": PromptTermList1, "PromptTermList2": PromptTermList2, "PromptTermList3": PromptTermList3, "PromptTermList4": PromptTermList4, "PromptTermList5": PromptTermList5, "PromptTermList6": PromptTermList6, - f"Resolution Scale /{__author__}": ResolutionScale, - f"Regex Text Chopper /{__author__}": RegExTextChopper, - f"Auto Save Workflow /{__author__}": AutoSaveWorkflow } -NODE_DISPLAY_NAME_MAPPINGS = {"PromptTermList1": f"PromptTermList 1 /{__author__}", +NODE_DISPLAY_NAME_MAPPINGS = {f"JsonPromptLoader -{__author__}": f"Json Prompt Loader /{__author__}", + f"Resolution Scale -{__author__}": f"Resolution Scale /{__author__}", + f"Regex Text Chopper -{__author__}": f"Regex Text Chopper /{__author__}", + f"Auto Save Workflow -{__author__}": f"Auto Save Workflow /{__author__}", + "PromptTermList1": f"PromptTermList 1 /{__author__}", "PromptTermList2": f"PromptTermList 2 /{__author__}", "PromptTermList3": f"PromptTermList 3 /{__author__}", "PromptTermList4": f"PromptTermList 4 /{__author__}", "PromptTermList5": f"PromptTermList 5 /{__author__}", "PromptTermList6": f"PromptTermList 6 /{__author__}", - f"Resolution Scale /{__author__}": - f"Resolution Scale /{__author__}", - f"Regex Text Chopper /{__author__}": - f"Regex Text Chopper /{__author__}", - f"Auto Save Workflow /{__author__}": - f"Auto Save Workflow /{__author__}", } diff --git a/pyproject.toml b/pyproject.toml index 57331b1..bbffc97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,14 @@ [project] -name = "comfyui-noembryo" +name = "noEmbryo" description = """ A collection of nodes for ComyUI. -* "PromptTermList (1-6)" nodes to help with the creation of Prompts. +* "JsonPromptLoader" is a node that enhances a propt using a .json file with `item:prompt` pairs. * "Resolution Scale" is a node that resizes an image using pixels or scale factor. * "Regex Text Chopper" is a node that cuts text based on regex. * "Auto Save Workflow" can save the current workflow to a .json file automatically. +* "PromptTermList (1-6)" nodes to help with the creation of Prompts. """ -version = "1.2.0" +version = "1.3.0" license = {text = "MIT License"} [project.urls] @@ -16,5 +17,5 @@ Repository = "https://github.com/noembryo/ComfyUI-noEmbryo" [tool.comfy] PublisherId = "noembryo" -DisplayName = "ComfyUI-noEmbryo" +DisplayName = "noEmbryo" Icon = "https://iili.io/JmSdmAJ.png" diff --git a/web/js/noEmbryo_utils.js b/web/js/noEmbryo_utils.js new file mode 100644 index 0000000..40421a0 --- /dev/null +++ b/web/js/noEmbryo_utils.js @@ -0,0 +1,112 @@ +import { ComfyApp, app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "Comfy.noEmbryo", + + nodeCreated(node, app) { + console.log("nodeCreated", node.comfyClass); + +// if(node.comfyClass === "LoadImage From Dir /noEmbryo") { +// +// // async function listAllFiles(dirHandle) { +// // const files = []; +// // for await (let [name, handle] of dirHandle) { +// // const {kind} = handle; +// // { +// // files.push({name, handle, kind}); +// // } +// // } +// // return files; +// // } +// // +// // async function onClickHandler(e) { +// // try { +// // const directoryHandle = await window.showDirectoryPicker() +// // console.log(directoryHandle) +// // const data = await listAllFiles(directoryHandle); +// // // console.log('files', files); +// // return data; +// // }catch(e) { +// // console.log(e); +// // } +// // } +// +// let path_i = 0; +// let images_i = 1; +// node._value = "None"; +// +// Object.defineProperty(node.widgets[path_i], "value", { +// set: (value) => { +// const stackTrace = new Error().stack; +// if(stackTrace.includes('inner_value_change')) { +// if(value !== "") { +// console.log(node.widgets[images_i]) +// let values = node.widgets[images_i].options.values; +// console.log(values) +// node.widgets[images_i].value = values[1]; +// // if(node.widgets_values) { +// // node.widgets_values[images_i] = node.widgets[path_i].value; +// // } +// } +// } +// +// node._value = value; +// }, +// get: () => { +// return node._value; +// } +// }); +// } + + if(node.comfyClass === "JsonPromptLoader -noEmbryo") { + console.log('Setting up JsonPromptLoader'); + let json_path_i = 0; + let selected_title_i = 1; + const originalCallback = node.widgets[json_path_i].callback; + node.widgets[json_path_i].callback = async (value) => { + // console.log('Callback called with path:', value); + if(originalCallback) originalCallback(value); + if(value) { + try { + const url = '/noembryo/get_json?path=' + encodeURIComponent(value); + // console.log('Fetching:', url); + const response = await fetch(url); + // console.log('Response status:', response.status); + const data = await response.json(); + // console.log('Data:', data); + if(data.error) { + console.log('Error:', data.error); + node.widgets[selected_title_i].options.values = ["None"]; + node.widgets[selected_title_i].value = "None"; + app.graph.setDirtyCanvas(true, true); + } else { + const titles = ["None", ...Object.keys(data)]; + // console.log('Titles:', titles); + node.widgets[selected_title_i].options.values = titles; + const currentValue = node.widgets[selected_title_i].value; + if (titles.includes(currentValue)) { + node.widgets[selected_title_i].value = currentValue; + } else { + node.widgets[selected_title_i].value = "None"; + } + app.graph.setDirtyCanvas(true, true); + } + } catch (e) { + console.log('Error loading JSON:', e); + node.widgets[selected_title_i].options.values = ["None"]; + node.widgets[selected_title_i].value = "None"; + } + } else { + node.widgets[selected_title_i].options.values = ["None"]; + node.widgets[selected_title_i].value = "None"; + } + }; + // Trigger callback if path is already set (e.g., from loaded workflow) + setTimeout(() => { + if(node.widgets[json_path_i].value) { + node.widgets[json_path_i].callback(node.widgets[json_path_i].value); + } + }, 100); + } + } +});