Compare commits

...
86 Commits
Author SHA1 Message Date
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
Rhys Yang 3ecceb0175 Update README.md 2023-11-01 04:53:36 +08:00
Rhys Yang 00aabca362 Bump version to 1.0.1 2023-11-01 04:42:21 +08:00
Rhys Yang aff17ccbf4 Update package_repo.yml 2023-11-01 04:32:42 +08:00
Rhys Yang 0a5bedf412 Fix %counter overwriting existing images #11 #14, and add a new file-naming mechanism to ensure naming uniqueness. 2023-11-01 04:22:07 +08:00
Rhys Yang b68ed7d7fa Fix the bug causing the Prompt Saver node fails to save images in jpg and webp formats #10 2023-11-01 02:38:15 +08:00
Rhys Yang 61c0487744 Merge pull request #12 from mcmonkey4eva/main
Use relative paths for js imports
2023-10-30 00:16:47 +08:00
Alex "mcmonkey" Goodwin b4df56f08a use relative paths for js imports 2023-10-29 09:07:29 -07:00
Rhys Yang c5a8fed1e6 Update README.md 2023-10-28 03:09:45 +08:00
receyuki d25727455b Merge remote-tracking branch 'origin/main' 2023-10-26 02:02:54 +08:00
receyuki 47551e38dd Bump version to 1.0.0 2023-10-26 02:02:09 +08:00
receyuki cb72f372aa Update core to 1.3.4 2023-10-26 02:01:24 +08:00
Rhys Yang b34c0d2c2e Fix Prompt Saver batch processing #5 2023-10-26 00:25:37 +08:00
receyuki 7f1e6dbfd7 Update core 2023-10-25 02:47:23 +08:00
receyuki b08ec0c5e9 Update README.md 2023-10-25 02:26:08 +08:00
receyuki 11058de872 Update README.md 2023-10-25 01:13:54 +08:00
receyuki 992536ca33 Swap FILE_NAME and SETTINGS of the Prompt Reader. 2023-10-24 23:01:05 +08:00
Rhys Yang bab9dba0d9 Remove model_name_str, sampler_name_str and scheduler_str 2023-10-19 05:23:36 +08:00
Rhys Yang 5886a538da Fix parameter_index 2023-10-18 23:33:34 +08:00
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
receyuki f9aa0f108e Update README.md 2023-09-28 22:59:04 +08:00
Rhys Yang 3b795c9c42 Add alternative str input for model_name, sampler_name and scheduler 2023-09-28 20:08:46 +08:00
Rhys Yang e5fd46b07e Update README.md 2023-09-28 05:52:17 +08:00
Rhys Yang 96ded420e1 Update README.md 2023-09-28 02:38:17 +08:00
receyuki 426d6710e0 Update README.md 2023-09-28 00:38:28 +08:00
receyuki 30aaa3ef9c Update README.md 2023-09-28 00:29:08 +08:00
receyuki 4d33bfff0a Update README.md 2023-09-28 00:27:55 +08:00
receyuki 5afc486fea Update README.md 2023-09-28 00:23:31 +08:00
receyuki 97bb1e7f61 Update README.md 2023-09-28 00:09:58 +08:00
25 changed files with 2029 additions and 160 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
+48
View File
@@ -0,0 +1,48 @@
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: |
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
with:
files: ${{ env.TAG_NAME }}.zip
body_path: installation.md
+17
View File
@@ -0,0 +1,17 @@
# Change Log
## 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
+117 -12
View File
@@ -5,11 +5,11 @@
<img alt="GitHub" src="https://img.shields.io/github/license/receyuki/comfyui-prompt-reader-node"></a>
<img alt="GitHub tag (with filter)" src="https://img.shields.io/github/v/tag/receyuki/comfyui-prompt-reader-node?label=node">
<a href="https://github.com/receyuki/stable-diffusion-prompt-reader">
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/receyuki/stable-diffusion-prompt-reader?label=core"></a>
<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>
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
@@ -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_v100b1.png">
<img src="./images/screenshot_v110.png">
</div>
@@ -42,14 +43,15 @@ 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)
~~Search for `SD Prompt Reader` in the ComfyUI Manager and install it.~~
Waiting for approval.
>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
git clone https://github.com/receyuki/comfyui-prompt-reader-node.git
git clone --recursive https://github.com/receyuki/comfyui-prompt-reader-node.git
```
3. Install dependencies
```shell
@@ -57,14 +59,117 @@ Waiting for approval.
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
Example workflow coming soon.
### 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).
It uses the Image Data Reader from the
[standalone SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader),
allowing it to support the same formats and receive updates along with the
[SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader).
- Due to custom nodes and complex workflows potentially causing issues with SD Prompt Reader's ability
to read image metadata correctly, it is recommended to embed the `Prompt Saver` Node within the workflow
to ensure maximum compatibility.
- For images containing multiple sets of parameters, such as those processed through `hires-fix` or `refiner`,
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.
<div align="center">
<img src="./images/reader.png" width="25%" height="25%" alt="reader node">
</div>
### Prompt Saver Node
- The `Prompt Saver` Node and The `Parameter Generator` Node are designed to be used together.
- The `Prompt Saver` Node will write additional metadata in the A1111 format to the output images
to be compatible with any tools that support the A1111 format,
including SD Prompt Reader and Civitai.
Due to custom nodes and complex workflows potentially causing issues with SD Prompt Reader's ability
to read image metadata correctly, it is recommended to embed this node within the workflow
to ensure maximum compatibility.
- 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`.
- Please refer to the following table for placeholders supported by the `filename` and `path`.
| Placeholder |
|-------------|
| %date |
| %time |
| %counter |
| %seed |
| %steps |
| %cfg |
| %extension |
| %model |
| %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/).
<div align="center">
<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)
and the [StableSwarmUI source code](https://github.com/Stability-AI/StableSwarmUI) (developed by Stability AI).
- `refiner_start` refers to the proportion of steps completed when the refiner starts running,
i.e., the proportion of base steps to total steps. This is used to calculate the `start_at_step` (`REFINER_START_STEP`)
required by the refiner `KSampler` under the selected step ratio.
<div align="center">
<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.
<div align="center">
<img src="./images/loader.png" width="25%" height="25%" alt="loader node">
</div>
### Prompt Merger Node & Type Converter Node
- Since the A1111 format cannot store `text_g` and `text_l` separately, SDXL users need to use
the `Prompt Merger` Node to combine `text_g` and `text_l` into a single prompt.
- Since `model_name`, `sampler_name`, and `scheduler` are special types
that cannot be directly used by some other nodes,
You can use the `Type Converter` Node to convert them into `STRING` type.
<div align="center">
<img src="./images/merger_converter.png" width="25%" height="25%" alt="merger and converter node">
</div>
### [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">
</div>
## 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 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
+11 -4
View File
@@ -10,8 +10,15 @@ 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",
"loaderDisplay.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.0b1"
VERSION = "1.1.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 KiB

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+36
View File
@@ -0,0 +1,36 @@
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;
}
// 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 fileList = createWidget(app, this, "fileList");
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];
};
}
},
});
+76
View File
@@ -0,0 +1,76 @@
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;
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
};
}
},
});
+5 -5
View File
@@ -1,5 +1,5 @@
import {app} from "/scripts/app.js";
import {ComfyWidgets} from "/scripts/widgets.js";
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) {
@@ -34,9 +34,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];
};
}
},
+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;
};
}
},
});
+542 -137
View File
@@ -7,6 +7,9 @@
import os
from datetime import datetime
from itertools import chain
import torch
import json
import numpy as np
@@ -50,19 +53,35 @@ 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 = []
@classmethod
def INPUT_TYPES(s):
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}),
"data_index": (
"image": (SDPromptReader.files, {"image_upload": True}),
},
"optional": {
"parameter_index": (
"INT",
{"default": 0, "min": 0, "max": 255, "step": 1},
),
@@ -79,6 +98,8 @@ class SDPromptReader:
"FLOAT",
"INT",
"INT",
any_type,
"STRING",
"STRING",
)
RETURN_NAMES = (
@@ -91,15 +112,20 @@ class SDPromptReader:
"CFG",
"WIDTH",
"HEIGHT",
"SETTING",
"MODEL_NAME",
"FILENAME",
"SETTINGS",
)
FUNCTION = "load_image"
CATEGORY = "SD Prompt Reader"
OUTPUT_NODE = True
def load_image(self, image, data_index):
image_path = folder_paths.get_annotated_filepath(image)
def load_image(self, image, parameter_index):
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")
@@ -111,24 +137,32 @@ 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
)
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)
@@ -150,6 +184,8 @@ class SDPromptReader:
cfg,
width,
height,
model,
file_path.stem,
image_data.setting,
),
}
@@ -160,14 +196,14 @@ class SDPromptReader:
return data_list[0] if len(data_list) == 1 else data_list[index]
@classmethod
def IS_CHANGED(s, image, data_index):
def IS_CHANGED(s, image, parameter_index):
image_path = folder_paths.get_annotated_filepath(image)
with open(Path(image_path), "rb") as f:
image_data = ImageDataReader(f)
return image_data.props
@classmethod
def VALIDATE_INPUTS(s, image, data_index):
def VALIDATE_INPUTS(s, image, parameter_index):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
@@ -184,8 +220,15 @@ 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": (
"INT",
{
@@ -209,28 +252,38 @@ class SDPromptSaver:
},
),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
# "sampler_name_str": ("STRING", {"default": ""}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"positive": ("STRING", {"default": "", "multiline": True}),
"negative": ("STRING", {"default": "", "multiline": True}),
"extension": (["png", "jpg", "webp"],),
},
"optional": {
# "scheduler_str": ("STRING", {"default": ""}),
"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"},
}
RETURN_TYPES = ()
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("FILENAME", "METADATA")
FUNCTION = "save_images"
OUTPUT_NODE = True
@@ -240,41 +293,80 @@ 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,
steps: int = 0,
cfg: float = 0.0,
sampler_name: str = "",
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,
counter,
subfolder,
filename_alt,
counter_alt,
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()
results = []
files = []
comments = []
for image in images:
# model_name_str, sampler_name_str, scheduler_str = None, None, None
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_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),
"%seed": seed,
"%steps": steps,
"%cfg": cfg,
"%extension": extension,
"%model": model_name_real,
"%sampler": sampler_name_real,
"%scheduler": scheduler_real,
"%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
model_hash = (
f"Model hash: {self.calculate_model_hash(model_name)}, "
f"Model hash: {self.calculate_model_hash(model_name_real)}, "
if calculate_model_hash
else ""
)
@@ -282,15 +374,19 @@ class SDPromptSaver:
f"{positive}\n"
f"Negative prompt: {negative}\n"
f"Steps: {steps}, "
f"Sampler: {sampler_name}{''if scheduler == 'normal' else '_'+scheduler}, "
f"Sampler: {sampler_name_real}{''if scheduler_real == 'normal' else '_'+scheduler_real}, "
f"CFG scale: {cfg}, "
f"Seed: {seed}, "
f"Size: {img.width if width==0 else width}x{img.height if height==0 else height}, "
f"{model_hash}"
f"Model: {Path(model_name).stem}, "
f"Model: {Path(model_name_real).stem}, "
f"Version: ComfyUI"
f"{extra_info_real}"
)
file = Path(full_output_folder) / f"{filename}_{counter:05}_.{extension}"
stem = self.get_path(filename, variable_map)
file = self.get_unique_filename(stem, extension, output_folder)
if extension == "png":
if not args.disable_metadata:
metadata = PngInfo()
@@ -301,12 +397,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(
{
@@ -317,13 +417,15 @@ class SDPromptSaver:
},
}
)
piexif.insert(metadata, str(file))
piexif.insert(metadata, str(output_folder / file))
results.append(
{"filename": file.name, "subfolder": subfolder, "type": self.type}
{"filename": file.name, "subfolder": str(subfolder), "type": self.type}
)
counter += 1
files.append(str(file))
output_to_terminal("Saved file: " + str(file))
comments.append(comment)
return {"ui": {"images": results}}
return {"ui": {"images": results}, "result": (files, comments)}
@staticmethod
def calculate_model_hash(model_name):
@@ -337,6 +439,307 @@ 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():
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 ""
@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 = {
"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,
}
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": (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"},
),
"config_name": (
["none"] + folder_paths.get_filename_list("configs"),
{"default": "none"},
),
"seed": (
"INT",
{"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",
{
"default": 8.0,
"min": 0.0,
"max": 100.0,
"step": 0.5,
"round": 0.01,
},
),
"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"] + SDParameterGenerator.DEFAULT_ASPECT_RATIO_DISPLAY,
{"default": "custom"},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
),
"height": (
"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",
"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",
"SEED",
"STEPS",
"REFINER_START_STEP",
"CFG",
"SAMPLER_NAME",
"SCHEDULER",
"POSITIVE_ASCORE",
"NEGATIVE_ASCORE",
"WIDTH",
"HEIGHT",
"BATCH_SIZE",
"PARAMETERS",
)
FUNCTION = "generate_parameter"
CATEGORY = "SD Prompt Reader"
def generate_parameter(
self,
ckpt_name,
vae_name,
model_version,
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,
):
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)
checkpoint = comfy.sd.load_checkpoint(
config_path,
ckpt_path,
output_vae=True,
output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
else:
checkpoint = comfy.sd.load_checkpoint_guess_config(
ckpt_path,
output_vae=True,
output_clip=True,
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_value][0]
* SDParameterGenerator.MODEL_SCALING_FACTOR[model_version]
)
height = int(
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"
f"Steps: {str(steps)},\n"
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"
)
return {
"ui": {
"text": (
aspect_ratio.split(" - ")[0],
model_version,
width,
height,
steps,
refiner_start,
base_steps,
refiner_steps,
SDParameterGenerator.ASPECT_RATIO_MAP,
SDParameterGenerator.MODEL_SCALING_FACTOR,
)
},
"result": (
(ckpt_name,)
+ checkpoint
+ (
seed,
steps,
base_steps,
cfg,
sampler_name,
scheduler,
positive_ascore,
negative_ascore,
width,
height,
batch_size,
parameters,
)
),
}
class SDPromptMerger:
@classmethod
@@ -364,129 +767,131 @@ class SDPromptMerger:
return (text_g + "\n" + text_l,)
class SDParameterGenerator:
class SDTypeConverter:
@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"),),
},
"required": {},
"optional": {
"seed": (
"INT",
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
"model_name": (
folder_paths.get_filename_list("checkpoints"),
{"forceInput": True},
),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": (
"FLOAT",
{
"default": 8.0,
"min": 0.0,
"max": 100.0,
"step": 0.5,
"round": 0.01,
},
),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"width": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
),
"height": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
"sampler_name": (
comfy.samplers.KSampler.SAMPLERS,
{"forceInput": True},
),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"forceInput": True}),
},
}
RETURN_TYPES = (
"MODEL",
"CLIP",
"VAE",
folder_paths.get_filename_list("checkpoints"),
"INT",
"INT",
"FLOAT",
comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS,
"INT",
"INT",
"STRING",
"STRING",
"STRING",
)
RETURN_NAMES = (
"MODEL",
"CLIP",
"VAE",
"MODEL_NAME",
"SEED",
"STEPS",
"CFG",
"SAMPLER_NAME",
"SCHEDULER",
"WIDTH",
"HEIGHT",
"MODEL_NAME_STR",
"SAMPLER_NAME_STR",
"SCHEDULER_STR",
)
FUNCTION = "generate_parameter"
FUNCTION = "convert_string"
CATEGORY = "SD Prompt Reader"
def generate_parameter(
self,
ckpt_name,
with_config,
config_name,
seed,
steps,
cfg,
sampler_name,
scheduler,
width,
height,
output_vae=True,
output_clip=True,
def convert_string(
self, model_name: str = "", sampler_name: str = "", scheduler: str = ""
):
config_path = folder_paths.get_full_path("configs", config_name)
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
if with_config:
checkpoint = comfy.sd.load_checkpoint(
config_path,
ckpt_path,
output_vae=True,
output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
else:
checkpoint = comfy.sd.load_checkpoint_guess_config(
ckpt_path,
output_vae=True,
output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)[:3]
return checkpoint + (
ckpt_name,
seed,
steps,
cfg,
return (
model_name,
sampler_name,
scheduler,
width,
height,
)
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,)
FUNCTION = "load_path"
CATEGORY = "SD Prompt Reader"
def load_path(
self,
path: str = "./input/",
image_load_limit: int = 0,
start_index: int = 0,
):
if 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 not Path(path).is_dir():
return f"Invalid directory: {path}"
return True
NODE_CLASS_MAPPINGS = {
"SDPromptReader": SDPromptReader,
"SDPromptSaver": SDPromptSaver,
"SDPromptMerger": SDPromptMerger,
"SDParameterGenerator": SDParameterGenerator,
"SDPromptMerger": SDPromptMerger,
"SDTypeConverter": SDTypeConverter,
"SDBatchLoader": SDBatchLoader,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SDPromptReader": "SD Prompt Reader",
"SDPromptSaver": "SD Prompt Saver",
"SDPromptMerger": "SD Prompt Merger",
"SDParameterGenerator": "SD Parameter Generator",
"SDPromptMerger": "SD Prompt Merger",
"SDTypeConverter": "SD Type Converter",
"SDBatchLoader": "SD Batch Loader",
}
+942
View File
@@ -0,0 +1,942 @@
{
"last_node_id": 9,
"last_link_id": 39,
"nodes": [
{
"id": 4,
"type": "EmptyLatentImage",
"pos": [
840,
500
],
"size": {
"0": 320,
"1": 74
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 10,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 11,
"widget": {
"name": "height"
}
},
{
"name": "batch_size",
"type": "INT",
"link": 9,
"widget": {
"name": "batch_size"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
840,
650
],
"size": {
"0": 320,
"1": 54
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 37,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
18
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 5,
"type": "VAEDecode",
"pos": [
840,
910
],
"size": {
"0": 320,
"1": 50
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 22,
"slot_index": 0
},
{
"name": "vae",
"type": "VAE",
"link": 21
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
34
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
840,
780
],
"size": {
"0": 320,
"1": 54
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 20,
"slot_index": 0
},
{
"name": "text",
"type": "STRING",
"link": 38,
"widget": {
"name": "text"
},
"slot_index": 1
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
39
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 3,
"type": "SDPromptSaver",
"pos": [
1210,
130
],
"size": {
"0": 320,
"1": 500
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 34
},
{
"name": "model_name",
"type": "COMBO",
"link": 16,
"widget": {
"name": "model_name"
}
},
{
"name": "seed",
"type": "INT",
"link": 26,
"widget": {
"name": "seed"
}
},
{
"name": "steps",
"type": "INT",
"link": null,
"widget": {
"name": "steps"
}
},
{
"name": "cfg",
"type": "FLOAT",
"link": null,
"widget": {
"name": "cfg"
}
},
{
"name": "sampler_name",
"type": "COMBO",
"link": 14,
"widget": {
"name": "sampler_name"
}
},
{
"name": "scheduler",
"type": "COMBO",
"link": 13,
"widget": {
"name": "scheduler"
}
},
{
"name": "width",
"type": "INT",
"link": 29,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 28,
"widget": {
"name": "height"
}
},
{
"name": "positive",
"type": "STRING",
"link": null,
"widget": {
"name": "positive"
}
},
{
"name": "negative",
"type": "STRING",
"link": null,
"widget": {
"name": "negative"
}
}
],
"outputs": [
{
"name": "FILENAME",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "METADATA",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SDPromptSaver"
},
"widgets_values": [
"ComfyUI_%time_%seed_%counter",
"%date/",
"CheckpointYesmix_v20.safetensors",
822116345397686,
"randomize",
20,
8,
"euler",
"normal",
1,
1,
"",
"",
"png",
false,
true,
100,
"%Y-%m-%d",
"%H%M%S",
"ComfyUI Example Image"
]
},
{
"id": 8,
"type": "KSamplerAdvanced",
"pos": [
840,
130
],
"size": {
"0": 320,
"1": 290
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 17
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 18
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 39
},
{
"name": "latent_image",
"type": "LATENT",
"link": 23
},
{
"name": "noise_seed",
"type": "INT",
"link": 24,
"widget": {
"name": "noise_seed"
},
"slot_index": 4
},
{
"name": "steps",
"type": "INT",
"link": 35,
"widget": {
"name": "steps"
}
},
{
"name": "cfg",
"type": "FLOAT",
"link": 36,
"widget": {
"name": "cfg"
}
},
{
"name": "sampler_name",
"type": "COMBO",
"link": 15,
"widget": {
"name": "sampler_name"
}
},
{
"name": "scheduler",
"type": "COMBO",
"link": 12,
"widget": {
"name": "scheduler"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
22
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSamplerAdvanced"
},
"widgets_values": [
"enable",
1033713264376921,
"randomize",
20,
8,
"euler",
"normal",
0,
10000,
"disable"
]
},
{
"id": 9,
"type": "SDPromptReader",
"pos": [
100,
130
],
"size": [
320,
900
],
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "POSITIVE",
"type": "STRING",
"links": [
37
],
"shape": 3,
"slot_index": 2
},
{
"name": "NEGATIVE",
"type": "STRING",
"links": [
38
],
"shape": 3,
"slot_index": 3
},
{
"name": "SEED",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "STEPS",
"type": "INT",
"links": [
35
],
"shape": 3,
"slot_index": 5
},
{
"name": "CFG",
"type": "FLOAT",
"links": [
36
],
"shape": 3,
"slot_index": 6
},
{
"name": "WIDTH",
"type": "INT",
"links": null,
"shape": 3,
"slot_index": 7
},
{
"name": "HEIGHT",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "MODEL_NAME",
"type": "*",
"links": null,
"shape": 3
},
{
"name": "FILENAME",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "SETTINGS",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SDPromptReader"
},
"widgets_values": [
"00067-2302238556.png",
"image",
0,
"ultra realistic 8k cg, HDR, epic, perfect artwork, masterpiece, bokeh, beautiful face, perfect female body, alluring, perfect female figure, ultra high res, beach, pink hair, seductive posture, seductive gaze, skindentation, 1girl, kpop idol, blush, street, earring, choker, close up, strap dress, half-length portrait, <lora:koreandolllikenessV20_v20:0.3>, <lora:fashigirl-v6-nai-5ep-resize:0.3>",
"grayscale, monochrome, low quality, worst quality, lowres, acnes, bad anatomy, bad hands, deformed, error, logo, mosaic censoring, sketches, skin blemishes, skin spots, text, watermark, FastNegativeV2",
"Steps: 35, Sampler: DPM++ 2M Karras, CFG scale: 7.5, Seed: 2302238556, Size: 512x512, Model hash: 18ed2b6c48, Model: xxmix9realistic_v40, Denoising strength: 0.45, Hires upscale: 2, Hires steps: 15, Hires upscaler: R-ESRGAN 4x+, Lora hashes: \"koreandolllikenessV20_v20: 8f3d16e6eada, fashigirl-v6-nai-5ep-resize: 4e0173ae8cef\", Version: 1.6.0"
]
},
{
"id": 2,
"type": "SDParameterGenerator",
"pos": [
470,
130
],
"size": {
"0": 320,
"1": 900
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL_NAME",
"type": "COMBO",
"links": [
16
],
"shape": 3,
"slot_index": 0
},
{
"name": "MODEL",
"type": "MODEL",
"links": [
17
],
"shape": 3,
"slot_index": 1
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
20,
32
],
"shape": 3,
"slot_index": 2
},
{
"name": "VAE",
"type": "VAE",
"links": [
21
],
"shape": 3,
"slot_index": 3
},
{
"name": "SEED",
"type": "INT",
"links": [
24,
26
],
"shape": 3,
"slot_index": 4
},
{
"name": "STEPS",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "REFINER_START_STEP",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "CFG",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "SAMPLER_NAME",
"type": "COMBO",
"links": [
14,
15
],
"shape": 3,
"slot_index": 8
},
{
"name": "SCHEDULER",
"type": "COMBO",
"links": [
12,
13
],
"shape": 3,
"slot_index": 9
},
{
"name": "POSITIVE_ASCORE",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "NEGATIVE_ASCORE",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "WIDTH",
"type": "INT",
"links": [
10,
29
],
"shape": 3,
"slot_index": 12
},
{
"name": "HEIGHT",
"type": "INT",
"links": [
11,
28
],
"shape": 3,
"slot_index": 13
},
{
"name": "BATCH_SIZE",
"type": "INT",
"links": [
9
],
"shape": 3,
"slot_index": 14
},
{
"name": "PARAMETERS",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SDParameterGenerator"
},
"widgets_values": [
"xxmix9realistic_v40.safetensors",
"baked VAE",
"SDv1 512px",
"none",
546889506427388,
30,
1,
8,
"dpmpp_2m",
"karras",
6,
6,
"1:1 - 512x512",
512,
512,
1,
"Total steps: 30,\nRefiner off",
"Optimal resolution for SDv1 512px model\nwith aspect ratio 1:1: 512 x 512",
null,
null,
null
]
}
],
"links": [
[
9,
2,
14,
4,
2,
"INT"
],
[
10,
2,
12,
4,
0,
"INT"
],
[
11,
2,
13,
4,
1,
"INT"
],
[
12,
2,
9,
8,
8,
"COMBO"
],
[
13,
2,
9,
3,
6,
"COMBO"
],
[
14,
2,
8,
3,
5,
"COMBO"
],
[
15,
2,
8,
8,
7,
"COMBO"
],
[
16,
2,
0,
3,
1,
"COMBO"
],
[
17,
2,
1,
8,
0,
"MODEL"
],
[
18,
7,
0,
8,
1,
"CONDITIONING"
],
[
20,
2,
2,
6,
0,
"CLIP"
],
[
21,
2,
3,
5,
1,
"VAE"
],
[
22,
8,
0,
5,
0,
"LATENT"
],
[
23,
4,
0,
8,
3,
"LATENT"
],
[
24,
2,
4,
8,
4,
"INT"
],
[
26,
2,
4,
3,
2,
"INT"
],
[
28,
2,
13,
3,
8,
"INT"
],
[
29,
2,
12,
3,
7,
"INT"
],
[
32,
2,
2,
7,
0,
"CLIP"
],
[
34,
5,
0,
3,
0,
"IMAGE"
],
[
35,
9,
5,
8,
5,
"INT"
],
[
36,
9,
6,
8,
6,
"FLOAT"
],
[
37,
9,
2,
7,
1,
"STRING"
],
[
38,
9,
3,
6,
1,
"STRING"
],
[
39,
6,
0,
8,
2,
"CONDITIONING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}