Compare commits

...
116 Commits
Author SHA1 Message Date
shadowcz007 5b8cab920c 增加示例 2023-12-24 10:56:56 +08:00
shadowcz007 8d3d327335 Update Utils.py 2023-12-24 10:53:05 +08:00
shadowcz007 32574050c4 增加从语音识别发送到chatgpt的方法 2023-12-24 10:44:25 +08:00
shadowcz007 8d45a90d9b Update Utils.py 2023-12-24 09:31:42 +08:00
gold3bear f66862a422 update DynamicDelayProcessor 2023-12-24 00:06:40 +08:00
gold3bear ebf6395de2 delay by text processor 2023-12-23 23:16:56 +08:00
shadow 99def24dd8 Merge pull request #73 from shadowcz007/v0.5-GamePal
支持换行的textimage
2023-12-23 14:13:31 +08:00
shadowcz007 f5b210d142 支持换行的textimage 2023-12-23 14:13:04 +08:00
shadow a137a23b48 Merge pull request #72 from shadowcz007/v0.5-GamePal
TextToNumber&audio input control
2023-12-23 13:16:08 +08:00
shadowcz007 6bbf06d9e9 TextToNumber&audio input control 2023-12-23 13:15:47 +08:00
shadowcz007 0b614b40cf Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-23 10:49:27 +08:00
shadowcz007 27673561bd 使用comfyui的ui来控制刷新率 2023-12-23 10:49:24 +08:00
shadow 6e2070410d Merge pull request #70 from shadowcz007/v0.3.2-3DImage
Delete layers-test-workflow.json
2023-12-22 20:36:39 +08:00
shadowcz007 7ccf21f74f Delete layers-test-workflow.json 2023-12-22 20:36:04 +08:00
shadow 3b2710f285 Merge pull request #69 from shadowcz007/v0.3.2-3DImage
v0.4.2
2023-12-22 20:28:33 +08:00
shadowcz007 c4b277235b 1 2023-12-22 20:27:53 +08:00
shadowcz007 a55318add1 v0.4.2 2023-12-22 20:24:19 +08:00
shadowcz007 b57123a4fe Update 3D-workflow.json 2023-12-22 20:21:57 +08:00
shadowcz007 04dcc00670 增加可视化选区 2023-12-22 20:20:03 +08:00
shadowcz007 746a02b49f test- 2023-12-22 12:16:39 +08:00
shadowcz007 bdbe3db2a9 Update Vae.py 2023-12-21 10:39:32 +08:00
shadowcz007 27ae99ad86 Update __init__.py 2023-12-21 10:26:16 +08:00
shadowcz007 38add89547 update style 2023-12-21 10:24:01 +08:00
shadowcz007 a9612fbb2f 增加一个resize节点 2023-12-20 16:12:09 +08:00
shadowcz007 429cc29b5b test 2023-12-20 15:08:13 +08:00
shadowcz007 8eca94e405 test 2023-12-20 14:32:57 +08:00
shadowcz007 ad71daafb6 Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-20 12:18:41 +08:00
shadowcz007 c936d83688 1 2023-12-20 12:18:38 +08:00
shadow c6684d680f Merge pull request #66 from shadowcz007/main
0.4.1
2023-12-20 10:50:26 +08:00
shadow fe358b0e13 0.4.1 2023-12-20 08:59:33 +08:00
shadowcz007 897f259a2a Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-20 00:10:05 +08:00
shadowcz007 f3302c1b3a update 2023-12-20 00:08:05 +08:00
shadow 573feeaaab Merge pull request #64 from shadowcz007/main
1
2023-12-20 00:05:39 +08:00
shadowcz007 019c98ecc1 update default style 2023-12-19 21:56:49 +08:00
shadowcz007 ad6a51a4b5 Update ImageNode.py 2023-12-19 12:59:59 +08:00
shadowcz007 f94278776e v0.4.0 2023-12-17 13:46:12 +08:00
shadowcz007 315885cb0b 3dimage & 2023-12-17 13:36:45 +08:00
shadow 7e605f8228 Merge pull request #59 from shadowcz007/v0.3.2-3DImage
V0.3.2 3 d image
2023-12-17 13:10:41 +08:00
shadow aed70435f9 Merge pull request #58 from shadowcz007/improve_mix-modal_ui
Improve mix modal UI
2023-12-17 12:57:56 +08:00
shadowcz007 1fb1728ede Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-17 12:57:11 +08:00
shadowcz007 497c4fe5a3 Update image_mixlab.js 2023-12-17 12:56:14 +08:00
shadow 09ad7764b8 Merge pull request #57 from shadowcz007/main
1
2023-12-17 12:54:09 +08:00
shadow bc3d24fddf Merge branch 'v0.3.2-3DImage' into main 2023-12-17 12:54:02 +08:00
shadowcz007 2d634d628a ing 2023-12-17 12:51:05 +08:00
shadowcz007 790c22d919 ing 2023-12-17 12:26:41 +08:00
gold3bear c275a56806 # 2023-12-17 00:40:26 +08:00
gold3bear 82c3c7addd improve mix-modal ui 2023-12-16 23:59:45 +08:00
shadowcz007 b464d85c04 更新 2023-12-16 21:38:28 +08:00
shadowcz007 078618b4cf 优化 2023-12-16 18:01:31 +08:00
shadowcz007 561805a417 优化下3dImage 2023-12-16 15:48:35 +08:00
shadowcz007 7bb4324365 fixbug 2023-12-16 13:52:28 +08:00
shadowcz007 0608653d35 Update ui_mixlab.js 2023-12-16 00:05:41 +08:00
shadowcz007 fa3472cdc5 find_the_node 2023-12-15 23:46:46 +08:00
shadowcz007 7d553b6fcf 修复svgImage的bug 2023-12-15 19:57:22 +08:00
shadowcz007 677627630e 右击节点获取readme 2023-12-14 20:02:27 +08:00
shadow 9f23172b22 Update README.md 2023-12-13 10:35:33 +08:00
gold3bear 14ceaa472d Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 21:17:36 +08:00
gold3bear 1188b9d3bc fix:macos fonts runtime erros 2023-12-12 21:16:51 +08:00
shadowcz007 424c9a9423 0.3.1 2023-12-12 20:26:57 +08:00
shadowcz007 8f60c81ef3 test 2023-12-12 20:22:46 +08:00
shadowcz007 dd648d1ae5 test 2023-12-12 20:14:43 +08:00
shadowcz007 02e839e272 新增示例 2023-12-12 18:11:47 +08:00
shadowcz007 ec8c56707b 0.3.0
v0.3.0 🚀🚗🚚🏃‍

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

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

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

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

