Compare commits

...
Author SHA1 Message Date
gold3bear c275a56806 # 2023-12-17 00:40:26 +08:00
gold3bear 82c3c7addd improve mix-modal ui 2023-12-16 23:59:45 +08:00
shadowcz007 561805a417 优化下3dImage 2023-12-16 15:48:35 +08:00
shadowcz007 7bb4324365 fixbug 2023-12-16 13:52:28 +08:00
shadowcz007 0608653d35 Update ui_mixlab.js 2023-12-16 00:05:41 +08:00
shadowcz007 fa3472cdc5 find_the_node 2023-12-15 23:46:46 +08:00
shadowcz007 7d553b6fcf 修复svgImage的bug 2023-12-15 19:57:22 +08:00
shadowcz007 677627630e 右击节点获取readme 2023-12-14 20:02:27 +08:00
shadow 9f23172b22 Update README.md 2023-12-13 10:35:33 +08:00
gold3bear 14ceaa472d Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 21:17:36 +08:00
gold3bear 1188b9d3bc fix:macos fonts runtime erros 2023-12-12 21:16:51 +08:00
shadowcz007 424c9a9423 0.3.1 2023-12-12 20:26:57 +08:00
shadowcz007 8f60c81ef3 test 2023-12-12 20:22:46 +08:00
shadowcz007 dd648d1ae5 test 2023-12-12 20:14:43 +08:00
shadowcz007 02e839e272 新增示例 2023-12-12 18:11:47 +08:00
shadowcz007 ec8c56707b 0.3.0
v0.3.0 🚀🚗🚚🏃‍

- Added support for setting proxies: HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy ✅

- Added a new Speech feature node, enabling the use of a voice assistant: SpeechRecognition & SpeechSynthesis 🎙️

- Added TextImage node, allowing conversion of text into image format 📷

- Added SvgImage node, enabling layout parsing and poster generation in conjunction with the Layer class node 🖼️

