Compare commits

...
18 Commits
Author SHA1 Message Date
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
14 changed files with 2021 additions and 184 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
__pycache__/
https/
https/
nodes/config.json
workflow/my_workflow.json
+11 -2
View File
@@ -1,6 +1,5 @@
##
In progress.
!!
![screenshare](./assets/screenshare.png)
@@ -61,6 +60,14 @@ pip3 install -r requirements.txt
![TransparentImage](./assets/TransparentImage.png)
> LoadImagesFromLocal
![watch](./assets/load-watch.png)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
> Consistency Decoder
[openai Consistency Decoder]( https://github.com/openai/consistencydecoder)
@@ -86,6 +93,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
+128 -14
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
@@ -166,8 +278,8 @@ NODE_CLASS_MAPPINGS = {
"VAEDecodeConsistencyDecoder":VAEDecode,
"ScreenShare":ScreenShareNode,
"FloatingVideo":FloatingVideo,
"CLIPSeg":CLIPSeg,
"CombineMasks":CombineMasks
"CLIPSeg_":CLIPSeg,
"CombineMasks_":CombineMasks
}
# 一个包含节点友好/可读的标题的字典
@@ -175,12 +287,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RandomPrompt": "Random Prompt #Example Node",
"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"
}
# web ui的节点功能
WEB_DIRECTORY = "./web"
print('--------------')
print('\033[34mMixlab Custom Nodes: \033[92mLoaded\033[0m')
print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print('--------------')
Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

+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
+5 -7
View File
@@ -49,7 +49,7 @@ 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')
@@ -62,9 +62,9 @@ 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)
# print('##########prompt',prompt)
return (im,mask,prompt)
@@ -73,9 +73,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 +87,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
+3
View File
@@ -1,2 +1,5 @@
numpy
pyOpenSSL
watchdog
opencv-python-headless
matplotlib
+39
View File
@@ -0,0 +1,39 @@
import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version='v0.2.4'
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!=version){
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
+538 -122
View File
@@ -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
+39
View File
@@ -0,0 +1,39 @@
import { app } from '../../../scripts/app.js'
const missingNodeGithub = missingNodeTypes => {
return Array.from(
new Set(missingNodeTypes)
,n=>{
const url = `https://github.com/search?q=${n}&type=code`;
return `<li style="color: white;
background: black;
padding: 8px;
font-size: 12px;">${n}<a href="${url}" target="_blank"> 🔗</a></li>`;
})
}
app.showMissingNodesError = function (missingNodeTypes, hasAddedNodes = true) {
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${ missingNodeGithub(missingNodeTypes)
.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;
}
@@ -0,0 +1,859 @@
{
"last_node_id": 23,
"last_link_id": 48,
"nodes": [
{
"id": 11,
"type": "VAEDecode",
"pos": [
1643,
295
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 12,
"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": 5,
"type": "KSampler",
"pos": [
1267,
277
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 40
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 29
},
{
"name": "latent_image",
"type": "LATENT",
"link": 30
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
1088992701378297,
"randomize",
4,
1.6,
"lcm",
"karras",
1
]
},
{
"id": 23,
"type": "Note",
"pos": [
-470,
840
],
"size": {
"0": 430.924072265625,
"1": 253.48239135742188
},
"flags": {},
"order": 0,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"index_variable needs to be set to 0, indicating the selection of the first image.\n\nnewest_files needs to be enabled, with the newest image placed at the top.\n\nwatcher needs to be enabled to automatically monitor file changes in the folder.\n\nNote: After setting the parameters, please make sure the workflow is running.\n\n\n\nindex_variable 需要设置为0 ,表示选取第一张图。\n\nnewest_files 需要开启,最新的图片排在第一位。\n\nwatcher 开启后,自动监听文件夹里的文件变化。\n\n注意:参数设置好后,请确认工作流已运行"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
233,
814
],
"size": [
315,
58
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
}
},
{
"id": 15,
"type": "ControlNetApply",
"pos": [
1285,
656
],
"size": {
"0": 317.4000244140625,
"1": 98
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 39
},
{
"name": "control_net",
"type": "CONTROL_NET",
"link": 38
},
{
"name": "image",
"type": "IMAGE",
"link": 27
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
40
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ControlNetApply"
},
"widgets_values": [
1
]
},
{
"id": 18,
"type": "ScribblePreprocessor",
"pos": [
914,
653
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 47
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
27
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ScribblePreprocessor"
},
"widgets_values": [
512
]
},
{
"id": 16,
"type": "ControlNetLoader",
"pos": [
922,
765
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"links": [
38
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ControlNetLoader"
},
"widgets_values": [
"control_scribble-fp16.safetensors"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
711,
173
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 5,
"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": [
241,
-74
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 6,
"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": 10,
"type": "CheckpointLoaderSimple",
"pos": [
93,
163
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"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
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [
-9,
812
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 46
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 9,
"type": "EmptyLatentImage",
"pos": [
104,
329
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
30
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
678,
-77
],
"size": [
400,
200
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 48,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
39
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"superman,"
]
},
{
"id": 22,
"type": "LoadImagesFromPath",
"pos": [
-462,
582
],
"size": {
"0": 315,
"1": 238
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
46,
47
],
"shape": 6,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 6
},
{
"name": "STRING",
"type": "STRING",
"links": [
48
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "LoadImagesFromPath"
}
}
],
"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"
],
[
27,
18,
0,
15,
2,
"IMAGE"
],
[
29,
7,
0,
5,
2,
"CONDITIONING"
],
[
30,
9,
0,
5,
3,
"LATENT"
],
[
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"
],
[
38,
16,
0,
15,
1,
"CONTROL_NET"
],
[
39,
8,
0,
15,
0,
"CONDITIONING"
],
[
40,
15,
0,
5,
1,
"CONDITIONING"
],
[
41,
11,
0,
20,
0,
"IMAGE"
],
[
44,
6,
0,
5,
0,
"MODEL"
],
[
45,
10,
1,
6,
1,
"CLIP"
],
[
46,
22,
0,
2,
0,
"IMAGE"
],
[
47,
22,
0,
18,
0,
"IMAGE"
],
[
48,
22,
2,
8,
1,
"STRING"
]
],
"groups": [
{
"title": "Group",
"bounding": [
-651,
510,
1315,
798
],
"color": "#3f789e",
"font_size": 24
}
],
"config": {},
"extra": {},
"version": 0.4
}