Compare commits

..
83 Commits
Author SHA1 Message Date
shadowcz007 0befe164cc v0.9.0 2024-01-01 16:12:58 +08:00
shadowcz007 d506c68a80 promptslide-appinfo-workflow.svg 2024-01-01 16:10:46 +08:00
shadowcz007 c59c429b75 update 2024-01-01 16:05:19 +08:00
shadowcz007 c726b6e4a2 prompt weight 提供选项 2024-01-01 15:56:07 +08:00
shadowcz007 96075ad4e1 Update ImageNode.py 2024-01-01 14:17:58 +08:00
shadowcz007 7a8dc07a8a Update ui_mixlab.js 2024-01-01 11:46:21 +08:00
shadowcz007 1fdac0bc09 Update index.html 2024-01-01 11:32:06 +08:00
shadowcz007 804b942a36 Update index.html 2024-01-01 11:16:56 +08:00
shadowcz007 6335d4378b 优化 2024-01-01 10:55:09 +08:00
shadowcz007 cfc2189616 v0.8.1 2023-12-31 23:58:16 +08:00
shadowcz007 e06e032701 fixbug 2023-12-31 23:56:30 +08:00
shadowcz007 c9499c2c79 update 2023-12-31 23:43:17 +08:00
shadowcz007 2bf43541bb Update appinfo-workflow.svg 2023-12-31 23:42:23 +08:00
shadowcz007 0386c7266d Update app_mixlab.js 2023-12-31 23:41:12 +08:00
shadowcz007 7aa6ed9a5d fixbug 2023-12-31 23:36:57 +08:00
shadowcz007 b71325afa5 fixbug 2023-12-31 22:50:40 +08:00
shadowcz007 2cc29bdf77 适配了最新版comfyui的py3.11 ,torch 2.1.2+cu121 2023-12-31 22:48:11 +08:00
shadowcz007 b5d602abc4 Update index.html 2023-12-31 22:01:26 +08:00
shadowcz007 33c45637ac app 2023-12-31 21:39:50 +08:00
shadowcz007 662d4478d0 Update ui_mixlab.js 2023-12-31 21:31:02 +08:00
shadowcz007 31d3809572 Update ui_mixlab.js 2023-12-31 21:28:45 +08:00
shadowcz007 024ff4a309 Update Lama.py 2023-12-31 21:24:40 +08:00
shadowcz007 9ae8d30b6b llma 2023-12-31 21:23:26 +08:00
shadowcz007 44349c10b0 Update index.html 2023-12-31 20:09:34 +08:00
shadowcz007 506520a3c4 Create Prompt-weight-workflow.json 2023-12-31 17:58:06 +08:00
shadowcz007 50f7020977 prompt-weight 2023-12-31 17:50:47 +08:00
shadowcz007 02a27a03cc Update PromptNode.py 2023-12-31 16:22:57 +08:00
shadowcz007 4ad6bacf7b 优化 2023-12-31 16:20:36 +08:00
shadowcz007 26ecc0fa44 新增 PromptSlide节点,实现滑块调节prompt的权重 2023-12-31 16:04:30 +08:00
shadowcz007 2619befca6 Update README.md 2023-12-31 13:14:29 +08:00
shadowcz007 ea24f52b13 Update checkVersion_mixlab.js 2023-12-31 13:12:34 +08:00
shadowcz007 4abfc47346 ### Update 0.8.0
v0.8.0 🚀🚗🚚🏃‍ LaMaInpainting
- 新增 LaMaInpainting
- 优化color节点的输出
- 修复高清显示屏上定位节点不准的情况

