Compare commits

...
34 Commits
Author SHA1 Message Date
shadowcz007 f13701c489 v0.15.0 2024-01-28 16:40:31 +08:00
shadow 31515b810e Merge pull request #159 from wfjsw/debloat-init-1
publish routes without having to replicate add_routes
2024-01-28 16:01:17 +08:00
shadowcz007 29e84e08a4 add CenterImage 2024-01-28 15:59:54 +08:00
shadowcz007 0ac9ad9757 修复批量保存本地图片的bug 2024-01-27 22:44:47 +08:00
shadowcz007 9a432e0608 Update Utils.py 2024-01-27 00:58:08 +08:00
shadowcz007 c83ba5fe7f Update PromptNode.py 2024-01-27 00:57:40 +08:00
shadowcz007 1b55c743ea 增加一些seed来控制节点 2024-01-27 00:33:49 +08:00
shadowcz007 a93579376c 不覆盖文件 2024-01-26 22:47:34 +08:00
shadowcz007 eba49f3c68 add SaveImageToLocal 2024-01-26 12:15:01 +08:00
shadowcz007 3a3da49c69 Update ImageNode.py 2024-01-25 20:10:29 +08:00
shadowcz007 3d68e48219 Update ImageNode.py 2024-01-25 20:07:31 +08:00
shadowcz007 4351fa6a0e 增加mask 的resize 2024-01-25 17:54:51 +08:00
shadowcz007 77222d2808 修复ImageCropByAlpha的bug 2024-01-25 15:40:00 +08:00
Jabasukuriputo Wang 36db7e5a9a publish routes without having to replicate add_routes 2024-01-25 00:44:51 -06:00
shadowcz007 3572368f16 fixbug 2024-01-25 10:38:23 +08:00
shadowcz007 0755dc1462 CreateLoraNames 2024-01-24 22:59:49 +08:00
shadowcz007 94f81b7102 fixbug 2024-01-24 17:45:15 +08:00
shadowcz007 08f8fe3d7e Update README.md 2024-01-23 23:44:47 +08:00
shadowcz007 a6cd383d67 add Sampler_names 2024-01-23 14:23:04 +08:00
shadowcz007 10face6ab0 CkptNames 2024-01-23 14:04:26 +08:00
shadowcz007 a6259ff600 add CkptNames 2024-01-23 13:58:59 +08:00
shadowcz007 d7d9e6cbfe add smart_connect_v1 2024-01-23 12:18:38 +08:00
shadowcz007 8263609470 优化LoadImageURL,增加seed,保证图片加载失败后可以继续 2024-01-23 10:03:55 +08:00
shadowcz007 fc2367de76 centerOnNode & fix node (widgets) 2024-01-21 20:31:57 +08:00
shadowcz007 9a4f2ebc70 Update ui_mixlab.js 2024-01-20 22:39:56 +08:00
shadowcz007 812879610a v0.14.0
发布新节点splitImage & gridoutput,用于分割图片和随机摆放元素
修复若干bug
2024-01-20 21:43:18 +08:00
shadowcz007 c5e521ccc1 add splitImage&gridoutput 2024-01-20 17:39:53 +08:00
shadowcz007 bc1c8fa351 Update index.html 2024-01-19 09:45:03 +08:00
shadowcz007 7271fcf9c1 api 2024-01-18 22:49:13 +08:00
shadowcz007 ace3b7707b Update README.md 2024-01-18 12:45:29 +08:00
shadowcz007 9eb65cc4ee Update ClipInterrogator.py 2024-01-18 11:12:03 +08:00
shadowcz007 c5c2bc779c add JoinWithDelimiter 2024-01-18 11:00:37 +08:00
shadowcz007 ae1751d9c0 Update Utils.py 2024-01-18 00:32:12 +08:00
shadowcz007 d17583ef7d fixbug 2024-01-17 17:56:23 +08:00
18 changed files with 1715 additions and 168 deletions
+8 -1
View File
@@ -1,5 +1,7 @@
> 适配了最新版comfyui的py3.11 ,torch 2.1.2+cu121
> [discord](https://discord.gg/cXs9vZSqeK)
## 🚀🚗🚚🏃 Workflow-to-APP
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- 支持多个web app 切换
@@ -115,6 +117,11 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
- [使用CkptNames 对比不同的模型效果](./workflow/ckpts-image-workflow.json)
- [CkptNames compare the effects of different models.](./workflow/ckpts-image-workflow.json)
## Other Nodes
@@ -173,7 +180,7 @@ An improvement has been made to directly redirect to GitHub to search for missin
[Download Salesforce/blip-image-captioning-base](https://huggingface.co/Salesforce/blip-image-captioning-base), move to : models/clip_interrogator/Salesforce/blip-image-captioning-base
[Download succinctly/text2image-prompt-generator](https://huggingface.co/succinctly/text2image-prompt-generator/tree/main),move to:text_generator/text2image-prompt-generator
[Download succinctly/text2image-prompt-generator](https://huggingface.co/succinctly/text2image-prompt-generator/tree/main),move to:prompt_generator/text2image-prompt-generator
[Download Helsinki-NLP/opus-mt-zh-en](https://huggingface.co/Helsinki-NLP/opus-mt-zh-en/tree/main),move to:prompt_generator/opus-mt-zh-en
+34 -29
View File
@@ -192,7 +192,7 @@ def read_workflow_json_files(folder_path ):
def get_workflows():
# print("#####path::", current_path)
workflow_path=os.path.join(current_path, "workflow")
print('workflow_path: ',workflow_path)
# print('workflow_path: ',workflow_path)
if not os.path.exists(workflow_path):
# 使用mkdir()方法创建新目录
os.mkdir(workflow_path)
@@ -231,8 +231,14 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
})
else:
category=''
input=None
output=None
if 'category' in x['app']:
category=x['app']['category']
if 'input' in x['app']:
input=x['app']['input']
if 'output' in x['app']:
output=x['app']['output']
apps.append({
"filename":item["filename"],
"category":category,
@@ -244,6 +250,8 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
"input":input,
"output":output
}
},
"date":item["date"]
@@ -271,8 +279,14 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
# print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
category=''
input=None
output=None
if 'category' in x['app']:
category=x['app']['category']
if 'input' in x['app']:
input=x['app']['input']
if 'output' in x['app']:
output=x['app']['output']
apps.append({
"filename":item["filename"],
# "category":category,
@@ -284,6 +298,8 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
"input":input,
"output":output
}
},
"date":item["date"]
@@ -414,7 +430,7 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
PromptServer.start=new_start
# 创建路由表
routes = web.RouteTableDef()
routes = PromptServer.instance.routes
@routes.post('/mixlab')
async def mixlab_hander(request):
@@ -500,27 +516,6 @@ async def nodes_map_hander(request):
return web.json_response(result)
# 把插件自定义的路由添加到comfyui server里
def new_add_routes(self):
import nodes
try:
self.user_manager.add_routes(self.routes)
except:
print('pls update')
self.app.add_routes(routes)
self.app.add_routes(self.routes)
for name, dir in nodes.EXTENSION_WEB_DIRS.items():
self.app.add_routes([
web.static('/extensions/' + urllib.parse.quote(name), dir, follow_symlinks=True),
])
self.app.add_routes([
web.static('/', self.web_root, follow_symlinks=True),
])
PromptServer.add_routes=new_add_routes
# 扩展api接口
# from server import PromptServer
@@ -534,15 +529,15 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage
from .nodes.ImageNode import GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.PromptNode import EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
from .nodes.ImageNode import SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import TESTNODE_,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
from .nodes.Mask import OutlineMask
from .nodes.Utils import CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
from .nodes.Mask import OutlineMask,FeatheredMask
# 要导出的所有节点及其名称的字典
@@ -569,6 +564,9 @@ NODE_CLASS_MAPPINGS = {
"ImageColorTransfer":ImageColorTransfer,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"SplitImage":SplitImage,
"CenterImage":CenterImage,
"GridOutput":GridOutput,
"MergeLayers":MergeLayers,
"SplitLongMask":SplitLongMask,
"FeatheredMask":FeatheredMask,
@@ -577,6 +575,7 @@ NODE_CLASS_MAPPINGS = {
"AreaToMask":AreaToMask,
"ImageCropByAlpha":ImageCropByAlpha,
"VAELoaderConsistencyDecoder":VAELoader,
"SaveImageToLocal":SaveImageToLocal,
"VAEDecodeConsistencyDecoder":VAEDecode,
"ScreenShare":ScreenShareNode,
"FloatingVideo":FloatingVideo,
@@ -598,7 +597,12 @@ NODE_CLASS_MAPPINGS = {
"GetImageSize_":GetImageSize_,
"SwitchByIndex":SwitchByIndex,
"LimitNumber":LimitNumber,
"OutlineMask":OutlineMask
"OutlineMask":OutlineMask,
"JoinWithDelimiter":JoinWithDelimiter,
"Seed_":CreateSeedNode,
"CkptNames_":CreateCkptNames,
"SamplerNames_":CreateSampler_names,
"LoraNames_":CreateLoraNames
# "LaMaInpainting":LaMaInpainting
# "GamePal":GamePal
}
@@ -625,7 +629,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"PromptGenerate_Mix":"PromptGenerate ♾️Mixlab",
"ChinesePrompt_Mix":"ChinesePrompt ♾️Mixlab",
"GamePal":"GamePal ♾️Mixlab",
"RembgNode_Mix":"Removebg"
"RembgNode_Mix":"Removebg",
"LoraNames_":"LoraName_TriggerWords.safetensors"
}
# web ui的节点功能
+11 -1
View File
@@ -4761,7 +4761,10 @@
],
"https://github.com/shadowcz007/comfyui-mixlab-nodes": [
[
"GridOutput",
"SplitImage",
"PromptGenerate_Mix",
"JoinWithDelimiter",
"ChinesePrompt_Mix",
"3DImage",
"AppInfo",
@@ -4770,11 +4773,16 @@
"ResizeImage",
"NoiseImage",
"PromptImage",
"SaveImageToLocal",
"AreaToMask",
"CLIPSeg_",
"CharacterInText",
"ChatGPTOpenAI",
"Color",
"Seed_",
"CkptNames_",
"SamplerNames_",
"LoraNames_",
"CombineMasks_",
"EnhanceImage",
"GradientImage",
@@ -4787,6 +4795,7 @@
"LoadImagesFromURL",
"MergeLayers",
"NewLayer",
"CenterImage",
"RandomPrompt",
"PromptSlide",
"PromptSimplification",
@@ -4807,7 +4816,8 @@
"TextToNumber",
"TextInput_",
"DynamicDelayProcessor",
"LaMaInpainting"
"LaMaInpainting",
"Moondream"
],
{
"title_aux": "comfyui-mixlab-nodes"
+8 -2
View File
@@ -24,6 +24,7 @@ def is_installed(package):
return False
return spec is not None
try:
if is_installed('clip_interrogator')==False:
import subprocess
@@ -36,20 +37,25 @@ try:
#检查命令执行结果
if result.returncode == 0:
print("#install success")
from transformers import AutoProcessor, BlipForConditionalGeneration
from clip_interrogator import Config, Interrogator
_available=True
else:
print("#install error")
else:
from transformers import AutoProcessor, BlipForConditionalGeneration
from clip_interrogator import Config, Interrogator
_available=True
except:
_available=False
try:
from transformers import AutoProcessor, BlipForConditionalGeneration
except:
_available=False
print('pls check transformers.__version__>=4.36.0:: AutoProcessor, BlipForConditionalGeneration')
def load_caption_model(model_path,config,t='blip-base'):
dtype=torch.float16 if config.device == 'cuda' else torch.float32
+303 -82
View File
@@ -9,11 +9,22 @@ from io import BytesIO
import folder_paths
import json,io
from comfy.cli_args import args
import cv2
import math
import cv2
import string
import math,glob
from .Watcher import FolderWatcher
def generate_random_string(length):
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for _ in range(length))
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
@@ -847,79 +858,6 @@ class SmoothMask:
class FeatheredMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
"start_offset":("INT", {"default": 1,
"min": -150,
"max": 150,
"step": 1,
"display": "slider"}),
"feathering_weight":("FLOAT", {"default": 0.1,
"min": 0.0,
"max": 1,
"step": 0.1,
"display": "slider"})
}
}
RETURN_TYPES = ('MASK',)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Mask"
OUTPUT_IS_LIST = (False,)
# 运行的函数
def run(self,mask,start_offset, feathering_weight):
# print(mask.shape,mask.size())
image=tensor2pil(mask)
# Open the image using PIL
image = image.convert("L")
if start_offset>0:
image=ImageOps.invert(image)
# Convert the image to a numpy array
image_np = np.array(image)
# Use Canny edge detection to get black contours
edges = cv2.Canny(image_np, 30, 150)
for i in range(0,abs(start_offset)):
# int(100*feathering_weight)
a=int(abs(start_offset)*0.1*i)
# Dilate the black contours to make them wider
kernel = np.ones((a, a), np.uint8)
dilated_edges = cv2.dilate(edges, kernel, iterations=1)
# dilated_edges = cv2.erode(edges, kernel, iterations=1)
# Smooth the dilated edges using Gaussian blur
smoothed_edges = cv2.GaussianBlur(dilated_edges, (5, 5), 0)
# Adjust the feathering weight
feathering_weight = max(0, min(feathering_weight, 1))
# Blend the smoothed edges with the original image to achieve feathering effect
image_np = cv2.addWeighted(image_np, 1, smoothed_edges, feathering_weight, feathering_weight)
# Convert the result back to PIL image
result_image = Image.fromarray(np.uint8(image_np))
result_image=result_image.convert("L")
if start_offset>0:
result_image=ImageOps.invert(result_image)
mask=pil2tensor(result_image)
# print(mask.shape,mask.size())
return mask
class SplitLongMask:
@@ -1249,7 +1187,7 @@ class ImageCropByAlpha:
if a != 0:
new_pixel_data[x, y] = (255, 255, 255, 255)
else:
new_pixel_data[x, y] = (r, g, b, a)
new_pixel_data[x, y] = (0,0,0,0)
# 保存修改后的图像
# new_image.save("output.png")
@@ -1323,6 +1261,9 @@ class LoadImagesFromURL:
return {"required": {
"url": ("STRING",{"multiline": True,"default": "https://","dynamicPrompts": False}),
},
"optional":{
"seed": (any_type, {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE","MASK",)
@@ -1339,7 +1280,7 @@ class LoadImagesFromURL:
global urls_image
urls_image={}
def run(self,url):
def run(self,url,seed=0):
global urls_image
print(urls_image)
def filter_http_urls(urls):
@@ -1671,6 +1612,183 @@ class NewLayer:
return (layer_n,)
def splitImage(image, num):
width, height = image.size
num_rows = int(num ** 0.5)
num_cols = int(num / num_rows)
grid_width = width // num_cols
grid_height = height // num_rows
grid_coordinates = []
for i in range(num_rows):
for j in range(num_cols):
x = j * grid_width
y = i * grid_height
grid_coordinates.append((x, y, grid_width, grid_height))
return grid_coordinates
def centerImage(margin,canvas):
w,h=canvas.size
l,t,r,b=margin
x=l
y=t
width=w-r-l
height=h-t-b
return (x,y,width,height)
# # 读取图片
# image = Image.open("path_to_your_image.jpg")
# # 定义要切割的区域数量
# num = 9
# # 切割图片
# grid_coordinates = splitImage(image, num)
# # 输出切割区域坐标
# for i, coordinates in enumerate(grid_coordinates):
# print(f"Region {i + 1}: x={coordinates[0]}, y={coordinates[1]}, width={coordinates[2]}, height={coordinates[3]}")
class SplitImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"num": ("INT",{
"default": 4,
"min": 1, #Minimum value
"max": 500, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"seed": ("INT",{
"default": 4,
"min": 1, #Minimum value
"max": 500, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
RETURN_TYPES = ("_GRID","_GRID",)
RETURN_NAMES = ("grids","grid")
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Layer"
INPUT_IS_LIST = False
# OUTPUT_IS_LIST = (True,)
def run(self,image,num,seed):
image=tensor2pil(image)
grids=splitImage(image,num)
if seed>=num:
num=int(seed / 500 * num)-1
else:
num=seed-1
num=max(0,num)
g=grids[num]
return (grids,g,)
class CenterImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"canvas": ("IMAGE",),
"left": ("INT",{
"default":24,
"min": 0, #Minimum value
"max": 5000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"top": ("INT",{
"default":24,
"min": 0, #Minimum value
"max": 5000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"right": ("INT",{
"default": 24,
"min": 0, #Minimum value
"max": 5000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"bottom": ("INT",{
"default": 24,
"min": 0, #Minimum value
"max": 5000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
RETURN_TYPES = ("_GRID",)
RETURN_NAMES = ("grid",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Layer"
INPUT_IS_LIST = False
# OUTPUT_IS_LIST = (True,)
def run(self,canvas,left,top,right,bottom):
canvas=tensor2pil(canvas)
grid=centerImage((left,top,right,bottom),canvas)
return (grid,)
class GridOutput:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"grid": ("_GRID",)
}
}
RETURN_TYPES = ("INT","INT","INT","INT",)
RETURN_NAMES = ("x","y","width","height",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Layer"
INPUT_IS_LIST = False
# OUTPUT_IS_LIST = (True,)
def run(self,grid):
x,y,w,h=grid
return (x,y,w,h,)
class ShowLayer:
@classmethod
def INPUT_TYPES(s):
@@ -2025,20 +2143,21 @@ class ResizeImage:
"image": ("IMAGE",),
"average_color": (["on",'off'],),
"fill_color":("STRING",{"multiline": False,"default": "#FFFFFF","dynamicPrompts": False}),
"mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE","IMAGE","STRING",)
RETURN_NAMES = ("image","average_image","average_hex",)
RETURN_TYPES = ("IMAGE","IMAGE","STRING","MASK",)
RETURN_NAMES = ("image","average_image","average_hex","mask",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,True,True,)
OUTPUT_IS_LIST = (True,True,True,True,)
def run(self,width,height,scale_option,image=None,average_color=['on'],fill_color=["#FFFFFF"]):
def run(self,width,height,scale_option,image=None,average_color=['on'],fill_color=["#FFFFFF"],mask=None):
w=width[0]
h=height[0]
@@ -2047,6 +2166,7 @@ class ResizeImage:
fill_color=fill_color[0]
imgs=[]
masks=[]
average_images=[]
hexs=[]
@@ -2075,8 +2195,20 @@ class ResizeImage:
a_im=pil2tensor(a_im)
average_images.append(a_im)
hexs.append(hex)
try:
for mas in mask:
for ma in mas:
ma=tensor2pil(ma)
ma=ma.convert('RGB')
ma=resize_image(ma,scale_option,w,h,fill_color)
ma=ma.convert('L')
ma=pil2tensor(ma)
masks.append(ma)
except:
print('')
return (imgs,average_images,hexs,)
return (imgs,average_images,hexs,masks,)
class MirroredImage:
@@ -2179,3 +2311,92 @@ class ImageColorTransfer:
class SaveImageToLocal:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ),
"file_path": ("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "♾️Mixlab/Image"
def save_images(self, images,file_path , prompt=None, extra_pnginfo=None):
filename_prefix = os.path.basename(file_path)
if file_path=='':
filename_prefix="ComfyUI"
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])
if not os.path.exists(file_path):
# 使用os.makedirs函数创建新目录
os.makedirs(file_path)
print("目录已创建")
else:
print("目录已存在")
# 使用glob模块获取当前目录下的所有文件
if file_path=="":
files = glob.glob(full_output_folder + '/*')
else:
files = glob.glob(file_path + '/*')
# 统计文件数量
file_count = len(files)
counter+=file_count
print('统计文件数量',file_count,counter)
results = list()
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
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]))
file = f"{filename}_{counter:05}_.png"
if file_path=="":
fp=os.path.join(full_output_folder, file)
if os.path.exists(fp):
file = f"{filename}_{counter:05}_{generate_random_string(8)}.png"
fp=os.path.join(full_output_folder, file)
img.save(fp, pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
else:
fp=os.path.join(file_path, file)
if os.path.exists(fp):
file = f"{filename}_{counter:05}_{generate_random_string(8)}.png"
fp=os.path.join(file_path, file)
img.save(os.path.join(file_path, file), pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": file_path,
"type": self.type
})
counter += 1
return ()
+103 -3
View File
@@ -1,10 +1,26 @@
import numpy as np
import scipy.ndimage
import torch
import comfy.utils
from nodes import MAX_RESOLUTION
import numpy as np
# from PIL import Image, ImageDraw
from PIL import Image, ImageOps
from comfy.cli_args import args
import cv2
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def grow(mask, expand, tapered_corners):
c = 0 if tapered_corners else 1
@@ -68,4 +84,88 @@ class OutlineMask:
m3=combine(m1,m2,0,0)
return (m3,)
return (m3,)
class FeatheredMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
"start_offset":("INT", {"default": 1,
"min": -150,
"max": 150,
"step": 1,
"display": "slider"}),
"feathering_weight":("FLOAT", {"default": 0.1,
"min": 0.0,
"max": 1,
"step": 0.1,
"display": "slider"})
}
}
RETURN_TYPES = ('MASK',)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Mask"
OUTPUT_IS_LIST = (True,)
# 运行的函数
def run(self,mask,start_offset, feathering_weight):
# print(mask.shape,mask.size())
num,_,_=mask.size()
masks=[]
for i in range(num):
mm=mask[i]
image=tensor2pil(mm)
# Open the image using PIL
image = image.convert("L")
if start_offset>0:
image=ImageOps.invert(image)
# Convert the image to a numpy array
image_np = np.array(image)
# Use Canny edge detection to get black contours
edges = cv2.Canny(image_np, 30, 150)
for i in range(0,abs(start_offset)):
# int(100*feathering_weight)
a=int(abs(start_offset)*0.1*i)
# Dilate the black contours to make them wider
kernel = np.ones((a, a), np.uint8)
dilated_edges = cv2.dilate(edges, kernel, iterations=1)
# dilated_edges = cv2.erode(edges, kernel, iterations=1)
# Smooth the dilated edges using Gaussian blur
smoothed_edges = cv2.GaussianBlur(dilated_edges, (5, 5), 0)
# Adjust the feathering weight
feathering_weight = max(0, min(feathering_weight, 1))
# Blend the smoothed edges with the original image to achieve feathering effect
image_np = cv2.addWeighted(image_np, 1, smoothed_edges, feathering_weight, feathering_weight)
# Convert the result back to PIL image
result_image = Image.fromarray(np.uint8(image_np))
result_image=result_image.convert("L")
if start_offset>0:
result_image=ImageOps.invert(result_image)
result_image=result_image.convert("L")
mt=pil2tensor(result_image)
masks.append(mt)
# print( mt.size())
return (masks,)
+50 -2
View File
@@ -23,6 +23,19 @@ def get_files_with_extension(directory, extension):
file_list.append(file_name)
return file_list
def join_with_(text_list,delimiter):
joined_text = delimiter.join(text_list)
return joined_text
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
default_prompt1='''Swing
Slide
@@ -65,7 +78,7 @@ def addWeight(text, weight=1):
if weight == 1:
return text
else:
return f"({text}:{round(weight,2)})"
return f"({text}:{round(weight,3)})"
def prompt_delete_words(sentence, new_words_length):
# 使用逗号分割句子,并去除空格
@@ -308,6 +321,10 @@ class RandomPrompt:
"default": 'sticker, Cartoon, ``'
}),
"random_sample": (["enable", "disable"],),
# "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
},
"optional":{
"seed": (any_type, {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
@@ -324,7 +341,7 @@ class RandomPrompt:
# 运行的函数
def run(self,max_count,mutable_prompt,immutable_prompt,random_sample):
def run(self,max_count,mutable_prompt,immutable_prompt,random_sample,seed=0):
# print('#运行的函数',mutable_prompt,immutable_prompt,max_count,random_sample)
# Split the text into an array of words
@@ -394,6 +411,7 @@ class EmbeddingPrompt:
# 运行的函数
def run(self,embedding,weight):
weight = round(weight, 3)
prompt='embedding:'+embedding
if weight!=1:
prompt='('+prompt+':'+str(weight)+')'
@@ -401,3 +419,33 @@ class EmbeddingPrompt:
# return (new_prompt)
return (prompt,)
RETURN_TYPES = (any_type,)
class JoinWithDelimiter:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text_list": (any_type,),
"delimiter":(["newline","comma"],),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Prompt"
INPUT_IS_LIST = True # 当true的时候,输入时list,当false的时候,如果输入是list,则会自动包一层for循环调用
OUTPUT_IS_LIST = (False,)
def run(self,text_list,delimiter):
delimiter=delimiter[0]
if delimiter =='newline':
delimiter='\n'
elif delimiter=='comma':
delimiter=','
t=''
if isinstance(text_list, list):
t=join_with_(text_list,delimiter)
return (t,)
+138 -12
View File
@@ -7,6 +7,8 @@ import folder_paths
import matplotlib.font_manager as fm
import torch
def recursive_search(directory, excluded_dir_names=None):
if not os.path.isdir(directory):
return [], {}
@@ -198,14 +200,17 @@ class TextToNumber:
return {"required": {
"text": ("STRING",{"multiline": False,"default": "1"}),
"random_number": (["enable", "disable"],),
"number":("INT", {
"default": 0,
"min": 0, #Minimum value
"max_num":("INT", {
"default": 10,
"min":2, #Minimum value
"max": 10000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
"optional":{
"seed": (any_type, {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("INT",)
@@ -218,7 +223,7 @@ class TextToNumber:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text,random_number,number):
def run(self,text,random_number,max_num,seed=0):
numbers = re.findall(r'\d+', text)
result=0
@@ -227,7 +232,7 @@ class TextToNumber:
# print(result)
if random_number=='enable' and result>0:
result= random.randint(1, 10000000000)
result= random.randint(1, max_num)
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
@@ -341,11 +346,18 @@ class MultiplicationNode:
def INPUT_TYPES(s):
return {"required": {
"numberA":(any_type,),
"numberB":("FLOAT", {
"multiply_by":("FLOAT", {
"default": 0,
"min": -1, #Minimum value
"min": -2, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.1, #Slider's step
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"add_by":("FLOAT", {
"default": 0,
"min": -2000, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
})
},
@@ -360,9 +372,9 @@ class MultiplicationNode:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,numberA,numberB):
b=int(numberA*numberB)
a=float(numberA*numberB)
def run(self,numberA,multiply_by,add_by):
b=int(numberA*multiply_by+add_by)
a=float(numberA*multiply_by+add_by)
return (a,b,)
class TextInput:
@@ -418,7 +430,7 @@ class DynamicDelayProcessor:
},
"optional":{
"any_input":(any_type,),
"delay_by_text":("STRING",{"multiline":True,}),
"delay_by_text":("STRING",{"multiline":True,"dynamicPrompts": False,}),
"words_per_seconds":("FLOAT",{ "default":1.50,"min": 0.0,"max": 1000.00,"display":"Chars per second?"}),
"replace_output": (["disable","enable"],),
"replace_value":("INT",{ "default":-1,"min": 0,"max": 1000000,"display":"Replacement value"})
@@ -705,3 +717,117 @@ class TESTNODE_:
result = list_stats.count_types(ANY)
return {"ui": {"data": result,"type":[str(type(ANY[0]))]}, "result": (ANY,)}
class CreateSeedNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("seed",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Utils"
def run(self, seed):
return (seed,)
class CreateCkptNames:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"ckpt_names": ("STRING",{"multiline": True,"default": "\n".join(folder_paths.get_filename_list("checkpoints")),"dynamicPrompts": False}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("ckpt_names",)
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,)
# OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Utils"
def run(self, ckpt_names):
ckpt_names=ckpt_names.split('\n')
ckpt_names = [name for name in ckpt_names if name.strip()]
return (ckpt_names,)
class CreateLoraNames:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"lora_names": ("STRING",{"multiline": True,"default": "\n".join(folder_paths.get_filename_list("loras")),"dynamicPrompts": False}),
}
}
RETURN_TYPES = (any_type,"STRING",)
RETURN_NAMES = ("lora_names","prompt",)
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,True,)
# OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Utils"
def run(self, lora_names):
lora_names=lora_names.split('\n')
lora_names = [name for name in lora_names if name.strip()]
prompts=[os.path.splitext(n)[0] for n in lora_names]
return (lora_names,prompts,)
class CreateSampler_names:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sampler_names": ("STRING",{"multiline": True,"default": "\n".join(comfy.samplers.KSampler.SAMPLERS),"dynamicPrompts": False}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("sampler_names",)
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,)
# OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Utils"
def run(self, sampler_names):
sampler_names=sampler_names.split('\n')
sampler_names = [name for name in sampler_names if name.strip()]
return (sampler_names,)
+2 -2
View File
@@ -4,5 +4,5 @@ watchdog
opencv-python-headless
matplotlib
openai
# simple-lama-inpainting
# clip-interrogator==0.6.0
simple-lama-inpainting
clip-interrogator==0.6.0
+2 -2
View File
@@ -391,14 +391,14 @@
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a>
</div>
<a target="_blank" href="https://github.com/shadowcz007/comfyui-mixlab-nodes" style="text-decoration: none;
<a target="_blank" href="https://www.mixcomfy.com/blog/" style="text-decoration: none;
color: black;font-size:12px">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true"
class="octicon octicon-mark-github v-align-middle color-fg-default">
<path
d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z">
</path>
</svg> Code
</svg> Community
</a>
</div>
+4 -1
View File
@@ -399,7 +399,7 @@ app.registerExtension({
}
},
async loadedGraphNode (node, app) {
console.log('#loadedGraphNode1111')
// console.log('#loadedGraphNode1111')
window._mixlab_app_json = null //切换workflow需要清空
if (node.type === 'AppInfo') {
let auto_save = node.widgets.filter(w => w.name == 'auto_save')[0]
@@ -408,6 +408,9 @@ app.registerExtension({
auto_save.value = 'enable'
}
}
// app.canvas.centerOnNode(node)
// app.canvas.setZoom(0.45)
}
}
})
+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.13.0'
const version = 'v0.15.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+38 -6
View File
@@ -156,6 +156,32 @@ const parseSvg = async svgContent => {
return { data, image: base64, svgElement }
}
function findImages(nodeId) {
// 检查当前节点是否有 imgs 字段
const n = app.graph.getNodeById(nodeId)
if (n.imgs) {
return n.imgs;
}
// 检查当前节点的 inputs 是否有 image 字段
if (n.inputs) {
for (let i = 0; i < n.inputs.length; i++) {
if (n.inputs[i].name==='image'||n.inputs[i].name==='images') {
// 获取新的 nodeId,并递归调用 findImages 函数
var linkId = n.inputs[i]?.link;
var origin_id = app.graph.links[linkId].origin_id
return findImages(origin_id);
}
}
}
// 如果没有找到 imgs 字段或者 image 字段,则返回 null
return null;
}
async function setArea (cw, ch, topBase64, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.8)
let div = document.createElement('div')
@@ -571,15 +597,19 @@ app.registerExtension({
}
}
try {
console.log('this.inputs', this.inputs)
let topLinkId = this.inputs[0].link
let topNodeId = app.graph.links[topLinkId].origin_id
let topIm = app.graph.getNodeById(topNodeId).imgs[0]
console.log('this.inputs', this.id)
let imgs=findImages(this.id)
// let topLinkId = this.inputs[0].link
// let topNodeId = app.graph.links[topLinkId].origin_id
let topIm = imgs[0]
let linkId = this.inputs[3].link
let nodeId = app.graph.links[linkId].origin_id
// console.log(linkId,this.inputs)
let im = app.graph.getNodeById(nodeId).imgs[0]
let imgs2=findImages(nodeId)
let im = imgs2[0]
console.log(topIm,im)
// let src = im.src
setArea(
im.naturalWidth,
@@ -589,7 +619,9 @@ app.registerExtension({
data,
updateValue
)
} catch (error) {}
} catch (error) {
console.log(error)
}
})
}
}
+9 -3
View File
@@ -1331,7 +1331,13 @@ app.registerExtension({
let w = 360,
s = widget.preview.videoWidth / widget.preview.videoHeight,
h = w / s || w
console.log(h)
// console.log(h)
if (!window.documentPictureInPicture) {
window.alert(
'This feature is available only in secure contexts (HTTPS), in some or all supporting browsers. https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API'
)
}
const pipWindow = await documentPictureInPicture.requestWindow({
width: w,
@@ -2137,8 +2143,8 @@ const node = {
loadedGraphNode (node, app) {
if (node.type === 'RandomPrompt') {
try {
let max_count = node.widgets.filter(w => w.name === "max_count")[0];
max_count.value= node.widgets_values[0]
let max_count = node.widgets.filter(w => w.name === 'max_count')[0]
max_count.value = node.widgets_values[0]
// console.log('RandomPrompt',max_count,node.widgets_values[0])
} catch (error) {
console.log(error)
+1 -10
View File
@@ -266,16 +266,7 @@ app.registerExtension({
},
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) {}
}
}
})
+302
View File
@@ -0,0 +1,302 @@
const smart_connect_config_input = [
{
node_type: 'CLIPTextEncode',
node_widget_name: 'text',
inputNodeName: 'RandomPrompt',
inputNode_output_name: 'STRING'
},
{
node_type: 'CLIPTextEncode',
node_widget_name: 'text',
inputNodeName: 'EmbeddingPrompt',
inputNode_output_name: 'STRING'
},
{
node_type: 'CLIPTextEncode',
node_widget_name: 'text',
inputNodeName: 'ChinesePrompt_Mix',
inputNode_output_name: 'prompt'
},
{
node_type: 'CheckpointLoaderSimple',
node_widget_name: 'ckpt_name',
inputNodeName: 'CkptNames_',
inputNode_output_name: 'ckpt_names'
},
{
node_type: 'KSampler',
node_widget_name: 'sampler_name',
inputNodeName: 'SamplerNames_',
inputNode_output_name: 'sampler_names'
},
{
node_type: 'LoraLoaderModelOnly',
node_widget_name: 'lora_name',
inputNodeName: 'LoraNames_',
inputNode_output_name: 'lora_names'
},
{
node_type: 'LoadLoRA',
node_widget_name: 'lora_name',
inputNodeName: 'LoraNames_',
inputNode_output_name: 'lora_names'
},
{
node_type: 'Moondream',
node_widget_name: 'image',
inputNodeName: 'LoadImage',
inputNode_output_name: 'IMAGE'
}
]
const smart_connect_config_output = [
{
node_type: 'LoadImage',
node_output_name: 'IMAGE',
outputNodeName: 'ClipInterrogator',
outputNode_input_name: 'image'
},
{
node_type: 'VAEDecode',
node_output_name: 'IMAGE',
outputNodeName: 'PromptImage',
outputNode_input_name: 'images'
},
{
node_type: 'VAEDecode',
node_output_name: 'IMAGE',
outputNodeName: 'PreviewImage',
outputNode_input_name: 'images'
},
{
node_type: 'VAEDecode',
node_output_name: 'IMAGE',
outputNodeName: 'SaveImage',
outputNode_input_name: 'images'
},
{
node_type: 'Moondream',
node_output_name: 'STRING',
outputNodeName: 'ShowTextForGPT',
outputNode_input_name: 'text'
}
]
// import {
// convertToInput,
// getConfig,
// isConvertableWidget
// } from '../../../extensions/core/widgetInputs.js'
const CONVERTED_TYPE = 'converted-widget'
const GET_CONFIG = Symbol()
function getConfig (widgetName) {
const { nodeData } = this.constructor
return (
nodeData?.input?.required[widgetName] ??
nodeData?.input?.optional?.[widgetName]
)
}
function hideWidget (node, widget, suffix = '') {
widget.origType = widget.type
widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
if (!node.inputs) {
return undefined
}
let node_input = node.inputs.find(i => i.widget?.name === widget.name)
if (!node_input || !node_input.link) {
return undefined
}
return widget.origSerializeValue
? widget.origSerializeValue()
: widget.value
}
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ':' + widget.name)
}
}
}
function convertToInput (node, widget, config) {
hideWidget(node, widget)
const type = config[0]
// Add input and store widget config for creating on primitive node
const sz = node.size
node.addInput(widget.name, type, {
widget: { name: widget.name, [GET_CONFIG]: () => config }
})
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
}
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
}
export function smart_init () {
LGraphCanvas.prototype._createNodeForInput = function (
node,
widget,
inputNodeName,
inputNode_slot
) {
// console.log(node.pos)
// var widget = node.widgets.filter(w => w.name === node_widget_name)[0]
if (widget) {
// 如果有存在的,没有连线输出的,自动连,不新建
let input_node = null
Array.from(app.graph.findNodesByType(inputNodeName), n => {
var links = n.outputs.filter(o => o.name === inputNode_slot)[0].links
// console.log(links)
if (!links || links?.length === 0) input_node = n
})
// 新建
if (!input_node) {
input_node = LiteGraph.createNode(inputNodeName)
input_node.pos = [node.pos[0] - node.size[0] - 24, node.pos[1] - 48]
app.canvas.graph.add(input_node, false)
} else {
input_node.pos = [node.pos[0] - node.size[0] - 24, node.pos[1] - 48]
}
const config = getConfig.call(node, widget.name) ?? [
widget.type,
widget.options || {}
]
let node_slotType = config[0]
// 如果input没有,则创建
if (!node.inputs?.filter(inp => inp.name === widget.name)[0]||!node.inputs)
convertToInput(node, widget, config)
input_node.connectByType(inputNode_slot, node, node_slotType)
}
}
LGraphCanvas.prototype._createNodeForOutput = function (
node,
widget,
outputNodeName,
outputNode_slot
) {
if (widget) {
let output_node
Array.from(app.graph.findNodesByType(outputNodeName), n => {
var links = n.inputs.filter(o => o.name === outputNode_slot)[0].links
// console.log(links)
if (!links || links?.length === 0) output_node = n
})
console.log('output_node', output_node, widget.name)
if (!output_node) {
// 新建
output_node = LiteGraph.createNode(outputNodeName)
output_node.pos = [node.pos[0] + node.size[0] + 24, node.pos[1] - 48]
app.canvas.graph.add(output_node, false)
} else {
output_node.pos = [node.pos[0] + node.size[0] + 24, node.pos[1] - 48]
}
const config = getConfig.call(node, widget.name) ?? [
widget.type,
widget.options || {}
]
let node_slotType = config[0]
console.log(node_slotType, output_node, outputNode_slot)
let type = output_node.inputs.filter(
inp => inp.name == outputNode_slot
)[0].type
node.connectByType(node_slotType, output_node, type)
}
}
}
export function addSmartMenu (options, node) {
let sopts = []
for (const sc of smart_connect_config_input) {
// 有智能推荐,则出现
if (node.type === sc.node_type) {
// console.log('smart',node)
// 则出现 randomPrompt
// CLIPTextEncode 的widget ,name== 'text'
let node_widget_name = sc.node_widget_name
let widget = node.widgets.filter(w => w.name === node_widget_name)[0]
if (!widget) {
// 控件没有,则查找inputs
widget = node.inputs.filter(w => w.name === node_widget_name)[0]
}
let isLinkNull = true
// 如果input里已经有,但是link为空
if (node.inputs?.filter(inp => inp.name === node_widget_name)[0]) {
isLinkNull =
node.inputs.filter(inp => inp.name === node_widget_name)[0].link ===
null
}
if (widget && isLinkNull) {
sopts.push({
content: sc.inputNodeName.split('_')[0] + '➡️',
callback: () => {
LGraphCanvas.prototype._createNodeForInput(
node, //当前node
widget, //当前node里需要自动连线的widget
sc.inputNodeName, //作为input的node type
sc.inputNode_output_name // 作为input的node的outputs的name. the input slot type of the target node
)
}
})
}
}
}
for (const sc of smart_connect_config_output) {
if (node.type === sc.node_type) {
let node_output_name = sc.node_output_name
const widget = node.outputs.filter(w => w.name === node_output_name)[0]
let isLinkNull = true
// 如果output里 link为空
if (node.outputs?.filter(inp => inp.name === node_output_name)[0]) {
isLinkNull =
node.outputs.filter(inp => inp.name === node_output_name)[0].links
?.length === 0
if (!node.outputs.filter(inp => inp.name === node_output_name)[0].links)
isLinkNull = true
}
if (widget && isLinkNull) {
sopts.push({
content: '➡️' + sc.outputNodeName.split('_')[0],
callback: () => {
LGraphCanvas.prototype._createNodeForOutput(
node, //当前node
widget, //当前node里需要自动连线的widget
sc.outputNodeName, //作为input的node type
sc.outputNode_input_name // 作为input的node的outputs的name. the input slot type of the target node
)
}
})
}
}
}
if (sopts.length > 0) options = [...sopts, null, ...options]
return options
}
+143 -11
View File
@@ -1,7 +1,4 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import { closeIcon } from './svg_icons.js'
import {
@@ -9,6 +6,67 @@ import {
GroupNodeHandler
} from '../../../extensions/core/groupNode.js'
import { smart_init, addSmartMenu } from './smart_connect.js'
function copyNodeValues (src, dest) {
// title
dest.title = src.title
// copy input connections
for (let i in src.inputs) {
let input = src.inputs[i]
if (input.link) {
let link = app.graph.links[input.link]
let src_node = app.graph.getNodeById(link.origin_id)
if (dest.inputs.filter(inp => inp.name === input.name).length === 0) {
// 没有,name换了
let dInp = dest.inputs.filter(inp => inp.type === input.type)
if (dInp.length === 1) {
src_node.connect(link.origin_slot, dest.id, dInp[0].name)
}
} else {
src_node.connect(link.origin_slot, dest.id, input.name)
}
}
}
// copy output connections
let output_links = {}
for (let i in src.outputs) {
let output = src.outputs[i]
if (output.links) {
let links = []
for (let j in output.links) {
links.push(app.graph.links[output.links[j]])
}
output_links[output.name] = links
}
}
for (let i in dest.outputs) {
let links = output_links[dest.outputs[i].name]
if (links) {
for (let j in links) {
let link = links[j]
let target_node = app.graph.getNodeById(link.target_id)
dest.connect(parseInt(i), target_node, link.target_slot)
}
}
}
// copy widgets
for (const w of src.widgets) {
for (const d of dest.widgets) {
if (w.name === d.name) {
d.value = w.value
}
}
}
app.graph.afterChange()
}
function deepEqual (obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
return false
@@ -575,22 +633,71 @@ app.registerExtension({
}
}
LGraphCanvas.prototype.fixTheNode = function (node) {
let new_node = LiteGraph.createNode(node.comfyClass)
new_node.pos = [node.pos[0], node.pos[1]]
app.canvas.graph.add(new_node, false)
copyNodeValues(node, new_node)
app.canvas.graph.remove(node)
}
smart_init()
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions // store the existing method
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// replace it
const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
console.log('getNodeMenuOptions', node.type == 'CLIPTextEncode')
return [
let opts = [
{
content: 'Help ♾️Mixlab', // with a name
callback: () => {
LGraphCanvas.prototype.helpAboutNode(node)
} // and the callback
},
null,
...options
] // and return the options
{
content: 'Fix node v2', // with a name
callback: () => {
LGraphCanvas.prototype.fixTheNode(node)
}
}
]
opts = addSmartMenu(opts,node)
// if (node.type == 'CLIPTextEncode') {
// // 则出现 randomPrompt
// // CLIPTextEncode 的widget ,name== 'text'
// let node_widget_name = 'text'
// const widget = node.widgets.filter(w => w.name === node_widget_name)[0]
// let mixlab_nodes_smart_connect= [{node_type:'CLIPTextEncode',
// node_widget_name:'text',
// inputNodeName:'RandomPrompt',
// inputNode_output_type:'STRING'}]
// if (widget) {
// opts = [
// {
// content: 'RandomPrompt',
// callback: () => {
// LGraphCanvas.prototype._createNodeForInput(
// node, //当前node
// widget,//当前node里需要自动连线的widget
// 'RandomPrompt',//作为input的node type
// 'STRING'// 作为input的node的outputs的type. the input slot type of the target node
// )
// }
// },
// null,
// ...opts
// ]
// }
// }
return [...opts, null, ...options] // and return the options
}
const getGroupMenuOptions = LGraphCanvas.prototype.getGroupMenuOptions // store the existing method
@@ -697,6 +804,17 @@ app.registerExtension({
})
} // and the callback
},
{
content: `Remove Group&Nodes ♾️Mixlab`, // with a name
callback: async (value, opts, e, menu, group) => {
// console.log(group)
let nodes = group._nodes
for (const node of nodes) {
app.graph.remove(node)
}
app.graph.remove(group)
} // and the callback
},
null,
...options
] // and return the options
@@ -722,7 +840,7 @@ app.registerExtension({
const apps = await get_my_app()
let apps_map = { '0': [] }
let apps_map = { 0: [] }
for (const app of apps) {
if (app.category) {
@@ -735,7 +853,7 @@ app.registerExtension({
let apps_opts = []
for (const category in apps_map) {
console.log('category',typeof(category))
console.log('category', typeof category)
if (category === '0') {
apps_opts.push(
...Array.from(apps_map[category], a => {
@@ -764,7 +882,7 @@ app.registerExtension({
} else {
// 二级
apps_opts.push({
content: '🚀 '+category,
content: '🚀 ' + category,
has_submenu: true,
disabled: false,
submenu: {
@@ -1047,7 +1165,7 @@ app.registerExtension({
has_submenu: true,
disabled: false,
submenu: {
options:apps_opts
options: apps_opts
}
}
)
@@ -1055,5 +1173,19 @@ app.registerExtension({
return options
}
}, 1000)
},
async loadedGraphNode (node, app) {
// console.log(
// '#ui init',
// app.graph._nodes[app.graph._nodes.length - 1].id,
// node.id
// )
try {
// 用来居中显示节点
if ((app.graph._nodes[app.graph._nodes.length - 1].id, node.id)) {
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.45)
}
} catch (error) {}
}
})
+558
View File
@@ -0,0 +1,558 @@
{
"last_node_id": 69,
"last_link_id": 72,
"nodes": [
{
"id": 37,
"type": "CLIPTextEncode",
"pos": [
6705,
-216
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 33
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
34
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 5,
"type": "CLIPTextEncode",
"pos": [
6693,
61
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 6
},
{
"name": "text",
"type": "STRING",
"link": 25,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
3
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 3,
"type": "EmptyLatentImage",
"pos": [
6689,
306
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 27,
"type": "EmbeddingPrompt",
"pos": [
6104,
23
],
"size": {
"0": 399.6408996582031,
"1": 82
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
25
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "EmbeddingPrompt"
},
"widgets_values": [
"negative-embed-verybadimagenegative_v1.3",
1
]
},
{
"id": 67,
"type": "VAEDecode",
"pos": [
7551,
-184
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 70
},
{
"name": "vae",
"type": "VAE",
"link": 69
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
71
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 1,
"type": "KSampler",
"pos": [
7187,
-145
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 34
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 3
},
{
"name": "latent_image",
"type": "LATENT",
"link": 4
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
70
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
408562451564429,
"fixed",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 61,
"type": "PromptImage",
"pos": [
7853,
-238
],
"size": [
465.6378949342379,
760.4568424013569
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 71
},
{
"name": "prompts",
"type": "STRING",
"link": 72,
"widget": {
"name": "prompts"
}
}
],
"properties": {
"Node name for S&R": "PromptImage"
},
"widgets_values": [
"",
"disable",
{
"_images": [
[
{
"filename": "mixlab_PromptImage_0_00027_.png",
"subfolder": "",
"type": "output"
}
],
[
{
"filename": "mixlab_PromptImage_1_00028_.png",
"subfolder": "",
"type": "output"
}
],
[
{
"filename": "mixlab_PromptImage_2_00029_.png",
"subfolder": "",
"type": "output"
}
],
[
{
"filename": "mixlab_PromptImage_3_00030_.png",
"subfolder": "",
"type": "output"
}
],
[
{
"filename": "mixlab_PromptImage_4_00031_.png",
"subfolder": "",
"type": "output"
}
],
[
{
"filename": "mixlab_PromptImage_5_00032_.png",
"subfolder": "",
"type": "output"
}
]
],
"prompts": [
"512-inpainting-ema.safetensors",
"SSD-1B.safetensors",
"awportrait_v12.safetensors",
"cardosAnime_v20.safetensors",
"deliberate_v2.safetensors",
"gameIconInstitute_v40.safetensors"
]
}
]
},
{
"id": 2,
"type": "CheckpointLoaderSimple",
"pos": [
6105,
-139
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "ckpt_name",
"type": [
"512-inpainting-ema.safetensors",
"SSD-1B.safetensors",
"awportrait_v12.safetensors",
"cardosAnime_v20.safetensors",
"deliberate_v2.safetensors",
"gameIconInstitute_v40.safetensors",
"illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors",
"sd_xl_turbo_1.0_fp16.safetensors",
"svd.safetensors"
],
"link": 64,
"widget": {
"name": "ckpt_name"
}
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
6,
33
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
69
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 56,
"type": "CkptNames_",
"pos": [
7860,
-494
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "ckpt_names",
"type": "*",
"links": [
64,
72
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CkptNames_"
},
"widgets_values": [
"512-inpainting-ema.safetensors\nSSD-1B.safetensors\nawportrait_v12.safetensors\ncardosAnime_v20.safetensors\ndeliberate_v2.safetensors\ngameIconInstitute_v40.safetensors"
]
}
],
"links": [
[
1,
2,
0,
1,
0,
"MODEL"
],
[
3,
5,
0,
1,
2,
"CONDITIONING"
],
[
4,
3,
0,
1,
3,
"LATENT"
],
[
6,
2,
1,
5,
0,
"CLIP"
],
[
25,
27,
0,
5,
1,
"STRING"
],
[
33,
2,
1,
37,
0,
"CLIP"
],
[
34,
37,
0,
1,
1,
"CONDITIONING"
],
[
64,
56,
0,
2,
0,
[
"512-inpainting-ema.safetensors",
"SSD-1B.safetensors",
"awportrait_v12.safetensors",
"cardosAnime_v20.safetensors",
"deliberate_v2.safetensors",
"gameIconInstitute_v40.safetensors",
"illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors",
"sd_xl_turbo_1.0_fp16.safetensors",
"svd.safetensors"
]
],
[
69,
2,
2,
67,
1,
"VAE"
],
[
70,
1,
0,
67,
0,
"LATENT"
],
[
71,
67,
0,
61,
0,
"IMAGE"
],
[
72,
56,
0,
61,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}