Compare commits

...
42 Commits
Author SHA1 Message Date
Dr.Lt.Data 3bcc1d43a0 fixed: LBW - Certain text encoders, such as T5, were not being reflected in CLIP.
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/185
2024-11-15 12:52:27 +09:00
Dr.Lt.Data 395869085c Merge pull request #187 from RUiNtheExtinct/fix/api-error
patch to fix error when workflow is called via API
2024-11-15 10:16:04 +09:00
Arghyadeep Karmakar 384284fe0b patch to fix error when workflow is called via API 2024-11-14 17:42:56 +05:30
Dr.Lt.Data c6d53ecd4d Merge pull request #181 from aurel-g/main
Fix type of "model" input of SeedExplorer node
2024-11-05 18:07:48 +09:00
aurel-g 44cfac4f0a Fix type of "model" input of SeedExplorer node
The type of this input was "model" in lowercase, which is unusual and does not work well with ComfyUI node search.
2024-11-04 21:23:31 +01:00
Dr.Lt.Data bfaaa6c570 improved: extra_model_paths.yaml support for prompt files
- You can use `inspire_prompts`

https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/177
2024-10-22 23:20:29 +09:00
Dr.Lt.Data d29c41809f FIXED: ConcatConditioningsWithMultiplier - version compatibility patch
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/160#issuecomment-2416008408
2024-10-16 21:33:53 +09:00
Dr.Lt.Data 667dead8c1 improved: IPAdapterModelHelper - support Kolors model 2024-10-16 00:24:58 +09:00
Dr.Lt.Data 08cdc7358b add opencv-python to requirements.txt 2024-10-13 17:25:11 +09:00
Dr.Lt.Data 81485bcf4c version marker 2024-10-13 17:22:06 +09:00
Dr.Lt.Data f6a8c65094 Merge pull request #173 from geroldmeisinger/main
support named colors via webcolors module
2024-10-13 17:21:10 +09:00
Gerold Meisinger 37bc51713f support named colors via webcolors module 2024-10-12 14:32:44 +02:00
Dr.Lt.Data 004a9534e4 Merge branch 'fix/backward_compatibility' 2024-10-02 01:58:16 +09:00
Dr.Lt.Data ca710caaab Merge pull request #170 from fAIseh00d/main
Add xinsir black pixel support to inpainting controlnet preprocessor
2024-10-02 01:58:07 +09:00
Dr.Lt.Data c657cf152a backward compatiblity patch 2024-10-02 01:56:15 +09:00
Ivan R 9678d685c2 Add xinsir black pixel support 2024-09-30 22:00:24 +05:00
Dr.Lt.Data 51cace6f1c better error message
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/163#issuecomment-2367951331
2024-09-24 01:00:59 +09:00
Dr.Lt.Data c0d54d8a88 fix: KSampler Progress - 1st latent shape mismatch if flux/sd3
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/161
2024-09-16 01:37:12 +09:00
Dr.Lt.Data 74b2856499 feat: LoadPromptFromFile/Dir - reload
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/156
2024-09-08 12:03:51 +09:00
Dr.Lt.Data 25ae8d522f feat: MakeLBW, ApplyLBW, SaveLBW, LoadLBW 2024-09-01 22:46:38 +09:00
Dr.Lt.Data 50d42c30b5 Upgrade LoRA Block Weight
- support % syntax
2024-08-29 01:35:27 +09:00
Dr.Lt.Data 89e24b7529 fixed: LoadImagesFromDirBatch - invalid mask processing
https://github.com/ltdrdata/ComfyUI-Inspire-Pack/issues/151
2024-08-28 20:33:00 +09:00
Dr.Lt.Data 6db6ca54c0 Merge pull request #150 from jesperol/fix_noise_return
fix: return_with_leftover_noise propagation
2024-08-28 10:45:11 +09:00
Jesper Olsson 69b7c486a9 fix: return_with_leftover_noise propagation 2024-08-21 17:22:31 +02:00
Dr.Lt.Data b09b295009 FIXED: mix_noise - device mismatch error 2024-08-21 01:45:10 +09:00
Dr.Lt.Data a15d3362d8 Merge pull request #143 from pixelprotest/feature/load-last-image-batch-from-dir
Add ability to use Load Image Batch from Dir node, to load the last / latest image in a directory
2024-08-20 22:56:12 +09:00
pixelprotest 019713040c Allow min start_index to drop to -1 to make node get last/latest image from dir 2024-08-20 13:30:59 +01:00
Dr.Lt.Data bc075b1a4f version marker 2024-08-18 19:32:37 +09:00
Dr.Lt.Data 452d8cfc00 Merge pull request #142 from anhkhoatranle30/main
Add filepaths as outputs
2024-08-18 19:30:24 +09:00
khoatrn 17716ddb52 feat: Add filepaths as outputs 2024-08-18 16:14:57 +07:00
Dr.Lt.Data 56f4f01f02 fix: LBW - no effect if weight is less than 0 2024-08-17 11:47:18 +09:00
Dr.Lt.Data f64c98714b improve: LBW - efficient patch
fix: LBW - preset patch
2024-08-17 11:28:03 +09:00
Dr.Lt.Data 30974e69c4 feat: LBW for FLUX 2024-08-17 00:36:48 +09:00
Dr.Lt.Data ec29936d16 fix: IPAdapterModelHelper - compatibility patch
- `insightface_model_name` is added.
2024-08-15 21:16:52 +09:00
Dr.Lt.Data 0bd92419de improve: FloatRange - support decrement 2024-08-14 12:43:03 +09:00
Dr.Lt.Data ec6831fd68 version marker 2024-08-07 23:13:40 +09:00
Dr.Lt.Data c876e477e8 Merge pull request #136 from bvhari/main
Add cosine interpolation for Scheduled CFG
2024-08-07 23:13:01 +09:00
BVH 0de0e4232c Change newline to fix diff 2024-08-07 03:14:06 +05:30
BVH 61ba155cf1 Add cosine interpolation for Scheduled CFG 2024-08-04 23:59:46 +05:30
Dr.Lt.Data 09ae2eaa9b KSamplerProgress fix and enhance
fix: ommit_start_latent - doesn't work if it is false
improve: add ommit_final_latent
modified: move progress latent to cpu
2024-08-04 11:22:35 +09:00
Dr.Lt.Data eab0b95df5 Merge pull request #134 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-08-03 15:10:48 +09:00
snomiao badc1c0cb4 chore(licence-update): Update PyProject Toml - License 2024-08-02 23:03:51 +00:00
17 changed files with 933 additions and 153 deletions
+9 -3
View File
@@ -13,12 +13,16 @@ 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.
@@ -57,7 +61,9 @@ This repository offers various extension nodes for ComfyUI. Nodes here have diff
* e.g. `prompts/example`
* `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`
* `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.
+1 -1
View File
@@ -7,7 +7,7 @@
import importlib
version_code = [0, 82, 6]
version_code = [1, 6, 1]
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})")
+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:
+1 -1
View File
@@ -332,7 +332,7 @@ def populate_wildcards(json_data):
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:
+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)
+9 -1
View File
@@ -18,9 +18,14 @@ class FloatRange:
CATEGORY = "InspirePack/Util"
def doit(self, start, stop, step, limit, ensure_end):
if start >= stop or step == 0:
if start == stop or step == 0:
return ([start], )
reverse = False
if start > stop:
reverse = True
start, stop = stop, start
res = []
x = start
last = x
@@ -36,6 +41,9 @@ class FloatRange:
res.append(stop)
if reverse:
res.reverse()
return (res, )
+574 -63
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,22 +368,19 @@ class LoraLoaderBlockWeight:
else:
raise ValueError(f"[LoraLoaderBlockWeight] invalid block_vector '{block_vector}'")
last_k_unet_num = None
new_modelpatcher = model.clone()
populated_ratio = strength_model
def parse_unet_num(s):
if s[1] == '.':
return int(s[0])
else:
return int(s)
# sort: input, middle, output, others
input_blocks = []
middle_blocks = []
output_blocks = []
double_blocks = []
single_blocks = []
others = []
for k, v in loaded.items():
for key, v in loaded.items():
if isinstance(key, tuple):
k = key[0]
else:
k = key
k_unet = k[len("diffusion_model."):]
if k_unet.startswith("input_blocks."):
@@ -178,18 +392,34 @@ class LoraLoaderBlockWeight:
elif k_unet.startswith("output_blocks."):
k_unet_num = k_unet[len("output_blocks."):len("output_blocks.")+2]
output_blocks.append((k, v, parse_unet_num(k_unet_num), k_unet))
elif k_unet.startswith("double_blocks."):
k_unet_num = k_unet[len("double_blocks."):len("double_blocks.")+2]
double_blocks.append((key, v, parse_unet_num(k_unet_num), k_unet))
elif k_unet.startswith("single_blocks."):
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.")+2]
single_blocks.append((key, v, parse_unet_num(k_unet_num), k_unet))
else:
others.append((k, v, k_unet))
input_blocks = sorted(input_blocks, key=lambda x: x[2])
middle_blocks = sorted(middle_blocks, key=lambda x: x[2])
output_blocks = sorted(output_blocks, key=lambda x: x[2])
double_blocks = sorted(double_blocks, key=lambda x: x[2])
single_blocks = sorted(single_blocks, key=lambda x: x[2])
# prepare patch
np.random.seed(seed % (2**31))
populated_vector_list = []
ratios = []
for k, v, k_unet_num, k_unet in (input_blocks + middle_blocks + output_blocks):
ratio = 1.0
vector_i = 1
last_k_unet_num = None
block_weights = {}
muted_weights = []
for k, v, k_unet_num, k_unet in (input_blocks + middle_blocks + output_blocks + double_blocks + single_blocks):
if last_k_unet_num != k_unet_num and len(vector) > vector_i:
ratios = LoraLoaderBlockWeight.convert_vector_value(A, B, vector[vector_i].strip())
ratio = ratios.pop(0)
@@ -205,6 +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
@@ -213,11 +445,10 @@ class LoraLoaderBlockWeight:
last_k_unet_num = k_unet_num
new_modelpatcher.add_patches({k: v}, strength_model * populated_ratio)
# if inverse:
# print(f"\t{k_unet} -> inv({ratio}) ")
# else:
# print(f"\t{k_unet} -> ({ratio}) ")
if populated_ratio != 0:
block_weights[k] = v, populated_ratio
else:
muted_weights.append(k)
# prepare base patch
ratios = LoraLoaderBlockWeight.convert_vector_value(A, B, vector[0].strip())
@@ -226,25 +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
@@ -261,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:
@@ -328,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
@@ -336,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()
@@ -368,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
@@ -485,7 +776,7 @@ class XYInput_LoraBlockWeight:
XY_Capsule_LoraBlockWeight(0, 2, '', 'diff', storage, common_params),
XY_Capsule_LoraBlockWeight(0, 3, '', 'heatmap', storage, common_params)]
return ((xy_type, x_values), (xy_type, y_values), )
return (xy_type, x_values), (xy_type, y_values),
class LoraBlockInfo:
@@ -531,12 +822,29 @@ class LoraBlockInfo:
output_blocks = []
output_blocks_map = {}
text_block_count = set()
text_blocks = []
text_blocks_map = {}
text_block_count1 = set()
text_blocks1 = []
text_blocks_map1 = {}
text_block_count2 = set()
text_blocks2 = []
text_blocks_map2 = {}
double_block_count = set()
double_blocks = []
double_blocks_map = {}
single_block_count = set()
single_blocks = []
single_blocks_map = {}
others = []
for k, v in loaded.items():
for key, v in loaded.items():
if isinstance(key, tuple):
k = key[0]
else:
k = key
k_unet = k[len("diffusion_model."):]
if k_unet.startswith("input_blocks."):
@@ -572,16 +880,49 @@ class LoraBlockInfo:
else:
output_blocks_map[k_unet_int] = [k_unet]
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_block_count.add(k_unet_int)
double_blocks.append(k_unet)
if k_unet_int in double_blocks_map:
double_blocks_map[k_unet_int].append(k_unet)
else:
double_blocks_map[k_unet_int] = [k_unet]
elif k_unet.startswith("single_blocks."):
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.") + 2]
k_unet_int = parse_unet_num(k_unet_num)
single_block_count.add(k_unet_int)
single_blocks.append(k_unet)
if k_unet_int in single_blocks_map:
single_blocks_map[k_unet_int].append(k_unet)
else:
single_blocks_map[k_unet_int] = [k_unet]
elif k_unet.startswith("er.text_model.encoder.layers."):
k_unet_num = k_unet[len("er.text_model.encoder.layers."):len("er.text_model.encoder.layers.")+2]
k_unet_int = parse_unet_num(k_unet_num)
text_block_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)
@@ -591,27 +932,48 @@ class LoraBlockInfo:
input_blocks = sorted(input_blocks)
middle_blocks = sorted(middle_blocks)
output_blocks = sorted(output_blocks)
double_blocks = sorted(double_blocks)
single_blocks = sorted(single_blocks)
others = sorted(others)
text += f"\n-------[Input blocks] ({len(input_block_count)}, Subs={len(input_blocks)})-------\n"
input_keys = sorted(input_blocks_map.keys())
for x in input_keys:
text += f" IN{x}: {len(input_blocks_map[x])}\n"
if len(input_block_count) > 0:
text += f"\n-------[Input blocks] ({len(input_block_count)}, Subs={len(input_blocks)})-------\n"
input_keys = sorted(input_blocks_map.keys())
for x in input_keys:
text += f" IN{x}: {len(input_blocks_map[x])}\n"
text += f"\n-------[Middle blocks] ({len(middle_block_count)}, Subs={len(middle_blocks)})-------\n"
middle_keys = sorted(middle_blocks_map.keys())
for x in middle_keys:
text += f" MID{x}: {len(middle_blocks_map[x])}\n"
if len(middle_block_count) > 0:
text += f"\n-------[Middle blocks] ({len(middle_block_count)}, Subs={len(middle_blocks)})-------\n"
middle_keys = sorted(middle_blocks_map.keys())
for x in middle_keys:
text += f" MID{x}: {len(middle_blocks_map[x])}\n"
text += f"\n-------[Output blocks] ({len(output_block_count)}, Subs={len(output_blocks)})-------\n"
output_keys = sorted(output_blocks_map.keys())
for x in output_keys:
text += f" OUT{x}: {len(output_blocks_map[x])}\n"
if len(output_block_count) > 0:
text += f"\n-------[Output blocks] ({len(output_block_count)}, Subs={len(output_blocks)})-------\n"
output_keys = sorted(output_blocks_map.keys())
for x in output_keys:
text += f" OUT{x}: {len(output_blocks_map[x])}\n"
text += f"\n-------[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"
if len(double_block_count) > 0:
text += f"\n-------[Double blocks(MMDiT)] ({len(double_block_count)}, Subs={len(double_blocks)})-------\n"
double_keys = sorted(double_blocks_map.keys())
for x in double_keys:
text += f" DOUBLE{x}: {len(double_blocks_map[x])}\n"
if len(single_block_count) > 0:
text += f"\n-------[Single blocks(DiT)] ({len(single_block_count)}, Subs={len(single_blocks)})-------\n"
single_keys = sorted(single_blocks_map.keys())
for x in single_keys:
text += f" SINGLE{x}: {len(single_blocks_map[x])}\n"
text += f"\n-------[Base blocks] ({len(text_block_count1) + len(text_block_count2) + len(others)}, Subs={len(text_blocks1) + len(text_blocks2) + len(others)})-------\n"
text_keys1 = sorted(text_blocks_map1.keys())
for x in text_keys1:
text += f" TXT_ENC{x}: {len(text_blocks_map1[x])}\n"
text_keys2 = sorted(text_blocks_map2.keys())
for x in text_keys2:
text += f" TXT_ENC{x} [B]: {len(text_blocks_map2[x])}\n"
for x in others:
text += f" {x}\n"
@@ -628,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",
}
+12 -4
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,13 +58,16 @@ class IPAdapterModelHelper:
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"preset": (list(model_preset.keys()),),
"lora_strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
"lora_strength_clip": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
"insightface_provider": (["CPU", "CUDA", "ROCM"], ),
"cache_mode": (["insightface only", "clip_vision only", "all", "none"], {"default": "insightface only"}),
},
"optional": {
"clip": ("CLIP",),
"insightface_model_name": (['buffalo_l', 'antelopev2'],),
},
"hidden": {"unique_id": "UNIQUE_ID"}
}
@@ -72,14 +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):
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)"})
@@ -157,7 +165,7 @@ class IPAdapterModelHelper:
if cache_mode in ["insightface only", "all"]:
icache_key = 'insightface-' + insightface_provider
if icache_key not in backend_support.cache:
backend_support.update_cache(icache_key, "insightface", (False, insight_face_loader(insightface_provider)[0]))
backend_support.update_cache(icache_key, "insightface", (False, insight_face_loader(provider=insightface_provider, model_name=insightface_model_name)[0]))
_, (_, insightface) = backend_support.cache[icache_key]
else:
insightface = insight_face_loader(insightface_provider)[0]
+150 -42
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,19 +40,28 @@ 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"}),
}
}
RETURN_TYPES = ("ZIPPED_PROMPT",)
OUTPUT_IS_LIST = (True,)
@@ -59,17 +71,56 @@ class LoadPromptsFromDir:
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):
if not reload:
return prompt_dir
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()
@staticmethod
def doit(prompt_dir, reload=False):
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)
@@ -85,7 +136,7 @@ class LoadPromptsFromDir:
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, )
@@ -93,20 +144,27 @@ class LoadPromptsFromDir:
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"}),
}
}
RETURN_TYPES = ("ZIPPED_PROMPT",)
OUTPUT_IS_LIST = (True,)
@@ -116,13 +174,52 @@ class LoadPromptsFromFile:
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):
md5 = hashlib.md5()
if text_data_opt is not None:
md5.update(text_data_opt)
return md5.hexdigest()
elif not reload:
return prompt_file
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()
@staticmethod
def doit(prompt_file, text_data_opt=None, reload=False):
matched_path = None
for d in folder_paths.get_folder_paths('inspire_prompts'):
matched_path = os.path.join(d, prompt_file)
if not os.path.exists(matched_path):
matched_path = None
else:
break
if matched_path:
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
@@ -131,8 +228,8 @@ class LoadPromptsFromFile:
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
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()
@@ -142,7 +239,7 @@ class LoadPromptsFromFile:
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, )
@@ -150,15 +247,16 @@ class LoadPromptsFromFile:
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 +276,17 @@ 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"[WARN] LoadSinglePromptFromFile: file not found '{prompt_file}'")
prompts = []
try:
@@ -205,7 +313,7 @@ class LoadSinglePromptFromFile:
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, )
@@ -566,7 +674,7 @@ class SeedExplorer:
"optional":
{
"variation_method": (["linear", "slerp"],),
"model": ("model",),
"model": ("MODEL",),
}
}
+4 -2
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:
+33 -13
View File
@@ -24,6 +24,7 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
"noise_mode": (["GPU(=A1111)", "CPU"],),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": True, "label_on": "True", "label_off": "False"}),
"omit_final_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"optional": {
"scheduler_func_opt": ("SCHEDULER_FUNC",),
@@ -36,27 +37,29 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
RETURN_NAMES = ("latent", "progress_latent")
@staticmethod
def doit(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, interval, omit_start_latent, scheduler_func_opt=None):
def doit(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
interval, omit_start_latent, omit_final_latent, scheduler_func_opt=None):
adv_steps = int(steps / denoise)
if omit_start_latent:
result = []
else:
result = [latent_image['samples']]
result = []
result = [comfy.sample.fix_empty_latent_channels(model, latent_image['samples']).cpu()]
def progress_callback(step, x0, x, total_steps):
if (total_steps-1) != step and step % interval != 0:
return
x = model.model.process_latent_out(x)
x = x.to(model_management.intermediate_device())
x = x.cpu()
result.append(x)
latent_image, noise = a1111_compat.KSamplerAdvanced_inspire.sample(model, True, seed, adv_steps, cfg, sampler_name, scheduler, positive, negative, latent_image, (adv_steps-steps),
adv_steps, noise_mode, False, callback=progress_callback, scheduler_func_opt=scheduler_func_opt)
if not omit_final_latent:
result.append(latent_image['samples'].cpu())
if len(result) > 0:
result = torch.cat(result)
result = {'samples': result}
@@ -86,6 +89,7 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
"omit_start_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
"omit_final_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"optional": {
"prev_progress_latent_opt": ("LATENT",),
@@ -100,25 +104,28 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
RETURN_TYPES = ("LATENT", "LATENT")
RETURN_NAMES = ("latent", "progress_latent")
def doit(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step,
noise_mode, return_with_leftover_noise, interval, omit_start_latent, prev_progress_latent_opt=None, scheduler_func_opt=None):
def doit(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
start_at_step, end_at_step, noise_mode, return_with_leftover_noise, interval, omit_start_latent, omit_final_latent,
prev_progress_latent_opt=None, scheduler_func_opt=None):
if omit_start_latent:
result = []
else:
result = [latent_image['samples']]
result = []
def progress_callback(step, x0, x, total_steps):
if (total_steps-1) != step and step % interval != 0:
return
x = model.model.process_latent_out(x)
x = x.to(model_management.intermediate_device())
x = x.cpu()
result.append(x)
latent_image, noise = a1111_compat.KSamplerAdvanced_inspire.sample(model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step,
noise_mode, False, callback=progress_callback, 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())
if len(result) > 0:
result = torch.cat(result)
@@ -165,6 +172,15 @@ def logarithmic_interpolation(from_cfg, to_cfg, i, steps):
return from_cfg + (to_cfg - from_cfg) * t
def cosine_interpolation(from_cfg, to_cfg, i, steps):
if (i == 0) or (i == steps-1):
return from_cfg
t = (1.0 + math.cos(math.pi*2*(i/steps))) / 2
return from_cfg + (to_cfg - from_cfg) * t
class Guider_scheduled(CFGGuider):
def __init__(self, model_patcher, sigmas, from_cfg, to_cfg, schedule):
super().__init__(model_patcher)
@@ -194,6 +210,8 @@ class Guider_scheduled(CFGGuider):
self.cfg_sigmas[k] = exponential_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'log':
self.cfg_sigmas[k] = logarithmic_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'cos':
self.cfg_sigmas[k] = cosine_interpolation(self.from_cfg, self.to_cfg, i, steps), i
else:
self.cfg_sigmas[k] = self.from_cfg + delta * i / steps, i
@@ -245,6 +263,8 @@ class Guider_PerpNeg_scheduled(Guider_PerpNeg):
self.cfg_sigmas[k] = exponential_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'log':
self.cfg_sigmas[k] = logarithmic_interpolation(self.from_cfg, self.to_cfg, i, steps), i
elif self.schedule == 'cos':
self.cfg_sigmas[k] = cosine_interpolation(self.from_cfg, self.to_cfg, i, steps), i
else:
self.cfg_sigmas[k] = self.from_cfg + delta * i / steps, i
@@ -276,7 +296,7 @@ class ScheduledCFGGuider:
"sigmas": ("SIGMAS", ),
"from_cfg": ("FLOAT", {"default": 6.5, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"to_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"schedule": (["linear", "log", "exp"], {'default': 'log'})
"schedule": (["linear", "log", "exp", "cos"], {'default': 'log'})
}
}
@@ -303,7 +323,7 @@ class ScheduledPerpNegCFGGuider:
"sigmas": ("SIGMAS", ),
"from_cfg": ("FLOAT", {"default": 6.5, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"to_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01}),
"schedule": (["linear", "log", "exp"], {'default': 'log'})
"schedule": (["linear", "log", "exp", "cos"], {'default': 'log'})
}
}
+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, )
+7 -1
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,6 +26,12 @@ 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";
Object.defineProperty(node.widgets[preset_i], "value", {
+2 -2
View File
@@ -1,8 +1,8 @@
[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.82.6"
license = "LICENSE"
version = "1.6.1"
license = { file = "LICENSE" }
dependencies = ["matplotlib", "cachetools"]
[project.urls]
+3 -1
View File
@@ -1,3 +1,5 @@
matplotlib
cachetools
numpy<2
numpy<2
webcolors
opencv-python
+71 -1
View File
@@ -29,6 +29,16 @@ SDXL-LyC-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
SDXL-LyC-INALL:1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0
SDXL-LyC-MIDALL:1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0
SDXL-LyC-OUTALL:1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1
FLUX-DBL-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
FLUX-DBL-FRONT7:1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-DBL-MID6:1,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0
FLUX-DBL-TAIL6:1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1
FLUX-SINGLE-ALL:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
FLUX-SINGLE-1to10:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-SINGLE-11to20:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
FLUX-SINGLE-21to30:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0
FLUX-SINGLE-31to37:1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1
FLUX-ALL:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
@SD-FULL-TEST:17
@SD-BLOCK1-TEST:17,12,1
@SD-BLOCK2-TEST:17,12,2
@@ -49,4 +59,64 @@ SDXL-LyC-OUTALL:1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1
@SD-BLOCK17-TEST:17,12,17
@SD-LyC-FULL-TEST:27
@SDXL-FULL-TEST:12
@SDXL-LyC-FULL-TEST:21
@SDXL-LyC-FULL-TEST:21
@FLUX-DBL-FULL:19
@FLUX-DBL-SGL-FULL:58
@FLUX-DBL0-TEST:19,14,2
@FLUX-DBL1-TEST:19,14,3
@FLUX-DBL2-TEST:19,14,4
@FLUX-DBL3-TEST:19,14,5
@FLUX-DBL4-TEST:19,14,6
@FLUX-DBL5-TEST:19,14,7
@FLUX-DBL6-TEST:19,14,8
@FLUX-DBL7-TEST:19,14,9
@FLUX-DBL8-TEST:19,14,10
@FLUX-DBL9-TEST:19,14,11
@FLUX-DBL10-TEST:19,14,12
@FLUX-DBL11-TEST:19,14,13
@FLUX-DBL12-TEST:19,14,14
@FLUX-DBL13-TEST:19,14,15
@FLUX-DBL14-TEST:19,14,16
@FLUX-DBL15-TEST:19,14,17
@FLUX-DBL16-TEST:19,14,18
@FLUX-DBL17-TEST:19,14,19
@FLUX-DBL18-TEST:19,14,20
@FLUX-SGL0-TEST:58,6,21
@FLUX-SGL1-TEST:58,6,22
@FLUX-SGL2-TEST:58,6,23
@FLUX-SGL3-TEST:58,6,24
@FLUX-SGL4-TEST:58,6,25
@FLUX-SGL5-TEST:58,6,26
@FLUX-SGL6-TEST:58,6,27
@FLUX-SGL7-TEST:58,6,28
@FLUX-SGL8-TEST:58,6,29
@FLUX-SGL9-TEST:58,6,30
@FLUX-SGL10-TEST:58,6,31
@FLUX-SGL11-TEST:58,6,32
@FLUX-SGL12-TEST:58,6,33
@FLUX-SGL13-TEST:58,6,34
@FLUX-SGL14-TEST:58,6,35
@FLUX-SGL15-TEST:58,6,36
@FLUX-SGL16-TEST:58,6,37
@FLUX-SGL17-TEST:58,6,38
@FLUX-SGL18-TEST:58,6,39
@FLUX-SGL19-TEST:58,6,40
@FLUX-SGL20-TEST:58,6,41
@FLUX-SGL21-TEST:58,6,42
@FLUX-SGL22-TEST:58,6,43
@FLUX-SGL23-TEST:58,6,44
@FLUX-SGL24-TEST:58,6,45
@FLUX-SGL25-TEST:58,6,46
@FLUX-SGL26-TEST:58,6,47
@FLUX-SGL27-TEST:58,6,48
@FLUX-SGL28-TEST:58,6,49
@FLUX-SGL29-TEST:58,6,50
@FLUX-SGL30-TEST:58,6,51
@FLUX-SGL31-TEST:58,6,52
@FLUX-SGL32-TEST:58,6,53
@FLUX-SGL33-TEST:58,6,54
@FLUX-SGL34-TEST:58,6,55
@FLUX-SGL35-TEST:58,6,56
@FLUX-SGL36-TEST:58,6,57
@FLUX-SGL37-TEST:58,6,58
@FLUX-SGL38-TEST:58,6,59