init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
+94
@@ -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"]
|
||||
@@ -0,0 +1,3 @@
|
||||
[default]
|
||||
wildcards_path = wildcards
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
'''
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 = ""
|
||||
@@ -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"; }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user