Compare commits

..
43 Commits
Author SHA1 Message Date
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
20 changed files with 1486 additions and 336 deletions
+32 -5
View File
@@ -2,6 +2,7 @@
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.
@@ -13,12 +14,17 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
## 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.
* `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.
* `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.
@@ -30,6 +36,7 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
`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.'
@@ -44,6 +51,7 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* `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.
@@ -55,9 +63,17 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* 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.
@@ -137,7 +153,11 @@ 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
* `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.
@@ -148,8 +168,13 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* `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:
@@ -173,3 +198,5 @@ cubiq/[ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus)
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
+2 -2
View File
@@ -2,12 +2,12 @@
@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
version_code = [0, 85, 1]
version_code = [1, 12]
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})")
+2 -2
View File
@@ -162,7 +162,7 @@ class KSamplerAdvanced_inspire:
"optional":
{
"variation_method": (["linear", "slerp"],),
"noise_opt": ("NOISE",),
"noise_opt": ("NOISE_IMAGE",),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
@@ -253,7 +253,7 @@ class KSamplerAdvanced_inspire_pipe:
},
"optional":
{
"noise_opt": ("NOISE",),
"noise_opt": ("NOISE_IMAGE",),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
}
}
+249 -2
View File
@@ -1,5 +1,6 @@
import json
import os
from .libs import common
import folder_paths
import nodes
@@ -7,6 +8,8 @@ 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:
@@ -400,6 +403,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))
print(f"[Inspire Pack] LoadDiffusionModelShared: diffusion model '{model_name}' is cached to '{key}'.")
else:
_, (_, model) = cache[key]
print(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))
print(f"[Inspire Pack] LoadTextEncoderShared: text encoder model set is cached to '{key}'.")
else:
_, (_, res) = cache[key]
print(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):
@@ -478,6 +649,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 +728,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 +746,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)"
}
+13 -8
View File
@@ -8,19 +8,24 @@ from nodes import MAX_RESOLUTION
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
"optional": {"multiplier1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), },
}
RETURN_TYPES = ("CONDITIONING",)
+9 -6
View File
@@ -18,7 +18,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, "step": 1}),
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
@@ -94,10 +94,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)
@@ -126,8 +126,9 @@ class LoadImagesFromDirList:
}
}
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"
@@ -159,6 +160,7 @@ class LoadImagesFromDirList:
images = []
masks = []
file_paths = []
limit_images = False
if image_load_cap > 0:
@@ -184,9 +186,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:
+40 -7
View File
@@ -6,6 +6,7 @@ from enum import Enum
from . import prompt_support
from aiohttp import web
from . import backend_support
from .libs import common
max_seed = 2**32 - 1
@@ -271,7 +272,17 @@ def populate_wildcards(json_data):
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]]
@@ -292,14 +303,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]]
@@ -327,21 +341,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']:
extra_pnginfo = json_data['extra_data']['extra_pnginfo']
if 'workflow' in extra_pnginfo and 'nodes' in extra_pnginfo['workflow']:
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] = False
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] = False
node['widgets_values'][5] = 'reproduce'
def force_reset_useless_params(json_data):
@@ -354,6 +371,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 = {}
@@ -367,6 +399,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
+27
View File
@@ -12,3 +12,30 @@ def impact_sampling(*args, **kwargs):
raise Exception(f"[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
print(f"keys: {changed_cache.keys()}")
return res
+14
View File
@@ -5,6 +5,7 @@ import torch
from PIL import Image, ImageDraw
import math
import cv2
import folder_paths
def apply_variation_noise(latent_image, noise_device, variation_seed, variation_strength, mask=None, variation_method='linear'):
@@ -56,6 +57,8 @@ def slerp(val, low, high):
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:
@@ -334,3 +337,14 @@ def flatten_non_zero_override(masks: torch.Tensor):
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)
+165 -2
View File
@@ -1,3 +1,6 @@
from comfy_execution.graph_utils import GraphBuilder, is_link
from .libs.utils import any_typ
class FloatRange:
@classmethod
def INPUT_TYPES(s):
@@ -15,7 +18,7 @@ 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:
@@ -47,10 +50,170 @@ class FloatRange:
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:
return ("stub", item_list[1:], item_list[0], initial_input)
return ("stub", [], None, 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 len(remained_list) == 0:
return (intermediate_output,)
# We want to loop
this_node = dynprompt.get_node(unique_id)
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)",
}
+491 -49
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
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,76 @@ def load_lbw_preset(filename):
return []
def parse_unet_num(s):
if s[1] == '.':
return int(s[0])
else:
return int(s)
class MakeLBW:
def __init__(self):
self.loaded_lora = None
@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 +134,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 +209,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 +360,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,16 +368,6 @@ 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 = []
@@ -204,6 +411,14 @@ class LoraLoaderBlockWeight:
np.random.seed(seed % (2**31))
populated_vector_list = []
ratios = []
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())
@@ -220,6 +435,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
@@ -228,13 +445,10 @@ class LoraLoaderBlockWeight:
last_k_unet_num = k_unet_num
if populated_ratio > 0:
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())
@@ -243,25 +457,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
@@ -278,7 +510,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:
@@ -345,6 +618,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
@@ -353,7 +627,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()
@@ -385,7 +659,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
@@ -548,9 +822,13 @@ 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 = []
@@ -628,12 +906,23 @@ class LoraBlockInfo:
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_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)
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_map[k_unet_int] = [k_unet]
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)
@@ -677,10 +966,14 @@ class LoraBlockInfo:
for x in single_keys:
text += f" SINGLE{x}: {len(single_blocks_map[x])}\n"
text += f"\n-------[Base blocks] ({len(text_block_count) + len(others)}, Subs={len(text_blocks) + len(others)})-------\n"
text_keys = sorted(text_blocks_map.keys())
for x in text_keys:
text += f" TXT_ENC{x}: {len(text_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"
for x in others:
text += f" {x}\n"
@@ -697,13 +990,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",
}
+8 -3
View File
@@ -19,6 +19,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 +30,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),
@@ -56,7 +58,6 @@ 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}),
@@ -64,6 +65,7 @@ class IPAdapterModelHelper:
"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"}
@@ -75,14 +77,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, insightface_model_name='buffalo_l'):
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'")
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)"})
+213 -66
View File
@@ -12,20 +12,23 @@ 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
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')
@@ -37,128 +40,249 @@ 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'")
print(f"[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, "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"
@staticmethod
def doit(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()
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:
for file_name in prompt_files:
print(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}'")
except Exception as e:
print(f"[ERROR] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}")
print(f"[ERROR] 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,)},
"optional": {"text_data_opt": ("STRING", {"defaultInput": True})}}
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, "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"
@staticmethod
def doit(prompt_file, text_data_opt=None):
prompt_path = os.path.join(prompts_path, prompt_file)
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:
print(f"[INFO] LoadPromptsFromFile: file found '{prompt_file}'")
else:
print(f"[WARN] LoadPromptsFromFile: file not found '{prompt_file}'")
prompts = []
try:
if not text_data_opt:
with open(prompt_path, "r", encoding="utf-8") as file:
with open(matched_path, "r", encoding="utf-8") as file:
prompt_data = file.read()
else:
prompt_data = text_data_opt
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
pattern = r"^(?:(?:positive:(?P<positive>.*?)|negative:(?P<negative>.*?)|name:(?P<name>.*?))\n*)+$"
for prompt in prompt_list:
matches = re.search(pattern, prompt, re.DOTALL)
for p in prompt_list:
matches = re.search(pattern, p, re.DOTALL)
if matches:
positive_text = matches.group(1).strip()
negative_text = matches.group(2).strip()
result_tuple = (positive_text, negative_text, prompt_file)
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:
print(f"[WARN] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'")
except Exception as e:
print(f"[ERROR] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
print(f"[ERROR] 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 = []
@@ -178,7 +302,19 @@ class LoadSinglePromptFromFile:
@staticmethod
def doit(prompt_file, index, text_data_opt=None):
prompt_path = os.path.join(prompts_path, prompt_file)
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:
print(f"[INFO] LoadSinglePromptFromFile: file found '{prompt_file}'")
else:
print(f"[WARN] LoadSinglePromptFromFile: file not found '{prompt_file}'")
prompts = []
try:
@@ -194,18 +330,19 @@ class LoadSinglePromptFromFile:
except Exception:
prompt = prompt_list[-1]
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, prompt_file)
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:
print(f"[WARN] LoadSinglePromptFromFile: invalid prompt format in '{prompt_file}'")
except Exception as e:
print(f"[ERROR] LoadSinglePromptFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
print(f"[ERROR] LoadSinglePromptFromFile: an error occurred while processing '{prompt_file}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
return (prompts, )
@@ -440,7 +577,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}),
@@ -481,7 +624,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)'}),
@@ -566,11 +713,11 @@ class SeedExplorer:
"optional":
{
"variation_method": (["linear", "slerp"],),
"model": ("model",),
"model": ("MODEL",),
}
}
RETURN_TYPES = ("NOISE",)
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
@@ -645,15 +792,15 @@ class CompositeNoise:
def INPUT_TYPES(s):
return {
"required": {
"destination": ("NOISE",),
"source": ("NOISE",),
"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",)
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Prompt"
+8 -6
View File
@@ -4,6 +4,7 @@ import comfy
import nodes
import torch
import re
import webcolors
from . import prompt_support
from .libs import utils, common
@@ -81,8 +82,9 @@ class RegionalPromptSimple:
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:
@@ -458,7 +460,7 @@ 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}),
@@ -469,7 +471,7 @@ class RegionalSeedExplorerMask:
{"variation_method": (["linear", "slerp"],), }
}
RETURN_TYPES = ("NOISE",)
RETURN_TYPES = ("NOISE_IMAGE",)
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
@@ -515,7 +517,7 @@ 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}),
@@ -526,7 +528,7 @@ class RegionalSeedExplorerColorMask:
{"variation_method": (["linear", "slerp"],), }
}
RETURN_TYPES = ("NOISE", "MASK")
RETURN_TYPES = ("NOISE_IMAGE", "MASK")
FUNCTION = "doit"
CATEGORY = "InspirePack/Regional"
+2 -2
View File
@@ -44,7 +44,7 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
if omit_start_latent:
result = []
else:
result = [latent_image['samples']]
result = [comfy.sample.fix_empty_latent_channels(model, latent_image['samples']).cpu()]
def progress_callback(step, x0, x, total_steps):
if (total_steps-1) != step and step % interval != 0:
@@ -122,7 +122,7 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
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, scheduler_func_opt=scheduler_func_opt)
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())
+21 -4
View File
@@ -110,6 +110,9 @@ 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',
@@ -120,7 +123,16 @@ class InpaintPreprocessor_wrapper:
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]
print(f"[Inspire Pack] Installed 'ComfyUI's ControlNet Auxiliary Preprocessors.' is outdated.")
return res
class TilePreprocessor_wrapper:
@@ -547,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, )
+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;
+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'; }
});
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-inspire-pack"
description = "This extension provides various nodes to support Lora Block Weight and the Impact Pack. Provides many easily applicable regional features and applications for Variation Seed."
version = "0.85.1"
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.12"
license = { file = "LICENSE" }
dependencies = ["matplotlib", "cachetools"]
+3 -1
View File
@@ -1,3 +1,5 @@
matplotlib
cachetools
numpy<2
numpy<2
webcolors
opencv-python