Compare commits

..
Author SHA1 Message Date
Dr.Lt.Data b35b22d563 improve: ForEachList - display progress of iteration
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/216
2025-03-23 23:29:58 +09:00
Dr.Lt.Data ca5d942cef fixed: backend caches - invalid handling about reserved key '*'
refactoring: ruff fixes
2025-03-03 00:48:53 +09:00
Dr.Lt.Data a9ece5f22e fixed: jxl PR bug
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/pull/212
2025-03-02 16:18:01 +09:00
Dr.Lt.Data a77979c027 Merge pull request #212 from yggdrasil75/patch-1
Update image_util.py
2025-03-02 16:09:43 +09:00
yggdrasil75 20ed3bb5b4 Update image_util.py
add loading jxl with the batch/list nodes
2025-02-27 09:36:38 -05:00
Dr.Lt.Data ecf8ab6f58 feat: noise_mode - add GPU+internal_seed, and CPU+internal_seed mode.
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/208
2025-02-08 14:18:50 +09:00
Dr.Lt.Data 0a009c1499 feat: samplers - support internal_seed
- Avoiding the issue where the GPU seed causes problems in ancestral mode.

https://github.com/comfyanonymous/ComfyUI/commit/916d1e14a93ef331adef7c0deff2fdcf443b05cf#commitcomment-151914788
2025-01-31 21:04:19 +09:00
Dr.Lt.Data 962c3fbda0 fixed: wildcards - cannot edit populated_text on 'fixed' mode
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/898
2025-01-30 16:56:46 +09:00
Dr.Lt.Data 6158b5aa44 fixed: assign max value to all start_index widget
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/206
2025-01-28 11:52:43 +09:00
Dr.Lt.Data 5aae916c66 improved: Load Prompts From Dir (Inspire), Load Prompts From File (Inspire) - add load_cap, start_index widget
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/207
2025-01-28 11:14:38 +09:00
Dr.Lt.Data f12b4bd54e feat: wildcards - add reproduce mode 2025-01-27 11:51:03 +09:00
Dr.Lt.Data 0284075392 feat: Shared Diffusion Model Loader (Inspire)
feat: Shared Text Encoder Loader (Inspire)

https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/205
2025-01-16 00:47:33 +09:00
Dr.Lt.Data bcbed07560 refactor: change type name NOISE to NOISE_IMAGE. 2024-12-20 14:58:38 +09:00
Dr.Lt.Data 685c5c0d00 Merge pull request #198 from stormwulfren/main
Zipped Prompts Parsing Enhancements
2024-12-19 18:43:45 +09:00
Ren Black 697f12fbdb Merge pull request #1 from stormwulfren/feat/zippedPromptsQoL
Enhance zipped prompt parsing
2024-12-18 13:12:10 +00:00
Ren Black e8817bc348 Enhance zipped prompt parsing
- Added 'count' to emits of LoadPromptsFromDir and LoadPromptsFromFile nodes. Represents the total number of extracted prompts in the batch.
- Modified regex pattern for prompt parsing to include an optional 'name' field. If 'name' is not present, the existing behaviour of using the filename is emitted instead. Additionally, 'positive', 'negative' and 'name' can be presented in arbitrary order.
2024-12-18 13:09:40 +00:00
Dr.Lt.Data 9c6065af6d feat: Is Cached (Inspire) node is added.
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/190#issuecomment-2507871877
2024-12-01 01:26:36 +09:00
Dr.Lt.Data 8db9411e94 FIXED: front compatibility fix
- selectors of wildcards, lbw, promptbuilder
2024-11-30 23:16:30 +09:00
Dr.Lt.Data 18f02d98b1 Merge pull request #189 from akuma/fix/warning-message-condition
Fix warning message condition and add info log
2024-11-25 23:24:27 +09:00
akuma cab499e30a Fix warning message condition and add info log
- Corrected the logic for displaying the "file not found" warning message.
- Added an info log message to indicate when the file is successfully found and loaded.
2024-11-25 18:38:38 +08:00
Dr.Lt.Data 71a68650fe feat: ForeachListBegin/End, WorklistToItemList 2024-11-22 18:21:02 +09:00
Dr.Lt.Data 3bcc1d43a0 fixed: LBW - Certain text encoders, such as T5, were not being reflected in CLIP.
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/185
2024-11-15 12:52:27 +09:00
Dr.Lt.Data 395869085c Merge pull request #187 from RUiNtheExtinct/fix/api-error
patch to fix error when workflow is called via API
2024-11-15 10:16:04 +09:00
Arghyadeep Karmakar 384284fe0b patch to fix error when workflow is called via API 2024-11-14 17:42:56 +05:30
Dr.Lt.Data c6d53ecd4d Merge pull request #181 from aurel-g/main
Fix type of "model" input of SeedExplorer node
2024-11-05 18:07:48 +09:00
aurel-g 44cfac4f0a Fix type of "model" input of SeedExplorer node
The type of this input was "model" in lowercase, which is unusual and does not work well with ComfyUI node search.
2024-11-04 21:23:31 +01:00
Dr.Lt.Data bfaaa6c570 improved: extra_model_paths.yaml support for prompt files
- You can use `inspire_prompts`

https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/177
2024-10-22 23:20:29 +09:00
Dr.Lt.Data d29c41809f FIXED: ConcatConditioningsWithMultiplier - version compatibility patch
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/160#issuecomment-2416008408
2024-10-16 21:33:53 +09:00
Dr.Lt.Data 667dead8c1 improved: IPAdapterModelHelper - support Kolors model 2024-10-16 00:24:58 +09:00
Dr.Lt.Data 08cdc7358b add opencv-python to requirements.txt 2024-10-13 17:25:11 +09:00
Dr.Lt.Data 81485bcf4c version marker 2024-10-13 17:22:06 +09:00
Dr.Lt.Data f6a8c65094 Merge pull request #173 from geroldmeisinger/main
support named colors via webcolors module
2024-10-13 17:21:10 +09:00
Gerold Meisinger 37bc51713f support named colors via webcolors module 2024-10-12 14:32:44 +02:00
Dr.Lt.Data 004a9534e4 Merge branch 'fix/backward_compatibility' 2024-10-02 01:58:16 +09:00
Dr.Lt.Data ca710caaab Merge pull request #170 from fAIseh00d/main
Add xinsir black pixel support to inpainting controlnet preprocessor
2024-10-02 01:58:07 +09:00
Dr.Lt.Data c657cf152a backward compatiblity patch 2024-10-02 01:56:15 +09:00
Ivan R 9678d685c2 Add xinsir black pixel support 2024-09-30 22:00:24 +05:00
Dr.Lt.Data 51cace6f1c better error message
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/163#issuecomment-2367951331
2024-09-24 01:00:59 +09:00
Dr.Lt.Data c0d54d8a88 fix: KSampler Progress - 1st latent shape mismatch if flux/sd3
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/161
2024-09-16 01:37:12 +09:00
Dr.Lt.Data 74b2856499 feat: LoadPromptFromFile/Dir - reload
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/156
2024-09-08 12:03:51 +09:00
Dr.Lt.Data 25ae8d522f feat: MakeLBW, ApplyLBW, SaveLBW, LoadLBW 2024-09-01 22:46:38 +09:00
Dr.Lt.Data 50d42c30b5 Upgrade LoRA Block Weight
- support % syntax
2024-08-29 01:35:27 +09:00
Dr.Lt.Data 89e24b7529 fixed: LoadImagesFromDirBatch - invalid mask processing
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/151
2024-08-28 20:33:00 +09:00
Dr.Lt.Data 6db6ca54c0 Merge pull request #150 from jesperol/fix_noise_return
fix: return_with_leftover_noise propagation
2024-08-28 10:45:11 +09:00
Jesper Olsson 69b7c486a9 fix: return_with_leftover_noise propagation 2024-08-21 17:22:31 +02:00
Dr.Lt.Data b09b295009 FIXED: mix_noise - device mismatch error 2024-08-21 01:45:10 +09:00
Dr.Lt.Data a15d3362d8 Merge pull request #143 from pixelprotest/feature/load-last-image-batch-from-dir
Add ability to use Load Image Batch from Dir node, to load the last / latest image in a directory
2024-08-20 22:56:12 +09:00
pixelprotest 019713040c Allow min start_index to drop to -1 to make node get last/latest image from dir 2024-08-20 13:30:59 +01:00
Dr.Lt.Data bc075b1a4f version marker 2024-08-18 19:32:37 +09:00
Dr.Lt.Data 452d8cfc00 Merge pull request #142 from anhkhoatranle30/main
Add filepaths as outputs
2024-08-18 19:30:24 +09:00
khoatrn 17716ddb52 feat: Add filepaths as outputs 2024-08-18 16:14:57 +07:00
Dr.Lt.Data 56f4f01f02 fix: LBW - no effect if weight is less than 0 2024-08-17 11:47:18 +09:00
Dr.Lt.Data f64c98714b improve: LBW - efficient patch
fix: LBW - preset patch
2024-08-17 11:28:03 +09:00
Dr.Lt.Data 30974e69c4 feat: LBW for FLUX 2024-08-17 00:36:48 +09:00
Dr.Lt.Data ec29936d16 fix: IPAdapterModelHelper - compatibility patch
- `insightface_model_name` is added.
2024-08-15 21:16:52 +09:00
Dr.Lt.Data 0bd92419de improve: FloatRange - support decrement 2024-08-14 12:43:03 +09:00
Dr.Lt.Data ec6831fd68 version marker 2024-08-07 23:13:40 +09:00
Dr.Lt.Data c876e477e8 Merge pull request #136 from bvhari/main
Add cosine interpolation for Scheduled CFG
2024-08-07 23:13:01 +09:00
BVH 0de0e4232c Change newline to fix diff 2024-08-07 03:14:06 +05:30
BVH 61ba155cf1 Add cosine interpolation for Scheduled CFG 2024-08-04 23:59:46 +05:30
Dr.Lt.Data 09ae2eaa9b KSamplerProgress fix and enhance
fix: ommit_start_latent - doesn't work if it is false
improve: add ommit_final_latent
modified: move progress latent to cpu
2024-08-04 11:22:35 +09:00
Dr.Lt.Data eab0b95df5 Merge pull request #134 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-08-03 15:10:48 +09:00
snomiao badc1c0cb4 chore(licence-update): Update PyProject Toml - License 2024-08-02 23:03:51 +00:00
Dr.Lt.Data cf9bae0718 fix: LoadPrompts - invalid opiton handling of text_data_opt
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/124
2024-07-30 16:25:43 +09:00
Dr.Lt.Data c90d6c8d0f fix: WildcardEncodeInspire - external seed isn't passed to 2nd population. 2nd populated text isn't reflected to populated_text 2024-07-28 00:12:23 +09:00
Dr.Lt.Data 7aa30a9633 fix: batch mask dilation issue
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/130#issuecomment-2247047978
2024-07-24 20:49:55 +09:00
Dr.Lt.Data 69af3ec749 improve: RegionalConditioningColorMask - add dilation
fix: invalid dilation logic
2024-07-24 02:20:34 +09:00
Dr.Lt.Data 2ba2fec053 fix: invalid dynamic widget control
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/129
2024-07-24 01:50:19 +09:00
Dr.Lt.Data ad0c179ec7 fix: prevent crash when execute via API
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/125
2024-07-22 05:35:05 +09:00
Dr.Lt.Data cadf604de5 apply updated IPAdapterPlus's weight type to RegionalIPAdapter nodes 2024-07-11 23:34:52 +09:00
Dr.Lt.Data ccd472ea9a feat: Load (Single) Prompts from File - add text_data_opt
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/120
2024-07-09 20:07:37 +09:00
Dr.Lt.Data d84435ef62 fix: potential ui bug 2024-07-03 00:02:54 +09:00
Dr.Lt.Data f1fb2a046d remove debug message 2024-06-22 21:30:32 +09:00
Dr.Lt.Data d150ca3adf hotfix: Scheduled CFGGuider - invalid fallback handling 2024-06-22 21:25:40 +09:00
Dr.Lt.Data 78444b5010 hotfix: Scheduled CFGGuider crash 2024-06-22 21:15:29 +09:00
Dr.Lt.Data 4b6ba88916 modified: Scheduled (PerpNeg) CFGGuider - change default value settings. 2024-06-22 20:36:55 +09:00
Dr.Lt.Data 8f8cac507d feat: add ScheduledPerpNegCFGGuider 2024-06-22 20:15:59 +09:00
Dr.Lt.Data 68fb465fe8 feat: add ScheduledCFGGuider 2024-06-22 15:07:11 +09:00
Dr.Lt.Data 55db53ac6b feat: support GITS scheduler. 2024-06-22 10:54:07 +09:00
Dr.Lt.Data 2b947baada hotfix: make sure numpy<2 2024-06-21 07:12:42 +09:00
Dr.Lt.Data bf54f25f07 feat: CompositeNoise 2024-06-19 23:16:35 +09:00
Dr.Lt.Data 5320c1c7d2 feat: RGB Hex To HSV (Inspire)
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/646
2024-06-19 03:01:10 +09:00
Dr.Lt.Data 34e64e7a6a fix: seed explorer - sd3 compatibility 2024-06-13 01:22:26 +09:00
Dr.Lt.Data 9b2d22a9a1 update README.md 2024-06-08 16:33:14 +09:00
Dr.Lt.Data 877103b7d6 feat: ColorMaskToDepthMask - support dilation, flatten_method
Apply ComfyUI patch.

