This commit is contained in:
rdp
2024-08-06 15:26:01 +03:00
parent 145cc52d30
commit 0fa7614aa4
20 changed files with 3083 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
+94
View File
@@ -0,0 +1,94 @@
"""
@author: SeniorPioner
@title: SP-Nodes
@nickname: SP-Nodes
@description: Node Pack: PromptChecker for token toggling, PyExec for Python code execution inside ComfyUI, KoboldCPP API, ModelMerging, Telegram-Bot-API, and more
"""
import shutil, os, folder_paths
import subprocess
import threading
import locale
import sys
import importlib.util
from .config import write_config
write_config()
WEB_DIRECTORY = "web"
# nodes.EXTENSION_WEB_DIRS["zaio-nodster"] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'web')
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
nodes_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'nodes')
def import_and_merge(file_path):
global NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
module_name = file_path.replace(nodes_directory, '')
module_name = module_name.replace('\\', '.').replace('/', '.')[:-3] # Remove .py extension
try:
module = importlib.import_module(".nodes{}".format(module_name), __name__)
if hasattr(module, 'NODE_CLASS_MAPPINGS'):
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'):
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
except Exception as e:
print(f"Error importing module {module_name}: {e}")
def find_py_files_and_import(dir_path):
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith('.py'):
file_path = os.path.join(root, file)
import_and_merge(file_path)
find_py_files_and_import(nodes_directory)
cwd_path = os.path.dirname(os.path.realpath(__file__))
comfy_path = folder_paths.base_path
def handle_stream(stream, is_stdout):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
if is_stdout:
print(msg, end="", file=sys.stdout)
else:
print(msg, end="", file=sys.stderr)
def process_wrap(cmd_str, cwd=None, handler=None):
print(f"[ZAIO] EXECUTE: {cmd_str} in '{cwd}'")
process = subprocess.Popen(cmd_str, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
bufsize=1)
if handler is None:
handler = handle_stream
stdout_thread = threading.Thread(target=handler, args=(process.stdout, True))
stderr_thread = threading.Thread(target=handler, args=(process.stderr, False))
stdout_thread.start()
stderr_thread.start()
stdout_thread.join()
stderr_thread.join()
return process.wait()
pip_install = [sys.executable, '-m', 'pip', 'install']
try:
import nltk
nltk.download('punkt')
except ImportError:
process_wrap(pip_install + ['nltk'])
import nltk
nltk.download('punkt')
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"]
+3
View File
@@ -0,0 +1,3 @@
[default]
wildcards_path = wildcards
+38
View File
@@ -0,0 +1,38 @@
import configparser
import os
my_path = os.path.dirname(__file__)
config_path = os.path.join(my_path, "config.ini")
wildcards_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "wildcards"))
def write_config():
if os.path.exists(config_path):
return
config = configparser.ConfigParser()
config['default'] = {
'wildcards_path': str(wildcards_path),
}
with open(config_path, 'w') as configfile:
config.write(configfile)
def read_config():
try:
config = configparser.ConfigParser()
config.read(config_path)
default_conf = config['default']
if not os.path.exists(default_conf['wildcards_path']):
print(f"[WARN] PromptChecker wildcards_path path not found: {default_conf['wildcards_path']}. Using default path.")
default_conf['wildcards_path'] = wildcards_path
return {
'wildcards_path': default_conf['wildcards_path'] if 'wildcards_path' in default_conf else wildcards_path,
}
except Exception:
print(f"[ERROR] PromptChecker wildcards_path path not found: {default_conf['wildcards_path']}. Using default path.")
return {
'wildcards_path': wildcards_path,
}
+100
View File
@@ -0,0 +1,100 @@
def sd3_multiply(block, m, attn_qkv, attn_proj, mlp_fc1, mlp_fc2, adaLN_modulation_1):
sd = m.model_state_dict()
for key in sd:
if key.endswith(f"{block}.attn.qkv.bias") or key.endswith(f"{block}.attn.qkv.weight"):
m.add_patches({key: (None,)}, 0.0, attn_qkv)
if key.endswith(f"{block}.attn.proj.bias") or key.endswith(f"{block}.attn.proj.weight"):
m.add_patches({key: (None,)}, 0.0, attn_proj)
if key.endswith(f"{block}.mlp.fc1.bias") or key.endswith(f"{block}.mlp.fc1.weight"):
m.add_patches({key: (None,)}, 0.0, mlp_fc1)
if key.endswith(f"{block}.mlp.fc2.bias") or key.endswith(f"{block}.mlp.fc2.weight"):
m.add_patches({key: (None,)}, 0.0, mlp_fc2)
if key.endswith(f"{block}.adaLN_modulation.1.bias") or key.endswith(f"{block}.adaLN_modulation.1.weight"):
m.add_patches({key: (None,)}, 0.0, adaLN_modulation_1)
return m
class SD3BlocksMultiply:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"b0": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b3": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b4": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b5": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b6": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b7": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b8": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b9": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b10": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b11": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b12": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b13": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b14": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b15": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b16": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b17": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b18": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b19": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b20": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b21": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b22": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"b23": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "_for_testing/attention_experiments"
def patch(self, model,
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9,
b10, b11, b12, b13, b14, b15, b16, b17, b18, b19,
b20, b21, b22, b23
):
m = model.clone()
sd = m.model_state_dict()
for i in range(24):
for key in sd:
if f'joint_blocks.{i}.' in key:
m.add_patches({key: (None,)}, 0.0, locals()['b' + str(i)])
return (m, )
class SD3Multiply:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"context_block_attn_qkv": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"context_block_attn_proj": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"context_block_mlp_fc1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"context_block_mlp_fc2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"context_block_adaLN_modulation_1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"x_block_attn_qkv": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"x_block_attn_proj": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"x_block_mlp_fc1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"x_block_mlp_fc2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"x_block_adaLN_modulation_1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "_for_testing/attention_experiments"
def patch(self, model, context_block_attn_qkv, context_block_attn_proj, context_block_mlp_fc1, context_block_mlp_fc2, context_block_adaLN_modulation_1,
x_block_attn_qkv, x_block_attn_proj, x_block_mlp_fc1, x_block_mlp_fc2, x_block_adaLN_modulation_1):
m = model.clone()
m = sd3_multiply('context_block', m, context_block_attn_qkv, context_block_attn_proj, context_block_mlp_fc1, context_block_mlp_fc2, context_block_adaLN_modulation_1)
m = sd3_multiply('x_block', m, x_block_attn_qkv, x_block_attn_proj, x_block_mlp_fc1, x_block_mlp_fc2, x_block_adaLN_modulation_1)
return (m, )
NODE_CLASS_MAPPINGS = {
"SD3Multiply": SD3Multiply,
"SD3BlocksMultiply": SD3BlocksMultiply,
}
+125
View File
@@ -0,0 +1,125 @@
import re
import requests
class CivitaiPrompts:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
inputs = {
"required": {
"limit": ("INT", {"default": 200, "min": 0, "max": 200}),
"nsfw": (["", "Soft", "Mature", "X"], {"default": "Soft"}),
"sort": (["Most Reactions", "Most Comments", "Newest"], {"default": "Most Reactions"}),
"period": (["AllTime", "Year", "Month", "Week", "Day"], {"default": "Week"}),
"delete_loras": (["False", "True"], {"default": "False"}),
"page": ("INT", {"default": 1, "min": 1, "max": 1000}),
},
"optional": {
},
}
return inputs
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "doit"
# OUTPUT_NODE = True
CATEGORY = 'SP-Nodes'
def doit(self, limit, nsfw, sort, period, delete_loras, page, **kwargs):
if not nsfw:
nsfw = None
params = {
"limit": limit, # 0 - 200
"nsfw": nsfw, # (None, Soft, Mature, X)
"sort": sort, # (Most Reactions, Most Comments, Newest)
"period": period, # (AllTime, Year, Month, Week, Day)
"page": page
}
response = requests.get("https://civitai.com/api/v1/images", params=params)
prompts = []
if response.status_code == 200:
data = response.json()
for i in data['items']:
meta = i.get('meta', None)
if not meta:
continue
prompt = meta.get('prompt', None)
if not prompt:
continue
prompt = prompt.replace('\r', '').replace('\n', ' ')
prompts.append(re.sub(r'<[^>]*>', '', prompt) if delete_loras == 'True' else prompt)
else:
print("Error:", response.status_code)
return '\n'.join(prompts),
NODE_CLASS_MAPPINGS = {
"CivitaiPrompts": CivitaiPrompts,
}
'''
"items":[
{
"id":234845,
"url":"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/cc00cfce-393c-476e-c71f-562722b77c00/width=512/cc00cfce-393c-476e-c71f-562722b77c00.jpeg",
"hash":"UFK,,857O?-o1iS~Ejj@~Wxu}?IASh9F4nod",
"width":512,
"height":1024,
"nsfwLevel":"Mature",
"nsfw":true,
"createdAt":"2023-03-12T06:29:56.461Z",
"postId":116584,
"stats":{
"cryCount":19,
"laughCount":41,
"likeCount":655,
"dislikeCount":14,
"heartCount":1037,
"commentCount":6
},
"meta":{
"ENSD":"31337",
"Size":"512x1024",
"seed":3984980528,
"Model":"chilloutmix_NiPrunedFp32Fix",
"steps":25,
"prompt":"<lora:virtualgirlRin_v30:0.3>, <lora:upshirtUnderboob_v10:1.35>,\n(RAW photo:1.2),(photorealistic:1.4),(masterpiece:1.3),(best quality:1.4),ultra high res, HDR,8k resolution,\ndreamlike, check commentary, commentary request, scenery,((no text)),\n1girl, (cleavage:1.5), (large breasts:1.5), (pubic hair:1.5),(lifting shirt), (detailed laced underpants:1.4), (full body), (looking down:1.5), (close up), look at the viewer, naughty face, (touching self hair:1.3), (tattoo:1.3), topless, arm, (close up:1.5), (focus on breasts),\n(detailed eyes),(detailed facial features), (detailed clothes features), (breast blush)\n\ntrending on cg society, plasticine, bob hair, (strong and toned abs), ((beautiful woman)), wearing choker, thigh choker, very pale white skin, in snow, feminine and muscular, smiling, wet skin, female focus,",
"sampler":"DPM++ SDE Karras",
"cfgScale":8,
"Clip skip":"2",
"resources":[
{
"name":"virtualgirlRin_v30",
"type":"lora",
"weight":0.3
},
{
"name":"upshirtUnderboob_v10",
"type":"lora",
"weight":1.35
},
{
"hash":"fc2511737a",
"name":"chilloutmix_NiPrunedFp32Fix",
"type":"model"
}
],
"Model hash":"fc2511737a",
"negativePrompt":"EasyNegative,bad_prompt,ng_deepnegative_v1_75t,(worst quality:2), (low quality:2), (normal quality:2), lowres, normal quality, ((monochrome)), ((grayscale)), skin spots, acnes, skin blemishes, age spot, (outdoor:1.6), manboobs, backlight,(ugly:1.331), (duplicate:1.331), (morbid:1.21), (mutilated:1.21), (tranny:1.331), mutated hands, (poorly drawn hands:1.331), blurry, (bad anatomy:1.21), (bad proportions:1.331), extra limbs, (disfigured:1.331), (more than 2 nipples:1.331), (missing arms:1.331), (extra legs:1.331), (fused fingers:1.61051), (too many fingers:1.61051), (unclear eyes:1.331), bad hands, missing fingers, extra digit, (futa:1.1), bad body, glans,",
"Face restoration":"GFPGAN"
},
"username":"noevils"
}
'''
+115
View File
@@ -0,0 +1,115 @@
import comfy.sd
import comfy.utils
import comfy.model_base
import comfy.model_sampling
import torch
import folder_paths
import json
import os
from comfy import model_management
from comfy.cli_args import args
# custom comfy.sd.save_checkpoint
def comfy_sd_save_checkpoint(output_path, model, clip=None, vae=None, clip_vision=None, metadata=None, extra_keys={}):
clip_sd = None
load_models = [model]
if clip is not None:
load_models.append(clip.load_model())
clip_sd = clip.get_sd()
model_management.load_models_gpu(load_models)
clip_vision_sd = clip_vision.get_sd() if clip_vision is not None else None
vae_sd = vae.get_sd() if vae is not None else None
sd = model.model.state_dict_for_saving(clip_sd, vae_sd, clip_vision_sd)
for k in extra_keys:
sd[k] = extra_keys[k]
for k in sd:
t = sd[k]
if not t.is_contiguous():
sd[k] = t.contiguous()
comfy.utils.save_torch_file(sd, output_path, metadata=metadata)
def save_checkpoint(model, clip=None, vae=None, clip_vision=None, filename_prefix=None, output_dir=None, prompt=None, extra_pnginfo=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, output_dir)
prompt_info = ""
if prompt is not None:
prompt_info = json.dumps(prompt)
metadata = {}
enable_modelspec = True
if isinstance(model.model, comfy.model_base.SDXL):
metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-base"
elif isinstance(model.model, comfy.model_base.SDXLRefiner):
metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-refiner"
else:
enable_modelspec = False
if enable_modelspec:
metadata["modelspec.sai_model_spec"] = "1.0.0"
metadata["modelspec.implementation"] = "sgm"
metadata["modelspec.title"] = "{} {}".format(filename, counter)
#TODO:
# "stable-diffusion-v1", "stable-diffusion-v1-inpainting", "stable-diffusion-v2-512",
# "stable-diffusion-v2-768-v", "stable-diffusion-v2-unclip-l", "stable-diffusion-v2-unclip-h",
# "v2-inpainting"
extra_keys = {}
model_sampling = model.get_model_object("model_sampling")
if isinstance(model_sampling, comfy.model_sampling.ModelSamplingContinuousEDM):
if isinstance(model_sampling, comfy.model_sampling.V_PREDICTION):
extra_keys["edm_vpred.sigma_max"] = torch.tensor(model_sampling.sigma_max).float()
extra_keys["edm_vpred.sigma_min"] = torch.tensor(model_sampling.sigma_min).float()
if model.model.model_type == comfy.model_base.ModelType.EPS:
metadata["modelspec.predict_key"] = "epsilon"
elif model.model.model_type == comfy.model_base.ModelType.V_PREDICTION:
metadata["modelspec.predict_key"] = "v"
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])
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
comfy_sd_save_checkpoint(output_checkpoint, model, clip, vae, clip_vision, metadata=metadata, extra_keys=extra_keys)
class CheckpointSave:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"filename_prefix": ("STRING", {"default": "checkpoints/ComfyUI"}),},
"optional": { "clip_opt": ("CLIP",),
"vae_opt": ("VAE",),},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "SP-Nodes"
def save(self, model, filename_prefix, clip_opt=None, vae_opt=None, prompt=None, extra_pnginfo=None):
save_checkpoint(model, clip=clip_opt, vae=vae_opt, filename_prefix=filename_prefix, output_dir=self.output_dir, prompt=prompt, extra_pnginfo=extra_pnginfo)
return {}
NODE_CLASS_MAPPINGS = {
"SP-CheckpointSave": CheckpointSave,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SP-CheckpointSave": "SP Custom Checkpoint Save",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+299
View File
@@ -0,0 +1,299 @@
import os
import random
import numpy as np
from PIL import Image, ImageOps
import torch
from datetime import datetime, timedelta
import sys
import io
import hashlib
import subprocess
import locale
import threading
import zipfile
import copy
import torchvision.transforms.v2 as T
CATEGORY = "SP-Nodes"
MAX_RESOLUTION=16384
class DatasetData:
def __init__(self, count: int, pct: float, x_minmax: tuple[int, int], y_minmax: tuple[int, int], rotate_degrees_delta: int) -> None:
self.count: int = count
self.pct: float = pct
self.x_minmax: tuple[int, int] = x_minmax
self.y_minmax: tuple[int, int] = y_minmax
self.rotate_degrees_delta: int = rotate_degrees_delta
def __str__(self) -> str:
return f'count: {self.count}; pct: {self.pct}; x_minmax: {self.x_minmax}; y_minmax: {self.y_minmax}; rotate_degrees_delta: {self.rotate_degrees_delta}'
class ScatterParamsBatchContainer:
def __init__(self, data) -> None:
self.data = data
class ScatterParams:
CATEGORY = CATEGORY
RETURN_TYPES = ("SCATTER_PARAMS",)
FUNCTION = "process"
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"count": ("INT", {"default": 5, "min": 1, "max": 50, "step": 1}),
"scale": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.0, "step": 0.1}),
"x_min": ("INT", {"default": -1, "min": -1, "max": 10000, "step": 1}),
"x_max": ("INT", {"default": -1, "min": -1, "max": 10000, "step": 1}),
"y_min": ("INT", {"default": -1, "min": -1, "max": 10000, "step": 1}),
"y_max": ("INT", {"default": -1, "min": -1, "max": 10000, "step": 1}),
"rotate_degrees_delta": ("INT", {"default": 25, "min": 0, "max": 180, "step": 5}),
},
"optional": {
},
}
return inputs
def process(self, count, scale, x_min, x_max, y_min, y_max, rotate_degrees_delta):
return (DatasetData(count, scale, (x_min, x_max), (y_min, y_max), rotate_degrees_delta), )
class ScatterParamsBatch:
CATEGORY = CATEGORY
RETURN_TYPES = ("SCATTER_PARAMS",)
FUNCTION = "process"
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"scatter_params1": ("SCATTER_PARAMS", ),
"scatter_params2": ("SCATTER_PARAMS", ),
},
"optional": {
"scatter_params3": ("SCATTER_PARAMS", ),
"scatter_params4": ("SCATTER_PARAMS", ),
"scatter_params5": ("SCATTER_PARAMS", ),
"scatter_params6": ("SCATTER_PARAMS", ),
},
}
return inputs
def process(self, **kwargs):
out = []
for v in kwargs.values():
if isinstance(v, ScatterParamsBatchContainer):
out.extend(v.data)
else:
out.append(v)
return (ScatterParamsBatchContainer(tuple(out)), )
def pil2tensor(image:Image) -> torch.Tensor:
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def tensor2pil(t_image: torch.Tensor) -> Image:
return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def crop_image_with_transparency_mask(image:Image, mask:Image) -> Image:
result = Image.new('RGBA', mask.size)
result.paste(image, mask=mask)
bbox = mask.getbbox()
if bbox:
result = result.crop(bbox)
return result
def get_bbox_with_pixels(image_path):
img = Image.open(image_path).convert("RGBA")
return img.crop(img.getbbox())
def calculate_coords(value: tuple[int, int], canvas_size: int, paste_size: int) -> tuple[int, int]:
min_val = 0 if value[0] == -1 else max(0, value[0])
max_val = canvas_size - paste_size if value[1] == -1 else min(canvas_size - paste_size, value[1] - paste_size)
if min_val > canvas_size - paste_size:
min_val = canvas_size - paste_size
if max_val < 0:
max_val = 0
return min_val, max_val
def paste_region_random_location(paste_img: Image, canvas_size: tuple[int, int], resize_pct: int, x_minmax: tuple[int, int], y_minmax: tuple[int, int], rotate_degrees_delta: int, allow_flip_images: bool):
canvas = Image.new('RGBA', canvas_size, color=(0, 0, 0, 0))
width = int(canvas.width * resize_pct)
height = int(canvas.height * resize_pct)
if allow_flip_images and random.choice([True, False]):
paste_img = paste_img.transpose(Image.FLIP_LEFT_RIGHT)
resized_region = paste_img.copy()
resized_region.thumbnail((width, height))
x_min, x_max = calculate_coords(x_minmax, canvas.width, resized_region.width)
y_min, y_max = calculate_coords(y_minmax, canvas.height, resized_region.height)
x = random.randint(min(x_min, x_max), max(x_min, x_max))
y = random.randint(min(y_min, y_max), max(y_min, y_max))
rotate_degree = random.randint(-rotate_degrees_delta, rotate_degrees_delta)
rotated_img = resized_region.rotate(rotate_degree)
canvas.paste(rotated_img, (x, y))
return canvas
def pb(image):
return image.permute([0,2,3,1])
class FaceScatter:
CATEGORY = CATEGORY
RETURN_TYPES = ("IMAGE","MASK",)
FUNCTION = "process"
def __init__(self):
pass
# def __del__(self):
# if self.observer:
# self._destroy_observer()
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"image": ("IMAGE", ),
"mask": ("MASK",),
"scatter_params": ("SCATTER_PARAMS", ),
"width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"height": ("INT", {"default": 768, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
},
"optional": {
},
}
return inputs
def process(self, image, mask, scatter_params: DatasetData, width, height):
image = tensor2pil(torch.unsqueeze(image, 0))
mask = tensor2pil(torch.unsqueeze(mask, 0)).convert('L')
face_image = crop_image_with_transparency_mask(image.convert('RGB'), mask)
scatter_data = [scatter_params] if not isinstance(scatter_params, ScatterParamsBatchContainer) else [param for param in scatter_params.data]
out = []
for data in scatter_data:
for i in range(data.count):
img = paste_region_random_location(face_image, (width, height), data.pct, data.x_minmax, data.y_minmax, data.rotate_degrees_delta)
out.append(T.ToTensor()(img))
# out.append(img)
out = torch.stack(out, dim=0)
out = pb(out)
mask = out[:, :, :, 3] if out.shape[3] == 4 else torch.ones_like(out[:, :, :, 0])
# return (torch.cat(tuple([pil2tensor(i) for i in out]), dim=0), mask, )
return (out[:, :, :, :3], mask, )
class FaceScatter2:
CATEGORY = CATEGORY
RETURN_TYPES = ("IMAGE","MASK",)
FUNCTION = "process"
def __init__(self):
pass
# def __del__(self):
# if self.observer:
# self._destroy_observer()
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"image": ("IMAGE", ),
"mask": ("MASK",),
"scatter_params": ("SCATTER_PARAMS", ),
"width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"height": ("INT", {"default": 768, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"transparency": ("BOOLEAN", {"default": False}),
"invert_masks": ("BOOLEAN", {"default": True}),
"allow_flip_images": ("BOOLEAN", {"default": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
},
}
return inputs
def process(self, image, mask, scatter_params: DatasetData, width, height, transparency: bool, invert_masks: bool, allow_flip_images: bool, seed):
random.seed(seed)
image = tensor2pil(torch.unsqueeze(image, 0))
mask = tensor2pil(torch.unsqueeze(mask, 0)).convert('L')
face_image = crop_image_with_transparency_mask(image.convert('RGB'), mask)
scatter_data = [scatter_params] if not isinstance(scatter_params, ScatterParamsBatchContainer) else [param for param in scatter_params.data]
out = []
out_masks = []
for data in scatter_data:
for i in range(data.count):
# image
img = paste_region_random_location(face_image, (width, height), data.pct, data.x_minmax, data.y_minmax, data.rotate_degrees_delta, allow_flip_images)
# out.append(T.ToTensor()(img))
out.append(img)
# masks
mask_image = img.convert("RGBA")
r, g, b, a = mask_image.split()
# if invert_masks:
# a = Image.fromarray(255 - np.array(a))
if invert_masks:
a = Image.eval(a, lambda x: 255 - x)
mask_image = pil2tensor(a.convert('L')) # T.ToTensor()(a.convert("L")).permute([0,2,3,1])
print('shape', mask_image.shape)
# mask_image = mask_image[:, :, :, 3] if mask_image.shape[3] == 4 else torch.ones_like(mask_image[:, :, :, 0])
# if invert_masks:
# mask_image = 1.0 - mask_image
out_masks.append(mask_image)
# out = torch.stack(out, dim=0)
# out = pb(out)
# mask = out[:, :, :, 3] if out.shape[3] == 4 else torch.ones_like(out[:, :, :, 0])
images = torch.cat(tuple([pil2tensor(i if transparency else i.convert("RGB")) for i in out]), dim=0)
masks = torch.cat(out_masks, dim=0)
# masks = pb(masks)
# masks = masks[:, :, :, 3] if masks.shape[3] == 4 else torch.ones_like(masks[:, :, :, 0])
return (images, masks, )
# return (out[:, :, :, :3], mask, )
NODE_CLASS_MAPPINGS = {
"ScatterParams": ScatterParams,
"ScatterParamsBatch": ScatterParamsBatch,
"FaceScatter": FaceScatter,
"FaceScatter2": FaceScatter2,
}
+250
View File
@@ -0,0 +1,250 @@
import os
import numpy as np
from PIL import Image, ImageOps
import torch
from datetime import datetime, timedelta
import sys
import io
import hashlib
import subprocess
import locale
import threading
import zipfile
def handle_stream(stream, is_stdout):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
if is_stdout:
print(msg, end="", file=sys.stdout)
else:
print(msg, end="", file=sys.stderr)
def process_wrap(cmd_str, cwd=None, handler=None):
print(f"[AiO-Node] EXECUTE: {cmd_str} in '{cwd}'")
process = subprocess.Popen(cmd_str, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
bufsize=1)
if handler is None:
handler = handle_stream
stdout_thread = threading.Thread(target=handler, args=(process.stdout, True))
stderr_thread = threading.Thread(target=handler, args=(process.stderr, False))
stdout_thread.start()
stderr_thread.start()
stdout_thread.join()
stderr_thread.join()
return process.wait()
# if "python_embeded" in sys.executable or "python_embedded" in sys.executable:
# pip_install = [sys.executable, '-s', '-m', 'pip', 'install']
# else:
pip_install = [sys.executable, '-m', 'pip', 'install']
try:
from psd_tools import PSDImage
except ImportError:
process_wrap(pip_install + ['--use-pep517', 'psd-tools'])
from psd_tools import PSDImage
try:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
except ImportError:
process_wrap(pip_install + ['watchdog'])
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
CATEGORY = "SP-Nodes"
class ImageMonitor:
CATEGORY = CATEGORY
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
LOADED_IMAGES_HASHES: dict = {}
def __init__(self):
self.observer = None
self.current_image_path = None
self.image_data = None
# def __del__(self):
# if self.observer:
# self._destroy_observer()
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"image_path": ("STRING", {"placeholder": "path_to_image"}),
},
"optional": {
},
}
return inputs
class ImageChangeDetector(FileSystemEventHandler):
def __init__(self, file_name, callback):
self.file_name = file_name
self.callback = callback
def on_modified(self, event):
if event.event_type != 'modified':
return
if not event.is_directory and event.src_path.lower().endswith(self.file_name.lower()):
self.callback(event.src_path)
def process(self, image_path):
if not self.observer:
self._load_image_data(image_path)
self._create_observer(image_path)
# print('not observer. create')
elif self.current_image_path != image_path:
self._destroy_observer()
self._create_observer(image_path)
image, mask, sha256 = self.image_data
return (image,)
def _load_image_data(self, image_path):
self.image_data = self.load_image(image_path)
image, mask, sha256 = self.image_data
# print(f'_load_image_data for {image_path}')
if not self.observer:
# bypass dual load image when init workflow
ImageMonitor.LOADED_IMAGES_HASHES[image_path] = ''
else:
ImageMonitor.LOADED_IMAGES_HASHES[image_path] = sha256
# print(f'new hash {sha256} for {image_path}')
def _on_file_changed(self, image_path):
self._print(f'File {image_path} has been modified.')
self._load_image_data(image_path)
def _create_observer(self, image_path):
event_handler = ImageMonitor.ImageChangeDetector(os.path.basename(image_path), self._on_file_changed)
self.observer = Observer()
self.observer.schedule(event_handler, path=os.path.dirname(image_path), recursive=False)
self.observer.start()
self.current_image_path = image_path
self._print(' - observer created')
@classmethod
def load_image(s, image_path: str):
with open(image_path, 'rb') as f:
bytes = f.read()
sha256 = s.get_sha256(bytes)
i = None
if image_path.lower().endswith('.psd'):
psd = PSDImage.open(io.BytesIO(bytes))
i = psd.topil()
elif image_path.lower().endswith('.kra'):
try:
with zipfile.ZipFile(image_path, 'r') as zip_file:
with zip_file.open('mergedimage.png') as file:
bytes = file.read()
i = Image.open(io.BytesIO(bytes))
except Exception as e:
raise e
else:
i = Image.open(io.BytesIO(bytes))
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
return image, mask, sha256
@classmethod
def _print(s, text, debug=False):
if debug:
with open('a:/image_monitor.log', 'a') as f:
f.write(f'{text}\n')
else:
print(text)
@classmethod
def get_sha256(s, bytes):
m = hashlib.sha256()
m.update(bytes)
return m.digest().hex()
def _destroy_observer(self):
# TODO: Kill destroyed nodes
self.observer.stop()
self.observer.join()
self.observer = None
self._print(' - observer destroyed')
@classmethod
def IS_CHANGED(s, image_path):
sha256 = s.LOADED_IMAGES_HASHES.get(image_path, '')
print(f'{image_path} changed. hash = {sha256}')
return sha256
@classmethod
def VALIDATE_INPUTS(s, image_path):
if not os.path.isfile(image_path):
return "Invalid image file: {}".format(image_path)
return True
class TESTER:
CATEGORY = CATEGORY
RETURN_TYPES = ("STRING",)
FUNCTION = "process"
counter = 0
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
},
"optional": {
},
}
return inputs
def process(self,):
return (f'val = {TESTER.counter}',)
@classmethod
def IS_CHANGED(s):
TESTER.counter += 1
print(TESTER.counter)
return (TESTER.counter // 5) * 5
NODE_CLASS_MAPPINGS = {
"ImageMonitor": ImageMonitor,
# "TESTER": TESTER,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageMonitor": "Image Monitor",
# "TESTER": "Image TESTER",
}
+108
View File
@@ -0,0 +1,108 @@
import requests, math
API_URL = 'http://localhost:5001/api/v1'
system_prompt = '''
ты теперь генератор промптов для stable diffusion.
правила генерации промптов:
1) я пишу короткое описание желаемого промпта, а ты отвечаешь "улучшенным" промптом на английском с добавлением/доработкой деталей по правилам написания промпта для Stable Diffusion.
2) если будет указан стиль в скобках в формате "описание промпта (стиль)", тогда дорабатывай промпт в соответствии с этим стилем
3) ты пишешь только готовый промпт и ничего больше!
4) основная идея должна быть сохранена и должна быть описана в начале промпта
'''
def generate_text(api_url, system_prompt, prompt, temperature_override=0, preset='default', max_length=200, seed=-1):
endpoint = f'{api_url}/generate'
headers = {
'Content-Type': 'application/json'
}
if preset=='default':
preset_dict = {"rep_pen": 1.1, "temperature": 0.66, "top_p": 1, "top_k": 0, "top_a": 0.96, "typical": 0.6, "tfs": 1, "rep_pen_range": 1024, "rep_pen_slope": 0.7, "sampler_order": [6, 4, 5, 1, 0, 2, 3]}
elif preset=='simple_logical':
preset_dict = {"rep_pen": 1.01, "temperature": 0.25, "top_p": 0.6, "top_k": 100, "top_a": 0, "typical": 1, "tfs": 1, "rep_pen_range": 320, "rep_pen_slope": 0.7, "sampler_order": [6, 0, 1, 3, 4, 2, 5]}
elif preset=='simple_balanced':
preset_dict = {"rep_pen": 1.07, "temperature": 0.7, "top_p": 0.92, "top_k": 100, "top_a": 0, "typical": 1, "tfs": 1, "rep_pen_range": 320, "rep_pen_slope": 0.7, "sampler_order": [6, 0, 1, 3, 4, 2, 5]}
elif preset=='simple_creative':
preset_dict = {"rep_pen": 1.15, "temperature": 1, "top_p": 0.98, "top_k": 100, "top_a": 0, "typical": 1, "tfs": 1, "rep_pen_range": 320, "rep_pen_slope": 0.7, "sampler_order": [6, 0, 1, 3, 4, 2, 5]}
elif preset=='silly_tavern':
preset_dict = {"rep_pen": 1.18, "temperature": 0.7, "top_p": 0.6, "top_k": 40, "top_a": 0, "typical": 1, "tfs": 1, "rep_pen_range": 1024, "rep_pen_slope": 0.8, "sampler_order": [6, 0, 1, 3, 4, 2, 5]}
elif preset=='coherent_creativity':
preset_dict = {"rep_pen": 1.2, "temperature": 0.5, "top_p": 1, "top_k": 0, "top_a": 0, "typical": 1, "tfs": 0.99, "rep_pen_range": 2048, "rep_pen_slope": 0, "sampler_order": [6, 5, 0, 2, 3, 1, 4]}
elif preset=='godlike':
preset_dict = {"rep_pen": 1.1, "temperature": 0.7, "top_p": 0.5, "top_k": 0, "top_a": 0.75, "typical": 0.19, "tfs": 0.97, "rep_pen_range": 1024, "rep_pen_slope": 0.7, "sampler_order": [6, 5, 4, 3, 2, 1, 0]}
elif preset=='liminal_drift':
preset_dict = {"rep_pen": 1.1, "temperature": 0.66, "top_p": 1, "top_k": 0, "top_a": 0.96, "typical": 0.6, "tfs": 1, "rep_pen_range": 1024, "rep_pen_slope": 0.7, "sampler_order": [6, 4, 5, 1, 0, 2, 3]}
else:
raise Exception('bad arg')
payload = {
"n": 1,
# "max_context_length": 8192,
"max_length": max_length,
'prompt': f"\nUser:{prompt}\nAI:",
'memory': system_prompt,
"sampler_seed": seed,
"dry_sequence_breakers": ["\n", ":", "\"", "*"],
"trim_stop": True,
"stop_sequence": ["User:", "\nUser ", "\nAI: "],
"quiet": True,
"use_default_badwordsids": False,
"bypass_eos": False,
"logit_bias": {},
"presence_penalty": 0,
"dry_allowed_length": 2,
"dry_base": 1.75,
"dry_multiplier": 0,
"render_special": False,
"banned_tokens": [],
"smoothing_factor": 0,
"dynatemp_exponent": 1,
"dynatemp_range": 0,
"min_p": 0
}
payload.update(preset_dict)
if not math.isclose(temperature_override, 0, abs_tol=1e-4):
print(f'temperature_override: {temperature_override}')
payload["temperature"] = temperature_override
response = requests.post(endpoint, json=payload, headers=headers)
if response.status_code == 200:
text = response.json()['results'][0]['text']
return text
else:
return f'Error: {response.status_code} - {response.text}'
class SP_KoboldCpp:
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"api_url": ("STRING", {"default": API_URL, "multiline": False}),
"system_prompt": ("STRING", {"default": system_prompt, "multiline": True}),
"prompt": ("STRING", {"default": '', "multiline": True}),
"preset": (['simple_logical', 'default', 'simple_balanced', 'simple_creative', 'silly_tavern', 'coherent_creativity', 'godlike', 'liminal_drift'], ),
"temperature_override": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.05}),
"max_length": ("INT", {"default": 100, "min": 10, "max": 512}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}
RETURN_TYPES = ('STRING',)
FUNCTION = "fn"
OUTPUT_NODE = False
CATEGORY = "SP-Nodes"
def fn(self, api_url, system_prompt, prompt, preset, temperature_override, max_length, seed):
return generate_text(api_url, system_prompt, prompt, temperature_override, preset, max_length=max_length, seed=seed).replace('User:', ''),
NODE_CLASS_MAPPINGS = {
"SP_KoboldCpp": SP_KoboldCpp,
}
+597
View File
@@ -0,0 +1,597 @@
import math
import random
import hashlib
import numpy as np
import torch
import time
import io
import comfy.utils
import comfy.model_management
from comfy_extras.nodes_model_merging import ModelMergeBlocks
from comfy.model_detection import count_blocks
def random_scale_blocked(seed=None):
if seed:
random.seed(seed)
center_values = [ random.random(), random.random(), random.random() ]
presets = []
input_blocks = 12
middle_blocks = 3
out_blocks = 12
for i in range(25):
if i < input_blocks:
if i == 5:
center_value = center_values[0]
t = 0.5
else:
center_value = center_values[0]
t = i / 5
elif i < input_blocks + middle_blocks:
if i == 12:
center_value = center_values[1]
t = 0.5
else:
center_value = center_values[1]
t = (i - 6) / 5
else:
if i == 21:
center_value = center_values[2]
t = 0.5
else:
center_value = center_values[2]
t = (i - 15) / 5
value = center_value + (random.random() - 0.5) * (1 - abs(2 * t - 1))
presets.append(value)
return presets
class GodnessMerger_Layer:
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"min": ("FLOAT", {"default": 0, "min": 0.0, "max": 1.0, "step": 0.1}),
"max": ("FLOAT", {"default": 1, "min": 0.0, "max": 1.0, "step": 0.1}),
"type": (["custom", "random"], ),
"custom_value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
def merge(self, seed, min, max, type, custom_value, **kwargs):
raise NotImplementedError()
def update_custom_value(self, value_type, value_custom, min, max):
if value_type == 'random':
return random.uniform(min, max)
return value_custom
class GodnessMerger_LayerExperimental:
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"min": ("FLOAT", {"default": 0, "min": 0.0, "max": 1.0, "step": 0.1}),
"max": ("FLOAT", {"default": 1, "min": 0.0, "max": 1.0, "step": 0.1}),
"type": (["custom", "random"], ),
"custom_value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
def merge(self, model, seed, min, max, type, custom_value, **kwargs):
raise NotImplementedError()
def update_custom_value(self, value_type, value_custom, min, max):
if value_type == 'random':
return random.uniform(min, max)
return value_custom
class GodnessMerger_TimeEmbed(GodnessMerger_Layer):
RETURN_TYPES = ("GM_TIME_EMBED",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(1000 + seed)
kwargs['time_embed.'] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_LabelEmb(GodnessMerger_Layer):
RETURN_TYPES = ("GM_LABEL_EMB",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(2000 + seed)
kwargs['label_emb.'] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_InputBlocks(GodnessMerger_Layer):
RETURN_TYPES = ("GM_INPUT_BLOCKS",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(3000 + seed)
for i in range(12):
kwargs[f"input_blocks.{i}."] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_InputBlocksExperimental(GodnessMerger_LayerExperimental):
RETURN_TYPES = ("GM_INPUT_BLOCKS",)
def merge(self, model, seed, min, max, type, custom_value, **kwargs):
random.seed(3000 + seed)
keys = model.model_state_dict().keys()
def organize_keys(keys):
blocks = {i: {'root': [], 'transformer_blocks': {j: [] for j in range(10)}} for i in range(12)}
for key in keys:
for i in range(12):
if key.startswith(f'diffusion_model.input_blocks.{i}'):
if 'transformer_blocks' in key:
for j in range(10):
if f'transformer_blocks.{j}.' in key:
blocks[i]['transformer_blocks'][j].append(key[len("diffusion_model."):])
else:
blocks[i]['root'].append(key[len("diffusion_model."):])
return blocks
keys = organize_keys(keys)
for layer in keys:
root_ratio = self.update_custom_value(type, custom_value, min, max)
for root in keys[layer]['root']:
kwargs[root] = root_ratio
for transformer_block in keys[layer]['transformer_blocks']:
transformer_ratio = self.update_custom_value(type, custom_value, min, max)
blocks = keys[layer]['transformer_blocks'][transformer_block]
for block in blocks:
kwargs[block] = transformer_ratio
return (kwargs, )
class GodnessMerger_MiddleBlock(GodnessMerger_Layer):
RETURN_TYPES = ("GM_MIDDLE_BLOCK",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(4000 + seed)
kwargs['middle_block.'] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_MiddleBlockExperimental(GodnessMerger_LayerExperimental):
RETURN_TYPES = ("GM_MIDDLE_BLOCK",)
def merge(self, model, seed, min, max, type, custom_value, **kwargs):
random.seed(4000 + seed)
keys = model.model_state_dict().keys()
def organize_keys(keys):
blocks = {i: {'root': [], 'transformer_blocks': {j: [] for j in range(10)}} for i in range(12)}
for key in keys:
for i in range(12):
if key.startswith(f'diffusion_model.middle_block.{i}'):
if 'transformer_blocks' in key:
for j in range(10):
if f'transformer_blocks.{j}.' in key:
blocks[i]['transformer_blocks'][j].append(key[len("diffusion_model."):])
else:
blocks[i]['root'].append(key[len("diffusion_model."):])
return blocks
keys = organize_keys(keys)
for layer in keys:
root_ratio = self.update_custom_value(type, custom_value, min, max)
for root in keys[layer]['root']:
kwargs[root] = root_ratio
for transformer_block in keys[layer]['transformer_blocks']:
transformer_ratio = self.update_custom_value(type, custom_value, min, max)
blocks = keys[layer]['transformer_blocks'][transformer_block]
for block in blocks:
kwargs[block] = transformer_ratio
return (kwargs, )
class GodnessMerger_OutputBlocks(GodnessMerger_Layer):
RETURN_TYPES = ("GM_OUTPUT_BLOCKS",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(5000 + seed)
for i in range(12):
kwargs[f"output_blocks.{i}."] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_OutputBlocksExperimental(GodnessMerger_LayerExperimental):
RETURN_TYPES = ("GM_OUTPUT_BLOCKS",)
def merge(self, model, seed, min, max, type, custom_value, **kwargs):
random.seed(5000 + seed)
keys = model.model_state_dict().keys()
def organize_keys(keys):
blocks = {i: {'root': [], 'transformer_blocks': {j: [] for j in range(10)}} for i in range(12)}
for key in keys:
for i in range(12):
if key.startswith(f'diffusion_model.output_blocks.{i}'):
if 'transformer_blocks' in key:
for j in range(10):
if f'transformer_blocks.{j}.' in key:
blocks[i]['transformer_blocks'][j].append(key[len("diffusion_model."):])
else:
blocks[i]['root'].append(key[len("diffusion_model."):])
return blocks
keys = organize_keys(keys)
for layer in keys:
root_ratio = self.update_custom_value(type, custom_value, min, max)
for root in keys[layer]['root']:
kwargs[root] = root_ratio
for transformer_block in keys[layer]['transformer_blocks']:
transformer_ratio = self.update_custom_value(type, custom_value, min, max)
blocks = keys[layer]['transformer_blocks'][transformer_block]
for block in blocks:
kwargs[block] = transformer_ratio
return (kwargs, )
class GodnessMerger_Out(GodnessMerger_Layer):
RETURN_TYPES = ("GM_OUT",)
def merge(self, seed, min, max, type, custom_value, **kwargs):
random.seed(6000 + seed)
kwargs['out.'] = self.update_custom_value(type, custom_value, min, max)
return (kwargs, )
class GodnessMerger_Apply:
RETURN_TYPES = ("MODEL","STRING")
RETURN_NAMES = ("MODEL", "RATIO_VALUES")
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model1": ("MODEL",),
"model2": ("MODEL",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
},
"optional": {
"time_embed": ("GM_TIME_EMBED",),
"label_emb": ("GM_LABEL_EMB",),
"input_blocks": ("GM_INPUT_BLOCKS",),
"middle_block": ("GM_MIDDLE_BLOCK",),
"output_blocks": ("GM_OUTPUT_BLOCKS",),
"out": ("GM_OUT",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
def merge(self, model1, model2, strength, unique_id, time_embed=None, label_emb=None, input_blocks=None, middle_block=None, output_blocks=None, out=None, **kwargs):
for config in [time_embed, label_emb, input_blocks, middle_block, output_blocks, out]:
if config is not None:
kwargs.update(config)
kwargs = {k: v * strength for k, v in kwargs.items()}
sb = io.StringIO()
layers = model1.model_state_dict().keys()
for k, v in kwargs.items():
if any(layer.startswith(f'diffusion_model.{k}') for layer in layers):
sb.write(f'{k}={v}\n')
if len(kwargs) > 0:
bm = ModelMergeBlocks()
model = bm.merge(model1, model2, **kwargs)[0]
else:
model = model1
return (model, sb.getvalue())
class GodnessMerger_RAW_Apply:
RETURN_TYPES = ("MODEL","STRING")
RETURN_NAMES = ("MODEL", "RATIO_VALUES")
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model1": ("MODEL",),
"model2": ("MODEL",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"text": ("STRING", {"multiline": True, "dynamicPrompts": False}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
def merge(self, model1, model2, strength, text: str, unique_id, **kwargs):
lines = [line.split('=') for line in text.splitlines() if line]
for k, v in lines:
kwargs[k] = float(v)
kwargs = {k: v * strength for k, v in kwargs.items()}
if len(kwargs) > 0:
bm = ModelMergeBlocks()
model = bm.merge(model1, model2, **kwargs)[0]
else:
model = model1
return (model, text)
class Random_Model_Merge:
RETURN_TYPES = ("MODEL","STRING")
RETURN_NAMES = ("MODEL", "VALUES")
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model1": ("MODEL",),
"model2": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
# "preset": (list(get_presets().keys()),),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"min": ("FLOAT", {"default": 0, "min": 0.0, "max": 1.0, "step": 0.1}),
"max": ("FLOAT", {"default": 1, "min": 0.0, "max": 1.0, "step": 0.1}),
"time_embed": (["default", "random", "custom"], ),
"time_embed_custom": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"label_emb": (["default", "random", "custom"], ),
"label_emb_custom": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"out": (["default", "random", "custom"], ),
"out_custom": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
def merge(self, model1, model2, seed, strength, min, max, time_embed, time_embed_custom, label_emb, label_emb_custom, out, out_custom, unique_id, **kwargs):
random.seed(seed)
ratios_values = [random.uniform(min, max) * strength for _ in range(25)]
block_types = ["input_blocks", "middle_block", "output_blocks"]
num_blocks = [12, 1, 12]
def update_custom_value(value_type, value_default, value_custom):
if value_type == 'custom':
return value_custom
elif value_type == 'random':
return random.uniform(min, max)
else:
return value_default
kwargs['time_embed.'] = update_custom_value(time_embed, 1.0, time_embed_custom)
kwargs['label_emb.'] = update_custom_value(label_emb, 1.0, label_emb_custom)
kwargs['out.'] = update_custom_value(out, 1.0, out_custom)
for block_type, num in zip(block_types, num_blocks):
for i in range(num):
ratio_key = "{}.{}.".format(block_type, i) if num > 1 else "{}.".format(block_type)
kwargs[ratio_key] = ratios_values.pop(0)
sb = io.StringIO()
layers = model1.model_state_dict().keys()
for k, v in kwargs.items():
if any(layer.startswith(f'diffusion_model.{k}') for layer in layers):
sb.write(f'{k}={v}\n')
bm = ModelMergeBlocks()
model = bm.merge(model1, model2, **kwargs)
return (model[0],sb.getvalue())
class GodnessMerger_NoiseInjection:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"operation": (["random", "gaussian"], {'default': "gaussian"}),
"mean": ("FLOAT", {"default": 0.05, "min": 0.0, "max": 1.0, "step": 0.01}),
"std": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.01}),
"ratio": ("FLOAT", {"default": 0.98, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
# "preset": (list(get_presets().keys()),),
# "preset_strength": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "merge"
CATEGORY = "SP-Nodes/model_merging"
def get_patched_state(self, model):
"""Uses a Comfy ModelPatcher to get the patched state dict of a model.
Args:
model (ModelPatcher): The model to get the patched state dict from.
Returns:
Dict[str, torch.Tensor]: The patched state dict.
"""
if len(model.patches) > 0:
print("Model has patches, applying them")
model.patch_model(None, True)
model_sd = model.model_state_dict()
model.unpatch_model()
else:
model_sd = model.model_state_dict()
return model_sd
def merge(self, model, operation, mean, std, ratio, seed, unique_id, **kwargs):
random.seed(seed)
torch.manual_seed(seed)
m = model.clone()
# model_sd = self.get_patched_state(m) # high vram usage
patches = model.get_key_patches("diffusion_model.")
model_sd = {k: patches[k][0] for k in patches}
# kp = model.get_key_patches("diffusion_model.")
# for k in kp:
# # #.to(device='cpu')
# # v = kp[k][0].half() #.to(device='cuda') # kp[k] = tuple (Tensor,)
# # v_mod = v + torch.normal(torch.zeros_like(v), v.std() * preset_strength)
# # # print(f'{k}: {v.std()} -> {v_mod.std()}')
# # m.add_patches({k: (v_mod,)}, 1.0 - ratio, ratio)
# weight = kp[k][0]
# temp_weight = weight.to(torch.float32, copy=True)
# # temp_weight += torch.normal(torch.zeros_like(temp_weight), temp_weight.std() * preset_strength)
# temp_weight += torch.normal(0, temp_weight.std() * preset_strength, size=temp_weight.size(), device=temp_weight.device)
# m.add_patches({k: (comfy.model_management.cast_to_device(temp_weight, weight.device, weight.dtype),)}, 1.0 - ratio, ratio)
for k in model_sd.keys():
w : torch.Tensor = model_sd[k]
a = w.to(torch.float32, copy=True)
if operation == "random":
# Create a random mask of the same shape as the given layer.
t_random = torch.rand(a.shape, device=a.device) - 0.5
else:
# Create a gaussian noise mask of the same shape as the given layer.
t_random = torch.normal(mean, std, size=a.shape, device=a.device) - mean
result_tensor = a + t_random
del t_random
# Merge our tensors
strength_patch = 1.0 - ratio
strength_model = ratio
m.add_patches({k: (comfy.model_management.cast_to_device(result_tensor, w.device, w.dtype),)}, strength_patch, strength_model)
return (m, )
def perf(self, model, seed, preset_strength, ratio, unique_id, **kwargs):
random.seed(seed)
torch.manual_seed(seed)
m = model.clone()
kp = m.get_key_patches("diffusion_model.")
create_w = 0.0
create_zeros_like = 0.0
create_std_noise = 0.0
create_noise = 0.0
add_op = 0.0
add_patches = 0.0
# start_time = time.time()
# for k in kp:
# weight = kp[k][0]
# temp_weight = weight.to(torch.float32, copy=True)
# v_mod = temp_weight + torch.normal(torch.zeros_like(temp_weight), temp_weight.std() * preset_strength)
# print(f"Simple: {time.time() - start_time}s")
start_time = time.time()
for k in kp:
weight = kp[k][0]
start_time = time.time()
temp_weight = weight.to(torch.float32, copy=True)
create_w += time.time() - start_time
start_time = time.time()
std_noise = temp_weight.std() * preset_strength
create_std_noise += time.time() - start_time
start_time = time.time()
zeros_like = torch.zeros_like(temp_weight)
create_zeros_like += time.time() - start_time
start_time = time.time()
noise = torch.normal(zeros_like, std_noise)
# noise = torch.normal(0, std_noise, size=temp_weight.size(), device=temp_weight.device)
create_noise += time.time() - start_time
start_time = time.time()
v_mod = temp_weight.add_(noise)
add_op += time.time() - start_time
start_time = time.time()
m.add_patches({k: (comfy.model_management.cast_to_device(v_mod, weight.device, weight.dtype),)}, 1.0 - ratio, ratio)
add_patches += time.time() - start_time
print(f"create_w: {create_w}s")
print(f"create_zeros_like: {create_zeros_like}s")
print(f"create_std_noise: {create_std_noise}s")
print(f"create_noise: {create_noise}s")
print(f"add_op: {add_op}s")
print(f"add_patches: {add_patches}s")
# start_time = time.time()
# for k in kp:
# weight = kp[k][0]
# temp_weight = weight.to(torch.float32, copy=True)
# noise_np = np.random.normal(0, temp_weight.std().item() * preset_strength, size=temp_weight.size())
# noise = torch.from_numpy(noise_np).to(temp_weight.device)
# v_mod = temp_weight + noise
# print(f"inplace+numpy: {time.time() - start_time}s")
return (m, )
NODE_CLASS_MAPPINGS = {
"Random_Model_Merge": Random_Model_Merge,
"GodnessMerger_Apply": GodnessMerger_Apply,
"GodnessMerger_RAW_Apply": GodnessMerger_RAW_Apply,
"GodnessMerger_TimeEmbed": GodnessMerger_TimeEmbed,
"GodnessMerger_LabelEmb": GodnessMerger_LabelEmb,
"GodnessMerger_InputBlocks": GodnessMerger_InputBlocks,
"GodnessMerger_MiddleBlock": GodnessMerger_MiddleBlock,
"GodnessMerger_OutputBlocks": GodnessMerger_OutputBlocks,
"GodnessMerger_InputBlocksExperimental": GodnessMerger_InputBlocksExperimental,
"GodnessMerger_MiddleBlockExperimental": GodnessMerger_MiddleBlockExperimental,
"GodnessMerger_OutputBlocksExperimental": GodnessMerger_OutputBlocksExperimental,
"GodnessMerger_Out": GodnessMerger_Out,
"GodnessMerger_NoiseInjection": GodnessMerger_NoiseInjection
}
+551
View File
@@ -0,0 +1,551 @@
import json
import os
import re
import numpy as np
import folder_paths
from PIL import Image, ImageOps
import io
import torch
import requests
from collections import OrderedDict
from PIL.PngImagePlugin import PngInfo
from collections import namedtuple
from datetime import datetime, timedelta
import sys
import random
import nltk
import contextlib
import codecs
import comfy, comfy_extras
from comfy_extras.nodes_tomesd import TomePatchModel
import comfy_extras.nodes_freelunch as nodes_freelunch
# ANSI escape codes for colors
RED = '\033[91m'
BLUE = '\033[94m'
GRAY = '\033[90m'
RESET = '\033[0m' # Reset color to default
# wildcard trick is taken from pythongossss's
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
# with open('a:/modules.txt', 'w') as f:
# f.write('\n'.join(sys.modules.keys()))
def dump_mods():
with open('modules.txt', 'w', encoding='utf-8') as f:
f.write(str(sys.modules))
# Define module constants after checking for their presence
ANY_TYPE = AnyType("*")
NODES = sys.modules['nodes']
API_URL = f"https://api.telegram.org/bot"
CATEGORY = "SP-Nodes"
class ImgMetaValueExtractor:
# batch i2i generated images(upscale/anything)
def __init__(s):
s.index = 0
@classmethod
def INPUT_TYPES(s):
return {"required": {"path": ("STRING", {"default": '', "multiline": False}),
"prompt_type": (["prompt", "workflow"],),
"value1": ("STRING", {"default": "[9][inputs][seed]"}), "value2": ("STRING", {"default": ""}),
"value3": ("STRING", {"default": ""}), "value4": ("STRING", {"default": ""}),
"value5": ("STRING", {"default": ""}), # "input": (ANY_TYPE, ),
}}
RETURN_TYPES = ("IMAGE", "STRING", ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE, "STRING")
RETURN_NAMES = ("image", "file_name_no_ext", "out1", "out2", "out3", "out4", "out5", "png_info")
FUNCTION = "doit"
# OUTPUT_NODE = True
CATEGORY = CATEGORY
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
def doit(s, path, prompt_type, value1, value2, value3, value4, value5):
png_path = s._get_next_png(path)
print(f'[{s.index}] png_path: {png_path}')
img = Image.open(png_path)
image = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
img.close()
if isinstance(img.info, dict) and prompt_type in img.info:
workflow = json.loads(img.info[prompt_type])
return (image,
os.path.splitext(os.path.basename(png_path))[0],
s._read_value(workflow, value1, prompt_type),
s._read_value(workflow, value2, prompt_type),
s._read_value(workflow, value3, prompt_type),
s._read_value(workflow, value4, prompt_type),
s._read_value(workflow, value5, prompt_type),
s._comfyui_prompt_to_str(img.info)
)
else:
return (None, None, None, None, None, None, None, None,)
def _comfyui_prompt_to_str(s, info):
result = {}
for key in ['prompt', 'workflow']:
if key not in info:
continue
result[key] = json.loads(info[key])
return json.dumps(result, indent=4)
def _read_value(s, workflow, path, prompt_type):
# todo: better parser with exception handler and print readable reason!
if not path:
return None
value = None
try:
matches = re.findall(r'\[(.*?)\]', path)
print(f'path: {path}, matches: {matches}')
value = workflow['nodes'] if prompt_type == 'workflow' else workflow
for i, m in enumerate(matches):
if prompt_type == 'workflow' and i == 0:
value = next((v for v in value if v['id'] == int(m)), None)
print(f'value: {value}')
else:
m_new = int(m) if isinstance(value, list) else m
print(f'm: {m}, m_new: {m_new}, m_new_type: {type(m_new)}')
value = value[m_new]
except Exception as ex:
print(f"Can't parse path {path} in workflow")
raise ex
return value
def _find_pngs(s, directory):
files = []
for root, dirs, subs in os.walk(directory):
for file in subs:
if not file.endswith('.png'):
continue
full_path = os.path.join(root, file)
files.append(full_path)
files.sort()
return files
def _get_next_png(s, path):
files = s._find_pngs(path)
if len(files) == 0:
return None
if s.index >= len(files):
s.index = 0
file = files[s.index]
s.index += 1
return file
class SendTelegramChatBot:
def __init__(self):
self._album = list()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",), "bot_token_env": ("STRING", {"default": "TG_BOT_TOKEN"}), "chat_id_env": ("STRING", {"default": "TG_BOT_CHATID"}),
"compress": ("BOOLEAN", {"default": False, "label_on": "true", "label_off": "false"}),
"send_as_document": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}),
# "include_prompt": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}),
"album_size": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "doit"
OUTPUT_NODE = True
CATEGORY = CATEGORY
def _send_media_group(self, token, chat_id, images, caption='', compress=False, send_as_document=False):
files = {}
media = []
for i, img in enumerate(images):
with io.BytesIO() as output:
if compress:
img[0].save(output, 'JPEG', quality=80)
else:
img[0].save(output, 'PNG', pnginfo=img[1], compress_level=4)
output.seek(0)
ext = 'jpg' if compress else 'png'
name = f'photo{i}.{ext}'
files[name] = output.read()
media.append(dict(type='document' if send_as_document else 'photo', media=f'attach://{name}'))
media[0]['caption'] = caption
response = requests.post(f'{API_URL}{token}/sendMediaGroup',
data={'chat_id': chat_id, 'media': json.dumps(media), 'parse_mode': None}, files=files)
if response.status_code == 200:
print("Image sent successfully!")
else:
print("Error sending image:", response.reason)
return response
def doit(self, images, bot_token_env, chat_id_env, compress, send_as_document, album_size, prompt=None,
extra_pnginfo=None):
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
self._album.append((img, metadata))
if len(self._album) >= album_size:
self._send_media_group(os.getenv(bot_token_env), chat_id=os.getenv(chat_id_env), images=self._album,
caption='', compress=compress,
send_as_document=send_as_document)
self._album.clear()
return (None,)
def convert_png_to_jpeg(self, img):
img = img.convert('RGB')
bio = io.BytesIO()
img.save(bio, 'JPEG', quality=80)
return Image.open(bio) # return bio.getvalue()
class BoolSwitchOutStr:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"enabled": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}),
}
}
# RETURN_TYPES = (any_typ, )
# RETURN_NAMES = ("output", )
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("state", )
FUNCTION = "doit"
# OUTPUT_NODE = True
CATEGORY = CATEGORY
def doit(s, enabled):
print(f'enabled: {enabled}')
# return (input, ) if enabled else None
return ('True',) if enabled else ('False',)
class LoraLoaderByPath:
def __init__(self):
self.loaded_lora = None
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"clip": ("CLIP", ),
"lora_path": ("STRING", {"default": "c:\\loras\\my_lora.safetensors"}),
"strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "load_lora"
CATEGORY = CATEGORY
def load_lora(self, model, clip, lora_path, strength_model, strength_clip):
if strength_model == 0 and strength_clip == 0:
return (model, clip)
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)
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
return (model_lora, clip_lora)
class LoraLoaderOnlyModelByPath:
def __init__(self):
self.loaded_lora = None
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"lora_path": ("STRING", {"default": "c:\\loras\\my_lora.safetensors"}),
"strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
FUNCTION = "load_lora"
CATEGORY = CATEGORY
def load_lora(self, model, lora_path, strength_model):
if strength_model == 0:
return (model)
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)
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0.0)
return (model_lora,)
class RandomPromptFromBook:
def __init__(self) -> None:
self._sentences: list[str] = None
self._latest_txt: str = None
@classmethod
def INPUT_TYPES(s):
return {"required":
{"book_txt": ("STRING", {"default": "c:\\books\\my_book.txt"}),
"min_choices": ("INT", {"default": 1, "min": 1, "max": 20}),
"max_choices": ("INT", {"default": 1, "min": 1, "max": 20}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"break_choises": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "get_prompt"
CATEGORY = CATEGORY
def get_prompt(self, book_txt, min_choices, max_choices, seed, break_choises):
random.seed(seed)
if not self._sentences or self._latest_txt != book_txt:
with open(book_txt, 'r', encoding='utf-8') as f:
text = f.read().replace('\t', ' ').replace('\r\n', '\n')
text = self._clean_string(text)
sentences = [self._replace_last_punctuation(sentence.replace("\n", " ")) for sentence in nltk.sent_tokenize(text)]
self._sentences = sentences
self._latest_txt = book_txt
choices = min_choices
if min_choices < max_choices:
choices = random.randint(min_choices, max_choices)
sep = ' BREAK ' if break_choises else ', '
prompt = sep.join(random.choices(self._sentences, k=choices))
return (prompt,)
def _clean_string(self, input_string):
return re.sub("[^a-zA-Z ,.!?]", '', input_string)
def _replace_last_punctuation(self, s):
if s[-1] in ['.', ',', '!', '?']:
s = s[:-1]
return s
class TextSplitJoinByDelimiter:
# from mixlab.ChatGPT.py
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"split_delimiter":("STRING", {"multiline": False,"default":",","dynamicPrompts": False}),
"start_index": ("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 1000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"skip_every": ("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 10, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"max_count": ("INT", {
"default": 10,
"min": 1, #Minimum value
"max": 1000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"join_delimiter":("STRING", {"multiline": False,"default":",","dynamicPrompts": False}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("STRING","STRING")
RETURN_NAMES = ("list_str","joined_str")
FUNCTION = "run"
# OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,False)
CATEGORY = CATEGORY
def run(self, text,split_delimiter,join_delimiter,start_index,skip_every,max_count):
if split_delimiter=="":
arr=[text.strip()]
else:
split_delimiter=codecs.decode(split_delimiter, 'unicode_escape')
arr= [line for line in text.split(split_delimiter) if line.strip()]
arr= arr[start_index:start_index + max_count * (skip_every+1):(skip_every+1)]
join_delimiter = codecs.decode(join_delimiter, 'unicode_escape')
return (arr,join_delimiter.join(arr),)
class PyExec:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"py": ("STRING", {"default": '', "multiline": True}),
},
"optional": {
"a1": (ANY_TYPE,),
"a2": (ANY_TYPE,),
"a3": (ANY_TYPE,),
"a4": (ANY_TYPE,),
"a5": (ANY_TYPE,),
},
}
RETURN_TYPES = (ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE)
RETURN_NAMES = ('r1', 'r2', 'r3', 'r4', 'r5')
OUTPUT_IS_LIST = (True,True,True,True,True)
FUNCTION = "doit"
CATEGORY = CATEGORY
OUTPUT_NODE = False
def doit(s, py, a1=None, a2=None, a3=None, a4=None, a5=None):
try:
output = io.StringIO()
local_vars = {
'a1': a1,
'a2': a2,
'a3': a3,
'a4': a4,
'a5': a5,
'r1': None,
'r2': None,
'r3': None,
'r4': None,
'r5': None
}
# Создаем объединение глобальных и локальных переменных
exec_globals = globals().copy()
exec_globals.update(local_vars)
with contextlib.redirect_stdout(output):
exec(py, exec_globals, exec_globals)
def to_list(value):
return value if isinstance(value, list) else [value]
result = (
to_list(exec_globals.get('r1', None)),
to_list(exec_globals.get('r2', None)),
to_list(exec_globals.get('r3', None)),
to_list(exec_globals.get('r4', None)),
to_list(exec_globals.get('r5', None))
)
captured_output = output.getvalue()
print(f'PyExec: {captured_output}')
return result
except Exception as e:
err = f"Произошла ошибка: {e}"
print(err)
return ([err],) * 5
# from easyUse
class AlwaysEqualProxy(str):
def __eq__(self, _):
return True
def __ne__(self, _):
return False
class StrToCombo:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("STRING", {"default": ''}),
}
}
RETURN_TYPES = (AlwaysEqualProxy('*'),)
RETURN_NAMES = ('COMBO',)
OUTPUT_IS_LIST = (True,)
FUNCTION = "doit"
CATEGORY = CATEGORY
def doit(s, value):
if not isinstance(value, list):
value = [value]
return value,
NODE_CLASS_MAPPINGS = {
"BoolSwitchOutStr": BoolSwitchOutStr,
"ImgMetaValueExtractor": ImgMetaValueExtractor,
"SendTelegramChatBot": SendTelegramChatBot,
"LoraLoaderByPath": LoraLoaderByPath,
"LoraLoaderOnlyModelByPath": LoraLoaderOnlyModelByPath,
"RandomPromptFromBook": RandomPromptFromBook,
"TextSplitJoinByDelimiter": TextSplitJoinByDelimiter,
"PyExec": PyExec,
"StrToCombo": StrToCombo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BoolSwitchOutStr": "Bool Switch Out Str",
"ImgMetaValueExtractor": "Image Load With Meta",
"SendTelegramChatBot": "Send Image To Telegram Bot",
"LoraLoaderByPath": "Lora Loader By Path",
"LoraLoaderOnlyModelByPath": "Lora Loader Only Model By Path",
"RandomPromptFromBook": "Random Prompt From Book",
}
+74
View File
@@ -0,0 +1,74 @@
import numpy as np
from PIL import Image
import folder_paths
import os
import json
import random
class PreviewImageWEBP:
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
methods = {"default": 4, "fastest": 0, "slowest": 6}
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ),
"quality": ("INT", {"default": 80, "min": 0, "max": 100}),
"method": (list(s.methods.keys()),),
# "num_frames": ("INT", {"default": 0, "min": 0, "max": 8192}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "image"
def save_images(self, images, quality, method, fps=6.0, filename_prefix="ComfyUI", lossless=False, num_frames=0, prompt=None, extra_pnginfo=None):
method = self.methods.get(method)
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
pil_images = []
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
pil_images.append(img)
metadata = pil_images[0].getexif()
if prompt is not None:
metadata[0x0110] = "prompt:{}".format(json.dumps(prompt))
if extra_pnginfo is not None:
inital_exif = 0x010f
for x in extra_pnginfo:
metadata[inital_exif] = "{}:{}".format(x, json.dumps(extra_pnginfo[x]))
inital_exif -= 1
if num_frames == 0:
num_frames = len(pil_images)
c = len(pil_images)
for i in range(0, c, num_frames):
file = f"{filename}_{counter:05}_.webp"
pil_images[i].save(os.path.join(full_output_folder, file), save_all=True, duration=int(1000.0/fps), append_images=pil_images[i + 1:i + num_frames], exif=metadata, lossless=lossless, quality=quality, method=method)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
animated = num_frames != 1
return { "ui": { "images": results, "animated": (animated,) } }
NODE_CLASS_MAPPINGS = {
"PreviewImageWEBP": PreviewImageWEBP,
}
+67
View File
@@ -0,0 +1,67 @@
from .wildcards import process, get_wildcard_list
from server import PromptServer
from aiohttp import web
DISABLED_TOKEN = '🔒'
CATEGORY = "SP-Nodes"
@PromptServer.instance.routes.get("/prompt_checker/wildcards/list")
async def wildcards_list(request):
data = {'data': get_wildcard_list()}
return web.json_response(data)
class PromptChecker:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt": ("STRING", {"default": 'ur prompt', 'multiline': True, "dynamicPrompts": False}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"Select to add Wildcard": (["Select the Wildcard to add to the text"],),
}
}
RETURN_TYPES = ('STRING','STRING')
RETURN_NAMES = ('prompt','wildcard')
FUNCTION = "doit"
CATEGORY = CATEGORY
def doit(s, prompt, seed, **kwargs):
tokens = [t for t in s.tokenize(prompt) if not t.startswith(DISABLED_TOKEN)]
prompt = ', '.join(tokens)
wildcard_prompt = process(prompt, seed)
return prompt, wildcard_prompt,
def tokenize(self, s):
tokens = []
current_token = ''
inside_brackets = 0
inside_parentheses = False
for char in s:
if char == '{':
inside_brackets += 1
current_token += char
elif char == '}':
inside_brackets -= 1
current_token += char
elif char == '(':
inside_parentheses = True
current_token += char
elif char == ')':
inside_parentheses = False
current_token += char
elif char == ',' and inside_brackets == 0 and not inside_parentheses:
tokens.append(current_token.strip())
current_token = ''
else:
current_token += char
if current_token:
tokens.append(current_token.strip())
return [token for token in tokens if token]
NODE_CLASS_MAPPINGS = {
"PromptChecker": PromptChecker,
}
+297
View File
@@ -0,0 +1,297 @@
# credits: impact-pack nodes
import re
import random
import os
import sys
import numpy as np
import threading
# from impact import utils
wildcard_lock = threading.Lock()
wildcard_dict = {}
def get_wildcard_list():
with wildcard_lock:
return [f"__{x}__" for x in wildcard_dict.keys()]
def get_wildcard_dict():
global wildcard_dict
with wildcard_lock:
return wildcard_dict
def wildcard_normalize(x):
return x.replace("\\", "/").lower()
def read_wildcard(k, v):
if isinstance(v, list):
k = wildcard_normalize(k)
wildcard_dict[k] = v
elif isinstance(v, dict):
for k2, v2 in v.items():
new_key = f"{k}/{k2}"
new_key = wildcard_normalize(new_key)
read_wildcard(new_key, v2)
def read_wildcard_dict(wildcard_path):
global wildcard_dict
for root, directories, files in os.walk(wildcard_path, followlinks=True):
for file in files:
if file.endswith('.txt'):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, wildcard_path)
key = os.path.splitext(rel_path)[0].replace('\\', '/').lower()
try:
with open(file_path, 'r', encoding="ISO-8859-1") as f:
lines = f.read().splitlines()
wildcard_dict[key] = lines
except UnicodeDecodeError:
with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f:
lines = f.read().splitlines()
wildcard_dict[key] = lines
elif file.endswith('.yaml'):
file_path = os.path.join(root, file)
with open(file_path, 'r') as f:
yaml_data = yaml.load(f, Loader=yaml.FullLoader)
for k, v in yaml_data.items():
read_wildcard(k, v)
return wildcard_dict
def process(text, seed=None):
if seed is not None:
random.seed(seed)
random_gen = np.random.default_rng(seed)
def unpack_wildcards(string):
def unpack_wildcard(match):
local_wildcard_dict = get_wildcard_dict()
string = match.group(1)
pattern = r"__([\w.\-+/*\\]+)__"
matches = re.findall(pattern, string)
replacements_found = False
for match in matches:
keyword = match.lower()
keyword = wildcard_normalize(keyword)
if keyword in local_wildcard_dict:
replacement = '|'.join(local_wildcard_dict[keyword])
replacements_found = True
string = string.replace(f"__{match}__", replacement, 1)
elif '*' in keyword:
subpattern = keyword.replace('*', '.*').replace('+','\+')
total_patterns = []
found = False
for k, v in local_wildcard_dict.items():
if re.match(subpattern, k) is not None:
total_patterns += v
found = True
if found:
replacement = '|'.join(total_patterns)
replacements_found = True
string = string.replace(f"__{match}__", replacement, 1)
elif '/' not in keyword:
string_fallback = string.replace(f"__{match}__", f"__*/{match}__", 1)
string = unpack_wildcards(string_fallback)
return string
pattern = r'({[^{}]*?})'
replaced_string = re.sub(pattern, unpack_wildcard, string)
return replaced_string
def replace_options(string):
replacements_found = False
def replace_option(match):
nonlocal replacements_found
options = match.group(1).split('|')
multi_select_pattern = options[0].split('$$')
select_range = None
select_sep = ' '
range_pattern = r'(\d+)(-(\d+))?'
range_pattern2 = r'-(\d+)'
# range_pattern3 = r'^(\d+)'
if len(multi_select_pattern) > 1:
r = re.match(range_pattern, options[0])
if r is None:
r = re.match(range_pattern2, options[0])
a = '1'
b = r.group(1).strip()
else:
a = r.group(1).strip()
if r.group(3):
b = r.group(3).strip()
else:
b = a
if r is not None:
if b is not None and is_numeric_string(a) and is_numeric_string(b):
# PATTERN: num1-num2
select_range = int(a), int(b)
elif is_numeric_string(a):
# PATTERN: num
x = int(a)
select_range = (x, x)
if select_range is not None and len(multi_select_pattern) == 2:
# PATTERN: count$$
options[0] = multi_select_pattern[1]
elif select_range is not None and len(multi_select_pattern) == 3:
# PATTERN: count$$ sep $$
select_sep = multi_select_pattern[1]
options[0] = multi_select_pattern[2]
adjusted_probabilities = []
total_prob = 0
for option in options:
parts = option.split('::', 1)
if len(parts) == 2 and is_numeric_string(parts[0].strip()):
config_value = float(parts[0].strip())
else:
config_value = 1 # Default value if no configuration is provided
adjusted_probabilities.append(config_value)
total_prob += config_value
normalized_probabilities = [prob / total_prob for prob in adjusted_probabilities]
if select_range is None:
select_count = 1
else:
select_count = random_gen.integers(low=select_range[0], high=select_range[1]+1, size=1)
if select_count > len(options):
random_gen.shuffle(options)
selected_items = options
else:
selected_items = random_gen.choice(options, p=normalized_probabilities, size=select_count, replace=False)
selected_items = set(selected_items) # wildcards here not replaced and removed dublicated
try_count = 0
while len(selected_items) < select_count and try_count < 10:
remaining_count = select_count - len(selected_items)
additional_items = random_gen.choice(options, p=normalized_probabilities, size=select_count, replace=False)
selected_items |= set(additional_items)
try_count += 1
selected_items2 = [re.sub(r'^\s*[0-9.]+::', '', x, 1) for x in selected_items]
replacement = select_sep.join(selected_items2)
if '::' in replacement:
pass
replacements_found = True
return replacement
string = unpack_wildcards(string)
pattern = r'{([^{}]*?)}'
replaced_string = re.sub(pattern, replace_option, string)
return replaced_string, replacements_found
def replace_wildcard(string):
local_wildcard_dict = get_wildcard_dict()
pattern = r"__([\w.\-+/*\\]+)__"
matches = re.findall(pattern, string)
replacements_found = False
for match in matches:
keyword = match.lower()
keyword = wildcard_normalize(keyword)
if keyword in local_wildcard_dict:
replacement = random_gen.choice(local_wildcard_dict[keyword])
replacements_found = True
string = string.replace(f"__{match}__", replacement, 1)
elif '*' in keyword:
subpattern = keyword.replace('*', '.*').replace('+','\+')
total_patterns = []
found = False
for k, v in local_wildcard_dict.items():
if re.match(subpattern, k) is not None:
total_patterns += v
found = True
if found:
replacement = random_gen.choice(total_patterns)
replacements_found = True
string = string.replace(f"__{match}__", replacement, 1)
elif '/' not in keyword:
string_fallback = string.replace(f"__{match}__", f"__*/{match}__", 1)
string, replacements_found = replace_wildcard(string_fallback)
return string, replacements_found
replace_depth = 100
stop_unwrap = False
while not stop_unwrap and replace_depth > 1:
replace_depth -= 1 # prevent infinite loop
# pass1: replace options
pass1, is_replaced1 = replace_options(text)
while is_replaced1:
pass1, is_replaced1 = replace_options(pass1)
# pass2: replace wildcards
text, is_replaced2 = replace_wildcard(pass1)
stop_unwrap = not is_replaced1 and not is_replaced2
return text
def is_numeric_string(input_str):
return re.match(r'^-?\d+(\.\d+)?$', input_str) is not None
if __name__ == '__main__':
read_wildcard_dict(r'v:\ComfyUI_windows_portable\ComfyUI\custom_nodes\ComfyUI-Impact-Pack\wildcards')
t1 = '{2$$1|2|3|4|5|6|7|8}'
t2 = '{2-4$$1|2|3|4|5|6|7|8}'
t3 = '{-4$$1|2|3|4|5|6|7|8}'
tt = 'hello world {2-4$$__w2*__} saassss'
tt2 = '__sk__ {__w1__|__sub/w*__} 1{girl is holding {blue pencil|red {__w*__}|colorful {__w2__}}|boy is riding {2-3$$__q1__}}'
t4 = 'hello world {2-4$$__cosplay__|__jenres__}'
t5 = 'hello world {2-4$$__cosplay__|{2-4$$1|2|3|4|5|6}|__jenres__}'
tfull = '(everything white color:1.2), {0.3::nsfw,|} {__200p/adj/adj-beauty__|__200p/adj/adj-general__|__200p/adj/adj-horror__} {__nation__|__200p/person_tweaks/class/*__|__200p/person_tweaks/occupation__} {woman|girl}, {__200p/person_tweaks/hair/*__}, {1-8$$__cosplay__|__200p/clothes/*__|__clothes*__}, {1-3$$__accessories__}, {__200p/person_tweaks/expression__|__emotions__} emotion, __200p/person_tweaks/eyeliner__, __200p/person_tweaks/lipstick__ __200p/person_tweaks/lipstick-shade__, __200p/person_tweaks/makeup__, __200p/person_tweaks/earrings__, (__200p/person_tweaks/skin-color__ skin:0.1), (__200p/body/*__ body{|0.3::tattoo}:1.15), __200p/person_tweaks/breastsize__, {__200p/scenario/*__|__crazy_actions__|__actions__}, {1-4$$__effects__|__200p/style/*__|__jenres__}, __200p/subject/*__, __200p/time__ , {1-3$$__bg__|__200p/location/*__} background, (by {2-4$$,$$__200p/artist/*__}:1.3)'
for i in range(10):
# print(f'=== i = {i} ===')
# print(process(t1, 1 + i))
# print(process(t2, 6565 + i))
# print(process(t3, 432 + i))
# print(process(t4, 432 + i))
print(process(tfull, 555 + i))
else:
# global wildcard_dict
# sys.path.extend([r'v:\ComfyUI_windows_portable\ComfyUI'])
# if 'impact.wildcards' in sys.modules:
# module = sys.modules['impact.wildcards']
# module.process = process
# wildcard_dict = module.wildcard_dict
# wildcard_lock = module.wildcard_lock
# print('== impact.wildcards patched! ==')
from ...config import read_config
config = read_config()
wildcard_dict = read_wildcard_dict(config['wildcards_path'])
def test():
pass
+73
View File
@@ -0,0 +1,73 @@
import base64
import io
import json
from PIL import Image, ImageOps
from io import BytesIO
import numpy as np
import struct
import comfy.utils
import time
from server import PromptServer, BinaryEventTypes
from comfy.cli_args import args
from PIL import Image, ImageOps, ImageSequence, ImageFile
from PIL.PngImagePlugin import PngInfo
#You can use this node to save full size images through the websocket, the
#images will be sent in exactly the same format as the image previews: as
#binary images on the websocket with a 8 byte header indicating the type
#of binary message (first 4 bytes) and the image format (next 4 bytes).
#Note that no metadata will be put in the images saved with this node.
class SP_WebsocketSendImage:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ),},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "api/image"
def save_images(self, images, prompt=None, extra_pnginfo=None):
results = []
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
byte_io = io.BytesIO()
img.save(byte_io, format='PNG', pnginfo=metadata)
server = PromptServer.instance
server.send_sync(
BinaryEventTypes.PREVIEW_IMAGE,
byte_io.getvalue(),
server.client_id,
)
results.append(
# Could put some kind of ID here, but for now just match them by index
{"source": "websocket", "content-type": "image/png", "type": "output"}
)
return {"ui": {"images": results}}
def IS_CHANGED(s, images):
return time.time()
NODE_CLASS_MAPPINGS = {
"SP_WebsocketSendImage": SP_WebsocketSendImage,
}
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-sp-nodes"
description = "Node Pack: PromptChecker for token toggling, PyExec for Python code execution inside ComfyUI, KoboldCPP API, ModelMerging, Telegram-Bot-API, and more"
version = "1.0.0"
license = { }
dependencies = []
[project.urls]
Repository = "https://github.com/bananasss00/ComfyUI-SP-Nodes"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "seniorpioner"
DisplayName = "SP-Nodes"
Icon = ""
+48
View File
@@ -0,0 +1,48 @@
import { ComfyApp, app } from "../../../scripts/app.js";
import { ComfyDialog, $el } from "../../../scripts/ui.js";
import { api } from "../../../scripts/api.js";
let wildcards_list = [];
async function load_wildcards() {
let res = await api.fetchApi('/prompt_checker/wildcards/list');
let data = await res.json();
wildcards_list = data.data;
}
load_wildcards();
app.registerExtension({
name: "comfy.sp_nodes.prompt_checker.impactpack_wildcards",
nodeCreated(node, app) {
if(node.comfyClass == "PromptChecker") {
node._wvalue = "Select the Wildcard to add to the text";
const combo_widget = node.widgets.find((w) => w.name == 'Select to add Wildcard');
const wildcard_text_widget = node.widgets.find((w) => w.name == 'prompt');
Object.defineProperty(combo_widget, "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;
}
}
},
get: () => { return "Select the Wildcard to add to the text"; }
});
Object.defineProperty(combo_widget.options, "values", {
set: (x) => {},
get: () => {
return wildcards_list;
}
});
// Preventing validation errors from occurring in any situation.
combo_widget.serializeValue = () => { return "Select the Wildcard to add to the text"; }
}
}
});
+83
View File
@@ -0,0 +1,83 @@
import { app } from '../../../scripts/app.js'
app.registerExtension({
name: "NodeSorter.Core",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) {
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var graph = canvas.graph;
if (!graph)
return;
function inner_onMenuAdded(base_category ,prev_menu){
var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)});
var entries = [];
categories.map(function(category){
if (!category)
return;
var base_category_regex = new RegExp('^(' + base_category + ')');
var category_name = category.replace(base_category_regex,"").split('/')[0];
var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/';
var name = category_name;
if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace
name = name.split("::")[1];
var index = entries.findIndex(function(entry){return entry.value === category_path});
if (index === -1) {
entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){
inner_onMenuAdded(value.value, contextMenu)
}});
}
});
var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter );
nodes.map(function(node){
if (node.skip_list)
return;
var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){
var first_event = contextMenu.getFirstEvent();
canvas.graph.beforeChange();
var node = LiteGraph.createNode(value.value);
if (node) {
node.pos = canvas.convertEventToCanvasOffset(first_event);
canvas.graph.add(node);
}
if(callback)
callback(node);
canvas.graph.afterChange();
}
}
entries.push(entry);
});
entries.sort(function(a, b) {
return a.content.localeCompare(b.content);
});
new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window );
}
inner_onMenuAdded('',prev_menu);
return false;
};
// console.log('patched');
}
});
+145
View File
@@ -0,0 +1,145 @@
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { $el } from "../../../scripts/ui.js";
const DISABLED_TOKEN = '🔒'
const DISABLED_TOKEN_BYTES = 2
app.registerExtension({
name: 'comfy.sp_nodes.prompt_checker',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == 'PromptChecker'){
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated && onNodeCreated.call(this);
const prompt = this.widgets[0];
let token_div = $el("div", {
id: "token-buttons"
});
function tokenize(str) {
const tokens = [];
let currentToken = '';
let insideBrackets = 0;
let insideParentheses = false;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '{') {
insideBrackets++;
currentToken += char;
} else if (char === '}') {
insideBrackets--;
currentToken += char;
} else if (char === '(') {
insideParentheses = true;
currentToken += char;
} else if (char === ')') {
insideParentheses = false;
currentToken += char;
} else if (char === ',' && insideBrackets === 0 && !insideParentheses) {
tokens.push(currentToken.trim());
currentToken = '';
} else {
currentToken += char;
}
}
if (currentToken) {
tokens.push(currentToken.trim());
}
return tokens.filter(token => token);
}
function toggleToken(token, button) {
let currentTokens = tokenize(prompt.value); //prompt.value.split(',').map(t => t.trim()).filter(t => t);
for (let i = 0; i < currentTokens.length; i++) {
if (currentTokens[i] === token) {
if (token.startsWith(DISABLED_TOKEN)) {
token = token.slice(DISABLED_TOKEN_BYTES);
button.style.backgroundColor = 'green';
} else {
token = `${DISABLED_TOKEN}${token}`;
button.style.backgroundColor = 'gray';
}
currentTokens[i] = token;
}
}
prompt.value = currentTokens.join(', ');
}
function adjustWeight(token, event) {
if (token.startsWith(DISABLED_TOKEN)) {
return token;
}
let currentTokens = tokenize(prompt.value);
const weightRegex = /^\((.*?):([0-9.]+)\)$/;
currentTokens = currentTokens.map(currentToken => {
let newToken = token
if (currentToken === token) {
let weightChange = event.deltaY < 0 ? 0.05 : -0.05;
if (weightRegex.test(token)) {
newToken = token.replace(weightRegex, (match, content, weight) => {
let newWeight = parseFloat(weight) + weightChange;
newWeight = Math.max(0.05, newWeight); // Prevent negative weight
return newWeight !== 1.0 ? `(${content}:${newWeight.toFixed(2)})` : content;
});
} else {
let newWeight = weightChange > 0 ? 1.05 : 0.95;
newToken = `(${token}:${newWeight})`;
}
}
return currentToken === token ? newToken : currentToken;
});
prompt.value = currentTokens.join(', ');
}
function updateTokens() {
const tokens = tokenize(prompt.value); //prompt.value.split(',').map(t => t.trim()).filter(t => t);
// Create buttons for each token
while (token_div.firstChild) {
token_div.removeChild(token_div.firstChild);
}
tokens.forEach(token => {
let button = $el("button", {
type: "button",
textContent: token.startsWith(DISABLED_TOKEN) ? token.slice(DISABLED_TOKEN_BYTES) : token,
style: {
color: 'white'
},
onclick: () => toggleToken(token, button),
onwheel: (event) => adjustWeight(token, event)
});
if (token.startsWith(DISABLED_TOKEN)) {
button.style.backgroundColor = 'gray';
} else {
button.style.backgroundColor = 'green';
}
token_div.appendChild(button)
});
}
prompt.callback = async()=>{
updateTokens();
}
this.addDOMWidget('values',"buttons",token_div)
return onNodeCreated;
}
}
}
})