Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdd38cb3cd | ||
|
|
8f3c760bad | ||
|
|
7ab1628c2a | ||
|
|
c4630346e6 | ||
|
|
202b1fdb06 | ||
|
|
a7fd3f11bd | ||
|
|
50e0456eca | ||
|
|
b5572bba48 | ||
|
|
d44b01659e | ||
|
|
97c6aa988d | ||
|
|
52882ee594 | ||
|
|
a6107989fb | ||
|
|
1a5850865f | ||
|
|
6ce83b3828 | ||
|
|
e67c358e2d | ||
|
|
111bc489d5 | ||
|
|
fe864be43a | ||
|
|
1da8db7d5c | ||
|
|
fa63e9a466 | ||
|
|
c025d474c5 | ||
|
|
570a59ea3d | ||
|
|
52dfd0456d | ||
|
|
49a9c4d041 | ||
|
|
c07548e3dc | ||
|
|
8dffa89ae9 | ||
|
|
f56c1e0c48 | ||
|
|
4f42729494 | ||
|
|
fc11266095 | ||
|
|
d02265dbce | ||
|
|
2950c76e9d | ||
|
|
07ecca0de4 | ||
|
|
3ecceb0175 | ||
|
|
00aabca362 | ||
|
|
aff17ccbf4 | ||
|
|
0a5bedf412 | ||
|
|
b68ed7d7fa | ||
|
|
61c0487744 | ||
|
|
b4df56f08a | ||
|
|
c5a8fed1e6 |
@@ -34,14 +34,3 @@ body:
|
||||

