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
25 changed files with 1124 additions and 2108 deletions
+1 -2
View File
@@ -2,5 +2,4 @@ __pycache__/
https/
nodes/config.json
workflow/my_workflow.json
workflow/my_workflow_app.json
app/*
workflow/my_workflow_app.json
+25 -47
View File
@@ -1,33 +1,32 @@
### Workflow-to-APP 🚀🚗🚚🏃
##
v0.6.0 🚀🚗🚚🏃‍ Workflow-to-APP
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- 支持多个web app 切换
- Support multiple web app switching.
- Add the AppInfo node, which allows you to transform the workflow into a web app by simple configuration.
![](./assets/0-m-app.png)
![](./assets/appinfo-readme.png)
Example:
- workflow
![APP info](./workflow/appinfo-workflow.svg)
[text-to-image](./workflow/Text-to-Image-app.json)
APP-JSON:
- [text-to-image](./example/text-to-image_1_Wed%20Dec%2027%202023.json)
- [image-to-image](./example/image-to-image_1_Wed%20Dec%2027%202023.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、IntNumber、CheckpointLoaderSimple、LoraLoader
> 暂时支持6种节点作为界面上的输入节点:Load Image、CLIPTextEncode、TextInput_、FloatSlider、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT
### 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! 💻🌐
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### 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! 💻🌐
>
![screenshare](./assets/screenshare.png)
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
@@ -38,7 +37,6 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
!! Please use the address with HTTPS (https://127.0.0.1).
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
@@ -47,23 +45,11 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
### GPT
> Support for calling multiple GPTs.ChatGPT、ChatGLM3 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
![gpt-workflow.svg](./assets/gpt-workflow.svg)
[workflow-5](./workflow/5-gpt-workflow.json)
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### 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.
![layers](./assets/layers-workflow.svg)
![poster](./assets/poster-workflow.svg)
### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
@@ -75,6 +61,13 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
> 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.
![layers](./assets/layers-workflow.svg)
![poster](./assets/poster-workflow.svg)
## Utils
> The Color node provides a color picker for easy color selection, the Font node offers built-in font selection for use with TextImage to generate text images, and the DynamicDelayByText node allows delayed execution based on the length of the input text.
@@ -114,10 +107,6 @@ Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
> LaMaInpainting
from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainting)
### Improvement
@@ -131,26 +120,14 @@ An improvement has been made to directly redirect to GitHub to search for missin
![node-not-found](./assets/node-not-found.png)
### Update
v0.8.0 🚀🚗🚚🏃‍ LaMaInpainting
- 新增 LaMaInpainting
- 优化color节点的输出
- 修复高清显示屏上定位节点不准的情况
- Add LaMaInpainting
- Optimize the output of the color node
- Fix the issue of inaccurate positioning node on high-definition display screens
### Models
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : models/clipseg
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : models/lama
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : model/clipseg
<!-- ### Workflow
[Workflow](./workflow.md) -->
## Installation
manually install, simply clone the repo into the custom_nodes directory with this command:
@@ -184,6 +161,7 @@ pip3 install -r requirements.txt
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
#### Thanks:
[ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
+30 -103
View File
@@ -4,7 +4,7 @@ import subprocess
import importlib.util
import sys,json
import urllib
import hashlib
import datetime
@@ -79,13 +79,6 @@ install_openai()
current_path = os.path.abspath(os.path.dirname(__file__))
def calculate_md5(string):
encoded_string = string.encode()
md5_hash = hashlib.md5(encoded_string).hexdigest()
return md5_hash
def create_key(key_p,crt_p):
import OpenSSL
# 生成自签名证书
@@ -169,75 +162,17 @@ def get_workflows():
workflows=read_workflow_json_files(workflow_path)
return workflows
def get_my_workflow_for_app(filename="my_workflow_app.json"):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
apps=[]
if filename==None:
data=read_workflow_json_files(app_path)
i=0
for item in data:
try:
x=item["data"]
if i==0:
apps.append({
"filename":item["filename"],
"data":x,
"date":item["date"]
})
else:
apps.append({
"filename":item["filename"],
"data":{
"app":{
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
}
},
"date":item["date"]
})
i+=1
except Exception as e:
print("发生异常:", str(e))
else:
app_workflow_path=os.path.join(app_path, filename)
# print('app_workflow_path: ',app_workflow_path)
try:
with open(app_workflow_path) as json_file:
apps = [{
'filename':filename,
'data':json.load(json_file)
}]
except Exception as e:
print("发生异常:", str(e))
if len(apps)==1:
data=read_workflow_json_files(app_path)
for item in data:
x=item["data"]
print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
apps.append({
"filename":item["filename"],
"data":{
"app":{
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
}
},
"date":item["date"]
})
return apps
def get_my_workflow_for_app():
# print("#####path::", current_path)
workflow_path=os.path.join(current_path, "workflow/my_workflow_app.json")
print('workflow_path: ',workflow_path)
json_data={}
try:
with open(workflow_path) as json_file:
json_data = json.load(json_file)
except:
print('-')
return json_data
def save_workflow_json(data):
workflow_path=os.path.join(current_path, "workflow/my_workflow.json")
@@ -245,22 +180,11 @@ def save_workflow_json(data):
json.dump(data, file)
return workflow_path
def save_workflow_for_app(data,filename="my_workflow_app.json"):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
app_workflow_path=os.path.join(app_path, filename)
try:
output_str = json.dumps(data['output'])
data['app']['id']=calculate_md5(output_str)
# id=data['app']['id']
except Exception as e:
print("发生异常:", str(e))
with open(app_workflow_path, 'w') as file:
def save_workflow_for_app(data):
workflow_path=os.path.join(current_path, "workflow/my_workflow_app.json")
with open(workflow_path, 'w') as file:
json.dump(data, file)
return filename
return workflow_path
def get_nodes_map():
# print("#####path::", current_path)
@@ -278,6 +202,8 @@ def get_nodes_map():
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
@@ -346,6 +272,10 @@ 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):
@@ -371,17 +301,14 @@ async def mixlab_workflow_hander(request):
'file_path':file_path
}
elif data['task']=='save_app':
file_path=save_workflow_for_app(data['data'],data['filename'])
file_path=save_workflow_for_app(data['data'])
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='my_app':
filename=None
if 'filename' in data:
filename=data['filename']
result={
'data':get_my_workflow_for_app(filename),
'data':get_my_workflow_for_app(),
'status':'success',
}
elif data['task']=='list':
@@ -438,14 +365,14 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,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,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,GetImageSize_,MultiplicationNode
from .nodes.Lama import LaMaInpainting
from .nodes.ShareNode import ShareToWeibo
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
@@ -492,7 +419,8 @@ NODE_CLASS_MAPPINGS = {
"GetImageSize_":GetImageSize_,
"SwitchByIndex":SwitchByIndex,
"LimitNumber":LimitNumber,
"LaMaInpainting":LaMaInpainting
"UploadImageForSMMS":UploadImageForSMMS,
"ShareToWeibo":ShareToWeibo
# "GamePal":GamePal
}
@@ -512,8 +440,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
"LaMaInpainting":"LaMaInpainting ♾️Mixlab"
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab"
# "GamePal":"GamePal ♾️Mixlab"
}
@@ -522,5 +449,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
WEB_DIRECTORY = "./web"
print('--------------')
print('\033[91m ### Mixlab Nodes: \033[93mLoaded\033[0m')
print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print('--------------')
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

+6 -2
View File
@@ -4793,14 +4793,18 @@
"SplitLongMask",
"SvgImage",
"TextImage",
"ResizeImageMixlab",
"TransparentImage",
"VAEDecodeConsistencyDecoder",
"VAELoaderConsistencyDecoder",
"TextToNumber",
"TextInput_",
"DynamicDelayProcessor",
"LaMaInpainting"
"MultiplicationNode",
"ShareToWeibo",
"LimitNumber",
"SwitchByIndex",
"UploadImageForSMMS",
"GetImageSize_"
],
{
"title_aux": "comfyui-mixlab-nodes"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -75,11 +75,11 @@ class ChatGPTNode:
"required": {
"api_key":("KEY", {"default": "", "multiline": True}),
"api_url":("URL", {"default": "", "multiline": True}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"prompt": ("STRING", {"multiline": True}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True,"dynamicPrompts": False
"multiline": True
}),
"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"}),
@@ -167,7 +167,7 @@ class ShowTextForGPT:
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True,"dynamicPrompts": False}),
"text": ("STRING", {"forceInput": True}),
}
}
@@ -189,8 +189,8 @@ class CharacterInText:
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"character": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"text": ("STRING", {"multiline": True}),
"character": ("STRING", {"multiline": True}),
"start_index": ("INT", {
"default": 1,
"min": 0, #Minimum value
+2 -2
View File
@@ -101,7 +101,7 @@ class CLIPSeg:
return {"required":
{
"image": ("IMAGE",),
"text": ("STRING", {"multiline": False,"dynamicPrompts": False}),
"text": ("STRING", {"multiline": False}),
},
"optional":
@@ -246,7 +246,7 @@ class CombineMasks:
# Resize heatmap and binary mask to match the original image dimensions
dimensions = (image_np.shape[1], image_np.shape[0])
# print('heatmap',heatmap)
print('heatmap',heatmap)
if dimensions is None or dimensions[0] == 0 or dimensions[1] == 0:
raise ValueError("Invalid dimensions")
+101 -10
View File
@@ -506,7 +506,6 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
y = 0
for line in lines:
for char in line:
#print('char',char)
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
@@ -515,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))+1) * (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)-1) * (font_size + spacing)) + font_size
height = (len(lines) * (font_size + spacing)) + spacing
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
@@ -561,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()
(
@@ -570,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)
@@ -587,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
@@ -991,8 +1026,8 @@ class TextImage:
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH,"dynamicPrompts": False}),
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲"}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH}),
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
@@ -1002,12 +1037,12 @@ class TextImage:
}),
"spacing": ("INT",{
"default":12,
"min": -200, #Minimum value
"min": 1, #Minimum value
"max": 200, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"text_color":("STRING",{"multiline": False,"default": "#000000","dynamicPrompts": False}),
"text_color":("STRING",{"multiline": False,"default": "#000000"}),
"vertical":("BOOLEAN", {"default": True},),
},
}
@@ -1677,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)
-85
View File
@@ -1,85 +0,0 @@
import os
import folder_paths
from simple_lama_inpainting import SimpleLama
from PIL import Image
import numpy as np
import torch
os.environ['LAMA_MODEL'] = os.path.join(folder_paths.models_dir, "lama/big-lama.pt")
if os.environ.get("LAMA_MODEL"):
model_path=os.environ.get("LAMA_MODEL")
if not os.path.exists(model_path):
os.environ['LAMA_MODEL']=''
raise FileNotFoundError(
f"lama torchscript model not found: {model_path}"
)
# 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)
# simple_lama = SimpleLama()
# img_path = "image.png"
# mask_path = "mask.png"
# image = Image.open(img_path)
# mask = Image.open(mask_path).convert('L')
# result = simple_lama(image, mask)
# result.save("inpainted.png")
class LaMaInpainting:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
global simple_lama
simple_lama = None
def run(self,image,mask):
global simple_lama
result=[]
if simple_lama==None:
simple_lama = SimpleLama()
else:
simple_lama.model.to("cuda" if torch.cuda.is_available() else "cpu")
for i in range(len(image)):
im=image[i]
ma=mask[i]
im=tensor2pil(im)
ma=tensor2pil(ma)
ma =ma.convert('L')
res = simple_lama(im, ma)
res=pil2tensor(res)
result.append(res)
# result.save("inpainted.png")
if simple_lama.device=='cuda':
simple_lama.model.to('cpu')
return (result,)
+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": ()}
+13 -16
View File
@@ -88,23 +88,18 @@ class ColorInput:
},
}
RETURN_TYPES = ("STRING","INT","INT","INT","FLOAT",)
RETURN_NAMES = ("hex","r","g","b","a",)
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,False,False,False,)
OUTPUT_IS_LIST = (False,False,)
def run(self,color):
h=color['hex']
r=color['r']
g=color['g']
b=color['b']
a=color['a']
return (h,r,g,b,a,)
return (color,)
@@ -374,14 +369,14 @@ class AppInfo:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"name": ("STRING",{"multiline": False,"default": "Mixlab-App","dynamicPrompts": False}),
"name": ("STRING",{"multiline": False,"default": "Mixlab-App"}),
"image": ("IMAGE",),
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"]),"dynamicPrompts": False}),
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"]),"dynamicPrompts": False}),
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"])}),
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"])}),
},
"optional":{
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
"description":("STRING",{"multiline": True,"default": ""}),
"version":("INT", {
"default": 1,
"min": 1,
@@ -389,7 +384,6 @@ class AppInfo:
"step": 1,
"display": "number"
}),
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
}
}
@@ -403,14 +397,15 @@ class AppInfo:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
OUTPUT_NODE = True
def run(self,name,image,input_ids,output_ids,description,version,share_prefix):
def run(self,name,image,input_ids,output_ids,description,version):
im=create_temp_file(image)
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix]}, "result": (image,)}
return {"ui": {"json": [name,im,input_ids,output_ids,description,version]}, "result": (image,)}
@@ -530,3 +525,5 @@ class LimitNumber:
return (nn,)
+1 -1
View File
@@ -4,4 +4,4 @@ watchdog
opencv-python-headless
matplotlib
openai
simple-lama-inpainting
# playwright
+74 -444
View File
@@ -13,77 +13,7 @@
margin-left: 5%;
}
.apps {
margin: 0 32px;
background: whitesmoke;
color: black;
padding: 12px;
cursor: pointer;
}
.apps .content {
display: flex;
flex-wrap: wrap;
}
.apps .card {
width: 200px;
margin: 12px;
display: flex;
flex-direction: row;
background: #f8f8f8;
cursor: pointer;
user-select: none;
}
.apps .selected {
box-shadow: 0px 0px 10px 10px #fbe9f0
}
.apps .card:hover {
box-shadow: 0px 0px 10px 10px #e9fbfa
}
.apps .card h5 {
font-size: 14px;
margin: 10px 0;
}
.apps .card p {
margin: 0;
padding: 0;
}
.apps .card img {
width: auto;
height: 100%;
margin: 0px;
}
.apps .card .item {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
}
.apps .card .icon {
width: 120px;
height: 120px;
overflow: hidden;
background: #e3e3e3;
display: flex;
justify-content: center;
align-items: center;
}
.apps .card .version {
font-size: 12px;
}
.status {
background: black;
@@ -112,24 +42,17 @@
display: flex;
flex-direction: column;
min-width: 400px;
/* background: #eee; */
background: #eee;
margin: 24px;
flex: 1;
align-items: center;
/* justify-content: center; */
}
.panel .header {
margin-bottom: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.panel h1 {
padding: 0 12px;
margin: 0;
margin-top: 12px;
margin-bottom: 0;
}
.panel img,
@@ -143,21 +66,15 @@
.input_card {
display: flex;
flex-direction: column;
width: 100%;
}
.output {
width: 90%;
margin: 12px;
}
.output_card {
height: 100%;
width: 100%;
margin-top: 24px;
box-shadow: 0px 0px 8px 3px #e6e7e7;
display: flex;
flex-wrap: wrap;
/* justify-content: center;
align-items: center; */
@@ -167,9 +84,6 @@
video {
max-width: 400px;
max-height: 600px;
margin: 8px;
min-width: 200px;
box-shadow: 0px 0px 20px 7px #e6e7e7;
}
.card {
@@ -211,7 +125,7 @@
border: 3px solid;
}
button:hover {
.run_btn:hover {
border-color: yellow;
color: yellow;
}
@@ -224,28 +138,28 @@
.upload_btn {
width: 188px;
cursor: pointer;
/* height: 188px; */
/* background: black; */
height: 188px;
background: black;
color: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 14px;
color: black;
margin-left: calc(50% - 94px);
margin-top: calc(30vh - 94px);
}
/* .upload_btn:hover {
.upload_btn:hover {
outline: 4px solid yellow;
color: yellow;
} */
}
.show_text {
font-size: 14px;
/* display: inline-block; */
margin: 8px;
/* margin: 13px; */
padding: 32px;
min-width: 200px;
background: #242424;
color: white;
}
@@ -256,39 +170,15 @@
font-size: 12px;
font-weight: 300;
}
label::after {
content: attr(data-content);
/* Set the initial content using the data-content attribute */
/* position: absolute; */
/* top: 100%;
left: 0; */
margin-left: 4px;
font-size: 12px;
color: #555;
}
select,
button,
input {
height: 32px;
cursor: pointer;
background: #000000bf;
color: white;
outline: none;
border: none;
border-radius: 4px;
}
</style>
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
</head>
<body>
<div style="margin: 0 24px;
margin-bottom: 24px;
padding: 8px;
color: #4a4a4a;
border-bottom: 1px dashed #595959;">Explore your creative potential with <a class="link"
<div style="margin: 24px;
background: #eee;
padding: 24px;
color: #4a4a4a;">Explore your creative potential with <a class="link"
href="https://github.com/shadowcz007/comfyui-mixlab-nodes" target="_blank">mixlab-nodes</a> / 尽情发挥你的创意
<br>
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a>
@@ -368,104 +258,46 @@
});
}
function success(isSuccess, btn, text) {
isSuccess ? btn.innerText = 'success' : text;
setTimeout(() => {
btn.innerText = text;
}, 5000)
}
async function get_my_app(filename = null) {
async function get_my_app() {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'my_app',
filename
task: 'my_app'
})
})
let result = await res.json();
let data = [];
try {
for (const res of result.data) {
let { output, app } = res.data;
if (app.filename) data.push({
...app,
data: output,
date: res.date
})
}
} catch (error) {
let { output, app } = result.data
return {
...app,
data: output
}
return data
}
function createOutputs(outputData) {
const container = document.createElement('div');
container.className = "output";
const action = document.createElement('div');
container.appendChild(action)
const copyHTML = document.createElement('button');
copyHTML.innerText = 'copy as html'
action.appendChild(copyHTML)
const copyImage = document.createElement('button');
copyImage.innerText = 'copy image'
action.appendChild(copyImage)
copyImage.style.marginLeft = '18px';
const copyText = document.createElement('button');
copyText.innerText = 'copy text for share'
action.appendChild(copyText)
copyText.style.marginLeft = '18px';
const output_card = document.createElement("div");
output_card.className = 'output_card'
container.appendChild(output_card)
copyText.addEventListener('click', e => {
e.preventDefault();
copyTextToClipboard((window._appData.share_prefix || '') + " " + output_card.outerHTML, (r) => success(r, copyText, 'copy text for share'))
})
copyImage.addEventListener('click', e => {
e.preventDefault();
// copyHtmlWithImagesToClipboard(output_card.outerHTML)
copyImagesToClipboard(output_card.outerHTML, (r) => success(r, copyImage, 'copy image'))
// copyTextToClipboard()
})
copyHTML.addEventListener('click', e => {
e.preventDefault();
copyHtmlWithImagesToClipboard((window._appData.share_prefix || '') + " " + output_card.outerHTML, (r) => success(r, copyHTML, 'copy as html'))
// copyImagesToClipboard(output_card.outerHTML)
})
// Array.from( window._appData.output,n=>n.id)
const container = document.createElement("div");
container.className = 'output_card'
for (const node of outputData) {
// console.log('output', node)
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 = Array.isArray(node.inputs.text) ? node.inputs.text[0] : node.inputs.text
output_card.appendChild(div);
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;
output_card.appendChild(img);
container.appendChild(img);
}
// video ,gif
@@ -481,7 +313,7 @@
v.appendChild(video);
v.appendChild(img);
output_card.appendChild(v);
container.appendChild(v);
}
}
@@ -530,134 +362,6 @@
}
}
function copyHtmlWithImagesToClipboard(data, cb) {
// 创建一个临时div元素
const tempDiv = document.createElement('div');
// 将HTML字符串赋值给div的innerHTML属性
tempDiv.innerHTML = data;
// 获取div中的所有图像元素
const images = tempDiv.getElementsByTagName('img');
// 遍历图像元素,并将图像数据转换为Base64编码
for (let i = 0; i < images.length; i++) {
const image = images[i];
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置canvas尺寸与图像尺寸相同
canvas.width = image.width;
canvas.height = image.height;
// 在canvas上绘制图像
context.drawImage(image, 0, 0);
// 将canvas转换为Base64编码
const imageData = canvas.toDataURL();
// 将Base64编码替换图像元素的src属性
image.src = imageData;
}
let richText = tempDiv.innerHTML;
// 创建一个新的Blob对象,并将富文本字符串作为数据传递进去
const blob = new Blob([richText], { type: 'text/html' });
// 创建一个ClipboardItem对象,并将Blob对象添加到其中
const clipboardItem = new ClipboardItem({ 'text/html': blob });
// 使用Clipboard API将内容复制到剪贴板
navigator.clipboard.write([clipboardItem])
.then(() => {
console.log('富文本已成功复制到剪贴板');
tempDiv.remove()
if (cb) cb(true)
})
.catch((error) => {
console.error('复制到剪贴板失败:', error);
tempDiv.remove()
if (cb) cb(false)
});
}
// const htmlWithImages = "<p>这是要复制的HTML内容</p><img src='data:image/png;base64,iVBORw0KG...'>"
// copyHtmlWithImagesToClipboard(htmlWithImages);
function copyImagesToClipboard(html, cb) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const images = tempDiv.querySelectorAll('img');
const promises = Array.from(images).map((image) => {
return new Promise((resolve) => {
const img = new Image();
img.src = image.src;
img.onload = () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
const clipboardItem = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([clipboardItem])
.then(() => {
resolve();
tempDiv.remove()
if (cb) cb(true)
})
.catch((error) => {
reject(error);
tempDiv.remove()
if (cb) cb(false)
});
});
};
});
});
Promise.all([...promises])
.then(() => {
console.log('所有图片已成功复制到剪贴板');
if (cb) cb(true)
tempDiv.remove()
})
.catch((error) => {
console.error('复制到剪贴板失败:', error);
if (cb) cb(false)
tempDiv.remove()
});
}
function copyTextToClipboard(html, cb) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const text = tempDiv.innerText;
const textData = new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) });
navigator.clipboard.write([textData])
.then(() => {
console.log('所有文本已成功复制到剪贴板', text);
if (cb) cb(true)
tempDiv.remove()
})
.catch((error) => {
console.error('复制到剪贴板失败:', error);
if (cb) cb(false)
tempDiv.remove()
});
}
// const htmlString = "<p>这是要复制的HTML内容</p><img src='url'><img src='url'>";
// copyImagesToClipboard(htmlString);
function createInputs(inputData) {
// Assuming you have an HTML element with the id "container" to hold the UI
const container = document.createElement("div");
@@ -689,7 +393,6 @@
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = data.title || "LoadImage: ";
nameLabel.style.marginBottom = '12px'
uploadContainer.appendChild(nameLabel);
let actionDiv = document.createElement('div');
@@ -705,7 +408,9 @@
actionDiv.appendChild(uploadImageInputHide);
const btnFromClipboard = document.createElement("button");
btnFromClipboard.style = `width: 156px; margin-left: 18px;`
btnFromClipboard.style = `width: 156px;
height: 24px;
margin-left: 18px;`
btnFromClipboard.innerText = 'paste from clipboard'
actionDiv.appendChild(btnFromClipboard);
@@ -764,14 +469,9 @@
if (['FloatSlider', 'IntNumber'].includes(data.class_type)) {
// 滑块输入
let silde = createNumSlide(data.title,
data.inputs.number,
(v) => {
window._appData.data[data.id].inputs.number = v;
},
0,
data.class_type === 'IntNumber' ? 6000 : 1,
data.class_type === 'IntNumber' ? 'int' : 'float')
let silde = createFloatSlide(data.title, data.inputs.number, (v) => {
window._appData.data[data.id].inputs.number = v;
})
container.appendChild(silde);
}
@@ -837,20 +537,19 @@
return container
}
function createNumSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1, type = 'float') {
function createFloatSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1) {
// 创建滑块输入元素
var slider = document.createElement("input");
slider.type = "range";
slider.min = minValue;
slider.max = maxValue;
slider.step = type == 'float' ? 0.01 : 1
slider.step = 0.01
slider.value = value;
// 创建标签元素
var label = document.createElement("label");
label.innerHTML = labelText;
label.setAttribute('data-content', value);
// 创建容器元素,并将滑块输入和标签添加到容器中
var container = document.createElement("div");
@@ -859,11 +558,9 @@
container.className = 'card'
// 添加change事件监听器
slider.addEventListener("input", function (event) {
slider.addEventListener("change", function (event) {
var value = event.target.value;
value = type == 'float' ? value : Math.round(value)
console.log("滑块输入的值为:" + value);
label.setAttribute('data-content', value);
// 在这里可以执行其他操作,根据需要进行相应的处理
callback && callback(value)
});
@@ -902,21 +599,21 @@
return [div, selectElement];
}
function getFilenameFromUrl(url) {
function getTypeFromUrl(url) {
const queryString = url.split('?')[1];
if (!queryString) {
return null;
}
const params = new URLSearchParams(queryString);
const filename = decodeURIComponent(params.get('filename'));
const type = params.get('type');
return filename;
return type;
}
function createUI(inputData, outputData, share = true) {
function createUI(inputData, outputData) {
let mainDiv = document.createElement('div');
let leftDiv = document.createElement('div');
@@ -934,26 +631,8 @@
// top: 12px;flex:0.6`
// 创建标题
let titleDiv = document.createElement('div');
titleDiv.className = 'header'
var title = document.createElement('h1');
title.textContent = 'My Application';
titleDiv.appendChild(title);
if (share) {
const shareBtn = document.createElement('button');
shareBtn.innerText = 'copy url';
shareBtn.addEventListener('click', e => {
e.preventDefault();
let url = `${get_url()}/mixlab/app?filename=${encodeURIComponent(window._appData.filename)}`;
copyTextToClipboard(url, success(e, shareBtn, 'copy url'));
})
titleDiv.appendChild(shareBtn);
}
let iconDes = document.createElement('div');
// 创建应用图标
@@ -984,7 +663,7 @@
submitButton.className = 'run_btn'
// 将所有UI元素添加到页面中
leftDiv.appendChild(titleDiv);
leftDiv.appendChild(title);
leftDiv.appendChild(iconDes);
// leftDiv.appendChild(des);
leftDiv.appendChild(status);
@@ -1071,16 +750,16 @@
});
}
},
}
};
}
function createUploadJson(detail) {
function createUploadJson() {
// 创建一个div元素
var div = document.createElement('div');
div.className = 'upload_btn card'
div.textContent = '上传并运行你的JSON文件';
div.className = 'upload_btn'
div.textContent = '点击上传JSON文件';
div.addEventListener('click', function () {
document.getElementById('jsonFileInput').click();
});
@@ -1096,11 +775,10 @@
reader.onload = function (e) {
var contents = e.target.result;
var jsonData = JSON.parse(contents);
Array.from(detail.querySelectorAll('.card'), c => c.classList.remove('selected'));
div.className = 'upload_btn card selected'
setTimeout(() => {
div.remove();
input.remove();
}, 500);
let { output, app } = jsonData;
@@ -1108,8 +786,8 @@
...app,
data: output
};
if (document.body.querySelector('.app')) document.body.querySelector('.app').remove()
createApp(window._appData, false);
createApp(window._appData);
// res(jsonData);
};
@@ -1117,16 +795,16 @@
});
// 将div和input元素添加到body中
// document.body.appendChild(div);
document.body.appendChild(div);
document.body.appendChild(input);
return div
}
async function createApp(appData, share = true) {
console.log(appData)
async function createApp(appData) {
// 使用示例:
var ui = createUI(appData.input, appData.output, share);
var ui = createUI(appData.input, appData.output);
// 更新标题
ui.title.update(appData.name || 'Mixlab APP');
@@ -1181,8 +859,8 @@
// 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) {
ui.output.update("text", Array.isArray(text) ? text[0] : text, detail.node)
} 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)
@@ -1208,72 +886,24 @@
api.api_base = ""
api.init();
}
// 创建app的选择菜单
function createAppList(apps = []) {
let details = document.createElement('details');
details.className = 'apps';
details.innerHTML = `<summary>ComfyUI APP Store / ${apps.length}</summary>
<div class="content"> </div>`
let div = details.querySelector('div');
for (let index = 0; index < apps.length; index++) {
const app = apps[index];
let d = document.createDocumentFragment();
let dd = document.createElement('div');
d.appendChild(dd);
dd.className = 'card' + (index == 0 ? ' selected' : '')
dd.innerHTML = `
<div class="item icon">
<img src="${app.icon || base64Df}"/>
</div>
<div class="item" style="margin-left: 24px;">
<div>
<h5>${app.name}</h5>
<p>${app.description}</p>
</div>
<div >
<p class="version">version: ${app.version}</p>
</div>
</div>
`
div.appendChild(d);
dd.addEventListener('click', async e => {
e.preventDefault();
Array.from(div.querySelectorAll('.card'), c => c.classList.remove('selected'));
dd.className = 'card selected'
// console.log(app.filename)
window._appData = (await get_my_app(app.filename))[0];
if (document.body.querySelector('.app')) document.body.querySelector('.app').remove()
createApp(window._appData);
})
// console.log(div)
};
let uploadApp = createUploadJson(details);
div.appendChild(uploadApp);
document.body.appendChild(details);
}
async function init_app() {
const filename = getFilenameFromUrl(location.href);
window._apps = await get_my_app(filename);
let appData = {}
window._appData = window._apps[0];
const type = getTypeFromUrl(location.href);
if (type === 'new') {
createUploadJson();
} else {
appData = await get_my_app();
// console.log(appData)
window._appData = appData;
createAppList(window._apps);
createApp(appData);
}
createApp(window._appData);
};
+15 -23
View File
@@ -81,9 +81,8 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
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
}else if(node.type === 'LoraLoader'){
options =node.widgets.filter(w=>w.name==='lora_name')[0].options.values
}
} catch (error) {}
@@ -120,8 +119,7 @@ async function save_app (json) {
method: 'POST',
body: JSON.stringify({
data: json,
task: 'save_app',
filename: json.app.filename
task: 'save_app'
})
})
return await res.json()
@@ -146,7 +144,6 @@ function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
async function save (json, download = false) {
const name = json[0],
version = json[5],
share_prefix = json[6], //用于分享的功能扩展
description = json[4],
inputIds = json[2].split('\n').filter(f => f),
outputIds = json[3].split('\n').filter(f => f)
@@ -173,9 +170,7 @@ async function save (json, download = false) {
description,
version,
input,
output,
share_prefix,
filename: `${name}_${version}_${new Date().toDateString()}.json`
output
}
try {
@@ -185,15 +180,14 @@ async function save (json, download = false) {
// let http_workflow = app.graph.serialize()
if (download) {
await save_app(data)
await downloadJsonFile(data, data.app.filename)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(data.app.filename)}`
await downloadJsonFile(
data,
`${data.app.name}_${data.app.version}_${new Date().toDateString()}.json`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(data.app.filename)}`
)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?type=new`
)
if (open) window.open(`${getUrl()}/mixlab/app?type=new`)
} else {
await save_app(data)
@@ -214,7 +208,7 @@ app.registerExtension({
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('#orig_nodeCreated', this)
// console.log(this)
const widget = {
type: 'div',
name: 'AppInfoRun',
@@ -224,7 +218,7 @@ app.registerExtension({
get_position_style(
ctx,
widget_width,
node.size[1] - widget_height,
node.widgets[4].last_y + 24,
node.size[1]
)
)
@@ -242,7 +236,7 @@ app.registerExtension({
widget.div = $el('div', {})
const btn = document.createElement('button')
btn.innerText = 'Save & Open'
btn.innerText = 'Save For App'
btn.style = style
btn.addEventListener('click', () => {
@@ -252,7 +246,6 @@ app.registerExtension({
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
this.widgets.filter(w => w.name === 'version')[0].value += 1
}
})
@@ -268,7 +261,6 @@ app.registerExtension({
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
this.widgets.filter(w => w.name === 'version')[0].value += 1
}
})
@@ -290,7 +282,7 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
onExecuted?.apply(this, arguments)
console.log(message.json)
// console.log(this.widgets)
window._mixlab_app_json = message.json
try {
+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.8.0'
const version = 'v0.6.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+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) {}
}
}
}
})
+5 -20
View File
@@ -4,7 +4,6 @@ import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import { closeIcon } from './svg_icons.js'
import {
GroupNodeConfig,
GroupNodeHandler
@@ -668,23 +667,10 @@ app.registerExtension({
...options
] // and return the options
}
LGraphCanvas.prototype.centerOnNode = function(node) {
var dpr = window.devicePixelRatio || 1; // 获取设备像素比
this.ds.offset[0] =
-node.pos[0] -
node.size[0] * 0.5 +
(this.canvas.width * 0.5) / (this.ds.scale * dpr); // 考虑设备像素比
this.ds.offset[1] =
-node.pos[1] -
node.size[1] * 0.5 +
(this.canvas.height * 0.5) / (this.ds.scale * dpr); // 考虑设备像素比
this.setDirty(true, true);
};
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
@@ -728,7 +714,7 @@ app.registerExtension({
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.style.fontSize = '12px'
textB.style.fontSize='12px';
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
btnB.style = `float: right; border: none; color: var(--input-text);
@@ -804,12 +790,11 @@ app.registerExtension({
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
if (nodesMap[n]) {
const { url, title: _title } = nodesMap[n]
let title = app.graph.getNodeById(nodeId).title || _title
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
@@ -825,10 +810,10 @@ app.registerExtension({
})
d.innerHTML = `
<span>${'#' + nodeId} ${title}</span>
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = n
d.title = title
nodesDiv.appendChild(d)
}
+10 -34
View File
@@ -2,7 +2,7 @@ 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 { addValueControlWidget } from '../../../scripts/widgets.js'
import { addValueControlWidget } from "../../../scripts/widgets.js";
const getLocalData = key => {
let data = {}
@@ -45,21 +45,6 @@ function get_position_style (ctx, widget_width, y, node_height) {
}
}
function hexToRGBA (hexColor) {
var hex = hexColor.replace('#', '')
var r = parseInt(hex.substring(0, 2), 16)
var g = parseInt(hex.substring(2, 4), 16)
var b = parseInt(hex.substring(4, 6), 16)
// 获取透明度的十六进制值
var alphaHex = hex.substring(6)
// 将透明度的十六进制值转换为十进制值
var alpha = parseInt(alphaHex, 16) / 255
return [r,g,b,alpha]
}
app.registerExtension({
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
@@ -75,16 +60,8 @@ app.registerExtension({
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color');
let hex=data[node.id] || '#000000'
let [r,g,b,a]=hexToRGBA(hex)
return {
hex,
r,
g,
b,
a
}
let data = getLocalData('_mixlab_utils_color')
return data[node.id] || '#000000'
}
}
// widget.something = something; // maybe adds stuff to it
@@ -132,7 +109,7 @@ app.registerExtension({
ip.style = `outline: none;
border: none;
padding: 4px;
width: 70%;cursor: pointer;
width: 100%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
@@ -187,17 +164,16 @@ app.registerExtension({
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TextToNumber') {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const random_number = this.widgets.filter(
w => w.name === 'random_number'
)[0]
if (random_number.value === 'enable') {
const n = this.widgets.filter(w => w.name === 'number')[0]
n.value = message.num[0]
const random_number=this.widgets.filter(w=>w.name==='random_number')[0]
if(random_number.value==='enable'){
const n=this.widgets.filter(w=>w.name==='number')[0]
n.value=message.num[0]
}
console.log('TextToNumber', random_number.value)
}
}
+685 -593
View File
File diff suppressed because it is too large Load Diff
-717
View File
@@ -1,717 +0,0 @@
{
"last_node_id": 25,
"last_link_id": 32,
"nodes": [
{
"id": 5,
"type": "CLIPTextEncode",
"pos": [
1029,
-2149
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 6
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
3
],
"slot_index": 0
}
],
"title": "负向prompt",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 6,
"type": "VAEDecode",
"pos": [
1867,
-2378
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 15,
"type": "EnhanceImage",
"pos": [
2591,
-2531
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 25
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
18
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EnhanceImage"
},
"widgets_values": [
1.1
]
},
{
"id": 21,
"type": "EmptyLatentImage",
"pos": [
998,
-2674
],
"size": [
315,
106
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 30,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 32,
"widget": {
"name": "height"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
26
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 24,
"type": "LimitNumber",
"pos": [
665,
-2958
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "number",
"type": "*",
"link": 29
}
],
"outputs": [
{
"name": "number",
"type": "*",
"links": [
30
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LimitNumber"
},
"widgets_values": [
512,
4089
]
},
{
"id": 25,
"type": "LimitNumber",
"pos": [
648,
-2659
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "number",
"type": "*",
"link": 31
}
],
"outputs": [
{
"name": "number",
"type": "*",
"links": [
32
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LimitNumber"
},
"widgets_values": [
512,
4089
]
},
{
"id": 22,
"type": "IntNumber",
"pos": [
302,
-2758
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"title": "Width",
"properties": {
"Node name for S&R": "IntNumber"
},
"widgets_values": [
512
]
},
{
"id": 23,
"type": "IntNumber",
"pos": [
299,
-2601
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
31
],
"shape": 3,
"slot_index": 0
}
],
"title": "Height",
"properties": {
"Node name for S&R": "IntNumber"
},
"widgets_values": [
512
]
},
{
"id": 2,
"type": "CheckpointLoaderSimple",
"pos": [
567,
-2422
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
5,
6
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"title": "Model",
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 1,
"type": "KSampler",
"pos": [
1509,
-2394
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 2
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 3
},
{
"name": "latent_image",
"type": "LATENT",
"link": 26,
"slot_index": 3
},
{
"name": "denoise",
"type": "FLOAT",
"link": 16,
"widget": {
"name": "denoise"
},
"slot_index": 4
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
730250870715434,
"randomize",
15,
6.9,
"euler",
"karras",
0.59
]
},
{
"id": 14,
"type": "FloatSlider",
"pos": [
1007,
-2819
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "FLOAT",
"type": "FLOAT",
"links": [
16
],
"shape": 3,
"slot_index": 0
}
],
"title": "denoise",
"properties": {
"Node name for S&R": "FloatSlider"
},
"widgets_values": [
1
]
},
{
"id": 16,
"type": "PreviewImage",
"pos": [
2949,
-2615
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 4,
"type": "CLIPTextEncode",
"pos": [
1022,
-2375
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
2
],
"slot_index": 0
}
],
"title": "prompt",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"superman,fat cat"
]
},
{
"id": 7,
"type": "AppInfo",
"pos": [
2134,
-2528
],
"size": {
"0": 408.4201965332031,
"1": 406.9195861816406
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 9
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "AppInfo"
},
"widgets_values": [
"Text-to-Image",
"4\n22\n23\n2\n\n\n",
"16",
"演示基本的文生图流程",
1,
"#comfyui-mixlab-nodes# ",
null
]
}
],
"links": [
[
1,
2,
0,
1,
0,
"MODEL"
],
[
2,
4,
0,
1,
1,
"CONDITIONING"
],
[
3,
5,
0,
1,
2,
"CONDITIONING"
],
[
5,
2,
1,
4,
0,
"CLIP"
],
[
6,
2,
1,
5,
0,
"CLIP"
],
[
7,
1,
0,
6,
0,
"LATENT"
],
[
8,
2,
2,
6,
1,
"VAE"
],
[
9,
6,
0,
7,
0,
"IMAGE"
],
[
16,
14,
0,
1,
4,
"FLOAT"
],
[
18,
15,
0,
16,
0,
"IMAGE"
],
[
25,
7,
0,
15,
0,
"IMAGE"
],
[
26,
21,
0,
1,
3,
"LATENT"
],
[
29,
22,
0,
24,
0,
"*"
],
[
30,
24,
0,
21,
0,
"INT"
],
[
31,
23,
0,
25,
0,
"*"
],
[
32,
25,
0,
21,
1,
"INT"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB