Compare commits

...
61 Commits
Author SHA1 Message Date
Rhys Yang a88722ce8f Merge pull request #107 from Comfy-Org/main
Checkout submodules before publishing
2025-02-01 23:51:39 +08:00
Robin Huang c0b66c4e07 Checkout submodules before publishing 2024-12-31 14:37:36 -08:00
Rhys Yang d191668607 Update pyproject.toml 2024-09-06 19:40:28 +08:00
Rhys Yang a70cbb0c8d Bump version to 1.3.4 2024-09-06 19:30:16 +08:00
Rhys Yang b95499617e Update CHANGELOG.md 2024-09-06 19:29:57 +08:00
Rhys Yang 77095ee38b Fix the input validation of the Parameter Generator node #89
Fix the input validation of the `Parameter Extractor` node #90
2024-09-06 18:24:13 +08:00
Rhys Yang 1e59445091 Merge pull request #87 from spagatticode/ExtractorFix
Update the `Parameter Extractor` node to support parsing quoted parameters
2024-09-06 18:22:55 +08:00
Spag@tti_code b3e66eb21c Merge branch 'ExtractorFix' of https://github.com/spagatticode/comfyui-prompt-reader-node into ExtractorFix
# Conflicts:
#	nodes.py
2024-08-13 13:31:06 +02:00
Spag@tti_code 145fbbb158 Parse parameters in quotes 2024-08-13 13:25:05 +02:00
Spag@tti_code 7c215dde00 Parse parameters in quotes 2024-08-13 12:59:20 +02:00
Rhys Yang 07a1a5314d Merge pull request #85 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-08-04 21:43:17 +08:00
snomiao ed6533422e chore(licence-update): Update PyProject Toml - License 2024-08-02 23:03:26 +00:00
Rhys Yang 278e32450f Fix the Parameter Generator unable to choose standard resolutions #82 2024-07-15 23:51:33 +08:00
Rhys Yang b248fcf346 Fix the Prompt Saver unable to correctly handle filenames containing . #80 2024-06-28 15:20:16 +08:00
Rhys Yang 4b35b41d08 Add Any Converter node #76 #78 2024-06-07 07:39:14 +08:00
Rhys Yang af96defbe0 Revert "Change the Type Converter node to a node that converts any type to a * type"
This reverts commit e8ca90fe35.
2024-06-07 07:04:14 +08:00
Rhys Yang e8ca90fe35 Change the Type Converter node to a node that converts any type to a * type 2024-06-07 01:32:18 +08:00
receyuki 659d35b9b8 Bump version to 1.3.3 2024-05-28 02:04:13 +08:00
receyuki ee54281e45 Update CHANGELOG.md 2024-05-28 02:03:38 +08:00
receyuki f94e130bef Update pyproject.toml
Update icon
2024-05-28 01:46:48 +08:00
Rhys Yang 6ab1534e85 Update pyproject.toml 2024-05-28 01:02:12 +08:00
Rhys Yang d960b6dbf1 Create publish_registry.yml 2024-05-28 00:56:44 +08:00
receyuki 0d0afd7f45 Update pyproject.toml
Add icon
2024-05-28 00:43:26 +08:00
Rhys Yang 2069d0c4fc Merge pull request #75 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-05-28 00:33:31 +08:00
haohaocreates 55a57a3b2e chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-22 16:57:15 -04:00
receyuki a2e6aff81c Bump version to 1.3.2 2024-05-22 00:30:23 +08:00
receyuki 2af4a879b5 Update core to 1.3.5 2024-05-22 00:28:49 +08:00
receyuki 12b5178915 Update CHANGELOG.md 2024-05-22 00:28:30 +08:00
receyuki a4f5891079 Update node description 2024-05-15 23:24:56 +08:00
receyuki 9042867e97 Update README.md 2024-05-15 23:16:49 +08:00
receyuki 5527096b53 Update README.md 2024-05-15 23:14:19 +08:00
receyuki 9a84eb186b Update core to 1.3.5b2 2024-05-15 23:12:29 +08:00
Rhys Yang faeea1f73c Merge pull request #66 from COvert96/patch-1
Add filename placeholders %width and %height to the`Prompt Saver` node
2024-04-09 02:19:54 +08:00
Chris Overtveld e6cca92351 Update nodes.py save_images to have %width and %height
There's probably a reason this is not in the main branch, I assume because of certain node conflicts but it works for me!
2024-04-07 14:26:51 +02:00
Rhys Yang 05c3553688 Update core to a7eaf3d 2024-03-22 00:06:30 +08:00
Rhys Yang 23b5afbc4f Update the Prompt Reader node to the new exception handling method 2024-03-22 00:05:10 +08:00
Rhys Yang c2ac789c44 Add support for pasted images to the Prompt Reader node #64 2024-03-07 22:32:40 +08:00
Rhys Yang bc62d2be03 Bump version to 1.3.1 2024-02-28 02:15:52 +08:00
Rhys Yang 74beb22df4 Update README.md
Update CHANGELOG.md
2024-02-28 02:15:26 +08:00
Rhys Yang 0d43174e14 Add JPEG to the image extension list 2024-02-28 02:11:00 +08:00
Rhys Yang 91336e987d Add user-friendly warnings and remove the exception raised during the reading of metadata #58 #63 2024-02-28 01:41:18 +08:00
Rhys Yang cbf771ff03 Fix the issue where the Prompt Saver node cannot consume filename from another Prompt Saver node #62 2024-02-27 03:10:02 +08:00
Rhys Yang 867f5eb6f1 Fix the Prompt Saver node freezes workflow when name includes %model #61 2024-02-27 02:38:47 +08:00
Rhys Yang 88ed9fa966 Remove ckpt validation of the Prompt Reader node #59 2024-02-27 00:58:09 +08:00
receyuki d6f8598946 Merge remote-tracking branch 'origin/dev' into dev 2024-01-22 03:08:59 +08:00
receyuki 63919f44a4 Bump version to 1.3.0 2024-01-22 03:08:20 +08:00
receyuki 2321ce5ae4 Update README.md
Update CHANGELOG.md
Update example workflow
2024-01-22 03:07:45 +08:00
Rhys Yang a8e217d9f1 Fix the error message caused by empty model_name 2024-01-20 21:01:05 +08:00
Rhys Yang e32a514adc Add support for embeddings auto-detection on Civitai 2024-01-17 21:19:09 +08:00
Rhys Yang 27bcc7273e Fix the parsing error caused by the loss of model data #43 2024-01-17 06:28:20 +08:00
Rhys Yang d1e6940491 Add Lora Loader node and Lora Selector node
Add `lora_name` input to the `Prompt Saver` node
Add `resource_hash` input to the `Prompt Saver` node
Add support for auto-detection on Civitai #35
2024-01-17 03:36:02 +08:00
Rhys Yang 3457877e2c Add WEB_DIRECTORY
Remove old js directory
2024-01-16 02:19:33 +08:00
Rhys Yang 4d85411aee Fix the error message caused by aspect_ratio 2024-01-16 02:15:01 +08:00
Rhys Yang 508bc56ca8 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
Update seedGen.js to rgthree's latest code
2024-01-16 02:12:26 +08:00
Rhys Yang bdcc5c7461 Add VAE_NAME output to the Parameter Generator node and add vae_name input to the Prompt Saver node #39 2024-01-06 01:12:04 +08:00
Rhys Yang f86625d71b Add resources hashes to metadata for auto-detection on Civitai 2024-01-06 01:12:02 +08:00
Rhys Yang cf4b85a0a0 Add temporary storage for model hash 2024-01-06 01:12:01 +08:00
Rhys Yang 5241cf50b9 Fix the input validation of the Batch Loader node #37 #38 2024-01-06 01:11:59 +08:00
Rhys Yang 6cd2b3414d Update core to cc3c8b2 2024-01-06 01:09:40 +08:00
Rhys Yang 9194663f09 Update example workflow 2024-01-06 01:08:54 +08:00
receyuki 103a5c826a Fix example workflow 2024-01-05 19:21:10 +08:00
29 changed files with 6886 additions and 791 deletions
+22
View File
@@ -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 }}
+1
View File
@@ -153,3 +153,4 @@ dmypy.json
cython_debug/ cython_debug/
.idea/ .idea/
preview/
+75
View File
@@ -1,4 +1,79 @@
# 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 ## v1.2.1
- Fix the input validation of the `Prompt Reader` node #37 - Fix the input validation of the `Prompt Reader` node #37
- Fix the `Batch Loader` node not working when not connected to any node - Fix the `Batch Loader` node not working when not connected to any node
+202 -79
View File
@@ -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 -17
View File
@@ -6,21 +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 = [
"utils.js",
"promptDisplay.js",
"parameterDisplay.js",
"seedGen.js",
"loaderDisplay.js",
"extractorDisplay.js",
]
for file in files_to_copy:
js_src_path = os.path.join(tk_nodes_path, "js", file)
shutil.copy(js_src_path, js_dest_path)
+1 -1
View File
@@ -1 +1 @@
VERSION = "1.2.1" VERSION = "1.3.4"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