- Added an experimental 3DImage node for loading 3D models 🌟
2023-12-12 17:22:37 +08:00
shadowcz007 1e8d317ee8 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 17:14:55 +08:00
shadowcz007 e81df111a7 0.3 ing 2023-12-12 17:14:52 +08:00
shadow 90e55ffe14 Merge pull request #45 from shadowcz007/v0.2.8-proxy
V0.2.8 proxy
2023-12-12 14:09:55 +08:00
shadowcz007 4e73b1d3fc fixbug 2023-12-12 14:07:01 +08:00
gold3bear 21dca1e34f test ok 2023-12-12 13:56:58 +08:00
shadowcz007 c2292850bb 1 2023-12-12 12:38:37 +08:00
BearXiong 04791c92e2 proxy new_request 2023-12-12 11:55:12 +08:00
shadowcz007 d9462b6d8a Update Utils.py 2023-12-11 11:49:36 +08:00
shadowcz007 be9b83559e test 2023-12-10 17:31:59 +08:00
shadowcz007 958889afee test-3d 2023-12-10 17:31:28 +08:00
shadowcz007 dce677035f 更新-字体和颜色选择 2023-12-10 11:31:06 +08:00
shadowcz007 d98a8855ad update 2023-12-09 18:07:16 +08:00
shadowcz007 7cd0587a64 COLOR冲突,改个名字 2023-12-09 12:33:58 +08:00
shadowcz007 bb3967f11e ing 2023-12-09 00:37:38 +08:00
shadowcz007 36ee203bd7 ing 2023-12-09 00:17:29 +08:00
shadowcz007 914919ba75 test 2023-12-08 13:01:00 +08:00
shadowcz007 a4514e8565 1 2023-12-08 00:15:13 +08:00
shadowcz007 116e983c50 新增textImage 2023-12-08 00:07:34 +08:00
shadowcz007 fb667b6c42 0.2.8 layers 预发布 2023-12-07 18:28:58 +08:00
shadowcz007 b0a090cf14 1 2023-12-07 11:42:54 +08:00
shadowcz007 110b470d33 Update README.md 2023-12-06 19:51:30 +08:00
shadowcz007 c517c4d015 v0.2.7 2023-12-06 19:49:26 +08:00
shadowcz007 35ed4f9101 Update main_mixlab.js 2023-12-06 19:41:29 +08:00
shadowcz007 18c723c2e3 seed 2023-12-06 19:35:40 +08:00
shadowcz007 631223602c Update gpt_mixlab.js 2023-12-06 19:01:30 +08:00
shadowcz007 a6cc907de2 v0.2.6 2023-12-06 18:18:35 +08:00
36 changed files with 13958 additions and 410 deletions
+83 -34
View File
@@ -1,11 +1,30 @@
##
v0.3.0 🚀🚗🚚🏃‍
- Added support for setting proxies: HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy ✅
- Added a new Speech feature node, enabling the use of a voice assistant: SpeechRecognition & SpeechSynthesis 🎙️
- Added TextImage node, allowing conversion of text into image format 📷
- Added SvgImage node, enabling layout parsing and poster generation in conjunction with the Layer class node 🖼️
- Added an experimental 3DImage node for loading 3D models 🌟
![screenshare](./assets/screenshare.png)
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
[Voice + Real-time Face Swap Workflow](./workflow/语音+实时换脸workflow.json)
### ScreenShareNode & FloatingVideoNode
> Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
>
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
@@ -16,7 +35,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop. Q: Translate into English
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
@@ -24,7 +43,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
### GPT
> ChatGPT、ChatGLM3 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1
> Support for calling multiple GPTs.ChatGPT、ChatGLM3 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
![gpt-workflow.svg](./assets/gpt-workflow.svg)
@@ -32,38 +51,14 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow-5](./workflow/5-gpt-workflow.json)
## Installation
### 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.
manually install, simply clone the repo into the custom_nodes directory with this command:
![layers](./assets/layers-workflow.svg)
```
cd ComfyUI/custom_nodes
![poster](./assets/poster-workflow.svg)
git clone https://github.com/shadowcz007/comfyui-mixlab-nodes.git
```
Install the requirements:
run directly:
```
cd ComfyUI_Mixlab
install.bat
```
or install the requirements using:
```
../../../python_embeded/python.exe -s -m pip install -r requirements.txt
```
If you are using a venv, make sure you have it activated before installation and use:
```
pip3 install -r requirements.txt
```
## Nodes
## Other Nodes
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
@@ -79,9 +74,6 @@ pip3 install -r requirements.txt
![TransparentImage](./assets/TransparentImage.png)
> Consistency Decoder
[openai Consistency Decoder]( https://github.com/openai/consistencydecoder)
@@ -111,6 +103,42 @@ An improvement has been made to directly redirect to GitHub to search for missin
<!-- ### Workflow
[Workflow](./workflow.md) -->
## Installation
manually install, simply clone the repo into the custom_nodes directory with this command:
```
cd ComfyUI/custom_nodes
git clone https://github.com/shadowcz007/comfyui-mixlab-nodes.git
```
Install the requirements:
run directly:
```
cd ComfyUI/custom_nodes/comfyui-mixlab-nodes
install.bat
```
or install the requirements using:
```
../../../python_embeded/python.exe -s -m pip install -r requirements.txt
```
If you are using a venv, make sure you have it activated before installation and use:
```
pip3 install -r requirements.txt
```
#### Chinese community
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
#### Thanks:
[ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
@@ -118,5 +146,26 @@ An improvement has been made to directly redirect to GitHub to search for missin
[discussions](https://github.com/shadowcz007/comfyui-mixlab-nodes/discussions)
### TODO:
- 音频播放节点:带可视化、支持多音轨、可配置音轨音量
- vector https://github.com/GeorgLegato/stable-diffusion-webui-vectorstudio
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date&theme=dark
"
/>
<source
media="(prefers-color-scheme: light)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date
"
/>
<img
alt="Star History Chart"
src="https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date"
/>
</picture>
+87 -8
View File
@@ -64,6 +64,18 @@ except ImportError:
sys.exit()
def install_openai():
# Helper function to install the OpenAI module if not already installed
try:
importlib.import_module('openai')
except ImportError:
import pip
pip.main(['install', 'openai'])
install_openai()
current_path = os.path.abspath(os.path.dirname(__file__))
@@ -156,8 +168,44 @@ def save_workflow_json(data):
json.dump(data, file)
return workflow_path
def get_nodes_map():
# print("#####path::", current_path)
data_path=os.path.join(current_path, "data")
print('data_path: ',data_path)
# if not os.path.exists(data_path):
# # 使用mkdir()方法创建新目录
# os.mkdir(data_path)
json_data={}
nodes_map=os.path.join(current_path, "data/extension-node-map.json")
if os.path.exists(nodes_map):
with open(nodes_map) as json_file:
json_data = json.load(json_file)
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
# 定义新的 get 方法
async def new_request(self, method, url, *args, **kwargs):
# 检查环境变量以确定是否使用代理
proxy = os.environ.get('HTTP_PROXY') or os.environ.get('HTTPS_PROXY') or os.environ.get('http_proxy') or os.environ.get('https_proxy')
# print('Proxy Config:',proxy)
if proxy and 'proxy' not in kwargs:
kwargs['proxy'] = proxy
print('Use Proxy:',proxy)
# 调用原始的 _request 方法
return await _original_request(self, method, url, *args, **kwargs)
# 应用 Monkey Patch
aiohttp.ClientSession._request = new_request
# https
async def new_start(self, address, port, verbose=True, call_on_start=None):
runner = web.AppRunner(self.app, access_log=None)
await runner.setup()
site = web.TCPSite(runner, address, port)
@@ -227,6 +275,20 @@ async def mixlab_workflow_hander(request):
return web.json_response(result)
@routes.post('/mixlab/nodes_map')
async def nodes_map_hander(request):
data = await request.json()
result={}
try:
result={
'data':get_nodes_map(),
'status':'success',
}
except Exception as e:
print(e)
return web.json_response(result)
def new_add_routes(self):
import nodes
self.app.add_routes(routes)
@@ -256,11 +318,14 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,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
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
@@ -268,7 +333,14 @@ NODE_CLASS_MAPPINGS = {
"RandomPrompt":RandomPrompt,
"TransparentImage":TransparentImage,
"LoadImagesFromPath":LoadImagesFromPath,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
"3DImage":Image3D,
"EmptyLayer":EmptyLayer,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"MergeLayers":MergeLayers,
"SplitLongMask":SplitLongMask,
"FeatheredMask":FeatheredMask,
"SmoothMask":SmoothMask,
@@ -281,21 +353,28 @@ NODE_CLASS_MAPPINGS = {
"FloatingVideo":FloatingVideo,
"CLIPSeg_":CLIPSeg,
"CombineMasks_":CombineMasks,
"ChatGPT":ChatGPTNode,
"ChatGPTOpenAI":ChatGPTNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText
"CharacterInText":CharacterInText,
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"Font":FontInput
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"RandomPrompt": "Random Prompt #Mixlab",
"RandomPrompt": "Random Prompt ♾️Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode",
"ScreenShare":"ScreenShare #Mixlab",
"FloatingVideo":"FloatingVideo #Mixlab",
"ChatGPT":"ChatGPT #Mixlab",
"ShowTextForGPT":"ShowTextForGPT #Mixlab"
"ScreenShare":"ScreenShare ♾️Mixlab",
"FloatingVideo":"FloatingVideo ♾️Mixlab",
"ChatGPTOpenAI":"ChatGPT ♾️Mixlab",
"ShowTextForGPT":"ShowTextForGPT ♾️Mixlab",
"MergeLayers":"MergeLayers ♾️Mixlab",
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab"
}
# web ui的节点功能
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
class SpeechRecognition:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("AUDIOINPUTMIX",), },
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/audio"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,upload):
return (upload,)
class SpeechSynthesis:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "♾️Mixlab/audio"
def run(self, text):
# print(session_history)
return {"ui": {"text": text}, "result": (text,)}
+21 -4
View File
@@ -46,7 +46,7 @@ def chat(client, model_name,messages ):
except (urllib.error.HTTPError, openai.OpenAIError) as ex:
if try_count >= 3:
raise ex
time.sleep(5)
time.sleep(3)
continue
finish_reason = response.choices[0].finish_reason
@@ -75,17 +75,21 @@ class ChatGPTNode:
"required": {
"api_key":("KEY", {"default": "", "multiline": True}),
"api_url":("URL", {"default": "", "multiline": True}),
"prompt": ("STRING", {"default": "", "multiline": True}),
"prompt": ("STRING", {"multiline": True}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True
}),
"model": (["gpt-3.5-turbo", "gpt-3.5-turbo-16k-0613", "gpt-4-0613","gpt-4-1106-preview"],
"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}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
@@ -102,7 +106,7 @@ class ChatGPTNode:
prompt,
system_content,
model,
seed,context_size):
seed,context_size,unique_id = None, extra_pnginfo=None):
# print(api_key!='',api_url,prompt,system_content,model,seed)
# 可以选择保留会话历史以维持上下文记忆
# 或者在此处清除会话历史 self.session_history.clear()
@@ -140,6 +144,19 @@ class ChatGPTNode:
response_content = chat(client,model,messages)
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
# if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
# workflow = extra_pnginfo[0]["workflow"]
# node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
# if node:
# node["widgets_values"] = ["",
# api_url,
# prompt,
# system_content,
# model,
# seed,
# context_size]
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
+7 -4
View File
@@ -1,3 +1,6 @@
#### Thanks:
# [ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
from PIL import Image
@@ -252,7 +255,7 @@ class CombineMasks:
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"CLIPSeg": CLIPSeg,
"CombineSegMasks": CombineMasks,
}
# NODE_CLASS_MAPPINGS = {
# "CLIPSeg": CLIPSeg,
# "CombineSegMasks": CombineMasks,
# }
+516 -3
View File
@@ -1,17 +1,18 @@
import numpy as np
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
import base64,os
from io import BytesIO
import folder_paths
import json
import json,io
from comfy.cli_args import args
import cv2
from .Watcher import FolderWatcher
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
MAX_RESOLUTION=8192
@@ -322,6 +323,117 @@ def areaToMask(x,y,w,h,image):
return mask
# def merge_images(bg_image, layer_image,mask, x, y, width, height):
# # 打开底图
# # bg_image = Image.open(background)
# bg_image=bg_image.convert("RGBA")
# # 打开图层
# layer_image=layer_image.convert("RGBA")
# layer_image = layer_image.resize((width, height))
# # mask = Image.new("L", layer_image.size, 255)
# mask = mask.resize((width, height))
# # 在底图上粘贴图层
# bg_image.paste(layer_image, (x, y), mask=mask)
# # 输出合成后的图片
# # bg_image.save("output.jpg")
# return bg_image
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option):
# 打开底图
bg_image = bg_image.convert("RGBA")
# 打开图层
layer_image = layer_image.convert("RGBA")
# layer_image = layer_image.resize((width, height))
# 根据缩放选项调整图像大小
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))
# 调整mask的大小
nw, nh = layer_image.size
mask = mask.resize((nw, nh))
# 在底图上粘贴图层
bg_image.paste(layer_image, (x, y), mask=mask)
# 输出合成后的图片
return bg_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
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 base64_to_image(base64_string):
# 去除前缀
prefix, base64_data = base64_string.split(",", 1)
# 从base64字符串中解码图像数据
image_data = base64.b64decode(base64_data)
# 创建一个内存流对象
image_stream = io.BytesIO(image_data)
# 使用PIL的Image模块打开图像数据
image = Image.open(image_stream)
return image
class SmoothMask:
@classmethod
@@ -494,6 +606,7 @@ class TransparentImage:
}
RETURN_TYPES = ('STRING','IMAGE','RGBA')
RETURN_NAMES = ("file_path","IMAGE","RGBA",)
OUTPUT_NODE = True
@@ -671,7 +784,7 @@ class LoadImagesFromPath:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
print('#prompt::::',prompt)
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
@@ -708,6 +821,117 @@ class ImageCropByAlpha:
return (img,)
class TextImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": False,"default": "龍馬精神迎新歲"}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH}),
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
"max": 1000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"spacing": ("INT",{
"default":12,
"min": 1, #Minimum value
"max": 200, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"text_color":("STRING",{"multiline": False,"default": "#000000"}),
"vertical":("BOOLEAN", {"default": True},),
},
}
RETURN_TYPES = ("IMAGE","MASK")
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,text,font_path,font_size,spacing,text_color,vertical):
text_list=list(text)
img,mask=generate_text_image(text_list,font_path,font_size,text_color,vertical,spacing)
img=pil2tensor(img)
mask=pil2tensor(mask)
return (img,mask,)
class SvgImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("SVG",),},
}
RETURN_TYPES = ("IMAGE","LAYER")
RETURN_NAMES = ("IMAGE","layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,True,)
def run(self,upload):
layers=[]
image = base64_to_image(upload['image'])
image=image.convert('RGB')
image=pil2tensor(image)
for layer in upload['data']:
layers.append(layer)
return (image,layers,)
class Image3D:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("THREED",), },
}
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,upload):
# print(upload['image'])
image = base64_to_image(upload['image'])
image=image.convert('RGB')
mask=image.convert('L')
mask=pil2tensor(mask)
image=pil2tensor(image)
return (image,mask,)
class AreaToMask:
@classmethod
def INPUT_TYPES(s):
@@ -770,3 +994,292 @@ class FaceToMask:
return (mask,)
class EmptyLayer:
@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"
}),
},
}
RETURN_TYPES = ("LAYER",)
RETURN_NAMES = ("layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
OUTPUT_IS_LIST = (True,)
def run(self, width,height):
blank_image = Image.new("RGB", (width, height))
mask=blank_image.convert('L')
blank_image=pil2tensor(blank_image)
mask=pil2tensor(mask)
layer_n=[{
"x":0,
"y":0,
"width":width,
"height":height,
"z_index":0,
"scale_option":'width',
"image":blank_image,
"mask":mask
}]
return (layer_n,)
class NewLayer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"x": ("INT",{
"default": 0,
"min": -100, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"y": ("INT",{
"default": 0,
"min": 0, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"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"
}),
"z_index": ("INT",{
"default": 0,
"min":0, #Minimum value
"max": 100, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
"image": ("IMAGE",),
},
"optional":{
"mask": ("MASK",{"default": None}),
"layers": ("LAYER",{"default": None}),
}
}
RETURN_TYPES = ("LAYER",)
RETURN_NAMES = ("layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self,x,y,width,height,z_index,scale_option,image,mask,layers):
# print(x,y,width,height,z_index,image,mask)
if mask==None:
im=tensor2pil(image)
mask=im.convert('L')
mask=pil2tensor(mask)
else:
mask=mask[0]
layer_n=[{
"x":x[0],
"y":y[0],
"width":width[0],
"height":height[0],
"z_index":z_index[0],
"scale_option":scale_option[0],
"image":image[0],
"mask":mask
}]
if layers!=None:
layer_n=layer_n+layers
return (layer_n,)
class ShowLayer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"edit": ("EDIT",),
"x": ("INT",{
"default": 0,
"min": -100, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"y": ("INT",{
"default": 0,
"min": 0, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"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"
}),
"z_index": ("INT",{
"default": 0,
"min":0, #Minimum value
"max": 100, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
# "image": ("IMAGE",),
},
"optional":{
# "mask": ("MASK",{"default": None}),
"layers": ("LAYER",{"default": None}),
}
}
RETURN_TYPES = ( )
RETURN_NAMES = ( )
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
def run(self,edit,x,y,width,height,z_index,scale_option,layers):
# print(x,y,width,height,z_index,image,mask)
# if mask==None:
# im=tensor2pil(image)
# mask=im.convert('L')
# mask=pil2tensor(mask)
# else:
# mask=mask[0]
# layers[edit[0]]={
# "x":x[0],
# "y":y[0],
# "width":width[0],
# "height":height[0],
# "z_index":z_index[0],
# "scale_option":scale_option[0],
# "image":image[0],
# "mask":mask
# }
return ( )
class MergeLayers:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"layers": ("LAYER",),
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
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)
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)
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")]
return (bg_image,mask,)
+19 -5
View File
@@ -82,23 +82,25 @@ class ScreenShareNode:
},
"optional":{
"prompt": ("PROMPT",),
"slide": ("SLIDE",),
"seed": ("SEED",),
# "seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
} }
RETURN_TYPES = ('IMAGE','STRING')
RETURN_TYPES = ('IMAGE','STRING','FLOAT',"INT")
RETURN_NAMES = ("IMAGE","PROMPT","FLOAT","INT")
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
# INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,False,False)
OUTPUT_IS_LIST = (False,False,False,False)
# 运行的函数
def run(self,image_base64,prompt):
def run(self,image_base64,prompt,slide,seed):
im,mask=base64_save(image_base64)
# print('##########prompt',prompt)
return (im,prompt)
return (im,prompt,slide,seed)
class FloatingVideo:
@@ -137,3 +139,15 @@ class FloatingVideo:
return { "ui": { "images_": results } }
# class SildeNode:
# CATEGORY = "quicknodes"
# @classmethod
# def INPUT_TYPES(s):
# return { "required":{} }
# RETURN_TYPES = ()
# RETURN_NAMES = ()
# FUNCTION = "func"
# def func(self):
# return ()
+83
View File
@@ -0,0 +1,83 @@
import os
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
import matplotlib.font_manager as fm
def get_font_files(directory):
font_files = {}
# 从指定目录加载字体
for file in os.listdir(directory):
if file.endswith('.ttf') or file.endswith('.otf'):
font_name = os.path.splitext(file)[0]
font_path = os.path.join(directory, file)
font_files[font_name] = os.path.abspath(font_path)
# 尝试获取系统字体
try:
font_paths = fm.findSystemFonts()
for path in font_paths:
try:
font_prop = fm.FontProperties(fname=path)
font_name = font_prop.get_name()
font_files[font_name] = path
except Exception as e:
print(f"Error processing font {path}: {e}")
except Exception as e:
print(f"Error finding system fonts: {e}")
return font_files
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
font_files = get_font_files(r_directory)
# print(font_files)
class ColorInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"color":("TCOLOR",),
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,color):
return (color,)
class FontInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"font": (list(font_files.keys()),),
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,font):
return (font_files[font],)
+311
View File
@@ -0,0 +1,311 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
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'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function speakText (text) {
const speechMsg = new SpeechSynthesisUtterance()
speechMsg.text = text
// 语音合成结束时触发的事件
speechMsg.onend = function (event) {
console.log('语音播放结束')
window._mixlab_speech_synthesis_onend = true
}
// 语音合成错误时触发的事件
speechMsg.onerror = function (event) {
console.error('语音播放错误:', event.error)
}
// 使用浏览器默认语音合成器进行语音播放
speechSynthesis.speak(speechMsg)
}
// 调用方法,将文字转换为语音播放
// speakText('Hello, how are you?');
// #MixCopilot
const start = (element, id, startBtn) => {
startBtn.className='loading_mixlab'
window.recognition = new webkitSpeechRecognition()
window.recognition.continuous = true
window.recognition.interimResults = true
window.recognition.lang = navigator.language
let timeoutId, intervalId
window.recognition.onstart = () => {
console.log('开始语音输入', window._mixlab_speech_synthesis_onend)
window._mixlab_speech_synthesis_onend = false
}
window.recognition.onresult = function (event) {
const result = event.results[event.results.length - 1][0].transcript
console.log('识别结果:', result)
element.value = result
let data = getLocalData('_mixlab_speech_recognition')
data[id] = result.trim()
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)
window.recognition?.stop()
window.recognition = null;
startBtn.className=''
startBtn.innerText = 'START'
timeoutId = null
intervalId = setInterval(() => {
if (
app.ui.lastQueueSize === 0 &&
!window.recognition &&
window._mixlab_speech_synthesis_onend
) {
start(element, id, startBtn)
startBtn.innerText = 'STOP'
if (intervalId) {
clearInterval(intervalId)
}
}
}, 2200)
}, 2000)
}
window.recognition.onend = function () {
console.log('语音输入结束')
}
window.recognition.onspeechend = function () {
console.log('onspeechend')
}
window.recognition.onerror = function (event) {
console.log('Error occurred in recognition: ' + event.error)
}
window.recognition.start()
}
app.registerExtension({
name: 'Mixlab.audio.SpeechRecognition',
async getCustomWidgets (app) {
return {
AUDIOINPUTMIX (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_speech_recognition')
return data[node.id] || 'Hello Mixlab'
}
}
// 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 == 'SpeechRecognition') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
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])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const startBtn = document.createElement('button')
const textArea = document.createElement('textarea')
textArea.className = `${'comfy-multiline-input'} ${placeholder}`
textArea.style = `margin-top: 14px;
height: 44px;`
div.style = `flex-direction: column;
display: flex;
margin: 0px 8px 6px;`
startBtn.style = `
outline: none;
border: none;
padding: 4px; `
startBtn.innerText = 'START'
div.appendChild(startBtn)
div.appendChild(textArea)
startBtn.addEventListener('click', () => {
if (window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className=''
} else {
start(textArea, this.id, startBtn)
startBtn.innerText = 'STOP'
}
})
return div
}
let inputAudio = inputDiv('_mixlab_speech_recognition', 'audio')
widget.div.appendChild(inputAudio)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputAudio.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'SpeechRecognition') {
let data = getLocalData('_mixlab_speech_recognition')
// console.log('_mixlab_speech_recognition', node.widgets)
let div = node.widgets.filter(f => f.type === 'div')[0]
if (div && data[node.id]) {
div.div.querySelector('textarea').value = data[node.id]
}
}
}
})
app.registerExtension({
name: 'Mixlab.audio.SpeechSynthesis',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'SpeechSynthesis') {
function populate (text) {
// console.log('SpeechSynthesis',this.widgets)
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === 'text')
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.()
}
this.widgets.length = pos
}
}
for (let list of text) {
const w = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
w.value = list
}
speakText(text.join('\n'))
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
sz[0] = this.size[0]
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1]
}
this.onResize?.(sz)
app.graph.setDirtyCanvas(true, false)
})
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
populate.call(this, message.text)
}
this.serialize_widgets = true //需要保存参数
}
}
})
+8 -4
View File
@@ -3,16 +3,20 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.2.5.2'
const version = 'v0.3.1'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
.then(data => {
const latestVersion = data.tag_name
console.log('Latest release version:', latestVersion)
// if (latestVersion === localStorage.getItem('_mixlab_nodes_vesion')) return
if (latestVersion != version) {
// localStorage.setItem('_mixlab_nodes_vesion', latestVersion)
if (
latestVersion &&
latestVersion === localStorage.getItem('_mixlab_nodes_vesion')
)
return
if (latestVersion && latestVersion != version) {
localStorage.setItem('_mixlab_nodes_vesion', latestVersion)
app.ui.dialog.show(`<h4 style="font-size: 18px;">${repoName} <br>
Latest release version: ${latestVersion}</h4>
<p>Please proceed to the official repository to download the latest version.</p>
+138 -101
View File
@@ -46,23 +46,33 @@ function get_position_style (ctx, widget_width, y, node_height) {
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
app.registerExtension({
name: 'Mixlab.GPT.ChatGPT',
name: 'Mixlab.GPT.ChatGPTOpenAI',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
return [128,32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
//localStorage.getItem('_mixlab_api_key') || ''
return 'by Mixlab'
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
@@ -74,15 +84,16 @@ app.registerExtension({
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
size: [128, 32], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return localStorage.getItem('_mixlab_api_url') || 'https://api.openai.com/v1'
let data = getLocalData('_mixlab_api_url')
return data[node.id] || 'https://api.openai.com/v1'
}
}
// widget.something = something; // maybe adds stuff to it
@@ -91,22 +102,21 @@ app.registerExtension({
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
// console.log(nodeType.comfyClass)
if (nodeType.comfyClass == 'ChatGPT') {
if (nodeType.comfyClass == 'ChatGPTOpenAI') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('ChatGPT widtget', this.widgets)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const api_url = this.widgets.filter(w => w.name == 'api_url')[0]
// console.log('api_key', api_key, api_url)
console.log('ChatGPTOpenAI nodeData', this.widgets)
const widget = {
type: 'div',
name: 'chatgpt div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
@@ -119,36 +129,43 @@ app.registerExtension({
document.body.appendChild(widget.div)
const inputKey = document.createElement('input'),
inputUrl = document.createElement('input')
inputKey.type = 'text'
inputUrl.type = 'text'
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
inputKey.placeholder = 'Key'
inputUrl.placeholder = 'URL'
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
inputKey.style = `margin:4px 48px;`
inputUrl.style = `margin:4px 48px`
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
inputUrl.value = localStorage.getItem('_mixlab_api_url') || 'https://api.openai.com/v1'
inputKey.value = localStorage.getItem('_mixlab_api_key') || 'by Mixlab'
let inputKey = inputDiv('_mixlab_api_key', 'Key')
let inputUrl = inputDiv('_mixlab_api_url', 'URL')
widget.div.appendChild(inputKey)
widget.div.appendChild(inputUrl)
inputKey.addEventListener('change', () => {
api_key.serializeValue = () => inputKey.value || 'by Mixlab'
localStorage.setItem('_mixlab_api_key', inputKey.value)
})
inputUrl.addEventListener('change', () => {
api_url.serializeValue = () => inputUrl.value || 'https://api.openai.com/v1'
localStorage.setItem('_mixlab_api_url', inputUrl.value)
})
/*
Add the widget, make sure we clean up nicely, and we do not want to be serialized!
*/
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
@@ -159,82 +176,102 @@ app.registerExtension({
return onRemoved?.()
}
this.serialize_widgets = false
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 === 'ChatGPTOpenAI') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key'),
url = getLocalData('_mixlab_api_url')
let id = node.id
// console.log('ChatGPTOpenAI serialize_widgets', this)
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
widget.div.querySelector('.URL').value =
url[id] || 'https://api.openai.com/v1'
}
}
})
app.registerExtension({
name: 'Mixlab.GPT.ShowTextForGPT',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'ShowTextForGPT') {
function populate (text) {
if (this.widgets) {
// const pos = this.widgets.findIndex((w) => w.name === "text");
// if (pos !== -1) {
// for (let i = pos; i < this.widgets.length; i++) {
// this.widgets[i].onRemove?.();
// }
// this.widgets.length = pos;
// }
for (let i = 0; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.()
}
this.widgets.length = 0
}
console.log('ShowTextForGPT', this.widgets, text)
for (const list of text) {
const w = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
let res = list
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "ShowTextForGPT") {
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "text");
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = pos;
}
}
// console.log('ShowTextForGPT',text)
for (let list of text) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
try {
res = JSON.stringify(JSON.parse(list), null, 2)
let data=JSON.parse(list);
data=Array.from(data,d=>{
return {
...d,
content:decodeURIComponent(d.content)
}
})
list=JSON.stringify(data,null,2)
} catch (error) {
// console.log(list)
// console.log(error)
}
w.value = res
}
w.value =list;
}
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
sz[0] = this.size[0]
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1]
}
this.onResize?.(sz)
app.graph.setDirtyCanvas(true, false)
})
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
populate.call(this, message.text)
}
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
const onConfigure = nodeType.prototype.onConfigure
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments)
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values)
}
}
}
}
this.serialize_widgets = true //需要保存参数
}
},
})
+659
View File
@@ -0,0 +1,659 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function uploadImage (blob, fileType = '.svg') {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append('image', new File([blob], 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 base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
function getContentTypeFromBase64 (base64Data) {
const regex = /^data:(.+);base64,/
const matches = base64Data.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
}
return null
}
// 示例用法
// const base64Data = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAAAAAAD/...'; // 替换为实际的base64图片数据
// const contentType = getContentTypeFromBase64(base64Data);
// console.log(contentType);
// // 示例用法
// const base64Data = '...'; // 替换为实际的base64图片数据
// const contentType = 'image/jpeg'; // 替换为实际的图片类型
// const blob = base64ToBlob(base64Data, contentType);
// console.log(blob);
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'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
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)
})
})
}
const parseSvg = async svgContent => {
let scale = 2
// 创建一个临时的DOM元素来解析SVG
const tempContainer = document.createElement('div')
tempContainer.innerHTML = svgContent
// 提取SVG元素
const svgElement = tempContainer.querySelector('svg')
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
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) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
// 填充颜色到canvas
var fill = rectElement.getAttribute('fill')
context.fillStyle = fill
context.fillRect(0, 0, width, height)
// 将canvas转换为base64格式
var base64 = canvas.toDataURL()
// 将数据转化为指定的JSON格式
var rectData = {
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height),
z_index: i + 1,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'rect'
}
// 将处理后的数据添加到数组中
data.push(rectData)
}
})
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth = viewBox.width
svgHeight = viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
canvas.height = svgHeight
var context = canvas.getContext('2d')
// 绘制SVG到canvas
var svgString = new XMLSerializer().serializeToString(svgElement)
var DOMURL = window.URL || window.webkitURL || window
var svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })
var url = DOMURL.createObjectURL(svgBlob)
let img = await createImage(url)
context.drawImage(img, 0, 0)
let base64 = canvas.toDataURL()
var rectData = {
x: 0,
y: 0,
width: parseInt(svgWidth),
height: parseInt(svgHeight),
z_index: 0,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log('layers', { data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
app.registerExtension({
name: 'Mixlab.image.SvgImage',
async getCustomWidgets (app) {
return {
SVG (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_svg_image')
// console.log('serializeValue',d)
if (d) {
let url = d[node.id]
let dt = await fetch(url)
let svgStr = await dt.text()
const { data, image } = (await parseSvg(svgStr)) || {}
// console.log(data, image)
return JSON.parse(JSON.stringify({ data, image }))
} else {
return
}
}
}
// console.log('##node',node.serialize)
// 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 == 'SvgImage') {
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('SvgImage nodeData',await uploadWidget.serializeValue())
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, 44, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, svgContainer) => {
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
ip.addEventListener('change', event => {
const file = event.target.files[0]
const reader = new FileReader()
// 读取文件内容
reader.onload = async e => {
const svgContent = e.target.result
var blob = new Blob([svgContent], { type: 'image/svg+xml' })
let url = await uploadImage(blob)
// console.log(url)
const { svgElement, data, image } = await parseSvg(svgContent)
// 将提取的SVG元素显示在页面上
let dd = getLocalData(key)
dd[that.id] = url
setLocalDataOfWin(key, dd)
// console.log(this.id, ip.value.trim())
svgElement.style = `width: 90%;padding: 5%;`
// 将提取的SVG元素显示在页面上
svgContainer.innerHTML = ''
svgContainer.appendChild(svgElement)
let h = ~~getComputedStyle(svgElement).height.replace('px', '')
if (that.size && that.size[1] < h) {
that.setSize([that.size[0], that.size[1] + h])
app.canvas.draw(true, true)
}
// console.log(that.size,~~getComputedStyle(svgElement).height.replace('px',''))
uploadWidget.value = await uploadWidget.serializeValue()
}
// 以文本形式读取文件
reader.readAsText(file)
})
return div
}
let svg = document.createElement('div')
svg.className = 'preview'
svg.style = `background:#eee;margin-top: 12px;`
let upload = inputDiv('_mixlab_svg_image', 'Svg', svg)
widget.div.appendChild(upload)
widget.div.appendChild(svg)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
svg.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 === 'SvgImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'upload-preview')[0]
let dd = getLocalData('_mixlab_svg_image')
let id = node.id
console.log('SvgImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let dt = await fetch(dd[id])
let svgStr = await dt.text()
const { svgElement, data, image } = await parseSvg(svgStr)
svgElement.style = `width: 90%;padding: 5%;`
// 将提取的SVG元素显示在页面上
widget.div.querySelector('.preview').innerHTML = ''
widget.div.querySelector('.preview').appendChild(svgElement)
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) {
let url = d[node.id]
let base64 = await parseImage(url)
return JSON.parse(JSON.stringify({ image: 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, 44, node.size[1])
)
}
}
widget.div = $el('div', {})
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
ip.addEventListener('change', event => {
const file = event.target.files[0]
const reader = new FileReader()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer
alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum"
src="${fileURL}"
ar
shadow-intensity="1"
camera-controls
touch-action="pan-y">
<div class="controls">
<div>Variant: <select class="variant"></select></div>
<div><button class="capture">Capture</button></div>
</div></model-viewer>`
preview.innerHTML = html;
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 capture = preview.querySelector('.capture')
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)
})
select.addEventListener('input', event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
})
capture.addEventListener('click', async () => {
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 dd = getLocalData(key)
dd[that.id] = url
setLocalDataOfWin(key, dd)
})
uploadWidget.value = await uploadWidget.serializeValue()
}
// 以文本形式读取文件
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;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
widget.div.appendChild(upload)
widget.div.appendChild(preview)
this.addCustomWidget(widget);
const onResize = this.onResize;
this.onResize = function () {
let m=preview.querySelector('model-viewer')
m.style.width=`${this.size[0]-24}px`
m.style.height=`${this.size[1]-48}px`
// console.log(this.size,preview)
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 = dd[id]
// let base64 = await parseImage(url)
widget.div.querySelector('.preview').innerHTML = `<img src="${url}"/>`
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)
}
}
})
+352
View File
@@ -0,0 +1,352 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
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'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
const parseSvg = async svgContent => {
// 创建一个临时的DOM元素来解析SVG
const tempContainer = document.createElement('div')
tempContainer.innerHTML = svgContent
// 提取SVG元素
const svgElement = tempContainer.querySelector('svg')
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
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) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
// 填充颜色到canvas
var fill = rectElement.getAttribute('fill')
context.fillStyle = fill
context.fillRect(0, 0, width, height)
// 将canvas转换为base64格式
var base64 = canvas.toDataURL()
// 将数据转化为指定的JSON格式
var rectData = {
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height),
z_index: i + 1,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t:'rect'
}
// 将处理后的数据添加到数组中
data.push(rectData)
}
})
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth =viewBox.width
svgHeight =viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
canvas.height = svgHeight
var context = canvas.getContext('2d')
// 绘制SVG到canvas
var svgString = new XMLSerializer().serializeToString(svgElement)
var DOMURL = window.URL || window.webkitURL || window
var svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })
var url = DOMURL.createObjectURL(svgBlob)
let img = await createImage(url)
context.drawImage(img, 0, 0)
let base64 = canvas.toDataURL()
var rectData = {
x: 0,
y: 0,
width: parseInt(svgWidth),
height: parseInt(svgHeight),
z_index: 0,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t:'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log('layers',{ data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
app.registerExtension({
name: 'Mixlab.layer.ShowLayer',
async getCustomWidgets (app) {
return {
EDIT (node, inputName, inputData, app) {
// console.log('EditLayer##node', node,inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 44], // a default size
draw (ctx, node, widget_width, y, widget_height) {
// console.log('EditLayer', this)
if (this.input)
Object.assign(
this.input.style,
get_position_style(ctx, widget_width, 32, node.size[1])
)
},
computeSize (...args) {
return [128, 44] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_edit_layer')
// console.log('EditLayer',d[node.id])
return d[node.id]
}
}
// 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 == 'ShowLayer') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
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
nodeId = app.graph.links.filter(link => link.id == linkId)[0]
?.origin_id
return findNode(nodeId)
} else {
return nodeId
}
}
// 获取layers数据
const getLayers = async () => {
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;
if(nodeId){
nodeId = findNode(nodeId)
}
// let node = app.graph._nodes_by_id[nodeId]
// if (node?.type == 'Reroute') {
// linkId = node.inputs[0].link
// nodeId = app.graph.links.filter(link => link.id == linkId)[0]
// ?.origin_id
// }
let d = getLocalData('_mixlab_svg_image')
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)
return data
} else {
return []
}
}
// 修改layers数据
const setLayer = async (editIndex, layers = null) => {
// let editIndex = 0
let lys = layers || (await getLayers())
let layer = lys[editIndex]
// console.log(layer)
const updateValue = name => {
const x = this.widgets.filter(w => w.name == name)[0]
x.value = layer[name]
}
if (layer) {
Array.from(['x', 'y', 'width', 'height', 'z_index'], n =>
updateValue(n)
)
}
}
let that = this
const save_edit_layer_index = i => {
let data = getLocalData('_mixlab_edit_layer')
data[that.id] = i
localStorage.setItem('_mixlab_edit_layer', JSON.stringify(data))
}
await setLayer(0)
save_edit_layer_index(0)
const edit = this.widgets.filter(w => w.name == 'edit')[0]
edit.input = $el('div', {})
edit.input.style = `
display: flex;
flex-direction:row;
align-items: center;
margin-top: 0;`
const ip = $el('input', {})
ip.className = 'comfy-multiline-input'
ip.type = 'number'
ip.min = 0
ip.step = 1
ip.max = Math.max(0, (await getLayers()).length - 1)
// ip.className = `${'comfy-multiline-input'} `
ip.value = 0
ip.style = `
background-color: var(--comfy-input-bg);
color: var(--input-text);
outline: none;
border: none;
padding: 4px;
width: 60%;
cursor: pointer;
height: 24px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = 'Layer Index'
edit.input.appendChild(label)
edit.input.appendChild(ip)
document.body.appendChild(edit.input)
ip.addEventListener('click', async event => {
console.log(await getLayers())
ip.max = Math.max(0, (await getLayers()).length - 1)
})
ip.addEventListener('change', async event => {
let index = ~~ip.value
let lys = await getLayers()
await setLayer(index, lys)
app.graph.setDirtyCanvas(true, true)
save_edit_layer_index(index)
})
// console.log('EditLayer nodeData', edit)
const onRemoved = this.onRemoved
this.onRemoved = () => {
edit.input.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = false //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'SvgImage') {
let widget = node.widgets.filter(w => w.div)[0]
let data = getLocalData('_mixlab_svg_image')
let id = node.id
// widget.div.querySelector('.Svg').value = data[id] || '#000000'
}
}
})
+556 -104
View File
@@ -106,24 +106,65 @@ async function uploadFile (file) {
// alert(navigator.mediaDevices)
async function shareScreen (webcamVideo, shareBtn, liveBtn, previewArea) {
async function shareScreen (
isCamera = false,
webcamVideo,
shareBtn,
liveBtn,
previewArea
) {
try {
// let webcamVideo = document.createElement('video')
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: true
})
let mediaStream
if (!isCamera) {
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: true
})
} else {
if (!localStorage.getItem('_mixlab_webcamera_select')) return
let constraints =
JSON.parse(localStorage.getItem('_mixlab_webcamera_select')) || {}
mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
}
webcamVideo.removeEventListener('timeupdate', videoTimeUpdateHandler)
webcamVideo.srcObject = mediaStream
webcamVideo.onloadedmetadata = () => {
let x = 0,
y = 0,
width = webcamVideo.videoWidth,
height = webcamVideo.videoHeight,
imgWidth = webcamVideo.videoWidth,
imgHeight = webcamVideo.videoHeight
let d = getSetAreaData()
if (
d &&
d.x >= 0 &&
d.imgWidth === imgWidth &&
d.imgHeight === imgHeight
) {
x = d.x
y = d.y
width = d.width
height = d.height
imgWidth = d.imgWidth
imgHeight = d.imgHeight
console.log('#screen_share::使用上一次选区')
}
updateSetAreaData(x, y, width, height, imgWidth, imgHeight)
webcamVideo.play()
createBlobFromVideo(webcamVideo, true)
webcamVideo.addEventListener('timeupdate', videoTimeUpdateHandler)
window._mixlab_screen_x = 0
window._mixlab_screen_y = 0
// console.log(webcamVideo)
window._mixlab_screen_width = webcamVideo.videoWidth
window._mixlab_screen_height = webcamVideo.videoHeight
// window._mixlab_screen_x = 0
// window._mixlab_screen_y = 0
// // console.log(webcamVideo)
// window._mixlab_screen_width = webcamVideo.videoWidth
// window._mixlab_screen_height = webcamVideo.videoHeight
}
mediaStream.addEventListener('inactive', handleStopSharing)
@@ -278,7 +319,7 @@ async function startLive (btn) {
previousImage,
currentImage
)
// console.log('#图片是否有变化:', imageChanged)
console.log('#图片是否有变化:', imageChanged)
if (imageChanged) {
window._mixlab_screen_imagePath = currentImage
@@ -328,26 +369,17 @@ async function createBlobFromVideoForArea (webcamVideo) {
return blob
}
async function createBlobFromVideo (webcamVideo) {
async function createBlobFromVideo (webcamVideo, updateImageBase64 = false) {
const videoW = webcamVideo.videoWidth
const videoH = webcamVideo.videoHeight
const aspectRatio = videoW / videoH
const WIDTH = window._mixlab_screen_width,
HEIGHT = window._mixlab_screen_height
const canvas = new OffscreenCanvas(WIDTH, HEIGHT)
const { x, y, width, height } = window._mixlab_share_screen
const canvas = new OffscreenCanvas(width, height)
const ctx = canvas.getContext('2d')
// console.log('#createBlobFromVideo', WIDTH, HEIGHT)
ctx.drawImage(
webcamVideo,
window._mixlab_screen_x,
window._mixlab_screen_y,
WIDTH,
HEIGHT,
0,
0,
WIDTH,
HEIGHT
)
ctx.drawImage(webcamVideo, x, y, width, height, 0, 0, width, height)
const blob = await canvas.convertToBlob({
type: 'image/jpeg',
@@ -356,12 +388,17 @@ async function createBlobFromVideo (webcamVideo) {
// imgElement.src = await blobToBase64(blob)
window._mixlab_screen_blob = blob
console.log('########')
// let currentImage = await blobToBase64(blob)
// // console.log(window._mixlab_screen_imagePath)
// if (!window._mixlab_screen_imagePath) {
// window._mixlab_screen_imagePath = currentImage
// }
console.log(
'########updateImageBase64 ',
updateImageBase64,
x,
y,
width,
height
)
if (updateImageBase64) {
window._mixlab_screen_imagePath = await blobToBase64(blob)
}
}
async function blobToBase64 (blob) {
@@ -391,6 +428,36 @@ function base64ToBlob (base64) {
return blob
}
async function requestCamera () {
// 请求授权
try {
let stream = await navigator.mediaDevices.getUserMedia({ video: true })
console.log('摄像头授权成功')
// 获取视频轨道
var videoTrack = stream.getVideoTracks()[0]
// 停止视频轨道
videoTrack.stop()
return true
} catch (error) {
// 用户拒绝授权或发生其他错误
console.error('摄像头授权失败:', error)
// 提示用户授权摄像头访问权限
if (error.name === 'NotAllowedError') {
alert('请授权摄像头访问权限 chrome://settings/content/camera')
} else {
alert('摄像头访问权限请求失败,请重试 chrome://settings/content/camera')
}
// // 跳转到浏览器的授权设置页面
// window.location.href = 'chrome://settings/content/camera'
}
return false
}
/*
A method that returns the required style for the html
*/
@@ -432,7 +499,7 @@ const base64Df =
app.registerExtension({
name: 'Mixlab.image.ScreenShareNode',
async getCustomWidgets (app) {
console.log('#Mixlab.image.ScreenShareNode', app)
// console.log('#Mixlab.image.ScreenShareNode', app)
return {
CHEESE (node, inputName, inputData, app) {
// We return an object containing a field CHEESE which has a function (taking node, name, data, app)
@@ -473,6 +540,46 @@ app.registerExtension({
// console.log('###widget', widget)
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
SLIDE (node, inputName, inputData, app) {
// We return an object containing a field CHEESE which has a function (taking node, name, data, app)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 12], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 12] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return window._mixlab_screen_slide_input || 0.5
}
}
// console.log('###widget', widget)
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
SEED (node, inputName, inputData, app) {
// We return an object containing a field CHEESE which has a function (taking node, name, data, app)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 12], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 12] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return window._mixlab_screen_seed_input || 0
}
}
// console.log('###widget', widget)
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
@@ -489,9 +596,15 @@ app.registerExtension({
type: 'HTML', // whatever
name: 'sreen_share', // whatever
draw (ctx, node, widget_width, y, widget_height) {
console.log('ScreenSHare', y, widget_height)
Object.assign(
this.card.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(
ctx,
widget_width,
widget_height * 5,
node.size[1]
)
)
}
}
@@ -510,8 +623,15 @@ app.registerExtension({
})
widget.previewArea = $el('div', {
style: {}
})
widget.shareDiv = $el('div', {
// innerText: 'Share Screen',
style: {
// position:'ab'
cursor: 'pointer',
fontWeight: '300',
display: 'flex'
}
})
@@ -521,7 +641,19 @@ app.registerExtension({
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
margin: '2px'
margin: '2px',
width: '100%'
}
})
widget.shareOfWebCamBtn = $el('button', {
innerText: 'Camera',
style: {
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
margin: '2px',
width: '100%'
}
})
@@ -563,20 +695,30 @@ app.registerExtension({
widget.previewCard.appendChild(widget.preview)
widget.previewCard.appendChild(widget.previewArea)
widget.card.appendChild(widget.shareBtn)
widget.card.appendChild(widget.shareDiv)
widget.shareDiv.appendChild(widget.shareBtn)
widget.shareDiv.appendChild(widget.shareOfWebCamBtn)
widget.card.appendChild(widget.openFloatingWinBtn)
widget.card.appendChild(widget.refreshInput)
widget.card.appendChild(widget.liveBtn)
widget.shareBtn.addEventListener('click', async () => {
const toggleShare = async (isCamera = false) => {
if (widget.preview.paused) {
window._mixlab_stopVideo = await shareScreen(
isCamera,
widget.preview,
widget.shareBtn,
widget.liveBtn,
widget.previewArea
)
widget.shareBtn.innerText = 'Stop Share'
if (isCamera) {
widget.shareOfWebCamBtn.innerText = 'Stop Share'
widget.shareBtn.innerText = 'Stop'
} else {
widget.shareOfWebCamBtn.innerText = 'Stop'
widget.shareBtn.innerText = 'Stop Share'
}
console.log('视频已暂停')
if (window._mixlab_stopLive) {
@@ -584,12 +726,15 @@ app.registerExtension({
window._mixlab_stopLive = null
widget.liveBtn.innerText = 'Live Run'
}
setTimeout(() => updateSetAreaDisplay(), 2000)
} else {
console.log('视频正在播放')
if (window._mixlab_stopVideo) {
window._mixlab_stopVideo()
window._mixlab_stopVideo = null
widget.shareBtn.innerText = 'Share Screen'
widget.shareOfWebCamBtn.innerText = 'Camera'
}
if (window._mixlab_stopLive) {
window._mixlab_stopLive()
@@ -597,6 +742,121 @@ app.registerExtension({
widget.liveBtn.innerText = 'Live Run'
}
}
}
// updateSetAreaDisplay(widget.previewArea, 200, 200)
widget.shareOfWebCamBtn.addEventListener('click', async () => {
if (!widget.preview.paused) {
if (window._mixlab_stopVideo) {
window._mixlab_stopVideo()
window._mixlab_stopVideo = null
widget.shareBtn.innerText = 'Share Screen'
widget.shareOfWebCamBtn.innerText = 'Camera'
}
if (window._mixlab_stopLive) {
window._mixlab_stopLive()
window._mixlab_stopLive = null
widget.liveBtn.innerText = 'Live Run'
}
return
}
let r = await requestCamera()
if (r === false) return
const devices = await navigator.mediaDevices.enumerateDevices()
// 查找摄像头设备
var cameras = devices.filter(function (device) {
// console.log(device)
return device.kind === 'videoinput'
})
// 创建 <select> 元素
var select = document.createElement('select')
// 创建默认选项
// var defaultOption = document.createElement('option')
// defaultOption.text = '请选择摄像头'
// defaultOption.disabled = true
// defaultOption.selected = true
// select.appendChild(defaultOption)
// 创建每个摄像头设备的选项
Array.from(cameras, (camera, i) => {
var option = document.createElement('option')
option.value = camera.deviceId
option.text = camera.label || 'Camera ' + (select.length - 1)
if (i === 0) option.selected = true
select.appendChild(option)
})
let modal = document.createElement('div')
modal.className = 'comfy-modal'
modal.style.display = 'flex'
let modalContent = document.createElement('div')
modalContent.className = 'comfy-modal-content'
let title = document.createElement('p')
title.innerText = 'Please select a camera'
modalContent.appendChild(title)
modalContent.appendChild(select)
let btns = document.createElement('div')
btns.style = `display: flex;
justify-content: space-between;
margin: 24px 0;`
let btn = document.createElement('button')
btn.innerText = 'OK'
btn.style = `width: 112px;`
let closeBtn = document.createElement('button')
closeBtn.innerText = 'Cancel'
closeBtn.style = `width: 112px;`
modalContent.appendChild(btns)
btns.appendChild(btn)
btns.appendChild(closeBtn)
modal.appendChild(modalContent)
document.body.appendChild(modal)
btn.addEventListener('click', () => {
// 获取所选择的选项的索引
var selectedIndex = select.selectedIndex
// 获取所选择的选项的值
var selectedValue = select.options[selectedIndex].value
if (selectedValue) {
const constraints = {
audio: false,
video: {
width: { ideal: 1920, max: 1920 },
height: { ideal: 1080, max: 1080 },
deviceId: selectedValue
}
}
localStorage.setItem(
'_mixlab_webcamera_select',
JSON.stringify(constraints)
)
toggleShare(true)
}
modal.remove()
})
closeBtn.addEventListener('click', () => {
modal.remove()
})
})
widget.shareBtn.addEventListener('click', async () => {
toggleShare()
})
widget.refreshInput.addEventListener('change', async () => {
@@ -630,12 +890,17 @@ app.registerExtension({
})
// console.log('widget.inputEl',widget.inputEl)
this.setSize([this.size[0],this.size[1]+450])
app.canvas.draw(true, true)
/*
Add the widget, make sure we clean up nicely, and we do not want to be serialized!
*/
this.addCustomWidget(widget)
this.onRemoved = function () {
widget.preview.remove()
widget.shareDiv.remove()
widget.shareOfWebCamBtn.remove()
widget.shareBtn.remove()
widget.liveBtn.remove()
widget.card.remove()
@@ -643,22 +908,114 @@ app.registerExtension({
widget.previewArea.remove()
widget.previewCard.remove()
}
this.serialize_widgets = false
this.serialize_widgets = true
}
}
}
})
function setArea (src) {
function updateSetAreaDisplay () {
try {
let canvas = document.createElement('canvas')
canvas.width = window._mixlab_screen_webcamVideo.videoWidth
canvas.height = window._mixlab_screen_webcamVideo.videoHeight
let ctx = canvas.getContext('2d')
const lineWidth = 2 // Width of the stroke line
const strokeColor = 'red' // Color of the stroke
// Draw the rectangle
ctx.strokeStyle = strokeColor // Set the stroke color
ctx.lineWidth = lineWidth // Set the stroke line width
ctx.fillStyle = 'rgba(255,0,0,0.35)'
let x = 0,
y = 0,
width = canvas.width,
height = canvas.height
if (!window._mixlab_share_screen) {
let d = getSetAreaData()
if (d) {
window._mixlab_share_screen = d
}
}
if (window._mixlab_share_screen) {
x = window._mixlab_share_screen.x
y = window._mixlab_share_screen.y
width = window._mixlab_share_screen.width
height = window._mixlab_share_screen.height
}
ctx.strokeRect(x, y, width, height) // Draw the stroked rectangle
ctx.fillRect(x, y, width, height)
canvas.style.width = '100%'
let area = graph._nodes
.filter(n => n.type === 'ScreenShare')[0]
.widgets.filter(w => w.name == 'sreen_share')[0].previewArea
area.innerHTML = ''
area.appendChild(canvas)
area.style = `
position: absolute;
width:100%%;
left:0;
top:0;
`
} catch (error) {
console.log(error)
}
}
function updateSetAreaData (left, top, width, height, imgWidth, imgHeight) {
window._mixlab_share_screen = {
x: left,
y: top,
width,
height,
imgWidth,
imgHeight
}
localStorage.setItem(
'_mixlab_share_screen',
JSON.stringify(window._mixlab_share_screen)
)
}
function getSetAreaData () {
try {
let data = JSON.parse(localStorage.getItem('_mixlab_share_screen')) || {}
if (data.width === 0 || data.height === 0 || data.width === undefined)
return
return data
} catch (error) {}
return
}
async function setArea (src) {
let displayHeight = Math.round(window.screen.availHeight * 0.6)
let div = document.createElement('div')
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0'>
<img id='ml_video' style='position: absolute; width: 500px; user-select: none; -webkit-user-drag: none;' />
<div id='ml_selection' style='position: absolute; border: 2px dashed red; pointer-events: none;'></div>
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
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;'></div>
</div>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
let im = await createImage(src)
let img = div.querySelector('#ml_video')
let overlay = div.querySelector('#ml_overlay')
let selection = div.querySelector('#ml_selection')
@@ -667,10 +1024,56 @@ function setArea (src) {
// Set video source
img.src = src
// init area
const data = getSetAreaData()
let x = 0,
y = 0,
width = (im.naturalWidth * displayHeight) / im.naturalHeight,
height = displayHeight
let imgWidth = im.naturalWidth
let imgHeight = im.naturalHeight
// console.log(
// '#screen_share::使用上一次选区 selection',
// data,
// imgWidth,
// img.width
// )
if (
data &&
data.width > 0 &&
data.height > 0 &&
data.imgWidth === imgWidth &&
data.imgHeight === imgHeight &&
data.imgHeight > 0
) {
// 相同尺寸窗口,恢复选区
x = (img.width * data.x) / data.imgWidth
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'
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)
overlay.addEventListener('click', remove)
function remove () {
overlay.removeEventListener('click', remove)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
div.remove()
}
function startSelection (event) {
if (start == false) {
@@ -732,49 +1135,27 @@ function setArea (src) {
// console.log('宽度: ' + width)
// console.log('高度: ' + height)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
// 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
// window._mixlab_screen_x = left
// window._mixlab_screen_y = top
// window._mixlab_screen_width = width
// window._mixlab_screen_height = height
try {
let canvas = document.createElement('canvas')
canvas.width = _mixlab_screen_webcamVideo.videoWidth
canvas.height = _mixlab_screen_webcamVideo.videoHeight
let ctx = canvas.getContext('2d')
const lineWidth = 1 // Width of the stroke line
const strokeColor = 'red' // Color of the stroke
if (width <= 0 && height <= 0) return remove()
// Draw the rectangle
ctx.strokeStyle = strokeColor // Set the stroke color
ctx.lineWidth = lineWidth // Set the stroke line width
ctx.strokeRect(
_mixlab_screen_x,
_mixlab_screen_y,
_mixlab_screen_width,
_mixlab_screen_height
) // Draw the stroked rectangle
updateSetAreaData(left, top, width, height, imgWidth, imgHeight)
canvas.style.width = '100%'
updateSetAreaDisplay()
let area = graph._nodes
.filter(n => n.type === 'ScreenShare')[0]
.widgets.filter(w => w.name == 'sreen_share')[0].previewArea
area.innerHTML = ''
area.appendChild(canvas)
area.style = `
position: absolute;
width:100%%;
left:0;
top:0;
`
} catch (error) {}
createBlobFromVideo(
window._mixlab_screen_webcamVideo,
!window._mixlab_screen_live
)
div.remove()
remove()
}
}
@@ -941,7 +1322,7 @@ app.registerExtension({
outline: none;background: black;`
let div = document.createElement('div')
div.style = `display:flex;position: fixed;
div.style = `display:flex;position: fixed;flex-direction: column;
bottom: 0px;
z-index: 9999;
left: 0px;
@@ -949,15 +1330,42 @@ app.registerExtension({
margin: 12px;`
let inputDiv = document.createElement('div')
inputDiv.style = `width: 100%;`
// inputDiv.style = ``
let infoDiv = document.createElement('div')
infoDiv.style = ` width: 100%;
infoDiv.style = `width: 100%;
display: flex;
justify-content: space-between;
height: 16px;
color: white;
margin-bottom: 4px;
font-size: 12px;
text-shadow: 1px 1px gray;`
infoDiv.id = 'info'
text-shadow: gray 1px 1px;
align-items: center;`
let infoText = document.createElement('div')
infoText.id = 'info'
let hideBtn = document.createElement('button')
hideBtn.innerText = '🤖'
hideBtn.style = `
border: none;
background: none;
cursor: pointer; height: 24px; margin: 4px; color: red;`
hideBtn.addEventListener('click', () => {
if (fnDiv.style.display == 'none') {
fnDiv.style.display = 'flex'
} else {
fnDiv.style.display = 'none'
}
try {
pipWindow.document.querySelector('#info').innerText = ''
} catch (error) {
console.log(error)
}
})
// Move the player to the Picture-in-Picture window.
let input = document.createElement('textarea')
@@ -1002,21 +1410,25 @@ app.registerExtension({
font-size: 16px;
margin-right: 6px;user-select: none;`
let btn = document.createElement('butotn')
btn.innerText = '❤'
btn.style = `cursor: pointer;height: 24px;margin:4px;
let seedBtn = document.createElement('butotn')
seedBtn.innerText = '🎲'
seedBtn.style = `cursor: pointer;height: 24px;margin:4px;
color: red;`
btn.addEventListener('click', () => {
if (input.style.display == 'none') {
input.style.display = 'block'
} else {
input.style.display = 'none'
}
seedBtn.addEventListener('click', () => {
window._mixlab_screen_seed_input = Math.round(
Math.floor(Math.random() * 0xffffffffffffffff)
)
try {
pipWindow.document.querySelector('#info').innerText = ''
pipWindow.document.querySelector('#info').innerText =window._mixlab_screen_seed_input
} catch (error) {
console.log(error)
}
// console.log(window._mixlab_screen_seed_input)
if (window._mixlab_screen_imagePath)
document.querySelector('#queue-button').click()
})
// TODO 需要判断是否有screenshare节点,没有的话,不需要添加
@@ -1074,7 +1486,9 @@ app.registerExtension({
console.log('##更新Prompt')
window._mixlab_screen_prompt =
window._mixlab_screen_prompt_input || window._mixlab_screen_prompt
document.querySelector('#queue-button').click()
if (window._mixlab_screen_imagePath)
document.querySelector('#queue-button').click()
try {
pipWindow.document.querySelector('#info').innerText =
@@ -1119,16 +1533,52 @@ app.registerExtension({
pipWindow.document.body.append(widget.preview)
pipWindow.document.body.append(div)
// 滑动条
const createSlide = () => {
let d = document.createElement('div')
d.style = `width: 100%;margin-bottom: 12px;`
let range = document.createElement('input')
range.type = 'range'
d.appendChild(range)
return range
}
let slideInp = createSlide()
slideInp.addEventListener('change', () => {
console.log(~~slideInp.value / 100)
window._mixlab_screen_slide_input = ~~slideInp.value / 100
try {
pipWindow.document.querySelector('#info').innerText =
window._mixlab_screen_slide_input
if (window._mixlab_screen_imagePath)
document.querySelector('#queue-button').click()
} catch (error) {
console.log(error)
}
})
// console.log(pipWindow)
div.appendChild(btnDiv)
btnDiv.appendChild(btn)
let fnDiv = document.createElement('div')
fnDiv.style = `display: flex;`
div.appendChild(infoDiv)
div.appendChild(fnDiv)
infoDiv.appendChild(infoText)
infoDiv.appendChild(hideBtn)
fnDiv.appendChild(btnDiv)
// 按钮区域
btnDiv.appendChild(seedBtn)
btnDiv.appendChild(pauseBtn)
btnDiv.appendChild(promptFinishBtn)
// 输入框
div.appendChild(inputDiv)
inputDiv.appendChild(infoDiv)
fnDiv.appendChild(inputDiv)
inputDiv.appendChild(slideInp)
inputDiv.appendChild(input)
input.addEventListener('input', () => {
@@ -1152,7 +1602,9 @@ app.registerExtension({
window._mixlab_screen_prompt =
window._mixlab_screen_prompt_input ||
window._mixlab_screen_prompt
document.querySelector('#queue-button').click()
if (window._mixlab_screen_imagePath)
document.querySelector('#queue-button').click()
try {
pipWindow.document.querySelector('#info').innerText =
@@ -1179,7 +1631,7 @@ app.registerExtension({
widget.card.remove()
widget.PictureInPicture.remove()
}
this.serialize_widgets = false
this.serialize_widgets = true
}
const onExecuted = nodeType.prototype.onExecuted
@@ -1628,7 +2080,7 @@ const node = {
},
async setup (a) {
for (const node of app.graph._nodes) {
console.log('#setup', node)
// console.log('#setup', node)
if (node.type === 'RandomPrompt') {
updateUI(node)
}
+1
View File
@@ -0,0 +1 @@
export const closeIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>'
+671 -22
View File
@@ -1,4 +1,99 @@
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'
function deepEqual (obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
return false
}
if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
return obj1 === obj2
}
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!deepEqual(obj1[key], obj2[key])) {
return false
}
}
return true
}
async function get_nodes_map () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab/nodes_map`, {
method: 'POST',
body: JSON.stringify({
data: 'json'
})
})
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);
}
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标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
injectCSS(`::-webkit-scrollbar {
width: 2px;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}`)
async function getCustomnodeMappings (mode = 'url') {
// mode = "local";
@@ -6,18 +101,39 @@ async function getCustomnodeMappings (mode = 'url') {
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
const data = await response.json()
let nodes = {}
try {
for (let url in data) {
let n = data[url]
for (let node of n[0]) {
nodes[node] = { url, title: n[1].title_aux }
}
const data = (await get_nodes_map()).data
for (let url in data) {
let n = data[url]
for (let node of n[0]) {
// if(node=='CLIPSeg')console.log('#CLIPSeg',n)
nodes[node] = { url, title: n[1].title_aux }
}
} catch (error) {}
}
// try {
// const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
// const data = await response.json()
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// } catch (error) {
// const data = (await get_nodes_map()).data
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// }
return nodes
}
@@ -55,13 +171,51 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
})
}
let nodesMap
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'
}
}
app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
) {
const nodesMap = await getCustomnodeMappings()
console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
// console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${missingNodeGithub(
missingNodeTypes,
@@ -77,12 +231,507 @@ app.showMissingNodesError = async function (
})
}
// app.ui.dialog.show = function (html) {
// console.log('###MIXLAB', html)
// if (typeof html === 'string') {
// this.textElement.innerHTML = html
// } else {
// this.textElement.replaceChildren(html)
// }
// this.element.style.display = 'flex'
// }
// app.registerExtension({
// name: 'Comfy.MDNote',
// registerCustomNodes () {
// class NoteNode {
// // color = LGraphCanvas.node_colors.yellow.color
// // bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
// // groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
// constructor () {
// if (!this.properties) {
// this.properties = {}
// this.properties.text = ''
// }
// console.log('NoteNode1', this)
// 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', {});
// widget.div.innerText='1111'
// document.body.appendChild(widget.div)
// this.addCustomWidget(widget)
// this.serialize_widgets = true
// this.isVirtualNode = true
// }
// }
// // Load default visibility
// LiteGraph.registerNodeType(
// 'MDNote',
// Object.assign(NoteNode, {
// title_mode: LiteGraph.NORMAL_TITLE,
// title: 'MDNote',
// collapsable: true
// })
// )
// NoteNode.category = '♾️Mixlab/utils'
// },
// })
async function fetchReadmeContent (url) {
try {
// var repo = 'owner/repo'; // 仓库的拥有者和名称
var match = url.match(/github.com\/([^/]+\/[^/]+)/)
var repo = match[1]
var url = `https://api.github.com/repos/${repo}/readme`
var response = await fetch(url)
var data = await response.json()
var readmeUrl = data.download_url
var readmeResponse = await fetch(readmeUrl)
var content = await readmeResponse.text()
// console.log(content) // 在控制台输出readme.md文件的内容
return content
} catch (error) {
console.log('获取readme.md文件信息失败:', error)
}
}
function createModal (url, markdown,title) {
// Create modal element
var div =
document.querySelector('#mix-modal') || document.createElement('div')
div.id = 'mix-modal'
div.innerHTML = ''
div.style.cssText = `width: 100%;
z-index: 9990;
height: 100vh;
display: flex;
color: var(--descrip-text);
position: fixed;
top: 0;
left: 0;
`
var modal = document.createElement('div')
div.appendChild(modal)
modal.classList.add("modal-body")
// Set modal styles
modal.style.cssText = `
background: white;
height: 80vh;
position: fixed;
overflow:hidden;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
border-radius: 4px;
box-shadow: 4px 4px 14px rgba(255,255,255,0.5);
`
// Create modal content area
var modalContent = document.createElement('div')
modalContent.classList.add('modal-content')
// Create modal header
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=`
color: var(--descrip-text);
font-size: 18px;
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
text-decoration: none;
font-weight: bold;
`
headTitleElement.onmouseenter = function(){
headTitleElement.style.color = 'var(--comfy-menu-bg)'
}
headTitleElement.onmouseleave = function(){
headTitleElement.style.color = 'var(--descrip-text)'
}
headTitleElement.textContent= title ||'';
headTitleElement.href = url
headTitleElement.target='_blank'
const linkIcon = document.createElement('small')
linkIcon.textContent = '🔗'
headTitleElement.appendChild(linkIcon);
headerElement.appendChild(headTitleElement)
// Create close button
const closeButton = document.createElement('span')
closeButton.classList.add('close')
closeButton.innerHTML = closeIcon
// Set close button styles
closeButton.style.cssText = `
padding: 4px;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
fill: var(--descrip-text);
`
closeButton.onmouseenter = function(){
closeButton.style.fill = 'var(--comfy-menu-bg)';
}
closeButton.onmouseleave = function(){
closeButton.style.fill = 'var(--descrip-text)';
}
headerElement.appendChild(closeButton)
// Click event to close the modal
function closeMixModal(){
div.style.display = 'none'
window.removeEventListener('keydown',MixModalEscKeyEvent)
}
closeButton.onclick = function () {
closeMixModal()
}
// Set modal content area styles
modalContent.style.cssText = `
position: relative;
padding: 0px;
overflow: hidden scroll;;
height: 100%;
min-width:300px
`
// Append close button to modal content area
modal.appendChild(headerElement)
// Create element for displaying Markdown content
var markdownContent = document.createElement('div')
markdownContent.classList.add('markdown-content','markdown-body')
markdownContent.style.cssText = `max-width: 50vw;padding: 0px 24px 100px 24px;`
showdown.setFlavor('github');
var converter = new showdown.Converter()
var html = converter.makeHtml(markdown)
// Hide images in the markdown when they fail to load
var regex = /<img[^>]+src="?([^"\s]+)"?[^>]*>/g
html = html.replace(regex, function (match, src) {
return `<img src="${src}" onerror="this.style.display='none'">`
})
// Open links in a new tab or window
html = html.replace(/<a/g, '<a target="_blank"')
// Fix href attribute to absolute path
html = html.replace(
/<a([^>]+href=["'])(?!https?:\/\/)([^"'>]+)/g,
function (match, prefix, path) {
var absolutePath = url + '/' + path
return '<a' + prefix + absolutePath
}
)
markdownContent.innerHTML = html
// Append Markdown content element to modal content area
modalContent.appendChild(markdownContent)
// Append modal content area to modal element
modal.appendChild(modalContent)
const footerElement = document.createElement('div')
footerElement.style.cssText=`
position: absolute;
left: 0;
right: 0;
text-align: right;
padding:10px;
font-size:12px
`
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.style.color = 'var(--input-text)'
}
footerText.onmouseleave=function(){
footerText.style.color = 'inherit'
}
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()
}
}
window.removeEventListener('keydown',MixModalEscKeyEvent)
window.addEventListener('keydown',MixModalEscKeyEvent)
const bgElement = document.createElement('div')
bgElement.classList.add('mix-modal-bg')
bgElement.style.cssText = `
width:100%;
height:100%;
background-color: rgba(0,0,0,0.8);
`
bgElement.onclick = function( ){
closeMixModal()
}
div.appendChild(bgElement)
}
app.registerExtension({
name: 'Comfy.Mixlab.ui',
init () {
LGraphCanvas.prototype.helpAboutNode = async function (node) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
console.log('node & node map', node, nodesMap, nodesMap[node.type])
let repo = nodesMap[node.type]
if (repo) {
let markdown = await fetchReadmeContent(repo.url)
createModal(repo.url,markdown,repo.title)
}
}
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions // store the existing method
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// 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
callback: () => {
LGraphCanvas.prototype.helpAboutNode(node)
} // 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);
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 = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 50px; width: 200px;
color: var(--descrip-text);
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;
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);
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
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
d.innerHTML = `
<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
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
}
})
d.innerHTML = `
<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;
};
}
})
+168
View File
@@ -0,0 +1,168 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.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 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'
}
}
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'
}
}
// 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]
)
)
}
}
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;
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 //需要保存参数
}
}
},
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'
}
}
})
+1 -1
View File
@@ -169,7 +169,7 @@ app.registerExtension({
this.onRemoved = function () {
// widget.card.remove()
}
this.serialize_widgets = false
this.serialize_widgets = true
}
}
}
+1077
View File
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long
+23 -2
View File
@@ -1,3 +1,24 @@
::-webkit-scrollbar {
width: 2px;
}
width: 2px;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}
+85 -63
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 22,
"last_link_id": 48,
"last_node_id": 23,
"last_link_id": 51,
"nodes": [
{
"id": 7,
@@ -105,7 +105,7 @@
{
"name": "text",
"type": "STRING",
"link": 48,
"link": 51,
"widget": {
"name": "text"
}
@@ -316,13 +316,13 @@
"1": 246
},
"flags": {},
"order": 6,
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 46
"link": 50
}
],
"properties": {
@@ -445,13 +445,13 @@
"1": 58
},
"flags": {},
"order": 7,
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 47
"link": 49
}
],
"outputs": [
@@ -472,45 +472,6 @@
512
]
},
{
"id": 22,
"type": "ScreenShare",
"pos": [
-63,
483
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
46,
47
],
"shape": 3,
"slot_index": 0
},
{
"name": "STRING",
"type": "STRING",
"links": [
48
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ScreenShare"
}
},
{
"id": 20,
"type": "FloatingVideo",
@@ -518,10 +479,10 @@
1928,
295
],
"size": [
315,
58
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 12,
"mode": 0,
@@ -534,7 +495,68 @@
],
"properties": {
"Node name for S&R": "FloatingVideo"
}
},
"widgets_values": [
null
]
},
{
"id": 23,
"type": "ScreenShare",
"pos": [
-65,
446
],
"size": {
"0": 315,
"1": 170
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
49,
50
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [
51
],
"shape": 3,
"slot_index": 1
},
{
"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
]
}
],
"links": [
@@ -787,24 +809,24 @@
"CLIP"
],
[
46,
22,
0,
2,
0,
"IMAGE"
],
[
47,
22,
49,
23,
0,
18,
0,
"IMAGE"
],
[
48,
22,
50,
23,
0,
2,
0,
"IMAGE"
],
[
51,
23,
1,
8,
1,
+66 -55
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 46,
"last_link_id": 42,
"last_node_id": 47,
"last_link_id": 46,
"nodes": [
{
"id": 27,
@@ -25,7 +25,7 @@
{
"name": "text",
"type": "STRING",
"link": 42,
"link": 46,
"widget": {
"name": "text"
}
@@ -136,7 +136,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
261818020972971,
971428736321335,
"randomize",
15,
8,
@@ -300,10 +300,10 @@
1171,
425
],
"size": [
432.46002197265625,
264.40771484375
],
"size": {
"0": 432.46002197265625,
"1": 264.40771484375
},
"flags": {},
"order": 6,
"mode": 0,
@@ -311,7 +311,7 @@
{
"name": "text",
"type": "STRING",
"link": 40,
"link": 44,
"widget": {
"name": "text"
}
@@ -329,7 +329,7 @@
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"system\",\n \"content\": \"你现在充当stable diffsion 提示词专家,根据我输入的场景或需求描述,生成提示词,由stable diffsion根据你的提示词生成画面。stable diffusion是文本到图像的扩散模型。你的任务是在各种情况下产生适当的提示,引导人工智能创造出所需的图像。\\n\\n一个优秀的提示词需要遵循下面的规范:\\n\\n“““\\n\\n简洁明了:避免过于复杂或模糊的描述,以免造成模型的混乱或错误。\\n\\n具体细致:提供足够的细节,以便模型能够准确地捕捉想要生成的图片的特征。\\n\\n逻辑连贯:避免出现矛盾或不合理的描述,以免影响模型的理解和生成。\\n\\n创意独特:有创意,展示自己的想象力和个性,以便模型能够生成有趣和新颖的图片。\\n\\n我建议你可以参考以下几个步骤思考:\\n\\n• 确定主题:首先,你需要确定想要生成图片的主题,例如风景、动物、人物等。\\n\\n• 确定风格:其次,根据主题要求,确定合适的风格(如极简、现代)或根据主题(人物、风景等)调整提示词的语言风格、词汇和描述角度。\\n\\n• 选择关键词:再次,你需要选择一些能够描述主题的关键词,常用的关键词类别包括:主体、媒介、风格、画家、网站、分辨率、额外细节、色调和光影。 可以使用名人的名字作为关键词,来控制人物形象,因为他们在训练集中出现频次较大而训练充分。\\n\\n• 组合句子:然后,你需要将关键词组合成一个或多个简单句子,提示词的句式通常如:【图片的风格】,【内容主题 】,【 细节描述】,【 绘画风格或者艺术家风格】, 用逗号或分号隔开,例如a blue sky with white clouds, a green field with yellow flowers。\\n\\n• 调整细节:最后,根据告诉你的内容主题,尽可能细致刻画画面,譬如,要画“大海”,你需要给出类似这样的提示:梦幻的大海,白沙滩岸边铺满了粉色的玫瑰花,月光轻柔的人洒在海面上,绿色发光的海浪。对于细节描述,我们可以拆分【形容词】+【视角】+【时间】+【颜色】+【其他】,形容词可以是梦幻,神秘,浪漫或者写实 ……视角可以是:超广角,俯视和仰视 ……,时间:秋天,清晨,黄昏,夜晚 ……,颜色可以是 红黄绿蓝橙紫……,其他可以包含图片的尺寸,4k,8k ,HD,光效,高细节等。\\n\\n• 如果想让生成的图片更加的艺术化、风格化,可以考虑在 提示词中添加绘画风格和艺术家。艺术绘画风格可以是一些美术风格:梵高风格,油画,水彩,古风,CG感,动漫,少女,赛博朋克,卡通画,中国画,黄昏等等,艺术家风格包含:现实主义,印象派,野兽派,新艺术,表现主义,立体主义,未来主义等等\\n\\n• 以简洁的英语输出。输出完整提示词后,把它翻译成中文,然后保留英语版本备用。\\n\\n“““\\n\\n为了让你更好的理解提示词,我收集了一些表现效果较好的提示词案例,你可以对照上面的规则学习吸收:\\n\\n“““\\n\\nMaximalist chaotic buenos aires, birds eye view, illustrated by hergé, style of tin tin comics, pen and ink\\n\\nHyper -realistic girl in blue coat, left in the woods, weird, otherworldly, real, vintage photograph, film set, 85mm lens, f/2.8 aperture\\n\\ninstagram photo of a Japanese and Scandinavian design style living room with lots of golden light, hyperrealistic surrealism, award winning masterpiece with incredible details\\n\\nPortrait of an owl, steampunk, indigo blue, colorful, illustration, highly detailed, simple, smooth, and clean vector, no jagged lines, vector art, smooth, made all with grey colored gears inspired by future technology\\n\\n“““\\n\\n请通过回复 “确认 “来确认你是否理解了这项任务。然后我将向你发送第一条命令。\"\n },\n {\n \"role\": \"user\",\n \"content\": \"证件照,女生,请认真回答噢,很重要这个问题\"\n }\n]"
"[\n {\n \"role\": \"system\",\n \"content\": \"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"\"\n }\n]"
]
},
{
@@ -339,10 +339,10 @@
1166,
740
],
"size": [
424.00797187258513,
430.6391176555039
],
"size": {
"0": 424.0079650878906,
"1": 430.6391296386719
},
"flags": {},
"order": 7,
"mode": 0,
@@ -350,7 +350,7 @@
{
"name": "text",
"type": "STRING",
"link": 39,
"link": 45,
"widget": {
"name": "text"
}
@@ -368,7 +368,7 @@
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"ChatGPT is an artificial intelligence chatbot program launched by OpenAI in November 2022. The program is based on a large language model GPT-3.5, trained with Instruction Tuning and Reinforcement Learning with Human Feedback (RLHF).\"\n },\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"GPT-3.5 is a language model developed by OpenAI, based on the Transformer architecture. It is designed to perform a wide range of natural language processing tasks, including language translation, text summarization, and text generation.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"Is there anything specific you would like to know about GPT-3.5? I am here to help.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"今天天气\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"I'm sorry, I am a text-based AI model and do not have the capability to access the current weather. However, I can tell you that it is likely to be a beautiful day, with the sun shining and a gentle breeze in the air. Enjoy your day!\"\n },\n {\n \"role\": \"user\",\n \"content\": \"今天天气不错噢,小明……\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"谢谢您的夸奖,不过我是一个人工智能模型,没有能力感知天气,只是通过语言理解回答问题。如果您需要查询当地的天气情况,建议您查询当地的天气预报。\"\n },\n {\n \"role\": \"user\",\n \"content\": \"今天天气不错噢,小明……\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"今天天气不错噢,小明……(这句话是重复的,之前已经回答过您的问题了。如果您需要查询当地的天气情况,建议您查询当地的天气预报。\"\n },\n {\n \"role\": \"user\",\n \"content\": \"证件照,女生\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"“女生分别为: Cut out of a colorful plaid skirt and white shirt, faced front, with a friendly smile, blonde-haired girl, age 22, blue eyes, naturalistic, clear, bright, young,以及人像,摄于户外,背景为美丽的自然风光,使用的相机是佳能5D Mark IV,镜头焦距为50mm,拍摄模式为肖像模式,使用的后期处理软件为Adobe Lightroom和Photoshop。\"\n },\n {\n \"role\": \"user\",\n \"content\": \"证件照,女生\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"• 拍摄证件照,要求提供女生的照片,背景可以是一片白色的天空,或者一片纯色的草地,不能有杂色。\\n• 视角应该从 above,即从上方俯视,让女生显得稳定自信。\\n• 时间应该选择在阳光明媚的早晨,这样可以让女生看起来更加青春活力。\\n• 颜色可以选择蓝色,这种颜色可以让人感到清新自然。\\n• 照片应该有高清清晰的效果,不能有模糊模糊的现象。\\n• 照片的尺寸可以选择2K或者4K,这样可以让照片更加清晰。\\n• 光效可以选择自然光,这样可以让照片看起来更加自然。\\n• 照片的风格可以选择日系或者韩系,这样可以让照片看起来更加时尚。\\n• 照片的背景可以选择浅色系,这样可以让照片看起来更加清新自然。\\n• 照片中不能有重大的人物或者动物,否则需要事先沟通好。\\n• 照片中不能有太强烈的光线或者阴影,需要调整好光线和阴影的平衡,使照片更加均匀。\\n• 照片中不能有太多的文字或者文字的内容,需要将文字或者文字的内容进行处理。\\n• 在拍摄证件照之前,需要让女生进行一些准备,比如保持好心态,调整好光线,保持好角度等等。\\n• 拍摄完证件照之后,需要将照片进行一些处理,比如进行美颜,磨皮,瘦脸等等。\"\n },\n {\n \"role\": \"user\",\n \"content\": \"证件照,女生,请认真回答噢,很重要这个问题\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"确认\"\n }\n]"
"[\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company.\"\n }\n]"
]
},
{
@@ -378,10 +378,10 @@
1092,
262
],
"size": [
635.8358236838778,
101.46092513017766
],
"size": {
"0": 635.8358154296875,
"1": 101.46092224121094
},
"flags": {},
"order": 4,
"mode": 0,
@@ -389,7 +389,7 @@
{
"name": "text",
"type": "STRING",
"link": 41,
"link": 43,
"widget": {
"name": "text"
}
@@ -407,20 +407,20 @@
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"确认"
"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company."
]
},
{
"id": 42,
"type": "ChatGPT",
"id": 47,
"type": "ChatGPTOpenAI",
"pos": [
611,
285
],
"size": [
417.3319772767625,
531.1873476643206
585,
306
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
@@ -429,8 +429,8 @@
"name": "text",
"type": "STRING",
"links": [
41,
42
43,
46
],
"shape": 3,
"slot_index": 0
@@ -439,7 +439,7 @@
"name": "messages",
"type": "STRING",
"links": [
40
44
],
"shape": 3,
"slot_index": 1
@@ -448,15 +448,26 @@
"name": "session_history",
"type": "STRING",
"links": [
39
45
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "ChatGPT"
}
"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-16k",
2220,
"randomize",
1,
null
]
}
],
"links": [
@@ -581,32 +592,32 @@
"IMAGE"
],
[
39,
42,
2,
43,
0,
"STRING"
],
[
40,
42,
1,
44,
0,
"STRING"
],
[
41,
42,
47,
0,
45,
0,
"STRING"
],
[
42,
42,
44,
47,
1,
44,
0,
"STRING"
],
[
45,
47,
2,
43,
0,
"STRING"
],
[
46,
47,
0,
27,
1,
+251
View File
@@ -0,0 +1,251 @@
{
"last_node_id": 5,
"last_link_id": 4,
"nodes": [
{
"id": 4,
"type": "ChatGPTOpenAI",
"pos": [
-512,
-236
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
2
],
"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-35-turbo",
6933,
"randomize",
1,
null
]
},
{
"id": 2,
"type": "ChatGPTOpenAI",
"pos": [
-36,
-234
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 2,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
3
],
"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,
"",
"增加丰富的细节和光影,摄影技巧,镜头语言,材质肌理",
"gpt-3.5-turbo",
2836,
"randomize",
1,
null
]
},
{
"id": 5,
"type": "ChatGPTOpenAI",
"pos": [
-30,
183
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 3,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
4
],
"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,
"",
"翻译成英文,并按照格式输出: 画面、主题、细节、灯光、氛围、艺术家、其他",
"gpt-3.5-turbo",
1085,
"randomize",
1,
null
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
447,
-229
],
"size": [
503.79851499517997,
356.0560985581077
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 4,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"Picture: The picture is composed of carefully chosen elements, capturing the subject matter in a visually striking way.\n\nTheme: The theme of the photograph could vary, from capturing nature's beauty to showcasing urban landscapes, human emotions, or abstract concepts.\n\nDetails: The photograph captures intricate details, bringing attention to the subject's textures, colors, shapes, and patterns.\n\nLighting: The photographer manipulates lighting, using techniques like natural light, dramatic shadows, or artificial lighting to enhance the mood and atmosphere of the photograph.\n\nAmbiance: The photograph evokes a specific ambiance or mood, whether it's serene, mysterious, joyful, melancholic, or any other emotional response.\n\nArtist: The photographer skillfully crafts the image, demonstrating their artistic vision, technical skills, and creative expression through the composition, framing, and post-processing choices.\n\nOthers: Apart from the elements mentioned above, the photograph may also incorporate other creative techniques like long exposure, multiple exposures, color grading, or unconventional perspectives to create a unique and captivating image.\n\nIn the future, as technology and imagination continue to advance, photography will likely continue to evolve and innovate, offering even more realistic and awe-inspiring visual experiences for humans."
]
}
],
"links": [
[
2,
4,
0,
2,
0,
"STRING"
],
[
3,
2,
0,
5,
0,
"STRING"
],
[
4,
5,
0,
3,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
+731
View File
@@ -0,0 +1,731 @@
{
"last_node_id": 24,
"last_link_id": 59,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
515,
130
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 33
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
515,
1268
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 59,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
51
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 6,
"type": "LoraLoader",
"pos": [
515,
460
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 31
},
{
"name": "clip",
"type": "CLIP",
"link": 45
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
44
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoraLoader"
},
"widgets_values": [
"lcm-lora-sdv1-5.safetensors",
1,
1
]
},
{
"id": 5,
"type": "KSampler",
"pos": [
1015,
130
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 51
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 29
},
{
"name": "latent_image",
"type": "LATENT",
"link": 54
},
{
"name": "denoise",
"type": "FLOAT",
"link": 56,
"widget": {
"name": "denoise"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
758428944049342,
"fixed",
4,
1.6,
"lcm",
"karras",
1
]
},
{
"id": 11,
"type": "VAEDecode",
"pos": [
1430,
130
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 35
},
{
"name": "vae",
"type": "VAE",
"link": 34
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
41
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
1740,
130
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [
515,
716
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 55
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 23,
"type": "VAEEncode",
"pos": [
515,
1092
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 57
},
{
"name": "vae",
"type": "VAE",
"link": 58
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
54
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
100,
130
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
31
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
32,
33,
45
],
"shape": 3,
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
34,
58
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 24,
"type": "ScreenShare",
"pos": [
100,
358
],
"size": {
"0": 315,
"1": 170
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
55,
57
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [
59
],
"shape": 3,
"slot_index": 1
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": [
56
],
"shape": 3,
"slot_index": 2
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
3,
6,
0,
5,
0,
"MODEL"
],
[
5,
8,
0,
5,
2,
"CONDITIONING"
],
[
6,
9,
0,
5,
3,
"LATENT"
],
[
7,
10,
0,
6,
0,
"MODEL"
],
[
8,
10,
1,
6,
1,
"CLIP"
],
[
9,
5,
0,
11,
0,
"LATENT"
],
[
10,
10,
2,
11,
1,
"VAE"
],
[
11,
6,
1,
7,
0,
"CLIP"
],
[
12,
6,
1,
8,
0,
"CLIP"
],
[
14,
11,
0,
3,
0,
"IMAGE"
],
[
17,
1,
2,
7,
1,
"STRING"
],
[
18,
7,
0,
15,
0,
"CONDITIONING"
],
[
19,
15,
0,
5,
1,
"CONDITIONING"
],
[
20,
16,
0,
15,
1,
"CONTROL_NET"
],
[
24,
1,
0,
18,
0,
"IMAGE"
],
[
25,
18,
0,
15,
2,
"IMAGE"
],
[
29,
7,
0,
5,
2,
"CONDITIONING"
],
[
31,
10,
0,
6,
0,
"MODEL"
],
[
32,
10,
1,
8,
0,
"CLIP"
],
[
33,
10,
1,
7,
0,
"CLIP"
],
[
34,
10,
2,
11,
1,
"VAE"
],
[
35,
5,
0,
11,
0,
"LATENT"
],
[
41,
11,
0,
20,
0,
"IMAGE"
],
[
44,
6,
0,
5,
0,
"MODEL"
],
[
45,
10,
1,
6,
1,
"CLIP"
],
[
51,
8,
0,
5,
1,
"CONDITIONING"
],
[
54,
23,
0,
5,
3,
"LATENT"
],
[
55,
24,
0,
2,
0,
"IMAGE"
],
[
56,
24,
2,
5,
4,
"FLOAT"
],
[
57,
24,
0,
23,
0,
"IMAGE"
],
[
58,
10,
2,
23,
1,
"VAE"
],
[
59,
24,
1,
8,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
+862
View File
@@ -0,0 +1,862 @@
{
"last_node_id": 28,
"last_link_id": 64,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
515,
130
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 33
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 6,
"type": "LoraLoader",
"pos": [
515,
460
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 31
},
{
"name": "clip",
"type": "CLIP",
"link": 45
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
44
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoraLoader"
},
"widgets_values": [
"lcm-lora-sdv1-5.safetensors",
1,
1
]
},
{
"id": 11,
"type": "VAEDecode",
"pos": [
1430,
130
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 35
},
{
"name": "vae",
"type": "VAE",
"link": 34
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
41
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 23,
"type": "VAEEncode",
"pos": [
515,
1092
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 57
},
{
"name": "vae",
"type": "VAE",
"link": 58
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
54
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
100,
130
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
31
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
32,
33,
45
],
"shape": 3,
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
34,
58
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
515,
1268
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 62,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
51
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
1274,
536
],
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 5,
"type": "KSampler",
"pos": [
1004,
130
],
"size": [
315,
262
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 51
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 29
},
{
"name": "latent_image",
"type": "LATENT",
"link": 54
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
758428944049342,
"fixed",
4,
1.6,
"lcm",
"karras",
0.61
]
},
{
"id": 24,
"type": "ScreenShare",
"pos": [
62,
543
],
"size": {
"0": 315,
"1": 194
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
55,
57
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [],
"shape": 3,
"slot_index": 1
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": [],
"shape": 3,
"slot_index": 2
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [
-416,
516
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 55
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 26,
"type": "ChatGPTOpenAI",
"pos": [
5,
1325
],
"size": [
400,
357.99993896484375
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 61,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
62,
63
],
"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,
"",
"你需要根据用户的输入,转化为英文单词的短句子组合,输出英文",
"gpt-35-turbo",
1249,
"randomize",
1,
null
]
},
{
"id": 28,
"type": "SpeechSynthesis",
"pos": [
542,
1570
],
"size": [
315,
76
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 63,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
"A very handsome elderly gentleman."
]
},
{
"id": 25,
"type": "SpeechRecognition",
"pos": [
-5,
1101
],
"size": [
392.5606180664065,
166.64923671875022
],
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
61
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
null
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
3,
6,
0,
5,
0,
"MODEL"
],
[
5,
8,
0,
5,
2,
"CONDITIONING"
],
[
6,
9,
0,
5,
3,
"LATENT"
],
[
7,
10,
0,
6,
0,
"MODEL"
],
[
8,
10,
1,
6,
1,
"CLIP"
],
[
9,
5,
0,
11,
0,
"LATENT"
],
[
10,
10,
2,
11,
1,
"VAE"
],
[
11,
6,
1,
7,
0,
"CLIP"
],
[
12,
6,
1,
8,
0,
"CLIP"
],
[
14,
11,
0,
3,
0,
"IMAGE"
],
[
17,
1,
2,
7,
1,
"STRING"
],
[
18,
7,
0,
15,
0,
"CONDITIONING"
],
[
19,
15,
0,
5,
1,
"CONDITIONING"
],
[
20,
16,
0,
15,
1,
"CONTROL_NET"
],
[
24,
1,
0,
18,
0,
"IMAGE"
],
[
25,
18,
0,
15,
2,
"IMAGE"
],
[
29,
7,
0,
5,
2,
"CONDITIONING"
],
[
31,
10,
0,
6,
0,
"MODEL"
],
[
32,
10,
1,
8,
0,
"CLIP"
],
[
33,
10,
1,
7,
0,
"CLIP"
],
[
34,
10,
2,
11,
1,
"VAE"
],
[
35,
5,
0,
11,
0,
"LATENT"
],
[
41,
11,
0,
20,
0,
"IMAGE"
],
[
44,
6,
0,
5,
0,
"MODEL"
],
[
45,
10,
1,
6,
1,
"CLIP"
],
[
51,
8,
0,
5,
1,
"CONDITIONING"
],
[
54,
23,
0,
5,
3,
"LATENT"
],
[
55,
24,
0,
2,
0,
"IMAGE"
],
[
57,
24,
0,
23,
0,
"IMAGE"
],
[
58,
10,
2,
23,
1,
"VAE"
],
[
61,
25,
0,
26,
0,
"STRING"
],
[
62,
26,
0,
8,
1,
"STRING"
],
[
63,
26,
0,
28,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}