Compare commits

...
70 Commits
Author SHA1 Message Date
shadowcz007 3191cf2af0 test 2023-12-29 13:27:48 +08:00
shadowcz007 7d3e1ae945 test 2023-12-29 12:59:27 +08:00
shadowcz007 bfced5b4da test 2023-12-29 12:49:05 +08:00
shadowcz007 efe73fb965 Update README.md 2023-12-29 10:39:01 +08:00
shadowcz007 c45a962fcc workflow-to-app支持checkpoints和lora 2023-12-29 10:38:22 +08:00
shadowcz007 f98a03e2e9 Update README.md 2023-12-29 00:00:16 +08:00
shadowcz007 5b6257814d 优化 2023-12-28 23:56:48 +08:00
shadowcz007 69a445d4ed 新增切换节点 2023-12-28 23:18:25 +08:00
shadowcz007 e82c786b8a 增加了从剪切板获取图片的控件 2023-12-28 18:36:34 +08:00
shadowcz007 eec2225c89 支持视频 2023-12-28 16:02:25 +08:00
shadowcz007 f7355e0b71 update 2023-12-28 14:33:59 +08:00
shadowcz007 6c6a99cfe4 优化LoadImagefromlocal ,新增LoadImageFromURL 2023-12-28 13:24:05 +08:00
shadowcz007 b4634e2e0d 修复clipseg的bug 2023-12-28 12:09:36 +08:00
shadowcz007 3f4cba0612 fixbug:textimage的高宽不对 2023-12-27 21:45:35 +08:00
shadowcz007 38db99cc75 支持showtext作为输出。GPT聊天也可以实现workflow-to-app了 2023-12-27 20:39:21 +08:00
shadowcz007 4d5906394b 优化newlayer的可视化效果 2023-12-27 20:12:55 +08:00
shadowcz007 2fc212b156 update 2023-12-27 19:38:39 +08:00
shadowcz007 53fbb5b027 fixbug 2023-12-27 17:48:49 +08:00
shadowcz007 4f24721450 Update README.md 2023-12-27 16:48:29 +08:00
shadow 83043727b5 Merge pull request #83 from shadowcz007/v0.6---simple-app
V0.6   simple app
2023-12-27 16:29:27 +08:00
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
shadowcz007 f5b210d142 支持换行的textimage 2023-12-23 14:13:04 +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
shadowcz007 6bbf06d9e9 TextToNumber&audio input control 2023-12-23 13:15:47 +08:00
shadowcz007 0b614b40cf Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-23 10:49:27 +08:00
shadowcz007 27673561bd 使用comfyui的ui来控制刷新率 2023-12-23 10:49:24 +08:00
shadow 6e2070410d Merge pull request #70 from shadowcz007/v0.3.2-3DImage
Delete layers-test-workflow.json
2023-12-22 20:36:39 +08:00
shadowcz007 7ccf21f74f Delete layers-test-workflow.json 2023-12-22 20:36:04 +08:00
shadow 3b2710f285 Merge pull request #69 from shadowcz007/v0.3.2-3DImage
v0.4.2
2023-12-22 20:28:33 +08:00
shadowcz007 c4b277235b 1 2023-12-22 20:27:53 +08:00
shadowcz007 a55318add1 v0.4.2 2023-12-22 20:24:19 +08:00
shadowcz007 b57123a4fe Update 3D-workflow.json 2023-12-22 20:21:57 +08:00
shadowcz007 04dcc00670 增加可视化选区 2023-12-22 20:20:03 +08:00
shadowcz007 746a02b49f test- 2023-12-22 12:16:39 +08:00
shadowcz007 bdbe3db2a9 Update Vae.py 2023-12-21 10:39:32 +08:00
shadowcz007 27ae99ad86 Update __init__.py 2023-12-21 10:26:16 +08:00
shadowcz007 38add89547 update style 2023-12-21 10:24:01 +08:00
shadowcz007 a9612fbb2f 增加一个resize节点 2023-12-20 16:12:09 +08:00
shadowcz007 429cc29b5b test 2023-12-20 15:08:13 +08:00
shadowcz007 8eca94e405 test 2023-12-20 14:32:57 +08:00
shadowcz007 ad71daafb6 Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-20 12:18:41 +08:00
shadowcz007 c936d83688 1 2023-12-20 12:18:38 +08:00
shadow c6684d680f Merge pull request #66 from shadowcz007/main
0.4.1
2023-12-20 10:50:26 +08:00
shadowcz007 897f259a2a Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-20 00:10:05 +08:00
shadowcz007 f3302c1b3a update 2023-12-20 00:08:05 +08:00
shadow 573feeaaab Merge pull request #64 from shadowcz007/main
1
2023-12-20 00:05:39 +08:00
39 changed files with 9577 additions and 2421 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
+29 -5
View File
@@ -1,8 +1,22 @@
##
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)
- text-to-text
> 暂时支持6种节点作为界面上的输入节点:Load Image、CLIPTextEncode、TextInput_、FloatSlider、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT
### 3D
![](./assets/3dimage.png)
@@ -43,6 +57,9 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### LoadImagesFromURL
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
### Layers
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
@@ -51,6 +68,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 +111,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.
+76 -8
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)
@@ -185,6 +202,8 @@ def get_nodes_map():
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
@@ -253,6 +272,22 @@ async def mixlab_hander(request):
print(e)
return web.json_response(data)
# @routes.post('/test')
# async def mixlab_hander(request):
# test_auto()
# return web.Response(text="test", status=200)
@routes.get('/mixlab/app')
async def mixlab_app_handler(request):
html_file = os.path.join(current_path, "web/index.html")
if os.path.exists(html_file):
with open(html_file, 'r', encoding='utf-8', errors='ignore') 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 +300,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 +335,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,26 +365,29 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,LoadImagesFromURL,UploadImageForSMMS,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import SpeechRecognition,SpeechSynthesis
from .nodes.Utils import ColorInput,FontInput
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.ShareNode import ShareToWeibo
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
NODE_CLASS_MAPPINGS = {
"AppInfo":AppInfo,
"RandomPrompt":RandomPrompt,
"NoiseImage":NoiseImage,
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
"LoadImagesFromURL":LoadImagesFromURL,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
"3DImage":Image3D,
"EmptyLayer":EmptyLayer,
"3DImage":Image3D,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"MergeLayers":MergeLayers,
@@ -359,11 +409,25 @@ NODE_CLASS_MAPPINGS = {
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"Font":FontInput
"FloatSlider":FloatSlider,
"IntNumber":IntNumber,
"TextInput_":TextInput,
"Font":FontInput,
"TextToNumber":TextToNumber,
"DynamicDelayProcessor":DynamicDelayProcessor,
"MultiplicationNode":MultiplicationNode,
"GetImageSize_":GetImageSize_,
"SwitchByIndex":SwitchByIndex,
"LimitNumber":LimitNumber,
"UploadImageForSMMS":UploadImageForSMMS,
"ShareToWeibo":ShareToWeibo
# "GamePal":GamePal
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"AppInfo":"AppInfo ♾️Mixlab",
"ResizeImageMixlab":"ResizeImage ♾️Mixlab",
"RandomPrompt": "Random Prompt ♾️Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
@@ -374,7 +438,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ShowTextForGPT":"ShowTextForGPT ♾️Mixlab",
"MergeLayers":"MergeLayers ♾️Mixlab",
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab"
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab"
# "GamePal":"GamePal ♾️Mixlab"
}
# web ui的节点功能
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

+17 -5
View File
@@ -4762,15 +4762,17 @@
"https://github.com/shadowcz007/comfyui-mixlab-nodes": [
[
"3DImage",
"AppInfo",
"IntNumber",
"FloatSlider",
"ResizeImage",
"NoiseImage",
"AreaToMask",
"CLIPSeg",
"CLIPSeg_",
"CharacterInText",
"ChatGPTOpenAI",
"Color",
"CombineMasks_",
"CombineSegMasks",
"EmptyLayer",
"EnhanceImage",
"FaceToMask",
"FeatheredMask",
@@ -4778,6 +4780,7 @@
"Font",
"ImageCropByAlpha",
"LoadImagesFromPath",
"LoadImagesFromURL",
"MergeLayers",
"NewLayer",
"RandomPrompt",
@@ -4792,10 +4795,19 @@
"TextImage",
"TransparentImage",
"VAEDecodeConsistencyDecoder",
"VAELoaderConsistencyDecoder"
"VAELoaderConsistencyDecoder",
"TextToNumber",
"TextInput_",
"DynamicDelayProcessor",
"MultiplicationNode",
"ShareToWeibo",
"LimitNumber",
"SwitchByIndex",
"UploadImageForSMMS",
"GetImageSize_"
],
{
"title_aux": "comfyui-mixlab-nodes [WIP]"
"title_aux": "comfyui-mixlab-nodes"
}
],
"https://github.com/shiimizu/ComfyUI_smZNodes": [
+56 -2
View File
@@ -7,6 +7,16 @@ class SpeechRecognition:
def INPUT_TYPES(s):
return {"required": {
"upload":("AUDIOINPUTMIX",), },
"optional":{
"start_by":("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 2048, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
RETURN_TYPES = ("STRING",)
@@ -19,8 +29,8 @@ class SpeechRecognition:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,upload):
return (upload,)
def run(self,upload,start_by):
return {"ui": {"start_by": [start_by]}, "result": (upload,)}
class SpeechSynthesis:
@@ -44,3 +54,47 @@ class SpeechSynthesis:
# print(session_history)
return {"ui": {"text": text}, "result": (text,)}
#
class GamePal:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_text": ("STRING",{"multiline": True,"default": ""}),
},
"optional": {
"input_num": ("INT",{
"default":100,
"min": -1, #Minimum value
"max": 0xffffffffffffffff, #Maximum value
"step": 1, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
"python_code": ("STRING",{"multiline": True,"default": "result= 1 if 'Mixlab' in input_text else 0"}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("INT",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,)
CATEGORY = "♾️Mixlab/audio"
def run(self, input_text,input_num,python_code):
exec(python_code)
res=None
try:
# 可能会引发异常的代码
res=result
except:
# 处理异常的代码
print('')
print(res)
# print(session_history)
return {"ui": {"text": [input_text],"num":[input_num]}, "result": (res,)}
+1 -1
View File
@@ -83,7 +83,7 @@ class ChatGPTNode:
}),
"model": (["gpt-3.5-turbo","gpt-35-turbo","gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-4-0613","gpt-4-1106-preview"],
{"default": "gpt-3.5-turbo"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
+17 -6
View File
@@ -35,6 +35,16 @@ if not os.path.exists(clipseg_model_dir):
"""Helper methods for CLIPSeg nodes"""
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def tensor_to_numpy(tensor: torch.Tensor) -> np.ndarray:
"""Convert a tensor to a numpy array and scale its values to 0-255."""
array = tensor.numpy().squeeze()
@@ -107,7 +117,7 @@ class CLIPSeg:
RETURN_NAMES = ("Mask","Heatmap Mask", "BW Mask")
# INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
OUTPUT_IS_LIST = (False,False,False,)
FUNCTION = "segment_image"
def segment_image(self, image: torch.Tensor, text: str, blur: float, threshold: float, dilation_factor: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -180,12 +190,13 @@ class CLIPSeg:
binary_mask_image = Image.fromarray(binary_mask_resized[..., 0])
# convert PIL image to numpy array
tensor_bw = binary_mask_image.convert("RGB")
tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
tensor_bw = torch.from_numpy(tensor_bw)[None,]
tensor_bw = tensor_bw.squeeze(0)[..., 0]
tensor_bw = binary_mask_image.convert("L")
tensor_bw=pil2tensor(tensor_bw)
# tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
# tensor_bw = torch.from_numpy(tensor_bw)[None,]
# tensor_bw = tensor_bw.squeeze(0)[..., 0]
return tensor_bw, image_out_heatmap, image_out_binary
return (tensor_bw, image_out_heatmap, image_out_binary,)
#OUTPUT_NODE = False
+561 -84
View File
@@ -1,8 +1,9 @@
import numpy as np
import requests
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
@@ -197,6 +198,24 @@ def load_image(fp,white_bg=False):
return images
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
content_type = response.headers.get('Content-Type')
image = Image.open(BytesIO(response.content))
# Create a mask from the image's alpha channel
mask = image.convert('RGBA').split()[-1]
# Convert the mask to a black and white image
mask = mask.convert('L')
image=image.convert('RGB')
return (image, mask)
# 获取图片s
def get_images_filepath(f,white_bg=False):
@@ -235,7 +254,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
# 对轮廓进行平滑
@@ -376,34 +418,119 @@ def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option)
return bg_image
def resize_image(layer_image,scale_option,width,height):
layer_image = layer_image.convert("RGB")
if scale_option == "height":
# 按照高度比例缩放
original_width, original_height = layer_image.size
scale = height / original_height
new_width = int(original_width * scale)
layer_image = layer_image.resize((new_width, height))
elif scale_option == "width":
# 按照宽度比例缩放
original_width, original_height = layer_image.size
scale = width / original_width
new_height = int(original_height * scale)
layer_image = layer_image.resize((width, new_height))
elif scale_option == "overall":
# 整体缩放
layer_image = layer_image.resize((width, height))
return layer_image
def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
# Load Chinese font
font = ImageFont.truetype(font_path, font_size)
# Calculate image size based on the number of characters and orientation
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
# # Load Chinese font
# font = ImageFont.truetype(font_path, font_size)
# # Calculate image size based on the number of characters and orientation
# if vertical:
# width = font_size + 100
# height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# else:
# width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# height = font_size + 100
# # Create a blank image
# image = Image.new('RGBA', (width, height), (255, 255, 255,0))
# draw = ImageDraw.Draw(image)
# # Draw text
# if vertical:
# for i, char in enumerate(text_list):
# char_position = (50, 50 + i * font_size)
# draw.text(char_position, char, font=font, fill=text_color)
# else:
# for i, char in enumerate(text_list):
# char_position = (50 + i * (font_size + spacing), 50)
# draw.text(char_position, char, font=font, fill=text_color)
# # Save the image
# # image.save(output_image_path)
# # 分离alpha通道
# alpha_channel = image.split()[3]
# # 创建一个只有alpha通道的新图像
# alpha_image = Image.new('L', image.size)
# alpha_image.putdata(alpha_channel.getdata())
# image=image.convert('RGB')
# return (image,alpha_image)
def generate_text_image(text, font_path, font_size, text_color, vertical=True, spacing=0):
# Split text into lines based on line breaks
lines = text.split("\n")
# 1. Determine layout direction
if vertical:
width = font_size + 100
height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
layout = "vertical"
else:
width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
height = font_size + 100
layout = "horizontal"
# Create a blank image
# 2. Calculate absolute coordinates for each character
char_coordinates = []
if layout == "vertical":
x = 0
y = 0
for i in range(len(lines)):
line=lines[i]
for char in line:
char_coordinates.append((x, y))
y += font_size + spacing
x += font_size + spacing
y = 0
# print(char_coordinates)
else:
x = 0
y = 0
for line in lines:
for char in line:
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
x = 0
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = (len(max(lines, key=len)) * (font_size + spacing)) + spacing
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = (len(lines) * (font_size + spacing)) + spacing
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(font_path, font_size)
index=0
for i, line in enumerate(lines):
for j, char in enumerate(line):
x, y = char_coordinates[index]
draw.text((x, y), char, font=font, fill=text_color)
index+=1
# Draw text
if vertical:
for i, char in enumerate(text_list):
char_position = (50, 50 + i * font_size)
draw.text(char_position, char, font=font, fill=text_color)
else:
for i, char in enumerate(text_list):
char_position = (50 + i * (font_size + spacing), 50)
draw.text(char_position, char, font=font, fill=text_color)
# Save the image
# image.save(output_image_path)
# 分离alpha通道
@@ -417,7 +544,6 @@ def generate_text_image(text_list, font_path, font_size, text_color, vertical=Tr
return (image,alpha_image)
def base64_to_image(base64_string):
# 去除前缀
prefix, base64_data = base64_string.split(",", 1)
@@ -434,6 +560,68 @@ def base64_to_image(base64_string):
return image
def create_temp_file(image,fn='material'):
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 [{
"filename": image_file,
"subfolder": subfolder,
"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
@@ -659,7 +847,7 @@ class TransparentImage:
# ui.images 节点里显示图片,和 传参,image_path自定义的数据,需要写节点的自定义ui
# result 里输出给下个节点的数据
print('TransparentImage',len(images_rgb))
# print('TransparentImage',len(images_rgb))
return {"ui":{"images": ui_images,"image_paths":image_paths},"result": (image_paths,images_rgb,images_rgba)}
@@ -736,7 +924,7 @@ class LoadImagesFromPath:
}
}
RETURN_TYPES = ('IMAGE','MASK','STRING')
RETURN_TYPES = ('IMAGE','MASK','STRING',)
FUNCTION = "run"
@@ -769,6 +957,11 @@ class LoadImagesFromPath:
images=get_images_filepath(file_path,white_bg=='enable')
# 当开启了监听,则取最新的,第一个文件
if watcher=='enable':
index_variable=0
newest_files='enable'
# 排序
sorted_files = sorted(images, key=lambda x: os.path.getmtime(x['file_path']), reverse=(newest_files=='enable'))
@@ -780,9 +973,13 @@ class LoadImagesFromPath:
masks.append(im['mask'])
# print('index_variable',index_variable)
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
try:
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
except Exception as e:
print("发生了一个未知的错误:", str(e))
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
@@ -822,12 +1019,14 @@ class ImageCropByAlpha:
class TextImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": False,"default": "龍馬精神迎新歲"}),
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲"}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH}),
"font_size": ("INT",{
"default":100,
@@ -848,7 +1047,7 @@ class TextImage:
},
}
RETURN_TYPES = ("IMAGE","MASK")
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
@@ -860,15 +1059,71 @@ 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_list,font_path,font_size,text_color,vertical,spacing)
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,spacing)
img=pil2tensor(img)
mask=pil2tensor(mask)
return (img,mask,)
class LoadImagesFromURL:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"url": ("STRING",{"multiline": True,"default": "https://","dynamicPrompts": False}),
},
}
RETURN_TYPES = ("IMAGE","MASK",)
RETURN_NAMES = ("images","masks",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,True,)
global urls_image
urls_image={}
def run(self,url):
global urls_image
print(urls_image)
def filter_http_urls(urls):
filtered_urls = []
for url in urls.split('\n'):
if url.startswith('http'):
filtered_urls.append(url)
return filtered_urls
filtered_urls = filter_http_urls(url)
images=[]
masks=[]
for img_url in filtered_urls:
try:
if img_url in urls_image:
img,mask=urls_image[img_url]
else:
img,mask=load_image_and_mask_from_url(img_url)
urls_image[img_url]=(img,mask)
img1=pil2tensor(img)
mask1=pil2tensor(mask)
images.append(img1)
masks.append(mask1)
except Exception as e:
print("发生了一个未知的错误:", str(e))
return (images,masks,)
class SvgImage:
@@ -906,29 +1161,41 @@ class Image3D:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("THREED",), },
"upload":("THREED",),},
"optional":{
"material": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE","MASK","IMAGE",)
RETURN_NAMES = ("IMAGE","MASK","BG_IMAGE",)
RETURN_TYPES = ("IMAGE","MASK","IMAGE","IMAGE",)
RETURN_NAMES = ("IMAGE","MASK","BG_IMAGE","MATERIAL",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,False,)
OUTPUT_IS_LIST = (False,False,False,False,)
OUTPUT_NODE = True
def run(self,upload):
# print(upload['image'])
def run(self,upload,material=None):
# print('material',material)
# print(upload )
image = base64_to_image(upload['image'])
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')
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)
@@ -937,7 +1204,12 @@ class Image3D:
mask=pil2tensor(mask)
image=pil2tensor(image)
return (image,mask,bg_image,)
m=[]
if not material is None:
m=create_temp_file(material[0])
return {"ui":{"material": m},"result": (image,mask,bg_image,mat,)}
@@ -1104,6 +1376,7 @@ class NewLayer:
"optional":{
"mask": ("MASK",{"default": None}),
"layers": ("LAYER",{"default": None}),
"canvas": ("IMAGE",{"default": None}),
}
}
@@ -1117,16 +1390,16 @@ class NewLayer:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self,x,y,width,height,z_index,scale_option,image,mask,layers):
def run(self,x,y,width,height,z_index,scale_option,image,mask=None,layers=None,canvas=None):
# print(x,y,width,height,z_index,image,mask)
if mask==None:
im=tensor2pil(image)
im=tensor2pil(image[0])
mask=im.convert('L')
mask=pil2tensor(mask)
else:
mask=mask[0]
layer_n=[{
"x":x[0],
"y":y[0],
@@ -1143,7 +1416,6 @@ class NewLayer:
return (layer_n,)
class ShowLayer:
@classmethod
def INPUT_TYPES(s):
@@ -1235,8 +1507,9 @@ class MergeLayers:
def INPUT_TYPES(s):
return {"required": {
"layers": ("LAYER",),
"image": ("IMAGE",),
"images": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
@@ -1249,46 +1522,250 @@ 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": {
"width": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"height": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
},
"optional":{
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,)
def run(self,width,height,scale_option,image=None):
w=width[0]
h=height[0]
scale_option=scale_option[0]
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,)
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)
+5 -3
View File
@@ -79,11 +79,13 @@ class ScreenShareNode:
def INPUT_TYPES(s):
return { "required":{
"image_base64": ("CHEESE",),
"refresh_rate": ("INT", {"default": 500, "min": 0,"step": 50, "max": 0xffffffffffffffff}),
},
"optional":{
"prompt": ("PROMPT",),
"slide": ("SLIDE",),
"seed": ("SEED",),
# "seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
} }
@@ -97,11 +99,11 @@ class ScreenShareNode:
OUTPUT_IS_LIST = (False,False,False,False)
# 运行的函数
def run(self,image_base64,prompt,slide,seed):
def run(self,image_base64,refresh_rate ,prompt,slide,seed):
im,mask=base64_save(image_base64)
# print('##########prompt',prompt)
return (im,prompt,slide,seed)
return {"ui":{"refresh_rate": [refresh_rate]},"result": (im,prompt,slide,seed,)}
class FloatingVideo:
@classmethod
+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": ()}
+449 -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 = {}
@@ -80,4 +124,406 @@ class FontInput:
def run(self,font):
return (font_files[font],)
return (font_files[font],)
class TextToNumber:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": False,"default": "1"}),
"random_number": (["enable", "disable"],),
"number":("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 10000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("INT",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text,random_number,number):
numbers = re.findall(r'\d+', text)
result=0
for n in numbers:
result = int(n)
# print(result)
if random_number=='enable' and result>0:
result= random.randint(1, 10000000000)
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.001, #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,)
class IntNumber:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("INT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number):
return (number,)
class MultiplicationNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"numberA":(any_type,),
"numberB":("FLOAT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
})
},
}
RETURN_TYPES = ("FLOAT","INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,numberA,numberB):
b=int(numberA*numberB)
a=float(numberA*numberB)
return (a,b,)
class TextInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": ""}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text):
return (text,)
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
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,)
OUTPUT_NODE = True
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,)}
class GetImageSize_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "get_size"
CATEGORY = "♾️Mixlab/utils"
def get_size(self, image):
_, height, width, _ = image.shape
return (width, height)
class SwitchByIndex:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"A":(any_type,),
"B":(any_type,),
"index":("INT", {
"default": -1,
"min": -1,
"max": 1000,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("C",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self, A,B,index):
C=[]
index=index[0]
for a in A:
C.append(a)
for b in B:
C.append(b)
if index>-1:
try:
C=[C[index]]
except Exception as e:
C=[]
return (C,)
class LimitNumber:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number":(any_type,),
"min_value":("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"max_value":("INT", {
"default": 1,
"min": 1,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("number",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self, number, min_value, max_value):
nn=number
if isinstance(number, int):
min_value=int(min_value)
max_value=int(max_value)
if isinstance(number, float):
min_value=float(min_value)
max_value=float(max_value)
if number < min_value:
nn= min_value
elif number > max_value:
nn= max_value
return (nn,)
+2 -2
View File
@@ -145,7 +145,7 @@ class VAELoader:
RETURN_TYPES = ("VAE",)
FUNCTION = "load_vae"
CATEGORY = "♾️Mixlab/ConsistencyDecoder"
CATEGORY = "♾️Mixlab/_test"
#TODO: scale factor?
def load_vae(self, vae_name):
@@ -165,7 +165,7 @@ class VAEDecode:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "decode"
CATEGORY = "♾️Mixlab/ConsistencyDecoder"
CATEGORY = "♾️Mixlab/_test"
def decode(self, vae, samples):
image = vae.decode(samples["samples"].to("cuda:0"))
+2 -1
View File
@@ -3,4 +3,5 @@ pyOpenSSL
watchdog
opencv-python-headless
matplotlib
openai
openai
# playwright
+914
View File
@@ -0,0 +1,914 @@
<!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;
width: 80%;
}
.description p {
max-width: 200px;
word-wrap: break-word;
}
.panel {
display: flex;
flex-direction: column;
min-width: 400px;
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,
video {
height: fit-content;
width: fit-content;
max-width: 100%;
margin-left: 12px;
}
.input_card {
display: flex;
flex-direction: column;
}
.output_card {
height: 100%;
width: 100%;
box-shadow: 0px 0px 8px 3px #e6e7e7;
display: flex;
/* justify-content: center;
align-items: center; */
}
.output_card img,
video {
max-width: 400px;
max-height: 600px;
}
.card {
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;
resize: vertical;
overflow: hidden;
}
.card img {
width: 100%;
margin-top: 12px;
}
.card .select {
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;
}
.show_text {
font-size: 14px;
/* display: inline-block; */
/* margin: 13px; */
padding: 32px;
background: #242424;
color: white;
}
.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) {
console.log('output', node)
if (node.class_type == "ShowTextForGPT") {
let div = document.createElement('div');
div.className = "show_text"
div.id = `output_${node.id}`;
div.innerText = node.inputs.text[0]
container.appendChild(div);
};
if (["SaveImage", "PreviewImage"].includes(node.class_type)) {
let img = new Image();
img.id = `output_${node.id}`;
img.src = base64Df;
container.appendChild(img);
}
// video ,gif
if (["VHS_VideoCombine"].includes(node.class_type)) {
let v = document.createElement('div');
let video = document.createElement('video'), img = new Image();
video.style.display = 'none'
video.controls = 'true'
video.autoplay = 'true'
video.loop = 'true'
v.id = `output_${node.id}`;
img.src = base64Df;
v.appendChild(video);
v.appendChild(img);
container.appendChild(v);
}
}
return container
}
async function calculateImageHash(blob) {
const buffer = await blob.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
return hashHex;
}
async function handleClipboardImage(imageElement, data) {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
if (type.startsWith('image/')) {
const fileBlob = await clipboardItem.getType(type);
// // 获取读取的文件内容,即 Blob 对象
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
let { url, name } = await uploadImage(fileBlob);
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
// const img = document.createElement('img');
// img.src = URL.createObjectURL(blob);
// document.body.appendChild(img);
// console.log( URL.createObjectURL(blob));
}
}
}
} catch (error) {
console.error('无法读取剪贴板中的图片:', error);
}
}
function createInputs(inputData) {
// Assuming you have an HTML element with the id "container" to hold the UI
const container = document.createElement("div");
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);
let actionDiv = document.createElement('div');
// Create an input field for the image name
const uploadImageInput = document.createElement("button");
uploadImageInput.style = `width: 88px;`;
uploadImageInput.innerText = 'upload'
const uploadImageInputHide = document.createElement('input');
uploadImageInputHide.type = "file";
uploadImageInputHide.style.display = "none"
actionDiv.appendChild(uploadImageInput);
actionDiv.appendChild(uploadImageInputHide);
const btnFromClipboard = document.createElement("button");
btnFromClipboard.style = `width: 156px;
height: 24px;
margin-left: 18px;`
btnFromClipboard.innerText = 'paste from clipboard'
actionDiv.appendChild(btnFromClipboard);
uploadContainer.appendChild(actionDiv)
// Create an image element to display the uploaded image
const imageElement = document.createElement("img");
imageElement.src = base64Df
imageElement.style.maxWidth = '200px';
btnFromClipboard.addEventListener('click', (event) => handleClipboardImage(imageElement, data));
uploadImageInput.addEventListener('click', (event) => {
uploadImageInputHide.click()
})
uploadImageInputHide.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 hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
let { url, name } = await uploadImage(fileBlob)
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
};
// 开始读取文件
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 (['FloatSlider', 'IntNumber'].includes(data.class_type)) {
// 滑块输入
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 (["TextInput_", "CLIPTextEncode"].includes(data.class_type)) {
// 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 textInput = document.createElement("textarea");
// uploadImageInput.type = "text";
textInput.value = data.inputs.text;
uploadContainer.appendChild(textInput);
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
textInput.addEventListener('input', (event) => {
// console.log(textInput.value)
autoResize(textInput);
window._appData.data[data.id].inputs.text = textInput.value;
})
// Append the upload container to the main container
container.appendChild(uploadContainer);
}
if (["CheckpointLoaderSimple", "LoraLoader"].includes(data.class_type)) {
let value = data.inputs.ckpt_name || data.inputs.lora_name;
let [div, selectDom] = createSelectWithOptions(data.title, Array.from(data.options, o => {
return {
value: o,
text: o
}
}), value);
selectDom.addEventListener('change', e => {
e.preventDefault();
// console.log(selectDom.value)
if (data.class_type === 'CheckpointLoaderSimple') {
window._appData.data[data.id].inputs.ckpt_name = selectDom.value;
}
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = selectDom.value;
}
})
container.appendChild(div);
}
});
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 createSelectWithOptions(title, options, defaultValue) {
const div = document.createElement("div");
div.className = 'card';
// Create a label for the upload control
const nameLabel = document.createElement("label");
nameLabel.textContent = title;
div.appendChild(nameLabel);
var selectElement = document.createElement("select");
selectElement.className = "select"
// 循环遍历选项数组
for (var i = 0; i < options.length; i++) {
var option = document.createElement("option");
option.value = options[i].value;
option.text = options[i].text;
selectElement.appendChild(option);
}
// 设置默认值
selectElement.value = defaultValue;
div.appendChild(selectElement)
return [div, selectElement];
}
function getTypeFromUrl(url) {
const queryString = url.split('?')[1];
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.height='70vh'
// 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 (type = "image", val, id) {
console.log(val, id)
if (type == "image" && output.querySelector(`#output_${id}`)) {
if (output.querySelector(`#output_${id} img`)) {
output.querySelector(`#output_${id} img`).src = val;
} else {
output.querySelector(`#output_${id}`).src = val;
}
}
if (type == "video" && output.querySelector(`#output_${id}`)) {
let video = output.querySelector(`#output_${id} video`);
let img = output.querySelector(`#output_${id} img`);
img.style.display = 'none';
video.style.display = 'block';
video.src = val;
}
if (type == "text" && output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).innerText = val;
}
},
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, type = "image") => {
// console.log(src)
ui.output.update(type, 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;
const text = detail?.output?.text;
const gifs = detail?.output?.gifs;
if (images) {
// if (!images) return;
const src = `${get_url()}/view?filename=${encodeURIComponent(images[0].filename)}&type=${images[0].type}&subfolder=${encodeURIComponent(images[0].subfolder)}&t=${+new Date()}`;
show(src, detail.node, 'image');
} else if (text && text[0]) {
ui.output.update("text", text[0], detail.node)
} else if (gifs && gifs[0]) {
// if (!images) return;
const src = `${get_url()}/view?filename=${encodeURIComponent(gifs[0].filename)}&type=${gifs[0].type}&subfolder=${encodeURIComponent(gifs[0].subfolder)
}&&format=${gifs[0].format}&t=${+new Date()}`;
show(src, detail.node, gifs[0].format.match('video') ? 'video' : 'image');
}
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>
+627
View File
@@ -0,0 +1,627 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function getContentTypeFromBase64 (base64Data) {
const regex = /^data:(.+);base64,/
const matches = base64Data.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
}
return null
}
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
const parseImage = url => {
return new Promise((res, rej) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result
res(base64data)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
.catch(error => {
console.log('发生错误:', error)
})
})
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // 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: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
async function extractMaterial (
modelViewerVariants,
selectMaterial,
material_img
) {
// 材质
const materialsNames = []
for (
let index = 0;
index < modelViewerVariants.model.materials.length;
index++
) {
let m = modelViewerVariants.model.materials[index]
let thumbUrl
try {
thumbUrl =
await m.pbrMetallicRoughness.baseColorTexture.texture.source.createThumbnail(
1024,
1024
)
} catch (error) {}
if (thumbUrl)
materialsNames.push({
value: m.name,
text: `#${index} ${m.name}`,
index,
thumbUrl
})
}
selectMaterial.innerHTML = ''
material_img.innerHTML = ''
for (let index = 0; index < materialsNames.length; index++) {
const name = materialsNames[index]
const option = document.createElement('option')
option.value = name.thumbUrl
option.textContent = name.text
option.setAttribute('data-index', index)
selectMaterial.appendChild(option)
let img = new Image()
img.src = name.thumbUrl
// img.setAttribute('data-index',name.index)
img.style.width = '40px'
material_img.appendChild(img)
if (index == 0) {
material_img.setAttribute('src', name.thumbUrl)
}
}
}
async function changeMaterial (
modelViewerVariants,
targetMaterial,
newImageUrl
) {
const targetTexture = await modelViewerVariants.createTexture(newImageUrl)
// 用图片创建纹理
targetMaterial.pbrMetallicRoughness.baseColorTexture.setTexture(targetTexture)
}
app.registerExtension({
name: 'Mixlab.3D.3DImage',
async getCustomWidgets (app) {
return {
THREED (node, inputName, inputData, app) {
// console.log('##node', node, inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 88], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 88] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let data = {}
if (url) {
data.image = await parseImage(url)
}
if (bg) {
data.bg_image = await parseImage(bg)
}
if (material) {
data.material = await parseImage(material)
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
}
}
}
node.addCustomWidget(widget)
return widget
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == '3DImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 88, node.size[1])
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
ip.addEventListener('change', async event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
touch-action="pan-y">
<div class="controls">
<div>Variant: <select class="variant"></select></div>
<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
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
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`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
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 }
} 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)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute(
'data-index'
)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
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()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
}
// 以文本形式读取文件
reader.readAsDataURL(file)
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
widget.div.appendChild(upload)
widget.div.appendChild(preview)
this.addCustomWidget(widget)
const onResize = this.onResize
let that = this
this.onResize = function () {
let modelViewerVariants = preview.querySelector('model-viewer')
// 更新尺寸
let dd = getLocalData('_mixlab_3d_image')
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
return onResize?.apply(this, arguments)
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
preview.remove()
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
// this.isVirtualNode = true
this.serialize_widgets = false //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const r = onExecuted?.apply?.(this, arguments)
let div = this.widgets.filter(d => d.div)[0]?.div
console.log('Test', this.widgets)
let material = message.material[0]
if (material) {
const { filename, subfolder, type } = material
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
filename
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
const modelViewerVariants = div.querySelector('model-viewer')
const selectMaterial = div.querySelector('.material')
let index =
~~selectMaterial.selectedOptions[0].getAttribute('data-index')
selectMaterial.setAttribute('data-new-material', src)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
src
)
}
this.onResize?.(this.size)
return r
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
if (node.type === '3DImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'upload-preview')[0]
let dd = getLocalData('_mixlab_3d_image')
let id = node.id
// console.log('3dImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let { url, bg } = dd[id]
if (!url) return
// let base64 = await parseImage(url)
let pre = widget.div.querySelector('.preview')
pre.style.width = `${node.size[0]}px`
pre.innerHTML = `
${url ? `<img src="${url}" style="width:100%"/>` : ''}
`
pre.style.backgroundImage = 'url(' + bg + ')'
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
}
}
})
+298
View File
@@ -0,0 +1,298 @@
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)
let options = []
// 模型
try {
if (node.type === 'CheckpointLoaderSimple') {
options = node.widgets.filter(w => w.name === 'ckpt_name')[0]
.options.values
}else if(node.type === 'LoraLoader'){
options =node.widgets.filter(w=>w.name==='lora_name')[0].options.values
}
} catch (error) {}
input[inputIds.indexOf(id)] = {
...data[id],
title: node.title,
id,
options
}
// input.push()
}
if (outputIds.includes(id)) {
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) {}
}
}
}
})
+102 -15
View File
@@ -68,9 +68,8 @@ function speakText (text) {
// speakText('Hello, how are you?');
// #MixCopilot
const start = (element, id, startBtn) => {
startBtn.className='loading_mixlab'
const start = (element, id, startBtn, node) => {
startBtn.className = 'loading_mixlab'
window.recognition = new webkitSpeechRecognition()
@@ -95,15 +94,26 @@ const start = (element, id, startBtn) => {
localStorage.setItem('_mixlab_speech_recognition', JSON.stringify(data))
if (timeoutId) clearTimeout(timeoutId)
if (!window.recognition) return
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=''
window.recognition = null
startBtn.className = ''
startBtn.innerText = 'START'
timeoutId = null
@@ -114,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)
@@ -171,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])
)
}
}
@@ -189,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}`
@@ -201,13 +226,18 @@ app.registerExtension({
margin: 0px 8px 6px;`
startBtn.style = `
outline: none;
border: none;
padding: 4px; `
margin-top:48px;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);
`
startBtn.innerText = 'START'
div.appendChild(startBtn)
// div.appendChild(sendTo);
div.appendChild(textArea)
startBtn.addEventListener('click', () => {
@@ -215,13 +245,17 @@ app.registerExtension({
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className=''
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
}
@@ -239,16 +273,69 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
// const onGraphConfigured=nodeType.prototype.onGraphConfigured;
// nodeType.prototype.onGraphConfigured = function (message) {
// onGraphConfigured?.apply(this, arguments)
// console.log('###SpeechRecognition onGraphConfigured',this,message)
// }
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')
if (open && !window.recognition) {
start(textArea, this.id, startBtn, this)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className = ''
}
}
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'SpeechRecognition') {
let data = getLocalData('_mixlab_speech_recognition')
// console.log('_mixlab_speech_recognition', node.widgets)
// console.log('_mixlab_speech_recognition', node )
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')
if (open && !window.recognition) {
start(textArea, node.id, startBtn, node)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className = ''
}
}
} 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.1'
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
+10 -391
View File
@@ -3,9 +3,7 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
@@ -195,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元素
@@ -358,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 = ''
@@ -403,7 +406,9 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
}
};
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
@@ -426,7 +431,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 = ''
@@ -434,392 +439,6 @@ app.registerExtension({
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
// let h=~~getComputedStyle(widget.div).height.replace('px','');
// let w=~~getComputedStyle(widget.div).width.replace('px','');
// // console.log('svg', w,h,node.size)
// node.setSize([
// w,h
// ])
// app.graph.setDirtyCanvas(true)
// console.log(node.widgets_values)
}
}
})
app.registerExtension({
name: 'Mixlab.image.3DImage',
async getCustomWidgets (app) {
return {
THREED (node, inputName, inputData, app) {
// console.log('##node', node, inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 88], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 88] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue',d)
if (d && d[node.id]) {
let { url, bg } = d[node.id]
let base64 = await parseImage(url)
let bg_base64 = await parseImage(bg)
return JSON.parse(
JSON.stringify({ image: base64, bg_image: bg_base64 })
)
} else {
return {}
}
}
}
node.addCustomWidget(widget)
return widget
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == '3DImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
// console.log('3d nodeData', this.inputs)
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 88, node.size[1])
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
ip.addEventListener('change', event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
touch-action="pan-y">
<div class="controls">
<div>Variant: <select class="variant"></select></div>
<div><button class="bg">BG</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
};
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const bg = preview.querySelector('.bg')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', () => {
const names = modelViewerVariants.availableVariants
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let w, h
let base64Data = modelViewerVariants.toDataURL()
// if (dd[that.id]) {
// w = dd[that.id].bg_w
// h = dd[that.id].bg_h
// }
// // 在这里触发相机停止变化的事件
// // console.log('在这里触发相机停止变化的事件')
// // let base64Data = modelViewerVariants.toDataURL()
// if (w && h) {
// base64Data = await exportModelViewerImage(
// modelViewerVariants.displaycanvas,
// w,
// h
// )
// } else {
// }
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
if (!dd[that.id]) dd[that.id] = { url, bg: '' }
dd[that.id] = { ...dd[that.id], url }
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
}
// 以文本形式读取文件
reader.readAsDataURL(file)
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
widget.div.appendChild(upload)
widget.div.appendChild(preview)
this.addCustomWidget(widget)
const onResize = this.onResize
let that=this;
this.onResize = function () {
let modelViewerVariants = preview.querySelector('model-viewer')
// 更新尺寸
let dd = getLocalData('_mixlab_3d_image')
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
return onResize?.apply(this, arguments)
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
preview.remove()
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
if (node.type === '3DImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'upload-preview')[0]
let dd = getLocalData('_mixlab_3d_image')
let id = node.id
// console.log('3dImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let { url, bg } = dd[id]
if (!url) return
// let base64 = await parseImage(url)
let pre = widget.div.querySelector('.preview')
pre.style.width = `${node.size[0]}px`
pre.innerHTML = `
${url ? `<img src="${url}" style="width:100%"/>` : ''}
`
pre.style.backgroundImage = 'url(' + bg + ')'
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
// let h=~~getComputedStyle(widget.div).height.replace('px','');
// let w=~~getComputedStyle(widget.div).width.replace('px','');
// // console.log('svg', w,h,node.size)
// node.setSize([
// w,h
// ])
// app.graph.setDirtyCanvas(true)
// console.log(node.widgets_values)
}
}
})
+279 -20
View File
@@ -69,12 +69,12 @@ const parseSvg = async svgContent => {
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
var x = ~~(rectElement.getAttribute('x')||0);
var y = ~~(rectElement.getAttribute('y')||0);
var x = ~~(rectElement.getAttribute('x') || 0)
var y = ~~(rectElement.getAttribute('y') || 0)
var width = ~~rectElement.getAttribute('width')
var height = ~~rectElement.getAttribute('height')
// console.log('rectElements',rectElement,x,y,width,height)
if (x != undefined && y != undefined&&width&&height) {
if (x != undefined && y != undefined && width && height) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
@@ -101,7 +101,7 @@ const parseSvg = async svgContent => {
image: base64,
mask: base64,
type: 'base64',
_t:'rect'
_t: 'rect'
}
// 将处理后的数据添加到数组中
@@ -115,9 +115,9 @@ const parseSvg = async svgContent => {
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth =viewBox.width
svgHeight =viewBox.height
svgWidth = viewBox.width
svgHeight = viewBox.height
}
// 创建一个新的canvas元素
@@ -147,15 +147,186 @@ const parseSvg = async svgContent => {
image: base64,
mask: base64,
type: 'base64',
_t:'canvas'
_t: 'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log('layers',{ data, image: base64, svgElement })
console.log('layers', { data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
async function setArea (cw, ch, topBase64, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.8)
let div = document.createElement('div')
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
z-index:999999;
width: 100%;'>
<img id='ml_video' style='position: absolute;
height: ${displayHeight}px;user-select: none;
-webkit-user-drag: none;
outline: 2px solid #eaeaea;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
pointer-events: none;
background-image: url("${topBase64}");
background-repeat: no-repeat;
background-size: cover;
'></div>
<div class="mx_close"> X </div>
</div>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
// let canvas = document.createElement('canvas')
// canvas.width = cw
// canvas.height = ch
let img = div.querySelector('#ml_video')
// let overlay = div.querySelector('#ml_overlay')
let selection = div.querySelector('#ml_selection')
let close = div.querySelector('.mx_close')
let startX, startY, endX, endY
let start = false
let setDone = false
// Set video source
img.src = base64
// canvas.toDataURL();
close.style = `cursor: pointer;
position: fixed;
left: 12px;
top: 12px;
z-index: 99999999;
background: black;
width: 44px;
height: 44px;
text-align: center;
line-height: 44px;`
// init area
// const data = getSetAreaData()
let x = 0,
y = 0,
width = (cw * displayHeight) / ch,
height = displayHeight
let imgWidth = cw
let imgHeight = ch
if (data && data.width > 0 && data.height > 0) {
// 相同尺寸窗口,恢复选区
x = (width * data.x) / imgWidth
y = (height * data.y) / imgHeight
width = (width * data.width) / imgWidth
height = (height * data.height) / imgHeight
}
selection.style.left = x + 'px'
selection.style.top = y + 'px'
selection.style.width = width + 'px'
selection.style.height = height + 'px'
// Add mouse events
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
const removeDiv = () => {
div.remove()
close.removeEventListener('click', removeDiv)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
img.removeEventListener('mousedown', setDoneCheck)
}
close.addEventListener('click', removeDiv)
const setDoneCheck = event => {
console.log(setDone)
if (setDone) {
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
setDone = false
start = false
startX = event.clientX
startY = event.clientY
}
}
img.addEventListener('mousedown', setDoneCheck)
function remove () {
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
setDone = true
// div.remove()
}
function startSelection (event) {
if (start == false) {
startX = event.clientX
startY = event.clientY
updateSelection(event)
start = true
} else {
}
}
function updateSelection (event) {
endX = event.clientX
endY = event.clientY
// Calculate width, height, and coordinates
let width = Math.abs(endX - startX)
let height = Math.abs(endY - startY)
let left = Math.min(startX, endX)
let top = Math.min(startY, endY)
// Set selection style
selection.style.left = left + 'px'
selection.style.top = top + 'px'
selection.style.width = width + 'px'
selection.style.height = height + 'px'
}
function endSelection (event) {
endX = event.clientX
endY = event.clientY
// 获取img元素的真实宽度和高度
let imgWidth = img.naturalWidth
let imgHeight = img.naturalHeight
// 换算起始坐标
let realStartX = (startX / img.offsetWidth) * imgWidth
let realStartY = (startY / img.offsetHeight) * imgHeight
// 换算起始坐标
let realEndX = (endX / img.offsetWidth) * imgWidth
let realEndY = (endY / img.offsetHeight) * imgHeight
startX = realStartX
startY = realStartY
endX = realEndX
endY = realEndY
// Calculate width, height, and coordinates
let width = Math.round(Math.abs(endX - startX))
let height = Math.round(Math.abs(endY - startY))
let left = Math.round(Math.min(startX, endX))
let top = Math.round(Math.min(startY, endY))
if (width <= 0 && height <= 0) return remove()
if (fn) fn(left, top, width, height)
remove()
}
}
app.registerExtension({
name: 'Mixlab.layer.ShowLayer',
async getCustomWidgets (app) {
@@ -199,8 +370,7 @@ app.registerExtension({
const findNode = nodeId => {
let node = app.graph._nodes_by_id[nodeId]
if (node?.type == 'Reroute') {
let linkId =node.inputs.filter(i=>i.type=='*')[0].link
let linkId = node.inputs.filter(i => i.type == '*')[0].link
nodeId = app.graph.links.filter(link => link.id == linkId)[0]
?.origin_id
return findNode(nodeId)
@@ -211,15 +381,17 @@ app.registerExtension({
// 获取layers数据
const getLayers = async () => {
console.log('getLayers1',this.inputs.filter(ip => ip.name === 'layers'))
console.log(
'getLayers1',
this.inputs.filter(ip => ip.name === 'layers')
)
let linkId = this.inputs.filter(ip => ip.name === 'layers')[0].link
let nodeId = app.graph.links?.filter(link => link.id == linkId)[0]
?.origin_id;
?.origin_id
if(nodeId){
if (nodeId) {
nodeId = findNode(nodeId)
}
// let node = app.graph._nodes_by_id[nodeId]
// if (node?.type == 'Reroute') {
@@ -227,18 +399,18 @@ app.registerExtension({
// nodeId = app.graph.links.filter(link => link.id == linkId)[0]
// ?.origin_id
// }
let d = getLocalData('_mixlab_svg_image')
console.log('test',d[nodeId])
console.log('test', d[nodeId])
if (d[nodeId]) {
let url = d[nodeId]
let dt = await fetch(url)
let svgStr = await dt.text()
const { data } = (await parseSvg(svgStr)) || {}
console.log('fetch',data)
console.log('fetch', data)
return data
} else {
return []
@@ -350,3 +522,90 @@ app.registerExtension({
}
}
})
app.registerExtension({
name: 'Mixlab.layer.NewLayer',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'NewLayer') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
let b = this.widgets.filter(w => w.type === 'button')[0]
// const [w, h, base64] = canvas
if (!b) {
const updateValue = (x1, y1, w1, h1) => {
if (this.widgets) {
for (const widget of this.widgets) {
if (widget.name === 'x') {
widget.value = x1
}
if (widget.name === 'y') {
widget.value = y1
}
if (widget.name === 'width') {
widget.value = w1
}
if (widget.name === 'height') {
widget.value = h1
}
}
}
}
this.addWidget('button', 'Set Area', '', () => {
let data = {}
for (const widget of this.widgets) {
if (widget.name === 'x') {
data.x = widget.value
}
if (widget.name === 'y') {
data.y = widget.value
}
if (widget.name === 'width') {
data.width = widget.value
}
if (widget.name === 'height') {
data.height = widget.value
}
}
try {
console.log('this.inputs', this.inputs)
let topLinkId = this.inputs[0].link
let topNodeId = app.graph.links[topLinkId].origin_id
let topIm = app.graph.getNodeById(topNodeId).imgs[0]
let linkId = this.inputs[3].link
let nodeId = app.graph.links[linkId].origin_id
// console.log(linkId,this.inputs)
let im = app.graph.getNodeById(nodeId).imgs[0]
// let src = im.src
setArea(
im.naturalWidth,
im.naturalHeight,
topIm.src,
im.src,
data,
updateValue
)
} catch (error) {}
})
}
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
// let b = this.widgets.filter(w => w.type === 'button')[0];
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
})
+45 -58
View File
@@ -461,7 +461,7 @@ async function requestCamera () {
/*
A method that returns the required style for the html
*/
function get_position_style (ctx, widget_width, y, node_height) {
function get_position_style (ctx, widget_width, y, node_height, top) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
@@ -478,7 +478,7 @@ function get_position_style (ctx, widget_width, y, node_height) {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
top: `${top}px`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
@@ -585,9 +585,6 @@ app.registerExtension({
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ScreenShare') {
/*
Hijack the onNodeCreated call to add our widget
*/
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
@@ -603,7 +600,8 @@ app.registerExtension({
ctx,
widget_width,
widget_height * 5,
node.size[1]
node.size[1],
40
)
)
}
@@ -693,22 +691,22 @@ app.registerExtension({
}
})
widget.refreshInput = $el('input', {
placeholder: ' Refresh rate:200 ms',
type: 'number',
min: 100,
step: 100,
style: {
cursor: 'pointer',
padding: '8px 24px',
fontWeight: '300',
margin: '2px',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
}
});
widget.refreshInput.className='comfy-multiline-input'
// widget.refreshInput = $el('input', {
// placeholder: ' Refresh rate:200 ms',
// type: 'number',
// min: 100,
// step: 100,
// style: {
// cursor: 'pointer',
// padding: '8px 24px',
// fontWeight: '300',
// margin: '2px',
// color: 'var(--descrip-text)',
// backgroundColor: 'var(--comfy-input-bg)'
// }
// });
// widget.refreshInput.className='comfy-multiline-input'
widget.liveBtn = $el('button', {
innerText: 'Live Run',
style: {
@@ -734,7 +732,7 @@ app.registerExtension({
widget.shareDiv.appendChild(widget.shareBtn)
widget.shareDiv.appendChild(widget.shareOfWebCamBtn)
widget.card.appendChild(widget.openFloatingWinBtn)
widget.card.appendChild(widget.refreshInput)
// widget.card.appendChild(widget.refreshInput)
widget.card.appendChild(widget.liveBtn)
const toggleShare = async (isCamera = false) => {
@@ -894,11 +892,11 @@ app.registerExtension({
toggleShare()
})
widget.refreshInput.addEventListener('change', async () => {
window._mixlab_screen_refresh_rate = Math.round(
widget.refreshInput.value
)
})
// widget.refreshInput.addEventListener('change', async () => {
// window._mixlab_screen_refresh_rate = Math.round(
// widget.refreshInput.value
// )
// })
widget.liveBtn.addEventListener('click', async () => {
if (window._mixlab_stopLive) {
@@ -939,12 +937,21 @@ app.registerExtension({
widget.shareBtn.remove()
widget.liveBtn.remove()
widget.card.remove()
widget.refreshInput.remove()
// widget.refreshInput.remove()
widget.previewArea.remove()
widget.previewCard.remove()
}
this.serialize_widgets = true
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// console.log('###ScreenShare', this, message.refresh_rate)
window._mixlab_screen_refresh_rate = Math.round(
message.refresh_rate[0] || 500
)
}
}
}
})
@@ -1036,6 +1043,7 @@ async function setArea (src) {
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
z-index:999999;
width: 100%;'>
<img id='ml_video' style='position: absolute;
height: ${displayHeight}px;user-select: none;
@@ -1067,12 +1075,7 @@ async function setArea (src) {
height = displayHeight
let imgWidth = im.naturalWidth
let imgHeight = im.naturalHeight
// console.log(
// '#screen_share::使用上一次选区 selection',
// data,
// imgWidth,
// img.width
// )
if (
data &&
data.width > 0 &&
@@ -1086,9 +1089,6 @@ async function setArea (src) {
y = (img.height * data.y) / data.imgHeight
width = (img.width * data.width) / data.imgWidth
height = (img.height * data.height) / data.imgHeight
// imgWidth = data.imgWidth
// imgHeight = data.imgHeight;
// console.log('#screen_share::使用上一次选区 selection', x, y, width, height)
}
selection.style.left = x + 'px'
@@ -1153,9 +1153,6 @@ async function setArea (src) {
let realEndX = (endX / img.offsetWidth) * imgWidth
let realEndY = (endY / img.offsetHeight) * imgHeight
// 输出结果到控制台
// console.log('真实宽度: ' + realWidth)
// console.log('真实高度: ' + realHeight)
startX = realStartX
startY = realStartY
endX = realEndX
@@ -1165,19 +1162,6 @@ async function setArea (src) {
let height = Math.abs(endY - startY)
let left = Math.min(startX, endX)
let top = Math.min(startY, endY)
// Output results to console
// console.log('坐标位置: (' + left + ', ' + top + ')')
// console.log('宽度: ' + width)
// console.log('高度: ' + height)
// img.removeEventListener('mousedown', startSelection)
// img.removeEventListener('mousemove', updateSelection)
// img.removeEventListener('mouseup', endSelection)
// window._mixlab_screen_x = left
// window._mixlab_screen_y = top
// window._mixlab_screen_width = width
// window._mixlab_screen_height = height
if (width <= 0 && height <= 0) return remove()
@@ -1189,7 +1173,6 @@ async function setArea (src) {
window._mixlab_screen_webcamVideo,
!window._mixlab_screen_live
)
remove()
}
}
@@ -1238,7 +1221,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.card.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(ctx, widget_width, y, node.size[1], 0)
)
}
}
@@ -1817,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
+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) {}
}
}
}
})
+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
}
}
})
+120 -107
View File
@@ -1,10 +1,8 @@
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";
const getLocalData = key => {
let data = {}
@@ -47,122 +45,137 @@ function get_position_style (ctx, widget_width, y, node_height) {
}
}
app.registerExtension({
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
return {
TCOLOR (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color')
return data[node.id] || '#000000'
}
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
return {
TCOLOR (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color')
return data[node.id] || '#000000'
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'Color') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('Color nodeData', this.widgets)
const widget = {
type: 'div',
name: 'input_color',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width,
44,
node.size[1]
)
)
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'Color') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('Color nodeData', this.widgets)
const widget = {
type: 'div',
name: 'input_color',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, value) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'color'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, value) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'color'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = value
ip.style = `outline: none;
ip.placeholder = placeholder
ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 100%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
// console.log(this.id, ip.value.trim())
})
return div
}
let inputColor = inputDiv('_mixlab_utils_color', 'Color', '#000000')
widget.div.appendChild(inputColor)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputColor.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
// console.log(this.id, ip.value.trim())
})
return div
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'Color') {
let widget = node.widgets.filter(w => w.div)[0]
let data = getLocalData('_mixlab_utils_color')
let id = node.id
widget.div.querySelector('.Color').value = data[id] || '#000000'
let inputColor = inputDiv('_mixlab_utils_color', 'Color', '#000000')
widget.div.appendChild(inputColor)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputColor.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
})
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'Color') {
let widget = node.widgets.filter(w => w.div)[0]
let data = getLocalData('_mixlab_utils_color')
let id = node.id
widget.div.querySelector('.Color').value = data[id] || '#000000'
}
}
})
app.registerExtension({
name: 'Mixlab.utils.TextToNumber',
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]
}
console.log('TextToNumber', random_number.value)
}
}
}
})
+10 -9
View File
@@ -295,7 +295,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
1115769645491668,
482859286431021,
"randomize",
4,
1.6,
@@ -479,10 +479,10 @@
1928,
295
],
"size": {
"0": 315,
"1": 58
},
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
@@ -507,10 +507,10 @@
-65,
446
],
"size": {
"0": 315,
"1": 170
},
"size": [
312.78457519531213,
606.2132135620109
],
"flags": {},
"order": 3,
"mode": 0,
@@ -552,6 +552,7 @@
},
"widgets_values": [
null,
2018,
null,
null,
null,
+67 -100
View File
File diff suppressed because one or more lines are too long
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 it is too large Load Diff
+828
View File
@@ -0,0 +1,828 @@
{
"last_node_id": 23,
"last_link_id": 0,
"nodes": [
{
"id": 7,
"type": "ResizeImageMixlab",
"pos": [
1096.229866976564,
731.8207909156248
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ResizeImageMixlab"
},
"widgets_values": [
512,
512,
"width"
]
},
{
"id": 8,
"type": "LoadImagesFromPath",
"pos": [
1101.229866976564,
900.8207909156248
],
"size": {
"0": 315,
"1": 238
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 6
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 6
},
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImagesFromPath"
},
"widgets_values": [
"C:\\Users\\38957\\Documents\\GitHub\\extract-anything\\outputs",
"disable",
"enable",
0,
"disable",
null,
null
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
1786,
252
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
""
]
},
{
"id": 9,
"type": "TextImage",
"pos": [
1448.4608623625004,
735.0514200906249
],
"size": {
"0": 315,
"1": 198
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "TextImage"
},
"widgets_values": [
"龍馬精神迎新歲",
"C:\\Users\\38957\\Documents\\ai-lab\\ComfyUI_windows_portable\\ComfyUI\\custom_nodes\\comfyui-mixlab-nodes\\assets\\王汉宗颜楷体繁.ttf",
100,
12,
"#000000",
true
]
},
{
"id": 2,
"type": "ChatGPTOpenAI",
"pos": [
1086,
235
],
"size": {
"0": 400,
"1": 358
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": null,
"shape": 3
},
{
"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",
0,
"randomize",
1,
null
]
},
{
"id": 16,
"type": "SpeechRecognition",
"pos": [
2892.502226325001,
724.176680771093
],
"size": {
"0": 322.593505859375,
"1": 135.22850036621094
},
"flags": {},
"order": 5,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
null
]
},
{
"id": 17,
"type": "SpeechSynthesis",
"pos": [
2901.502226325001,
924.1766807710948
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
""
]
},
{
"id": 18,
"type": "ShowLayer",
"pos": [
2157.913220668749,
232.7887044208986
],
"size": {
"0": 315,
"1": 226
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "layers",
"type": "LAYER",
"link": null
}
],
"properties": {
"Node name for S&R": "ShowLayer"
}
},
{
"id": 19,
"type": "NewLayer",
"pos": [
2494.913220668749,
232.7887044208986
],
"size": {
"0": 315,
"1": 218
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
},
{
"name": "mask",
"type": "MASK",
"link": null
},
{
"name": "layers",
"type": "LAYER",
"link": null
}
],
"outputs": [
{
"name": "layers",
"type": "LAYER",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "NewLayer"
},
"widgets_values": [
0,
0,
512,
512,
0,
"width"
]
},
{
"id": 20,
"type": "MergeLayers",
"pos": [
2163.913220668749,
515.7887044208991
],
"size": {
"0": 315.47509765625,
"1": 69.53152465820312
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "layers",
"type": "LAYER",
"link": null
},
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "MergeLayers"
}
},
{
"id": 4,
"type": "CharacterInText",
"pos": [
1778,
386
],
"size": {
"0": 319.45068359375,
"1": 194.45606994628906
},
"flags": {},
"order": 10,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "CharacterInText"
},
"widgets_values": [
"",
"",
1
]
},
{
"id": 21,
"type": "Note",
"pos": [
2507.333181606249,
517.1286262958992
],
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 11,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"TODO:可视化操作"
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 22,
"type": "Note",
"pos": [
1533,
347
],
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 12,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"funtion call"
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 11,
"type": "3DImage",
"pos": [
1794.6918193937495,
724.2820719460937
],
"size": {
"0": 309.2974548339844,
"1": 433.12554931640625
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "material",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "BG_IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MATERIAL",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "3DImage"
}
},
{
"id": 13,
"type": "ScreenShare",
"pos": [
2138.691819393749,
728.2820719460937
],
"size": {
"0": 322.9458923339844,
"1": 594.0604858398438
},
"flags": {},
"order": 14,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "PROMPT",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
},
{
"id": 14,
"type": "FloatingVideo",
"pos": [
2486.691819393749,
726.2820719460937
],
"size": {
"0": 302.3232421875,
"1": 303.61279296875
},
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 6,
"type": "TransparentImage",
"pos": [
2496.691819393749,
1157.2820719460938
],
"size": {
"0": 315,
"1": 146
},
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
},
{
"name": "masks",
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "file_path",
"type": "STRING",
"links": null,
"shape": 6
},
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 6
},
{
"name": "RGBA",
"type": "RGBA",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "TransparentImage"
},
"widgets_values": [
"yes",
"yes",
"Mixlab_save"
]
},
{
"id": 12,
"type": "ImageCropByAlpha",
"pos": [
1806.6918193937495,
1240.2820719460938
],
"size": {
"0": 289.27215576171875,
"1": 46
},
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
},
{
"name": "RGBA",
"type": "RGBA",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ImageCropByAlpha"
}
},
{
"id": 5,
"type": "EnhanceImage",
"pos": [
1098.6918193937504,
1217.2820719460938
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 18,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "EnhanceImage"
},
"widgets_values": [
0.5
]
},
{
"id": 10,
"type": "SvgImage",
"pos": [
1454.6918193937504,
1074.2820719460938
],
"size": {
"0": 315,
"1": 170
},
"flags": {},
"order": 19,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "layers",
"type": "LAYER",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SvgImage"
},
"widgets_values": [
null,
null
]
}
],
"links": [],
"groups": [
{
"title": "GPT",
"bounding": [
1083,
157,
1039,
450
],
"color": "#b06634",
"font_size": 24,
"locked": false
},
{
"title": "Image",
"bounding": [
1086,
650,
1735,
682
],
"color": "#A88",
"font_size": 24,
"locked": false
},
{
"title": "Audio",
"bounding": [
2882,
650,
344,
342
],
"color": "#8A8",
"font_size": 24,
"locked": false
},
{
"title": "Layer",
"bounding": [
2148,
158,
672,
437
],
"color": "#a1309b",
"font_size": 24,
"locked": false
}
],
"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)