Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88722ce8f | ||
|
|
c0b66c4e07 | ||
|
|
d191668607 | ||
|
|
a70cbb0c8d | ||
|
|
b95499617e | ||
|
|
77095ee38b | ||
|
|
1e59445091 | ||
|
|
b3e66eb21c | ||
|
|
145fbbb158 | ||
|
|
7c215dde00 | ||
|
|
07a1a5314d | ||
|
|
ed6533422e | ||
|
|
278e32450f | ||
|
|
b248fcf346 | ||
|
|
4b35b41d08 | ||
|
|
af96defbe0 | ||
|
|
e8ca90fe35 | ||
|
|
659d35b9b8 | ||
|
|
ee54281e45 | ||
|
|
f94e130bef | ||
|
|
6ab1534e85 | ||
|
|
d960b6dbf1 | ||
|
|
0d0afd7f45 | ||
|
|
2069d0c4fc | ||
|
|
55a57a3b2e | ||
|
|
a2e6aff81c | ||
|
|
2af4a879b5 | ||
|
|
12b5178915 | ||
|
|
a4f5891079 | ||
|
|
9042867e97 | ||
|
|
5527096b53 | ||
|
|
9a84eb186b | ||
|
|
faeea1f73c | ||
|
|
e6cca92351 | ||
|
|
05c3553688 | ||
|
|
23b5afbc4f | ||
|
|
c2ac789c44 | ||
|
|
bc62d2be03 | ||
|
|
74beb22df4 | ||
|
|
0d43174e14 | ||
|
|
91336e987d | ||
|
|
cbf771ff03 | ||
|
|
867f5eb6f1 | ||
|
|
88ed9fa966 | ||
|
|
d6f8598946 | ||
|
|
63919f44a4 | ||
|
|
2321ce5ae4 | ||
|
|
a8e217d9f1 | ||
|
|
e32a514adc | ||
|
|
27bcc7273e | ||
|
|
d1e6940491 | ||
|
|
3457877e2c | ||
|
|
4d85411aee | ||
|
|
508bc56ca8 | ||
|
|
bdcc5c7461 | ||
|
|
f86625d71b | ||
|
|
cf4b85a0a0 | ||
|
|
5241cf50b9 | ||
|
|
6cd2b3414d | ||
|
|
9194663f09 | ||
|
|
103a5c826a | ||
|
|
cdd38cb3cd | ||
|
|
8f3c760bad | ||
|
|
7ab1628c2a | ||
|
|
c4630346e6 | ||
|
|
202b1fdb06 |
@@ -0,0 +1,22 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@main
|
||||||
|
with:
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
@@ -153,3 +153,4 @@ dmypy.json
|
|||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
preview/
|
||||||
@@ -1,4 +1,83 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
## v1.3.4
|
||||||
|
> [!TIP]
|
||||||
|
> Starting from v1.3, to support auto-detection on Civitai, `calculate_model_hash` will be renamed as
|
||||||
|
> `calculate_hash` and it will be enabled by default. Due to the addition of temporary storage of the model hash values,
|
||||||
|
> the first image generated after switching to the new model will take more time to calculate the hash value,
|
||||||
|
> but it will not affect the generation speed afterwards until the server is restarted.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> comfyui-prompt-reader-node is now published on the [Comfy Registry](https://www.comfyregistry.org/).
|
||||||
|
|
||||||
|
- Add `Any Converter` node #76 #78
|
||||||
|
- Fix the input validation of the `Parameter Generator` node #89
|
||||||
|
- Fix the input validation of the `Parameter Extractor` node #90
|
||||||
|
- Fix the `Parameter Generator` unable to choose standard resolutions #82
|
||||||
|
- Fix the `Prompt Saver` unable to correctly handle filenames containing `.` #80
|
||||||
|
- Update the `Parameter Extractor` node to support parsing quoted parameters #87 @spagatticode
|
||||||
|
- Update PyProject Toml - License #85 @haohaocreates
|
||||||
|
|
||||||
|
## v1.3.3
|
||||||
|
> [!TIP]
|
||||||
|
> Starting from v1.3, to support auto-detection on Civitai, `calculate_model_hash` will be renamed as
|
||||||
|
> `calculate_hash` and it will be enabled by default. Due to the addition of temporary storage of the model hash values,
|
||||||
|
> the first image generated after switching to the new model will take more time to calculate the hash value,
|
||||||
|
> but it will not affect the generation speed afterwards until the server is restarted.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> comfyui-prompt-reader-node is now published on the [Comfy Registry](https://www.comfyregistry.org/).
|
||||||
|
|
||||||
|
- Add pyproject.toml for Custom Node Registry #75 @haohaocreates
|
||||||
|
|
||||||
|
## v1.3.2
|
||||||
|
> Starting from v1.3, to support auto-detection on Civitai, `calculate_model_hash` will be renamed as
|
||||||
|
> `calculate_hash` and it will be enabled by default. Due to the addition of temporary storage of the model hash values,
|
||||||
|
> the first image generated after switching to the new model will take more time to calculate the hash value,
|
||||||
|
> but it will not affect the generation speed afterwards until the server is restarted.
|
||||||
|
|
||||||
|
- Add support for pasted images to the `Prompt Reader` node #64
|
||||||
|
- Update core to [1.3.5](https://github.com/receyuki/stable-diffusion-prompt-reader/releases/tag/v1.3.5)
|
||||||
|
- Update nodes.py save_images to have %width and %height #66 @COvert96
|
||||||
|
- Update the `Prompt Reader` node to the new exception handling method
|
||||||
|
|
||||||
|
## v1.3.1
|
||||||
|
> Starting from v1.3, to support auto-detection on Civitai, `calculate_model_hash` will be renamed as
|
||||||
|
> `calculate_hash` and it will be enabled by default. Due to the addition of temporary storage of the model hash values,
|
||||||
|
> the first image generated after switching to the new model will take more time to calculate the hash value,
|
||||||
|
> but it will not affect the generation speed afterwards until the server is restarted.
|
||||||
|
|
||||||
|
- Add user-friendly warnings and remove the exception raised during the reading of metadata #58 #63
|
||||||
|
- Add JPEG to the image extension list
|
||||||
|
- Fix the issue where the `Prompt Saver` node cannot consume filename from another `Prompt Saver` node #62
|
||||||
|
- Fix the `Prompt Saver` node freezes workflow when name includes `%model` #61
|
||||||
|
- Remove ckpt validation of the `Prompt Reader` node #59
|
||||||
|
|
||||||
|
## v1.3.0
|
||||||
|
> Starting from this version, to support auto-detection on Civitai, `calculate_model_hash` will be renamed as
|
||||||
|
> `calculate_hash` and it will be enabled by default. Due to the addition of temporary storage of the model hash values,
|
||||||
|
> the first image generated after switching to the new model will take more time to calculate the hash value,
|
||||||
|
> but it will not affect the generation speed afterwards until the server is restarted.
|
||||||
|
|
||||||
|
- Add `Lora Loader` node and `Lora Selector` node
|
||||||
|
- Add `VAE_NAME` output to the `Parameter Generator` node and add `vae_name` input to the `Prompt Saver` node #39
|
||||||
|
- Add `lora_name` input to the `Prompt Saver` node
|
||||||
|
- Add `resource_hash` switch to the `Prompt Saver` node
|
||||||
|
- Add resources hashes to metadata for auto-detection on Civitai #35
|
||||||
|
- Add temporary storage for model hashes
|
||||||
|
- Add `WEB_DIRECTORY` and remove old js directory
|
||||||
|
- Fix adding the `Parameter Generator` node to a workflow disables the queue button #45
|
||||||
|
- Fix the `Parameter Generator` node doesn't load seed value from generated images #44
|
||||||
|
- Fix the input validation of the `Batch Loader` node #37 #38
|
||||||
|
- Fix the parsing error caused by the loss of model data #43
|
||||||
|
- Fix the error message caused by aspect_ratio
|
||||||
|
- Rename `calculate_model_hash` as `calculate_hash`
|
||||||
|
- Update seedGen.js to rgthree's latest code
|
||||||
|
- Update core to cc3c8b2
|
||||||
|
|
||||||
|
## 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
|
## v1.2.0
|
||||||
- Add `Parameter Extractor` node #24
|
- Add `Parameter Extractor` node #24
|
||||||
- Add a model matching feature to the `Prompt Reader` node #24
|
- Add a model matching feature to the `Prompt Reader` node #24
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="icon" src="https://github.com/receyuki/stable-diffusion-prompt-reader/raw/master/sd_prompt_reader/resources/icon.png" width=20% height=20%>
|
<img alt="icon" src="https://github.com/receyuki/stable-diffusion-prompt-reader/raw/master/sd_prompt_reader/resources/icon-cube.png" width=20% height=20%>
|
||||||
<h1>SD Prompt Reader Node</h1>
|
<h1>SD Prompt Reader Node</h1>
|
||||||
<a href="https://github.com/receyuki/comfyui-prompt-reader-node/blob/master/LICENSE">
|
<a href="https://github.com/receyuki/comfyui-prompt-reader-node/blob/master/LICENSE">
|
||||||
<img alt="GitHub" src="https://img.shields.io/github/license/receyuki/comfyui-prompt-reader-node"></a>
|
<img alt="GitHub" src="https://img.shields.io/github/license/receyuki/comfyui-prompt-reader-node"></a>
|
||||||
@@ -23,27 +23,32 @@ additional metadata to ensure compatibility with metadata detection on websites
|
|||||||
<a href="./CHANGELOG.md">Change Log</a> •
|
<a href="./CHANGELOG.md">Change Log</a> •
|
||||||
<a href="#credits">Credits</a>
|
<a href="#credits">Credits</a>
|
||||||
</p>
|
</p>
|
||||||
<img src="./images/screenshot_v120.png">
|
<img src="./images/screenshot_v130.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## Supported Formats
|
## Supported Formats
|
||||||
| | PNG | JPEG | WEBP | TXT* |
|
| | PNG | JPEG | WEBP | TXT* |
|
||||||
|--------------------------------------------------------------------------|:---:|:----:|:----:|:----:|
|
|----------------------------------------------------------------------------------------|:---:|:----:|:----:|:----:|
|
||||||
| [A1111's webUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | ✅ | ✅ | ✅ | ✅ |
|
| [A1111's webUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [Easy Diffusion](https://github.com/easydiffusion/easydiffusion) | ✅ | ✅ | ✅ | |
|
| [Easy Diffusion](https://github.com/easydiffusion/easydiffusion) | ✅ | ✅ | ✅ | |
|
||||||
| [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI)* | ✅ | ✅ | | |
|
| [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI)* | ✅ | ✅ | | |
|
||||||
| [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE)* | ✅ | ✅ | | |
|
| [StableSwarmUI (prior to 0.5.8-alpha)](https://github.com/Stability-AI/StableSwarmUI)* | ✅ | ✅ | | |
|
||||||
| [InvokeAI](https://github.com/invoke-ai/InvokeAI) | ✅ | | | |
|
| [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE)* | ✅ | ✅ | | |
|
||||||
| [ComfyUI](https://github.com/comfyanonymous/ComfyUI)* | ✅ | | | |
|
| [NovelAI (stealth pnginfo)](https://novelai.net/) | ✅ | | ✅ | |
|
||||||
| [NovelAI](https://novelai.net/) | ✅ | | | |
|
| [NovelAI (legacy)](https://novelai.net/) | ✅ | | | |
|
||||||
| [Draw Things](https://drawthings.ai/) | ✅ | | | |
|
| [InvokeAI](https://github.com/invoke-ai/InvokeAI) | ✅ | | | |
|
||||||
| Naifu(4chan) | ✅ | | | |
|
| [InvokeAI (prior to 2.3.5-post.2)](https://github.com/invoke-ai/InvokeAI) | ✅ | | | |
|
||||||
|
| [InvokeAI (prior to 1.15)](https://github.com/invoke-ai/InvokeAI) | ✅ | | | |
|
||||||
|
| [ComfyUI](https://github.com/comfyanonymous/ComfyUI)* | ✅ | | | |
|
||||||
|
| [Draw Things](https://drawthings.ai/) | ✅ | | | |
|
||||||
|
| Naifu(4chan) | ✅ | | | |
|
||||||
|
|
||||||
See [SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader#supported-formats) for details
|
See [SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader#supported-formats) for details
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
>While ZIP package is available, it is strongly recommended not to use it for installation.
|
> [!NOTE]
|
||||||
|
> 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)
|
### 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.
|
Search for `SD Prompt Reader` in the ComfyUI Manager and install it.
|
||||||
### Install manually
|
### Install manually
|
||||||
@@ -66,21 +71,44 @@ git pull --recurse-submodules
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
### Prompt Reader Node
|
### Prompt Reader Node
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/reader.png" width="25%" height="25%" alt="reader node">
|
||||||
|
</div>
|
||||||
|
|
||||||
- The `Prompt Reader` Node works exactly the same as the
|
- The `Prompt Reader` Node works exactly the same as the
|
||||||
[standalone SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader).
|
[standalone SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader).
|
||||||
It uses the Image Data Reader from the
|
It uses the Image Data Reader from the
|
||||||
[standalone SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader),
|
[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
|
allowing it to support the same formats and receive updates along with the
|
||||||
[SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader).
|
[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
|
> [!IMPORTANT]
|
||||||
to read image metadata correctly, it is recommended to embed the `Prompt Saver` Node within the workflow
|
> Due to custom nodes and complex workflows potentially causing issues with SD Prompt Reader's ability
|
||||||
to ensure maximum compatibility.
|
> to read image metadata correctly, it is recommended to embed the `Prompt Saver` Node within the workflow
|
||||||
|
> to ensure maximum compatibility.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
|
#### `parameter_index`
|
||||||
- For images containing multiple sets of parameters, such as those processed through `hires-fix` or `refiner`,
|
- 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
|
you will need to modify the `parameter_index` to select the parameters you need
|
||||||
|
#### SDXL
|
||||||
- For images generated by SDXL and containing multiple sets of prompts,
|
- For images generated by SDXL and containing multiple sets of prompts,
|
||||||
the `text_g` will be combined with `text_l` into a single prompt
|
the `text_g` will be combined with `text_l` into a single prompt
|
||||||
- For batch processing, please use the `Batch Loader` node.
|
#### Batch Read
|
||||||
|
- For batch processing, please use the `Batch Loader` node. When using the `Batch Loader` node for bulk reading,
|
||||||
|
the preview image will not update, and the text box will only display the metadata of the last image.
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/loader2reader.png" width="50%" height="50%" alt="connect batch loader to prompt reader">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#### Additional Parameters
|
||||||
- To read parameters other than the existing output, please connect the `settings` to the `Parameter Extractor` node.
|
- To read parameters other than the existing output, please connect the `settings` to the `Parameter Extractor` node.
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/reader2extractor.png" width="50%" height="50%" alt="connect prompt reader to parameter extractor">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#### `MODEL_NAME`
|
||||||
- `MODEL_NAME` is a special output that matches the model name in the metadata with the existing models on the server
|
- `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:
|
according to the following priority:
|
||||||
1. Identical path, filename, and extension.
|
1. Identical path, filename, and extension.
|
||||||
@@ -90,108 +118,203 @@ e.g. `sd_xl_base.safetensors` will be matched with `SDXL\sd_xl_base.safetensors`
|
|||||||
e.g. `sd_xl_base` will be matched with `SDXL\sd_xl_base.safetensors`, and vice versa.
|
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.
|
4. If a matching model cannot be found, the original name will be outputted.
|
||||||
|
|
||||||
<div align="center">
|
</details>
|
||||||
<img src="./images/reader.png" width="25%" height="25%" alt="reader node">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Prompt Saver Node
|
### 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/).
|
|
||||||
- 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">
|
<div align="center">
|
||||||
<img src="./images/saver.png" width="25%" height="25%" alt="saver node">
|
<img src="./images/saver.png" width="25%" height="25%" alt="saver node">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Parameter Generator Node
|
- The `Prompt Saver` Node and the `Parameter Generator` Node are designed to be used together.
|
||||||
- Since it's not possible to directly extract metadata from `KSampler`, it is necessary to
|
- The `Prompt Saver` Node will write additional metadata in the A1111 format to the output images
|
||||||
use the `Parameter Generator` Node to generate parameters and simultaneously output them to both
|
to be compatible with any tools that support the A1111 format,
|
||||||
the `Prompt Saver` Node and `KSampler`.
|
including SD Prompt Reader and Civitai.
|
||||||
- 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/).
|
- Due to custom nodes and complex workflows potentially causing issues with SD Prompt Reader's ability
|
||||||
- The `model_version` and `aspect_ratio` are used only for calculating the optimal resolution of the selected model version
|
to read image metadata correctly, it's recommended to embed this node within the workflow
|
||||||
under the chosen aspect ratio. The calculation method is based on the
|
to ensure maximum compatibility.
|
||||||
[Stability AI development documentation](https://platform.stability.ai/docs/features/api-parameters#about-dimensions)
|
> [!TIP]
|
||||||
and the [StableSwarmUI source code](https://github.com/Stability-AI/StableSwarmUI) (developed by Stability AI).
|
> Since it's not possible to directly extract metadata from `KSampler`, it's necessary to
|
||||||
- `refiner_start` refers to the proportion of steps completed when the refiner starts running,
|
> use the `Parameter Generator` Node to generate parameters and simultaneously output them to
|
||||||
i.e., the proportion of base steps to total steps. This is used to calculate the `start_at_step` (`REFINER_START_STEP`)
|
> the `Prompt Saver` Node and `KSampler` Node.
|
||||||
required by the refiner `KSampler` under the selected step ratio.
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
|
#### Image Format
|
||||||
|
- Only PNG format supports embedding both metadata and workflow at the same time. Other formats can only embed metadata.
|
||||||
|
#### Duplicate Filename
|
||||||
|
- 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`.
|
||||||
|
#### Hashes & Auto-Detection on Civitai
|
||||||
|
- When `calculate_hash` is enabled, the node will compute the hash values of checkpoint, VAE, Lora,
|
||||||
|
and embedding/Textual Inversion, and write them into the metadata. After the server restarts, or a new checkpoint,
|
||||||
|
VAE, Lora, or embedding/Textual Inversion is loaded, the first image generation may take a longer time for hash calculation.
|
||||||
|
The hash value will be stored in temporary storage without the need for repeated calculation,
|
||||||
|
until the server is restarted.
|
||||||
|
- When `resource_hash` is enabled, the resource hashes will be written into the metadata to support auto-detection
|
||||||
|
on Civitai. This function will only run when `calculate_hash` is enabled.
|
||||||
|
- If you need to calculate the hash of Lora, please use the `Lora Loader` Node or the `Lora Selector` Node.
|
||||||
|
The hash value of the embedding/Textual Inversion will be automatically detected from the prompt.
|
||||||
|
#### `save_metadata_file`
|
||||||
|
- When the `save_metadata_file` is turned on, the metadata will be saved as a TXT file with the same name
|
||||||
|
alongside the image.
|
||||||
|
#### `date_format` & `time_format`
|
||||||
|
- For the `date_format` and `time_format`, please refer to
|
||||||
|
[strftime.org](https://strftime.org/) or [www.strfti.me](https://www.strfti.me/).
|
||||||
|
#### `filename` & `path`
|
||||||
|
- `%counter` cannot be used for `path`, it can only be used for `filename`. This `%counter` is slightly different
|
||||||
|
from the `%counter` in the built-in `Saver` node, it will count all image files in the `path`.
|
||||||
|
- Please refer to the following table for placeholders supported by the `filename` and `path`.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|------------|------------|
|
||||||
|
| %seed | %date |
|
||||||
|
| %steps | %time |
|
||||||
|
| %cfg | %counter |
|
||||||
|
| %model | %extension |
|
||||||
|
| %sampler | %quality |
|
||||||
|
| %scheduler | |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Parameter Generator Node
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./images/generator.png" width="25%" height="25%" alt="generator node">
|
<img src="./images/generator.png" width="25%" height="25%" alt="generator node">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
- Since it's not possible to directly extract metadata from `KSampler`, it's necessary to
|
||||||
|
use the `Parameter Generator` Node to generate parameters and simultaneously output them to both
|
||||||
|
the `Prompt Saver` Node and `KSampler` Node.
|
||||||
|
> [!TIP]
|
||||||
|
> 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/).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
|
#### Optimal Resolution
|
||||||
|
- 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`
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Batch Loader Node
|
### Batch Loader Node
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/loader.png" width="25%" height="25%" alt="loader node">
|
||||||
|
</div>
|
||||||
|
|
||||||
- The `Batch Loader` Node is specifically designed for the `Prompt Reader` Node to batch-read image files in a directory
|
- 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.
|
and cannot be used with other custom nodes.
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
- For batch processing, please connect the `IMAGE` output of the `Batch Loader` Node to the `image` input of
|
- For batch processing, please connect the `IMAGE` output of the `Batch Loader` Node to the `image` input of
|
||||||
the `Prompt Reader` Node.
|
the `Prompt Reader` Node.
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/loader2reader.png" width="50%" height="50%" alt="connect prompt reader to parameter extractor">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#### `path`
|
||||||
- The `path` supports relative paths such as `./input/` or absolute paths like `C:/Users/receyuki/Pictures`.
|
- The `path` supports relative paths such as `./input/` or absolute paths like `C:/Users/receyuki/Pictures`.
|
||||||
- Both `\ ` and `/` are acceptable.
|
- Both `\ ` and `/` are acceptable.
|
||||||
- You can also input a single file or a list of files into the `path`,
|
- 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.
|
in which case the `image_load_limit` and `start_index` will not function.
|
||||||
|
|
||||||
<div align="center">
|
</details>
|
||||||
<img src="./images/loader.png" width="25%" height="25%" alt="loader node">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Parameter Extractor Node
|
### 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">
|
<div align="center">
|
||||||
<img src="./images/extractor.png" width="25%" height="25%" alt="extractor node">
|
<img src="./images/extractor.png" width="25%" height="25%" alt="extractor node">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
- 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`
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
|
- 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/reader2extractor.png" width="50%" height="50%" alt="connect prompt reader to parameter extractor">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
### Prompt Merger Node & Type Converter Node
|
### Prompt Merger Node & Type Converter Node
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/merger_converter.png" width="25%" height="25%" alt="merger and converter node">
|
||||||
|
</div>
|
||||||
|
|
||||||
- Since the A1111 format cannot store `text_g` and `text_l` separately, SDXL users need to use
|
- 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.
|
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
|
- Since `model_name`, `sampler_name`, and `scheduler` are special types
|
||||||
that cannot be directly used by some other nodes,
|
that cannot be directly used by some other nodes,
|
||||||
You can use the `Type Converter` Node to convert them into `STRING` type.
|
You can use the `Type Converter` Node to convert them into `STRING` type.
|
||||||
|
|
||||||
|
### Lora Loader Node & Lora Selector Node
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./images/merger_converter.png" width="25%" height="25%" alt="merger and converter node">
|
<img src="./images/lora.png" width="50%" height="50%" alt="lora loader and selector node">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Example Workflow](https://raw.githubusercontent.com/receyuki/comfyui-prompt-reader-node/main/workflows/example_workflow.json)
|
- The `Lora Loader` Node and `Lora Selector` Node are used to write Lora data into metadata and support auto-detection
|
||||||
|
on Civitai.
|
||||||
|
<details>
|
||||||
|
<summary><b>More Information</b></summary>
|
||||||
|
|
||||||
|
- Replace the original loader with the `Lora Loader` Node, or connect the `LORA_NAME` output of the `Lora Selector` Node
|
||||||
|
to the `lora_name` input of other lora loaders (built-in or custom), and link the `NEXT_LORA` output to the `lora_name`
|
||||||
|
input of the `Prompt Saver` Node. Both of these nodes have the same function, please choose according to your needs.
|
||||||
|
#### Multiple Loras
|
||||||
|
- If you need to load multiple Loras, please connect the `Lora Loader` Node or `Lora Selector` Node head to tail
|
||||||
|
through `last_lora` and `NEXT_LORA`, and connect the `NEXT_LORA` at the end of the Lora chain to the `lora_name` input
|
||||||
|
of the `Prompt Saver` Node.
|
||||||
|
1. Lora Loader chain
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./images/example_workflow.png" width="100%" height="100%" alt="example workflow">
|
<img src="./images/lora_loader_chain.png" width="100%" height="100%" alt="lora loader chain">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
2. Lora Selector chain
|
||||||
|
<div align="center">
|
||||||
|
<img src="./images/lora_selector_chain.png" width="100%" height="100%" alt="lora loader chain">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Example Workflow
|
||||||
|
<details>
|
||||||
|
<summary><b>Simple Example</b></summary>
|
||||||
|
<div align="center">
|
||||||
|
<img src="./workflows/simple_example.png" width="100%" height="100%" alt="example workflow">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Lora Example</b></summary>
|
||||||
|
<div align="center">
|
||||||
|
<img src="./workflows/lora_example.png" width="100%" height="100%" alt="example workflow">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Hires-fix Example</b></summary>
|
||||||
|
<div align="center">
|
||||||
|
<img src="./workflows/hires_fix_example.png" width="100%" height="100%" alt="example workflow">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>SDXL Example</b></summary>
|
||||||
|
<div align="center">
|
||||||
|
<img src="./workflows/sdxl_example.png" width="100%" height="100%" alt="example workflow">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## Credits
|
## 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 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 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)
|
||||||
|
|||||||
@@ -6,20 +6,10 @@ import shutil
|
|||||||
import folder_paths
|
import folder_paths
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
WEB_DIRECTORY = "./js"
|
||||||
|
|
||||||
|
# remove old directory
|
||||||
comfy_path = os.path.dirname(folder_paths.__file__)
|
comfy_path = os.path.dirname(folder_paths.__file__)
|
||||||
tk_nodes_path = os.path.join(os.path.dirname(__file__))
|
old_dir = os.path.join(comfy_path, "web", "extensions", "SDPromptReader")
|
||||||
|
if os.path.exists(old_dir):
|
||||||
js_dest_path = os.path.join(comfy_path, "web", "extensions", "SDPromptReader")
|
shutil.rmtree(old_dir)
|
||||||
os.makedirs(js_dest_path, exist_ok=True)
|
|
||||||
|
|
||||||
files_to_copy = [
|
|
||||||
"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)
|
|
||||||
shutil.copy(js_src_path, js_dest_path)
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION = "1.2.0"
|
VERSION = "1.3.4"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 512 KiB After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 978 KiB |
@@ -1,14 +1,6 @@
|
|||||||
import {app} from "../../scripts/app.js";
|
import {app} from "../../scripts/app.js";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||||
|
import {createTextWidget} from "./utils.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({
|
app.registerExtension({
|
||||||
name: "sd_prompt_reader.extractorDisplay",
|
name: "sd_prompt_reader.extractorDisplay",
|
||||||
@@ -20,7 +12,8 @@ app.registerExtension({
|
|||||||
const result = onNodeCreated?.apply(this, arguments);
|
const result = onNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
// Create widgets
|
// Create widgets
|
||||||
const value_display = createWidget(app, this, "value_display", "STRING");
|
const styles = {textAlign: "center", fontSize: "0.75rem"}
|
||||||
|
const value_display = createTextWidget(app, this, "value_display", styles);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update widgets
|
// Update widgets
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {app} from "../../scripts/app.js";
|
import {app} from "../../scripts/app.js";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||||
|
import {createTextWidget} from "./utils.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
|
// Displays file list on the node
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
@@ -20,7 +13,8 @@ app.registerExtension({
|
|||||||
const result = onNodeCreated?.apply(this, arguments);
|
const result = onNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
// Create prompt and setting widgets
|
// Create prompt and setting widgets
|
||||||
const fileList = createWidget(app, this, "fileList");
|
const styles = {opacity: 0.7}
|
||||||
|
const fileList =createTextWidget(app, this, "fileList", styles);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import {app} from "../../scripts/app.js";
|
import {app} from "../../scripts/app.js";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||||
|
import {createTextWidget} from "./utils.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({
|
app.registerExtension({
|
||||||
name: "sd_prompt_reader.parameterDisplay",
|
name: "sd_prompt_reader.parameterDisplay",
|
||||||
@@ -20,8 +12,9 @@ app.registerExtension({
|
|||||||
const result = onNodeCreated?.apply(this, arguments);
|
const result = onNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
// Create widgets
|
// Create widgets
|
||||||
const steps_display = createWidget(app, this, "steps_display", "STRING");
|
const styles = {textAlign: "center", fontSize: "0.75rem"}
|
||||||
const aspect_ratio_display = createWidget(app, this, "aspect_ratio_display", "STRING");
|
const steps_display = createTextWidget(app, this, "steps_display", styles);
|
||||||
|
const aspect_ratio_display = createTextWidget(app, this, "aspect_ratio_display", styles);
|
||||||
|
|
||||||
// Resize the node
|
// Resize the node
|
||||||
const nodeWidth = this.size[0];
|
const nodeWidth = this.size[0];
|
||||||
@@ -66,10 +59,11 @@ Refiner start at step: ${message.text[6]} (${base_percentage})`;
|
|||||||
return `${ratio} - ${width*scalingFactor}x${height*scalingFactor}`;
|
return `${ratio} - ${width*scalingFactor}x${height*scalingFactor}`;
|
||||||
});
|
});
|
||||||
aspectRatioArray.unshift("custom")
|
aspectRatioArray.unshift("custom")
|
||||||
|
if (message.text[0] !== "custom") {
|
||||||
const aspectRatio = `${message.text[0]} - ${message.text[8][message.text[0]][0]*scalingFactor}x${message.text[8][message.text[0]][1]*scalingFactor}`
|
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").value = aspectRatio
|
||||||
|
}
|
||||||
this.widgets.find(obj => obj.name === "aspect_ratio").options.values = aspectRatioArray
|
this.widgets.find(obj => obj.name === "aspect_ratio").options.values = aspectRatioArray
|
||||||
this.widgets.find(obj => obj.name === "aspect_ratio").value = aspectRatio
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {app} from "../../scripts/app.js";
|
import {app} from "../../scripts/app.js";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||||
|
import {createTextWidget} from "./utils.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 prompt and setting on the node
|
// Displays prompt and setting on the node
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
@@ -20,9 +13,10 @@ app.registerExtension({
|
|||||||
const result = onNodeCreated?.apply(this, arguments);
|
const result = onNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
// Create prompt and setting widgets
|
// Create prompt and setting widgets
|
||||||
const positive = createWidget(app, this, "positive");
|
const styles = {opacity: 0.7}
|
||||||
const negative = createWidget(app, this, "negative");
|
const positive = createTextWidget(app, this, "positive", styles);
|
||||||
const setting = createWidget(app, this, "setting");
|
const negative = createTextWidget(app, this, "negative", styles);
|
||||||
|
const setting = createTextWidget(app, this, "setting", styles);
|
||||||
// Resize the node
|
// Resize the node
|
||||||
const nodeWidth = this.size[0];
|
const nodeWidth = this.size[0];
|
||||||
const nodeHeight = this.size[1];
|
const nodeHeight = this.size[1];
|
||||||
|
|||||||
@@ -1,16 +1,115 @@
|
|||||||
/**
|
/**
|
||||||
* Modified from: https://github.com/rgthree/rgthree-comfy/blob/main/web/seed.js
|
* Modified from: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/seed.js
|
||||||
* Modified by: receyuki
|
* Modified by: receyuki
|
||||||
*/
|
*/
|
||||||
import {app} from "../../scripts/app.js";
|
import {app} from "../../scripts/app.js";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||||
|
|
||||||
const LAST_SEED_BUTTON_LABEL = "(Use Last Queued Seed)";
|
const LAST_SEED_BUTTON_LABEL = "(Use last queued seed)";
|
||||||
const SPECIAL_SEED_RANDOM = -1;
|
const SPECIAL_SEED_RANDOM = -1;
|
||||||
const SPECIAL_SEED_INCREMENT = -2;
|
const SPECIAL_SEED_INCREMENT = -2;
|
||||||
const SPECIAL_SEED_DECREMENT = -3;
|
const SPECIAL_SEED_DECREMENT = -3;
|
||||||
const SPECIAL_SEEDS = [SPECIAL_SEED_RANDOM, SPECIAL_SEED_INCREMENT, SPECIAL_SEED_DECREMENT];
|
const SPECIAL_SEEDS = [SPECIAL_SEED_RANDOM, SPECIAL_SEED_INCREMENT, SPECIAL_SEED_DECREMENT];
|
||||||
|
|
||||||
|
function getResolver(timeout = 5000) {
|
||||||
|
const resolver = {};
|
||||||
|
resolver.id = generateId(8);
|
||||||
|
resolver.completed = false;
|
||||||
|
resolver.resolved = false;
|
||||||
|
resolver.rejected = false;
|
||||||
|
resolver.promise = new Promise((resolve, reject) => {
|
||||||
|
resolver.reject = () => {
|
||||||
|
resolver.completed = true;
|
||||||
|
resolver.rejected = true;
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
resolver.resolve = (data) => {
|
||||||
|
resolver.completed = true;
|
||||||
|
resolver.resolved = true;
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
resolver.timeout = setTimeout(() => {
|
||||||
|
if (!resolver.completed) {
|
||||||
|
resolver.reject();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateId(length) {
|
||||||
|
const arr = new Uint8Array(length / 2);
|
||||||
|
crypto.getRandomValues(arr);
|
||||||
|
return Array.from(arr, dec2hex).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dec2hex(dec) {
|
||||||
|
return dec.toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
let graphResolver = null;
|
||||||
|
|
||||||
|
function waitForGraph() {
|
||||||
|
if (graphResolver === null) {
|
||||||
|
graphResolver = getResolver();
|
||||||
|
|
||||||
|
function _wait() {
|
||||||
|
if (!graphResolver.completed) {
|
||||||
|
if (app === null || app === void 0 ? void 0 : app.graph) {
|
||||||
|
graphResolver.resolve(app.graph);
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(_wait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_wait();
|
||||||
|
}
|
||||||
|
return graphResolver.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rgthree extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.processingQueue = false;
|
||||||
|
this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = null;
|
||||||
|
this.initializeGraphAndCanvasHooks();
|
||||||
|
this.initializeComfyUIHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeGraphAndCanvasHooks() {
|
||||||
|
const rgthree = this;
|
||||||
|
const [graph] = await Promise.all([waitForGraph()]);
|
||||||
|
const onSerialize = graph.onSerialize;
|
||||||
|
graph.onSerialize = (data) => {
|
||||||
|
this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = data;
|
||||||
|
onSerialize === null || onSerialize === void 0 ? void 0 : onSerialize.call(graph, data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeComfyUIHooks() {
|
||||||
|
const rgthree = this;
|
||||||
|
const queuePrompt = app.queuePrompt;
|
||||||
|
app.queuePrompt = async function () {
|
||||||
|
rgthree.dispatchEvent(new CustomEvent("queue"));
|
||||||
|
rgthree.processingQueue = true;
|
||||||
|
try {
|
||||||
|
await queuePrompt.apply(app, [...arguments]);
|
||||||
|
} finally {
|
||||||
|
rgthree.processingQueue = false;
|
||||||
|
rgthree.dispatchEvent(new CustomEvent("queue-end"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeFromInitialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff(node) {
|
||||||
|
var _a, _b, _c;
|
||||||
|
return ((_c = (_b = (_a = this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff) === null || _a === void 0 ? void 0 : _a.nodes) === null || _b === void 0 ? void 0 : _b.find((n) => n.id === node.id)) !== null && _c !== void 0 ? _c : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgthree = new Rgthree();
|
||||||
|
|
||||||
class SeedControl {
|
class SeedControl {
|
||||||
constructor(node) {
|
constructor(node) {
|
||||||
this.lastSeed = undefined;
|
this.lastSeed = undefined;
|
||||||
@@ -33,6 +132,7 @@ class SeedControl {
|
|||||||
for (const [i, w] of this.node.widgets.entries()) {
|
for (const [i, w] of this.node.widgets.entries()) {
|
||||||
if (w.name === "seed") {
|
if (w.name === "seed") {
|
||||||
this.seedWidget = w;
|
this.seedWidget = w;
|
||||||
|
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||||
} else if (w.name === "control_after_generate") {
|
} else if (w.name === "control_after_generate") {
|
||||||
this.node.widgets.splice(i, 1);
|
this.node.widgets.splice(i, 1);
|
||||||
}
|
}
|
||||||
@@ -58,6 +158,9 @@ class SeedControl {
|
|||||||
this.lastSeedButton.disabled = true;
|
this.lastSeedButton.disabled = true;
|
||||||
this.seedWidget.serializeValue = async (node, index) => {
|
this.seedWidget.serializeValue = async (node, index) => {
|
||||||
const inputSeed = this.seedWidget.value;
|
const inputSeed = this.seedWidget.value;
|
||||||
|
if (!rgthree.processingQueue) {
|
||||||
|
return inputSeed;
|
||||||
|
}
|
||||||
this.serializedCtx = {
|
this.serializedCtx = {
|
||||||
inputSeed: this.seedWidget.value,
|
inputSeed: this.seedWidget.value,
|
||||||
};
|
};
|
||||||
@@ -76,7 +179,13 @@ class SeedControl {
|
|||||||
} else {
|
} else {
|
||||||
this.serializedCtx.seedUsed = this.seedWidget.value;
|
this.serializedCtx.seedUsed = this.seedWidget.value;
|
||||||
}
|
}
|
||||||
node.widgets_values[index] = this.serializedCtx.seedUsed;
|
const n = rgthree.getNodeFromInitialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff(node);
|
||||||
|
if (n) {
|
||||||
|
n.widgets_values[index] = this.serializedCtx.seedUsed;
|
||||||
|
} else {
|
||||||
|
console.warn('No serialized node found in workflow. May be attributed to '
|
||||||
|
+ 'https://github.com/comfyanonymous/ComfyUI/issues/2193');
|
||||||
|
}
|
||||||
this.seedWidget.value = this.serializedCtx.seedUsed;
|
this.seedWidget.value = this.serializedCtx.seedUsed;
|
||||||
this.lastSeed = this.serializedCtx.seedUsed;
|
this.lastSeed = this.serializedCtx.seedUsed;
|
||||||
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) {
|
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) {
|
||||||
@@ -97,42 +206,6 @@ class SeedControl {
|
|||||||
}
|
}
|
||||||
this.serializedCtx = {};
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +215,8 @@ app.registerExtension({
|
|||||||
if (nodeData.name === "SDParameterGenerator") {
|
if (nodeData.name === "SDParameterGenerator") {
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
const result = onNodeCreated?.apply(this, []);
|
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||||
this.seedControl = new SeedControl(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;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@author: receyuki
|
@author: receyuki
|
||||||
@title: SD Prompt Reader
|
@title: SD Prompt Reader
|
||||||
@nickname: SD Prompt Reader
|
@nickname: SD Prompt Reader
|
||||||
@description: ComfyUI node version of the SD Prompt Reader
|
@description: The ultimate solution for managing image metadata and multi-tool compatibility. ComfyUI node version of the SD Prompt Reader
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +30,6 @@ import folder_paths
|
|||||||
|
|
||||||
from .stable_diffusion_prompt_reader.sd_prompt_reader.constants import (
|
from .stable_diffusion_prompt_reader.sd_prompt_reader.constants import (
|
||||||
SUPPORTED_FORMATS,
|
SUPPORTED_FORMATS,
|
||||||
MESSAGE,
|
|
||||||
)
|
)
|
||||||
from .stable_diffusion_prompt_reader.sd_prompt_reader.image_data_reader import (
|
from .stable_diffusion_prompt_reader.sd_prompt_reader.image_data_reader import (
|
||||||
ImageDataReader,
|
ImageDataReader,
|
||||||
@@ -45,6 +44,15 @@ BLUE = "\033[1;34m"
|
|||||||
CYAN = "\033[36m"
|
CYAN = "\033[36m"
|
||||||
RESET = "\033[0m"
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
ERROR_MESSAGE = {
|
||||||
|
"format_error": "No data detected or unsupported format. "
|
||||||
|
"Please see the README for more details.\n"
|
||||||
|
"https://github.com/receyuki/comfyui-prompt-reader-node#supported-formats",
|
||||||
|
"complex_workflow": "The workflow is overly complex, or unsupported custom nodes have been used. "
|
||||||
|
"Please see the README for more details.\n"
|
||||||
|
"https://github.com/receyuki/comfyui-prompt-reader-node#prompt-reader-node",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def output_to_terminal(text: str):
|
def output_to_terminal(text: str):
|
||||||
print(f"{RESET+BLUE}" f"[SD Prompt Reader] " f"{CYAN+text+RESET}")
|
print(f"{RESET+BLUE}" f"[SD Prompt Reader] " f"{CYAN+text+RESET}")
|
||||||
@@ -133,6 +141,8 @@ class SDPromptReader:
|
|||||||
def load_image(self, image, parameter_index):
|
def load_image(self, image, parameter_index):
|
||||||
if image in SDPromptReader.files:
|
if image in SDPromptReader.files:
|
||||||
image_path = folder_paths.get_annotated_filepath(image)
|
image_path = folder_paths.get_annotated_filepath(image)
|
||||||
|
elif image.startswith("pasted/"):
|
||||||
|
image_path = folder_paths.get_annotated_filepath(image)
|
||||||
else:
|
else:
|
||||||
image_path = image
|
image_path = image
|
||||||
i = Image.open(image_path)
|
i = Image.open(image_path)
|
||||||
@@ -148,29 +158,45 @@ class SDPromptReader:
|
|||||||
|
|
||||||
file_path = Path(image_path)
|
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(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
image_data = ImageDataReader(f)
|
image_data = ImageDataReader(f)
|
||||||
if not image_data.tool:
|
if image_data.status.name == "COMFYUI_ERROR":
|
||||||
output_to_terminal(MESSAGE["format_error"][1])
|
output_to_terminal(ERROR_MESSAGE["complex_workflow"])
|
||||||
raise ValueError(MESSAGE["format_error"][1])
|
return self.error_output(
|
||||||
|
error_message=ERROR_MESSAGE["complex_workflow"],
|
||||||
|
image=image,
|
||||||
|
mask=mask,
|
||||||
|
width=i.width,
|
||||||
|
height=i.height,
|
||||||
|
filename=file_path.stem,
|
||||||
|
)
|
||||||
|
elif image_data.status.name in ["FORMAT_ERROR", "UNREAD"]:
|
||||||
|
output_to_terminal(ERROR_MESSAGE["format_error"])
|
||||||
|
return self.error_output(
|
||||||
|
error_message=ERROR_MESSAGE["format_error"],
|
||||||
|
image=image,
|
||||||
|
mask=mask,
|
||||||
|
width=i.width,
|
||||||
|
height=i.height,
|
||||||
|
filename=file_path.stem,
|
||||||
|
)
|
||||||
|
|
||||||
seed = int(
|
seed = int(
|
||||||
self.param_parser(image_data.parameter.get("seed"), parameter_index)
|
self.param_parser(image_data.parameter.get("seed", 0), parameter_index)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
steps = int(
|
steps = int(
|
||||||
self.param_parser(image_data.parameter.get("steps"), parameter_index)
|
self.param_parser(image_data.parameter.get("steps", 0), parameter_index)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
cfg = float(
|
cfg = float(
|
||||||
self.param_parser(image_data.parameter.get("cfg"), parameter_index) or 0
|
self.param_parser(image_data.parameter.get("cfg", 0), parameter_index)
|
||||||
|
or 0
|
||||||
)
|
)
|
||||||
model = str(
|
model = str(
|
||||||
self.param_parser(image_data.parameter.get("model"), parameter_index)
|
self.param_parser(
|
||||||
|
image_data.parameter.get("model", ""), parameter_index
|
||||||
|
)
|
||||||
or ""
|
or ""
|
||||||
)
|
)
|
||||||
width = int(image_data.width or 0)
|
width = int(image_data.width or 0)
|
||||||
@@ -204,8 +230,12 @@ class SDPromptReader:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def param_parser(data: str, index: int):
|
def param_parser(data: str, index: int):
|
||||||
data_list = data.strip("()").split(",")
|
try:
|
||||||
return data_list[0] if len(data_list) == 1 else data_list[index]
|
data_list = data.strip("()").split(",")
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return data_list[0] if len(data_list) == 1 else data_list[index]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_model(model: str):
|
def search_model(model: str):
|
||||||
@@ -228,21 +258,52 @@ class SDPromptReader:
|
|||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error_output(
|
||||||
|
error_message, image=None, mask=None, width=0, height=0, filename=""
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"ui": {"text": ("", "", error_message)},
|
||||||
|
"result": (
|
||||||
|
image,
|
||||||
|
mask,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
"",
|
||||||
|
filename,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def IS_CHANGED(s, image, parameter_index):
|
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:
|
with open(Path(image_path), "rb") as f:
|
||||||
image_data = ImageDataReader(f)
|
image_data = ImageDataReader(f)
|
||||||
return image_data.props
|
return image_data.props
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def VALIDATE_INPUTS(s, image, parameter_index):
|
def VALIDATE_INPUTS(s, image):
|
||||||
if not folder_paths.exists_annotated_filepath(image):
|
|
||||||
return "Invalid image file: {}".format(image)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class SDPromptSaver:
|
class SDPromptSaver:
|
||||||
|
model_hash_dict = {}
|
||||||
|
vae_hash_dict = {}
|
||||||
|
lora_hash_dict = {}
|
||||||
|
ti_hash_dict = {}
|
||||||
|
ti_paths = []
|
||||||
|
ti_names = []
|
||||||
|
ti_stems = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.output_dir = folder_paths.get_output_directory()
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
self.type = "output"
|
self.type = "output"
|
||||||
@@ -250,6 +311,10 @@ class SDPromptSaver:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
|
for file in folder_paths.get_filename_list("embeddings"):
|
||||||
|
SDPromptSaver.ti_paths.append(file)
|
||||||
|
SDPromptSaver.ti_names.append(Path(file).name)
|
||||||
|
SDPromptSaver.ti_stems.append(Path(file).stem)
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
@@ -262,6 +327,7 @@ class SDPromptSaver:
|
|||||||
"path": ("STRING", {"default": "%date/", "multiline": False}),
|
"path": ("STRING", {"default": "%date/", "multiline": False}),
|
||||||
"model_name": (folder_paths.get_filename_list("checkpoints"),),
|
"model_name": (folder_paths.get_filename_list("checkpoints"),),
|
||||||
# "model_name_str": ("STRING", {"default": ""}),
|
# "model_name_str": ("STRING", {"default": ""}),
|
||||||
|
"vae_name": (folder_paths.get_filename_list("vae"),),
|
||||||
"seed": (
|
"seed": (
|
||||||
"INT",
|
"INT",
|
||||||
{
|
{
|
||||||
@@ -288,18 +354,20 @@ class SDPromptSaver:
|
|||||||
# "sampler_name_str": ("STRING", {"default": ""}),
|
# "sampler_name_str": ("STRING", {"default": ""}),
|
||||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
|
||||||
# "scheduler_str": ("STRING", {"default": ""}),
|
# "scheduler_str": ("STRING", {"default": ""}),
|
||||||
|
"lora_name": any_type,
|
||||||
"width": (
|
"width": (
|
||||||
"INT",
|
"INT",
|
||||||
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 8},
|
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 1},
|
||||||
),
|
),
|
||||||
"height": (
|
"height": (
|
||||||
"INT",
|
"INT",
|
||||||
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 8},
|
{"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 1},
|
||||||
),
|
),
|
||||||
"positive": ("STRING", {"default": "", "multiline": True}),
|
"positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
"negative": ("STRING", {"default": "", "multiline": True}),
|
"negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extension": (["png", "jpg", "webp"],),
|
"extension": (["png", "jpg", "jpeg", "webp"],),
|
||||||
"calculate_model_hash": ("BOOLEAN", {"default": False}),
|
"calculate_hash": ("BOOLEAN", {"default": True}),
|
||||||
|
"resource_hash": ("BOOLEAN", {"default": True}),
|
||||||
"lossless_webp": ("BOOLEAN", {"default": True}),
|
"lossless_webp": ("BOOLEAN", {"default": True}),
|
||||||
"jpg_webp_quality": ("INT", {"default": 100, "min": 1, "max": 100}),
|
"jpg_webp_quality": ("INT", {"default": 100, "min": 1, "max": 100}),
|
||||||
"date_format": (
|
"date_format": (
|
||||||
@@ -331,6 +399,7 @@ class SDPromptSaver:
|
|||||||
path: str = "%date/",
|
path: str = "%date/",
|
||||||
model_name: str = "",
|
model_name: str = "",
|
||||||
model_name_str: str = "",
|
model_name_str: str = "",
|
||||||
|
vae_name: str = "",
|
||||||
seed: int = 0,
|
seed: int = 0,
|
||||||
steps: int = 0,
|
steps: int = 0,
|
||||||
cfg: float = 0.0,
|
cfg: float = 0.0,
|
||||||
@@ -338,12 +407,14 @@ class SDPromptSaver:
|
|||||||
sampler_name_str: str = "",
|
sampler_name_str: str = "",
|
||||||
scheduler: str = "",
|
scheduler: str = "",
|
||||||
scheduler_str: str = "",
|
scheduler_str: str = "",
|
||||||
|
lora_name=None,
|
||||||
width: int = 1,
|
width: int = 1,
|
||||||
height: int = 1,
|
height: int = 1,
|
||||||
positive: str = "",
|
positive: str = "",
|
||||||
negative: str = "",
|
negative: str = "",
|
||||||
extension: str = "png",
|
extension: str = "png",
|
||||||
calculate_model_hash: bool = False,
|
calculate_hash: bool = True,
|
||||||
|
resource_hash: bool = True,
|
||||||
lossless_webp: bool = True,
|
lossless_webp: bool = True,
|
||||||
jpg_webp_quality: int = 100,
|
jpg_webp_quality: int = 100,
|
||||||
date_format: str = "%Y-%m-%d",
|
date_format: str = "%Y-%m-%d",
|
||||||
@@ -385,8 +456,10 @@ class SDPromptSaver:
|
|||||||
"%seed": seed,
|
"%seed": seed,
|
||||||
"%steps": steps,
|
"%steps": steps,
|
||||||
"%cfg": cfg,
|
"%cfg": cfg,
|
||||||
|
"%width": width,
|
||||||
|
"%height": height,
|
||||||
"%extension": extension,
|
"%extension": extension,
|
||||||
"%model": model_name_real,
|
"%model": Path(model_name_real).stem,
|
||||||
"%sampler": sampler_name_real,
|
"%sampler": sampler_name_real,
|
||||||
"%scheduler": scheduler_real,
|
"%scheduler": scheduler_real,
|
||||||
"%quality": jpg_webp_quality,
|
"%quality": jpg_webp_quality,
|
||||||
@@ -401,11 +474,68 @@ class SDPromptSaver:
|
|||||||
i = 255.0 * image.cpu().numpy()
|
i = 255.0 * image.cpu().numpy()
|
||||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||||
metadata = None
|
metadata = None
|
||||||
model_hash = (
|
|
||||||
f"Model hash: {self.calculate_model_hash(model_name_real)}, "
|
model_hash_str = ""
|
||||||
if calculate_model_hash
|
vae_hash_str = ""
|
||||||
else ""
|
vae_str = ""
|
||||||
|
lora_hash_dict = {}
|
||||||
|
lora_hash_str = ""
|
||||||
|
ti_hash_dict = {}
|
||||||
|
ti_hash_str = ""
|
||||||
|
|
||||||
|
if vae_name:
|
||||||
|
vae_str = f"VAE: {Path(vae_name).stem}, "
|
||||||
|
|
||||||
|
hashes = {}
|
||||||
|
if calculate_hash:
|
||||||
|
if model_name_real:
|
||||||
|
model_hash = self.calculate_hash(model_name_real, "model")
|
||||||
|
model_hash_str = f"Model hash: {model_hash}, "
|
||||||
|
hashes["model"] = model_hash
|
||||||
|
|
||||||
|
if vae_name:
|
||||||
|
vae_hash = self.calculate_hash(vae_name, "vae")
|
||||||
|
vae_hash_str = f"VAE hash: {vae_hash}, "
|
||||||
|
hashes["vae"] = vae_hash
|
||||||
|
|
||||||
|
if lora_name:
|
||||||
|
lora_names = (
|
||||||
|
lora_name if isinstance(lora_name, list) else [lora_name]
|
||||||
|
)
|
||||||
|
lora_names_unique = list(set(lora_names))
|
||||||
|
for name in lora_names_unique:
|
||||||
|
lora_hash = self.calculate_hash(name, "lora")
|
||||||
|
lora_hash_dict[Path(name).stem] = lora_hash
|
||||||
|
hashes[f"lora:{Path(name).stem}"] = lora_hash
|
||||||
|
lora_hash_items = [f"{k}: {v}" for k, v in lora_hash_dict.items()]
|
||||||
|
lora_hash_str_value = ", ".join(lora_hash_items)
|
||||||
|
lora_hash_str = f'Lora hashes: "{lora_hash_str_value}", '
|
||||||
|
|
||||||
|
ti_pattern = (
|
||||||
|
r"(?:\(|\s|,)?" # match an optional opening parenthesis, space, or comma
|
||||||
|
r"embedding:" # match the literal text "embedding:"
|
||||||
|
r"([^\s:,()]+)" # match a string that does not contain spaces, colons, commas, or parentheses
|
||||||
|
r"(?:\.(?:pt|safetensors))?" # optionally match a file extension ".pt" or ".safetensors"
|
||||||
|
r"(?::\d+(?:\.\d+)?)?" # optionally match a colon followed by numbers,
|
||||||
|
# with an optional decimal part (e.g., ":1" or ":1.0")
|
||||||
|
r"(?:\)|,|\s)?" # optionally match a closing parenthesis, comma, or space
|
||||||
|
)
|
||||||
|
ti_names = re.findall(ti_pattern, f"{positive}/n{negative}")
|
||||||
|
ti_names_with_ext = [self.search_ti(name) for name in ti_names]
|
||||||
|
|
||||||
|
for name in ti_names_with_ext:
|
||||||
|
if name:
|
||||||
|
ti_hash = self.calculate_hash(name, "ti")
|
||||||
|
ti_hash_dict[Path(name).stem] = ti_hash
|
||||||
|
hashes[f"embed:{Path(name).stem}"] = ti_hash
|
||||||
|
ti_hash_items = [f"{k}: {v}" for k, v in ti_hash_dict.items()]
|
||||||
|
ti_hash_str_value = ", ".join(ti_hash_items)
|
||||||
|
ti_hash_str = f'TI hashes: "{ti_hash_str_value}", '
|
||||||
|
|
||||||
|
hashes_str = (
|
||||||
|
f", Hashes: {json.dumps(hashes)}" if (hashes and resource_hash) else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
comment = (
|
comment = (
|
||||||
f"{positive}\n"
|
f"{positive}\n"
|
||||||
f"Negative prompt: {negative}\n"
|
f"Negative prompt: {negative}\n"
|
||||||
@@ -414,9 +544,14 @@ class SDPromptSaver:
|
|||||||
f"CFG scale: {cfg}, "
|
f"CFG scale: {cfg}, "
|
||||||
f"Seed: {seed}, "
|
f"Seed: {seed}, "
|
||||||
f"Size: {img.width if width==0 else width}x{img.height if height==0 else height}, "
|
f"Size: {img.width if width==0 else width}x{img.height if height==0 else height}, "
|
||||||
f"{model_hash}"
|
f"{model_hash_str}"
|
||||||
f"Model: {Path(model_name_real).stem}, "
|
f"Model: {Path(model_name_real).stem}, "
|
||||||
|
f"{vae_hash_str}"
|
||||||
|
f"{vae_str}"
|
||||||
|
f"{lora_hash_str}"
|
||||||
|
f"{ti_hash_str}"
|
||||||
f"Version: ComfyUI"
|
f"Version: ComfyUI"
|
||||||
|
f"{hashes_str}"
|
||||||
f"{extra_info_real}"
|
f"{extra_info_real}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -468,19 +603,47 @@ class SDPromptSaver:
|
|||||||
output_to_terminal("Saved file: " + str(file))
|
output_to_terminal("Saved file: " + str(file))
|
||||||
comments.append(comment)
|
comments.append(comment)
|
||||||
|
|
||||||
return {"ui": {"images": results}, "result": (files, file_paths, comments)}
|
return {
|
||||||
|
"ui": {"images": results},
|
||||||
|
"result": (
|
||||||
|
self.unpack_singleton(files),
|
||||||
|
self.unpack_singleton(file_paths),
|
||||||
|
self.unpack_singleton(comments),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_model_hash(model_name):
|
def calculate_hash(name, hash_type):
|
||||||
|
match hash_type:
|
||||||
|
case "model":
|
||||||
|
hash_dict = SDPromptSaver.model_hash_dict
|
||||||
|
file_name = folder_paths.get_full_path("checkpoints", name)
|
||||||
|
case "vae":
|
||||||
|
hash_dict = SDPromptSaver.vae_hash_dict
|
||||||
|
file_name = folder_paths.get_full_path("vae", name)
|
||||||
|
case "lora":
|
||||||
|
hash_dict = SDPromptSaver.lora_hash_dict
|
||||||
|
file_name = folder_paths.get_full_path("loras", name)
|
||||||
|
case "ti":
|
||||||
|
hash_dict = SDPromptSaver.ti_hash_dict
|
||||||
|
file_name = folder_paths.get_full_path("embeddings", name)
|
||||||
|
case _:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if hash_value := hash_dict.get(name):
|
||||||
|
return hash_value
|
||||||
|
|
||||||
hash_sha256 = hashlib.sha256()
|
hash_sha256 = hashlib.sha256()
|
||||||
blksize = 1024 * 1024
|
blksize = 1024 * 1024
|
||||||
file_name = folder_paths.get_full_path("checkpoints", model_name)
|
|
||||||
|
|
||||||
with open(file_name, "rb") as f:
|
with open(file_name, "rb") as f:
|
||||||
for chunk in iter(lambda: f.read(blksize), b""):
|
for chunk in iter(lambda: f.read(blksize), b""):
|
||||||
hash_sha256.update(chunk)
|
hash_sha256.update(chunk)
|
||||||
|
|
||||||
return hash_sha256.hexdigest()[:10]
|
hash_value = hash_sha256.hexdigest()[:10]
|
||||||
|
hash_dict[name] = hash_value
|
||||||
|
|
||||||
|
return hash_value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_counter(directory: Path):
|
def get_counter(directory: Path):
|
||||||
@@ -506,16 +669,33 @@ class SDPromptSaver:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_unique_filename(stem: Path, extension: str, output_folder: Path):
|
def get_unique_filename(stem: Path, extension: str, output_folder: Path):
|
||||||
file = stem.with_suffix(f".{extension}")
|
file = stem.with_suffix(f"{stem.suffix}.{extension}")
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
while (output_folder / file).exists():
|
while (output_folder / file).exists():
|
||||||
index += 1
|
index += 1
|
||||||
new_stem = f"{stem}_{index}"
|
new_stem = Path(f"{stem}_{index}")
|
||||||
file = Path(new_stem).with_suffix(f".{extension}")
|
file = new_stem.with_suffix(f"{new_stem.suffix}.{extension}")
|
||||||
|
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def search_ti(ti: str):
|
||||||
|
if not ti or ti in SDPromptSaver.ti_paths:
|
||||||
|
return ti
|
||||||
|
|
||||||
|
if ti in SDPromptSaver.ti_stems:
|
||||||
|
return SDPromptSaver.ti_paths[SDPromptSaver.ti_stems.index(ti)]
|
||||||
|
|
||||||
|
if ti in SDPromptSaver.ti_names:
|
||||||
|
return SDPromptSaver.ti_paths[SDPromptSaver.ti_names.index(ti)]
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unpack_singleton(arr: list):
|
||||||
|
return arr[0] if len(arr) == 1 else arr
|
||||||
|
|
||||||
|
|
||||||
class SDParameterGenerator:
|
class SDParameterGenerator:
|
||||||
ASPECT_RATIO_MAP = {
|
ASPECT_RATIO_MAP = {
|
||||||
@@ -608,11 +788,11 @@ class SDParameterGenerator:
|
|||||||
),
|
),
|
||||||
"width": (
|
"width": (
|
||||||
"INT",
|
"INT",
|
||||||
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
|
{"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8},
|
||||||
),
|
),
|
||||||
"height": (
|
"height": (
|
||||||
"INT",
|
"INT",
|
||||||
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8},
|
{"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8},
|
||||||
),
|
),
|
||||||
"batch_size": (
|
"batch_size": (
|
||||||
"INT",
|
"INT",
|
||||||
@@ -627,6 +807,7 @@ class SDParameterGenerator:
|
|||||||
|
|
||||||
RETURN_TYPES = (
|
RETURN_TYPES = (
|
||||||
folder_paths.get_filename_list("checkpoints"),
|
folder_paths.get_filename_list("checkpoints"),
|
||||||
|
folder_paths.get_filename_list("vae"),
|
||||||
"MODEL",
|
"MODEL",
|
||||||
"CLIP",
|
"CLIP",
|
||||||
"VAE",
|
"VAE",
|
||||||
@@ -646,6 +827,7 @@ class SDParameterGenerator:
|
|||||||
|
|
||||||
RETURN_NAMES = (
|
RETURN_NAMES = (
|
||||||
"MODEL_NAME",
|
"MODEL_NAME",
|
||||||
|
"VAE_NAME",
|
||||||
"MODEL",
|
"MODEL",
|
||||||
"CLIP",
|
"CLIP",
|
||||||
"VAE",
|
"VAE",
|
||||||
@@ -687,9 +869,6 @@ class SDParameterGenerator:
|
|||||||
output_vae=True,
|
output_vae=True,
|
||||||
output_clip=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)
|
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
||||||
if config_name != "none":
|
if config_name != "none":
|
||||||
config_path = folder_paths.get_full_path("configs", config_name)
|
config_path = folder_paths.get_full_path("configs", config_name)
|
||||||
@@ -709,10 +888,15 @@ class SDParameterGenerator:
|
|||||||
)[:3]
|
)[:3]
|
||||||
|
|
||||||
if vae_name != "baked VAE":
|
if vae_name != "baked VAE":
|
||||||
|
vae_name_real = vae_name
|
||||||
vae_path = folder_paths.get_full_path("vae", vae_name)
|
vae_path = folder_paths.get_full_path("vae", vae_name)
|
||||||
sd = comfy.utils.load_torch_file(vae_path)
|
sd = comfy.utils.load_torch_file(vae_path)
|
||||||
vae = comfy.sd.VAE(sd=sd)
|
vae = comfy.sd.VAE(sd=sd)
|
||||||
checkpoint = (*checkpoint[:2], vae)
|
checkpoint = (*checkpoint[:2], vae)
|
||||||
|
vae_str = f"VAE: {vae_name}, \n"
|
||||||
|
else:
|
||||||
|
vae_str = ""
|
||||||
|
vae_name_real = ""
|
||||||
|
|
||||||
if aspect_ratio != "custom":
|
if aspect_ratio != "custom":
|
||||||
aspect_ratio_value = aspect_ratio.split(" - ")[0]
|
aspect_ratio_value = aspect_ratio.split(" - ")[0]
|
||||||
@@ -738,6 +922,7 @@ class SDParameterGenerator:
|
|||||||
|
|
||||||
parameters = (
|
parameters = (
|
||||||
f"Model: {ckpt_name},\n"
|
f"Model: {ckpt_name},\n"
|
||||||
|
f"{vae_str}"
|
||||||
f"Seed: {str(seed)},\n"
|
f"Seed: {str(seed)},\n"
|
||||||
f"Steps: {str(steps)},\n"
|
f"Steps: {str(steps)},\n"
|
||||||
f"CFG scale: {str(cfg)},\n"
|
f"CFG scale: {str(cfg)},\n"
|
||||||
@@ -764,7 +949,10 @@ class SDParameterGenerator:
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
"result": (
|
"result": (
|
||||||
(ckpt_name,)
|
(
|
||||||
|
ckpt_name,
|
||||||
|
vae_name_real,
|
||||||
|
)
|
||||||
+ checkpoint
|
+ checkpoint
|
||||||
+ (
|
+ (
|
||||||
seed,
|
seed,
|
||||||
@@ -783,6 +971,10 @@ class SDParameterGenerator:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def VALIDATE_INPUTS(s, aspect_ratio):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class SDPromptMerger:
|
class SDPromptMerger:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -852,6 +1044,30 @@ class SDTypeConverter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SDAnyConverter:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {},
|
||||||
|
"optional": {
|
||||||
|
"any_type_input": (
|
||||||
|
any_type,
|
||||||
|
{"forceInput": True},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (any_type,)
|
||||||
|
|
||||||
|
RETURN_NAMES = ("ANY_TYPE_OUTPUT",)
|
||||||
|
|
||||||
|
FUNCTION = "convert_any"
|
||||||
|
CATEGORY = "SD Prompt Reader"
|
||||||
|
|
||||||
|
def convert_any(self, any_type_input: str = ""):
|
||||||
|
return (any_type_input,)
|
||||||
|
|
||||||
|
|
||||||
class SDBatchLoader:
|
class SDBatchLoader:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
@@ -869,6 +1085,7 @@ class SDBatchLoader:
|
|||||||
|
|
||||||
RETURN_NAMES = ("IMAGE",)
|
RETURN_NAMES = ("IMAGE",)
|
||||||
OUTPUT_IS_LIST = (True,)
|
OUTPUT_IS_LIST = (True,)
|
||||||
|
OUTPUT_NODE = True
|
||||||
FUNCTION = "load_path"
|
FUNCTION = "load_path"
|
||||||
CATEGORY = "SD Prompt Reader"
|
CATEGORY = "SD Prompt Reader"
|
||||||
|
|
||||||
@@ -923,19 +1140,6 @@ class SDBatchLoader:
|
|||||||
):
|
):
|
||||||
return os.listdir(path)
|
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:
|
class SDParameterExtractor:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1018,17 +1222,20 @@ class SDParameterExtractor:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_setting(settings):
|
def parse_setting(settings):
|
||||||
pattern = re.compile(r"([^:,]+):\s*\(([^)]+)\)|([^:,]+):\s*([^,]+)")
|
pattern = re.compile(r"([^:,]+):\s*\(([^)]+)\)|([^:,]+):\s*\"([^\"]+)\"|([^:,]+):\s*([^,]+)")
|
||||||
|
|
||||||
matches = pattern.findall(settings)
|
matches = pattern.findall(settings)
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
for match in matches:
|
for match in matches:
|
||||||
key, value_paren, key_nonparen, value_nonparen = match
|
key_paren, value_paren, key_quotes, value_quotes, key_nonparen, value_nonparen = match
|
||||||
if key:
|
if key_paren:
|
||||||
key = key.strip()
|
key = key_paren.strip()
|
||||||
value = value_paren.strip()
|
value = value_paren.strip()
|
||||||
value = tuple(v.strip() for v in value.split(","))
|
value = tuple(v.strip() for v in value.split(","))
|
||||||
|
elif key_quotes:
|
||||||
|
key = key_quotes.strip()
|
||||||
|
value = value_quotes.strip()
|
||||||
else:
|
else:
|
||||||
key = key_nonparen.strip()
|
key = key_nonparen.strip()
|
||||||
value = value_nonparen.strip()
|
value = value_nonparen.strip()
|
||||||
@@ -1036,6 +1243,93 @@ class SDParameterExtractor:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def VALIDATE_INPUTS(s, parameter):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SDLoraLoader:
|
||||||
|
def __init__(self):
|
||||||
|
self.loaded_lora = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"model": ("MODEL",),
|
||||||
|
"clip": ("CLIP",),
|
||||||
|
"lora_name": (folder_paths.get_filename_list("loras"),),
|
||||||
|
"strength_model": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01},
|
||||||
|
),
|
||||||
|
"strength_clip": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"last_lora": (any_type,),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("MODEL", "CLIP", any_type)
|
||||||
|
RETURN_NAMES = ("MODEL", "CLIP", "NEXT_LORA")
|
||||||
|
FUNCTION = "load_lora"
|
||||||
|
|
||||||
|
CATEGORY = "SD Prompt Reader"
|
||||||
|
|
||||||
|
def load_lora(
|
||||||
|
self, model, clip, lora_name, strength_model, strength_clip, last_lora=None
|
||||||
|
):
|
||||||
|
if strength_model == 0 and strength_clip == 0:
|
||||||
|
return (model, clip, lora_name)
|
||||||
|
|
||||||
|
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||||
|
lora = None
|
||||||
|
if self.loaded_lora is not None:
|
||||||
|
if self.loaded_lora[0] == lora_path:
|
||||||
|
lora = self.loaded_lora[1]
|
||||||
|
else:
|
||||||
|
temp = self.loaded_lora
|
||||||
|
self.loaded_lora = None
|
||||||
|
del temp
|
||||||
|
|
||||||
|
if lora is None:
|
||||||
|
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
||||||
|
self.loaded_lora = (lora_path, lora)
|
||||||
|
|
||||||
|
model_lora, clip_lora = comfy.sd.load_lora_for_models(
|
||||||
|
model, clip, lora, strength_model, strength_clip
|
||||||
|
)
|
||||||
|
|
||||||
|
next_lora = last_lora + [lora_name] if last_lora else [lora_name]
|
||||||
|
|
||||||
|
return (model_lora, clip_lora, next_lora)
|
||||||
|
|
||||||
|
|
||||||
|
class SDLoraSelector:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"lora_name": (folder_paths.get_filename_list("loras"),),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"last_lora": (any_type,),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (folder_paths.get_filename_list("loras"), any_type)
|
||||||
|
RETURN_NAMES = ("LORA_NAME", "NEXT_LORA")
|
||||||
|
FUNCTION = "get_name"
|
||||||
|
|
||||||
|
CATEGORY = "SD Prompt Reader"
|
||||||
|
|
||||||
|
def get_name(self, lora_name, last_lora=None):
|
||||||
|
next_lora = last_lora + [lora_name] if last_lora else [lora_name]
|
||||||
|
return (lora_name, next_lora)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"SDPromptReader": SDPromptReader,
|
"SDPromptReader": SDPromptReader,
|
||||||
@@ -1043,8 +1337,11 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SDParameterGenerator": SDParameterGenerator,
|
"SDParameterGenerator": SDParameterGenerator,
|
||||||
"SDPromptMerger": SDPromptMerger,
|
"SDPromptMerger": SDPromptMerger,
|
||||||
"SDTypeConverter": SDTypeConverter,
|
"SDTypeConverter": SDTypeConverter,
|
||||||
|
"SDAnyConverter": SDAnyConverter,
|
||||||
"SDBatchLoader": SDBatchLoader,
|
"SDBatchLoader": SDBatchLoader,
|
||||||
"SDParameterExtractor": SDParameterExtractor,
|
"SDParameterExtractor": SDParameterExtractor,
|
||||||
|
"SDLoraLoader": SDLoraLoader,
|
||||||
|
"SDLoraSelector": SDLoraSelector,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -1053,6 +1350,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SDParameterGenerator": "SD Parameter Generator",
|
"SDParameterGenerator": "SD Parameter Generator",
|
||||||
"SDPromptMerger": "SD Prompt Merger",
|
"SDPromptMerger": "SD Prompt Merger",
|
||||||
"SDTypeConverter": "SD Type Converter",
|
"SDTypeConverter": "SD Type Converter",
|
||||||
|
"SDAnyConverter": "SD Any Converter",
|
||||||
"SDBatchLoader": "SD Batch Loader",
|
"SDBatchLoader": "SD Batch Loader",
|
||||||
"SDParameterExtractor": "SD Parameter Extractor",
|
"SDParameterExtractor": "SD Parameter Extractor",
|
||||||
|
"SDLoraLoader": "SD Lora Loader",
|
||||||
|
"SDLoraSelector": "SD Lora Selector",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-prompt-reader-node"
|
||||||
|
description = "The ultimate solution for managing image metadata and multi-tool compatibility. ComfyUI node version of the SD Prompt Reader."
|
||||||
|
version = "1.3.4"
|
||||||
|
license = { text = "MIT License" }
|
||||||
|
dependencies = ["piexif"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/receyuki/comfyui-prompt-reader-node"
|
||||||
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "receyuki"
|
||||||
|
DisplayName = "comfyui-prompt-reader-node"
|
||||||
|
Icon = "https://raw.githubusercontent.com/receyuki/comfyui-prompt-reader-node/main/images/icon-400.png"
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 719 KiB |
|
After Width: | Height: | Size: 655 KiB |
|
After Width: | Height: | Size: 632 KiB |