Compare commits

...
12 Commits
Author SHA1 Message Date
Dr.Lt.Data ac3668d946 feat: CustomSamplerDetailerHookProvider is added
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/957
2025-06-11 21:52:25 +09:00
Emmanuel Ferdman 78d3793a77 Resolve regex library warnings (#1011)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-06-10 12:35:46 +09:00
Dr.Lt.Data 2346b67766 fixed: Switch (Any) - copy&paste error
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1012
2025-06-01 04:52:24 +09:00
Dr.Lt.Data 025db4b581 fixed: MaskRectArea - type error
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1001
2025-06-01 04:43:46 +09:00
Dr.Lt.Data f8e16df2be bump version 2025-05-19 08:33:30 +09:00
Dr.Lt.Data 7df60b2107 fixed: install.py - user friendly message with exception handling 2025-05-19 08:31:07 +09:00
Dr.Lt.Data b394c158ea modified: requirements.txt remove pinning <2 for numpy 2025-05-19 05:16:00 +09:00
Dr.Lt.Data 3e3cf3a5b4 support nunchaku lora loading in the wildcard feature
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/993
2025-05-18 18:14:42 +09:00
Dr.Lt.Data 93fc248503 fixed: cannot connect wildcard input/output to the Switch / InversedSwitch nodes
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/992
2025-05-16 04:42:26 +09:00
Dr.Lt.Data 16ffa7d462 hotfix: potential front error
```
TypeError: Cannot read properties of undefined (reading 'type')
```

https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/989
2025-05-16 03:59:10 +09:00
filtered cd34cfdd63 Fix empty widgets array throws in console (#988) 2025-05-03 14:37:31 +09:00
Dr.Lt.Data 38bb9ffdf6 fixed: Switch - buggy behavior on firefox browser
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/983#issuecomment-2833471721
2025-04-28 00:54:31 +09:00
12 changed files with 135 additions and 56 deletions
+1
View File
@@ -189,6 +189,7 @@ NOTE: The UltralyticsDetectorProvider node is not part of the ComfyUI-Impact-Pac
* `PreviewDetailerHook` - Connecting this hook node helps provide assistance for viewing previews whenever SEGS Detailing tasks are completed. When working with a large number of SEGS, such as Make Tile SEGS, it allows for monitoring the situation as improvements progress incrementally.
* Since this is the hook applied when pasting onto the original image, it has no effect on nodes like `SEGSDetailer`.
* `VariationNoiseDetailerHookProvider` - Apply variation seed to the detailer. It can be applied in multiple stages through combine.
* `CustomSamplerDetailerHookProvider` - Apply a hook that allows you to use a custom sampler in the Detailer nodes. When using `DetailerHookCombine`, the sampler from the first hook is applied.
### Iterative Upscale nodes
* `Iterative Upscale (Latent/on Pixel Space)` - The upscaler takes the input upscaler and splits the scale_factor into steps, then iteratively performs upscaling.
+1
View File
@@ -122,6 +122,7 @@ NODE_CLASS_MAPPINGS = {
"UnsamplerHookProvider": UnsamplerHookProvider,
"CoreMLDetailerHookProvider": CoreMLDetailerHookProvider,
"PreviewDetailerHookProvider": PreviewDetailerHookProvider,
"CustomSamplerDetailerHookProvider": CustomSamplerDetailerHookProvider,
"DetailerHookCombine": DetailerHookCombine,
"NoiseInjectionDetailerHookProvider": NoiseInjectionDetailerHookProvider,
+14 -12
View File
@@ -68,7 +68,6 @@ def process_wrap(cmd_str, cwd=None, handler=None, env=None):
try:
import platform
from torchvision.datasets.utils import download_url
import impact.config
@@ -99,7 +98,7 @@ try:
if not os.path.exists(os.path.join(sam_path, "sam_vit_b_01ec64.pth")):
download_url("https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", sam_path)
except:
print(f"[Impact Pack] Failed to auto-download model files. Please download them manually.")
print("[Impact Pack] Failed to auto-download model files. Please download them manually.")
if not os.path.exists(onnx_path):
print(f"### ComfyUI-Impact-Pack: onnx model directory created ({onnx_path})")
@@ -108,18 +107,21 @@ try:
impact.config.write_config()
# Remove legacy subpack
subpack_path = os.path.join(os.path.dirname(__file__), 'impact_subpack')
if os.path.exists(subpack_path):
shutil.rmtree(subpack_path)
print(f"Legacy subpack is detected. '{subpack_path}' is removed.")
subpack_path = os.path.join(os.path.dirname(__file__), 'subpack')
if os.path.exists(subpack_path):
shutil.rmtree(subpack_path)
print(f"Legacy subpack is detected. '{subpack_path}' is removed.")
try:
subpack_path = os.path.join(os.path.dirname(__file__), 'impact_subpack')
if os.path.exists(subpack_path):
shutil.rmtree(subpack_path)
print(f"Legacy subpack is detected. '{subpack_path}' is removed.")
subpack_path = os.path.join(os.path.dirname(__file__), 'subpack')
if os.path.exists(subpack_path):
shutil.rmtree(subpack_path)
print(f"Legacy subpack is detected. '{subpack_path}' is removed.")
except:
print(f"ERROT: Failed to delete legacy subpack '{subpack_path}'\nPlease delete the folder after terminate ComfyUI.")
install()
except Exception as e:
except Exception:
print("[ERROR] ComfyUI-Impact-Pack: Dependency installation has failed. Please install manually.")
traceback.print_exc()
+25 -13
View File
@@ -370,12 +370,12 @@ app.registerExtension({
if(type == 2) {
// connect output
if(connected){
if(app.graph._nodes_by_id[link_info.target_id].type == 'Reroute') {
if(app.graph._nodes_by_id[link_info.target_id]?.type == 'Reroute') {
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
}
if(this.outputs[0].type == '*'){
if(link_info.type == '*') {
if(link_info.type == '*' && app.graph.getNodeById(link_info.target_id).slots[link_info.target_slot].type != '*') {
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
}
else {
@@ -392,7 +392,7 @@ app.registerExtension({
}
}
else {
if(app.graph._nodes_by_id[link_info.origin_id].type == 'Reroute')
if(app.graph._nodes_by_id[link_info.origin_id]?.type == 'Reroute')
this.disconnectInput(link_info.target_slot);
// connect input
@@ -404,7 +404,7 @@ app.registerExtension({
return; // fallback
}
if(origin_type == '*') {
if(origin_type == '*' && app.graph.getNodeById(link_info.origin_id).slots[link_info.origin_slot].type != '*') {
this.disconnectInput(link_info.target_slot);
return;
}
@@ -428,8 +428,9 @@ app.registerExtension({
!stackTrace.includes('LGraphNode.prototype.connect') && // for touch device
!stackTrace.includes('LGraphNode.connect') && // for mouse device
!stackTrace.includes('loadGraphData')) {
if(this.outputs[link_info.origin_slot].links.length == 0)
if(this.outputs[link_info.origin_slot].links.length == 0) {
this.removeOutput(link_info.origin_slot);
}
}
}
@@ -442,9 +443,12 @@ app.registerExtension({
slot_i++;
}
let last_slot = this.outputs[this.outputs.length - 1];
if (last_slot.slot_index == link_info.origin_slot) {
this.addOutput(`output${slot_i}`, this.outputs[0].type);
if(connected) {
// NOTE: node.slot_index is different with link_info.origin_slot
let last_slot_index = this.outputs.length - 1;
if (last_slot_index == link_info.origin_slot) {
this.addOutput(`output${slot_i}`, this.outputs[0].type);
}
}
let select_slot = this.inputs.find(x => x.name == "select");
@@ -508,8 +512,16 @@ app.registerExtension({
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
const stackTrace = new Error().stack;
if(stackTrace.includes('LGraph.configure')) {
if(this.widgets) {
if(stackTrace.includes('loadGraphData')) {
if(this.widgets?.[0]) {
this.widgets[0].options.max = this.inputs.length-3;
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
}
return;
}
if(stackTrace.includes('pasteFromClipboard')) {
if(this.widgets?.[0]) {
this.widgets[0].options.max = this.inputs.length-3;
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
}
@@ -527,7 +539,7 @@ app.registerExtension({
}
if(this.outputs[0].type == '*'){
if(link_info.type == '*') {
if(link_info.type == '*' && app.graph.getNodeById(link_info.target_id).slots[link_info.target_slot].type != '*') {
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
}
else {
@@ -563,7 +575,7 @@ app.registerExtension({
node.connect(link_info.origin_slot, node.id, 'input1');
}
if(origin_type == '*') {
if(origin_type == '*' && app.graph.getNodeById(link_info.origin_id).slots[link_info.origin_slot].type != '*') {
this.disconnectInput(link_info.target_slot);
return;
}
@@ -606,7 +618,7 @@ app.registerExtension({
this.addInput(`${input_name}${slot_i}`, this.outputs[0].type);
}
if(this.widgets) {
if(this.widgets?.[0]) {
this.widgets[0].options.max = this.inputs.length-3;
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
}
+1 -1
View File
@@ -1,7 +1,7 @@
import configparser
import os
version_code = [8, 14, 1]
version_code = [8, 16]
version = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
dependency_version = 24
+11 -2
View File
@@ -344,6 +344,10 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
refined_latent = latent_image
sampler_opt=None
if detailer_hook is not None:
sampler_opt = detailer_hook.get_custom_sampler()
# ksampler
for i in range(0, cycle):
if detailer_hook is not None:
@@ -364,7 +368,7 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
refined_latent = impact_sampling.ksampler_wrapper(model2, seed2, steps2, cfg2, sampler_name2, scheduler2, positive2, negative2,
refined_latent, denoise2, refiner_ratio, refiner_model, refiner_clip, refiner_positive, refiner_negative,
noise=noise, scheduler_func=scheduler_func)
noise=noise, scheduler_func=scheduler_func, sampler_opt=sampler_opt)
if detailer_hook is not None:
refined_latent = detailer_hook.pre_decode(refined_latent)
@@ -513,11 +517,16 @@ def enhance_detail_for_animatediff(image_frames, model, clip, vae, guide_size, g
'samples': latent_frames
}
sampler_opt=None
if detailer_hook is not None:
sampler_opt = detailer_hook.get_custom_sampler()
if detailer_hook is not None:
latent = detailer_hook.post_encode(latent)
refined_latent = impact_sampling.ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
latent, denoise, refiner_ratio, refiner_model, refiner_clip, refiner_positive, refiner_negative, scheduler_func=scheduler_func)
latent, denoise, refiner_ratio, refiner_model, refiner_clip, refiner_positive, refiner_negative, scheduler_func=scheduler_func, sampler_opt=sampler_opt)
if detailer_hook is not None:
refined_latent = detailer_hook.pre_decode(refined_latent)
+18
View File
@@ -109,6 +109,12 @@ class DetailerHookCombine(PixelKSampleHookCombine):
noise_2nd, is_touched = self.hook2.get_custom_noise(seed, noise, is_touched)
return noise, is_touched
def get_custom_sampler():
if self.hook1.get_custom_sampler() is not None:
return self.hook1.get_custom_sampler()
else:
return self.hook2.get_custom_sampler()
class SimpleCfgScheduleHook(PixelKSampleHook):
target_cfg = 0
@@ -173,6 +179,18 @@ class DetailerHook(PixelKSampleHook):
def get_custom_noise(self, seed, noise, is_touched):
return noise, is_touched
def get_custom_sampler(self):
return None
class CustomSamplerDetailerHookProvider(DetailerHook):
def __init__(self, sampler):
super().__init__()
self.sampler = sampler
def get_custom_sampler(self):
return self.sampler
# class CustomNoiseDetailerHookProvider(DetailerHook):
# def __init__(self, noise):
+21 -1
View File
@@ -811,6 +811,26 @@ class CoreMLDetailerHookProvider:
return (hook, )
class CustomSamplerDetailerHookProvider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"sampler": ("SAMPLER", ),
},
}
RETURN_TYPES = ("DETAILER_HOOK",)
FUNCTION = "doit"
CATEGORY = "ImpactPack/Detailer"
DESCRIPTION = "Apply a hook that allows you to use a custom sampler in the Detailer nodes. When using `DetailerHookCombine`, the sampler from the first hook is applied."
def doit(self, sampler):
hook = hooks.CustomSamplerDetailerHookProvider(sampler)
return (hook, )
class CfgScheduleHookProvider:
schedules = ["simple"]
@@ -1906,7 +1926,7 @@ class MaskRectArea:
# search for node
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
if str(node["id"]) == unique_id:
min_x = node["properties"].get("x", 0) / 100
min_y = node["properties"].get("y", 0) / 100
width = node["properties"].get("w", 0) / 100
+4 -4
View File
@@ -194,7 +194,7 @@ def impact_sample(model, seed, steps, cfg, sampler_name, scheduler, positive, ne
def ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None, refiner_negative=None, sigma_factor=1.0, noise=None, scheduler_func=None):
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None, refiner_negative=None, sigma_factor=1.0, noise=None, scheduler_func=None, sampler_opt=None):
if refiner_ratio is None or refiner_model is None or refiner_clip is None or refiner_positive is None or refiner_negative is None:
# Use separated_sample instead of KSampler for `AYS scheduler`
@@ -206,7 +206,7 @@ def ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive,
refined_latent = separated_sample(model, True, seed, advanced_steps, cfg, sampler_name, scheduler,
positive, negative, latent_image, start_at_step, end_at_step, False,
sigma_ratio=sigma_factor, noise=noise, scheduler_func=scheduler_func)
sigma_ratio=sigma_factor, sampler_opt=sampler_opt, noise=noise, scheduler_func=scheduler_func)
else:
advanced_steps = math.floor(steps / denoise)
start_at_step = advanced_steps - steps
@@ -215,7 +215,7 @@ def ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive,
# print(f"pre: {start_at_step} .. {end_at_step} / {advanced_steps}")
temp_latent = separated_sample(model, True, seed, advanced_steps, cfg, sampler_name, scheduler,
positive, negative, latent_image, start_at_step, end_at_step, True,
sigma_ratio=sigma_factor, noise=noise, scheduler_func=scheduler_func)
sigma_ratio=sigma_factor, sampler_opt=sampler_opt, noise=noise, scheduler_func=scheduler_func)
if 'noise_mask' in latent_image:
# noise_latent = \
@@ -229,7 +229,7 @@ def ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive,
# print(f"post: {end_at_step} .. {advanced_steps + 1} / {advanced_steps}")
refined_latent = separated_sample(refiner_model, False, seed, advanced_steps, cfg, sampler_name, scheduler,
refiner_positive, refiner_negative, temp_latent, end_at_step, advanced_steps + 1, False,
sigma_ratio=sigma_factor, scheduler_func=scheduler_func)
sigma_ratio=sigma_factor, sampler_opt=sampler_opt, scheduler_func=scheduler_func)
return refined_latent
+37 -21
View File
@@ -8,6 +8,7 @@ import numpy as np
import threading
from impact import utils
from impact import config
import logging
wildcards_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "wildcards"))
@@ -211,7 +212,7 @@ def process(text, seed=None):
selected_items = random_gen.choice(options, p=normalized_probabilities, size=select_count, replace=False)
# x may be numpy.int32, convert to string
selected_items2 = [re.sub(r'^\s*[0-9.]+::', '', str(x), 1) for x in selected_items]
selected_items2 = [re.sub(r'^\s*[0-9.]+::', '', str(x), count=1) for x in selected_items]
replacement = select_sep.join(selected_items2)
if '::' in replacement:
pass
@@ -278,7 +279,7 @@ def process(text, seed=None):
normalized_probabilities = [prob / total_prob for prob in adjusted_probabilities]
selected_item = random_gen.choice(options, p=normalized_probabilities, replace=False)
replacement = re.sub(r'^\s*[0-9.]+::', '', selected_item, 1)
replacement = re.sub(r'^\s*[0-9.]+::', '', selected_item, count=1)
replacements_found = True
string = string.replace(f"__{match}__", replacement, 1)
elif '*' in keyword:
@@ -358,6 +359,7 @@ def extract_lora_values(string):
lbw = None
lbw_a = None
lbw_b = None
loader = None
if len(item) > 0:
lora = item[0]
@@ -376,6 +378,8 @@ def extract_lora_values(string):
lbw_b = safe_float(lbw_item[2:].strip())
elif lbw_item.strip() != '':
lbw = lbw_item
elif sub_item.startswith("LOADER="):
loader = sub_item[7:]
if a is None:
a = 1.0
@@ -383,7 +387,7 @@ def extract_lora_values(string):
b = a
if lora is not None and lora not in added:
result.append((lora, a, b, lbw, lbw_a, lbw_b))
result.append((lora, a, b, lbw, lbw_a, lbw_b, loader))
added.add(lora)
return result
@@ -407,6 +411,8 @@ def resolve_lora_name(lora_name_cache, name):
if x.endswith(name):
return x
return None
def process_with_loras(wildcard_opt, model, clip, clip_encoder=None, seed=None, processed=None):
"""
@@ -427,7 +433,7 @@ def process_with_loras(wildcard_opt, model, clip, clip_encoder=None, seed=None,
loras = extract_lora_values(pass1)
pass2 = remove_lora_tags(pass1)
for lora_name, model_weight, clip_weight, lbw, lbw_a, lbw_b in loras:
for lora_name, model_weight, clip_weight, lbw, lbw_a, lbw_b, loader in loras:
lora_name_ext = lora_name.split('.')
if ('.'+lora_name_ext[-1]) not in folder_paths.supported_pt_extensions:
lora_name = lora_name+".safetensors"
@@ -441,26 +447,36 @@ def process_with_loras(wildcard_opt, model, clip, clip_encoder=None, seed=None,
path = None
if path is not None:
print(f"LOAD LORA: {lora_name}: {model_weight}, {clip_weight}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
logging.info(f"LOAD LORA: {lora_name}: {model_weight}, {clip_weight}, LBW={lbw}, A={lbw_a}, B={lbw_b}, LOADER={loader}")
def default_lora():
return nodes.LoraLoader().load_lora(model, clip, lora_name, model_weight, clip_weight)
if lbw is not None:
if 'LoraLoaderBlockWeight //Inspire' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node(
'https://github.com/ltdrdata/ComfyUI-Inspire-Pack',
"To use 'LBW=' syntax in wildcards, 'Inspire Pack' extension is required.")
print(f"'LBW(Lora Block Weight)' is given, but the 'Inspire Pack' is not installed. The LBW= attribute is being ignored.")
model, clip = default_lora()
if loader is not None:
if loader == 'nunchaku':
if 'NunchakuFluxLoraLoader' not in nodes.NODE_CLASS_MAPPINGS:
logging.warning(f"To use `LOADER=nunchaku`, 'ComfyUI-nunchaku' is required. The LOADER= attribute is being ignored.")
cls = nodes.NODE_CLASS_MAPPINGS['NunchakuFluxLoraLoader']
model = cls().load_lora(model, lora_name, model_weight)[0]
else:
cls = nodes.NODE_CLASS_MAPPINGS['LoraLoaderBlockWeight //Inspire']
model, clip, _ = cls().doit(model, clip, lora_name, model_weight, clip_weight, False, 0, lbw_a, lbw_b, "", lbw)
logging.warning(f"LORA LOADER NOT FOUND: '{loader}'")
else:
model, clip = default_lora()
def default_lora():
return nodes.LoraLoader().load_lora(model, clip, lora_name, model_weight, clip_weight)
if lbw is not None:
if 'LoraLoaderBlockWeight //Inspire' not in nodes.NODE_CLASS_MAPPINGS:
utils.try_install_custom_node(
'https://github.com/ltdrdata/ComfyUI-Inspire-Pack',
"To use 'LBW=' syntax in wildcards, 'Inspire Pack' extension is required.")
logging.warning(f"'LBW(Lora Block Weight)' is given, but the 'Inspire Pack' is not installed. The LBW= attribute is being ignored.")
model, clip = default_lora()
else:
cls = nodes.NODE_CLASS_MAPPINGS['LoraLoaderBlockWeight //Inspire']
model, clip, _ = cls().doit(model, clip, lora_name, model_weight, clip_weight, False, 0, lbw_a, lbw_b, "", lbw)
else:
model, clip = default_lora()
else:
print(f"LORA NOT FOUND: {orig_lora_name}")
logging.warning(f"LORA NOT FOUND: {orig_lora_name}")
pass3 = [x.strip() for x in pass2.split("BREAK")]
pass3 = [x for x in pass3 if x != '']
@@ -469,7 +485,7 @@ def process_with_loras(wildcard_opt, model, clip, clip_encoder=None, seed=None,
pass3 = ['']
pass3_str = [f'[{x}]' for x in pass3]
print(f"CLIP: {str.join(' + ', pass3_str)}")
logging.info(f"CLIP: {str.join(' + ', pass3_str)}")
result = None
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-impact-pack"
description = "This node pack offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler."
version = "8.14.1"
version = "8.16"
license = { file = "LICENSE.txt" }
dependencies = ["segment-anything", "scikit-image", "piexif", "transformers", "opencv-python-headless", "GitPython", "scipy>=1.11.4"]
+1 -1
View File
@@ -4,6 +4,6 @@ piexif
transformers
opencv-python-headless
scipy>=1.11.4
numpy<2
numpy
dill
matplotlib