Compare commits

..
Author SHA1 Message Date
Rhys Yang f61e51864c Update README.md 2023-10-18 07:23:29 +08:00
Rhys Yang 7b3b6279c1 Update README.md 2023-10-18 07:21:19 +08:00
Rhys Yang 50b0d9ff96 Update README.md 2023-10-18 06:48:17 +08:00
Rhys Yang c67cc3df84 Remove REFINER_STEPS and change BASE_STEPS to REFINER_START_STEP 2023-10-18 06:46:50 +08:00
Rhys Yang bd8f669d71 Change some of the parameter names in the Prompt Reader 2023-10-18 06:45:14 +08:00
Rhys Yang 4c8589be19 Update width and height when aspect ratio is selected 2023-10-18 05:31:34 +08:00
Rhys Yang 88a2c8a39f Increase the precision of refiner_start 2023-10-18 04:49:33 +08:00
Rhys Yang 368be01cdc Add js to web extensions 2023-10-18 04:47:46 +08:00
Rhys Yang dfc8f7ddd5 Fix seed decrement 2023-10-18 04:24:59 +08:00
Rhys Yang a5d16cabd2 Update package_repo.yml 2023-10-18 03:52:09 +08:00
Rhys Yang 3ef54b705f Update package_repo.yml 2023-10-18 03:43:18 +08:00
Rhys Yang 3ccf6c6bd9 Update package_repo.yml 2023-10-18 02:33:56 +08:00
Rhys Yang 72aef6526c Update package_repo.yml 2023-10-18 02:04:46 +08:00
Rhys Yang ce145417bd Update package_repo.yml 2023-10-18 02:01:02 +08:00
Rhys Yang ff2cc91bc2 Update package_repo.yml 2023-10-18 01:47:34 +08:00
Rhys Yang a1f415d5be Update package_repo.yml 2023-10-18 01:40:52 +08:00
Rhys Yang 44d03539e7 Update package_repo.yml 2023-10-18 01:29:08 +08:00
Rhys Yang 00fb6c7528 Update package_repo.yml 2023-10-18 01:22:08 +08:00
Rhys Yang bb069f599d Update package_repo.yml 2023-10-18 01:19:51 +08:00
Rhys Yang d25c5e8bac Create package_repo.yml 2023-10-18 01:15:45 +08:00
Rhys Yang e4dca4d391 Update README.md 2023-10-18 00:43:27 +08:00
Rhys Yang 7daa149bda Bump version to 1.0.0b3 2023-10-18 00:43:20 +08:00
Rhys Yang 4180d56074 Change the order of inputs and outputs for the Parameter Generator, add parameters debug output 2023-10-17 23:28:10 +08:00
Rhys Yang baceed2a01 Add extra_info to the Prompt Saver 2023-10-17 23:26:02 +08:00
Rhys Yang f67ae16743 Change the order of inputs for the Prompt Saver 2023-10-17 23:24:55 +08:00
Rhys Yang 31cd4f6f41 Fix Parameter Generator seed default value 2023-10-17 23:22:45 +08:00
Rhys Yang a276df0aec Change the order of the info windows 2023-10-17 23:21:07 +08:00
Rhys Yang 289d918ad2 Update core to 1.3.4b2 2023-10-14 00:09:59 +08:00
Rhys Yang 6a2df87b1b Formatting js 2023-10-14 00:08:20 +08:00
Rhys Yang df6a43ab6a Update seedGen.js 2023-10-13 23:56:13 +08:00
Rhys Yang 300669de66 Add seedGen.js 2023-10-13 23:55:08 +08:00
Rhys Yang 6ab02a2311 Add filename output to the Prompt Reader 2023-10-13 19:02:09 +08:00
Rhys Yang faa29e83c8 Add base and refiner step calculator 2023-10-13 00:32:17 +08:00
Rhys Yang 8d6d871d59 Add aspect ratio calculator 2023-10-11 00:48:50 +08:00
Rhys Yang 2027b48457 Add batch_size and ascore to the Prompter Generator, and change disable in the config_name to none. 2023-10-10 23:08:12 +08:00
Rhys Yang b0c2163298 Merge branch 'main' into dev 2023-10-09 21:08:06 +08:00
Rhys Yang a1360bafbb Remove with_config and add "disable" to config_name 2023-10-09 21:05:49 +08:00
Rhys Yang d4e50ff463 Update README.md 2023-10-09 21:05:49 +08:00
receyuki e1c948ec4f Add ISSUE_TEMPLATE 2023-10-09 04:53:22 +08:00
Rhys Yang 832098367b Add customisable filename and path, and remove filename_prefix #1 2023-10-09 01:08:24 +08:00
Rhys Yang f73ff88921 Remove with_config and add "disable" to config_name 2023-10-08 23:17:55 +08:00
Rhys Yang c94cf4ce40 Update README.md 2023-10-08 22:09:23 +08:00
receyuki 67e59b9ceb Fix scheduler_str 2023-09-29 02:36:03 +08:00
receyuki a187932d84 Update README.md 2023-09-29 02:35:38 +08:00
11 changed files with 592 additions and 56 deletions
+47
View File
@@ -0,0 +1,47 @@
name: "Bug Report"
description: Create a new ticket for a bug.
title: "[BUG] - title"
labels: [
"bug"
]
body:
- type: textarea
id: description
attributes:
label: "Description"
description: Please enter an explicit description of your issue.
placeholder: Short and explicit description of your incident...
validations:
required: true
- type: textarea
id: reprod
attributes:
label: "Reproduction steps"
description: Please enter an explicit description of your issue.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
id: image
attributes:
label: "Image file"
description: If applicable, add screenshots to help explain your problem.
placeholder: |
![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
@@ -0,0 +1,31 @@
name: "Feature Request"
description: Create a new ticket for a new feature request.
title: "[FEATURE REQUEST] - title"
labels: [
"question"
]
body:
- type: textarea
id: summary
attributes:
label: "Summary"
description: Provide a brief explanation of the feature.
placeholder: Describe in a few lines your feature request.
validations:
required: true
- type: textarea
id: basic_example
attributes:
label: "Basic Example"
description: Indicate here some basic examples of your feature.
placeholder: A few specific words about your feature request.
validations:
required: true
- type: textarea
id: reference_issues
attributes:
label: "Reference Issues."
description: Common issues
placeholder: "#Issues IDs"
validations:
required: false
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+45
View File
@@ -0,0 +1,45 @@
name: Package Repository with Submodules
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: true
- name: Update Submodules
run: git submodule update --init --recursive
- name: Get Tag Name
id: get_tag
run: echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Create ZIP Archive
run: |
tag_name="${{ env.TAG_NAME }}"
mkdir "comfyui-prompt-reader-node"
rsync -av --exclude='.github' \
--exclude="comfyui-prompt-reader-node/" \
--exclude="stable_diffusion_prompt_reader/images/" \
--exclude="stable_diffusion_prompt_reader/whl/" \
* comfyui-prompt-reader-node
zip -r "${tag_name}.zip" "comfyui-prompt-reader-node"
- name: Extract Section
id: extract_section
run: sed -n '/## Installation/,/## Usage/ {/## Usage/!p;}' README.md > installation.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: ${{ env.TAG_NAME }}.zip
body_path: installation.md
+13 -4
View File
@@ -8,8 +8,10 @@
<img alt="GitHub tag (with filter)" src="https://img.shields.io/github/v/tag/receyuki/stable-diffusion-prompt-reader?label=core"></a>
<a href="https://github.com/psf/black">
<img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<br><br>
<h3>This project is currently in beta status. If you encounter any issues or have any suggestions, please let me know.</h3>
<br>
<blockquote>
This project is currently in beta status. If you encounter any issues or have any suggestions, please let me know.
</blockquote>
This is a subproject of the
<a href="https://github.com/receyuki/stable-diffusion-prompt-reader">SD Prompt Reader.</a>
It helps you extract metadata from images in any format supported by the
@@ -42,9 +44,11 @@ additional metadata to ensure compatibility with metadata detection on websites
See [SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader#supported-formats) for details
## Installation
### Install via [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) (Recommended)
>While ZIP package is available, it is strongly recommended not to use it for installation.
### Install and update via [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) (Recommended)
Search for `SD Prompt Reader` in the ComfyUI Manager and install it.
### Install manually
Please make sure to install the submodules along with the main repository.
1. `cd` to the `custom_node` folder
2. Clone this repo
```shell
@@ -56,12 +60,13 @@ Search for `SD Prompt Reader` in the ComfyUI Manager and install it.
pip install -r requirements.txt
```
#### Update
Please ensure that you update the submodules along with the main repo.
When updating, don't forget to include the submodules along with the main repository.
```shell
git pull --recurse-submodules
```
## Usage
>The following section may be outdated and could differ from the actual nodes.
### Prompt Reader Node
- The Prompt Reader Node works exactly the same as the
[standalone SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader).
@@ -110,6 +115,8 @@ have higher priority, so do not connect `STRING` inputs unless necessary, as the
</div>
### [Example Workflow](./workflows/example_workflow.json)
>The example workflow is outdated and will be updated in the stable release.
<div align="center">
<img src="./images/example_workflow.png" width="100%" height="100%" alt="example workflow">
</div>
@@ -117,3 +124,5 @@ have higher priority, so do not connect `STRING` inputs unless necessary, as the
## Credits
- 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
+6 -4
View File
@@ -10,8 +10,10 @@ comfy_path = os.path.dirname(folder_paths.__file__)
tk_nodes_path = os.path.join(os.path.dirname(__file__))
js_dest_path = os.path.join(comfy_path, "web", "extensions", "SDPromptReader")
if not os.path.exists(js_dest_path):
os.makedirs(js_dest_path)
os.makedirs(js_dest_path, exist_ok=True)
js_src_path = os.path.join(tk_nodes_path, "js", "promptDisplay.js")
shutil.copy(js_src_path, js_dest_path)
files_to_copy = ["promptDisplay.js", "parameterDisplay.js", "seedGen.js"]
for file in files_to_copy:
js_src_path = os.path.join(tk_nodes_path, "js", file)
shutil.copy(js_src_path, js_dest_path)
+1 -1
View File
@@ -1 +1 @@
VERSION = "1.0.0b2"
VERSION = "1.0.0b3"
+65
View File
@@ -0,0 +1,65 @@
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;
}
app.registerExtension({
name: "sd_prompt_reader.parameterDisplay",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SDParameterGenerator") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
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");
// Resize the node
const nodeWidth = this.size[0];
const nodeHeight = this.size[1];
this.setSize([nodeWidth * 2, nodeHeight * 1.2]);
return result;
};
// Update widgets
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
let ar_message;
if (message.text[0] === "custom") {
ar_message = "Custom aspect ratio: " + message.text[2] + " x " + message.text[3];
} else {
ar_message = `Optimal resolution for ${message.text[1]} model
with aspect ratio ${message.text[0]}: ${message.text[2]} x ${message.text[3]}`;
this.widgets.find(obj => obj.name === "width").value = message.text[2];
this.widgets.find(obj => obj.name === "height").value = message.text[3];
}
const start_at_float = parseFloat(message.text[5])
const base_percentage = Math.round(start_at_float * 100) + "%";
// const refiner_percentage = Math.round((1 - start_at_float) * 100) + "%";
let step_message;
if (start_at_float === 1) {
step_message = `Total steps: ${message.text[4]},
Refiner off`;
} else {
step_message = `Total steps: ${message.text[4]},
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;
};
}
},
});
+154
View File
@@ -0,0 +1,154 @@
/**
* Modified from: https://github.com/rgthree/rgthree-comfy/blob/main/web/seed.js
* Modified by: receyuki
*/
import {app} from "../../scripts/app.js";
import {ComfyWidgets} from "../../scripts/widgets.js";
const LAST_SEED_BUTTON_LABEL = "(Use Last Queued Seed)";
const SPECIAL_SEED_RANDOM = -1;
const SPECIAL_SEED_INCREMENT = -2;
const SPECIAL_SEED_DECREMENT = -3;
const SPECIAL_SEEDS = [SPECIAL_SEED_RANDOM, SPECIAL_SEED_INCREMENT, SPECIAL_SEED_DECREMENT];
class SeedControl {
constructor(node) {
this.lastSeed = undefined;
this.serializedCtx = {};
this.lastSeedValue = null;
this.node = node;
this.node.constructor.exposedActions = ["Randomize seed each time", "Use last queued seed"];
const handleAction = this.node.handleAction;
this.node.handleAction = async (action) => {
handleAction && handleAction.call(this.node, action);
if (action === "Randomize each time") {
this.seedWidget.value = SPECIAL_SEED_RANDOM;
} else if (action === "Use last queued seed") {
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
};
this.node.properties = this.node.properties || {};
for (const [i, w] of this.node.widgets.entries()) {
if (w.name === "seed") {
this.seedWidget = w;
} else if (w.name === "control_after_generate") {
this.node.widgets.splice(i, 1);
}
}
if (!this.seedWidget) {
throw new Error("Something's wrong; expected seed widget");
}
const randMax = Math.min(1125899906842624, this.seedWidget.options.max);
const randMin = Math.max(0, this.seedWidget.options.min);
const randomRange = (randMax - Math.max(0, randMin)) / (this.seedWidget.options.step / 10);
this.node.addWidget("button", "Randomize seed each time", null, () => {
this.seedWidget.value = SPECIAL_SEED_RANDOM;
}, {serialize: false});
this.node.addWidget("button", "New fixed random seed", null, () => {
this.seedWidget.value =
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
}, {serialize: false});
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}, {serialize: false});
this.lastSeedButton.disabled = true;
this.seedWidget.serializeValue = async (node, index) => {
const inputSeed = this.seedWidget.value;
this.serializedCtx = {
inputSeed: this.seedWidget.value,
};
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) {
if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) {
if (inputSeed === SPECIAL_SEED_INCREMENT) {
this.serializedCtx.seedUsed = this.lastSeed + 1;
} else if (inputSeed === SPECIAL_SEED_DECREMENT) {
this.serializedCtx.seedUsed = this.lastSeed - 1;
}
}
if (!this.serializedCtx.seedUsed || SPECIAL_SEEDS.includes(this.serializedCtx.seedUsed)) {
this.serializedCtx.seedUsed =
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
}
} else {
this.serializedCtx.seedUsed = this.seedWidget.value;
}
node.widgets_values[index] = this.serializedCtx.seedUsed;
this.seedWidget.value = this.serializedCtx.seedUsed;
this.lastSeed = this.serializedCtx.seedUsed;
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) {
this.lastSeedButton.name = `${this.serializedCtx.seedUsed}`;
this.lastSeedButton.disabled = false;
if (this.lastSeedValue) {
this.lastSeedValue.value = `Last Seed: ${this.serializedCtx.seedUsed}`;
}
} else {
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
return this.serializedCtx.seedUsed;
};
this.seedWidget.afterQueued = () => {
if (this.serializedCtx.inputSeed) {
this.seedWidget.value = this.serializedCtx.inputSeed;
}
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();
}
},
});
};
}
addLastSeedValue() {
if (this.lastSeedValue)
return;
this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", {multiline: true}], app).widget;
this.lastSeedValue.inputEl.readOnly = true;
this.lastSeedValue.inputEl.style.fontSize = "0.75rem";
this.lastSeedValue.inputEl.style.textAlign = "center";
this.lastSeedValue.serializeValue = async (node, index) => {
node.widgets_values[index] = "";
return "";
};
this.node.computeSize();
}
removeLastSeedValue() {
if (!this.lastSeedValue)
return;
this.lastSeedValue.inputEl.remove();
this.node.widgets.splice(this.node.widgets.indexOf(this.lastSeedValue), 1);
this.lastSeedValue = null;
this.node.computeSize();
}
}
app.registerExtension({
name: "sd_prompt_reader.seedGen",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "SDParameterGenerator") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, []);
this.seedControl = new SeedControl(this);
const nodeWidth = this.size[0];
const nodeHeight = this.size[1];
this.setSize([nodeWidth * 1.5, nodeHeight * 1.1]);
return result;
};
}
},
});
+228 -46
View File
@@ -7,6 +7,8 @@
import os
from datetime import datetime
import torch
import json
import numpy as np
@@ -62,7 +64,7 @@ class SDPromptReader:
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
"data_index": (
"parameter_index": (
"INT",
{"default": 0, "min": 0, "max": 255, "step": 1},
),
@@ -80,6 +82,7 @@ class SDPromptReader:
"INT",
"INT",
"STRING",
"STRING",
)
RETURN_NAMES = (
"IMAGE",
@@ -91,14 +94,15 @@ class SDPromptReader:
"CFG",
"WIDTH",
"HEIGHT",
"SETTING",
"SETTINGS",
"FILE_NAME",
)
FUNCTION = "load_image"
CATEGORY = "SD Prompt Reader"
OUTPUT_NODE = True
def load_image(self, image, data_index):
def load_image(self, image, parameter_index):
image_path = folder_paths.get_annotated_filepath(image)
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
@@ -111,24 +115,28 @@ class SDPromptReader:
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
if Path(image_path).suffix not in SUPPORTED_FORMATS:
file_path = Path(image_path)
if file_path.suffix not in SUPPORTED_FORMATS:
output_to_terminal(MESSAGE["suffix_error"][1])
raise ValueError(MESSAGE["suffix_error"][1])
with open(Path(image_path), "rb") as f:
with open(file_path, "rb") as f:
image_data = ImageDataReader(f)
if not image_data.tool:
output_to_terminal(MESSAGE["format_error"][1])
raise ValueError(MESSAGE["format_error"][1])
seed = int(
self.param_parser(image_data.parameter.get("seed"), data_index) or 0
self.param_parser(image_data.parameter.get("seed"), parameter_index)
or 0
)
steps = int(
self.param_parser(image_data.parameter.get("steps"), data_index) or 0
self.param_parser(image_data.parameter.get("steps"), parameter_index)
or 0
)
cfg = float(
self.param_parser(image_data.parameter.get("cfg"), data_index) or 0
self.param_parser(image_data.parameter.get("cfg"), parameter_index) or 0
)
width = int(image_data.width or 0)
height = int(image_data.height or 0)
@@ -151,6 +159,7 @@ class SDPromptReader:
width,
height,
image_data.setting,
file_path.stem,
),
}
@@ -184,9 +193,13 @@ class SDPromptSaver:
return {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"filename": (
"STRING",
{"default": "ComfyUI_%time_%seed_%counter", "multiline": False},
),
"path": ("STRING", {"default": "%date/", "multiline": False}),
"model_name": (folder_paths.get_filename_list("checkpoints"),),
"model_name_str": ("STRING", {"default": ""}),
"seed": (
@@ -215,20 +228,29 @@ class SDPromptSaver:
"sampler_name_str": ("STRING", {"default": ""}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"scheduler_str": ("STRING", {"default": ""}),
"positive": ("STRING", {"default": "", "multiline": True}),
"negative": ("STRING", {"default": "", "multiline": True}),
"extension": (["png", "jpg", "webp"],),
"width": (
"INT",
{"default": 0, "min": 1, "max": MAX_RESOLUTION, "step": 8},
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 8},
),
"height": (
"INT",
{"default": 0, "min": 1, "max": MAX_RESOLUTION, "step": 8},
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 8},
),
"positive": ("STRING", {"default": "", "multiline": True}),
"negative": ("STRING", {"default": "", "multiline": True}),
"extension": (["png", "jpg", "webp"],),
"calculate_model_hash": ("BOOLEAN", {"default": False}),
"lossless_webp": ("BOOLEAN", {"default": True}),
"jpg_webp_quality": ("INT", {"default": 100, "min": 1, "max": 100}),
"date_format": (
"STRING",
{"default": "%Y-%m-%d", "multiline": False},
),
"time_format": (
"STRING",
{"default": "%H%M%S", "multiline": False},
),
"extra_info": ("STRING", {"default": "", "multiline": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
@@ -243,7 +265,8 @@ class SDPromptSaver:
def save_images(
self,
images,
filename_prefix,
filename: str = "ComfyUI_%time_%seed_%counter",
path: str = "%date/",
model_name: str = "",
model_name_str: str = "",
seed: int = 0,
@@ -253,32 +276,51 @@ class SDPromptSaver:
sampler_name_str: str = "",
scheduler: str = "",
scheduler_str: str = "",
width: int = 1,
height: int = 1,
positive: str = "",
negative: str = "",
extension: str = "png",
width: int = 0,
height: int = 0,
calculate_model_hash: bool = False,
lossless_webp: bool = True,
jpg_webp_quality: int = 100,
date_format: str = "%Y-%m-%d",
time_format: str = "%H%M%S",
extra_info: str = "",
prompt=None,
extra_pnginfo=None,
):
filename_prefix += self.prefix_append
(
full_output_folder,
filename,
filename_alt,
counter,
subfolder,
subfolder_alt,
filename_prefix,
) = folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
self.prefix_append, self.output_dir, images[0].shape[1], images[0].shape[0]
)
results = list()
model_name_real = model_name_str if model_name_str else model_name
sampler_name_real = sampler_name_str if sampler_name_str else sampler_name
scheduler_real = scheduler if scheduler_str else scheduler
scheduler_real = scheduler_str if scheduler_str else scheduler
extra_info_real = f", Extra info: {extra_info}" if extra_info else ""
variable_map = {
"%date": self.get_time(date_format),
"%time": self.get_time(time_format),
"%counter": f"{counter:05}",
"%seed": seed,
"%steps": steps,
"%cfg": cfg,
"%extension": extension,
"%model": model_name_real,
"%sampler": sampler_name_real,
"%scheduler": scheduler_real,
"%quality": jpg_webp_quality,
}
for image in images:
i = 255.0 * image.cpu().numpy()
@@ -300,8 +342,13 @@ class SDPromptSaver:
f"{model_hash}"
f"Model: {Path(model_name_real).stem}, "
f"Version: ComfyUI"
f"{extra_info_real}"
)
file = Path(full_output_folder) / f"{filename}_{counter:05}_.{extension}"
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)
if extension == "png":
if not args.disable_metadata:
metadata = PngInfo()
@@ -312,12 +359,16 @@ class SDPromptSaver:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
img.save(
file,
output_folder / file,
pnginfo=metadata,
compress_level=4,
)
else:
img.save(file, quality=jpg_webp_quality, lossless=lossless_webp)
img.save(
output_folder / file,
quality=jpg_webp_quality,
lossless=lossless_webp,
)
if not args.disable_metadata:
metadata = piexif.dump(
{
@@ -330,7 +381,7 @@ class SDPromptSaver:
)
piexif.insert(metadata, str(file))
results.append(
{"filename": file.name, "subfolder": subfolder, "type": self.type}
{"filename": file.name, "subfolder": str(subfolder), "type": self.type}
)
counter += 1
@@ -348,22 +399,65 @@ class SDPromptSaver:
return hash_sha256.hexdigest()[:10]
@staticmethod
def get_path(name, variable_map):
for variable, value in variable_map.items():
name = name.replace(variable, str(value))
return Path(name)
@staticmethod
def get_time(time_format):
now = datetime.now()
try:
time_str = now.strftime(time_format)
return time_str
except:
return ""
class SDParameterGenerator:
ASPECT_RATIO_MAP = {
"1:1": (512, 512),
"4:3": (576, 448),
"3:4": (448, 576),
"3:2": (608, 416),
"2:3": (416, 608),
"16:9": (672, 384),
"9:16": (384, 672),
"21:9": (768, 320),
"9:21": (320, 768),
}
MODEL_SCALING_FACTOR = {
"SDv1 512px": 1.0,
"SDv2 768px": 1.5,
"SDXL 1024px": 2.0,
}
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
"with_config": ("BOOLEAN", {"default": False}),
"config_name": (folder_paths.get_filename_list("configs"),),
},
"optional": {
"model_version": (
list(SDParameterGenerator.MODEL_SCALING_FACTOR.keys()),
{"default": "SDv1 512px"},
),
"config_name": (
["none"] + folder_paths.get_filename_list("configs"),
{"default": "none"},
),
"seed": (
"INT",
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
{"default": -1, "min": -3, "max": 0xFFFFFFFFFFFFFFFF},
),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"refiner_start": (
"FLOAT",
{"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01},
),
"cfg": (
"FLOAT",
{
@@ -376,6 +470,18 @@ class SDParameterGenerator:
),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"positive_ascore": (
"FLOAT",
{"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01},
),
"negative_ascore": (
"FLOAT",
{"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01},
),
"aspect_ratio": (
["custom"] + list(SDParameterGenerator.ASPECT_RATIO_MAP.keys()),
{"default": "custom"},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
@@ -384,35 +490,53 @@ class SDParameterGenerator:
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
),
"batch_size": (
"INT",
{
"default": 1,
"min": 1,
"max": 4096,
},
),
},
}
RETURN_TYPES = (
folder_paths.get_filename_list("checkpoints"),
"MODEL",
"CLIP",
"VAE",
folder_paths.get_filename_list("checkpoints"),
"INT",
"INT",
"INT",
"FLOAT",
comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS,
"FLOAT",
"FLOAT",
"INT",
"INT",
"INT",
"STRING",
)
RETURN_NAMES = (
"MODEL_NAME",
"MODEL",
"CLIP",
"VAE",
"MODEL_NAME",
"SEED",
"STEPS",
"REFINER_START_STEP",
"CFG",
"SAMPLER_NAME",
"SCHEDULER",
"POSITIVE_ASCORE",
"NEGATIVE_ASCORE",
"WIDTH",
"HEIGHT",
"BATCH_SIZE",
"PARAMETERS",
)
FUNCTION = "generate_parameter"
@@ -420,22 +544,27 @@ class SDParameterGenerator:
def generate_parameter(
self,
model_version,
ckpt_name,
with_config,
config_name,
seed,
steps,
refiner_start,
cfg,
sampler_name,
scheduler,
positive_ascore,
negative_ascore,
aspect_ratio,
width,
height,
batch_size,
output_vae=True,
output_clip=True,
):
config_path = folder_paths.get_full_path("configs", config_name)
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
if with_config:
if config_name != "none":
config_path = folder_paths.get_full_path("configs", config_name)
checkpoint = comfy.sd.load_checkpoint(
config_path,
ckpt_path,
@@ -450,17 +579,64 @@ class SDParameterGenerator:
output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)[:3]
return checkpoint + (
ckpt_name,
seed,
steps,
cfg,
sampler_name,
scheduler,
width,
height,
if aspect_ratio != "custom":
width = int(
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio][0]
* SDParameterGenerator.MODEL_SCALING_FACTOR[model_version]
)
height = int(
SDParameterGenerator.ASPECT_RATIO_MAP[aspect_ratio][1]
* SDParameterGenerator.MODEL_SCALING_FACTOR[model_version]
)
base_steps = int(steps * refiner_start)
refiner_steps = steps - base_steps
parameters = (
f"Model: {ckpt_name},\n"
f"Seed: {str(seed)},\n"
f"Steps: {str(steps)},\n"
f"CFG scale: {str(cfg)},\n"
f"Sampler: {sampler_name},\n"
f"Scheduler: {scheduler},\n"
f"Size: {str(width)}x{str(height)},\n"
f"Batch size: {str(batch_size)}\n"
)
return {
"ui": {
"text": (
aspect_ratio,
model_version,
width,
height,
steps,
refiner_start,
base_steps,
refiner_steps,
)
},
"result": (
(ckpt_name,)
+ checkpoint
+ (
seed,
steps,
base_steps,
cfg,
sampler_name,
scheduler,
positive_ascore,
negative_ascore,
width,
height,
batch_size,
parameters,
)
),
}
class SDPromptMerger:
@classmethod
@@ -494,9 +670,15 @@ class SDTypeConverter:
return {
"required": {},
"optional": {
"model_name": (folder_paths.get_filename_list("checkpoints"),),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"model_name": (
folder_paths.get_filename_list("checkpoints"),
{"forceInput": True},
),
"sampler_name": (
comfy.samplers.KSampler.SAMPLERS,
{"forceInput": True},
),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"forceInput": True}),
},
}