Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3191cf2af0 | ||
|
|
7d3e1ae945 | ||
|
|
bfced5b4da | ||
|
|
efe73fb965 | ||
|
|
c45a962fcc | ||
|
|
f98a03e2e9 | ||
|
|
5b6257814d | ||
|
|
69a445d4ed | ||
|
|
e82c786b8a | ||
|
|
eec2225c89 | ||
|
|
f7355e0b71 | ||
|
|
6c6a99cfe4 | ||
|
|
b4634e2e0d | ||
|
|
3f4cba0612 | ||
|
|
38db99cc75 | ||
|
|
4d5906394b | ||
|
|
2fc212b156 | ||
|
|
53fbb5b027 | ||
|
|
4f24721450 | ||
|
|
83043727b5 | ||
|
|
2d336afb85 | ||
|
|
4d309435c8 | ||
|
|
099ce9cdfd | ||
|
|
8914e60cb8 | ||
|
|
dbd30a40e9 | ||
|
|
c9a598fd59 | ||
|
|
e331e588cf | ||
|
|
2011557771 | ||
|
|
f0ba45d14e | ||
|
|
8352a521b7 | ||
|
|
aa3d4d79f8 | ||
|
|
4f650d760c | ||
|
|
ea4b792627 | ||
|
|
883605239a | ||
|
|
5b8cab920c | ||
|
|
8d3d327335 | ||
|
|
32574050c4 | ||
|
|
8d45a90d9b | ||
|
|
f66862a422 | ||
|
|
6a56be3a9b | ||
|
|
ebf6395de2 | ||
|
|
5df9fbf50d | ||
|
|
ff961155c9 | ||
|
|
14838d06a8 | ||
|
|
99def24dd8 | ||
|
|
f5b210d142 | ||
|
|
a137a23b48 | ||
|
|
6bbf06d9e9 | ||
|
|
0b614b40cf | ||
|
|
27673561bd | ||
|
|
6e2070410d | ||
|
|
7ccf21f74f | ||
|
|
3b2710f285 | ||
|
|
c4b277235b | ||
|
|
a55318add1 | ||
|
|
b57123a4fe | ||
|
|
04dcc00670 | ||
|
|
746a02b49f | ||
|
|
bdbe3db2a9 | ||
|
|
27ae99ad86 | ||
|
|
38add89547 | ||
|
|
a9612fbb2f | ||
|
|
429cc29b5b | ||
|
|
8eca94e405 | ||
|
|
ad71daafb6 | ||
|
|
c936d83688 | ||
|
|
c6684d680f | ||
|
|
897f259a2a | ||
|
|
f3302c1b3a | ||
|
|
573feeaaab |
@@ -1,4 +1,5 @@
|
||||
__pycache__/
|
||||
https/
|
||||
nodes/config.json
|
||||
workflow/my_workflow.json
|
||||
workflow/my_workflow.json
|
||||
workflow/my_workflow_app.json
|
||||
@@ -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.
|
||||
|
||||

|
||||
Example:
|
||||
- workflow
|
||||

|
||||
|
||||
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
|
||||

|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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的节点功能
|
||||
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 9.9 MiB |
@@ -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": [
|
||||
|
||||
@@ -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,)}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": ()}
|
||||
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -3,4 +3,5 @@ pyOpenSSL
|
||||
watchdog
|
||||
opencv-python-headless
|
||||
matplotlib
|
||||
openai
|
||||
openai
|
||||
# playwright
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 //需要保存参数
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||

|
||||

|
||||
|
||||