Compare commits

...
Author SHA1 Message Date
shadowcz007 2d336afb85 v0.6.0 2023-12-27 16:29:00 +08:00
shadowcz007 4d309435c8 Update index.html 2023-12-26 17:15:33 +08:00
shadowcz007 099ce9cdfd 1 2023-12-26 16:32:01 +08:00
shadowcz007 8914e60cb8 初步打通 2023-12-26 16:23:43 +08:00
shadowcz007 dbd30a40e9 init 2023-12-26 12:06:55 +08:00
shadowcz007 c9a598fd59 更新下workflow示例 2023-12-26 10:56:14 +08:00
shadowcz007 e331e588cf v0.5.2
The bug of missing texture mapping for 3D nodes has been fixed.
2023-12-25 22:28:53 +08:00
shadowcz007 2011557771 fixbug 2023-12-25 22:24:56 +08:00
shadowcz007 f0ba45d14e GLB can export 2023-12-25 09:14:39 +08:00
shadowcz007 8352a521b7 v0.5.1 2023-12-24 23:07:01 +08:00
shadowcz007 aa3d4d79f8 fixbug 2023-12-24 23:04:16 +08:00
shadowcz007 4f650d760c fixbug-mergeLayer的多图片支持 2023-12-24 22:57:43 +08:00
shadowcz007 ea4b792627 v0.5.0 2023-12-24 11:16:34 +08:00
shadow 883605239a Merge pull request #75 from shadowcz007/v0.5_delay_node
V0.5 delay node
2023-12-24 10:57:21 +08:00
shadowcz007 5b8cab920c 增加示例 2023-12-24 10:56:56 +08:00
shadowcz007 8d3d327335 Update Utils.py 2023-12-24 10:53:05 +08:00
shadowcz007 32574050c4 增加从语音识别发送到chatgpt的方法 2023-12-24 10:44:25 +08:00
shadowcz007 8d45a90d9b Update Utils.py 2023-12-24 09:31:42 +08:00
gold3bear f66862a422 update DynamicDelayProcessor 2023-12-24 00:06:40 +08:00
shadowcz007 6a56be3a9b clone group & save to templete 2023-12-23 23:35:04 +08:00
gold3bear ebf6395de2 delay by text processor 2023-12-23 23:16:56 +08:00
shadowcz007 5df9fbf50d 图层支持视频合成(多image 2023-12-23 17:06:04 +08:00
shadowcz007 ff961155c9 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-23 15:59:20 +08:00
shadowcz007 14838d06a8 增加noise_image节点 2023-12-23 15:59:16 +08:00
shadow 99def24dd8 Merge pull request #73 from shadowcz007/v0.5-GamePal
支持换行的textimage
2023-12-23 14:13:31 +08:00
shadow a137a23b48 Merge pull request #72 from shadowcz007/v0.5-GamePal
TextToNumber&audio input control
2023-12-23 13:16:08 +08:00
25 changed files with 6468 additions and 288 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
__pycache__/
https/
nodes/config.json
workflow/my_workflow.json
workflow/my_workflow.json
workflow/my_workflow_app.json
+20 -5
View File
@@ -1,8 +1,16 @@
##
v0.4.0 🚀🚗🚚🏃‍
- Add "help" option to the context menu for each node.
- Add "find the node" option to the global context menu.
- Optimize the 3D Image node and add workflow.
v0.6.0 🚀🚗🚚🏃‍ Workflow-to-APP
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- Add the AppInfo node, which allows you to transform the workflow into a web app by simple configuration.
![](./assets/appinfo-readme.png)
Example:
- workflow
![APP info](./workflow/appinfo-workflow.svg)
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)
### 3D
![](./assets/3dimage.png)
@@ -51,6 +59,13 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
![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.
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
## Other Nodes
![main](./assets/all-workflow.svg)
@@ -87,7 +102,7 @@ Add edges to an image.
### Improvement
- Add "help" option to the context menu for each node.
- Add "find the node" option to the global context menu.
- Add "Nodes Map" option to the global context menu.
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
+52 -4
View File
@@ -162,12 +162,29 @@ def get_workflows():
workflows=read_workflow_json_files(workflow_path)
return workflows
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")
with open(workflow_path, 'w') as file:
json.dump(data, file)
return workflow_path
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 workflow_path
def get_nodes_map():
# print("#####path::", current_path)
@@ -253,6 +270,18 @@ async def mixlab_hander(request):
print(e)
return web.json_response(data)
@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:
html_data = f.read()
return web.Response(text=html_data, content_type='text/html')
else:
return web.Response(text="HTML file not found", status=404)
@routes.post('/mixlab/workflow')
async def mixlab_workflow_hander(request):
data = await request.json()
@@ -265,6 +294,17 @@ async def mixlab_workflow_hander(request):
'status':'success',
'file_path':file_path
}
elif data['task']=='save_app':
file_path=save_workflow_for_app(data['data'])
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='my_app':
result={
'data':get_my_workflow_for_app(),
'status':'success',
}
elif data['task']=='list':
result={
'data':get_workflows(),
@@ -289,6 +329,7 @@ async def nodes_map_hander(request):
return web.json_response(result)
# 把插件自定义的路由添加到comfyui server里
def new_add_routes(self):
import nodes
self.app.add_routes(routes)
@@ -318,19 +359,21 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,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 ColorInput,FontInput,TextToNumber
from .nodes.Utils import AppInfo,FloatSlider,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
NODE_CLASS_MAPPINGS = {
"AppInfo":AppInfo,
"RandomPrompt":RandomPrompt,
"NoiseImage":NoiseImage,
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
@@ -359,14 +402,17 @@ NODE_CLASS_MAPPINGS = {
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"FloatSlider":FloatSlider,
"Font":FontInput,
"TextToNumber":TextToNumber
"TextToNumber":TextToNumber,
"DynamicDelayProcessor":DynamicDelayProcessor
# "GamePal":GamePal
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"ResizeImageMixlab":"ResizeImage",
"AppInfo":"AppInfo ♾️Mixlab",
"ResizeImageMixlab":"ResizeImage ♾️Mixlab",
"RandomPrompt": "Random Prompt ♾️Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
@@ -379,6 +425,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab"
# "GamePal":"GamePal ♾️Mixlab"
}
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.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 9.9 MiB

+7 -5
View File
@@ -4762,15 +4762,15 @@
"https://github.com/shadowcz007/comfyui-mixlab-nodes": [
[
"3DImage",
"AppInfo",
"ResizeImage",
"NoiseImage",
"AreaToMask",
"CLIPSeg",
"CLIPSeg_",
"CharacterInText",
"ChatGPTOpenAI",
"Color",
"CombineMasks_",
"CombineSegMasks",
"EmptyLayer",
"EnhanceImage",
"FaceToMask",
"FeatheredMask",
@@ -4792,10 +4792,12 @@
"TextImage",
"TransparentImage",
"VAEDecodeConsistencyDecoder",
"VAELoaderConsistencyDecoder"
"VAELoaderConsistencyDecoder",
"TextToNumber",
"DynamicDelayProcessor"
],
{
"title_aux": "comfyui-mixlab-nodes [WIP]"
"title_aux": "comfyui-mixlab-nodes"
}
],
"https://github.com/shiimizu/ComfyUI_smZNodes": [
+182 -56
View File
@@ -2,7 +2,7 @@ import numpy as np
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
import base64,os
import base64,os,random
from io import BytesIO
import folder_paths
import json,io
@@ -235,7 +235,30 @@ def get_images_filepath(f,white_bg=False):
return images
# 创建噪声图像
def create_noisy_image(width, height, mode="RGB", noise_level=128):
# 创建空白图像
image = Image.new(mode, (width, height))
# 遍历每个像素,并随机设置像素值
pixels = image.load()
for i in range(width):
for j in range(height):
# 随机生成噪声值
noise_r = random.randint(-noise_level, noise_level)
noise_g = random.randint(-noise_level, noise_level)
noise_b = random.randint(-noise_level, noise_level)
# 像素值加上噪声值,并限制在0-255的范围内
r = max(0, min(pixels[i, j][0] + noise_r, 255))
g = max(0, min(pixels[i, j][1] + noise_g, 255))
b = max(0, min(pixels[i, j][2] + noise_b, 255))
# 设置像素值
pixels[i, j] = (r, g, b)
image=image.convert(mode)
return image
# 对轮廓进行平滑
@@ -458,7 +481,7 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
y += font_size + spacing
x += font_size + spacing
y = 0
print(char_coordinates)
# print(char_coordinates)
else:
x = 0
y = 0
@@ -972,7 +995,7 @@ class TextImage:
def run(self,text,font_path,font_size,spacing,text_color,vertical):
text_list=list(text)
# text_list=list(text)
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,spacing)
@@ -1037,16 +1060,22 @@ class Image3D:
def run(self,upload,material=None):
# print('material',material)
# print(upload['image'])
# print(upload )
image = base64_to_image(upload['image'])
mat=base64_to_image(upload['material'])
mat=None
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
mask = image.split()[3]
image=image.convert('RGB')
mat=mat.convert('RGB')
mask=mask.convert('L')
bg_image=None
if upload['bg_image']:
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
@@ -1054,8 +1083,7 @@ class Image3D:
mask=pil2tensor(mask)
image=pil2tensor(image)
mat=pil2tensor(mat)
m=[]
if not material is None:
m=create_temp_file(material[0])
@@ -1359,8 +1387,9 @@ class MergeLayers:
def INPUT_TYPES(s):
return {"required": {
"layers": ("LAYER",),
"image": ("IMAGE",),
"images": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
@@ -1373,59 +1402,148 @@ class MergeLayers:
INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (False,)
def run(self,layers,image):
# print(len(layers),len(image))
bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
def run(self,layers,images):
mask=base64_to_image(mask)
mask=mask.convert('L')
bg_images=[]
masks=[]
# print(len(images),images[0].shape)
# 1 torch.Size([2, 512, 512, 3])
# 4 torch.Size([1, 1024, 768, 3])
for img in images:
for bg_image in img:
# bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
bg_images.append(bg_image)
masks.append(mask)
return (bg_image,mask,)
bg_images=torch.cat(bg_images, dim=0)
masks=torch.cat(masks, dim=0)
return (bg_images,masks,)
class NoiseImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"width": ("INT",{
"default": 512,
"min": 1, # 最小值
"max": 8192, # 最大值
"step": 1, # 间隔
"display": "number" # 控件类型: 输入框 number、滑块 slider
}),
"height": ("INT",{
"default": 512,
"min": 1,
"max": 8192,
"step": 1,
"display": "number"
}),
"noise_level": ("INT",{
"default": 128,
"min": 0,
"max": 8192,
"step": 1,
"display": "slider"
}),
},
}
# 输出的数据类型
RETURN_TYPES = ("IMAGE",)
# 运行时方法名称
FUNCTION = "run"
# 右键菜单目录
CATEGORY = "♾️Mixlab/image"
# 输入是否为列表
INPUT_IS_LIST = False
# 输出是否为列表
OUTPUT_IS_LIST = (False,)
def run(self,width,height,noise_level):
# 创建噪声图像
im=create_noisy_image(width,height,"RGB",noise_level)
#获取临时目录:temp
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('tmp_', output_dir)
image_file = f"{filename}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
# 保存图片
im.save(image_path,compress_level=6)
# 把PIL数据类型转为tensor
im=pil2tensor(im)
# 定义ui字段,数据将回传到web前端的 nodeType.prototype.onExecuted
# result是节点的输出
return {"ui":{"images": [{
"filename": image_file,
"subfolder": subfolder,
"type":"temp"
}]},"result": (im,)}
class ResizeImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"width": ("INT",{
"default": 512,
"min": 1, #Minimum value
@@ -1443,6 +1561,10 @@ class ResizeImage:
"scale_option": (["width","height",'overall'],),
},
"optional":{
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
@@ -1454,16 +1576,20 @@ class ResizeImage:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,)
def run(self,image,width,height,scale_option):
def run(self,width,height,scale_option,image=None):
w=width[0]
h=height[0]
scale_option=scale_option[0]
im=image[0]
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=im.convert('RGB')
if image==None:
im=create_noisy_image(w,h,"RGB")
else:
im=image[0]
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=im.convert('RGB')
im=pil2tensor(im)
return (im,)
+216 -3
View File
@@ -1,9 +1,53 @@
import os
import re,random
from PIL import Image
import numpy as np
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
import folder_paths
import matplotlib.font_manager as fm
# import json
# import hashlib
# def get_json_hash(json_content):
# json_string = json.dumps(json_content, sort_keys=True)
# hash_object = hashlib.sha256(json_string.encode())
# hash_value = hash_object.hexdigest()
# return hash_value
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def create_temp_file(image):
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('tmp', 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 [{
"filename": image_file,
"subfolder": subfolder,
"type": "temp"
}]
def get_font_files(directory):
font_files = {}
@@ -114,8 +158,177 @@ class TextToNumber:
result=0
for n in numbers:
result = int(n)
print(result)
# print(result)
if random_number=='enable' and result>0:
result= random.randint(1, 10000000000)
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
class FloatSlider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("FLOAT", {
"default": 0,
"min": 0, #Minimum value
"max": 1, #Maximum value
"step": 0.01, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number):
return (number,)
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
import folder_paths
# import time
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
import time
class DynamicDelayProcessor:
@classmethod
def INPUT_TYPES(cls):
# print("print INPUT_TYPES",cls)
return {
"required":{
"delay_seconds":("INT",{
"default":1,
"min": 0,
"max": 1000000,
}),
},
"optional":{
"any_input":(any_type,),
"delay_by_text":("STRING",{"multiline":True,}),
"words_per_seconds":("FLOAT",{ "default":1.50,"min": 0.0,"max": 1000.00,"display":"Chars per second?"}),
"replace_output": (["disable","enable"],),
"replace_value":("INT",{ "default":-1,"min": 0,"max": 1000000,"display":"Replacement value"})
}
}
@classmethod
def calculate_words_length(cls,text):
chinese_char_pattern = re.compile(r'[\u4e00-\u9fff]')
english_word_pattern = re.compile(r'\b[a-zA-Z]+\b')
number_pattern = re.compile(r'\b[0-9]+\b')
words_length = 0
for segment in text.split():
if chinese_char_pattern.search(segment):
# 中文字符,每个字符计为 1
words_length += len(segment)
elif number_pattern.match(segment):
# 数字,每个字符计为 1
words_length += len(segment)
elif english_word_pattern.match(segment):
# 英文单词,整个单词计为 1
words_length += 1
return words_length
FUNCTION = "run"
RETURN_TYPES = (any_type,)
RETURN_NAMES = ('output',)
CATEGORY = "♾️Mixlab/utils"
def run(self,any_input,delay_seconds,delay_by_text,words_per_seconds,replace_output,replace_value):
# print(f"Delay text:",delay_by_text )
# 获取开始时间戳
start_time = time.time()
# 计算延迟时间
delay_time = delay_seconds
if delay_by_text and isinstance(delay_by_text, str) and words_per_seconds > 0:
words_length = self.calculate_words_length(delay_by_text)
print(f"Delay text: {delay_by_text}, Length: {words_length}")
delay_time += words_length / words_per_seconds
# 延迟执行
print(f"延迟执行: {delay_time}")
time.sleep(delay_time)
# 获取结束时间戳并计算间隔
end_time = time.time()
elapsed_time = end_time - start_time
print(f"实际延迟时间: {elapsed_time} 秒")
# 根据 replace_output 决定输出值
return (max(0, replace_value),) if replace_output == "enable" else (any_input,)
# app 配置节点
class AppInfo:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"name": ("STRING",{"multiline": False,"default": "Mixlab-App"}),
"image": ("IMAGE",),
"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": ""}),
"version":("INT", {
"default": 1,
"min": 1,
"max": 10000,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
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]}, "result": (image,)}
+694
View File
@@ -0,0 +1,694 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixlab APP</title>
<style>
.app {
display: flex;
width: 90%;
min-width: 400px;
margin-left: 5%;
}
.status {
background: black;
color: white;
display: flex;
width: fit-content;
padding: 4px;
font-size: 12px;
margin: 12px;
}
.description {
display: flex;
margin: 12px;
background: white;
padding: 8px;
}
.panel {
display: flex;
flex-direction: column;
min-width: 300px;
background: #eee;
margin: 24px;
flex: 1;
align-items: center;
justify-content: center;
}
.panel h1 {
padding: 0 12px;
margin-top: 12px;
margin-bottom: 0;
}
.panel img {
width: 100%;
height: fit-content;
}
.input_card {
display: flex;
flex-direction: column;
}
.output_card {
box-shadow: 0px 0px 8px 3px #e6e7e7;
}
.card {
background-color: #eee;
display: flex;
flex-direction: column;
padding: 24px;
font-size: 12px;
}
.card textarea {
width: 100%;
height: 200px;
/* min-width: 300px; */
margin-top: 12px;
}
.card img {
width: 100%;
margin-top: 12px;
}
.run_btn {
background: black;
color: white;
width: 88px;
height: 88px;
position: fixed;
bottom: 72px;
left: calc(50% - 44px);
border-radius: 100%;
cursor: pointer;
border: 3px solid;
}
.run_btn:hover {
border-color: yellow;
color: yellow;
}
.disabled {
background-color: #eee !important;
color: #4a4a4a !important;
}
.upload_btn {
width: 188px;
cursor: pointer;
height: 188px;
background: black;
color: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 14px;
margin-left: calc(50% - 94px);
margin-top: calc(30vh - 94px);
}
.upload_btn:hover {
outline: 4px solid yellow;
color: yellow;
}
.link {
text-decoration: none;
color: gray;
font-size: 12px;
font-weight: 300;
}
</style>
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
</head>
<body>
<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>
</div>
<div></div>
<script type="module">
import { api } from "../../../scripts/api.js";
// console.log('api', api)
const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
function get_url() {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
async function uploadImage(blob, fileType = '.png', filename) {
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const url = get_url()
const resp = await fetch(`${url}/upload/image`, {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
let src = `${url}/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}&rand=${Math.random()}`
return { url: src, name }
}
function randomSeed(data) {
for (const key in data) {
if (data[key].inputs.seed != undefined) {
data[key].inputs.seed = Math.round(Math.random() * 1849378600828930)
console.log('new Seed', data[key])
}
}
return data
}
function queuePrompt(promptWorkflow, client_id) {
// 随机seed
promptWorkflow = randomSeed(promptWorkflow);
let url = get_url()
const data = JSON.stringify({ prompt: promptWorkflow, client_id });
fetch(`${url}/prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data,
})
.then(response => {
// Handle response here
console.log(response)
})
.catch(error => {
// Handle error here
});
}
async function get_my_app() {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'my_app'
})
})
let result = await res.json();
let { output, app } = result.data
return {
...app,
data: output
}
}
function createOutputs(outputData) {
// Array.from( window._appData.output,n=>n.id)
const container = document.createElement("div");
container.className = 'output_card'
for (const node of outputData) {
let img = new Image();
img.id = `output_${node.id}`;
img.src = base64Df;
container.appendChild(img);
}
return container
}
function createInputs(inputData) {
// Assuming you have an HTML element with the id "container" to hold the UI
const container = document.createElement("div");
container.className = 'input_card'
// const inputData = [
// {
// inputs: {
// image: "1703554480406.png",
// upload: "image"
// },
// class_type: "LoadImage"
// },
// {
// inputs: {
// image: "6b7f3c570ee13ef22aad3d26dcc7414.png",
// upload: "image"
// },
// class_type: "LoadImage"
// }
// ];
inputData.forEach(data => {
// Check if the class_type is "LoadImage"
if (data.class_type === "LoadImage") {
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = data.title || "LoadImage: ";
uploadContainer.appendChild(nameLabel);
// Create an input field for the image name
const nameInput = document.createElement("input");
nameInput.type = "file";
nameInput.style = `width: 88px;`
uploadContainer.appendChild(nameInput);
// Create an image element to display the uploaded image
const imageElement = document.createElement("img");
imageElement.src = base64Df
imageElement.style.maxWidth='200px'
nameInput.addEventListener('change', (event) => {
// 获取用户选择的文件
const file = event.target.files[0];
// 创建一个 FileReader 对象
const reader = new FileReader();
// 读取文件并在读取完成后执行回调函数
reader.onloadend = async function () {
// 获取读取的文件内容,即 Blob 对象
const fileBlob = new Blob([reader.result], { type: file.type });
let { url, name } = await uploadImage(fileBlob)
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
console.log("上传的文件:", url, data.id, name);
};
// 开始读取文件
reader.readAsArrayBuffer(file);
})
// imageElement.src = `${get_url()}/view?filename=${encodeURIComponent(data.inputs.image)}&type=${type}&subfolder=${subfolder}`;
uploadContainer.appendChild(imageElement);
// Append the upload container to the main container
container.appendChild(uploadContainer);
}
if (data.class_type === 'FloatSlider') {
// 滑块输入
let silde = createFloatSlide(data.title, data.inputs.number, (v) => {
window._appData.data[data.id].inputs.number = v;
})
container.appendChild(silde);
}
// Check if the class_type is "CLIPTextEncode"
if (data.class_type === "CLIPTextEncode") {
// Create a container for the upload control
const uploadContainer = document.createElement("div");
uploadContainer.className = 'card';
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = data.title || "CLIPTextEncode: ";
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);
nameInput.addEventListener('input', (event) => {
console.log(nameInput.value)
window._appData.data[data.id].inputs.text = nameInput.value;
})
// Append the upload container to the main container
container.appendChild(uploadContainer);
}
});
return container
}
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=0.01
slider.value=value;
// 创建标签元素
var label = document.createElement("label");
label.innerHTML = labelText;
// 创建容器元素,并将滑块输入和标签添加到容器中
var container = document.createElement("div");
container.appendChild(label);
container.appendChild(slider);
container.className='card'
// 添加change事件监听器
slider.addEventListener("change", function (event) {
var value = event.target.value;
console.log("滑块输入的值为:" + value);
// 在这里可以执行其他操作,根据需要进行相应的处理
callback && callback(value)
});
// 返回容器元素
return container;
}
function getTypeFromUrl(url) {
const queryString = url.split('?')[1];
if (!queryString) {
return null;
}
const params = new URLSearchParams(queryString);
const type = params.get('type');
return type;
}
function createUI(inputData, outputData) {
let mainDiv = document.createElement('div');
let leftDiv = document.createElement('div');
let rightDiv = document.createElement('div');
mainDiv.className = 'app'
leftDiv.className = 'panel'
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`
// 创建标题
var title = document.createElement('h1');
title.textContent = 'My Application';
let iconDes = document.createElement('div');
// 创建应用图标
var icon = document.createElement('img');
icon.style.width = '48px';
// icon.style.height = '98px';
icon.src = base64Df;
var des = document.createElement('p');
des.style = `margin-left: 12px; font-size: 14px;`
iconDes.appendChild(icon)
iconDes.appendChild(des);
iconDes.className = 'description'
// 创建状态标签
var status = document.createElement('div');
status.textContent = 'Status';
status.className = 'status';
// 创建输入框
var input1 = createInputs(inputData)
var output = createOutputs(outputData)
// 创建提交按钮
var submitButton = document.createElement('button');
submitButton.textContent = 'Create';
submitButton.className = 'run_btn'
// 将所有UI元素添加到页面中
leftDiv.appendChild(title);
leftDiv.appendChild(iconDes);
// leftDiv.appendChild(des);
leftDiv.appendChild(status);
leftDiv.appendChild(input1);
leftDiv.appendChild(submitButton);
rightDiv.appendChild(output);
mainDiv.appendChild(leftDiv);
mainDiv.appendChild(rightDiv);
document.body.appendChild(mainDiv)
// 返回每个UI元素的引用和对应的更新方法
return {
title: {
element: title,
update: function (newTitle) {
title.textContent = newTitle;
}
},
icon: {
element: icon,
update: function (newIconPath) {
icon.src = newIconPath;
}
},
des: {
element: des,
update: function (text) {
des.textContent = text;
}
},
status: {
element: status,
update: function (newStatus) {
status.textContent = newStatus;
}
},
input1: {
element: input1,
update: function () {
// 可以在这里添加上传图片的逻辑
}
},
output: {
element: output,
update: function (url, id) {
console.log(url, id)
if (output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).src = url
}
},
submitButton: {
element: submitButton,
update: function (callback) {
submitButton.addEventListener('dblclick', (e) => {
submitButton.classList.remove('disabled');
});
submitButton.addEventListener('click', (e) => {
if (!submitButton.classList.contains('disabled')) {
callback && callback();
submitButton.classList.add('disabled');
setTimeout(() => submitButton.classList.remove('disabled'), 500)
}
});
}
}
};
}
function createUploadJson() {
// 创建一个div元素
var div = document.createElement('div');
div.className = 'upload_btn'
div.textContent = '点击上传JSON文件';
div.addEventListener('click', function () {
document.getElementById('jsonFileInput').click();
});
// 创建一个input元素
var input = document.createElement('input');
input.type = 'file';
input.id = 'jsonFileInput';
input.style.display = 'none';
input.addEventListener('change', function (event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function (e) {
var contents = e.target.result;
var jsonData = JSON.parse(contents);
setTimeout(() => {
div.remove();
input.remove();
}, 500);
let { output, app } = jsonData;
window._appData = {
...app,
data: output
};
createApp(window._appData);
// res(jsonData);
};
reader.readAsText(file);
});
// 将div和input元素添加到body中
document.body.appendChild(div);
document.body.appendChild(input);
}
async function createApp(appData) {
// 使用示例:
var ui = createUI(appData.input, appData.output);
// 更新标题
ui.title.update(appData.name || 'Mixlab APP');
// 更新应用图标
ui.icon.update(appData.icon || base64Df);
ui.des.update(appData.description || '-');
// 更新状态标签
ui.status.update(appData ? 'READY' : '-');
// 添加提交按钮点击事件
ui.submitButton.update(function () {
// 在提交按钮点击时执行的逻辑
queuePrompt(window._appData.data, api.clientId)
});
const show = (src, id) => {
// console.log(src)
ui.output.update(src, id)
};
api.addEventListener("status", ({ detail }) => {
console.log("status", detail);
try {
ui.status.update(`queue#${detail.exec_info.queue_remaining}`);
} catch (error) {
}
});
api.addEventListener("progress", ({ detail }) => {
console.log("progress", detail);
try {
ui.status.update(`${detail.value}/${detail.max}`);
} catch (error) {
}
});
api.addEventListener("executed", ({ detail }) => {
console.log("executed", detail)
// if (!enabled) return;
const images = detail?.output?.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);
try {
ui.status.update(`executed_#${detail.node}`);
} catch (error) {
}
});
api.addEventListener("b_preview", ({ detail }) => {
// if (!enabled) return;
console.log("b_preview", detail)
show(URL.createObjectURL(detail));
});
api.api_base = ""
api.init();
}
async function init_app() {
let appData = {}
const type = getTypeFromUrl(location.href);
if (type === 'new') {
createUploadJson();
} else {
appData = await get_my_app();
// console.log(appData)
window._appData = appData;
createApp(appData);
}
};
init_app()
</script>
</body>
</html>
+42 -23
View File
@@ -187,20 +187,22 @@ app.registerExtension({
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
console.log('serializeValue', node)
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let base64 = await parseImage(url)
let bg_base64 = await parseImage(bg)
let material_base64 = await parseImage(material)
let data = {}
if (url) {
data.image = await parseImage(url)
}
if (bg) {
data.bg_image = await parseImage(bg)
}
return JSON.parse(
JSON.stringify({
image: base64,
bg_image: bg_base64,
material: material_base64
})
)
if (material) {
data.material = await parseImage(material)
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
}
@@ -281,6 +283,8 @@ app.registerExtension({
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
<div><button class="export">Export GLB</button></div>
</div></model-viewer>`
preview.innerHTML = html
@@ -294,6 +298,7 @@ app.registerExtension({
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
@@ -341,21 +346,26 @@ app.registerExtension({
let url = await uploadImage(blob, '.png')
// console.log(url)
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log(tUrl)
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id])
dd[that.id] = { url, bg: url_bg, material: tUrl }
dd[that.id] = { ...dd[that.id], url, material: tUrl }
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
@@ -367,11 +377,11 @@ app.registerExtension({
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', event => {
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
extractMaterial(
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
@@ -456,6 +466,15 @@ app.registerExtension({
input.click()
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
+282
View File
@@ -0,0 +1,282 @@
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'
}
}
async function drawImageToCanvas (imageUrl) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
var img = new Image()
await new Promise((resolve, reject) => {
img.onload = function () {
var scaleFactor = 320 / img.width
var canvasWidth = img.width * scaleFactor
var canvasHeight = img.height * scaleFactor
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight)
resolve()
}
img.onerror = function () {
reject(new Error('Failed to load image'))
}
img.src = imageUrl
})
var base64 = canvas.toDataURL('image/jpeg')
// console.log(base64); // 输出Base64数据
return base64
// 可以在这里执行其他操作,比如将Base64数据保存到服务器或显示在页面上
}
function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
const data = jsonData
const input = []
const output = []
for (const id in data) {
if (data.hasOwnProperty(id)) {
if (inputIds.includes(id)) {
let node = app.graph.getNodeById(id)
input[inputIds.indexOf(id)] = { ...data[id], title: node.title, id }
// input.push()
}
if (outputIds.includes(id)) {
let node = app.graph.getNodeById(id)
// output.push()
output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id }
}
}
}
return { input, output }
}
function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
async function save_app (json) {
let url = getUrl()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
data: json,
task: 'save_app'
})
})
return await res.json()
}
function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
const dataString = JSON.stringify(jsonData)
const blob = new Blob([dataString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
// 释放URL对象
setTimeout(() => {
URL.revokeObjectURL(url)
}, 0)
}
async function save (json, download = false) {
const name = json[0],
version = json[5],
description = json[4],
inputIds = json[2].split('\n').filter(f => f),
outputIds = json[3].split('\n').filter(f => f)
const iconData = json[1][0]
let { filename, subfolder, type } = iconData
let iconUrl = api.apiURL(
`/view?filename=${encodeURIComponent(
filename
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
try {
let data = await app.graphToPrompt()
const { input, output } = extractInputAndOutputData(
data.output,
inputIds,
outputIds
)
data.app = {
name,
description,
version,
input,
output
}
try {
data.app.icon = await drawImageToCanvas(iconUrl)
} catch (error) {}
// console.log(data.app)
// let http_workflow = app.graph.serialize()
if (download) {
await downloadJsonFile(
data,
`${data.app.name}_${data.app.version}_${new Date().toDateString()}.json`
)
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)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app`
)
if (open) window.open(`${getUrl()}/mixlab/app`)
}
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
app.registerExtension({
name: 'Mixlab.utils.AppInfo',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'AppInfo') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
// console.log(this)
const widget = {
type: 'div',
name: 'AppInfoRun',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width,
node.widgets[4].last_y + 24,
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 = 'Save For App'
btn.style = style
btn.addEventListener('click', () => {
// console.log('hahhah')
if (window._mixlab_app_json) {
save(window._mixlab_app_json)
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
}
})
const download = document.createElement('button')
download.innerText = 'Download For App'
download.style = style
download.style.marginLeft = '12px'
download.addEventListener('click', () => {
// console.log('hahhah')
if (window._mixlab_app_json) {
save(window._mixlab_app_json, true)
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
}
})
document.body.appendChild(widget.div)
widget.div.appendChild(btn)
widget.div.appendChild(download)
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_app_json = message.json
try {
const div = this.widgets.filter(w => w.div)[0].div
Array.from(
div.querySelectorAll('button'),
b => (b.style.background = 'yellow')
)
} catch (error) {}
}
}
}
})
+44 -14
View File
@@ -68,7 +68,7 @@ function speakText (text) {
// speakText('Hello, how are you?');
// #MixCopilot
const start = (element, id, startBtn) => {
const start = (element, id, startBtn, node) => {
startBtn.className = 'loading_mixlab'
window.recognition = new webkitSpeechRecognition()
@@ -99,7 +99,18 @@ const start = (element, id, startBtn) => {
timeoutId = setTimeout(function () {
console.log('结果传递::', result)
app.queuePrompt(0, 1)
// 把数据发送到chatgpt的输入prompt里
try {
const sendToId = node.widgets.filter(
w => w.name === 'Send to ChatGPT #'
)[0].value
app.graph
.getNodeById(sendToId)
.widgets.filter(w => w.name === 'prompt')[0].value = result
} catch (error) {}
setTimeout(() => app.queuePrompt(0, 1), 100)
window.recognition?.stop()
window.recognition = null
startBtn.className = ''
@@ -113,7 +124,7 @@ const start = (element, id, startBtn) => {
!window.recognition &&
window._mixlab_speech_synthesis_onend
) {
start(element, id, startBtn)
start(element, id, startBtn, node)
startBtn.innerText = 'STOP'
if (intervalId) {
clearInterval(intervalId)
@@ -170,13 +181,21 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const sendTo = ComfyWidgets.INT(
this,
'Send to ChatGPT #',
['INT', { default: 0 }],
app
)
// console.log('sendTo',sendTo)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
get_position_style(ctx, widget_width, 78, node.size[1])
)
}
}
@@ -188,7 +207,14 @@ app.registerExtension({
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const startBtn = document.createElement('button')
const textArea = document.createElement('textarea')
textArea.placeholder = 'speak text'
// sendTo.type='range';
// sendTo.min=0;
// sendTo.max=2000;
// sendTo.step=1;
// sendTo.className='comfy-multiline-input'
textArea.className = `${'comfy-multiline-input'} ${placeholder}`
@@ -211,6 +237,7 @@ app.registerExtension({
startBtn.innerText = 'START'
div.appendChild(startBtn)
// div.appendChild(sendTo);
div.appendChild(textArea)
startBtn.addEventListener('click', () => {
@@ -220,11 +247,15 @@ app.registerExtension({
startBtn.innerText = 'START'
startBtn.className = ''
} else {
start(textArea, this.id, startBtn)
start(textArea, this.id, startBtn, this)
startBtn.innerText = 'STOP'
}
})
// sendTo.addEventListener('change',()=>{
// console.log(sendTo.value)
// })
return div
}
@@ -243,8 +274,6 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
// const onGraphConfigured=nodeType.prototype.onGraphConfigured;
// nodeType.prototype.onGraphConfigured = function (message) {
// onGraphConfigured?.apply(this, arguments)
@@ -254,15 +283,17 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// console.log('this.widgets', this.widgets)
try {
// 是否根据start by 开启
let open = message.start_by[0] > 0
if (open) {
const div = this.widgets.filter(w => w.name == 'chatgptdiv')[0].div
const startBtn = div.querySelector('button')
let textArea= div.querySelector('textarea')
let textArea = div.querySelector('textarea')
if (open && !window.recognition) {
start(textArea, this.id, startBtn)
start(textArea, this.id, startBtn, this)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
@@ -284,16 +315,16 @@ app.registerExtension({
let div = node.widgets.filter(f => f.type === 'div')[0]
if (div && data[node.id]) {
div.div.querySelector('textarea').value = data[node.id]
};
}
try {
let open = node.widgets_values[1] > 0
if (open) {
const div = node.widgets.filter(w => w.name == 'chatgptdiv')[0].div
const startBtn = div.querySelector('button')
let textArea= div.querySelector('textarea')
let textArea = div.querySelector('textarea')
if (open && !window.recognition) {
start(textArea, node.id, startBtn)
start(textArea, node.id, startBtn, node)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
@@ -305,7 +336,6 @@ app.registerExtension({
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
}
})
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.4.2'
const version = 'v0.6.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+1 -1
View File
@@ -61,7 +61,7 @@ app.registerExtension({
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##node', node)
console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
+8 -4
View File
@@ -193,6 +193,11 @@ const parseSvg = async svgContent => {
svgWidth = viewBox.width
svgHeight = viewBox.height
} else {
try {
svgWidth = ~~svgWidth.replace('px', '')
svgHeight = ~~svgHeight.replace('px', '')
} catch (error) {}
}
// 创建一个新的canvas元素
@@ -356,7 +361,7 @@ app.registerExtension({
setLocalDataOfWin(key, dd)
// console.log(this.id, ip.value.trim())
svgElement.style = `width: 90%;padding: 5%;`
svgElement.style = `width: 90%;padding: 5%;height: auto;`
// 将提取的SVG元素显示在页面上
svgContainer.innerHTML = ''
@@ -424,7 +429,7 @@ app.registerExtension({
let svgStr = await dt.text()
const { svgElement, data, image } = await parseSvg(svgStr)
svgElement.style = `width: 90%;padding: 5%;`
svgElement.style = `width: 90%;padding: 5%;height:auto`
// 将提取的SVG元素显示在页面上
widget.div.querySelector('.preview').innerHTML = ''
@@ -432,7 +437,6 @@ app.registerExtension({
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
}
}
})
})
+6 -2
View File
@@ -1800,11 +1800,15 @@ const updateUI = node => {
pw.inputEl.title = `Total of ${prompts.length} prompts`
} else {
// 动态添加
console.log('ComfyWidgets',ComfyWidgets.STRING(
node,
'prompts',
['STRING', { multiline: true }]
))
const w = ComfyWidgets.STRING(
node,
'prompts',
['STRING', { multiline: true }],
app
['STRING', { multiline: true }]
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
+271 -165
View File
@@ -2,7 +2,12 @@ import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import {closeIcon} from './svg_icons.js'
import { closeIcon } from './svg_icons.js'
import {
GroupNodeConfig,
GroupNodeHandler
} from '../../../extensions/core/groupNode.js'
function deepEqual (obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
@@ -43,16 +48,17 @@ async function get_nodes_map () {
return await res.json()
}
function loadCSS(url) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
document.getElementsByTagName('head')[0].appendChild(link);
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = url
document.getElementsByTagName('head')[0].appendChild(link)
}
var cssURL = 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css';
loadCSS(cssURL);
var cssURL =
'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css'
loadCSS(cssURL)
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
@@ -112,7 +118,7 @@ async function getCustomnodeMappings (mode = 'url') {
nodes[node] = { url, title: n[1].title_aux }
}
}
// try {
// const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
// const data = await response.json()
@@ -309,8 +315,7 @@ async function fetchReadmeContent (url) {
}
}
function createModal (url, markdown,title) {
function createModal (url, markdown, title) {
// Create modal element
var div =
document.querySelector('#mix-modal') || document.createElement('div')
@@ -329,7 +334,7 @@ function createModal (url, markdown,title) {
var modal = document.createElement('div')
div.appendChild(modal)
modal.classList.add("modal-body")
modal.classList.add('modal-body')
// Set modal styles
modal.style.cssText = `
background: white;
@@ -348,17 +353,17 @@ function createModal (url, markdown,title) {
var modalContent = document.createElement('div')
modalContent.classList.add('modal-content')
// Create modal header
const headerElement = document.createElement('div')
const headerElement = document.createElement('div')
headerElement.classList.add('modal-header')
headerElement.style.cssText = `
display: flex;
padding: 20px 24px 8px 24px;
justify-content: space-between;
`
const headTitleElement = document.createElement('a')
headTitleElement.classList.add('header-title')
headTitleElement.style.cssText=`
headTitleElement.style.cssText = `
color: var(--descrip-text);
font-size: 18px;
display: flex;
@@ -368,22 +373,20 @@ function createModal (url, markdown,title) {
text-decoration: none;
font-weight: bold;
`
headTitleElement.onmouseenter = function(){
headTitleElement.onmouseenter = function () {
headTitleElement.style.color = 'var(--comfy-menu-bg)'
}
headTitleElement.onmouseleave = function(){
headTitleElement.onmouseleave = function () {
headTitleElement.style.color = 'var(--descrip-text)'
}
headTitleElement.textContent= title ||'';
headTitleElement.textContent = title || ''
headTitleElement.href = url
headTitleElement.target='_blank'
headTitleElement.target = '_blank'
const linkIcon = document.createElement('small')
linkIcon.textContent = '🔗'
headTitleElement.appendChild(linkIcon);
headTitleElement.appendChild(linkIcon)
headerElement.appendChild(headTitleElement)
// Create close button
const closeButton = document.createElement('span')
closeButton.classList.add('close')
@@ -400,20 +403,19 @@ function createModal (url, markdown,title) {
user-select: none;
fill: var(--descrip-text);
`
closeButton.onmouseenter = function(){
closeButton.style.fill = 'var(--comfy-menu-bg)';
closeButton.onmouseenter = function () {
closeButton.style.fill = 'var(--comfy-menu-bg)'
}
closeButton.onmouseleave = function(){
closeButton.style.fill = 'var(--descrip-text)';
closeButton.onmouseleave = function () {
closeButton.style.fill = 'var(--descrip-text)'
}
headerElement.appendChild(closeButton)
// Click event to close the modal
function closeMixModal(){
function closeMixModal () {
div.style.display = 'none'
window.removeEventListener('keydown',MixModalEscKeyEvent)
window.removeEventListener('keydown', MixModalEscKeyEvent)
}
closeButton.onclick = function () {
closeMixModal()
@@ -433,10 +435,10 @@ function createModal (url, markdown,title) {
// Create element for displaying Markdown content
var markdownContent = document.createElement('div')
markdownContent.classList.add('markdown-content','markdown-body')
markdownContent.classList.add('markdown-content', 'markdown-body')
markdownContent.style.cssText = `max-width: 50vw;padding: 0px 24px 100px 24px;`
showdown.setFlavor('github');
showdown.setFlavor('github')
var converter = new showdown.Converter()
var html = converter.makeHtml(markdown)
@@ -468,7 +470,7 @@ function createModal (url, markdown,title) {
modal.appendChild(modalContent)
const footerElement = document.createElement('div')
footerElement.style.cssText=`
footerElement.style.cssText = `
position: absolute;
left: 0;
right: 0;
@@ -478,37 +480,35 @@ function createModal (url, markdown,title) {
`
const footerText = document.createElement('a')
footerText.href ="https://github.com/shadowcz007/comfyui-mixlab-nodes"
footerText.innerText = "Support by Mixlab"
footerText.style.cssText=`color:inherit`
footerText.target = "_blank"
footerText.onmouseenter=function(){
footerText.href = 'https://github.com/shadowcz007/comfyui-mixlab-nodes'
footerText.innerText = 'Support by Mixlab'
footerText.style.cssText = `color:inherit`
footerText.target = '_blank'
footerText.onmouseenter = function () {
footerText.style.color = 'var(--input-text)'
}
footerText.onmouseleave=function(){
footerText.onmouseleave = function () {
footerText.style.color = 'inherit'
}
footerText.onclick = function(e){
footerText.onclick = function (e) {
e.stopPropagation()
}
footerElement.appendChild(footerText)
div.appendChild(footerElement)
// Append modal element to the page
if (!document.querySelector('#mix-modal')) {
document.body.appendChild(div)
}
function MixModalEscKeyEvent(event){
if(event.key == "Escape"){
closeMixModal()
}
function MixModalEscKeyEvent (event) {
if (event.key == 'Escape') {
closeMixModal()
}
}
window.removeEventListener('keydown',MixModalEscKeyEvent)
window.addEventListener('keydown',MixModalEscKeyEvent)
window.removeEventListener('keydown', MixModalEscKeyEvent)
window.addEventListener('keydown', MixModalEscKeyEvent)
const bgElement = document.createElement('div')
bgElement.classList.add('mix-modal-bg')
@@ -517,13 +517,11 @@ function createModal (url, markdown,title) {
height:100%;
background-color: rgba(0,0,0,0.8);
`
bgElement.onclick = function( ){
bgElement.onclick = function () {
closeMixModal()
}
div.appendChild(bgElement)
}
app.registerExtension({
@@ -539,7 +537,7 @@ app.registerExtension({
let repo = nodesMap[node.type]
if (repo) {
let markdown = await fetchReadmeContent(repo.url)
createModal(repo.url,markdown,repo.title)
createModal(repo.url, markdown, repo.title)
}
}
@@ -548,17 +546,7 @@ app.registerExtension({
// replace it
const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
// options.splice(
// options.length - 1,
// 0, // splice a new option in at the end
// {
// content: '♾️Mixlab', // with a name
// callback: () => {
// LGraphCanvas.prototype.helpAboutNode(node)
// } // and the callback
// },
// null // a divider
// )
return [
{
content: 'Help ♾️Mixlab', // with a name
@@ -570,32 +558,140 @@ app.registerExtension({
...options
] // and return the options
}
const getGroupMenuOptions = LGraphCanvas.prototype.getGroupMenuOptions // store the existing method
LGraphCanvas.prototype.getGroupMenuOptions = function (node) {
// replace it
const options = getGroupMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
// templete
const key = 'Comfy.NodeTemplates'
let templates = localStorage.getItem(key)
if (templates) {
templates = JSON.parse(templates)
} else {
templates = []
}
const store = () => localStorage.setItem(key, JSON.stringify(templates))
return [
{
content: 'Clone Group ♾️Mixlab', // with a name
callback: async (value, opts, e, menu, group) => {
const clipboardAction = async cb => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem('litegrapheditor_clipboard')
await cb()
localStorage.setItem('litegrapheditor_clipboard', old)
}
clipboardAction(async () => {
let name = group.title
let nodes = group._nodes
app.canvas.copyToClipboard(nodes)
let data = localStorage.getItem('litegrapheditor_clipboard')
data = JSON.parse(data)
for (let i = 0; i < nodes.length; i++) {
const node = app.graph.getNodeById(nodes[i].id)
const nodeData = node.serialize()
let groupData = GroupNodeHandler.getGroupData(node)
if (groupData) {
groupData = groupData.nodeData
if (!data.groupNodes) {
data.groupNodes = {}
}
data.groupNodes[nodeData.name] = groupData
data.nodes[i].type = nodeData.name
}
}
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {})
localStorage.setItem(
'litegrapheditor_clipboard',
JSON.stringify(data)
)
app.canvas.pasteFromClipboard()
})
} // and the callback
},
{
content: 'Save Group as Template ♾️Mixlab', // with a name
callback: async (value, opts, e, menu, group) => {
// console.log(options)
const clipboardAction = async cb => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem('litegrapheditor_clipboard')
await cb()
localStorage.setItem('litegrapheditor_clipboard', old)
}
clipboardAction(() => {
let name = group.title + ' ♾️Mixlab'
let nodes = group._nodes
app.canvas.copyToClipboard(nodes)
let data = localStorage.getItem('litegrapheditor_clipboard')
data = JSON.parse(data)
for (let i = 0; i < nodes.length; i++) {
const node = app.graph.getNodeById(nodes[i].id)
const nodeData = node.serialize()
let groupData = GroupNodeHandler.getGroupData(node)
if (groupData) {
groupData = groupData.nodeData
if (!data.groupNodes) {
data.groupNodes = {}
}
data.groupNodes[nodeData.name] = groupData
data.nodes[i].type = nodeData.name
}
}
templates.push({
name,
data: JSON.stringify(data)
})
store()
})
} // and the callback
},
null,
...options
] // and return the options
}
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null, {
content: `Nodes Map ♾️Mixlab`,
disabled: false, // or a function determining whether to disable
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
options.push(null, {
content: `Find ♾️Mixlab`,
disabled: false, // or a function determining whether to disable
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
@@ -604,63 +700,65 @@ app.registerExtension({
background-color: var(--comfy-menu-bg);
padding: 10px;
border: 1px solid black;z-index: 999999999;padding-top: 0;`
div.innerHTML = ''
let btn = document.createElement('div')
btn.style=`display: flex;
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 24px);
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 32px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.innerText = `Find The Node`
btnB.style = `float: right; border: none; color: var(--input-text);
height: 44px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.style.fontSize='12px';
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[nodeId].class_type
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[nodeId].class_type
if (nodesMap[n]) {
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;`
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
@@ -675,25 +773,27 @@ app.registerExtension({
updateNodes(n, nd)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nd.appendChild(d)
}
}
let nodesDivv = document.createElement('div')
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
}
let nodesDivv = document.createElement('div')
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
if (nodesMap[n]) {
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;`
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
@@ -708,30 +808,36 @@ app.registerExtension({
updateNodes(n, nodesDivv)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nodesDiv.appendChild(d)
}
nodesDivv.appendChild(nodesDiv)
nodesDivv.style=`overflow: scroll;
height: 70vh;width: 100%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
});
return options;
};
nodesDivv.appendChild(nodesDiv)
nodesDivv.style = `overflow: scroll;
height: 70vh;width: 100%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
})
// options.push({
// content: `Save For App ♾️Mixlab`,
// disabled: false, // or a function determining whether to disable
// callback: async () => {
// }
// })
return options
}
}
})
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

+314
View File
@@ -0,0 +1,314 @@
{
"last_node_id": 10,
"last_link_id": 16,
"nodes": [
{
"id": 9,
"type": "SpeechSynthesis",
"pos": [
40,
356
],
"size": {
"0": 352.8227844238281,
"1": 95.3553237915039
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 12,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
13,
16
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
"作为一个人工智能助手,我没有性别,也无法进行社交互动。我的主要任务是为用户提供帮助和信息。请问有什么问题我可以为您解答吗?"
]
},
{
"id": 8,
"type": "SpeechRecognition",
"pos": [
537,
321
],
"size": {
"0": 228.5580291748047,
"1": 239.85951232910156
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "start_by",
"type": "INT",
"link": 14,
"widget": {
"name": "start_by"
}
}
],
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
15
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
1,
4,
null
]
},
{
"id": 4,
"type": "ChatGPTOpenAI",
"pos": [
-403,
14
],
"size": {
"0": 400,
"1": 358
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
12
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"跟你一样泡妞",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo",
354,
"randomize",
1,
null
]
},
{
"id": 10,
"type": "ShowTextForGPT",
"pos": [
867,
349
],
"size": {
"0": 315,
"1": 76.00000762939453
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 15,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"纽斯"
]
},
{
"id": 5,
"type": "DynamicDelayProcessor",
"pos": [
496,
4
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "any_input",
"type": "*",
"link": 13
},
{
"name": "delay_by_text",
"type": "STRING",
"link": 16,
"widget": {
"name": "delay_by_text"
}
}
],
"outputs": [
{
"name": "output",
"type": "*",
"links": [
14
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "DynamicDelayProcessor"
},
"widgets_values": [
1,
"",
2.5,
"enable",
1
]
}
],
"links": [
[
4,
2,
0,
5,
0,
"*"
],
[
7,
4,
0,
6,
0,
"STRING"
],
[
8,
6,
0,
5,
1,
"STRING"
],
[
9,
5,
0,
7,
0,
"INT"
],
[
12,
4,
0,
9,
0,
"STRING"
],
[
13,
9,
0,
5,
0,
"*"
],
[
14,
5,
0,
8,
0,
"INT"
],
[
15,
8,
0,
10,
0,
"STRING"
],
[
16,
9,
0,
5,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
![workflow](./assets/City_Snapshot_00005_.png)
![workflow](./assets/挖掘机_00076_1.png)