+4 -3
View File
@@ -59,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
}; };
} }
}, },
+113 -44
View File
@@ -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;
}; };
} }
}, },
+358 -56
View File
@@ -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,6 +258,28 @@ 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):
if image in SDPromptReader.files: if image in SDPromptReader.files:
@@ -238,8 +290,20 @@ class SDPromptReader:
image_data = ImageDataReader(f) image_data = ImageDataReader(f)
return image_data.props return image_data.props
@classmethod
def VALIDATE_INPUTS(s, image):
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"
@@ -247,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",),
@@ -259,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",
{ {
@@ -285,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": (
@@ -328,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,
@@ -335,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",
@@ -382,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,
@@ -398,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"
@@ -411,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}"
) )
@@ -465,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):
@@ -503,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 = {
@@ -605,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",
@@ -624,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",
@@ -643,6 +827,7 @@ class SDParameterGenerator:
RETURN_NAMES = ( RETURN_NAMES = (
"MODEL_NAME", "MODEL_NAME",
"VAE_NAME",
"MODEL", "MODEL",
"CLIP", "CLIP",
"VAE", "VAE",
@@ -684,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)
@@ -706,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]
@@ -735,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"
@@ -761,7 +949,10 @@ class SDParameterGenerator:
) )
}, },
"result": ( "result": (
(ckpt_name,) (
ckpt_name,
vae_name_real,
)
+ checkpoint + checkpoint
+ ( + (
seed, seed,
@@ -780,6 +971,10 @@ class SDParameterGenerator:
), ),
} }
@classmethod
def VALIDATE_INPUTS(s, aspect_ratio):
return True
class SDPromptMerger: class SDPromptMerger:
@classmethod @classmethod
@@ -849,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):
@@ -921,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
@@ -1016,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()
@@ -1034,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,
@@ -1041,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 = {
@@ -1051,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",
} }
+15
View File
@@ -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"
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB