Compare commits

...
43 Commits
Author SHA1 Message Date
shadow 1daa1a4603 Merge pull request #111 from shadowcz007/v.11.0-PromptImage-node-图片和prompt匹配
V0.11.0 PromptImage & PromptSimplification
2024-01-06 00:17:26 +08:00
shadowcz007 062773d929 v0.11.0
> PromptImage & PromptSimplification,Assist in simplifying prompt words, comparing images and prompt word nodes.
2024-01-06 00:16:33 +08:00
shadowcz007 57decadaef Update index.html 2024-01-06 00:14:04 +08:00
shadowcz007 ec804ab7c9 优化 2024-01-05 23:09:08 +08:00
shadowcz007 3ee7533098 Update prompt_mixlab.js 2024-01-05 16:40:30 +08:00
shadowcz007 0d383ccc1f add PromptImage 2024-01-05 15:27:13 +08:00
shadowcz007 e2f2257c34 Update index.html 2024-01-05 12:22:23 +08:00
shadowcz007 d62b9fc4c6 ClipInterrogator可以作为输出 2024-01-05 11:52:04 +08:00
shadowcz007 be7ad0c7fb Update index.html 2024-01-04 23:35:56 +08:00
shadowcz007 156864cc8b PromptSimplification 2024-01-04 23:33:24 +08:00
shadowcz007 7240e496cc Update PromptNode.py 2024-01-04 23:02:48 +08:00
shadowcz007 8acdf4018d test PromptSimplification 2024-01-04 20:32:43 +08:00
shadowcz007 1ea7c3e203 修复floatSlide最大值问题 2024-01-04 19:43:33 +08:00
shadowcz007 e54aeb6125 Update index.html 2024-01-04 18:35:54 +08:00
shadowcz007 d59f51fbcf Update README.md 2024-01-04 18:20:52 +08:00
shadowcz007 a667eb6982 修复seed 为fixed 的运行按钮bug & 支持sd-xl 的SamplerCustom 2024-01-04 18:20:00 +08:00
shadowcz007 8d72732247 Update index.html 2024-01-04 17:19:46 +08:00
shadowcz007 f0f3b30a62 Update index.html 2024-01-04 17:03:36 +08:00
shadowcz007 d99fe24542 fixbug 2024-01-04 16:05:27 +08:00
shadowcz007 ea4c7381bd Update index.html 2024-01-04 14:07:38 +08:00
shadow e900d20641 Merge pull request #107 from shadowcz007/v0.10-add-clip-interrogator
Update index.html
2024-01-04 14:02:42 +08:00
shadowcz007 8a46647d8c Update index.html 2024-01-04 14:02:19 +08:00
shadow 968178bf57 Merge pull request #106 from shadowcz007/v0.10-add-clip-interrogator
V0.10 add clip interrogator
2024-01-04 13:37:31 +08:00
shadowcz007 406a255db0 v0.10.0 增加 ClipInterrogator、优化APP功能 2024-01-04 13:37:07 +08:00
shadowcz007 574557810e Update index.html 2024-01-04 13:24:50 +08:00
shadowcz007 998a02c3a4 上一次输入记录 2024-01-04 13:12:02 +08:00
shadowcz007 c6f964c921 textarea输入,增加上一次 输入记录 2024-01-04 12:57:55 +08:00
shadowcz007 efb0e147c5 Update index.html 2024-01-04 12:45:00 +08:00
shadowcz007 9cf7356f98 Update index.html 2024-01-04 12:33:59 +08:00
shadowcz007 af05c43174 支持image的batch输出 2024-01-04 12:31:45 +08:00
shadowcz007 380c68ff2b EnhanceImage节点支持batch多张输入和输出 2024-01-04 12:03:27 +08:00
shadowcz007 068b00b99f update 2024-01-04 11:24:00 +08:00
shadowcz007 cd6a42ab64 clip-interrogator 2024-01-04 11:03:25 +08:00
shadowcz007 f115abec92 add clip interrogator 2024-01-04 11:01:08 +08:00
shadowcz007 f0e23cf878 AIPC大赛模板 2024-01-03 22:28:38 +08:00
shadowcz007 a94f11d809 Update index.html 2024-01-03 20:22:24 +08:00
shadowcz007 38972bea5f 更新AIPC大赛模板-直接合成,免去ps 2024-01-03 18:03:33 +08:00
shadowcz007 a761ff552a Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-01-03 17:54:25 +08:00
shadowcz007 dbeb84ea9a resizeImage 缩放图像新增center模式,多余的背景可以设定填充颜色 2024-01-03 17:54:23 +08:00
shadow 0a4938f39a Merge pull request #105 from shadowcz007/v0.9.2-中断生成
修复3d image的bug,未上传bg图也可以运行了
2024-01-03 14:21:26 +08:00
shadowcz007 b2182c716d 修复3d image的bug,未上传bg图也可以运行了 2024-01-03 14:20:48 +08:00
shadow 8253be73f6 Merge pull request #104 from shadowcz007/v0.9.2-中断生成
添加中断生成的功能
2024-01-03 09:35:12 +08:00
shadowcz007 20318e296e 添加中断生成的功能 2024-01-03 09:32:29 +08:00
19 changed files with 3365 additions and 77 deletions
+9 -3
View File
@@ -33,6 +33,8 @@ APP-JSON:
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine
> seed统一输入控件,支持:SamplerCustom、KSampler
## 🏃🚗🚚🚀 Real-time Design
> ScreenShareNode & FloatingVideoNode. Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
@@ -71,6 +73,12 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
![randomPrompt](./assets/randomPrompt.png)
> ClipInterrogator
[add clip-interrogator](https://github.com/pharmapsychotic/clip-interrogator)
> PromptImage & PromptSimplification,Assist in simplifying prompt words, comparing images and prompt word nodes.
### Layers
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
@@ -167,9 +175,7 @@ v0.8.0 🚀🚗🚚🏃‍ LaMaInpainting
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : models/lama
<!-- ### Workflow
[Workflow](./workflow.md) -->
[Download Salesforce\blip-image-captioning-base](https://huggingface.co/Salesforce/blip-image-captioning-base), move to : models/clip_interrogator/Salesforce/blip-image-captioning-base
## Installation
+5 -1
View File
@@ -503,7 +503,7 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt,PromptSlide
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.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
@@ -512,6 +512,7 @@ from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,GetImageSize_,MultiplicationNode
from .nodes.Lama import LaMaInpainting
from .nodes.ClipInterrogator import ClipInterrogator
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
@@ -519,6 +520,9 @@ NODE_CLASS_MAPPINGS = {
"AppInfo":AppInfo,
"RandomPrompt":RandomPrompt,
"PromptSlide":PromptSlide,
"PromptSimplification":PromptSimplification,
"PromptImage":PromptImage,
"ClipInterrogator":ClipInterrogator,
"NoiseImage":NoiseImage,
"GradientImage":GradientImage,
"TransparentImage":TransparentImage,
Binary file not shown.
+2
View File
@@ -4786,6 +4786,8 @@
"NewLayer",
"RandomPrompt",
"PromptSlide",
"PromptSimplification",
"ClipInterrogator",
"ScreenShare",
"ShowLayer",
"ShowTextForGPT",
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -180,8 +180,9 @@ class ShowTextForGPT:
CATEGORY = "♾️Mixlab/GPT"
def run(self, text):
# print(session_history)
# print(text)
return {"ui": {"text": text}, "result": (text,)}
class CharacterInText:
+160
View File
@@ -0,0 +1,160 @@
import os
import folder_paths
from PIL import Image
import comfy.utils
import numpy as np
import json
import torch
from transformers import AutoProcessor, BlipForConditionalGeneration
from clip_interrogator import Config, Interrogator
def load_caption_model(model_path,config,t='blip-base'):
dtype=torch.float16 if config.device == 'cuda' else torch.float32
caption_model = BlipForConditionalGeneration.from_pretrained(model_path, torch_dtype=dtype)
caption_processor = AutoProcessor.from_pretrained(model_path)
caption_model.eval()
if not config.caption_offload:
caption_model = caption_model.to(config.device)
return (caption_model,caption_processor)
caption_model_path=os.path.join(folder_paths.models_dir, "clip_interrogator/Salesforce/blip-image-captioning-base")
if not os.path.exists(caption_model_path):
print(f"## clip_interrogator_model not found: {caption_model_path}, pls download from https://huggingface.co/Salesforce/blip-image-captioning-base")
cache_path=os.path.join(folder_paths.models_dir, "clip_interrogator")
# 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 image_analysis_fn(ci,image):
image = image.convert('RGB')
image_features = ci.image_to_features(image)
top_mediums = ci.mediums.rank(image_features, 5)
top_artists = ci.artists.rank(image_features, 5)
top_movements = ci.movements.rank(image_features, 5)
top_trendings = ci.trendings.rank(image_features, 5)
top_flavors = ci.flavors.rank(image_features, 5)
medium_ranks = {medium: sim for medium, sim in zip(top_mediums, ci.similarities(image_features, top_mediums))}
artist_ranks = {artist: sim for artist, sim in zip(top_artists, ci.similarities(image_features, top_artists))}
movement_ranks = {movement: sim for movement, sim in zip(top_movements, ci.similarities(image_features, top_movements))}
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
def image_to_prompt(ci,image, mode):
ci.config.chunk_size = 2048 if ci.config.clip_model_name == "ViT-L-14/openai" else 1024
ci.config.flavor_intermediate_count = 2048 if ci.config.clip_model_name == "ViT-L-14/openai" else 1024
image = image.convert('RGB')
if mode == 'best':
return ci.interrogate(image)
elif mode == 'classic':
return ci.interrogate_classic(image)
elif mode == 'fast':
return ci.interrogate_fast(image)
elif mode == 'negative':
return ci.interrogate_negative(image)
# image = Image.open(image_path).convert('RGB')
# ci = Interrogator(Config(clip_model_name="ViT-L-14/openai"))
# print(ci.interrogate(image))
class ClipInterrogator:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"prompt_mode": (['fast','classic','best','negative'],),
"image_analysis": (["off","on"],),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/prompt"
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
global ci
ci = None
def run(self,image,prompt_mode,image_analysis):
global ci
prompt_mode=prompt_mode[0]
analysis=image_analysis[0]
prompt_result=[]
analysis_result=[]
# 进度条
pbar = comfy.utils.ProgressBar(len(image)*(2 if analysis=='on' else 1))
if ci==None:
config=Config(
clip_model_name="ViT-L-14/openai",
device="cuda" if torch.cuda.is_available() else "cpu",
download_cache=True,
clip_model_path=cache_path,
cache_path=cache_path
)
config.apply_low_vram_defaults()
caption_model,caption_processor=load_caption_model(caption_model_path,config)
config.caption_model= caption_model
config.caption_processor= caption_processor
ci = Interrogator(config)
# else:
# simple_lama.model.to("cuda" if torch.cuda.is_available() else "cpu")
for i in range(len(image)):
im=image[i]
im=tensor2pil(im)
im=im.convert('RGB')
if analysis=='on':
analysis_res=image_analysis_fn(ci,im)
analysis_result.append( analysis_res )
pbar.update(1)
prompt=image_to_prompt(ci,im,prompt_mode)
pbar.update(1)
prompt_result.append(prompt)
# result.save("inpainted.png")
if ci.config.clip_offload and not ci.clip_offloaded:
ci.clip_model = ci.clip_model.to('cpu')
ci.clip_offloaded = True
if ci.config.caption_offload and not ci.caption_offloaded:
ci.caption_model = ci.caption_model.to('cpu')
ci.caption_offloaded = True
# analysis_result=[]
return {"ui":{"prompt": prompt_result,"analysis":analysis_result},"result": (prompt_result,)}
+42 -17
View File
@@ -516,26 +516,42 @@ def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option)
return bg_image
def resize_image(layer_image,scale_option,width,height):
def resize_image(layer_image, scale_option, width, height,color="white"):
layer_image = layer_image.convert("RGB")
original_width, original_height = layer_image.size
if scale_option == "height":
# 按照高度比例缩放
original_width, original_height = layer_image.size
# Scale image based on height
scale = height / original_height
new_width = int(original_width * scale)
layer_image = layer_image.resize((new_width, height))
elif scale_option == "width":
# 按照宽度比例缩放
original_width, original_height = layer_image.size
# Scale image based on width
scale = width / original_width
new_height = int(original_height * scale)
layer_image = layer_image.resize((width, new_height))
elif scale_option == "overall":
# 整体缩放
# Scale image overall
layer_image = layer_image.resize((width, height))
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)
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")
return resized_image
return layer_image
# 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)
@@ -937,20 +953,27 @@ class EnhanceImage:
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,)
OUTPUT_IS_LIST = (True,)
# 运行的函数
def run(self,image,contrast):
# print('EnhanceImage',image.shape)
image=tensor2pil(image)
image=enhance_depth_map(image,contrast)
# print('EnhanceImage',len(image),image[0].shape)
contrast=contrast[0]
res=[]
for ims in image:
for im in ims:
image=pil2tensor(image)
image=tensor2pil(im)
image=enhance_depth_map(image,contrast)
image=pil2tensor(image)
res.append(image)
return (image,)
return (res,)
@@ -1818,13 +1841,14 @@ class ResizeImage:
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
"scale_option": (["width","height",'overall','center'],),
},
"optional":{
"image": ("IMAGE",),
"average_color": (["on",'off'],),
"fill_color":("STRING",{"multiline": False,"default": "#FFFFFF","dynamicPrompts": False}),
}
}
@@ -1838,12 +1862,13 @@ class ResizeImage:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,True,)
def run(self,width,height,scale_option,image=None,average_color=['on']):
def run(self,width,height,scale_option,image=None,average_color=['on'],fill_color=["#FFFFFF"]):
w=width[0]
h=height[0]
scale_option=scale_option[0]
average_color=average_color[0]
fill_color=fill_color[0]
imgs=[]
average_images=[]
@@ -1860,7 +1885,7 @@ class ResizeImage:
else:
for im in image:
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=resize_image(im,scale_option,w,h,fill_color)
im=im.convert('RGB')
a_im=get_average_color_image(im)
+157 -3
View File
@@ -1,9 +1,11 @@
import random
import comfy.utils
import json
import os
import numpy as np
from urllib import request, parse
import folder_paths
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
# def queue_prompt(prompt_workflow):
# p = {"prompt": prompt_workflow}
# data = json.dumps(p).encode('utf-8')
@@ -45,12 +47,164 @@ default_prompt1='''Swing
default_prompt1="\n".join([p.strip() for p in default_prompt1.split('\n') if p.strip()!=''])
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def addWeight(text, weight=1):
if weight == 1:
return text
else:
return f"({text}:{round(weight,2)})"
def prompt_delete_words(sentence, new_words_length):
# 使用逗号分割句子,并去除空格
words = [word.strip() for word in sentence.split(",")]
# 计算需要删除的单词数量
num_to_delete = len(words) - new_words_length
words_to=[w for w in words]
# 逐个删除单词并存储在新列表中
new_words = []
for i in range(len(words)):
if num_to_delete > 0:
num_to_delete -= 1
else:
words_to.pop()
if len(words_to)>0:
new_words.append(", ".join(words_to))
return new_words
# # 测试方法
# sentence = "a computer, a glass tablet with a keyboard on a dark background, 3d illustration, reflection, cgi 8k, clear glass, archaic, cut-away, white outline"
# new_words_length = 5
# result = prompt_delete_words(sentence, new_words_length)
# print(result)
class PromptImage:
def __init__(self):
self.temp_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "PromptImage"
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompts": ("STRING",
{
"multiline": True,
"default": '',
"dynamicPrompts": False
}),
"images": ("IMAGE",{"default": None}),
"save_to_image": (["enable", "disable"],),
}
}
RETURN_TYPES = ()
OUTPUT_NODE = True
INPUT_IS_LIST = True
FUNCTION = "run"
CATEGORY = "♾️Mixlab/prompt"
# 运行的函数
def run(self,prompts,images,save_to_image):
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])
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
return { "ui": { "_images": results,"prompts":prompts } }
class PromptSimplification:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt": ("STRING",
{
"multiline": True,
"default": '',
"dynamicPrompts": False
}),
"length":("INT", {"default": 5, "min": 1,"max":100, "step": 1, "display": "number"}),
# "min_value":("FLOAT", {
# "default": -2,
# "min": -10,
# "max": 0xffffffffffffffff,
# "step": 0.01,
# "display": "number"
# }),
# "max_value":("FLOAT", {
# "default": 2,
# "min": -10,
# "max": 0xffffffffffffffff,
# "step": 0.01,
# "display": "number"
# }),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompts",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/prompt"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
# 运行的函数
def run(self,prompt,length):
length=length[0]
result=[]
for p in prompt:
nps=prompt_delete_words(p,length)
for n in nps:
result.append(n)
return {"ui": {"prompts": result}, "result": (result,)}
class PromptSlide:
+20 -7
View File
@@ -34,13 +34,13 @@ def create_temp_file(image):
) = folder_paths.get_save_image_path('tmp', output_dir)
image=tensor2pil(image)
im=tensor2pil(image)
image_file = f"{filename}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
image.save(image_path,compress_level=4)
im.save(image_path,compress_level=4)
return [{
"filename": image_file,
@@ -188,7 +188,7 @@ class FloatSlider:
"number":("FLOAT", {
"default": 0,
"min": 0, #Minimum value
"max": 1, #Maximum value
"max": 0xffffffffffffffff, #Maximum value
"step": 0.001, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
@@ -262,7 +262,7 @@ class IntNumber:
"default": 1,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step":1,
"step":1,
"display": "number"
}),
},
@@ -461,12 +461,25 @@ class AppInfo:
CATEGORY = "♾️Mixlab"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self,name,image,input_ids,output_ids,description,version,share_prefix,link,category):
name=name[0]
im=image[0][0]
# image [img,] img[batch,w,h,a] 列表里面是batch,
im=create_temp_file(image)
input_ids=input_ids[0]
output_ids=output_ids[0]
description=description[0]
version=version[0]
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])
+2 -1
View File
@@ -4,4 +4,5 @@ watchdog
opencv-python-headless
matplotlib
openai
simple-lama-inpainting
simple-lama-inpainting
clip-interrogator==0.6.0
+314 -41
View File
@@ -276,7 +276,7 @@
.show_text {
font-size: 14px;
/* display: inline-block; */
user-select: text;
margin: 8px;
padding: 32px;
min-width: 200px;
@@ -302,7 +302,7 @@
color: #555;
}
select,
button,
@@ -323,6 +323,15 @@
height: 56px !important;
outline: 1px solid white;
}
/* 给prompt image 节点使用 */
.prompt_image {
color: #2f2f2f;
padding: 0 10px;
font-size: 12px;
width: 200px;
}
</style>
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
<link href="/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css" rel="stylesheet">
@@ -399,23 +408,71 @@
}
async function getQueue(clientId) {
try {
const res = await fetch(`${get_url()}/queue`);
const data = await res.json();
return {
// Running action uses a different endpoint for cancelling
Running: Array.from(data.queue_running, prompt => {
if (prompt[3].client_id === clientId) {
let prompt_id = prompt[1];
return {
prompt_id,
remove: () => interrupt(),
}
}
}),
Pending: data.queue_pending.map((prompt) => ({ prompt })),
};
} catch (error) {
console.error(error);
return { Running: [], Pending: [] };
}
}
async function interrupt() {
try {
await fetch(`${get_url()}/interrupt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: undefined
});
} catch (error) {
console.error(error)
}
return true
}
function randomSeed(seed, data) {
for (const id in data) {
if (data[id].inputs.seed != undefined
&& !Array.isArray(data[id].inputs.seed) //如果是数组,则由其他节点控制
&& ['increment', 'decrement', 'randomize'].includes(seed[id])) {
data[id].inputs.seed = Math.round(Math.random() * 1849378600828930)
// console.log('new Seed', data[id])
}
if (data[id].inputs.noise_seed != undefined
&& !Array.isArray(data[id].inputs.noise_seed) //如果是数组,则由其他节点控制
&& ['increment', 'decrement', 'randomize'].includes(seed[id])) {
data[id].inputs.noise_seed = Math.round(Math.random() * 1849378600828930)
}
console.log('new Seed', data[id])
}
return data
}
function updateSeed(id, val) {
console.log(val)
if (!Array.isArray(window._appData.data[id].inputs.seed)) window._appData.data[id].inputs.seed = Math.round(val);
// console.log(val)
if (window._appData.data[id].inputs.seed && !Array.isArray(window._appData.data[id].inputs.seed)) window._appData.data[id].inputs.seed = Math.round(val);
if (window._appData.data[id].inputs.noise_seed && !Array.isArray(window._appData.data[id].inputs.noise_seed)) window._appData.data[id].inputs.noise_seed = Math.round(val);
}
@@ -555,7 +612,16 @@
div.innerText = Array.isArray(node.inputs.text) ? node.inputs.text[0] : node.inputs.text
output_card.appendChild(div);
};
if (["SaveImage", "PreviewImage"].includes(node.class_type)) {
if (node.class_type == "ClipInterrogator") {
let div = document.createElement('div');
div.className = "show_text";
div.id = `output_${node.id}`;
div.innerText = '#ClipInterrogator: …… '
output_card.appendChild(div);
};
if (["SaveImage", "PreviewImage", "PromptImage"].includes(node.class_type)) {
let a = document.createElement('a');
a.id = `output_${node.id}`
@@ -888,7 +954,7 @@
const label = data.title;
if (!options.keywords.includes(data.inputs.prompt_keyword)) {
if (options.keywords && !options.keywords?.includes(data.inputs.prompt_keyword)) {
options.keywords = [data.inputs.prompt_keyword, ...options.keywords]
};
@@ -918,16 +984,20 @@
let silde = createNumSlide(data.title,
data.inputs.number,
(v) => {
// console.log(data.id,window._appData.data[data.id])
window._appData.data[data.id].inputs.number = v;
},
options.min,
options.max,
data.class_type === 'IntNumber' ? 'int' : 'float')
data.class_type === 'IntNumber' ? 'int' : 'float',
null,
data.id
)
container.appendChild(silde);
}
// Check if the class_type is "CLIPTextEncode"
if (["TextInput_", "CLIPTextEncode"].includes(data.class_type)) {
if (["TextInput_", "CLIPTextEncode", "PromptSimplification"].includes(data.class_type)) {
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
@@ -939,9 +1009,34 @@
// Create an input field for the image name
const textInput = document.createElement("textarea");
// textInput.className=;
if (data.class_type == "PromptSimplification") {
textInput.value = data.inputs.prompt;
} else {
textInput.value = data.inputs.text;
}
// uploadImageInput.type = "text";
textInput.value = data.inputs.text;
let json = localStorage.getItem(`t_${data.id}`)
try {
// 缓存
const { value, height } = JSON.parse(json);
textInput.value = value;
textInput.style.height = height;
if (data.class_type == "PromptSimplification") {
window._appData.data[data.id].inputs.prompt = textInput.value;
} else {
window._appData.data[data.id].inputs.text = textInput.value;
}
} catch (error) {
}
uploadContainer.appendChild(textInput);
// autoResize(textInput);
function autoResize(textarea) {
textarea.style.height = 'auto';
@@ -951,7 +1046,16 @@
textInput.addEventListener('input', (event) => {
// console.log(textInput.value)
autoResize(textInput);
window._appData.data[data.id].inputs.text = textInput.value;
if (data.class_type == "PromptSimplification") {
window._appData.data[data.id].inputs.prompt = textInput.value;
} else {
window._appData.data[data.id].inputs.text = textInput.value;
}
localStorage.setItem(`t_${data.id}`, JSON.stringify({
value: textInput.value,
height: textInput.style.height
}));
})
// Append the upload container to the main container
@@ -962,6 +1066,22 @@
if (["CheckpointLoaderSimple", "LoraLoader"].includes(data.class_type)) {
let value = data.inputs.ckpt_name || data.inputs.lora_name;
try {
let v = localStorage.getItem(`_model_${data.id}_${data.class_type}`)
if (v) {
value = v;
if (data.class_type === 'CheckpointLoaderSimple') {
window._appData.data[data.id].inputs.ckpt_name = value;
}
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = value;
}
}
} catch (error) {
}
let [div, selectDom] = createSelectWithOptions(data.title, Array.from(data.options, o => {
return {
value: o,
@@ -978,6 +1098,8 @@
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = selectDom.value;
}
localStorage.setItem(`_model_${data.id}_${data.class_type}`, selectDom.value)
})
container.appendChild(div);
@@ -1104,6 +1226,15 @@
function createNumSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1, type = 'float', keywords = null, targetId = null) {
value = 'float' ? parseFloat(value.toFixed(3)) : parseInt(value)
try {
let i = parseFloat(localStorage.getItem(`_slider_${targetId}`))
if (!!i) {
value = i;
callback && callback(value)
}
} catch (error) {
}
// 输入div
let inputDiv = document.createElement('div');
@@ -1126,13 +1257,19 @@
if (keywords && keywords[0]) {
// label.innerHTML = ``
// 有备选的关键词
let selectTag = createSelect(Array.from(keywords, (k,i) => {
let defaultValue = (targetId ? localStorage.getItem(`_slide_${targetId}`) : '') || keywords[0];
window._appData.data[targetId].inputs.prompt_keyword = defaultValue;
let selectTag = createSelect(Array.from(keywords, (k, i) => {
return {
value: k,
text: k,
selected:i==0
selected: i == 0
}
}), keywords[0]);
}), defaultValue);
selectTag.style = `background: none;
color: black; max-width: 300px;
border-bottom: 1px solid #acacac;
@@ -1141,8 +1278,8 @@
selectTag.addEventListener('change', e => {
e.preventDefault();
window._appData.data[targetId].inputs.prompt_keyword = selectTag.value;
// label.querySelector('.label').innerText = selectTag.value
// console.log(window._appData.data[targetId].inputs.prompt_keyword,selectTag.value)
targetId ? localStorage.setItem(`_slide_${targetId}`, selectTag.value) : ''
})
label.appendChild(selectTag);
}
@@ -1177,6 +1314,8 @@
label.setAttribute('data-content', value);
// 在这里可以执行其他操作,根据需要进行相应的处理
callback && callback(value)
localStorage.setItem(`_slider_${targetId}`, value)
});
// 返回容器元素
@@ -1199,7 +1338,7 @@
// 设置默认值
selectElement.value = defaultValue;
console.log(defaultValue,options)
// console.log(defaultValue, options)
return selectElement
}
@@ -1433,7 +1572,6 @@
update: async function (type = "image", val, id) {
console.log(val, id)
if (val && type == "image" && output.querySelector(`#output_${id} img`)) {
// if (output.querySelector(`#output_${id}`)) {
let im = await createImage(val)
@@ -1445,10 +1583,52 @@
a.setAttribute('target', "_blank");
a.setAttribute('href', val);
// }
// else {
// output.querySelector(`#output_${id}`).src = val;
// }
}
if (val && (type == "images" || type == 'images_prompts') && output.querySelector(`#output_${id} img`)) {
let imgDiv = output.querySelector(`#output_${id}`)
imgDiv.style.display = 'none';
// 清空
// Array.from(imgDiv.parentElement.querySelectorAll('.output_images'), im => im.remove());
for (const v of val) {
let url = v, prompt = ''
if (type == 'images_prompts') {
// 是个数组,多了对应的prompt
url = v[0];
prompt = v[1];
}
let im = await createImage(url);
// 构建新的
let a = document.createElement('a');
a.className = `${imgDiv.id} output_images`
a.setAttribute('data-pswp-width', im.naturalWidth);
a.setAttribute('data-pswp-height', im.naturalHeight);
a.setAttribute('target', "_blank");
a.setAttribute('href', url);
let img = new Image();
// img;
img.src = url;
a.appendChild(img);
if (prompt) {
a.style.textDecoration = 'none';
let p = document.createElement('p')
p.className = 'prompt_image'
p.innerText = prompt;
a.appendChild(p)
}
// imgDiv.parentElement.appendChild(a);
imgDiv.parentElement.insertBefore(a, imgDiv.parentElement.firstChild);
}
}
@@ -1478,19 +1658,42 @@
},
submitButton: {
element: submitButton,
update: function (callback) {
update: function (runFn, cancelFn) {
submitButton.addEventListener('dblclick', (e) => {
e.preventDefault()
submitButton.classList.remove('disabled');
});
submitButton.addEventListener('click', (e) => {
e.preventDefault();
if (submitButton.classList.contains('data-click')) {
return
} else {
submitButton.classList.add('data-click')
setTimeout(() => submitButton.classList.remove('data-click'), 500)
}
if (!submitButton.classList.contains('disabled')) {
callback && callback();
runFn && runFn();
submitButton.classList.add('disabled');
setTimeout(() => submitButton.classList.remove('disabled'), 500)
submitButton.innerText = 'Cancel'
} else {
// 如果能取消
let canCancel = cancelFn && cancelFn();
if (canCancel) submitButton.classList.remove('disabled');
if (canCancel) submitButton.innerText = 'Create';
}
});
},
running: () => {
submitButton.innerText = 'Cancel';
if (!submitButton.classList.contains('disabled')) submitButton.classList.add('disabled');
},
reset: () => {
submitButton.innerText = 'Create';
submitButton.classList.remove('disabled');
submitButton.classList.remove('data-click');
}
},
};
@@ -1522,7 +1725,6 @@
Array.from(detail.querySelectorAll('.card'), c => c.classList.remove('selected'));
div.className = 'upload_btn card selected'
let { output, app } = jsonData;
window._appData = {
@@ -1561,10 +1763,19 @@
ui.status.update(appData ? 'READY' : '-');
// 添加提交按钮点击事件
ui.submitButton.update(function () {
// 在提交按钮点击时执行的逻辑
queuePrompt(window._appData.data, window._appData.seed, api.clientId)
});
ui.submitButton.update(
() => {
// 在提交按钮点击时执行的逻辑
queuePrompt(window._appData.data, window._appData.seed, api.clientId);
}, () => {
// 取消
if (api.runningCancel) {
api.runningCancel();
api.runningCancel = null;
return true
}
});
const show = (src, id, type = "image") => {
@@ -1573,52 +1784,97 @@
};
api.addEventListener("status", ({ detail }) => {
console.log("status", detail);
console.log("status", detail, detail.exec_info?.queue_remaining);
try {
ui.status.update(`queue#${detail.exec_info.queue_remaining}`);
ui.status.update(`queue#${detail.exec_info?.queue_remaining}`);
if (detail.exec_info?.queue_remaining === 0) {
// 运行按钮重设
ui.submitButton.reset()
console.log('运行按钮重设')
}
} catch (error) {
console.log(error)
}
});
api.addEventListener("progress", ({ detail }) => {
console.log("progress", detail);
const class_type = window._appData.data[detail?.node]?.class_type || ''
try {
ui.status.update(`${detail.value}/${detail.max}`);
ui.status.update(`${parseFloat(100 * detail.value / detail.max).toFixed(1)}% ${class_type}`);
ui.submitButton.running()
} catch (error) {
}
});
api.addEventListener("executed", ({ detail }) => {
api.addEventListener("executed", async ({ detail }) => {
console.log("executed", detail)
// if (!enabled) return;
const images = detail?.output?.images;
const text = detail?.output?.text;
const gifs = detail?.output?.gifs;
const prompt = detail?.output?.prompt;
const analysis = detail?.output?.analysis;
const _images = detail?.output?._images;
const prompts = detail?.output?.prompts;
if (images) {
// if (!images) return;
const src = `${get_url()}/view?filename=${encodeURIComponent(images[0].filename)}&type=${images[0].type}&subfolder=${encodeURIComponent(images[0].subfolder)}&t=${+new Date()}`;
show(src, detail.node, 'image');
// if (!images) return;
let url = get_url();
show(Array.from(images, img => {
return `${url}/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}&t=${+new Date()}`;
}), detail.node, 'images');
} 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');
} else if (text) {
ui.output.update("text", Array.isArray(text) ? text[0] : text, detail.node)
ui.output.update("text", Array.isArray(text) ? text.join('\n\n') : text, detail.node)
} else if (gifs && gifs[0]) {
// if (!images) return;
const src = `${get_url()}/view?filename=${encodeURIComponent(gifs[0].filename)}&type=${gifs[0].type}&subfolder=${encodeURIComponent(gifs[0].subfolder)
}&&format=${gifs[0].format}&t=${+new Date()}`;
show(src, detail.node, gifs[0].format.match('video') ? 'video' : 'image');
} else if (prompt && analysis) {
// #ClipInterrogator: ……
ui.output.update("text", `${prompt.join('\n\n')}\n${JSON.stringify(analysis, null, 2)}`, detail.node)
}
try {
ui.status.update(`executed_#${detail.node}`);
ui.status.update(`executed_#${window._appData.data[detail.node]?.class_type}`);
ui.submitButton.reset()
} catch (error) {
}
// console.log(Running, Pending);
try {
const { Running, Pending } = await getQueue(api.clientId);
if (Running && Running[0]) {
api.runningCancel = Running[0].remove;
ui.submitButton.running()
} else {
api.runningCancel = null;
}
} catch (error) {
api.runningCancel = null;
}
});
// api.addEventListener("b_preview", ({ detail }) => {
@@ -1626,15 +1882,32 @@
// console.log("b_preview", detail)
// show(URL.createObjectURL(detail));
// });
api.addEventListener("execution_error", ({ detail }) => {
console.log("execution_error", detail)
// show(URL.createObjectURL(detail));
});
api.addEventListener('execution_start', ({ detail }) => {
api.addEventListener('execution_start', async ({ detail }) => {
console.log("execution_start", detail)
try {
ui.status.update(`execution_start`);
ui.submitButton.running()
} catch (error) {
}
try {
const { Running, Pending } = await getQueue(api.clientId);
if (Running && Running[0]) {
api.runningCancel = Running[0].remove;
} else {
api.runningCancel = null;
}
} catch (error) {
api.runningCancel = null;
}
})
api.api_base = ""
+3
View File
@@ -196,6 +196,9 @@ app.registerExtension({
}
if (bg) {
data.bg_image = await parseImage(bg)
if (!data.bg_image.match('data:image/')) {
delete data.bg_image
}
}
if (material) {
+2 -2
View File
@@ -126,11 +126,11 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id }
}
if (node.type === 'KSampler') {
if (node.type === 'KSampler'||node.type=='SamplerCustom') {
// seed 的类型收集
try {
seed[id] = node.widgets.filter(
w => w.name === 'seed'
w => (w.name === 'seed'||w.name=='noise_seed')
)[0].linkedWidgets[0].value
} catch (error) {}
}
+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.9.1'
const version = 'v0.11.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+65
View File
@@ -0,0 +1,65 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
app.registerExtension({
name: 'Mixlab.prompt.ClipInterrogator',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'ClipInterrogator') {
function populate (prompts, items) {
if (this.widgets) {
for (let i = 0; i < this.widgets.length; i++) {
if (this.widgets[i].type !== 'combo') this.widgets[i].onRemove?.()
}
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
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)
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
sz[0] = this.size[0]
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1]
}
this.onResize?.(sz)
app.graph.setDirtyCanvas(true, false)
})
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log('##', message)
populate.call(this, message.prompt, message.analysis)
}
this.serialize_widgets = true //需要保存参数
}
}
})
+1
View File
@@ -256,6 +256,7 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
console.log('##',message.text)
populate.call(this, message.text);
};
+164
View File
@@ -35,6 +35,24 @@ function get_position_style (ctx, widget_width, y, node_height) {
justifyContent: 'space-between'
}
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
async function fetchImage (url) {
try {
const response = await fetch(url)
const blob = await response.blob()
return blob
} catch (error) {
console.error('出现错误:', error)
}
}
const getLocalData = key => {
let data = {}
@@ -133,7 +151,9 @@ app.registerExtension({
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()
@@ -200,3 +220,147 @@ app.registerExtension({
}
}
})
app.registerExtension({
name: 'Mixlab.prompt.PromptImage',
_createResult: async (node, widget, message) => {
widget.div.innerHTML = ``
const width = node.size[0] * 0.5 - 12
let height_add = 0
for (let index = 0; index < message._images.length; index++) {
const img = message._images[index]
let url = api.apiURL(
`/view?filename=${encodeURIComponent(img.filename)}&type=${
img.type
}&subfolder=${
img.subfolder
}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
let image = await createImage(url)
// 创建card
let div = document.createElement('div')
div.className = 'card'
div.draggable = true
div.ondragend = async event => {
console.log('拖动停止')
let url = div.querySelector('img').src
let blob = await fetchImage(url)
let imageNode = null
// No image node selected: add a new one
if (!imageNode) {
const newNode = LiteGraph.createNode('LoadImage')
newNode.pos = [...app.canvas.graph_mouse]
imageNode = app.graph.add(newNode)
app.graph.change()
}
// const blob = item.getAsFile();
imageNode.pasteFile(blob)
}
div.setAttribute('data-scale', image.naturalHeight / image.naturalWidth)
let h = (image.naturalHeight * width) / image.naturalWidth
if (index % 2 === 0) height_add += h
div.style = `width: ${width}px;height:${h}px;position: relative;margin: 4px;`
div.innerHTML = `<img src="${url}" style='width: 100%'/>
<p style="position: absolute;
bottom: 0;
left: 0;
background:#444444c2;
margin: 0;
font-size: 12px;
padding: 5px;
text-align: left;">${message.prompts[index]}</p>`
widget.div.appendChild(div)
}
node.size[1] = 98 + height_add
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'PromptImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('#orig_nodeCreated', this)
const widget = {
type: 'div',
name: 'result',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(this.div.style, {
...get_position_style(ctx, widget_width, y, node.size[1]),
flexWrap: 'wrap',
justifyContent: 'space-between',
// outline: '1px solid red',
paddingLeft: '0px',
width: widget_width + 'px'
})
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
const onResize = this.onResize
this.onResize = function () {
// 缩放发生
// console.log('##缩放发生', this.size)
let w = this.size[0] * 0.5 - 12
Array.from(widget.div.querySelectorAll('.card'), card => {
card.style.width = `${w}px`
card.style.height = `${
w * parseFloat(card.getAttribute('data-scale'))
}px`
})
return onResize?.apply(this, arguments)
}
// this.serialize_widgets = true //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
onExecuted?.apply(this, arguments)
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) {}
}
this.serialize_widgets = true //需要保存参数
}
},
async loadedGraphNode (node, app) {
if (node.type === 'PromptImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'result')[0]
console.log('widget.value', widget.value)
let cards = widget.div.querySelectorAll('.card')
if (cards.length == 0) node.size = [280, 120]
this._createResult(node, widget, widget.value)
}
}
})
File diff suppressed because it is too large Load Diff