Compare commits

..
94 Commits
Author SHA1 Message Date
yolain 34882bca10 Bump Version 2025-08-08 19:01:55 +08:00
yolain 43b94be806 Update ImageChooser frontend code 2025-08-08 18:58:59 +08:00
yolain 0349d81694 Revamp ImageChooser and removed Preview&Choose in easy samplers #838 2025-08-08 18:54:24 +08:00
yolain 93254a4c07 Can replace the default fooocus_styles with files of the same name under styles dir. 2025-08-06 17:13:22 +08:00
yolain 8485447325 Fix globalSeed to work with Partial Execution #844 2025-08-06 17:12:06 +08:00
yolain 717092a3ce Add easy loraPromptApply 2025-07-26 19:39:01 +08:00
yolain 14a1121860 Add easy loraSwitcher 2025-07-26 17:20:39 +08:00
yolain 8c1eec2858 Fix front-end v1.24.2 and later failed to display the latent preview image in easy kSamplers during initial sampling. 2025-07-24 19:04:51 +08:00
yolain b6bb4a3055 Fixed segformer_b3_clothes download link error #831 2025-07-17 14:34:16 +08:00
yolain 6873492872 Fixed getStylesList value transfer error 2025-07-15 18:24:18 +08:00
yolain 2d71b3e647 Update StylesSelector 2025-07-15 18:02:12 +08:00
yolain e7320ec0c4 Remove easy showAnything error messages #776 2025-07-12 21:13:15 +08:00
yolainandyolain 560be6aee7 Update HumanSegmentation (#826)
* Change to new mask components on humanSegmentation

* Change segformer index

* Add segformer_b3_clothes and fashion

* Add face_parsing

* Fix face_parsing output error images

---------

Co-authored-by: yolain <me@yolain.com>
2025-07-10 18:32:06 +08:00
yolain 54614079ca Fixed makeImageForIcLora issue that occurred when the heights of two images were the same during image stitching on. 2025-07-05 13:48:08 +08:00
yolain b0cd0bcb5b Add easy joyCaption3API 2025-07-05 00:03:12 +08:00
yolain e46f8a45d0 Add easy promptAwait node (#818)
* Add submodule

* Set submodule branch to main

* Add PromptAwait node

* Fix loop has started for the second time, but the prompt word has not output new content

* Add input_1

* Change select widget to toolbar

* Modify some field names and displays

* Change select max-width

* Add output random seed in promptAwait
2025-06-30 01:22:13 +08:00
yolain 1616dd6602 Upgrade v1.3.1 to ComfyRegistry 2025-06-29 11:43:22 +08:00
yolain 282eedfea6 Rewrite drawNodeWidget and fix the GroupNode preview issue 2025-06-28 18:37:56 +08:00
yolain 17b163e234 Fix typo in EN tooltip for Nodes Map sidebar icon #816 2025-06-26 16:43:19 +08:00
Laegel 501d97bb5c chore: Now able to store metadata in ImageChooser (#813) 2025-06-23 16:22:23 +08:00
WathomeBo de92038f88 Update util.py (#809)
补充了用于选择lora模型的 setLoraName
2025-06-17 12:19:42 +08:00
Thomas Ward 530333d72d Update logic.py: properly handle overwrite mode (#807)
In low-level `OPEN` logic at the system, there are two modes of opening files for writing: `WRITE` which clobbers existing file data, and `APPEND` which allows appending of data.

In the current code, using `if not overwrite: pass` does nothing to define if you're actually appending or overwriting the file in your selection, and instead you should define the file open mode based on analysis of whether you have `overwrite` set to True or not.

This code patch does this.

(discovered as a result of helping someone via the ComfyUI discord)
2025-06-17 12:19:27 +08:00
yolain 041f49540c Forced override of drawNodeWidget for nodes containing hidden widget on the official theme #801 2025-06-14 14:22:54 +08:00
MakinoHaruka 71c7865d2d locale typo (#797) 2025-06-05 10:55:11 +08:00
yolain c7fbf05970 Implement error handling for all frontend hijack attempts. If EasyUse fails (e.g., due to official frontend changes), fall back to the native callback function. 2025-06-04 23:28:45 +08:00
yolain 2986a01469 When using the easy theme to draw node components, the draw method removed from front-end v1.21.3 is supplemented #793 2025-06-03 13:27:18 +08:00
yolain fa7c5d8b4d Fix ImagePreviewWidget can not display image in v1.21.3 frontend 2025-06-01 15:35:52 +08:00
Mike KinneyandMike Kinney 1d8db7510b Update XY Plot Labels (#792)
* Add lora weight to XY title axis. Trim lora desc.

* Add weights to lora names in xyPlots. Re-add because original lost in git merge mistake.

* Only add common label, if it's not an axis type.

* Remove bad comment.

---------

Co-authored-by: Mike Kinney <mike.kinney@valorepartners.com>
2025-06-01 13:15:55 +08:00
yolain 7ef0612ce7 Fix stepping changes not working in new front-end versions #789 2025-05-27 18:04:15 +08:00
yolain 7ff4790493 Fix update node height only if the node is preSamplingcustom listening for scheduler changes #788 2025-05-27 12:26:39 +08:00
yolain 640ef31625 Fix uniform width didn't work when sizes were inconsistent 2025-05-26 13:24:09 +08:00
yolain e4ac947d96 Fix precision issues with nodes related to float numbers #779 2025-05-22 11:06:52 +08:00
yolain d287e28e5c Fix fluxLoader using widget options instead of getting ckpt_names globally #772 2025-05-19 12:42:36 +08:00
yolain f33c17f762 Fix fluxLoader duplicate fetching of node information #772 2025-05-19 11:02:56 +08:00
yolain e07b8cc7bf Fix widgets being hidden in connections 2025-05-18 13:34:42 +08:00
yolain 6abe07bb79 Merge pull request #766 from mekinney/bugfix-xyplot-optional-lora
Use previously generated model/clip for next loaded lora
2025-05-15 16:27:31 +08:00
Mike Kinney 7fbd03bda7 Merge branch 'main' into bugfix-xyplot-optional-lora 2025-05-14 07:20:08 -07:00
Mike Kinney 9cc2ac02da Use previously generated model/clip for next loaded lora 2025-05-14 06:27:36 -07:00
Mike Kinney 2f2a3035a2 Merge pull request #3 from mekinney/bug-xyplot-save-model-and-clip-when-processing-lora-stack-in-xyplot
Update clip and model when adding loras
2025-05-13 16:52:23 -07:00
Mike Kinney 5d8f0a3b0a Update clip and model when adding loras 2025-05-13 16:49:49 -07:00
Mike Kinney 8aadd72494 Merge pull request #2 from mekinney/Change-Load-LORA-formatting-to-2-digits
Updated formatting for load lora for strength displays to 3 digits
2025-05-13 07:05:24 -07:00
Mike Kinney fceec754a4 Updated formatting for load lora for strength displays to 3 digits 2025-05-13 07:03:59 -07:00
Mike Kinney 419b7c985c Merge pull request #1 from mekinney/XYPlot-Footer
Add core XYPlot Footer
2025-05-13 06:51:23 -07:00
Mike Kinney ce62fc73da Add core XYPlot Footer 2025-05-13 06:35:45 -07:00
yolain 4f31641da3 Adding text truncation to widgets of type text 2025-05-13 12:35:31 +08:00
yolain deec62ab76 Set the minimum height of the initial display when imageChooser is paused. #755 2025-05-12 11:43:44 +08:00
yolain 5c8cdb58c7 Add easy seedList node (It's useful for in loops) 2025-05-11 00:43:27 +08:00
yolain d820842e39 Upgrade v1.3.0 to ComfyRegistry 2025-05-10 00:02:13 +08:00
yolain 2f78a523b3 Fix easy imageConcat match image size bug 2025-05-10 00:01:03 +08:00
yolain b2a8666423 Fix easy humanSegmentation not being selected 2025-05-09 16:10:33 +08:00
yolain 2c02a471d0 Fix latest commit #758 2025-05-09 07:46:20 +08:00
yolain ea521e0303 Set loop nodes maximum number of inputs or outputs to 20 2025-05-09 00:50:13 +08:00
yolain 9b5daac023 Fix cannot redefine property: value 2025-05-08 15:17:56 +08:00
yolain 0de83f88dc Force default web version to v2 2025-05-06 16:15:06 +08:00
yolain 342ce8ccad Fix last commit 2025-05-06 14:04:42 +08:00
yolain b7881d84b1 Add uniform width method to easy makeImageForICLora 2025-05-06 14:01:11 +08:00
yolain 0f5ad38384 Add apikey_override to easy joycaption2API 2025-05-06 12:56:07 +08:00
yolain a3f487c822 Merge pull request #752 from yolain/wildcardsPromptMatrix
Add output_limit to `easy wildcardsMatrix`
2025-04-30 18:03:08 +08:00
yolain d0f496adc1 Add output_limit to easy wildcardsMatrix 2025-04-30 18:01:14 +08:00
yolain 665861ff35 Merge wildcardsPromptMatrix Node from Rosmeowtis/main 2025-04-29 12:09:00 +08:00
yolain 61568e021c Update wildcardsPromptMatrix Node #743 2025-04-29 12:03:54 +08:00
yolain a2edc37d89 Merge pull request #743 from Rosmeowtis/main
Add wildcardsPromptMatrix Node
2025-04-29 11:37:02 +08:00
yolain 1c4cb43f7b Fix line are removed at the end of a connection on easy related nodes #748 2025-04-28 19:11:42 +08:00
yolain 66143b0e20 Merge pull request #746 from Hapseleg/Pipe-info-fix
missing vars
2025-04-27 11:03:03 +08:00
Hapseleg f0da5e25c9 missing vars 2025-04-26 20:03:18 +02:00
rosmeowtis ebf25b585f update descriptions to conform to reality 2025-04-25 18:10:35 +08:00
rosmeowtis fb8968d438 wildcardsPromptMatrix node will treat offset in cycle 2025-04-25 18:06:07 +08:00
rosmeowtis 15cfeedf7b Add wildcardsPromptMatrix Node:
1. wildcard-replaced prompt will be returned in order rather than randomly
2. will return the amount of probilities and amount of probilities each option or wildcard
3. the prompt can be selected by offset argument, the order of probilities is fixed
4. even if the offset exceeds the total, it will not stop, but will always return to the last probility, requiring additional nodes to control.
2025-04-25 03:07:49 +08:00
yolain 50ae13a993 Fix hidden item judgment needs to be delayed when first loading the page #741 2025-04-24 14:36:56 +08:00
yolain aedf917067 Remove getModelsThumbnail API #702 2025-04-22 13:48:14 +08:00
yolain 69ac5e52a0 EasyUse still works when layerDiffuse-related diffusers error 2025-04-21 19:56:17 +08:00
yolain 368f7e508d EasyUse still works when brushnet-related diffusers error 2025-04-21 11:00:38 +08:00
yolain 44f0676323 Fix an issue where some widgets' associated input sockets fail to display in the new release #734 2025-04-16 22:21:20 +08:00
yolain 98273b37f2 Fix Easy KSamplers preview&choose bug #733 2025-04-16 18:39:28 +08:00
yolain eff718c13f Upgrade v1.2.9 to ComfyRegistry 2025-04-15 14:20:58 +08:00
yolain 615a2abcfe Fix ImageChooser causes workflow processing to cancel #732 2025-04-15 13:28:53 +08:00
yolain 2b4b38ce03 Fix brushnet tensor(640) error 2025-04-13 02:11:55 +08:00
yolain dbbd2ffef3 Fix missing output optional_clip when Apply Lora Stack is disabled #729 2025-04-13 01:50:34 +08:00
yolain b1a875b151 Fix last commit bug 2025-04-08 00:59:38 +08:00
yolain 6a39ea1188 Fix widgets not hidden in v1.6.0 frontend 2025-04-08 00:49:37 +08:00
yolain 69aac075e8 Compatible drawNodeShape with stable front-end version 2025-04-06 15:53:48 +08:00
yolain e1dc9250b9 Fix missing strokeStyle on nodes during restart resulting in misconnections. 2025-04-06 15:46:59 +08:00
yolain 8b9c577f55 Fix outer border color should be red when node doesn't exist 2025-04-06 13:31:48 +08:00
yolain 6d8c266b04 Fix missing progressBar in latest frontend version 2025-04-06 12:48:41 +08:00
yolain 9292f22862 Removed global changes to the control widget, ComfyUI frontend was fixed this issue #714 2025-03-30 12:59:50 +08:00
yolain 10e9629ca3 Fix the context menu to miss Add Reroute #713 2025-03-29 07:23:35 +08:00
yolain 4f694195a2 Fix samplers can't display output image 2025-03-27 15:22:02 +08:00
yolain ff6c0f0e39 Fix image chooser can not select images #706 2025-03-26 11:37:42 +08:00
yolain a6e8783605 Fix save image simple doesn't show preview #708 2025-03-26 10:39:26 +08:00
yolain 3e84b8cd77 Fix contextMenu monkey patching to affect custom scripts (pysssss) nodes 2025-03-17 09:36:49 +08:00
yolain 9e70cc0090 Merge pull request #697 from Naix2012/main
Update prompt.py
2025-03-16 12:02:43 +08:00
Naix2012 7dddd2d6e5 Update prompt.py 2025-03-16 01:16:43 +08:00
yolain 63a1ca5ec6 Merge pull request #693 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-03-14 15:06:39 +08:00
snomiao 17e022a7aa chore(publish): update GitHub Actions workflow for node publishing
- Add permissions for issue writing
- Update action version to v1 for publish-node-action
- Add condition to run job only for 'yolain' repository owner
2025-01-20 21:28:03 +00:00
48 changed files with 3498 additions and 2308 deletions
+6 -2
View File
@@ -7,15 +7,19 @@ on:
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'yolain' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
-1
View File
@@ -9,7 +9,6 @@ workflow/**
autocomplete/**
web_beta/**
web_version/dev/**
ComfyUI-Easy-Use-Frontend/
docs/**
.vscode/
.idea/
+4
View File
@@ -0,0 +1,4 @@
[submodule "ComfyUI-Easy-Use-Frontend"]
path = ComfyUI-Easy-Use-Frontend
url = https://github.com/yolain/ComfyUI-Easy-Use-Frontend.git
branch = main
+29 -1
View File
@@ -52,6 +52,35 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
## 📜 更新日志
**v1.3.2**
- 改造 `easy imageChooser` 节点以兼容 frontend>=v1.24.2, 解决方案参考自 [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
- 改造 `easy stylesSelector` 节点, 你可在 [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) 下载到 `styles` 文件夹下
- 改造 `easy humanSegmentation` 节点
- 修复 `easy makeImageForICLora` 节点.
- 添加 `easy joycaption3API` 节点
- 添加 `easy promptAwait` 节点
**v1.3.1**
- 重写 drawNodeWidget 修复组节点预览的问题.
- 更新了一些 XYPlot 的功能 by [mekinney](https://github.com/mekinney)
- 添加 `easy seedList` 节点 (它对循环节点有用)
**v1.3.0**
- 将循环节点设置为最大输入和输出数量为20
- 添加 `uniform width` 方式到 `easy makeImageForICLora`
- 增加 `wildcardsPromptMatrix` 通配符提示词矩阵,由 [Rosmeowtis](https://github.com/Rosmeowtis) 贡献
**v1.2.9**
- 修复 Imagechooser 会导致工作流处理取消
- 修复 brushnet tensor(640) 错误
- 修复v1.6.0前端之后无法隐藏小部件的bug
- 修复图像选择器无法选择图像
- 修复ContextMenu Monkey修补以影响自定义脚本(PYSSSS)节点
**v1.2.8**
- 修复了一些BUG (😹)
@@ -501,7 +530,6 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
**Comfyui-Easy-Use** 是一个 GPL 许可的开源项目。为了项目取得更好、可持续的发展,我希望能够获得更多的支持。 如果我的自定义节点为您的一天增添了价值,请考虑喝杯咖啡来进一步补充能量! 💖感谢您的支持,每一杯咖啡都是我创作的动力!
- [BiliBili充电](https://space.bilibili.com/1840885116)
- [爱发电](https://afdian.com/a/yolain)
- [Wechat/Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
感谢您的捐助,我将用这些费用来租用 GPU 或购买其他 GPT 服务,以便更好地调试和完善 ComfyUI-Easy-Use 功能
+29 -1
View File
@@ -47,6 +47,35 @@ Double-click install.bat to install the required dependencies
## 📜 Changelog
**v1.3.2**
- Revamp `easy imageChooser` node to adapt frontend>=v1.24.2, solution referenced from [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
- Revamp `easy stylesSelector` node, and you can download [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) to the `styles` folder
- Revamp `easy humanSegmentation` node
- Fix `easy makeImageForICLora` node issue, that occurred when the heights of two images were the same during image stitching on.
- Add `easy joycaption3API` node
- Add `easy promptAwait` node
**v1.3.1**
- Rewrite drawNodeWidget and fix the GroupNode preview issue.
- Updated some features of XYPlot by [mekinney](https://github.com/mekinney)
- Add `easy seedList` node (It's useful for in loops)
**v1.3.0**
- Set loop nodes maximum number of inputs and outputs to 20
- Add `uniform width` method to `easy makeImageForICLora`
- Add `wildcardsPromptMatrix` Node by [Rosmeowtis](https://github.com/Rosmeowtis)
**v1.2.9**
- Fix ImageChooser causes workflow processing to cancel
- Fix brushnet tensor(640) error
- Fix widgets not hidden after v1.6.0 frontend
- Fix image chooser can not select images
- Fix contextMenu monkey patching to affect custom scripts (pysssss) nodes
**v1.2.8**
- Added the multi-language catalog
@@ -485,7 +514,6 @@ If my custom nodes has added value to your day, consider indulging in a coffee t
💖You can support me in any of the following ways:
- [BiliBili](https://space.bilibili.com/1840885116)
- [Afdian](https://afdian.com/a/yolain)
- [Wechat / Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
## 🌟Stargazers
+2 -31
View File
@@ -1,4 +1,4 @@
__version__ = "1.2.8"
__version__ = "1.3.2"
import yaml
import json
@@ -15,36 +15,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
importlib.import_module('.py.routes', __name__)
importlib.import_module('.py.server', __name__)
nodes_list = ["util", "seed", "prompt", "loaders", "adapter", "inpaint", "preSampling", "samplers", "fix", "pipe", "xyplot", "image", "logic", "api", "deprecated"]
# locale = {}
for module_name in nodes_list:
imported_module = importlib.import_module(".py.nodes.{}".format(module_name), __name__)
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
# transfer python nodes to locale file
# for i in imported_module.NODE_CLASS_MAPPINGS:
# locale[i] = {
# "display_name": imported_module.NODE_DISPLAY_NAME_MAPPINGS[i] if i in imported_module.NODE_DISPLAY_NAME_MAPPINGS else i,
# "inputs":{},
# "outputs":{},
# }
# node_class = imported_module.NODE_CLASS_MAPPINGS[i]
# input_types = node_class.INPUT_TYPES()
# if "required" in input_types:
# for j in input_types["required"]:
# locale[i]['inputs'][j] = {"name": j}
# if "optional" in input_types:
# for j in input_types["optional"]:
# locale[i]['inputs'][j] = {"name": j}
# count = 0
# if "RETURN_NAMES" in node_class.__dict__:
# for j in node_class.RETURN_NAMES:
# locale[i]['outputs'][str(count)] = {"name": j}
# count+=1
# en_json_path = os.path.join(cwd_path,'locales/en/nodeDefs.json')
# with open(en_json_path, 'w', encoding='utf-8') as f:
# json.dump(locale, f, ensure_ascii=False, indent=2)
#Wildcards
from .py.libs.wildcards import read_wildcard_dict
@@ -86,11 +60,8 @@ if not os.path.exists(example_path):
with open(example_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
# get comfyui revision
from .py.libs.utils import compare_revision
new_frontend_revision = 2546
web_default_version = 'v2' if compare_revision(new_frontend_revision) else 'v1'
web_default_version = 'v2'
# web directory
config_path = os.path.join(cwd_path, "config.yaml")
if os.path.isfile(config_path):
+2 -1
View File
@@ -2,7 +2,8 @@
"settingsCategories": {
"Hotkeys": "Hotkeys",
"Nodes": "Nodes",
"NodesMap": "NodesMap"
"NodesMap": "NodesMap",
"StylesSelector": "StylesSelector"
},
"nodeCategories": {
"Util": "Util",
+29
View File
@@ -145,6 +145,35 @@
}
}
},
"easy wildcardsMatrix": {
"display_name": "Wildcards Matrix",
"inputs": {
"Select to add LoRA": {
"name": "Select to add LoRA"
},
"Select to add Wildcard": {
"name": "Select to add Wildcard"
},
"offset": {
"name": "Offset in All Probilities"
},
"output_limit": {
"name": "Output Limit",
"tooltip": "Output n fill wildcards, -1 is output all possibilities (force offset to zero), the default value is 1"
}
},
"outputs": {
"0": {
"name": "Replaced Prompt"
},
"1": {
"name": "Total Count of the Probilities"
},
"2": {
"name": "Factors"
}
}
},
"easy prompt": {
"display_name": "Prompt",
"inputs": {
+2 -1
View File
@@ -2,7 +2,8 @@
"settingsCategories": {
"Hotkeys": "快捷键",
"Nodes": "节点相关",
"NodesMap": "管理节点组"
"NodesMap": "管理节点组",
"StylesSelector": "样式选择器"
},
"nodeCategories": {
"Util": "工具",
+182 -125
View File
@@ -75,6 +75,35 @@
}
}
},
"easy wildcardsMatrix": {
"display_name": "通配符提示词矩阵",
"inputs": {
"Select to add LoRA": {
"name": "选择添加Lora"
},
"Select to add Wildcard": {
"name": "选择添加通配符"
},
"offset": {
"name": "偏移量"
},
"output_limit": {
"name": "输出个数限制",
"tooltip": "输出n个填充后通配符, -1为输出所有可能性(偏移值失效),默认值为1"
}
},
"outputs": {
"0": {
"name": "通配填充词"
},
"1": {
"name": "可能性总数"
},
"2": {
"name": "可能性总数(每通配符)"
}
}
},
"easy prompt": {
"display_name": "提示词",
"inputs": {
@@ -136,6 +165,35 @@
}
}
},
"easy promptAwait": {
"display_name": "提示词等待",
"inputs": {
"now": {
"name": "当前"
},
"prev": {
"name": "上一步"
},
"prompt": {
"name": "提示词",
"placeholder": "输入提示词或使用语音输入转文字"
}
},
"outputs": {
"0": {
"name": "输出"
},
"1": {
"name": "提示词"
},
"2": {
"name": "继续"
},
"3": {
"name": "随机种"
}
}
},
"easy promptConcat": {
"display_name": "提示词联结",
"inputs": {
@@ -1096,6 +1154,31 @@
}
}
},
"easy loraSwitcher": {
"display_name": "简易Lora切换器",
"inputs": {
"optional_lora_stack": {
"name": "LoRA堆(可选)"
},
"toggle": {
"name": "开关"
},
"select": {
"name": "选择项"
},
"num_loras": {
"name": "LoRA数量"
}
},
"outputs": {
"0": {
"name": "LoRA堆"
},
"1": {
"name": "LoRA名称"
}
}
},
"easy controlnetStack": {
"display_name": "简易 ControlNet 堆",
"inputs": {
@@ -1727,6 +1810,35 @@
}
}
},
"easy seedList": {
"display_name": "随机种列表",
"description": "可用于for循环的随机数种子列表,通过与easy forLoopStart节点的索引与easy indexAny节点相连接可实现在循环中使用不同种子值进行采样",
"inputs": {
"min_num": {
"name": "最小值"
},
"max_num": {
"name": "最大值"
},
"method": {
"name": "生成方式"
},
"total": {
"name": "总量"
},
"seed": {
"name": "列表序号"
}
},
"outputs": {
"0": {
"name": "随机种"
},
"1": {
"name": "总量"
}
}
},
"easy globalSeed": {
"display_name": "全局随机种",
"inputs": {
@@ -4216,6 +4328,37 @@
}
}
},
"easy loraPromptApply":{
"display_name": "应用提示词LoRA",
"inputs": {
"model": {
"name": "模型"
},
"clip": {
"name": "CLIP"
},
"positive":{
"name": "正面提示词"
},
"negative":{
"name": "负面提示词"
}
},
"outputs":{
"0": {
"name": "模型"
},
"1": {
"name": "CLIP"
},
"2": {
"name": "正面提示词"
},
"3": {
"name": "负面提示词"
}
}
},
"easy loraStackApply": {
"display_name": "应用LoRA堆",
"inputs": {
@@ -5691,6 +5834,9 @@
},
"pixels": {
"name": "限制像素"
},
"method": {
"name": "限制方式"
}
},
"outputs": {
@@ -6052,73 +6198,13 @@
"easy whileLoopStart": {
"display_name": "While循环-开始",
"inputs": {
"initial_value0": {
"name": "初始值0"
},
"initial_value1": {
"name": "初始值1"
},
"initial_value2": {
"name": "初始值2"
},
"initial_value3": {
"name": "初始值3"
},
"initial_value4": {
"name": "初始值4"
},
"initial_value5": {
"name": "初始值5"
},
"initial_value6": {
"name": "初始值6"
},
"initial_value7": {
"name": "初始值7"
},
"initial_value8": {
"name": "初始值8"
},
"initial_value9": {
"name": "初始值9"
},
"condition": {
"name": "条件"
"name": "开始循环"
}
},
"outputs": {
"0": {
"name": "开始"
},
"1": {
"name": "值0"
},
"2": {
"name": "值1"
},
"3": {
"name": "值2"
},
"4": {
"name": "值3"
},
"5": {
"name": "值4"
},
"6": {
"name": "值5"
},
"7": {
"name": "值6"
},
"8": {
"name": "值7"
},
"9": {
"name": "值8"
},
"10": {
"name": "值9"
}
}
},
@@ -6128,71 +6214,11 @@
"flow": {
"name": "结束"
},
"initial_value0": {
"name": "初始值0"
},
"initial_value1": {
"name": "初始值1"
},
"initial_value2": {
"name": "初始值2"
},
"initial_value3": {
"name": "初始值3"
},
"initial_value4": {
"name": "初始值4"
},
"initial_value5": {
"name": "初始值5"
},
"initial_value6": {
"name": "初始值6"
},
"initial_value7": {
"name": "初始值7"
},
"initial_value8": {
"name": "初始值8"
},
"initial_value9": {
"name": "初始值9"
},
"condition": {
"name": "条件"
"name": "继续循环"
}
},
"outputs": {
"0": {
"name": "值0"
},
"1": {
"name": "值1"
},
"2": {
"name": "值2"
},
"3": {
"name": "值3"
},
"4": {
"name": "值4"
},
"5": {
"name": "值5"
},
"6": {
"name": "值6"
},
"7": {
"name": "值7"
},
"8": {
"name": "值8"
},
"9": {
"name": "值9"
}
}
},
"easy forLoopStart": {
@@ -6333,7 +6359,7 @@
"name": "高度"
},
"scale": {
"name": "缩放洗漱"
"name": "缩放系数"
},
"flip_w/h": {
"name": "翻转宽高"
@@ -6650,7 +6676,38 @@
"name": "温度"
},
"max_tokens": {
"name": "最大词令牌数"
"name": "最大词元数"
},
"caption_type": {
"name": "提示词类型"
},
"caption_length": {
"name": "提示词长度"
},
"name_input": {
"name": "名称输入"
}
},
"outputs": {
"0": {
"name": "提示词"
}
}
},
"easy joyCaption3API": {
"display_name": "JoyCaption3(硅基流动)",
"inputs": {
"image": {
"name": "图像"
},
"do_sample": {
"name": "执行采样"
},
"temperature": {
"name": "温度"
},
"max_tokens": {
"name": "最大词元数"
},
"caption_type": {
"name": "提示词类型"
@@ -6676,4 +6733,4 @@
}
}
}
}
}
+8
View File
@@ -63,5 +63,13 @@
"EasyUse_NodesMap_Enable": {
"name": "启用管理节点组",
"tooltip": "您需要刷新页面以成功更新"
},
"EasyUse_StylesSelector_DisplayType": {
"name": "样式选择器显示类型",
"tooltip": "样式选择器显示类型,如果设置为“网格”,则显示为网格,如果设置为“列表”,则显示为列表",
"options": {
"Gird": "网格",
"List": "列表"
}
}
}
+9
View File
@@ -370,6 +370,15 @@ HUMANPARSING_MODELS = {
},
"human-parts":{
"model_url":"https://huggingface.co/Metal3d/deeplabv3p-resnet50-human/resolve/main/deeplabv3p-resnet50-human.onnx",
},
"segformer_b3_clothes":{
"model_name": "sayeed99/segformer_b3_clothes",
},
"segformer_b3_fashion":{
"model_name": "sayeed99/segformer-b3-fashion",
},
"face_parsing":{
"model_name": "jonathandinu/face-parsing"
}
}
+8 -4
View File
@@ -56,10 +56,14 @@ class BizyAIRAPI:
f"Failed to connect to the server: {e}, if you have no key, "
)
# joycaptionTwo
def joyCaption2(self, payload, image):
api_key = self.getAPIKey()
url = f"{self.base_url}/supernode/joycaption2"
# joycaption
def joyCaption(self, payload, image, apikey_override=None, API_URL='/supernode/joycaption2'):
if apikey_override is not None:
api_key = apikey_override
else:
api_key = self.getAPIKey()
url = f"{self.base_url}{API_URL}"
print('Sending request to:', url)
auth = f"Bearer {api_key}"
headers = {
"accept": "application/json",
+134 -38
View File
@@ -1,52 +1,148 @@
from threading import Event
from server import PromptServer
from aiohttp import web
from comfy import model_management as mm
import time
class ChooserCancelled(Exception):
pass
class ChooserMessage:
stash = {}
messages = {}
cancelled = False
def get_chooser_cache():
"""获取选择器缓存"""
if not hasattr(PromptServer.instance, '_easyuse_chooser_node'):
PromptServer.instance._easyuse_chooser_node = {}
return PromptServer.instance._easyuse_chooser_node
@classmethod
def addMessage(cls, id, message):
if message == '__cancel__':
cls.messages = {}
cls.cancelled = True
elif message == '__start__':
cls.messages = {}
cls.stash = {}
cls.cancelled = False
else:
cls.messages[str(id)] = message
def cleanup_session_data(node_id):
"""清理会话数据"""
node_data = get_chooser_cache()
if node_id in node_data:
session_keys = ["event", "selected", "images", "total_count", "cancelled"]
for key in session_keys:
if key in node_data[node_id]:
del node_data[node_id][key]
def wait_for_chooser(id, images, mode, period=0.1):
try:
node_data = get_chooser_cache()
if mode == "Keep Last Selection":
if id in node_data and "last_selection" in node_data[id]:
last_selection = node_data[id]["last_selection"]
if last_selection and len(last_selection) > 0:
valid_indices = [idx for idx in last_selection if 0 <= idx < len(images)]
if valid_indices:
try:
PromptServer.instance.send_sync("easyuse-image-keep-selection", {
"id": id,
"selected": valid_indices
})
except Exception as e:
pass
cleanup_session_data(id)
indices_str = ','.join(str(i) for i in valid_indices)
return {"result": ([images[idx] for idx in valid_indices],)}
if id in node_data:
del node_data[id]
event = Event()
node_data[id] = {
"event": event,
"images": images,
"selected": None,
"total_count": len(images),
"cancelled": False,
}
while id in node_data:
node_info = node_data[id]
if node_info.get("cancelled", False):
cleanup_session_data(id)
raise ChooserCancelled("Manual selection cancelled")
if "selected" in node_info and node_info["selected"] is not None:
break
@classmethod
def waitForMessage(cls, id, period=0.1, asList=False):
sid = str(id)
while not (sid in cls.messages) and not ("-1" in cls.messages):
if cls.cancelled:
cls.cancelled = False
raise ChooserCancelled()
time.sleep(period)
if cls.cancelled:
cls.cancelled = False
raise ChooserCancelled()
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
try:
if asList:
return [int(x.strip()) for x in message.split(",")]
if id in node_data:
node_info = node_data[id]
selected_indices = node_info.get("selected")
if selected_indices is not None and len(selected_indices) > 0:
valid_indices = [idx for idx in selected_indices if 0 <= idx < len(images)]
if valid_indices:
selected_images = [images[idx] for idx in valid_indices]
if id not in node_data:
node_data[id] = {}
node_data[id]["last_selection"] = valid_indices
cleanup_session_data(id)
indices_str = ','.join(str(i) for i in valid_indices)
return {"result": (selected_images, indices_str)}
else:
cleanup_session_data(id)
return {"result": ([images[0]] if len(images) > 0 else [], "0" if len(images) > 0 else "")}
else:
return int(message.strip())
except ValueError:
print(
f"ERROR IN IMAGE_CHOOSER - failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}")
return [1] if asList else 1
cleanup_session_data(id)
return {
"result": ([images[0]] if len(images) > 0 else [],)}
else:
return {"result": ([images[0]] if len(images) > 0 else [],)}
except ChooserCancelled:
raise mm.InterruptProcessingException()
except Exception as e:
node_data = get_chooser_cache()
if id in node_data:
cleanup_session_data(id)
if 'image_list' in locals() and len(images) > 0:
return {"result": ([images[0]])}
else:
return {"result": ([])}
@PromptServer.instance.routes.post('/easyuse/image_chooser_message')
async def make_image_selection(request):
post = await request.post()
ChooserMessage.addMessage(post.get("id"), post.get("message"))
return web.json_response({})
async def handle_image_selection(request):
try:
data = await request.json()
node_id = data.get("node_id")
selected = data.get("selected", [])
action = data.get("action")
node_data = get_chooser_cache()
if node_id not in node_data:
return web.json_response({"code": -1, "error": "Node data does not exist"})
try:
node_info = node_data[node_id]
if "total_count" not in node_info:
return web.json_response({"code": -1, "error": "The node has been processed"})
if action == "cancel":
node_info["cancelled"] = True
node_info["selected"] = []
elif action == "select" and isinstance(selected, list):
valid_indices = [idx for idx in selected if isinstance(idx, int) and 0 <= idx < node_info["total_count"]]
if valid_indices:
node_info["selected"] = valid_indices
node_info["cancelled"] = False
else:
return web.json_response({"code": -1, "error": "Invalid Selection Index"})
else:
return web.json_response({"code": -1, "error": "Invalid operation"})
node_info["event"].set()
return web.json_response({"code": 1})
except Exception as e:
if node_id in node_data and "event" in node_data[node_id]:
node_data[node_id]["event"].set()
return web.json_response({"code": -1, "message": "Processing Failed"})
except Exception as e:
return web.json_response({"code": -1, "message": "Request Failed"})
+29 -1
View File
@@ -125,7 +125,35 @@ class ResizeMode(Enum):
return 2
assert False, "NOTREACHED"
# credit by https://github.com/chflame163/ComfyUI_LayerStyle/blob/main/py/imagefunc.py#L591C1-L617C22
def fit_resize_image(image: Image, target_width: int, target_height: int, fit: str, resize_sampler: str,
background_color: str = '#000000') -> Image:
image = image.convert('RGB')
orig_width, orig_height = image.size
if image is not None:
if fit == 'letterbox':
if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑
fit_width = target_width
fit_height = int(target_width / orig_width * orig_height)
else: # 更瘦,左右留黑
fit_height = target_height
fit_width = int(target_height / orig_height * orig_width)
fit_image = image.resize((fit_width, fit_height), resize_sampler)
ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color)
ret_image.paste(fit_image, box=((target_width - fit_width) // 2, (target_height - fit_height) // 2))
elif fit == 'crop':
if orig_width / orig_height > target_width / target_height: # 更宽,裁左右
fit_width = int(orig_height * target_width / target_height)
fit_image = image.crop(
((orig_width - fit_width) // 2, 0, (orig_width - fit_width) // 2 + fit_width, orig_height))
else: # 更瘦,裁上下
fit_height = int(orig_width * target_height / target_width)
fit_image = image.crop(
(0, (orig_height - fit_height) // 2, orig_width, (orig_height - fit_height) // 2 + fit_height))
ret_image = fit_image.resize((target_width, target_height), resize_sampler)
else:
ret_image = image.resize((target_width, target_height), resize_sampler)
return ret_image
# CLIP反推
import comfy.utils
+5 -1
View File
@@ -351,7 +351,7 @@ class easyLoader:
lora_path = None
if lora_path is not None:
log_node_info("Load LORA",f"{lora_name}: {model_strength}, {clip_strength}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
log_node_info("Load LORA",f"{lora_name}: model={model_strength:.3f}, clip={clip_strength:.3f}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
if lbw:
lbw = lora["lbw"]
lbw_a = lora["lbw_a"]
@@ -432,10 +432,13 @@ class easyLoader:
clip_vision = None
lora_stack = []
# Check for model override
can_load_lora = True
# 判断是否存在 模型或Lora叠加xyplot, 若存在优先缓存第一个模型
# Determine whether there is a model or Lora overlapping xyplot, and if there is, prioritize caching the first model.
xy_model_id = next((x for x in prompt if str(prompt[x]["class_type"]) in ["easy XYInputs: ModelMergeBlocks",
"easy XYInputs: Checkpoint"]), None)
# This will find nodes that aren't actively connected to anything, and skip loading lora's for them.
xy_lora_id = next((x for x in prompt if str(prompt[x]["class_type"]) == "easy XYInputs: Lora"), None)
if xy_lora_id is not None:
can_load_lora = False
@@ -461,6 +464,7 @@ class easyLoader:
if optional_lora_stack is not None and can_load_lora:
for lora in optional_lora_stack:
# This is a subtle bit of code because it uses the model created by the last call, and passes it to the next call.
lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1],
"clip_strength": lora[2]}
model, clip = self.load_lora(lora)
+55
View File
@@ -0,0 +1,55 @@
from server import PromptServer
from aiohttp import web
import time
import json
class MessageCancelled(Exception):
pass
class Message:
stash = {}
messages = {}
cancelled = False
@classmethod
def addMessage(cls, id, message):
if message == '__cancel__':
cls.messages = {}
cls.cancelled = True
elif message == '__start__':
cls.messages = {}
cls.stash = {}
cls.cancelled = False
else:
cls.messages[str(id)] = message
@classmethod
def waitForMessage(cls, id, period=0.1, asList=False):
sid = str(id)
while not (sid in cls.messages) and not ("-1" in cls.messages):
if cls.cancelled:
cls.cancelled = False
raise MessageCancelled()
time.sleep(period)
if cls.cancelled:
cls.cancelled = False
raise MessageCancelled()
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
try:
if asList:
return [str(x.strip()) for x in message.split(",")]
else:
try:
return json.loads(message)
except ValueError:
return message
except ValueError:
print( f"ERROR IN MESSAGE - failed to parse '${message}' as ${'comma separated list of strings' if asList else 'string'}")
return [message] if asList else message
@PromptServer.instance.routes.post('/easyuse/message_callback')
async def message_callback(request):
post = await request.post()
Message.addMessage(post.get("id"), post.get("message"))
return web.json_response({})
+177 -5
View File
@@ -1,9 +1,13 @@
import re
import random
import os
import folder_paths
import yaml
import json
import os
import random
import re
from math import prod
import yaml
import folder_paths
from .log import log_node_info
easy_wildcard_dict = {}
@@ -302,3 +306,171 @@ def process_with_loras(wildcard_opt, model, clip, title="Positive", seed=None, c
log_node_info("easy wildcards",f'{title}_decode: {pass1}')
return model, clip, pass2, pass1, show_wildcard_prompt, pipe_lora_stack
def expand_wildcard(keyword: str) -> tuple[str]:
"""传入文件通配符的关键词,从 easy_wildcard_dict 中获取通配符的所有选项。"""
global easy_wildcard_dict
if keyword in easy_wildcard_dict:
return tuple(easy_wildcard_dict[keyword])
elif '*' in keyword:
subpattern = keyword.replace('*', '.*').replace('+', r"\+")
total_pattern = []
for k, v in easy_wildcard_dict.items():
if re.match(subpattern, k) is not None:
total_pattern.extend(v)
if total_pattern:
return tuple(total_pattern)
elif '/' not in keyword:
return expand_wildcard(f"*/{keyword}")
def expand_options(options: str) -> tuple[str]:
"""传入去掉 {} 的选项。
展开选项通配符,返回该选项中的每一项,这里的每一项都是一个替换项。
不会对选项内容进行任何处理,即便存在空格或特殊符号,也会原样返回。"""
return tuple(options.split("|"))
def decimal_to_irregular(n, bases):
"""
将十进制数转换为不规则进制
:param n: 十进制数
:param bases: 各位置的基数列表,从低位到高位
:return: 不规则进制表示的列表,从低位到高位
"""
if n == 0:
return [0] * len(bases) if bases else [0]
digits = []
remaining = n
# 从低位到高位处理
for base in bases:
digit = remaining % base
digits.append(digit)
remaining = remaining // base
return digits
class WildcardProcessor:
"""通配符处理器
通配符格式:
+ option : {a|b}
+ wildcard: __keyword__ 通配符内容将从 Easy-Use 插件提供的 easy_wildcard_dict 中获取
"""
RE_OPTIONS = re.compile(r"{([^{}]*?)}")
RE_WILDCARD = re.compile(r"__([\w\s.\-+/*\\]+?)__")
RE_REPLACER = re.compile(r"{([^{}]*?)}|__([\w\s.\-+/*\\]+?)__")
# 将输入的提示词转化成符合 python str.format 要求格式的模板,并将 option 和 wildcard 按照顺序在模板中留下 {0}, {1} 等占位符
template: str
# option、wildcard 的替换项列表,按照在模板中出现的顺序排列,相同的替换项列表只保留第一份
replacers: dict[int, tuple[str]]
# 占位符的编号和替换项列表的索引的映射,占位符编号按照在模板中出现的顺序排列,方便减少替换项的存储占用
placeholder_mapping: dict[str, int] # placeholder_id => replacer_id
# 各替换项列表的项数,按照在模板中出现的顺序排列,提前计算,方便后续使用
placeholder_choices: dict[str, int] # placeholder_id => len(replacer)
def __init__(self, text: str):
self.__make_template(text)
self.__total = None
def random(self, seed=None) -> str:
"从所有可能性中随机获取一个"
if seed is not None:
random.seed(seed)
return self.getn(random.randint(0, self.total() - 1))
def getn(self, n: int) -> str:
"从所有可能性中获取第 n 个,以 self.total() 为周期循环"
n = n % self.total()
indice = decimal_to_irregular(n, self.placeholder_choices.values())
replacements = {
placeholder_id: self.replacers[self.placeholder_mapping[placeholder_id]][i]
for placeholder_id, i in zip(self.placeholder_mapping.keys(), indice)
}
return self.template.format(**replacements)
def getmany(self, limit: int, offset: int = 0) -> list[str]:
"""返回一组可能性组成的列表,为了避免结果太长导致内存占用超限,使用 limit 限制列表的长度,使用 offset 调整偏移。
若 limit 和 offset 的设置导致预期的结果长度超过剩下的实际长度,则会回到开头。
"""
return [self.getn(n) for n in range(offset, offset + limit)]
def total(self) -> int:
"计算可能性的数目"
if self.__total is None:
self.__total = prod(self.placeholder_choices.values())
return self.__total
def __make_template(self, text: str):
"""将输入的提示词转化成符合 python str.format 要求格式的模板,
并将 option 和 wildcard 按照顺序在模板中留下 {r0}, {r1} 等占位符,
即使遇到相同的 option 或 wildcard,留下的占位符编号也不同,从而使每项都独立变化。
"""
self.placeholder_mapping = {}
placeholder_id = 0
replacer_id = 0
replacers_rev = {} # replacers => id
blocks = []
# 记录所处理过的通配符末尾在文本中的位置,用于拼接完整的模板
tail = 0
for match in self.RE_REPLACER.finditer(text):
# 提取并展开通配符内容
m = match.group(0)
if m.startswith("{"):
choices = expand_options(m[1:-1])
elif m.startswith("__"):
keyword = m[2:-2].lower()
keyword = wildcard_normalize(keyword)
choices = expand_wildcard(keyword)
else:
raise ValueError(f"{m!r} is not a wildcard or option")
# 记录通配符的替换项列表和ID,相同的通配符只保留第一个
if choices not in replacers_rev:
replacers_rev[choices] = replacer_id
replacer_id += 1
# 拼接通配符前方文本
start, end = match.span()
blocks.append(text[tail:start])
tail = end
# 将通配符替换为占位符,并记录占位符和替换项列表的索引的映射
blocks.append(f"{{r{placeholder_id}}}")
self.placeholder_mapping[f"r{placeholder_id}"] = replacers_rev[choices]
placeholder_id += 1
if tail < len(text):
blocks.append(text[tail:])
self.template = "".join(blocks)
self.replacers = {v: k for k, v in replacers_rev.items()}
self.placeholder_choices = {
placeholder_id: len(self.replacers[replacer_id])
for placeholder_id, replacer_id in self.placeholder_mapping.items()
}
def test_option():
text = "{|a|b|c}"
answer = ["", "a", "b", "c"]
p = WildcardProcessor(text)
assert p.total() == len(answer)
assert p.getn(0) == answer[0]
assert p.getmany(4) == answer
assert p.getmany(4, 1) == answer[1:]
def test_same():
text = "{a|b},{a|b}"
answer = ["a,a", "b,a", "a,b", "b,b"]
p = WildcardProcessor(text)
assert p.total() == len(answer)
assert p.getn(0) == answer[0]
assert p.getmany(4) == answer
assert p.getmany(4, 1) == answer[1:]
+90 -15
View File
@@ -8,6 +8,7 @@ from .log import log_node_warn
from ..modules.layer_diffuse import LayerDiffuse
from ..config import RESOURCES_DIR
from nodes import CLIPTextEncode
import pprint
try:
from comfy_extras.nodes_flux import FluxGuidance
except:
@@ -52,7 +53,7 @@ class easyXYPlot():
plot_image_vars[value_type] = value
if value_type in ["seed", "Seeds++ Batch"]:
value_label = f"{value}"
value_label = f"seed: {value}"
else:
value_label = f"{value_type}: {value}"
@@ -63,7 +64,9 @@ class easyXYPlot():
arr = value.split(',')
model_name = os.path.basename(os.path.splitext(arr[0])[0])
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr[3]) > 2 else ''
value_label = f"{model_name}{trigger_words}"
lora_weight = float(arr[1]) if value_type == 'Lora' and len(arr) > 1 else 0
lora_weight_desc = f"({lora_weight:.2f})" if lora_weight > 0 else ''
value_label = f"{model_name[:30]}{lora_weight_desc} {trigger_words}"
if value_type in ["ModelMergeBlocks"]:
if ":" in value:
@@ -118,24 +121,32 @@ class easyXYPlot():
def calculate_background_dimensions(self):
border_size = int((self.max_width // 8) * 1.5) if self.y_type != "None" or self.x_type != "None" else 0
bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * (
self.y_type != "None")
bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * (
self.x_type != "None")
# Add space at the bottom of the image for common informaiton about the image
bg_height = bg_height + (border_size*2)
# print(f"Grid Size: width = {bg_width} height = {bg_height} border_size = {border_size}")
x_offset_initial = border_size if self.y_type != "None" else 0
y_offset = border_size if self.x_type != "None" else 0
return bg_width, bg_height, x_offset_initial, y_offset
def adjust_font_size(self, text, initial_font_size, label_width):
font = self.get_font(initial_font_size, self.custom_font)
text_width = font.getbbox(text)
# pprint.pp(f"Initial font size: {initial_font_size}, text: {text}, text_width: {text_width}")
if text_width and text_width[2]:
text_width = text_width[2]
scaling_factor = 0.9
if text_width > (label_width * scaling_factor):
# print(f"Adjusting font size from {initial_font_size} to fit text width {text_width} into label width {label_width} scaling_factor {scaling_factor}")
return int(initial_font_size * (label_width / text_width) * scaling_factor)
else:
return initial_font_size
@@ -144,15 +155,22 @@ class easyXYPlot():
_, _, width, height = d.textbbox((0, 0), text=text, font=font)
return width, height
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10):
label_width = img.width if is_x_label else img.height
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10, label_width=0, label_height=0):
# if the label_width is specified, leave it along. Otherwise do the old logic.
if label_width == 0:
label_width = img.width if is_x_label else img.height
text_lines = text.split('\n')
longest_line = max(text_lines, key=len)
# Adjust font size
font_size = self.adjust_font_size(text, initial_font_size, label_width)
font_size = self.adjust_font_size(longest_line, initial_font_size, label_width)
font_size = min(max_font_size, font_size) # Ensure font isn't too large
font_size = max(min_font_size, font_size) # Ensure font isn't too small
label_height = int(font_size * 1.5) if is_x_label else font_size
if label_height == 0:
label_height = int(font_size * 1.5) if is_x_label else font_size
label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0))
d = ImageDraw.Draw(label_bg)
@@ -166,7 +184,7 @@ class easyXYPlot():
text = text + '...'
# Compute text width and height for multi-line text
text_lines = text.split('\n')
text_widths, text_heights = zip(*[self.textsize(d, line, font=font) for line in text_lines])
max_text_width = max(text_widths)
total_text_height = sum(text_heights)
@@ -195,8 +213,7 @@ class easyXYPlot():
clip = clip if clip is not None else plot_image_vars["clip"]
steps = plot_image_vars['steps'] if "steps" in plot_image_vars else 1
sd_version = get_sd_version(plot_image_vars['model'])
sd_version = get_sd_version(plot_image_vars['model'])
# 高级用法
if plot_image_vars["x_node_type"] == "advanced" or plot_image_vars["y_node_type"] == "advanced":
if self.x_type == "Seeds++ Batch" or self.y_type == "Seeds++ Batch":
@@ -347,17 +364,24 @@ class easyXYPlot():
# Lora
if self.x_type == "Lora" or self.y_type == "Lora":
# print(f"Lora: {x_value} {y_value}")
model = model if model is not None else plot_image_vars["model"]
clip = clip if clip is not None else plot_image_vars["clip"]
xy_values = x_value if self.x_type == "Lora" else y_value
lora_name, lora_model_strength, lora_clip_strength, _ = xy_values.split(",")
lora_stack = [{"lora_name": lora_name, "model": model, "clip" :clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}]
# print(f"new_lora_stack: {new_lora_stack}")
if 'lora_stack' in plot_image_vars:
lora_stack = lora_stack + plot_image_vars['lora_stack']
if lora_stack is not None and lora_stack != []:
for lora in lora_stack:
# Each generation of the model, must use the reference to previously created model / clip objects.
lora['model'] = model
lora['clip'] = clip
model, clip = self.easyCache.load_lora(lora)
# 提示词
@@ -464,6 +488,7 @@ class easyXYPlot():
plot_image_vars['negative_weight_interpretation'], w_max=1.0,
apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps)
model = model if model is not None else plot_image_vars["model"]
vae = vae if vae is not None else plot_image_vars["vae"]
positive = positive if positive is not None else plot_image_vars["positive_cond"]
@@ -582,11 +607,10 @@ class easyXYPlot():
return self.latents_plot
def plot_images_and_labels(self):
# Calculate the background dimensions
def plot_images_and_labels(self, plot_image_vars):
bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions()
# Create the white background image
background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
output_image = []
@@ -618,4 +642,55 @@ class easyXYPlot():
y_offset += img.height + self.grid_spacing
return (self.sampler.pil2tensor(background), output_image)
# lookup used models in the image
common_label = ""
# Update to add a function to do the heavy lifting. Parameters are plot_image_vars name, label to use, names of the axis,
# pprint.pp(plot_image_vars)
# We don't process LORAs here because there can be multiple of them.
labels = [
{"id": "ckpt_name", "id_desc": "ckpt", "axis_type" : "Checkpoint"},
{"id": "vae_name", "id_desc": '', "axis_type" : "vae_name"},
{"id": "sampler_name", "id_desc": "sampler", "axis_type" : "Sampler"},
{"id": "scheduler", "id_desc": '', "axis_type" : "Scheduler"},
{"id": "steps", "id_desc": '', "axis_type" : "Steps"},
{"id": "Flux Guidance", "id_desc": 'guidance', "axis_type" : "Flux Guidance"},
{"id": "seed", "id_desc": '', "axis_type" : "Seeds++ Batch"}
]
for item in labels:
# Only add the label if it's not one of the axis
# print(f"Checking item: {item['id']} axis_type {item['axis_type']} x_type: {self.x_type} y_type: {self.y_type}")
if self.x_type != item['axis_type'] and self.y_type != item['axis_type']:
common_label += self.add_common_label(item['id'], plot_image_vars, item['id_desc'])
common_label += f"\n"
if plot_image_vars['lora_stack'] is not None and plot_image_vars['lora_stack'] != []:
# print(f"lora_stack: {plot_image_vars['lora_stack']}")
for lora in plot_image_vars['lora_stack']:
lora_name = lora['lora_name']
lora_weight = lora['model_strength']
if lora_name is not None and len(lora_name) > 0 and lora_weight > 0:
common_label += f"LORA: {lora_name} weight: {lora_weight:.2f} \n"
common_label = common_label.strip()
if len(common_label) > 0:
label_height = background.height - y_offset
label_bg = self.create_label(background, common_label, int(48 * background.width / 512), label_width=background.width, label_height=label_height)
label_x = (background.width - label_bg.width) // 2
label_y = y_offset
# print(f"Adding common label: {common_label} x = {label_x} y = {label_y}")
background.alpha_composite(label_bg, (label_x, label_y))
return (self.sampler.pil2tensor(background), output_image)
def add_common_label(self, tag, plot_image_vars, description = ''):
label = ''
if description == '': description = tag
if tag in plot_image_vars and plot_image_vars[tag] is not None and plot_image_vars[tag] != 'None':
label += f"{description}: {plot_image_vars[tag]} "
# print(f"add_common_label: {tag} description: {description} label: {label}" )
return label
+32 -16
View File
@@ -5,13 +5,21 @@ import os
import types
import torch
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
try:
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
except:
init_empty_weights, load_checkpoint_and_dispatch = None, None
import comfy
from .model import BrushNetModel, PowerPaintModel
from .model_patch import add_model_patch_option, patch_model_function_wrapper
from .powerpaint_utils import TokenizerWrapper, add_tokens
try:
from .model import BrushNetModel, PowerPaintModel
from .model_patch import add_model_patch_option, patch_model_function_wrapper
from .powerpaint_utils import TokenizerWrapper, add_tokens
except:
BrushNetModel, PowerPaintModel = None, None
add_model_patch_option, patch_model_function_wrapper = None, None
TokenizerWrapper, add_tokens = None, None
cwd_path = os.path.dirname(os.path.realpath(__file__))
brushnet_config_file = os.path.join(cwd_path, 'config', 'brushnet.json')
@@ -272,11 +280,11 @@ class BrushNet:
# unload vae
del vae
for loaded_model in comfy.model_management.current_loaded_models:
if type(loaded_model.model.model) in ModelsToUnload:
comfy.model_management.current_loaded_models.remove(loaded_model)
loaded_model.model_unload()
del loaded_model
# for loaded_model in comfy.model_management.current_loaded_models:
# if type(loaded_model.model.model) in ModelsToUnload:
# comfy.model_management.current_loaded_models.remove(loaded_model)
# loaded_model.model_unload()
# del loaded_model
# prepare embeddings
prompt_embeds = positive[0][0].to(dtype=torch_dtype).to(brushnet['brushnet'].device)
@@ -449,11 +457,11 @@ class BrushNet:
# unload vae and CLIPs
del vae
del clip
for loaded_model in comfy.model_management.current_loaded_models:
if type(loaded_model.model.model) in ModelsToUnload:
comfy.model_management.current_loaded_models.remove(loaded_model)
loaded_model.model_unload()
del loaded_model
# for loaded_model in comfy.model_management.current_loaded_models:
# if type(loaded_model.model.model) in ModelsToUnload:
# comfy.model_management.current_loaded_models.remove(loaded_model)
# loaded_model.model_unload()
# del loaded_model
# apply patch to model
@@ -663,8 +671,16 @@ def add_brushnet_patch(model, brushnet, torch_dtype, conditioning_latents,
is_SDXL = isinstance(model.model.model_config, comfy.supported_models.SDXL)
if model.model.model_config.custom_operations is None:
fp8 = model.model.model_config.optimizations.get("fp8", model.model.model_config.scaled_fp8 is not None)
operations = comfy.ops.pick_operations(model.model.model_config.unet_config.get("dtype", None), model.model.manual_cast_dtype,
fp8_optimizations=fp8, scaled_fp8=model.model.model_config.scaled_fp8)
else:
# such as gguf
operations = model.model.model_config.custom_operations
if is_SDXL:
input_blocks = [[0, comfy.ops.manual_cast.Conv2d],
input_blocks = [[0, operations.Conv2d],
[1, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[2, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample],
@@ -686,7 +702,7 @@ def add_brushnet_patch(model, brushnet, torch_dtype, conditioning_latents,
[7, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[8, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock]]
else:
input_blocks = [[0, comfy.ops.manual_cast.Conv2d],
input_blocks = [[0, operations.Conv2d],
[1, comfy.ldm.modules.attention.SpatialTransformer],
[2, comfy.ldm.modules.attention.SpatialTransformer],
[3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample],
+5 -2
View File
@@ -9,7 +9,10 @@ from enum import Enum
from comfy.utils import load_torch_file
from comfy.conds import CONDRegular
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
try:
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
except:
ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel = None, None, None
from .attension_sharing import AttentionSharingPatcher
from ...config import LAYER_DIFFUSION, LAYER_DIFFUSION_DIR, LAYER_DIFFUSION_VAE
from ...libs.utils import to_lora_patch_dict, get_local_filepath, get_sd_version
@@ -51,7 +54,7 @@ class LayerDiffuse:
return (write_c_concat(cond), write_c_concat(uncond))
def apply_layer_diffusion(self, model: ModelPatcher, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
def apply_layer_diffusion(self, model, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
control_img: Optional[torch.TensorType] = None
sd_version = get_sd_version(model)
model_url = LAYER_DIFFUSION[method.value][sd_version]["model_url"]
+30 -1
View File
@@ -9,12 +9,39 @@ from ..config import *
from ..libs.log import log_node_info, log_node_warn
from ..libs.utils import get_local_filepath, get_sd_version
from ..libs.wildcards import process_with_loras
from ..libs.controlnet import easyControlnet
from ..libs.conditioning import prompt_to_cond
from ..libs import cache as backend_cache
from .. import easyCache
class applyLoraPrompt:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"positive": ("STRING", {"default": "", "forceInput": True}),
},
"optional": {
"negative": ("STRING", {"default": "", "forceInput": True}),
}
}
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING")
RETURN_NAMES = ("model", "clip", "positive", "negative")
CATEGORY = "EasyUse/Adapter"
FUNCTION = "apply"
def apply(self, model, clip, positive, negative=None):
model, clip, positive, _, _, _ = process_with_loras(positive, model, clip, 'Positive', easyCache=easyCache)
if negative is not None:
model, clip, negative, _, _, _ = process_with_loras(negative, model, clip, 'Negative', easyCache=easyCache)
return (model, clip, positive, negative if negative is not None else "")
class applyLoraStack:
@classmethod
def INPUT_TYPES(s):
@@ -40,7 +67,7 @@ class applyLoraStack:
lora = {"lora_name": lora[0], "model": model, "clip": optional_clip, "model_strength": lora[1],
"clip_strength": lora[2]}
model, clip = easyCache.load_lora(lora, model, optional_clip, use_cache=False)
return (model, clip)
return (model, optional_clip if clip is None else clip)
class applyControlnetStack:
@classmethod
@@ -1284,6 +1311,7 @@ class applyPulIDADV(applyPulID):
NODE_CLASS_MAPPINGS = {
"easy loraPromptApply": applyLoraPrompt,
"easy loraStackApply": applyLoraStack,
"easy controlnetStackApply": applyControlnetStack,
"easy ipadapterApply": ipadapterApply,
@@ -1303,6 +1331,7 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy loraPromptApply": "Easy Apply LoraPrompt",
"easy loraStackApply": "Easy Apply LoraStack",
"easy controlnetStackApply": "Easy Apply CnetStack",
"easy ipadapterApply": "Easy Apply IPAdapter",
+13 -35
View File
@@ -3,37 +3,8 @@ from ..libs.api.fluxai import fluxaiAPI
from ..libs.api.bizyair import bizyairAPI, encode_data
from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
class fluxPromptGenAPI:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"describe": ("STRING", {"default": "", "placeholder": "Describe your image idea (you can use any language)", "multiline": True}),
},
"optional": {
"cookie_override": ("STRING", {"default": "", "forceInput": True}),
},
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "generate"
OUTPUT_NODE = False
CATEGORY = "EasyUse/API"
def generate(self, describe, cookie_override=None, prompt=None, unique_id=None, extra_pnginfo=None):
prompt = fluxaiAPI.promptGenerate(describe, cookie_override)
return (prompt,)
class joyCaption2API:
API_URL = f"/supernode/joycaption2"
@classmethod
def INPUT_TYPES(s):
@@ -101,18 +72,21 @@ class joyCaption2API:
"multiline": True,
},
),
},
"optional":{
"apikey_override": ("STRING", {"default": "", "forceInput": True, "tooltip":"Override the API key in the local config"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("caption",)
FUNCTION = "joycaption2"
FUNCTION = "joycaption"
OUTPUT_NODE = False
CATEGORY = "EasyUse/API"
def joycaption2(
def joycaption(
self,
image,
do_sample,
@@ -123,6 +97,7 @@ class joyCaption2API:
extra_options,
name_input,
custom_prompt,
apikey_override=None
):
pbar = comfy.utils.ProgressBar(100)
pbar.update_absolute(10)
@@ -145,17 +120,20 @@ class joyCaption2API:
}
pbar.update_absolute(30)
caption = bizyairAPI.joyCaption2(payload, image)
caption = bizyairAPI.joyCaption(payload, image, apikey_override, API_URL=self.API_URL)
pbar.update_absolute(100)
return (caption,)
class joyCaption3API(joyCaption2API):
API_URL = f"/supernode/joycaption3"
NODE_CLASS_MAPPINGS = {
"easy fluxPromptGenAPI": fluxPromptGenAPI,
"easy joyCaption2API": joyCaption2API,
"easy joyCaption3API": joyCaption3API,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy fluxPromptGenAPI": "Prompt Gen (FluxAI)",
"easy joyCaption2API": "JoyCaption2 (BizyAIR)",
"easy joyCaption3API": "JoyCaption3 (BizyAIR)",
}
+196 -112
View File
@@ -4,19 +4,19 @@ import torch
import numpy as np
import comfy.utils
import comfy.model_management
import shutil
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from server import PromptServer
from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
from PIL import Image, ImageDraw, ImageFilter, ImageOps
import torch.nn.functional as F
from torchvision.transforms import Resize, CenterCrop, GaussianBlur
from torchvision.transforms import Resize, CenterCrop, GaussianBlur, ToPILImage
from torchvision.transforms.functional import to_pil_image
from ..libs.log import log_node_info
from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple
from ..libs.cache import cache, update_cache, remove_cache
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image, fit_resize_image
from ..libs.colorfix import adain_color_fix, wavelet_color_fix
from ..libs.chooser import ChooserMessage, ChooserCancelled
from ..config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR
any_type = AlwaysEqualProxy("*")
@@ -485,8 +485,7 @@ class imageSaveSimple:
def save(self, images, filename_prefix="ComfyUI", only_preview=False, prompt=None, extra_pnginfo=None):
if only_preview:
PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
return ()
return PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
else:
return SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
@@ -803,7 +802,20 @@ class imageConcat:
elif image2 is None:
return (image1,)
if match_image_size:
image2 = torch.nn.functional.interpolate(image2, size=(image1.shape[2], image1.shape[3]), mode="bilinear")
# Convert tensor to PIL for proper aspect ratio resizing
pil_image2 = tensor2pil(image2)
if direction in ['right', 'left']:
aspect_ratio = pil_image2.width / pil_image2.height
new_height = image1.shape[1]
new_width = int(aspect_ratio * new_height)
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
else: # 'up' or 'down'
aspect_ratio = pil_image2.height / pil_image2.width
new_width = image1.shape[2]
new_height = int(aspect_ratio * new_width)
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
image2 = pil2tensor(pil_image2)
if direction == 'right':
row = torch.cat((image1, image2), dim=2)
elif direction == 'down':
@@ -993,6 +1005,7 @@ class imageRemBg:
"result": (new_images, masks)}
# 图像选择器
from ..libs.chooser import wait_for_chooser
class imageChooser(PreviewImage):
@classmethod
def INPUT_TYPES(self):
@@ -1029,50 +1042,30 @@ class imageChooser(PreviewImage):
def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
id = my_unique_id[0]
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
if id not in ChooserMessage.stash:
ChooserMessage.stash[id] = {}
my_stash = ChooserMessage.stash[id]
# enable stashing. If images is None, we are operating in read-from-stash mode
if 'images' in kwargs:
my_stash['images'] = kwargs['images']
else:
kwargs['images'] = my_stash.get('images', None)
if (kwargs['images'] is None):
return (None, None, None, "")
return (None,)
images_in = torch.cat(kwargs.pop('images'))
self.batch = images_in.shape[0]
for x in kwargs: kwargs[x] = kwargs[x][0]
result = self.save_images(images=images_in, prompt=prompt)
images = result['ui']['images']
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
try:
pnginfo = extra_pnginfo[0]
except:
pnginfo = None
result = self.save_images(images=images_in, prompt=prompt, extra_pnginfo=pnginfo)
if "ui" in result and "images" in result['ui']:
images = result["ui"]["images"]
else:
images = []
try:
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
except Exception as e:
pass
# 获取上次选择
mode = kwargs.pop('mode', 'Always Pause')
last_choosen = None
if mode == 'Keep Last Selection':
if not extra_pnginfo:
print("Error: extra_pnginfo is empty")
elif (not isinstance(extra_pnginfo[0], dict) or "workflow" not in extra_pnginfo[0]):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == id), None)
if node:
last_choosen = node['properties']['values']
# wait for selection
try:
selections = ChooserMessage.waitForMessage(id, asList=True) if last_choosen is None or len(last_choosen)<1 else last_choosen
choosen = [x for x in selections if x >= 0] if len(selections)>1 else [0]
except ChooserCancelled:
raise comfy.model_management.InterruptProcessingException()
return {"ui": {"images": images},
"result": (self.tensor_bundle(images_in, choosen),)}
return wait_for_chooser(id, images_in, mode)
class imageColorMatch(PreviewImage):
@classmethod
@@ -1260,13 +1253,22 @@ class humanSegmentation:
@classmethod
def INPUT_TYPES(cls):
return {
"required":{
"image": ("IMAGE",),
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)"],),
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)", "segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"],),
"confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},),
"crop_multi": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.001},),
"mask_components":(
"EASY_COMBO",{
"options": [{'label':'Background','value':0}],
"multi_select": {
"placeholder": "select mask components",
"chip": True,
"max_selected_labels": 4,
}
}
)
},
"hidden": {
"prompt": "PROMPT",
@@ -1292,12 +1294,7 @@ class humanSegmentation:
numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB)
return mp.Image(image_format=image_format, data=numpy_image)
def parsing(self, image, confidence, method, crop_multi, prompt=None, my_unique_id=None):
mask_components = []
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['mask_components']:
mask_components = prompt[my_unique_id]["inputs"]['mask_components'].split(',')
mask_components = list(map(int, mask_components))
def parsing(self, image, confidence, method, crop_multi, mask_components, prompt=None, my_unique_id=None):
if method == 'selfie_multiclass_256x256':
try:
import mediapipe as mp
@@ -1425,6 +1422,107 @@ class humanSegmentation:
output_image = torch.cat(ret_images, dim=0)
mask = torch.cat(ret_masks, dim=0)
elif method in ["segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"]:
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
# 分割
def get_segmentation_from_model(tensor_image, model, processor):
cloth = tensor2pil(tensor_image)
inputs = processor(images=cloth, return_tensors="pt")
outputs = model(**inputs)
logits = outputs.logits.cpu()
upsampled_logits = F.interpolate(logits, size=cloth.size[::-1], mode="bilinear",
align_corners=False)
pred_seg = upsampled_logits.argmax(dim=1)[0].numpy()
return pred_seg, cloth
if method in cache:
_, (processor, model) = cache[method][1]
else:
model_folder_path = os.path.join(folder_paths.models_dir, method)
if os.path.exists(model_folder_path):
print(f"Start to load existing model...")
else:
from huggingface_hub import snapshot_download
PromptServer.instance.send_sync("easyuse-toast", {"content": f"Model not found locally. Downloading {method}...", "type": 'loading', "duration": 10000})
print(f"Model not found locally. Downloading {method}...")
model_path_cache = os.path.join(folder_paths.models_dir, "cache-"+method)
snapshot_download(
repo_id=HUMANPARSING_MODELS[method]['model_name'],
local_dir=model_path_cache,
local_dir_use_symlinks=False,
resume_download=True
)
shutil.move(model_path_cache, model_folder_path)
print(f"Model downloaded to {model_folder_path}...")
try:
model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[method][0][0])
except:
pass
processor = SegformerImageProcessor.from_pretrained(model_folder_path)
model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path)
update_cache(method, 'human_segmentation', (False, (processor, model)))
ret_images = []
ret_masks = []
if method == "face_parsing":
import matplotlib
import torchvision.transforms as T
transform = ToPILImage()
colormap = matplotlib.colormaps['viridis']
device = model.device
results = []
images = []
for img in image:
size = img.shape[:2]
inputs = processor(images=transform(img.permute(2, 0, 1)), return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}
outputs = model(**inputs)
logits = outputs.logits
upsampled_logits = F.interpolate(
logits,
size=size,
mode="bilinear",
align_corners=False)
pred_seg = upsampled_logits.argmax(dim=1)[0]
pred_seg_np = pred_seg.cpu().detach().numpy().astype(np.uint8)
results.append(torch.tensor(pred_seg_np))
results_out = torch.stack(results, dim=0)
for img, result_item in zip(image, results_out):
mask = torch.zeros(result_item.shape, dtype=torch.uint8)
for i in mask_components:
mask = mask | torch.where(result_item == i, 1, 0)
# 将mask转换为numpy数组,并确保数据类型正确
mask_np = (mask * 255).numpy().astype(np.uint8)
_mask = Image.fromarray(mask_np)
# 处理图像输出
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
else:
for img in image:
pred_seg, cloth = get_segmentation_from_model(img, model, processor)
i = torch.unsqueeze(img, 0)
i = pil2tensor(tensor2pil(i).convert('RGB'))
mask = np.isin(pred_seg, mask_components).astype(np.uint8)
_mask = Image.fromarray(mask * 255)
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
output_image = torch.cat(ret_images, dim=0)
mask = torch.cat(ret_masks, dim=0)
# use crop
bbox = [[0, 0, 0, 0]]
if crop_multi > 0.0:
@@ -1875,47 +1973,6 @@ class loadImagesForLoop:
"result": tuple(["stub", index, image, mask, name] + outputs),
"expand": graph.finalize(),
}
# 姿势编辑器
# class poseEditor:
# @classmethod
# def INPUT_TYPES(self):
# temp_dir = folder_paths.get_temp_directory()
#
# if not os.path.isdir(temp_dir):
# os.makedirs(temp_dir)
#
# temp_dir = folder_paths.get_temp_directory()
#
# return {"required":
# {"image": (sorted(os.listdir(temp_dir)),)},
# }
#
# RETURN_TYPES = ("IMAGE",)
# FUNCTION = "output_pose"
#
# CATEGORY = "EasyUse/🚫 Deprecated"
#
# def output_pose(self, image):
# image_path = os.path.join(folder_paths.get_temp_directory(), image)
# # print(f"Create: {image_path}")
#
# i = Image.open(image_path)
# image = i.convert("RGB")
# image = np.array(image).astype(np.float32) / 255.0
# image = torch.from_numpy(image)[None,]
#
# return (image,)
#
# @classmethod
# def IS_CHANGED(self, image):
# image_path = os.path.join(
# folder_paths.get_temp_directory(), image)
# # print(f'Change: {image_path}')
#
# m = hashlib.sha256()
# with open(image_path, 'rb') as f:
# m.update(f.read())
# return m.digest().hex()
class makeImageForICRepaint:
@classmethod
@@ -1925,6 +1982,7 @@ class makeImageForICRepaint:
"image_1": ("IMAGE",),
"direction": (["top-bottom", "left-right"], {"default": "left-right"}),
"pixels": ("INT", {"default": 0, "max": MAX_RESOLUTION, "min": 0, "step": 8, "tooltip": "The pixel of the output image is not set when it is 0"}),
"method": (["uniform height", "uniform width", "auto"],{"default": "auto"}),
},
"optional": {
"image_2": ("IMAGE",),
@@ -1951,26 +2009,52 @@ class makeImageForICRepaint:
b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
return torch.cat((r, g, b), dim=-1)
def make(self, image_1, direction, pixels=0, image_2=None, mask_1=None, mask_2=None):
def resize_image_and_mask(self, image, mask, w, h ,fit='fill'):
ret_images = []
ret_masks = []
_mask = Image.new('L', size=(w, h), color='black')
_image = Image.new('RGB', size=(w, h), color='black')
if image is not None and len(image) > 0:
for i in image:
_image = tensor2pil(i).convert('RGB')
_image = fit_resize_image(_image, w, h, fit, Image.LANCZOS, '#000000')
ret_images.append(pil2tensor(_image))
if mask is not None and len(mask) > 0:
for m in mask:
_mask = tensor2pil(m).convert('L')
_mask = fit_resize_image(_mask, w, h, fit, Image.LANCZOS).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) > 0:
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
elif len(ret_images) > 0 and len(ret_masks) == 0:
return (torch.cat(ret_images, dim=0), None,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
return (None, torch.cat(ret_masks, dim=0),)
else:
return (None, None)
def make(self, image_1, direction, pixels, method, image_2=None, mask_1=None, mask_2=None):
if image_2 is None:
image_2 = self.emptyImage(image_1.shape[2], image_1.shape[1])
mask_2 = torch.full((1, image_1.shape[1], image_1.shape[2]), 1, dtype=torch.float32, device="cpu")
elif image_2 is not None and mask_2 is None:
raise ValueError("mask_2 is required when image_2 is provided")
mask_2 = torch.full((1, image_2.shape[1], image_2.shape[2]), 1, dtype=torch.float32, device="cpu")
if pixels > 0:
_, img2_h, img2_w, _ = image_2.shape
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
if method == "uniform height":
h = pixels
w = int(img2_w * (pixels / img2_h))
elif method == "uniform width":
w = pixels
h = int(img2_h * (pixels / img2_w))
else:
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
image_2 = image_2.movedim(-1, 1)
image_2 = comfy.utils.common_upscale(image_2, w, h, 'bicubic', 'disabled')
image_2 = image_2.movedim(1, -1)
orig_image_2 = tensor2pil(image_2)
orig_mask_2 = tensor2pil(mask_2).convert('L')
orig_mask_2 = orig_mask_2.resize(orig_image_2.size)
mask_2 = pil2tensor(orig_mask_2)
image_2, mask_2 = self.resize_image_and_mask(image_2, mask_2, w, h)
_, img1_h, img1_w, _ = image_1.shape
_, img2_h, img2_w, _ = image_2.shape
@@ -1980,16 +2064,16 @@ class makeImageForICRepaint:
# resize
if img1_h != img2_h and img1_w != img2_w:
width, height = img2_w, img2_h
if direction == 'left-right' and img1_h != img2_h:
scale_factor = img2_h / img1_h
width = round(img1_w * scale_factor)
elif direction == 'top-bottom' and img1_w != img2_w:
scale_factor = img2_w / img1_w
height = round(img1_h * scale_factor)
image_1 = image_1.movedim(-1, 1)
image_1 = comfy.utils.common_upscale(image_1, width, height, 'bicubic', 'disabled')
image_1 = image_1.movedim(1, -1)
fit = 'crop'
if method != 'uniform width':
if direction == 'left-right' and img1_h != img2_h:
scale_factor = img2_h / img1_h
width = round(img1_w * scale_factor)
elif direction == 'top-bottom' and img1_w != img2_w:
scale_factor = img2_w / img1_w
height = round(img1_h * scale_factor)
fit = 'fill'
image_1, mask_1 = self.resize_image_and_mask(image_1, mask_1, width, height, fit)
if mask_1 is None:
mask_1 = torch.full((1, image_1.shape[1], image_1.shape[2]), 0, dtype=torch.float32, device="cpu")
+55 -2
View File
@@ -8,7 +8,7 @@ from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
from ..libs.log import log_node_info, log_node_error, log_node_warn
from ..libs.wildcards import process_with_loras
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version, AlwaysEqualProxy
from ..libs.sampler import easySampler
from ..libs.controlnet import easyControlnet, union_controlnet_types
from ..libs.conditioning import prompt_to_cond
@@ -19,6 +19,7 @@ from ..config import *
from .. import easyCache, sampler
any_type = AlwaysEqualProxy("*")
# 简易加载器完整
resolution_strings = [f"{width} x {height} (custom)" if width == 'width' and height == 'height' else f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
class fullLoader:
@@ -1146,6 +1147,56 @@ class mochiLoader(fullLoader):
my_unique_id=my_unique_id
)
# lora
class loraSwitcher:
@classmethod
def INPUT_TYPES(s):
max_lora_num = 50
inputs = {
"required": {
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
"select": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
"lora_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
},
"optional": {
"optional_lora_stack": ("LORA_STACK",),
},
}
for i in range(1, max_lora_num + 1):
inputs["optional"][f"lora_{i}_name"] = (
["None"] + folder_paths.get_filename_list("loras"), {"default": "None"})
return inputs
RETURN_TYPES = ("LORA_STACK", any_type)
RETURN_NAMES = ("lora_stack", "lora_name")
FUNCTION = "stack"
CATEGORY = "EasyUse/Loaders"
def stack(self, toggle, select,num_loras, lora_strength, optional_lora_stack=None, **kwargs):
if (toggle in [False, None, "False"]) or not kwargs:
return (None,'')
loras = []
# Import Stack values
if optional_lora_stack is not None:
loras.extend([l for l in optional_lora_stack if l[0] != "None"])
# Import Lora values
lora_name = kwargs.get(f"lora_{select}_name")
if not lora_name or lora_name == "None":
return (None,'')
loras.append((lora_name, lora_strength, lora_strength))
name = os.path.splitext(os.path.basename(str(lora_name)))[0]
return (loras, name)
class loraStack:
def __init__(self):
pass
@@ -1155,7 +1206,7 @@ class loraStack:
max_lora_num = 10
inputs = {
"required": {
"toggle": ("BOOLEAN", {"label_on": "enabled", "label_off": "disabled"}),
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
"mode": (["simple", "advanced"],),
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
},
@@ -1482,6 +1533,7 @@ NODE_CLASS_MAPPINGS = {
"easy hunyuanDiTLoader": hunyuanDiTLoader,
"easy pixArtLoader": pixArtLoader,
"easy mochiLoader": mochiLoader,
"easy loraSwitcher": loraSwitcher,
"easy loraStack": loraStack,
"easy controlnetStack": controlnetStack,
"easy controlnetLoader": controlnetSimple,
@@ -1503,6 +1555,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy hunyuanDiTLoader": "EasyLoader (HunyuanDiT)",
"easy pixArtLoader": "EasyLoader (PixArt)",
"easy mochiLoader": "EasyLoader (Mochi)",
"easy loraSwitcher": "EasyLoraSwitcher",
"easy loraStack": "EasyLoraStack",
"easy controlnetStack": "EasyControlnetStack",
"easy controlnetLoader": "EasyControlnet",
+24 -20
View File
@@ -18,7 +18,7 @@ import comfy.utils
import folder_paths
DEFAULT_FLOW_NUM = 2
MAX_FLOW_NUM = 10
MAX_FLOW_NUM = 20
lazy_options = {"lazy": True} if compare_revision(2543) else {}
any_type = AlwaysEqualProxy("*")
@@ -166,7 +166,7 @@ class Float:
@classmethod
def INPUT_TYPES(s):
return {
"required": {"value": ("FLOAT", {"default": 0, "step": 0.01, "min": -999999, "max": 999999, })},
"required": {"value": ("FLOAT", {"default": 0, "step": 0.01, "min":-0xffffffffffffffff, "max": 0xffffffffffffffff, })},
}
RETURN_TYPES = ("FLOAT",)
@@ -175,7 +175,7 @@ class Float:
CATEGORY = "EasyUse/Logic/Type"
def execute(self, value):
return (value,)
return (round(value, 3),)
# 浮点数范围
@@ -239,9 +239,9 @@ class RangeFloat:
error_if_mismatched_list_args(locals())
getcontext().prec = 12
start = [Decimal(s) for s in start]
stop = [Decimal(s) for s in stop]
step = [Decimal(s) for s in step]
start = [round(Decimal(s),2) for s in start]
stop = [round(Decimal(s),2) for s in stop]
step = [round(Decimal(s),2) for s in step]
ranges = []
range_sizes = []
@@ -573,17 +573,17 @@ class mathFloatOperation:
def float_math_operation(self, a, b, operation):
if operation == "add":
return (a + b,)
return (round(a + b,3),)
elif operation == "subtract":
return (a - b,)
return (round(a - b,3),)
elif operation == "multiply":
return (a * b,)
return (round(a * b,3),)
elif operation == "divide":
return (a / b,)
return (round(a / b,3),)
elif operation == "modulo":
return (a % b,)
return (round(a % b,3),)
elif operation == "power":
return (a ** b,)
return (round(a ** b,3),)
class mathStringOperation:
@@ -1369,10 +1369,12 @@ class showAnything:
if "anything" in kwargs:
for val in kwargs['anything']:
try:
if type(val) is str:
if isinstance(val, str):
values.append(val)
elif type(val) is list:
elif isinstance(val, list):
values = val
elif isinstance(val, (int, float, bool)):
values.append(str(val))
else:
val = json.dumps(val)
values.append(str(val))
@@ -1381,9 +1383,9 @@ class showAnything:
pass
if not extra_pnginfo:
print("Error: extra_pnginfo is empty")
pass
elif (not isinstance(extra_pnginfo[0], dict) or "workflow" not in extra_pnginfo[0]):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
pass
else:
workflow = extra_pnginfo[0]["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
@@ -1607,8 +1609,10 @@ class saveText:
if not os.path.exists(output_file_path):
os.makedirs(output_file_path)
if not overwrite:
pass
if overwrite:
file_mode = "w"
else:
file_mode = "a"
log_node_info("Save Text", f"Saving to {filepath}")
@@ -1617,13 +1621,13 @@ class saveText:
for i in text.split("\n"):
text_list.append(i.strip())
with open(filepath, "w", newline="", encoding='utf-8') as csv_file:
with open(filepath, file_mode, newline="", encoding='utf-8') as csv_file:
csv_writer = csv.writer(csv_file)
# Write each line as a separate row in the CSV file
for line in text_list:
csv_writer.writerow([line])
else:
with open(filepath, "w", newline="", encoding='utf-8') as text_file:
with open(filepath, file_mode, newline="", encoding='utf-8') as text_file:
for line in text:
text_file.write(line)
+124 -22
View File
@@ -1,12 +1,15 @@
import os
import json
import folder_paths
import os
from urllib.request import urlopen
from ..libs.log import log_node_info
from ..libs.wildcards import get_wildcard_list, process
from ..libs.utils import AlwaysEqualProxy
from ..config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE
import folder_paths
from .. import easyCache
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
from ..libs.log import log_node_info
from ..libs.utils import AlwaysEqualProxy
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
# 正面提示词
class positivePrompt:
@@ -40,7 +43,7 @@ class wildcardsPrompt:
def INPUT_TYPES(s):
wildcard_list = get_wildcard_list()
return {"required": {
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support wildcard)"}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
@@ -56,9 +59,6 @@ class wildcardsPrompt:
CATEGORY = "EasyUse/Prompt"
def translate(self, text):
return text
def main(self, *args, **kwargs):
prompt = kwargs["prompt"] if "prompt" in kwargs else None
seed = kwargs["seed"]
@@ -73,16 +73,58 @@ class wildcardsPrompt:
_text = []
text = text.split("\n")
for t in text:
t = self.translate(t)
_text.append(t)
populated_text.append(process(t, seed))
text = _text
else:
text = self.translate(text)
populated_text = [process(text, seed)]
text = [text]
return {"ui": {"value": [seed]}, "result": (text, populated_text)}
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
class wildcardsPromptMatrix:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
wildcard_list = get_wildcard_list()
return {"required": {
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
"offset": ("INT", {"default": 0, "min": 0, "step": 1, "control_after_generate": True}),
},
"optional":{
"output_limit": ("INT", {"default": 1, "min": -1, "step": 1, "tooltip": "Output All Probilities"})
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING", "INT", "INT")
RETURN_NAMES = ("populated_text", "total", "factors")
OUTPUT_IS_LIST = (True, False, True)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
def main(self, *args, **kwargs):
prompt = kwargs["prompt"] if "prompt" in kwargs else None
offset = kwargs["offset"]
output_limit = kwargs.get("output_limit", 1)
# Clean loaded_objects
if prompt:
easyCache.update_loaded_objects(prompt)
text = kwargs['text']
p = WildcardProcessor(text)
total = p.total()
limit = total if output_limit > total or output_limit == -1 else output_limit
offset = 0 if output_limit == -1 else offset
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
return {"ui": {"value": [offset]}, "result": (populated_text, p.total(), list(p.placeholder_choices.values()))}
# 负面提示词
class negativePrompt:
@@ -115,7 +157,9 @@ class stylesPromptSelector:
for file_name in os.listdir(styles_dir):
file = os.path.join(styles_dir, file_name)
if os.path.isfile(file) and file_name.endswith(".json"):
styles.append(file_name.split(".")[0])
if file_name != "fooocus_styles.json":
styles.append(file_name.split(".")[0])
return {
"required": {
"styles": (styles, {"default": "fooocus_styles"}),
@@ -123,6 +167,7 @@ class stylesPromptSelector:
"optional": {
"positive": ("STRING", {"forceInput": True}),
"negative": ("STRING", {"forceInput": True}),
"select_styles": ("EASY_PROMPT_STYLES", {}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
@@ -133,11 +178,12 @@ class stylesPromptSelector:
CATEGORY = 'EasyUse/Prompt'
FUNCTION = 'run'
def run(self, styles, positive='', negative='', prompt=None, extra_pnginfo=None, my_unique_id=None):
def run(self, styles, positive='', negative='', select_styles=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
values = []
all_styles = {}
positive_prompt, negative_prompt = '', negative
if styles == "fooocus_styles":
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
if styles == "fooocus_styles" and not os.path.exists(fooocus_custom_dir):
file = os.path.join(RESOURCES_DIR, styles + '.json')
else:
file = os.path.join(FOOOCUS_STYLES_DIR, styles + '.json')
@@ -146,9 +192,14 @@ class stylesPromptSelector:
f.close()
for d in data:
all_styles[d['name']] = d
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['select_styles']:
values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
# if my_unique_id in prompt:
# if prompt[my_unique_id]["inputs"]['select_styles']:
# values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
if isinstance(select_styles, str):
values = select_styles.split(',')
else:
values = select_styles if select_styles else []
has_prompt = False
if len(values) == 0:
@@ -159,13 +210,15 @@ class stylesPromptSelector:
if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False:
positive_prompt = all_styles[val]['prompt'].replace('{prompt}', positive)
has_prompt = True
else:
elif "{prompt}" in all_styles[val]['prompt']:
positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '')
else:
positive_prompt = all_styles[val]['prompt'] if positive_prompt == '' else positive_prompt + ', ' + all_styles[val]['prompt']
if 'negative_prompt' in all_styles[val]:
negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt']
if has_prompt == False and positive:
positive_prompt = positive + ', '
positive_prompt = positive + positive_prompt + ', '
return (positive_prompt, negative_prompt)
@@ -266,11 +319,56 @@ class promptLine:
return (rows, rows)
import comfy.utils
from server import PromptServer
from ..libs.messages import MessageCancelled, Message
any_type = AlwaysEqualProxy("*")
class promptAwait:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"now": (any_type,),
"prompt": ("STRING", {"multiline": True, "default": "", "placeholder":"Enter a prompt or use voice to enter to text"}),
"toolbar":("EASY_PROMPT_AWAIT_BAR",),
},
"optional":{
"prev": (any_type,),
},
"hidden": {"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = (any_type, "STRING", "BOOLEAN", "INT")
RETURN_NAMES = ("output", "prompt", "continue", "seed")
FUNCTION = "await_select"
CATEGORY = "EasyUse/Prompt"
def await_select(self, now, prompt, toolbar, prev=None, workflow_prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
id = my_unique_id
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
if ":" in id:
id = id.split(":")[0]
pbar = comfy.utils.ProgressBar(100)
pbar.update_absolute(30)
PromptServer.instance.send_sync('easyuse_prompt_await', {"id": id})
try:
res = Message.waitForMessage(id, asList=False)
if res is None or res == "-1":
result = (now, prompt, False, 0)
else:
input = now if res['select'] == 'now' or prev is None else prev
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
pbar.update_absolute(100)
return result
except MessageCancelled:
pbar.update_absolute(100)
raise comfy.model_management.InterruptProcessingException()
class promptConcat:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
},
return {"required": {},
"optional": {
"prompt1": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
"prompt2": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
@@ -518,9 +616,11 @@ NODE_CLASS_MAPPINGS = {
"easy positive": positivePrompt,
"easy negative": negativePrompt,
"easy wildcards": wildcardsPrompt,
"easy wildcardsMatrix": wildcardsPromptMatrix,
"easy prompt": prompt,
"easy promptList": promptList,
"easy promptLine": promptLine,
"easy promptAwait": promptAwait,
"easy promptConcat": promptConcat,
"easy promptReplace": promptReplace,
"easy stylesSelector": stylesPromptSelector,
@@ -531,9 +631,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy positive": "Positive",
"easy negative": "Negative",
"easy wildcards": "Wildcards",
"easy wildcardsMatrix": "Wildcards Matrix",
"easy prompt": "Prompt",
"easy promptList": "PromptList",
"easy promptLine": "PromptLine",
"easy promptAwait": "PromptAwait",
"easy promptConcat": "PromptConcat",
"easy promptReplace": "PromptReplace",
"easy stylesSelector": "Styles Selector",
+10 -33
View File
@@ -15,7 +15,6 @@ from ..libs.log import log_node_warn
from ..libs.utils import easySave, get_local_filepath, get_sd_version
from ..libs.sampler import alignYourStepsScheduler, gitsScheduler
from ..libs.xyplot import easyXYPlot
from ..libs.chooser import ChooserMessage, ChooserCancelled
from .. import easyCache, sampler
@@ -30,7 +29,7 @@ class samplerFull:
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -389,40 +388,18 @@ class samplerFull:
"loader_settings": {
**pipe["loader_settings"],
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"add_noise": add_noise,
"spent_time": spent_time
}
}
del pipe
if image_output == 'Preview&Choose':
if my_unique_id not in ChooserMessage.stash:
ChooserMessage.stash[my_unique_id] = {}
my_stash = ChooserMessage.stash[my_unique_id]
PromptServer.instance.send_sync("easyuse-image-choose", {"id": my_unique_id, "urls": results})
# wait for selection
try:
selections = ChooserMessage.waitForMessage(my_unique_id, asList=True)
samples = samp_samples['samples']
samples = [samples[x] for x in selections if x >= 0] if len(selections) > 1 else [samples[0]]
new_images = [new_images[x] for x in selections if x >= 0] if len(selections) > 1 else [new_images[0]]
samp_images = [samp_images[x] for x in selections if x >= 0] if len(selections) > 1 else [samp_images[0]]
new_images = torch.stack(new_images, dim=0)
samp_images = torch.stack(samp_images, dim=0)
samples = torch.stack(samples, dim=0)
samp_samples = {"samples": samples}
new_pipe['samples'] = samp_samples
new_pipe['loader_settings']['batch_size'] = len(new_images)
except ChooserCancelled:
raise comfy.model_management.InterruptProcessingException()
new_pipe['images'] = new_images
new_pipe['samp_images'] = samp_images
return {"ui": {"images": results},
"result": sampler.get_output(new_pipe,)}
if image_output in ("Hide", "Hide&Save", "None"):
return {"ui":{}, "result":sampler.get_output(new_pipe,)}
@@ -507,7 +484,7 @@ class samplerFull:
samp_samples = {"samples": latents_plot}
images, image_list = sampleXYplot.plot_images_and_labels()
images, image_list = sampleXYplot.plot_images_and_labels(plot_image_vars)
# Generate output_images
output_images = torch.stack([tensor.squeeze() for tensor in image_list])
@@ -588,7 +565,7 @@ class samplerSimple(samplerFull):
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -620,7 +597,7 @@ class samplerSimpleCustom(samplerFull):
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
+51
View File
@@ -1,4 +1,6 @@
from ..config import MAX_SEED_NUM
import hashlib
import random
class easySeed:
@classmethod
@@ -19,6 +21,53 @@ class easySeed:
def doit(self, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
return seed,
class seedList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"min_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
"max_num": ("INT", {"default": MAX_SEED_NUM, "min": 0 }),
"method": (["random", "increment", "decrement"], {"default": "random"}),
"total": ("INT", {"default": 1, "min": 1, "max": 100000}),
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM,}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("seed", "total")
FUNCTION = "doit"
DESCRIPTION = "Random number seed that can be used in a for loop, by connecting index and easy indexAny node to realize different seed values in the loop."
CATEGORY = "EasyUse/Seed"
def doit(self, min_num, max_num, method, total, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
random.seed(seed)
seed_list = []
if min_num > max_num:
min_num, max_num = max_num, min_num
for i in range(total):
if method == 'random':
s = random.randint(min_num, max_num)
elif method == 'increment':
s = min_num + i
if s > max_num:
s = max_num
elif method == 'decrement':
s = max_num - i
if s < min_num:
s = min_num
seed_list.append(s)
return seed_list, total
@classmethod
def IS_CHANGED(s, seed, **kwargs):
m = hashlib.sha256()
m.update(seed)
return m.digest().hex()
# 全局随机种
class globalSeed:
@classmethod
@@ -46,10 +95,12 @@ class globalSeed:
NODE_CLASS_MAPPINGS = {
"easy seed": easySeed,
"easy seedList": seedList,
"easy globalSeed": globalSeed,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy seed": "EasySeed",
"easy seedList": "EasySeedList",
"easy globalSeed": "EasyGlobalSeed",
}
+20 -1
View File
@@ -106,6 +106,23 @@ class setControlName:
def set_name(self, controlnet_name):
return (controlnet_name,)
class setLoraName:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"lora_name": (folder_paths.get_filename_list("loras"),),
}
}
RETURN_TYPES = (AlwaysEqualProxy('*'),)
RETURN_NAMES = ("lora_name",)
FUNCTION = "set_name"
CATEGORY = "EasyUse/Util"
def set_name(self, lora_name):
return (lora_name,)
NODE_CLASS_MAPPINGS = {
@@ -113,6 +130,7 @@ NODE_CLASS_MAPPINGS = {
"easy sliderControl": sliderControl,
"easy ckptNames": setCkptName,
"easy controlnetNames": setControlName,
"easy loraNames": setLoraName,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -120,4 +138,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy sliderControl": "Easy Slider Control",
"easy ckptNames": "Ckpt Names",
"easy controlnetNames": "ControlNet Names",
}
"easy loraNames": "Lora Names",
}
+30 -32
View File
@@ -78,13 +78,14 @@ async def parse_csv(request):
@PromptServer.instance.routes.get("/easyuse/prompt/styles")
async def getStylesList(request):
if "name" in request.rel_url.query:
name = request.rel_url.query["name"]
if name == 'fooocus_styles':
file = os.path.join(RESOURCES_DIR, name+'.json')
cn_file = os.path.join(RESOURCES_DIR, name + '_cn.json')
style_name = request.rel_url.query["name"]
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
if style_name == 'fooocus_styles' and not os.path.exists(fooocus_custom_dir):
file = os.path.join(RESOURCES_DIR, style_name+'.json')
cn_file = os.path.join(RESOURCES_DIR, style_name + '_cn.json')
else:
file = os.path.join(FOOOCUS_STYLES_DIR, name+'.json')
cn_file = os.path.join(FOOOCUS_STYLES_DIR, name + '_cn.json')
file = os.path.join(FOOOCUS_STYLES_DIR, style_name+'.json')
cn_file = os.path.join(FOOOCUS_STYLES_DIR, style_name + '_cn.json')
cn_data = None
if os.path.isfile(cn_file):
f = open(cn_file, 'r', encoding='utf-8')
@@ -103,13 +104,25 @@ async def getStylesList(request):
key = ' '.join(
word.upper() if word.lower() in ['mre', 'sai', '3d'] else word.capitalize() for word in
words)
img_name = '_'.join(words).lower()
if "name_cn" in d:
nd['name_cn'] = d['name_cn']
elif cn_data:
nd['name_cn'] = cn_data[key] if key in cn_data else key
nd["name"] = d['name']
nd['imgName'] = img_name
if "thumbnail" in d:
thumbnail = d['thumbnail']
if isinstance(d['thumbnail'], str):
nd['thumbnail'] = thumbnail if "http" in thumbnail else f'/easyuse/prompt/styles/image?path={thumbnail}'
elif isinstance(d['thumbnail'], list):
nd['thumbnail'] = [thumb if "http" in thumb else f'/easyuse/prompt/styles/image?path={thumb}' for thumb in thumbnail]
else:
nd['thumbnail'] = f'/easyuse/prompt/styles/image?name={name}&styles_name={style_name}'
if "thumbnail_variant" in d:
nd['thumbnailVariant'] = d['thumbnail_variant']
if "media_type" in d:
nd['mediaType'] = d['media_type']
if "media_subtype" in d:
nd['mediaSubtype'] = d['media_subtype']
if "prompt" in d:
nd['prompt'] = d['prompt']
if "negative_prompt" in d:
@@ -122,7 +135,15 @@ async def getStylesList(request):
@PromptServer.instance.routes.get("/easyuse/prompt/styles/image")
async def getStylesImage(request):
styles_name = request.rel_url.query["styles_name"] if "styles_name" in request.rel_url.query else None
if "name" in request.rel_url.query:
if "path" in request.rel_url.query:
path = request.rel_url.query["path"]
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', path)
parent_file = os.path.join(FOOOCUS_STYLES_DIR, path)
if os.path.isfile(file):
return web.FileResponse(file)
elif os.path.isfile(parent_file):
return web.FileResponse(parent_file)
elif "name" in request.rel_url.query:
name = request.rel_url.query["name"]
if os.path.exists(os.path.join(FOOOCUS_STYLES_DIR, 'samples')):
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', name + '.jpg')
@@ -146,29 +167,6 @@ async def getModelsList(request):
else:
return web.Response(status=400)
# get models thumbnails
@PromptServer.instance.routes.get("/easyuse/models/thumbnail")
async def getModelsThumbnail(request):
limit = 500
if "limit" in request.rel_url.query:
limit = request.rel_url.query.get("limit")
limit = int(limit)
checkpoints = folder_paths.get_filename_list("checkpoints_thumb")
loras = folder_paths.get_filename_list("loras_thumb")
checkpoints_full = []
loras_full = []
if len(checkpoints) + len(loras) >= limit:
return web.Response(status=400)
for index, i in enumerate(checkpoints):
full_path = folder_paths.get_full_path('checkpoints_thumb', str(i))
if full_path:
checkpoints_full.append(full_path)
for index, i in enumerate(loras):
full_path = folder_paths.get_full_path('loras_thumb', str(i))
if full_path:
loras_full.append(full_path)
return web.json_response(checkpoints_full + loras_full)
@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}")
async def save_notes(request):
name = request.match_info["name"]
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-easy-use"
description = "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes."
version = "1.2.8"
version = "1.3.2"
license = { file = "LICENSE" }
dependencies = ["diffusers", "accelerate", "clip_interrogator>=0.6.0", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python", "matplotlib", "peft"]
+1923 -1373
View File
File diff suppressed because it is too large Load Diff
-279
View File
@@ -1,279 +0,0 @@
{
"Fooocus V2": "Fooocus V2扩展词",
"Default (Slightly Cinematic)": "默认(轻微的电影感)",
"Fooocus Enhance": "Fooocus-优化增强",
"Fooocus Cinematic": "Fooocus-电影感",
"Fooocus Sharp": "Fooocus-锐化",
"Fooocus Masterpiece": "Fooocus-杰作",
"Fooocus Photograph": "Fooocus-照片",
"Fooocus Negative": "Fooocus-反向提示词",
"SAI 3D Model": "SAI-3D模型",
"SAI Analog Film": "SAI-模拟电影",
"SAI Anime": "SAI-动漫",
"SAI Cinematic": "SAI-电影片段",
"SAI Comic Book": "SAI-漫画",
"SAI Craft Clay": "SAI-工艺粘土",
"SAI Digital Art": "SAI-数字艺术",
"SAI Enhance": "SAI-增强",
"SAI Fantasy Art": "SAI-奇幻艺术",
"SAI Isometric": "SAI-等距风格",
"SAI Line Art": "SAI-线条艺术",
"SAI Lowpoly": "SAI-低多边形",
"SAI Neonpunk": "SAI-霓虹朋克",
"SAI Origami": "SAI-折纸",
"SAI Photographic": "SAI-摄影",
"SAI Pixel Art": "SAI-像素艺术",
"SAI Texture": "SAI-纹理",
"MRE Cinematic Dynamic": "MRE-史诗电影",
"MRE Spontaneous Picture": "MRE-自然的抓拍照片",
"MRE Artistic Vision": "MRE-艺术视觉",
"MRE Dark Dream": "MRE-黑暗梦境",
"MRE Gloomy Art": "MRE-阴郁艺术",
"MRE Bad Dream": "MRE-噩梦",
"MRE Underground": "MRE-阴森地下",
"MRE Surreal Painting": "MRE-超现实主义绘画",
"MRE Dynamic Illustration": "MRE-动态插画",
"MRE Undead Art": "MRE-遗忘艺术家作品",
"MRE Elemental Art": "MRE-元素艺术",
"MRE Space Art": "MRE-空间艺术",
"MRE Ancient Illustration": "MRE-古代插图",
"MRE Brave Art": "MRE-勇敢艺术",
"MRE Heroic Fantasy": "MRE-英雄幻想",
"MRE Dark Cyberpunk": "MRE-黑暗赛博朋克",
"MRE Lyrical Geometry": "MRE-抒情几何抽象画",
"MRE Sumi E Symbolic": "MRE-墨绘长笔画",
"MRE Sumi E Detailed": "MRE-精细墨绘画",
"MRE Manga": "MRE-日本漫画",
"MRE Anime": "MRE-日本动画片",
"MRE Comic": "MRE-成人漫画书插画",
"Ads Advertising": "广告-广告",
"Ads Automotive": "广告-汽车",
"Ads Corporate": "广告-企业品牌",
"Ads Fashion Editorial": "广告-时尚编辑",
"Ads Food Photography": "广告-食品摄影",
"Ads Gourmet Food Photography": "广告-顶级美食摄影",
"Ads Luxury": "广告-奢侈品",
"Ads Real Estate": "广告-房地产",
"Ads Retail": "广告-零售",
"Artstyle Abstract": "艺术风格-抽象",
"Artstyle Abstract Expressionism": "艺术风格-抽象表现主义",
"Artstyle Art Deco": "艺术风格-装饰艺术",
"Artstyle Art Nouveau": "艺术风格-新艺术",
"Artstyle Constructivist": "艺术风格-构造主义",
"Artstyle Cubist": "艺术风格-立体主义",
"Artstyle Expressionist": "艺术风格-表现主义",
"Artstyle Graffiti": "艺术风格-涂鸦",
"Artstyle Hyperrealism": "艺术风格-超写实主义",
"Artstyle Impressionist": "艺术风格-印象派",
"Artstyle Pointillism": "艺术风格-点彩派",
"Artstyle Pop Art": "艺术风格-波普艺术",
"Artstyle Psychedelic": "艺术风格-迷幻",
"Artstyle Renaissance": "艺术风格-文艺复兴",
"Artstyle Steampunk": "艺术风格-蒸汽朋克",
"Artstyle Surrealist": "艺术风格-超现实主义",
"Artstyle Typography": "艺术风格-字体设计",
"Artstyle Watercolor": "艺术风格-水彩",
"Futuristic Biomechanical": "未来主义-生物机械",
"Futuristic Biomechanical Cyberpunk": "未来主义-生物机械-赛博朋克",
"Futuristic Cybernetic": "未来主义-人机融合",
"Futuristic Cybernetic Robot": "未来主义-人机融合-机器人",
"Futuristic Cyberpunk Cityscape": "未来主义-赛博朋克城市",
"Futuristic Futuristic": "未来主义-未来主义",
"Futuristic Retro Cyberpunk": "未来主义-复古赛博朋克",
"Futuristic Retro Futurism": "未来主义-复古未来主义",
"Futuristic Sci Fi": "未来主义-科幻",
"Futuristic Vaporwave": "未来主义-蒸汽波",
"Game Bubble Bobble": "游戏-泡泡龙",
"Game Cyberpunk Game": "游戏-赛博朋克游戏",
"Game Fighting Game": "游戏-格斗游戏",
"Game Gta": "游戏-侠盗猎车手",
"Game Mario": "游戏-马里奥",
"Game Minecraft": "游戏-我的世界",
"Game Pokemon": "游戏-宝可梦",
"Game Retro Arcade": "游戏-复古街机",
"Game Retro Game": "游戏-复古游戏",
"Game Rpg Fantasy Game": "游戏-角色扮演幻想游戏",
"Game Strategy Game": "游戏-策略游戏",
"Game Streetfighter": "游戏-街头霸王",
"Game Zelda": "游戏-塞尔达传说",
"Misc Architectural": "其他-建筑",
"Misc Disco": "其他-迪斯科",
"Misc Dreamscape": "其他-梦境",
"Misc Dystopian": "其他-反乌托邦",
"Misc Fairy Tale": "其他-童话故事",
"Misc Gothic": "其他-哥特风",
"Misc Grunge": "其他-垮掉的",
"Misc Horror": "其他-恐怖",
"Misc Kawaii": "其他-可爱",
"Misc Lovecraftian": "其他-洛夫克拉夫特",
"Misc Macabre": "其他-恐怖",
"Misc Manga": "其他-漫画",
"Misc Metropolis": "其他-大都市",
"Misc Minimalist": "其他-极简主义",
"Misc Monochrome": "其他-单色",
"Misc Nautical": "其他-航海",
"Misc Space": "其他-太空",
"Misc Stained Glass": "其他-彩色玻璃",
"Misc Techwear Fashion": "其他-科技时尚",
"Misc Tribal": "其他-部落",
"Misc Zentangle": "其他-禅绕画",
"Papercraft Collage": "手工艺-拼贴",
"Papercraft Flat Papercut": "手工艺-平面剪纸",
"Papercraft Kirigami": "手工艺-切纸",
"Papercraft Paper Mache": "手工艺-纸浆塑造",
"Papercraft Paper Quilling": "手工艺-纸艺卷轴",
"Papercraft Papercut Collage": "手工艺-剪纸拼贴",
"Papercraft Papercut Shadow Box": "手工艺-剪纸影箱",
"Papercraft Stacked Papercut": "手工艺-层叠剪纸",
"Papercraft Thick Layered Papercut": "手工艺-厚层剪纸",
"Photo Alien": "摄影-外星人",
"Photo Film Noir": "摄影-黑色电影",
"Photo Glamour": "摄影-魅力",
"Photo Hdr": "摄影-高动态范围",
"Photo Iphone Photographic": "摄影-苹果手机摄影",
"Photo Long Exposure": "摄影-长曝光",
"Photo Neon Noir": "摄影-霓虹黑色",
"Photo Silhouette": "摄影-轮廓",
"Photo Tilt Shift": "摄影-移轴",
"Cinematic Diva": "电影女主角",
"Abstract Expressionism": "抽象表现主义",
"Academia": "学术",
"Action Figure": "动作人偶",
"Adorable 3D Character": "可爱的3D角色",
"Adorable Kawaii": "可爱的卡哇伊",
"Art Deco": "装饰艺术",
"Art Nouveau": "新艺术,美丽艺术",
"Astral Aura": "星体光环",
"Avant Garde": "前卫",
"Baroque": "巴洛克",
"Bauhaus Style Poster": "包豪斯风格海报",
"Blueprint Schematic Drawing": "蓝图示意图",
"Caricature": "漫画",
"Cel Shaded Art": "卡通渲染",
"Character Design Sheet": "角色设计表",
"Classicism Art": "古典主义艺术",
"Color Field Painting": "色彩领域绘画",
"Colored Pencil Art": "彩色铅笔艺术",
"Conceptual Art": "概念艺术",
"Constructivism": "建构主义",
"Cubism": "立体主义",
"Dadaism": "达达主义",
"Dark Fantasy": "黑暗奇幻",
"Dark Moody Atmosphere": "黑暗忧郁气氛",
"Dmt Art Style": "迷幻艺术风格",
"Doodle Art": "涂鸦艺术",
"Double Exposure": "双重曝光",
"Dripping Paint Splatter Art": "滴漆飞溅艺术",
"Expressionism": "表现主义",
"Faded Polaroid Photo": "褪色的宝丽来照片",
"Fauvism": "野兽派",
"Flat 2d Art": "平面 2D 艺术",
"Fortnite Art Style": "堡垒之夜艺术风格",
"Futurism": "未来派",
"Glitchcore": "故障核心",
"Glo Fi": "光明高保真",
"Googie Art Style": "古吉艺术风格",
"Graffiti Art": "涂鸦艺术",
"Harlem Renaissance Art": "哈莱姆文艺复兴艺术",
"High Fashion": "高级时装",
"Idyllic": "田园诗般",
"Impressionism": "印象派",
"Infographic Drawing": "信息图表绘图",
"Ink Dripping Drawing": "滴墨绘画",
"Japanese Ink Drawing": "日式水墨画",
"Knolling Photography": "规律摆放摄影",
"Light Cheery Atmosphere": "轻松愉快的气氛",
"Logo Design": "标志设计",
"Luxurious Elegance": "奢华优雅",
"Macro Photography": "微距摄影",
"Mandola Art": "曼陀罗艺术",
"Marker Drawing": "马克笔绘图",
"Medievalism": "中世纪主义",
"Minimalism": "极简主义",
"Neo Baroque": "新巴洛克",
"Neo Byzantine": "新拜占庭",
"Neo Futurism": "新未来派",
"Neo Impressionism": "新印象派",
"Neo Rococo": "新洛可可",
"Neoclassicism": "新古典主义",
"Op Art": "欧普艺术",
"Ornate And Intricate": "华丽而复杂",
"Pencil Sketch Drawing": "铅笔素描",
"Pop Art 2": "流行艺术2",
"Rococo": "洛可可",
"Silhouette Art": "剪影艺术",
"Simple Vector Art": "简单矢量艺术",
"Sketchup": "草图",
"Steampunk 2": "赛博朋克2",
"Surrealism": "超现实主义",
"Suprematism": "至上主义",
"Terragen": "地表风景",
"Tranquil Relaxing Atmosphere": "宁静轻松的氛围",
"Sticker Designs": "贴纸设计",
"Vibrant Rim Light": "生动的边缘光",
"Volumetric Lighting": "体积照明",
"Watercolor 2": "水彩2",
"Whimsical And Playful": "异想天开、俏皮",
"Mk Chromolithography": "MK 色彩版画",
"Mk Cross Processing Print": "MK 交叉过程打印",
"Mk Dufaycolor Photograph": "MK 杜法色彩照片",
"Mk Herbarium": "MK 植物标本馆",
"Mk Punk Collage": "MK 朋克拼贴画",
"Mk Mosaic": "MK 镶嵌图",
"Mk Van Gogh": "MK 梵高",
"Mk Coloring Book": "MK 色彩书",
"Mk Singer Sargent": "MK 辛格 · 萨尔生特",
"Mk Pollock": "MK 波洛克",
"Mk Basquiat": "MK 巴斯奎特",
"Mk Andy Warhol": "MK 安迪 · 沃霍尔",
"Mk Halftone Print": "MK 半色版画",
"Mk Gond Painting": "MK 贡德绘画",
"Mk Albumen Print": "MK 白蛋清印刷",
"Mk Aquatint Print": "MK 水蚀刻印刷",
"Mk Anthotype Print": "MK 花纹版画",
"Mk Inuit Carving": "MK 因纽特雕塑",
"Mk Bromoil Print": "MK 溴油印刷",
"Mk Calotype Print": "MK 卡洛雅图印刷",
"Mk Color Sketchnote": "MK色彩素描笔记",
"Mk Cibulak Porcelain": "MK 西布拉瓷器",
"Mk Alcohol Ink Art": "MK 酒精水彩艺术",
"Mk One Line Art": "MK 一线画",
"Mk Blacklight Paint": "MK 黑光油漆",
"Mk Carnival Glass": "MK 嘉年华玻璃",
"Mk Cyanotype Print": "MK 青色版画",
"Mk Cross Stitching": "MK 交叉针织",
"Mk Encaustic Paint": "MK 蜡漆",
"Mk Embroidery": "MK 刺绣",
"Mk Gyotaku": "MK 鱼拓版画",
"Mk Luminogram": "MK 光感影像",
"Mk Lite Brite Art": "MK 彩色灯泡艺术",
"Mk Mokume Gane": "MK 木金工艺",
"Pebble Art": "MK 鹅卵石艺术",
"Mk Palekh": "MK 帕列赫",
"Mk Suminagashi": "MK 澄洗画",
"Mk Scrimshaw": "MK 丝线绣",
"Mk Shibori": "MK 湿布雕版印刷",
"Mk Vitreous Enamel": "MK 玻璃珐琅",
"Mk Ukiyo E": "MK 浮世绘",
"Mk Vintage Airline Poster": "MK 古董航空公司海报",
"Mk Vintage Travel Poster": "MK 古董旅行海报",
"Mk Bauhaus Style": "Mk 包豪斯风格",
"Mk Afrofuturism": "Mk 非洲未来主义",
"Mk Atompunk": "Mk 原子朋克",
"Mk Constructivism": "Mk 构成派",
"Mk Chicano Art": "Mk 西班牙裔美国艺术",
"Mk De Stijl": "Mk 去风格派",
"Mk Dayak Art": "Mk 达雅克艺术",
"Mk Fayum Portrait": "Mk 法尤姆肖像画",
"Mk Illuminated Manuscript": "Mk 彩绘手稿",
"Mk Kalighat Painting": "Mk 卡利加特绘画",
"Mk Madhubani Painting": "Mk 马杜班尼绘画",
"Mk Pictorialism": "Mk 描绘主义",
"Mk Pichwai Painting": "Mk 皮奇瓦伊绘画",
"Mk Patachitra Painting": "Mk 帕塔基特拉绘画",
"Mk Samoan Art Inspired": "Mk 萨莫亚艺术启发的",
"Mk Tlingit Art": "Mk 特林吉特艺术",
"Mk Adnate Style": "Mk 阿达内特风格",
"Mk Ron English Style": "Mk 罗恩英国风格",
"Mk Shepard Fairey Style": "Mk 舒帕德 · 费尔利风格"
}
+147 -117
View File
@@ -12,22 +12,22 @@ const ipadapterNodes = ["easy ipadapterApply", "easy ipadapterApplyADV" ,"easy i
const pipeNodes = ['easy pipeIn','easy pipeOut', 'easy pipeEdit']
const xyNodes = ['easy XYPlot', 'easy XYPlotAdvanced']
const extraNodes = ['easy setNode']
const modelNormalNodes = [...["Reroute"],...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes]
const modelNormalNodes = [...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes]
const suggestions = {
// prompt
"easy seed":{
"from":{
"INT": [...["Reroute"],...preSamplingNodes,...['easy fullkSampler']]
"INT": [...preSamplingNodes,...['easy fullkSampler']]
}
},
"easy positive":{
"from":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy negative":{
"from":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy wildcards":{
@@ -53,214 +53,225 @@ const suggestions = {
// sd相关
"easy fullLoader": {
"from":{
"PIPE_LINE": [...["Reroute"],...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes],
"PIPE_LINE": [...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes],
"MODEL":modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy a1111Loader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy comfyLoader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy svdLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy zero123Loader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy sv3dLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy preSampling": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
},
},
"easy preSamplingAdvanced": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingDynamicCFG": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingCustom": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingLayerDiffusion": {
"from": {
"PIPE_LINE": [...["Reroute", "easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [...["easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingNoiseIn": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
// ksampler
"easy fullkSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes]
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes]
}
},
"easy kSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes],
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes],
}
},
// cn
"easy controlnetLoader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
"easy controlnetLoaderADV":{
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
// instant
"easy instantIDApply": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy instantIDApplyADV":{
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApply":{
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApplyADV":{
"to":{
"STRING": [...["Reroute", "easy sliderControl"], ...propmts],
"COMBO": [...["Reroute", "easy promptLine"]]
"STRING": [...["easy sliderControl"], ...propmts],
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterStyleComposition":{
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
// fix
"easy preDetailerFix":{
"from": {
"PIPE_LINE": [...["Reroute", "easy detailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
},
"to":{
"PIPE_LINE": [...["Reroute", "easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]]
"PIPE_LINE": [...["easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]]
}
},
"easy preMaskDetailerFix":{
"from": {
"PIPE_LINE": [...["Reroute", "easy detailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy samLoaderPipe": {
"from":{
"PIPE_LINE": [...["Reroute", "easy preDetailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy ultralyticsDetectorPipe": {
"from":{
"PIPE_LINE": [...["Reroute", "easy preDetailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
// cascade相关
"easy cascadeLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes.filter(cate => !ipadapterNodes.includes(cate))
}
},
"easy fullCascadeKSampler":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
"easy preSamplingCascade":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes]
}
},
"easy cascadeKSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
}
class NullGraphError extends Error {
constructor(message="Attempted to access LGraph reference that was null or undefined.", cause) {
super(message, {cause})
this.name = "NullGraphError"
}
}
app.registerExtension({
name: "comfy.easyuse.suggestions",
async setup(app) {
async setup() {
const createDefaultNodeForSlot = LGraphCanvas.prototype.createDefaultNodeForSlot;
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
,optPass
);
var that = this;
const opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
, optPass || {}
);
const { afterRerouteId } = opts
const that = this;
var isFrom = opts.nodeFrom && opts.slotFrom!==null;
var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
const isFrom = opts.nodeFrom && opts.slotFrom!==null;
const isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return createDefaultNodeForSlot.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo);
@@ -271,24 +282,24 @@ app.registerExtension({
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
var nodeType = nodeX.type
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
const nodeType = nodeX.type
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "undefined":
default:
// bad ?
@@ -324,8 +335,7 @@ app.registerExtension({
for(var typeX in slotTypesDefault[fromSlotType]){
if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){
nodeNewType = slotTypesDefault[fromSlotType][typeX];
// console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType);
break; // --------
break;
}
}
}else{
@@ -379,9 +389,7 @@ app.registerExtension({
// add the node
that.graph.add(newNode);
newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0)
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
//that.graph.afterChange();
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
// connect the two!
if (isFrom){
@@ -390,11 +398,6 @@ app.registerExtension({
opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType );
}
// if connecting in between
if (isFrom && isTo){
// TODO
}
return true;
}else{
@@ -405,43 +408,54 @@ app.registerExtension({
return false;
}
let showConnectionMenu = LGraphCanvas.prototype.showConnectionMenu
LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,e: null
}
,optPass
);
var that = this;
const opts = Object.assign({
nodeFrom: null, // input
slotFrom: null, // input
nodeTo: null, // output
slotTo: null, // output
e: undefined,
allow_searchbox: this.allow_searchbox,
showSearchBox: this.showSearchBox,
}
,optPass || {}
);
const that = this;
const { graph } = this
const { afterRerouteId } = opts
const isFrom = opts.nodeFrom && opts.slotFrom;
const isTo = !isFrom && opts.nodeTo && opts.slotTo;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
var isFrom = opts.nodeFrom && opts.slotFrom;
var isTo = !isFrom && opts.nodeTo && opts.slotTo;
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return showConnectionMenu.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to showConnectionMenu");
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.")
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
default:
// bad ?
//iSlotConn = 0;
@@ -449,9 +463,8 @@ app.registerExtension({
return false;
}
var options = ["Add Node",null];
if (that.allow_searchbox){
const options = ["Add Node", "Add Reroute", null]
if (opts.allow_searchbox){
options.push("Search");
options.push(null);
}
@@ -479,8 +492,12 @@ app.registerExtension({
// build menu
var menu = new LiteGraph.ContextMenu(options, {
event: opts.e,
title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked
extra: slotX,
title:
(slotX && slotX.name != ""
? slotX.name + (fromSlotType ? " | " : "")
: "") + (slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked,
});
// callback
@@ -488,32 +505,45 @@ app.registerExtension({
//console.log("Process showConnectionMenu selection");
switch (v) {
case "Add Node":
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){
if (isFrom){
opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType );
}else{
opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType );
LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) {
if (!node) return
if (isFrom) {
opts.nodeFrom?.connectByType(iSlotConn, node, fromSlotType, { afterRerouteId })
} else {
opts.nodeTo?.connectByTypeOutput(iSlotConn, node, fromSlotType, { afterRerouteId })
}
});
})
break;
case "Add Reroute":
const node = isFrom ? opts.nodeFrom : opts.nodeTo
const slot = options.extra
if (!graph) throw new NullGraphError()
if (!node) throw new TypeError("Cannot add reroute: node was null")
if (!slot) throw new TypeError("Cannot add reroute: slot was null")
if (!opts.e) throw new TypeError("Cannot add reroute: CanvasPointerEvent was null")
const reroute = node.connectFloatingReroute([opts.e.canvasX, opts.e.canvasY], slot, afterRerouteId)
if (!reroute) throw new Error("Failed to create reroute")
that.dirty_canvas = true
that.dirty_bgcanvas = true
break
case "Search":
if(isFrom){
that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
opts.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
}else{
that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
opts.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
}
break;
default:
// check for defaults nodes for this slottype
var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY]
,nodeType: v
}));
if (nodeCreated){
// new node created
//console.log("node "+v+" created")
}else{
// failed or v is not in defaults
const customProps = {
position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0],
nodeType: v,
afterRerouteId,
}
// check for defaults nodes for this slottype
that.createDefaultNodeForSlot(Object.assign(opts, customProps))
break;
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long