- Added an experimental 3DImage node for loading 3D models 🌟
2023-12-12 17:22:37 +08:00
shadowcz007 1e8d317ee8 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 17:14:55 +08:00
shadowcz007 e81df111a7 0.3 ing 2023-12-12 17:14:52 +08:00
shadow 90e55ffe14 Merge pull request #45 from shadowcz007/v0.2.8-proxy
V0.2.8 proxy
2023-12-12 14:09:55 +08:00
shadowcz007 4e73b1d3fc fixbug 2023-12-12 14:07:01 +08:00
gold3bear 21dca1e34f test ok 2023-12-12 13:56:58 +08:00
shadowcz007 c2292850bb 1 2023-12-12 12:38:37 +08:00
BearXiong 04791c92e2 proxy new_request 2023-12-12 11:55:12 +08:00
shadowcz007 d9462b6d8a Update Utils.py 2023-12-11 11:49:36 +08:00
shadowcz007 be9b83559e test 2023-12-10 17:31:59 +08:00
shadowcz007 958889afee test-3d 2023-12-10 17:31:28 +08:00
shadowcz007 dce677035f 更新-字体和颜色选择 2023-12-10 11:31:06 +08:00
shadowcz007 d98a8855ad update 2023-12-09 18:07:16 +08:00
shadowcz007 7cd0587a64 COLOR冲突,改个名字 2023-12-09 12:33:58 +08:00
shadowcz007 bb3967f11e ing 2023-12-09 00:37:38 +08:00
shadowcz007 36ee203bd7 ing 2023-12-09 00:17:29 +08:00
shadowcz007 914919ba75 test 2023-12-08 13:01:00 +08:00
shadowcz007 a4514e8565 1 2023-12-08 00:15:13 +08:00
shadowcz007 116e983c50 新增textImage 2023-12-08 00:07:34 +08:00
shadowcz007 fb667b6c42 0.2.8 layers 预发布 2023-12-07 18:28:58 +08:00
shadowcz007 b0a090cf14 1 2023-12-07 11:42:54 +08:00
shadowcz007 110b470d33 Update README.md 2023-12-06 19:51:30 +08:00
shadowcz007 c517c4d015 v0.2.7 2023-12-06 19:49:26 +08:00
shadowcz007 35ed4f9101 Update main_mixlab.js 2023-12-06 19:41:29 +08:00
shadowcz007 18c723c2e3 seed 2023-12-06 19:35:40 +08:00
shadowcz007 631223602c Update gpt_mixlab.js 2023-12-06 19:01:30 +08:00
shadowcz007 a6cc907de2 v0.2.6 2023-12-06 18:18:35 +08:00
shadowcz007 f847dcccf4 v0.2.5.2 2023-12-05 17:22:51 +08:00
shadowcz007 ff3f8f52d0 update 2023-12-05 17:22:28 +08:00
shadowcz007 d7d46682fc 优化GPT 2023-12-05 13:47:08 +08:00
shadowcz007 dfe720f3ec v0.2.5.1 2023-12-05 00:22:43 +08:00
shadowcz007 2c68662c22 文件名冲突引起的插件不生效 2023-12-05 00:21:43 +08:00
shadowcz007 bc1b998ba5 Update gpt.js 2023-12-04 23:56:44 +08:00
shadowcz007 9d5ccc3389 v0.2.5 2023-12-04 20:15:10 +08:00
shadowcz007 a811f884cc update 2023-12-04 20:07:29 +08:00
shadowcz007 8b86c379d1 v0.2.5
新增GPT节点
2023-12-04 20:01:51 +08:00
shadowcz007 8c0321b1cf Update ui.js 2023-12-02 20:03:38 +08:00
shadowcz007 62ab2c3514 readme 2023-12-02 17:22:24 +08:00
shadowcz007 ed61ca761a v0.2.4
Clicking on the floating window image can copy it to the clipboard.
2023-12-02 11:55:15 +08:00
shadowcz007 95ca17d816 点击悬浮窗图片可以拷贝到剪切板 2023-12-02 11:54:30 +08:00
shadowcz007 db6c721a8f 单击图片可复制到剪切板 2023-12-02 11:35:27 +08:00
shadow b5c68751aa Merge pull request #17 from shadowcz007/v0.3-psd读取分层
V0.3 psd读取分层
2023-12-02 00:42:46 +08:00
shadowcz007 7780bfd671 v0.2.3 2023-12-02 00:42:21 +08:00
shadow a56970693a Merge pull request #15 from shadowcz007/main
1
2023-12-01 23:37:02 +08:00
shadowcz007 c8b24fe84b Update Watcher.py 2023-12-01 23:00:45 +08:00
shadowcz007 798aabf333 bugfix 2023-12-01 22:10:27 +08:00
shadow d38a7aa558 Merge pull request #14 from shadowcz007/v0.3-psd读取分层
v0.2.2
2023-12-01 19:54:15 +08:00
shadowcz007 0bdf1e47a6 v0.2.2
- 本地读取节点也可以更新prompt了
2023-12-01 19:53:50 +08:00
shadowcz007 a44016e57c Update README.md 2023-12-01 18:20:03 +08:00
shadowcz007 35a9351e53 1 2023-12-01 17:51:31 +08:00
shadowcz007 c1b2112bb8 Update requirements.txt 2023-12-01 16:39:16 +08:00
shadowcz007 42a3dd261f v0.2.1 2023-12-01 13:31:00 +08:00
shadowcz007 e14487fab1 优化体验
更友好的https提示
版本更新提示
缺失节点提示
2023-12-01 13:30:19 +08:00
shadowcz007 c16df1a473 Update checkVersion.js 2023-11-30 23:50:43 +08:00
53 changed files with 18270 additions and 992 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
__pycache__/
https/
nodes/config.json
nodes/config.json
workflow/my_workflow.json
+108 -38
View File
@@ -1,12 +1,20 @@
##
In progress.
!!
![screenshare](./assets/screenshare.png)
v0.4.0 🚀🚗🚚🏃‍
- Add "help" option to the context menu for each node.
- Add "find the node" option to the global context menu.
- Optimize the 3D Image node and add workflow.
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### ScreenShareNode & FloatingVideoNode
> Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
>
![screenshare](./assets/screenshare.png)
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
@@ -15,40 +23,37 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
!! Please use the address with HTTPS (https://127.0.0.1).
## Installation
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
manually install, simply clone the repo into the custom_nodes directory with this command:
[Voice + Real-time Face Swap Workflow](./workflow/语音+实时换脸workflow.json)
```
cd ComfyUI/custom_nodes
git clone https://github.com/shadowcz007/comfyui-mixlab-nodes.git
```
Install the requirements:
run directly:
```
cd ComfyUI_Mixlab
install.bat
```
or install the requirements using:
```
../../../python_embeded/python.exe -s -m pip install -r requirements.txt
```
If you are using a venv, make sure you have it activated before installation and use:
```
pip3 install -r requirements.txt
```
### 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)
## Nodes
[workflow-5](./workflow/5-gpt-workflow.json)
![main](./assets/all.png)
### 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)
### Layers
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
![layers](./assets/layers-workflow.svg)
![poster](./assets/poster-workflow.svg)
## Other Nodes
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
[workflow-1](./workflow/1-workflow.json)
@@ -62,13 +67,6 @@ pip3 install -r requirements.txt
![TransparentImage](./assets/TransparentImage.png)
>LoadImagesFromLocal
![watch](./assets/load-watch.png)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
> Consistency Decoder
[openai Consistency Decoder]( https://github.com/openai/consistencydecoder)
@@ -85,16 +83,88 @@ Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
### Improvement
- Add "help" option to the context menu for each node.
- Add "find the node" 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.
![help](./assets/help.png)
![node-not-found](./assets/node-not-found.png)
### Models
[Download CLIPSeg](https://huggingface.co/CIDAS/clipseg-rd64-refined/tree/main), move to : model/clipseg
<!-- ### Workflow
[Workflow](./workflow.md) -->
## Installation
manually install, simply clone the repo into the custom_nodes directory with this command:
```
cd ComfyUI/custom_nodes
git clone https://github.com/shadowcz007/comfyui-mixlab-nodes.git
```
Install the requirements:
run directly:
```
cd ComfyUI/custom_nodes/comfyui-mixlab-nodes
install.bat
```
or install the requirements using:
```
../../../python_embeded/python.exe -s -m pip install -r requirements.txt
```
If you are using a venv, make sure you have it activated before installation and use:
```
pip3 install -r requirements.txt
```
#### Chinese community
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
#### Thanks:
[ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
#### discussions:
[discussions](https://github.com/shadowcz007/comfyui-mixlab-nodes/discussions)
### TODO:
- 音频播放节点:带可视化、支持多音轨、可配置音轨音量
- vector https://github.com/GeorgLegato/stable-diffusion-webui-vectorstudio
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date&theme=dark
"
/>
<source
media="(prefers-color-scheme: light)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date
"
/>
<img
alt="Star History Chart"
src="https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date"
/>
</picture>
+175 -14
View File
@@ -5,6 +5,9 @@ import importlib.util
import sys,json
import urllib
import datetime
python = sys.executable
@@ -61,6 +64,18 @@ except ImportError:
sys.exit()
def install_openai():
# Helper function to install the OpenAI module if not already installed
try:
importlib.import_module('openai')
except ImportError:
import pip
pip.main(['install', 'openai'])
install_openai()
current_path = os.path.abspath(os.path.dirname(__file__))
@@ -95,25 +110,102 @@ def create_key(key_p,crt_p):
return
def create_for_https():
# print("#####path::", current_path)
https_key_path=os.path.join(current_path, "https")
crt=os.path.join(https_key_path, "certificate.crt")
key=os.path.join(https_key_path, "private.key")
# print("##https_key_path", crt,key)
print('\033[91mhttps_key: ', crt,key)
if not os.path.exists(https_key_path):
# 使用mkdir()方法创建新目录
os.mkdir(https_key_path)
if not os.path.exists(crt):
create_key(key,crt)
print('https_key OK: ', crt,key)
return (crt,key)
# workflow
def read_workflow_json_files(folder_path):
json_files = []
for filename in os.listdir(folder_path):
if filename.endswith('.json'):
json_files.append(filename)
data = []
for file in json_files:
file_path = os.path.join(folder_path, file)
try:
with open(file_path) as json_file:
json_data = json.load(json_file)
creation_time=datetime.datetime.fromtimestamp(os.path.getctime(file_path))
numeric_timestamp = creation_time.timestamp()
file_info = {
'filename': file,
'data': json_data,
'date': numeric_timestamp
}
data.append(file_info)
except Exception as e:
print(e)
sorted_data = sorted(data, key=lambda x: x['date'], reverse=True)
return sorted_data
def get_workflows():
# print("#####path::", current_path)
workflow_path=os.path.join(current_path, "workflow")
print('workflow_path: ',workflow_path)
if not os.path.exists(workflow_path):
# 使用mkdir()方法创建新目录
os.mkdir(workflow_path)
workflows=read_workflow_json_files(workflow_path)
return workflows
def save_workflow_json(data):
workflow_path=os.path.join(current_path, "workflow/my_workflow.json")
with open(workflow_path, 'w') as file:
json.dump(data, file)
return workflow_path
def get_nodes_map():
# print("#####path::", current_path)
data_path=os.path.join(current_path, "data")
print('data_path: ',data_path)
# if not os.path.exists(data_path):
# # 使用mkdir()方法创建新目录
# os.mkdir(data_path)
json_data={}
nodes_map=os.path.join(current_path, "data/extension-node-map.json")
if os.path.exists(nodes_map):
with open(nodes_map) as json_file:
json_data = json.load(json_file)
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
# 定义新的 get 方法
async def new_request(self, method, url, *args, **kwargs):
# 检查环境变量以确定是否使用代理
proxy = os.environ.get('HTTP_PROXY') or os.environ.get('HTTPS_PROXY') or os.environ.get('http_proxy') or os.environ.get('https_proxy')
# print('Proxy Config:',proxy)
if proxy and 'proxy' not in kwargs:
kwargs['proxy'] = proxy
print('Use Proxy:',proxy)
# 调用原始的 _request 方法
return await _original_request(self, method, url, *args, **kwargs)
# 应用 Monkey Patch
aiohttp.ClientSession._request = new_request
# https
async def new_start(self, address, port, verbose=True, call_on_start=None):
runner = web.AppRunner(self.app, access_log=None)
await runner.setup()
site = web.TCPSite(runner, address, port)
@@ -152,13 +244,51 @@ routes = web.RouteTableDef()
async def mixlab_hander(request):
config=os.path.join(current_path, "nodes/config.json")
data={}
# print(config)
if os.path.exists(config):
with open(config, 'r') as f:
data = json.load(f)
# print(data)
try:
if os.path.exists(config):
with open(config, 'r') as f:
data = json.load(f)
# print(data)
except Exception as e:
print(e)
return web.json_response(data)
@routes.post('/mixlab/workflow')
async def mixlab_workflow_hander(request):
data = await request.json()
result={}
try:
if 'task' in data:
if data['task']=='save':
file_path=save_workflow_json(data['data'])
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='list':
result={
'data':get_workflows(),
'status':'success',
}
except Exception as e:
print(e)
return web.json_response(result)
@routes.post('/mixlab/nodes_map')
async def nodes_map_hander(request):
data = await request.json()
result={}
try:
result={
'data':get_nodes_map(),
'status':'success',
}
except Exception as e:
print(e)
return web.json_response(result)
def new_add_routes(self):
import nodes
self.app.add_routes(routes)
@@ -188,18 +318,29 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import TransparentImage,LoadImagesFromPath,ResizeImage,TextImage,SvgImage,Image3D,EmptyLayer,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
NODE_CLASS_MAPPINGS = {
"RandomPrompt":RandomPrompt,
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
"3DImage":Image3D,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"MergeLayers":MergeLayers,
"SplitLongMask":SplitLongMask,
"FeatheredMask":FeatheredMask,
"SmoothMask":SmoothMask,
@@ -210,18 +351,38 @@ NODE_CLASS_MAPPINGS = {
"VAEDecodeConsistencyDecoder":VAEDecode,
"ScreenShare":ScreenShareNode,
"FloatingVideo":FloatingVideo,
"CLIPSeg":CLIPSeg,
"CombineMasks":CombineMasks
"CLIPSeg_":CLIPSeg,
"CombineMasks_":CombineMasks,
"ChatGPTOpenAI":ChatGPTNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText,
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"Font":FontInput,
"TextToNumber":TextToNumber,
"DynamicDelayProcessor":DynamicDelayProcessor
# "GamePal":GamePal
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS = {
"RandomPrompt": "Random Prompt #Example Node",
"ResizeImageMixlab":"ResizeImage",
"RandomPrompt": "Random Prompt ♾️Mixlab",
"SplitLongMask":"Splitting a long image into sections",
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode",
"ScreenShare":"ScreenShare #Mixlab",
"FloatingVideo":"FloatingVideo #Mixlab"
"ScreenShare":"ScreenShare ♾️Mixlab",
"FloatingVideo":"FloatingVideo ♾️Mixlab",
"ChatGPTOpenAI":"ChatGPT ♾️Mixlab",
"ShowTextForGPT":"ShowTextForGPT ♾️Mixlab",
"MergeLayers":"MergeLayers ♾️Mixlab",
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab"
# "GamePal":"GamePal ♾️Mixlab"
}
# web ui的节点功能
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
class SpeechRecognition:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("AUDIOINPUTMIX",), },
"optional":{
"start_by":("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 2048, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/audio"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,upload,start_by):
return {"ui": {"start_by": [start_by]}, "result": (upload,)}
class SpeechSynthesis:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "♾️Mixlab/audio"
def run(self, text):
# print(session_history)
return {"ui": {"text": text}, "result": (text,)}
#
class GamePal:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_text": ("STRING",{"multiline": True,"default": ""}),
},
"optional": {
"input_num": ("INT",{
"default":100,
"min": -1, #Minimum value
"max": 0xffffffffffffffff, #Maximum value
"step": 1, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
"python_code": ("STRING",{"multiline": True,"default": "result= 1 if 'Mixlab' in input_text else 0"}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("INT",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,)
CATEGORY = "♾️Mixlab/audio"
def run(self, input_text,input_num,python_code):
exec(python_code)
res=None
try:
# 可能会引发异常的代码
res=result
except:
# 处理异常的代码
print('')
print(res)
# print(session_history)
return {"ui": {"text": [input_text],"num":[input_num]}, "result": (res,)}
+217
View File
@@ -0,0 +1,217 @@
import openai
import time
import urllib.error
import re,json
# 判断是否是azure服务
def is_azure_url(url):
pattern = r'.*\.azure\.com$'
if re.match(pattern, url):
return True
else:
return False
def azure_client(key,url):
client = openai.AzureOpenAI(
api_key=key,
# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning
api_version="2023-07-01-preview",
# https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
azure_endpoint=url
)
return client
def openai_client(key,url):
client = openai.OpenAI(
api_key=key,
base_url=url
)
return client
def chat(client, model_name,messages ):
try_count = 0
while True:
try_count += 1
try:
response = client.chat.completions.create(
model=model_name,
messages=messages
)
break
except openai.AuthenticationError as ex:
raise ex
except (urllib.error.HTTPError, openai.OpenAIError) as ex:
if try_count >= 3:
raise ex
time.sleep(3)
continue
finish_reason = response.choices[0].finish_reason
if finish_reason != "stop":
raise RuntimeError("API finished with unexpected reason: " + finish_reason)
content=""
try:
content=response.choices[0].message.content
except:
content=response.choices[0].delta['content']
return content
class ChatGPTNode:
def __init__(self):
# self.__client = OpenAI()
self.session_history = [] # 用于存储会话历史的列表
# self.seed=0
self.system_content="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key":("KEY", {"default": "", "multiline": True}),
"api_url":("URL", {"default": "", "multiline": True}),
"prompt": ("STRING", {"multiline": True}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True
}),
"model": (["gpt-3.5-turbo","gpt-35-turbo","gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-4-0613","gpt-4-1106-preview"],
{"default": "gpt-3.5-turbo"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
RETURN_NAMES = ("text","messages","session_history",)
FUNCTION = "generate_contextual_text"
CATEGORY = "♾️Mixlab/GPT"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,False,)
def generate_contextual_text(self,
api_key,
api_url,
prompt,
system_content,
model,
seed,context_size,unique_id = None, extra_pnginfo=None):
# print(api_key!='',api_url,prompt,system_content,model,seed)
# 可以选择保留会话历史以维持上下文记忆
# 或者在此处清除会话历史 self.session_history.clear()
# if seed!=self.seed:
# self.seed=seed
# self.session_history=[]
# 把系统信息和初始信息添加到会话历史中
if system_content:
self.system_content=system_content
# self.session_history=[]
# self.session_history.append({"role": "system", "content": system_content})
#
if is_azure_url(api_url):
client=azure_client(api_key,api_url)
else:
client=openai_client(api_key,api_url)
print('openai url')
# 把用户的提示添加到会话历史中
# 调用API时传递整个会话历史
def crop_list_tail(lst, size):
if size >= len(lst):
return lst
elif size==0:
return []
else:
return lst[-size:]
session_history=crop_list_tail(self.session_history,context_size)
messages=[{"role": "system", "content": self.system_content}]+session_history+[{"role": "user", "content": prompt}]
response_content = chat(client,model,messages)
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
# if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
# workflow = extra_pnginfo[0]["workflow"]
# node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
# if node:
# node["widgets_values"] = ["",
# api_url,
# prompt,
# system_content,
# model,
# seed,
# context_size]
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
class ShowTextForGPT:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "♾️Mixlab/GPT"
def run(self, text):
# print(session_history)
return {"ui": {"text": text}, "result": (text,)}
class CharacterInText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True}),
"character": ("STRING", {"multiline": True}),
"start_index": ("INT", {
"default": 1,
"min": 0, #Minimum value
"max": 1024, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("INT",)
FUNCTION = "run"
# OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,)
CATEGORY = "♾️Mixlab/GPT"
def run(self, text,character,start_index):
# print(text,character,start_index)
b=1 if character in text else 0
return (b+start_index,)
+9 -6
View File
@@ -1,3 +1,6 @@
#### Thanks:
# [ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
from PIL import Image
@@ -99,7 +102,7 @@ class CLIPSeg:
}
}
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
RETURN_TYPES = ("MASK", "IMAGE", "IMAGE",)
RETURN_NAMES = ("Mask","Heatmap Mask", "BW Mask")
@@ -204,7 +207,7 @@ class CombineMasks:
},
}
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
RETURN_TYPES = ("MASK", "IMAGE", "IMAGE",)
RETURN_NAMES = ("Combined Mask","Heatmap Mask", "BW Mask")
@@ -252,7 +255,7 @@ class CombineMasks:
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"CLIPSeg": CLIPSeg,
"CombineSegMasks": CombineMasks,
}
# NODE_CLASS_MAPPINGS = {
# "CLIPSeg": CLIPSeg,
# "CombineSegMasks": CombineMasks,
# }
+778 -52
View File
@@ -1,17 +1,18 @@
import numpy as np
import torch
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
import base64,os
from io import BytesIO
import folder_paths
import json
import json,io
from comfy.cli_args import args
import cv2
from .Watcher import FolderWatcher
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
MAX_RESOLUTION=8192
@@ -153,25 +154,48 @@ def get_not_transparent_area(image):
return (x, y, w, h)
# 读取不了分层
def load_psd(image):
layers=[]
print('load_psd',image.format)
if image.format=='PSD':
layers = [frame.copy() for frame in ImageSequence.Iterator(image)]
print('#PSD',len(layers))
else:
image = ImageOps.exif_transpose(image) #校对方向
layers.append(image)
return layers
def load_image(fp,white_bg=False):
i = Image.open(fp)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
if white_bg==True:
nw = mask.unsqueeze(0).unsqueeze(-1).repeat(1, 1, 1, 3)
# 将mask的黑色部分对image进行白色处理
image[nw == 1] = 1.0
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
return (image,mask)
im = Image.open(fp)
# ims=load_psd(im)
im = ImageOps.exif_transpose(im) #校对方向
ims=[im]
images=[]
for i in ims:
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
if white_bg==True:
nw = mask.unsqueeze(0).unsqueeze(-1).repeat(1, 1, 1, 3)
# 将mask的黑色部分对image进行白色处理
image[nw == 1] = 1.0
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
images.append({
"image":image,
"mask":mask
})
return images
# 获取图片s
@@ -183,23 +207,27 @@ def get_images_filepath(f,white_bg=False):
for file in files:
file_path = os.path.join(root, file)
try:
(im,mask)=load_image(file_path,white_bg)
images.append({
"image":im,
"mask":mask,
"file_path":file_path
})
imgs=load_image(file_path,white_bg)
for img in imgs:
images.append({
"image":img['image'],
"mask":img['mask'],
"file_path":file_path,
"psd":len(imgs)>1
})
except:
print('非图片',file_path)
elif os.path.isfile(f):
try:
(im,mask)=load_image(f,white_bg)
images.append({
"image":im,
"mask":mask,
"file_path":f
})
imgs=load_image(f,white_bg)
for img in imgs:
images.append({
"image":img['image'],
"mask":img['mask'],
"file_path":file_path,
"psd":len(imgs)>1
})
except:
print('非图片',f)
else:
@@ -295,6 +323,227 @@ def areaToMask(x,y,w,h,image):
return mask
# def merge_images(bg_image, layer_image,mask, x, y, width, height):
# # 打开底图
# # bg_image = Image.open(background)
# bg_image=bg_image.convert("RGBA")
# # 打开图层
# layer_image=layer_image.convert("RGBA")
# layer_image = layer_image.resize((width, height))
# # mask = Image.new("L", layer_image.size, 255)
# mask = mask.resize((width, height))
# # 在底图上粘贴图层
# bg_image.paste(layer_image, (x, y), mask=mask)
# # 输出合成后的图片
# # bg_image.save("output.jpg")
# return bg_image
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option):
# 打开底图
bg_image = bg_image.convert("RGBA")
# 打开图层
layer_image = layer_image.convert("RGBA")
# layer_image = layer_image.resize((width, height))
# 根据缩放选项调整图像大小
if scale_option == "height":
# 按照高度比例缩放
original_width, original_height = layer_image.size
scale = height / original_height
new_width = int(original_width * scale)
layer_image = layer_image.resize((new_width, height))
elif scale_option == "width":
# 按照宽度比例缩放
original_width, original_height = layer_image.size
scale = width / original_width
new_height = int(original_height * scale)
layer_image = layer_image.resize((width, new_height))
elif scale_option == "overall":
# 整体缩放
layer_image = layer_image.resize((width, height))
# 调整mask的大小
nw, nh = layer_image.size
mask = mask.resize((nw, nh))
# 在底图上粘贴图层
bg_image.paste(layer_image, (x, y), mask=mask)
# 输出合成后的图片
return bg_image
def resize_image(layer_image,scale_option,width,height):
layer_image = layer_image.convert("RGB")
if scale_option == "height":
# 按照高度比例缩放
original_width, original_height = layer_image.size
scale = height / original_height
new_width = int(original_width * scale)
layer_image = layer_image.resize((new_width, height))
elif scale_option == "width":
# 按照宽度比例缩放
original_width, original_height = layer_image.size
scale = width / original_width
new_height = int(original_height * scale)
layer_image = layer_image.resize((width, new_height))
elif scale_option == "overall":
# 整体缩放
layer_image = layer_image.resize((width, height))
return layer_image
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
# # Load Chinese font
# font = ImageFont.truetype(font_path, font_size)
# # Calculate image size based on the number of characters and orientation
# if vertical:
# width = font_size + 100
# height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# else:
# width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# height = font_size + 100
# # Create a blank image
# image = Image.new('RGBA', (width, height), (255, 255, 255,0))
# draw = ImageDraw.Draw(image)
# # Draw text
# if vertical:
# for i, char in enumerate(text_list):
# char_position = (50, 50 + i * font_size)
# draw.text(char_position, char, font=font, fill=text_color)
# else:
# for i, char in enumerate(text_list):
# char_position = (50 + i * (font_size + spacing), 50)
# draw.text(char_position, char, font=font, fill=text_color)
# # Save the image
# # image.save(output_image_path)
# # 分离alpha通道
# alpha_channel = image.split()[3]
# # 创建一个只有alpha通道的新图像
# alpha_image = Image.new('L', image.size)
# alpha_image.putdata(alpha_channel.getdata())
# image=image.convert('RGB')
# return (image,alpha_image)
def generate_text_image(text, font_path, font_size, text_color, vertical=True, spacing=0):
# Split text into lines based on line breaks
lines = text.split("\n")
# 1. Determine layout direction
if vertical:
layout = "vertical"
else:
layout = "horizontal"
# 2. Calculate absolute coordinates for each character
char_coordinates = []
if layout == "vertical":
x = 0
y = 0
for i in range(len(lines)):
line=lines[i]
for char in line:
char_coordinates.append((x, y))
y += font_size + spacing
x += font_size + spacing
y = 0
print(char_coordinates)
else:
x = 0
y = 0
for line in lines:
for char in line:
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
x = 0
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = (len(lines) * (font_size + spacing)) - spacing
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255,0))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(font_path, font_size)
index=0
for i, line in enumerate(lines):
for j, char in enumerate(line):
x, y = char_coordinates[index]
draw.text((x, y), char, font=font, fill=text_color)
index+=1
# image.save(output_image_path)
# 分离alpha通道
alpha_channel = image.split()[3]
# 创建一个只有alpha通道的新图像
alpha_image = Image.new('L', image.size)
alpha_image.putdata(alpha_channel.getdata())
image=image.convert('RGB')
return (image,alpha_image)
def base64_to_image(base64_string):
# 去除前缀
prefix, base64_data = base64_string.split(",", 1)
# 从base64字符串中解码图像数据
image_data = base64.b64decode(base64_data)
# 创建一个内存流对象
image_stream = io.BytesIO(image_data)
# 使用PIL的Image模块打开图像数据
image = Image.open(image_stream)
return image
def create_temp_file(image):
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('material', 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"
}]
class SmoothMask:
@classmethod
@@ -314,7 +563,7 @@ class SmoothMask:
FUNCTION = "run"
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
INPUT_IS_LIST = False
@@ -362,7 +611,7 @@ class FeatheredMask:
FUNCTION = "run"
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
OUTPUT_IS_LIST = (False,)
@@ -429,7 +678,7 @@ class SplitLongMask:
FUNCTION = "run"
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
OUTPUT_IS_LIST = (True,)
@@ -467,12 +716,13 @@ class TransparentImage:
}
RETURN_TYPES = ('STRING','IMAGE','RGBA')
RETURN_NAMES = ("file_path","IMAGE","RGBA",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
# INPUT_IS_LIST = True, 一个batch传进来
OUTPUT_IS_LIST = (True,True,True,)
@@ -519,7 +769,7 @@ class TransparentImage:
# ui.images 节点里显示图片,和 传参,image_path自定义的数据,需要写节点的自定义ui
# result 里输出给下个节点的数据
print('TransparentImage',len(images_rgb))
# print('TransparentImage',len(images_rgb))
return {"ui":{"images": ui_images,"image_paths":image_paths},"result": (image_paths,images_rgb,images_rgba)}
@@ -541,7 +791,7 @@ class EnhanceImage:
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
@@ -583,43 +833,45 @@ class LoadImagesFromPath:
"white_bg": (["disable","enable"],),
"newest_files": (["enable", "disable"],),
"index_variable":("INT", {
"default": -1,
"default": 0,
"min": -1, #Minimum value
"max": 2048, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"watcher":(["disable","enable"],),
"result": ("WATCHER",),
"result": ("WATCHER",),#为了激活本节点运行
"prompt": ("PROMPT",),
# "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ('IMAGE','MASK',)
RETURN_TYPES = ('IMAGE','MASK','STRING')
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
# INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,True,)
OUTPUT_IS_LIST = (True,True,False,)
global watcher_folder
watcher_folder=None
# 运行的函数
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result):
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result,prompt):
global watcher_folder
# print('###监听:',watcher_folder,watcher,file_path,result)
if watcher=='enable':
if watcher_folder==None:
watcher_folder = FolderWatcher(file_path)
if watcher_folder==None:
watcher_folder = FolderWatcher(file_path)
watcher_folder.set_folder_path(file_path)
if watcher=='enable':
# 在这里可以进行其他操作,监听会在后台持续
watcher_folder.set_folder_path(file_path)
watcher_folder.start()
else:
if watcher_folder!=None:
watcher_folder.stop()
@@ -642,8 +894,8 @@ class LoadImagesFromPath:
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
masks=[masks[index_variable]] if index_variable < len(masks) else None
return (imgs,masks,)
# print('#prompt::::',prompt)
return (imgs,masks,prompt,)
# TODO 扩大选区的功能,重新输出mask
@@ -659,7 +911,7 @@ class ImageCropByAlpha:
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
@@ -679,6 +931,140 @@ class ImageCropByAlpha:
return (img,)
class TextImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲"}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH}),
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
"max": 1000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"spacing": ("INT",{
"default":12,
"min": 1, #Minimum value
"max": 200, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"text_color":("STRING",{"multiline": False,"default": "#000000"}),
"vertical":("BOOLEAN", {"default": True},),
},
}
RETURN_TYPES = ("IMAGE","MASK")
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,text,font_path,font_size,spacing,text_color,vertical):
text_list=list(text)
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,spacing)
img=pil2tensor(img)
mask=pil2tensor(mask)
return (img,mask,)
class SvgImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("SVG",),},
}
RETURN_TYPES = ("IMAGE","LAYER")
RETURN_NAMES = ("IMAGE","layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,True,)
def run(self,upload):
layers=[]
image = base64_to_image(upload['image'])
image=image.convert('RGB')
image=pil2tensor(image)
for layer in upload['data']:
layers.append(layer)
return (image,layers,)
class Image3D:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("THREED",),},
"optional":{
"material": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE","MASK","IMAGE","IMAGE",)
RETURN_NAMES = ("IMAGE","MASK","BG_IMAGE","MATERIAL",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,False,False,)
OUTPUT_NODE = True
def run(self,upload,material=None):
# print('material',material)
# print(upload['image'])
image = base64_to_image(upload['image'])
mat=base64_to_image(upload['material'])
mask = image.split()[3]
image=image.convert('RGB')
mat=mat.convert('RGB')
mask=mask.convert('L')
bg_image=None
if upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mask=pil2tensor(mask)
image=pil2tensor(image)
mat=pil2tensor(mat)
m=[]
if not material is None:
m=create_temp_file(material[0])
return {"ui":{"material": m},"result": (image,mask,bg_image,mat,)}
class AreaToMask:
@classmethod
def INPUT_TYPES(s):
@@ -690,7 +1076,7 @@ class AreaToMask:
FUNCTION = "run"
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
@@ -725,7 +1111,7 @@ class FaceToMask:
FUNCTION = "run"
CATEGORY = "Mixlab/mask"
CATEGORY = "♾️Mixlab/mask"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
@@ -741,3 +1127,343 @@ class FaceToMask:
return (mask,)
class EmptyLayer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"width": ("INT",{
"default":512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"height": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("LAYER",)
RETURN_NAMES = ("layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
OUTPUT_IS_LIST = (True,)
def run(self, width,height):
blank_image = Image.new("RGB", (width, height))
mask=blank_image.convert('L')
blank_image=pil2tensor(blank_image)
mask=pil2tensor(mask)
layer_n=[{
"x":0,
"y":0,
"width":width,
"height":height,
"z_index":0,
"scale_option":'width',
"image":blank_image,
"mask":mask
}]
return (layer_n,)
class NewLayer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"x": ("INT",{
"default": 0,
"min": -1024, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"y": ("INT",{
"default": 0,
"min": -1024, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"width": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"height": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"z_index": ("INT",{
"default": 0,
"min":0, #Minimum value
"max": 100, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
"image": ("IMAGE",),
},
"optional":{
"mask": ("MASK",{"default": None}),
"layers": ("LAYER",{"default": None}),
"canvas": ("IMAGE",{"default": None}),
}
}
RETURN_TYPES = ("LAYER",)
RETURN_NAMES = ("layers",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self,x,y,width,height,z_index,scale_option,image,mask=None,layers=None,canvas=None):
# print(x,y,width,height,z_index,image,mask)
if mask==None:
im=tensor2pil(image[0])
mask=im.convert('L')
mask=pil2tensor(mask)
else:
mask=mask[0]
layer_n=[{
"x":x[0],
"y":y[0],
"width":width[0],
"height":height[0],
"z_index":z_index[0],
"scale_option":scale_option[0],
"image":image[0],
"mask":mask
}]
if layers!=None:
layer_n=layer_n+layers
return (layer_n,)
class ShowLayer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"edit": ("EDIT",),
"x": ("INT",{
"default": 0,
"min": -100, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"y": ("INT",{
"default": 0,
"min": 0, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"width": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"height": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"z_index": ("INT",{
"default": 0,
"min":0, #Minimum value
"max": 100, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
# "image": ("IMAGE",),
},
"optional":{
# "mask": ("MASK",{"default": None}),
"layers": ("LAYER",{"default": None}),
}
}
RETURN_TYPES = ( )
RETURN_NAMES = ( )
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
def run(self,edit,x,y,width,height,z_index,scale_option,layers):
# print(x,y,width,height,z_index,image,mask)
# if mask==None:
# im=tensor2pil(image)
# mask=im.convert('L')
# mask=pil2tensor(mask)
# else:
# mask=mask[0]
# layers[edit[0]]={
# "x":x[0],
# "y":y[0],
# "width":width[0],
# "height":height[0],
# "z_index":z_index[0],
# "scale_option":scale_option[0],
# "image":image[0],
# "mask":mask
# }
return ( )
class MergeLayers:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"layers": ("LAYER",),
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/layer"
INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (False,)
def run(self,layers,image):
# print(len(layers),len(image))
bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
return (bg_image,mask,)
class ResizeImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"width": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"height": ("INT",{
"default": 512,
"min": 1, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"scale_option": (["width","height",'overall'],),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,)
def run(self,image,width,height,scale_option):
w=width[0]
h=height[0]
scale_option=scale_option[0]
im=image[0]
im=tensor2pil(im)
im=resize_image(im,scale_option,w,h)
im=im.convert('RGB')
im=pil2tensor(im)
return (im,)
+2 -2
View File
@@ -79,7 +79,7 @@ class RandomPrompt:
FUNCTION = "run"
CATEGORY = "Mixlab/prompt"
CATEGORY = "♾️Mixlab/prompt"
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
@@ -158,7 +158,7 @@ class RunWorkflow:
FUNCTION = "run"
CATEGORY = "Mixlab/workflow"
CATEGORY = "♾️Mixlab/workflow"
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
+57 -8
View File
@@ -24,9 +24,42 @@ def base64_save(base64_data):
return (image,mask)
# # 把白色部分处理成黑色
# def convert_to_bw(image):
# # 读取图片
# # image = Image.open(image_path)
# # 获取图片的宽度和高度
# width, height = image.size
# # 遍历图片的每个像素点
# for x in range(width):
# for y in range(height):
# # 获取当前像素点的RGB值
# r, g, b = image.getpixel((x, y))
# # 判断当前像素点是否为白色
# if r == 255 and g == 255 and b == 255:
# # 将白色部分处理成黑色
# image.putpixel((x, y), (0, 0, 0))
# else:
# # 将非白色部分处理成白色
# image.putpixel((x, y), (255, 255, 255))
# # 转换为黑白图
# mask = image.convert("L")
# # # 保存处理后的图片
# # image.save("black_white_image.jpg")
# # print("图片处理完成!")
# return mask
def load_image(i,white_bg=False):
# i = Image.open(fp)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
@@ -46,27 +79,31 @@ class ScreenShareNode:
def INPUT_TYPES(s):
return { "required":{
"image_base64": ("CHEESE",),
"refresh_rate": ("INT", {"default": 500, "min": 0,"step": 50, "max": 0xffffffffffffffff}),
},
"optional":{
"prompt": ("PROMPT",),
"slide": ("SLIDE",),
"seed": ("SEED",),
# "seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
} }
RETURN_TYPES = ('IMAGE','MASK','STRING')
RETURN_TYPES = ('IMAGE','STRING','FLOAT',"INT")
RETURN_NAMES = ("IMAGE","PROMPT","FLOAT","INT")
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
# INPUT_IS_LIST = True
OUTPUT_IS_LIST = (False,False,False)
OUTPUT_IS_LIST = (False,False,False,False)
# 运行的函数
def run(self,image_base64,prompt):
def run(self,image_base64,refresh_rate ,prompt,slide,seed):
im,mask=base64_save(image_base64)
# print('##########prompt',prompt)
return (im,mask,prompt)
return {"ui":{"refresh_rate": [refresh_rate]},"result": (im,prompt,slide,seed,)}
class FloatingVideo:
@classmethod
@@ -81,7 +118,7 @@ class FloatingVideo:
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "Mixlab/image"
CATEGORY = "♾️Mixlab/image"
# INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (False,False,)
@@ -104,3 +141,15 @@ class FloatingVideo:
return { "ui": { "images_": results } }
# class SildeNode:
# CATEGORY = "quicknodes"
# @classmethod
# def INPUT_TYPES(s):
# return { "required":{} }
# RETURN_TYPES = ()
# RETURN_NAMES = ()
# FUNCTION = "func"
# def func(self):
# return ()
+212
View File
@@ -0,0 +1,212 @@
import os
import re,random
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
import matplotlib.font_manager as fm
def get_font_files(directory):
font_files = {}
# 从指定目录加载字体
for file in os.listdir(directory):
if file.endswith('.ttf') or file.endswith('.otf'):
font_name = os.path.splitext(file)[0]
font_path = os.path.join(directory, file)
font_files[font_name] = os.path.abspath(font_path)
# 尝试获取系统字体
try:
font_paths = fm.findSystemFonts()
for path in font_paths:
try:
font_prop = fm.FontProperties(fname=path)
font_name = font_prop.get_name()
font_files[font_name] = path
except Exception as e:
print(f"Error processing font {path}: {e}")
except Exception as e:
print(f"Error finding system fonts: {e}")
return font_files
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
font_files = get_font_files(r_directory)
# print(font_files)
class ColorInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"color":("TCOLOR",),
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,color):
return (color,)
class FontInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"font": (list(font_files.keys()),),
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,font):
return (font_files[font],)
class TextToNumber:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING",{"multiline": False,"default": "1"}),
"random_number": (["enable", "disable"],),
"number":("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 10000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
},
}
RETURN_TYPES = ("INT",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/utils"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,text,random_number,number):
numbers = re.findall(r'\d+', text)
result=0
for n in numbers:
result = int(n)
# print(result)
if random_number=='enable' and result>0:
result= random.randint(1, 10000000000)
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
import folder_paths
# import time
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
import time
class DynamicDelayProcessor:
@classmethod
def INPUT_TYPES(cls):
print("print INPUT_TYPES",cls)
return {
"required":{
"delay_seconds":("INT",{
"default":1,
"min": 0,
"max": 1000000,
}),
},
"optional":{
"any_input":(any_type,),
"delay_by_text":("STRING",{"multiline":True,}),
"words_per_seconds":("FLOAT",{ "default":1.50,"min": 0.0,"max": 1000.00,"display":"Chars per second?"}),
"replace_output": (["disable","enable"],),
"replace_value":("INT",{ "default":-1,"min": 0,"max": 1000000,"display":"Replacement value"})
}
}
@classmethod
def calculate_words_length(cls,text):
chinese_char_pattern = re.compile(r'[\u4e00-\u9fff]')
english_word_pattern = re.compile(r'\b[a-zA-Z]+\b')
number_pattern = re.compile(r'\b[0-9]+\b')
words_length = 0
for segment in text.split():
if chinese_char_pattern.search(segment):
# 中文字符,每个字符计为 1
words_length += len(segment)
elif number_pattern.match(segment):
# 数字,每个字符计为 1
words_length += len(segment)
elif english_word_pattern.match(segment):
# 英文单词,整个单词计为 1
words_length += 1
return words_length
FUNCTION = "run"
RETURN_TYPES = (any_type,)
RETURN_NAMES = ('output',)
CATEGORY = "♾️Mixlab/utils"
def run(self,any_input,delay_seconds,delay_by_text,words_per_seconds,replace_output,replace_value):
# print(f"Delay text:",delay_by_text )
# 获取开始时间戳
start_time = time.time()
# 计算延迟时间
delay_time = delay_seconds
if delay_by_text and isinstance(delay_by_text, str) and words_per_seconds > 0:
words_length = self.calculate_words_length(delay_by_text)
print(f"Delay text: {delay_by_text}, Length: {words_length}")
delay_time += words_length / words_per_seconds
# 延迟执行
print(f"延迟执行: {delay_time}")
time.sleep(delay_time)
# 获取结束时间戳并计算间隔
end_time = time.time()
elapsed_time = end_time - start_time
print(f"实际延迟时间: {elapsed_time} 秒")
# 根据 replace_output 决定输出值
return (max(0, replace_value),) if replace_output == "enable" else (any_input,)
+2 -2
View File
@@ -145,7 +145,7 @@ class VAELoader:
RETURN_TYPES = ("VAE",)
FUNCTION = "load_vae"
CATEGORY = "Mixlab/ConsistencyDecoder"
CATEGORY = "♾️Mixlab/_test"
#TODO: scale factor?
def load_vae(self, vae_name):
@@ -165,7 +165,7 @@ class VAEDecode:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "decode"
CATEGORY = "Mixlab/ConsistencyDecoder"
CATEGORY = "♾️Mixlab/_test"
def decode(self, vae, samples):
image = vae.decode(samples["samples"].to("cuda:0"))
+27 -17
View File
@@ -8,25 +8,34 @@ import os
# print('Watcher:',current_directory)
def save_to_json(file_path, data):
with open(file_path, 'w') as f:
json.dump(data, f)
try:
with open(file_path, 'w') as f:
json.dump(data, f)
except Exception as e:
print(e)
def read_from_json(file_path):
with open(file_path, 'r') as f:
data = json.load(f)
data={}
try:
with open(file_path, 'r') as f:
data = json.load(f)
except Exception as e:
print(e)
return data
# read_from_json()
current_path = os.path.abspath(os.path.dirname(__file__))
config_json=os.path.join(current_path,'config.json')
print('Watcher:',config_json)
# print('Watcher:',config_json)
def read_config():
config={}
if os.path.exists(config_json):
# print('exists')
config=read_from_json(config_json)
try:
if os.path.exists(config_json):
config=read_from_json(config_json)
except Exception as e:
print(e)
return config
@@ -39,10 +48,10 @@ class FolderWatcher:
config['folder_path']=folder_path
save_to_json(config_json,config)
# self.observer = Observer()
self.observer = None
self.event_handler = self._create_event_handler()
self.status = "Not started"
self.event_type=''
self.event_type='-'
def _create_event_handler(self):
@@ -77,22 +86,23 @@ class FolderWatcher:
config=read_config()
config['folder_path']=new_folder_path
save_to_json(config_json,config)
self.event_type=''
self.event_type='-'
def start(self):
self.observer = Observer()
self.observer.schedule(self.event_handler, self.folder_path, recursive=True)
self.observer.start()
self.status = "Listening"
self.event_type=''
self.event_type='-'
print('Listening')
def stop(self):
self.observer.stop()
self.observer.join()
self.observer=None
self.status = "Stopped"
self.event_type=''
if self.observer!=None:
self.observer.stop()
self.observer.join()
self.observer=None
self.status = "Stopped"
self.event_type='-'
print('Stopped')
+4 -1
View File
@@ -1,3 +1,6 @@
numpy
pyOpenSSL
watchdog
watchdog
opencv-python-headless
matplotlib
openai
+608
View File
@@ -0,0 +1,608 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function getContentTypeFromBase64 (base64Data) {
const regex = /^data:(.+);base64,/
const matches = base64Data.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
}
return null
}
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
const parseImage = url => {
return new Promise((res, rej) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result
res(base64data)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
.catch(error => {
console.log('发生错误:', error)
})
})
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
async function extractMaterial (
modelViewerVariants,
selectMaterial,
material_img
) {
// 材质
const materialsNames = []
for (
let index = 0;
index < modelViewerVariants.model.materials.length;
index++
) {
let m = modelViewerVariants.model.materials[index]
let thumbUrl
try {
thumbUrl =
await m.pbrMetallicRoughness.baseColorTexture.texture.source.createThumbnail(
1024,
1024
)
} catch (error) {}
if (thumbUrl)
materialsNames.push({
value: m.name,
text: `#${index} ${m.name}`,
index,
thumbUrl
})
}
selectMaterial.innerHTML = ''
material_img.innerHTML = ''
for (let index = 0; index < materialsNames.length; index++) {
const name = materialsNames[index]
const option = document.createElement('option')
option.value = name.thumbUrl
option.textContent = name.text
option.setAttribute('data-index', index)
selectMaterial.appendChild(option)
let img = new Image()
img.src = name.thumbUrl
// img.setAttribute('data-index',name.index)
img.style.width = '40px'
material_img.appendChild(img)
if (index == 0) {
material_img.setAttribute('src', name.thumbUrl)
}
}
}
async function changeMaterial (
modelViewerVariants,
targetMaterial,
newImageUrl
) {
const targetTexture = await modelViewerVariants.createTexture(newImageUrl)
// 用图片创建纹理
targetMaterial.pbrMetallicRoughness.baseColorTexture.setTexture(targetTexture)
}
app.registerExtension({
name: 'Mixlab.3D.3DImage',
async getCustomWidgets (app) {
return {
THREED (node, inputName, inputData, app) {
// console.log('##node', node, inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 88], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 88] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let base64 = await parseImage(url)
let bg_base64 = await parseImage(bg)
let material_base64 = await parseImage(material)
return JSON.parse(
JSON.stringify({
image: base64,
bg_image: bg_base64,
material: material_base64
})
)
} else {
return {}
}
}
}
node.addCustomWidget(widget)
return widget
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == '3DImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 88, node.size[1])
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
ip.addEventListener('change', async event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
touch-action="pan-y">
<div class="controls">
<div>Variant: <select class="variant"></select></div>
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
// 材质贴图
let 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')
if (!dd[that.id])
dd[that.id] = { url, bg: url_bg, material: tUrl }
dd[that.id] = { ...dd[that.id], url, material: tUrl }
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute(
'data-index'
)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
}
// 以文本形式读取文件
reader.readAsDataURL(file)
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
widget.div.appendChild(upload)
widget.div.appendChild(preview)
this.addCustomWidget(widget)
const onResize = this.onResize
let that = this
this.onResize = function () {
let modelViewerVariants = preview.querySelector('model-viewer')
// 更新尺寸
let dd = getLocalData('_mixlab_3d_image')
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
return onResize?.apply(this, arguments)
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
preview.remove()
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
// this.isVirtualNode = true
this.serialize_widgets = false //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const r = onExecuted?.apply?.(this, arguments)
let div = this.widgets.filter(d => d.div)[0]?.div
console.log('Test', this.widgets)
let material = message.material[0]
if (material) {
const { filename, subfolder, type } = material
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
filename
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
const modelViewerVariants = div.querySelector('model-viewer')
const selectMaterial = div.querySelector('.material')
let index =
~~selectMaterial.selectedOptions[0].getAttribute('data-index')
selectMaterial.setAttribute('data-new-material', src)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
src
)
}
this.onResize?.(this.size)
return r
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
if (node.type === '3DImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'upload-preview')[0]
let dd = getLocalData('_mixlab_3d_image')
let id = node.id
// console.log('3dImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let { url, bg } = dd[id]
if (!url) return
// let base64 = await parseImage(url)
let pre = widget.div.querySelector('.preview')
pre.style.width = `${node.size[0]}px`
pre.innerHTML = `
${url ? `<img src="${url}" style="width:100%"/>` : ''}
`
pre.style.backgroundImage = 'url(' + bg + ')'
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
}
}
})
+398
View File
@@ -0,0 +1,398 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function speakText (text) {
const speechMsg = new SpeechSynthesisUtterance()
speechMsg.text = text
// 语音合成结束时触发的事件
speechMsg.onend = function (event) {
console.log('语音播放结束')
window._mixlab_speech_synthesis_onend = true
}
// 语音合成错误时触发的事件
speechMsg.onerror = function (event) {
console.error('语音播放错误:', event.error)
}
// 使用浏览器默认语音合成器进行语音播放
speechSynthesis.speak(speechMsg)
}
// 调用方法,将文字转换为语音播放
// speakText('Hello, how are you?');
// #MixCopilot
const start = (element, id, startBtn, node) => {
startBtn.className = 'loading_mixlab'
window.recognition = new webkitSpeechRecognition()
window.recognition.continuous = true
window.recognition.interimResults = true
window.recognition.lang = navigator.language
let timeoutId, intervalId
window.recognition.onstart = () => {
console.log('开始语音输入', window._mixlab_speech_synthesis_onend)
window._mixlab_speech_synthesis_onend = false
}
window.recognition.onresult = function (event) {
const result = event.results[event.results.length - 1][0].transcript
console.log('识别结果:', result)
element.value = result
let data = getLocalData('_mixlab_speech_recognition')
data[id] = result.trim()
localStorage.setItem('_mixlab_speech_recognition', JSON.stringify(data))
if (timeoutId) clearTimeout(timeoutId)
if (!window.recognition) return
timeoutId = setTimeout(function () {
console.log('结果传递::', result)
// 把数据发送到chatgpt的输入prompt里
try {
const sendToId = node.widgets.filter(
w => w.name === 'Send to ChatGPT #'
)[0].value
app.graph
.getNodeById(sendToId)
.widgets.filter(w => w.name === 'prompt')[0].value = result
} catch (error) {}
setTimeout(() => app.queuePrompt(0, 1), 100)
window.recognition?.stop()
window.recognition = null
startBtn.className = ''
startBtn.innerText = 'START'
timeoutId = null
intervalId = setInterval(() => {
if (
app.ui.lastQueueSize === 0 &&
!window.recognition &&
window._mixlab_speech_synthesis_onend
) {
start(element, id, startBtn, node)
startBtn.innerText = 'STOP'
if (intervalId) {
clearInterval(intervalId)
}
}
}, 2200)
}, 2000)
}
window.recognition.onend = function () {
console.log('语音输入结束')
}
window.recognition.onspeechend = function () {
console.log('onspeechend')
}
window.recognition.onerror = function (event) {
console.log('Error occurred in recognition: ' + event.error)
}
window.recognition.start()
}
app.registerExtension({
name: 'Mixlab.audio.SpeechRecognition',
async getCustomWidgets (app) {
return {
AUDIOINPUTMIX (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_speech_recognition')
return data[node.id] || 'Hello Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'SpeechRecognition') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const sendTo = ComfyWidgets.INT(
this,
'Send to ChatGPT #',
['INT', { default: 0 }],
app
)
// console.log('sendTo',sendTo)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 78, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const startBtn = document.createElement('button')
const textArea = document.createElement('textarea')
textArea.placeholder = 'speak text'
// sendTo.type='range';
// sendTo.min=0;
// sendTo.max=2000;
// sendTo.step=1;
// sendTo.className='comfy-multiline-input'
textArea.className = `${'comfy-multiline-input'} ${placeholder}`
textArea.style = `margin-top: 14px;
height: 44px;`
div.style = `flex-direction: column;
display: flex;
margin: 0px 8px 6px;`
startBtn.style = `
margin-top:48px;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);
`
startBtn.innerText = 'START'
div.appendChild(startBtn)
// div.appendChild(sendTo);
div.appendChild(textArea)
startBtn.addEventListener('click', () => {
if (window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className = ''
} else {
start(textArea, this.id, startBtn, this)
startBtn.innerText = 'STOP'
}
})
// sendTo.addEventListener('change',()=>{
// console.log(sendTo.value)
// })
return div
}
let inputAudio = inputDiv('_mixlab_speech_recognition', 'audio')
widget.div.appendChild(inputAudio)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputAudio.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
// const onGraphConfigured=nodeType.prototype.onGraphConfigured;
// nodeType.prototype.onGraphConfigured = function (message) {
// onGraphConfigured?.apply(this, arguments)
// console.log('###SpeechRecognition onGraphConfigured',this,message)
// }
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// console.log('this.widgets', this.widgets)
try {
// 是否根据start by 开启
let open = message.start_by[0] > 0
if (open) {
const div = this.widgets.filter(w => w.name == 'chatgptdiv')[0].div
const startBtn = div.querySelector('button')
let textArea = div.querySelector('textarea')
if (open && !window.recognition) {
start(textArea, this.id, startBtn, this)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className = ''
}
}
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'SpeechRecognition') {
let data = getLocalData('_mixlab_speech_recognition')
// console.log('_mixlab_speech_recognition', node )
let div = node.widgets.filter(f => f.type === 'div')[0]
if (div && data[node.id]) {
div.div.querySelector('textarea').value = data[node.id]
}
try {
let open = node.widgets_values[1] > 0
if (open) {
const div = node.widgets.filter(w => w.name == 'chatgptdiv')[0].div
const startBtn = div.querySelector('button')
let textArea = div.querySelector('textarea')
if (open && !window.recognition) {
start(textArea, node.id, startBtn, node)
startBtn.innerText = 'STOP'
} else if (!open && window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className = ''
}
}
} catch (error) {
console.log('###SpeechRecognition', error)
}
}
}
})
app.registerExtension({
name: 'Mixlab.audio.SpeechSynthesis',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'SpeechSynthesis') {
function populate (text) {
// console.log('SpeechSynthesis',this.widgets)
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === 'text')
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.()
}
this.widgets.length = pos
}
}
for (let list of text) {
const w = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
w.value = list
}
speakText(text.join('\n'))
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
sz[0] = this.size[0]
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1]
}
this.onResize?.(sz)
app.graph.setDirtyCanvas(true, false)
})
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
populate.call(this, message.text)
}
this.serialize_widgets = true //需要保存参数
}
}
})
-23
View File
@@ -1,23 +0,0 @@
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version='v0.1'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
.then(data => {
const latestVersion = data.tag_name
console.log('Latest release version:', latestVersion)
if(latestVersion!=version){
window.alert(
`Please proceed to the official repository to download the latest version.https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/`
)
window.open(
'https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/'
)
}
})
.catch(error => {
console.error('Error fetching release information:', error)
})
// #MixCopilot
+42
View File
@@ -0,0 +1,42 @@
import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.4.2'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
.then(data => {
const latestVersion = data.tag_name
console.log('Latest release version:', latestVersion)
if (
latestVersion &&
latestVersion === localStorage.getItem('_mixlab_nodes_vesion')
)
return
if (latestVersion && latestVersion != version) {
localStorage.setItem('_mixlab_nodes_vesion', latestVersion)
app.ui.dialog.show(`<h4 style="font-size: 18px;">${repoName} <br>
Latest release version: ${latestVersion}</h4>
<p>Please proceed to the official repository to download the latest version.</p>
<a style="color: #2196F3;
font-size: 18px;
font-weight: 800;
letter-spacing: 2px;
}"
href="https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/">https://github.com/shadowcz007/comfyui-mixlab-nodes/releases</a>
`)
// window.alert(
// `Please proceed to the official repository to download the latest version.https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/`
// )
// window.open(
// 'https://github.com/shadowcz007/comfyui-mixlab-nodes/releases/'
// )
}
})
.catch(error => {
console.error('Error fetching release information:', error)
})
// #MixCopilot
+277
View File
@@ -0,0 +1,277 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function getConfig () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab`, {
method: 'POST'
})
return await res.json()
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
app.registerExtension({
name: 'Mixlab.GPT.ChatGPTOpenAI',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128,32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
URL (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_url')
return data[node.id] || 'https://api.openai.com/v1'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ChatGPTOpenAI') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const api_url = this.widgets.filter(w => w.name == 'api_url')[0]
console.log('ChatGPTOpenAI nodeData', this.widgets)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
let inputKey = inputDiv('_mixlab_api_key', 'Key')
let inputUrl = inputDiv('_mixlab_api_url', 'URL')
widget.div.appendChild(inputKey)
widget.div.appendChild(inputUrl)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputUrl.remove()
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'ChatGPTOpenAI') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key'),
url = getLocalData('_mixlab_api_url')
let id = node.id
// console.log('ChatGPTOpenAI serialize_widgets', this)
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
widget.div.querySelector('.URL').value =
url[id] || 'https://api.openai.com/v1'
}
}
})
app.registerExtension({
name: 'Mixlab.GPT.ShowTextForGPT',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "ShowTextForGPT") {
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "text");
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = pos;
}
}
// console.log('ShowTextForGPT',text)
for (let list of text) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
try {
let data=JSON.parse(list);
data=Array.from(data,d=>{
return {
...d,
content:decodeURIComponent(d.content)
}
})
list=JSON.stringify(data,null,2)
} catch (error) {
// console.log(error)
}
w.value =list;
}
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
this.serialize_widgets = true //需要保存参数
}
},
})
+438
View File
@@ -0,0 +1,438 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
function getContentTypeFromBase64 (base64Data) {
const regex = /^data:(.+);base64,/
const matches = base64Data.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
}
return null
}
// 示例用法
// const base64Data = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAAAAAAD/...'; // 替换为实际的base64图片数据
// const contentType = getContentTypeFromBase64(base64Data);
// console.log(contentType);
// // 示例用法
// const base64Data = '...'; // 替换为实际的base64图片数据
// const contentType = 'image/jpeg'; // 替换为实际的图片类型
// const blob = base64ToBlob(base64Data, contentType);
// console.log(blob);
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
const parseImage = url => {
return new Promise((res, rej) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result
res(base64data)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
.catch(error => {
console.log('发生错误:', error)
})
})
}
const parseSvg = async svgContent => {
let scale = 2
// 创建一个临时的DOM元素来解析SVG
const tempContainer = document.createElement('div')
tempContainer.innerHTML = svgContent
// 提取SVG元素
const svgElement = tempContainer.querySelector('svg')
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
var x = ~~(rectElement.getAttribute('x') || 0)
var y = ~~(rectElement.getAttribute('y') || 0)
var width = ~~rectElement.getAttribute('width')
var height = ~~rectElement.getAttribute('height')
// console.log('rectElements',rectElement,x,y,width,height)
if (x != undefined && y != undefined && width && height) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
// 填充颜色到canvas
var fill = rectElement.getAttribute('fill')
context.fillStyle = fill
context.fillRect(0, 0, width, height)
// 将canvas转换为base64格式
var base64 = canvas.toDataURL()
// 将数据转化为指定的JSON格式
var rectData = {
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height),
z_index: i + 1,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'rect'
}
// 将处理后的数据添加到数组中
data.push(rectData)
}
})
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth = viewBox.width
svgHeight = viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
canvas.height = svgHeight
var context = canvas.getContext('2d')
// 绘制SVG到canvas
var svgString = new XMLSerializer().serializeToString(svgElement)
var DOMURL = window.URL || window.webkitURL || window
var svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })
var url = DOMURL.createObjectURL(svgBlob)
let img = await createImage(url)
context.drawImage(img, 0, 0)
let base64 = canvas.toDataURL()
var rectData = {
x: 0,
y: 0,
width: parseInt(svgWidth),
height: parseInt(svgHeight),
z_index: 0,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log('layers', { data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
function exportModelViewerImage (
modelViewer,
width,
height,
format = 'image/png',
quality = 1.0
) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
return new Promise((resolve, reject) => {
context.drawImage(modelViewer, 0, 0, width, height)
resolve(canvas.toDataURL(format, quality))
})
}
app.registerExtension({
name: 'Mixlab.image.SvgImage',
async getCustomWidgets (app) {
return {
SVG (node, inputName, inputData, app) {
// console.log('##node', node, inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 88], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 88] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_svg_image')
// console.log('serializeValue',d)
if (d) {
let url = d[node.id]
let dt = await fetch(url)
let svgStr = await dt.text()
const { data, image } = (await parseSvg(svgStr)) || {}
// console.log(data, image)
return JSON.parse(JSON.stringify({ data, image }))
} else {
return
}
}
}
// console.log('##node',node.serialize)
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'SvgImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
// console.log('SvgImage nodeData',await uploadWidget.serializeValue())
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, svgContainer) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
let that = this
ip.addEventListener('change', event => {
const file = event.target.files[0]
const reader = new FileReader()
// 读取文件内容
reader.onload = async e => {
const svgContent = e.target.result
var blob = new Blob([svgContent], { type: 'image/svg+xml' })
let url = await uploadImage(blob)
// console.log(url)
const { svgElement, data, image } = await parseSvg(svgContent)
// 将提取的SVG元素显示在页面上
let dd = getLocalData(key)
dd[that.id] = url
setLocalDataOfWin(key, dd)
// console.log(this.id, ip.value.trim())
svgElement.style = `width: 90%;padding: 5%;`
// 将提取的SVG元素显示在页面上
svgContainer.innerHTML = ''
svgContainer.appendChild(svgElement)
let h = ~~getComputedStyle(svgElement).height.replace('px', '')
if (that.size && that.size[1] < h) {
that.setSize([that.size[0], that.size[1] + h])
app.canvas.draw(true, true)
}
// console.log(that.size,~~getComputedStyle(svgElement).height.replace('px',''))
uploadWidget.value = await uploadWidget.serializeValue()
}
// 以文本形式读取文件
reader.readAsText(file)
})
return div
}
let svg = document.createElement('div')
svg.className = 'preview'
svg.style = `background:#eee;margin-top: 12px;`
let upload = inputDiv('_mixlab_svg_image', 'Svg', svg)
widget.div.appendChild(upload)
widget.div.appendChild(svg)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
svg.remove()
widget.div.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
if (node.type === 'SvgImage') {
// await sleep(0)
let widget = node.widgets.filter(w => w.name === 'upload-preview')[0]
let dd = getLocalData('_mixlab_svg_image')
let id = node.id
console.log('SvgImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let dt = await fetch(dd[id])
let svgStr = await dt.text()
const { svgElement, data, image } = await parseSvg(svgStr)
svgElement.style = `width: 90%;padding: 5%;`
// 将提取的SVG元素显示在页面上
widget.div.querySelector('.preview').innerHTML = ''
widget.div.querySelector('.preview').appendChild(svgElement)
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
}
}
})
+559
View File
@@ -0,0 +1,559 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
// flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
const parseSvg = async svgContent => {
// 创建一个临时的DOM元素来解析SVG
const tempContainer = document.createElement('div')
tempContainer.innerHTML = svgContent
// 提取SVG元素
const svgElement = tempContainer.querySelector('svg')
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
var x = ~~(rectElement.getAttribute('x') || 0)
var y = ~~(rectElement.getAttribute('y') || 0)
var width = ~~rectElement.getAttribute('width')
var height = ~~rectElement.getAttribute('height')
// console.log('rectElements',rectElement,x,y,width,height)
if (x != undefined && y != undefined && width && height) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
// 填充颜色到canvas
var fill = rectElement.getAttribute('fill')
context.fillStyle = fill
context.fillRect(0, 0, width, height)
// 将canvas转换为base64格式
var base64 = canvas.toDataURL()
// 将数据转化为指定的JSON格式
var rectData = {
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height),
z_index: i + 1,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'rect'
}
// 将处理后的数据添加到数组中
data.push(rectData)
}
})
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth = viewBox.width
svgHeight = viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
canvas.height = svgHeight
var context = canvas.getContext('2d')
// 绘制SVG到canvas
var svgString = new XMLSerializer().serializeToString(svgElement)
var DOMURL = window.URL || window.webkitURL || window
var svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })
var url = DOMURL.createObjectURL(svgBlob)
let img = await createImage(url)
context.drawImage(img, 0, 0)
let base64 = canvas.toDataURL()
var rectData = {
x: 0,
y: 0,
width: parseInt(svgWidth),
height: parseInt(svgHeight),
z_index: 0,
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64',
_t: 'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log('layers', { data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
async function setArea (cw, ch, base64, data, fn) {
let displayHeight = Math.round(window.screen.availHeight * 0.6)
let div = document.createElement('div')
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
z-index:999999;
width: 100%;'>
<img id='ml_video' style='position: absolute;
height: ${displayHeight}px;user-select: none;
-webkit-user-drag: none;
outline: 2px solid #eaeaea;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
pointer-events: none;'></div>
</div>`
// document.body.querySelector('#ml_overlay')
document.body.appendChild(div)
// let canvas = document.createElement('canvas')
// canvas.width = cw
// canvas.height = ch
let img = div.querySelector('#ml_video')
let overlay = div.querySelector('#ml_overlay')
let selection = div.querySelector('#ml_selection')
let startX, startY, endX, endY
let start = false
// Set video source
img.src = base64
// canvas.toDataURL();
// init area
// const data = getSetAreaData()
let x = 0,
y = 0,
width = (cw * displayHeight) / ch,
height = displayHeight
let imgWidth = cw
let imgHeight = ch
if (data && data.width > 0 && data.height > 0) {
// 相同尺寸窗口,恢复选区
x = (width * data.x) / imgWidth
y = (height * data.y) / imgHeight
width = (width * data.width) / imgWidth
height = (height * data.height) / imgHeight
}
selection.style.left = x + 'px'
selection.style.top = y + 'px'
selection.style.width = width + 'px'
selection.style.height = height + 'px'
// Add mouse events
img.addEventListener('mousedown', startSelection)
img.addEventListener('mousemove', updateSelection)
img.addEventListener('mouseup', endSelection)
overlay.addEventListener('click', remove)
function remove () {
overlay.removeEventListener('click', remove)
img.removeEventListener('mousedown', startSelection)
img.removeEventListener('mousemove', updateSelection)
img.removeEventListener('mouseup', endSelection)
div.remove()
}
function startSelection (event) {
if (start == false) {
startX = event.clientX
startY = event.clientY
updateSelection(event)
start = true
} else {
}
}
function updateSelection (event) {
endX = event.clientX
endY = event.clientY
// Calculate width, height, and coordinates
let width = Math.abs(endX - startX)
let height = Math.abs(endY - startY)
let left = Math.min(startX, endX)
let top = Math.min(startY, endY)
// Set selection style
selection.style.left = left + 'px'
selection.style.top = top + 'px'
selection.style.width = width + 'px'
selection.style.height = height + 'px'
}
function endSelection (event) {
endX = event.clientX
endY = event.clientY
// 获取img元素的真实宽度和高度
let imgWidth = img.naturalWidth
let imgHeight = img.naturalHeight
// 换算起始坐标
let realStartX = (startX / img.offsetWidth) * imgWidth
let realStartY = (startY / img.offsetHeight) * imgHeight
// 换算起始坐标
let realEndX = (endX / img.offsetWidth) * imgWidth
let realEndY = (endY / img.offsetHeight) * imgHeight
startX = realStartX
startY = realStartY
endX = realEndX
endY = realEndY
// Calculate width, height, and coordinates
let width = Math.round(Math.abs(endX - startX))
let height = Math.round(Math.abs(endY - startY))
let left = Math.round(Math.min(startX, endX))
let top = Math.round(Math.min(startY, endY))
if (width <= 0 && height <= 0) return remove()
if (fn) fn(left, top, width, height)
remove()
}
}
app.registerExtension({
name: 'Mixlab.layer.ShowLayer',
async getCustomWidgets (app) {
return {
EDIT (node, inputName, inputData, app) {
// console.log('EditLayer##node', node,inputName, inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 44], // a default size
draw (ctx, node, widget_width, y, widget_height) {
// console.log('EditLayer', this)
if (this.input)
Object.assign(
this.input.style,
get_position_style(ctx, widget_width, 32, node.size[1])
)
},
computeSize (...args) {
return [128, 44] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_edit_layer')
// console.log('EditLayer',d[node.id])
return d[node.id]
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ShowLayer') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const findNode = nodeId => {
let node = app.graph._nodes_by_id[nodeId]
if (node?.type == 'Reroute') {
let linkId = node.inputs.filter(i => i.type == '*')[0].link
nodeId = app.graph.links.filter(link => link.id == linkId)[0]
?.origin_id
return findNode(nodeId)
} else {
return nodeId
}
}
// 获取layers数据
const getLayers = async () => {
console.log(
'getLayers1',
this.inputs.filter(ip => ip.name === 'layers')
)
let linkId = this.inputs.filter(ip => ip.name === 'layers')[0].link
let nodeId = app.graph.links?.filter(link => link.id == linkId)[0]
?.origin_id
if (nodeId) {
nodeId = findNode(nodeId)
}
// let node = app.graph._nodes_by_id[nodeId]
// if (node?.type == 'Reroute') {
// linkId = node.inputs[0].link
// nodeId = app.graph.links.filter(link => link.id == linkId)[0]
// ?.origin_id
// }
let d = getLocalData('_mixlab_svg_image')
console.log('test', d[nodeId])
if (d[nodeId]) {
let url = d[nodeId]
let dt = await fetch(url)
let svgStr = await dt.text()
const { data } = (await parseSvg(svgStr)) || {}
console.log('fetch', data)
return data
} else {
return []
}
}
// 修改layers数据
const setLayer = async (editIndex, layers = null) => {
// let editIndex = 0
let lys = layers || (await getLayers())
let layer = lys[editIndex]
// console.log(layer)
const updateValue = name => {
const x = this.widgets.filter(w => w.name == name)[0]
x.value = layer[name]
}
if (layer) {
Array.from(['x', 'y', 'width', 'height', 'z_index'], n =>
updateValue(n)
)
}
}
let that = this
const save_edit_layer_index = i => {
let data = getLocalData('_mixlab_edit_layer')
data[that.id] = i
localStorage.setItem('_mixlab_edit_layer', JSON.stringify(data))
}
await setLayer(0)
save_edit_layer_index(0)
const edit = this.widgets.filter(w => w.name == 'edit')[0]
edit.input = $el('div', {})
edit.input.style = `
display: flex;
flex-direction:row;
align-items: center;
margin-top: 0;`
const ip = $el('input', {})
ip.className = 'comfy-multiline-input'
ip.type = 'number'
ip.min = 0
ip.step = 1
ip.max = Math.max(0, (await getLayers()).length - 1)
// ip.className = `${'comfy-multiline-input'} `
ip.value = 0
ip.style = `
background-color: var(--comfy-input-bg);
color: var(--input-text);
outline: none;
border: none;
padding: 4px;
width: 60%;
cursor: pointer;
height: 24px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = 'Layer Index'
edit.input.appendChild(label)
edit.input.appendChild(ip)
document.body.appendChild(edit.input)
ip.addEventListener('click', async event => {
console.log(await getLayers())
ip.max = Math.max(0, (await getLayers()).length - 1)
})
ip.addEventListener('change', async event => {
let index = ~~ip.value
let lys = await getLayers()
await setLayer(index, lys)
app.graph.setDirtyCanvas(true, true)
save_edit_layer_index(index)
})
// console.log('EditLayer nodeData', edit)
const onRemoved = this.onRemoved
this.onRemoved = () => {
edit.input.remove()
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = false //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'SvgImage') {
let widget = node.widgets.filter(w => w.div)[0]
let data = getLocalData('_mixlab_svg_image')
let id = node.id
// widget.div.querySelector('.Svg').value = data[id] || '#000000'
}
}
})
app.registerExtension({
name: 'Mixlab.layer.NewLayer',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'NewLayer') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
let b = this.widgets.filter(w => w.type === 'button')[0]
// const [w, h, base64] = canvas
if (!b) {
const updateValue = (x1, y1, w1, h1) => {
if (this.widgets) {
for (const widget of this.widgets) {
if (widget.name === 'x') {
widget.value = x1
}
if (widget.name === 'y') {
widget.value = y1
}
if (widget.name === 'width') {
widget.value = w1
}
if (widget.name === 'height') {
widget.value = h1
}
}
}
}
this.addWidget('button', 'Set Area', '', () => {
let data = {}
for (const widget of this.widgets) {
if (widget.name === 'x') {
data.x = widget.value
}
if (widget.name === 'y') {
data.y = widget.value
}
if (widget.name === 'width') {
data.width = widget.value
}
if (widget.name === 'height') {
data.height = widget.value
}
}
try {
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)
} catch (error) {}
})
}
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
// let b = this.widgets.filter(w => w.type === 'button')[0];
return onRemoved?.()
}
if (this.onResize) {
this.onResize(this.size)
}
this.serialize_widgets = true //需要保存参数
}
}
})
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
export const closeIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>'
+737
View File
@@ -0,0 +1,737 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import {closeIcon} from './svg_icons.js'
function deepEqual (obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
return false
}
if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
return obj1 === obj2
}
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!deepEqual(obj1[key], obj2[key])) {
return false
}
}
return true
}
async function get_nodes_map () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab/nodes_map`, {
method: 'POST',
body: JSON.stringify({
data: 'json'
})
})
return await res.json()
}
function loadCSS(url) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
document.getElementsByTagName('head')[0].appendChild(link);
}
var cssURL = 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css';
loadCSS(cssURL);
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
injectCSS(`::-webkit-scrollbar {
width: 2px;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}`)
async function getCustomnodeMappings (mode = 'url') {
// mode = "local";
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
let nodes = {}
const data = (await get_nodes_map()).data
for (let url in data) {
let n = data[url]
for (let node of n[0]) {
// if(node=='CLIPSeg')console.log('#CLIPSeg',n)
nodes[node] = { url, title: n[1].title_aux }
}
}
// try {
// const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
// const data = await response.json()
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// } catch (error) {
// const data = (await get_nodes_map()).data
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// }
return nodes
}
const missingNodeGithub = (missingNodeTypes, nodesMap) => {
let ts = {}
Array.from(new Set(missingNodeTypes), n => {
if (nodesMap[n]) {
let title = nodesMap[n].title
if (!ts[title]) {
ts[title] = {
title,
nodes: {},
url: nodesMap[n].url
}
}
ts[title].nodes[n] = 1
} else {
ts[n] = {
title: n,
nodes: {},
url: `https://github.com/search?q=${n}&type=code`
}
ts[n].nodes[n] = 1
}
})
return Array.from(Object.values(ts), n => {
const url = n.url
return `<li style="color: white;
background: black;
padding: 8px;
font-size: 12px;">${n.title}<a href="${url}" target="_blank"> 🔗</a></li>`
})
}
let nodesMap
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
// console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${missingNodeGithub(
missingNodeTypes,
nodesMap
).join('')}</ul>${
hasAddedNodes
? 'Nodes that have failed to load will show as red on the graph.'
: ''
}`
)
this.logging.addEntry('Comfy.App', 'warn', {
MissingNodes: missingNodeTypes
})
}
// app.registerExtension({
// name: 'Comfy.MDNote',
// registerCustomNodes () {
// class NoteNode {
// // color = LGraphCanvas.node_colors.yellow.color
// // bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
// // groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
// constructor () {
// if (!this.properties) {
// this.properties = {}
// this.properties.text = ''
// }
// console.log('NoteNode1', this)
// const widget = {
// type: 'div',
// name: 'input_color',
// draw (ctx, node, widget_width, y, widget_height) {
// Object.assign(
// this.div.style,
// get_position_style(
// ctx,
// widget_width,
// 44,
// node.size[1]
// )
// )
// }
// }
// widget.div = $el('div', {});
// widget.div.innerText='1111'
// document.body.appendChild(widget.div)
// this.addCustomWidget(widget)
// this.serialize_widgets = true
// this.isVirtualNode = true
// }
// }
// // Load default visibility
// LiteGraph.registerNodeType(
// 'MDNote',
// Object.assign(NoteNode, {
// title_mode: LiteGraph.NORMAL_TITLE,
// title: 'MDNote',
// collapsable: true
// })
// )
// NoteNode.category = '♾️Mixlab/utils'
// },
// })
async function fetchReadmeContent (url) {
try {
// var repo = 'owner/repo'; // 仓库的拥有者和名称
var match = url.match(/github.com\/([^/]+\/[^/]+)/)
var repo = match[1]
var url = `https://api.github.com/repos/${repo}/readme`
var response = await fetch(url)
var data = await response.json()
var readmeUrl = data.download_url
var readmeResponse = await fetch(readmeUrl)
var content = await readmeResponse.text()
// console.log(content) // 在控制台输出readme.md文件的内容
return content
} catch (error) {
console.log('获取readme.md文件信息失败:', error)
}
}
function createModal (url, markdown,title) {
// Create modal element
var div =
document.querySelector('#mix-modal') || document.createElement('div')
div.id = 'mix-modal'
div.innerHTML = ''
div.style.cssText = `width: 100%;
z-index: 9990;
height: 100vh;
display: flex;
color: var(--descrip-text);
position: fixed;
top: 0;
left: 0;
`
var modal = document.createElement('div')
div.appendChild(modal)
modal.classList.add("modal-body")
// Set modal styles
modal.style.cssText = `
background: white;
height: 80vh;
position: fixed;
overflow:hidden;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
border-radius: 4px;
box-shadow: 4px 4px 14px rgba(255,255,255,0.5);
`
// Create modal content area
var modalContent = document.createElement('div')
modalContent.classList.add('modal-content')
// Create modal header
const headerElement = document.createElement('div')
headerElement.classList.add('modal-header')
headerElement.style.cssText = `
display: flex;
padding: 20px 24px 8px 24px;
justify-content: space-between;
`
const headTitleElement = document.createElement('a')
headTitleElement.classList.add('header-title')
headTitleElement.style.cssText=`
color: var(--descrip-text);
font-size: 18px;
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
text-decoration: none;
font-weight: bold;
`
headTitleElement.onmouseenter = function(){
headTitleElement.style.color = 'var(--comfy-menu-bg)'
}
headTitleElement.onmouseleave = function(){
headTitleElement.style.color = 'var(--descrip-text)'
}
headTitleElement.textContent= title ||'';
headTitleElement.href = url
headTitleElement.target='_blank'
const linkIcon = document.createElement('small')
linkIcon.textContent = '🔗'
headTitleElement.appendChild(linkIcon);
headerElement.appendChild(headTitleElement)
// Create close button
const closeButton = document.createElement('span')
closeButton.classList.add('close')
closeButton.innerHTML = closeIcon
// Set close button styles
closeButton.style.cssText = `
padding: 4px;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
fill: var(--descrip-text);
`
closeButton.onmouseenter = function(){
closeButton.style.fill = 'var(--comfy-menu-bg)';
}
closeButton.onmouseleave = function(){
closeButton.style.fill = 'var(--descrip-text)';
}
headerElement.appendChild(closeButton)
// Click event to close the modal
function closeMixModal(){
div.style.display = 'none'
window.removeEventListener('keydown',MixModalEscKeyEvent)
}
closeButton.onclick = function () {
closeMixModal()
}
// Set modal content area styles
modalContent.style.cssText = `
position: relative;
padding: 0px;
overflow: hidden scroll;;
height: 100%;
min-width:300px
`
// Append close button to modal content area
modal.appendChild(headerElement)
// Create element for displaying Markdown content
var markdownContent = document.createElement('div')
markdownContent.classList.add('markdown-content','markdown-body')
markdownContent.style.cssText = `max-width: 50vw;padding: 0px 24px 100px 24px;`
showdown.setFlavor('github');
var converter = new showdown.Converter()
var html = converter.makeHtml(markdown)
// Hide images in the markdown when they fail to load
var regex = /<img[^>]+src="?([^"\s]+)"?[^>]*>/g
html = html.replace(regex, function (match, src) {
return `<img src="${src}" onerror="this.style.display='none'">`
})
// Open links in a new tab or window
html = html.replace(/<a/g, '<a target="_blank"')
// Fix href attribute to absolute path
html = html.replace(
/<a([^>]+href=["'])(?!https?:\/\/)([^"'>]+)/g,
function (match, prefix, path) {
var absolutePath = url + '/' + path
return '<a' + prefix + absolutePath
}
)
markdownContent.innerHTML = html
// Append Markdown content element to modal content area
modalContent.appendChild(markdownContent)
// Append modal content area to modal element
modal.appendChild(modalContent)
const footerElement = document.createElement('div')
footerElement.style.cssText=`
position: absolute;
left: 0;
right: 0;
text-align: right;
padding:10px;
font-size:12px
`
const footerText = document.createElement('a')
footerText.href ="https://github.com/shadowcz007/comfyui-mixlab-nodes"
footerText.innerText = "Support by Mixlab"
footerText.style.cssText=`color:inherit`
footerText.target = "_blank"
footerText.onmouseenter=function(){
footerText.style.color = 'var(--input-text)'
}
footerText.onmouseleave=function(){
footerText.style.color = 'inherit'
}
footerText.onclick = function(e){
e.stopPropagation()
}
footerElement.appendChild(footerText)
div.appendChild(footerElement)
// Append modal element to the page
if (!document.querySelector('#mix-modal')) {
document.body.appendChild(div)
}
function MixModalEscKeyEvent(event){
if(event.key == "Escape"){
closeMixModal()
}
}
window.removeEventListener('keydown',MixModalEscKeyEvent)
window.addEventListener('keydown',MixModalEscKeyEvent)
const bgElement = document.createElement('div')
bgElement.classList.add('mix-modal-bg')
bgElement.style.cssText = `
width:100%;
height:100%;
background-color: rgba(0,0,0,0.8);
`
bgElement.onclick = function( ){
closeMixModal()
}
div.appendChild(bgElement)
}
app.registerExtension({
name: 'Comfy.Mixlab.ui',
init () {
LGraphCanvas.prototype.helpAboutNode = async function (node) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
console.log('node & node map', node, nodesMap, nodesMap[node.type])
let repo = nodesMap[node.type]
if (repo) {
let markdown = await fetchReadmeContent(repo.url)
createModal(repo.url,markdown,repo.title)
}
}
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions // store the existing method
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// replace it
const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
// options.splice(
// options.length - 1,
// 0, // splice a new option in at the end
// {
// content: '♾️Mixlab', // with a name
// callback: () => {
// LGraphCanvas.prototype.helpAboutNode(node)
// } // and the callback
// },
// null // a divider
// )
return [
{
content: 'Help ♾️Mixlab', // with a name
callback: () => {
LGraphCanvas.prototype.helpAboutNode(node)
} // and the callback
},
null,
...options
] // and return the options
}
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null, {
content: `Find ♾️Mixlab`,
disabled: false, // or a function determining whether to disable
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 50px; width: 200px;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
padding: 10px;
border: 1px solid black;z-index: 999999999;padding-top: 0;`
div.innerHTML = ''
let btn = document.createElement('div')
btn.style=`display: flex;
width: calc(100% - 24px);
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 32px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.innerText = `Find The Node`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[nodeId].class_type
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nd.appendChild(d)
}
}
let nodesDivv = document.createElement('div')
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nodesDiv.appendChild(d)
}
nodesDivv.appendChild(nodesDiv)
nodesDivv.style=`overflow: scroll;
height: 70vh;width: 100%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
});
return options;
};
}
})
+181
View File
@@ -0,0 +1,181 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import { addValueControlWidget } from "../../../scripts/widgets.js";
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
app.registerExtension({
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
return {
TCOLOR (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color')
return data[node.id] || '#000000'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'Color') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('Color nodeData', this.widgets)
const widget = {
type: 'div',
name: 'input_color',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, value) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'color'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 100%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
// console.log(this.id, ip.value.trim())
})
return div
}
let inputColor = inputDiv('_mixlab_utils_color', 'Color', '#000000')
widget.div.appendChild(inputColor)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputColor.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'Color') {
let widget = node.widgets.filter(w => w.div)[0]
let data = getLocalData('_mixlab_utils_color')
let id = node.id
widget.div.querySelector('.Color').value = data[id] || '#000000'
}
}
})
app.registerExtension({
name: 'Mixlab.utils.TextToNumber',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TextToNumber') {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const random_number=this.widgets.filter(w=>w.name==='random_number')[0]
if(random_number.value==='enable'){
const n=this.widgets.filter(w=>w.name==='number')[0]
n.value=message.num[0]
}
console.log('TextToNumber', random_number.value)
}
}
}
})
@@ -14,6 +14,10 @@ async function getConfig () {
return await res.json()
}
if (!window._mixlab_screen_prompt)
window._mixlab_screen_prompt =
'beautiful scenery nature glass bottle landscape,under water'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
@@ -57,7 +61,28 @@ app.registerExtension({
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
// // 绘制文件图标的函数
// function drawFileIcon () {
// // 清空画布
// // ctx.clearRect(0, 0, canvas.width, canvas.height)
// // 绘制文件外框
// ctx.fillStyle = '#000'
// ctx.fillRect(5, 5, 40, 40)
// // 绘制文件夹图标
// ctx.fillStyle = '#f00'
// ctx.fillRect(10, 15, 30, 20)
// // 绘制监听符号
// ctx.beginPath()
// ctx.arc(30, 35, 5, 0, 2 * Math.PI)
// ctx.fillStyle = '#00f'
// ctx.fill()
// }
// // 调用绘制函数
// drawFileIcon()
},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
@@ -69,6 +94,26 @@ app.registerExtension({
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
PROMPT (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 24] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
return window._mixlab_screen_prompt || ''
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
@@ -79,11 +124,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
// console.log(
// 'watch widtget',
// this.widgets.filter(w => w.name == 'watcher')[0]
// )
console.log('watch widtget', this.widgets)
const watcher = this.widgets.filter(w => w.name == 'watcher')[0]
@@ -111,7 +152,6 @@ app.registerExtension({
}
}
// 上次路径填充
getConfig().then(json => {
let w = this.widgets.filter(w => w.name == 'file_path')[0]
@@ -120,7 +160,6 @@ app.registerExtension({
}
// console.log(json.event_type)
window._mixlab_file_path_watcher = json.event_type
})
/*
@@ -130,7 +169,7 @@ app.registerExtension({
this.onRemoved = function () {
// widget.card.remove()
}
this.serialize_widgets = false
this.serialize_widgets = true
}
}
}
+1077
View File
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long
+23 -2
View File
@@ -1,3 +1,24 @@
::-webkit-scrollbar {
width: 2px;
}
width: 2px;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}
+131 -114
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 21,
"last_link_id": 45,
"last_node_id": 23,
"last_link_id": 51,
"nodes": [
{
"id": 7,
@@ -14,7 +14,7 @@
"1": 200
},
"flags": {},
"order": 7,
"order": 4,
"mode": 0,
"inputs": [
{
@@ -89,12 +89,12 @@
504,
33
],
"size": [
400,
200
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 6,
"order": 8,
"mode": 0,
"inputs": [
{
@@ -105,7 +105,7 @@
{
"name": "text",
"type": "STRING",
"link": 43,
"link": 51,
"widget": {
"name": "text"
}
@@ -295,7 +295,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
613900833686415,
482859286431021,
"randomize",
4,
1.6,
@@ -311,94 +311,24 @@
338,
786
],
"size": [
210,
246
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 4,
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 37
"link": 50
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 19,
"type": "ScreenShare",
"pos": [
-55,
512
],
"size": {
"0": 325.3117370605469,
"1": 459.7692565917969
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
37,
42
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "STRING",
"type": "STRING",
"links": [
43
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "ScreenShare"
}
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
2041,
277
],
"size": [
315,
106
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
}
},
{
"id": 6,
"type": "LoraLoader",
@@ -411,7 +341,7 @@
"1": 126
},
"flags": {},
"order": 8,
"order": 5,
"mode": 0,
"inputs": [
{
@@ -463,7 +393,7 @@
"1": 98
},
"flags": {},
"order": 3,
"order": 2,
"mode": 0,
"outputs": [
{
@@ -515,13 +445,13 @@
"1": 58
},
"flags": {},
"order": 5,
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 42
"link": 49
}
],
"outputs": [
@@ -541,6 +471,93 @@
"widgets_values": [
512
]
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
1928,
295
],
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 23,
"type": "ScreenShare",
"pos": [
-65,
446
],
"size": [
312.78457519531213,
606.2132135620109
],
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
49,
50
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [
51
],
"shape": 3,
"slot_index": 1
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
2018,
null,
null,
null,
null
]
}
],
"links": [
@@ -744,14 +761,6 @@
0,
"LATENT"
],
[
37,
19,
0,
2,
0,
"IMAGE"
],
[
38,
16,
@@ -784,22 +793,6 @@
0,
"IMAGE"
],
[
42,
19,
0,
18,
0,
"IMAGE"
],
[
43,
19,
2,
8,
1,
"STRING"
],
[
44,
6,
@@ -815,6 +808,30 @@
6,
1,
"CLIP"
],
[
49,
23,
0,
18,
0,
"IMAGE"
],
[
50,
23,
0,
2,
0,
"IMAGE"
],
[
51,
23,
1,
8,
1,
"STRING"
]
],
"groups": [],
File diff suppressed because it is too large Load Diff
+75 -50
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 23,
"last_link_id": 47,
"last_link_id": 48,
"nodes": [
{
"id": 11,
@@ -94,7 +94,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
882790958612696,
1088992701378297,
"randomize",
4,
1.6,
@@ -110,10 +110,10 @@
-470,
840
],
"size": [
430.924072265625,
253.48239135742188
],
"size": {
"0": 430.924072265625,
"1": 253.48239135742188
},
"flags": {},
"order": 0,
"mode": 0,
@@ -212,7 +212,7 @@
"1": 58
},
"flags": {},
"order": 9,
"order": 8,
"mode": 0,
"inputs": [
{
@@ -271,45 +271,6 @@
"control_scribble-fp16.safetensors"
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
678,
-77
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
39
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"superman,"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
@@ -322,7 +283,7 @@
"1": 200
},
"flags": {},
"order": 6,
"order": 5,
"mode": 0,
"inputs": [
{
@@ -361,7 +322,7 @@
"1": 126
},
"flags": {},
"order": 7,
"order": 6,
"mode": 0,
"inputs": [
{
@@ -465,7 +426,7 @@
"1": 246
},
"flags": {},
"order": 8,
"order": 7,
"mode": 0,
"inputs": [
{
@@ -512,6 +473,53 @@
1
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
678,
-77
],
"size": [
400,
200
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 48,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
39
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"superman,"
]
},
{
"id": 22,
"type": "LoadImagesFromPath",
@@ -521,7 +529,7 @@
],
"size": {
"0": 315,
"1": 202
"1": 238
},
"flags": {},
"order": 4,
@@ -542,6 +550,15 @@
"type": "MASK",
"links": null,
"shape": 6
},
{
"name": "STRING",
"type": "STRING",
"links": [
48
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
@@ -813,6 +830,14 @@
18,
0,
"IMAGE"
],
[
48,
22,
2,
8,
1,
"STRING"
]
],
"groups": [
+631
View File
@@ -0,0 +1,631 @@
{
"last_node_id": 47,
"last_link_id": 46,
"nodes": [
{
"id": 27,
"type": "CLIPTextEncode",
"pos": [
1961.178268896482,
527.6060791015625
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 24
},
{
"name": "text",
"type": "STRING",
"link": 46,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
21
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 28,
"type": "CLIPTextEncode",
"pos": [
1958.7452854980445,
266
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 25
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
22
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 24,
"type": "KSampler",
"pos": [
2434.0233006347635,
80
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 21
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 22
},
{
"name": "latent_image",
"type": "LATENT",
"link": 23
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
26
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
971428736321335,
"randomize",
15,
8,
"euler",
"karras",
1
]
},
{
"id": 29,
"type": "VAEDecode",
"pos": [
2799.0233006347635,
80
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 26
},
{
"name": "vae",
"type": "VAE",
"link": 27
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
31
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 26,
"type": "EmptyLatentImage",
"pos": [
2069.0233006347635,
80
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
23
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 25,
"type": "CheckpointLoaderSimple",
"pos": [
1593.7452854980445,
80
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
24,
25
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
27
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 31,
"type": "PreviewImage",
"pos": [
2439,
408
],
"size": {
"0": 563.4671630859375,
"1": 720.8866577148438
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 31
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 44,
"type": "ShowTextForGPT",
"pos": [
1171,
425
],
"size": {
"0": 432.46002197265625,
"1": 264.40771484375
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 44,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"system\",\n \"content\": \"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"\"\n }\n]"
]
},
{
"id": 43,
"type": "ShowTextForGPT",
"pos": [
1166,
740
],
"size": {
"0": 424.0079650878906,
"1": 430.6391296386719
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 45,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company.\"\n }\n]"
]
},
{
"id": 45,
"type": "ShowTextForGPT",
"pos": [
1092,
262
],
"size": {
"0": 635.8358154296875,
"1": 101.46092224121094
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 43,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company."
]
},
{
"id": 47,
"type": "ChatGPTOpenAI",
"pos": [
585,
306
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
43,
46
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": [
44
],
"shape": 3,
"slot_index": 1
},
{
"name": "session_history",
"type": "STRING",
"links": [
45
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo-16k",
2220,
"randomize",
1,
null
]
}
],
"links": [
[
2,
4,
0,
5,
0,
"STRING"
],
[
3,
6,
0,
4,
0,
"STRING"
],
[
4,
6,
1,
7,
0,
"JSON"
],
[
8,
10,
0,
11,
0,
"STRING"
],
[
12,
10,
1,
14,
0,
"STRING"
],
[
13,
10,
1,
16,
0,
"JSON"
],
[
20,
25,
0,
24,
0,
"MODEL"
],
[
21,
27,
0,
24,
1,
"CONDITIONING"
],
[
22,
28,
0,
24,
2,
"CONDITIONING"
],
[
23,
26,
0,
24,
3,
"LATENT"
],
[
24,
25,
1,
27,
0,
"CLIP"
],
[
25,
25,
1,
28,
0,
"CLIP"
],
[
26,
24,
0,
29,
0,
"LATENT"
],
[
27,
25,
2,
29,
1,
"VAE"
],
[
31,
29,
0,
31,
0,
"IMAGE"
],
[
43,
47,
0,
45,
0,
"STRING"
],
[
44,
47,
1,
44,
0,
"STRING"
],
[
45,
47,
2,
43,
0,
"STRING"
],
[
46,
47,
0,
27,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
+251
View File
@@ -0,0 +1,251 @@
{
"last_node_id": 5,
"last_link_id": 4,
"nodes": [
{
"id": 4,
"type": "ChatGPTOpenAI",
"pos": [
-512,
-236
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
2
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"描述一个科幻的场景",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-35-turbo",
6933,
"randomize",
1,
null
]
},
{
"id": 2,
"type": "ChatGPTOpenAI",
"pos": [
-36,
-234
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 2,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
3
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"增加丰富的细节和光影,摄影技巧,镜头语言,材质肌理",
"gpt-3.5-turbo",
2836,
"randomize",
1,
null
]
},
{
"id": 5,
"type": "ChatGPTOpenAI",
"pos": [
-30,
183
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 3,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
4
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"翻译成英文,并按照格式输出: 画面、主题、细节、灯光、氛围、艺术家、其他",
"gpt-3.5-turbo",
1085,
"randomize",
1,
null
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
447,
-229
],
"size": [
503.79851499517997,
356.0560985581077
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 4,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"Picture: The picture is composed of carefully chosen elements, capturing the subject matter in a visually striking way.\n\nTheme: The theme of the photograph could vary, from capturing nature's beauty to showcasing urban landscapes, human emotions, or abstract concepts.\n\nDetails: The photograph captures intricate details, bringing attention to the subject's textures, colors, shapes, and patterns.\n\nLighting: The photographer manipulates lighting, using techniques like natural light, dramatic shadows, or artificial lighting to enhance the mood and atmosphere of the photograph.\n\nAmbiance: The photograph evokes a specific ambiance or mood, whether it's serene, mysterious, joyful, melancholic, or any other emotional response.\n\nArtist: The photographer skillfully crafts the image, demonstrating their artistic vision, technical skills, and creative expression through the composition, framing, and post-processing choices.\n\nOthers: Apart from the elements mentioned above, the photograph may also incorporate other creative techniques like long exposure, multiple exposures, color grading, or unconventional perspectives to create a unique and captivating image.\n\nIn the future, as technology and imagination continue to advance, photography will likely continue to evolve and innovate, offering even more realistic and awe-inspiring visual experiences for humans."
]
}
],
"links": [
[
2,
4,
0,
2,
0,
"STRING"
],
[
3,
2,
0,
5,
0,
"STRING"
],
[
4,
5,
0,
3,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
+314
View File
@@ -0,0 +1,314 @@
{
"last_node_id": 10,
"last_link_id": 16,
"nodes": [
{
"id": 9,
"type": "SpeechSynthesis",
"pos": [
40,
356
],
"size": {
"0": 352.8227844238281,
"1": 95.3553237915039
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 12,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
13,
16
],
"shape": 6,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
"如果您想购买工程车,您需要考虑以下几个因素:\n\n1. 预算:确定您有多少钱可以用来购买工程车。\n\n2. 用途:确定您购买工程车的目的,这将帮助您选择合适的车型和配置。\n\n3. 品牌和型号:选择一个知名品牌和适合您需求的型号。\n\n4. 购买地点:考虑在哪个地区购买,这将影响价格和可用性。\n\n5. 保养和维修:考虑车辆的保养和维修成本,以确保购买后的维护成本合理。\n\n请告诉我您需要更多的哪方面的信息,我将能更好地帮助您。"
]
},
{
"id": 8,
"type": "SpeechRecognition",
"pos": [
537,
321
],
"size": {
"0": 228.5580291748047,
"1": 239.85951232910156
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "start_by",
"type": "INT",
"link": 14,
"widget": {
"name": "start_by"
}
}
],
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
15
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
1,
4,
null
]
},
{
"id": 4,
"type": "ChatGPTOpenAI",
"pos": [
-403,
14
],
"size": {
"0": 400,
"1": 358
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
12
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"买",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo",
8034,
"randomize",
1,
null
]
},
{
"id": 10,
"type": "ShowTextForGPT",
"pos": [
867,
349
],
"size": {
"0": 315,
"1": 76.00000762939453
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 15,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"工程车子什么"
]
},
{
"id": 5,
"type": "DynamicDelayProcessor",
"pos": [
496,
4
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "any_input",
"type": "*",
"link": 13
},
{
"name": "delay_by_text",
"type": "STRING",
"link": 16,
"widget": {
"name": "delay_by_text"
}
}
],
"outputs": [
{
"name": "output",
"type": "*",
"links": [
14
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "DynamicDelayProcessor"
},
"widgets_values": [
1,
"",
0.5,
"enable",
1
]
}
],
"links": [
[
4,
2,
0,
5,
0,
"*"
],
[
7,
4,
0,
6,
0,
"STRING"
],
[
8,
6,
0,
5,
1,
"STRING"
],
[
9,
5,
0,
7,
0,
"INT"
],
[
12,
4,
0,
9,
0,
"STRING"
],
[
13,
9,
0,
5,
0,
"*"
],
[
14,
5,
0,
8,
0,
"INT"
],
[
15,
8,
0,
10,
0,
"STRING"
],
[
16,
9,
0,
5,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
+828
View File
@@ -0,0 +1,828 @@
{
"last_node_id": 23,
"last_link_id": 0,
"nodes": [
{
"id": 7,
"type": "ResizeImageMixlab",
"pos": [
1096.229866976564,
731.8207909156248
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ResizeImageMixlab"
},
"widgets_values": [
512,
512,
"width"
]
},
{
"id": 8,
"type": "LoadImagesFromPath",
"pos": [
1101.229866976564,
900.8207909156248
],
"size": {
"0": 315,
"1": 238
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 6
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 6
},
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImagesFromPath"
},
"widgets_values": [
"C:\\Users\\38957\\Documents\\GitHub\\extract-anything\\outputs",
"disable",
"enable",
0,
"disable",
null,
null
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
1786,
252
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
""
]
},
{
"id": 9,
"type": "TextImage",
"pos": [
1448.4608623625004,
735.0514200906249
],
"size": {
"0": 315,
"1": 198
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "TextImage"
},
"widgets_values": [
"龍馬精神迎新歲",
"C:\\Users\\38957\\Documents\\ai-lab\\ComfyUI_windows_portable\\ComfyUI\\custom_nodes\\comfyui-mixlab-nodes\\assets\\王汉宗颜楷体繁.ttf",
100,
12,
"#000000",
true
]
},
{
"id": 2,
"type": "ChatGPTOpenAI",
"pos": [
1086,
235
],
"size": {
"0": 400,
"1": 358
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo",
0,
"randomize",
1,
null
]
},
{
"id": 16,
"type": "SpeechRecognition",
"pos": [
2892.502226325001,
724.176680771093
],
"size": {
"0": 322.593505859375,
"1": 135.22850036621094
},
"flags": {},
"order": 5,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
null
]
},
{
"id": 17,
"type": "SpeechSynthesis",
"pos": [
2901.502226325001,
924.1766807710948
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
""
]
},
{
"id": 18,
"type": "ShowLayer",
"pos": [
2157.913220668749,
232.7887044208986
],
"size": {
"0": 315,
"1": 226
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "layers",
"type": "LAYER",
"link": null
}
],
"properties": {
"Node name for S&R": "ShowLayer"
}
},
{
"id": 19,
"type": "NewLayer",
"pos": [
2494.913220668749,
232.7887044208986
],
"size": {
"0": 315,
"1": 218
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
},
{
"name": "mask",
"type": "MASK",
"link": null
},
{
"name": "layers",
"type": "LAYER",
"link": null
}
],
"outputs": [
{
"name": "layers",
"type": "LAYER",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "NewLayer"
},
"widgets_values": [
0,
0,
512,
512,
0,
"width"
]
},
{
"id": 20,
"type": "MergeLayers",
"pos": [
2163.913220668749,
515.7887044208991
],
"size": {
"0": 315.47509765625,
"1": 69.53152465820312
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "layers",
"type": "LAYER",
"link": null
},
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "MergeLayers"
}
},
{
"id": 4,
"type": "CharacterInText",
"pos": [
1778,
386
],
"size": {
"0": 319.45068359375,
"1": 194.45606994628906
},
"flags": {},
"order": 10,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "CharacterInText"
},
"widgets_values": [
"",
"",
1
]
},
{
"id": 21,
"type": "Note",
"pos": [
2507.333181606249,
517.1286262958992
],
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 11,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"TODO:可视化操作"
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 22,
"type": "Note",
"pos": [
1533,
347
],
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 12,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"funtion call"
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 11,
"type": "3DImage",
"pos": [
1794.6918193937495,
724.2820719460937
],
"size": {
"0": 309.2974548339844,
"1": 433.12554931640625
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "material",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "BG_IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "MATERIAL",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "3DImage"
}
},
{
"id": 13,
"type": "ScreenShare",
"pos": [
2138.691819393749,
728.2820719460937
],
"size": {
"0": 322.9458923339844,
"1": 594.0604858398438
},
"flags": {},
"order": 14,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "PROMPT",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
},
{
"id": 14,
"type": "FloatingVideo",
"pos": [
2486.691819393749,
726.2820719460937
],
"size": {
"0": 302.3232421875,
"1": 303.61279296875
},
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 6,
"type": "TransparentImage",
"pos": [
2496.691819393749,
1157.2820719460938
],
"size": {
"0": 315,
"1": 146
},
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
},
{
"name": "masks",
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "file_path",
"type": "STRING",
"links": null,
"shape": 6
},
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 6
},
{
"name": "RGBA",
"type": "RGBA",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "TransparentImage"
},
"widgets_values": [
"yes",
"yes",
"Mixlab_save"
]
},
{
"id": 12,
"type": "ImageCropByAlpha",
"pos": [
1806.6918193937495,
1240.2820719460938
],
"size": {
"0": 289.27215576171875,
"1": 46
},
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
},
{
"name": "RGBA",
"type": "RGBA",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ImageCropByAlpha"
}
},
{
"id": 5,
"type": "EnhanceImage",
"pos": [
1098.6918193937504,
1217.2820719460938
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 18,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "EnhanceImage"
},
"widgets_values": [
0.5
]
},
{
"id": 10,
"type": "SvgImage",
"pos": [
1454.6918193937504,
1074.2820719460938
],
"size": {
"0": 315,
"1": 170
},
"flags": {},
"order": 19,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "layers",
"type": "LAYER",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SvgImage"
},
"widgets_values": [
null,
null
]
}
],
"links": [],
"groups": [
{
"title": "GPT",
"bounding": [
1083,
157,
1039,
450
],
"color": "#b06634",
"font_size": 24,
"locked": false
},
{
"title": "Image",
"bounding": [
1086,
650,
1735,
682
],
"color": "#A88",
"font_size": 24,
"locked": false
},
{
"title": "Audio",
"bounding": [
2882,
650,
344,
342
],
"color": "#8A8",
"font_size": 24,
"locked": false
},
{
"title": "Layer",
"bounding": [
2148,
158,
672,
437
],
"color": "#a1309b",
"font_size": 24,
"locked": false
}
],
"config": {},
"extra": {},
"version": 0.4
}
+862
View File
@@ -0,0 +1,862 @@
{
"last_node_id": 28,
"last_link_id": 64,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
515,
130
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 33
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 6,
"type": "LoraLoader",
"pos": [
515,
460
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 31
},
{
"name": "clip",
"type": "CLIP",
"link": 45
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
44
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoraLoader"
},
"widgets_values": [
"lcm-lora-sdv1-5.safetensors",
1,
1
]
},
{
"id": 11,
"type": "VAEDecode",
"pos": [
1430,
130
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 35
},
{
"name": "vae",
"type": "VAE",
"link": 34
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
41
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 23,
"type": "VAEEncode",
"pos": [
515,
1092
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 57
},
{
"name": "vae",
"type": "VAE",
"link": 58
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
54
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
100,
130
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
31
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
32,
33,
45
],
"shape": 3,
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
34,
58
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
515,
1268
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 62,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
51
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
1274,
536
],
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 5,
"type": "KSampler",
"pos": [
1004,
130
],
"size": [
315,
262
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 51
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 29
},
{
"name": "latent_image",
"type": "LATENT",
"link": 54
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
758428944049342,
"fixed",
4,
1.6,
"lcm",
"karras",
0.61
]
},
{
"id": 24,
"type": "ScreenShare",
"pos": [
62,
543
],
"size": {
"0": 315,
"1": 194
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
55,
57
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [],
"shape": 3,
"slot_index": 1
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": [],
"shape": 3,
"slot_index": 2
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [
-416,
516
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 55
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 26,
"type": "ChatGPTOpenAI",
"pos": [
5,
1325
],
"size": [
400,
357.99993896484375
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 61,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
62,
63
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"你需要根据用户的输入,转化为英文单词的短句子组合,输出英文",
"gpt-35-turbo",
1249,
"randomize",
1,
null
]
},
{
"id": 28,
"type": "SpeechSynthesis",
"pos": [
542,
1570
],
"size": [
315,
76
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 63,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
"A very handsome elderly gentleman."
]
},
{
"id": 25,
"type": "SpeechRecognition",
"pos": [
-5,
1101
],
"size": [
392.5606180664065,
166.64923671875022
],
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
61
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
null
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
3,
6,
0,
5,
0,
"MODEL"
],
[
5,
8,
0,
5,
2,
"CONDITIONING"
],
[
6,
9,
0,
5,
3,
"LATENT"
],
[
7,
10,
0,
6,
0,
"MODEL"
],
[
8,
10,
1,
6,
1,
"CLIP"
],
[
9,
5,
0,
11,
0,
"LATENT"
],
[
10,
10,
2,
11,
1,
"VAE"
],
[
11,
6,
1,
7,
0,
"CLIP"
],
[
12,
6,
1,
8,
0,
"CLIP"
],
[
14,
11,
0,
3,
0,
"IMAGE"
],
[
17,
1,
2,
7,
1,
"STRING"
],
[
18,
7,
0,
15,
0,
"CONDITIONING"
],
[
19,
15,
0,
5,
1,
"CONDITIONING"
],
[
20,
16,
0,
15,
1,
"CONTROL_NET"
],
[
24,
1,
0,
18,
0,
"IMAGE"
],
[
25,
18,
0,
15,
2,
"IMAGE"
],
[
29,
7,
0,
5,
2,
"CONDITIONING"
],
[
31,
10,
0,
6,
0,
"MODEL"
],
[
32,
10,
1,
8,
0,
"CLIP"
],
[
33,
10,
1,
7,
0,
"CLIP"
],
[
34,
10,
2,
11,
1,
"VAE"
],
[
35,
5,
0,
11,
0,
"LATENT"
],
[
41,
11,
0,
20,
0,
"IMAGE"
],
[
44,
6,
0,
5,
0,
"MODEL"
],
[
45,
10,
1,
6,
1,
"CLIP"
],
[
51,
8,
0,
5,
1,
"CONDITIONING"
],
[
54,
23,
0,
5,
3,
"LATENT"
],
[
55,
24,
0,
2,
0,
"IMAGE"
],
[
57,
24,
0,
23,
0,
"IMAGE"
],
[
58,
10,
2,
23,
1,
"VAE"
],
[
61,
25,
0,
26,
0,
"STRING"
],
[
62,
26,
0,
8,
1,
"STRING"
],
[
63,
26,
0,
28,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}