Compare commits

...
Author SHA1 Message Date
shadowcz007 3191cf2af0 test 2023-12-29 13:27:48 +08:00
shadowcz007 7d3e1ae945 test 2023-12-29 12:59:27 +08:00
shadowcz007 bfced5b4da test 2023-12-29 12:49:05 +08:00
shadowcz007 efe73fb965 Update README.md 2023-12-29 10:39:01 +08:00
shadowcz007 c45a962fcc workflow-to-app支持checkpoints和lora 2023-12-29 10:38:22 +08:00
shadowcz007 f98a03e2e9 Update README.md 2023-12-29 00:00:16 +08:00
shadowcz007 5b6257814d 优化 2023-12-28 23:56:48 +08:00
shadowcz007 69a445d4ed 新增切换节点 2023-12-28 23:18:25 +08:00
shadowcz007 e82c786b8a 增加了从剪切板获取图片的控件 2023-12-28 18:36:34 +08:00
shadowcz007 eec2225c89 支持视频 2023-12-28 16:02:25 +08:00
shadowcz007 f7355e0b71 update 2023-12-28 14:33:59 +08:00
shadowcz007 6c6a99cfe4 优化LoadImagefromlocal ,新增LoadImageFromURL 2023-12-28 13:24:05 +08:00
shadowcz007 b4634e2e0d 修复clipseg的bug 2023-12-28 12:09:36 +08:00
shadowcz007 3f4cba0612 fixbug:textimage的高宽不对 2023-12-27 21:45:35 +08:00
shadowcz007 38db99cc75 支持showtext作为输出。GPT聊天也可以实现workflow-to-app了 2023-12-27 20:39:21 +08:00
shadowcz007 4d5906394b 优化newlayer的可视化效果 2023-12-27 20:12:55 +08:00
shadowcz007 2fc212b156 update 2023-12-27 19:38:39 +08:00
shadowcz007 53fbb5b027 fixbug 2023-12-27 17:48:49 +08:00
shadowcz007 4f24721450 Update README.md 2023-12-27 16:48:29 +08:00
shadow 83043727b5 Merge pull request #83 from shadowcz007/v0.6---simple-app
V0.6   simple app
2023-12-27 16:29:27 +08:00
14 changed files with 933 additions and 79 deletions
+9
View File
@@ -11,6 +11,12 @@ Example:
APP-JSON:
- [text-to-image](./app/text-to-image_1_Wed%20Dec%2027%202023.json)
- [image-to-image](./app/image-to-image_1_Wed%20Dec%2027%202023.json)
- text-to-text
> 暂时支持6种节点作为界面上的输入节点:Load Image、CLIPTextEncode、TextInput_、FloatSlider、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT
### 3D
![](./assets/3dimage.png)
@@ -51,6 +57,9 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### LoadImagesFromURL
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
### 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.
+20 -5
View File
@@ -202,6 +202,8 @@ def get_nodes_map():
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
@@ -270,12 +272,16 @@ async def mixlab_hander(request):
print(e)
return web.json_response(data)
# @routes.post('/test')
# async def mixlab_hander(request):
# test_auto()
# return web.Response(text="test", status=200)
@routes.get('/mixlab/app')
async def mixlab_app_handler(request):
html_file = os.path.join(current_path, "web/index.html")
if os.path.exists(html_file):
with open(html_file, 'r') as f:
with open(html_file, 'r', encoding='utf-8', errors='ignore') as f:
html_data = f.read()
return web.Response(text=html_data, content_type='text/html')
else:
@@ -359,14 +365,14 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,LoadImagesFromURL,UploadImageForSMMS,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import AppInfo,FloatSlider,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
from .nodes.Utils import AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,GetImageSize_,MultiplicationNode
from .nodes.ShareNode import ShareToWeibo
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
@@ -377,6 +383,7 @@ NODE_CLASS_MAPPINGS = {
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
"LoadImagesFromURL":LoadImagesFromURL,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
@@ -403,9 +410,17 @@ NODE_CLASS_MAPPINGS = {
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"FloatSlider":FloatSlider,
"IntNumber":IntNumber,
"TextInput_":TextInput,
"Font":FontInput,
"TextToNumber":TextToNumber,
"DynamicDelayProcessor":DynamicDelayProcessor
"DynamicDelayProcessor":DynamicDelayProcessor,
"MultiplicationNode":MultiplicationNode,
"GetImageSize_":GetImageSize_,
"SwitchByIndex":SwitchByIndex,
"LimitNumber":LimitNumber,
"UploadImageForSMMS":UploadImageForSMMS,
"ShareToWeibo":ShareToWeibo
# "GamePal":GamePal
}
+11 -1
View File
@@ -4763,6 +4763,8 @@
[
"3DImage",
"AppInfo",
"IntNumber",
"FloatSlider",
"ResizeImage",
"NoiseImage",
"AreaToMask",
@@ -4778,6 +4780,7 @@
"Font",
"ImageCropByAlpha",
"LoadImagesFromPath",
"LoadImagesFromURL",
"MergeLayers",
"NewLayer",
"RandomPrompt",
@@ -4794,7 +4797,14 @@
"VAEDecodeConsistencyDecoder",
"VAELoaderConsistencyDecoder",
"TextToNumber",
"DynamicDelayProcessor"
"TextInput_",
"DynamicDelayProcessor",
"MultiplicationNode",
"ShareToWeibo",
"LimitNumber",
"SwitchByIndex",
"UploadImageForSMMS",
"GetImageSize_"
],
{
"title_aux": "comfyui-mixlab-nodes"
+1 -1
View File
@@ -83,7 +83,7 @@ class ChatGPTNode:
}),
"model": (["gpt-3.5-turbo","gpt-35-turbo","gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-4-0613","gpt-4-1106-preview"],
{"default": "gpt-3.5-turbo"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
+17 -6
View File
@@ -35,6 +35,16 @@ if not os.path.exists(clipseg_model_dir):
"""Helper methods for CLIPSeg nodes"""
# 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 tensor_to_numpy(tensor: torch.Tensor) -> np.ndarray:
"""Convert a tensor to a numpy array and scale its values to 0-255."""
array = tensor.numpy().squeeze()
@@ -107,7 +117,7 @@ class CLIPSeg:
RETURN_NAMES = ("Mask","Heatmap Mask", "BW Mask")
# INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
OUTPUT_IS_LIST = (False,False,False,)
FUNCTION = "segment_image"
def segment_image(self, image: torch.Tensor, text: str, blur: float, threshold: float, dilation_factor: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -180,12 +190,13 @@ class CLIPSeg:
binary_mask_image = Image.fromarray(binary_mask_resized[..., 0])
# convert PIL image to numpy array
tensor_bw = binary_mask_image.convert("RGB")
tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
tensor_bw = torch.from_numpy(tensor_bw)[None,]
tensor_bw = tensor_bw.squeeze(0)[..., 0]
tensor_bw = binary_mask_image.convert("L")
tensor_bw=pil2tensor(tensor_bw)
# tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
# tensor_bw = torch.from_numpy(tensor_bw)[None,]
# tensor_bw = tensor_bw.squeeze(0)[..., 0]
return tensor_bw, image_out_heatmap, image_out_binary
return (tensor_bw, image_out_heatmap, image_out_binary,)
#OUTPUT_NODE = False
+186 -10
View File
@@ -1,4 +1,5 @@
import numpy as np
import requests
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
@@ -197,6 +198,24 @@ def load_image(fp,white_bg=False):
return images
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
content_type = response.headers.get('Content-Type')
image = Image.open(BytesIO(response.content))
# Create a mask from the image's alpha channel
mask = image.convert('RGBA').split()[-1]
# Convert the mask to a black and white image
mask = mask.convert('L')
image=image.convert('RGB')
return (image, mask)
# 获取图片s
def get_images_filepath(f,white_bg=False):
@@ -495,10 +514,10 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = (len(max(lines, key=len)) * (font_size + spacing)) + spacing
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = (len(lines) * (font_size + spacing)) - spacing
height = (len(lines) * (font_size + spacing)) + spacing
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
@@ -541,7 +560,7 @@ def base64_to_image(base64_string):
return image
def create_temp_file(image):
def create_temp_file(image,fn='material'):
output_dir = folder_paths.get_temp_directory()
(
@@ -550,7 +569,7 @@ def create_temp_file(image):
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('material', output_dir)
) = folder_paths.get_save_image_path(fn, output_dir)
image=tensor2pil(image)
@@ -567,6 +586,42 @@ def create_temp_file(image):
"type": "temp"
}]
def create_temp_file_for_upload(image,fn='tmp'):
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path(fn, output_dir)
image=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)
return image_path
def upload_smms(fp,token):
# print(json.dumps(res, indent=4))
image_url=''
try:
headers = {'Authorization': token}
files = {'smfile': open(fp, 'rb')}
url = 'https://smms.app/api/v2/upload'
res = requests.post(url, files=files, headers=headers).json()
image_url=res['data']['url']
except:
print('upload error')
return image_url
class SmoothMask:
@classmethod
@@ -869,7 +924,7 @@ class LoadImagesFromPath:
}
}
RETURN_TYPES = ('IMAGE','MASK','STRING')
RETURN_TYPES = ('IMAGE','MASK','STRING',)
FUNCTION = "run"
@@ -902,6 +957,11 @@ class LoadImagesFromPath:
images=get_images_filepath(file_path,white_bg=='enable')
# 当开启了监听,则取最新的,第一个文件
if watcher=='enable':
index_variable=0
newest_files='enable'
# 排序
sorted_files = sorted(images, key=lambda x: os.path.getmtime(x['file_path']), reverse=(newest_files=='enable'))
@@ -913,9 +973,13 @@ class LoadImagesFromPath:
masks.append(im['mask'])
# print('index_variable',index_variable)
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
try:
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
except Exception as e:
print("发生了一个未知的错误:", str(e))
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
@@ -983,7 +1047,7 @@ class TextImage:
},
}
RETURN_TYPES = ("IMAGE","MASK")
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
@@ -1004,6 +1068,62 @@ class TextImage:
return (img,mask,)
class LoadImagesFromURL:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"url": ("STRING",{"multiline": True,"default": "https://","dynamicPrompts": False}),
},
}
RETURN_TYPES = ("IMAGE","MASK",)
RETURN_NAMES = ("images","masks",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,True,)
global urls_image
urls_image={}
def run(self,url):
global urls_image
print(urls_image)
def filter_http_urls(urls):
filtered_urls = []
for url in urls.split('\n'):
if url.startswith('http'):
filtered_urls.append(url)
return filtered_urls
filtered_urls = filter_http_urls(url)
images=[]
masks=[]
for img_url in filtered_urls:
try:
if img_url in urls_image:
img,mask=urls_image[img_url]
else:
img,mask=load_image_and_mask_from_url(img_url)
urls_image[img_url]=(img,mask)
img1=pil2tensor(img)
mask1=pil2tensor(mask)
images.append(img1)
masks.append(mask1)
except Exception as e:
print("发生了一个未知的错误:", str(e))
return (images,masks,)
class SvgImage:
@@ -1592,4 +1712,60 @@ class ResizeImage:
im=pil2tensor(im)
return (im,)
return (im,)
class UploadImageForSMMS:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"token": ("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("url",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,image,token):
# print(image,token)
fp=create_temp_file_for_upload(image)
url=upload_smms(fp,token)
return (url,)
# 压缩到5M
# from PIL import Image
# import os
# def compress_image(input_image_path, output_image_path):
# image = Image.open(input_image_path)
# image.save(output_image_path, optimize=True, quality=50)
# def get_image_size(image_path):
# return os.path.getsize(image_path) / (1024 * 1024) # 将文件大小从字节转换为兆字节
# def compress_to_5mb(input_image_path, output_image_path):
# compress_image(input_image_path, output_image_path)
# max_iterations = 10 # 设置最大循环次数
# iterations = 0
# while get_image_size(output_image_path) > 5 and iterations < max_iterations:
# compress_image(output_image_path, output_image_path)
# iterations += 1
# # 示例用法
# input_image_path = "input.jpg"
# output_image_path = "output.jpg"
# compress_to_5mb(input_image_path, output_image_path)
+37
View File
@@ -0,0 +1,37 @@
import urllib.parse
# 分享到微博
class ShareToWeibo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"title":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
"pic_url":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
"url":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
}
}
RETURN_TYPES = ()
# RETURN_NAMES = ("number",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/share"
INPUT_IS_LIST = False
OUTPUT_NODE = True
# OUTPUT_IS_LIST = ()
def run(self, title, pic_url, url):
encoded_title = urllib.parse.quote(title)
encoded_pic_url = urllib.parse.quote(pic_url)
encoded_url = urllib.parse.quote(url)
url = "https://service.weibo.com/share/share.php?title={}&pic={}&url={}".format(encoded_title,encoded_pic_url,encoded_url)
print(url)
return {"ui": {"url": [url]}, "result": ()}
+196 -1
View File
@@ -174,7 +174,7 @@ class FloatSlider:
"default": 0,
"min": 0, #Minimum value
"max": 1, #Maximum value
"step": 0.01, #Slider's step
"step": 0.001, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
},
@@ -193,6 +193,84 @@ class FloatSlider:
return (number,)
class IntNumber:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("INT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number):
return (number,)
class MultiplicationNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"numberA":(any_type,),
"numberB":("FLOAT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
})
},
}
RETURN_TYPES = ("FLOAT","INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,numberA,numberB):
b=int(numberA*numberB)
a=float(numberA*numberB)
return (a,b,)
class TextInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": ""}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text):
return (text,)
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
@@ -319,6 +397,7 @@ class AppInfo:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
OUTPUT_NODE = True
def run(self,name,image,input_ids,output_ids,description,version):
@@ -331,4 +410,120 @@ class AppInfo:
class GetImageSize_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "get_size"
CATEGORY = "♾️Mixlab/utils"
def get_size(self, image):
_, height, width, _ = image.shape
return (width, height)
class SwitchByIndex:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"A":(any_type,),
"B":(any_type,),
"index":("INT", {
"default": -1,
"min": -1,
"max": 1000,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("C",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self, A,B,index):
C=[]
index=index[0]
for a in A:
C.append(a)
for b in B:
C.append(b)
if index>-1:
try:
C=[C[index]]
except Exception as e:
C=[]
return (C,)
class LimitNumber:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number":(any_type,),
"min_value":("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"max_value":("INT", {
"default": 1,
"min": 1,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("number",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self, number, min_value, max_value):
nn=number
if isinstance(number, int):
min_value=int(min_value)
max_value=int(max_value)
if isinstance(number, float):
min_value=float(min_value)
max_value=float(max_value)
if number < min_value:
nn= min_value
elif number > max_value:
nn= max_value
return (nn,)
+2 -1
View File
@@ -3,4 +3,5 @@ pyOpenSSL
watchdog
opencv-python-headless
matplotlib
openai
openai
# playwright
+261 -41
View File
@@ -30,17 +30,23 @@
margin: 12px;
background: white;
padding: 8px;
width: 80%;
}
.description p {
max-width: 200px;
word-wrap: break-word;
}
.panel {
display: flex;
flex-direction: column;
min-width: 300px;
min-width: 400px;
background: #eee;
margin: 24px;
flex: 1;
align-items: center;
justify-content: center;
/* justify-content: center; */
}
.panel h1 {
@@ -49,9 +55,12 @@
margin-bottom: 0;
}
.panel img {
width: 100%;
.panel img,
video {
height: fit-content;
width: fit-content;
max-width: 100%;
margin-left: 12px;
}
.input_card {
@@ -60,7 +69,21 @@
}
.output_card {
height: 100%;
width: 100%;
box-shadow: 0px 0px 8px 3px #e6e7e7;
display: flex;
/* justify-content: center;
align-items: center; */
}
.output_card img,
video {
max-width: 400px;
max-height: 600px;
}
.card {
@@ -73,9 +96,11 @@
.card textarea {
width: 100%;
height: 200px;
/* height: 200px; */
/* min-width: 300px; */
margin-top: 12px;
resize: vertical;
overflow: hidden;
}
.card img {
@@ -83,6 +108,10 @@
margin-top: 12px;
}
.card .select {
margin-top: 12px;
}
.run_btn {
background: black;
color: white;
@@ -126,6 +155,15 @@
color: yellow;
}
.show_text {
font-size: 14px;
/* display: inline-block; */
/* margin: 13px; */
padding: 32px;
background: #242424;
color: white;
}
.link {
text-decoration: none;
color: gray;
@@ -247,14 +285,83 @@
container.className = 'output_card'
for (const node of outputData) {
let img = new Image();
img.id = `output_${node.id}`;
img.src = base64Df;
container.appendChild(img);
console.log('output', node)
if (node.class_type == "ShowTextForGPT") {
let div = document.createElement('div');
div.className = "show_text"
div.id = `output_${node.id}`;
div.innerText = node.inputs.text[0]
container.appendChild(div);
};
if (["SaveImage", "PreviewImage"].includes(node.class_type)) {
let img = new Image();
img.id = `output_${node.id}`;
img.src = base64Df;
container.appendChild(img);
}
// video ,gif
if (["VHS_VideoCombine"].includes(node.class_type)) {
let v = document.createElement('div');
let video = document.createElement('video'), img = new Image();
video.style.display = 'none'
video.controls = 'true'
video.autoplay = 'true'
video.loop = 'true'
v.id = `output_${node.id}`;
img.src = base64Df;
v.appendChild(video);
v.appendChild(img);
container.appendChild(v);
}
}
return container
}
async function calculateImageHash(blob) {
const buffer = await blob.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
return hashHex;
}
async function handleClipboardImage(imageElement, data) {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
if (type.startsWith('image/')) {
const fileBlob = await clipboardItem.getType(type);
// // 获取读取的文件内容,即 Blob 对象
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
let { url, name } = await uploadImage(fileBlob);
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
// const img = document.createElement('img');
// img.src = URL.createObjectURL(blob);
// document.body.appendChild(img);
// console.log( URL.createObjectURL(blob));
}
}
}
} catch (error) {
console.error('无法读取剪贴板中的图片:', error);
}
}
function createInputs(inputData) {
// Assuming you have an HTML element with the id "container" to hold the UI
const container = document.createElement("div");
@@ -288,18 +395,40 @@
nameLabel.textContent = data.title || "LoadImage: ";
uploadContainer.appendChild(nameLabel);
let actionDiv = document.createElement('div');
// Create an input field for the image name
const nameInput = document.createElement("input");
nameInput.type = "file";
nameInput.style = `width: 88px;`
uploadContainer.appendChild(nameInput);
const uploadImageInput = document.createElement("button");
uploadImageInput.style = `width: 88px;`;
uploadImageInput.innerText = 'upload'
const uploadImageInputHide = document.createElement('input');
uploadImageInputHide.type = "file";
uploadImageInputHide.style.display = "none"
actionDiv.appendChild(uploadImageInput);
actionDiv.appendChild(uploadImageInputHide);
const btnFromClipboard = document.createElement("button");
btnFromClipboard.style = `width: 156px;
height: 24px;
margin-left: 18px;`
btnFromClipboard.innerText = 'paste from clipboard'
actionDiv.appendChild(btnFromClipboard);
uploadContainer.appendChild(actionDiv)
// Create an image element to display the uploaded image
const imageElement = document.createElement("img");
imageElement.src = base64Df
imageElement.style.maxWidth='200px'
imageElement.style.maxWidth = '200px';
nameInput.addEventListener('change', (event) => {
btnFromClipboard.addEventListener('click', (event) => handleClipboardImage(imageElement, data));
uploadImageInput.addEventListener('click', (event) => {
uploadImageInputHide.click()
})
uploadImageInputHide.addEventListener('change', (event) => {
// 获取用户选择的文件
const file = event.target.files[0];
@@ -311,10 +440,16 @@
reader.onloadend = async function () {
// 获取读取的文件内容,即 Blob 对象
const fileBlob = new Blob([reader.result], { type: file.type });
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
let { url, name } = await uploadImage(fileBlob)
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
};
@@ -332,7 +467,7 @@
container.appendChild(uploadContainer);
}
if (data.class_type === 'FloatSlider') {
if (['FloatSlider', 'IntNumber'].includes(data.class_type)) {
// 滑块输入
let silde = createFloatSlide(data.title, data.inputs.number, (v) => {
window._appData.data[data.id].inputs.number = v;
@@ -341,7 +476,7 @@
}
// Check if the class_type is "CLIPTextEncode"
if (data.class_type === "CLIPTextEncode") {
if (["TextInput_", "CLIPTextEncode"].includes(data.class_type)) {
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
@@ -352,15 +487,20 @@
uploadContainer.appendChild(nameLabel);
// Create an input field for the image name
const nameInput = document.createElement("textarea");
// nameInput.type = "text";
nameInput.value = data.inputs.text;
uploadContainer.appendChild(nameInput);
const textInput = document.createElement("textarea");
// uploadImageInput.type = "text";
textInput.value = data.inputs.text;
uploadContainer.appendChild(textInput);
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
nameInput.addEventListener('input', (event) => {
console.log(nameInput.value)
window._appData.data[data.id].inputs.text = nameInput.value;
textInput.addEventListener('input', (event) => {
// console.log(textInput.value)
autoResize(textInput);
window._appData.data[data.id].inputs.text = textInput.value;
})
// Append the upload container to the main container
@@ -368,6 +508,29 @@
}
if (["CheckpointLoaderSimple", "LoraLoader"].includes(data.class_type)) {
let value = data.inputs.ckpt_name || data.inputs.lora_name;
let [div, selectDom] = createSelectWithOptions(data.title, Array.from(data.options, o => {
return {
value: o,
text: o
}
}), value);
selectDom.addEventListener('change', e => {
e.preventDefault();
// console.log(selectDom.value)
if (data.class_type === 'CheckpointLoaderSimple') {
window._appData.data[data.id].inputs.ckpt_name = selectDom.value;
}
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = selectDom.value;
}
})
container.appendChild(div);
}
});
@@ -381,8 +544,8 @@
slider.type = "range";
slider.min = minValue;
slider.max = maxValue;
slider.step=0.01
slider.value=value;
slider.step = 0.01
slider.value = value;
// 创建标签元素
var label = document.createElement("label");
@@ -392,7 +555,7 @@
var container = document.createElement("div");
container.appendChild(label);
container.appendChild(slider);
container.className='card'
container.className = 'card'
// 添加change事件监听器
slider.addEventListener("change", function (event) {
@@ -407,6 +570,34 @@
}
function createSelectWithOptions(title, options, defaultValue) {
const div = document.createElement("div");
div.className = 'card';
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = title;
div.appendChild(nameLabel);
var selectElement = document.createElement("select");
selectElement.className = "select"
// 循环遍历选项数组
for (var i = 0; i < options.length; i++) {
var option = document.createElement("option");
option.value = options[i].value;
option.text = options[i].text;
selectElement.appendChild(option);
}
// 设置默认值
selectElement.value = defaultValue;
div.appendChild(selectElement)
return [div, selectElement];
}
function getTypeFromUrl(url) {
const queryString = url.split('?')[1];
@@ -433,10 +624,11 @@
leftDiv.style.alignItems = 'flex-start';
rightDiv.className = 'panel'
leftDiv.style.flex = 0.4
rightDiv.style.flex = 0.6
// rightDiv.style=`position: fixed;
// right: 0;
// top: 12px;flex:0.6`
rightDiv.style.flex = 0.6;
// rightDiv.style.height='70vh'
// rightDiv.style=`position: fixed;
// right: 0;
// top: 12px;flex:0.6`
// 创建标题
var title = document.createElement('h1');
@@ -519,9 +711,26 @@
},
output: {
element: output,
update: function (url, id) {
console.log(url, id)
if (output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).src = url
update: function (type = "image", val, id) {
console.log(val, id)
if (type == "image" && output.querySelector(`#output_${id}`)) {
if (output.querySelector(`#output_${id} img`)) {
output.querySelector(`#output_${id} img`).src = val;
} else {
output.querySelector(`#output_${id}`).src = val;
}
}
if (type == "video" && output.querySelector(`#output_${id}`)) {
let video = output.querySelector(`#output_${id} video`);
let img = output.querySelector(`#output_${id} img`);
img.style.display = 'none';
video.style.display = 'block';
video.src = val;
}
if (type == "text" && output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).innerText = val;
}
},
@@ -615,9 +824,9 @@
});
const show = (src, id) => {
const show = (src, id, type = "image") => {
// console.log(src)
ui.output.update(src, id)
ui.output.update(type, src, id)
};
api.addEventListener("status", ({ detail }) => {
@@ -643,11 +852,23 @@
console.log("executed", detail)
// if (!enabled) return;
const images = detail?.output?.images;
if (!images) return;
const text = detail?.output?.text;
const gifs = detail?.output?.gifs;
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');
} else if (text && text[0]) {
ui.output.update("text", text[0], 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');
}
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);
try {
ui.status.update(`executed_#${detail.node}`);
@@ -675,7 +896,6 @@
const type = getTypeFromUrl(location.href);
if (type === 'new') {
createUploadJson();
} else {
appData = await get_my_app();
// console.log(appData)
+17 -1
View File
@@ -75,7 +75,23 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
if (data.hasOwnProperty(id)) {
if (inputIds.includes(id)) {
let node = app.graph.getNodeById(id)
input[inputIds.indexOf(id)] = { ...data[id], title: node.title, id }
let options = []
// 模型
try {
if (node.type === 'CheckpointLoaderSimple') {
options = node.widgets.filter(w => w.name === 'ckpt_name')[0]
.options.values
}else if(node.type === 'LoraLoader'){
options =node.widgets.filter(w=>w.name==='lora_name')[0].options.values
}
} catch (error) {}
input[inputIds.indexOf(id)] = {
...data[id],
title: node.title,
id,
options
}
// input.push()
}
if (outputIds.includes(id)) {
+3 -1
View File
@@ -406,7 +406,9 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
}
};
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
+63 -11
View File
@@ -156,8 +156,8 @@ const parseSvg = async svgContent => {
return { data, image: base64, svgElement }
}
async function setArea (cw, ch, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.6)
async function setArea (cw, ch, topBase64, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.8)
let div = document.createElement('div')
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
@@ -170,8 +170,13 @@ async function setArea (cw, ch, base64, data, fn) {
outline: 2px solid #eaeaea;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
pointer-events: none;'></div>
border: 2px dashed red;
pointer-events: none;
background-image: url("${topBase64}");
background-repeat: no-repeat;
background-size: cover;
'></div>
<div class="mx_close"> X </div>
</div>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
@@ -181,13 +186,25 @@ async function setArea (cw, ch, base64, data, fn) {
// canvas.height = ch
let img = div.querySelector('#ml_video')
let overlay = div.querySelector('#ml_overlay')
// let overlay = div.querySelector('#ml_overlay')
let selection = div.querySelector('#ml_selection')
let close = div.querySelector('.mx_close')
let startX, startY, endX, endY
let start = false
let setDone = false
// Set video source
img.src = base64
// canvas.toDataURL();
close.style = `cursor: pointer;
position: fixed;
left: 12px;
top: 12px;
z-index: 99999999;
background: black;
width: 44px;
height: 44px;
text-align: center;
line-height: 44px;`
// init area
// const data = getSetAreaData()
@@ -216,14 +233,37 @@ async function setArea (cw, ch, base64, data, fn) {
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
overlay.addEventListener('click', remove)
function remove () {
overlay.removeEventListener('click', remove)
const removeDiv = () => {
div.remove()
close.removeEventListener('click', removeDiv)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
div.remove()
img.removeEventListener('mousedown', setDoneCheck)
}
close.addEventListener('click', removeDiv)
const setDoneCheck = event => {
console.log(setDone)
if (setDone) {
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
setDone = false
start = false
startX = event.clientX
startY = event.clientY
}
}
img.addEventListener('mousedown', setDoneCheck)
function remove () {
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
setDone = true
// div.remove()
}
function startSelection (event) {
@@ -531,12 +571,24 @@ 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]
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 src = im.src
setArea(im.naturalWidth, im.naturalHeight, src, data, updateValue)
// let src = im.src
setArea(
im.naturalWidth,
im.naturalHeight,
topIm.src,
im.src,
data,
updateValue
)
} catch (error) {}
})
}
+110
View File
@@ -0,0 +1,110 @@
import { app } from '../../../scripts/app.js'
import { $el } from '../../../scripts/ui.js'
import { api } from '../../../scripts/api.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 12 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'row',
// alignItems: 'center',
justifyContent: 'flex-start'
}
}
app.registerExtension({
name: 'Mixlab.share.ShareToWeibo',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ShareToWeibo') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
// console.log(this)
const widget = {
type: 'div',
name: 'ShareToWeiboBtn',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width,
node.widgets[2].last_y +16,
node.size[1]
)
)
}
}
const style = `
flex-direction: row;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);`
widget.div = $el('div', {})
const btn = document.createElement('button')
btn.innerText = 'Share'
btn.style = style
btn.addEventListener('click', () => {
if (window._mixlab_share_to_weibo)
window.open(window._mixlab_share_to_weibo)
})
document.body.appendChild(widget.div)
widget.div.appendChild(btn)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
onExecuted?.apply(this, arguments)
// console.log(this.widgets)
window._mixlab_share_to_weibo = message.url
try {
const div = this.widgets.filter(w => w.div)[0].div
Array.from(
div.querySelectorAll('button'),
b => (b.style.background = 'yellow')
)
} catch (error) {}
}
}
}
})