- Add LaMaInpainting
- Optimize the output of the color node
- Fix the issue of inaccurate positioning node on high-definition display screens
2023-12-31 13:11:31 +08:00
shadowcz007 3b95010d06 新增LaMaInpainting & 优化color节点的输出 2023-12-31 13:04:28 +08:00
shadow b3c1b96088 Merge pull request #97 from shadowcz007/fix_hidpi_node_move_center
Fix:node can't move to center on HiDPI device
2023-12-31 10:07:07 +08:00
shadowcz007 a9f1326873 update 2023-12-30 23:52:50 +08:00
shadowcz007 75a696fb64 update 2023-12-30 23:39:36 +08:00
shadowcz007 24aaacba6d update 2023-12-30 23:38:52 +08:00
shadowcz007 e3cd7d5f91 更新示例 2023-12-30 21:05:07 +08:00
shadow 2e228e8db5 Merge pull request #95 from shadowcz007/v0.7-apps
V0.7 apps
2023-12-30 20:53:19 +08:00
shadowcz007 5c9dd80370 fixbug 2023-12-30 20:51:31 +08:00
shadowcz007 727f5f2e48 upate 2023-12-30 20:37:09 +08:00
shadowcz007 1c238f7697 sharebutton 2023-12-30 20:16:51 +08:00
shadowcz007 119d7cce15 0.7.0 2023-12-30 18:19:18 +08:00
shadowcz007 4afc8f6083 Support multiple web app switching. 支持多个web app 切换 2023-12-30 18:16:06 +08:00
shadowcz007 a9ec3af066 改进input range 2023-12-30 18:07:43 +08:00
shadowcz007 9edae81fee update 2023-12-30 17:48:59 +08:00
shadowcz007 b941b12f12 update 2023-12-30 17:40:28 +08:00
shadowcz007 3069de188a 1 2023-12-30 17:02:44 +08:00
shadowcz007 a968f08abd update 2023-12-30 14:16:44 +08:00
shadowcz007 4153d3e5ff 1 2023-12-30 12:37:03 +08:00
shadowcz007 9d9c1a6c84 update 2023-12-30 12:29:01 +08:00
shadowcz007 16ef10a4d9 优化node map 2023-12-30 10:10:21 +08:00
shadowcz007 b3766e440a VHS_VideoCombine 2023-12-30 09:56:08 +08:00
gold3bear 6359c3f70f Fix:node can't move to center on HiDPI device 2023-12-30 02:09:45 +08:00
shadowcz007 efe73fb965 Update README.md 2023-12-29 10:39:01 +08:00
shadowcz007 c45a962fcc workflow-to-app支持checkpoints和lora 2023-12-29 10:38:22 +08:00
shadowcz007 f98a03e2e9 Update README.md 2023-12-29 00:00:16 +08:00
shadowcz007 5b6257814d 优化 2023-12-28 23:56:48 +08:00
shadowcz007 69a445d4ed 新增切换节点 2023-12-28 23:18:25 +08:00
shadowcz007 e82c786b8a 增加了从剪切板获取图片的控件 2023-12-28 18:36:34 +08:00
shadowcz007 eec2225c89 支持视频 2023-12-28 16:02:25 +08:00
shadowcz007 f7355e0b71 update 2023-12-28 14:33:59 +08:00
shadowcz007 6c6a99cfe4 优化LoadImagefromlocal ,新增LoadImageFromURL 2023-12-28 13:24:05 +08:00
shadowcz007 b4634e2e0d 修复clipseg的bug 2023-12-28 12:09:36 +08:00
shadowcz007 3f4cba0612 fixbug:textimage的高宽不对 2023-12-27 21:45:35 +08:00
shadowcz007 38db99cc75 支持showtext作为输出。GPT聊天也可以实现workflow-to-app了 2023-12-27 20:39:21 +08:00
shadowcz007 4d5906394b 优化newlayer的可视化效果 2023-12-27 20:12:55 +08:00
shadowcz007 2fc212b156 update 2023-12-27 19:38:39 +08:00
shadowcz007 53fbb5b027 fixbug 2023-12-27 17:48:49 +08:00
shadowcz007 4f24721450 Update README.md 2023-12-27 16:48:29 +08:00
shadow 83043727b5 Merge pull request #83 from shadowcz007/v0.6---simple-app
V0.6   simple app
2023-12-27 16:29:27 +08:00
shadowcz007 2d336afb85 v0.6.0 2023-12-27 16:29:00 +08:00
shadowcz007 4d309435c8 Update index.html 2023-12-26 17:15:33 +08:00
shadowcz007 099ce9cdfd 1 2023-12-26 16:32:01 +08:00
shadowcz007 8914e60cb8 初步打通 2023-12-26 16:23:43 +08:00
shadowcz007 dbd30a40e9 init 2023-12-26 12:06:55 +08:00
shadowcz007 c9a598fd59 更新下workflow示例 2023-12-26 10:56:14 +08:00
shadowcz007 e331e588cf v0.5.2
The bug of missing texture mapping for 3D nodes has been fixed.
2023-12-25 22:28:53 +08:00
shadowcz007 2011557771 fixbug 2023-12-25 22:24:56 +08:00
shadowcz007 f0ba45d14e GLB can export 2023-12-25 09:14:39 +08:00
shadowcz007 8352a521b7 v0.5.1 2023-12-24 23:07:01 +08:00
shadowcz007 aa3d4d79f8 fixbug 2023-12-24 23:04:16 +08:00
shadowcz007 4f650d760c fixbug-mergeLayer的多图片支持 2023-12-24 22:57:43 +08:00
42 changed files with 12799 additions and 983 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
__pycache__/
https/
nodes/config.json
workflow/my_workflow.json
workflow/my_workflow.json
workflow/my_workflow_app.json
app/*
+79 -33
View File
@@ -1,30 +1,39 @@
##
v0.5.0 🚀🚗🚚🏃‍
- Added video composition support to the MergeLayers.
- Enhanced visual selection support for the NewLayer node.
- Introduced the NoiseImage node and ResizeImage node.
- Improved compatibility for TextImage with line breaks.
- Optimized the 3DImage node to export textures for modification.
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
> 适配了最新版comfyui的py3.11 ,torch 2.1.2+cu121
## 🚀🚗🚚🏃 Workflow-to-APP
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- 支持多个web app 切换
- 发布为app的workflow,可以在右键里再次编辑了
- Support multiple web app switching.
- Add the AppInfo node, which allows you to transform the workflow into a web app by simple configuration.
- The workflow, which is now released as an app, can also be edited again by right-clicking.
- 为MergeLayers添加了视频合成功能。
- NewLayer节点增加了视觉选择支持。
- 添加了NoiseImage节点和ResizeImage节点。
- 支持带有换行的文本图像。
- 对3D节点进行了优化,支持导出纹理以进行修改。
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
![](./assets/0-m-app.png)
![](./assets/appinfo-readme.png)
![](./assets/appinfo-2.png)
Example:
- workflow
![APP info](./workflow/appinfo-workflow.svg)
[text-to-image](./workflow/Text-to-Image-app.json)
APP-JSON:
- [text-to-image](./example/Text-to-Image_3.json)
- [image-to-image](./example/Image-to-Image_2.json)
- text-to-text
> 暂时支持6种节点作为界面上的输入节点:Load Image、CLIPTextEncode、TextInput_、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
## 🏃🚗🚚🚀 Real-time Design
> ScreenShareNode & FloatingVideoNode. Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
### ScreenShareNode & FloatingVideoNode
> Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
>
![screenshare](./assets/screenshare.png)
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
@@ -35,6 +44,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
!! Please use the address with HTTPS (https://127.0.0.1).
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
@@ -43,17 +53,20 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
### GPT
> Support for calling multiple GPTs.ChatGPT、ChatGLM3 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
![gpt-workflow.svg](./assets/gpt-workflow.svg)
[workflow-5](./workflow/5-gpt-workflow.json)
### 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.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
## Prompt
> PromptSlide
![](./assets/prompt_weight.png)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
![](./workflow/promptslide-appinfo-workflow.svg)
> randomPrompt
![randomPrompt](./assets/randomPrompt.png)
### Layers
@@ -63,9 +76,29 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
![poster](./assets/poster-workflow.svg)
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### 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.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### LoadImagesFromURL
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
## Utils
> The Color node provides a color picker for easy color selection, the Font node offers built-in font selection for use with TextImage to generate text images, and the DynamicDelayByText node allows delayed execution based on the length of the input text.
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
## Other Nodes
@@ -75,9 +108,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow-1](./workflow/1-workflow.json)
> randomPrompt
![randomPrompt](./assets/randomPrompt.png)
> TransparentImage
@@ -100,11 +131,15 @@ Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
> LaMaInpainting
from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainting)
### Improvement
- Add "help" option to the context menu for each node.
- Add "find the node" option to the global context menu.
- Add "Nodes Map" option to the global context menu.
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
@@ -113,14 +148,26 @@ An improvement has been made to directly redirect to GitHub to search for missin
![node-not-found](./assets/node-not-found.png)
### Update
v0.8.0 🚀🚗🚚🏃‍ LaMaInpainting
- 新增 LaMaInpainting
- 优化color节点的输出
- 修复高清显示屏上定位节点不准的情况
- Add LaMaInpainting
- Optimize the output of the color node
- Fix the issue of inaccurate positioning node on high-definition display screens
### Models
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : model/clipseg
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : models/clipseg
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : models/lama
<!-- ### Workflow
[Workflow](./workflow.md) -->
## Installation
manually install, simply clone the repo into the custom_nodes directory with this command:
@@ -154,7 +201,6 @@ pip3 install -r requirements.txt
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
#### Thanks:
[ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
+143 -9
View File
@@ -4,7 +4,7 @@ import subprocess
import importlib.util
import sys,json
import urllib
import hashlib
import datetime
@@ -79,6 +79,13 @@ install_openai()
current_path = os.path.abspath(os.path.dirname(__file__))
def calculate_md5(string):
encoded_string = string.encode()
md5_hash = hashlib.md5(encoded_string).hexdigest()
return md5_hash
def create_key(key_p,crt_p):
import OpenSSL
# 生成自签名证书
@@ -162,12 +169,98 @@ def get_workflows():
workflows=read_workflow_json_files(workflow_path)
return workflows
def get_my_workflow_for_app(filename="my_workflow_app.json"):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
apps=[]
if filename==None:
data=read_workflow_json_files(app_path)
i=0
for item in data:
try:
x=item["data"]
if i==0:
apps.append({
"filename":item["filename"],
"data":x,
"date":item["date"]
})
else:
apps.append({
"filename":item["filename"],
"data":{
"app":{
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
}
},
"date":item["date"]
})
i+=1
except Exception as e:
print("发生异常:", str(e))
else:
app_workflow_path=os.path.join(app_path, filename)
# print('app_workflow_path: ',app_workflow_path)
try:
with open(app_workflow_path) as json_file:
apps = [{
'filename':filename,
'data':json.load(json_file)
}]
except Exception as e:
print("发生异常:", str(e))
if len(apps)==1:
data=read_workflow_json_files(app_path)
for item in data:
x=item["data"]
# print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
apps.append({
"filename":item["filename"],
"data":{
"app":{
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
"name":x['app']['name'],
"version":x['app']['version'],
}
},
"date":item["date"]
})
return apps
def save_workflow_json(data):
workflow_path=os.path.join(current_path, "workflow/my_workflow.json")
with open(workflow_path, 'w') as file:
json.dump(data, file)
return workflow_path
def save_workflow_for_app(data,filename="my_workflow_app.json"):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
app_workflow_path=os.path.join(app_path, filename)
try:
output_str = json.dumps(data['output'])
data['app']['id']=calculate_md5(output_str)
# id=data['app']['id']
except Exception as e:
print("发生异常:", str(e))
with open(app_workflow_path, 'w') as file:
json.dump(data, file)
return filename
def get_nodes_map():
# print("#####path::", current_path)
@@ -253,6 +346,18 @@ async def mixlab_hander(request):
print(e)
return web.json_response(data)
@routes.get('/mixlab/app')
async def mixlab_app_handler(request):
html_file = os.path.join(current_path, "web/index.html")
if os.path.exists(html_file):
with open(html_file, 'r', encoding='utf-8', errors='ignore') as f:
html_data = f.read()
return web.Response(text=html_data, content_type='text/html')
else:
return web.Response(text="HTML file not found", status=404)
@routes.post('/mixlab/workflow')
async def mixlab_workflow_hander(request):
data = await request.json()
@@ -265,6 +370,20 @@ async def mixlab_workflow_hander(request):
'status':'success',
'file_path':file_path
}
elif data['task']=='save_app':
file_path=save_workflow_for_app(data['data'],data['filename'])
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='my_app':
filename=None
if 'filename' in data:
filename=data['filename']
result={
'data':get_my_workflow_for_app(filename),
'status':'success',
}
elif data['task']=='list':
result={
'data':get_workflows(),
@@ -289,6 +408,7 @@ async def nodes_map_hander(request):
return web.json_response(result)
# 把插件自定义的路由添加到comfyui server里
def new_add_routes(self):
import nodes
self.app.add_routes(routes)
@@ -317,24 +437,27 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.PromptNode import RandomPrompt,PromptSlide
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
from .nodes.Utils import AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,GetImageSize_,MultiplicationNode
from .nodes.Lama import LaMaInpainting
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
NODE_CLASS_MAPPINGS = {
"AppInfo":AppInfo,
"RandomPrompt":RandomPrompt,
"PromptSlide":PromptSlide,
"NoiseImage":NoiseImage,
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
"LoadImagesFromURL":LoadImagesFromURL,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
@@ -360,15 +483,24 @@ NODE_CLASS_MAPPINGS = {
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"FloatSlider":FloatSlider,
"IntNumber":IntNumber,
"TextInput_":TextInput,
"Font":FontInput,
"TextToNumber":TextToNumber,
"DynamicDelayProcessor":DynamicDelayProcessor
"DynamicDelayProcessor":DynamicDelayProcessor,
"MultiplicationNode":MultiplicationNode,
"GetImageSize_":GetImageSize_,
"SwitchByIndex":SwitchByIndex,
"LimitNumber":LimitNumber,
"LaMaInpainting":LaMaInpainting
# "GamePal":GamePal
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"ResizeImageMixlab":"ResizeImage",
"AppInfo":"AppInfo ♾️Mixlab",
"ResizeImageMixlab":"ResizeImage ♾️Mixlab",
"RandomPrompt": "Random Prompt ♾️Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
@@ -381,7 +513,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab"
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
"LaMaInpainting":"LaMaInpainting ♾️Mixlab",
"PromptSlide":"PromptSlide ♾️Mixlab"
# "GamePal":"GamePal ♾️Mixlab"
}
@@ -390,5 +524,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
WEB_DIRECTORY = "./web"
print('--------------')
print('\033[91mMixlab Nodes: \033[93mLoaded\033[0m')
print('\033[91m ### Mixlab Nodes: \033[93mLoaded\033[0m')
print('--------------')
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 9.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

+10
View File
@@ -0,0 +1,10 @@
Chibi Anime Style
Gakuen Anime Style
Gekiga Anime Style
Jidaimono Anime Style
Kawaii Anime Style
Mecha Anime Style
Realistic Anime Style
Semi-Realistic Anime Style
Shoji Anime Style
Kemonomimi Anime Style
+2052
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
GoPro
Drone
polaroid
black and white film
Kodachrome
shot on 8mm
shot on 16mm
shot on 35mm
Microscopic
Fisheye Lens
Wide Angle
Ultra-Wide Angle
Panorama
Short Exposure
Long Exposure
Double Exposure
f2.8
Depth of Field
Soft Focus
Deep Focus
Shallow Focus
Vanishing Point
Vantage Point
+14 -5
View File
@@ -4762,15 +4762,17 @@
"https://github.com/shadowcz007/comfyui-mixlab-nodes": [
[
"3DImage",
"AppInfo",
"IntNumber",
"FloatSlider",
"ResizeImage",
"NoiseImage",
"AreaToMask",
"CLIPSeg",
"CLIPSeg_",
"CharacterInText",
"ChatGPTOpenAI",
"Color",
"CombineMasks_",
"CombineSegMasks",
"EmptyLayer",
"EnhanceImage",
"FaceToMask",
"FeatheredMask",
@@ -4778,9 +4780,11 @@
"Font",
"ImageCropByAlpha",
"LoadImagesFromPath",
"LoadImagesFromURL",
"MergeLayers",
"NewLayer",
"RandomPrompt",
"PromptSlide",
"ScreenShare",
"ShowLayer",
"ShowTextForGPT",
@@ -4790,12 +4794,17 @@
"SplitLongMask",
"SvgImage",
"TextImage",
"ResizeImageMixlab",
"TransparentImage",
"VAEDecodeConsistencyDecoder",
"VAELoaderConsistencyDecoder"
"VAELoaderConsistencyDecoder",
"TextToNumber",
"TextInput_",
"DynamicDelayProcessor",
"LaMaInpainting"
],
{
"title_aux": "comfyui-mixlab-nodes [WIP]"
"title_aux": "comfyui-mixlab-nodes"
}
],
"https://github.com/shiimizu/ComfyUI_smZNodes": [
+16
View File
@@ -0,0 +1,16 @@
Mood Lighting
Moody Lighting
Studio Lighting
Cove Lighting
Soft Lighting
Hard Lighting
Volumetric Lighting
Low-Key Lighting
High-Key Lighting
Epic Light
Rembrandt Lighting
Contre-Jour
Veiling Flare
Crepuscular Rays
Rays of Shimmering Light
Godrays
+132
View File
@@ -0,0 +1,132 @@
Aaron Siskind
Alessio Albi
Alfred Eisenstaedt
Alfred Stieglitz
Alyssa Monks
André Kertész
Andreas Gursky
Andrew Wyeth
Anne Geddes
Annie Leibovitz
Ansel Adams
Arnold Newman
August Sander
Balthus
Berenice Abbott
Bill Brandt
Bill Henson
Brassaï (Gyula Halász)
Brooke Shaden
Bruce Davidson
Bruce Weber
Bunny Yeager
Carleton Watkins
Carrie Mae Weems
Chuck Close
Cindy Sherman
Clarence H. White
Claude Cahun
Danny Lyon
David LaChapelle
Dawoud Bey
Diane Arbus
Don McCullin
Dora Maar
Dorothea Lange
Duane Michals
Eadweard Muybridge
Edward Burtynsky
Edward Curtis
Edward Ruscha
Edward Steichen
Edward Weston
Elliott Erwitt
Ernst Haas
Eugene Atget
Fan Ho
Francesca Woodman
Frans Lanting
Garry Winogrand
Georges Melies
Gerda Taro
Gertrude Käsebier
Gordon Parks
Graciela Iturbide
Gregory Crewdson
Harold Edgerton
Helen Levitt
Helmut Newton
Hendrik Kerstens
Henri Cartier-Bresson
Hugh Kretschmer
Irving Penn
Jacques Henri Lartigue
James Nachtwey
James Van Der Zee
Jay Maisel
Jerry Uelsmann
Joel Peter Witkin
Joel Sartore
John Frederick William Herschel
Josef Sudek
Julia Margaret Cameron
Karl Blossfeldt
Larry Burrows
László Moholy-Nagy (photography)
Lee Jeffries
Lewis Hine
Lorna Simpson
Lynsey Addario
Margaret Bourke-White
Mario Testino
Martin Parr
Martin Schoeller
Mary Ellen Mark
Mathew B. Brady
Méret Oppenheim
Meryl McMaster
Mick Rock
Miles Aldridge
Minor Martin White
Nan Goldin
Nathan Wirth
Olive Cotton
Olivier Rousteing
Patrick Demarchelier
Paul Nicklen
Paul Outerbridge
Paul Strand
Pete Souza
Peter Dombrovskis
Peter Henry Emerson
Peter Lik
Peter Lindbergh
Philip-Lorca diCorcia
Philippe Halsman
Ralph Gibson
Richard Avedon
Robert Adams
Robert Bechtle
Robert Capa
Robert Frank
Robert Mapplethorpe
Roger Fenton
Ruth Bernhard
Sally Mann
Sebastião Salgado
Shirin Neshat
Stefan Gesell
Steven Meisel
Susan Meiselas
Vivian Maier
Vivian Maier
Viviane Sassen
Walker Evans
Wes Anderson
William Eggleston
William Eugene Smith
William Henry Fox Talbot
Yinka Shonibare
Yousuf Karsh
Man Ray
Robert Mapplethorpe
+135
View File
@@ -0,0 +1,135 @@
Vintage
Grain
Sepia
High Key
Low Key
High Dynamic Range
Cross Process
Radial Blur
Infrared
Lomo
Photocopy
Pencil Sketch
Pop Art
Orton
Mosaic
Selective Black and White
Torn Paper
Tilt-Shift
Double Exposure
Polaroid
Liquid Ink
Color Splash
Sketch
Water Drops
Polarizer
Chinese Painting
Water Droplets
Polarization
Color Inversion
Fish-eye
Soft Focus
Solarization
Posterize
Comic Book
Duotone
Gradient Map
Edge Detection
Oil Painting
Reflection
Mirror
ASCII Art
Glitch
Time-Lapse
Day to Night
Surreal
Black and White
Sepia Tone
Vintage Film
Grainy Texture
High Key Lighting
Low Key Lighting
Cross Processed Film
Infrared Photography
Photocopy
Pencil Drawing
Pop Art Filter
Mosaic Filter
Selective Desaturation
Torn Paper
Tilt-Shift Photography
Double Exposure
Polaroid Style Frame
Water Drops Texture
Polarizer
Chinese Painting
Water Droplets Texture
Polarization
Color Inversion
Fish-eye Lens
Soft Focus
Solarize Filter
Edge Detection
Oil Painting
Reflection
Mirror Image
Time-Lapse Photography
Day to Night Transition
Surreal Art Style
Abstract Expressionism
Acrylic Painting
Anime
Art Deco
Biomorphic Abstraction
Black and White Photograph
Cartoon
Charcoal Sketch
Chibi Anime
Chinese Painting
Classicist Painting
Collage
Concept Art
Cyberpunk
Dada Art
Digital Art
Fantasy Art
Fashion Art
Fashion Sketch
Fish-Eye lens Photograph
Goth Art
Graffiti
Harlem Renaissance
High Key Photograph
Hyperrealist Pencil Sketch
Impressionist Painting
Josei Anime
Long Exposure Photograph
Low Key Photograph
Macro Photograph
Manga
Metal Sculpture
Mid Century Modern Illustration
Mixed Media
Modern Art
Moe Anime
Nihonga
Origami
Paper Mache
Pen and Ink
Pencil Sketch
Photograph
Photorealism
Pinup Art
Romanticist Painting
Sci-Fi Art
Semi Realistic Fantasy Art
Semi Realistic Cyberpunk Art
Shallow Depth of Field Photograph
Steam Punk Art
Stone Sculpture
Superhero Comic
Surrealist Art
Tempura Painting
Underground Comic
Watercolor Painting
Zulu Urban Art
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -75,15 +75,15 @@ class ChatGPTNode:
"required": {
"api_key":("KEY", {"default": "", "multiline": True}),
"api_url":("URL", {"default": "", "multiline": True}),
"prompt": ("STRING", {"multiline": True}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True
"multiline": True,"dynamicPrompts": False
}),
"model": (["gpt-3.5-turbo","gpt-35-turbo","gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-4-0613","gpt-4-1106-preview"],
{"default": "gpt-3.5-turbo"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
@@ -167,7 +167,7 @@ class ShowTextForGPT:
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
"text": ("STRING", {"forceInput": True,"dynamicPrompts": False}),
}
}
@@ -189,8 +189,8 @@ class CharacterInText:
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True}),
"character": ("STRING", {"multiline": True}),
"text": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"character": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"start_index": ("INT", {
"default": 1,
"min": 0, #Minimum value
+19 -8
View File
@@ -31,10 +31,20 @@ logger = logging.getLogger('CLIPSeg nodes')
clipseg_model_dir = os.path.join(folder_paths.models_dir, "clipseg")
if not os.path.exists(clipseg_model_dir):
print(f"## clipseg model not found: {clipseg_model_dir},pls download from https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main")
clipseg_model_dir='CIDAS/clipseg-rd64-refined'
"""Helper methods for CLIPSeg nodes"""
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def tensor_to_numpy(tensor: torch.Tensor) -> np.ndarray:
"""Convert a tensor to a numpy array and scale its values to 0-255."""
array = tensor.numpy().squeeze()
@@ -91,7 +101,7 @@ class CLIPSeg:
return {"required":
{
"image": ("IMAGE",),
"text": ("STRING", {"multiline": False}),
"text": ("STRING", {"multiline": False,"dynamicPrompts": False}),
},
"optional":
@@ -107,7 +117,7 @@ class CLIPSeg:
RETURN_NAMES = ("Mask","Heatmap Mask", "BW Mask")
# INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
OUTPUT_IS_LIST = (False,False,False,)
FUNCTION = "segment_image"
def segment_image(self, image: torch.Tensor, text: str, blur: float, threshold: float, dilation_factor: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -180,12 +190,13 @@ class CLIPSeg:
binary_mask_image = Image.fromarray(binary_mask_resized[..., 0])
# convert PIL image to numpy array
tensor_bw = binary_mask_image.convert("RGB")
tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
tensor_bw = torch.from_numpy(tensor_bw)[None,]
tensor_bw = tensor_bw.squeeze(0)[..., 0]
tensor_bw = binary_mask_image.convert("L")
tensor_bw=pil2tensor(tensor_bw)
# tensor_bw = np.array(tensor_bw).astype(np.float32) / 255.0
# tensor_bw = torch.from_numpy(tensor_bw)[None,]
# tensor_bw = tensor_bw.squeeze(0)[..., 0]
return tensor_bw, image_out_heatmap, image_out_binary
return (tensor_bw, image_out_heatmap, image_out_binary,)
#OUTPUT_NODE = False
@@ -235,7 +246,7 @@ class CombineMasks:
# Resize heatmap and binary mask to match the original image dimensions
dimensions = (image_np.shape[1], image_np.shape[0])
print('heatmap',heatmap)
# print('heatmap',heatmap)
if dimensions is None or dimensions[0] == 0 or dimensions[1] == 0:
raise ValueError("Invalid dimensions")
+236 -72
View File
@@ -1,4 +1,5 @@
import numpy as np
import requests
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
@@ -197,6 +198,24 @@ def load_image(fp,white_bg=False):
return images
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
content_type = response.headers.get('Content-Type')
image = Image.open(BytesIO(response.content))
# Create a mask from the image's alpha channel
mask = image.convert('RGBA').split()[-1]
# Convert the mask to a black and white image
mask = mask.convert('L')
image=image.convert('RGB')
return (image, mask)
# 获取图片s
def get_images_filepath(f,white_bg=False):
@@ -235,10 +254,59 @@ def get_images_filepath(f,white_bg=False):
return images
def get_average_color_image(image):
# 打开图片
# image = Image.open(image_path)
# 将图片转换为RGB模式
image = image.convert("RGB")
# 获取图片的像素值
pixel_data = image.load()
# 初始化颜色总和和像素数量
total_red = 0
total_green = 0
total_blue = 0
pixel_count = 0
# 遍历图片的每个像素
for i in range(image.width):
for j in range(image.height):
# 获取像素的RGB值
r, g, b = pixel_data[i, j]
# 累加颜色值
total_red += r
total_green += g
total_blue += b
# 像素数量加1
pixel_count += 1
# 计算平均颜色值
average_red = int(total_red // pixel_count)
average_green = int(total_green // pixel_count)
average_blue = int(total_blue // pixel_count)
# 返回平均颜色值
im = Image.new("RGB", (image.width, image.height), (average_red, average_green, average_blue))
return im
# 创建噪声图像
def create_noisy_image(width, height, mode="RGB", noise_level=128):
def create_noisy_image(width, height, mode="RGB", noise_level=128, background_color="#FFFFFF"):
background_rgb = tuple(int(background_color[i:i+2], 16) for i in (1, 3, 5))
image = Image.new(mode, (width, height), background_rgb)
# 创建空白图像
image = Image.new(mode, (width, height))
# image = Image.new(mode, (width, height))
# 遍历每个像素,并随机设置像素值
pixels = image.load()
@@ -487,6 +555,7 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
y = 0
for line in lines:
for char in line:
#print('char',char)
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
@@ -495,10 +564,10 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = ((len(max(lines, key=len))+1) * (font_size + spacing)) + spacing
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = (len(lines) * (font_size + spacing)) - spacing
height = ((len(lines)-1) * (font_size + spacing)) + font_size
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
@@ -850,7 +919,7 @@ class LoadImagesFromPath:
def INPUT_TYPES(s):
return {
"required": {
"file_path": ("STRING",{"multiline": False,"default": ""}),
"file_path": ("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
},
"optional":{
"white_bg": (["disable","enable"],),
@@ -869,7 +938,7 @@ class LoadImagesFromPath:
}
}
RETURN_TYPES = ('IMAGE','MASK','STRING')
RETURN_TYPES = ('IMAGE','MASK','STRING',)
FUNCTION = "run"
@@ -902,6 +971,11 @@ class LoadImagesFromPath:
images=get_images_filepath(file_path,white_bg=='enable')
# 当开启了监听,则取最新的,第一个文件
if watcher=='enable':
index_variable=0
newest_files='enable'
# 排序
sorted_files = sorted(images, key=lambda x: os.path.getmtime(x['file_path']), reverse=(newest_files=='enable'))
@@ -913,9 +987,13 @@ class LoadImagesFromPath:
masks.append(im['mask'])
# print('index_variable',index_variable)
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
try:
if index_variable!=-1:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
except Exception as e:
print("发生了一个未知的错误:", str(e))
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
@@ -962,8 +1040,8 @@ class TextImage:
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲"}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH}),
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH,"dynamicPrompts": False}),
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
@@ -973,17 +1051,17 @@ class TextImage:
}),
"spacing": ("INT",{
"default":12,
"min": 1, #Minimum value
"min": -200, #Minimum value
"max": 200, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"text_color":("STRING",{"multiline": False,"default": "#000000"}),
"text_color":("STRING",{"multiline": False,"default": "#000000","dynamicPrompts": False}),
"vertical":("BOOLEAN", {"default": True},),
},
}
RETURN_TYPES = ("IMAGE","MASK")
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
@@ -1004,6 +1082,62 @@ class TextImage:
return (img,mask,)
class LoadImagesFromURL:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"url": ("STRING",{"multiline": True,"default": "https://","dynamicPrompts": False}),
},
}
RETURN_TYPES = ("IMAGE","MASK",)
RETURN_NAMES = ("images","masks",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,True,)
global urls_image
urls_image={}
def run(self,url):
global urls_image
print(urls_image)
def filter_http_urls(urls):
filtered_urls = []
for url in urls.split('\n'):
if url.startswith('http'):
filtered_urls.append(url)
return filtered_urls
filtered_urls = filter_http_urls(url)
images=[]
masks=[]
for img_url in filtered_urls:
try:
if img_url in urls_image:
img,mask=urls_image[img_url]
else:
img,mask=load_image_and_mask_from_url(img_url)
urls_image[img_url]=(img,mask)
img1=pil2tensor(img)
mask1=pil2tensor(mask)
images.append(img1)
masks.append(mask1)
except Exception as e:
print("发生了一个未知的错误:", str(e))
return (images,masks,)
class SvgImage:
@@ -1060,16 +1194,22 @@ class Image3D:
def run(self,upload,material=None):
# print('material',material)
# print(upload['image'])
# print(upload )
image = base64_to_image(upload['image'])
mat=base64_to_image(upload['material'])
mat=None
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
mask = image.split()[3]
image=image.convert('RGB')
mat=mat.convert('RGB')
mask=mask.convert('L')
bg_image=None
if upload['bg_image']:
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
@@ -1077,8 +1217,7 @@ class Image3D:
mask=pil2tensor(mask)
image=pil2tensor(image)
mat=pil2tensor(mat)
m=[]
if not material is None:
m=create_temp_file(material[0])
@@ -1402,49 +1541,55 @@ class MergeLayers:
bg_images=[]
masks=[]
for bg_image in images[0]:
# bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
# print(len(images),images[0].shape)
# 1 torch.Size([2, 512, 512, 3])
# 4 torch.Size([1, 1024, 768, 3])
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
for img in images:
for bg_image in img:
# bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
bg_images.append(bg_image)
masks.append(mask)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
bg_images.append(bg_image)
masks.append(mask)
bg_images=torch.cat(bg_images, dim=0)
masks=torch.cat(masks, dim=0)
@@ -1477,7 +1622,7 @@ class NoiseImage:
"step": 1,
"display": "slider"
}),
"color_hex": ("STRING",{"multiline": False,"default": "#FFFFFF","dynamicPrompts": False}),
},
}
@@ -1496,9 +1641,9 @@ class NoiseImage:
# 输出是否为列表
OUTPUT_IS_LIST = (False,)
def run(self,width,height,noise_level):
def run(self,width,height,noise_level,color_hex):
# 创建噪声图像
im=create_noisy_image(width,height,"RGB",noise_level)
im=create_noisy_image(width,height,"RGB",noise_level,color_hex)
#获取临时目录:temp
output_dir = folder_paths.get_temp_directory()
@@ -1553,32 +1698,51 @@ class ResizeImage:
"optional":{
"image": ("IMAGE",),
"average_color": (["on",'off'],),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_TYPES = ("IMAGE","IMAGE")
RETURN_NAMES = ("image","average_image",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,)
OUTPUT_IS_LIST = (True,True,)
def run(self,width,height,scale_option,image=None):
def run(self,width,height,scale_option,image=None,average_color=['on']):
w=width[0]
h=height[0]
scale_option=scale_option[0]
average_color=average_color[0]
imgs=[]
average_images=[]
if image==None:
im=create_noisy_image(w,h,"RGB")
else:
im=image[0]
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=im.convert('RGB')
a_im=get_average_color_image(im)
im=pil2tensor(im)
imgs.append(im)
im=pil2tensor(im)
a_im=pil2tensor(a_im)
average_images.append(a_im)
else:
for im in image:
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=im.convert('RGB')
a_im=get_average_color_image(im)
im=pil2tensor(im)
imgs.append(im)
a_im=pil2tensor(a_im)
average_images.append(a_im)
return (im,)
return (imgs,average_images,)
+84
View File
@@ -0,0 +1,84 @@
import os
import folder_paths
from simple_lama_inpainting import SimpleLama
from PIL import Image
import numpy as np
import torch
llma_model_path=os.path.join(folder_paths.models_dir, "lama/big-lama.pt")
if not os.path.exists(llma_model_path):
os.environ['LAMA_MODEL']=''
print(f"## lama torchscript model not found: {llma_model_path},pls download from https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt")
else:
os.environ['LAMA_MODEL'] = llma_model_path
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
# simple_lama = SimpleLama()
# img_path = "image.png"
# mask_path = "mask.png"
# image = Image.open(img_path)
# mask = Image.open(mask_path).convert('L')
# result = simple_lama(image, mask)
# result.save("inpainted.png")
class LaMaInpainting:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
global simple_lama
simple_lama = None
def run(self,image,mask):
global simple_lama
result=[]
if simple_lama==None:
simple_lama = SimpleLama()
else:
simple_lama.model.to("cuda" if torch.cuda.is_available() else "cpu")
for i in range(len(image)):
im=image[i]
ma=mask[i]
im=tensor2pil(im)
ma=tensor2pil(ma)
ma =ma.convert('L')
res = simple_lama(im, ma)
res=pil2tensor(res)
result.append(res)
# result.save("inpainted.png")
if simple_lama.device=='cuda':
simple_lama.model.to('cpu')
return (result,)
+121 -53
View File
@@ -4,12 +4,11 @@ import json
from urllib import request, parse
def queue_prompt(prompt_workflow):
p = {"prompt": prompt_workflow}
data = json.dumps(p).encode('utf-8')
req = request.Request("http://127.0.0.1:8188/prompt", data=data)
request.urlopen(req)
# def queue_prompt(prompt_workflow):
# p = {"prompt": prompt_workflow}
# data = json.dumps(p).encode('utf-8')
# req = request.Request("http://127.0.0.1:8188/prompt", data=data)
# request.urlopen(req)
default_prompt1='''Swing
@@ -45,6 +44,73 @@ default_prompt1='''Swing
'''
default_prompt1="\n".join([p.strip() for p in default_prompt1.split('\n') if p.strip()!=''])
def addWeight(text, weight=1):
if weight == 1:
return text
else:
return f"({text}:{round(weight,2)})"
class PromptSlide:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt_keyword": ("STRING",
{
"multiline": False,
"default": '',
"dynamicPrompts": False
}),
"weight":("FLOAT", {"default": 1, "min": -3,"max": 3,
"step": 0.01,
"display": "slider"}),
# "min_value":("FLOAT", {
# "default": -2,
# "min": -10,
# "max": 0xffffffffffffffff,
# "step": 0.01,
# "display": "number"
# }),
# "max_value":("FLOAT", {
# "default": 2,
# "min": -10,
# "max": 0xffffffffffffffff,
# "step": 0.01,
# "display": "number"
# }),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/prompt"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
OUTPUT_NODE = False
# 运行的函数
def run(self,prompt_keyword,weight):
# if weight < min_value:
# weight= min_value
# elif weight > max_value:
# weight= max_value
p=addWeight(prompt_keyword,weight)
return (p,)
class RandomPrompt:
'''
@@ -87,7 +153,7 @@ class RandomPrompt:
# 运行的函数
def run(self,max_count,mutable_prompt,immutable_prompt,random_sample):
print('#运行的函数',mutable_prompt,immutable_prompt,max_count,random_sample)
# print('#运行的函数',mutable_prompt,immutable_prompt,max_count,random_sample)
# Split the text into an array of words
words1 = mutable_prompt.split("\n")
@@ -106,6 +172,8 @@ class RandomPrompt:
w1=w1.strip()
for w2 in words2:
w2=w2.strip()
if '``' not in w2:
w2=w2+',``'
if w1!='' and w2!='':
prompts.append(w2.replace('``', w1))
pbar.update(1)
@@ -126,62 +194,62 @@ class RandomPrompt:
class RunWorkflow:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"workflow": ("STRING", {
"multiline": False,
"default": ''
}),
"prompt": ("STRING", {
"multiline": False,
"default": ''
}),
"image": ("IMAGE",),
"input_node": ("STRING", {
"multiline": False,
"default": ''
}),
"output_node": ("STRING", {
"multiline": False,
"default": ''
}),
},
# class RunWorkflow:
# @classmethod
# def INPUT_TYPES(s):
# return {
# "required": {
# "workflow": ("STRING", {
# "multiline": False,
# "default": ''
# }),
# "prompt": ("STRING", {
# "multiline": False,
# "default": ''
# }),
# "image": ("IMAGE",),
# "input_node": ("STRING", {
# "multiline": False,
# "default": ''
# }),
# "output_node": ("STRING", {
# "multiline": False,
# "default": ''
# }),
# },
}
# }
RETURN_TYPES = ("IMAGE","STRING",)
# RETURN_TYPES = ("IMAGE","STRING",)
FUNCTION = "run"
# FUNCTION = "run"
CATEGORY = "♾️Mixlab/workflow"
# CATEGORY = "♾️Mixlab/workflow"
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
# OUTPUT_IS_LIST = (True,)
# OUTPUT_NODE = True
# 运行的函数
def run(self,workflow,prompt,image,input_node,output_node):
print('#运行的函数',prompt,image,input_node,output_node)
workflow=json.loads(workflow)
input_node=input_node.split(".")
workflow[input_node[0]][input_node[1]][input_node[2]]=prompt
# # 运行的函数
# def run(self,workflow,prompt,image,input_node,output_node):
# print('#运行的函数',prompt,image,input_node,output_node)
# workflow=json.loads(workflow)
# input_node=input_node.split(".")
# workflow[input_node[0]][input_node[1]][input_node[2]]=prompt
workflow_new={}
# 遍历,seed设为随机
for key, value in workflow.items():
if 'inputs' in value:
if 'seed' in value['inputs']:
value['inputs']['seed']= random.randint(1, 18446744073709551614)
workflow_new[key]=value
# workflow_new={}
# # 遍历,seed设为随机
# for key, value in workflow.items():
# if 'inputs' in value:
# if 'seed' in value['inputs']:
# value['inputs']['seed']= random.randint(1, 18446744073709551614)
# workflow_new[key]=value
queue_prompt(workflow_new)
print('#运行的函数',workflow_new[input_node[0]])
# queue_prompt(workflow_new)
# print('#运行的函数',workflow_new[input_node[0]])
# return (new_prompt)
return {"ui":{"images": []},"result": ([image],['text'],)}
# # return (new_prompt)
# return {"ui":{"images": []},"result": ([image],['text'],)}
+377 -8
View File
@@ -1,9 +1,53 @@
import os
import re,random
from PIL import Image
import numpy as np
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
import folder_paths
import matplotlib.font_manager as fm
# import json
# import hashlib
# def get_json_hash(json_content):
# json_string = json.dumps(json_content, sort_keys=True)
# hash_object = hashlib.sha256(json_string.encode())
# hash_value = hash_object.hexdigest()
# return hash_value
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def create_temp_file(image):
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('tmp', output_dir)
image=tensor2pil(image)
image_file = f"{filename}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
image.save(image_path,compress_level=4)
return [{
"filename": image_file,
"subfolder": subfolder,
"type": "temp"
}]
def get_font_files(directory):
font_files = {}
@@ -44,18 +88,23 @@ class ColorInput:
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
RETURN_TYPES = ("STRING","INT","INT","INT","FLOAT",)
RETURN_NAMES = ("hex","r","g","b","a",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
OUTPUT_IS_LIST = (False,False,False,False,False,)
def run(self,color):
return (color,)
h=color['hex']
r=color['r']
g=color['g']
b=color['b']
a=color['a']
return (h,r,g,b,a,)
@@ -76,10 +125,10 @@ class FontInput:
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
OUTPUT_IS_LIST = (False,)
def run(self,font):
return (font_files[font],)
class TextToNumber:
@@ -120,6 +169,161 @@ class TextToNumber:
result= random.randint(1, 10000000000)
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
class FloatSlider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("FLOAT", {
"default": 0,
"min": 0, #Minimum value
"max": 1, #Maximum value
"step": 0.001, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
"min_value":("FLOAT", {
"default": 0,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step": 0.001,
"display": "number"
}),
"max_value":("FLOAT", {
"default": 1,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step": 0.001,
"display": "number"
}),
"step":("FLOAT", {
"default": 0.001,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step": 0.001,
"display": "number"
}),
},
}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number,min_value,max_value,step):
if number < min_value:
number= min_value
elif number > max_value:
number= max_value
return (number,)
class IntNumber:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("INT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"min_value":("INT", {
"default": 0,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"max_value":("INT", {
"default": 1,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"step":("INT", {
"default": 1,
"min": -0xffffffffffffffff,
"max": 0xffffffffffffffff,
"step":1,
"display": "number"
}),
},
}
RETURN_TYPES = ("INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,number,min_value,max_value,step):
if number < min_value:
number= min_value
elif number > max_value:
number= max_value
return (number,)
class MultiplicationNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"numberA":(any_type,),
"numberB":("FLOAT", {
"default": 0,
"min": -1, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
})
},
}
RETURN_TYPES = ("FLOAT","INT",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,numberA,numberB):
b=int(numberA*numberB)
a=float(numberA*numberB)
return (a,b,)
class TextInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": ""}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text):
return (text,)
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
@@ -141,7 +345,7 @@ class DynamicDelayProcessor:
@classmethod
def INPUT_TYPES(cls):
print("print INPUT_TYPES",cls)
# print("print INPUT_TYPES",cls)
return {
"required":{
"delay_seconds":("INT",{
@@ -210,3 +414,168 @@ class DynamicDelayProcessor:
# 根据 replace_output 决定输出值
return (max(0, replace_value),) if replace_output == "enable" else (any_input,)
# app 配置节点
class AppInfo:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"name": ("STRING",{"multiline": False,"default": "Mixlab-App","dynamicPrompts": False}),
"image": ("IMAGE",),
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"]),"dynamicPrompts": False}),
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"]),"dynamicPrompts": False}),
},
"optional":{
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
"version":("INT", {
"default": 1,
"min": 1,
"max": 10000,
"step": 1,
"display": "number"
}),
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,name,image,input_ids,output_ids,description,version,share_prefix,link):
im=create_temp_file(image)
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link]}, "result": (image,)}
class GetImageSize_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "get_size"
CATEGORY = "♾️Mixlab/utils"
def get_size(self, image):
_, height, width, _ = image.shape
return (width, height)
class SwitchByIndex:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"A":(any_type,),
"B":(any_type,),
"index":("INT", {
"default": -1,
"min": -1,
"max": 1000,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("C",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self, A,B,index):
C=[]
index=index[0]
for a in A:
C.append(a)
for b in B:
C.append(b)
if index>-1:
try:
C=[C[index]]
except Exception as e:
C=[]
return (C,)
class LimitNumber:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number":(any_type,),
"min_value":("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
"max_value":("INT", {
"default": 1,
"min": 1,
"max": 0xffffffffffffffff,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("number",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self, number, min_value, max_value):
nn=number
if isinstance(number, int):
min_value=int(min_value)
max_value=int(max_value)
if isinstance(number, float):
min_value=float(min_value)
max_value=float(max_value)
if number < min_value:
nn= min_value
elif number > max_value:
nn= max_value
return (nn,)
+2 -1
View File
@@ -3,4 +3,5 @@ pyOpenSSL
watchdog
opencv-python-headless
matplotlib
openai
openai
simple-lama-inpainting
+1455
View File
File diff suppressed because it is too large Load Diff
+42 -23
View File
@@ -187,20 +187,22 @@ app.registerExtension({
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
console.log('serializeValue', node)
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let base64 = await parseImage(url)
let bg_base64 = await parseImage(bg)
let material_base64 = await parseImage(material)
let data = {}
if (url) {
data.image = await parseImage(url)
}
if (bg) {
data.bg_image = await parseImage(bg)
}
return JSON.parse(
JSON.stringify({
image: base64,
bg_image: bg_base64,
material: material_base64
})
)
if (material) {
data.material = await parseImage(material)
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
}
@@ -281,6 +283,8 @@ app.registerExtension({
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
<div><button class="export">Export GLB</button></div>
</div></model-viewer>`
preview.innerHTML = html
@@ -294,6 +298,7 @@ app.registerExtension({
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
@@ -341,21 +346,26 @@ app.registerExtension({
let url = await uploadImage(blob, '.png')
// console.log(url)
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log(tUrl)
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id])
dd[that.id] = { url, bg: url_bg, material: tUrl }
dd[that.id] = { ...dd[that.id], url, material: tUrl }
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
@@ -367,11 +377,11 @@ app.registerExtension({
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', event => {
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
extractMaterial(
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
@@ -456,6 +466,15 @@ app.registerExtension({
input.click()
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
+351
View File
@@ -0,0 +1,351 @@
import { app } from '../../../scripts/app.js'
import { $el } from '../../../scripts/ui.js'
import { api } from '../../../scripts/api.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 12 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'row',
// alignItems: 'center',
justifyContent: 'flex-start'
}
}
async function drawImageToCanvas (imageUrl) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
var img = new Image()
await new Promise((resolve, reject) => {
img.onload = function () {
var scaleFactor = 320 / img.width
var canvasWidth = img.width * scaleFactor
var canvasHeight = img.height * scaleFactor
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight)
resolve()
}
img.onerror = function () {
reject(new Error('Failed to load image'))
}
img.src = imageUrl
})
var base64 = canvas.toDataURL('image/jpeg')
// console.log(base64); // 输出Base64数据
return base64
// 可以在这里执行其他操作,比如将Base64数据保存到服务器或显示在页面上
}
function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
const data = jsonData
const input = []
const output = []
const seed = {}
for (const id in data) {
if (data.hasOwnProperty(id)) {
let node = app.graph.getNodeById(id)
if (inputIds.includes(id)) {
// let node = app.graph.getNodeById(id)
let options = []
// 模型
try {
if (node.type === 'CheckpointLoaderSimple') {
options = node.widgets.filter(w => w.name === 'ckpt_name')[0]
.options.values
} else if (node.type === 'LoraLoader') {
options = node.widgets.filter(w => w.name === 'lora_name')[0]
.options.values
}
} catch (error) {}
if (node.type == 'IntNumber' || node.type == 'FloatSlider') {
// min max step
let [v, min, max, step] = Array.from(node.widgets, w => w.value)
options = { min, max, step }
// node.widgets.filter(w => w.type === 'number')[0].options
}
if (node.type == 'PromptSlide') {
// min max step
options = node.widgets.filter(w => w.type === 'slider')[0].options
// 备选的keywords清单
let keywords=getLocalData(`${id}_PromptSlide`);
// console.log('keywords',keywords)
if(keywords&&keywords[0]){
options.keywords=keywords;
}
}
input[inputIds.indexOf(id)] = {
...data[id],
title: node.title,
id,
options
}
// input.push()
}
if (outputIds.includes(id)) {
// let node = app.graph.getNodeById(id)
// output.push()
output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id }
}
if (node.type === 'KSampler') {
// seed 的类型收集
try {
seed[id] = node.widgets.filter(
w => w.name === 'seed'
)[0].linkedWidgets[0].value
} catch (error) {}
}
}
}
return { input, output, seed }
}
function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
async function save_app (json) {
let url = getUrl()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
data: json,
task: 'save_app',
filename: json.app.filename
})
})
return await res.json()
}
function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
const dataString = JSON.stringify(jsonData)
const blob = new Blob([dataString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
// 释放URL对象
setTimeout(() => {
URL.revokeObjectURL(url)
}, 0)
}
async function save (json, download = false) {
const name = json[0],
version = json[5],
share_prefix = json[6], //用于分享的功能扩展
link=json[7],//用于创建界面上的跳转链接
description = json[4],
inputIds = json[2].split('\n').filter(f => f),
outputIds = json[3].split('\n').filter(f => f)
const iconData = json[1][0]
let { filename, subfolder, type } = iconData
let iconUrl = api.apiURL(
`/view?filename=${encodeURIComponent(
filename
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
try {
let data = await app.graphToPrompt()
const { input, output, seed } = extractInputAndOutputData(
data.output,
inputIds,
outputIds
)
data.app = {
name,
description,
version,
input,
output,
seed, //控制是fixed 还是random
share_prefix,
link,
filename: `${name}_${version}.json`
}
try {
data.app.icon = await drawImageToCanvas(iconUrl)
} catch (error) {}
// console.log(data.app)
// let http_workflow = app.graph.serialize()
if (download) {
await save_app(data)
await downloadJsonFile(data, data.app.filename)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}`
)
} else {
await save_app(data)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app`
)
if (open) window.open(`${getUrl()}/mixlab/app`)
}
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
app.registerExtension({
name: 'Mixlab.utils.AppInfo',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'AppInfo') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('#orig_nodeCreated', this)
const widget = {
type: 'div',
name: 'AppInfoRun',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width,
node.size[1] - widget_height,
node.size[1]
)
)
}
}
const style = `
flex-direction: row;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);`
widget.div = $el('div', {})
const btn = document.createElement('button')
btn.innerText = 'Save & Open'
btn.style = style
btn.addEventListener('click', () => {
// console.log('hahhah')
if (window._mixlab_app_json) {
save(window._mixlab_app_json)
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
this.widgets.filter(w => w.name === 'version')[0].value += 1
}
})
const download = document.createElement('button')
download.innerText = 'Download For App'
download.style = style
download.style.marginLeft = '12px'
download.addEventListener('click', () => {
// console.log('hahhah')
if (window._mixlab_app_json) {
save(window._mixlab_app_json, true)
} else {
alert('Please run the workflow before saving')
// app.queuePrompt(0, 1)
this.widgets.filter(w => w.name === 'version')[0].value += 1
}
})
document.body.appendChild(widget.div)
widget.div.appendChild(btn)
widget.div.appendChild(download)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
onExecuted?.apply(this, arguments)
console.log(message.json)
window._mixlab_app_json = message.json
try {
const div = this.widgets.filter(w => w.div)[0].div
Array.from(
div.querySelectorAll('button'),
b => (b.style.background = 'yellow')
)
} catch (error) {}
}
}
}
})
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.5.0'
const version = 'v0.9.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+3 -1
View File
@@ -406,7 +406,9 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
}
};
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
+63 -11
View File
@@ -156,8 +156,8 @@ const parseSvg = async svgContent => {
return { data, image: base64, svgElement }
}
async function setArea (cw, ch, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.6)
async function setArea (cw, ch, topBase64, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.8)
let div = document.createElement('div')
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
@@ -170,8 +170,13 @@ async function setArea (cw, ch, base64, data, fn) {
outline: 2px solid #eaeaea;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
pointer-events: none;'></div>
border: 2px dashed red;
pointer-events: none;
background-image: url("${topBase64}");
background-repeat: no-repeat;
background-size: cover;
'></div>
<div class="mx_close"> X </div>
</div>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
@@ -181,13 +186,25 @@ async function setArea (cw, ch, base64, data, fn) {
// canvas.height = ch
let img = div.querySelector('#ml_video')
let overlay = div.querySelector('#ml_overlay')
// let overlay = div.querySelector('#ml_overlay')
let selection = div.querySelector('#ml_selection')
let close = div.querySelector('.mx_close')
let startX, startY, endX, endY
let start = false
let setDone = false
// Set video source
img.src = base64
// canvas.toDataURL();
close.style = `cursor: pointer;
position: fixed;
left: 12px;
top: 12px;
z-index: 99999999;
background: black;
width: 44px;
height: 44px;
text-align: center;
line-height: 44px;`
// init area
// const data = getSetAreaData()
@@ -216,14 +233,37 @@ async function setArea (cw, ch, base64, data, fn) {
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
overlay.addEventListener('click', remove)
function remove () {
overlay.removeEventListener('click', remove)
const removeDiv = () => {
div.remove()
close.removeEventListener('click', removeDiv)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
div.remove()
img.removeEventListener('mousedown', setDoneCheck)
}
close.addEventListener('click', removeDiv)
const setDoneCheck = event => {
console.log(setDone)
if (setDone) {
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
setDone = false
start = false
startX = event.clientX
startY = event.clientY
}
}
img.addEventListener('mousedown', setDoneCheck)
function remove () {
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
setDone = true
// div.remove()
}
function startSelection (event) {
@@ -531,12 +571,24 @@ app.registerExtension({
}
}
try {
console.log('this.inputs', this.inputs)
let topLinkId = this.inputs[0].link
let topNodeId = app.graph.links[topLinkId].origin_id
let topIm = app.graph.getNodeById(topNodeId).imgs[0]
let linkId = this.inputs[3].link
let nodeId = app.graph.links[linkId].origin_id
// console.log(linkId,this.inputs)
let im = app.graph.getNodeById(nodeId).imgs[0]
let src = im.src
setArea(im.naturalWidth, im.naturalHeight, src, data, updateValue)
// let src = im.src
setArea(
im.naturalWidth,
im.naturalHeight,
topIm.src,
im.src,
data,
updateValue
)
} catch (error) {}
})
}
+199
View File
@@ -0,0 +1,199 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2 - 24}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
paddingLeft: '12px',
display: 'flex',
flexDirection: 'row',
// alignItems: 'center',
justifyContent: 'space-between'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
const createSelect = (select, opts, targetWidget) => {
select.style.display = 'block'
let html = ''
let isMatch = false
for (const opt of opts) {
html += `<option value='${opt}' ${
targetWidget.value === opt ? 'selected' : ''
}>${opt}</option>`
if (targetWidget.value === opt) isMatch = true
}
select.innerHTML = html
if (!isMatch) targetWidget.value = opts[0]
// 添加change事件监听器
select.addEventListener('change', function () {
// 获取选中的选项的值
var selectedOption = select.options[select.selectedIndex].value
targetWidget.value = selectedOption
// console.log(widget,selectedOption)
})
}
app.registerExtension({
name: 'Mixlab.prompt.PromptSlide',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'PromptSlide') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const prompt_keyword = this.widgets.filter(
w => w.name == 'prompt_keyword'
)[0]
// console.log('PromptSlide nodeData', prompt_keyword)
const widget = {
type: 'div',
name: 'upload',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
)
}
}
widget.div = $el('div', {})
const btn = document.createElement('button')
btn.innerText = 'Upload Keywords'
btn.style = `cursor: pointer;
font-weight: 300;
margin: 2px;
color: var(--descrip-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid; height: 30px;min-width: 122px;
`
const select = document.createElement('select')
select.style = `display:none;cursor: pointer;
font-weight: 300;
margin: 2px;
color: var(--descrip-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid; height: 30px;min-width: 100px;
`
widget.select = select
// const btn=document.createElement('button');
// btn.innerText='Upload'
btn.addEventListener('click', () => {
let inp = document.createElement('input')
inp.type = 'file'
inp.accept = '.txt'
inp.click()
inp.addEventListener('change', event => {
// 获取选择的文件
const file = event.target.files[0]
// 创建文件读取器
const reader = new FileReader()
// 定义读取完成事件的回调函数
reader.onload = (event)=> {
// 读取完成后的文本内容
const fileContent = event.target.result.split('\n')
const keywords = Array.from(fileContent, f => f.trim()).filter(
f => f
)
// 打印文件内容
// console.log(keywords)
// widget.value = keywords
setLocalDataOfWin(`${this.id}_PromptSlide`,keywords)
createSelect(select, keywords, prompt_keyword)
inp.remove()
}
// 以文本方式读取文件
reader.readAsText(file)
})
})
widget.div.appendChild(btn)
widget.div.appendChild(select)
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'PromptSlide') {
try {
let prompt = node.widgets.filter(w => w.name === 'prompt_keyword')[0]
let keywords= getLocalData( `${node.id}_PromptSlide`)
// console.log('keywords',keywords)
let widget = node.widgets.filter(w => w.select)[0]
if (keywords && keywords[0]) {
// let widget = node.widgets.filter(w => w.select)[0]
// console.log('select',widget,widget.value)
widget.select.style.display = 'block'
createSelect(widget.select, keywords, prompt)
}
} catch (error) {}
}
}
})
+137 -53
View File
@@ -48,6 +48,38 @@ async function get_nodes_map () {
return await res.json()
}
function get_url () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
async function get_my_app (filename = null) {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'my_app',
filename
})
})
let result = await res.json()
let data = []
try {
for (const res of result.data) {
let { app, workflow } = res.data
if (app.filename)
data.push({
...app,
data: workflow,
date: res.date
})
}
} catch (error) {}
return data
}
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
@@ -633,7 +665,7 @@ app.registerExtension({
}
clipboardAction(() => {
let name = group.title+' ♾️Mixlab'
let name = group.title + ' ♾️Mixlab'
let nodes = group._nodes
app.canvas.copyToClipboard(nodes)
@@ -667,16 +699,32 @@ app.registerExtension({
...options
] // and return the options
}
LGraphCanvas.prototype.centerOnNode = function (node) {
// console.log(node)
var dpr = window.devicePixelRatio || 1 // 获取设备像素比
this.ds.offset[0] =
-node.pos[0] -
node.size[0] * 0.5 +
(this.canvas.width * 0.5) / (this.ds.scale * dpr) // 考虑设备像素比
this.ds.offset[1] =
-node.pos[1] -
node.size[1] * 0.5 +
(this.canvas.height * 0.5) / (this.ds.scale * dpr) // 考虑设备像素比
this.setDirty(true, true)
}
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
const apps = await get_my_app()
// console.log('apps',apps)
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
options.push(null, {
content: `Find ♾️Mixlab`,
disabled: false, // or a function determining whether to disable
content: `Nodes Map ♾️Mixlab`,
disabled: false,
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
@@ -709,12 +757,13 @@ app.registerExtension({
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 32px;`
height: 44px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.innerText = `Find The Node`
textB.style.fontSize = '12px'
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
@@ -753,32 +802,36 @@ app.registerExtension({
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[nodeId].class_type
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
if (nodesMap[n]) {
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
console.log('node')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nd.appendChild(d)
nd.appendChild(d)
}
}
}
@@ -786,32 +839,36 @@ app.registerExtension({
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
}
})
if (nodesMap[n]) {
const { url, title: _title } = nodesMap[n]
let title = app.graph.getNodeById(nodeId).title || _title
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
console.log('click')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
d.innerHTML = `
<span>${'#' + nodeId} ${title}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = n
nodesDiv.appendChild(d)
nodesDiv.appendChild(d)
}
}
nodesDivv.appendChild(nodesDiv)
@@ -823,7 +880,34 @@ app.registerExtension({
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
},{
content: 'Workflow App ♾️Mixlab',
has_submenu: true,
disabled: false,
submenu: {
options: Array.from(apps, a => {
return {
content: a.name,
callback: async () => {
try {
let item = (await get_my_app(a.filename))[0]
if (item) {
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
})
}
})
return options
}
}
+34 -10
View File
@@ -2,7 +2,7 @@ import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import { addValueControlWidget } from "../../../scripts/widgets.js";
import { addValueControlWidget } from '../../../scripts/widgets.js'
const getLocalData = key => {
let data = {}
@@ -45,6 +45,21 @@ function get_position_style (ctx, widget_width, y, node_height) {
}
}
function hexToRGBA (hexColor) {
var hex = hexColor.replace('#', '')
var r = parseInt(hex.substring(0, 2), 16)
var g = parseInt(hex.substring(2, 4), 16)
var b = parseInt(hex.substring(4, 6), 16)
// 获取透明度的十六进制值
var alphaHex = hex.substring(6)
// 将透明度的十六进制值转换为十进制值
var alpha = parseInt(alphaHex, 16) / 255
return [r,g,b,alpha]
}
app.registerExtension({
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
@@ -60,8 +75,16 @@ app.registerExtension({
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color')
return data[node.id] || '#000000'
let data = getLocalData('_mixlab_utils_color');
let hex=data[node.id] || '#000000'
let [r,g,b,a]=hexToRGBA(hex)
return {
hex,
r,
g,
b,
a
}
}
}
// widget.something = something; // maybe adds stuff to it
@@ -109,7 +132,7 @@ app.registerExtension({
ip.style = `outline: none;
border: none;
padding: 4px;
width: 100%;cursor: pointer;
width: 70%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
@@ -164,16 +187,17 @@ app.registerExtension({
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TextToNumber') {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const random_number=this.widgets.filter(w=>w.name==='random_number')[0]
if(random_number.value==='enable'){
const n=this.widgets.filter(w=>w.name==='number')[0]
n.value=message.num[0]
const random_number = this.widgets.filter(
w => w.name === 'random_number'
)[0]
if (random_number.value === 'enable') {
const n = this.widgets.filter(w => w.name === 'number')[0]
n.value = message.num[0]
}
console.log('TextToNumber', random_number.value)
}
}
+592 -684
View File
File diff suppressed because it is too large Load Diff
+723
View File
@@ -0,0 +1,723 @@
{
"last_node_id": 22,
"last_link_id": 23,
"nodes": [
{
"id": 9,
"type": "CLIPTextEncode",
"pos": [
2070,
830
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 7
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 7,
"type": "EmptyLatentImage",
"pos": [
2070,
1060
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
5
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 6,
"type": "CheckpointLoaderSimple",
"pos": [
1640,
920
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
2
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
6,
7
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
9
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
2080,
630
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 6
},
{
"name": "text",
"type": "STRING",
"link": 16,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
3
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 10,
"type": "VAEDecode",
"pos": [
2870,
630
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 8
},
{
"name": "vae",
"type": "VAE",
"link": 9
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
12
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 16,
"type": "ShowTextForGPT",
"pos": [
3288,
-323
],
"size": {
"0": 449.1168212890625,
"1": 76
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 20,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"a girl face,super,(Pop Art:1.26),(Black and White:1.26)"
]
},
{
"id": 11,
"type": "PreviewImage",
"pos": [
3319,
-161
],
"size": {
"0": 435.8727111816406,
"1": 511.8609619140625
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 5,
"type": "KSampler",
"pos": [
2520,
630
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 2
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 3
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 4
},
{
"name": "latent_image",
"type": "LATENT",
"link": 5
},
{
"name": "seed",
"type": "INT",
"link": 21,
"widget": {
"name": "seed"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
8
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
1063141893699118,
"fixed",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 18,
"type": "IntNumber",
"pos": [
2852,
275
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
21
],
"shape": 3,
"slot_index": 0
}
],
"title": "Seed",
"properties": {
"Node name for S&R": "IntNumber"
},
"widgets_values": [
-1
]
},
{
"id": 22,
"type": "PromptSlide",
"pos": [
2364,
-294
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PromptSlide"
},
"widgets_values": [
"Pop Art",
1.26
]
},
{
"id": 12,
"type": "AppInfo",
"pos": [
2924,
846
],
"size": {
"0": 400,
"1": 272
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 12
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
13
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "AppInfo"
},
"widgets_values": [
"Prompt-weight-test",
"21\n22\n18",
"11\n16",
"",
1,
"",
null
]
},
{
"id": 21,
"type": "PromptSlide",
"pos": [
2360,
-544
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
22
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PromptSlide"
},
"widgets_values": [
"Black and White",
1.26
]
},
{
"id": 13,
"type": "RandomPrompt",
"pos": [
2840,
-330
],
"size": {
"0": 400,
"1": 224
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "mutable_prompt",
"type": "STRING",
"link": 22,
"widget": {
"name": "mutable_prompt"
}
},
{
"name": "immutable_prompt",
"type": "STRING",
"link": 23,
"widget": {
"name": "immutable_prompt"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
19
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "RandomPrompt"
},
"widgets_values": [
1,
"",
" ``",
"disable",
""
]
},
{
"id": 15,
"type": "RandomPrompt",
"pos": [
2840,
-30
],
"size": {
"0": 400,
"1": 224
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "mutable_prompt",
"type": "STRING",
"link": 19,
"widget": {
"name": "mutable_prompt"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
16,
20
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "RandomPrompt"
},
"widgets_values": [
1,
"",
" a girl face,super,``",
"disable",
""
]
}
],
"links": [
[
2,
6,
0,
5,
0,
"MODEL"
],
[
3,
8,
0,
5,
1,
"CONDITIONING"
],
[
4,
9,
0,
5,
2,
"CONDITIONING"
],
[
5,
7,
0,
5,
3,
"LATENT"
],
[
6,
6,
1,
8,
0,
"CLIP"
],
[
7,
6,
1,
9,
0,
"CLIP"
],
[
8,
5,
0,
10,
0,
"LATENT"
],
[
9,
6,
2,
10,
1,
"VAE"
],
[
12,
10,
0,
12,
0,
"IMAGE"
],
[
13,
12,
0,
11,
0,
"IMAGE"
],
[
16,
15,
0,
8,
1,
"STRING"
],
[
19,
13,
0,
15,
0,
"STRING"
],
[
20,
15,
0,
16,
0,
"STRING"
],
[
21,
18,
0,
5,
4,
"INT"
],
[
22,
21,
0,
13,
0,
"STRING"
],
[
23,
22,
0,
13,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
+717
View File
@@ -0,0 +1,717 @@
{
"last_node_id": 25,
"last_link_id": 32,
"nodes": [
{
"id": 5,
"type": "CLIPTextEncode",
"pos": [
1029,
-2149
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 6
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
3
],
"slot_index": 0
}
],
"title": "负向prompt",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 6,
"type": "VAEDecode",
"pos": [
1867,
-2378
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 15,
"type": "EnhanceImage",
"pos": [
2591,
-2531
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 25
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
18
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EnhanceImage"
},
"widgets_values": [
1.1
]
},
{
"id": 21,
"type": "EmptyLatentImage",
"pos": [
998,
-2674
],
"size": [
315,
106
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 30,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 32,
"widget": {
"name": "height"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
26
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 24,
"type": "LimitNumber",
"pos": [
665,
-2958
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "number",
"type": "*",
"link": 29
}
],
"outputs": [
{
"name": "number",
"type": "*",
"links": [
30
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LimitNumber"
},
"widgets_values": [
512,
4089
]
},
{
"id": 25,
"type": "LimitNumber",
"pos": [
648,
-2659
],
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "number",
"type": "*",
"link": 31
}
],
"outputs": [
{
"name": "number",
"type": "*",
"links": [
32
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LimitNumber"
},
"widgets_values": [
512,
4089
]
},
{
"id": 22,
"type": "IntNumber",
"pos": [
302,
-2758
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"title": "Width",
"properties": {
"Node name for S&R": "IntNumber"
},
"widgets_values": [
512
]
},
{
"id": 23,
"type": "IntNumber",
"pos": [
299,
-2601
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
31
],
"shape": 3,
"slot_index": 0
}
],
"title": "Height",
"properties": {
"Node name for S&R": "IntNumber"
},
"widgets_values": [
512
]
},
{
"id": 2,
"type": "CheckpointLoaderSimple",
"pos": [
567,
-2422
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
5,
6
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"title": "Model",
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 1,
"type": "KSampler",
"pos": [
1509,
-2394
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 2
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 3
},
{
"name": "latent_image",
"type": "LATENT",
"link": 26,
"slot_index": 3
},
{
"name": "denoise",
"type": "FLOAT",
"link": 16,
"widget": {
"name": "denoise"
},
"slot_index": 4
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
730250870715434,
"randomize",
15,
6.9,
"euler",
"karras",
0.59
]
},
{
"id": 14,
"type": "FloatSlider",
"pos": [
1007,
-2819
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "FLOAT",
"type": "FLOAT",
"links": [
16
],
"shape": 3,
"slot_index": 0
}
],
"title": "denoise",
"properties": {
"Node name for S&R": "FloatSlider"
},
"widgets_values": [
1
]
},
{
"id": 16,
"type": "PreviewImage",
"pos": [
2949,
-2615
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 4,
"type": "CLIPTextEncode",
"pos": [
1022,
-2375
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
2
],
"slot_index": 0
}
],
"title": "prompt",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"superman,fat cat"
]
},
{
"id": 7,
"type": "AppInfo",
"pos": [
2134,
-2528
],
"size": {
"0": 408.4201965332031,
"1": 406.9195861816406
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 9
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "AppInfo"
},
"widgets_values": [
"Text-to-Image",
"4\n22\n23\n2\n\n\n",
"16",
"演示基本的文生图流程",
1,
"#comfyui-mixlab-nodes# ",
null
]
}
],
"links": [
[
1,
2,
0,
1,
0,
"MODEL"
],
[
2,
4,
0,
1,
1,
"CONDITIONING"
],
[
3,
5,
0,
1,
2,
"CONDITIONING"
],
[
5,
2,
1,
4,
0,
"CLIP"
],
[
6,
2,
1,
5,
0,
"CLIP"
],
[
7,
1,
0,
6,
0,
"LATENT"
],
[
8,
2,
2,
6,
1,
"VAE"
],
[
9,
6,
0,
7,
0,
"IMAGE"
],
[
16,
14,
0,
1,
4,
"FLOAT"
],
[
18,
15,
0,
16,
0,
"IMAGE"
],
[
25,
7,
0,
15,
0,
"IMAGE"
],
[
26,
21,
0,
1,
3,
"LATENT"
],
[
29,
22,
0,
24,
0,
"*"
],
[
30,
24,
0,
21,
0,
"INT"
],
[
31,
23,
0,
25,
0,
"*"
],
[
32,
25,
0,
21,
1,
"INT"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.5 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

-2
View File
@@ -1,2 +0,0 @@
![workflow](./assets/City_Snapshot_00005_.png)
![workflow](./assets/挖掘机_00076_1.png)