Compare commits

...
26 Commits
Author SHA1 Message Date
shadowcz007 dfe720f3ec v0.2.5.1 2023-12-05 00:22:43 +08:00
shadowcz007 2c68662c22 文件名冲突引起的插件不生效 2023-12-05 00:21:43 +08:00
shadowcz007 bc1b998ba5 Update gpt.js 2023-12-04 23:56:44 +08:00
shadowcz007 9d5ccc3389 v0.2.5 2023-12-04 20:15:10 +08:00
shadowcz007 a811f884cc update 2023-12-04 20:07:29 +08:00
shadowcz007 8b86c379d1 v0.2.5
新增GPT节点
2023-12-04 20:01:51 +08:00
shadowcz007 8c0321b1cf Update ui.js 2023-12-02 20:03:38 +08:00
shadowcz007 62ab2c3514 readme 2023-12-02 17:22:24 +08:00
shadowcz007 ed61ca761a v0.2.4
Clicking on the floating window image can copy it to the clipboard.
2023-12-02 11:55:15 +08:00
shadowcz007 95ca17d816 点击悬浮窗图片可以拷贝到剪切板 2023-12-02 11:54:30 +08:00
shadowcz007 db6c721a8f 单击图片可复制到剪切板 2023-12-02 11:35:27 +08:00
shadow b5c68751aa Merge pull request #17 from shadowcz007/v0.3-psd读取分层
V0.3 psd读取分层
2023-12-02 00:42:46 +08:00
shadowcz007 7780bfd671 v0.2.3 2023-12-02 00:42:21 +08:00
shadow a56970693a Merge pull request #15 from shadowcz007/main
1
2023-12-01 23:37:02 +08:00
shadowcz007 c8b24fe84b Update Watcher.py 2023-12-01 23:00:45 +08:00
shadowcz007 798aabf333 bugfix 2023-12-01 22:10:27 +08:00
shadow d38a7aa558 Merge pull request #14 from shadowcz007/v0.3-psd读取分层
v0.2.2
2023-12-01 19:54:15 +08:00
shadowcz007 0bdf1e47a6 v0.2.2
- 本地读取节点也可以更新prompt了
2023-12-01 19:53:50 +08:00
shadowcz007 a44016e57c Update README.md 2023-12-01 18:20:03 +08:00
shadowcz007 35a9351e53 1 2023-12-01 17:51:31 +08:00
shadowcz007 c1b2112bb8 Update requirements.txt 2023-12-01 16:39:16 +08:00
shadowcz007 42a3dd261f v0.2.1 2023-12-01 13:31:00 +08:00
shadowcz007 e14487fab1 优化体验
更友好的https提示
版本更新提示
缺失节点提示
2023-12-01 13:30:19 +08:00
shadowcz007 c16df1a473 Update checkVersion.js 2023-11-30 23:50:43 +08:00
shadowcz007 9272844251 loadimagefromlocal 2023-11-30 23:48:51 +08:00
shadowcz007 b30c1d19cc v0.2 2023-11-30 23:45:51 +08:00
23 changed files with 2952 additions and 712 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
__pycache__/
https/
https/
nodes/config.json
workflow/my_workflow.json
+33 -3
View File
@@ -1,6 +1,5 @@
##
In progress.
!!
![screenshare](./assets/screenshare.png)
@@ -15,6 +14,24 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
!! Please use the address with HTTPS (https://127.0.0.1).
### 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
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### GPT
> ChatGPT、ChatGLM3 , Some code provided by rui.
![gpt-workflow.svg](./assets/gpt-workflow.svg)
[workflow-5](./workflow/5-gpt-workflow.json)
## Installation
manually install, simply clone the repo into the custom_nodes directory with this command:
@@ -48,7 +65,7 @@ pip3 install -r requirements.txt
## Nodes
![main](./assets/all.png)
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
[workflow-1](./workflow/1-workflow.json)
@@ -61,6 +78,10 @@ pip3 install -r requirements.txt
![TransparentImage](./assets/TransparentImage.png)
> Consistency Decoder
[openai Consistency Decoder]( https://github.com/openai/consistencydecoder)
@@ -77,6 +98,13 @@ Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
### Improvement
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
![node-not-found](./assets/node-not-found.png)
### Models
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : model/clipseg
@@ -86,6 +114,8 @@ Add edges to an image.
#### Thanks:
[ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
#### discussions:
[discussions](https://github.com/shadowcz007/comfyui-mixlab-nodes/discussions)
### TODO:
- vector https://github.com/GeorgLegato/stable-diffusion-webui-vectorstudio
+134 -15
View File
@@ -2,12 +2,17 @@
import os
import subprocess
import importlib.util
import sys
import sys,json
import urllib
import datetime
python = sys.executable
from server import PromptServer
try:
import aiohttp
from aiohttp import web
@@ -36,7 +41,7 @@ def is_installed(package, package_overwrite=None):
if result.returncode != 0:
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
else:
print('##pyOpenSSL OK')
print(package+'## OK')
try:
import OpenSSL
@@ -48,6 +53,19 @@ except ImportError:
is_installed('pyOpenSSL')
sys.exit()
try:
import watchdog
except ImportError:
print("Module 'watchdog' not installed. Please install it via:")
print("pip install watchdog")
print("or")
print("pip install -r requirements.txt")
is_installed('watchdog')
sys.exit()
current_path = os.path.abspath(os.path.dirname(__file__))
def create_key(key_p,crt_p):
import OpenSSL
@@ -79,23 +97,65 @@ def create_key(key_p,crt_p):
f.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, certificate))
return
def create_for_https():
current_path = os.path.abspath(os.path.dirname(__file__))
# print("#####path::", current_path)
# TODO 处理路径
def create_for_https():
# print("#####path::", current_path)
https_key_path=os.path.join(current_path, "https")
crt=os.path.join(https_key_path, "certificate.crt")
key=os.path.join(https_key_path, "private.key")
print("##https_key_path", crt,key)
# print("##https_key_path", crt,key)
if not os.path.exists(https_key_path):
# 使用mkdir()方法创建新目录
os.mkdir(https_key_path)
if not os.path.exists(crt):
create_key(key,crt)
print('https_key OK: ', crt,key)
return (crt,key)
# workflow
def read_workflow_json_files(folder_path):
json_files = []
for filename in os.listdir(folder_path):
if filename.endswith('.json'):
json_files.append(filename)
data = []
for file in json_files:
file_path = os.path.join(folder_path, file)
try:
with open(file_path) as json_file:
json_data = json.load(json_file)
creation_time=datetime.datetime.fromtimestamp(os.path.getctime(file_path))
numeric_timestamp = creation_time.timestamp()
file_info = {
'filename': file,
'data': json_data,
'date': numeric_timestamp
}
data.append(file_info)
except Exception as e:
print(e)
sorted_data = sorted(data, key=lambda x: x['date'], reverse=True)
return sorted_data
def get_workflows():
# print("#####path::", current_path)
workflow_path=os.path.join(current_path, "workflow")
print('workflow_path: ',workflow_path)
if not os.path.exists(workflow_path):
# 使用mkdir()方法创建新目录
os.mkdir(workflow_path)
workflows=read_workflow_json_files(workflow_path)
return workflows
def save_workflow_json(data):
workflow_path=os.path.join(current_path, "workflow/my_workflow.json")
with open(workflow_path, 'w') as file:
json.dump(data, file)
return workflow_path
# https
async def new_start(self, address, port, verbose=True, call_on_start=None):
runner = web.AppRunner(self.app, access_log=None)
@@ -113,9 +173,10 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
if address == '':
address = '0.0.0.0'
if verbose:
print("Starting server\n")
print("To see the GUI go to: http://{}:{}".format(address, port))
print("To see the GUI go to: https://{}:{}".format(address, port+1))
# print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print("\033[93mStarting server\n")
print("\033[93mTo see the GUI go to: http://{}:{}".format(address, port))
print("\033[93mTo see the GUI go to: https://{}:{}\033[0m".format(address, port+1))
if call_on_start is not None:
call_on_start(address, port)
@@ -128,6 +189,57 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
PromptServer.start=new_start
# 创建路由表
routes = web.RouteTableDef()
@routes.post('/mixlab')
async def mixlab_hander(request):
config=os.path.join(current_path, "nodes/config.json")
data={}
try:
if os.path.exists(config):
with open(config, 'r') as f:
data = json.load(f)
# print(data)
except Exception as e:
print(e)
return web.json_response(data)
@routes.post('/mixlab/workflow')
async def mixlab_workflow_hander(request):
data = await request.json()
result={}
try:
if 'task' in data:
if data['task']=='save':
file_path=save_workflow_json(data['data'])
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='list':
result={
'data':get_workflows(),
'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)
self.app.add_routes(self.routes)
for name, dir in nodes.EXTENSION_WEB_DIRS.items():
self.app.add_routes([
web.static('/extensions/' + urllib.parse.quote(name), dir, follow_symlinks=True),
])
self.app.add_routes([
web.static('/', self.web_root, follow_symlinks=True),
])
PromptServer.add_routes=new_add_routes
@@ -148,6 +260,7 @@ from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,AreaToMask,Smoo
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,SessionHistory
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
@@ -166,21 +279,27 @@ NODE_CLASS_MAPPINGS = {
"VAEDecodeConsistencyDecoder":VAEDecode,
"ScreenShare":ScreenShareNode,
"FloatingVideo":FloatingVideo,
"CLIPSeg":CLIPSeg,
"CombineMasks":CombineMasks
"CLIPSeg_":CLIPSeg,
"CombineMasks_":CombineMasks,
"ChatGPT":ChatGPTNode,
"SessionHistory":SessionHistory
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"RandomPrompt": "Random Prompt #Example Node",
"RandomPrompt": "Random Prompt #Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode"
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode",
"ScreenShare":"ScreenShare #Mixlab",
"FloatingVideo":"FloatingVideo #Mixlab",
"ChatGPT":"ChatGPT #Mixlab",
"SessionHistory":"SessionHistory #Mixlab"
}
# web ui的节点功能
WEB_DIRECTORY = "./web"
print('--------------')
print('\033[34mMixlab Custom Nodes: \033[92mLoaded\033[0m')
print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print('--------------')
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 522 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+160
View File
@@ -0,0 +1,160 @@
import openai
import time
import urllib.error
import re,json
# 判断是否是azure服务
def is_azure_url(url):
pattern = r'.*\.azure\.com$'
if re.match(pattern, url):
return True
else:
return False
def azure_client(key,url):
client = openai.AzureOpenAI(
api_key=key,
# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning
api_version="2023-07-01-preview",
# https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
azure_endpoint=url
)
return client
def openai_client(key,url):
client = openai.OpenAI(
api_key=key,
base_url=url
)
return client
def chat(client, model_name,messages ):
try_count = 0
while True:
try_count += 1
try:
response = client.chat.completions.create(
model=model_name,
messages=messages
)
break
except openai.AuthenticationError as ex:
raise ex
except (urllib.error.HTTPError, openai.OpenAIError) as ex:
if try_count >= 3:
raise ex
time.sleep(5)
continue
finish_reason = response.choices[0].finish_reason
if finish_reason != "stop":
raise RuntimeError("API finished with unexpected reason: " + finish_reason)
content=""
try:
content=response.choices[0].message.content
except:
content=response.choices[0].delta['content']
return content
class ChatGPTNode:
def __init__(self):
# self.__client = OpenAI()
self.session_history = [] # 用于存储会话历史的列表
self.seed=0
self.system_content="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key":("KEY", {"default": "", "multiline": True}),
"api_url":("URL", {"default": "", "multiline": True}),
"prompt": ("STRING", {"default": "", "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"],
{"default": "gpt-3.5-turbo"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
},
}
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("text","session_history",)
FUNCTION = "generate_contextual_text"
CATEGORY = "Mixlab/GPT"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def generate_contextual_text(self,
api_key,
api_url,
prompt,
system_content,
model,
seed):
print(api_key,
api_url,
prompt,
system_content,
model,
seed)
# 可以选择保留会话历史以维持上下文记忆
# 或者在此处清除会话历史 self.session_history.clear()
if seed!=self.seed:
self.seed=seed
self.session_history=[]
# 把系统信息和初始信息添加到会话历史中
if system_content:
self.system_content=system_content
# self.session_history=[]
# self.session_history.append({"role": "system", "content": system_content})
#
if is_azure_url(api_url):
client=azure_client(api_key,api_url)
else:
client=openai_client(api_key,api_url)
print('openai url')
# 把用户的提示添加到会话历史中
# 调用API时传递整个会话历史
messages=[{"role": "system", "content": self.system_content}]+self.session_history+[{"role": "user", "content": prompt}]
response_content = chat(client,model,messages)
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
return (response_content,json.dumps(self.session_history, indent=4),)
class SessionHistory:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"session_history": ("STRING", {"forceInput": True}),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "Mixlab/GPT"
def run(self, session_history):
# print(session_history)
return {"ui": {"text": session_history}, "result": (session_history,)}
+94 -38
View File
@@ -1,6 +1,6 @@
import numpy as np
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence
from PIL.PngImagePlugin import PngInfo
import base64,os
from io import BytesIO
@@ -9,6 +9,9 @@ import json
from comfy.cli_args import args
import cv2
from .Watcher import FolderWatcher
MAX_RESOLUTION=8192
@@ -150,25 +153,48 @@ def get_not_transparent_area(image):
return (x, y, w, h)
# 读取不了分层
def load_psd(image):
layers=[]
print('load_psd',image.format)
if image.format=='PSD':
layers = [frame.copy() for frame in ImageSequence.Iterator(image)]
print('#PSD',len(layers))
else:
image = ImageOps.exif_transpose(image) #校对方向
layers.append(image)
return layers
def load_image(fp,white_bg=False):
i = Image.open(fp)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
if white_bg==True:
nw = mask.unsqueeze(0).unsqueeze(-1).repeat(1, 1, 1, 3)
# 将mask的黑色部分对image进行白色处理
image[nw == 1] = 1.0
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
return (image,mask)
im = Image.open(fp)
# ims=load_psd(im)
im = ImageOps.exif_transpose(im) #校对方向
ims=[im]
images=[]
for i in ims:
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
if white_bg==True:
nw = mask.unsqueeze(0).unsqueeze(-1).repeat(1, 1, 1, 3)
# 将mask的黑色部分对image进行白色处理
image[nw == 1] = 1.0
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
images.append({
"image":image,
"mask":mask
})
return images
# 获取图片s
@@ -180,23 +206,27 @@ def get_images_filepath(f,white_bg=False):
for file in files:
file_path = os.path.join(root, file)
try:
(im,mask)=load_image(file_path,white_bg)
images.append({
"image":im,
"mask":mask,
"file_path":file_path
})
imgs=load_image(file_path,white_bg)
for img in imgs:
images.append({
"image":img['image'],
"mask":img['mask'],
"file_path":file_path,
"psd":len(imgs)>1
})
except:
print('非图片',file_path)
elif os.path.isfile(f):
try:
(im,mask)=load_image(f,white_bg)
images.append({
"image":im,
"mask":mask,
"file_path":f
})
imgs=load_image(f,white_bg)
for img in imgs:
images.append({
"image":img['image'],
"mask":img['mask'],
"file_path":file_path,
"psd":len(imgs)>1
})
except:
print('非图片',f)
else:
@@ -555,6 +585,9 @@ class EnhanceImage:
return (image,)
'''
("STRING",{"multiline": False,"default": "Hello World!"})
对应 widgets.js 里:
@@ -566,39 +599,61 @@ const multiline = !!inputData[1].multiline;
# 支持输出1张
#
class LoadImagesFromPath:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"file_path": ("STRING",{"multiline": False,"default": ""})
"file_path": ("STRING",{"multiline": False,"default": ""}),
},
"optional":{
"white_bg": (["disable","enable"],),
"newest_files": (["enable", "disable"],),
"index_variable":("INT", {
"default": -1,
"default": 0,
"min": -1, #Minimum value
"max": 2048, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"watcher":(["disable","enable"],),
"result": ("WATCHER",),#为了激活本节点运行
"prompt": ("PROMPT",),
# "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ('IMAGE','MASK')
RETURN_TYPES = ('IMAGE','MASK','STRING')
FUNCTION = "run"
CATEGORY = "Mixlab/image"
# INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,True,)
OUTPUT_IS_LIST = (True,True,False,)
global watcher_folder
watcher_folder=None
# 运行的函数
def run(self,file_path,white_bg,newest_files,index_variable,seed):
print(file_path)
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result,prompt):
global watcher_folder
# print('###监听:',watcher_folder,watcher,file_path,result)
if watcher_folder==None:
watcher_folder = FolderWatcher(file_path)
watcher_folder.set_folder_path(file_path)
if watcher=='enable':
# 在这里可以进行其他操作,监听会在后台持续
watcher_folder.set_folder_path(file_path)
watcher_folder.start()
else:
if watcher_folder!=None:
watcher_folder.stop()
images=get_images_filepath(file_path,white_bg=='enable')
# 排序
@@ -616,7 +671,8 @@ class LoadImagesFromPath:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
return (imgs,masks)
print('#prompt::::',prompt)
return (imgs,masks,prompt,)
# TODO 扩大选区的功能,重新输出mask
+40 -9
View File
@@ -24,9 +24,42 @@ def base64_save(base64_data):
return (image,mask)
# # 把白色部分处理成黑色
# def convert_to_bw(image):
# # 读取图片
# # image = Image.open(image_path)
# # 获取图片的宽度和高度
# width, height = image.size
# # 遍历图片的每个像素点
# for x in range(width):
# for y in range(height):
# # 获取当前像素点的RGB值
# r, g, b = image.getpixel((x, y))
# # 判断当前像素点是否为白色
# if r == 255 and g == 255 and b == 255:
# # 将白色部分处理成黑色
# image.putpixel((x, y), (0, 0, 0))
# else:
# # 将非白色部分处理成白色
# image.putpixel((x, y), (255, 255, 255))
# # 转换为黑白图
# mask = image.convert("L")
# # # 保存处理后的图片
# # image.save("black_white_image.jpg")
# # print("图片处理完成!")
# return mask
def load_image(i,white_bg=False):
# i = Image.open(fp)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
@@ -49,10 +82,10 @@ class ScreenShareNode:
},
"optional":{
"prompt": ("PROMPT",),
"seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
# "seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
} }
RETURN_TYPES = ('IMAGE','MASK','STRING')
RETURN_TYPES = ('IMAGE','STRING')
FUNCTION = "run"
@@ -62,10 +95,10 @@ class ScreenShareNode:
OUTPUT_IS_LIST = (False,False,False)
# 运行的函数
def run(self,image_base64,prompt,seed):
def run(self,image_base64,prompt):
im,mask=base64_save(image_base64)
print('##########prompt',prompt)
return (im,mask,prompt)
# print('##########prompt',prompt)
return (im,prompt)
class FloatingVideo:
@@ -73,9 +106,7 @@ class FloatingVideo:
def INPUT_TYPES(s):
return { "required":{
"images": ("IMAGE",)
}, "optional":{
"seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
} }
}, }
# RETURN_TYPES = ('IMAGE','MASK')
RETURN_TYPES = ()
@@ -89,7 +120,7 @@ class FloatingVideo:
# OUTPUT_IS_LIST = (False,False,)
# 运行的函数
def run(self,images,seed):
def run(self,images):
results = list()
+123
View File
@@ -0,0 +1,123 @@
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import time
import json
import os
# current_directory = os.getcwd()
# print('Watcher:',current_directory)
def save_to_json(file_path, data):
try:
with open(file_path, 'w') as f:
json.dump(data, f)
except Exception as e:
print(e)
def read_from_json(file_path):
data={}
try:
with open(file_path, 'r') as f:
data = json.load(f)
except Exception as e:
print(e)
return data
# read_from_json()
current_path = os.path.abspath(os.path.dirname(__file__))
config_json=os.path.join(current_path,'config.json')
# print('Watcher:',config_json)
def read_config():
config={}
try:
if os.path.exists(config_json):
config=read_from_json(config_json)
except Exception as e:
print(e)
return config
class FolderWatcher:
def __init__(self, folder_path):
self.folder_path = folder_path
if folder_path:
config=read_config()
config['folder_path']=folder_path
save_to_json(config_json,config)
self.observer = None
self.event_handler = self._create_event_handler()
self.status = "Not started"
self.event_type='-'
def _create_event_handler(self):
class EventHandler(FileSystemEventHandler):
def on_any_event(self, event):
if event.is_directory:
return
elif event.event_type == 'created':
print(f"File created: {event.src_path}")
self.event_type=event.event_type+'_'+str(int(time.time()))
elif event.event_type == 'deleted':
print(f"File deleted: {event.src_path}")
self.event_type=event.event_type+'_'+str(int(time.time()))
elif event.event_type == 'modified':
print(f"File modified: {event.src_path}")
self.event_type=event.event_type+'_'+str(int(time.time()))
elif event.event_type == 'moved':
print(f"File moved: {event.src_path} to {event.dest_path}")
self.event_type=event.event_type+'_'+str(int(time.time()))
print(event.event_type)
config=read_config()
config['event_type']=self.event_type
save_to_json(config_json,config)
return EventHandler()
def set_folder_path(self, new_folder_path):
self.folder_path = new_folder_path
if new_folder_path:
config=read_config()
config['folder_path']=new_folder_path
save_to_json(config_json,config)
self.event_type='-'
def start(self):
self.observer = Observer()
self.observer.schedule(self.event_handler, self.folder_path, recursive=True)
self.observer.start()
self.status = "Listening"
self.event_type='-'
print('Listening')
def stop(self):
if self.observer!=None:
self.observer.stop()
self.observer.join()
self.observer=None
self.status = "Stopped"
self.event_type='-'
print('Stopped')
# # 示例用法
# if __name__ == "__main__":
# folder_path = "C:\\\\Users\\\\38957\\\\Documents\\\\GitHub\\\\extract-anything\\\\outputs" # 替换为您要监听的文件夹路径
# watcher_folder = FolderWatcher(folder_path)
# watcher_folder.start()
# watcher_folder.stop()
# watcher_folder.start()
# watcher_folder.stop()
# while True:
# 1
+4
View File
@@ -1,2 +1,6 @@
numpy
pyOpenSSL
watchdog
opencv-python-headless
matplotlib
openai
+38
View File
@@ -0,0 +1,38 @@
import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.2.5.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)
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>
<a style=" color: #2196F3;
font-size: 18px;
font-weight: 800;
letter-spacing: 2px;
}"
href="https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/">https://github.com/shadowcz007/comfyui-mixlab-nodes/releases</a>
`)
// window.alert(
// `Please proceed to the official repository to download the latest version.https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/`
// )
// window.open(
// 'https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/'
// )
}
})
.catch(error => {
console.error('Error fetching release information:', error)
})
// #MixCopilot
+239
View File
@@ -0,0 +1,239 @@
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 getConfig () {
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`, {
method: 'POST'
})
return await res.json()
}
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.GPT.ChatGPT',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return localStorage.getItem('_mixlab_api_key') || ''
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
URL (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // 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
},
async serializeValue (nodeId, widgetIndex) {
return localStorage.getItem('_mixlab_api_url') || ''
}
}
// 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) {
// console.log(nodeType.comfyClass)
if (nodeType.comfyClass == 'ChatGPT') {
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)
const widget = {
type: 'div',
name: 'chatgpt div',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputKey = document.createElement('input'),
inputUrl = document.createElement('input')
inputKey.type = 'text'
inputUrl.type = 'text'
inputKey.placeholder = 'Key'
inputUrl.placeholder = 'URL'
inputKey.style = `margin:4px 48px;`
inputUrl.style = `margin:4px 48px`
inputUrl.value = localStorage.getItem('_mixlab_api_url') || ''
inputKey.value = localStorage.getItem('_mixlab_api_key') || ''
widget.div.appendChild(inputKey)
widget.div.appendChild(inputUrl)
inputKey.addEventListener('change', () => {
api_key.serializeValue = () => inputKey.value || ''
localStorage.setItem('_mixlab_api_key', inputKey.value)
})
inputUrl.addEventListener('change', () => {
api_url.serializeValue = () => inputUrl.value || ''
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
this.onRemoved = () => {
inputUrl.remove()
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = false
}
}
}
})
app.registerExtension({
name: 'Mixlab.GPT.SessionHistory',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'SessionHistory') {
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('SessionHistory', 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
try {
res = JSON.stringify(JSON.parse(list), null, 2)
} catch (error) {
// console.log(list)
}
w.value = res
}
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)
}
const onConfigure = nodeType.prototype.onConfigure
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments)
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values)
}
}
}
}
})
@@ -1,5 +1,5 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
@@ -29,6 +29,36 @@ async function interrupt () {
})
}
async function clipboardWriteImage (win, url) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// console.log(url)
const img = await createImage(url)
// console.log(img)
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0)
// 将canvas转为blob
canvas.toBlob(async blob => {
const data = [
new ClipboardItem({
[blob.type]: blob
})
]
win.navigator.clipboard
.write(data)
.then(() => {
console.log('Image copied to clipboard')
})
.catch(error => {
console.error('Failed to copy image to clipboard:', error)
})
})
}
async function uploadFile (file) {
try {
const body = new FormData()
@@ -76,7 +106,7 @@ async function uploadFile (file) {
// alert(navigator.mediaDevices)
async function shareScreen (webcamVideo, shareBtn, liveBtn) {
async function shareScreen (webcamVideo, shareBtn, liveBtn, previewArea) {
try {
// let webcamVideo = document.createElement('video')
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
@@ -100,7 +130,7 @@ async function shareScreen (webcamVideo, shareBtn, liveBtn) {
// 停止共享的回调函数
function handleStopSharing () {
console.log('用户停止了共享')
// console.log('用户停止了共享')
// 执行其他操作
if (window._mixlab_stopVideo) {
window._mixlab_stopVideo()
@@ -119,7 +149,6 @@ async function shareScreen (webcamVideo, shareBtn, liveBtn) {
async function videoTimeUpdateHandler () {
if (!window._mixlab_screen_live) return
createBlobFromVideo(webcamVideo)
}
} catch (error) {
@@ -133,6 +162,7 @@ async function shareScreen (webcamVideo, shareBtn, liveBtn) {
webcamVideo.srcObject = null // 清空视频源对象
window._mixlab_screen_live = false
window._mixlab_screen_blob = null
previewArea.innerHTML = ''
interrupt()
}
}
@@ -145,26 +175,124 @@ async function sleep (t = 200) {
})
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
async function compareImages (threshold, previousImage, currentImage) {
// 将 base64 转换为 Image 对象
var previousImg = await createImage(previousImage)
var currentImg = await createImage(currentImage)
if (
previousImg.naturalWidth != currentImg.naturalWidth ||
previousImg.naturalHeight != currentImg.naturalHeight
) {
return true // 图片有变化
}
// 创建一个 canvas 元素
var canvas1 = document.createElement('canvas')
canvas1.width = previousImg.naturalWidth
canvas1.height = previousImg.naturalHeight
var context1 = canvas1.getContext('2d')
// 将图片绘制到 canvas 上
context1.drawImage(previousImg, 0, 0)
// 获取图片的像素数据
var previousData = context1.getImageData(
0,
0,
previousImg.naturalWidth,
previousImg.naturalHeight
).data
var canvas2 = document.createElement('canvas')
canvas2.width = currentImg.naturalWidth
canvas2.height = currentImg.naturalHeight
var context2 = canvas2.getContext('2d')
context2.drawImage(currentImg, 0, 0)
var currentData = context2.getImageData(
0,
0,
currentImg.naturalWidth,
currentImg.naturalHeight
).data
// 遍历每个像素点,计算像素差异
var pixelDiff = 0
for (var i = 0; i < previousData.length; i += 4) {
var diffR = Math.abs(previousData[i] - currentData[i])
var diffG = Math.abs(previousData[i + 1] - currentData[i + 1])
var diffB = Math.abs(previousData[i + 2] - currentData[i + 2])
// 计算像素差异总和
pixelDiff += diffR + diffG + diffB
}
// 计算平均像素差异
var averageDiff = pixelDiff / (previousData.length / 4)
// 判断平均像素差异是否超过阈值
// console.log(
// pixelDiff,
// averageDiff,
// threshold,
// currentImg.naturalWidth,
// currentImg.naturalHeight,previousData,currentData
// )
if (averageDiff > threshold) {
return true // 图片有变化
} else {
return false // 图片无变化
}
}
async function startLive (btn) {
if (btn) window._mixlab_screen_live = !window._mixlab_screen_live
if (btn) btn.innerText = `Stop Live`
// console.log('#ML', 'live run', window._mixlab_screen_time)
if (window._mixlab_screen_time) {
// console.log('#ML', 'live')
return
}
// if (window._mixlab_screen_time) {
// // console.log('#ML', 'live')
// return
// }
const { Pending, Running } = await getQueue()
// console.log('#ML', Pending, window._mixlab_screen_blob)
if (Pending <= 1 && window._mixlab_screen_blob && Running === 0) {
window._mixlab_screen_time = true
// console.log(file)
window._mixlab_screen_imagePath = await blobToBase64(
window._mixlab_screen_blob
)
// window._mixlab_screen_time = true
const threshold = 1 // 阈值
const previousImage = window._mixlab_screen_imagePath // 上一张图片的 base64
let currentImage = await blobToBase64(window._mixlab_screen_blob)
if (previousImage) {
// 现在新的图片的 base64
const imageChanged = await compareImages(
threshold,
previousImage,
currentImage
)
// console.log('#图片是否有变化:', imageChanged)
if (imageChanged) {
window._mixlab_screen_imagePath = currentImage
document.querySelector('#queue-button').click()
}
} else {
window._mixlab_screen_imagePath = currentImage
// console.log(window._mixlab_screen_imagePath)
document.querySelector('#queue-button').click()
}
// await uploadFile(file)
window._mixlab_screen_time = false
document.querySelector('#queue-button').click()
// window._mixlab_screen_time = false
await sleep(window._mixlab_screen_refresh_rate || 200)
// console.log('#ML', window._mixlab_screen_imagePath)
}
@@ -196,7 +324,7 @@ async function createBlobFromVideoForArea (webcamVideo) {
type: 'image/jpeg',
quality: 1
})
// imgElement.src = await blobToBase64(blob)
return blob
}
@@ -208,6 +336,7 @@ async function createBlobFromVideo (webcamVideo) {
HEIGHT = window._mixlab_screen_height
const canvas = new OffscreenCanvas(WIDTH, HEIGHT)
const ctx = canvas.getContext('2d')
// console.log('#createBlobFromVideo', WIDTH, HEIGHT)
ctx.drawImage(
webcamVideo,
window._mixlab_screen_x,
@@ -226,6 +355,13 @@ 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
// }
}
async function blobToBase64 (blob) {
@@ -296,6 +432,7 @@ const base64Df =
app.registerExtension({
name: 'Mixlab.image.ScreenShareNode',
async getCustomWidgets (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)
@@ -313,8 +450,8 @@ app.registerExtension({
return window._mixlab_screen_imagePath || base64Df
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
// console.log('#Mixlab.image.ScreenShareNode',widget)
node.addCustomWidget(widget)
return widget // and returns it.
},
PROMPT (node, inputName, inputData, app) {
@@ -333,7 +470,7 @@ app.registerExtension({
return window._mixlab_screen_prompt || ''
}
}
console.log('###widget', widget)
// console.log('###widget', widget)
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
@@ -361,6 +498,8 @@ app.registerExtension({
widget.card = $el('div', {})
widget.previewCard = $el('div', {})
widget.preview = $el('video', {
style: {
width: '100%'
@@ -419,25 +558,26 @@ app.registerExtension({
})
document.body.appendChild(widget.card)
widget.card.appendChild(widget.preview)
widget.card.appendChild(widget.previewArea)
widget.card.appendChild(widget.previewCard)
widget.previewCard.appendChild(widget.preview)
widget.previewCard.appendChild(widget.previewArea)
widget.card.appendChild(widget.shareBtn)
widget.card.appendChild(widget.openFloatingWinBtn)
widget.card.appendChild(widget.refreshInput)
widget.card.appendChild(widget.liveBtn)
// widget.inputEl.addEventListener('click', () => {
// shareScreen(widget.inputEl)
// })
widget.shareBtn.addEventListener('click', async () => {
if (widget.preview.paused) {
window._mixlab_stopVideo = await shareScreen(
widget.preview,
widget.shareBtn,
widget.liveBtn
widget.liveBtn,
widget.previewArea
)
widget.shareBtn.innerText = 'Stop Share'
console.log('视频已暂停')
if (window._mixlab_stopLive) {
window._mixlab_stopLive()
@@ -477,7 +617,11 @@ app.registerExtension({
})
widget.openFloatingWinBtn.addEventListener('click', async () => {
// console.log()
// if (window._mixlab_stopLive) {
// window._mixlab_stopLive()
// window._mixlab_stopLive = null
// widget.liveBtn.innerText = 'Live Run'
// }
let blob = await createBlobFromVideoForArea(
window._mixlab_screen_webcamVideo
)
@@ -497,6 +641,7 @@ app.registerExtension({
widget.card.remove()
widget.refreshInput.remove()
widget.previewArea.remove()
widget.previewCard.remove()
}
this.serialize_widgets = false
}
@@ -511,7 +656,7 @@ function setArea (src) {
<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>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
let img = div.querySelector('#ml_video')
@@ -534,61 +679,6 @@ function setArea (src) {
updateSelection(event)
start = true
} else {
// 获取img元素的真实宽度和高度
let imgWidth = img.naturalWidth
let imgHeight = img.naturalHeight
// 换算起始坐标
let realStartX = (startX / img.offsetWidth) * imgWidth
let realStartY = (startY / img.offsetHeight) * imgHeight
// 换算起始坐标
let realEndX = (endX / img.offsetWidth) * imgWidth
let realEndY = (endY / img.offsetHeight) * imgHeight
// 输出结果到控制台
// console.log('真实宽度: ' + realWidth)
// console.log('真实高度: ' + realHeight)
startX = realStartX
startY = realStartY
endX = realEndX
endY = realEndY
// Calculate width, height, and coordinates
let width = Math.abs(endX - startX)
let height = Math.abs(endY - startY)
let left = Math.min(startX, endX)
let top = Math.min(startY, endY)
// Output results to console
// console.log('坐标位置: (' + left + ', ' + top + ')')
// console.log('宽度: ' + width)
// console.log('高度: ' + height)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
div.remove()
window._mixlab_screen_x = left
window._mixlab_screen_y = top
window._mixlab_screen_width = width
window._mixlab_screen_height = height
try {
let area = graph._nodes
.filter(n => n.type === 'ScreenShare')[0]
.widgets.filter(w => w.name == 'sreen_share')[0].previewArea
area.style = `
position: absolute;
background: red;
width:${
100 * (_mixlab_screen_width / _mixlab_screen_webcamVideo.videoWidth)
}%;
height:${
100 * (_mixlab_screen_height / _mixlab_screen_webcamVideo.videoWidth)
}%;
left:${100 * (_mixlab_screen_x / _mixlab_screen_webcamVideo.videoWidth)}%;
top:${100 * (_mixlab_screen_y / _mixlab_screen_webcamVideo.videoHeight)}%;
`
} catch (error) {}
}
}
@@ -612,9 +702,112 @@ function setArea (src) {
function endSelection (event) {
endX = event.clientX
endY = event.clientY
// 获取img元素的真实宽度和高度
let imgWidth = img.naturalWidth
let imgHeight = img.naturalHeight
// 换算起始坐标
let realStartX = (startX / img.offsetWidth) * imgWidth
let realStartY = (startY / img.offsetHeight) * imgHeight
// 换算起始坐标
let realEndX = (endX / img.offsetWidth) * imgWidth
let realEndY = (endY / img.offsetHeight) * imgHeight
// 输出结果到控制台
// console.log('真实宽度: ' + realWidth)
// console.log('真实高度: ' + realHeight)
startX = realStartX
startY = realStartY
endX = realEndX
endY = realEndY
// Calculate width, height, and coordinates
let width = Math.abs(endX - startX)
let height = Math.abs(endY - startY)
let left = Math.min(startX, endX)
let top = Math.min(startY, endY)
// Output results to console
// console.log('坐标位置: (' + left + ', ' + top + ')')
// console.log('宽度: ' + width)
// console.log('高度: ' + height)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
window._mixlab_screen_x = left
window._mixlab_screen_y = top
window._mixlab_screen_width = width
window._mixlab_screen_height = height
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
// 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
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) {}
div.remove()
}
}
async function save_workflow (json) {
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/workflow`, {
method: 'POST',
body: JSON.stringify({
data: json,
task: 'save'
})
})
return await res.json()
}
async function get_my_workflow () {
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/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'list'
})
})
let result = await res.json()
return result.data
}
app.registerExtension({
name: 'Mixlab.image.FloatingVideo',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
@@ -638,6 +831,7 @@ app.registerExtension({
widget.preview = $el('video', {
controls: true,
draggable: true,
style: {
width: '100%'
},
@@ -645,6 +839,28 @@ app.registerExtension({
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
})
// })
// const dropTarget = document.getElementById('your-drop-target-id')
// dropTarget.addEventListener('dragover', event => {
// event.preventDefault()
// })
// dropTarget.addEventListener('drop', event => {
// event.preventDefault()
// const imageUrl = event.dataTransfer.getData('text/plain')
// navigator.clipboard
// .writeText(imageUrl)
// .then(() => {
// console.log('Image URL copied to clipboard')
// })
// .catch(error => {
// console.error('Failed to copy image URL to clipboard:', error)
// })
// })
widget.canvas = $el('canvas', {
style: {
display: 'none'
@@ -655,7 +871,6 @@ app.registerExtension({
innerText: 'PictureInPicture',
style: {
display: 'pictureInPictureEnabled' in document ? 'block' : 'none',
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
@@ -668,25 +883,52 @@ app.registerExtension({
widget.card.appendChild(widget.preview)
widget.card.appendChild(widget.canvas)
widget.preview.addEventListener('click', event => {
const imageUrl = window._mixlab_screen_result || ''
// console.log(imageUrl)
try {
if (imageUrl) clipboardWriteImage(pipWindow, imageUrl)
} catch (error) {
console.log(error)
if (imageUrl) clipboardWriteImage(window, imageUrl)
}
// pipWindow.navigator.permissions
// .query({ name: 'clipboard-write' })
// .then(result => {
// if (result.state === 'granted' || result.state === 'prompt') {
// // 执行复制操作
// } else {
// console.error('Clipboard write permission denied')
// }
// })
})
// widget.card.appendChild(widget.PictureInPicture)
widget.PictureInPicture.addEventListener('click', async () => {
if (window.location.protocol != 'https:') {
let http_workflow = app.graph.serialize()
await save_workflow(http_workflow)
window.alert(
`About to redirect to HTTPS access.https://${
`Redirecting to HTTPS access due to the requirement of the floating window. https://${
window.location.hostname
}:${~~window.location.port + 1}`
)
window.open(
`https://${window.location.hostname}:${
~~window.location.port + 1
}`
}?workflow=1`
)
}
// Open a Picture-in-Picture window.
let w = 360,
s = widget.preview.videoWidth / widget.preview.videoHeight,
h = w / s
h = w / s || w
console.log(h)
const pipWindow = await documentPictureInPicture.requestWindow({
width: w,
height: Math.round(h) + 120
@@ -704,9 +946,19 @@ app.registerExtension({
z-index: 9999;
left: 0px;
width: calc(100% - 24px);
margin: 12px;`
margin: 12px;`
let inputDiv = document.createElement('div')
// inputDiv.style = ``
let infoDiv = document.createElement('div')
infoDiv.style = ` width: 100%;
height: 16px;
color: white;
margin-bottom: 4px;
font-size: 12px;
text-shadow: 1px 1px gray;`
infoDiv.id = 'info'
// console.log(pipWindow.document)
// Move the player to the Picture-in-Picture window.
let input = document.createElement('textarea')
input.style = `
@@ -724,6 +976,15 @@ app.registerExtension({
border: 1px solid rgb(91, 91, 91);
font-family: sans-serif;
`
// Create a style element
const style = document.createElement('style')
// Define the CSS rule for scrollbar width
const cssRule = `::-webkit-scrollbar { width: 2px;}`
// Add the CSS rule to the style element
style.appendChild(document.createTextNode(cssRule))
// Append the style element to the document head
pipWindow.document.head.appendChild(style)
window._mixlab_screen_prompt =
window._mixlab_screen_prompt ||
@@ -739,7 +1000,7 @@ app.registerExtension({
align-items: center;
width: 24px;
font-size: 16px;
margin-right: 6px;`
margin-right: 6px;user-select: none;`
let btn = document.createElement('butotn')
btn.innerText = '❤'
@@ -751,8 +1012,14 @@ app.registerExtension({
} else {
input.style.display = 'none'
}
try {
pipWindow.document.querySelector('#info').innerText = ''
} catch (error) {
console.log(error)
}
})
// TODO 需要判断是否有screenshare节点,没有的话,不需要添加
let pauseBtn = document.createElement('butotn')
pauseBtn.innerText = '⏸'
pauseBtn.style = `cursor: pointer;height: 24px;margin:4px;
@@ -773,6 +1040,13 @@ app.registerExtension({
if (w) {
w.liveBtn.innerText = 'Live Run'
}
try {
pipWindow.document.querySelector('#info').innerText =
'Stop Live'
} catch (error) {
console.log(error)
}
} else {
pauseBtn.innerText = '⏸'
let node = this.graph._nodes.filter(
@@ -784,20 +1058,112 @@ app.registerExtension({
window._mixlab_stopLive = await startLive(w.liveBtn)
console.log('window._mixlab_stopLive', window._mixlab_stopLive)
}
try {
pipWindow.document.querySelector('#info').innerText = 'Live'
} catch (error) {
console.log(error)
}
}
})
let promptFinishBtn = document.createElement('butotn')
promptFinishBtn.innerText = '🚀'
promptFinishBtn.style = `cursor: pointer;height: 24px;margin:4px;`
promptFinishBtn.addEventListener('click', () => {
console.log('##更新Prompt')
window._mixlab_screen_prompt =
window._mixlab_screen_prompt_input || window._mixlab_screen_prompt
document.querySelector('#queue-button').click()
try {
pipWindow.document.querySelector('#info').innerText =
'Update Prompt'
} catch (error) {
console.log(error)
}
})
widget.preview.addEventListener('click', event => {
const imageUrl = window._mixlab_screen_result || ''
// console.log(imageUrl)
try {
if (imageUrl) clipboardWriteImage(pipWindow, imageUrl)
} catch (error) {
console.log(error)
if (imageUrl) clipboardWriteImage(window, imageUrl)
}
try {
pipWindow.document.querySelector('#info').innerText =
'Image copied to clipboard'
setTimeout(
() =>
(pipWindow.document.querySelector('#info').innerText = ''),
8000
)
} catch (error) {
console.log(error)
}
// pipWindow.navigator.permissions
// .query({ name: 'clipboard-write' })
// .then(result => {
// if (result.state === 'granted' || result.state === 'prompt') {
// // 执行复制操作
// } else {
// console.error('Clipboard write permission denied')
// }
// })
})
pipWindow.document.body.append(widget.preview)
pipWindow.document.body.append(div)
// console.log(pipWindow)
div.appendChild(btnDiv)
btnDiv.appendChild(btn)
btnDiv.appendChild(pauseBtn)
div.appendChild(input)
btnDiv.appendChild(promptFinishBtn)
// 输入框
div.appendChild(inputDiv)
inputDiv.appendChild(infoDiv)
inputDiv.appendChild(input)
input.addEventListener('input', () => {
window._mixlab_screen_prompt = input.value
window._mixlab_screen_prompt_input = input.value
try {
pipWindow.document.querySelector('#info').innerText = ''
} catch (error) {
console.log(error)
}
})
input.addEventListener('keydown', handleKeyDown)
function handleKeyDown (event) {
if (event.key === 'Enter') {
if (!event.shiftKey) {
// 回车键被按下且未同时按下Shift键,执行你的操作
event.preventDefault() // 阻止默认行为(如提交表单)
// 在这里添加你的代码
console.log('##更新Prompt')
window._mixlab_screen_prompt =
window._mixlab_screen_prompt_input ||
window._mixlab_screen_prompt
document.querySelector('#queue-button').click()
try {
pipWindow.document.querySelector('#info').innerText =
'Update Prompt'
} catch (error) {
console.log(error)
}
}
}
}
// Move the player back when the Picture-in-Picture window closes.
pipWindow.addEventListener('pagehide', event => {
widget.card.appendChild(widget.preview)
@@ -829,7 +1195,11 @@ app.registerExtension({
const videoTrack = stream.getVideoTracks()[0]
video.preview.srcObject = new MediaStream([videoTrack])
video.preview.play()
try {
video.preview.play()
} catch (error) {
console.log(error)
}
// 检查浏览器是否支持画中画模式
if ('pictureInPictureEnabled' in document) {
@@ -862,7 +1232,7 @@ app.registerExtension({
const context = canvas.getContext('2d')
if (message?.images_) {
const base64 = message.images_[0]
window._mixlab_screen_result = `data:image/png;base64,${message.images_[0]}`
const image = new Image()
image.onload = function () {
canvas.width = image.width
@@ -870,7 +1240,7 @@ app.registerExtension({
context.drawImage(image, 0, 0)
}
// console.log(`data:image/jpeg;base64,${base64}`)
image.src = `data:image/jpeg;base64,${base64}`
image.src = window._mixlab_screen_result
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
@@ -1208,31 +1578,77 @@ const importWorkflow = my => {
app.canvas.pasteFromClipboard()
}
function getURLParameters (url) {
var params = {}
var paramStr = url.split('?')[1]
if (paramStr) {
var paramArr = paramStr.split('&')
for (var i = 0; i < paramArr.length; i++) {
var param = paramArr[i].split('=')
var paramName = decodeURIComponent(param[0])
var paramValue = decodeURIComponent(param[1] || '')
if (paramName) {
if (params[paramName]) {
params[paramName] = Array.isArray(params[paramName])
? [...params[paramName], paramValue]
: [params[paramName], paramValue]
} else {
params[paramName] = paramValue
}
}
}
}
return params
}
// var url = "http://example.com/page?param1=value1&param2=value2&param3=value3";
// var params = getURLParameters(url);
// console.log(params);
const node = {
name: 'RandomPrompt',
async init (app) {
// Any initial setup to run as soon as the page loads
console.log('[logging]', 'extension init')
if (window.location.href.match('/?')) {
const { workflow } = getURLParameters(window.location.href)
if (workflow)
get_my_workflow().then(data => {
console.log('#get_my_workflow', data)
let my_workflow = data.filter(
d => d.filename == 'my_workflow.json'
)[0]
if (my_workflow?.data) {
// app.loadGraphData(my_workflow.data)
localStorage.setItem('workflow', JSON.stringify(my_workflow.data))
}
})
}
},
async setup (a) {
for (const node of app.graph._nodes) {
if (node.comfyClass === 'RandomPrompt') {
console.log('#setup', node)
console.log('#setup', node)
if (node.type === 'RandomPrompt') {
updateUI(node)
}
}
console.log('[logging]', 'loaded graph node: ', exportGraph(app.graph))
// console.log('[logging]', 'loaded graph node: ', exportGraph(app.graph))
},
addCustomNodeDefs (defs, app) {
console.log(
'[logging]',
'add custom node definitions',
'current nodes:',
defs
)
// console.log(
// '[logging]',
// 'add custom node definitions',
// 'current nodes:',
// defs
// )
// 在这里进行 语言切换
for (const nodeName in defs) {
if (nodeName === 'RandomPrompt') {
// defs[nodeName].category
// defs[nodeName].display_name
}
}
// for (const nodeName in defs) {
// if (nodeName === 'RandomPrompt') {
// // defs[nodeName].category
// // defs[nodeName].display_name
// }
// }
},
loadedGraphNode (node, app) {
// Fires for each node when loading/dragging/etc a workflow json or png
@@ -1241,11 +1657,11 @@ const node = {
// console.log("[logging]", "loaded graph node: ", exportGraph(node.graph));
},
async nodeCreated (node) {
if (node.comfyClass === 'RandomPrompt') {
if (node.type === 'RandomPrompt') {
updateUI(node)
}
if (node.comfyClass === 'RunWorkflow') {
if (node.type === 'RunWorkflow') {
const pw = node.widgets.filter(w => w.name === 'workflow')[0]
console.log('nodeCreated', pw)
// if (pw) {
@@ -1265,17 +1681,17 @@ const node = {
// 汉化
// app.graph._nodes // title ='123'
if (nodeData.name === 'SaveTransparentImage') {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const r = onExecuted?.apply?.(this, arguments)
console.log('executed', message)
const { image_path } = message
if (image_path) {
}
return r
}
}
// if (nodeData.name === 'SaveTransparentImage') {
// const onExecuted = nodeType.prototype.onExecuted
// nodeType.prototype.onExecuted = function (message) {
// const r = onExecuted?.apply?.(this, arguments)
// console.log('executed', message)
// const { image_path } = message
// if (image_path) {
// }
// return r
// }
// }
if (nodeData.name === 'WSServer') {
// Create the button widget for selecting the files
+88
View File
@@ -0,0 +1,88 @@
import { app } from '../../../scripts/app.js'
async function getCustomnodeMappings (mode = 'url') {
// mode = "local";
let api_host = `${window.location.hostname}:${window.location.port}`
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 }
}
}
} catch (error) {}
return nodes
}
const missingNodeGithub = (missingNodeTypes, nodesMap) => {
let ts = {}
Array.from(new Set(missingNodeTypes), n => {
if (nodesMap[n]) {
let title = nodesMap[n].title
if (!ts[title]) {
ts[title] = {
title,
nodes: {},
url: nodesMap[n].url
}
}
ts[title].nodes[n] = 1
} else {
ts[n] = {
title: n,
nodes: {},
url: `https://github.com/search?q=${n}&type=code`
}
ts[n].nodes[n] = 1
}
})
return Array.from(Object.values(ts), n => {
const url = n.url
return `<li style="color: white;
background: black;
padding: 8px;
font-size: 12px;">${n.title}<a href="${url}" target="_blank"> 🔗</a></li>`
})
}
app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
) {
const nodesMap = await getCustomnodeMappings()
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,
nodesMap
).join('')}</ul>${
hasAddedNodes
? 'Nodes that have failed to load will show as red on the graph.'
: ''
}`
)
this.logging.addEntry('Comfy.App', 'warn', {
MissingNodes: missingNodeTypes
})
}
// 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'
// }
+176
View File
@@ -0,0 +1,176 @@
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 getConfig () {
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`, {
method: 'POST'
})
return await res.json()
}
if (!window._mixlab_screen_prompt)
window._mixlab_screen_prompt =
'beautiful scenery nature glass bottle landscape,under water'
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.image.LoadImagesFromPath',
async getCustomWidgets (app) {
return {
WATCHER (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {
// // 绘制文件图标的函数
// function drawFileIcon () {
// // 清空画布
// // ctx.clearRect(0, 0, canvas.width, canvas.height)
// // 绘制文件外框
// ctx.fillStyle = '#000'
// ctx.fillRect(5, 5, 40, 40)
// // 绘制文件夹图标
// ctx.fillStyle = '#f00'
// ctx.fillRect(10, 15, 30, 20)
// // 绘制监听符号
// ctx.beginPath()
// ctx.arc(30, 35, 5, 0, 2 * Math.PI)
// ctx.fillStyle = '#00f'
// ctx.fill()
// }
// // 调用绘制函数
// drawFileIcon()
},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return window._mixlab_file_path_watcher || ''
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
PROMPT (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // 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
},
async serializeValue (nodeId, widgetIndex) {
return window._mixlab_screen_prompt || ''
}
}
// 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) {
// console.log(nodeType.comfyClass)
if (nodeType.comfyClass == 'LoadImagesFromPath') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('watch widtget', this.widgets)
const watcher = this.widgets.filter(w => w.name == 'watcher')[0]
watcher.callback = () => {
if (watcher.value === 'enable') {
if (window._mixlab_watcher_t)
clearInterval(window._mixlab_watcher_t)
window._mixlab_watcher_t = setInterval(() => {
// 上次路径填充
getConfig().then(json => {
console.log(json.event_type)
if (json.event_type != window._mixlab_file_path_watcher) {
window._mixlab_file_path_watcher = json.event_type
// widget.card.innerText = window._mixlab_file_path_watcher || ''
//运行
document.querySelector('#queue-button').click()
}
})
}, 1000)
} else {
if (window._mixlab_watcher_t) {
clearInterval(window._mixlab_watcher_t)
}
window._mixlab_watcher_t = null
}
}
// 上次路径填充
getConfig().then(json => {
let w = this.widgets.filter(w => w.name == 'file_path')[0]
if (w) {
w.value = json.folder_path || ''
}
// console.log(json.event_type)
window._mixlab_file_path_watcher = json.event_type
})
/*
Add the widget, make sure we clean up nicely, and we do not want to be serialized!
*/
// this.addCustomWidget(widget)
this.onRemoved = function () {
// widget.card.remove()
}
this.serialize_widgets = false
}
}
}
})
+3
View File
@@ -0,0 +1,3 @@
::-webkit-scrollbar {
width: 2px;
}
+108 -114
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 21,
"last_link_id": 45,
"last_node_id": 22,
"last_link_id": 48,
"nodes": [
{
"id": 7,
@@ -14,7 +14,7 @@
"1": 200
},
"flags": {},
"order": 7,
"order": 4,
"mode": 0,
"inputs": [
{
@@ -89,12 +89,12 @@
504,
33
],
"size": [
400,
200
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 6,
"order": 8,
"mode": 0,
"inputs": [
{
@@ -105,7 +105,7 @@
{
"name": "text",
"type": "STRING",
"link": 43,
"link": 48,
"widget": {
"name": "text"
}
@@ -295,7 +295,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
613900833686415,
1115769645491668,
"randomize",
4,
1.6,
@@ -311,94 +311,24 @@
338,
786
],
"size": [
210,
246
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 4,
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 37
"link": 46
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 19,
"type": "ScreenShare",
"pos": [
-55,
512
],
"size": {
"0": 325.3117370605469,
"1": 459.7692565917969
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
37,
42
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "STRING",
"type": "STRING",
"links": [
43
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "ScreenShare"
}
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
2041,
277
],
"size": [
315,
106
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
}
},
{
"id": 6,
"type": "LoraLoader",
@@ -411,7 +341,7 @@
"1": 126
},
"flags": {},
"order": 8,
"order": 5,
"mode": 0,
"inputs": [
{
@@ -463,7 +393,7 @@
"1": 98
},
"flags": {},
"order": 3,
"order": 2,
"mode": 0,
"outputs": [
{
@@ -515,13 +445,13 @@
"1": 58
},
"flags": {},
"order": 5,
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 42
"link": 47
}
],
"outputs": [
@@ -541,6 +471,70 @@
"widgets_values": [
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",
"pos": [
1928,
295
],
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
}
}
],
"links": [
@@ -744,14 +738,6 @@
0,
"LATENT"
],
[
37,
19,
0,
2,
0,
"IMAGE"
],
[
38,
16,
@@ -784,22 +770,6 @@
0,
"IMAGE"
],
[
42,
19,
0,
18,
0,
"IMAGE"
],
[
43,
19,
2,
8,
1,
"STRING"
],
[
44,
6,
@@ -815,6 +785,30 @@
6,
1,
"CLIP"
],
[
46,
22,
0,
2,
0,
"IMAGE"
],
[
47,
22,
0,
18,
0,
"IMAGE"
],
[
48,
22,
1,
8,
1,
"STRING"
]
],
"groups": [],
File diff suppressed because it is too large Load Diff
+618
View File
@@ -0,0 +1,618 @@
{
"last_node_id": 34,
"last_link_id": 35,
"nodes": [
{
"id": 27,
"type": "CLIPTextEncode",
"pos": [
1961.178268896482,
527.6060791015625
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 24
},
{
"name": "text",
"type": "STRING",
"link": 35,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
21
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 28,
"type": "CLIPTextEncode",
"pos": [
1958.7452854980445,
266
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 25
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
22
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 24,
"type": "KSampler",
"pos": [
2434.0233006347635,
80
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 21
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 22
},
{
"name": "latent_image",
"type": "LATENT",
"link": 23
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
26
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
264070254513477,
"randomize",
15,
8,
"euler",
"karras",
1
]
},
{
"id": 29,
"type": "VAEDecode",
"pos": [
2799.0233006347635,
80
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 26
},
{
"name": "vae",
"type": "VAE",
"link": 27
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
31
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 26,
"type": "EmptyLatentImage",
"pos": [
2069.0233006347635,
80
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
23
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 25,
"type": "CheckpointLoaderSimple",
"pos": [
1593.7452854980445,
80
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
24,
25
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
27
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 31,
"type": "PreviewImage",
"pos": [
2439,
408
],
"size": {
"0": 563.4671630859375,
"1": 720.8866577148438
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 31
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 33,
"type": "SessionHistory",
"pos": [
1300,
430
],
"size": [
580.1640810546876,
223.1105875244142
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "session_history",
"type": "STRING",
"link": 33,
"widget": {
"name": "session_history"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SessionHistory"
},
"widgets_values": [
"[\n {\n \"role\": \"user\",\n \"content\": \"黑客与画家\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"\\\"Hacker and Painter\\\" is a popular Chinese comic series written and illustrated by Cheng先辈, which has gained a large following worldwide.\\n\\nThe series revolves around the lives of two main characters, a young hacker and an artist, who embark on a journey of self-discovery and personal growth. Set in a futuristic, dystopian world, the story combines elements of science fiction, technology, and action with a touch of humor and satire.\\n\\nThe art style in \\\"Hacker and Painter\\\" is a mix of sci-fi and traditional Chinese ink painting, with bold lines, vibrant colors, and intricate details. Cheng先辈 employs a unique painting technique, using watercolor washes and ink to create a dynamic, expressive, and visually striking visual style.\\n\\nThe artist's use of photography and various camera angles adds an extra layer of depth to the storytelling, allowing the viewer to experience the world from different perspectives and gain a deeper understanding of the characters' emotions and motivations.\\n\\nLighting plays a significant role in the series, with the use of dramatic, contrasting light sources adding to the intensity and emotion of various scenes. Cheng先辈 expertly utilizes lighting to enhance the mood and setting, drawing the audience into the world of \\\"Hacker and Painter\\\" and making them feel like they are part of the action.\\n\\nOverall, \\\"Hacker and Painter\\\" is a visually stunning and thought-provoking series that combines the best of science fiction, action, and art, making it a must-read for fans of all ages.\"\n }\n]"
]
},
{
"id": 34,
"type": "SessionHistory",
"pos": [
1300,
230
],
"size": [
562.8610810546875,
125.32558752441412
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "session_history",
"type": "STRING",
"link": 34,
"widget": {
"name": "session_history"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
35
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SessionHistory"
},
"widgets_values": [
"\"Hacker and Painter\" is a popular Chinese comic series written and illustrated by Cheng先辈, which has gained a large following worldwide.\n\nThe series revolves around the lives of two main characters, a young hacker and an artist, who embark on a journey of self-discovery and personal growth. Set in a futuristic, dystopian world, the story combines elements of science fiction, technology, and action with a touch of humor and satire.\n\nThe art style in \"Hacker and Painter\" is a mix of sci-fi and traditional Chinese ink painting, with bold lines, vibrant colors, and intricate details. Cheng先辈 employs a unique painting technique, using watercolor washes and ink to create a dynamic, expressive, and visually striking visual style.\n\nThe artist's use of photography and various camera angles adds an extra layer of depth to the storytelling, allowing the viewer to experience the world from different perspectives and gain a deeper understanding of the characters' emotions and motivations.\n\nLighting plays a significant role in the series, with the use of dramatic, contrasting light sources adding to the intensity and emotion of various scenes. Cheng先辈 expertly utilizes lighting to enhance the mood and setting, drawing the audience into the world of \"Hacker and Painter\" and making them feel like they are part of the action.\n\nOverall, \"Hacker and Painter\" is a visually stunning and thought-provoking series that combines the best of science fiction, action, and art, making it a must-read for fans of all ages."
]
},
{
"id": 32,
"type": "PrimitiveNode",
"pos": [
1314,
1075
],
"size": {
"0": 571.9803466796875,
"1": 100.6700210571289
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
32
],
"widget": {
"name": "prompt"
}
}
],
"title": "prompt",
"properties": {},
"widgets_values": [
"黑客与画家"
]
},
{
"id": 10,
"type": "ChatGPT",
"pos": [
1300,
720
],
"size": {
"0": 586.7649536132812,
"1": 301.5857849121094
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 32,
"widget": {
"name": "prompt"
},
"slot_index": 0
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
34
],
"shape": 3,
"slot_index": 0
},
{
"name": "JSON",
"type": "STRING",
"links": [
33
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ChatGPT"
}
}
],
"links": [
[
2,
4,
0,
5,
0,
"STRING"
],
[
3,
6,
0,
4,
0,
"STRING"
],
[
4,
6,
1,
7,
0,
"JSON"
],
[
8,
10,
0,
11,
0,
"STRING"
],
[
12,
10,
1,
14,
0,
"STRING"
],
[
13,
10,
1,
16,
0,
"JSON"
],
[
20,
25,
0,
24,
0,
"MODEL"
],
[
21,
27,
0,
24,
1,
"CONDITIONING"
],
[
22,
28,
0,
24,
2,
"CONDITIONING"
],
[
23,
26,
0,
24,
3,
"LATENT"
],
[
24,
25,
1,
27,
0,
"CLIP"
],
[
25,
25,
1,
28,
0,
"CLIP"
],
[
26,
24,
0,
29,
0,
"LATENT"
],
[
27,
25,
2,
29,
1,
"VAE"
],
[
31,
29,
0,
31,
0,
"IMAGE"
],
[
32,
32,
0,
10,
0,
"STRING"
],
[
33,
10,
1,
33,
0,
"STRING"
],
[
34,
10,
0,
34,
0,
"STRING"
],
[
35,
34,
0,
27,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}