|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: What version are you running?
|
||||
multiple: false
|
||||
options:
|
||||
- 1.0.0
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -36,7 +36,10 @@ jobs:
|
||||
|
||||
- name: Extract Section
|
||||
id: extract_section
|
||||
run: sed -n '/## Installation/,/## Usage/ {/## Usage/!p;}' README.md > installation.md
|
||||
run: |
|
||||
tag_name="${{ env.TAG_NAME }}"
|
||||
sed -n "/## $tag_name/,/## /p" CHANGELOG.md | sed '$d' >> installation.md
|
||||
sed -n '/## Installation/,/## Usage/ {/## Usage/!p;}' README.md >> installation.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Change Log
|
||||
## v1.2.1
|
||||
- Fix the input validation of the `Prompt Reader` node #37
|
||||
- Fix the `Batch Loader` node not working when not connected to any node
|
||||
|
||||
## v1.2.0
|
||||
- Add `Parameter Extractor` node #24
|
||||
- Add a model matching feature to the `Prompt Reader` node #24
|
||||
- Add `save_metadata_file` option to the `Prompt Saver` node #30
|
||||
- Add `FILE_PATH` output to the `Prompt Saver` node #26
|
||||
- Fix the issue where the `Prompt Merger` node threw an error when merging empty strings #29
|
||||
- Enhance the `Batch Loader` node to support processing either a single file or a list of files #26
|
||||
- Update core to 1.3.4.post1
|
||||
|
||||
## v1.1.0
|
||||
- Add `Batch Loader` node #13
|
||||
- Add `MODEL_NAME` output to the `Prompt Reader` node #23
|
||||
- Add VAE selector to the `Parameter Generator` node #15
|
||||
- Add pixel dimensions display to the `aspect_ratio` in the `Parameter Generator` node #6
|
||||
- Add Positive and Negative Aesthetic Score to the `PARAMETERS` in the `Parameter Generator` node #8
|
||||
- Add `FILENAME` and `METADATA` output to the `Prompt Saver` node #16
|
||||
|
||||
## v1.0.1
|
||||
- Add a new file-naming mechanism to ensure naming uniqueness
|
||||
- Fix `%counter` overwriting existing images #11 #14
|
||||
- Fix the bug causing the `Prompt Saver` node fails to save images in jpg and webp formats #10
|
||||
- Use relative paths for js imports #12
|
||||
|
||||
## v1.0.0
|
||||
- Initial release
|
||||
@@ -20,9 +20,10 @@ additional metadata to ensure compatibility with metadata detection on websites
|
||||
<a href="#supported-formats">Supported Formats</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#usage">Usage</a> •
|
||||
<a href="./CHANGELOG.md">Change Log</a> •
|
||||
<a href="#credits">Credits</a>
|
||||
</p>
|
||||
<img src="./images/screenshot_v100.png">
|
||||
<img src="./images/screenshot_v120.png">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -78,6 +79,16 @@ to ensure maximum compatibility.
|
||||
you will need to modify the `parameter_index` to select the parameters you need
|
||||
- For images generated by SDXL and containing multiple sets of prompts,
|
||||
the `text_g` will be combined with `text_l` into a single prompt
|
||||
- For batch processing, please use the `Batch Loader` node.
|
||||
- To read parameters other than the existing output, please connect the `settings` to the `Parameter Extractor` node.
|
||||
- `MODEL_NAME` is a special output that matches the model name in the metadata with the existing models on the server
|
||||
according to the following priority:
|
||||
1. Identical path, filename, and extension.
|
||||
2. Identical filename, and extension.
|
||||
e.g. `sd_xl_base.safetensors` will be matched with `SDXL\sd_xl_base.safetensors`, and vice versa.
|
||||
3. Identical filename.
|
||||
e.g. `sd_xl_base` will be matched with `SDXL\sd_xl_base.safetensors`, and vice versa.
|
||||
4. If a matching model cannot be found, the original name will be outputted.
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/reader.png" width="25%" height="25%" alt="reader node">
|
||||
@@ -109,18 +120,24 @@ the `Prompt Saver` Node and `KSampler`.
|
||||
| %sampler |
|
||||
| %scheduler |
|
||||
| %quality |
|
||||
|
||||
- `%counter` cannot be used for the `path`. This `%counter` is slightly different from the `%counter`
|
||||
in the built-in Saver node, it will count all image files in the `path`.
|
||||
- When the filename already exists, an index will be added at the end of the filename,
|
||||
e.g. `file.png, file_1.png, file_2.png`.
|
||||
- For the `date_format` and `time_format`, please refer to
|
||||
[strftime.org](https://strftime.org/) or [www.strfti.me](https://www.strfti.me/).
|
||||
- When the `save_metadata_file` is turned on, the metadata will be saved as a TXT file with the same name
|
||||
alongside the image.
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/saver.png" width="25%" height="25%" alt="generator and saver node">
|
||||
<img src="./images/saver.png" width="25%" height="25%" alt="saver node">
|
||||
</div>
|
||||
|
||||
## Parameter Generator Node
|
||||
- Since it's not possible to directly extract metadata from `KSampler`, it is necessary to
|
||||
use the `Parameter Generator` Node to generate parameters and simultaneously output them to both
|
||||
the `Prompt Saver` Node and `KSampler`.
|
||||
- The `Parameter Generator` Node can also be used as a control panel for complex ComfyUI workflows, just like the [AP workflow](https://perilli.com/ai/comfyui/).
|
||||
- The `model_version` and `aspect_ratio` are used only for calculating the optimal resolution of the selected model version
|
||||
under the chosen aspect ratio. The calculation method is based on the
|
||||
[Stability AI development documentation](https://platform.stability.ai/docs/features/api-parameters#about-dimensions)
|
||||
@@ -130,7 +147,32 @@ i.e., the proportion of base steps to total steps. This is used to calculate the
|
||||
required by the refiner `KSampler` under the selected step ratio.
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/generator.png" width="25%" height="25%" alt="generator and saver node">
|
||||
<img src="./images/generator.png" width="25%" height="25%" alt="generator node">
|
||||
</div>
|
||||
|
||||
### Batch Loader Node
|
||||
- The `Batch Loader` Node is specifically designed for the `Prompt Reader` Node to batch-read image files in a directory
|
||||
and cannot be used with other custom nodes.
|
||||
- For batch processing, please connect the `IMAGE` output of the `Batch Loader` Node to the `image` input of
|
||||
the `Prompt Reader` Node.
|
||||
- The `path` supports relative paths such as `./input/` or absolute paths like `C:/Users/receyuki/Pictures`.
|
||||
- Both `\ ` and `/` are acceptable.
|
||||
- You can also input a single file or a list of files into the `path`,
|
||||
in which case the `image_load_limit` and `start_index` will not function.
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/loader.png" width="25%" height="25%" alt="loader node">
|
||||
</div>
|
||||
|
||||
### Parameter Extractor Node
|
||||
- The `Parameter Extractor` Node is an extension of the `Prompt Reader` Node, designed to retrieve the values
|
||||
of all parameters in the settings (including those parameters that the `Prompt Reader` Node cannot output).
|
||||
e.g. `Hires upscaler`
|
||||
- Connect the `SETTINGS` of the `Prompt Reader` Node to the `settings` of the `Parameter Extractor` Node.
|
||||
After the first run, the parameter list will be loaded.
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/extractor.png" width="25%" height="25%" alt="extractor node">
|
||||
</div>
|
||||
|
||||
### Prompt Merger Node & Type Converter Node
|
||||
@@ -144,7 +186,7 @@ You can use the `Type Converter` Node to convert them into `STRING` type.
|
||||
<img src="./images/merger_converter.png" width="25%" height="25%" alt="merger and converter node">
|
||||
</div>
|
||||
|
||||
### [Example Workflow](./workflows/example_workflow.json)
|
||||
### [Example Workflow](https://raw.githubusercontent.com/receyuki/comfyui-prompt-reader-node/main/workflows/example_workflow.json)
|
||||
|
||||
<div align="center">
|
||||
<img src="./images/example_workflow.png" width="100%" height="100%" alt="example workflow">
|
||||
@@ -154,4 +196,4 @@ You can use the `Type Converter` Node to convert them into `STRING` type.
|
||||
- The SD Prompt Reader node is based on [ComfyUI Load Image With Metadata](https://github.com/tkoenig89/ComfyUI_Load_Image_With_Metadata)
|
||||
- The SD Prompt Saver node is based on [Comfy Image Saver](https://github.com/giriss/comfy-image-saver) & [Stable Diffusion Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui)
|
||||
- The seed generator in the SD Parameter Generator is modified from [rgthree's Comfy Nodes](https://github.com/rgthree/rgthree-comfy#rgthrees-comfy-nodes)
|
||||
- A special thanks to [@alessandroperilli](https://github.com/alessandroperilli) and his [AP Workflow](https://perilli.com/ai/comfyui/) for providing numerous suggestions
|
||||
- A special thanks to [@alessandroperilli](https://github.com/alessandroperilli) and his [AP Workflow](https://perilli.com/ai/comfyui/) for providing numerous suggestions
|
||||
|
||||
@@ -12,7 +12,14 @@ tk_nodes_path = os.path.join(os.path.dirname(__file__))
|
||||
js_dest_path = os.path.join(comfy_path, "web", "extensions", "SDPromptReader")
|
||||
os.makedirs(js_dest_path, exist_ok=True)
|
||||
|
||||
files_to_copy = ["promptDisplay.js", "parameterDisplay.js", "seedGen.js"]
|
||||
files_to_copy = [
|
||||
"utils.js",
|
||||
"promptDisplay.js",
|
||||
"parameterDisplay.js",
|
||||
"seedGen.js",
|
||||
"loaderDisplay.js",
|
||||
"extractorDisplay.js",
|
||||
]
|
||||
|
||||
for file in files_to_copy:
|
||||
js_src_path = os.path.join(tk_nodes_path, "js", file)
|
||||
|
||||
@@ -1 +1 @@
|
||||
VERSION = "1.0.0"
|
||||
VERSION = "1.2.1"
|
||||
|
||||
|
Before Width: | Height: | Size: 761 KiB After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,31 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
import {createTextWidget} from "./utils.js"
|
||||
|
||||
app.registerExtension({
|
||||
name: "sd_prompt_reader.extractorDisplay",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SDParameterExtractor") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Create widgets
|
||||
const styles = {textAlign: "center", fontSize: "0.75rem"}
|
||||
const value_display = createTextWidget(app, this, "value_display", styles);
|
||||
};
|
||||
|
||||
// Update widgets
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
this.widgets.find(obj => obj.name === "value_display").value = message.text[1]
|
||||
this.widgets.find(obj => obj.name === "parameter").options.values = message.text[0]
|
||||
if (this.widgets.find(obj => obj.name === "parameter").value === "parameters not loaded") {
|
||||
this.widgets.find(obj => obj.name === "parameter").value = message.text[0][0]
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
import {createTextWidget} from "./utils.js"
|
||||
|
||||
// Displays file list on the node
|
||||
app.registerExtension({
|
||||
name: "sd_prompt_reader.loaderDisplay",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SDBatchLoader") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Create prompt and setting widgets
|
||||
const styles = {opacity: 0.7}
|
||||
const fileList =createTextWidget(app, this, "fileList", styles);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Update widgets
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
this.widgets.find(obj => obj.name === "fileList").value = message.text[0];
|
||||
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,6 @@
|
||||
import {app} from "/scripts/app.js";
|
||||
import {ComfyWidgets} from "/scripts/widgets.js";
|
||||
|
||||
// Create a read-only string widget
|
||||
function createWidget(app, node, widgetName, type) {
|
||||
const widget = ComfyWidgets[type](node, widgetName, ["STRING", {multiline: true}], app).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
widget.inputEl.style.textAlign = "center";
|
||||
widget.inputEl.style.fontSize = "0.75rem";
|
||||
return widget;
|
||||
}
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
import {createTextWidget} from "./utils.js"
|
||||
|
||||
app.registerExtension({
|
||||
name: "sd_prompt_reader.parameterDisplay",
|
||||
@@ -20,8 +12,9 @@ app.registerExtension({
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Create widgets
|
||||
const steps_display = createWidget(app, this, "steps_display", "STRING");
|
||||
const aspect_ratio_display = createWidget(app, this, "aspect_ratio_display", "STRING");
|
||||
const styles = {textAlign: "center", fontSize: "0.75rem"}
|
||||
const steps_display = createTextWidget(app, this, "steps_display", styles);
|
||||
const aspect_ratio_display = createTextWidget(app, this, "aspect_ratio_display", styles);
|
||||
|
||||
// Resize the node
|
||||
const nodeWidth = this.size[0];
|
||||
@@ -59,6 +52,17 @@ Refiner start at step: ${message.text[6]} (${base_percentage})`;
|
||||
|
||||
this.widgets.find(obj => obj.name === "steps_display").value = step_message;
|
||||
this.widgets.find(obj => obj.name === "aspect_ratio_display").value = ar_message;
|
||||
|
||||
const scalingFactor = message.text[9][message.text[1]]
|
||||
const aspectRatioArray = Object.entries(message.text[8]).map(([ratio, dimensions]) => {
|
||||
const [width, height] = dimensions;
|
||||
return `${ratio} - ${width*scalingFactor}x${height*scalingFactor}`;
|
||||
});
|
||||
aspectRatioArray.unshift("custom")
|
||||
|
||||
const aspectRatio = `${message.text[0]} - ${message.text[8][message.text[0]][0]*scalingFactor}x${message.text[8][message.text[0]][1]*scalingFactor}`
|
||||
this.widgets.find(obj => obj.name === "aspect_ratio").options.values = aspectRatioArray
|
||||
this.widgets.find(obj => obj.name === "aspect_ratio").value = aspectRatio
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {app} from "/scripts/app.js";
|
||||
import {ComfyWidgets} from "/scripts/widgets.js";
|
||||
|
||||
// Create a read-only string widget with opacity set
|
||||
function createWidget(app, node, widgetName) {
|
||||
const widget = ComfyWidgets["STRING"](node, widgetName, ["STRING", {multiline: true}], app).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
widget.inputEl.style.opacity = 0.7;
|
||||
return widget;
|
||||
}
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
import {createTextWidget} from "./utils.js"
|
||||
|
||||
// Displays prompt and setting on the node
|
||||
app.registerExtension({
|
||||
@@ -20,9 +13,10 @@ app.registerExtension({
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Create prompt and setting widgets
|
||||
const positive = createWidget(app, this, "positive");
|
||||
const negative = createWidget(app, this, "negative");
|
||||
const setting = createWidget(app, this, "setting");
|
||||
const styles = {opacity: 0.7}
|
||||
const positive = createTextWidget(app, this, "positive", styles);
|
||||
const negative = createTextWidget(app, this, "negative", styles);
|
||||
const setting = createTextWidget(app, this, "setting", styles);
|
||||
// Resize the node
|
||||
const nodeWidth = this.size[0];
|
||||
const nodeHeight = this.size[1];
|
||||
@@ -34,9 +28,9 @@ app.registerExtension({
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
this.widgets[3].value = message.text[0];
|
||||
this.widgets[4].value = message.text[1];
|
||||
this.widgets[5].value = message.text[2];
|
||||
this.widgets.find(obj => obj.name === "positive").value = message.text[0];
|
||||
this.widgets.find(obj => obj.name === "negative").value = message.text[1];
|
||||
this.widgets.find(obj => obj.name === "setting").value = message.text[2];
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -97,19 +97,19 @@ class SeedControl {
|
||||
}
|
||||
this.serializedCtx = {};
|
||||
};
|
||||
this.node.getExtraMenuOptions = (_, options) => {
|
||||
options.splice(options.length - 1, 0, {
|
||||
content: "Show/Hide Last Seed Value",
|
||||
callback: (_value, _options, _event, _parentMenu, _node) => {
|
||||
this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"];
|
||||
if (this.node.properties["showLastSeed"]) {
|
||||
this.addLastSeedValue();
|
||||
} else {
|
||||
this.removeLastSeedValue();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
// this.node.getExtraMenuOptions = (_, options) => {
|
||||
// options.splice(options.length - 1, 0, {
|
||||
// content: "Show/Hide Last Seed Value",
|
||||
// callback: (_value, _options, _event, _parentMenu, _node) => {
|
||||
// this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"];
|
||||
// if (this.node.properties["showLastSeed"]) {
|
||||
// this.addLastSeedValue();
|
||||
// } else {
|
||||
// this.removeLastSeedValue();
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
}
|
||||
|
||||
addLastSeedValue() {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
|
||||
// Create a read-only string widget
|
||||
export function createTextWidget(app, node, widgetName, styles = {}) {
|
||||
const widget = ComfyWidgets["STRING"](node, widgetName, ["STRING", {multiline: true}], app).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
Object.assign(widget.inputEl.style, styles);
|
||||
return widget;
|
||||
}
|
||||
@@ -8,9 +8,11 @@
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
|
||||
import torch
|
||||
import json
|
||||
import re
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageOps
|
||||
@@ -52,18 +54,42 @@ output_to_terminal("Node version: " + NODE_VERSION)
|
||||
output_to_terminal("Core version: " + CORE_VERSION)
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
"""A special type that can be connected to any other types. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
class SDPromptReader:
|
||||
files = []
|
||||
ckpt_paths = []
|
||||
ckpt_names = []
|
||||
ckpt_stems = []
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
for path in folder_paths.get_filename_list("checkpoints"):
|
||||
SDPromptReader.ckpt_paths.append(path)
|
||||
SDPromptReader.ckpt_names.append(Path(path).name)
|
||||
SDPromptReader.ckpt_stems.append(Path(path).stem)
|
||||
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [
|
||||
f
|
||||
for f in os.listdir(input_dir)
|
||||
if os.path.isfile(os.path.join(input_dir, f))
|
||||
]
|
||||
SDPromptReader.files = sorted(
|
||||
[
|
||||
f
|
||||
for f in os.listdir(input_dir)
|
||||
if os.path.isfile(os.path.join(input_dir, f))
|
||||
]
|
||||
)
|
||||
return {
|
||||
"required": {
|
||||
"image": (sorted(files), {"image_upload": True}),
|
||||
"image": (SDPromptReader.files, {"image_upload": True}),
|
||||
},
|
||||
"optional": {
|
||||
"parameter_index": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": 255, "step": 1},
|
||||
@@ -81,6 +107,7 @@ class SDPromptReader:
|
||||
"FLOAT",
|
||||
"INT",
|
||||
"INT",
|
||||
any_type,
|
||||
"STRING",
|
||||
"STRING",
|
||||
)
|
||||
@@ -94,7 +121,8 @@ class SDPromptReader:
|
||||
"CFG",
|
||||
"WIDTH",
|
||||
"HEIGHT",
|
||||
"FILE_NAME",
|
||||
"MODEL_NAME",
|
||||
"FILENAME",
|
||||
"SETTINGS",
|
||||
)
|
||||
|
||||
@@ -103,7 +131,10 @@ class SDPromptReader:
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def load_image(self, image, parameter_index):
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
if image in SDPromptReader.files:
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
else:
|
||||
image_path = image
|
||||
i = Image.open(image_path)
|
||||
i = ImageOps.exif_transpose(i)
|
||||
image = i.convert("RGB")
|
||||
@@ -138,12 +169,19 @@ class SDPromptReader:
|
||||
cfg = float(
|
||||
self.param_parser(image_data.parameter.get("cfg"), parameter_index) or 0
|
||||
)
|
||||
model = str(
|
||||
self.param_parser(image_data.parameter.get("model"), parameter_index)
|
||||
or ""
|
||||
)
|
||||
width = int(image_data.width or 0)
|
||||
height = int(image_data.height or 0)
|
||||
|
||||
output_to_terminal("Positive: \n" + image_data.positive)
|
||||
output_to_terminal("Negative: \n" + image_data.negative)
|
||||
output_to_terminal("Setting: \n" + image_data.setting)
|
||||
|
||||
model = self.search_model(model)
|
||||
|
||||
return {
|
||||
"ui": {
|
||||
"text": (image_data.positive, image_data.negative, image_data.setting)
|
||||
@@ -158,6 +196,7 @@ class SDPromptReader:
|
||||
cfg,
|
||||
width,
|
||||
height,
|
||||
model,
|
||||
file_path.stem,
|
||||
image_data.setting,
|
||||
),
|
||||
@@ -168,19 +207,37 @@ class SDPromptReader:
|
||||
data_list = data.strip("()").split(",")
|
||||
return data_list[0] if len(data_list) == 1 else data_list[index]
|
||||
|
||||
@staticmethod
|
||||
def search_model(model: str):
|
||||
if not model or model in SDPromptReader.ckpt_paths:
|
||||
return model
|
||||
|
||||
model_path = Path(model)
|
||||
model_name = model_path.name
|
||||
model_stem = model_path.stem
|
||||
|
||||
if model_name in SDPromptReader.ckpt_names:
|
||||
return SDPromptReader.ckpt_paths[
|
||||
SDPromptReader.ckpt_names.index(model_name)
|
||||
]
|
||||
|
||||
if model_stem in SDPromptReader.ckpt_stems:
|
||||
return SDPromptReader.ckpt_paths[
|
||||
SDPromptReader.ckpt_stems.index(model_stem)
|
||||
]
|
||||
|
||||
return model
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, image, parameter_index):
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
if image in SDPromptReader.files:
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
else:
|
||||
image_path = image
|
||||
with open(Path(image_path), "rb") as f:
|
||||
image_data = ImageDataReader(f)
|
||||
return image_data.props
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, image, parameter_index):
|
||||
if not folder_paths.exists_annotated_filepath(image):
|
||||
return "Invalid image file: {}".format(image)
|
||||
return True
|
||||
|
||||
|
||||
class SDPromptSaver:
|
||||
def __init__(self):
|
||||
@@ -250,12 +307,14 @@ class SDPromptSaver:
|
||||
"STRING",
|
||||
{"default": "%H%M%S", "multiline": False},
|
||||
),
|
||||
"save_metadata_file": ("BOOLEAN", {"default": False}),
|
||||
"extra_info": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("FILENAME", "FILE_PATH", "METADATA")
|
||||
FUNCTION = "save_images"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
@@ -286,6 +345,7 @@ class SDPromptSaver:
|
||||
jpg_webp_quality: int = 100,
|
||||
date_format: str = "%Y-%m-%d",
|
||||
time_format: str = "%H%M%S",
|
||||
save_metadata_file: bool = False,
|
||||
extra_info: str = "",
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
@@ -293,7 +353,7 @@ class SDPromptSaver:
|
||||
(
|
||||
full_output_folder,
|
||||
filename_alt,
|
||||
counter,
|
||||
counter_alt,
|
||||
subfolder_alt,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(
|
||||
@@ -303,7 +363,10 @@ class SDPromptSaver:
|
||||
images[0].shape[0],
|
||||
)
|
||||
|
||||
results = list()
|
||||
results = []
|
||||
files = []
|
||||
comments = []
|
||||
file_paths = []
|
||||
for image in images:
|
||||
# model_name_str, sampler_name_str, scheduler_str = None, None, None
|
||||
|
||||
@@ -316,7 +379,6 @@ class SDPromptSaver:
|
||||
variable_map = {
|
||||
"%date": self.get_time(date_format),
|
||||
"%time": self.get_time(time_format),
|
||||
"%counter": f"{counter:05}",
|
||||
"%seed": seed,
|
||||
"%steps": steps,
|
||||
"%cfg": cfg,
|
||||
@@ -327,6 +389,12 @@ class SDPromptSaver:
|
||||
"%quality": jpg_webp_quality,
|
||||
}
|
||||
|
||||
subfolder = self.get_path(path, variable_map)
|
||||
output_folder = Path(full_output_folder) / subfolder
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
counter = self.get_counter(output_folder)
|
||||
variable_map["%counter"] = f"{counter:05}"
|
||||
|
||||
i = 255.0 * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
metadata = None
|
||||
@@ -348,10 +416,10 @@ class SDPromptSaver:
|
||||
f"Version: ComfyUI"
|
||||
f"{extra_info_real}"
|
||||
)
|
||||
subfolder = self.get_path(path, variable_map)
|
||||
output_folder = Path(full_output_folder) / subfolder
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
file = self.get_path(filename, variable_map).with_suffix("." + extension)
|
||||
|
||||
stem = self.get_path(filename, variable_map)
|
||||
file = self.get_unique_filename(stem, extension, output_folder)
|
||||
file_path = output_folder / file
|
||||
|
||||
if extension == "png":
|
||||
if not args.disable_metadata:
|
||||
@@ -363,13 +431,13 @@ class SDPromptSaver:
|
||||
for x in extra_pnginfo:
|
||||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||||
img.save(
|
||||
output_folder / file,
|
||||
file_path,
|
||||
pnginfo=metadata,
|
||||
compress_level=4,
|
||||
)
|
||||
else:
|
||||
img.save(
|
||||
output_folder / file,
|
||||
file_path,
|
||||
quality=jpg_webp_quality,
|
||||
lossless=lossless_webp,
|
||||
)
|
||||
@@ -383,13 +451,21 @@ class SDPromptSaver:
|
||||
},
|
||||
}
|
||||
)
|
||||
piexif.insert(metadata, str(file))
|
||||
piexif.insert(metadata, str(file_path))
|
||||
|
||||
if save_metadata_file:
|
||||
with open(file_path.with_suffix(".txt"), "w", encoding="utf-8") as f:
|
||||
f.write(comment)
|
||||
|
||||
results.append(
|
||||
{"filename": file.name, "subfolder": str(subfolder), "type": self.type}
|
||||
)
|
||||
counter += 1
|
||||
files.append(str(file))
|
||||
file_paths.append(str(file_path))
|
||||
output_to_terminal("Saved file: " + str(file))
|
||||
comments.append(comment)
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
return {"ui": {"images": results}, "result": (files, file_paths, comments)}
|
||||
|
||||
@staticmethod
|
||||
def calculate_model_hash(model_name):
|
||||
@@ -403,6 +479,13 @@ class SDPromptSaver:
|
||||
|
||||
return hash_sha256.hexdigest()[:10]
|
||||
|
||||
@staticmethod
|
||||
def get_counter(directory: Path):
|
||||
img_files = list(
|
||||
chain(*(directory.rglob(f"*{suffix}") for suffix in SUPPORTED_FORMATS))
|
||||
)
|
||||
return len(img_files) + 1
|
||||
|
||||
@staticmethod
|
||||
def get_path(name, variable_map):
|
||||
for variable, value in variable_map.items():
|
||||
@@ -418,6 +501,18 @@ class SDPromptSaver:
|
||||
except:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_unique_filename(stem: Path, extension: str, output_folder: Path):
|
||||
file = stem.with_suffix(f".{extension}")
|
||||
index = 0
|
||||
|
||||
while (output_folder / file).exists():
|
||||
index += 1
|
||||
new_stem = f"{stem}_{index}"
|
||||
file = Path(new_stem).with_suffix(f".{extension}")
|
||||
|
||||
return file
|
||||
|
||||
|
||||
class SDParameterGenerator:
|
||||
ASPECT_RATIO_MAP = {
|
||||
@@ -438,13 +533,35 @@ class SDParameterGenerator:
|
||||
"SDXL 1024px": 2.0,
|
||||
}
|
||||
|
||||
DEFAULT_ASPECT_RATIO_DISPLAY = list(
|
||||
map(
|
||||
lambda x, scaling_factor=MODEL_SCALING_FACTOR: (
|
||||
f"{x[0]} - "
|
||||
f"{int(x[1][0]*scaling_factor['SDv1 512px'])}x"
|
||||
f"{int(x[1][1]*scaling_factor['SDv1 512px'])} | "
|
||||
f"{int(x[1][0]*scaling_factor['SDv2 768px'])}x"
|
||||
f"{int(x[1][1]*scaling_factor['SDv2 768px'])} | "
|
||||
f"{int(x[1][0]*scaling_factor['SDXL 1024px'])}x"
|
||||
f"{int(x[1][1]*scaling_factor['SDXL 1024px'])}"
|
||||
),
|
||||
ASPECT_RATIO_MAP.items(),
|
||||
)
|
||||
)
|
||||
|
||||
ckpt_list = []
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
SDParameterGenerator.ckpt_list = folder_paths.get_filename_list("checkpoints")
|
||||
return {
|
||||
"required": {
|
||||
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
|
||||
"ckpt_name": (SDParameterGenerator.ckpt_list,),
|
||||
},
|
||||
"optional": {
|
||||
"vae_name": (
|
||||
["baked VAE"] + folder_paths.get_filename_list("vae"),
|
||||
{"default": "baked VAE"},
|
||||
),
|
||||
"model_version": (
|
||||
list(SDParameterGenerator.MODEL_SCALING_FACTOR.keys()),
|
||||
{"default": "SDv1 512px"},
|
||||
@@ -483,7 +600,7 @@ class SDParameterGenerator:
|
||||
{"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01},
|
||||
),
|
||||
"aspect_ratio": (
|
||||
["custom"] + list(SDParameterGenerator.ASPECT_RATIO_MAP.keys()),
|
||||
["custom"] + SDParameterGenerator.DEFAULT_ASPECT_RATIO_DISPLAY,
|
||||
{"default": "custom"},
|
||||
),
|
||||
"width": (
|
||||
@@ -548,8 +665,9 @@ class SDParameterGenerator:
|
||||
|
||||
def generate_parameter(
|
||||
self,
|
||||
model_version,
|
||||
ckpt_name,
|
||||
vae_name,
|
||||
model_version,
|
||||
config_name,
|
||||
seed,
|
||||
steps,
|
||||
@@ -566,6 +684,9 @@ class SDParameterGenerator:
|
||||
output_vae=True,
|
||||
output_clip=True,
|
||||
):
|
||||
if ckpt_name not in SDParameterGenerator.ckpt_list:
|
||||
raise FileNotFoundError(f"Invalid ckpt_name: {ckpt_name}")
|
||||
|
||||
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
||||
if config_name != "none":
|
||||
config_path = folder_paths.get_full_path("configs", config_name)
|
||||
@@ -584,19 +705,34 @@ class SDParameterGenerator:
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
||||
)[:3]
|
||||
|
||||
if vae_name != "baked VAE":
|
||||
vae_path = folder_paths.get_full_path("vae", vae_name)
|
||||
sd = comfy.utils.load_torch_file(vae_path)
|
||||
vae = comfy.sd.VAE(sd=sd)
|
||||
checkpoint = (*checkpoint[:2], vae)
|
||||
|
||||
if aspect_ratio != "custom":
|
||||
aspect_ratio_value = aspect_ratio.split(" - ")[0]
|
||||
width = int(
|
||||
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio][0]
|
||||
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio_value][0]
|
||||
* SDParameterGenerator.MODEL_SCALING_FACTOR[model_version]
|
||||
)
|
||||
height = int(
|
||||
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio][1]
|
||||
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio_value][1]
|
||||
* SDParameterGenerator.MODEL_SCALING_FACTOR[model_version]
|
||||
)
|
||||
|
||||
base_steps = int(steps * refiner_start)
|
||||
refiner_steps = steps - base_steps
|
||||
|
||||
if model_version == "SDXL 1024px":
|
||||
ascore = (
|
||||
f"Positive aesthetic score: {positive_ascore},\n"
|
||||
f"Negative aesthetic score: {negative_ascore},\n"
|
||||
)
|
||||
else:
|
||||
ascore = ""
|
||||
|
||||
parameters = (
|
||||
f"Model: {ckpt_name},\n"
|
||||
f"Seed: {str(seed)},\n"
|
||||
@@ -604,6 +740,7 @@ class SDParameterGenerator:
|
||||
f"CFG scale: {str(cfg)},\n"
|
||||
f"Sampler: {sampler_name},\n"
|
||||
f"Scheduler: {scheduler},\n"
|
||||
f"{ascore}"
|
||||
f"Size: {str(width)}x{str(height)},\n"
|
||||
f"Batch size: {str(batch_size)}\n"
|
||||
)
|
||||
@@ -611,7 +748,7 @@ class SDParameterGenerator:
|
||||
return {
|
||||
"ui": {
|
||||
"text": (
|
||||
aspect_ratio,
|
||||
aspect_ratio.split(" - ")[0],
|
||||
model_version,
|
||||
width,
|
||||
height,
|
||||
@@ -619,6 +756,8 @@ class SDParameterGenerator:
|
||||
refiner_start,
|
||||
base_steps,
|
||||
refiner_steps,
|
||||
SDParameterGenerator.ASPECT_RATIO_MAP,
|
||||
SDParameterGenerator.MODEL_SCALING_FACTOR,
|
||||
)
|
||||
},
|
||||
"result": (
|
||||
@@ -646,7 +785,8 @@ class SDPromptMerger:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"text_g": (
|
||||
"STRING",
|
||||
{"default": "", "multiline": True, "forceInput": True},
|
||||
@@ -662,10 +802,8 @@ class SDPromptMerger:
|
||||
FUNCTION = "merge_prompt"
|
||||
CATEGORY = "SD Prompt Reader"
|
||||
|
||||
def merge_prompt(self, text_g, text_l):
|
||||
if text_l == "":
|
||||
return text_g
|
||||
return (text_g + "\n" + text_l,)
|
||||
def merge_prompt(self, text_g="", text_l=""):
|
||||
return (text_g + ("\n" + text_l if text_g and text_l else text_l),)
|
||||
|
||||
|
||||
class SDTypeConverter:
|
||||
@@ -711,12 +849,200 @@ class SDTypeConverter:
|
||||
)
|
||||
|
||||
|
||||
class SDBatchLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"path": ("STRING", {"default": "./input/"}),
|
||||
},
|
||||
"optional": {
|
||||
"image_load_limit": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
|
||||
RETURN_NAMES = ("IMAGE",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "load_path"
|
||||
CATEGORY = "SD Prompt Reader"
|
||||
|
||||
def load_path(
|
||||
self,
|
||||
path: str = "./input/",
|
||||
image_load_limit: int = 0,
|
||||
start_index: int = 0,
|
||||
):
|
||||
if isinstance(path, list):
|
||||
files_str = [str(Path(p)) for p in path if Path(p).exists()]
|
||||
return {
|
||||
"ui": {
|
||||
"text": ("\n".join(files_str),),
|
||||
},
|
||||
"result": (files_str,),
|
||||
}
|
||||
elif Path(path).is_file():
|
||||
return {
|
||||
"ui": {
|
||||
"text": (str(Path(path)),),
|
||||
},
|
||||
"result": ([str(Path(path))],),
|
||||
}
|
||||
elif not Path(path).is_dir():
|
||||
raise FileNotFoundError(f"Invalid directory: {path}")
|
||||
|
||||
files = list(
|
||||
filter(lambda file: file.suffix in SUPPORTED_FORMATS, Path(path).iterdir())
|
||||
)
|
||||
|
||||
files = (
|
||||
sorted(files)[start_index : start_index + image_load_limit]
|
||||
if image_load_limit > 0
|
||||
else sorted(files)[start_index:]
|
||||
)
|
||||
|
||||
files_str = list(map(str, files))
|
||||
return {
|
||||
"ui": {
|
||||
"text": ("\n".join(files_str),),
|
||||
},
|
||||
"result": (files_str,),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(
|
||||
s,
|
||||
path,
|
||||
image_load_limit,
|
||||
start_index,
|
||||
):
|
||||
return os.listdir(path)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(
|
||||
s,
|
||||
path,
|
||||
image_load_limit,
|
||||
start_index,
|
||||
):
|
||||
if Path(path).is_file():
|
||||
return True
|
||||
if not Path(path).is_dir():
|
||||
return f"Invalid directory: {path}"
|
||||
return True
|
||||
|
||||
|
||||
class SDParameterExtractor:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"settings": (
|
||||
"STRING",
|
||||
{"default": "", "multiline": True, "forceInput": True},
|
||||
)
|
||||
},
|
||||
"optional": {
|
||||
"parameter": (
|
||||
["parameters not loaded"],
|
||||
{"default": "parameters not loaded"},
|
||||
),
|
||||
"value_type": (["STRING", "INT", "FLOAT"], {"default": "STRING"}),
|
||||
"parameter_index": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": 255, "step": 1},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("VALUE",)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "extract_param"
|
||||
CATEGORY = "SD Prompt Reader"
|
||||
|
||||
def extract_param(
|
||||
self,
|
||||
settings: str = "",
|
||||
parameter: str = "",
|
||||
value_type: str = "STRING",
|
||||
parameter_index: int = 0,
|
||||
):
|
||||
setting_dict = self.parse_setting(settings)
|
||||
if not settings or not parameter or parameter == "parameters not loaded":
|
||||
return {
|
||||
"ui": {
|
||||
"text": (list(setting_dict.keys()), ""),
|
||||
},
|
||||
"result": ("",),
|
||||
}
|
||||
|
||||
result = setting_dict.get(parameter)
|
||||
|
||||
try:
|
||||
if isinstance(result, tuple):
|
||||
result = result[parameter_index]
|
||||
if value_type == "INT":
|
||||
result = int(result)
|
||||
elif value_type == "FLOAT":
|
||||
result = float(result)
|
||||
except IndexError:
|
||||
return {
|
||||
"ui": {
|
||||
"text": (list(setting_dict.keys()), "Parameter index out of range"),
|
||||
},
|
||||
"result": ("",),
|
||||
}
|
||||
except (ValueError, TypeError):
|
||||
return {
|
||||
"ui": {
|
||||
"text": (
|
||||
list(setting_dict.keys()),
|
||||
f"{parameter}: {result}\n"
|
||||
f"{result} is not a valid number; it will be output as STRING",
|
||||
),
|
||||
},
|
||||
"result": (result,),
|
||||
}
|
||||
return {
|
||||
"ui": {
|
||||
"text": (list(setting_dict.keys()), f"{parameter}: {result}"),
|
||||
},
|
||||
"result": (result,),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_setting(settings):
|
||||
pattern = re.compile(r"([^:,]+):\s*\(([^)]+)\)|([^:,]+):\s*([^,]+)")
|
||||
|
||||
matches = pattern.findall(settings)
|
||||
|
||||
result = {}
|
||||
for match in matches:
|
||||
key, value_paren, key_nonparen, value_nonparen = match
|
||||
if key:
|
||||
key = key.strip()
|
||||
value = value_paren.strip()
|
||||
value = tuple(v.strip() for v in value.split(","))
|
||||
else:
|
||||
key = key_nonparen.strip()
|
||||
value = value_nonparen.strip()
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SDPromptReader": SDPromptReader,
|
||||
"SDPromptSaver": SDPromptSaver,
|
||||
"SDParameterGenerator": SDParameterGenerator,
|
||||
"SDPromptMerger": SDPromptMerger,
|
||||
"SDTypeConverter": SDTypeConverter,
|
||||
"SDBatchLoader": SDBatchLoader,
|
||||
"SDParameterExtractor": SDParameterExtractor,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -725,4 +1051,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SDParameterGenerator": "SD Parameter Generator",
|
||||
"SDPromptMerger": "SD Prompt Merger",
|
||||
"SDTypeConverter": "SD Type Converter",
|
||||
"SDBatchLoader": "SD Batch Loader",
|
||||
"SDParameterExtractor": "SD Parameter Extractor",
|
||||
}
|
||||
|
||||