Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04223778ea | ||
|
|
3fc749d6ba | ||
|
|
c2ccf7f879 | ||
|
|
8e99bebdfe | ||
|
|
8814b8901a | ||
|
|
291d3dd13f | ||
|
|
5619e51b01 | ||
|
|
0f38db4180 | ||
|
|
e2d18c21ba | ||
|
|
13fda9df3f | ||
|
|
035f37d1d1 | ||
|
|
9d179f0193 | ||
|
|
5891049f2b | ||
|
|
b35b22d563 | ||
|
|
ca5d942cef | ||
|
|
a9ece5f22e | ||
|
|
a77979c027 | ||
|
|
20ed3bb5b4 | ||
|
|
ecf8ab6f58 | ||
|
|
0a009c1499 | ||
|
|
962c3fbda0 | ||
|
|
6158b5aa44 | ||
|
|
5aae916c66 | ||
|
|
f12b4bd54e | ||
|
|
f64211c3f3 | ||
|
|
0284075392 | ||
|
|
bcbed07560 | ||
|
|
685c5c0d00 | ||
|
|
697f12fbdb | ||
|
|
e8817bc348 | ||
|
|
9c6065af6d | ||
|
|
8db9411e94 | ||
|
|
18f02d98b1 | ||
|
|
cab499e30a | ||
|
|
71a68650fe | ||
|
|
3bcc1d43a0 | ||
|
|
395869085c | ||
|
|
384284fe0b | ||
|
|
c6d53ecd4d | ||
|
|
44cfac4f0a | ||
|
|
bfaaa6c570 | ||
|
|
d29c41809f | ||
|
|
667dead8c1 | ||
|
|
08cdc7358b | ||
|
|
81485bcf4c | ||
|
|
f6a8c65094 | ||
|
|
37bc51713f | ||
|
|
004a9534e4 | ||
|
|
ca710caaab | ||
|
|
c657cf152a | ||
|
|
9678d685c2 | ||
|
|
51cace6f1c | ||
|
|
c0d54d8a88 | ||
|
|
74b2856499 | ||
|
|
25ae8d522f | ||
|
|
50d42c30b5 | ||
|
|
89e24b7529 | ||
|
|
6db6ca54c0 | ||
|
|
69b7c486a9 | ||
|
|
b09b295009 | ||
|
|
a15d3362d8 | ||
|
|
019713040c | ||
|
|
bc075b1a4f | ||
|
|
452d8cfc00 | ||
|
|
17716ddb52 | ||
|
|
56f4f01f02 |
@@ -7,15 +7,19 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ltdrdata' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
This repository offers various extension nodes for ComfyUI. Nodes here have different characteristics compared to those in the ComfyUI Impact Pack. The Impact Pack has become too large now...
|
||||
|
||||
## Notice:
|
||||
* V1.18: To use the 'OSS' Scheduler, please update to ComfyUI version 0.3.28 or later (April 13th or newer) and Impact Pack version V8.11 or higher.
|
||||
* 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 +15,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 +37,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 +52,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 +64,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 +154,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 +169,14 @@ 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)`: An 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'.
|
||||
* `Drop List (Inspire)`: Removes all items from the ITEM_LIST. If the ITEM_LIST generated through this node is passed to ForeachListEnd, the process is immediately terminated.
|
||||
|
||||
### 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 +200,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
|
||||
|
||||
+4
-13
@@ -2,14 +2,15 @@
|
||||
@author: Dr.Lt.Data
|
||||
@title: Inspire Pack
|
||||
@nickname: Inspire Pack
|
||||
@description: This extension provides various nodes to support Lora Block Weight and the Impact Pack.
|
||||
@description: This extension provides various nodes to support Lora Block Weight, Regional Nodes, Backend Cache, Prompt Utils, List Utils and the Impact Pack.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
version_code = [0, 85, 1]
|
||||
version_code = [1, 21]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
print(f"### Loading: ComfyUI-Inspire-Pack ({version_str})")
|
||||
logging.info(f"### Loading: ComfyUI-Inspire-Pack ({version_str})")
|
||||
|
||||
node_list = [
|
||||
"lora_block_weight",
|
||||
@@ -38,13 +39,3 @@ for module_name in node_list:
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
try:
|
||||
import cm_global
|
||||
cm_global.register_extension('ComfyUI-Inspire-Pack',
|
||||
{'version': version_code,
|
||||
'name': 'Inspire Pack',
|
||||
'nodes': set(NODE_CLASS_MAPPINGS.keys()),
|
||||
'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.', })
|
||||
except:
|
||||
pass
|
||||
|
||||
+57
-31
@@ -5,12 +5,20 @@ from einops import rearrange
|
||||
import random
|
||||
import math
|
||||
from .libs import common
|
||||
import logging
|
||||
|
||||
|
||||
supported_noise_modes = ["GPU(=A1111)", "CPU", "GPU+internal_seed", "CPU+internal_seed"]
|
||||
|
||||
|
||||
class Inspire_RandomNoise:
|
||||
def __init__(self, seed, mode, incremental_seed_mode, variation_seed, variation_strength, variation_method="linear"):
|
||||
def __init__(self, seed, mode, incremental_seed_mode, variation_seed, variation_strength, variation_method="linear", internal_seed=None):
|
||||
device = comfy.model_management.get_torch_device()
|
||||
self.seed = seed
|
||||
# HOTFIX: https://github.com/comfyanonymous/ComfyUI/commit/916d1e14a93ef331adef7c0deff2fdcf443b05cf#commitcomment-151914788
|
||||
# seed value should be different with generated noise
|
||||
|
||||
self.seed = internal_seed
|
||||
self.noise_seed = seed
|
||||
self.noise_device = "cpu" if mode == "CPU" else device
|
||||
self.incremental_seed_mode = incremental_seed_mode
|
||||
self.variation_seed = variation_seed
|
||||
@@ -20,7 +28,7 @@ class Inspire_RandomNoise:
|
||||
def generate_noise(self, input_latent):
|
||||
latent_image = input_latent["samples"]
|
||||
batch_inds = input_latent["batch_index"] if "batch_index" in input_latent else None
|
||||
noise = utils.prepare_noise(latent_image, self.seed, batch_inds, self.noise_device, self.incremental_seed_mode,
|
||||
noise = utils.prepare_noise(latent_image, self.noise_seed, batch_inds, self.noise_device, self.incremental_seed_mode,
|
||||
variation_seed=self.variation_seed, variation_strength=self.variation_strength, variation_method=self.variation_method)
|
||||
return noise.cpu()
|
||||
|
||||
@@ -29,30 +37,36 @@ class RandomNoise:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
|
||||
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
},
|
||||
"optional":
|
||||
{"variation_method": (["linear", "slerp"],), }
|
||||
{
|
||||
"variation_method": (["linear", "slerp"],),
|
||||
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("NOISE",)
|
||||
FUNCTION = "get_noise"
|
||||
CATEGORY = "InspirePack/a1111_compat"
|
||||
|
||||
def get_noise(self, noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method="linear"):
|
||||
return (Inspire_RandomNoise(noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method=variation_method),)
|
||||
def get_noise(self, noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method="linear", internal_seed=None):
|
||||
if internal_seed is None:
|
||||
internal_seed = noise_seed
|
||||
|
||||
return (Inspire_RandomNoise(noise_seed, noise_mode, batch_seed_mode, variation_seed, variation_strength, variation_method=variation_method, internal_seed=internal_seed),)
|
||||
|
||||
|
||||
def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0,
|
||||
noise_mode="CPU", disable_noise=False, start_step=None, last_step=None, force_full_denoise=False,
|
||||
incremental_seed_mode="comfy", variation_seed=None, variation_strength=None, noise=None, callback=None, variation_method="linear",
|
||||
scheduler_func=None):
|
||||
scheduler_func=None, internal_seed=None):
|
||||
device = comfy.model_management.get_torch_device()
|
||||
noise_device = "cpu" if noise_mode == "CPU" else device
|
||||
noise_device = "cpu" if 'cpu' in noise_mode.lower() else device
|
||||
latent_image = latent["samples"]
|
||||
if hasattr(comfy.sample, 'fix_empty_latent_channels'):
|
||||
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
|
||||
@@ -60,7 +74,7 @@ def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
|
||||
latent = latent.copy()
|
||||
|
||||
if noise is not None and latent_image.shape[1] != noise.shape[1]:
|
||||
print("[Inspire Pack] inspire_ksampler: The type of latent input for noise generation does not match the model's latent type. When using the SD3 model, you must use the SD3 Empty Latent.")
|
||||
logging.info("[Inspire Pack] inspire_ksampler: The type of latent input for noise generation does not match the model's latent type. When using the SD3 model, you must use the SD3 Empty Latent.")
|
||||
raise Exception("The type of latent input for noise generation does not match the model's latent type. When using the SD3 model, you must use the SD3 Empty Latent.")
|
||||
|
||||
if noise is None:
|
||||
@@ -80,6 +94,12 @@ def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
|
||||
start_step = advanced_steps - steps
|
||||
steps = advanced_steps
|
||||
|
||||
if internal_seed is None:
|
||||
internal_seed = seed
|
||||
|
||||
if 'internal_seed' in noise_mode:
|
||||
seed = internal_seed
|
||||
|
||||
try:
|
||||
samples = common.impact_sampling(
|
||||
model=model, add_noise=not disable_noise, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative,
|
||||
@@ -87,7 +107,7 @@ def inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
|
||||
scheduler_func=scheduler_func)
|
||||
except Exception as e:
|
||||
if "unexpected keyword argument 'scheduler_func'" in str(e):
|
||||
print(f"[Inspire Pack] Impact Pack is outdated. (Cannot use GITS scheduler.)")
|
||||
logging.info("[Inspire Pack] Impact Pack is outdated. (Cannot use GITS scheduler.)")
|
||||
|
||||
samples = common.impact_sampling(
|
||||
model=model, add_noise=not disable_noise, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative,
|
||||
@@ -103,7 +123,7 @@ class KSampler_inspire:
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"model": ("MODEL",),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
@@ -112,7 +132,7 @@ class KSampler_inspire:
|
||||
"negative": ("CONDITIONING", ),
|
||||
"latent_image": ("LATENT", ),
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (supported_noise_modes,),
|
||||
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
|
||||
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
@@ -121,6 +141,7 @@ class KSampler_inspire:
|
||||
{
|
||||
"variation_method": (["linear", "slerp"],),
|
||||
"scheduler_func_opt": ("SCHEDULER_FUNC",),
|
||||
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +152,11 @@ class KSampler_inspire:
|
||||
|
||||
@staticmethod
|
||||
def doit(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
|
||||
batch_seed_mode="comfy", variation_seed=None, variation_strength=None, variation_method="linear", scheduler_func_opt=None):
|
||||
batch_seed_mode="comfy", variation_seed=None, variation_strength=None, variation_method="linear", scheduler_func_opt=None,
|
||||
internal_seed=None):
|
||||
return (inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode,
|
||||
incremental_seed_mode=batch_seed_mode, variation_seed=variation_seed, variation_strength=variation_strength, variation_method=variation_method,
|
||||
scheduler_func=scheduler_func_opt)[0], )
|
||||
scheduler_func=scheduler_func_opt, internal_seed=internal_seed)[0], )
|
||||
|
||||
|
||||
class KSamplerAdvanced_inspire:
|
||||
@@ -143,7 +165,7 @@ class KSamplerAdvanced_inspire:
|
||||
return {"required":
|
||||
{"model": ("MODEL",),
|
||||
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
@@ -153,7 +175,7 @@ class KSamplerAdvanced_inspire:
|
||||
"latent_image": ("LATENT", ),
|
||||
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (supported_noise_modes,),
|
||||
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
|
||||
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
|
||||
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
@@ -162,8 +184,9 @@ class KSamplerAdvanced_inspire:
|
||||
"optional":
|
||||
{
|
||||
"variation_method": (["linear", "slerp"],),
|
||||
"noise_opt": ("NOISE",),
|
||||
"noise_opt": ("NOISE_IMAGE",),
|
||||
"scheduler_func_opt": ("SCHEDULER_FUNC",),
|
||||
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +197,7 @@ class KSamplerAdvanced_inspire:
|
||||
|
||||
@staticmethod
|
||||
def sample(model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise,
|
||||
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, callback=None, variation_method="linear", scheduler_func_opt=None):
|
||||
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, callback=None, variation_method="linear", scheduler_func_opt=None, internal_seed=None):
|
||||
force_full_denoise = True
|
||||
|
||||
if return_with_leftover_noise:
|
||||
@@ -189,7 +212,7 @@ class KSamplerAdvanced_inspire:
|
||||
denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step,
|
||||
force_full_denoise=force_full_denoise, noise_mode=noise_mode, incremental_seed_mode=batch_seed_mode,
|
||||
variation_seed=variation_seed, variation_strength=variation_strength, noise=noise_opt, callback=callback, variation_method=variation_method,
|
||||
scheduler_func=scheduler_func_opt)
|
||||
scheduler_func=scheduler_func_opt, internal_seed=internal_seed)
|
||||
|
||||
def doit(self, *args, **kwargs):
|
||||
return (self.sample(*args, **kwargs)[0],)
|
||||
@@ -200,14 +223,14 @@ class KSampler_inspire_pipe:
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"basic_pipe": ("BASIC_PIPE",),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
"scheduler": (common.SCHEDULERS, ),
|
||||
"latent_image": ("LATENT", ),
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (supported_noise_modes,),
|
||||
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
|
||||
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"variation_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
@@ -215,6 +238,7 @@ class KSampler_inspire_pipe:
|
||||
"optional":
|
||||
{
|
||||
"scheduler_func_opt": ("SCHEDULER_FUNC",),
|
||||
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,10 +248,10 @@ class KSampler_inspire_pipe:
|
||||
CATEGORY = "InspirePack/a1111_compat"
|
||||
|
||||
def sample(self, basic_pipe, seed, steps, cfg, sampler_name, scheduler, latent_image, denoise, noise_mode, batch_seed_mode="comfy",
|
||||
variation_seed=None, variation_strength=None, scheduler_func_opt=None):
|
||||
variation_seed=None, variation_strength=None, scheduler_func_opt=None, internal_seed=None):
|
||||
model, clip, vae, positive, negative = basic_pipe
|
||||
latent = inspire_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, noise_mode, incremental_seed_mode=batch_seed_mode,
|
||||
variation_seed=variation_seed, variation_strength=variation_strength, scheduler_func=scheduler_func_opt)[0]
|
||||
variation_seed=variation_seed, variation_strength=variation_strength, scheduler_func=scheduler_func_opt, internal_seed=internal_seed)[0]
|
||||
return latent, vae
|
||||
|
||||
|
||||
@@ -237,7 +261,7 @@ class KSamplerAdvanced_inspire_pipe:
|
||||
return {"required":
|
||||
{"basic_pipe": ("BASIC_PIPE",),
|
||||
"add_noise": ("BOOLEAN", {"default": True, "label_on": "enable", "label_off": "disable"}),
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed for the initial noise applied to the latent."}),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.5, "round": 0.01}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
@@ -245,7 +269,7 @@ class KSamplerAdvanced_inspire_pipe:
|
||||
"latent_image": ("LATENT", ),
|
||||
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (supported_noise_modes,),
|
||||
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
|
||||
"batch_seed_mode": (["incremental", "comfy", "variation str inc:0.01", "variation str inc:0.05"],),
|
||||
"variation_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
@@ -253,8 +277,9 @@ class KSamplerAdvanced_inspire_pipe:
|
||||
},
|
||||
"optional":
|
||||
{
|
||||
"noise_opt": ("NOISE",),
|
||||
"noise_opt": ("NOISE_IMAGE",),
|
||||
"scheduler_func_opt": ("SCHEDULER_FUNC",),
|
||||
"internal_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "This is the seed used for generating noise in intermediate steps when using ancestral and SDE-based samplers.\nNOTE: If `noise_mode` is in GPU mode and `internal_seed` is the same as `seed`, the generated image may be distorted."}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +289,7 @@ class KSamplerAdvanced_inspire_pipe:
|
||||
CATEGORY = "InspirePack/a1111_compat"
|
||||
|
||||
def sample(self, basic_pipe, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, latent_image, start_at_step, end_at_step, noise_mode, return_with_leftover_noise,
|
||||
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, scheduler_func_opt=None):
|
||||
denoise=1.0, batch_seed_mode="comfy", variation_seed=None, variation_strength=None, noise_opt=None, scheduler_func_opt=None, internal_seed=None):
|
||||
model, clip, vae, positive, negative = basic_pipe
|
||||
latent = KSamplerAdvanced_inspire().sample(model=model, add_noise=add_noise, noise_seed=noise_seed,
|
||||
steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler,
|
||||
@@ -272,7 +297,8 @@ class KSamplerAdvanced_inspire_pipe:
|
||||
start_at_step=start_at_step, end_at_step=end_at_step,
|
||||
noise_mode=noise_mode, return_with_leftover_noise=return_with_leftover_noise,
|
||||
denoise=denoise, batch_seed_mode=batch_seed_mode, variation_seed=variation_seed,
|
||||
variation_strength=variation_strength, noise_opt=noise_opt, scheduler_func_opt=scheduler_func_opt)[0]
|
||||
variation_strength=variation_strength, noise_opt=noise_opt, scheduler_func_opt=scheduler_func_opt,
|
||||
internal_seed=internal_seed)[0]
|
||||
return latent, vae
|
||||
|
||||
|
||||
@@ -387,7 +413,7 @@ class HyperTileInspire:
|
||||
nh = random_divisor(h, latent_tile_size * factor, swap_size, rand_obj)
|
||||
nw = random_divisor(w, latent_tile_size * factor, swap_size, rand_obj)
|
||||
|
||||
print(f"factor: {factor} <--- params.depth: {apply_to.index(model_chans)} / scale_depth: {scale_depth} / latent_tile_size={latent_tile_size}")
|
||||
logging.debug(f"factor: {factor} <--- params.depth: {apply_to.index(model_chans)} / scale_depth: {scale_depth} / latent_tile_size={latent_tile_size}")
|
||||
# print(f"h: {h}, w:{w} --> nh: {nh}, nw: {nw}")
|
||||
|
||||
if nh * nw > 1:
|
||||
@@ -396,7 +422,7 @@ class HyperTileInspire:
|
||||
# else:
|
||||
# temp = None
|
||||
|
||||
print(f"q={q} / k={k} / v={v}")
|
||||
logging.debug(f"q={q} / k={k} / v={v}")
|
||||
return q, k, v
|
||||
|
||||
return q, k, v
|
||||
|
||||
+270
-19
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from .libs import common
|
||||
|
||||
import folder_paths
|
||||
import nodes
|
||||
@@ -7,13 +8,15 @@ from server import PromptServer
|
||||
|
||||
from .libs.utils import TaggedCache, any_typ
|
||||
|
||||
import logging
|
||||
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
settings_file = os.path.join(root_dir, 'cache_settings.json')
|
||||
try:
|
||||
with open(settings_file) as f:
|
||||
cache_settings = json.load(f)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.error(e)
|
||||
cache_settings = {}
|
||||
cache = TaggedCache(cache_settings)
|
||||
cache_count = {}
|
||||
@@ -57,11 +60,13 @@ class CacheBackendData:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def doit(self, key, tag, data):
|
||||
@staticmethod
|
||||
def doit(key, tag, data):
|
||||
global cache
|
||||
|
||||
if key == '*':
|
||||
print(f"[Inspire Pack] CacheBackendData: '*' is reserved key. Cannot use that key")
|
||||
logging.warning("[Inspire Pack] CacheBackendData: '*' is reserved key. Cannot use that key")
|
||||
return (None,)
|
||||
|
||||
update_cache(key, tag, (False, data))
|
||||
return (data,)
|
||||
@@ -87,7 +92,8 @@ class CacheBackendDataNumberKey:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def doit(self, key, tag, data):
|
||||
@staticmethod
|
||||
def doit(key, tag, data):
|
||||
global cache
|
||||
|
||||
update_cache(key, tag, (False, data))
|
||||
@@ -117,11 +123,13 @@ class CacheBackendDataList:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def doit(self, key, tag, data):
|
||||
@staticmethod
|
||||
def doit(key, tag, data):
|
||||
global cache
|
||||
|
||||
if key == '*':
|
||||
print(f"[Inspire Pack] CacheBackendDataList: '*' is reserved key. Cannot use that key")
|
||||
logging.warning("[Inspire Pack] CacheBackendDataList: '*' is reserved key. Cannot use that key")
|
||||
return (None,)
|
||||
|
||||
update_cache(key[0], tag[0], (True, data))
|
||||
return (data,)
|
||||
@@ -180,8 +188,8 @@ class RetrieveBackendData:
|
||||
v = cache.get(key)
|
||||
|
||||
if v is None:
|
||||
print(f"[RetrieveBackendData] '{key}' is unregistered key.")
|
||||
return (None,)
|
||||
logging.warning(f"[RetrieveBackendData] '{key}' is unregistered key.")
|
||||
return ([None],)
|
||||
|
||||
is_list, data = v[1]
|
||||
|
||||
@@ -235,7 +243,7 @@ class RemoveBackendData:
|
||||
elif key in cache:
|
||||
del cache[key]
|
||||
else:
|
||||
print(f"[Inspire Pack] RemoveBackendData: invalid data key {key}")
|
||||
logging.warning(f"[Inspire Pack] RemoveBackendData: invalid data key {key}")
|
||||
|
||||
return (signal_opt,)
|
||||
|
||||
@@ -259,7 +267,7 @@ class RemoveBackendDataNumberKey(RemoveBackendData):
|
||||
if key in cache:
|
||||
del cache[key]
|
||||
else:
|
||||
print(f"[Inspire Pack] RemoveBackendDataNumberKey: invalid data key {key}")
|
||||
logging.warning(f"[Inspire Pack] RemoveBackendDataNumberKey: invalid data key {key}")
|
||||
|
||||
return (signal_opt,)
|
||||
|
||||
@@ -319,7 +327,6 @@ class ShowCachedInfo:
|
||||
# tag settings is not changed
|
||||
return
|
||||
|
||||
# print(f'set to {new_tag_settings}')
|
||||
new_cache = TaggedCache(new_tag_settings)
|
||||
for k, v in cache.items():
|
||||
new_cache[k] = v
|
||||
@@ -367,10 +374,10 @@ class CheckpointLoaderSimpleShared(nodes.CheckpointLoaderSimple):
|
||||
res = self.load_checkpoint(ckpt_name)
|
||||
update_cache(key, "ckpt", (False, res))
|
||||
cache_kind = 'ckpt'
|
||||
print(f"[Inspire Pack] CheckpointLoaderSimpleShared: Ckpt '{ckpt_name}' is cached to '{key}'.")
|
||||
logging.info(f"[Inspire Pack] CheckpointLoaderSimpleShared: Ckpt '{ckpt_name}' is cached to '{key}'.")
|
||||
else:
|
||||
cache_kind, (_, res) = cache[key]
|
||||
print(f"[Inspire Pack] CheckpointLoaderSimpleShared: Cached ckpt '{key}' is loaded. (Loading skip)")
|
||||
logging.info(f"[Inspire Pack] CheckpointLoaderSimpleShared: Cached ckpt '{key}' is loaded. (Loading skip)")
|
||||
|
||||
if cache_kind == 'ckpt':
|
||||
model, clip, vae = res
|
||||
@@ -400,6 +407,174 @@ class CheckpointLoaderSimpleShared(nodes.CheckpointLoaderSimple):
|
||||
return (None, cache_weak_hash(key))
|
||||
|
||||
|
||||
class LoadDiffusionModelShared(nodes.UNETLoader):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "model_name": (folder_paths.get_filename_list("diffusion_models"), {"tooltip": "Diffusion Model Name"}),
|
||||
"weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],),
|
||||
"key_opt": ("STRING", {"multiline": False, "placeholder": "If empty, use 'model_name' as the key."}),
|
||||
"mode": (['Auto', 'Override Cache', 'Read Only'],),
|
||||
}
|
||||
}
|
||||
RETURN_TYPES = ("MODEL", "STRING")
|
||||
RETURN_NAMES = ("model", "cache key")
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Backend"
|
||||
|
||||
def doit(self, model_name, weight_dtype, key_opt, mode='Auto'):
|
||||
if mode == 'Read Only':
|
||||
if key_opt.strip() == '':
|
||||
raise Exception("[LoadDiffusionModelShared] key_opt cannot be omit if mode is 'Read Only'")
|
||||
key = key_opt.strip()
|
||||
elif key_opt.strip() == '':
|
||||
key = f"{model_name}_{weight_dtype}"
|
||||
else:
|
||||
key = key_opt.strip()
|
||||
|
||||
if key not in cache or mode == 'Override Cache':
|
||||
model = self.load_unet(model_name, weight_dtype)[0]
|
||||
update_cache(key, "diffusion", (False, model))
|
||||
logging.info(f"[Inspire Pack] LoadDiffusionModelShared: diffusion model '{model_name}' is cached to '{key}'.")
|
||||
else:
|
||||
_, (_, model) = cache[key]
|
||||
logging.info(f"[Inspire Pack] LoadDiffusionModelShared: Cached diffusion model '{key}' is loaded. (Loading skip)")
|
||||
|
||||
return model, key
|
||||
|
||||
@staticmethod
|
||||
def IS_CHANGED(model_name, weight_dtype, key_opt, mode='Auto'):
|
||||
if mode == 'Read Only':
|
||||
if key_opt.strip() == '':
|
||||
raise Exception("[LoadDiffusionModelShared] key_opt cannot be omit if mode is 'Read Only'")
|
||||
key = key_opt.strip()
|
||||
elif key_opt.strip() == '':
|
||||
key = f"{model_name}_{weight_dtype}"
|
||||
else:
|
||||
key = key_opt.strip()
|
||||
|
||||
if mode == 'Read Only':
|
||||
return None, cache_weak_hash(key)
|
||||
elif mode == 'Override Cache':
|
||||
return model_name, key
|
||||
|
||||
return None, cache_weak_hash(key)
|
||||
|
||||
|
||||
class LoadTextEncoderShared:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "model_name1": (folder_paths.get_filename_list("text_encoders"), ),
|
||||
"model_name2": (["None"] + folder_paths.get_filename_list("text_encoders"), ),
|
||||
"model_name3": (["None"] + folder_paths.get_filename_list("text_encoders"), ),
|
||||
"type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "sdxl", "flux", "hunyuan_video"], ),
|
||||
"key_opt": ("STRING", {"multiline": False, "placeholder": "If empty, use 'model_name' as the key."}),
|
||||
"mode": (['Auto', 'Override Cache', 'Read Only'],),
|
||||
},
|
||||
"optional": { "device": (["default", "cpu"], {"advanced": True}), }
|
||||
}
|
||||
RETURN_TYPES = ("CLIP", "STRING")
|
||||
RETURN_NAMES = ("clip", "cache key")
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Backend"
|
||||
|
||||
DESCRIPTION = \
|
||||
("[Recipes single]\n"
|
||||
"stable_diffusion: clip-l\n"
|
||||
"stable_cascade: clip-g\n"
|
||||
"sd3: t5 / clip-g / clip-l\n"
|
||||
"stable_audio: t5\n"
|
||||
"mochi: t5\n"
|
||||
"cosmos: old t5 xxl\n\n"
|
||||
"[Recipes dual]\n"
|
||||
"sdxl: clip-l, clip-g\n"
|
||||
"sd3: clip-l, clip-g / clip-l, t5 / clip-g, t5\n"
|
||||
"flux: clip-l, t5\n\n"
|
||||
"[Recipes triple]\n"
|
||||
"sd3: clip-l, clip-g, t5")
|
||||
|
||||
def doit(self, model_name1, model_name2, model_name3, type, key_opt, mode='Auto', device="default"):
|
||||
if mode == 'Read Only':
|
||||
if key_opt.strip() == '':
|
||||
raise Exception("[LoadTextEncoderShared] key_opt cannot be omit if mode is 'Read Only'")
|
||||
key = key_opt.strip()
|
||||
elif key_opt.strip() == '':
|
||||
key = model_name1
|
||||
if model_name2 is not None:
|
||||
key += f"_{model_name2}"
|
||||
if model_name3 is not None:
|
||||
key += f"_{model_name3}"
|
||||
key += f"_{type}_{device}"
|
||||
else:
|
||||
key = key_opt.strip()
|
||||
|
||||
if key not in cache or mode == 'Override Cache':
|
||||
if model_name2 != "None" and model_name3 != "None": # triple text encoder
|
||||
if len({model_name1, model_name2, model_name3}) < 3:
|
||||
logging.error("[LoadTextEncoderShared] The same model has been selected multiple times.")
|
||||
raise ValueError("The same model has been selected multiple times.")
|
||||
|
||||
if type not in ["sd3"]:
|
||||
logging.error("[LoadTextEncoderShared] Currently, the triple text encoder is only supported in `sd3`.")
|
||||
raise ValueError("Currently, the triple text encoder is only supported in `sd3`.")
|
||||
|
||||
res = nodes.NODE_CLASS_MAPPINGS["TripleCLIPLoader"]().load_clip(model_name1, model_name2, model_name3)[0]
|
||||
|
||||
elif model_name2 != "None" or model_name3 != "None": # dual text encoder
|
||||
second_model = model_name2 if model_name2 != "None" else model_name3
|
||||
|
||||
if model_name1 == second_model:
|
||||
logging.error("[LoadTextEncoderShared] You have selected the same model for both.")
|
||||
raise ValueError("[LoadTextEncoderShared] You have selected the same model for both.")
|
||||
|
||||
if type not in ["sdxl", "sd3", "flux", "hunyuan_video"]:
|
||||
logging.error("[LoadTextEncoderShared] Currently, the triple text encoder is only supported in `sdxl, sd3, flux, hunyuan_video`.")
|
||||
raise ValueError("Currently, the triple text encoder is only supported in `sdxl, sd3, flux, hunyuan_video`.")
|
||||
|
||||
res = nodes.NODE_CLASS_MAPPINGS["DualCLIPLoader"]().load_clip(model_name1, second_model, type=type, device=device)[0]
|
||||
|
||||
else: # single text encoder
|
||||
if type not in ["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos"]:
|
||||
logging.error("[LoadTextEncoderShared] Currently, the single text encoder is only supported in `stable_diffusion, stable_cascade, sd3, stable_audio, mochi, ltxv, pixart, cosmos`.")
|
||||
raise ValueError("Currently, the single text encoder is only supported in `stable_diffusion, stable_cascade, sd3, stable_audio, mochi, ltxv, pixart, cosmos`.")
|
||||
|
||||
res = nodes.NODE_CLASS_MAPPINGS["CLIPLoader"]().load_clip(model_name1, type=type, device=device)[0]
|
||||
|
||||
update_cache(key, "diffusion", (False, res))
|
||||
logging.info(f"[Inspire Pack] LoadTextEncoderShared: text encoder model set is cached to '{key}'.")
|
||||
else:
|
||||
_, (_, res) = cache[key]
|
||||
logging.info(f"[Inspire Pack] LoadTextEncoderShared: Cached text encoder model set '{key}' is loaded. (Loading skip)")
|
||||
|
||||
return res, key
|
||||
|
||||
@staticmethod
|
||||
def IS_CHANGED(model_name1, model_name2, model_name3, type, key_opt, mode='Auto', device="default"):
|
||||
if mode == 'Read Only':
|
||||
if key_opt.strip() == '':
|
||||
raise Exception("[LoadTextEncoderShared] key_opt cannot be omit if mode is 'Read Only'")
|
||||
key = key_opt.strip()
|
||||
elif key_opt.strip() == '':
|
||||
key = model_name1
|
||||
if model_name2 is not None:
|
||||
key += f"_{model_name2}"
|
||||
if model_name3 is not None:
|
||||
key += f"_{model_name3}"
|
||||
key += f"_{type}_{device}"
|
||||
else:
|
||||
key = key_opt.strip()
|
||||
|
||||
if mode == 'Read Only':
|
||||
return None, cache_weak_hash(key)
|
||||
elif mode == 'Override Cache':
|
||||
return f"{model_name1}_{model_name2}_{model_name3}_{type}_{device}", key
|
||||
|
||||
return None, cache_weak_hash(key)
|
||||
|
||||
|
||||
class StableCascade_CheckpointLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -455,10 +630,10 @@ class StableCascade_CheckpointLoader:
|
||||
if key_b not in cache:
|
||||
res_b = nodes.CheckpointLoaderSimple().load_checkpoint(ckpt_name=stage_b)
|
||||
update_cache(key_b, "ckpt", (False, res_b))
|
||||
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_b}' is cached to '{key_b}'.")
|
||||
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_b}' is cached to '{key_b}'.")
|
||||
else:
|
||||
_, (_, res_b) = cache[key_b]
|
||||
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_b}' is loaded. (Loading skip)")
|
||||
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_b}' is loaded. (Loading skip)")
|
||||
b_model, clip, b_vae = res_b
|
||||
else:
|
||||
b_model, clip, b_vae = nodes.CheckpointLoaderSimple().load_checkpoint(ckpt_name=stage_b)
|
||||
@@ -467,10 +642,10 @@ class StableCascade_CheckpointLoader:
|
||||
if key_c not in cache:
|
||||
res_c = nodes.unCLIPCheckpointLoader().load_checkpoint(ckpt_name=stage_c)
|
||||
update_cache(key_c, "unclip_ckpt", (False, res_c))
|
||||
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_c}' is cached to '{key_c}'.")
|
||||
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Ckpt '{stage_c}' is cached to '{key_c}'.")
|
||||
else:
|
||||
_, (_, res_c) = cache[key_c]
|
||||
print(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_c}' is loaded. (Loading skip)")
|
||||
logging.info(f"[Inspire Pack] StableCascade_CheckpointLoader: Cached ckpt '{key_c}' is loaded. (Loading skip)")
|
||||
c_model, _, c_vae, clip_vision = res_c
|
||||
else:
|
||||
c_model, _, c_vae, clip_vision = nodes.unCLIPCheckpointLoader().load_checkpoint(ckpt_name=stage_c)
|
||||
@@ -478,6 +653,74 @@ class StableCascade_CheckpointLoader:
|
||||
return b_model, b_vae, c_model, c_vae, clip_vision, clip, key_b, key_c
|
||||
|
||||
|
||||
class IsCached:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"key": ("STRING", {"multiline": False}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("BOOLEAN", )
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Backend"
|
||||
|
||||
@staticmethod
|
||||
def IS_CHANGED(key, unique_id):
|
||||
return common.is_changed(unique_id, key in cache)
|
||||
|
||||
def doit(self, key, unique_id):
|
||||
return (key in cache,)
|
||||
|
||||
|
||||
# WIP: not properly working, yet
|
||||
class CacheBridge:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"value": (any_typ,),
|
||||
"mode": ("BOOLEAN", {"default": True, "label_off": "cached", "label_on": "passthrough"}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_typ, )
|
||||
RETURN_NAMES = ("value",)
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Backend"
|
||||
|
||||
@staticmethod
|
||||
def IS_CHANGED(value, mode, unique_id):
|
||||
if not mode and unique_id in common.changed_cache:
|
||||
return common.not_changed_value(unique_id)
|
||||
else:
|
||||
return common.changed_value(unique_id)
|
||||
|
||||
def doit(self, value, mode, unique_id):
|
||||
if not mode:
|
||||
# cache mode
|
||||
if unique_id not in common.changed_cache:
|
||||
common.changed_cache[unique_id] = value
|
||||
common.changed_count_cache[unique_id] = 0
|
||||
|
||||
return (common.changed_cache[unique_id],)
|
||||
else:
|
||||
common.changed_cache[unique_id] = value
|
||||
common.changed_count_cache[unique_id] = 0
|
||||
|
||||
return (common.changed_cache[unique_id],)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CacheBackendData //Inspire": CacheBackendData,
|
||||
"CacheBackendDataNumberKey //Inspire": CacheBackendDataNumberKey,
|
||||
@@ -489,7 +732,11 @@ NODE_CLASS_MAPPINGS = {
|
||||
"RemoveBackendDataNumberKey //Inspire": RemoveBackendDataNumberKey,
|
||||
"ShowCachedInfo //Inspire": ShowCachedInfo,
|
||||
"CheckpointLoaderSimpleShared //Inspire": CheckpointLoaderSimpleShared,
|
||||
"StableCascade_CheckpointLoader //Inspire": StableCascade_CheckpointLoader
|
||||
"LoadDiffusionModelShared //Inspire": LoadDiffusionModelShared,
|
||||
"LoadTextEncoderShared //Inspire": LoadTextEncoderShared,
|
||||
"StableCascade_CheckpointLoader //Inspire": StableCascade_CheckpointLoader,
|
||||
"IsCached //Inspire": IsCached,
|
||||
# "CacheBridge //Inspire": CacheBridge,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -503,5 +750,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RemoveBackendDataNumberKey //Inspire": "Remove Backend Data [NumberKey] (Inspire)",
|
||||
"ShowCachedInfo //Inspire": "Show Cached Info (Inspire)",
|
||||
"CheckpointLoaderSimpleShared //Inspire": "Shared Checkpoint Loader (Inspire)",
|
||||
"StableCascade_CheckpointLoader //Inspire": "Stable Cascade Checkpoint Loader (Inspire)"
|
||||
"LoadDiffusionModelShared //Inspire": "Shared Diffusion Model Loader (Inspire)",
|
||||
"LoadTextEncoderShared //Inspire": "Shared Text Encoder Loader (Inspire)",
|
||||
"StableCascade_CheckpointLoader //Inspire": "Stable Cascade Checkpoint Loader (Inspire)",
|
||||
"IsCached //Inspire": "Is Cached (Inspire)",
|
||||
# "CacheBridge //Inspire": "Cache Bridge (Inspire)"
|
||||
}
|
||||
|
||||
@@ -3,24 +3,30 @@ import nodes
|
||||
import inspect
|
||||
from .libs import utils
|
||||
from nodes import MAX_RESOLUTION
|
||||
import logging
|
||||
|
||||
|
||||
class ConcatConditioningsWithMultiplier:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
flex_inputs = {}
|
||||
|
||||
stack = inspect.stack()
|
||||
if stack[1].function == 'get_input_data':
|
||||
if stack[1].function == 'get_input_info':
|
||||
# bypass validation
|
||||
for x in range(0, 100):
|
||||
flex_inputs[f"multiplier{x}"] = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
|
||||
else:
|
||||
flex_inputs["multiplier1"] = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
|
||||
class AllContainer:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def __getitem__(self, key):
|
||||
return "FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}
|
||||
|
||||
return {
|
||||
"required": {"conditioning1": ("CONDITIONING",), },
|
||||
"optional": AllContainer()
|
||||
}
|
||||
|
||||
return {
|
||||
"required": {"conditioning1": ("CONDITIONING",), },
|
||||
"optional": flex_inputs
|
||||
"optional": {"multiplier1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), },
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
@@ -46,7 +52,7 @@ class ConcatConditioningsWithMultiplier:
|
||||
|
||||
out = []
|
||||
if len(conditioning_from) > 1:
|
||||
print(f"Warning: ConcatConditioningsWithMultiplier {k} contains more than 1 cond, only the first one will actually be applied to conditioning1.")
|
||||
logging.warning(f"[Inspire Pack] ConcatConditioningsWithMultiplier {k} contains more than 1 cond, only the first one will actually be applied to conditioning1.")
|
||||
|
||||
mkey = 'multiplier' + k[12:]
|
||||
multiplier = float(kwargs[mkey])
|
||||
|
||||
+73
-14
@@ -2,11 +2,61 @@ import os
|
||||
|
||||
import torch
|
||||
from PIL import ImageOps
|
||||
try:
|
||||
import pillow_jxl # noqa: F401
|
||||
jxl = True
|
||||
except ImportError:
|
||||
jxl = False
|
||||
import comfy
|
||||
import folder_paths
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from .libs.utils import *
|
||||
from .libs.utils import ByPassTypeTuple, empty_pil_tensor, empty_latent
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
def extract_first_number(s):
|
||||
match = re.search(r'\d+', s)
|
||||
return int(match.group()) if match else float('inf')
|
||||
|
||||
|
||||
sort_methods = [
|
||||
"None",
|
||||
"Alphabetical (ASC)",
|
||||
"Alphabetical (DESC)",
|
||||
"Numerical (ASC)",
|
||||
"Numerical (DESC)",
|
||||
"Datetime (ASC)",
|
||||
"Datetime (DESC)"
|
||||
]
|
||||
|
||||
|
||||
def sort_by(items, base_path='.', method=None):
|
||||
def fullpath(x): return os.path.join(base_path, x)
|
||||
|
||||
def get_timestamp(path):
|
||||
try:
|
||||
return os.path.getmtime(path)
|
||||
except FileNotFoundError:
|
||||
return float('-inf')
|
||||
|
||||
if method == "Alphabetical (ASC)":
|
||||
return sorted(items)
|
||||
elif method == "Alphabetical (DESC)":
|
||||
return sorted(items, reverse=True)
|
||||
elif method == "Numerical (ASC)":
|
||||
return sorted(items, key=lambda x: extract_first_number(os.path.splitext(x)[0]))
|
||||
elif method == "Numerical (DESC)":
|
||||
return sorted(items, key=lambda x: extract_first_number(os.path.splitext(x)[0]), reverse=True)
|
||||
elif method == "Datetime (ASC)":
|
||||
return sorted(items, key=lambda x: get_timestamp(fullpath(x)))
|
||||
elif method == "Datetime (DESC)":
|
||||
return sorted(items, key=lambda x: get_timestamp(fullpath(x)), reverse=True)
|
||||
else:
|
||||
return items
|
||||
|
||||
|
||||
class LoadImagesFromDirBatch:
|
||||
@@ -18,8 +68,9 @@ class LoadImagesFromDirBatch:
|
||||
},
|
||||
"optional": {
|
||||
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"start_index": ("INT", {"default": 0, "min": -1, "max": 0xffffffffffffffff, "step": 1}),
|
||||
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
|
||||
"sort_method": (sort_methods,),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +86,7 @@ class LoadImagesFromDirBatch:
|
||||
else:
|
||||
return hash(frozenset(kwargs))
|
||||
|
||||
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False):
|
||||
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False, sort_method=None):
|
||||
if not os.path.isdir(directory):
|
||||
raise FileNotFoundError(f"Directory '{directory} cannot be found.'")
|
||||
dir_files = os.listdir(directory)
|
||||
@@ -44,9 +95,11 @@ class LoadImagesFromDirBatch:
|
||||
|
||||
# Filter files by extension
|
||||
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
|
||||
if jxl:
|
||||
valid_extensions.extend('.jxl')
|
||||
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
|
||||
|
||||
dir_files = sorted(dir_files)
|
||||
dir_files = sort_by(dir_files, directory, sort_method)
|
||||
dir_files = [os.path.join(directory, x) for x in dir_files]
|
||||
|
||||
# start at start_index
|
||||
@@ -94,10 +147,10 @@ class LoadImagesFromDirBatch:
|
||||
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
|
||||
image1 = torch.cat((image1, image2), dim=0)
|
||||
|
||||
for mask2 in masks[1:]:
|
||||
for mask2 in masks:
|
||||
if has_non_empty_mask:
|
||||
if image1.shape[1:3] != mask2.shape:
|
||||
mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[2], image1.shape[1]), mode='bilinear', align_corners=False)
|
||||
mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[1], image1.shape[2]), mode='bilinear', align_corners=False)
|
||||
mask2 = mask2.squeeze(0)
|
||||
else:
|
||||
mask2 = mask2.unsqueeze(0)
|
||||
@@ -121,13 +174,15 @@ class LoadImagesFromDirList:
|
||||
},
|
||||
"optional": {
|
||||
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
|
||||
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
|
||||
"sort_method": (sort_methods,),
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@@ -140,7 +195,7 @@ class LoadImagesFromDirList:
|
||||
else:
|
||||
return hash(frozenset(kwargs))
|
||||
|
||||
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False):
|
||||
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False, sort_method=None):
|
||||
if not os.path.isdir(directory):
|
||||
raise FileNotFoundError(f"Directory '{directory}' cannot be found.")
|
||||
dir_files = os.listdir(directory)
|
||||
@@ -149,9 +204,11 @@ class LoadImagesFromDirList:
|
||||
|
||||
# Filter files by extension
|
||||
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
|
||||
if jxl:
|
||||
valid_extensions.extend('.jxl')
|
||||
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
|
||||
|
||||
dir_files = sorted(dir_files)
|
||||
dir_files = sort_by(dir_files, directory, sort_method)
|
||||
dir_files = [os.path.join(directory, x) for x in dir_files]
|
||||
|
||||
# start at start_index
|
||||
@@ -159,6 +216,7 @@ class LoadImagesFromDirList:
|
||||
|
||||
images = []
|
||||
masks = []
|
||||
file_paths = []
|
||||
|
||||
limit_images = False
|
||||
if image_load_cap > 0:
|
||||
@@ -184,9 +242,10 @@ class LoadImagesFromDirList:
|
||||
|
||||
images.append(image)
|
||||
masks.append(mask)
|
||||
file_paths.append(str(image_path))
|
||||
image_count += 1
|
||||
|
||||
return images, masks
|
||||
return (images, masks, file_paths)
|
||||
|
||||
|
||||
class LoadImageInspire:
|
||||
@@ -245,7 +304,7 @@ class ChangeImageBatchSize:
|
||||
output_tensor = input_tensor[:batch_size, :, :, :]
|
||||
return output_tensor
|
||||
else:
|
||||
print(f"[WARN] ChangeImage(Latent)BatchSize: Unknown mode `{mode}` - ignored")
|
||||
logging.warning(f"[Inspire Pack] ChangeImage(Latent)BatchSize: Unknown mode `{mode}` - ignored")
|
||||
return input_tensor
|
||||
|
||||
@staticmethod
|
||||
@@ -390,7 +449,7 @@ class ColorMapToMasks:
|
||||
|
||||
def doit(self, color_map, max_count, min_pixels):
|
||||
if len(color_map) > 0:
|
||||
print(f"[Inspire Pack] WARN: ColorMapToMasks - Sure, here's the translation: `color_map` can only be a single image. Only the first image will be processed. If you want to utilize the remaining images, convert the Image Batch to an Image List.")
|
||||
logging.warning("[Inspire Pack] ColorMapToMasks - Sure, here's the translation: `color_map` can only be a single image. Only the first image will be processed. If you want to utilize the remaining images, convert the Image Batch to an Image List.")
|
||||
|
||||
top_colors = top_k_colors(color_map[0], max_count, min_pixels)
|
||||
|
||||
|
||||
+46
-12
@@ -6,6 +6,8 @@ from enum import Enum
|
||||
from . import prompt_support
|
||||
from aiohttp import web
|
||||
from . import backend_support
|
||||
from .libs import common
|
||||
import logging
|
||||
|
||||
|
||||
max_seed = 2**32 - 1
|
||||
@@ -49,7 +51,7 @@ async def set_cache_settings(request):
|
||||
try:
|
||||
backend_support.ShowCachedInfo.set_cache_settings(data)
|
||||
return web.Response(text='OK', status=200)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
except Exception as e:
|
||||
return web.Response(text=f"{e}", status=500)
|
||||
|
||||
|
||||
@@ -262,16 +264,26 @@ def populate_wildcards(json_data):
|
||||
|
||||
if 'ImpactWildcardProcessor' in nodes.NODE_CLASS_MAPPINGS:
|
||||
if not hasattr(nodes.NODE_CLASS_MAPPINGS['ImpactWildcardProcessor'], 'process'):
|
||||
print(f"[Inspire Pack] Your Impact Pack is outdated. Please update to the latest version.")
|
||||
logging.warning("[Inspire Pack] Your Impact Pack is outdated. Please update to the latest version.")
|
||||
return
|
||||
|
||||
|
||||
wildcard_process = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardProcessor'].process
|
||||
updated_widget_values = {}
|
||||
mbp_updated_widget_values = {}
|
||||
for k, v in prompt.items():
|
||||
if 'class_type' in v and v['class_type'] == 'WildcardEncode //Inspire':
|
||||
inputs = v['inputs']
|
||||
if inputs['mode'] and isinstance(inputs['populated_text'], str):
|
||||
|
||||
# legacy adapter
|
||||
if isinstance(inputs['mode'], bool):
|
||||
if inputs['mode']:
|
||||
new_mode = 'populate'
|
||||
else:
|
||||
new_mode = 'fixed'
|
||||
|
||||
inputs['mode'] = new_mode
|
||||
|
||||
if inputs['mode'] == 'populate' and isinstance(inputs['populated_text'], str):
|
||||
if isinstance(inputs['seed'], list):
|
||||
try:
|
||||
input_node = prompt[inputs['seed'][0]]
|
||||
@@ -284,7 +296,7 @@ def populate_wildcards(json_data):
|
||||
if not isinstance(input_seed, int):
|
||||
continue
|
||||
else:
|
||||
print(f"[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
|
||||
logging.warning("[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
@@ -292,14 +304,17 @@ def populate_wildcards(json_data):
|
||||
input_seed = int(inputs['seed'])
|
||||
|
||||
inputs['populated_text'] = wildcard_process(text=inputs['wildcard_text'], seed=input_seed)
|
||||
inputs['mode'] = False
|
||||
inputs['mode'] = 'reproduce'
|
||||
|
||||
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "populated_text", "type": "text", "data": inputs['populated_text']})
|
||||
updated_widget_values[k] = inputs['populated_text']
|
||||
|
||||
if inputs['mode'] == 'reproduce':
|
||||
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "mode", "type": "text", "value": 'populate'})
|
||||
|
||||
elif 'class_type' in v and v['class_type'] == 'MakeBasicPipe //Inspire':
|
||||
inputs = v['inputs']
|
||||
if inputs['wildcard_mode'] and (isinstance(inputs['positive_populated_text'], str) or isinstance(inputs['negative_populated_text'], str)):
|
||||
if inputs['wildcard_mode'] == 'populate' and (isinstance(inputs['positive_populated_text'], str) or isinstance(inputs['negative_populated_text'], str)):
|
||||
if isinstance(inputs['seed'], list):
|
||||
try:
|
||||
input_node = prompt[inputs['seed'][0]]
|
||||
@@ -312,7 +327,7 @@ def populate_wildcards(json_data):
|
||||
if not isinstance(input_seed, int):
|
||||
continue
|
||||
else:
|
||||
print(f"[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
|
||||
logging.warning("[Inspire Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
@@ -327,21 +342,24 @@ def populate_wildcards(json_data):
|
||||
inputs['negative_populated_text'] = wildcard_process(text=inputs['negative_wildcard_text'], seed=input_seed)
|
||||
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "negative_populated_text", "type": "text", "data": inputs['negative_populated_text']})
|
||||
|
||||
inputs['wildcard_mode'] = False
|
||||
inputs['wildcard_mode'] = 'reproduce'
|
||||
mbp_updated_widget_values[k] = inputs['positive_populated_text'], inputs['negative_populated_text']
|
||||
|
||||
if inputs['wildcard_mode'] == 'reproduce':
|
||||
server.PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": k, "widget_name": "wildcard_mode", "type": "text", "value": 'populate'})
|
||||
|
||||
if 'extra_data' in json_data and 'extra_pnginfo' in json_data['extra_data']:
|
||||
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 +372,21 @@ def force_reset_useless_params(json_data):
|
||||
return json_data
|
||||
|
||||
|
||||
def clear_unused_node_changed_cache(json_data):
|
||||
prompt = json_data['prompt']
|
||||
|
||||
unused = []
|
||||
for x in common.changed_cache.keys():
|
||||
if x not in prompt:
|
||||
unused.append(x)
|
||||
|
||||
for x in unused:
|
||||
del common.changed_cache[x]
|
||||
del common.changed_count_cache[x]
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
def onprompt(json_data):
|
||||
prompt_support.list_counter_map = {}
|
||||
|
||||
@@ -367,6 +400,7 @@ def onprompt(json_data):
|
||||
populate_wildcards(json_data)
|
||||
|
||||
force_reset_useless_params(json_data)
|
||||
clear_unused_node_changed_cache(json_data)
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
+71
-2
@@ -1,14 +1,83 @@
|
||||
import comfy
|
||||
import nodes
|
||||
from . import utils
|
||||
import logging
|
||||
from server import PromptServer
|
||||
|
||||
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD', "GITS[coeff=1.2]"]
|
||||
|
||||
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD', "GITS[coeff=1.2]", 'OSS FLUX', 'OSS Wan', 'OSS Chroma']
|
||||
|
||||
|
||||
def impact_sampling(*args, **kwargs):
|
||||
if 'RegionalSampler' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
"'Impact Pack' extension is required.")
|
||||
raise Exception(f"[ERROR] You need to install 'ComfyUI-Impact-Pack'")
|
||||
raise Exception("[ERROR] You need to install 'ComfyUI-Impact-Pack'")
|
||||
|
||||
return nodes.NODE_CLASS_MAPPINGS['RegionalSampler'].separated_sample(*args, **kwargs)
|
||||
|
||||
|
||||
changed_count_cache = {}
|
||||
changed_cache = {}
|
||||
|
||||
|
||||
def changed_value(uid):
|
||||
v = changed_count_cache.get(uid, 0)
|
||||
changed_count_cache[uid] = v + 1
|
||||
return v + 1
|
||||
|
||||
|
||||
def not_changed_value(uid):
|
||||
return changed_count_cache.get(uid, 0)
|
||||
|
||||
|
||||
def is_changed(uid, value):
|
||||
if uid not in changed_cache or changed_cache[uid] != value:
|
||||
res = changed_value(uid)
|
||||
else:
|
||||
res = not_changed_value(uid)
|
||||
|
||||
changed_cache[uid] = value
|
||||
|
||||
logging.info(f"keys: {changed_cache.keys()}")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def update_node_status(node, text, progress=None):
|
||||
if PromptServer.instance.client_id is None:
|
||||
return
|
||||
|
||||
PromptServer.instance.send_sync("inspire/update_status", {
|
||||
"node": node,
|
||||
"progress": progress,
|
||||
"text": text
|
||||
}, PromptServer.instance.client_id)
|
||||
|
||||
|
||||
class ListWrapper:
|
||||
def __init__(self, data, aux=None):
|
||||
if isinstance(data, ListWrapper):
|
||||
self._data = data
|
||||
if aux is None:
|
||||
self.aux = data.aux
|
||||
else:
|
||||
self.aux = aux
|
||||
else:
|
||||
self._data = list(data)
|
||||
self.aux = aux
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice):
|
||||
return ListWrapper(self._data[index], self.aux)
|
||||
else:
|
||||
return self._data[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
self._data[index] = value
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ListWrapper({self._data}, aux={self.aux})"
|
||||
|
||||
+18
-3
@@ -5,6 +5,8 @@ import torch
|
||||
from PIL import Image, ImageDraw
|
||||
import math
|
||||
import cv2
|
||||
import folder_paths
|
||||
import logging
|
||||
|
||||
|
||||
def apply_variation_noise(latent_image, noise_device, variation_seed, variation_strength, mask=None, variation_method='linear'):
|
||||
@@ -56,6 +58,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:
|
||||
@@ -195,9 +199,9 @@ def try_install_custom_node(custom_node_url, msg):
|
||||
import cm_global
|
||||
cm_global.try_call(api='cm.try-install-custom-node',
|
||||
sender="Inspire Pack", custom_node_url=custom_node_url, msg=msg)
|
||||
except Exception as e:
|
||||
print(msg)
|
||||
print(f"[Inspire Pack] ComfyUI-Manager is outdated. The custom node installation feature is not available.")
|
||||
except Exception as e: # noqa: F841
|
||||
logging.error(msg)
|
||||
logging.error("[Inspire Pack] ComfyUI-Manager is outdated. The custom node installation feature is not available.")
|
||||
|
||||
|
||||
def empty_latent():
|
||||
@@ -334,3 +338,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)
|
||||
|
||||
+212
-2
@@ -1,3 +1,9 @@
|
||||
import logging
|
||||
|
||||
from comfy_execution.graph_utils import GraphBuilder, is_link
|
||||
from .libs.utils import any_typ
|
||||
from .libs.common import update_node_status, ListWrapper
|
||||
|
||||
class FloatRange:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -15,7 +21,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 +53,214 @@ 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:
|
||||
next_list = ListWrapper(item_list[1:])
|
||||
next_item = item_list[0]
|
||||
else:
|
||||
next_list = ListWrapper([])
|
||||
next_item = None
|
||||
|
||||
if next_list.aux is None:
|
||||
next_list.aux = len(item_list), None
|
||||
|
||||
return "stub", next_list, next_item, initial_input
|
||||
|
||||
|
||||
class ForeachListEnd:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"flow_control": ("FOREACH_LIST_CONTROL", {"rawLink": True, "tooltip": "Directly connect the output of ForeachListBegin, the starting node of the iteration."}),
|
||||
"remained_list": ("ITEM_LIST", {"tooltip":"Directly connect the output of ForeachListBegin, the starting node of the iteration."}),
|
||||
"intermediate_output": (any_typ, {"tooltip":"Connect the intermediate outputs processed within the iteration here."}),
|
||||
},
|
||||
"hidden": {
|
||||
"dynprompt": "DYNPROMPT",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_typ,)
|
||||
RETURN_NAMES = ("result",)
|
||||
OUTPUT_TOOLTIPS = ("This is the final output value.",)
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
DESCRIPTION = "A end node for performing iterative tasks by retrieving items one by one from the ITEM_LIST.\nNOTE:Directly connect the outputs of ForeachListBegin to 'flow_control' and 'remained_list'."
|
||||
|
||||
CATEGORY = "InspirePack/List"
|
||||
|
||||
def explore_dependencies(self, node_id, dynprompt, upstream):
|
||||
node_info = dynprompt.get_node(node_id)
|
||||
if "inputs" not in node_info:
|
||||
return
|
||||
for k, v in node_info["inputs"].items():
|
||||
if is_link(v):
|
||||
parent_id = v[0]
|
||||
if parent_id not in upstream:
|
||||
upstream[parent_id] = []
|
||||
self.explore_dependencies(parent_id, dynprompt, upstream)
|
||||
upstream[parent_id].append(node_id)
|
||||
|
||||
def collect_contained(self, node_id, upstream, contained):
|
||||
if node_id not in upstream:
|
||||
return
|
||||
for child_id in upstream[node_id]:
|
||||
if child_id not in contained:
|
||||
contained[child_id] = True
|
||||
self.collect_contained(child_id, upstream, contained)
|
||||
|
||||
def doit(self, flow_control, remained_list, intermediate_output, dynprompt, unique_id):
|
||||
if hasattr(remained_list, "aux"):
|
||||
if remained_list.aux[1] is None:
|
||||
remained_list.aux = (remained_list.aux[0], unique_id)
|
||||
|
||||
update_node_status(remained_list.aux[1], f"{(remained_list.aux[0]-len(remained_list))}/{remained_list.aux[0]} steps", (remained_list.aux[0]-len(remained_list))/remained_list.aux[0])
|
||||
else:
|
||||
logging.warning("[Inspire Pack] ForeachListEnd: `remained_list` did not come from ForeachList.")
|
||||
|
||||
if len(remained_list) == 0:
|
||||
return (intermediate_output,)
|
||||
|
||||
# We want to loop
|
||||
upstream = {}
|
||||
|
||||
# Get the list of all nodes between the open and close nodes
|
||||
self.explore_dependencies(unique_id, dynprompt, upstream)
|
||||
|
||||
contained = {}
|
||||
open_node = flow_control[0]
|
||||
self.collect_contained(open_node, upstream, contained)
|
||||
contained[unique_id] = True
|
||||
contained[open_node] = True
|
||||
|
||||
# We'll use the default prefix, but to avoid having node names grow exponentially in size,
|
||||
# we'll use "Recurse" for the name of the recursively-generated copy of this node.
|
||||
graph = GraphBuilder()
|
||||
for node_id in contained:
|
||||
original_node = dynprompt.get_node(node_id)
|
||||
node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id)
|
||||
node.set_override_display_id(node_id)
|
||||
|
||||
for node_id in contained:
|
||||
original_node = dynprompt.get_node(node_id)
|
||||
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
|
||||
for k, v in original_node["inputs"].items():
|
||||
if is_link(v) and v[0] in contained:
|
||||
parent = graph.lookup_node(v[0])
|
||||
node.set_input(k, parent.out(v[1]))
|
||||
else:
|
||||
node.set_input(k, v)
|
||||
|
||||
new_open = graph.lookup_node(open_node)
|
||||
|
||||
new_open.set_input("item_list", remained_list)
|
||||
new_open.set_input("initial_input", intermediate_output)
|
||||
|
||||
my_clone = graph.lookup_node("Recurse" )
|
||||
result = (my_clone.out(0),)
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
|
||||
class DropItems:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": { "item_list": ("ITEM_LIST", {"tooltip":"Directly connect the output of ForeachListBegin, the starting node of the iteration."}), },
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_typ,)
|
||||
RETURN_NAMES = ("ITEM_LIST",)
|
||||
OUTPUT_TOOLTIPS = ("This is the final output value.",)
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
DESCRIPTION = ""
|
||||
|
||||
CATEGORY = "InspirePack/List"
|
||||
|
||||
def doit(self, item_list):
|
||||
l = ListWrapper([])
|
||||
if hasattr(item_list, 'aux'):
|
||||
l.aux = item_list.aux
|
||||
else:
|
||||
logging.warning("[Inspire Pack] DropItems: `item_list` did not come from ForeachList.")
|
||||
|
||||
return (l,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FloatRange //Inspire": FloatRange,
|
||||
"WorklistToItemList //Inspire": WorklistToItemList,
|
||||
"ForeachListBegin //Inspire": ForeachListBegin,
|
||||
"ForeachListEnd //Inspire": ForeachListEnd,
|
||||
"DropItems //Inspire": DropItems,
|
||||
}
|
||||
|
||||
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)",
|
||||
"DropItems //Inspire": "Drop Items (Inspire)",
|
||||
}
|
||||
|
||||
+490
-51
@@ -6,11 +6,20 @@ import torch
|
||||
import numpy as np
|
||||
import nodes
|
||||
import re
|
||||
import json
|
||||
from comfy.cli_args import args
|
||||
from safetensors.torch import safe_open
|
||||
import ast
|
||||
import logging
|
||||
|
||||
from server import PromptServer
|
||||
from .libs import utils
|
||||
|
||||
|
||||
model_path = folder_paths.models_dir
|
||||
utils.add_folder_path_and_extensions("lbw_models", [os.path.join(model_path, "lbw_models")], {'.safetensors'})
|
||||
|
||||
|
||||
def is_numeric_string(input_str):
|
||||
return re.match(r'^-?\d+(\.\d+)?$', input_str) is not None
|
||||
|
||||
@@ -34,6 +43,73 @@ def load_lbw_preset(filename):
|
||||
return []
|
||||
|
||||
|
||||
def parse_unet_num(s):
|
||||
if s[1] == '.':
|
||||
return int(s[0])
|
||||
else:
|
||||
return int(s)
|
||||
|
||||
|
||||
class MakeLBW:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
preset = ["Preset"] # 20
|
||||
preset += load_lbw_preset("lbw-preset.txt")
|
||||
preset += load_lbw_preset("lbw-preset.custom.txt")
|
||||
preset = [name for name in preset if not name.startswith('@')]
|
||||
|
||||
lora_names = folder_paths.get_filename_list("loras")
|
||||
lora_dirs = [os.path.dirname(name) for name in lora_names]
|
||||
lora_dirs = ["All"] + list(set(lora_dirs))
|
||||
|
||||
return {"required": {"model": ("MODEL",),
|
||||
"clip": ("CLIP", ),
|
||||
"category_filter": (lora_dirs,),
|
||||
"lora_name": (lora_names, ),
|
||||
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False", "tooltip": "Apply the following weights for each block:\nTrue: 1 - weight\nFalse: weight"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": ""}),
|
||||
"A": ("FLOAT", {"default": 4.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"B": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"preset": (preset,),
|
||||
"block_vector": ("STRING", {"multiline": True, "placeholder": "block weight vectors", "default": "1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", "pysssss.autocomplete": False}),
|
||||
"bypass": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LBW_MODEL", "STRING")
|
||||
RETURN_NAMES = ("lbw_model", "populated_vector")
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/LoraBlockWeight"
|
||||
|
||||
DESCRIPTION = "Instead of directly applying the LoRA Block Weight to the MODEL, it is generated in a separate LBW_MODEL form."
|
||||
|
||||
def __init__(self):
|
||||
self.loaded_lora = None
|
||||
|
||||
def doit(self, model, clip, lora_name, inverse, seed, A, B, preset, block_vector, bypass=False, category_filter=None):
|
||||
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||
lora = None
|
||||
if self.loaded_lora is not None:
|
||||
if self.loaded_lora[0] == lora_path:
|
||||
lora = self.loaded_lora[1]
|
||||
else:
|
||||
temp = self.loaded_lora
|
||||
self.loaded_lora = None
|
||||
del temp
|
||||
|
||||
if lora is None:
|
||||
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
||||
self.loaded_lora = (lora_path, lora)
|
||||
|
||||
block_weights, muted_weights, populated_vector = LoraLoaderBlockWeight.load_lbw(model, clip, lora, inverse, seed, A, B, block_vector)
|
||||
lbw_model = {
|
||||
'blocks': block_weights,
|
||||
'muted': muted_weights
|
||||
}
|
||||
return lbw_model, populated_vector
|
||||
|
||||
|
||||
class LoraLoaderBlockWeight:
|
||||
def __init__(self):
|
||||
self.loaded_lora = None
|
||||
@@ -55,8 +131,8 @@ class LoraLoaderBlockWeight:
|
||||
"lora_name": (lora_names, ),
|
||||
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"inverse": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False", "tooltip": "Apply the following weights for each block:\nTrue: 1 - weight\nFalse: weight"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": ""}),
|
||||
"A": ("FLOAT", {"default": 4.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"B": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"preset": (preset,),
|
||||
@@ -130,11 +206,150 @@ class LoraLoaderBlockWeight:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector):
|
||||
def block_spec_parser(loaded, spec):
|
||||
if not spec.startswith("%"):
|
||||
return spec
|
||||
else:
|
||||
items = [x.strip() for x in spec[1:].split(',')]
|
||||
|
||||
input_blocks_set = set()
|
||||
middle_blocks_set= set()
|
||||
output_blocks_set = set()
|
||||
double_blocks_set = set()
|
||||
single_blocks_set = set()
|
||||
|
||||
for key, v in loaded.items():
|
||||
if isinstance(key, tuple):
|
||||
k = key[0]
|
||||
else:
|
||||
k = key
|
||||
|
||||
k_unet = k[len("diffusion_model."):]
|
||||
|
||||
if k_unet.startswith("input_blocks."):
|
||||
k_unet_num = k_unet[len("input_blocks."):len("input_blocks.")+2]
|
||||
k_unet_int = parse_unet_num(k_unet_num)
|
||||
input_blocks_set.add(k_unet_int)
|
||||
elif k_unet.startswith("middle_block."):
|
||||
k_unet_num = k_unet[len("middle_block."):len("middle_block.")+2]
|
||||
k_unet_int = parse_unet_num(k_unet_num)
|
||||
middle_blocks_set.add(k_unet_int)
|
||||
elif k_unet.startswith("output_blocks."):
|
||||
k_unet_num = k_unet[len("output_blocks."):len("output_blocks.")+2]
|
||||
k_unet_int = parse_unet_num(k_unet_num)
|
||||
output_blocks_set.add(k_unet_int)
|
||||
elif k_unet.startswith("double_blocks."):
|
||||
k_unet_num = k_unet[len("double_blocks."):len("double_blocks.") + 2]
|
||||
k_unet_int = parse_unet_num(k_unet_num)
|
||||
double_blocks_set.add(k_unet_int)
|
||||
elif k_unet.startswith("single_blocks."):
|
||||
k_unet_num = k_unet[len("single_blocks."):len("single_blocks.") + 2]
|
||||
k_unet_int = parse_unet_num(k_unet_num)
|
||||
single_blocks_set.add(k_unet_int)
|
||||
|
||||
pat1 = re.compile(r"(default|base)=([0-9.]+)")
|
||||
pat2 = re.compile(r"(in|out|mid|double|single)([0-9]+)-([0-9]+)=([0-9.]+)")
|
||||
pat3 = re.compile(r"(in|out|mid|double|single)([0-9]+)=([0-9.]+)")
|
||||
pat4 = re.compile(r"(in|out|mid|double|single)=([0-9.]+)")
|
||||
|
||||
base_spec = None
|
||||
default_spec = 1.0
|
||||
|
||||
for item in items:
|
||||
match = pat1.match(item)
|
||||
if match:
|
||||
if match[1] == 'base':
|
||||
base_spec = match[2]
|
||||
continue
|
||||
|
||||
if match[1] == 'default':
|
||||
default_spec = match[2]
|
||||
continue
|
||||
|
||||
if base_spec is None:
|
||||
base_spec = default_spec
|
||||
|
||||
input_blocks = [default_spec] * len(input_blocks_set)
|
||||
middle_blocks = [default_spec] * len(middle_blocks_set)
|
||||
output_blocks = [default_spec] * len(output_blocks_set)
|
||||
double_blocks = [default_spec] * len(double_blocks_set)
|
||||
single_blocks = [default_spec] * len(single_blocks_set)
|
||||
|
||||
for item in items:
|
||||
match = pat2.match(item)
|
||||
if match:
|
||||
for x in range(int(match[2])-1, int(match[3])):
|
||||
value = float(match[4])
|
||||
|
||||
if x < 0:
|
||||
continue
|
||||
|
||||
if match[1] == 'in' and len(input_blocks) > x:
|
||||
input_blocks[x] = value
|
||||
elif match[1] == 'out' and len(output_blocks) > x:
|
||||
output_blocks[x] = value
|
||||
elif match[1] == 'mid' and len(middle_blocks) > x:
|
||||
middle_blocks[x] = value
|
||||
elif match[1] == 'double' and len(double_blocks) > x:
|
||||
double_blocks[x] = value
|
||||
elif match[1] == 'single' and len(single_blocks) > x:
|
||||
single_blocks[x] = value
|
||||
|
||||
continue
|
||||
|
||||
match = pat3.match(item)
|
||||
if match:
|
||||
value = float(match[3])
|
||||
x = int(match[2]) - 1
|
||||
|
||||
if x < 0:
|
||||
continue
|
||||
|
||||
if match[1] == 'in' and len(input_blocks) > x:
|
||||
input_blocks[x] = value
|
||||
elif match[1] == 'out' and len(output_blocks) > x:
|
||||
output_blocks[x] = value
|
||||
elif match[1] == 'mid' and len(middle_blocks) > x:
|
||||
middle_blocks[x] = value
|
||||
elif match[1] == 'double' and len(double_blocks) > x:
|
||||
double_blocks[x] = value
|
||||
elif match[1] == 'single' and len(single_blocks) > x:
|
||||
single_blocks[x] = value
|
||||
|
||||
continue
|
||||
|
||||
match = pat4.match(item)
|
||||
if match:
|
||||
value = float(match[2])
|
||||
|
||||
if match[1] == 'in':
|
||||
input_blocks = [value] * len(input_blocks)
|
||||
elif match[1] == 'out':
|
||||
output_blocks = [value] * len(output_blocks)
|
||||
elif match[1] == 'mid':
|
||||
middle_blocks = [value] * len(middle_blocks)
|
||||
elif match[1] == 'double':
|
||||
double_blocks = [value] * len(double_blocks)
|
||||
elif match[1] == 'single':
|
||||
single_blocks = [value] * len(single_blocks)
|
||||
|
||||
continue
|
||||
|
||||
# concat specs
|
||||
res = [str(base_spec)]
|
||||
for x in (input_blocks + middle_blocks + output_blocks + double_blocks + single_blocks):
|
||||
res.append(str(x))
|
||||
|
||||
return ",".join(res)
|
||||
|
||||
@staticmethod
|
||||
def load_lbw(model, clip, lora, inverse, seed, A, B, block_vector):
|
||||
key_map = comfy.lora.model_lora_keys_unet(model.model)
|
||||
key_map = comfy.lora.model_lora_keys_clip(clip.cond_stage_model, key_map)
|
||||
loaded = comfy.lora.load_lora(lora, key_map)
|
||||
|
||||
block_vector = LoraLoaderBlockWeight.block_spec_parser(loaded, block_vector)
|
||||
|
||||
block_vector = block_vector.split(":")
|
||||
if len(block_vector) > 1:
|
||||
block_vector = block_vector[1]
|
||||
@@ -142,7 +357,6 @@ class LoraLoaderBlockWeight:
|
||||
block_vector = block_vector[0]
|
||||
|
||||
vector = block_vector.split(",")
|
||||
vector_i = 1
|
||||
|
||||
if not LoraLoaderBlockWeight.validate(vector):
|
||||
preset_dict = load_preset_dict()
|
||||
@@ -151,16 +365,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 +408,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 +432,8 @@ class LoraLoaderBlockWeight:
|
||||
else:
|
||||
if len(ratios) > 0:
|
||||
ratio = ratios.pop(0)
|
||||
else:
|
||||
pass # use last used ratio if no more user specified ratio is given
|
||||
|
||||
if inverse:
|
||||
populated_ratio = 1 - ratio
|
||||
@@ -228,13 +442,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 +454,43 @@ class LoraLoaderBlockWeight:
|
||||
if inverse:
|
||||
populated_ratio = 1 - ratio
|
||||
else:
|
||||
populated_ratio = 1
|
||||
populated_ratio = ratio
|
||||
|
||||
populated_vector_list.insert(0, LoraLoaderBlockWeight.norm_value(populated_ratio))
|
||||
|
||||
for k, v, k_unet in others:
|
||||
new_modelpatcher.add_patches({k: v}, strength_model * populated_ratio)
|
||||
# if inverse:
|
||||
# print(f"\t{k_unet} -> inv({ratio}) ")
|
||||
# else:
|
||||
# print(f"\t{k_unet} -> ({ratio}) ")
|
||||
if populated_ratio != 0:
|
||||
block_weights[k] = v, populated_ratio
|
||||
else:
|
||||
muted_weights.append(k)
|
||||
|
||||
new_clip = clip.clone()
|
||||
new_clip.add_patches(loaded, strength_clip)
|
||||
populated_vector = ','.join(map(str, populated_vector_list))
|
||||
return (new_modelpatcher, new_clip, populated_vector)
|
||||
return block_weights, muted_weights, populated_vector
|
||||
|
||||
@staticmethod
|
||||
def load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector):
|
||||
block_weights, muted_weights, populated_vector = LoraLoaderBlockWeight.load_lbw(model, clip, lora, inverse, seed, A, B, block_vector)
|
||||
|
||||
new_modelpatcher = model.clone()
|
||||
new_clip = clip.clone()
|
||||
|
||||
muted_weights = set(muted_weights)
|
||||
|
||||
for k, v in block_weights.items():
|
||||
weights, ratio = v
|
||||
|
||||
if k in muted_weights:
|
||||
pass
|
||||
elif 'text' in k or 'encoder' in k:
|
||||
new_clip.add_patches({k: weights}, strength_clip * ratio)
|
||||
else:
|
||||
new_modelpatcher.add_patches({k: weights}, strength_model * ratio)
|
||||
|
||||
return new_modelpatcher, new_clip, populated_vector
|
||||
|
||||
def doit(self, model, clip, lora_name, strength_model, strength_clip, inverse, seed, A, B, preset, block_vector, bypass=False, category_filter=None):
|
||||
if strength_model == 0 and strength_clip == 0 or bypass:
|
||||
return (model, clip, "")
|
||||
return model, clip, ""
|
||||
|
||||
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||
lora = None
|
||||
@@ -278,7 +507,48 @@ class LoraLoaderBlockWeight:
|
||||
self.loaded_lora = (lora_path, lora)
|
||||
|
||||
model_lora, clip_lora, populated_vector = LoraLoaderBlockWeight.load_lora_for_models(model, clip, lora, strength_model, strength_clip, inverse, seed, A, B, block_vector)
|
||||
return (model_lora, clip_lora, populated_vector)
|
||||
return model_lora, clip_lora, populated_vector
|
||||
|
||||
|
||||
class ApplyLBW:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"model": ("MODEL", ),
|
||||
"clip": ("CLIP", ),
|
||||
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"lbw_model": ("LBW_MODEL",),
|
||||
}}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP")
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/LoraBlockWeight"
|
||||
|
||||
DESCRIPTION = "Apply LBW_MODEL to MODEL and CLIP"
|
||||
|
||||
@staticmethod
|
||||
def doit(model, clip, strength_model, strength_clip, lbw_model):
|
||||
block_weights = lbw_model['blocks']
|
||||
muted_weights = lbw_model['muted']
|
||||
|
||||
new_modelpatcher = model.clone()
|
||||
new_clip = clip.clone()
|
||||
|
||||
muted_weights = set(muted_weights)
|
||||
|
||||
for k, v in block_weights.items():
|
||||
weights, ratio = v
|
||||
|
||||
if k in muted_weights:
|
||||
pass
|
||||
elif 'text' in k or 'encoder' in k:
|
||||
new_clip.add_patches({k: weights}, strength_clip * ratio)
|
||||
else:
|
||||
new_modelpatcher.add_patches({k: weights}, strength_model * ratio)
|
||||
|
||||
return new_modelpatcher, new_clip
|
||||
|
||||
|
||||
class XY_Capsule_LoraBlockWeight:
|
||||
@@ -300,10 +570,10 @@ class XY_Capsule_LoraBlockWeight:
|
||||
|
||||
def set_result(self, image, latent):
|
||||
if self.another_capsule is not None:
|
||||
print(f"XY_Capsule_LoraBlockWeight: ({self.another_capsule.x, self.y}) is processed.")
|
||||
logging.info(f"XY_Capsule_LoraBlockWeight: ({self.another_capsule.x, self.y}) is processed.")
|
||||
self.storage[(self.another_capsule.x, self.y)] = image
|
||||
else:
|
||||
print(f"XY_Capsule_LoraBlockWeight: ({self.x, self.y}) is processed.")
|
||||
logging.info(f"XY_Capsule_LoraBlockWeight: ({self.x, self.y}) is processed.")
|
||||
|
||||
def patch_model(self, model, clip):
|
||||
lora_name, strength_model, strength_clip, inverse, block_vectors, seed, A, B, heatmap_palette, heatmap_alpha, heatmap_strength, xyplot_mode = self.params
|
||||
@@ -345,6 +615,7 @@ class XY_Capsule_LoraBlockWeight:
|
||||
else:
|
||||
image = torch.abs(weighted_image - reference_image)
|
||||
self.storage[(self.another_capsule.x, self.y)] = image
|
||||
|
||||
elif self.y == 3:
|
||||
import matplotlib.cm as cm
|
||||
# heatmap
|
||||
@@ -353,7 +624,7 @@ class XY_Capsule_LoraBlockWeight:
|
||||
if image == "fail":
|
||||
image = utils.empty_pil_tensor(8,8)
|
||||
latent = utils.empty_latent()
|
||||
return (image, latent)
|
||||
return image, latent
|
||||
else:
|
||||
image = image.clone()
|
||||
|
||||
@@ -385,7 +656,7 @@ class XY_Capsule_LoraBlockWeight:
|
||||
image = heatmap_alpha * heatmap + (1 - heatmap_alpha) * image
|
||||
|
||||
latent = nodes.VAEEncode().encode(vae, image)[0]
|
||||
return (image, latent)
|
||||
return image, latent
|
||||
|
||||
def getLabel(self):
|
||||
return self.label
|
||||
@@ -548,9 +819,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 +903,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 +963,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 +987,162 @@ class LoraBlockInfo:
|
||||
return {}
|
||||
|
||||
|
||||
class LoadLBW:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
files = folder_paths.get_filename_list('lbw_models')
|
||||
return {"required": {
|
||||
"lbw_model": [sorted(files), ]},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LBW_MODEL",)
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/LoraBlockWeight"
|
||||
|
||||
DESCRIPTION = "Load LBW_MODEL from .lbw.safetensors file"
|
||||
|
||||
@staticmethod
|
||||
def decode_dict(encoded_dict, tensor_dict):
|
||||
original_dict = {}
|
||||
|
||||
def decode_value(value):
|
||||
if isinstance(value, str) and value.startswith('t') and value[1:].isdigit():
|
||||
return tensor_dict[value]
|
||||
return value
|
||||
|
||||
for k, tuple_value in encoded_dict.items():
|
||||
decoded_tuple = tuple(decode_value(v) for v in tuple_value[0][1])
|
||||
key = ast.literal_eval(k) if isinstance(k, str) and (k.startswith('(') or k.startswith('[')) else k
|
||||
original_dict[key] = ((tuple_value[0][0], decoded_tuple), tuple_value[1])
|
||||
|
||||
return original_dict
|
||||
|
||||
@staticmethod
|
||||
def load(file):
|
||||
tensor_dict = comfy.utils.load_torch_file(file)
|
||||
|
||||
with safe_open(file, framework="pt") as f:
|
||||
metadata = f.metadata()
|
||||
|
||||
encoded_dict = json.loads(metadata.get('blocks', '{}'))
|
||||
muted_blocks = ast.literal_eval(metadata.get('muted_blocks', '[]'))
|
||||
|
||||
decoded_dict = LoadLBW.decode_dict(encoded_dict, tensor_dict)
|
||||
|
||||
lbw_model = {
|
||||
'blocks': decoded_dict,
|
||||
'muted': muted_blocks
|
||||
}
|
||||
|
||||
return lbw_model, metadata
|
||||
|
||||
def doit(self, lbw_model):
|
||||
lbw_path = folder_paths.get_full_path("lbw_models", lbw_model)
|
||||
lbw_model, _ = LoadLBW.load(lbw_path)
|
||||
return (lbw_model,)
|
||||
|
||||
|
||||
class SaveLBW:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_folder_paths('lbw_models')[-1]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "lbw_model": ("LBW_MODEL", ),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}) },
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "doit"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "InspirePack/LoraBlockWeight"
|
||||
|
||||
DESCRIPTION = "Save LBW_MODEL as a .lbw.safetensors file"
|
||||
|
||||
@staticmethod
|
||||
def encode_dict(original_dict):
|
||||
tensor_dict = {}
|
||||
encoded_dict = {}
|
||||
counter = 0
|
||||
|
||||
def generate_unique_id():
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
return f"t{counter}"
|
||||
|
||||
def encode_value(value):
|
||||
if isinstance(value, torch.Tensor):
|
||||
unique_id = generate_unique_id()
|
||||
tensor_dict[unique_id] = value
|
||||
return unique_id
|
||||
return value
|
||||
|
||||
for k, tuple_value in original_dict.items():
|
||||
encoded_tuple = tuple(encode_value(v) for v in tuple_value[0][1])
|
||||
encoded_dict[str(k)] = (tuple_value[0][0], encoded_tuple), tuple_value[1]
|
||||
|
||||
return encoded_dict, tensor_dict
|
||||
|
||||
@staticmethod
|
||||
def save(lbw_model, file, metadata):
|
||||
metadata['format'] = 'Inspire LBW 1.0'
|
||||
weighted_blocks = lbw_model['blocks']
|
||||
metadata['muted_blocks'] = str(lbw_model['muted'])
|
||||
encoded_dict, tensor_dict = SaveLBW.encode_dict(weighted_blocks)
|
||||
metadata['blocks'] = json.dumps(encoded_dict)
|
||||
|
||||
comfy.utils.save_torch_file(tensor_dict, file, metadata=metadata)
|
||||
|
||||
def doit(self, lbw_model, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
|
||||
# support save metadata for lbw sharing
|
||||
prompt_info = ""
|
||||
if prompt is not None:
|
||||
prompt_info = json.dumps(prompt)
|
||||
|
||||
metadata = {}
|
||||
if not args.disable_metadata:
|
||||
metadata = {"prompt": prompt_info}
|
||||
if extra_pnginfo is not None:
|
||||
for x in extra_pnginfo:
|
||||
metadata[x] = json.dumps(extra_pnginfo[x])
|
||||
|
||||
file = f"{filename}_{counter:05}_.lbw.safetensors"
|
||||
|
||||
results = list()
|
||||
results.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": "output"
|
||||
})
|
||||
|
||||
file = os.path.join(full_output_folder, file)
|
||||
|
||||
SaveLBW.save(lbw_model, file, metadata)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"XY Input: Lora Block Weight //Inspire": XYInput_LoraBlockWeight,
|
||||
"LoraLoaderBlockWeight //Inspire": LoraLoaderBlockWeight,
|
||||
"LoraBlockInfo //Inspire": LoraBlockInfo,
|
||||
"MakeLBW //Inspire": MakeLBW,
|
||||
"ApplyLBW //Inspire": ApplyLBW,
|
||||
"SaveLBW //Inspire": SaveLBW,
|
||||
"LoadLBW //Inspire": LoadLBW,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"XY Input: Lora Block Weight //Inspire": "XY Input: Lora Block Weight",
|
||||
"LoraLoaderBlockWeight //Inspire": "Lora Loader (Block Weight)",
|
||||
"LoraBlockInfo //Inspire": "Lora Block Info",
|
||||
"XY Input: Lora Block Weight //Inspire": "XY Input: LoRA Block Weight",
|
||||
"LoraLoaderBlockWeight //Inspire": "LoRA Loader (Block Weight)",
|
||||
"LoraBlockInfo //Inspire": "LoRA Block Info",
|
||||
"MakeLBW //Inspire": "Make LoRA Block Weight",
|
||||
"ApplyLBW //Inspire": "Apply LoRA Block Weight",
|
||||
"SaveLBW //Inspire": "Save LoRA Block Weight",
|
||||
"LoadLBW //Inspire": "Load LoRA Block Weight",
|
||||
}
|
||||
|
||||
+16
-8
@@ -5,6 +5,8 @@ import server
|
||||
from .libs import utils
|
||||
from . import backend_support
|
||||
from comfy import sdxl_clip
|
||||
import logging
|
||||
|
||||
|
||||
model_preset = {
|
||||
# base
|
||||
@@ -19,6 +21,7 @@ model_preset = {
|
||||
"SDXL ViT-H": ("ip-adapter_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
|
||||
"SDXL Plus ViT-H": ("ip-adapter-plus_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
|
||||
"SDXL Plus Face ViT-H": ("ip-adapter-plus-face_sdxl_vit-h", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
|
||||
"Kolors Plus": ("Kolors-IP-Adapter-Plus", "clip-vit-large-patch14-336", None, False),
|
||||
|
||||
# faceid
|
||||
"SD1.5 FaceID": ("ip-adapter-faceid_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", "ip-adapter-faceid_sd15_lora", True),
|
||||
@@ -27,8 +30,10 @@ model_preset = {
|
||||
"SD1.5 FaceID Portrait v11": ("ip-adapter-faceid-portrait-v11_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
|
||||
"SD1.5 FaceID Portrait": ("ip-adapter-faceid-portrait_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
|
||||
"SDXL FaceID": ("ip-adapter-faceid_sdxl", "CLIP-ViT-H-14-laion2B-s32B-b79K", "ip-adapter-faceid_sdxl_lora", True),
|
||||
"SDXL FaceID Plus v2": ("ip-adapter-faceid-plusv2_sdxl", "CLIP-ViT-H-14-laion2B-s32B-b79K", "ip-adapter-faceid-plusv2_sdxl_lora", True),
|
||||
"SDXL FaceID Portrait": ("ip-adapter-faceid-portrait_sdxl", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
|
||||
"SDXL FaceID Portrait unnorm": ("ip-adapter-faceid-portrait_sdxl_unnorm", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, True),
|
||||
"Kolors FaceID Plus": ("Kolors-IP-Adapter-FaceID-Plus", "clip-vit-large-patch14-336", None, True),
|
||||
|
||||
# composition
|
||||
"SD1.5 Plus Composition": ("ip-adapter_sd15", "CLIP-ViT-H-14-laion2B-s32B-b79K", None, False),
|
||||
@@ -46,7 +51,7 @@ def lookup_model(model_dir, name):
|
||||
if len(resolved_name) > 0:
|
||||
return resolved_name[0], "OK"
|
||||
else:
|
||||
print(f"[ERROR] IPAdapterModelHelper: The `{name}` model file does not exist in `{model_dir}` model dir.")
|
||||
logging.error(f"[Inspire Pack] IPAdapterModelHelper: The `{name}` model file does not exist in `{model_dir}` model dir.")
|
||||
return None, "FAIL"
|
||||
|
||||
|
||||
@@ -56,7 +61,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 +68,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 +80,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'")
|
||||
raise Exception("[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
|
||||
|
||||
is_sdxl_preset = 'SDXL' in preset
|
||||
is_sdxl_model = isinstance(clip.tokenizer, sdxl_clip.SDXLTokenizer)
|
||||
if clip is not None:
|
||||
is_sdxl_model = isinstance(clip.tokenizer, sdxl_clip.SDXLTokenizer)
|
||||
else:
|
||||
is_sdxl_model = False
|
||||
|
||||
if is_sdxl_preset != is_sdxl_model:
|
||||
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 1, "label": "IPADAPTER (fail)"})
|
||||
@@ -90,7 +98,7 @@ class IPAdapterModelHelper:
|
||||
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 3, "label": "INSIGHTFACE (fail)"})
|
||||
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 4, "label": "MODEL (fail)"})
|
||||
server.PromptServer.instance.send_sync("inspire-node-output-label", {"node_id": unique_id, "output_idx": 5, "label": "CLIP (fail)"})
|
||||
print(f"[ERROR] IPAdapterModelHelper: You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
|
||||
logging.error("[Inspire Pack] IPAdapterModelHelper: You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
|
||||
raise Exception("[ERROR] You cannot mix SDXL and SD1.5 in the checkpoint and IPAdapter.")
|
||||
|
||||
ipadapter, clipvision, lora, is_insightface = model_preset[preset]
|
||||
@@ -149,13 +157,13 @@ class IPAdapterModelHelper:
|
||||
if 'IPAdapterInsightFaceLoader' in nodes.NODE_CLASS_MAPPINGS:
|
||||
insight_face_loader = nodes.NODE_CLASS_MAPPINGS['IPAdapterInsightFaceLoader']().load_insightface
|
||||
else:
|
||||
print("'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
|
||||
logging.warning("'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
|
||||
insight_face_loader = None
|
||||
|
||||
icache_key = ""
|
||||
if is_insightface:
|
||||
if insight_face_loader is None:
|
||||
raise Exception(f"[ERROR] 'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
|
||||
raise Exception("[ERROR] 'ComfyUI IPAdapter Plus' extension is either too outdated or not installed.")
|
||||
|
||||
if cache_mode in ["insightface only", "all"]:
|
||||
icache_key = 'insightface-' + insightface_provider
|
||||
|
||||
+260
-80
@@ -12,20 +12,24 @@ import folder_paths
|
||||
import comfy
|
||||
import traceback
|
||||
import random
|
||||
import hashlib
|
||||
|
||||
from server import PromptServer
|
||||
from .libs import utils, common
|
||||
from .backend_support import CheckpointLoaderSimpleShared
|
||||
|
||||
import logging
|
||||
|
||||
model_path = folder_paths.models_dir
|
||||
utils.add_folder_path_and_extensions("inspire_prompts", [os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "prompts"))], {'.txt'})
|
||||
|
||||
|
||||
prompt_builder_preset = {}
|
||||
|
||||
|
||||
resource_path = os.path.join(os.path.dirname(__file__), "..", "resources")
|
||||
resource_path = os.path.abspath(resource_path)
|
||||
|
||||
prompts_path = os.path.join(os.path.dirname(__file__), "..", "prompts")
|
||||
prompts_path = os.path.abspath(prompts_path)
|
||||
|
||||
|
||||
try:
|
||||
pb_yaml_path = os.path.join(resource_path, 'prompt-builder.yaml')
|
||||
@@ -36,129 +40,250 @@ try:
|
||||
|
||||
with open(pb_yaml_path, 'r', encoding="utf-8") as f:
|
||||
prompt_builder_preset = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
print(f"[Inspire Pack] Failed to load 'prompt-builder.yaml'")
|
||||
except Exception as e: # noqa: F841
|
||||
logging.error("[Inspire Pack] Failed to load 'prompt-builder.yaml'\nNOTE: Only files with UTF-8 encoding are supported.")
|
||||
|
||||
|
||||
class LoadPromptsFromDir:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
global prompts_path
|
||||
try:
|
||||
prompt_dirs = [d for d in os.listdir(prompts_path) if os.path.isdir(os.path.join(prompts_path, d))]
|
||||
prompt_dirs = []
|
||||
for x in folder_paths.get_folder_paths('inspire_prompts'):
|
||||
for d in os.listdir(x):
|
||||
if os.path.isdir(os.path.join(x, d)):
|
||||
prompt_dirs.append(d)
|
||||
except Exception:
|
||||
prompt_dirs = []
|
||||
|
||||
return {"required": {"prompt_dir": (prompt_dirs,)}}
|
||||
return {"required": {
|
||||
"prompt_dir": (prompt_dirs,)
|
||||
},
|
||||
"optional": {
|
||||
"reload": ("BOOLEAN", { "default": False, "label_on": "if file changed", "label_off": "if value changed"}),
|
||||
"load_cap": ("INT", {"default": 0, "min": 0, "step": 1, "advanced": True, "tooltip": "The amount of prompts to load at once:\n0: Load all\n1 or higher: Load a specified number"}),
|
||||
"start_index": ("INT", {"default": 0, "min": -1, "step": 1, "max": 0xffffffffffffffff, "advanced": True, "tooltip": "Starting index for loading prompts:\n-1: The last prompt\n0 or higher: Load from the specified index"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("ZIPPED_PROMPT",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
RETURN_TYPES = ("ZIPPED_PROMPT", "INT", "INT")
|
||||
RETURN_NAMES = ("zipped_prompt", "count", "remaining_count")
|
||||
OUTPUT_IS_LIST = (True, False, False)
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Prompt"
|
||||
|
||||
@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:
|
||||
print(f"file_name: {file_name}")
|
||||
for file_name in prompt_files:
|
||||
logging.info(f"file_name: {file_name}")
|
||||
try:
|
||||
with open(os.path.join(prompt_dir, file_name), "r", encoding="utf-8") as file:
|
||||
with open(file_name, "r", encoding="utf-8") as file:
|
||||
prompt_data = file.read()
|
||||
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
|
||||
|
||||
for prompt in prompt_list:
|
||||
pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)"
|
||||
matches = re.search(pattern, prompt, re.DOTALL)
|
||||
pattern = r"^(?:(?:positive:(?P<positive>.*?)|negative:(?P<negative>.*?)|name:(?P<name>.*?))\n*)+$"
|
||||
matches = re.search(pattern, prompt, re.DOTALL | re.IGNORECASE)
|
||||
|
||||
if matches:
|
||||
positive_text = matches.group(1).strip()
|
||||
negative_text = matches.group(2).strip()
|
||||
result_tuple = (positive_text, negative_text, file_name)
|
||||
positive_text = matches.group('positive').strip()
|
||||
negative_text = matches.group('negative').strip()
|
||||
name_text = matches.group('name').strip() if matches.group('name') else file_name
|
||||
result_tuple = (positive_text, negative_text, name_text)
|
||||
prompts.append(result_tuple)
|
||||
else:
|
||||
print(f"[WARN] LoadPromptsFromDir: invalid prompt format in '{file_name}'")
|
||||
logging.warning(f"[Inspire Pack] LoadPromptsFromDir: invalid prompt format in '{file_name}'")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}")
|
||||
logging.error(f"[Inspire Pack] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
|
||||
|
||||
return (prompts, )
|
||||
# slicing [start_index ~ start_index + load_cap]
|
||||
total_prompts = len(prompts)
|
||||
prompts = prompts[start_index:]
|
||||
remaining_count = False
|
||||
if load_cap > 0:
|
||||
remaining_count = max(0, len(prompts) - load_cap)
|
||||
prompts = prompts[:load_cap]
|
||||
|
||||
return prompts, total_prompts, remaining_count
|
||||
|
||||
|
||||
class LoadPromptsFromFile:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
global prompts_path
|
||||
prompt_files = []
|
||||
try:
|
||||
prompt_files = []
|
||||
for root, dirs, files in os.walk(prompts_path):
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
file_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(file_path, prompts_path)
|
||||
prompt_files.append(rel_path)
|
||||
prompts_paths = folder_paths.get_folder_paths('inspire_prompts')
|
||||
for prompts_path in prompts_paths:
|
||||
for root, dirs, files in os.walk(prompts_path):
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
file_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(file_path, prompts_path)
|
||||
prompt_files.append(rel_path)
|
||||
except Exception:
|
||||
prompt_files = []
|
||||
|
||||
return {"required": {"prompt_file": (prompt_files,)},
|
||||
"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, "max": 0xffffffffffffffff, "step": 1, "advanced": True, "tooltip": "Starting index for loading prompts:\n-1: The last prompt\n0 or higher: Load from the specified index"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("ZIPPED_PROMPT",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
RETURN_TYPES = ("ZIPPED_PROMPT", "INT", "INT")
|
||||
RETURN_NAMES = ("zipped_prompt", "count", "remaining_count")
|
||||
OUTPUT_IS_LIST = (True, False, False)
|
||||
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Prompt"
|
||||
|
||||
@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:
|
||||
logging.info(f"[Inspire Pack] LoadPromptsFromFile: file found '{prompt_file}'")
|
||||
else:
|
||||
logging.warning(f"[Inspire Pack] 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}'")
|
||||
logging.warning(f"[Inspire Pack] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
|
||||
logging.error(f"[Inspire Pack] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
|
||||
|
||||
return (prompts, )
|
||||
# slicing [start_index ~ start_index + load_cap]
|
||||
total_prompts = len(prompts)
|
||||
prompts = prompts[start_index:]
|
||||
remaining_count = 0
|
||||
if load_cap > 0:
|
||||
remaining_count = max(0, len(prompts) - load_cap)
|
||||
prompts = prompts[:load_cap]
|
||||
|
||||
return prompts, total_prompts, remaining_count
|
||||
|
||||
|
||||
class LoadSinglePromptFromFile:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
global prompts_path
|
||||
prompt_files = []
|
||||
try:
|
||||
prompt_files = []
|
||||
for root, dirs, files in os.walk(prompts_path):
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
file_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(file_path, prompts_path)
|
||||
prompt_files.append(rel_path)
|
||||
prompts_paths = folder_paths.get_folder_paths('inspire_prompts')
|
||||
for prompts_path in prompts_paths:
|
||||
for root, dirs, files in os.walk(prompts_path):
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
file_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(file_path, prompts_path)
|
||||
prompt_files.append(rel_path)
|
||||
except Exception:
|
||||
prompt_files = []
|
||||
|
||||
@@ -178,7 +303,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:
|
||||
logging.info(f"[Inspire Pack] LoadSinglePromptFromFile: file found '{prompt_file}'")
|
||||
else:
|
||||
logging.warning(f"[Inspire Pack] LoadSinglePromptFromFile: file not found '{prompt_file}'")
|
||||
|
||||
prompts = []
|
||||
try:
|
||||
@@ -187,25 +324,26 @@ class LoadSinglePromptFromFile:
|
||||
prompt_data = file.read()
|
||||
else:
|
||||
prompt_data = text_data_opt
|
||||
|
||||
|
||||
prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data)
|
||||
try:
|
||||
prompt = prompt_list[index]
|
||||
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}'")
|
||||
logging.warning(f"[Inspire Pack] LoadSinglePromptFromFile: invalid prompt format in '{prompt_file}'")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] LoadSinglePromptFromFile: an error occurred while processing '{prompt_file}': {str(e)}")
|
||||
logging.error(f"[Inspire Pack] LoadSinglePromptFromFile: an error occurred while processing '{prompt_file}': {str(e)}\nNOTE: Only files with UTF-8 encoding are supported.")
|
||||
|
||||
return (prompts, )
|
||||
|
||||
@@ -349,6 +487,37 @@ class GlobalSeed:
|
||||
return {}
|
||||
|
||||
|
||||
class SeedLogger:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"seeds": ("STRING", {"multiline": True, "dynamicPrompts": False, "control_after_generate": False}),
|
||||
"limit": ("INT", {"default": 5, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Prompt"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def doit(self, seed, seeds: str, limit, unique_id):
|
||||
|
||||
if limit > 0:
|
||||
lines = seeds.split('\n')
|
||||
res = str(seed) + '\n' + '\n'.join(lines[:limit-1])
|
||||
else:
|
||||
res = str(seed) + '\n' + seeds
|
||||
|
||||
PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": unique_id, "widget_name": "seeds", "type": "text", "data": res})
|
||||
return {}
|
||||
|
||||
|
||||
class GlobalSampler:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -426,7 +595,7 @@ class BNK_EncoderWrapper:
|
||||
if 'BNK_CLIPTextEncodeAdvanced' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb',
|
||||
"To use 'WildcardEncodeInspire' node, 'ComfyUI_ADV_CLIP_emb' extension is required.")
|
||||
raise Exception(f"[ERROR] To use WildcardEncodeInspire, you need to install 'Advanced CLIP Text Encode'")
|
||||
raise Exception("[ERROR] To use WildcardEncodeInspire, you need to install 'Advanced CLIP Text Encode'")
|
||||
return nodes.NODE_CLASS_MAPPINGS['BNK_CLIPTextEncodeAdvanced']().encode(clip, text, self.token_normalization, self.weight_interpretation)
|
||||
|
||||
|
||||
@@ -440,7 +609,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}),
|
||||
@@ -461,7 +636,7 @@ class WildcardEncodeInspire:
|
||||
if 'ImpactWildcardEncode' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
"To use 'Wildcard Encode (Inspire)' node, 'Impact Pack' extension is required.")
|
||||
raise Exception(f"[ERROR] To use 'Wildcard Encode (Inspire)', you need to install 'Impact Pack'")
|
||||
raise Exception("[ERROR] To use 'Wildcard Encode (Inspire)', you need to install 'Impact Pack'")
|
||||
|
||||
processed = []
|
||||
model, clip, conditioning = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardEncode'].process_with_loras(wildcard_opt=populated, model=kwargs['model'], clip=kwargs['clip'], seed=kwargs['seed'], clip_encoder=clip_encoder, processed=processed)
|
||||
@@ -481,7 +656,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)'}),
|
||||
@@ -490,7 +669,6 @@ class MakeBasicPipe:
|
||||
"weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"], {'default': 'comfy++'}),
|
||||
|
||||
"stop_at_clip_layer": ("INT", {"default": -2, "min": -24, "max": -1, "step": 1}),
|
||||
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -513,7 +691,7 @@ class MakeBasicPipe:
|
||||
if 'ImpactWildcardEncode' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
"To use 'Make Basic Pipe (Inspire)' node, 'Impact Pack' extension is required.")
|
||||
raise Exception(f"[ERROR] To use 'Make Basic Pipe (Inspire)', you need to install 'Impact Pack'")
|
||||
raise Exception("[ERROR] To use 'Make Basic Pipe (Inspire)', you need to install 'Impact Pack'")
|
||||
|
||||
model, clip, vae, key = CheckpointLoaderSimpleShared().doit(ckpt_name=kwargs['ckpt_name'], key_opt=kwargs['ckpt_key_opt'])
|
||||
clip = nodes.CLIPSetLastLayer().set_last_layer(clip, kwargs['stop_at_clip_layer'])[0]
|
||||
@@ -566,11 +744,11 @@ class SeedExplorer:
|
||||
"optional":
|
||||
{
|
||||
"variation_method": (["linear", "slerp"],),
|
||||
"model": ("model",),
|
||||
"model": ("MODEL",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("NOISE",)
|
||||
RETURN_TYPES = ("NOISE_IMAGE",)
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "InspirePack/Prompt"
|
||||
@@ -591,7 +769,7 @@ class SeedExplorer:
|
||||
|
||||
noise = utils.apply_variation_noise(noise, noise_device, variation_seed, variation_strength, mask=mask, variation_method=variation_method)
|
||||
except Exception:
|
||||
print(f"[ERROR] IGNORED: SeedExplorer failed to processing '{x}'")
|
||||
logging.error(f"[Inspire Pack] IGNORED: SeedExplorer failed to processing '{x}'")
|
||||
traceback.print_exc()
|
||||
return noise
|
||||
|
||||
@@ -632,7 +810,7 @@ class SeedExplorer:
|
||||
return (noise,)
|
||||
|
||||
except Exception:
|
||||
print(f"[ERROR] IGNORED: SeedExplorer failed")
|
||||
logging.error("[Inspire Pack] IGNORED: SeedExplorer failed")
|
||||
traceback.print_exc()
|
||||
|
||||
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout,
|
||||
@@ -645,15 +823,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"
|
||||
@@ -846,6 +1024,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ZipPrompt //Inspire": ZipPrompt,
|
||||
"PromptExtractor //Inspire": PromptExtractor,
|
||||
"GlobalSeed //Inspire": GlobalSeed,
|
||||
"SeedLogger //Inspire": SeedLogger,
|
||||
"GlobalSampler //Inspire": GlobalSampler,
|
||||
"BindImageListPromptList //Inspire": BindImageListPromptList,
|
||||
"WildcardEncode //Inspire": WildcardEncodeInspire,
|
||||
@@ -857,7 +1036,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"MakeBasicPipe //Inspire": MakeBasicPipe,
|
||||
"RemoveControlNet //Inspire": RemoveControlNet,
|
||||
"RemoveControlNetFromRegionalPrompts //Inspire": RemoveControlNetFromRegionalPrompts,
|
||||
"CompositeNoise //Inspire": CompositeNoise
|
||||
"CompositeNoise //Inspire": CompositeNoise,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -868,6 +1047,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ZipPrompt //Inspire": "Zip Prompt (Inspire)",
|
||||
"PromptExtractor //Inspire": "Prompt Extractor (Inspire)",
|
||||
"GlobalSeed //Inspire": "Global Seed (Inspire)",
|
||||
"SeedLogger //Inspire": "Seed Logger (Inspire)",
|
||||
"GlobalSampler //Inspire": "Global Sampler (Inspire)",
|
||||
"BindImageListPromptList //Inspire": "Bind [ImageList, PromptList] (Inspire)",
|
||||
"WildcardEncode //Inspire": "Wildcard Encode (Inspire)",
|
||||
|
||||
+15
-13
@@ -4,10 +4,12 @@ import comfy
|
||||
import nodes
|
||||
import torch
|
||||
import re
|
||||
import webcolors
|
||||
|
||||
from . import prompt_support
|
||||
from .libs import utils, common
|
||||
|
||||
import logging
|
||||
|
||||
class RegionalPromptSimple:
|
||||
@classmethod
|
||||
@@ -42,7 +44,7 @@ class RegionalPromptSimple:
|
||||
if 'RegionalPrompt' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
"To use 'RegionalPromptSimple' node, 'Impact Pack' extension is required.")
|
||||
raise Exception(f"[ERROR] To use RegionalPromptSimple, you need to install 'ComfyUI-Impact-Pack'")
|
||||
raise Exception("[ERROR] To use RegionalPromptSimple, you need to install 'ComfyUI-Impact-Pack'")
|
||||
|
||||
model, clip, vae, positive, negative = basic_pipe
|
||||
|
||||
@@ -81,12 +83,13 @@ 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:
|
||||
raise Exception(f"[ERROR] Invalid mask_color value. mask_color should be a color value for RGB")
|
||||
raise Exception("[ERROR] Invalid mask_color value. mask_color should be a color value for RGB")
|
||||
|
||||
temp = (torch.clamp(color_mask, 0, 1.0) * 255.0).round().to(torch.int)
|
||||
temp = torch.bitwise_left_shift(temp[:, :, :, 0], 16) + torch.bitwise_left_shift(temp[:, :, :, 1], 8) + temp[:, :, :, 2]
|
||||
@@ -258,7 +261,7 @@ class IPAdapterConditioning:
|
||||
if 'IPAdapterAdvanced' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/cubiq/ComfyUI_IPAdapter_plus',
|
||||
"To use 'Regional IPAdapter' node, 'ComfyUI IPAdapter Plus' extension is required.")
|
||||
raise Exception(f"[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
|
||||
raise Exception("[ERROR] To use IPAdapterModelHelper, you need to install 'ComfyUI IPAdapter Plus'")
|
||||
|
||||
if self.embeds is None:
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['IPAdapterAdvanced']
|
||||
@@ -285,7 +288,7 @@ def IPADAPTER_WEIGHT_TYPES():
|
||||
try:
|
||||
IPADAPTER_WEIGHT_TYPES_CACHE = nodes.NODE_CLASS_MAPPINGS['IPAdapterAdvanced']().INPUT_TYPES()['required']['weight_type'][0]
|
||||
except Exception:
|
||||
print(f"[Inspire Pack] IPAdapterPlus is not installed.")
|
||||
logging.error("[Inspire Pack] IPAdapterPlus is not installed.")
|
||||
IPADAPTER_WEIGHT_TYPES_CACHE = ["IPAdapterPlus is not installed"]
|
||||
|
||||
return IPADAPTER_WEIGHT_TYPES_CACHE
|
||||
@@ -332,7 +335,6 @@ class RegionalIPAdapterColorMask:
|
||||
"required": {
|
||||
"color_mask": ("IMAGE",),
|
||||
"mask_color": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
|
||||
"image": ("IMAGE",),
|
||||
"weight": ("FLOAT", {"default": 0.7, "min": -1, "max": 3, "step": 0.05}),
|
||||
"noise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
@@ -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"
|
||||
@@ -499,7 +501,7 @@ class RegionalSeedExplorerMask:
|
||||
|
||||
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask, variation_method=variation_method)
|
||||
except Exception:
|
||||
print(f"[ERROR] IGNORED: RegionalSeedExplorerColorMask is failed.")
|
||||
logging.error("[Inspire Pack] IGNORED: RegionalSeedExplorerColorMask is failed.")
|
||||
traceback.print_exc()
|
||||
|
||||
noise = noise.cpu()
|
||||
@@ -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"
|
||||
@@ -557,7 +559,7 @@ class RegionalSeedExplorerColorMask:
|
||||
|
||||
noise = prompt_support.SeedExplorer.apply_variation(noise, items, noise_device, mask, variation_method=variation_method)
|
||||
except Exception:
|
||||
print(f"[ERROR] IGNORED: RegionalSeedExplorerColorMask is failed.")
|
||||
logging.error("[Inspire Pack] IGNORED: RegionalSeedExplorerColorMask is failed.")
|
||||
traceback.print_exc()
|
||||
|
||||
color_mask.cpu()
|
||||
|
||||
@@ -2,11 +2,11 @@ import torch
|
||||
from . import a1111_compat
|
||||
import comfy
|
||||
from .libs import common
|
||||
from comfy import model_management
|
||||
from comfy.samplers import CFGGuider
|
||||
from comfy_extras.nodes_perpneg import Guider_PerpNeg
|
||||
import math
|
||||
|
||||
|
||||
class KSampler_progress(a1111_compat.KSampler_inspire):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -21,7 +21,7 @@ class KSampler_progress(a1111_compat.KSampler_inspire):
|
||||
"negative": ("CONDITIONING", ),
|
||||
"latent_image": ("LATENT", ),
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (a1111_compat.supported_noise_modes,),
|
||||
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
|
||||
"omit_start_latent": ("BOOLEAN", {"default": True, "label_on": "True", "label_off": "False"}),
|
||||
"omit_final_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||||
@@ -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:
|
||||
@@ -85,7 +85,7 @@ class KSamplerAdvanced_progress(a1111_compat.KSamplerAdvanced_inspire):
|
||||
"latent_image": ("LATENT", ),
|
||||
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
||||
"noise_mode": (["GPU(=A1111)", "CPU"],),
|
||||
"noise_mode": (a1111_compat.supported_noise_modes,),
|
||||
"return_with_leftover_noise": ("BOOLEAN", {"default": False, "label_on": "enable", "label_off": "disable"}),
|
||||
"interval": ("INT", {"default": 1, "min": 1, "max": 10000}),
|
||||
"omit_start_latent": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||||
@@ -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())
|
||||
|
||||
+37
-20
@@ -2,7 +2,7 @@ import nodes
|
||||
import numpy as np
|
||||
import torch
|
||||
from .libs import utils
|
||||
|
||||
import logging
|
||||
|
||||
def normalize_size_base_64(w, h):
|
||||
short_side = min(w, h)
|
||||
@@ -28,12 +28,12 @@ class MediaPipeFaceMeshDetector:
|
||||
if 'MediaPipe-FaceMeshPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'MediaPipeFaceMeshDetector' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
if 'MediaPipeFaceMeshToSEGS' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
"To use 'MediaPipeFaceMeshDetector' node, 'Impact Pack' extension is required.")
|
||||
raise Exception(f"[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI-Impact-Pack'")
|
||||
raise Exception("[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'ComfyUI-Impact-Pack'")
|
||||
|
||||
pre_obj = nodes.NODE_CLASS_MAPPINGS['MediaPipe-FaceMeshPreprocessor']
|
||||
seg_obj = nodes.NODE_CLASS_MAPPINGS['MediaPipeFaceMeshToSEGS']
|
||||
@@ -63,7 +63,7 @@ class MediaPipe_FaceMesh_Preprocessor_wrapper:
|
||||
if 'MediaPipe-FaceMeshPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
if self.upscale_factor != 1.0:
|
||||
image = nodes.ImageScaleBy().upscale(image, 'bilinear', self.upscale_factor)[0]
|
||||
@@ -78,7 +78,7 @@ class AnimeLineArt_Preprocessor_wrapper:
|
||||
if 'AnimeLineArtPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'AnimeLineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use AnimeLineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use AnimeLineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['AnimeLineArtPreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -90,7 +90,7 @@ class Manga2Anime_LineArt_Preprocessor_wrapper:
|
||||
if 'Manga2Anime_LineArt_Preprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'Manga2Anime_LineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use Manga2Anime_LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use Manga2Anime_LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['Manga2Anime_LineArt_Preprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -102,7 +102,7 @@ class Color_Preprocessor_wrapper:
|
||||
if 'ColorPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'Color_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use Color_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use Color_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['ColorPreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -110,17 +110,29 @@ class Color_Preprocessor_wrapper:
|
||||
|
||||
|
||||
class InpaintPreprocessor_wrapper:
|
||||
def __init__(self, black_pixel_for_xinsir_cn):
|
||||
self.black_pixel_for_xinsir_cn = black_pixel_for_xinsir_cn
|
||||
|
||||
def apply(self, image, mask=None):
|
||||
if 'InpaintPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'InpaintPreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use InpaintPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use InpaintPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['InpaintPreprocessor']()
|
||||
if mask is None:
|
||||
mask = torch.ones((image.shape[1], image.shape[2]), dtype=torch.float32, device="cpu").unsqueeze(0)
|
||||
|
||||
return obj.preprocess(image, mask)[0]
|
||||
try:
|
||||
res = obj.preprocess(image, mask, black_pixel_for_xinsir_cn=self.black_pixel_for_xinsir_cn)[0]
|
||||
except Exception as e:
|
||||
if self.black_pixel_for_xinsir_cn:
|
||||
raise e
|
||||
else:
|
||||
res = obj.preprocess(image, mask)[0]
|
||||
logging.warning("[Inspire Pack] Installed 'ComfyUI's ControlNet Auxiliary Preprocessors.' is outdated.")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class TilePreprocessor_wrapper:
|
||||
@@ -131,7 +143,7 @@ class TilePreprocessor_wrapper:
|
||||
if 'TilePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'TilePreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use TilePreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use TilePreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -143,7 +155,7 @@ class MeshGraphormerDepthMapPreprocessorProvider_wrapper:
|
||||
if 'MeshGraphormer-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'MeshGraphormerDepthMapPreprocessorProvider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use MeshGraphormerDepthMapPreprocessorProvider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use MeshGraphormerDepthMapPreprocessorProvider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['MeshGraphormer-DepthMapPreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -158,7 +170,7 @@ class LineArt_Preprocessor_wrapper:
|
||||
if 'LineArtPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'LineArt_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use LineArt_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
coarse = 'enable' if self.coarse else 'disable'
|
||||
|
||||
@@ -178,7 +190,7 @@ class OpenPose_Preprocessor_wrapper:
|
||||
if 'OpenposePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'OpenPose_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use OpenPose_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use OpenPose_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
detect_hand = 'enable' if self.detect_hand else 'disable'
|
||||
detect_body = 'enable' if self.detect_body else 'disable'
|
||||
@@ -205,7 +217,7 @@ class DWPreprocessor_wrapper:
|
||||
if 'DWPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'DWPreprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use DWPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use DWPreprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
detect_hand = 'enable' if self.detect_hand else 'disable'
|
||||
detect_body = 'enable' if self.detect_body else 'disable'
|
||||
@@ -229,7 +241,7 @@ class LeReS_DepthMap_Preprocessor_wrapper:
|
||||
if 'LeReS-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'LeReS_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use LeReS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use LeReS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
boost = 'enable' if self.boost else 'disable'
|
||||
|
||||
@@ -247,7 +259,7 @@ class MiDaS_DepthMap_Preprocessor_wrapper:
|
||||
if 'MiDaS-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'MiDaS_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use MiDaS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use MiDaS_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['MiDaS-DepthMapPreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -259,7 +271,7 @@ class Zoe_DepthMap_Preprocessor_wrapper:
|
||||
if 'Zoe-DepthMapPreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
|
||||
utils.try_install_custom_node('https://github.com/Fannovel16/comfyui_controlnet_aux',
|
||||
"To use 'Zoe_DepthMap_Preprocessor_Provider' node, 'ComfyUI's ControlNet Auxiliary Preprocessors.' extension is required.")
|
||||
raise Exception(f"[ERROR] To use Zoe_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
raise Exception("[ERROR] To use Zoe_DepthMap_Preprocessor_Provider, you need to install 'ComfyUI's ControlNet Auxiliary Preprocessors.'")
|
||||
|
||||
obj = nodes.NODE_CLASS_MAPPINGS['Zoe-DepthMapPreprocessor']()
|
||||
resolution = normalize_size_base_64(image.shape[2], image.shape[1])
|
||||
@@ -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, )
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { inspireProgressBadge } from "./progress-badge.js"
|
||||
|
||||
export function register_loop_node(nodeType, nodeData, app) {
|
||||
if(nodeData.name == 'ForeachListEnd //Inspire') {
|
||||
inspireProgressBadge.addStatusHandler(nodeType);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ComfyApp, app } from "../../scripts/app.js";
|
||||
import { register_concat_conditionings_with_multiplier_node, register_splitter } from "./inspire-flex.js";
|
||||
import { register_cache_info } from "./inspire-backend.js";
|
||||
import { register_loop_node } from "./inspire-loop.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Inspire",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
await register_concat_conditionings_with_multiplier_node(nodeType, nodeData, app);
|
||||
await register_loop_node(nodeType, nodeData, app);
|
||||
},
|
||||
|
||||
nodeCreated(node, app) {
|
||||
|
||||
+91
-87
@@ -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;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
// copying from https://github.com/pythongosssss/ComfyUI-WD14-Tagger
|
||||
class InspireProgressBadge {
|
||||
constructor() {
|
||||
if (!window.__progress_badge__) {
|
||||
window.__progress_badge__ = Symbol("__inspire_progress_badge__");
|
||||
}
|
||||
this.symbol = window.__progress_badge__;
|
||||
}
|
||||
|
||||
getState(node) {
|
||||
return node[this.symbol] || {};
|
||||
}
|
||||
|
||||
setState(node, state) {
|
||||
node[this.symbol] = state;
|
||||
app.canvas.setDirty(true);
|
||||
}
|
||||
|
||||
addStatusHandler(nodeType) {
|
||||
if (nodeType[this.symbol]?.statusTagHandler) {
|
||||
return;
|
||||
}
|
||||
if (!nodeType[this.symbol]) {
|
||||
nodeType[this.symbol] = {};
|
||||
}
|
||||
nodeType[this.symbol] = {
|
||||
statusTagHandler: true,
|
||||
};
|
||||
|
||||
api.addEventListener("inspire/update_status", ({ detail }) => {
|
||||
let { node, progress, text } = detail;
|
||||
const n = app.graph.getNodeById(+(node || app.runningNodeId));
|
||||
if (!n) return;
|
||||
const state = this.getState(n);
|
||||
state.status = Object.assign(state.status || {}, { progress: text ? progress : null, text: text || null });
|
||||
this.setState(n, state);
|
||||
});
|
||||
|
||||
const self = this;
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
const state = self.getState(this);
|
||||
if (!state?.status?.text) {
|
||||
return r;
|
||||
}
|
||||
|
||||
const { fgColor, bgColor, text, progress, progressColor } = { ...state.status };
|
||||
|
||||
ctx.save();
|
||||
ctx.font = "12px sans-serif";
|
||||
const sz = ctx.measureText(text);
|
||||
ctx.fillStyle = bgColor || "dodgerblue";
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
|
||||
ctx.fill();
|
||||
|
||||
if (progress) {
|
||||
ctx.fillStyle = progressColor || "green";
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, (sz.width + 12) * progress, 20, 5);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.fillStyle = fgColor || "#fff";
|
||||
ctx.fillText(text, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
|
||||
ctx.restore();
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const inspireProgressBadge = new InspireProgressBadge();
|
||||
|
||||
+94
-82
@@ -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
@@ -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.21"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["matplotlib", "cachetools"]
|
||||
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
matplotlib
|
||||
cachetools
|
||||
numpy<2
|
||||
numpy
|
||||
webcolors
|
||||
opencv-python
|
||||
|
||||
Reference in New Issue
Block a user