Compare commits

..
31 Commits
Author SHA1 Message Date
Rhys Yang cdd38cb3cd Bump version to 1.2.1 2024-01-03 00:21:01 +08:00
Rhys Yang 8f3c760bad Update CHANGELOG.md 2024-01-03 00:20:44 +08:00
Rhys Yang 7ab1628c2a Code cleanup 2024-01-03 00:19:19 +08:00
Rhys Yang c4630346e6 Fix the Batch Loader node not working when not connected to any node 2024-01-03 00:17:09 +08:00
Rhys Yang 202b1fdb06 Fix the input validation of the Prompt Reader node #37 2024-01-02 23:40:08 +08:00
receyuki a7fd3f11bd Bump version to 1.2.0 2023-12-11 22:46:30 +08:00
receyuki 50e0456eca Update README.md 2023-12-11 22:43:05 +08:00
receyuki b5572bba48 Fix Parameter Extractor node 2023-12-11 22:35:55 +08:00
receyuki d44b01659e Update core to 1.3.4.post1 2023-12-11 21:41:11 +08:00
Rhys Yang 97c6aa988d Add Parameter Extractor node #24 2023-12-11 19:55:03 +08:00
Rhys Yang 52882ee594 Add a model search feature for the Prompt Reader node #24 2023-12-11 02:26:34 +08:00
Rhys Yang a6107989fb Add an option to the Prompt Saver node for saving metadata as a text file with the same name as the image #30 2023-12-11 01:04:40 +08:00
Rhys Yang 1a5850865f Fix the issue where the Prompt Merger node threw an error when merging empty strings #29 2023-12-11 00:39:36 +08:00
Rhys Yang 6ce83b3828 Enhance the Batch Loader node to support processing either a single file or a list of files #26 2023-12-11 00:01:10 +08:00
Rhys Yang e67c358e2d Add FILE_PATH output to the Prompt Saver node #26 2023-12-10 23:52:18 +08:00
Rhys Yang 111bc489d5 Update BUG-REPORT.yml 2023-11-24 04:23:35 +08:00
receyuki fe864be43a Update README.md 2023-11-24 03:06:43 +08:00
receyuki 1da8db7d5c Bump version to 1.1.0 2023-11-24 03:04:40 +08:00
receyuki fa63e9a466 Update CHANGELOG.md 2023-11-24 03:04:18 +08:00
receyuki c025d474c5 Update example workflow 2023-11-24 03:04:09 +08:00
receyuki 570a59ea3d Update README.md 2023-11-24 03:03:46 +08:00
Rhys Yang 52dfd0456d Add MODEL_NAME output to the Prompt Reader node #23 2023-11-24 01:05:35 +08:00
Rhys Yang 49a9c4d041 Remove Show/Hide Last Seed Value from menu options 2023-11-24 01:00:05 +08:00
Rhys Yang c07548e3dc Add loaderDisplay.js to web extensions 2023-11-24 00:40:36 +08:00
Rhys Yang 8dffa89ae9 Code cleanup 2023-11-24 00:20:38 +08:00
Rhys Yang f56c1e0c48 Add Batch Loader node #13 2023-11-24 00:07:20 +08:00
Rhys Yang 4f42729494 Add FILENAME and METADATA output to the Prompt Saver node #16 2023-11-22 03:22:40 +08:00
Rhys Yang fc11266095 Add VAE selector to the Parameter Generator node #15 2023-11-22 02:15:33 +08:00
Rhys Yang d02265dbce Add pixel dimensions to the aspect_ratio #6 2023-11-22 00:53:39 +08:00
Rhys Yang 2950c76e9d Add Positive and Negative Aesthetic Score to the parameters #8 2023-11-22 00:51:02 +08:00
receyuki 07ecca0de4 Update example workflow 2023-11-14 23:40:13 +08:00
22 changed files with 1118 additions and 705 deletions
-11
View File
@@ -34,14 +34,3 @@ body:
![DESCRIPTION](LINK.png)
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
+21
View File
@@ -1,4 +1,25 @@
# 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
+42 -4
View File
@@ -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">
@@ -115,9 +126,11 @@ in the built-in Saver node, it will count all image files in the `path`.
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
@@ -134,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
@@ -148,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">
+8 -1
View File
@@ -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
View File
@@ -1 +1 @@
VERSION = "1.0.1"
VERSION = "1.2.1"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 112 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 552 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+31
View File
@@ -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]
}
};
}
},
});
+30
View File
@@ -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];
};
}
},
});
+15 -11
View File
@@ -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 {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
};
}
},
+8 -14
View File
@@ -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 {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];
};
}
},
+13 -13
View File
@@ -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() {
+10
View File
@@ -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;
}
+337 -32
View File
@@ -12,6 +12,7 @@ from itertools import chain
import torch
import json
import re
import numpy as np
from pathlib import Path
from PIL import Image, ImageOps
@@ -53,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},
@@ -82,6 +107,7 @@ class SDPromptReader:
"FLOAT",
"INT",
"INT",
any_type,
"STRING",
"STRING",
)
@@ -95,7 +121,8 @@ class SDPromptReader:
"CFG",
"WIDTH",
"HEIGHT",
"FILE_NAME",
"MODEL_NAME",
"FILENAME",
"SETTINGS",
)
@@ -104,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")
@@ -139,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)
@@ -159,6 +196,7 @@ class SDPromptReader:
cfg,
width,
height,
model,
file_path.stem,
image_data.setting,
),
@@ -169,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):
@@ -251,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
@@ -287,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,
@@ -304,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
@@ -357,6 +419,7 @@ class SDPromptSaver:
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:
@@ -368,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,
)
@@ -388,12 +451,21 @@ class SDPromptSaver:
},
}
)
piexif.insert(metadata, str(output_folder / 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}
)
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):
@@ -461,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"},
@@ -506,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": (
@@ -571,8 +665,9 @@ class SDParameterGenerator:
def generate_parameter(
self,
model_version,
ckpt_name,
vae_name,
model_version,
config_name,
seed,
steps,
@@ -589,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)
@@ -607,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"
@@ -627,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"
)
@@ -634,7 +748,7 @@ class SDParameterGenerator:
return {
"ui": {
"text": (
aspect_ratio,
aspect_ratio.split(" - ")[0],
model_version,
width,
height,
@@ -642,6 +756,8 @@ class SDParameterGenerator:
refiner_start,
base_steps,
refiner_steps,
SDParameterGenerator.ASPECT_RATIO_MAP,
SDParameterGenerator.MODEL_SCALING_FACTOR,
)
},
"result": (
@@ -669,7 +785,8 @@ class SDPromptMerger:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"required": {},
"optional": {
"text_g": (
"STRING",
{"default": "", "multiline": True, "forceInput": True},
@@ -685,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:
@@ -734,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 = {
@@ -748,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",
}
File diff suppressed because it is too large Load Diff