This commit is contained in:
noembryo
2026-01-14 21:09:55 +02:00
parent 1d3b07eea5
commit 8a8195738f
5 changed files with 371 additions and 65 deletions
+76 -40
View File
@@ -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)
<!-- <p align="center">
<a href="https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/Screen2.png">
<img src="https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/Screen2.png" height="180"></a>
</p> -->
### 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
<u>**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.**</u>
![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
+41 -6
View File
@@ -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({})
+137 -15
View File
@@ -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__}",
}
+5 -4
View File
@@ -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"
+112
View File
@@ -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);
}
}
});