https://github.com/comfyanonymous/ComfyUI/commit/6cd8ffc465ed363b078249b081ea3f975e77cf15
2024-06-08 16:24:47 +09:00
Dr.Lt.Data b940df0d5c fix: LoraBlockInfo 2024-06-06 16:03:19 +09:00
Dr.Lt.Data cc2ae35891 update README.md 2024-06-04 00:32:33 +09:00
Dr.Lt.Data bc558bf42b feat: ColorMaskToDepthMask 2024-06-03 21:54:51 +09:00
Dr.Lt.Data 5d84ac9e88 feat: Regional CFG 2024-06-03 03:17:07 +09:00
Dr.Lt.Data 2b7a79090c feat: RegionalPrompt - support variation seed
improve: SeedExplorer - support variation method
- and fix linear method scaling
refactor: convert several doit method to staticmethod
2024-06-02 17:30:28 +09:00
Dr.Lt.Data e255ff46fb fix: ImageListToImageBatch - crash when input is single image. 2024-05-28 00:27:55 +09:00
Dr.Lt.Data 1b88905a62 feat: variation_method for noise variation
- slerp is added
2024-05-26 15:49:55 +09:00
Dr.Lt.Data 0916454bce update pyproject.toml 2024-05-23 00:48:17 +09:00
Dr.Lt.Data ea7a88be49 Merge pull request #109 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-05-23 00:37:36 +09:00
Dr.Lt.Data ee446d1f90 Merge pull request #108 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-05-23 00:37:26 +09:00
haohaocreates 6be70a0618 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-22 11:29:27 -04:00
haohaocreates 51a7845ec5 chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-22 11:29:24 -04:00
Dr.Lt.Data 186b601e65 feat: ConditioningUpscale, ConditioningStretch 2024-05-22 23:45:19 +09:00
Dr.Lt.Data 34de5e338f fix: result of MediaPipeFaceMeshDetector doesn't match to original resolution 2024-05-19 01:25:29 +09:00
Dr.Lt.Data 0dec9404fa compatibility patch for updated Impact Pack. 2024-05-15 01:26:51 +09:00
Dr.Lt.Data 9c578996b0 fix: KSampler Progress - interval doesn't work 2024-05-10 00:26:54 +09:00
Dr.Lt.Data a967cc96ab compatibility patch for updated Impact Pack. 2024-05-09 03:18:54 +09:00
Dr.Lt.Data 9c5fa36a51 fix: KSampler_progress - use process_latent_out instead of hard-coded value. 2024-05-04 00:47:45 +09:00
28 changed files with 3011 additions and 655 deletions
+21
View File
@@ -0,0 +1,21 @@
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
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+92 -42
View File
@@ -2,6 +2,8 @@
This repository offers various extension nodes for ComfyUI. Nodes here have different characteristics compared to those in the ComfyUI Impact Pack. The Impact Pack has become too large now...
## Notice:
* v1.9.1 To avoid confusion with the `NOISE` type in core, the type name has been changed to `NOISE_IMAGE`.
* V0.73 The Variation Seed feature is added to Regional Prompt nodes, and it is only compatible with versions Impact Pack V5.10 and above.
* V0.69 incompatible with the outdated **ComfyUI IPAdapter Plus**. (A version dated March 24th or later is required.)
* V0.64 add sigma_factor to RegionalPrompt... nodes required Impact Pack V4.76 or later.
* V0.62 support faceid in Regional IPAdapter
@@ -10,46 +12,68 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* WARN: If you use version **0.12 to 0.12.2** without a GlobalSeed node, your workflow's seed may have been erased. Please update immediately.
## Nodes
* Lora Block Weight - This is a node that provides functionality related to Lora block weight.
* This provides similar functionality to [sd-webui-lora-block-weight](https://github.com/hako-mikan/sd-webui-lora-block-weight)
* `Lora Loader (Block Weight)`: When loading Lora, the block weight vector is applied.
* In the block vector, you can use numbers, R, A, a, B, and b.
* R is determined sequentially based on a random seed, while A and B represent the values of the A and B parameters, respectively. a and b are half of the values of A and B, respectively.
* `XY Input: Lora Block Weight`: This is a node in the [Efficiency Nodes](https://github.com/LucianoCirino/efficiency-nodes-comfyui)' XY Plot that allows you to use Lora block weight.
* You must ensure that X and Y connections are made, and dependencies should be connected to the XY Plot.
* Note: To use this feature, update `Efficient Nodes` to a version released after September 3rd.
### Lora Block Weight - This is a node that provides functionality related to Lora block weight.
* This provides similar functionality to [sd-webui-lora-block-weight](https://github.com/hako-mikan/sd-webui-lora-block-weight)
* `LoRA Loader (Block Weight)`: When loading Lora, the block weight vector is applied.
* In the block vector, you can use numbers, R, A, a, B, and b.
* R is determined sequentially based on a random seed, while A and B represent the values of the A and B parameters, respectively. a and b are half of the values of A and B, respectively.
* `XY Input: LoRA Block Weight`: This is a node in the [Efficiency Nodes](https://github.com/LucianoCirino/efficiency-nodes-comfyui)' XY Plot that allows you to use Lora block weight.
* You must ensure that X and Y connections are made, and dependencies should be connected to the XY Plot.
* Note: To use this feature, update `Efficient Nodes` to a version released after September 3rd.
* Make LoRA Block Weight: Instead of directly applying the LoRA Block Weight to the MODEL, it is generated in a separate LBW_MODEL form
* Apply LoRA Block Weight: Apply LBW_MODEL to MODEL and CLIP
* Save LoRA Block Weight: Save LBW_MODEL as a .lbw.safetensors file
* Load LoRA Block Weight: Load LBW_MODEL from .lbw.safetensors file
* SEGS Supports nodes - This is a node that supports ApplyControlNet (SEGS) from the Impact Pack.
* `OpenPose Preprocessor Provider (SEGS)`: OpenPose preprocessor is applied for the purpose of using OpenPose ControlNet in SEGS.
* You need to install [ControlNet Auxiliary Preprocessors](https://github.com/Fannovel16/comfyui_controlnet_aux) to use this.
* `Canny Preprocessor Provider (SEGS)`: Canny preprocessor is applied for the purpose of using Canny ControlNet in SEGS.
* `DW Preprocessor Provider (SEGS)`, `MiDaS Depth Map Preprocessor Provider (SEGS)`, `LeReS Depth Map Preprocessor Provider (SEGS)`,
`MediaPipe FaceMesh Preprocessor Provider (SEGS)`, `HED Preprocessor Provider (SEGS)`, `Fake Scribble Preprocessor (SEGS)`,
`AnimeLineArt Preprocessor Provider (SEGS)`, `Manga2Anime LineArt Preprocessor Provider (SEGS)`, `LineArt Preprocessor Provider (SEGS)`,
`Color Preprocessor Provider (SEGS)`, `Inpaint Preprocessor Provider (SEGS)`, `Tile Preprocessor Provider (SEGS)`, `MeshGraphormer Depth Map Preprocessor Provider (SEGS)`
* `MediaPipeFaceMeshDetectorProvider`: This node provides `BBOX_DETECTOR` and `SEGM_DETECTOR` that can be used in Impact Pack's Detector using the `MediaPipe-FaceMesh Preprocessor` of ControlNet Auxiliary Preprocessors.
* A1111 Compatibility support - These nodes assists in replicating the creation of A1111 in ComfyUI exactly.
* `KSampler (Inspire)`: ComfyUI uses the CPU for generating random noise, while A1111 uses the GPU. One of the three factors that significantly impact reproducing A1111's results in ComfyUI can be addressed using `KSampler (Inspire)`.
* Other point #1 : Please make sure you haven't forgotten to include 'embedding:' in the embedding used in the prompt, like 'embedding:easynegative.'
* Other point #2 : ComfyUI and A1111 have different interpretations of weighting. To align them, you need to use [BlenderNeko/Advanced CLIP Text Encode](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb).
* `KSamplerAdvanced (Inspire)`: Inspire Pack version of `KSampler (Advanced)`.
* Common Parameters
* `batch_seed_mode` determines how seeds are applied to batch latents:
* `comfy`: This method applies the noise to batch latents all at once. This is advantageous to prevent duplicate images from being generated due to seed duplication when creating images.
* `incremental`: Similar to the A1111 case, this method incrementally increases the seed and applies noise sequentially for each batch. This approach is beneficial for straightforward reproduction using only the seed.
* `variation_strength`: In each batch, the variation strength starts from the set `variation_strength` and increases by `xxx`.
* `variation_seed` and `variation_strength` - Initial noise generated by the seed is transformed to the shape of `variation_seed` by `variation_strength`. If `variation_strength` is 0, it only relies on the influence of the seed, and if `variation_strength` is 1.0, it is solely influenced by `variation_seed`.
* These parameters are used when you want to maintain the composition of an image generated by the seed but wish to introduce slight changes.
### SEGS Supports nodes - This is a node that supports ApplyControlNet (SEGS) from the Impact Pack.
* `OpenPose Preprocessor Provider (SEGS)`: OpenPose preprocessor is applied for the purpose of using OpenPose ControlNet in SEGS.
* You need to install [ControlNet Auxiliary Preprocessors](https://github.com/Fannovel16/comfyui_controlnet_aux) to use this.
* `Canny Preprocessor Provider (SEGS)`: Canny preprocessor is applied for the purpose of using Canny ControlNet in SEGS.
* `DW Preprocessor Provider (SEGS)`, `MiDaS Depth Map Preprocessor Provider (SEGS)`, `LeReS Depth Map Preprocessor Provider (SEGS)`,
`MediaPipe FaceMesh Preprocessor Provider (SEGS)`, `HED Preprocessor Provider (SEGS)`, `Fake Scribble Preprocessor (SEGS)`,
`AnimeLineArt Preprocessor Provider (SEGS)`, `Manga2Anime LineArt Preprocessor Provider (SEGS)`, `LineArt Preprocessor Provider (SEGS)`,
`Color Preprocessor Provider (SEGS)`, `Inpaint Preprocessor Provider (SEGS)`, `Tile Preprocessor Provider (SEGS)`, `MeshGraphormer Depth Map Preprocessor Provider (SEGS)`
* `MediaPipeFaceMeshDetectorProvider`: This node provides `BBOX_DETECTOR` and `SEGM_DETECTOR` that can be used in Impact Pack's Detector using the `MediaPipe-FaceMesh Preprocessor` of ControlNet Auxiliary Preprocessors.
* Prompt Support - These are nodes for supporting prompt processing.
### A1111 Compatibility support - These nodes assists in replicating the creation of A1111 in ComfyUI exactly.
* `KSampler (Inspire)`: ComfyUI uses the CPU for generating random noise, while A1111 uses the GPU. One of the three factors that significantly impact reproducing A1111's results in ComfyUI can be addressed using `KSampler (Inspire)`.
* Other point #1 : Please make sure you haven't forgotten to include 'embedding:' in the embedding used in the prompt, like 'embedding:easynegative.'
* Other point #2 : ComfyUI and A1111 have different interpretations of weighting. To align them, you need to use [BlenderNeko/Advanced CLIP Text Encode](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb).
* `KSamplerAdvanced (Inspire)`: Inspire Pack version of `KSampler (Advanced)`.
* `RandomNoise (inspire)`: Inspire Pack version of `RandomNoise`.
* Common Parameters
* `batch_seed_mode` determines how seeds are applied to batch latents:
* `comfy`: This method applies the noise to batch latents all at once. This is advantageous to prevent duplicate images from being generated due to seed duplication when creating images.
* `incremental`: Similar to the A1111 case, this method incrementally increases the seed and applies noise sequentially for each batch. This approach is beneficial for straightforward reproduction using only the seed.
* `variation_strength`: In each batch, the variation strength starts from the set `variation_strength` and increases by `xxx`.
* `variation_seed` and `variation_strength` - Initial noise generated by the seed is transformed to the shape of `variation_seed` by `variation_strength`. If `variation_strength` is 0, it only relies on the influence of the seed, and if `variation_strength` is 1.0, it is solely influenced by `variation_seed`.
* These parameters are used when you want to maintain the composition of an image generated by the seed but wish to introduce slight changes.
### Sampler nodes
* `KSampler Progress (Inspire)` - In KSampler, the sampling process generates latent batches. By using `Video Combine` node from [ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite), you can create a video from the progress.
* `Scheduled CFGGuider (Inspire)` - This is a CFGGuider that adjusts the schedule from from_cfg to to_cfg using linear, log, and exp methods.
* `Scheduled PerpNeg CFGGuider (Inspire)` - This is a PerpNeg CFGGuider that adjusts the schedule from from_cfg to to_cfg using linear, log, and exp methods.
### Prompt Support - These are nodes for supporting prompt processing.
* `Load Prompts From Dir (Inspire)`: It sequentially reads prompts files from the specified directory. The output it returns is ZIPPED_PROMPT.
* Specify the directories located under `ComfyUI-Inspire-Pack/prompts/`
* One prompts file can have multiple prompts separated by `---`.
* e.g. `prompts/example`
* `Load Prompts From File (Inspire)`: It sequentially reads prompts from the specified file. The output it returns is ZIPPED_PROMPT.
* **NOTE**: This node provides advanced option via `Show advanced`
* load_cap, start_index
* `Load Prompts From File (Inspire)`: It sequentially reads prompts from the specified file. The output it returns is ZIPPED_PROMPT.
* Specify the file located under `ComfyUI-Inspire-Pack/prompts/`
* e.g. `prompts/example/prompt2.txt`
* e.g. `prompts/example/prompt2.txt`
* **NOTE**: This node provides advanced option via `Show advanced`
* load_cap, start_index
* `Load Single Prompt From File (Inspire)`: Loads a single prompt from a file containing multiple prompts by using an index.
* The prompts file directory can be specified as `inspire_prompts` in `extra_model_paths.yaml`
* `Unzip Prompt (Inspire)`: Separate ZIPPED_PROMPT into `positive`, `negative`, and name components.
* `positive` and `negative` represent text prompts, while `name` represents the name of the prompt. When loaded from a file using `Load Prompts From File (Inspire)`, the name corresponds to the file name.
* `Zip Prompt (Inspire)`: Create ZIPPED_PROMPT from positive, negative, and name_opt.
@@ -71,12 +95,13 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* In the `seed_prompt`, the first seed is considered the initial seed, and the reflection rate is omitted, always defaulting to 1.0.
* Each prompt is separated by a comma, and from the second seed onwards, it should follow the format `seed:strength`.
* Pressing the "Add to prompt" button will append `additional_seed:additional_strength` to the prompt.
* `Composite Noise (Inspire)`: This node overwrites a specific area on top of the destination noise with the source noise.
* `Random Generator for List (Inspire)`: When connecting the list output to the signal input, this node generates random values for all items in the list.
* `Make Basic Pipe (Inspire)`: This is a node that creates a BASIC_PIPE using Wildcard Encode. The `Add select to` determines whether the selected item from the `Select to...` combo will be input as positive wildcard text or negative wildcard text.
* `Remove ControlNet (Inspire)`, `Remove ControlNet [RegionalPrompts] (Inspire)`: Remove ControlNet from CONDITIONING or REGIONAL_PROMPTS.
* `Remove ControlNet [RegionalPrompts] (Inspire)` requires Impact Pack V4.73.1 or above.
* Regional Nodes - These node simplifies the application of prompts by region.
### Regional Nodes - These node simplifies the application of prompts by region.
* Regional Sampler - These nodes assists in the easy utilization of the regional sampler in the `Impact Pack`.
* `Regional Prompt Simple (Inspire)`: This node takes `mask` and `basic_pipe` as inputs and simplifies the creation of `REGIONAL_PROMPTS`.
* `Regional Prompt By Color Mask (Inspire)`: Similar to `Regional Prompt Simple (Inspire)`, this function accepts a color mask image as input and defines the region using the color value that will be used as the mask, instead of directly receiving the mask.
@@ -91,8 +116,18 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* Regional Seed Explorer - These nodes restrict the variation through a seed prompt, applying it only to the masked areas.
* `Regional Seed Explorer By Mask (Inspire)`
* `Regional Seed Explorer By Color Mask (Inspire)`
* `Regional CFG (Inspire)` - By applying a mask as a multiplier to the configured cfg, it allows different areas to have different cfg settings.
* `Color Mask To Depth Mask (Inspire)` - Convert the color map from the spec text into a mask with depth values ranging from 0.0 to 1.0.
* The range of the mask value is limited to 0.0 to 1.0.
* base_value: Sets the value of the base mask.
* dilation: Dilation applied to each mask layer before flattening.
* flatten_method: The method of flattening the mask layers.
* The layers are flattened including the base layer set by base_value.
* override: Each pixel is overwritten by the non-zero value of the upper layer.
* sum: Each pixel is flattened by summing the values of all layers.
* max: Each pixel is flattened by taking the maximum value from all layers.
* Image Util
### Image Util
* `Load Image Batch From Dir (Inspire)`: This is almost same as `LoadImagesFromDirectory` of [ComfyUI-Advanced-Controlnet](https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet). This is just a modified version. Just note that this node forcibly normalizes the size of the loaded image to match the size of the first image, even if they are not the same size, to create a batch image.
* `Load Image List From Dir (Inspire)`: This is almost same as `Load Image Batch From Dir (Inspire)`. However, note that this node loads data in a list format, not as a batch, so it returns images at their original size without normalizing the size.
* `Load Image (Inspire)`: This node is similar to LoadImage, but the loaded image information is stored in the workflow. The image itself is stored in the workflow, making it easier to reproduce image generation on other computers.
@@ -102,10 +137,8 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* `ImageBatchSplitter //Inspire`, `LatentBatchSplitter //Inspire`: The script divides a batch of images/latents into individual images/latents, each with a quantity equal to the specified `split_count`. An additional output slot is added for each `split_count`. If the number of images/latents exceeds the `split_count`, the remaining ones are returned as the "remained" output.
* `Color Map To Masks (Inspire)`: From the color_map, it extracts the top max_count number of colors and creates masks. min_pixels represents the minimum number of pixels for each color.
* `Select Nth Mask (Inspire)`: Extracts the nth mask from the mask batch.
* KSampler Progress - In KSampler, the sampling process generates latent batches. By using `Video Combine` node from [ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite), you can create a video from the progress.
* Backend Cache - Nodes for storing arbitrary data from the backend in a cache and sharing it across multiple workflows.
### Backend Cache - Nodes for storing arbitrary data from the backend in a cache and sharing it across multiple workflows.
* `Cache Backend Data (Inspire)`: Stores any backend data in the cache using a string key. Tags are for quick reference.
* `Retrieve Backend Data (Inspire)`: Retrieves cached backend data using a string key.
* `Remove Backend Data (Inspire)`: Removes cached backend data.
@@ -120,20 +153,31 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* `Shared Checkpoint Loader (Inspire)`: When loading a checkpoint through this loader, it is automatically cached in the backend cache. Additionally, if it is already cached, it retrieves it from the cache instead of loading it anew.
* When `key_opt` is empty, the `ckpt_name` is set as the cache key. The cache key output can be used for deletion purposes with Remove Back End.
* This node resolves the issue of reloading checkpoints during workflow switching.
* `Shared Diffusion Model Loader (Inspire)`: Similar to the `Shared Checkpoint Loader (Inspire)` but used for loading Diffusion models instead of Checkpoints.
* `Shared Text Encoder Loader (Inspire)`: Similar to the `Shared Checkpoint Loader (Inspire)` but used for loading Text Encoder models instead of Checkpoints.
* This node also functions as a unified node for `CLIPLoader`, `DualCLIPLoader`, and `TripleCLIPLoader`.
* `Stable Cascade Checkpoint Loader (Inspire)`: This node provides a feature that allows you to load the `stage_b` and `stage_c` checkpoints of Stable Cascade at once, and it also provides a backend caching feature, optionally.
* `Is Cached (Inspire)`: Returns whether the cache exists.
* Conditioning - Nodes for conditionings
### Conditioning - Nodes for conditionings
* `Concat Conditionings with Multiplier (Inspire)`: Concatenating an arbitrary number of Conditionings while applying a multiplier for each Conditioning. The multiplier depends on `comfy_PoP`, so [comfy_PoP](https://github.com/picturesonpictures/comfy_PoP) must be installed.
* `Conditioning Upscale (Inspire)`: When upscaling an image, it helps to expand the conditioning area according to the upscale factor. Taken from [ComfyUI_Dave_CustomNode](https://github.com/Davemane42/ComfyUI_Dave_CustomNode)
* `Conditioning Stretch (Inspire)`: When upscaling an image, it helps to expand the conditioning area by specifying the original resolution and the new resolution to be applied. Taken from [ComfyUI_Dave_CustomNode](https://github.com/Davemane42/ComfyUI_Dave_CustomNode)
* Models - Nodes for models
### Models - Nodes for models
* `IPAdapter Model Helper (Inspire)`: This provides presets that allow for easy loading of the IPAdapter related models. However, it is essential for the model's name to be accurate.
* You can download the appropriate model through ComfyUI-Manager.
* Util - Utilities
### List - Nodes for List processing
* `Float Range (Inspire)`: Create a float list that increases the value by `step` from `start` to `stop`. A list as large as the maximum limit is created, and when `ensure_end` is enabled, the last value of the list becomes the stop value.
* `Worklist To Item List (Inspire)`: The list in ComfyUI allows for repeated execution of a sub-workflow. This groups these repetitions (a.k.a. list) into a single ITEM_LIST output. ITEM_LIST can then be used in ForeachList.
* `▶Foreach List (Inspire)`: A starting node for performing iterative tasks by retrieving items one by one from the ITEM_LIST.\nGenerate a new intermediate_output using item and intermediate_output as inputs, then connect it to ForeachListEnd.\nNOTE:If initial_input is omitted, the first item in item_list is used as the initial value, and the processing starts from the second item in item_list.
* `Foreach List◀ (Inspire)`: A end node for performing iterative tasks by retrieving items one by one from the ITEM_LIST.\nNOTE:Directly connect the outputs of ForeachListBegin to 'flow_control' and 'remained_list'.
### Util - Utilities
* `ToIPAdapterPipe (Inspire)`, `FromIPAdapterPipe (Inspire)`: These nodes assists in conveniently using the bundled ipadapter_model, clip_vision, and model required for applying IPAdapter.
* `List Counter (Inspire)`: When each item in the list traverses through this node, it increments a counter by one, generating an integer value.
* `RGB Hex To HSV (Inspire)`: Convert an RGB hex string like `#FFD500` to HSV:
## Credits
@@ -149,4 +193,10 @@ Kosinkadink/[ComfyUI-Advanced-Controlnet](https://github.com/Kosinkadink/ComfyUI
Trung0246/[ComfyUI-0246](https://github.com/Trung0246/ComfyUI-0246) - Nice bypass hack!
cubiq/[ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) - IPAdapter related nodes depend on this extension.
cubiq/[ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) - IPAdapter related nodes depend on this extension.
Davemane42/[ComfyUI_Dave_CustomNode](https://github.com/Davemane42/ComfyUI_Dave_CustomNode) - Original author of ConditioningStretch, ConditioningUpscale
BlenderNeko/[ComfyUI_Noise](https://github.com/BlenderNeko/ComfyUI_Noise) - slerp code for noise variation
BadCafeCode/[execution-inversion-demo-comfyui](https://github.com/BadCafeCode/execution-inversion-demo-comfyui) - reference loop implementation for ComfyUI
+6 -4
View File
@@ -2,14 +2,15 @@
@author: Dr.Lt.Data
@title: Inspire Pack
@nickname: Inspire Pack
@description: This extension provides various nodes to support Lora Block Weight and the Impact Pack.
@description: This extension provides various nodes to support Lora Block Weight, Regional Nodes, Backend Cache, Prompt Utils, List Utils and the Impact Pack.
"""
import importlib
import logging
version_code = [0, 69, 2]
version_code = [1, 15]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
print(f"### Loading: ComfyUI-Inspire-Pack ({version_str})")
logging.info(f"### Loading: ComfyUI-Inspire-Pack ({version_str})")
node_list = [
"lora_block_weight",
@@ -23,7 +24,8 @@ node_list = [
"backend_support",
"list_nodes",
"conditioning_nodes",
"model_nodes"
"model_nodes",
"util_nodes"
]
NODE_CLASS_MAPPINGS = {}
+167 -56
View File
@@ -5,14 +5,77 @@ from einops import rearrange
import random
import math
from .libs import common
import logging
supported_noise_modes = ["GPU(=A1111)", "CPU", "GPU+internal_seed", "CPU+internal_seed"]
class Inspire_RandomNoise:
def __init__(self, seed, mode, incremental_seed_mode, variation_seed, variation_strength, variation_method="linear", internal_seed=None):
device = comfy.model_management.get_torch_device()
# HOTFIX: https://github.com/comfyanonymous/ComfyUI/commit/916d1e14a93ef331adef7c0deff2fdcf443b05cf#commitcomment-151914788
# seed value should be different with generated noise
self.seed = internal_seed
self.noise_seed = seed
self.noise_device = "cpu" if mode == "CPU" else device
self.incremental_seed_mode = incremental_seed_mode
self.variation_seed = variation_seed
self.variation_strength = variation_strength
self.variation_method = variation_method
def generate_noise(self, input_latent):
latent_image = input_latent["samples"]
batch_inds = input_latent["batch_index"] if "batch_index" in input_latent else None
noise = utils.prepare_noise(latent_image, self.noise_seed, batch_inds, self.noise_device, self.incremental_seed_mode,
variation_seed=self.variation_seed, variation_strength=self.variation_strength, variation_method=self.variation_method)
return noise.cpu()
class RandomNoise:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"optional":
{
"variation_method": (["linear", "slerp"],),
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
}
}
RETURN_TYPES = ("NOISE",)
FUNCTION = "get_noise"
CATEGORY = "InspirePack/a1111_compat"
def get_noise(self, noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method="linear", internal_seed=None):
if internal_seed is None:
internal_seed = noise_seed
return (Inspire_RandomNoise(noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method=variation_method, internal_seed=internal_seed),)
def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0,
noise_mode="CPU", disable_noise=False, start_step=None, last_step=None, force_full_denoise=False,
incremental_seed_mode="comfy", variation_seed=None, variation_strength=None, noise=None, callback=None):
incremental_seed_mode="comfy", variation_seed=None, variation_strength=None, noise=None, callback=None, variation_method="linear",
scheduler_func=None, internal_seed=None):
device = comfy.model_management.get_torch_device()
noise_device = "cpu" if noise_mode == "CPU" else device
noise_device = "cpu" if 'cpu' in noise_mode.lower() else device
latent_image = latent["samples"]
if hasattr(comfy.sample, 'fix_empty_latent_channels'):
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
latent = latent.copy()
if noise is not None and latent_image.shape[1] != noise.shape[1]:
logging.info("[Inspire Pack] inspire_ksampler: The type of latent input for noise generation does not match the model's latent type. When using the SD3 model, you must use the SD3 Empty Latent.")
raise Exception("The type of latent input for noise generation does not match the model's latent type. When using the SD3 model, you must use the SD3 Empty Latent.")
if noise is None:
if disable_noise:
@@ -20,7 +83,8 @@ def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device=noise_device)
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = utils.prepare_noise(latent_image, seed, batch_inds, noise_device, incremental_seed_mode, variation_seed=variation_seed, variation_strength=variation_strength)
noise = utils.prepare_noise(latent_image, seed, batch_inds, noise_device, incremental_seed_mode,
variation_seed=variation_seed, variation_strength=variation_strength, variation_method=variation_method)
if start_step is None:
if denoise == 1.0:
@@ -30,9 +94,27 @@ def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
start_step = advanced_steps - steps
steps = advanced_steps
samples = common.impact_sampling(
model=model, add_noise=not disable_noise, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative,
latent_image=latent, start_at_step=start_step, end_at_step=last_step, return_with_leftover_noise=not force_full_denoise, noise=noise, callback=callback)
if internal_seed is None:
internal_seed = seed
if 'internal_seed' in noise_mode:
seed = internal_seed
try:
samples = common.impact_sampling(
model=model, add_noise=not disable_noise, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative,
latent_image=latent, start_at_step=start_step, end_at_step=last_step, return_with_leftover_noise=not force_full_denoise, noise=noise, callback=callback,
scheduler_func=scheduler_func)
except Exception as e:
if "unexpected keyword argument 'scheduler_func'" in str(e):
logging.info("[Inspire Pack] Impact Pack is outdated. (Cannot use GITS scheduler.)")
samples = common.impact_sampling(
model=model, add_noise=not disable_noise, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative,
latent_image=latent, start_at_step=start_step, end_at_step=last_step, return_with_leftover_noise=not force_full_denoise, noise=noise, callback=callback)
else:
raise e
return samples, noise
@@ -41,7 +123,7 @@ class KSampler_inspire:
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
@@ -50,47 +132,16 @@ class KSampler_inspire:
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "doit"
CATEGORY = "InspirePack/a1111_compat"
def doit(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, batch_seed_mode="comfy", variation_seed=None, variation_strength=None):
return (inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, incremental_seed_mode=batch_seed_mode, variation_seed=variation_seed, variation_strength=variation_strength)[0],)
class KSamplerAdvanced_inspire:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"noise_mode": (supported_noise_modes,),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"optional":
{
"noise_opt": ("NOISE",),
"variation_method": (["linear", "slerp"],),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
}
}
@@ -100,7 +151,53 @@ class KSamplerAdvanced_inspire:
CATEGORY = "InspirePack/a1111_compat"
@staticmethod
def sample(model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise, denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, callback=None):
def doit(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
batch_seed_mode="comfy", variation_seed=None, variation_strength=None, variation_method="linear", scheduler_func_opt=None,
internal_seed=None):
return (inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
incremental_seed_mode=batch_seed_mode, variation_seed=variation_seed, variation_strength=variation_strength, variation_method=variation_method,
scheduler_func=scheduler_func_opt, internal_seed=internal_seed)[0], )
class KSamplerAdvanced_inspire:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"noise_mode": (supported_noise_modes,),
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"optional":
{
"variation_method": (["linear", "slerp"],),
"noise_opt": ("NOISE_IMAGE",),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "doit"
CATEGORY = "InspirePack/a1111_compat"
@staticmethod
def sample(model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise,
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, callback=None, variation_method="linear", scheduler_func_opt=None, internal_seed=None):
force_full_denoise = True
if return_with_leftover_noise:
@@ -114,7 +211,8 @@ class KSamplerAdvanced_inspire:
return inspire_ksampler(model, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step,
force_full_denoise=force_full_denoise, noise_mode=noise_mode, incremental_seed_mode=batch_seed_mode,
variation_seed=variation_seed, variation_strength=variation_strength, noise=noise_opt, callback=callback)
variation_seed=variation_seed, variation_strength=variation_strength, noise=noise_opt, callback=callback, variation_method=variation_method,
scheduler_func=scheduler_func_opt, internal_seed=internal_seed)
def doit(self, *args, **kwargs):
return (self.sample(*args, **kwargs)[0],)
@@ -125,18 +223,23 @@ class KSampler_inspire_pipe:
def INPUT_TYPES(s):
return {"required":
{"basic_pipe": ("BASIC_PIPE",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"latent_image": ("LATENT", ),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"noise_mode": (supported_noise_modes,),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
},
"optional":
{
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
}
}
RETURN_TYPES = ("LATENT", "VAE")
@@ -144,9 +247,11 @@ class KSampler_inspire_pipe:
CATEGORY = "InspirePack/a1111_compat"
def sample(self, basic_pipe, seed, steps, cfg, sampler_name, scheduler, latent_image, denoise, noise_mode, batch_seed_mode="comfy", variation_seed=None, variation_strength=None):
def sample(self, basic_pipe, seed, steps, cfg, sampler_name, scheduler, latent_image, denoise, noise_mode, batch_seed_mode="comfy",
variation_seed=None, variation_strength=None, scheduler_func_opt=None, internal_seed=None):
model, clip, vae, positive, negative = basic_pipe
latent = inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, incremental_seed_mode=batch_seed_mode, variation_seed=variation_seed, variation_strength=variation_strength)[0]
latent = inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, incremental_seed_mode=batch_seed_mode,
variation_seed=variation_seed, variation_strength=variation_strength, scheduler_func=scheduler_func_opt, internal_seed=internal_seed)[0]
return latent, vae
@@ -156,7 +261,7 @@ class KSamplerAdvanced_inspire_pipe:
return {"required":
{"basic_pipe": ("BASIC_PIPE",),
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
@@ -164,7 +269,7 @@ class KSamplerAdvanced_inspire_pipe:
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"noise_mode": (supported_noise_modes,),
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
@@ -172,7 +277,9 @@ class KSamplerAdvanced_inspire_pipe:
},
"optional":
{
"noise_opt": ("NOISE",),
"noise_opt": ("NOISE_IMAGE",),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
}
}
@@ -181,7 +288,8 @@ class KSamplerAdvanced_inspire_pipe:
CATEGORY = "InspirePack/a1111_compat"
def sample(self, basic_pipe, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise, denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None):
def sample(self, basic_pipe, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise,
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, scheduler_func_opt=None, internal_seed=None):
model, clip, vae, positive, negative = basic_pipe
latent = KSamplerAdvanced_inspire().sample(model=model, add_noise=add_noise, noise_seed=noise_seed,
steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler,
@@ -189,7 +297,8 @@ class KSamplerAdvanced_inspire_pipe:
start_at_step=start_at_step, end_at_step=end_at_step,
noise_mode=noise_mode, return_with_leftover_noise=return_with_leftover_noise,
denoise=denoise, batch_seed_mode=batch_seed_mode, variation_seed=variation_seed,
variation_strength=variation_strength, noise_opt=noise_opt)[0]
variation_strength=variation_strength, noise_opt=noise_opt, scheduler_func_opt=scheduler_func_opt,
internal_seed=internal_seed)[0]
return latent, vae
@@ -304,7 +413,7 @@ class HyperTileInspire:
nh = random_divisor(h, latent_tile_size * factor, swap_size, rand_obj)
nw = random_divisor(w, latent_tile_size * factor, swap_size, rand_obj)
print(f"factor: {factor} <--- params.depth: {apply_to.index(model_chans)} / scale_depth: {scale_depth} / latent_tile_size={latent_tile_size}")
logging.debug(f"factor: {factor} <--- params.depth: {apply_to.index(model_chans)} / scale_depth: {scale_depth} / latent_tile_size={latent_tile_size}")
# print(f"h: {h}, w:{w} --> nh: {nh}, nw: {nw}")
if nh * nw > 1:
@@ -313,7 +422,7 @@ class HyperTileInspire:
# else:
# temp = None
print(f"q={q} / k={k} / v={v}")
logging.debug(f"q={q} / k={k} / v={v}")
return q, k, v
return q, k, v
@@ -338,6 +447,7 @@ NODE_CLASS_MAPPINGS = {
"KSamplerAdvanced //Inspire": KSamplerAdvanced_inspire,
"KSamplerPipe //Inspire": KSampler_inspire_pipe,
"KSamplerAdvancedPipe //Inspire": KSamplerAdvanced_inspire_pipe,
"RandomNoise //Inspire": RandomNoise,
"HyperTile //Inspire": HyperTileInspire
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -345,5 +455,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"KSamplerAdvanced //Inspire": "KSamplerAdvanced (inspire)",
"KSamplerPipe //Inspire": "KSampler [pipe] (inspire)",
"KSamplerAdvancedPipe //Inspire": "KSamplerAdvanced [pipe] (inspire)",
"RandomNoise //Inspire": "RandomNoise (inspire)",
"HyperTile //Inspire": "HyperTile (Inspire)"
}
+269 -18
View File
@@ -1,5 +1,6 @@
import json
import os
from .libs import common
import folder_paths
import nodes
@@ -7,13 +8,15 @@ from server import PromptServer
from .libs.utils import TaggedCache, any_typ
import logging
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
settings_file = os.path.join(root_dir, 'cache_settings.json')
try:
with open(settings_file) as f:
cache_settings = json.load(f)
except Exception as e:
print(e)
logging.error(e)
cache_settings = {}
cache = TaggedCache(cache_settings)
cache_count = {}
@@ -57,11 +60,13 @@ class CacheBackendData:
OUTPUT_NODE = True
def doit(self, key, tag, data):
@staticmethod
def doit(key, tag, data):
global cache
if key == '*':
print(f"[Inspire Pack] CacheBackendData: '*' is reserved key. Cannot use that key")
logging.warning("[Inspire Pack] CacheBackendData: '*' is reserved key. Cannot use that key")
return (None,)
update_cache(key, tag, (False, data))
return (data,)
@@ -87,7 +92,8 @@ class CacheBackendDataNumberKey:
OUTPUT_NODE = True
def doit(self, key, tag, data):
@staticmethod
def doit(key, tag, data):
global cache
update_cache(key, tag, (False, data))
@@ -117,11 +123,13 @@ class CacheBackendDataList:
OUTPUT_NODE = True
def doit(self, key, tag, data):
@staticmethod
def doit(key, tag, data):
global cache
if key == '*':
print(f"[Inspire Pack] CacheBackendDataList: '*' is reserved key. Cannot use that key")
logging.warning("[Inspire Pack] CacheBackendDataList: '*' is reserved key. Cannot use that key")
return (None,)
update_cache(key[0], tag[0], (True, data))
return (data,)
@@ -180,7 +188,7 @@ class RetrieveBackendData:
v = cache.get(key)
if v is None:
print(f"[RetrieveBackendData] '{key}' is unregistered key.")
logging.warning(f"[RetrieveBackendData] '{key}' is unregistered key.")
return (None,)
is_list, data = v[1]
@@ -235,7 +243,7 @@ class RemoveBackendData:
elif key in cache:
del cache[key]
else:
print(f"[Inspire Pack] RemoveBackendData: invalid data key {key}")
logging.warning(f"[Inspire Pack] RemoveBackendData: invalid data key {key}")
return (signal_opt,)
@@ -259,7 +267,7 @@ class RemoveBackendDataNumberKey(RemoveBackendData):
if key in cache:
del cache[key]
else:
print(f"[Inspire Pack] RemoveBackendDataNumberKey: invalid data key {key}")
logging.warning(f"[Inspire Pack] RemoveBackendDataNumberKey: invalid data key {key}")
return (signal_opt,)
@@ -319,7 +327,6 @@ class ShowCachedInfo:
# tag settings is not changed
return
# print(f'set to {new_tag_settings}')
new_cache = TaggedCache(new_tag_settings)
for k, v in cache.items():
new_cache[k] = v
@@ -367,10 +374,10 @@ class CheckpointLoaderSimpleShared(nodes.CheckpointLoaderSimple):
res = self.load_checkpoint(ckpt_name)
update_cache(key, "ckpt", (False, res))
cache_kind = 'ckpt'
print(f"[Inspire Pack] CheckpointLoaderSimpleShared: Ckpt '{ckpt_name}' is cached to '{key}'.")
logging.info(f"[Inspire Pack] CheckpointLoaderSimpleShared: Ckpt '{ckpt_name}' is cached to '{key}'.")
else:
cache_kind, (_, res) = cache[key]
print(f"[Inspire Pack] CheckpointLoaderSimpleShared: Cached ckpt '{key}' is loaded. (Loading skip)")
logging.info(f"[Inspire Pack] CheckpointLoaderSimpleShared: Cached ckpt '{key}' is loaded. (Loading skip)")
if cache_kind == 'ckpt':
model, clip, vae = res
@@ -400,6 +407,174 @@ class CheckpointLoaderSimpleShared(nodes.CheckpointLoaderSimple):
return (None, cache_weak_hash(key))
class LoadDiffusionModelShared(nodes.UNETLoader):
@classmethod
def INPUT_TYPES(s):
return {"required": { "model_name": (folder_paths.get_filename_list("diffusion_models"), {"tooltip": "Diffusion Model Name"}),
"weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],),
"key_opt": ("STRING", {"multiline": False, "placeholder": "If empty, use 'model_name' as the key."}),
"mode": (['Auto', 'Override Cache', 'Read Only'],),
}
}
RETURN_TYPES = ("MODEL", "STRING")
RETURN_NAMES = ("model", "cache key")
FUNCTION = "doit"
CATEGORY = "InspirePack/Backend"
def doit(self, model_name, weight_dtype, key_opt, mode='Auto'):
if mode == 'Read Only':
if key_opt.strip() == '':
raise Exception("[LoadDiffusionModelShared] key_opt cannot be omit if mode is 'Read Only'")
key = key_opt.strip()
elif key_opt.strip() == '':
key = f"{model_name}_{weight_dtype}"
else:
key = key_opt.strip()
if key not in cache or mode == 'Override Cache':
model = self.load_unet(model_name, weight_dtype)[0]
update_cache(key, "diffusion", (False, model))
logging.info(f"[Inspire Pack] LoadDiffusionModelShared: diffusion model '{model_name}' is cached to '{key}'.")
else:
_, (_, model) = cache[key]
logging.info(f"[Inspire Pack] LoadDiffusionModelShared: Cached diffusion model '{key}' is loaded. (Loading skip)")
return model, key
@staticmethod
def IS_CHANGED(model_name, weight_dtype, key_opt, mode='Auto'):
if mode == 'Read Only':
if key_opt.strip() == '':
raise Exception("[LoadDiffusionModelShared] key_opt cannot be omit if mode is 'Read Only'")
key = key_opt.strip()
elif key_opt.strip() == '':
key = f"{model_name}_{weight_dtype}"
else:
key = key_opt.strip()
if mode == 'Read Only':
return None, cache_weak_hash(key)
elif mode == 'Override Cache':
return model_name, key
return None, cache_weak_hash(key)
class LoadTextEncoderShared:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model_name1": (folder_paths.get_filename_list("text_encoders"), ),
"model_name2": (["None"] + folder_paths.get_filename_list("text_encoders"), ),
"model_name3": (["None"] + folder_paths.get_filename_list("text_encoders"), ),
"type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "sdxl", "flux", "hunyuan_video"], ),
"key_opt": ("STRING", {"multiline": False, "placeholder": "If empty, use 'model_name' as the key."}),
"mode": (['Auto', 'Override Cache', 'Read Only'],),
},
"optional": { "device": (["default", "cpu"], {"advanced": True}), }
}
RETURN_TYPES = ("CLIP", "STRING")
RETURN_NAMES = ("clip", "cache key")
FUNCTION = "doit"
CATEGORY = "InspirePack/Backend"
DESCRIPTION = \
("[Recipes single]\n"
"stable_diffusion: clip-l\n"
"stable_cascade: clip-g\n"
"sd3: t5 / clip-g / clip-l\n"
"stable_audio: t5\n"
"mochi: t5\n"
"cosmos: old t5 xxl\n\n"
"[Recipes dual]\n"
"sdxl: clip-l, clip-g\n"
"sd3: clip-l, clip-g / clip-l, t5 / clip-g, t5\n"
"flux: clip-l, t5\n\n"
"[Recipes triple]\n"
"sd3: clip-l, clip-g, t5")
def doit(self, model_name1, model_name2, model_name3, type, key_opt, mode='Auto', device="default"):
if mode == 'Read Only':
if key_opt.strip() == '':
raise Exception("[LoadTextEncoderShared] key_opt cannot be omit if mode is 'Read Only'")
key = key_opt.strip()
elif key_opt.strip() == '':
key = model_name1
if model_name2 is not None:
key += f"_{model_name2}"
if model_name3 is not None:
key += f"_{model_name3}"
key += f"_{type}_{device}"
else:
key = key_opt.strip()
if key not in cache or mode == 'Override Cache':
if model_name2 != "None" and model_name3 != "None": # triple text encoder
if len({model_name1, model_name2, model_name3}) < 3:
logging.error("[LoadTextEncoderShared] The same model has been selected multiple times.")
raise ValueError("The same model has been selected multiple times.")
if type not in ["sd3"]:
logging.error("[LoadTextEncoderShared] Currently, the triple text encoder is only supported in `sd3`.")
raise ValueError("Currently, the triple text encoder is only supported in `sd3`.")
res = nodes.NODE_CLASS_MAPPINGS["TripleCLIPLoader"]().load_clip(model_name1, model_name2, model_name3)[0]
elif model_name2 != "None" or model_name3 != "None": # dual text encoder
second_model = model_name2 if model_name2 != "None" else model_name3
if model_name1 == second_model:
logging.error("[LoadTextEncoderShared] You have selected the same model for both.")
raise ValueError("[LoadTextEncoderShared] You have selected the same model for both.")
if type not in ["sdxl", "sd3", "flux", "hunyuan_video"]:
logging.error("[LoadTextEncoderShared] Currently, the triple text encoder is only supported in `sdxl, sd3, flux, hunyuan_video`.")
raise ValueError("Currently, the triple text encoder is only supported in `sdxl, sd3, flux, hunyuan_video`.")
res = nodes.NODE_CLASS_MAPPINGS["DualCLIPLoader"]().load_clip(model_name1, second_model, type=type, device=device)[0]
else: # single text encoder
if type not in ["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos"]:
logging.error("[LoadTextEncoderShared] Currently, the single text encoder is only supported in `stable_diffusion, stable_cascade, sd3, stable_audio, mochi, ltxv, pixart, cosmos`.")
raise ValueError("Currently, the single text encoder is only supported in `stable_diffusion, stable_cascade, sd3, stable_audio, mochi, ltxv, pixart, cosmos`.")
res = nodes.NODE_CLASS_MAPPINGS["CLIPLoader"]().load_clip(model_name1, type=type, device=device)[0]
update_cache(key, "diffusion", (False, res))
logging.info(f"[Inspire Pack] LoadTextEncoderShared: text encoder model set is cached to '{key}'.")
else:
_, (_, res) = cache[key]
logging.info(f"[Inspire Pack] LoadTextEncoderShared: Cached text encoder model set '{key}' is loaded. (Loading skip)")
return res, key
@staticmethod
def IS_CHANGED(model_name1, model_name2, model_name3, type, key_opt, mode='Auto', device="default"):
if mode == 'Read Only':
if key_opt.strip() == '':
raise Exception("[LoadTextEncoderShared] key_opt cannot be omit if mode is 'Read Only'")
key = key_opt.strip()
elif key_opt.strip() == '':
key = model_name1
if model_name2 is not None:
key += f"_{model_name2}"
if model_name3 is not None:
key += f"_{model_name3}"
key += f"_{type}_{device}"
else:
key = key_opt.strip()
if mode == 'Read Only':
return None, cache_weak_hash(key)
elif mode == 'Override Cache':
return f"{model_name1}_{model_name2}_{model_name3}_{type}_{device}", key
return None, cache_weak_hash(key)
class StableCascade_CheckpointLoader:
@classmethod
def INPUT_TYPES(s):
@@ -455,10 +630,10 @@ class StableCascade_CheckpointLoader:
if key_b not in cache:
res_b = nodes.CheckpointLoaderSimple().load_checkpoint(ckpt_name=stage_b)
update_cache(key_b, "ckpt", (False, res_b))
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_b}' is cached to '{key_b}'.")
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_b}' is cached to '{key_b}'.")
else:
_, (_, res_b) = cache[key_b]
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_b}' is loaded. (Loading skip)")
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_b}' is loaded. (Loading skip)")
b_model, clip, b_vae = res_b
else:
b_model, clip, b_vae = nodes.CheckpointLoaderSimple().load_checkpoint(ckpt_name=stage_b)
@@ -467,10 +642,10 @@ class StableCascade_CheckpointLoader:
if key_c not in cache:
res_c = nodes.unCLIPCheckpointLoader().load_checkpoint(ckpt_name=stage_c)
update_cache(key_c, "unclip_ckpt", (False, res_c))
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_c}' is cached to '{key_c}'.")
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_c}' is cached to '{key_c}'.")
else:
_, (_, res_c) = cache[key_c]
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_c}' is loaded. (Loading skip)")
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_c}' is loaded. (Loading skip)")
c_model, _, c_vae, clip_vision = res_c
else:
c_model, _, c_vae, clip_vision = nodes.unCLIPCheckpointLoader().load_checkpoint(ckpt_name=stage_c)
@@ -478,6 +653,74 @@ class StableCascade_CheckpointLoader:
return b_model, b_vae, c_model, c_vae, clip_vision, clip, key_b, key_c
class IsCached:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"key": ("STRING", {"multiline": False}),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = ("BOOLEAN", )
FUNCTION = "doit"
CATEGORY = "InspirePack/Backend"
@staticmethod
def IS_CHANGED(key, unique_id):
return common.is_changed(unique_id, key in cache)
def doit(self, key, unique_id):
return (key in cache,)
# WIP: not properly working, yet
class CacheBridge:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": (any_typ,),
"mode": ("BOOLEAN", {"default": True, "label_off": "cached", "label_on": "passthrough"}),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = (any_typ, )
RETURN_NAMES = ("value",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Backend"
@staticmethod
def IS_CHANGED(value, mode, unique_id):
if not mode and unique_id in common.changed_cache:
return common.not_changed_value(unique_id)
else:
return common.changed_value(unique_id)
def doit(self, value, mode, unique_id):
if not mode:
# cache mode
if unique_id not in common.changed_cache:
common.changed_cache[unique_id] = value
common.changed_count_cache[unique_id] = 0
return (common.changed_cache[unique_id],)
else:
common.changed_cache[unique_id] = value
common.changed_count_cache[unique_id] = 0
return (common.changed_cache[unique_id],)
NODE_CLASS_MAPPINGS = {
"CacheBackendData //Inspire": CacheBackendData,
"CacheBackendDataNumberKey //Inspire": CacheBackendDataNumberKey,
@@ -489,7 +732,11 @@ NODE_CLASS_MAPPINGS = {
"RemoveBackendDataNumberKey //Inspire": RemoveBackendDataNumberKey,
"ShowCachedInfo //Inspire": ShowCachedInfo,
"CheckpointLoaderSimpleShared //Inspire": CheckpointLoaderSimpleShared,
"StableCascade_CheckpointLoader //Inspire": StableCascade_CheckpointLoader
"LoadDiffusionModelShared //Inspire": LoadDiffusionModelShared,
"LoadTextEncoderShared //Inspire": LoadTextEncoderShared,
"StableCascade_CheckpointLoader //Inspire": StableCascade_CheckpointLoader,
"IsCached //Inspire": IsCached,
# "CacheBridge //Inspire": CacheBridge,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -503,5 +750,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RemoveBackendDataNumberKey //Inspire": "Remove Backend Data [NumberKey] (Inspire)",
"ShowCachedInfo //Inspire": "Show Cached Info (Inspire)",
"CheckpointLoaderSimpleShared //Inspire": "Shared Checkpoint Loader (Inspire)",
"StableCascade_CheckpointLoader //Inspire": "Stable Cascade Checkpoint Loader (Inspire)"
"LoadDiffusionModelShared //Inspire": "Shared Diffusion Model Loader (Inspire)",
"LoadTextEncoderShared //Inspire": "Shared Text Encoder Loader (Inspire)",
"StableCascade_CheckpointLoader //Inspire": "Stable Cascade Checkpoint Loader (Inspire)",
"IsCached //Inspire": "Is Cached (Inspire)",
# "CacheBridge //Inspire": "Cache Bridge (Inspire)"
}
+103 -14
View File
@@ -2,25 +2,32 @@ import torch
import nodes
import inspect
from .libs import utils
from nodes import MAX_RESOLUTION
import logging
class ConcatConditioningsWithMultiplier:
@classmethod
def INPUT_TYPES(s):
flex_inputs = {}
stack = inspect.stack()
if stack[1].function == 'get_input_data':
if stack[1].function == 'get_input_info':
# bypass validation
for x in range(0, 100):
flex_inputs[f"multiplier{x}"] = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
else:
flex_inputs["multiplier1"] = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
class AllContainer:
def __contains__(self, item):
return True
def __getitem__(self, key):
return "FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}
return {
"required": {"conditioning1": ("CONDITIONING",), },
"optional": AllContainer()
}
return {
"required": {"conditioning1": ("CONDITIONING",), },
"optional": flex_inputs
}
"required": {"conditioning1": ("CONDITIONING",), },
"optional": {"multiplier1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), },
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "doit"
@@ -45,9 +52,9 @@ class ConcatConditioningsWithMultiplier:
out = []
if len(conditioning_from) > 1:
print(f"Warning: ConcatConditioningsWithMultiplier {k} contains more than 1 cond, only the first one will actually be applied to conditioning1.")
logging.warning(f"[Inspire Pack] ConcatConditioningsWithMultiplier {k} contains more than 1 cond, only the first one will actually be applied to conditioning1.")
mkey = 'multiplier'+k[12:]
mkey = 'multiplier' + k[12:]
multiplier = float(kwargs[mkey])
conditioning_from = obj.multiply_conditioning_strength(conditioning=conditioning_from, multiplier=multiplier)[0]
cond_from = conditioning_from[0][0]
@@ -61,16 +68,98 @@ class ConcatConditioningsWithMultiplier:
conditioning_to = out
if out is None:
return (kwargs['conditioning1'], )
return (kwargs['conditioning1'],)
else:
return (out,)
# CREDIT for ConditioningStretch, ConditioningUpscale: Davemane42
# Imported to support archived custom nodes.
# original code: https://github.com/Davemane42/ComfyUI_Dave_CustomNode/blob/main/MultiAreaConditioning.py
class ConditioningStretch:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conditioning": ("CONDITIONING",),
"resolutionX": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
"resolutionY": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
"newWidth": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
"newHeight": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
# "scalar": ("INT", {"default": 2, "min": 1, "max": 100, "step": 0.5}),
},
}
RETURN_TYPES = ("CONDITIONING",)
CATEGORY = "InspirePack/conditioning"
FUNCTION = 'upscale'
@staticmethod
def upscale(conditioning, resolutionX, resolutionY, newWidth, newHeight, scalar=1):
c = []
for t in conditioning:
n = [t[0], t[1].copy()]
if 'area' in n[1]:
newWidth *= scalar
newHeight *= scalar
x = ((n[1]['area'][3] * 8) * newWidth / resolutionX) // 8
y = ((n[1]['area'][2] * 8) * newHeight / resolutionY) // 8
w = ((n[1]['area'][1] * 8) * newWidth / resolutionX) // 8
h = ((n[1]['area'][0] * 8) * newHeight / resolutionY) // 8
n[1]['area'] = tuple(map(lambda x: (((int(x) + 7) >> 3) << 3), [h, w, y, x]))
c.append(n)
return (c,)
class ConditioningUpscale:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conditioning": ("CONDITIONING",),
"scalar": ("INT", {"default": 2, "min": 1, "max": 100, "step": 0.5}),
},
}
RETURN_TYPES = ("CONDITIONING",)
CATEGORY = "InspirePack/conditioning"
FUNCTION = 'upscale'
@staticmethod
def upscale(conditioning, scalar):
c = []
for t in conditioning:
n = [t[0], t[1].copy()]
if 'area' in n[1]:
n[1]['area'] = tuple(map(lambda x: ((x * scalar + 7) >> 3) << 3, n[1]['area']))
c.append(n)
return (c,)
NODE_CLASS_MAPPINGS = {
"ConcatConditioningsWithMultiplier //Inspire": ConcatConditioningsWithMultiplier,
"ConditioningUpscale //Inspire": ConditioningUpscale,
"ConditioningStretch //Inspire": ConditioningStretch,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ConcatConditioningsWithMultiplier //Inspire": "Concat Conditionings with Multiplier (Inspire)",
"ConditioningUpscale //Inspire": "Conditioning Upscale (Inspire)",
"ConditioningStretch //Inspire": "Conditioning Stretch (Inspire)",
}
+25 -10
View File
@@ -2,11 +2,19 @@ import os
import torch
from PIL import ImageOps
try:
import pillow_jxl # noqa: F401
jxl = True
except ImportError:
jxl = False
import comfy
import folder_paths
import base64
from io import BytesIO
from .libs.utils import *
from .libs.utils import ByPassTypeTuple, empty_pil_tensor, empty_latent
from PIL import Image
import numpy as np
import logging
class LoadImagesFromDirBatch:
@@ -18,7 +26,7 @@ class LoadImagesFromDirBatch:
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": -1, "max": 0xffffffffffffffff, "step": 1}),
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
@@ -44,6 +52,8 @@ class LoadImagesFromDirBatch:
# Filter files by extension
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
if jxl:
valid_extensions.extend('.jxl')
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
dir_files = sorted(dir_files)
@@ -94,10 +104,10 @@ class LoadImagesFromDirBatch:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
for mask2 in masks[1:]:
for mask2 in masks:
if has_non_empty_mask:
if image1.shape[1:3] != mask2.shape:
mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[2], image1.shape[1]), mode='bilinear', align_corners=False)
mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[1], image1.shape[2]), mode='bilinear', align_corners=False)
mask2 = mask2.squeeze(0)
else:
mask2 = mask2.unsqueeze(0)
@@ -121,13 +131,14 @@ class LoadImagesFromDirList:
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
OUTPUT_IS_LIST = (True, True)
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
RETURN_NAMES = ("IMAGE", "MASK", "FILE PATH")
OUTPUT_IS_LIST = (True, True, True)
FUNCTION = "load_images"
@@ -149,6 +160,8 @@ class LoadImagesFromDirList:
# Filter files by extension
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
if jxl:
valid_extensions.extend('.jxl')
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
dir_files = sorted(dir_files)
@@ -159,6 +172,7 @@ class LoadImagesFromDirList:
images = []
masks = []
file_paths = []
limit_images = False
if image_load_cap > 0:
@@ -184,9 +198,10 @@ class LoadImagesFromDirList:
images.append(image)
masks.append(mask)
file_paths.append(str(image_path))
image_count += 1
return images, masks
return (images, masks, file_paths)
class LoadImageInspire:
@@ -245,7 +260,7 @@ class ChangeImageBatchSize:
output_tensor = input_tensor[:batch_size, :, :, :]
return output_tensor
else:
print(f"[WARN] ChangeImage(Latent)BatchSize: Unknown mode `{mode}` - ignored")
logging.warning(f"[Inspire Pack] ChangeImage(Latent)BatchSize: Unknown mode `{mode}` - ignored")
return input_tensor
@staticmethod
@@ -390,7 +405,7 @@ class ColorMapToMasks:
def doit(self, color_map, max_count, min_pixels):
if len(color_map) > 0:
print(f"[Inspire Pack] WARN: ColorMapToMasks - Sure, here's the translation: `color_map` can only be a single image. Only the first image will be processed. If you want to utilize the remaining images, convert the Image Batch to an Image List.")
logging.warning("[Inspire Pack] ColorMapToMasks - Sure, here's the translation: `color_map` can only be a single image. Only the first image will be processed. If you want to utilize the remaining images, convert the Image Batch to an Image List.")
top_colors = top_k_colors(color_map[0], max_count, min_pixels)
+54 -18
View File
@@ -6,6 +6,8 @@ from enum import Enum
from . import prompt_support
from aiohttp import web
from . import backend_support
from .libs import common
import logging
max_seed = 2**32 - 1
@@ -49,7 +51,7 @@ async def set_cache_settings(request):
try:
backend_support.ShowCachedInfo.set_cache_settings(data)
return web.Response(text='OK', status=200)
except Exception as e: # pylint: disable=broad-except
except Exception as e:
return web.Response(text=f"{e}", status=500)
@@ -262,16 +264,26 @@ def populate_wildcards(json_data):
if 'ImpactWildcardProcessor' in nodes.NODE_CLASS_MAPPINGS:
if not hasattr(nodes.NODE_CLASS_MAPPINGS['ImpactWildcardProcessor'], 'process'):
print(f"[Inspire Pack] Your Impact Pack is outdated. Please update to the latest version.")
logging.warning("[Inspire Pack] Your Impact Pack is outdated. Please update to the latest version.")
return
wildcard_process = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardProcessor'].process
updated_widget_values = {}
mbp_updated_widget_values = {}
for k, v in prompt.items():
if 'class_type' in v and v['class_type'] == 'WildcardEncode //Inspire':
inputs = v['inputs']
if inputs['mode'] and isinstance(inputs['populated_text'], str):
# legacy adapter
if isinstance(inputs['mode'], bool):
if inputs['mode']:
new_mode = 'populate'
else:
new_mode = 'fixed'
inputs['mode'] = new_mode
if inputs['mode'] == 'populate' and isinstance(inputs['populated_text'], str):
if isinstance(inputs['seed'], list):
try:
input_node = prompt[inputs['seed'][0]]
@@ -284,7 +296,7 @@ def populate_wildcards(json_data):
if not isinstance(input_seed, int):
continue
else:
print(f"[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
logging.warning("[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
continue
except:
continue
@@ -292,14 +304,17 @@ def populate_wildcards(json_data):
input_seed = int(inputs['seed'])
inputs['populated_text'] = wildcard_process(text=inputs['wildcard_text'], seed=input_seed)
inputs['mode'] = False
inputs['mode'] = 'reproduce'
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "populated_text", "type": "text", "data": inputs['populated_text']})
updated_widget_values[k] = inputs['populated_text']
if inputs['mode'] == 'reproduce':
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "mode", "type": "text", "value": 'populate'})
elif 'class_type' in v and v['class_type'] == 'MakeBasicPipe //Inspire':
inputs = v['inputs']
if inputs['wildcard_mode'] and (isinstance(inputs['positive_populated_text'], str) or isinstance(inputs['negative_populated_text'], str)):
if inputs['wildcard_mode'] == 'populate' and (isinstance(inputs['positive_populated_text'], str) or isinstance(inputs['negative_populated_text'], str)):
if isinstance(inputs['seed'], list):
try:
input_node = prompt[inputs['seed'][0]]
@@ -312,7 +327,7 @@ def populate_wildcards(json_data):
if not isinstance(input_seed, int):
continue
else:
print(f"[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
logging.warning("[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
continue
except:
continue
@@ -327,19 +342,24 @@ def populate_wildcards(json_data):
inputs['negative_populated_text'] = wildcard_process(text=inputs['negative_wildcard_text'], seed=input_seed)
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "negative_populated_text", "type": "text", "data": inputs['negative_populated_text']})
inputs['wildcard_mode'] = False
inputs['wildcard_mode'] = 'reproduce'
mbp_updated_widget_values[k] = inputs['positive_populated_text'], inputs['negative_populated_text']
if inputs['wildcard_mode'] == 'reproduce':
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "wildcard_mode", "type": "text", "value": 'populate'})
if 'extra_data' in json_data and 'extra_pnginfo' in json_data['extra_data']:
for node in json_data['extra_data']['extra_pnginfo']['workflow']['nodes']:
key = str(node['id'])
if key in updated_widget_values:
node['widgets_values'][3] = updated_widget_values[key]
node['widgets_values'][4] = False
if key in mbp_updated_widget_values:
node['widgets_values'][7] = mbp_updated_widget_values[key][0]
node['widgets_values'][8] = mbp_updated_widget_values[key][1]
node['widgets_values'][5] = False
extra_pnginfo = json_data['extra_data']['extra_pnginfo']
if 'workflow' in extra_pnginfo and extra_pnginfo['workflow'] is not None and 'nodes' in extra_pnginfo['workflow']:
for node in extra_pnginfo['workflow']['nodes']:
key = str(node['id'])
if key in updated_widget_values:
node['widgets_values'][3] = updated_widget_values[key]
node['widgets_values'][4] = 'reproduce'
if key in mbp_updated_widget_values:
node['widgets_values'][7] = mbp_updated_widget_values[key][0]
node['widgets_values'][8] = mbp_updated_widget_values[key][1]
node['widgets_values'][5] = 'reproduce'
def force_reset_useless_params(json_data):
@@ -352,6 +372,21 @@ def force_reset_useless_params(json_data):
return json_data
def clear_unused_node_changed_cache(json_data):
prompt = json_data['prompt']
unused = []
for x in common.changed_cache.keys():
if x not in prompt:
unused.append(x)
for x in unused:
del common.changed_cache[x]
del common.changed_count_cache[x]
return json_data
def onprompt(json_data):
prompt_support.list_counter_map = {}
@@ -365,6 +400,7 @@ def onprompt(json_data):
populate_wildcards(json_data)
force_reset_useless_params(json_data)
clear_unused_node_changed_cache(json_data)
return json_data
+73 -2
View File
@@ -1,14 +1,85 @@
import traceback
import comfy
import nodes
from . import utils
import logging
from server import PromptServer
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD']
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD', "GITS[coeff=1.2]"]
def impact_sampling(*args, **kwargs):
if 'RegionalSampler' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
"'Impact Pack' extension is required.")
raise Exception(f"[ERROR] You need to install 'ComfyUI-Impact-Pack'")
raise Exception("[ERROR] You need to install 'ComfyUI-Impact-Pack'")
return nodes.NODE_CLASS_MAPPINGS['RegionalSampler'].separated_sample(*args, **kwargs)
changed_count_cache = {}
changed_cache = {}
def changed_value(uid):
v = changed_count_cache.get(uid, 0)
changed_count_cache[uid] = v + 1
return v + 1
def not_changed_value(uid):
return changed_count_cache.get(uid, 0)
def is_changed(uid, value):
if uid not in changed_cache or changed_cache[uid] != value:
res = changed_value(uid)
else:
res = not_changed_value(uid)
changed_cache[uid] = value
logging.info(f"keys: {changed_cache.keys()}")
return res
def update_node_status(node, text, progress=None):
if PromptServer.instance.client_id is None:
return
PromptServer.instance.send_sync("inspire/update_status", {
"node": node,
"progress": progress,
"text": text
}, PromptServer.instance.client_id)
class ListWrapper:
def __init__(self, data, aux=None):
if isinstance(data, ListWrapper):
self._data = data
if aux is None:
self.aux = data.aux
else:
self.aux = aux
else:
self._data = list(data)
self.aux = aux
def __getitem__(self, index):
if isinstance(index, slice):
return ListWrapper(self._data[index], self.aux)
else:
return self._data[index]
def __setitem__(self, index, value):
self._data[index] = value
def __len__(self):
return len(self._data)
def __repr__(self):
return f"ListWrapper({self._data}, aux={self.aux})"
+105 -11
View File
@@ -4,9 +4,12 @@ import numpy as np
import torch
from PIL import Image, ImageDraw
import math
import cv2
import folder_paths
import logging
def apply_variation_noise(latent_image, noise_device, variation_seed, variation_strength, mask=None):
def apply_variation_noise(latent_image, noise_device, variation_seed, variation_strength, mask=None, variation_method='linear'):
latent_size = latent_image.size()
latent_size_1batch = [1, latent_size[1], latent_size[2], latent_size[3]]
@@ -27,12 +30,50 @@ def apply_variation_noise(latent_image, noise_device, variation_seed, variation_
result = (1 - variation_strength) * latent_image + variation_strength * variation_noise
else:
# this seems precision is not enough when variation_strength is 0.0
result = (mask == 1).float() * ((1 - variation_strength) * latent_image + variation_strength * variation_noise * mask) + (mask == 0).float() * latent_image
mixed_noise = mix_noise(latent_image, variation_noise, variation_strength, variation_method=variation_method)
result = (mask == 1).float() * mixed_noise + (mask == 0).float() * latent_image
return result
def prepare_noise(latent_image, seed, noise_inds=None, noise_device="cpu", incremental_seed_mode="comfy", variation_seed=None, variation_strength=None):
# CREDIT: https://github.com/BlenderNeko/ComfyUI_Noise/blob/afb14757216257b12268c91845eac248727a55e2/nodes.py#L68
# https://discuss.pytorch.org/t/help-regarding-slerp-function-for-generative-model-sampling/32475/3
def slerp(val, low, high):
dims = low.shape
low = low.reshape(dims[0], -1)
high = high.reshape(dims[0], -1)
low_norm = low/torch.norm(low, dim=1, keepdim=True)
high_norm = high/torch.norm(high, dim=1, keepdim=True)
low_norm[low_norm != low_norm] = 0.0
high_norm[high_norm != high_norm] = 0.0
omega = torch.acos((low_norm*high_norm).sum(1))
so = torch.sin(omega)
res = (torch.sin((1.0-val)*omega)/so).unsqueeze(1)*low + (torch.sin(val*omega)/so).unsqueeze(1) * high
return res.reshape(dims)
def mix_noise(from_noise, to_noise, strength, variation_method):
to_noise = to_noise.to(from_noise.device)
if variation_method == 'slerp':
mixed_noise = slerp(strength, from_noise, to_noise)
else:
# linear
mixed_noise = (1 - strength) * from_noise + strength * to_noise
# NOTE: Since the variance of the Gaussian noise in mixed_noise has changed, it must be corrected through scaling.
scale_factor = math.sqrt((1 - strength) ** 2 + strength ** 2)
mixed_noise /= scale_factor
return mixed_noise
def prepare_noise(latent_image, seed, noise_inds=None, noise_device="cpu", incremental_seed_mode="comfy", variation_seed=None, variation_strength=None, variation_method="linear"):
"""
creates random noise given a latent image and a seed.
optional arg skip can be used to skip and discard x number of noise generations for a given seed
@@ -63,13 +104,10 @@ def prepare_noise(latent_image, seed, noise_inds=None, noise_device="cpu", incre
strength += strength_up
variation_noise = variation_latent.expand(input_latent.size()[0], -1, -1, -1)
mixed_noise = (1 - strength) * input_latent + strength * variation_noise
# NOTE: Since the variance of the Gaussian noise in mixed_noise has changed, it must be corrected through scaling.
scale_factor = math.sqrt((1 - strength) ** 2 + strength ** 2)
corrected_noise = mixed_noise / scale_factor
mixed_noise = mix_noise(input_latent, variation_noise, strength, variation_method)
return corrected_noise
return mixed_noise
# method: incremental seed batch noise
if noise_inds is None and incremental_seed_mode == "incremental":
@@ -161,9 +199,9 @@ def try_install_custom_node(custom_node_url, msg):
import cm_global
cm_global.try_call(api='cm.try-install-custom-node',
sender="Inspire Pack", custom_node_url=custom_node_url, msg=msg)
except Exception as e:
print(msg)
print(f"[Inspire Pack] ComfyUI-Manager is outdated. The custom node installation feature is not available.")
except Exception as e: # noqa: F841
logging.error(msg)
logging.error("[Inspire Pack] ComfyUI-Manager is outdated. The custom node installation feature is not available.")
def empty_latent():
@@ -255,3 +293,59 @@ class TaggedCache:
def clear(self):
# clear all cache
self._data = {}
def make_3d_mask(mask):
if len(mask.shape) == 4:
return mask.squeeze(0)
elif len(mask.shape) == 2:
return mask.unsqueeze(0)
return mask
def dilate_mask(mask: torch.Tensor, dilation_factor: float) -> torch.Tensor:
"""Dilate a mask using a square kernel with a given dilation factor."""
kernel_size = int(dilation_factor * 2) + 1
kernel = np.ones((abs(kernel_size), abs(kernel_size)), np.uint8)
masks = make_3d_mask(mask).numpy()
dilated_masks = []
for m in masks:
if dilation_factor > 0:
m2 = cv2.dilate(m, kernel, iterations=1)
else:
m2 = cv2.erode(m, kernel, iterations=1)
dilated_masks.append(torch.from_numpy(m2))
return torch.stack(dilated_masks)
def flatten_non_zero_override(masks: torch.Tensor):
"""
flatten multiple layer mask tensor to 1 layer mask tensor.
Override the lower layer with the tensor from the upper layer, but only override non-zero values.
:param masks: 3d mask
:return: flatten mask
"""
final_mask = masks[0]
for i in range(1, masks.size(0)):
non_zero_mask = masks[i] != 0
final_mask[non_zero_mask] = masks[i][non_zero_mask]
return final_mask
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
for full_folder_path in full_folder_paths:
folder_paths.add_model_folder_path(folder_name, full_folder_path)
if folder_name in folder_paths.folder_names_and_paths:
current_paths, current_extensions = folder_paths.folder_names_and_paths[folder_name]
updated_extensions = current_extensions | extensions
folder_paths.folder_names_and_paths[folder_name] = (current_paths, updated_extensions)
else:
folder_paths.folder_names_and_paths[folder_name] = (full_folder_paths, extensions)
+187 -3
View File
@@ -1,3 +1,7 @@
from comfy_execution.graph_utils import GraphBuilder, is_link
from .libs.utils import any_typ
from .libs.common import update_node_status, ListWrapper
class FloatRange:
@classmethod
def INPUT_TYPES(s):
@@ -15,12 +19,17 @@ class FloatRange:
FUNCTION = "doit"
CATEGORY = "InspirePack/Util"
CATEGORY = "InspirePack/List"
def doit(self, start, stop, step, limit, ensure_end):
if start >= stop or step == 0:
if start == stop or step == 0:
return ([start], )
reverse = False
if start > stop:
reverse = True
start, stop = stop, start
res = []
x = start
last = x
@@ -36,13 +45,188 @@ class FloatRange:
res.append(stop)
if reverse:
res.reverse()
return (res, )
class WorklistToItemList:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"item": (any_typ, ),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("ITEM_LIST",)
RETURN_NAMES = ("item_list",)
FUNCTION = "doit"
DESCRIPTION = "The list in ComfyUI allows for repeated execution of a sub-workflow.\nThis groups these repetitions (a.k.a. list) into a single ITEM_LIST output.\nITEM_LIST can then be used in ForeachList."
CATEGORY = "InspirePack/List"
def doit(self, item):
return (item, )
# Loop nodes are implemented based on BadCafeCode's reference loop implementation
# https://github.com/BadCafeCode/execution-inversion-demo-comfyui/blob/main/flow_control.py
class ForeachListBegin:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"item_list": ("ITEM_LIST", {"tooltip": "ITEM_LIST containing items to be processed iteratively."}),
},
"optional": {
"initial_input": (any_typ, {"tooltip": "If initial_input is omitted, the first item in item_list is used as the initial value, and the processing starts from the second item in item_list."}),
}
}
RETURN_TYPES = ("FOREACH_LIST_CONTROL", "ITEM_LIST", any_typ, any_typ)
RETURN_NAMES = ("flow_control", "remained_list", "item", "intermediate_output")
OUTPUT_TOOLTIPS = (
"Pass ForeachListEnd as is to indicate the end of the iteration.",
"Output the ITEM_LIST containing the remaining items during the iteration, passing ForeachListEnd as is to indicate the end of the iteration.",
"Output the current item during the iteration.",
"Output the intermediate results during the iteration.")
FUNCTION = "doit"
DESCRIPTION = "A starting node for performing iterative tasks by retrieving items one by one from the ITEM_LIST.\nGenerate a new intermediate_output using item and intermediate_output as inputs, then connect it to ForeachListEnd.\nNOTE:If initial_input is omitted, the first item in item_list is used as the initial value, and the processing starts from the second item in item_list."
CATEGORY = "InspirePack/List"
def doit(self, item_list, initial_input=None):
if initial_input is None:
initial_input = item_list[0]
item_list = item_list[1:]
if len(item_list) > 0:
next_list = ListWrapper(item_list[1:])
next_item = item_list[0]
else:
next_list = ListWrapper([])
next_item = None
if next_list.aux is None:
next_list.aux = len(item_list), None
return "stub", next_list, next_item, initial_input
class ForeachListEnd:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"flow_control": ("FOREACH_LIST_CONTROL", {"rawLink": True, "tooltip": "Directly connect the output of ForeachListBegin, the starting node of the iteration."}),
"remained_list": ("ITEM_LIST", {"tooltip":"Directly connect the output of ForeachListBegin, the starting node of the iteration."}),
"intermediate_output": (any_typ, {"tooltip":"Connect the intermediate outputs processed within the iteration here."}),
},
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = (any_typ,)
RETURN_NAMES = ("result",)
OUTPUT_TOOLTIPS = ("This is the final output value.",)
FUNCTION = "doit"
DESCRIPTION = "A end node for performing iterative tasks by retrieving items one by one from the ITEM_LIST.\nNOTE:Directly connect the outputs of ForeachListBegin to 'flow_control' and 'remained_list'."
CATEGORY = "InspirePack/List"
def explore_dependencies(self, node_id, dynprompt, upstream):
node_info = dynprompt.get_node(node_id)
if "inputs" not in node_info:
return
for k, v in node_info["inputs"].items():
if is_link(v):
parent_id = v[0]
if parent_id not in upstream:
upstream[parent_id] = []
self.explore_dependencies(parent_id, dynprompt, upstream)
upstream[parent_id].append(node_id)
def collect_contained(self, node_id, upstream, contained):
if node_id not in upstream:
return
for child_id in upstream[node_id]:
if child_id not in contained:
contained[child_id] = True
self.collect_contained(child_id, upstream, contained)
def doit(self, flow_control, remained_list, intermediate_output, dynprompt, unique_id):
if remained_list.aux[1] is None:
remained_list.aux = (remained_list.aux[0], unique_id)
update_node_status(remained_list.aux[1], f"{(remained_list.aux[0]-len(remained_list))}/{remained_list.aux[0]} steps", (remained_list.aux[0]-len(remained_list))/remained_list.aux[0])
if len(remained_list) == 0:
return (intermediate_output,)
# We want to loop
upstream = {}
# Get the list of all nodes between the open and close nodes
self.explore_dependencies(unique_id, dynprompt, upstream)
contained = {}
open_node = flow_control[0]
self.collect_contained(open_node, upstream, contained)
contained[unique_id] = True
contained[open_node] = True
# We'll use the default prefix, but to avoid having node names grow exponentially in size,
# we'll use "Recurse" for the name of the recursively-generated copy of this node.
graph = GraphBuilder()
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id)
node.set_override_display_id(node_id)
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
for k, v in original_node["inputs"].items():
if is_link(v) and v[0] in contained:
parent = graph.lookup_node(v[0])
node.set_input(k, parent.out(v[1]))
else:
node.set_input(k, v)
new_open = graph.lookup_node(open_node)
new_open.set_input("item_list", remained_list)
new_open.set_input("initial_input", intermediate_output)
my_clone = graph.lookup_node("Recurse" )
result = (my_clone.out(0),)
return {
"result": result,
"expand": graph.finalize(),
}
NODE_CLASS_MAPPINGS = {
"FloatRange //Inspire": FloatRange,
"WorklistToItemList //Inspire": WorklistToItemList,
"ForeachListBegin //Inspire": ForeachListBegin,
"ForeachListEnd //Inspire": ForeachListEnd,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FloatRange //Inspire": "Float Range (Inspire)"
"FloatRange //Inspire": "Float Range (Inspire)",
"WorklistToItemList //Inspire": "Worklist To Item List (Inspire)",
"ForeachListBegin //Inspire": "▶Foreach List (Inspire)",
"ForeachListEnd //Inspire": "Foreach List◀ (Inspire)",
}
+575 -68
View File
@@ -6,11 +6,20 @@ import torch
import numpy as np
import nodes
import re
import json
from comfy.cli_args import args
from safetensors.torch import safe_open
import ast
import logging
from server import PromptServer
from .libs import utils
model_path = folder_paths.models_dir
utils.add_folder_path_and_extensions("lbw_models", [os.path.join(model_path, "lbw_models")], {'.safetensors'})
def is_numeric_string(input_str):
return re.match(r'^-?\d+(\.\d+)?$', input_str) is not None
@@ -34,6 +43,73 @@ def load_lbw_preset(filename):
return []
def parse_unet_num(s):
if s[1] == '.':
return int(s[0])
else:
return int(s)
class MakeLBW:
@classmethod
def INPUT_TYPES(s):
preset = ["Preset"] # 20
preset += load_lbw_preset("lbw-preset.txt")
preset += load_lbw_preset("lbw-preset.custom.txt")
preset = [name for name in preset if not name.startswith('@')]
lora_names = folder_paths.get_filename_list("loras")
lora_dirs = [os.path.dirname(name) for name in lora_names]
lora_dirs = ["All"] + list(set(lora_dirs))
return {"required": {"model": ("MODEL",),
"clip": ("CLIP", ),
"category_filter": (lora_dirs,),
"lora_name": (lora_names, ),
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False", "tooltip": "Apply the following weights for each block:\nTrue: 1 - weight\nFalse: weight"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": ""}),
"A": ("FLOAT", {"default": 4.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"B": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"preset": (preset,),
"block_vector": ("STRING", {"multiline": True, "placeholder": "block weight vectors", "default": "1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", "pysssss.autocomplete": False}),
"bypass": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
}
}
RETURN_TYPES = ("LBW_MODEL", "STRING")
RETURN_NAMES = ("lbw_model", "populated_vector")
FUNCTION = "doit"
CATEGORY = "InspirePack/LoraBlockWeight"
DESCRIPTION = "Instead of directly applying the LoRA Block Weight to the MODEL, it is generated in a separate LBW_MODEL form."
def __init__(self):
self.loaded_lora = None
def doit(self, model, clip, lora_name, inverse, seed, A, B, preset, block_vector, bypass=False, category_filter=None):
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)
block_weights, muted_weights, populated_vector = LoraLoaderBlockWeight.load_lbw(model, clip, lora, inverse, seed, A, B, block_vector)
lbw_model = {
'blocks': block_weights,
'muted': muted_weights
}
return lbw_model, populated_vector
class LoraLoaderBlockWeight:
def __init__(self):
self.loaded_lora = None
@@ -55,8 +131,8 @@ class LoraLoaderBlockWeight:
"lora_name": (lora_names, ),
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False", "tooltip": "Apply the following weights for each block:\nTrue: 1 - weight\nFalse: weight"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": ""}),
"A": ("FLOAT", {"default": 4.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"B": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"preset": (preset,),
@@ -130,11 +206,150 @@ class LoraLoaderBlockWeight:
return value
@staticmethod
def load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector):
def block_spec_parser(loaded, spec):
if not spec.startswith("%"):
return spec
else:
items = [x.strip() for x in spec[1:].split(',')]
input_blocks_set = set()
middle_blocks_set= set()
output_blocks_set = set()
double_blocks_set = set()
single_blocks_set = set()
for key, v in loaded.items():
if isinstance(key, tuple):
k = key[0]
else:
k = key
k_unet = k[len("diffusion_model."):]
if k_unet.startswith("input_blocks."):
k_unet_num = k_unet[len("input_blocks."):len("input_blocks.")+2]
k_unet_int = parse_unet_num(k_unet_num)
input_blocks_set.add(k_unet_int)
elif k_unet.startswith("middle_block."):
k_unet_num = k_unet[len("middle_block."):len("middle_block.")+2]
k_unet_int = parse_unet_num(k_unet_num)
middle_blocks_set.add(k_unet_int)
elif k_unet.startswith("output_blocks."):
k_unet_num = k_unet[len("output_blocks."):len("output_blocks.")+2]
k_unet_int = parse_unet_num(k_unet_num)
output_blocks_set.add(k_unet_int)
elif k_unet.startswith("double_blocks."):
k_unet_num = k_unet[len("double_blocks."):len("double_blocks.") + 2]
k_unet_int = parse_unet_num(k_unet_num)
double_blocks_set.add(k_unet_int)
elif k_unet.startswith("single_blocks."):
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.") + 2]
k_unet_int = parse_unet_num(k_unet_num)
single_blocks_set.add(k_unet_int)
pat1 = re.compile(r"(default|base)=([0-9.]+)")
pat2 = re.compile(r"(in|out|mid|double|single)([0-9]+)-([0-9]+)=([0-9.]+)")
pat3 = re.compile(r"(in|out|mid|double|single)([0-9]+)=([0-9.]+)")
pat4 = re.compile(r"(in|out|mid|double|single)=([0-9.]+)")
base_spec = None
default_spec = 1.0
for item in items:
match = pat1.match(item)
if match:
if match[1] == 'base':
base_spec = match[2]
continue
if match[1] == 'default':
default_spec = match[2]
continue
if base_spec is None:
base_spec = default_spec
input_blocks = [default_spec] * len(input_blocks_set)
middle_blocks = [default_spec] * len(middle_blocks_set)
output_blocks = [default_spec] * len(output_blocks_set)
double_blocks = [default_spec] * len(double_blocks_set)
single_blocks = [default_spec] * len(single_blocks_set)
for item in items:
match = pat2.match(item)
if match:
for x in range(int(match[2])-1, int(match[3])):
value = float(match[4])
if x < 0:
continue
if match[1] == 'in' and len(input_blocks) > x:
input_blocks[x] = value
elif match[1] == 'out' and len(output_blocks) > x:
output_blocks[x] = value
elif match[1] == 'mid' and len(middle_blocks) > x:
middle_blocks[x] = value
elif match[1] == 'double' and len(double_blocks) > x:
double_blocks[x] = value
elif match[1] == 'single' and len(single_blocks) > x:
single_blocks[x] = value
continue
match = pat3.match(item)
if match:
value = float(match[3])
x = int(match[2]) - 1
if x < 0:
continue
if match[1] == 'in' and len(input_blocks) > x:
input_blocks[x] = value
elif match[1] == 'out' and len(output_blocks) > x:
output_blocks[x] = value
elif match[1] == 'mid' and len(middle_blocks) > x:
middle_blocks[x] = value
elif match[1] == 'double' and len(double_blocks) > x:
double_blocks[x] = value
elif match[1] == 'single' and len(single_blocks) > x:
single_blocks[x] = value
continue
match = pat4.match(item)
if match:
value = float(match[2])
if match[1] == 'in':
input_blocks = [value] * len(input_blocks)
elif match[1] == 'out':
output_blocks = [value] * len(output_blocks)
elif match[1] == 'mid':
middle_blocks = [value] * len(middle_blocks)
elif match[1] == 'double':
double_blocks = [value] * len(double_blocks)
elif match[1] == 'single':
single_blocks = [value] * len(single_blocks)
continue
# concat specs
res = [str(base_spec)]
for x in (input_blocks + middle_blocks + output_blocks + double_blocks + single_blocks):
res.append(str(x))
return ",".join(res)
@staticmethod
def load_lbw(model, clip, lora, inverse, seed, A, B, block_vector):
key_map = comfy.lora.model_lora_keys_unet(model.model)
key_map = comfy.lora.model_lora_keys_clip(clip.cond_stage_model, key_map)
loaded = comfy.lora.load_lora(lora, key_map)
block_vector = LoraLoaderBlockWeight.block_spec_parser(loaded, block_vector)
block_vector = block_vector.split(":")
if len(block_vector) > 1:
block_vector = block_vector[1]
@@ -142,7 +357,6 @@ class LoraLoaderBlockWeight:
block_vector = block_vector[0]
vector = block_vector.split(",")
vector_i = 1
if not LoraLoaderBlockWeight.validate(vector):
preset_dict = load_preset_dict()
@@ -151,22 +365,19 @@ class LoraLoaderBlockWeight:
else:
raise ValueError(f"[LoraLoaderBlockWeight] invalid block_vector '{block_vector}'")
last_k_unet_num = None
new_modelpatcher = model.clone()
populated_ratio = strength_model
def parse_unet_num(s):
if s[1] == '.':
return int(s[0])
else:
return int(s)
# sort: input, middle, output, others
input_blocks = []
middle_blocks = []
output_blocks = []
double_blocks = []
single_blocks = []
others = []
for k, v in loaded.items():
for key, v in loaded.items():
if isinstance(key, tuple):
k = key[0]
else:
k = key
k_unet = k[len("diffusion_model."):]
if k_unet.startswith("input_blocks."):
@@ -178,18 +389,34 @@ class LoraLoaderBlockWeight:
elif k_unet.startswith("output_blocks."):
k_unet_num = k_unet[len("output_blocks."):len("output_blocks.")+2]
output_blocks.append((k, v, parse_unet_num(k_unet_num), k_unet))
elif k_unet.startswith("double_blocks."):
k_unet_num = k_unet[len("double_blocks."):len("double_blocks.")+2]
double_blocks.append((key, v, parse_unet_num(k_unet_num), k_unet))
elif k_unet.startswith("single_blocks."):
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.")+2]
single_blocks.append((key, v, parse_unet_num(k_unet_num), k_unet))
else:
others.append((k, v, k_unet))
input_blocks = sorted(input_blocks, key=lambda x: x[2])
middle_blocks = sorted(middle_blocks, key=lambda x: x[2])
output_blocks = sorted(output_blocks, key=lambda x: x[2])
double_blocks = sorted(double_blocks, key=lambda x: x[2])
single_blocks = sorted(single_blocks, key=lambda x: x[2])
# prepare patch
np.random.seed(seed % (2**31))
populated_vector_list = []
ratios = []
for k, v, k_unet_num, k_unet in (input_blocks + middle_blocks + output_blocks):
ratio = 1.0
vector_i = 1
last_k_unet_num = None
block_weights = {}
muted_weights = []
for k, v, k_unet_num, k_unet in (input_blocks + middle_blocks + output_blocks + double_blocks + single_blocks):
if last_k_unet_num != k_unet_num and len(vector) > vector_i:
ratios = LoraLoaderBlockWeight.convert_vector_value(A, B, vector[vector_i].strip())
ratio = ratios.pop(0)
@@ -205,6 +432,8 @@ class LoraLoaderBlockWeight:
else:
if len(ratios) > 0:
ratio = ratios.pop(0)
else:
pass # use last used ratio if no more user specified ratio is given
if inverse:
populated_ratio = 1 - ratio
@@ -213,11 +442,10 @@ class LoraLoaderBlockWeight:
last_k_unet_num = k_unet_num
new_modelpatcher.add_patches({k: v}, strength_model * populated_ratio)
# if inverse:
# print(f"\t{k_unet} -> inv({ratio}) ")
# else:
# print(f"\t{k_unet} -> ({ratio}) ")
if populated_ratio != 0:
block_weights[k] = v, populated_ratio
else:
muted_weights.append(k)
# prepare base patch
ratios = LoraLoaderBlockWeight.convert_vector_value(A, B, vector[0].strip())
@@ -226,25 +454,43 @@ class LoraLoaderBlockWeight:
if inverse:
populated_ratio = 1 - ratio
else:
populated_ratio = 1
populated_ratio = ratio
populated_vector_list.insert(0, LoraLoaderBlockWeight.norm_value(populated_ratio))
for k, v, k_unet in others:
new_modelpatcher.add_patches({k: v}, strength_model * populated_ratio)
# if inverse:
# print(f"\t{k_unet} -> inv({ratio}) ")
# else:
# print(f"\t{k_unet} -> ({ratio}) ")
if populated_ratio != 0:
block_weights[k] = v, populated_ratio
else:
muted_weights.append(k)
new_clip = clip.clone()
new_clip.add_patches(loaded, strength_clip)
populated_vector = ','.join(map(str, populated_vector_list))
return (new_modelpatcher, new_clip, populated_vector)
return block_weights, muted_weights, populated_vector
@staticmethod
def load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector):
block_weights, muted_weights, populated_vector = LoraLoaderBlockWeight.load_lbw(model, clip, lora, inverse, seed, A, B, block_vector)
new_modelpatcher = model.clone()
new_clip = clip.clone()
muted_weights = set(muted_weights)
for k, v in block_weights.items():
weights, ratio = v
if k in muted_weights:
pass
elif 'text' in k or 'encoder' in k:
new_clip.add_patches({k: weights}, strength_clip * ratio)
else:
new_modelpatcher.add_patches({k: weights}, strength_model * ratio)
return new_modelpatcher, new_clip, populated_vector
def doit(self, model, clip, lora_name, strength_model, strength_clip, inverse, seed, A, B, preset, block_vector, bypass=False, category_filter=None):
if strength_model == 0 and strength_clip == 0 or bypass:
return (model, clip, "")
return model, clip, ""
lora_path = folder_paths.get_full_path("loras", lora_name)
lora = None
@@ -261,7 +507,48 @@ class LoraLoaderBlockWeight:
self.loaded_lora = (lora_path, lora)
model_lora, clip_lora, populated_vector = LoraLoaderBlockWeight.load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector)
return (model_lora, clip_lora, populated_vector)
return model_lora, clip_lora, populated_vector
class ApplyLBW:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model": ("MODEL", ),
"clip": ("CLIP", ),
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lbw_model": ("LBW_MODEL",),
}}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "doit"
CATEGORY = "InspirePack/LoraBlockWeight"
DESCRIPTION = "Apply LBW_MODEL to MODEL and CLIP"
@staticmethod
def doit(model, clip, strength_model, strength_clip, lbw_model):
block_weights = lbw_model['blocks']
muted_weights = lbw_model['muted']
new_modelpatcher = model.clone()
new_clip = clip.clone()
muted_weights = set(muted_weights)
for k, v in block_weights.items():
weights, ratio = v
if k in muted_weights:
pass
elif 'text' in k or 'encoder' in k:
new_clip.add_patches({k: weights}, strength_clip * ratio)
else:
new_modelpatcher.add_patches({k: weights}, strength_model * ratio)
return new_modelpatcher, new_clip
class XY_Capsule_LoraBlockWeight:
@@ -283,10 +570,10 @@ class XY_Capsule_LoraBlockWeight:
def set_result(self, image, latent):
if self.another_capsule is not None:
print(f"XY_Capsule_LoraBlockWeight: ({self.another_capsule.x, self.y}) is processed.")
logging.info(f"XY_Capsule_LoraBlockWeight: ({self.another_capsule.x, self.y}) is processed.")
self.storage[(self.another_capsule.x, self.y)] = image
else:
print(f"XY_Capsule_LoraBlockWeight: ({self.x, self.y}) is processed.")
logging.info(f"XY_Capsule_LoraBlockWeight: ({self.x, self.y}) is processed.")
def patch_model(self, model, clip):
lora_name, strength_model, strength_clip, inverse, block_vectors, seed, A, B, heatmap_palette, heatmap_alpha, heatmap_strength, xyplot_mode = self.params
@@ -328,6 +615,7 @@ class XY_Capsule_LoraBlockWeight:
else:
image = torch.abs(weighted_image - reference_image)
self.storage[(self.another_capsule.x, self.y)] = image
elif self.y == 3:
import matplotlib.cm as cm
# heatmap
@@ -336,7 +624,7 @@ class XY_Capsule_LoraBlockWeight:
if image == "fail":
image = utils.empty_pil_tensor(8,8)
latent = utils.empty_latent()
return (image, latent)
return image, latent
else:
image = image.clone()
@@ -368,7 +656,7 @@ class XY_Capsule_LoraBlockWeight:
image = heatmap_alpha * heatmap + (1 - heatmap_alpha) * image
latent = nodes.VAEEncode().encode(vae, image)[0]
return (image, latent)
return image, latent
def getLabel(self):
return self.label
@@ -485,7 +773,7 @@ class XYInput_LoraBlockWeight:
XY_Capsule_LoraBlockWeight(0, 2, '', 'diff', storage, common_params),
XY_Capsule_LoraBlockWeight(0, 3, '', 'heatmap', storage, common_params)]
return ((xy_type, x_values), (xy_type, y_values), )
return (xy_type, x_values), (xy_type, y_values),
class LoraBlockInfo:
@@ -531,12 +819,29 @@ class LoraBlockInfo:
output_blocks = []
output_blocks_map = {}
text_block_count = set()
text_blocks = []
text_blocks_map = {}
text_block_count1 = set()
text_blocks1 = []
text_blocks_map1 = {}
text_block_count2 = set()
text_blocks2 = []
text_blocks_map2 = {}
double_block_count = set()
double_blocks = []
double_blocks_map = {}
single_block_count = set()
single_blocks = []
single_blocks_map = {}
others = []
for k, v in loaded.items():
for key, v in loaded.items():
if isinstance(key, tuple):
k = key[0]
else:
k = key
k_unet = k[len("diffusion_model."):]
if k_unet.startswith("input_blocks."):
@@ -572,16 +877,49 @@ class LoraBlockInfo:
else:
output_blocks_map[k_unet_int] = [k_unet]
elif k_unet.startswith("_model.encoder.layers."):
k_unet_num = k_unet[len("_model.encoder.layers."):len("_model.encoder.layers.")+2]
elif k_unet.startswith("double_blocks."):
k_unet_num = k_unet[len("double_blocks."):len("double_blocks.") + 2]
k_unet_int = parse_unet_num(k_unet_num)
text_block_count.add(k_unet_int)
text_blocks.append(k_unet)
if k_unet_int in text_blocks_map:
text_blocks_map[k_unet_int].append(k_unet)
double_block_count.add(k_unet_int)
double_blocks.append(k_unet)
if k_unet_int in double_blocks_map:
double_blocks_map[k_unet_int].append(k_unet)
else:
text_blocks_map[k_unet_int] = [k_unet]
double_blocks_map[k_unet_int] = [k_unet]
elif k_unet.startswith("single_blocks."):
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.") + 2]
k_unet_int = parse_unet_num(k_unet_num)
single_block_count.add(k_unet_int)
single_blocks.append(k_unet)
if k_unet_int in single_blocks_map:
single_blocks_map[k_unet_int].append(k_unet)
else:
single_blocks_map[k_unet_int] = [k_unet]
elif k_unet.startswith("er.text_model.encoder.layers."):
k_unet_num = k_unet[len("er.text_model.encoder.layers."):len("er.text_model.encoder.layers.")+2]
k_unet_int = parse_unet_num(k_unet_num)
text_block_count1.add(k_unet_int)
text_blocks1.append(k_unet)
if k_unet_int in text_blocks_map1:
text_blocks_map1[k_unet_int].append(k_unet)
else:
text_blocks_map1[k_unet_int] = [k_unet]
elif k_unet.startswith("r.encoder.block."):
k_unet_num = k_unet[len("r.encoder.block."):len("r.encoder.block.")+2]
k_unet_int = parse_unet_num(k_unet_num)
text_block_count2.add(k_unet_int)
text_blocks2.append(k_unet)
if k_unet_int in text_blocks_map2:
text_blocks_map2[k_unet_int].append(k_unet)
else:
text_blocks_map2[k_unet_int] = [k_unet]
else:
others.append(k_unet)
@@ -591,29 +929,49 @@ class LoraBlockInfo:
input_blocks = sorted(input_blocks)
middle_blocks = sorted(middle_blocks)
output_blocks = sorted(output_blocks)
double_blocks = sorted(double_blocks)
single_blocks = sorted(single_blocks)
others = sorted(others)
text += f"\n-------[Input blocks] ({len(input_block_count)}, Subs={len(input_blocks)})-------\n"
input_keys = sorted(input_blocks_map.keys())
for x in input_keys:
text += f" IN{x}: {len(input_blocks_map[x])}\n"
if len(input_block_count) > 0:
text += f"\n-------[Input blocks] ({len(input_block_count)}, Subs={len(input_blocks)})-------\n"
input_keys = sorted(input_blocks_map.keys())
for x in input_keys:
text += f" IN{x}: {len(input_blocks_map[x])}\n"
text += f"\n-------[Middle blocks] ({len(middle_block_count)}, Subs={len(middle_blocks)})-------\n"
middle_keys = sorted(middle_blocks_map.keys())
for x in middle_keys:
text += f" MID{x}: {len(middle_blocks_map[x])}\n"
if len(middle_block_count) > 0:
text += f"\n-------[Middle blocks] ({len(middle_block_count)}, Subs={len(middle_blocks)})-------\n"
middle_keys = sorted(middle_blocks_map.keys())
for x in middle_keys:
text += f" MID{x}: {len(middle_blocks_map[x])}\n"
text += f"\n-------[Output blocks] ({len(output_block_count)}, Subs={len(output_blocks)})-------\n"
output_keys = sorted(output_blocks_map.keys())
for x in output_keys:
text += f" OUT{x}: {len(output_blocks_map[x])}\n"
if len(output_block_count) > 0:
text += f"\n-------[Output blocks] ({len(output_block_count)}, Subs={len(output_blocks)})-------\n"
output_keys = sorted(output_blocks_map.keys())
for x in output_keys:
text += f" OUT{x}: {len(output_blocks_map[x])}\n"
text += f"\n-------[Text blocks] ({len(text_block_count)}, Subs={len(text_blocks)})-------\n"
text_keys = sorted(text_blocks_map.keys())
for x in text_keys:
text += f" CLIP{x}: {len(text_blocks_map[x])}\n"
if len(double_block_count) > 0:
text += f"\n-------[Double blocks(MMDiT)] ({len(double_block_count)}, Subs={len(double_blocks)})-------\n"
double_keys = sorted(double_blocks_map.keys())
for x in double_keys:
text += f" DOUBLE{x}: {len(double_blocks_map[x])}\n"
if len(single_block_count) > 0:
text += f"\n-------[Single blocks(DiT)] ({len(single_block_count)}, Subs={len(single_blocks)})-------\n"
single_keys = sorted(single_blocks_map.keys())
for x in single_keys:
text += f" SINGLE{x}: {len(single_blocks_map[x])}\n"
text += f"\n-------[Base blocks] ({len(text_block_count1) + len(text_block_count2) + len(others)}, Subs={len(text_blocks1) + len(text_blocks2) + len(others)})-------\n"
text_keys1 = sorted(text_blocks_map1.keys())
for x in text_keys1:
text += f" TXT_ENC{x}: {len(text_blocks_map1[x])}\n"
text_keys2 = sorted(text_blocks_map2.keys())
for x in text_keys2:
text += f" TXT_ENC{x} [B]: {len(text_blocks_map2[x])}\n"
text += f"\n-------[Base blocks] ({len(others)})-------\n"
for x in others:
text += f" {x}\n"
@@ -629,13 +987,162 @@ class LoraBlockInfo:
return {}
class LoadLBW:
@classmethod
def INPUT_TYPES(s):
files = folder_paths.get_filename_list('lbw_models')
return {"required": {
"lbw_model": [sorted(files), ]},
}
RETURN_TYPES = ("LBW_MODEL",)
FUNCTION = "doit"
CATEGORY = "InspirePack/LoraBlockWeight"
DESCRIPTION = "Load LBW_MODEL from .lbw.safetensors file"
@staticmethod
def decode_dict(encoded_dict, tensor_dict):
original_dict = {}
def decode_value(value):
if isinstance(value, str) and value.startswith('t') and value[1:].isdigit():
return tensor_dict[value]
return value
for k, tuple_value in encoded_dict.items():
decoded_tuple = tuple(decode_value(v) for v in tuple_value[0][1])
key = ast.literal_eval(k) if isinstance(k, str) and (k.startswith('(') or k.startswith('[')) else k
original_dict[key] = ((tuple_value[0][0], decoded_tuple), tuple_value[1])
return original_dict
@staticmethod
def load(file):
tensor_dict = comfy.utils.load_torch_file(file)
with safe_open(file, framework="pt") as f:
metadata = f.metadata()
encoded_dict = json.loads(metadata.get('blocks', '{}'))
muted_blocks = ast.literal_eval(metadata.get('muted_blocks', '[]'))
decoded_dict = LoadLBW.decode_dict(encoded_dict, tensor_dict)
lbw_model = {
'blocks': decoded_dict,
'muted': muted_blocks
}
return lbw_model, metadata
def doit(self, lbw_model):
lbw_path = folder_paths.get_full_path("lbw_models", lbw_model)
lbw_model, _ = LoadLBW.load(lbw_path)
return (lbw_model,)
class SaveLBW:
def __init__(self):
self.output_dir = folder_paths.get_folder_paths('lbw_models')[-1]
@classmethod
def INPUT_TYPES(s):
return {"required": { "lbw_model": ("LBW_MODEL", ),
"filename_prefix": ("STRING", {"default": "ComfyUI"}) },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "doit"
OUTPUT_NODE = True
CATEGORY = "InspirePack/LoraBlockWeight"
DESCRIPTION = "Save LBW_MODEL as a .lbw.safetensors file"
@staticmethod
def encode_dict(original_dict):
tensor_dict = {}
encoded_dict = {}
counter = 0
def generate_unique_id():
nonlocal counter
counter += 1
return f"t{counter}"
def encode_value(value):
if isinstance(value, torch.Tensor):
unique_id = generate_unique_id()
tensor_dict[unique_id] = value
return unique_id
return value
for k, tuple_value in original_dict.items():
encoded_tuple = tuple(encode_value(v) for v in tuple_value[0][1])
encoded_dict[str(k)] = (tuple_value[0][0], encoded_tuple), tuple_value[1]
return encoded_dict, tensor_dict
@staticmethod
def save(lbw_model, file, metadata):
metadata['format'] = 'Inspire LBW 1.0'
weighted_blocks = lbw_model['blocks']
metadata['muted_blocks'] = str(lbw_model['muted'])
encoded_dict, tensor_dict = SaveLBW.encode_dict(weighted_blocks)
metadata['blocks'] = json.dumps(encoded_dict)
comfy.utils.save_torch_file(tensor_dict, file, metadata=metadata)
def doit(self, lbw_model, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
# support save metadata for lbw sharing
prompt_info = ""
if prompt is not None:
prompt_info = json.dumps(prompt)
metadata = {}
if not args.disable_metadata:
metadata = {"prompt": prompt_info}
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])
file = f"{filename}_{counter:05}_.lbw.safetensors"
results = list()
results.append({
"filename": file,
"subfolder": subfolder,
"type": "output"
})
file = os.path.join(full_output_folder, file)
SaveLBW.save(lbw_model, file, metadata)
return {}
NODE_CLASS_MAPPINGS = {
"XY Input: Lora Block Weight //Inspire": XYInput_LoraBlockWeight,
"LoraLoaderBlockWeight //Inspire": LoraLoaderBlockWeight,
"LoraBlockInfo //Inspire": LoraBlockInfo,
"MakeLBW //Inspire": MakeLBW,
"ApplyLBW //Inspire": ApplyLBW,
"SaveLBW //Inspire": SaveLBW,
"LoadLBW //Inspire": LoadLBW,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"XY Input: Lora Block Weight //Inspire": "XY Input: Lora Block Weight",
"LoraLoaderBlockWeight //Inspire": "Lora Loader (Block Weight)",
"LoraBlockInfo //Inspire": "Lora Block Info",
"XY Input: Lora Block Weight //Inspire": "XY Input: LoRA Block Weight",
"LoraLoaderBlockWeight //Inspire": "LoRA Loader (Block Weight)",
"LoraBlockInfo //Inspire": "LoRA Block Info",
"MakeLBW //Inspire": "Make LoRA Block Weight",
"ApplyLBW //Inspire": "Apply LoRA Block Weight",
"SaveLBW //Inspire": "Save LoRA Block Weight",
"LoadLBW //Inspire": "Load LoRA Block Weight",
}
+19 -9
View File
@@ -5,6 +5,8 @@ import server
from .libs import utils
from . import backend_support
from comfy import sdxl_clip
import logging
model_preset = {
# base
@@ -19,6 +21,7 @@ model_preset = {
"SDXL ViT-H": ("ip-adapter_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
"SDXL Plus ViT-H": ("ip-adapter-plus_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
"SDXL Plus Face ViT-H": ("ip-adapter-plus-face_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
"Kolors Plus": ("Kolors-IP-Adapter-Plus", "clip-vit-large-patch14-336", None, False),
# faceid
"SD1.5 FaceID": ("ip-adapter-faceid_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", "ip-adapter-faceid_sd15_lora", True),
@@ -29,6 +32,7 @@ model_preset = {
"SDXL FaceID": ("ip-adapter-faceid_sdxl", "CLIP-ViT-H-14-laion2B-s32B-b79K", "ip-adapter-faceid_sdxl_lora", True),
"SDXL FaceID Portrait": ("ip-adapter-faceid-portrait_sdxl", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
"SDXL FaceID Portrait unnorm": ("ip-adapter-faceid-portrait_sdxl_unnorm", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
"Kolors FaceID Plus": ("Kolors-IP-Adapter-FaceID-Plus", "clip-vit-large-patch14-336", None, True),
# composition
"SD1.5 Plus Composition": ("ip-adapter_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
@@ -46,7 +50,7 @@ def lookup_model(model_dir, name):
if len(resolved_name) > 0:
return resolved_name[0], "OK"
else:
print(f"[ERROR] IPAdapterModelHelper: The `{name}` model file does not exist in `{model_dir}` model dir.")
logging.error(f"[Inspire Pack] IPAdapterModelHelper: The `{name}` model file does not exist in `{model_dir}` model dir.")
return None, "FAIL"
@@ -56,13 +60,16 @@ class IPAdapterModelHelper:
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"preset": (list(model_preset.keys()),),
"lora_strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
"lora_strength_clip": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
"insightface_provider": (["CPU", "CUDA", "ROCM"], ),
"cache_mode": (["insightface only", "clip_vision only", "all", "none"], {"default": "insightface only"}),
},
"optional": {
"clip": ("CLIP",),
"insightface_model_name": (['buffalo_l', 'antelopev2'],),
},
"hidden": {"unique_id": "UNIQUE_ID"}
}
@@ -72,14 +79,17 @@ class IPAdapterModelHelper:
CATEGORY = "InspirePack/models"
def doit(self, model, clip, preset, lora_strength_model, lora_strength_clip, insightface_provider, cache_mode="none", unique_id=None):
def doit(self, model, preset, lora_strength_model, lora_strength_clip, insightface_provider, clip=None, cache_mode="none", unique_id=None, insightface_model_name='buffalo_l'):
if 'IPAdapter' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/cubiq/ComfyUI_IPAdapter_plus',
"To use 'IPAdapterModelHelper' node, 'ComfyUI IPAdapter Plus' extension is required.")
raise Exception(f"[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
raise Exception("[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
is_sdxl_preset = 'SDXL' in preset
is_sdxl_model = isinstance(clip.tokenizer, sdxl_clip.SDXLTokenizer)
if clip is not None:
is_sdxl_model = isinstance(clip.tokenizer, sdxl_clip.SDXLTokenizer)
else:
is_sdxl_model = False
if is_sdxl_preset != is_sdxl_model:
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 1, "label": "IPADAPTER (fail)"})
@@ -87,7 +97,7 @@ class IPAdapterModelHelper:
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 3, "label": "INSIGHTFACE (fail)"})
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 4, "label": "MODEL (fail)"})
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 5, "label": "CLIP (fail)"})
print(f"[ERROR] IPAdapterModelHelper: You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
logging.error("[Inspire Pack] IPAdapterModelHelper: You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
raise Exception("[ERROR] You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
ipadapter, clipvision, lora, is_insightface = model_preset[preset]
@@ -146,18 +156,18 @@ class IPAdapterModelHelper:
if 'IPAdapterInsightFaceLoader' in nodes.NODE_CLASS_MAPPINGS:
insight_face_loader = nodes.NODE_CLASS_MAPPINGS['IPAdapterInsightFaceLoader']().load_insightface
else:
print("'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
logging.warning("'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
insight_face_loader = None
icache_key = ""
if is_insightface:
if insight_face_loader is None:
raise Exception(f"[ERROR] 'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
raise Exception("[ERROR] 'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
if cache_mode in ["insightface only", "all"]:
icache_key = 'insightface-' + insightface_provider
if icache_key not in backend_support.cache:
backend_support.update_cache(icache_key, "insightface", (False, insight_face_loader(insightface_provider)[0]))
backend_support.update_cache(icache_key, "insightface", (False, insight_face_loader(provider=insightface_provider, model_name=insightface_model_name)[0]))
_, (_, insightface) = backend_support.cache[icache_key]
else:
insightface = insight_face_loader(insightface_provider)[0]
+344 -105
View File
@@ -12,20 +12,24 @@ import folder_paths
import comfy
import traceback
import random
import hashlib
from server import PromptServer
from .libs import utils, common
from .backend_support import CheckpointLoaderSimpleShared
import logging
model_path = folder_paths.models_dir
utils.add_folder_path_and_extensions("inspire_prompts", [os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "prompts"))], {'.txt'})
prompt_builder_preset = {}
resource_path = os.path.join(os.path.dirname(__file__), "..", "resources")
resource_path = os.path.abspath(resource_path)
prompts_path = os.path.join(os.path.dirname(__file__), "..", "prompts")
prompts_path = os.path.abspath(prompts_path)
try:
pb_yaml_path = os.path.join(resource_path, 'prompt-builder.yaml')
@@ -36,130 +40,259 @@ try:
with open(pb_yaml_path, 'r', encoding="utf-8") as f:
prompt_builder_preset = yaml.load(f, Loader=yaml.FullLoader)
except Exception as e:
print(f"[Inspire Pack] Failed to load 'prompt-builder.yaml'")
except Exception as e: # noqa: F841
logging.error("[Inspire Pack] Failed to load 'prompt-builder.yaml'\nNOTE: Only files with UTF-8 encoding are supported.")
class LoadPromptsFromDir:
@classmethod
def INPUT_TYPES(cls):
global prompts_path
try:
prompt_dirs = [d for d in os.listdir(prompts_path) if os.path.isdir(os.path.join(prompts_path, d))]
prompt_dirs = []
for x in folder_paths.get_folder_paths('inspire_prompts'):
for d in os.listdir(x):
if os.path.isdir(os.path.join(x, d)):
prompt_dirs.append(d)
except Exception:
prompt_dirs = []
return {"required": {"prompt_dir": (prompt_dirs,)}}
return {"required": {
"prompt_dir": (prompt_dirs,)
},
"optional": {
"reload": ("BOOLEAN", { "default": False, "label_on": "if file changed", "label_off": "if value changed"}),
"load_cap": ("INT", {"default": 0, "min": 0, "step": 1, "advanced": True, "tooltip": "The amount of prompts to load at once:\n0: Load all\n1 or higher: Load a specified number"}),
"start_index": ("INT", {"default": 0, "min": -1, "step": 1, "max": 0xffffffffffffffff, "advanced": True, "tooltip": "Starting index for loading prompts:\n-1: The last prompt\n0 or higher: Load from the specified index"}),
}
}
RETURN_TYPES = ("ZIPPED_PROMPT",)
OUTPUT_IS_LIST = (True,)
RETURN_TYPES = ("ZIPPED_PROMPT", "INT", "INT")
RETURN_NAMES = ("zipped_prompt", "count", "remaining_count")
OUTPUT_IS_LIST = (True, False, False)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
def doit(self, prompt_dir):
global prompts_path
prompt_dir = os.path.join(prompts_path, prompt_dir)
files = [f for f in os.listdir(prompt_dir) if f.endswith(".txt")]
files.sort()
@staticmethod
def IS_CHANGED(prompt_dir, reload=False, load_cap=0, start_index=-1):
if not reload:
return prompt_dir, load_cap, start_index
else:
candidates = []
for d in folder_paths.get_folder_paths('inspire_prompts'):
candidates.append(os.path.join(d, prompt_dir))
prompt_files = []
for x in candidates:
for root, dirs, files in os.walk(x):
for file in files:
if file.endswith(".txt"):
prompt_files.append(os.path.join(root, file))
prompt_files.sort()
md5 = hashlib.md5()
for file_name in prompt_files:
md5.update(file_name.encode('utf-8'))
with open(folder_paths.get_full_path('inspire_prompts', file_name), 'rb') as f:
while True:
chunk = f.read(4096)
if not chunk:
break
md5.update(chunk)
return md5.hexdigest(), load_cap, start_index
@staticmethod
def doit(prompt_dir, reload=False, load_cap=0, start_index=-1):
candidates = []
for d in folder_paths.get_folder_paths('inspire_prompts'):
candidates.append(os.path.join(d, prompt_dir))
prompt_files = []
for x in candidates:
for root, dirs, files in os.walk(x):
for file in files:
if file.endswith(".txt"):
prompt_files.append(os.path.join(root, file))
prompt_files.sort()
prompts = []
for file_name in files:
print(f"file_name: {file_name}")
for file_name in prompt_files:
logging.info(f"file_name: {file_name}")
try:
with open(os.path.join(prompt_dir, file_name), "r", encoding="utf-8") as file:
with open(file_name, "r", encoding="utf-8") as file:
prompt_data = file.read()
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
for prompt in prompt_list:
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
pattern = r"^(?:(?:positive:(?P<positive>.*?)|negative:(?P<negative>.*?)|name:(?P<name>.*?))\n*)+$"
matches = re.search(pattern, prompt, re.DOTALL)
if matches:
positive_text = matches.group(1).strip()
negative_text = matches.group(2).strip()
result_tuple = (positive_text, negative_text, file_name)
positive_text = matches.group('positive').strip()
negative_text = matches.group('negative').strip()
name_text = matches.group('name').strip() if matches.group('name') else file_name
result_tuple = (positive_text, negative_text, name_text)
prompts.append(result_tuple)
else:
print(f"[WARN] LoadPromptsFromDir: invalid prompt format in '{file_name}'")
logging.warning(f"[Inspire Pack] LoadPromptsFromDir: invalid prompt format in '{file_name}'")
except Exception as e:
print(f"[ERROR] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}")
logging.error(f"[Inspire Pack] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
return (prompts, )
# slicing [start_index ~ start_index + load_cap]
total_prompts = len(prompts)
prompts = prompts[start_index:]
remaining_count = False
if load_cap > 0:
remaining_count = max(0, len(prompts) - load_cap)
prompts = prompts[:load_cap]
return prompts, total_prompts, remaining_count
class LoadPromptsFromFile:
@classmethod
def INPUT_TYPES(cls):
global prompts_path
prompt_files = []
try:
prompt_files = []
for root, dirs, files in os.walk(prompts_path):
for file in files:
if file.endswith(".txt"):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, prompts_path)
prompt_files.append(rel_path)
prompts_paths = folder_paths.get_folder_paths('inspire_prompts')
for prompts_path in prompts_paths:
for root, dirs, files in os.walk(prompts_path):
for file in files:
if file.endswith(".txt"):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, prompts_path)
prompt_files.append(rel_path)
except Exception:
prompt_files = []
return {"required": {"prompt_file": (prompt_files,)}}
return {"required": {
"prompt_file": (prompt_files,)
},
"optional": {
"text_data_opt": ("STRING", {"defaultInput": True}),
"reload": ("BOOLEAN", {"default": False, "label_on": "if file changed", "label_off": "if value changed"}),
"load_cap": ("INT", {"default": 0, "min": 0, "step": 1, "advanced": True, "tooltip": "The amount of prompts to load at once:\n0: Load all\n1 or higher: Load a specified number"}),
"start_index": ("INT", {"default": 0, "min": -1, "max": 0xffffffffffffffff, "step": 1, "advanced": True, "tooltip": "Starting index for loading prompts:\n-1: The last prompt\n0 or higher: Load from the specified index"}),
}
}
RETURN_TYPES = ("ZIPPED_PROMPT",)
OUTPUT_IS_LIST = (True,)
RETURN_TYPES = ("ZIPPED_PROMPT", "INT", "INT")
RETURN_NAMES = ("zipped_prompt", "count", "remaining_count")
OUTPUT_IS_LIST = (True, False, False)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
def doit(self, prompt_file):
prompt_path = os.path.join(prompts_path, prompt_file)
@staticmethod
def IS_CHANGED(prompt_file, text_data_opt=None, reload=False, load_cap=0, start_index=-1):
md5 = hashlib.md5()
if text_data_opt is not None:
md5.update(text_data_opt)
return md5.hexdigest(), load_cap, start_index
elif not reload:
return prompt_file, load_cap, start_index
else:
matched_path = None
for x in folder_paths.get_folder_paths('inspire_prompts'):
matched_path = os.path.join(x, prompt_file)
if not os.path.exists(matched_path):
matched_path = None
else:
break
if matched_path is None:
return float('NaN')
with open(matched_path, 'rb') as f:
while True:
chunk = f.read(4096)
if not chunk:
break
md5.update(chunk)
return md5.hexdigest(), load_cap, start_index
@staticmethod
def doit(prompt_file, text_data_opt=None, reload=False, load_cap=0, start_index=-1):
matched_path = None
for d in folder_paths.get_folder_paths('inspire_prompts'):
matched_path = os.path.join(d, prompt_file)
if os.path.exists(matched_path):
break
else:
matched_path = None
if matched_path:
logging.info(f"[Inspire Pack] LoadPromptsFromFile: file found '{prompt_file}'")
else:
logging.warning(f"[Inspire Pack] LoadPromptsFromFile: file not found '{prompt_file}'")
prompts = []
try:
with open(prompt_path, "r", encoding="utf-8") as file:
prompt_data = file.read()
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
if not text_data_opt:
with open(matched_path, "r", encoding="utf-8") as file:
prompt_data = file.read()
else:
prompt_data = text_data_opt
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
for prompt in prompt_list:
matches = re.search(pattern, prompt, re.DOTALL)
pattern = r"^(?:(?:positive:(?P<positive>.*?)|negative:(?P<negative>.*?)|name:(?P<name>.*?))\n*)+$"
if matches:
positive_text = matches.group(1).strip()
negative_text = matches.group(2).strip()
result_tuple = (positive_text, negative_text, prompt_file)
prompts.append(result_tuple)
else:
print(f"[WARN] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'")
for p in prompt_list:
matches = re.search(pattern, p, re.DOTALL)
if matches:
positive_text = matches.group('positive').strip()
negative_text = matches.group('negative').strip()
name_text = matches.group('name').strip() if matches.group('name') else prompt_file
result_tuple = (positive_text, negative_text, name_text)
prompts.append(result_tuple)
else:
logging.warning(f"[Inspire Pack] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'")
except Exception as e:
print(f"[ERROR] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
logging.error(f"[Inspire Pack] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
return (prompts, )
# slicing [start_index ~ start_index + load_cap]
total_prompts = len(prompts)
prompts = prompts[start_index:]
remaining_count = 0
if load_cap > 0:
remaining_count = max(0, len(prompts) - load_cap)
prompts = prompts[:load_cap]
return prompts, total_prompts, remaining_count
class LoadSinglePromptFromFile:
@classmethod
def INPUT_TYPES(cls):
global prompts_path
prompt_files = []
try:
prompt_files = []
for root, dirs, files in os.walk(prompts_path):
for file in files:
if file.endswith(".txt"):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, prompts_path)
prompt_files.append(rel_path)
prompts_paths = folder_paths.get_folder_paths('inspire_prompts')
for prompts_path in prompts_paths:
for root, dirs, files in os.walk(prompts_path):
for file in files:
if file.endswith(".txt"):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, prompts_path)
prompt_files.append(rel_path)
except Exception:
prompt_files = []
return {"required": {
"prompt_file": (prompt_files,),
"index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
"prompt_file": (prompt_files,),
"index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {"text_data_opt": ("STRING", {"defaultInput": True})}
}
RETURN_TYPES = ("ZIPPED_PROMPT",)
OUTPUT_IS_LIST = (True,)
@@ -168,31 +301,49 @@ class LoadSinglePromptFromFile:
CATEGORY = "InspirePack/Prompt"
def doit(self, prompt_file, index):
prompt_path = os.path.join(prompts_path, prompt_file)
@staticmethod
def doit(prompt_file, index, text_data_opt=None):
prompt_path = None
prompts_paths = folder_paths.get_folder_paths('inspire_prompts')
for d in prompts_paths:
prompt_path = os.path.join(d, prompt_file)
if os.path.exists(prompt_path):
break
else:
prompt_path = None
if prompt_path:
logging.info(f"[Inspire Pack] LoadSinglePromptFromFile: file found '{prompt_file}'")
else:
logging.warning(f"[Inspire Pack] LoadSinglePromptFromFile: file not found '{prompt_file}'")
prompts = []
try:
with open(prompt_path, "r", encoding="utf-8") as file:
prompt_data = file.read()
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
try:
prompt = prompt_list[index]
except Exception:
prompt = prompt_list[-1]
if not text_data_opt:
with open(prompt_path, "r", encoding="utf-8") as file:
prompt_data = file.read()
else:
prompt_data = text_data_opt
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
matches = re.search(pattern, prompt, re.DOTALL)
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
try:
prompt = prompt_list[index]
except Exception:
prompt = prompt_list[-1]
if matches:
positive_text = matches.group(1).strip()
negative_text = matches.group(2).strip()
result_tuple = (positive_text, negative_text, prompt_file)
prompts.append(result_tuple)
else:
print(f"[WARN] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'")
pattern = r"^(?:(?:positive:(?P<positive>.*?)|negative:(?P<negative>.*?)|name:(?P<name>.*?))\n*)+$"
matches = re.search(pattern, prompt, re.DOTALL)
if matches:
positive_text = matches.group('positive').strip()
negative_text = matches.group('negative').strip()
name_text = matches.group('name').strip() if matches.group('name') else prompt_file
result_tuple = (positive_text, negative_text, name_text)
prompts.append(result_tuple)
else:
logging.warning(f"[Inspire Pack] LoadSinglePromptFromFile: invalid prompt format in '{prompt_file}'")
except Exception as e:
print(f"[ERROR] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
logging.error(f"[Inspire Pack] LoadSinglePromptFromFile: an error occurred while processing '{prompt_file}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
return (prompts, )
@@ -235,9 +386,8 @@ class ZipPrompt:
return ((positive, negative, name_opt), )
prompt_blacklist = set([
'filename_prefix'
])
prompt_blacklist = set(['filename_prefix'])
class PromptExtractor:
@classmethod
@@ -414,7 +564,7 @@ class BNK_EncoderWrapper:
if 'BNK_CLIPTextEncodeAdvanced' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb',
"To use 'WildcardEncodeInspire' node, 'ComfyUI_ADV_CLIP_emb' extension is required.")
raise Exception(f"[ERROR] To use WildcardEncodeInspire, you need to install 'Advanced CLIP Text Encode'")
raise Exception("[ERROR] To use WildcardEncodeInspire, you need to install 'Advanced CLIP Text Encode'")
return nodes.NODE_CLASS_MAPPINGS['BNK_CLIPTextEncodeAdvanced']().encode(clip, text, self.token_normalization, self.weight_interpretation)
@@ -428,7 +578,13 @@ class WildcardEncodeInspire:
"weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"], {'default': 'comfy++'}),
"wildcard_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Wildcard Prompt (User Input)'}),
"populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Populated Prompt (Will be generated automatically)'}),
"mode": ("BOOLEAN", {"default": True, "label_on": "Populate", "label_off": "Fixed"}),
"mode": (["populate", "fixed", "reproduce"], {"default": "populate", "tooltip":
"populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\n"
"fixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode.\n"
"reproduce: This mode operates as 'fixed' mode only once for reproduction, and then it switches to 'populate' mode."
}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"), ),
"Select to add Wildcard": (["Select the Wildcard to add to the text"],),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
@@ -449,10 +605,11 @@ class WildcardEncodeInspire:
if 'ImpactWildcardEncode' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
"To use 'Wildcard Encode (Inspire)' node, 'Impact Pack' extension is required.")
raise Exception(f"[ERROR] To use 'Wildcard Encode (Inspire)', you need to install 'Impact Pack'")
raise Exception("[ERROR] To use 'Wildcard Encode (Inspire)', you need to install 'Impact Pack'")
model, clip, conditioning = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardEncode'].process_with_loras(wildcard_opt=populated, model=kwargs['model'], clip=kwargs['clip'], clip_encoder=clip_encoder)
return (model, clip, conditioning, populated)
processed = []
model, clip, conditioning = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardEncode'].process_with_loras(wildcard_opt=populated, model=kwargs['model'], clip=kwargs['clip'], seed=kwargs['seed'], clip_encoder=clip_encoder, processed=processed)
return (model, clip, conditioning, processed[0])
class MakeBasicPipe:
@@ -468,7 +625,11 @@ class MakeBasicPipe:
"Add selection to": ("BOOLEAN", {"default": True, "label_on": "Positive", "label_off": "Negative"}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
"Select to add Wildcard": (["Select the Wildcard to add to the text"],),
"wildcard_mode": ("BOOLEAN", {"default": True, "label_on": "Populate", "label_off": "Fixed"}),
"wildcard_mode": (["populate", "fixed", "reproduce"], {"default": "populate", "tooltip":
"populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\n"
"fixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode.\n"
"reproduce: This mode operates as 'fixed' mode only once for reproduction, and then it switches to 'populate' mode."
}),
"positive_populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Populated Positive Prompt (Will be generated automatically)'}),
"negative_populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Populated Negative Prompt (Will be generated automatically)'}),
@@ -477,7 +638,6 @@ class MakeBasicPipe:
"weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"], {'default': 'comfy++'}),
"stop_at_clip_layer": ("INT", {"default": -2, "min": -24, "max": -1, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
@@ -500,7 +660,7 @@ class MakeBasicPipe:
if 'ImpactWildcardEncode' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
"To use 'Make Basic Pipe (Inspire)' node, 'Impact Pack' extension is required.")
raise Exception(f"[ERROR] To use 'Make Basic Pipe (Inspire)', you need to install 'Impact Pack'")
raise Exception("[ERROR] To use 'Make Basic Pipe (Inspire)', you need to install 'Impact Pack'")
model, clip, vae, key = CheckpointLoaderSimpleShared().doit(ckpt_name=kwargs['ckpt_name'], key_opt=kwargs['ckpt_key_opt'])
clip = nodes.CLIPSetLastLayer().set_last_layer(clip, kwargs['stop_at_clip_layer'])[0]
@@ -549,16 +709,21 @@ class SeedExplorer:
"additional_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"initial_batch_seed_mode": (["incremental", "comfy"],),
}
},
"optional":
{
"variation_method": (["linear", "slerp"],),
"model": ("MODEL",),
}
}
RETURN_TYPES = ("NOISE",)
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
@staticmethod
def apply_variation(start_noise, seed_items, noise_device, mask=None):
def apply_variation(start_noise, seed_items, noise_device, mask=None, variation_method='linear'):
noise = start_noise
for x in seed_items:
if isinstance(x, str):
@@ -571,15 +736,20 @@ class SeedExplorer:
variation_seed = int(item[0])
variation_strength = float(item[1])
noise = utils.apply_variation_noise(noise, noise_device, variation_seed, variation_strength, mask=mask)
noise = utils.apply_variation_noise(noise, noise_device, variation_seed, variation_strength, mask=mask, variation_method=variation_method)
except Exception:
print(f"[ERROR] IGNORED: SeedExplorer failed to processing '{x}'")
logging.error(f"[Inspire Pack] IGNORED: SeedExplorer failed to processing '{x}'")
traceback.print_exc()
return noise
def doit(self, latent, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode,
initial_batch_seed_mode):
@staticmethod
def doit(latent, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode,
initial_batch_seed_mode, variation_method='linear', model=None):
latent_image = latent["samples"]
if hasattr(comfy.sample, 'fix_empty_latent_channels') and model is not None:
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
device = comfy.model_management.get_torch_device()
noise_device = "cpu" if noise_mode == "CPU" else device
@@ -603,13 +773,13 @@ class SeedExplorer:
noise = utils.prepare_noise(latent_image, hd_seed, None, noise_device, initial_batch_seed_mode)
noise = noise.to(device)
noise = SeedExplorer.apply_variation(noise, tl, noise_device)
noise = SeedExplorer.apply_variation(noise, tl, noise_device, variation_method=variation_method)
noise = noise.cpu()
return (noise,)
except Exception:
print(f"[ERROR] IGNORED: SeedExplorer failed")
logging.error("[Inspire Pack] IGNORED: SeedExplorer failed")
traceback.print_exc()
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout,
@@ -617,6 +787,72 @@ class SeedExplorer:
return (noise,)
class CompositeNoise:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"destination": ("NOISE_IMAGE",),
"source": ("NOISE_IMAGE",),
"mode": (["center", "left-top", "right-top", "left-bottom", "right-bottom", "xy"], ),
"x": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 8}),
"y": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 8}),
},
}
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
def doit(self, destination, source, mode, x, y):
new_tensor = destination.clone()
if mode == 'center':
y1 = (new_tensor.size(2) - source.size(2)) // 2
x1 = (new_tensor.size(3) - source.size(3)) // 2
elif mode == 'left-top':
y1 = 0
x1 = 0
elif mode == 'right-top':
y1 = 0
x1 = new_tensor.size(2) - source.size(2)
elif mode == 'left-bottom':
y1 = new_tensor.size(3) - source.size(3)
x1 = 0
elif mode == 'right-bottom':
y1 = new_tensor.size(3) - source.size(3)
x1 = new_tensor.size(2) - source.size(2)
else: # mode == 'xy':
x1 = max(0, x)
y1 = max(0, y)
# raw coordinates
y2 = y1 + source.size(2)
x2 = x1 + source.size(3)
# bounding for destination
top = max(0, y1)
left = max(0, x1)
bottom = min(new_tensor.size(2), y2)
right = min(new_tensor.size(3), x2)
# bounding for source
left_gap = left - x1
top_gap = top - y1
width = right - left
height = bottom - top
height = min(height, y1 + source.size(2) - top)
width = min(width, x1 + source.size(3) - left)
# composite
new_tensor[:, :, top:top + height, left:left + width] = source[:, :, top_gap:top_gap + height, left_gap:left_gap + width]
return (new_tensor,)
list_counter_map = {}
@@ -768,7 +1004,9 @@ NODE_CLASS_MAPPINGS = {
"MakeBasicPipe //Inspire": MakeBasicPipe,
"RemoveControlNet //Inspire": RemoveControlNet,
"RemoveControlNetFromRegionalPrompts //Inspire": RemoveControlNetFromRegionalPrompts,
"CompositeNoise //Inspire": CompositeNoise
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadPromptsFromDir //Inspire": "Load Prompts From Dir (Inspire)",
"LoadPromptsFromFile //Inspire": "Load Prompts From File (Inspire)",
@@ -787,5 +1025,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RandomGeneratorForList //Inspire": "Random Generator for List (Inspire)",
"MakeBasicPipe //Inspire": "Make Basic Pipe (Inspire)",
"RemoveControlNet //Inspire": "Remove ControlNet (Inspire)",
"RemoveControlNetFromRegionalPrompts //Inspire": "Remove ControlNet [RegionalPrompts] (Inspire)"
"RemoveControlNetFromRegionalPrompts //Inspire": "Remove ControlNet [RegionalPrompts] (Inspire)",
"CompositeNoise //Inspire": "Composite Noise (Inspire)"
}
+195 -37
View File
@@ -3,9 +3,13 @@ import traceback
import comfy
import nodes
import torch
import re
import webcolors
from . import prompt_support
from .libs import utils, common
import logging
class RegionalPromptSimple:
@classmethod
@@ -21,6 +25,12 @@ class RegionalPromptSimple:
"controlnet_in_pipe": ("BOOLEAN", {"default": False, "label_on": "Keep", "label_off": "Override"}),
"sigma_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
},
"optional": {
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"variation_method": (["linear", "slerp"],),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
RETURN_TYPES = ("REGIONAL_PROMPTS", )
@@ -28,11 +38,13 @@ class RegionalPromptSimple:
CATEGORY = "InspirePack/Regional"
def doit(self, basic_pipe, mask, cfg, sampler_name, scheduler, wildcard_prompt, controlnet_in_pipe=False, sigma_factor=1.0):
@staticmethod
def doit(basic_pipe, mask, cfg, sampler_name, scheduler, wildcard_prompt,
controlnet_in_pipe=False, sigma_factor=1.0, variation_seed=0, variation_strength=0.0, variation_method='linear', scheduler_func_opt=None):
if 'RegionalPrompt' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
"To use 'RegionalPromptSimple' node, 'Impact Pack' extension is required.")
raise Exception(f"[ERROR] To use RegionalPromptSimple, you need to install 'ComfyUI-Impact-Pack'")
raise Exception("[ERROR] To use RegionalPromptSimple, you need to install 'ComfyUI-Impact-Pack'")
model, clip, vae, positive, negative = basic_pipe
@@ -41,7 +53,7 @@ class RegionalPromptSimple:
rp = nodes.NODE_CLASS_MAPPINGS['RegionalPrompt']()
if wildcard_prompt != "":
model, clip, new_positive, _ = iwe.doit(model=model, clip=clip, populated_text=wildcard_prompt)
model, clip, new_positive, _ = iwe.doit(model=model, clip=clip, populated_text=wildcard_prompt, seed=None)
if controlnet_in_pipe:
prev_cnet = None
@@ -60,20 +72,24 @@ class RegionalPromptSimple:
basic_pipe = model, clip, vae, new_positive, negative
sampler = kap.doit(cfg, sampler_name, scheduler, basic_pipe, sigma_factor=sigma_factor)[0]
regional_prompts = rp.doit(mask, sampler)[0]
sampler = kap.doit(cfg, sampler_name, scheduler, basic_pipe, sigma_factor=sigma_factor, scheduler_func_opt=scheduler_func_opt)[0]
try:
regional_prompts = rp.doit(mask, sampler, variation_seed=variation_seed, variation_strength=variation_strength, variation_method=variation_method)[0]
except:
raise Exception("[Inspire-Pack] ERROR: Impact Pack is outdated. Update Impact Pack to latest version to use this.")
return (regional_prompts, )
def color_to_mask(color_mask, mask_color):
try:
if mask_color.startswith("#"):
selected = int(mask_color[1:], 16)
if mask_color.startswith("#") or mask_color.isalpha():
hex = mask_color[1:] if mask_color.startswith("#") else webcolors.name_to_hex(mask_color)[1:]
selected = int(hex, 16)
else:
selected = int(mask_color, 10)
except Exception:
raise Exception(f"[ERROR] Invalid mask_color value. mask_color should be a color value for RGB")
raise Exception("[ERROR] Invalid mask_color value. mask_color should be a color value for RGB")
temp = (torch.clamp(color_mask, 0, 1.0) * 255.0).round().to(torch.int)
temp = torch.bitwise_left_shift(temp[:, :, :, 0], 16) + torch.bitwise_left_shift(temp[:, :, :, 1], 8) + temp[:, :, :, 2]
@@ -96,6 +112,12 @@ class RegionalPromptColorMask:
"controlnet_in_pipe": ("BOOLEAN", {"default": False, "label_on": "Keep", "label_off": "Override"}),
"sigma_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
},
"optional": {
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"variation_method": (["linear", "slerp"],),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
RETURN_TYPES = ("REGIONAL_PROMPTS", "MASK")
@@ -103,10 +125,13 @@ class RegionalPromptColorMask:
CATEGORY = "InspirePack/Regional"
def doit(self, basic_pipe, color_mask, mask_color, cfg, sampler_name, scheduler, wildcard_prompt, controlnet_in_pipe=False, sigma_factor=1.0):
@staticmethod
def doit(basic_pipe, color_mask, mask_color, cfg, sampler_name, scheduler, wildcard_prompt,
controlnet_in_pipe=False, sigma_factor=1.0, variation_seed=0, variation_strength=0.0, variation_method="linear", scheduler_func_opt=None):
mask = color_to_mask(color_mask, mask_color)
rp = RegionalPromptSimple().doit(basic_pipe, mask, cfg, sampler_name, scheduler, wildcard_prompt, controlnet_in_pipe, sigma_factor=sigma_factor)[0]
return (rp, mask)
rp = RegionalPromptSimple().doit(basic_pipe, mask, cfg, sampler_name, scheduler, wildcard_prompt, controlnet_in_pipe,
sigma_factor=sigma_factor, variation_seed=variation_seed, variation_strength=variation_strength, variation_method=variation_method, scheduler_func_opt=scheduler_func_opt)[0]
return rp, mask
class RegionalConditioningSimple:
@@ -127,7 +152,8 @@ class RegionalConditioningSimple:
CATEGORY = "InspirePack/Regional"
def doit(self, clip, mask, strength, set_cond_area, prompt):
@staticmethod
def doit(clip, mask, strength, set_cond_area, prompt):
conditioning = nodes.CLIPTextEncode().encode(clip, prompt)[0]
conditioning = nodes.ConditioningSetMask().append(conditioning, mask, set_cond_area, strength)[0]
return (conditioning, )
@@ -145,6 +171,9 @@ class RegionalConditioningColorMask:
"set_cond_area": (["default", "mask bounds"],),
"prompt": ("STRING", {"multiline": True, "placeholder": "prompt"}),
},
"optional": {
"dilation": ("INT", {"default": 0, "min": -512, "max": 512, "step": 1}),
}
}
RETURN_TYPES = ("CONDITIONING", "MASK")
@@ -152,12 +181,16 @@ class RegionalConditioningColorMask:
CATEGORY = "InspirePack/Regional"
def doit(self, clip, color_mask, mask_color, strength, set_cond_area, prompt):
@staticmethod
def doit(clip, color_mask, mask_color, strength, set_cond_area, prompt, dilation=0):
mask = color_to_mask(color_mask, mask_color)
if dilation != 0:
mask = utils.dilate_mask(mask, dilation)
conditioning = nodes.CLIPTextEncode().encode(clip, prompt)[0]
conditioning = nodes.ConditioningSetMask().append(conditioning, mask, set_cond_area, strength)[0]
return (conditioning, mask)
return conditioning, mask
class ToIPAdapterPipe:
@@ -179,7 +212,8 @@ class ToIPAdapterPipe:
CATEGORY = "InspirePack/Util"
def doit(self, ipadapter, model, clip_vision, insightface=None):
@staticmethod
def doit(ipadapter, model, clip_vision, insightface=None):
pipe = ipadapter, model, clip_vision, insightface, lambda x: x
return (pipe,)
@@ -227,7 +261,7 @@ class IPAdapterConditioning:
if 'IPAdapterAdvanced' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/cubiq/ComfyUI_IPAdapter_plus',
"To use 'Regional IPAdapter' node, 'ComfyUI IPAdapter Plus' extension is required.")
raise Exception(f"[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
raise Exception("[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
if self.embeds is None:
obj = nodes.NODE_CLASS_MAPPINGS['IPAdapterAdvanced']
@@ -244,6 +278,22 @@ class IPAdapterConditioning:
return model
IPADAPTER_WEIGHT_TYPES_CACHE = None
def IPADAPTER_WEIGHT_TYPES():
global IPADAPTER_WEIGHT_TYPES_CACHE
if IPADAPTER_WEIGHT_TYPES_CACHE is None:
try:
IPADAPTER_WEIGHT_TYPES_CACHE = nodes.NODE_CLASS_MAPPINGS['IPAdapterAdvanced']().INPUT_TYPES()['required']['weight_type'][0]
except Exception:
logging.error("[Inspire Pack] IPAdapterPlus is not installed.")
IPADAPTER_WEIGHT_TYPES_CACHE = ["IPAdapterPlus is not installed"]
return IPADAPTER_WEIGHT_TYPES_CACHE
class RegionalIPAdapterMask:
@classmethod
def INPUT_TYPES(s):
@@ -254,7 +304,7 @@ class RegionalIPAdapterMask:
"image": ("IMAGE",),
"weight": ("FLOAT", {"default": 0.7, "min": -1, "max": 3, "step": 0.05}),
"noise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"weight_type": (["original", "linear", "channel penalty"],),
"weight_type": (IPADAPTER_WEIGHT_TYPES(), ),
"start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"unfold_batch": ("BOOLEAN", {"default": False}),
@@ -272,7 +322,8 @@ class RegionalIPAdapterMask:
CATEGORY = "InspirePack/Regional"
def doit(self, mask, image, weight, noise, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, faceid_v2=False, weight_v2=False, combine_embeds="concat", neg_image=None):
@staticmethod
def doit(mask, image, weight, noise, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, faceid_v2=False, weight_v2=False, combine_embeds="concat", neg_image=None):
cond = IPAdapterConditioning(mask, weight, weight_type, noise=noise, image=image, neg_image=neg_image, start_at=start_at, end_at=end_at, unfold_batch=unfold_batch, weight_v2=weight_v2, combine_embeds=combine_embeds)
return (cond, )
@@ -284,11 +335,10 @@ class RegionalIPAdapterColorMask:
"required": {
"color_mask": ("IMAGE",),
"mask_color": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
"image": ("IMAGE",),
"weight": ("FLOAT", {"default": 0.7, "min": -1, "max": 3, "step": 0.05}),
"noise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"weight_type": (["original", "linear", "channel penalty"], ),
"weight_type": (IPADAPTER_WEIGHT_TYPES(), ),
"start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"unfold_batch": ("BOOLEAN", {"default": False}),
@@ -306,7 +356,8 @@ class RegionalIPAdapterColorMask:
CATEGORY = "InspirePack/Regional"
def doit(self, color_mask, mask_color, image, weight, noise, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, faceid_v2=False, weight_v2=False, combine_embeds="concat", neg_image=None):
@staticmethod
def doit(color_mask, mask_color, image, weight, noise, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, faceid_v2=False, weight_v2=False, combine_embeds="concat", neg_image=None):
mask = color_to_mask(color_mask, mask_color)
cond = IPAdapterConditioning(mask, weight, weight_type, noise=noise, image=image, neg_image=neg_image, start_at=start_at, end_at=end_at, unfold_batch=unfold_batch, weight_v2=weight_v2, combine_embeds=combine_embeds)
return (cond, mask)
@@ -321,7 +372,7 @@ class RegionalIPAdapterEncodedMask:
"embeds": ("EMBEDS",),
"weight": ("FLOAT", {"default": 0.7, "min": -1, "max": 3, "step": 0.05}),
"weight_type": (["original", "linear", "channel penalty"],),
"weight_type": (IPADAPTER_WEIGHT_TYPES(), ),
"start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"unfold_batch": ("BOOLEAN", {"default": False}),
@@ -336,7 +387,8 @@ class RegionalIPAdapterEncodedMask:
CATEGORY = "InspirePack/Regional"
def doit(self, mask, embeds, weight, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, neg_embeds=None):
@staticmethod
def doit(mask, embeds, weight, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, neg_embeds=None):
cond = IPAdapterConditioning(mask, weight, weight_type, embeds=embeds, start_at=start_at, end_at=end_at, unfold_batch=unfold_batch, neg_embeds=neg_embeds)
return (cond, )
@@ -351,7 +403,7 @@ class RegionalIPAdapterEncodedColorMask:
"embeds": ("EMBEDS",),
"weight": ("FLOAT", {"default": 0.7, "min": -1, "max": 3, "step": 0.05}),
"weight_type": (["original", "linear", "channel penalty"],),
"weight_type": (IPADAPTER_WEIGHT_TYPES(), ),
"start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"unfold_batch": ("BOOLEAN", {"default": False}),
@@ -366,7 +418,8 @@ class RegionalIPAdapterEncodedColorMask:
CATEGORY = "InspirePack/Regional"
def doit(self, color_mask, mask_color, embeds, weight, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, neg_embeds=None):
@staticmethod
def doit(color_mask, mask_color, embeds, weight, weight_type, start_at=0.0, end_at=1.0, unfold_batch=False, neg_embeds=None):
mask = color_to_mask(color_mask, mask_color)
cond = IPAdapterConditioning(mask, weight, weight_type, embeds=embeds, start_at=start_at, end_at=end_at, unfold_batch=unfold_batch, neg_embeds=neg_embeds)
return (cond, mask)
@@ -386,7 +439,8 @@ class ApplyRegionalIPAdapters:
CATEGORY = "InspirePack/Regional"
def doit(self, **kwargs):
@staticmethod
def doit(**kwargs):
ipadapter_pipe = kwargs['ipadapter_pipe']
ipadapter, model, clip_vision, insightface, lora_loader = ipadapter_pipe
@@ -406,21 +460,24 @@ class RegionalSeedExplorerMask:
"required": {
"mask": ("MASK",),
"noise": ("NOISE",),
"noise": ("NOISE_IMAGE",),
"seed_prompt": ("STRING", {"multiline": True, "dynamicPrompts": False, "pysssss.autocomplete": False}),
"enable_additional": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}),
"additional_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"additional_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
},
"optional":
{"variation_method": (["linear", "slerp"],), }
}
RETURN_TYPES = ("NOISE",)
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
def doit(self, mask, noise, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode):
@staticmethod
def doit(mask, noise, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode, variation_method='linear'):
device = comfy.model_management.get_torch_device()
noise_device = "cpu" if noise_mode == "CPU" else device
@@ -442,9 +499,9 @@ class RegionalSeedExplorerMask:
if enable_additional:
items.append((additional_seed, additional_strength))
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask)
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask, variation_method=variation_method)
except Exception:
print(f"[ERROR] IGNORED: RegionalSeedExplorerColorMask is failed.")
logging.error("[Inspire Pack] IGNORED: RegionalSeedExplorerColorMask is failed.")
traceback.print_exc()
noise = noise.cpu()
@@ -460,21 +517,24 @@ class RegionalSeedExplorerColorMask:
"color_mask": ("IMAGE",),
"mask_color": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
"noise": ("NOISE",),
"noise": ("NOISE_IMAGE",),
"seed_prompt": ("STRING", {"multiline": True, "dynamicPrompts": False, "pysssss.autocomplete": False}),
"enable_additional": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}),
"additional_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"additional_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
},
"optional":
{"variation_method": (["linear", "slerp"],), }
}
RETURN_TYPES = ("NOISE", "MASK")
RETURN_TYPES = ("NOISE_IMAGE", "MASK")
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
def doit(self, color_mask, mask_color, noise, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode):
@staticmethod
def doit(color_mask, mask_color, noise, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode, variation_method='linear'):
device = comfy.model_management.get_torch_device()
noise_device = "cpu" if noise_mode == "CPU" else device
@@ -497,9 +557,9 @@ class RegionalSeedExplorerColorMask:
if enable_additional:
items.append((additional_seed, additional_strength))
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask)
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask, variation_method=variation_method)
except Exception:
print(f"[ERROR] IGNORED: RegionalSeedExplorerColorMask is failed.")
logging.error("[Inspire Pack] IGNORED: RegionalSeedExplorerColorMask is failed.")
traceback.print_exc()
color_mask.cpu()
@@ -508,6 +568,100 @@ class RegionalSeedExplorerColorMask:
return (noise, original_mask)
class ColorMaskToDepthMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"color_mask": ("IMAGE",),
"spec": ("STRING", {"multiline": True, "default": "#FF0000:1.0\n#000000:1.0"}),
"base_value": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0}),
"dilation": ("INT", {"default": 0, "min": -512, "max": 512, "step": 1}),
"flatten_method": (["override", "sum", "max"],),
},
}
RETURN_TYPES = ("MASK", )
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
def doit(self, color_mask, spec, base_value, dilation, flatten_method):
specs = spec.split('\n')
pat = re.compile("(?P<color_code>#[A-F0-9]+):(?P<cfg>[0-9]+(.[0-9]*)?)")
masks = [torch.ones((1, color_mask.shape[1], color_mask.shape[2])) * base_value]
for x in specs:
match = pat.match(x)
if match:
mask = color_to_mask(color_mask=color_mask, mask_color=match['color_code']) * float(match['cfg'])
mask = utils.dilate_mask(mask, dilation)
masks.append(mask)
if masks:
masks = torch.cat(masks, dim=0)
if flatten_method == 'override':
masks = utils.flatten_non_zero_override(masks)
elif flatten_method == 'max':
masks = torch.max(masks, dim=0)[0]
else: # flatten_method == 'sum':
masks = torch.sum(masks, dim=0)
masks = torch.clamp(masks, min=0.0, max=1.0)
masks = masks.unsqueeze(0)
else:
masks = torch.tensor([])
return (masks, )
class RegionalCFG:
@classmethod
def INPUT_TYPES(s):
return {"required": {"model": ("MODEL",),
"mask": ("MASK",),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
@staticmethod
def doit(model, mask):
if len(mask.shape) == 2:
mask = mask.unsqueeze(0).unsqueeze(0)
elif len(mask.shape) == 3:
mask = mask.unsqueeze(0)
size = None
def regional_cfg(args):
nonlocal mask
nonlocal size
x = args['input']
if mask.device != x.device:
mask = mask.to(x.device)
if size != (x.shape[2], x.shape[3]):
size = (x.shape[2], x.shape[3])
mask = torch.nn.functional.interpolate(mask, size=size, mode='bilinear', align_corners=False)
cond_pred = args["cond_denoised"]
uncond_pred = args["uncond_denoised"]
cond_scale = args["cond_scale"]
cfg_result = uncond_pred + (cond_pred - uncond_pred) * cond_scale * mask
return x - cfg_result
m = model.clone()
m.set_model_sampler_cfg_function(regional_cfg)
return (m,)
NODE_CLASS_MAPPINGS = {
"RegionalPromptSimple //Inspire": RegionalPromptSimple,
"RegionalPromptColorMask //Inspire": RegionalPromptColorMask,
@@ -522,6 +676,8 @@ NODE_CLASS_MAPPINGS = {
"ToIPAdapterPipe //Inspire": ToIPAdapterPipe,
"FromIPAdapterPipe //Inspire": FromIPAdapterPipe,
"ApplyRegionalIPAdapters //Inspire": ApplyRegionalIPAdapters,
"RegionalCFG //Inspire": RegionalCFG,
"ColorMaskToDepthMask //Inspire": ColorMaskToDepthMask,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -537,5 +693,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RegionalSeedExplorerColorMask //Inspire": "Regional Seed Explorer By Color Mask (Inspire)",
"ToIPAdapterPipe //Inspire": "ToIPAdapterPipe (Inspire)",
"FromIPAdapterPipe //Inspire": "FromIPAdapterPipe (Inspire)",
"ApplyRegionalIPAdapters //Inspire": "Apply Regional IPAdapters (Inspire)"
"ApplyRegionalIPAdapters //Inspire": "Apply Regional IPAdapters (Inspire)",
"RegionalCFG //Inspire": "Regional CFG (Inspire)",
"ColorMaskToDepthMask //Inspire": "Color Mask To Depth Mask (Inspire)",
}
+277 -51
View File
@@ -2,27 +2,33 @@ import torch
from . import a1111_compat
import comfy
from .libs import common
from comfy import model_management
from comfy.samplers import CFGGuider
from comfy_extras.nodes_perpneg import Guider_PerpNeg
import math
class KSampler_progress(a1111_compat.KSampler_inspire):
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": True, "label_on": "True", "label_off": "False"}),
}
return {"required": {
"model": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"noise_mode": (a1111_compat.supported_noise_modes,),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": True, "label_on": "True", "label_off": "False"}),
"omit_final_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"optional": {
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
CATEGORY = "InspirePack/analysis"
@@ -30,22 +36,29 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
RETURN_TYPES = ("LATENT", "LATENT")
RETURN_NAMES = ("latent", "progress_latent")
def doit(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, interval, omit_start_latent):
@staticmethod
def doit(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
interval, omit_start_latent, omit_final_latent, scheduler_func_opt=None):
adv_steps = int(steps / denoise)
if omit_start_latent:
result = []
else:
result = [latent_image['samples']]
result = []
result = [comfy.sample.fix_empty_latent_channels(model, latent_image['samples']).cpu()]
def progress_callback(step, x0, x, total_steps):
x0 = model.model.process_latent_out(x0)
x0 = x0.to(model_management.intermediate_device())
result.append(x0)
if (total_steps-1) != step and step % interval != 0:
return
latent_image, noise = a1111_compat.KSamplerAdvanced_inspire.sample(model, True, seed, adv_steps, cfg, sampler_name, scheduler, positive, negative, latent_image, (adv_steps-steps), adv_steps, noise_mode, False, callback=progress_callback)
x = model.model.process_latent_out(x)
x = x.cpu()
result.append(x)
latent_image, noise = a1111_compat.KSamplerAdvanced_inspire.sample(model, True, seed, adv_steps, cfg, sampler_name, scheduler, positive, negative, latent_image, (adv_steps-steps),
adv_steps, noise_mode, False, callback=progress_callback, scheduler_func_opt=scheduler_func_opt)
if not omit_final_latent:
result.append(latent_image['samples'].cpu())
if len(result) > 0:
result = torch.cat(result)
@@ -59,25 +72,29 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"noise_mode": (["GPU(=A1111)", "CPU"],),
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"optional": {"prev_progress_latent_opt": ("LATENT",), }
return {"required": {
"model": ("MODEL",),
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (common.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"noise_mode": (a1111_compat.supported_noise_modes,),
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
"omit_final_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"optional": {
"prev_progress_latent_opt": ("LATENT",),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
FUNCTION = "doit"
@@ -87,22 +104,28 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
RETURN_TYPES = ("LATENT", "LATENT")
RETURN_NAMES = ("latent", "progress_latent")
def doit(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step,
noise_mode, return_with_leftover_noise, interval, omit_start_latent, prev_progress_latent_opt=None):
def doit(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
start_at_step, end_at_step, noise_mode, return_with_leftover_noise, interval, omit_start_latent, omit_final_latent,
prev_progress_latent_opt=None, scheduler_func_opt=None):
if omit_start_latent:
result = []
else:
result = [latent_image['samples']]
result = []
def progress_callback(step, x0, x, total_steps):
x0 = model.model.process_latent_out(x0)
x0 = x0.to(model_management.intermediate_device())
result.append(x0)
if (total_steps-1) != step and step % interval != 0:
return
x = model.model.process_latent_out(x)
x = x.cpu()
result.append(x)
latent_image, noise = a1111_compat.KSamplerAdvanced_inspire.sample(model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step,
noise_mode, False, callback=progress_callback)
noise_mode, return_with_leftover_noise, callback=progress_callback, scheduler_func_opt=scheduler_func_opt)
if not omit_final_latent:
result.append(latent_image['samples'].cpu())
if len(result) > 0:
result = torch.cat(result)
@@ -116,12 +139,215 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
return latent_image, result
def exponential_interpolation(from_cfg, to_cfg, i, steps):
if i == steps-1:
return to_cfg
if from_cfg == to_cfg:
return from_cfg
if from_cfg == 0:
return to_cfg * (1 - math.exp(-5 * i / steps)) / (1 - math.exp(-5))
elif to_cfg == 0:
return from_cfg * (math.exp(-5 * i / steps) - math.exp(-5)) / (1 - math.exp(-5))
else:
log_from = math.log(from_cfg)
log_to = math.log(to_cfg)
log_value = log_from + (log_to - log_from) * i / steps
return math.exp(log_value)
def logarithmic_interpolation(from_cfg, to_cfg, i, steps):
if i == 0:
return from_cfg
if i == steps-1:
return to_cfg
log_i = math.log(i + 1)
log_steps = math.log(steps + 1)
t = log_i / log_steps
return from_cfg + (to_cfg - from_cfg) * t
def cosine_interpolation(from_cfg, to_cfg, i, steps):
if (i == 0) or (i == steps-1):
return from_cfg
t = (1.0 + math.cos(math.pi*2*(i/steps))) / 2
return from_cfg + (to_cfg - from_cfg) * t
class Guider_scheduled(CFGGuider):
def __init__(self, model_patcher, sigmas, from_cfg, to_cfg, schedule):
super().__init__(model_patcher)
self.default_cfg = self.cfg
self.sigmas = sigmas
self.cfg_sigmas = None
self.cfg_sigmas_i = None
self.from_cfg = from_cfg
self.to_cfg = to_cfg
self.schedule = schedule
self.last_i = 0
self.renew_cfg_sigmas()
def set_cfg(self, cfg):
self.default_cfg = cfg
self.renew_cfg_sigmas()
def renew_cfg_sigmas(self):
self.cfg_sigmas = {}
self.cfg_sigmas_i = {}
i = 0
steps = len(self.sigmas) - 1
for x in self.sigmas:
k = float(x)
delta = self.to_cfg - self.from_cfg
if self.schedule == 'exp':
self.cfg_sigmas[k] = exponential_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'log':
self.cfg_sigmas[k] = logarithmic_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'cos':
self.cfg_sigmas[k] = cosine_interpolation(self.from_cfg, self.to_cfg, i, steps), i
else:
self.cfg_sigmas[k] = self.from_cfg + delta * i / steps, i
self.cfg_sigmas_i[i] = self.cfg_sigmas[k]
i += 1
def predict_noise(self, x, timestep, model_options={}, seed=None):
k = float(timestep[0])
v = self.cfg_sigmas.get(k)
if v is None:
# fallback
v = self.cfg_sigmas_i[self.last_i+1]
self.cfg_sigmas[k] = v
self.last_i = v[1]
self.cfg = v[0]
return super().predict_noise(x, timestep, model_options, seed)
class Guider_PerpNeg_scheduled(Guider_PerpNeg):
def __init__(self, model_patcher, sigmas, from_cfg, to_cfg, schedule, neg_scale):
super().__init__(model_patcher)
self.default_cfg = self.cfg
self.sigmas = sigmas
self.cfg_sigmas = None
self.cfg_sigmas_i = None
self.from_cfg = from_cfg
self.to_cfg = to_cfg
self.schedule = schedule
self.neg_scale = neg_scale
self.last_i = 0
self.renew_cfg_sigmas()
def set_cfg(self, cfg):
self.default_cfg = cfg
self.renew_cfg_sigmas()
def renew_cfg_sigmas(self):
self.cfg_sigmas = {}
self.cfg_sigmas_i = {}
i = 0
steps = len(self.sigmas) - 1
for x in self.sigmas:
k = float(x)
delta = self.to_cfg - self.from_cfg
if self.schedule == 'exp':
self.cfg_sigmas[k] = exponential_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'log':
self.cfg_sigmas[k] = logarithmic_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'cos':
self.cfg_sigmas[k] = cosine_interpolation(self.from_cfg, self.to_cfg, i, steps), i
else:
self.cfg_sigmas[k] = self.from_cfg + delta * i / steps, i
self.cfg_sigmas_i[i] = self.cfg_sigmas[k]
i += 1
def predict_noise(self, x, timestep, model_options={}, seed=None):
k = float(timestep[0])
v = self.cfg_sigmas.get(k)
if v is None:
# fallback
v = self.cfg_sigmas_i[self.last_i+1]
self.cfg_sigmas[k] = v
self.last_i = v[1]
self.cfg = v[0]
return super().predict_noise(x, timestep, model_options, seed)
class ScheduledCFGGuider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model": ("MODEL", ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"sigmas": ("SIGMAS", ),
"from_cfg": ("FLOAT", {"default": 6.5, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"to_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"schedule": (["linear", "log", "exp", "cos"], {'default': 'log'})
}
}
RETURN_TYPES = ("GUIDER", "SIGMAS")
FUNCTION = "get_guider"
CATEGORY = "sampling/custom_sampling/guiders"
def get_guider(self, model, positive, negative, sigmas, from_cfg, to_cfg, schedule):
guider = Guider_scheduled(model, sigmas, from_cfg, to_cfg, schedule)
guider.set_conds(positive, negative)
return guider, sigmas
class ScheduledPerpNegCFGGuider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model": ("MODEL", ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"empty_conditioning": ("CONDITIONING", ),
"neg_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01}),
"sigmas": ("SIGMAS", ),
"from_cfg": ("FLOAT", {"default": 6.5, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"to_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"schedule": (["linear", "log", "exp", "cos"], {'default': 'log'})
}
}
RETURN_TYPES = ("GUIDER", "SIGMAS")
FUNCTION = "get_guider"
CATEGORY = "sampling/custom_sampling/guiders"
def get_guider(self, model, positive, negative, empty_conditioning, neg_scale, sigmas, from_cfg, to_cfg, schedule):
guider = Guider_PerpNeg_scheduled(model, sigmas, from_cfg, to_cfg, schedule, neg_scale)
guider.set_conds(positive, negative, empty_conditioning)
return guider, sigmas
NODE_CLASS_MAPPINGS = {
"KSamplerProgress //Inspire": KSampler_progress,
"KSamplerAdvancedProgress //Inspire": KSamplerAdvanced_progress,
"ScheduledCFGGuider //Inspire": ScheduledCFGGuider,
"ScheduledPerpNegCFGGuider //Inspire": ScheduledPerpNegCFGGuider
}
NODE_DISPLAY_NAME_MAPPINGS = {
"KSamplerProgress //Inspire": "KSampler Progress (Inspire)",
"KSamplerAdvancedProgress //Inspire": "KSampler Advanced Progress (Inspire)",
"ScheduledCFGGuider //Inspire": "Scheduled CFGGuider (Inspire)",
"ScheduledPerpNegCFGGuider //Inspire": "Scheduled PerpNeg CFGGuider (Inspire)"
}
+40 -20
View File
@@ -2,7 +2,7 @@ import nodes
import numpy as np
import torch
from .libs import utils
import logging
def normalize_size_base_64(w, h):
short_side = min(w, h)
@@ -28,18 +28,21 @@ class MediaPipeFaceMeshDetector:
if 'MediaPipe-FaceMeshPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'MediaPipeFaceMeshDetector' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
if 'MediaPipeFaceMeshToSEGS' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
"To use 'MediaPipeFaceMeshDetector' node, 'Impact Pack' extension is required.")
raise Exception(f"[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI-Impact-Pack'")
raise Exception("[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI-Impact-Pack'")
pre_obj = nodes.NODE_CLASS_MAPPINGS['MediaPipe-FaceMeshPreprocessor']
seg_obj = nodes.NODE_CLASS_MAPPINGS['MediaPipeFaceMeshToSEGS']
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
facemesh_image = pre_obj().detect(image, self.max_faces, threshold, resolution=resolution)[0]
facemesh_image = nodes.ImageScale().upscale(facemesh_image, "bilinear", image.shape[2], image.shape[1], "disabled")[0]
segs = seg_obj().doit(facemesh_image, crop_factor, not self.is_segm, crop_min_size, drop_size, dilation,
self.face, self.mouth, self.left_eyebrow, self.left_eye, self.left_pupil,
self.right_eyebrow, self.right_eye, self.right_pupil)[0]
@@ -60,7 +63,7 @@ class MediaPipe_FaceMesh_Preprocessor_wrapper:
if 'MediaPipe-FaceMeshPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
if self.upscale_factor != 1.0:
image = nodes.ImageScaleBy().upscale(image, 'bilinear', self.upscale_factor)[0]
@@ -75,7 +78,7 @@ class AnimeLineArt_Preprocessor_wrapper:
if 'AnimeLineArtPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'AnimeLineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use AnimeLineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use AnimeLineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['AnimeLineArtPreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -87,7 +90,7 @@ class Manga2Anime_LineArt_Preprocessor_wrapper:
if 'Manga2Anime_LineArt_Preprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'Manga2Anime_LineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use Manga2Anime_LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use Manga2Anime_LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['Manga2Anime_LineArt_Preprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -99,7 +102,7 @@ class Color_Preprocessor_wrapper:
if 'ColorPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'Color_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use Color_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use Color_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['ColorPreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -107,17 +110,29 @@ class Color_Preprocessor_wrapper:
class InpaintPreprocessor_wrapper:
def __init__(self, black_pixel_for_xinsir_cn):
self.black_pixel_for_xinsir_cn = black_pixel_for_xinsir_cn
def apply(self, image, mask=None):
if 'InpaintPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'InpaintPreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use InpaintPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use InpaintPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['InpaintPreprocessor']()
if mask is None:
mask = torch.ones((image.shape[1], image.shape[2]), dtype=torch.float32, device="cpu").unsqueeze(0)
return obj.preprocess(image, mask)[0]
try:
res = obj.preprocess(image, mask, black_pixel_for_xinsir_cn=self.black_pixel_for_xinsir_cn)[0]
except Exception as e:
if self.black_pixel_for_xinsir_cn:
raise e
else:
res = obj.preprocess(image, mask)[0]
logging.warning("[Inspire Pack] Installed 'ComfyUI's ControlNet Auxiliary Preprocessors.' is outdated.")
return res
class TilePreprocessor_wrapper:
@@ -128,7 +143,7 @@ class TilePreprocessor_wrapper:
if 'TilePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'TilePreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use TilePreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use TilePreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -140,7 +155,7 @@ class MeshGraphormerDepthMapPreprocessorProvider_wrapper:
if 'MeshGraphormer-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'MeshGraphormerDepthMapPreprocessorProvider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use MeshGraphormerDepthMapPreprocessorProvider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use MeshGraphormerDepthMapPreprocessorProvider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['MeshGraphormer-DepthMapPreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -155,7 +170,7 @@ class LineArt_Preprocessor_wrapper:
if 'LineArtPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'LineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
coarse = 'enable' if self.coarse else 'disable'
@@ -175,7 +190,7 @@ class OpenPose_Preprocessor_wrapper:
if 'OpenposePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'OpenPose_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use OpenPose_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use OpenPose_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
detect_hand = 'enable' if self.detect_hand else 'disable'
detect_body = 'enable' if self.detect_body else 'disable'
@@ -202,7 +217,7 @@ class DWPreprocessor_wrapper:
if 'DWPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'DWPreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use DWPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use DWPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
detect_hand = 'enable' if self.detect_hand else 'disable'
detect_body = 'enable' if self.detect_body else 'disable'
@@ -226,7 +241,7 @@ class LeReS_DepthMap_Preprocessor_wrapper:
if 'LeReS-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'LeReS_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use LeReS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use LeReS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
boost = 'enable' if self.boost else 'disable'
@@ -244,7 +259,7 @@ class MiDaS_DepthMap_Preprocessor_wrapper:
if 'MiDaS-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'MiDaS_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use MiDaS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use MiDaS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['MiDaS-DepthMapPreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -256,7 +271,7 @@ class Zoe_DepthMap_Preprocessor_wrapper:
if 'Zoe-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
"To use 'Zoe_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
raise Exception(f"[ERROR] To use Zoe_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
raise Exception("[ERROR] To use Zoe_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
obj = nodes.NODE_CLASS_MAPPINGS['Zoe-DepthMapPreprocessor']()
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
@@ -544,14 +559,19 @@ class Color_Preprocessor_Provider_for_SEGS:
class InpaintPreprocessor_Provider_for_SEGS:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
return {
"required": {},
"optional": {
"black_pixel_for_xinsir_cn": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
}
}
RETURN_TYPES = ("SEGS_PREPROCESSOR",)
FUNCTION = "doit"
CATEGORY = "InspirePack/SEGS/ControlNet"
def doit(self):
obj = InpaintPreprocessor_wrapper()
def doit(self, black_pixel_for_xinsir_cn=False):
obj = InpaintPreprocessor_wrapper(black_pixel_for_xinsir_cn)
return (obj, )
+41
View File
@@ -0,0 +1,41 @@
import colorsys
def hex_to_hsv(hex_color):
hex_color = hex_color.lstrip('#')
r, g, b = tuple(int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4))
h, s, v = colorsys.rgb_to_hsv(r, g, b)
hue = h * 360
saturation = s
value = v
return hue, saturation, value
class RGB_HexToHSV:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"rgb_hex": ("STRING", {"defaultInput": True}),
},
}
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("hue", "saturation", "value")
FUNCTION = "doit"
CATEGORY = "InspirePack/Util"
def doit(self, rgb_hex):
return hex_to_hsv(rgb_hex)
NODE_CLASS_MAPPINGS = {
"RGB_HexToHSV //Inspire": RGB_HexToHSV,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RGB_HexToHSV //Inspire": "RGB Hex To HSV (Inspire)",
}
+1 -1
View File
@@ -44,7 +44,7 @@ app.registerExtension({
Object.defineProperty(node, 'imgs', {
set(v) {
if (!v[0].complete) {
if (v && !v[0].complete) {
let orig_onload = v[0].onload;
v[0].onload = function(v2) {
if(orig_onload)
+57 -15
View File
@@ -158,21 +158,57 @@ export function register_concat_conditionings_with_multiplier_node(nodeType, nod
}
async function ensure_multipliers() {
let ncon = get_input_count('conditioning', true);
let nmul = get_input_count('multiplier', false) + get_widget_count('multiplier');
if(self.ensuring_multipliers) {
return;
}
try {
self.ensuring_multipliers = true;
if(ncon == 0 && nmul == 0)
ncon = 1;
let ncon = get_input_count('conditioning', true);
let nmul = get_input_count('multiplier', false) + get_widget_count('multiplier');
for(let i = nmul+1; i<=ncon; i++) {
let config = { min: 0, max: 10, step: 0.1, round: 0.01, precision: 2 };
let widget = await self.addWidget("number", `multiplier${i}`, 1.0, function (v) {
if (config.round) {
self.value = Math.round(v/config.round)*config.round;
} else {
self.value = v;
}
}, config);
if(ncon == 0 && nmul == 0)
ncon = 1;
for(let i = nmul+1; i<=ncon; i++) {
let config = { min: 0, max: 10, step: 0.1, round: 0.01, precision: 2 };
// NOTE: addWidget trigger calling ensure_multipliers
let widget = await self.addWidget("number", `multiplier${i}`, 1.0, function (v) {
if (config.round) {
self.value = Math.round(v/config.round)*config.round;
} else {
self.value = v;
}
}, config);
}
}
finally{
self.ensuring_multipliers = null;
}
}
async function recover_multipliers() {
if(self.recover_multipliers) {
return;
}
try {
self.recover_multipliers = true;
for(let i = 1; i<self.widgets_values.length; i++) {
let config = { min: 0, max: 10, step: 0.1, round: 0.01, precision: 2 };
// NOTE: addWidget trigger calling recover_multipliers
let widget = await self.addWidget("number", `multiplier${i+1}`, 1.0, function (v) {
if (config.round) {
self.value = Math.round(v/config.round)*config.round;
} else {
self.value = v;
}
}, config);
}
}
finally{
self.recover_multipliers = null;
}
}
@@ -183,12 +219,18 @@ export function register_concat_conditionings_with_multiplier_node(nodeType, nod
}
}
if(!Error().stack.includes('pasteFromClipboard')) {
const stackTrace = new Error().stack;
if(!stackTrace.includes('loadGraphData') && !stackTrace.includes('pasteFromClipboard')) {
await remove_garbage();
await ensure_inputs();
}
await ensure_multipliers();
if(!stackTrace.includes('loadGraphData')) {
await ensure_multipliers();
}
else {
await recover_multipliers();
}
await this.setSize( this.computeSize() );
}
+7
View File
@@ -0,0 +1,7 @@
import { inspireProgressBadge } from "./progress-badge.js"
export function register_loop_node(nodeType, nodeData, app) {
if(nodeData.name == 'ForeachListEnd //Inspire') {
inspireProgressBadge.addStatusHandler(nodeType);
}
}
+2
View File
@@ -1,11 +1,13 @@
import { ComfyApp, app } from "../../scripts/app.js";
import { register_concat_conditionings_with_multiplier_node, register_splitter } from "./inspire-flex.js";
import { register_cache_info } from "./inspire-backend.js";
import { register_loop_node } from "./inspire-loop.js";
app.registerExtension({
name: "Comfy.Inspire",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
await register_concat_conditionings_with_multiplier_node(nodeType, nodeData, app);
await register_loop_node(nodeType, nodeData, app);
},
nodeCreated(node, app) {
+91 -87
View File
@@ -4,7 +4,7 @@ app.registerExtension({
name: "Comfy.Inspire.LBW",
nodeCreated(node, app) {
if(node.comfyClass == "LoraLoaderBlockWeight //Inspire") {
if(node.comfyClass == "LoraLoaderBlockWeight //Inspire" || node.comfyClass == "MakeLBW //Inspire") {
// category filter
const lora_names_widget = node.widgets[node.widgets.findIndex(obj => obj.name === 'lora_name')];
var full_lora_list = lora_names_widget.options.values;
@@ -26,21 +26,25 @@ app.registerExtension({
// vector selector
let preset_i = 9;
let vector_i = 10;
if(node.comfyClass == "MakeLBW //Inspire") {
preset_i = 7;
vector_i = 8;
}
node._value = "Preset";
node.widgets[preset_i].callback = (v, canvas, node, pos, e) => {
node.widgets[vector_i].value = node._value.split(':')[1];
if(node.widgets_values) {
node.widgets_values[vector_i] = node.widgets[preset_i].value;
}
}
Object.defineProperty(node.widgets[preset_i], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Preset") {
node.widgets[vector_i].value = value.split(':')[1];
if(node.widgets_values) {
node.widgets_values[vector_i] = node.widgets[preset_i].value;
}
}
}
node._value = value;
if(value != "Preset")
node._value = value;
},
get: () => {
return node._value;
@@ -71,86 +75,86 @@ app.registerExtension({
let preset_i = 9;
let vector_i = 10;
node._value = "Preset";
Object.defineProperty(node.widgets[preset_i], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Preset") {
if(!value.startsWith('@') && node.widgets[vector_i].value != "")
node.widgets[vector_i].value += "\n";
if(value.startsWith('@')) {
let spec = value.split(':')[1];
var n;
var sub_n = null;
var block = null;
if(isNaN(spec)) {
let sub_spec = spec.split(',');
node.widgets[preset_i].callback = (v, canvas, node, pos, e) => {
let value = node._value;
if(!value.startsWith('@') && node.widgets[vector_i].value != "")
node.widgets[vector_i].value += "\n";
if(value.startsWith('@')) {
let spec = value.split(':')[1];
var n;
var sub_n = null;
var block = null;
if(sub_spec.length != 3) {
node.widgets_values[vector_i] = '!! SPEC ERROR !!';
node._value = '';
return;
}
if(isNaN(spec)) {
let sub_spec = spec.split(',');
n = parseInt(sub_spec[0].trim());
sub_n = parseInt(sub_spec[1].trim());
block = parseInt(sub_spec[2].trim());
}
else {
n = parseInt(spec.trim());
}
node.widgets[vector_i].value = "";
if(sub_n == null) {
for(let i=1; i<=n; i++) {
var temp = "";
for(let j=1; j<=n; j++) {
if(temp!='')
temp += ',';
if(j==i)
temp += 'A';
else
temp += '0';
}
node.widgets[vector_i].value += `B${i}:${temp}\n`;
}
}
else {
for(let i=1; i<=sub_n; i++) {
var temp = "";
for(let j=1; j<=n; j++) {
if(temp!='')
temp += ',';
if(block!=j)
temp += '0';
else {
temp += ' ';
for(let k=1; k<=sub_n; k++) {
if(k==i)
temp += 'A ';
else
temp += '0 ';
}
}
}
node.widgets[vector_i].value += `B${block}.SUB${i}:${temp}\n`;
}
}
}
else {
node.widgets[vector_i].value += `${value}/${value.split(':')[0]}`;
}
if(node.widgets_values) {
node.widgets_values[vector_i] = node.widgets[preset_i].value;
}
}
if(sub_spec.length != 3) {
node.widgets_values[vector_i] = '!! SPEC ERROR !!';
node._value = '';
return;
}
node._value = value;
n = parseInt(sub_spec[0].trim());
sub_n = parseInt(sub_spec[1].trim());
block = parseInt(sub_spec[2].trim());
}
else {
n = parseInt(spec.trim());
}
node.widgets[vector_i].value = "";
if(sub_n == null) {
for(let i=1; i<=n; i++) {
var temp = "";
for(let j=1; j<=n; j++) {
if(temp!='')
temp += ',';
if(j==i)
temp += 'A';
else
temp += '0';
}
node.widgets[vector_i].value += `B${i}:${temp}\n`;
}
}
else {
for(let i=1; i<=sub_n; i++) {
var temp = "";
for(let j=1; j<=n; j++) {
if(temp!='')
temp += ',';
if(block!=j)
temp += '0';
else {
temp += ' ';
for(let k=1; k<=sub_n; k++) {
if(k==i)
temp += 'A ';
else
temp += '0 ';
}
}
}
node.widgets[vector_i].value += `B${block}.SUB${i}:${temp}\n`;
}
}
}
else {
node.widgets[vector_i].value += `${value}/${value.split(':')[0]}`;
}
if(node.widgets_values) {
node.widgets_values[vector_i] = node.widgets[preset_i].value;
}
}
Object.defineProperty(node.widgets[preset_i], "value", {
set: (value) => {
if(value != 'Preset')
node._value = value;
},
get: () => {
return node._value;
+76
View File
@@ -0,0 +1,76 @@
import { api } from "../../scripts/api.js";
// copying from https://github.com/pythongosssss/ComfyUI-WD14-Tagger
class InspireProgressBadge {
constructor() {
if (!window.__progress_badge__) {
window.__progress_badge__ = Symbol("__inspire_progress_badge__");
}
this.symbol = window.__progress_badge__;
}
getState(node) {
return node[this.symbol] || {};
}
setState(node, state) {
node[this.symbol] = state;
app.canvas.setDirty(true);
}
addStatusHandler(nodeType) {
if (nodeType[this.symbol]?.statusTagHandler) {
return;
}
if (!nodeType[this.symbol]) {
nodeType[this.symbol] = {};
}
nodeType[this.symbol] = {
statusTagHandler: true,
};
api.addEventListener("inspire/update_status", ({ detail }) => {
let { node, progress, text } = detail;
const n = app.graph.getNodeById(+(node || app.runningNodeId));
if (!n) return;
const state = this.getState(n);
state.status = Object.assign(state.status || {}, { progress: text ? progress : null, text: text || null });
this.setState(n, state);
});
const self = this;
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
const state = self.getState(this);
if (!state?.status?.text) {
return r;
}
const { fgColor, bgColor, text, progress, progressColor } = { ...state.status };
ctx.save();
ctx.font = "12px sans-serif";
const sz = ctx.measureText(text);
ctx.fillStyle = bgColor || "dodgerblue";
ctx.beginPath();
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
ctx.fill();
if (progress) {
ctx.fillStyle = progressColor || "green";
ctx.beginPath();
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, (sz.width + 12) * progress, 20, 5);
ctx.fill();
}
ctx.fillStyle = fgColor || "#fff";
ctx.fillText(text, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
ctx.restore();
return r;
};
}
}
export const inspireProgressBadge = new InspireProgressBadge();
+94 -82
View File
@@ -42,35 +42,38 @@ app.registerExtension({
// lora selector, wildcard selector
let combo_id = 5;
Object.defineProperty(node.widgets[combo_id], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Select the LoRA to add to the text") {
let lora_name = value;
if (lora_name.endsWith('.safetensors')) {
lora_name = lora_name.slice(0, -12);
}
// lora
node.widgets[combo_id].callback = (value, canvas, node, pos, e) => {
let lora_name = node._value;
if(lora_name.endsWith('.safetensors')) {
lora_name = lora_name.slice(0, -12);
}
wildcard_text_widget.value += `<lora:${lora_name}>`;
}
}
},
get: () => { return "Select the LoRA to add to the text"; }
});
wildcard_text_widget.value += `<lora:${lora_name}>`;
}
Object.defineProperty(node.widgets[combo_id], "value", {
set: (value) => {
if (value !== "Select the LoRA to add to the text")
node._value = value;
},
get: () => { return "Select the LoRA to add to the text"; }
});
// wildcard
node.widgets[combo_id+1].callback = (value, canvas, node, pos, e) => {
if(wildcard_text_widget.value != '')
wildcard_text_widget.value += ', '
wildcard_text_widget.value += node._wildcard_value;
}
Object.defineProperty(node.widgets[combo_id+1], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Select the Wildcard to add to the text") {
if(wildcard_text_widget.value != '')
wildcard_text_widget.value += ', '
wildcard_text_widget.value += value;
}
}
},
if (value !== "Select the Wildcard to add to the text")
node._wildcard_value = value;
},
get: () => { return "Select the Wildcard to add to the text"; }
});
@@ -92,14 +95,20 @@ app.registerExtension({
// mode combo
Object.defineProperty(mode_widget, "value", {
set: (value) => {
node._mode_value = value == true || value == "Populate";
populated_text_widget.inputEl.disabled = value == true || value == "Populate";
if(value == true)
node._mode_value = "populate";
else if(value == false)
node._mode_value = "fixed";
else
node._mode_value = value; // combo value
populated_text_widget.inputEl.disabled = node._mode_value == 'populate';
},
get: () => {
if(node._mode_value != undefined)
return node._mode_value;
else
return true;
return 'populate';
}
});
}
@@ -115,47 +124,46 @@ app.registerExtension({
// lora selector, wildcard selector
let combo_id = 5;
node.widgets[combo_id].callback = (value, canvas, node, pos, e) => {
let lora_name = node._lora_value;
if (lora_name.endsWith('.safetensors')) {
lora_name = lora_name.slice(0, -12);
}
if(direction_widget.value) {
pos_wildcard_text_widget.value += `<lora:${lora_name}>`;
}
else {
neg_wildcard_text_widget.value += `<lora:${lora_name}>`;
}
}
Object.defineProperty(node.widgets[combo_id], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Select the LoRA to add to the text") {
let lora_name = value;
if (lora_name.endsWith('.safetensors')) {
lora_name = lora_name.slice(0, -12);
}
if(direction_widget.value) {
pos_wildcard_text_widget.value += `<lora:${lora_name}>`;
}
else {
neg_wildcard_text_widget.value += `<lora:${lora_name}>`;
}
}
}
if (value !== "Select the LoRA to add to the text")
node._lora_value = value;
},
get: () => { return "Select the LoRA to add to the text"; }
});
node.widgets[combo_id+1].callback = (value, canvas, node, pos, e) => {
let w = null;
if(direction_widget.value) {
w = pos_wildcard_text_widget;
}
else {
w = neg_wildcard_text_widget;
}
if(w.value != '')
w.value += ', '
w.value += node._wildcard_value;
}
Object.defineProperty(node.widgets[combo_id+1], "value", {
set: (value) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(value != "Select the Wildcard to add to the text") {
let w = null;
if(direction_widget.value) {
w = pos_wildcard_text_widget;
}
else {
w = neg_wildcard_text_widget;
}
if(w.value != '')
w.value += ', '
w.value += value;
}
}
if (value !== "Select the Wildcard to add to the text")
node._wildcard_value = value;
},
get: () => { return "Select the Wildcard to add to the text"; }
});
@@ -178,15 +186,21 @@ app.registerExtension({
// mode combo
Object.defineProperty(mode_widget, "value", {
set: (value) => {
pos_populated_text_widget.inputEl.disabled = node._mode_value;
neg_populated_text_widget.inputEl.disabled = node._mode_value;
node._mode_value = value;
if(value == true)
node._mode_value = "populate";
else if(value == false)
node._mode_value = "fixed";
else
node._mode_value = value; // combo value
pos_populated_text_widget.inputEl.disabled = node._mode_value == 'populate';
neg_populated_text_widget.inputEl.disabled = node._mode_value == 'populate';
},
get: () => {
if(node._mode_value != undefined)
return node._mode_value;
else
return true;
return 'populate';
}
});
}
@@ -205,24 +219,22 @@ app.registerExtension({
}
});
preset_widget.callback = (value, canvas, node, pos, e) => {
if(node.widgets[2].value) {
node.widgets[2].value += ', ';
}
const y = node._preset_value.split(':');
if(y.length == 2)
node.widgets[2].value += y[1].trim();
else
node.widgets[2].value += node._preset_value.trim();
}
Object.defineProperty(preset_widget, "value", {
set: (x) => {
const stackTrace = new Error().stack;
if(stackTrace.includes('inner_value_change')) {
if(node.widgets[2].value) {
node.widgets[2].value += ', ';
}
const y = x.split(':');
if(y.length == 2)
node.widgets[2].value += y[1].trim();
else
node.widgets[2].value += x.trim();
if(node.widgets_values) {
node.widgets_values[2] = node.widgets[2].values;
}
};
set: (value) => {
if (value !== "#PRESET")
node._preset_value = value;
},
get: () => { return '#PRESET'; }
});
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-inspire-pack"
description = "This extension provides various nodes to support Lora Block Weight, Regional Nodes, Backend Cache, Prompt Utils, List Utils, Noise(Seed) Utils, ... and the Impact Pack."
version = "1.15"
license = { file = "LICENSE" }
dependencies = ["matplotlib", "cachetools"]
[project.urls]
Repository = "https://github.com/ltdrdata/ComfyUI-Inspire-Pack"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "drltdata"
DisplayName = "ComfyUI Inspire Pack"
Icon = ""
+4 -1
View File
@@ -1,2 +1,5 @@
matplotlib
cachetools
cachetools
numpy<2
webcolors
opencv-python
+71 -1
View File
@@ -29,6 +29,16 @@ SDXL-LyC-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
SDXL-LyC-INALL:1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0
SDXL-LyC-MIDALL:1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0
SDXL-LyC-OUTALL:1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1
FLUX-DBL-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
FLUX-DBL-FRONT7:1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-DBL-MID6:1,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0
FLUX-DBL-TAIL6:1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1
FLUX-SINGLE-ALL:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
FLUX-SINGLE-1to10:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-SINGLE-11to20:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-SINGLE-21to30:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0
FLUX-SINGLE-31to37:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1
FLUX-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
@SD-FULL-TEST:17
@SD-BLOCK1-TEST:17,12,1
@SD-BLOCK2-TEST:17,12,2
@@ -49,4 +59,64 @@ SDXL-LyC-OUTALL:1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1
@SD-BLOCK17-TEST:17,12,17
@SD-LyC-FULL-TEST:27
@SDXL-FULL-TEST:12
@SDXL-LyC-FULL-TEST:21
@SDXL-LyC-FULL-TEST:21
@FLUX-DBL-FULL:19
@FLUX-DBL-SGL-FULL:58
@FLUX-DBL0-TEST:19,14,2
@FLUX-DBL1-TEST:19,14,3
@FLUX-DBL2-TEST:19,14,4
@FLUX-DBL3-TEST:19,14,5
@FLUX-DBL4-TEST:19,14,6
@FLUX-DBL5-TEST:19,14,7
@FLUX-DBL6-TEST:19,14,8
@FLUX-DBL7-TEST:19,14,9
@FLUX-DBL8-TEST:19,14,10
@FLUX-DBL9-TEST:19,14,11
@FLUX-DBL10-TEST:19,14,12
@FLUX-DBL11-TEST:19,14,13
@FLUX-DBL12-TEST:19,14,14
@FLUX-DBL13-TEST:19,14,15
@FLUX-DBL14-TEST:19,14,16
@FLUX-DBL15-TEST:19,14,17
@FLUX-DBL16-TEST:19,14,18
@FLUX-DBL17-TEST:19,14,19
@FLUX-DBL18-TEST:19,14,20
@FLUX-SGL0-TEST:58,6,21
@FLUX-SGL1-TEST:58,6,22
@FLUX-SGL2-TEST:58,6,23
@FLUX-SGL3-TEST:58,6,24
@FLUX-SGL4-TEST:58,6,25
@FLUX-SGL5-TEST:58,6,26
@FLUX-SGL6-TEST:58,6,27
@FLUX-SGL7-TEST:58,6,28
@FLUX-SGL8-TEST:58,6,29
@FLUX-SGL9-TEST:58,6,30
@FLUX-SGL10-TEST:58,6,31
@FLUX-SGL11-TEST:58,6,32
@FLUX-SGL12-TEST:58,6,33
@FLUX-SGL13-TEST:58,6,34
@FLUX-SGL14-TEST:58,6,35
@FLUX-SGL15-TEST:58,6,36
@FLUX-SGL16-TEST:58,6,37
@FLUX-SGL17-TEST:58,6,38
@FLUX-SGL18-TEST:58,6,39
@FLUX-SGL19-TEST:58,6,40
@FLUX-SGL20-TEST:58,6,41
@FLUX-SGL21-TEST:58,6,42
@FLUX-SGL22-TEST:58,6,43
@FLUX-SGL23-TEST:58,6,44
@FLUX-SGL24-TEST:58,6,45
@FLUX-SGL25-TEST:58,6,46
@FLUX-SGL26-TEST:58,6,47
@FLUX-SGL27-TEST:58,6,48
@FLUX-SGL28-TEST:58,6,49
@FLUX-SGL29-TEST:58,6,50
@FLUX-SGL30-TEST:58,6,51
@FLUX-SGL31-TEST:58,6,52
@FLUX-SGL32-TEST:58,6,53
@FLUX-SGL33-TEST:58,6,54
@FLUX-SGL34-TEST:58,6,55
@FLUX-SGL35-TEST:58,6,56
@FLUX-SGL36-TEST:58,6,57
@FLUX-SGL37-TEST:58,6,58
@FLUX-SGL38-TEST:58,6,59