Compare commits

..
19 Commits
Author SHA1 Message Date
shadowcz007 a2cd0e0a38 v0.11.2
- 优化appinfo - 自动保存
- 优化mergeLayer,可以输出合成的mask
- 添加一个实验性的节点 ImageColorTransfer
- random prompt ,增加上传关键词功能
- 修复LoadImageFromPath的bug
2024-01-08 23:03:37 +08:00
shadowcz007 7bccc0e236 优化appinfo-不需要输出,每次运行都会自动更新数据 2024-01-08 23:01:31 +08:00
shadowcz007 a3a649a79f 修复 LoadImagesFromPath 不更新的bug 2024-01-08 22:11:10 +08:00
shadowcz007 2a14d30552 添加一个实验性的节点 ImageColorTransfer 2024-01-08 16:40:00 +08:00
shadowcz007 313bef0609 ClipInterrogator优化 2024-01-07 22:55:24 +08:00
shadowcz007 960a80aeca Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-01-07 19:59:24 +08:00
shadowcz007 20e6d50a98 修复bug 2024-01-07 19:59:22 +08:00
shadow 507c3417d6 Merge pull request #114 from shadowcz007/fix_ssl_prot_occupied
fix:ssl prot occupied
2024-01-07 17:08:08 +08:00
gold3bear d1adb8d4ed fix:ssl prot occupied 2024-01-07 15:56:23 +08:00
shadowcz007 915ff12747 支持图片的batch 2024-01-06 23:20:43 +08:00
shadowcz007 fbade79137 Update utils_mixlab.js 2024-01-06 20:15:17 +08:00
shadowcz007 a240a677d0 Update utils_mixlab.js 2024-01-06 20:14:17 +08:00
shadowcz007 ab64cf31f6 FloatSlider 优化 2024-01-06 20:13:00 +08:00
shadowcz007 67f2e32dae floatSlider 优化 2024-01-06 20:00:58 +08:00
shadowcz007 0dc40fe052 修复bug 2024-01-06 17:55:26 +08:00
shadowcz007 b7225be552 Update ImageNode.py 2024-01-06 16:25:35 +08:00
shadowcz007 629e00ec94 fixbug 2024-01-06 12:14:02 +08:00
shadowcz007 6d033c9314 random prompt ,增加上传关键词功能 2024-01-06 08:40:27 +08:00
shadowcz007 4f8926ed00 promptslide 上传txt后写入workflow保留列表数据 2024-01-06 08:12:59 +08:00
13 changed files with 781 additions and 191 deletions
+36 -9
View File
@@ -351,33 +351,59 @@ async def new_request(self, method, url, *args, **kwargs):
# 应用 Monkey Patch
aiohttp.ClientSession._request = new_request
import socket
async def check_port_available(address, port):
#检查端口是否可用
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((address, port))
return True
except socket.error:
return False
# https
async def new_start(self, address, port, verbose=True, call_on_start=None):
try:
runner = web.AppRunner(self.app, access_log=None)
await runner.setup()
if not await check_port_available(address, port):
raise RuntimeError(f"Port {port} is already in use.")
site = web.TCPSite(runner, address, port)
await site.start()
import ssl
crt,key=create_for_https()
crt, key = create_for_https()
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(crt,key)
site2 = web.TCPSite(runner, address, port+1,ssl_context=ssl_context)
await site2.start()
ssl_context.load_cert_chain(crt, key)
success = False
for i in range(10): # 尝试最多10次
if await check_port_available(address, port + 1 + i):
https_port = port + 1 + i
site2 = web.TCPSite(runner, address, https_port, ssl_context=ssl_context)
await site2.start()
success = True
break
if not success:
raise RuntimeError(f"Ports {port + 1} to {port + 10} are all in use.")
if address == '':
address = '0.0.0.0'
if verbose:
# print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print("\033[93mStarting server\n")
print("\033[93mTo see the GUI go to: http://{}:{}".format(address, port))
print("\033[93mTo see the GUI go to: https://{}:{}\033[0m".format(address, port+1))
print("\033[93mTo see the GUI go to: https://{}:{}\033[0m".format(address, https_port))
if call_on_start is not None:
call_on_start(address, port)
except Exception as e:
print(f"Error starting the server: {e}")
# import webbrowser
# if os.name == 'nt' and address == '0.0.0.0':
# address = '127.0.0.1'
@@ -504,7 +530,7 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt,PromptSlide,PromptSimplification,PromptImage
from .nodes.ImageNode import NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
@@ -533,6 +559,7 @@ NODE_CLASS_MAPPINGS = {
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
"3DImage":Image3D,
"ImageColorTransfer":ImageColorTransfer,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"MergeLayers":MergeLayers,
+30
View File
@@ -0,0 +1,30 @@
Jony Ive
Dieter Rams
Philippe Starck
Karim Rashid
Yves Béhar
Marc Newson
Naoto Fukasawa
Jonathan Adler
Patricia Urquiola
Ross Lovegrove
Tom Dixon
Jasper Morrison
Charles Eames
Ray Eames
Achille Castiglioni
Ron Arad
Konstantin Grcic
Marcel Wanders
Maarten Baas
Stefan Sagmeister
Ingo Maurer
Hella Jongerius
Sam Hecht
Kim Colin
Jaime Hayon
Michael Anastassiades
Nendo
Oki Sato
Matali Crasset
Tokujin Yoshioka
+73 -5
View File
@@ -6,6 +6,7 @@ import comfy.utils
import numpy as np
import json
import torch
import random
from transformers import AutoProcessor, BlipForConditionalGeneration
@@ -58,7 +59,53 @@ def image_analysis_fn(ci,image):
trending_ranks = {trending: sim for trending, sim in zip(top_trendings, ci.similarities(image_features, top_trendings))}
flavor_ranks = {flavor: sim for flavor, sim in zip(top_flavors, ci.similarities(image_features, top_flavors))}
return medium_ranks, artist_ranks, movement_ranks, trending_ranks, flavor_ranks
return {
"medium_ranks":medium_ranks,
"artist_ranks":artist_ranks,
"movement_ranks":movement_ranks,
"trending_ranks":trending_ranks,
"flavor_ranks":flavor_ranks
}
def generate_sentences(data):
sentences = []
# Get the length of data
data_length = len(data)
# Use a recursive function to handle variable-length data
def generate_recursive(index, current_sentence, current_score):
# Check if recursion is complete
if index == data_length:
sentences.append({"sentence": current_sentence, "score": current_score})
return
# Get the current level data
current_data = data[index]
# Iterate through the current level data
for phrase in current_data:
sentence = current_sentence + ("," if current_sentence.strip() else "") + phrase
score = current_score + current_data[phrase]
generate_recursive(index + 1, sentence, score)
# Start recursive generation of sentences
generate_recursive(0, "", 0)
# Sort the generated sentences by score in descending order
sentences.sort(key=lambda x: x["score"], reverse=True)
def get_random_elements(elements, num):
return random.sample(elements, num)
ps = get_random_elements(sentences, 5)
ps = [s["sentence"] for s in sorted(ps, key=lambda x: x["score"], reverse=True)]
return ps
def image_to_prompt(ci,image, mode):
ci.config.chunk_size = 2048 if ci.config.clip_model_name == "ViT-L-14/openai" else 1024
@@ -86,17 +133,22 @@ class ClipInterrogator:
"prompt_mode": (['fast','classic','best','negative'],),
"image_analysis": (["off","on"],),
},
# "optional":{
# "output":("CLIPINTERROGATOR", {"multiline": True,"default": "", "dynamicPrompts": False})
# },
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("prompt","random_samples",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/prompt"
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
OUTPUT_IS_LIST = (True,True,)
global ci
ci = None
def run(self,image,prompt_mode,image_analysis):
@@ -156,5 +208,21 @@ class ClipInterrogator:
ci.caption_offloaded = True
# analysis_result=[]
# items = app.graph.getNodeById(31).widgets[2].value["items"]
random_samples=[]
return {"ui":{"prompt": prompt_result,"analysis":analysis_result},"result": (prompt_result,)}
for r in analysis_result:
random_sample = generate_sentences([r['medium_ranks'], r['artist_ranks'],r['movement_ranks'],r['trending_ranks'],r['flavor_ranks']])
for s in random_sample:
random_samples.append(s)
# print(len(random_samples))
# print('-----')
# print( random_samples)
return {
"ui":{
"prompt": prompt_result,
"analysis":analysis_result,
"random_samples":random_samples
},
"result": (prompt_result,random_samples,)}
+146 -37
View File
@@ -10,7 +10,7 @@ import folder_paths
import json,io
from comfy.cli_args import args
import cv2
import math
from .Watcher import FolderWatcher
@@ -27,6 +27,52 @@ def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
# 颜色迁移
# Color-Transfer-between-Images https://github.com/chia56028/Color-Transfer-between-Images/blob/master/color_transfer.py
def get_mean_and_std(x):
x_mean, x_std = cv2.meanStdDev(x)
x_mean = np.hstack(np.around(x_mean,2))
x_std = np.hstack(np.around(x_std,2))
return x_mean, x_std
def color_transfer(source,target):
# sources = ['s1','s2','s3','s4','s5','s6']
# targets = ['t1','t2','t3','t4','t5','t6']
# 将PIL的Image类型转换为OpenCV的numpy数组
source = cv2.cvtColor(np.array(source), cv2.COLOR_RGB2LAB)
target = cv2.cvtColor(np.array(target), cv2.COLOR_RGB2LAB)
s_mean, s_std = get_mean_and_std(source)
t_mean, t_std = get_mean_and_std(target)
height, width, channel = source.shape
for i in range(0,height):
for j in range(0,width):
for k in range(0,channel):
x = source[i,j,k]
x = ((x-s_mean[k])*(t_std[k]/s_std[k]))+t_mean[k]
# round or +0.5
x = round(x)
# boundary check
x = 0 if x<0 else x
x = 255 if x>255 else x
source[i,j,k] = x
source = cv2.cvtColor(source,cv2.COLOR_LAB2RGB)
# 创建PIL图像对象
image_pil = Image.fromarray(source)
return image_pil
def naive_cutout(img, mask,invert=True):
"""
Perform a simple cutout operation on an image using a mask.
@@ -516,7 +562,7 @@ def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option)
return bg_image
# TODO 几个像素点的底
def resize_image(layer_image, scale_option, width, height,color="white"):
layer_image = layer_image.convert("RGB")
original_width, original_height = layer_image.size
@@ -540,11 +586,11 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
elif scale_option == "center":
# Scale image to minimum of width and height, center it, and fill extra area with black
scale = min(width / original_width, height / original_height)
new_width = int(original_width * scale)
new_height = int(original_height * scale)
new_width = math.ceil(original_width * scale)
new_height = math.ceil(original_height * scale)
resized_image = Image.new("RGB", (width, height), color=color)
resized_image.paste(layer_image.resize((new_width, new_height)), ((width - new_width) // 2, (height - new_height) // 2))
resized_image=resized_image.convert("RGB")
resized_image = resized_image.convert("RGB")
return resized_image
return layer_image
@@ -552,6 +598,8 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
# # Load Chinese font
# font = ImageFont.truetype(font_path, font_size)
@@ -591,8 +639,7 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
# image=image.convert('RGB')
# return (image,alpha_image)
def generate_text_image(text, font_path, font_size, text_color, vertical=True, spacing=0):
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0):
# Split text into lines based on line breaks
lines = text.split("\n")
@@ -608,19 +655,17 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
x = 0
y = 0
for i in range(len(lines)):
line=lines[i]
line = lines[i]
for char in line:
char_coordinates.append((x, y))
y += font_size + spacing
x += font_size + spacing
y = 0
# print(char_coordinates)
else:
x = 0
y = 0
for line in lines:
for char in line:
#print('char',char)
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
@@ -629,35 +674,42 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = ((len(max(lines, key=len))+1) * (font_size + spacing)) + spacing
height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = ((len(lines)-1) * (font_size + spacing)) + font_size
height = ((len(lines) - 1) * (font_size + spacing)) + font_size
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(font_path, font_size)
index=0
index = 0
for i, line in enumerate(lines):
for j, char in enumerate(line):
x, y = char_coordinates[index]
if stroke:
draw.text((x-stroke_width, y), char, font=font, fill=stroke_color)
draw.text((x+stroke_width, y), char, font=font, fill=stroke_color)
draw.text((x, y-stroke_width), char, font=font, fill=stroke_color)
draw.text((x, y+stroke_width), char, font=font, fill=stroke_color)
draw.text((x, y), char, font=font, fill=text_color)
index+=1
index += 1
# image.save(output_image_path)
# 分离alpha通道
# Separate alpha channel
alpha_channel = image.split()[3]
# 创建一个只有alpha通道的新图像
# Create a new image with only the alpha channel
alpha_image = Image.new('L', image.size)
alpha_image.putdata(alpha_channel.getdata())
image=image.convert('RGB')
image = image.convert('RGB')
return (image, alpha_image)
return (image,alpha_image)
def base64_to_image(base64_string):
# 去除前缀
@@ -1071,7 +1123,7 @@ class LoadImagesFromPath:
print("发生了一个未知的错误:", str(e))
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,)}
# TODO 扩大选区的功能,重新输出mask
@@ -1133,6 +1185,7 @@ class TextImage:
}),
"text_color":("STRING",{"multiline": False,"default": "#000000","dynamicPrompts": False}),
"vertical":("BOOLEAN", {"default": True},),
"stroke":("BOOLEAN", {"default": False},),
},
}
@@ -1146,11 +1199,11 @@ class TextImage:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,text,font_path,font_size,spacing,text_color,vertical):
def run(self,text,font_path,font_size,spacing,text_color,vertical,stroke):
# text_list=list(text)
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,spacing)
# stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing)
img=pil2tensor(img)
mask=pil2tensor(mask)
@@ -1601,7 +1654,7 @@ class MergeLayers:
}
RETURN_TYPES = ("IMAGE",)
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
@@ -1627,7 +1680,10 @@ class MergeLayers:
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
width, height = bg_image.size
final_mask= Image.new('L', (width, height), 0)
for layer in layers_new:
image=layer['image']
mask=layer['mask']
@@ -1653,25 +1709,32 @@ class MergeLayers:
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
final_mask=merge_images(final_mask,
layer_mask.convert('RGB'),
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
final_mask=final_mask.convert('L')
# mask=bg_image.convert('RGBA')
final_mask=pil2tensor(final_mask)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
bg_images.append(bg_image)
masks.append(mask)
masks.append(final_mask)
bg_images=torch.cat(bg_images, dim=0)
masks=torch.cat(masks, dim=0)
return (bg_images,masks,)
class GradientImage:
@classmethod
def INPUT_TYPES(s):
@@ -1896,4 +1959,50 @@ class ResizeImage:
a_im=pil2tensor(a_im)
average_images.append(a_im)
return (imgs,average_images,)
return (imgs,average_images,)
class ImageColorTransfer:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"source": ("IMAGE",),
"target": ("IMAGE",),
},
}
# 输出的数据类型
RETURN_TYPES = ("IMAGE",)
# 运行时方法名称
FUNCTION = "run"
# 右键菜单目录
CATEGORY = "♾️Mixlab/_test"
# 输入是否为列表
INPUT_IS_LIST = True
# 输出是否为列表
OUTPUT_IS_LIST = (True,)
def run(self,source,target):
res=[]
target=target[0][0]
print(target.shape)
target=tensor2pil(target)
for ims in source:
for im in ims:
image=tensor2pil(im)
image=color_transfer(image,target)
image=pil2tensor(image)
res.append(image)
return (res,)
+30 -21
View File
@@ -85,8 +85,8 @@ def prompt_delete_words(sentence, new_words_length):
class PromptImage:
def __init__(self):
self.temp_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = "PromptImage"
self.compress_level = 4
@@ -121,31 +121,36 @@ class PromptImage:
filename_prefix="mixlab_"
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, self.temp_dir, images[0].shape[1], images[0].shape[0])
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
save_to_image=save_to_image[0]=='enable'
for index in range(len(images)):
image=images[index]
img=tensor2pil(image)
metadata = None
if save_to_image:
metadata = PngInfo()
prompt_text=prompts[index]
if prompt_text is not None:
metadata.add_text("prompt_text", prompt_text)
file = f"{filename}_{index}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
res=[]
imgs=images[index]
for image in imgs:
img=tensor2pil(image)
metadata = None
if save_to_image:
metadata = PngInfo()
prompt_text=prompts[index]
if prompt_text is not None:
metadata.add_text("prompt_text", prompt_text)
file = f"{filename}_{index}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
res.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
results.append(res)
return { "ui": { "_images": results,"prompts":prompts } }
@@ -203,6 +208,8 @@ class PromptSimplification:
for n in nps:
result.append(n)
result= [elem.strip() for elem in result if elem.strip()]
return {"ui": {"prompts": result}, "result": (result,)}
@@ -341,6 +348,8 @@ class RandomPrompt:
else:
prompts = prompts[:min(max_count,len(prompts))]
prompts= [elem.strip() for elem in prompts if elem.strip()]
# return (new_prompt)
return {"ui": {"prompts": prompts}, "result": (prompts,)}
+18 -14
View File
@@ -225,12 +225,13 @@ class FloatSlider:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number,min_value,max_value,step):
def run(self, number, min_value, max_value, step):
if number < min_value:
number= min_value
number = min_value
elif number > max_value:
number= max_value
return (number,)
number = max_value
scaled_number = (number - min_value) / (max_value - min_value)
return (scaled_number,)
class IntNumber:
@@ -433,12 +434,12 @@ class AppInfo:
def INPUT_TYPES(s):
return {"required": {
"name": ("STRING",{"multiline": False,"default": "Mixlab-App","dynamicPrompts": False}),
"image": ("IMAGE",),
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"]),"dynamicPrompts": False}),
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"]),"dynamicPrompts": False}),
},
"optional":{
"LOGO": ("IMAGE",),
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
"version":("INT", {
"default": 1,
@@ -450,12 +451,13 @@ class AppInfo:
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
"category":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
"auto_save": (["enable","disable"],),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
RETURN_TYPES = ()
# RETURN_NAMES = ("IMAGE",)
FUNCTION = "run"
@@ -463,11 +465,16 @@ class AppInfo:
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
# OUTPUT_IS_LIST = (True,)
def run(self,name,image,input_ids,output_ids,description,version,share_prefix,link,category):
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save):
name=name[0]
im=image[0][0]
im=None
if image:
im=image[0][0]
#TODO batch 的方式需要处理
im=create_temp_file(im)
# image [img,] img[batch,w,h,a] 列表里面是batch,
input_ids=input_ids[0]
@@ -477,13 +484,10 @@ class AppInfo:
share_prefix=share_prefix[0]
link=link[0]
category=category[0]
#TODO batch 的方式需要处理
im=create_temp_file(im)
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": (image,)}
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": ()}
+14 -6
View File
@@ -985,7 +985,7 @@
data.inputs.number,
(v) => {
// console.log(data.id,window._appData.data[data.id])
window._appData.data[data.id].inputs.number = v;
window._appData.data[data.id].inputs.number = data.class_type === 'IntNumber' ? parseInt(v) : parseFloat(v);
},
options.min,
options.max,
@@ -1834,11 +1834,19 @@
} else if (_images && prompts) {
let url = get_url();
show(Array.from(_images, (img, i) => {
return [`${url}/view?filename=${encodeURIComponent(img.filename)
}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)
}&t=${+new Date()}`, prompts[i]];
}), detail.node, 'images_prompts');
let items = [];
// 支持图片的batch
Array.from(_images, (imgs, i) => {
for (const img of imgs) {
items.push([`${url}/view?filename=${encodeURIComponent(img.filename)
}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)
}&t=${+new Date()}`, prompts[i]])
}
})
show(items, detail.node, 'images_prompts');
} else if (text) {
ui.output.update("text", Array.isArray(text) ? text.join('\n\n') : text, detail.node)
+85 -14
View File
@@ -108,8 +108,7 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
}
}
if(node.type=='Color'){
if (node.type == 'Color') {
}
input[inputIds.indexOf(id)] = {
@@ -126,11 +125,11 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id }
}
if (node.type === 'KSampler'||node.type=='SamplerCustom') {
if (node.type === 'KSampler' || node.type == 'SamplerCustom') {
// seed 的类型收集
try {
seed[id] = node.widgets.filter(
w => (w.name === 'seed'||w.name=='noise_seed')
w => w.name === 'seed' || w.name == 'noise_seed'
)[0].linkedWidgets[0].value
} catch (error) {}
}
@@ -188,7 +187,7 @@ function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
}, 0)
}
async function save (json, download = false) {
async function save (json, download = false, showInfo = true) {
const name = json[0],
version = json[5],
share_prefix = json[6], //用于分享的功能扩展
@@ -237,22 +236,53 @@ async function save (json, download = false) {
if (download) {
await downloadJsonFile(data, data.app.filename)
}
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}&category=${encodeURIComponent(data.app.category)}`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
if (showInfo) {
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}&category=${encodeURIComponent(data.app.category)}`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}&category=${encodeURIComponent(data.app.category)}`
)
}
} catch (error) {
console.log('###error', error)
}
}
function getInputsAndOutputs () {
const inputs =
`LoadImage CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
' '
),
outputs = `PreviewImage SaveImage ShowTextForGPT VHS_VideoCombine`.split(
' '
)
let inputsId = [],
outputsId = []
for (let node of app.graph._nodes) {
if (inputs.includes(node.type)) {
inputsId.push(node.id)
}
if (outputs.includes(node.type)) {
outputsId.push(node.id)
}
}
return {
input: inputsId,
output: outputsId
}
}
app.registerExtension({
name: 'Mixlab.utils.AppInfo',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
@@ -260,7 +290,16 @@ app.registerExtension({
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('#orig_nodeCreated', this)
// console.log('#orig_nodeCreated', this)
// 自动计算workflow里哪些节点支持
let input_ids = this.widgets.filter(w => w.name == 'input_ids')[0],
output_ids = this.widgets.filter(w => w.name == 'output_ids')[0]
const { input, output } = getInputsAndOutputs()
input_ids.value = input.join('\n')
output_ids.value = output.join('\n')
const widget = {
type: 'div',
name: 'AppInfoRun',
@@ -339,6 +378,12 @@ app.registerExtension({
console.log(message.json)
window._mixlab_app_json = message.json
try {
let a = this.widgets.filter(w => w.name === 'AppInfoRun')[0]
if (a) {
if (!a.value) a.value = 0
a.value += 1
}
const div = this.widgets.filter(w => w.div)[0].div
Array.from(
div.querySelectorAll('button'),
@@ -347,5 +392,31 @@ app.registerExtension({
} catch (error) {}
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'AppInfo') {
let auto_save = node.widgets.filter(w => w.name == 'auto_save')[0]
if (auto_save) {
if (!['enable', 'disable'].includes(auto_save.value)) {
auto_save.value = 'enable'
}
}
}
}
})
api.addEventListener('executed', async ({ detail }) => {
console.log('#executed', detail)
const { output } = getInputsAndOutputs()
if (output.includes(parseInt(detail.node))) {
let appinfo = app.graph.findNodesByType('AppInfo')[0]
if (appinfo) {
let auto_save = appinfo.widgets.filter(w => w.name == 'auto_save')[0]
if (auto_save?.value === 'enable') {
// 自动保存
console.log('auto_save')
if (window._mixlab_app_json) save(window._mixlab_app_json,false,false)
}
}
}
})
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.11.0'
const version = 'v0.11.2'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+79 -25
View File
@@ -3,11 +3,57 @@ import { app } from '../../../scripts/app.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
function getRandomElements (arr, num) {
var result = []
var len = arr.length
for (var i = 0; i < num; i++) {
var randomIndex = Math.floor(Math.random() * len)
result.push(arr[randomIndex])
}
return result
}
const createPrompt = (node, prompts, items, sample) => {
const w = ComfyWidgets['STRING'](
node,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
w.value = typeof prompts === 'string' ? prompts : prompts.join('\n\n')
const w2 = ComfyWidgets['STRING'](
node,
'text',
['STRING', { multiline: true }],
app
).widget
w2.inputEl.readOnly = true
w2.inputEl.style.opacity = 0.6
w2.value = typeof items === 'string' ? items : JSON.stringify(items, null, 2)
const w3 = ComfyWidgets['STRING'](
node,
'text',
['STRING', { multiline: true }],
app
).widget
w3.inputEl.readOnly = true
w3.inputEl.style.opacity = 0.6
w3.value = typeof sample === 'string' ? sample : sample.join('\n\n')
}
app.registerExtension({
name: 'Mixlab.prompt.ClipInterrogator',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'ClipInterrogator') {
function populate (prompts, items) {
function populate (prompts, items, random_samples) {
if (this.widgets) {
for (let i = 0; i < this.widgets.length; i++) {
if (this.widgets[i].type !== 'combo') this.widgets[i].onRemove?.()
@@ -15,29 +61,9 @@ app.registerExtension({
this.widgets.length = 2
}
const w = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
createPrompt(this, prompts, items, random_samples)
w.value = prompts.join('\n\n')
const w2 = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w2.inputEl.readOnly = true
w2.inputEl.style.opacity = 0.6
w2.value = JSON.stringify(items, null, 2)
console.log('ClipInterrogator',w,w2)
// console.log('ClipInterrogator', w, w2)
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
@@ -55,11 +81,39 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log('##', message)
populate.call(this, message.prompt, message.analysis)
// console.log('##', message)
populate.call(
this,
message.prompt,
message.analysis,
message.random_samples
)
}
this.serialize_widgets = true //需要保存参数
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'ClipInterrogator') {
try {
let widgets_values = node.widgets_values
console.log(widgets_values )
try {
if (widgets_values[2] && widgets_values[3] && widgets_values[4])
createPrompt(
node,
widgets_values[2],
widgets_values[3],
widgets_values[4]
)
} catch (error) {
console.log(error)
}
} catch (error) {}
}
}
})
+141 -26
View File
@@ -47,7 +47,7 @@ async function fetchImage (url) {
try {
const response = await fetch(url)
const blob = await response.blob()
return blob
} catch (error) {
console.error('出现错误:', error)
@@ -90,6 +90,116 @@ const createSelect = (select, opts, targetWidget) => {
})
}
app.registerExtension({
name: 'Mixlab.prompt.RandomPrompt',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'RandomPrompt') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
name
const mutable_prompt = this.widgets.filter(
w => w.name == 'mutable_prompt'
)[0]
// console.log('PromptSlide nodeData', prompt_keyword)
const widget = {
type: 'div',
name: 'upload',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
)
}
}
widget.div = $el('div', {})
const btn = document.createElement('button')
btn.innerText = 'Upload Keywords'
btn.style = `cursor: pointer;
font-weight: 300;
margin: 2px;
color: var(--descrip-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid; height: 30px;min-width: 122px;
`
// const btn=document.createElement('button');
// btn.innerText='Upload'
btn.addEventListener('click', () => {
let inp = document.createElement('input')
inp.type = 'file'
inp.accept = '.txt'
inp.click()
inp.addEventListener('change', event => {
// 获取选择的文件
const file = event.target.files[0]
this.title = file.name.split('.')[0]
// console.log(file.name.split('.')[0])
// 创建文件读取器
const reader = new FileReader()
// 定义读取完成事件的回调函数
reader.onload = event => {
// 读取完成后的文本内容
const fileContent = event.target.result.split('\n')
const keywords = Array.from(fileContent, f => f.trim()).filter(
f => f
)
// 打印文件内容
// console.log(keywords)
mutable_prompt.value = keywords.join('\n')
inp.remove()
}
// 以文本方式读取文件
reader.readAsText(file)
})
})
widget.div.appendChild(btn)
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'RandomPrompt') {
// try {
// let mutable_prompt = node.widgets.filter(w => w.name === 'mutable_prompt')[0]
// // let ks = getLocalData(`_mixlab_PromptSlide`)
// let uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
// // console.log('##widget', uploadWidget.value)
// let keywords = JSON.parse(uploadWidget.value)
// if (keywords && keywords[0]) {
// mutable_prompt.value=keywords.join('\n')
// }
// } catch (error) {}
}
}
})
app.registerExtension({
name: 'Mixlab.prompt.PromptSlide',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
@@ -167,10 +277,11 @@ app.registerExtension({
// 打印文件内容
// console.log(keywords)
// widget.value = keywords
let ks = getLocalData(`_mixlab_PromptSlide`)
ks[this.id] = keywords
setLocalDataOfWin(`_mixlab_PromptSlide`, ks)
widget.value = JSON.stringify(keywords)
// let ks = getLocalData(`_mixlab_PromptSlide`)
// ks[this.id] = keywords
// setLocalDataOfWin(`_mixlab_PromptSlide`, ks)
createSelect(select, keywords, prompt_keyword)
@@ -205,14 +316,13 @@ app.registerExtension({
if (node.type === 'PromptSlide') {
try {
let prompt = node.widgets.filter(w => w.name === 'prompt_keyword')[0]
let ks = getLocalData(`_mixlab_PromptSlide`)
let keywords = ks[node.id]
// let ks = getLocalData(`_mixlab_PromptSlide`)
let uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
// console.log('##widget', uploadWidget.value)
let keywords = JSON.parse(uploadWidget.value)
// console.log('keywords',keywords)
let widget = node.widgets.filter(w => w.select)[0]
if (keywords && keywords[0]) {
// let widget = node.widgets.filter(w => w.select)[0]
// console.log('select',widget,widget.value)
widget.select.style.display = 'block'
createSelect(widget.select, keywords, prompt)
}
@@ -221,17 +331,17 @@ app.registerExtension({
}
})
app.registerExtension({
name: 'Mixlab.prompt.PromptImage',
_createResult: async (node, widget, message) => {
widget.div.innerHTML = ``
const _createResult = async (node, widget, message) => {
widget.div.innerHTML = ``
const width = node.size[0] * 0.5 - 12
const width = node.size[0] * 0.5 - 12
let height_add = 0
let height_add = 0
for (let index = 0; index < message._images.length; index++) {
const img = message._images[index]
for (let index = 0; index < message._images.length; index++) {
const imgs = message._images[index]
for (const img of imgs) {
let url = api.apiURL(
`/view?filename=${encodeURIComponent(img.filename)}&type=${
img.type
@@ -262,7 +372,6 @@ app.registerExtension({
app.graph.change()
}
// const blob = item.getAsFile();
imageNode.pasteFile(blob)
}
@@ -283,9 +392,14 @@ app.registerExtension({
text-align: left;">${message.prompts[index]}</p>`
widget.div.appendChild(div)
}
}
node.size[1] = 98 + height_add
}
app.registerExtension({
name: 'Mixlab.prompt.PromptImage',
node.size[1] = 98 + height_add
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'PromptImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
@@ -339,14 +453,15 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
onExecuted?.apply(this, arguments)
console.log('PromptImage', message.prompts, message._images)
console.log('#PromptImage', message.prompts, message._images)
// window._mixlab_app_json = message.json
try {
let widget = this.widgets.filter(w => w.name === 'result')[0]
widget.value = message
this._createResult(this, widget, message)
} catch (error) {}
_createResult(this, widget, { ...message })
} catch (error) {
console.log(error)
}
}
this.serialize_widgets = true //需要保存参数
@@ -360,7 +475,7 @@ app.registerExtension({
let cards = widget.div.querySelectorAll('.card')
if (cards.length == 0) node.size = [280, 120]
this._createResult(node, widget, widget.value)
_createResult(node, widget, widget.value)
}
}
})
+69 -3
View File
@@ -159,7 +159,7 @@ app.registerExtension({
el: `#${inputColor.id}`,
theme: 'classic', // or 'monolith', or 'nano'
// closeOnScroll: true,
default:'#000000',
default: '#000000',
swatches: [
'rgba(244, 67, 54, 1)',
'rgba(233, 30, 99, 0.95)',
@@ -257,7 +257,6 @@ app.registerExtension({
app.registerExtension({
name: 'Mixlab.utils.TextToNumber',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TextToNumber') {
const onExecuted = nodeType.prototype.onExecuted
@@ -270,9 +269,76 @@ app.registerExtension({
const n = this.widgets.filter(w => w.name === 'number')[0]
n.value = message.num[0]
}
console.log('TextToNumber', random_number.value)
}
}
}
})
const min_max = node => {
if(node.widgets){
const min_value = node.widgets.filter(w => w.name === 'min_value')[0]
const max_value = node.widgets.filter(w => w.name === 'max_value')[0]
const number = node.widgets.filter(w => w.name === 'number')[0]
if (number) {
number.options.min = min_value.value
number.options.max = max_value.value
number.value = Math.min(number.options.max, number.value)
number.value = Math.max(number.options.min, number.value)
}
if (min_value)
min_value.callback = e => {
number.options.min = e
number.value = e
}
if (max_value)
max_value.callback = e => {
number.options.max = e
number.value = e
}
}
}
app.registerExtension({
name: 'Mixlab.utils.FloatSlider',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'FloatSlider') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'FloatSlider') {
min_max(node)
}
}
})
app.registerExtension({
name: 'Mixlab.utils.IntNumber',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'IntNumber') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'IntNumber') {
min_max(node)
}
}
})
+59 -30
View File
@@ -60,30 +60,7 @@ app.registerExtension({
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {
// // 绘制文件图标的函数
// function drawFileIcon () {
// // 清空画布
// // ctx.clearRect(0, 0, canvas.width, canvas.height)
// // 绘制文件外框
// ctx.fillStyle = '#000'
// ctx.fillRect(5, 5, 40, 40)
// // 绘制文件夹图标
// ctx.fillStyle = '#f00'
// ctx.fillRect(10, 15, 30, 20)
// // 绘制监听符号
// ctx.beginPath()
// ctx.arc(30, 35, 5, 0, 2 * Math.PI)
// ctx.fillStyle = '#00f'
// ctx.fill()
// }
// // 调用绘制函数
// drawFileIcon()
},
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
},
@@ -124,11 +101,19 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('watch widtget', this.widgets)
// 虚拟的widget,用于更新节点,让其每次都运行
const widget = {
type: 'div',
name: 'seed',
draw (ctx, node, widget_width, y, widget_height) {}
}
this.addCustomWidget(widget)
const watcher = this.widgets.filter(w => w.name == 'watcher')[0]
watcher.callback = () => {
console.log('watcher', watcher.value)
if (watcher.value === 'enable') {
if (window._mixlab_watcher_t)
clearInterval(window._mixlab_watcher_t)
@@ -140,7 +125,7 @@ app.registerExtension({
window._mixlab_file_path_watcher = json.event_type
// widget.card.innerText = window._mixlab_file_path_watcher || ''
//运行
document.querySelector('#queue-button').click()
// document.querySelector('#queue-button').click()
}
})
}, 1000)
@@ -162,15 +147,59 @@ app.registerExtension({
window._mixlab_file_path_watcher = json.event_type
})
/*
Add the widget, make sure we clean up nicely, and we do not want to be serialized!
*/
// this.addCustomWidget(widget)
this.onRemoved = function () {
// widget.card.remove()
}
this.serialize_widgets = true
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log(message)
try {
let seed = this.widgets.filter(w => w.name === 'seed')[0]
if (seed) {
if (!seed.value) seed.value = 0
seed.value += 1
}
} catch (error) {}
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadImagesFromPath') {
const watcher = node.widgets.filter(w => w.name == 'watcher')[0]
if (watcher) {
if (watcher.value === 'enable') {
if (window._mixlab_watcher_t) clearInterval(window._mixlab_watcher_t)
window._mixlab_watcher_t = setInterval(() => {
// 上次路径填充
getConfig().then(json => {
console.log(json.event_type)
if (json.event_type != window._mixlab_file_path_watcher) {
window._mixlab_file_path_watcher = json.event_type
// widget.card.innerText = window._mixlab_file_path_watcher || ''
//运行
// document.querySelector('#queue-button').click()
}
})
}, 1000)
} else {
if (window._mixlab_watcher_t) {
clearInterval(window._mixlab_watcher_t)
}
window._mixlab_watcher_t = null
}
}
try {
let seed = node.widgets.filter(w => w.name === 'seed')[0]
if (seed) {
if (!seed.value) seed.value = 0
seed.value += 1
}
} catch (error) {}
}
}
})