Compare commits

...
49 Commits
Author SHA1 Message Date
yolain 38851372e1 Upgrade version 1.1.8 to comfyregistry 2024-06-04 23:32:31 +08:00
yolain 1e9ffc5ffc add:auto translate chinese prompt to english 2024-06-03 14:44:28 +08:00
yolain ccb17f18b8 fix:xyplot error #192 2024-06-03 10:19:50 +08:00
yolain 380c596d9a fix:easy preSamplingCustom error 2024-06-02 02:21:49 +08:00
yolain 4c3328797b fix:compatibility powerpaint and brushnet new version 2024-06-01 15:35:55 +08:00
yolain d22f1f44f8 add:remove backend cache when clean gpu used 2024-05-31 15:51:03 +08:00
yolain fb435d47ea fix:easy imageChooser can not cancel queue 2024-05-29 19:08:58 +08:00
yolain d9d597bf83 fix:image object has not attribute movedim in layerDiffuse 2024-05-28 21:58:48 +08:00
yolain 1913c65d6f add:swapper for brushnet&powerpaint 2024-05-27 20:49:21 +08:00
yolain b8b24040eb add:easy controlnetStack 2024-05-26 21:40:01 +08:00
yolain e9bed88d63 optimized code for easy loader 2024-05-25 18:51:38 +08:00
yolain d95772147f 💖Upgrade to v1.1.8 2024-05-25 11:47:51 +08:00
yolain 7cbc2a2a2b modify the pyproject.toml file 2024-05-23 13:33:03 +08:00
yolain 63811206c9 Merge pull request #182 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-05-23 13:30:16 +08:00
yolain 48a4b5bfc6 Merge pull request #183 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-05-23 13:30:00 +08:00
yolain 7980f4eef4 fix:compatible with new brushnet versions #181 2024-05-23 12:33:45 +08:00
haohaocreates b19c8b07ea chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-22 18:08:19 -04:00
haohaocreates 3f506d265c chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-22 18:08:16 -04:00
yolain d8af918e7d fix:the modal for selecting an image can not be closed #180 2024-05-21 15:54:02 +08:00
yolain 33566e8474 fix:run error when input optional image in easy fullkSampler #178 2024-05-21 15:41:39 +08:00
yolain ea0350c2dc fix:preview image offset in easy stylesSelector when zooming the page #176 2024-05-18 15:35:28 +08:00
yolain 24526623fb fix:iclight cache bug #173 2024-05-17 17:00:39 +08:00
yolain 7715ebfd06 Merge pull request #170 from chenpx976/main
feat: optimize GPU memory management and model unloading
2024-05-16 12:52:09 +08:00
color 2ba814c131 feat: optimize GPU memory management and model unloading 2024-05-16 12:35:40 +08:00
yolain d5ad332666 fix:icLightModel add to cache 2024-05-16 11:57:19 +08:00
yolain fee7e7bf73 fix:some models were not successfully written to easyCache,resulting in slow secondary diffusion 2024-05-16 01:01:03 +08:00
yolain 01f17ff02b remove:unnecessary import modules 2024-05-15 17:07:14 +08:00
yolain da7120219a fix:diffusers version>0.26.0 use different module #70 2024-05-15 10:50:19 +08:00
yolain d616d18069 fix:compatible with cg-image-picker #169 2024-05-15 10:04:00 +08:00
yolain dbf76f288c Update README.md 2024-05-14 16:49:16 +08:00
yolain 05124006ba fix:set_clip_options no longer compare version #165 2024-05-13 14:23:55 +08:00
yolain 4586af311c fix:fooocus+dd wrong 2024-05-13 14:19:33 +08:00
yolain 1cea58c7cf add:remove_bg in easy icLightApply 2024-05-11 02:13:55 +08:00
yolain a84f7c4a58 fix:brushnet error in easy presamplingInpainting 2024-05-11 00:42:14 +08:00
yolain 1d9bf86560 add:easy icLightApply 2024-05-10 17:26:04 +08:00
yolain 8d352b85bc add:easy imageSplitGrid 2024-05-09 16:08:03 +08:00
yolain 403562575c add:easy imageCropFromMask & imageUncropFromBBOX 2024-05-06 12:52:37 +08:00
yolain dbe2cd6569 fix:compatibility with comfyui-brushnet new commit 2024-05-05 11:30:41 +08:00
yolain b3b0a961c5 fix:raise excenption when comfyui-brushnet is not installed 2024-05-04 19:26:18 +08:00
yolain 9dfb8b9c15 fix:layerdiffuse everything config #158 2024-05-04 19:19:01 +08:00
yolain b4ea58946c fix:supported brushnet for ays 2024-05-04 01:01:35 +08:00
yolain 513fc4b67e support for brushnet model loading 2024-05-03 18:24:47 +08:00
yolain 924a16e31c fix:easy kSamplerInpainting and js file set full relative path 2024-05-02 12:01:56 +08:00
yolain ad43f8e0bb fix:preview&choose return new batch #153 2024-04-30 17:24:21 +08:00
yolain b7e1ce8a3c fix:easy imageChooser is not working #150 2024-04-29 12:03:09 +08:00
yolain 299090d184 add:denoise value to alignYourStepsScheduler #146 2024-04-29 11:32:49 +08:00
yolain 3aa7bca86b add:align_your_steps in all easy preSampling 2024-04-26 19:39:26 +08:00
yolain 56de6f0bc9 add:alignYourSteps of scheduler in easy preSamplingCustom #146 2024-04-26 15:35:06 +08:00
yolain f6b5f5c99c Upgrade to v1.1.6 2024-04-26 11:55:12 +08:00
65 changed files with 4031 additions and 1190 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+2 -1
View File
@@ -11,4 +11,5 @@ docs/**
.vscode/
.idea/
mmb-preset.custom.txt
config.yaml
config.yaml
node.tar.gz
+59 -18
View File
@@ -30,10 +30,37 @@
- Background removal nodes for the RMBG-1.4 model supporting BriaAI, [BriaAI Guide](https://huggingface.co/briaai/RMBG-1.4)
- Forcibly cleared the memory usage of the comfy UI model are supported
- Stable Diffusion 3 multi-account API nodes are supported
-
## Changelog
**v1.1.5 (2024/4/24)**
**v1.1.8**
- Added `easy controlnetStack`
- Added `easy applyBrushNet` - [Workflow Example](https://github.com/yolain/ComfyUI-Yolain-Workflows/blob/main/workflows/2_advanced/2-4inpainting/2-4brushnet_1.1.8.json)
- Added `easy applyPowerPaint` - [Workflow Example](https://github.com/yolain/ComfyUI-Yolain-Workflows/blob/main/workflows/2_advanced/2-4inpainting/2-4powerpaint_outpaint_1.1.8.json)
**v1.1.7**
- Added `easy prompt` - Subject and light presets, maybe adjusted later
- Added `easy icLightApply` - Light and shadow migration, Code based on [ComfyUI-IC-Light](https://github.com/huchenlei/ComfyUI-IC-Light)
- Added `easy imageSplitGrid`
- `easy kSamplerInpainting` added options such as different diffusion and brushnet in **additional** widget
- Support for brushnet model loading - [ComfyUI-BrushNet](https://github.com/nullquant/ComfyUI-BrushNet)
- Added `easy applyFooocusInpaint` - Replace FooocusInpaintLoader
- Removed `easy fooocusInpaintLoader`
**v1.1.6**
- Added **alignYourSteps** to **schedulder** widget in all `easy preSampling` and `easy fullkSampler`
- Added **Preview&Choose** to **image_output** widget in `easy kSampler` & `easy fullkSampler`
- Added `easy styleAlignedBatchAlign` - Credit of [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy)
- Added `easy ckptNames`
- Added `easy controlnetNames`
- Added `easy imagesSplitimage` - Batch images split into single images
- Added `easy imageCount` - Get Image Count
- Added `easy textSwitch` - Text Switch
**v1.1.5**
- Rewrite `easy cleanGPUUsed` - the memory usage of the comfyUI can to be cleared
- Added `easy humanSegmentation` - Human Part Segmentation
@@ -43,7 +70,7 @@
- Added `easy imageInterrogator` - Image To Prompt
- Added `easy stableDiffusion3API` - Easy Stable Diffusion 3 Multiple accounts API Node
**v1.1.4 (2024/4/10)**
**v1.1.4**
- Added `easy preSamplingCustom` - Custom-PreSampling, can be supported cosXL-edit
- Added `easy ipadapterStyleComposition`
@@ -51,7 +78,7 @@
- Fixed `easy preSamplingNoiseIn`、`easy latentNoisy`、`east Unsampler` compatible with ComfyUI Revision>=2098 [0542088e] or later
**v1.1.3 (2024/4/4)**
**v1.1.3**
- `easy ipadapterApply` Added **COMPOSITION** preset
- Supported [ResAdapter](https://huggingface.co/jiaxiangc/res-adapter) when load ResAdapter lora
@@ -60,7 +87,7 @@
- Added `easy promptConcat`
- `easy wildcards` Added **multiline_mode**
**v1.1.2 (39c5ccf)**
**v1.1.2**
- Optimized some of the recommended nodes for slots related to EasyUse
- Added **Enable ContextMenu Auto Nest Subdirectories** The setting item is enabled by default, and it can be classified into subdirectories, checkpoints and loras previews
@@ -76,7 +103,7 @@
- Fixed layerDiffuse xyplot bug
- `easy kSamplerInpainting` add *additional* widget,you can choose 'Differential Diffusion' or 'Only InpaintModelConditioning'
**v1.1.1 (2024/3/16)**
**v1.1.1**
- The issue that the seed is 0 when a node with a seed control is added and **control before generate** is fixed for the first time run queue prompt.
- `easy preSamplingAdvanced` Added **return_with_leftover_noise**
@@ -86,7 +113,7 @@
- Remove forced **control_before_generate** settings。 If you want to use control_before_generate, change widget_value_control_mode to before in system settings
- Added `easy imageRemBg` - The default is BriaAI's RMBG-1.4 model, which removes the background effect more and faster
**v1.1.0 (d5ff84e)**
**v1.1.0**
- Added `easy imageSplitList` - to split every N images
- Added `easy preSamplingDiffusionADDTL` - It can modify foreground、background or blended additional prompt
@@ -102,15 +129,18 @@
- Fixed `easy instantIDApply` mask not input right
**v1.0.9 (ff1add1)**
<details>
<summary><b>v1.0.9</b></summary>
- Fixed the error when ComfyUI-Impack-Pack and ComfyUI_InstantID were not installed
- Fixed `easy pipeIn`
- Added `easy instantIDApply` - you need installed [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) fisrt, Workflow[Example](https://github.com/yolain/ComfyUI-Easy-Use/blob/main/README.en.md#InstantID)
- Fixed `easy detailerFix` not added to the list of nodes available for saving images formatting extensions
- Fixed `easy XYInputs: PromptSR` errors are reported when replacing negative prompts
</details>
**v1.0.8 (f28cbf7)**
<details>
<summary><b>v1.0.8</b></summary>
- `easy cascadeLoader` stage_c and stage_b support the checkpoint model (Download [checkpoints](https://huggingface.co/stabilityai/stable-cascade/tree/main/comfyui_checkpoints) models)
- `easy styleSelector` The search box is modified to be case-insensitive
@@ -120,8 +150,10 @@
- Fixed the error of SDXLClipModel in ComfyUI revision 2016[c2cb8e88] and above (the revision number was judged to be compatible with the old revision)
- Fixed `easy detailerFix` generation error when batch size is greater than 1
- Optimize the code, reduce a lot of redundant code and improve the running speed
</details>
**v1.0.7 (2024-02-19)**
<details>
<summary><b>v1.0.7</b></summary>
- Added `easy cascadeLoader` - stable cascade Loader
- Added `easy preSamplingCascade` - stable cascade preSampling Settings
@@ -129,22 +161,28 @@
- Added `easy cascadeKSampler` - stable cascade stage-c ksampler simple
-
- Optimize the image to image[Example](https://github.com/yolain/ComfyUI-Easy-Use/blob/main/README.en.md#image-to-image)
</details>
**v1.0.6**
<details>
<summary><b>v1.0.6</b></summary>
- Added `easy XYInputs: Checkpoint`
- Added `easy XYInputs: Lora`
- `easy seed` can manually switch the random seed when increasing the fixed seed value
- Fixed `easy fullLoader` and all loaders to automatically adjust the node size when switching LoRa
- Removed the original ttn image saving logic and adapted to the default image saving format extension of ComfyUI
</details>
- **v1.0.5**
<details>
<summary><b>v1.0.5</b></summary>
- Added `easy isSDXL`
- Added prompt word control on `easy svdLoader`, which can be used with open_clip model
- Added **populated_text** on `easy wildcards`, wildcard populated text can be output
</details>
**v1.0.4**
<details>
<summary><b>v1.0.4</b></summary>
- `easy showAnything` added support for converting other types (e.g., tensor conditions, images, etc.)
- Added `easy showLoaderSettingsNames` can display the model and VAE name in the output loader assembly
@@ -164,9 +202,10 @@
- Changing the first-time install node package no longer automatically replaces the theme, you need to manually adjust and refresh the page
- `easy imageSave` added **only_preivew**
- Adjust the `easy latentCompositeMaskedWithCond` node
</details>
**v1.0.3**
<details>
<summary><b>v1.0.3</b></summary>
- Added `easy stylesSelector`
- Added **scale_soft_weights** in `easy controlnetLoader` and `easy controlnetLoaderADV`
@@ -184,9 +223,10 @@
- Adjust the UI theme, divided into two sets of styles: the official default background and the dark black background, which can be switched in the color palette in the settings
- Modify the styles path to be compatible with other environments
</details>
**v1.0.2**
<details>
<summary><b>v1.0.2</b></summary>
- Added `easy XYPlotAdvanced` and some nodes about `easy XYInputs`
- Added **Alt+1-Alt+9** Shortcut keys to quickly paste node presets for Node templates (corresponding to 1~9 sequences)
@@ -203,7 +243,7 @@
- Removed `easy imageRemBg`
- Remove the introductory diagram and workflow files from the package to reduce the package size
- Replaced the font file used in the generation of XY diagrams
</details>
<details>
<summary><b>v1.0.1</b></summary>
@@ -298,6 +338,7 @@ Disclaimer: Opened source was not easy. I have a lot of respect for the contribu
| easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply... |
| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter |
| easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser |
| easy styleAlignedBatchAlign | [style_aligned_comfy](https://github.com/chrisgoringe/cg-image-picker) | styleAlignedBatchAlign |
## Workflow Examples
+58 -47
View File
@@ -12,7 +12,7 @@
**ComfyUI-Easy-Use** 是一个化繁为简的节点整合包, 在 [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) 的基础上进行延展,并针对了诸多主流的节点包做了整合与优化,以达到更快更方便使用ComfyUI的目的,在保证自由度的同时还原了本属于Stable Diffusion的极致畅快出图体验。
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Docs/workflow_node_compare.png">
[![ComfyUI-Yolain-Workflows](https://github.com/yolain/ComfyUI-Easy-Use/assets/73304135/9a3f54bc-a677-4bf1-a196-8845dd57c942)](https://github.com/yolain/ComfyUI-Yolain-Workflows)
## 特色介绍
@@ -23,21 +23,54 @@
- 可多选的风格化提示词选择器,默认是Fooocus的样式json,可自定义json放在styles底下,samples文件夹里可放预览图(名称和name一致,图片文件名如有空格需转为下划线'_')
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像,需先安装 [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes)
- 可使用`easy latentNoisy`或`easy preSamplingNoiseIn`节点实现对潜空间的噪声注入
- 简化 SD1.x、SD2.x、SDXL、SVD、Zero123等流程 [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion)
- 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade)
- 简化 Layer Diffuse [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#LayerDiffusion), 首次使用您可能需要运行 `pip install -r requirements.txt` 安装所需依赖
- 简化 InstantID [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), 需先保证自定义节点包中安装了 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID)
- 简化 SD1.x、SD2.x、SDXL、SVD、Zero123等流程
- 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#1-13-stable-cascade)
- 简化 Layer Diffuse [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#2-3-layerdiffusion)
- 简化 InstantID [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#2-2-instantid), 需先保证自定义节点包中安装了 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID)
- 简化 IPAdapter, 需先保证自定义节点包中安装最新版v2的 [ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus)
- 扩展 XYplot 的可用性
- 整合了Fooocus Inpaint功能
- 整合了常用的逻辑计算、转换类型、展示所有类型等
- 支持节点上checkpoint、lora模型子目录分类及预览图 (请在设置中开启上下文菜单嵌套子目录)
- 支持BriaAI的RMBG-1.4模型的背景去除节点,[技术参考](https://huggingface.co/briaai/RMBG-1.4)
- 支持 强制清理comfyUI模型显存占用
- 支持Stable Diffusion 3 多账号API节点
- 支持IC-Light的应用 [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#2-5-ic-light) | [代码整合来源](https://github.com/huchenlei/ComfyUI-IC-Light) | [技术参考](https://github.com/lllyasviel/IC-Light)
- 中文提示词自动识别,使用[opus-mt-zh-en模型](https://huggingface.co/Helsinki-NLP/opus-mt-zh-en)
## 更新日志
**v1.1.5 (2024/4/24)**
**v1.1.8**
- 增加中文提示词自动翻译,使用[opus-mt-zh-en模型](https://huggingface.co/Helsinki-NLP/opus-mt-zh-en), 默认已对wildcard、lora正则处理, 其他需要保留的中文,可使用`@你的提示词@`包裹 (若依赖安装完成后报错, 请重启),测算大约会占0.3GB显存
- 增加 `easy controlnetStack` - controlnet堆
- 增加 `easy applyBrushNet` - [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows/blob/main/workflows/2_advanced/2-4inpainting/2-4brushnet_1.1.8.json)
- 增加 `easy applyPowerPaint` - [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows/blob/main/workflows/2_advanced/2-4inpainting/2-4powerpaint_outpaint_1.1.8.json)
**v1.1.7**
- 修复 一些模型(如controlnet模型等)未成功写入缓存,导致修改前置节点束参数(如提示词)需要二次载入模型的问题
- 增加 `easy prompt` - 主体和光影预置项,后期可能会调整
- 增加 `easy icLightApply` - 重绘光影, 从[ComfyUI-IC-Light](https://github.com/huchenlei/ComfyUI-IC-Light)优化
- 增加 `easy imageSplitGrid` - 图像网格拆分
- `easy kSamplerInpainting` 的 **additional** 属性增加差异扩散和brushnet等相关选项
- 增加 brushnet模型加载的支持 - [ComfyUI-BrushNet](https://github.com/nullquant/ComfyUI-BrushNet)
- 增加 `easy applyFooocusInpaint` - Fooocus内补节点 替代原有的 FooocusInpaintLoader
- 移除 `easy fooocusInpaintLoader` - 容易bug,不再使用
- 修改 easy kSampler等采样器中并联的model 不再替换输出中pipe里的model
**v1.1.6**
- 增加步调齐整适配 - 在所有的预采样和全采样器节点中的 调度器(schedulder) 增加了 **alignYourSteps** 选项
- `easy kSampler` 和 `easy fullkSampler` 的 **image_output** 增加 **Preview&Choose**选项
- 增加 `easy styleAlignedBatchAlign` - 风格对齐 [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy)
- 增加 `easy ckptNames`
- 增加 `easy controlnetNames`
- 增加 `easy imagesSplitimage` - 批次图像拆分单张
- 增加 `easy imageCount` - 图像数量
- 增加 `easy textSwitch` - 文字切换
**v1.1.5**
- 重写 `easy cleanGPUUsed` - 可强制清理comfyUI的模型显存占用
- 增加 `easy humanSegmentation` - 多类分割、人像分割
@@ -47,7 +80,7 @@
- 增加 `easy imageInterrogator` - 图像反推
- 增加 `easy stableDiffusion3API` - 简易的Stable Diffusion 3 多账号API节点
**v1.1.4 (2024/4/13)**
**v1.1.4**
- 增加 `easy imageChooser` - 从[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker)简化的图片选择器
- 增加 `easy preSamplingCustom` - 自定义预采样,可支持cosXL-edit
@@ -56,7 +89,7 @@
- 修复 `easy preSamplingNoiseIn`、`easy latentNoisy`、`east Unsampler` 以兼容ComfyUI Revision>=2098 [0542088e] 以上版本
- 修复 FooocusInpaint修改ModelPatcher计算权重引发的问题,理应在生成model后重置ModelPatcher为默认值
**v1.1.3 (2024/4/4)**
**v1.1.3**
- `easy ipadapterApply` 增加 **COMPOSITION** 预置项
- 增加 对[ResAdapter](https://huggingface.co/jiaxiangc/res-adapter) lora模型 的加载支持
@@ -66,7 +99,7 @@
- `easy wildcards` 增加 **multiline_mode**属性
- 增加 当节点需要下载模型时,若huggingface连接超时,会切换至镜像地址下载模型
**v1.1.2 (39c5ccf)**
**v1.1.2**
- 改写 EasyUse 相关节点的部分插槽推荐节点
- 增加 **启用上下文菜单自动嵌套子目录** 设置项,默认为启用状态,可分类子目录及checkpoints、loras预览图
@@ -82,7 +115,7 @@
- 修复 `easy pipeEdit` 提示词输入lora时报错
- 修复 layerDiffuse xyplot相关bug
**v1.1.1 (5c8af8f)**
**v1.1.1**
- 修复首次添加含seed的节点且当前模式为control_before_generate时,seed为0的问题
- `easy preSamplingAdvanced` 增加 **return_with_leftover_noise**
@@ -93,7 +126,7 @@
- 去除强制**control_before_generate**设定
- 增加 `easy imageRemBg` - 默认为BriaAI的RMBG-1.4模型, 移除背景效果更加,速度更快
**v1.1.0 (d5ff84e)**
**v1.1.0**
- 增加 `easy imageSplitList` - 拆分每 N 张图像
- 增加 `easy preSamplingDiffusionADDTL` - 可配置前景、背景、blended的additional_prompt等
@@ -107,15 +140,18 @@
- 修复 `easy instantIDApply` mask 未传入正确值
- 修复 在 非a1111提示词风格下 BREAK 不生效的问题
**v1.0.9 (ff1add1)**
<details>
<summary><b>v1.0.9</b></summary>
- 修复未安装 ComfyUI-Impack-Pack 和 ComfyUI_InstantID 时报错
- 修复 `easy pipeIn` - pipe设为可不必选
- 增加 `easy instantIDApply` - 需要先安装 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID), 工作流参考[示例](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID)
- 增加 `easy instantIDApply` - 需要先安装 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID), 工作流参考[示例](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#2-2-instantid)
- 修复 `easy detailerFix` 未添加到保存图片格式化扩展名可用节点列表
- 修复 `easy XYInputs: PromptSR` 在替换负面提示词时报错
</details>
**v1.0.8 (f28cbf7)**
<details>
<summary><b>v1.0.8</b></summary>
- `easy cascadeLoader` stage_c 与 stage_b 支持checkpoint模型 (需要下载[checkpoints](https://huggingface.co/stabilityai/stable-cascade/tree/main/comfyui_checkpoints))
- `easy styleSelector` 搜索框修改为不区分大小写匹配
@@ -129,13 +165,16 @@
(翻译对照已由 [AIGODLIKE-COMFYUI-TRANSLATION](https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Translation) 统一维护啦!
首次下载或者版本较早的朋友请更新 AIGODLIKE-COMFYUI-TRANSLATION 和本节点包至最新版本。)
</details>
**v1.0.7**
<details>
<summary><b>v1.0.7</b></summary>
- 增加 `easy cascadeLoader` - stable cascade 加载器
- 增加 `easy preSamplingCascade` - stabled cascade stage_c 预采样参数
- 增加 `easy fullCascadeKSampler` - stable cascade stage_c 完整版采样器
- 增加 `easy cascadeKSampler` - stable cascade stage-c ksampler simple
</details>
<details>
<summary><b>v1.0.6</b></summary>
@@ -310,38 +349,8 @@
| easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply等 |
| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter |
| easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser |
## 示例
导入后请自行更换您目录里的大模型
### StableDiffusion
#### 文生图
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/Simple/text_to_image.png">
#### 图生图+controlnet
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/Simple/image_to_image_controlnet.png">
#### InstantID
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/Simple/instantID.png">
### LayerDiffusion
#### SD15
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/Simple/layer_diffusion_sd15.png">
#### SDXL
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/Simple/layer_diffusion_example.png">
### StableCascade
#### 文生图
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/StableCascade/text_to_image.png">
#### 图生图
<img src="https://raw.githubusercontent.com/yolain/yolain-comfyui-workflow/main/Workflows/StableCascade/image_to_image.png">
| easy styleAlignedBatchAlign | [style_aligned_comfy](https://github.com/chrisgoringe/cg-image-picker) | styleAlignedBatchAlign |
| easy icLightApply | [ComfyUI-IC-Light](https://github.com/huchenlei/ComfyUI-IC-Light) | ICLightApply等 |
## Credits
@@ -368,3 +377,5 @@
[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss 小蛇🐍脚本
[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) - 图片选择器
[ComfyUI-BrushNet](https://github.com/nullquant/ComfyUI-BrushNet) - BrushNet 内补节点
+4 -32
View File
@@ -1,5 +1,6 @@
__version__ = "1.1.8"
import os
import glob
import folder_paths
import importlib
from pathlib import Path
@@ -24,7 +25,7 @@ cwd_path = os.path.dirname(os.path.realpath(__file__))
comfy_path = folder_paths.base_path
#Wildcards读取
from .py.wildcards import read_wildcard_dict
from .py.libs.wildcards import read_wildcard_dict
wildcards_path = os.path.join(os.path.dirname(__file__), "wildcards")
if os.path.exists(wildcards_path):
read_wildcard_dict(wildcards_path)
@@ -41,35 +42,6 @@ else:
os.mkdir(styles_path)
os.mkdir(samples_path)
#合并autocomplete覆盖到pyssss包
pyssss_path = os.path.join(comfy_path, "custom_nodes", "ComfyUI-Custom-Scripts", "user")
combine_folder = os.path.join(cwd_path, "autocomplete")
if os.path.exists(combine_folder):
pass
else:
os.mkdir(combine_folder)
if os.path.exists(pyssss_path):
output_file = os.path.join(pyssss_path, "autocomplete.txt")
# 遍历 combine 目录下的所有 txt 文件,读取内容并合并
merged_content = ''
for file_path in glob.glob(os.path.join(combine_folder, '*.txt')):
with open(file_path, 'r', encoding='utf-8', errors='ignore') as file:
try:
file_content = file.read()
merged_content += file_content + '\n'
except UnicodeDecodeError:
pass
# 备份之前的autocomplete
# bak_file = os.path.join(pyssss_path, "autocomplete.txt.bak")
# if os.path.exists(bak_file):
# pass
# elif os.path.exists(output_file):
# shutil.copy(output_file, bak_file)
if merged_content != '':
# 将合并的内容写入目标文件 autocomplete.txt,并指定编码为 utf-8
with open(output_file, 'w', encoding='utf-8') as target_file:
target_file.write(merged_content)
# ComfyUI-Easy-PS相关 (需要把模型预览图暴露给PS读取,此处借鉴了 AIGODLIKE-ComfyUI-Studio 的部分代码)
from .py.libs.add_resources import add_static_resource
from .py.libs.model import easyModelManager
@@ -85,4 +57,4 @@ WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"]
print('\033[34mComfy-Easy-Use (v1.1.5): \033[92mLoaded\033[0m')
print(f'\033[34mComfy-Easy-Use v{__version__}: \033[92mLoaded\033[0m')
+16
View File
@@ -0,0 +1,16 @@
@echo off
set "requirements_txt=%~dp0\requirements.txt"
set "python_exec=..\..\..\python_embeded\python.exe"
echo Installing EasyUse Requirements...
if exist "%python_exec%" (
echo Installing with ComfyUI Portable
"%python_exec%" -s -m pip install -r "%requirements_txt%"
) else (
echo Installing with system Python
pip install -r "%requirements_txt%"
)
pause
+34
View File
@@ -0,0 +1,34 @@
import folder_paths
import os
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
for full_folder_path in full_folder_paths:
folder_paths.add_model_folder_path(folder_name, full_folder_path)
if folder_name in folder_paths.folder_names_and_paths:
current_paths, current_extensions = folder_paths.folder_names_and_paths[folder_name]
updated_extensions = current_extensions | extensions
folder_paths.folder_names_and_paths[folder_name] = (current_paths, updated_extensions)
else:
folder_paths.folder_names_and_paths[folder_name] = (full_folder_paths, extensions)
image_suffixs = set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff", ".svg", ".ico", ".apng", ".tif", ".hdr", ".exr"])
model_path = folder_paths.models_dir
add_folder_path_and_extensions("ultralytics_bbox", [os.path.join(model_path, "ultralytics", "bbox")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("ultralytics_segm", [os.path.join(model_path, "ultralytics", "segm")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("ultralytics", [os.path.join(model_path, "ultralytics")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets_bbox", [os.path.join(model_path, "mmdets", "bbox")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets_segm", [os.path.join(model_path, "mmdets", "segm")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets", [os.path.join(model_path, "mmdets")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("sams", [os.path.join(model_path, "sams")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("onnx", [os.path.join(model_path, "onnx")], {'.onnx'})
add_folder_path_and_extensions("instantid", [os.path.join(model_path, "instantid")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("layer_model", [os.path.join(model_path, "layer_model")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("rembg", [os.path.join(model_path, "rembg")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("ipadapter", [os.path.join(model_path, "ipadapter")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("dynamicrafter_models", [os.path.join(model_path, "dynamicrafter_models")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mediapipe", [os.path.join(model_path, "mediapipe")], set(['.tflite','.pth']))
add_folder_path_and_extensions("inpaint", [os.path.join(model_path, "inpaint")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("prompt_generator", [os.path.join(model_path, "prompt_generator")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("checkpoints_thumb", [os.path.join(model_path, "checkpoints")], image_suffixs)
add_folder_path_and_extensions("loras_thumb", [os.path.join(model_path, "loras")], image_suffixs)
+28 -2
View File
@@ -9,7 +9,9 @@ from server import PromptServer
from .config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_STYLES_SAMPLES
from .logic import ConvertAnything
from .libs.model import easyModelManager
from .libs.utils import getMetadata,cleanGPUUsedForce
from .libs.utils import getMetadata, cleanGPUUsedForce, get_local_filepath
from .libs.cache import remove_cache
from .libs.translate import has_chinese, zh_to_en
try:
import aiohttp
@@ -23,11 +25,21 @@ except ImportError:
def cleanGPU(request):
try:
cleanGPUUsedForce()
remove_cache('*')
return web.Response(status=200)
except Exception as e:
return web.Response(status=500)
pass
@PromptServer.instance.routes.post("/easyuse/translate")
async def translate(request):
post = await request.post()
text = post.get("text")
if has_chinese(text):
return web.json_response({"text": zh_to_en([text])[0]})
else:
return web.json_response({"text": text})
@PromptServer.instance.routes.get("/easyuse/reboot")
def reboot(request):
try:
@@ -138,7 +150,7 @@ async def getModelsThumbnail(request):
loras = folder_paths.get_filename_list("loras_thumb")
checkpoints_full = []
loras_full = []
if len(checkpoints) + len(loras) >= 300:
if len(checkpoints) + len(loras) >= 500:
return web.Response(status=400)
for index, i in enumerate(checkpoints):
full_path = folder_paths.get_full_path('checkpoints_thumb', str(i))
@@ -268,5 +280,19 @@ async def save_preview(request):
"image": type + "/" + os.path.basename(image_path)
})
@PromptServer.instance.routes.post("/easyuse/model/download")
async def download_model(request):
post = await request.post()
url = post.get("url")
local_dir = post.get("local_dir")
if local_dir not in ['checkpoints', 'loras', 'controlnet', 'onnx', 'instantid', 'ipadapter', 'dynamicrafter_models', 'mediapipe', 'rembg', 'layer_model']:
return web.Response(status=400)
local_path = os.path.join(folder_paths.models_dir, local_dir)
try:
get_local_filepath(url, local_path)
return web.Response(status=200)
except:
return web.Response(status=500)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
+53 -9
View File
@@ -38,7 +38,7 @@ MAX_SEED_NUM = 1125899906842624
RESOURCES_DIR = os.path.join(Path(__file__).parent.parent, "resources")
# fooocus
# inpaint
INPAINT_DIR = os.path.join(folder_paths.models_dir, "inpaint")
FOOOCUS_STYLES_DIR = os.path.join(Path(__file__).parent.parent, "styles")
FOOOCUS_STYLES_SAMPLES = 'https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/'
@@ -58,6 +58,29 @@ FOOOCUS_INPAINT_PATCH = {
"model_url": "https://huggingface.co/lllyasviel/fooocus_inpaint/resolve/main/inpaint.fooocus.patch"
},
}
BRUSHNET_MODELS = {
"random_mask": {
"sd1": {
"model_url": "https://huggingface.co/Kijai/BrushNet-fp16/resolve/main/brushnet_random_mask_fp16.safetensors"
},
"sdxl": {
"model_url": "https://huggingface.co/yolain/brushnet/resolve/main/brushnet_random_mask_sdxl.safetensors"
}
},
"segmentation_mask": {
"sd1": {
"model_url": "https://huggingface.co/Kijai/BrushNet-fp16/resolve/main/brushnet_segmentation_mask_fp16.safetensors"
},
"sdxl": {
"model_url": "https://huggingface.co/yolain/brushnet/resolve/main/brushnet_segmentation_mask_sdxl.safetensors"
}
}
}
POWERPAINT_CLIP = {
"base_fp16":{
"model_url":"https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/text_encoder/model.fp16.safetensors"
}
}
# layerDiffuse
LAYER_DIFFUSION_DIR = os.path.join(folder_paths.models_dir, "layer_model")
@@ -68,7 +91,7 @@ LAYER_DIFFUSION_VAE = {
}
},
"decode": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_vae_transparent_decoder.safetensors"
},
"sdxl": {
@@ -78,7 +101,7 @@ LAYER_DIFFUSION_VAE = {
}
LAYER_DIFFUSION = {
"Attention Injection": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_transparent_attn.safetensors"
},
"sdxl": {
@@ -89,12 +112,12 @@ LAYER_DIFFUSION = {
"sdxl": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_transparent_conv.safetensors"
},
"sd15": {
"sd1": {
"model_url": None
}
},
"Everything": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_joint.safetensors"
},
"sdxl": {
@@ -102,7 +125,7 @@ LAYER_DIFFUSION = {
}
},
"Foreground": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_fg2bg.safetensors"
},
"sdxl": {
@@ -110,7 +133,7 @@ LAYER_DIFFUSION = {
}
},
"Foreground to Background": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_fg2bg.safetensors"
},
"sdxl": {
@@ -118,7 +141,7 @@ LAYER_DIFFUSION = {
}
},
"Background": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_bg2fg.safetensors"
},
"sdxl": {
@@ -126,7 +149,7 @@ LAYER_DIFFUSION = {
}
},
"Background to Foreground": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_bg2fg.safetensors"
},
"sdxl": {
@@ -135,6 +158,27 @@ LAYER_DIFFUSION = {
},
}
# IC Light
IC_LIGHT_MODELS = {
"Foreground": {
"sd1": {
"model_url": "https://huggingface.co/huchenlei/IC-Light-ldm/resolve/main/iclight_sd15_fc_unet_ldm.safetensors"
},
"sdxl": {
"model_url": None
}
},
"Foreground&Background": {
"sd1": {
"model_url": "https://huggingface.co/huchenlei/IC-Light-ldm/resolve/main/iclight_sd15_fbc_unet_ldm.safetensors"
},
"sdxl": {
"model_url": None
}
}
}
# REMBG
REMBG_DIR = os.path.join(folder_paths.models_dir, "rembg")
REMBG_MODELS = {
+1007 -526
View File
File diff suppressed because it is too large Load Diff
+185
View File
@@ -0,0 +1,185 @@
import torch
import numpy as np
from typing import Tuple, TypedDict, Callable
import comfy.model_management
from comfy.sd import load_unet
from comfy.ldm.models.autoencoder import AutoencoderKL
from comfy.model_base import BaseModel
from PIL import Image
from nodes import VAEEncode
from ..layer_diffuse.model import ModelPatcher, calculate_weight_adjust_channel
from ..libs.image import np2tensor, pil2tensor
class UnetParams(TypedDict):
input: torch.Tensor
timestep: torch.Tensor
c: dict
cond_or_uncond: torch.Tensor
class VAEEncodeArgMax(VAEEncode):
def encode(self, vae, pixels):
assert isinstance(
vae.first_stage_model, AutoencoderKL
), "ArgMax only supported for AutoencoderKL"
original_sample_mode = vae.first_stage_model.regularization.sample
vae.first_stage_model.regularization.sample = False
ret = super().encode(vae, pixels)
vae.first_stage_model.regularization.sample = original_sample_mode
return ret
class ICLight:
@staticmethod
def apply_c_concat(params: UnetParams, concat_conds) -> UnetParams:
"""Apply c_concat on unet call."""
sample = params["input"]
params["c"]["c_concat"] = torch.cat(
(
[concat_conds.to(sample.device)]
* (sample.shape[0] // concat_conds.shape[0])
),
dim=0,
)
return params
@staticmethod
def create_custom_conv(
original_conv: torch.nn.Module,
dtype: torch.dtype,
device=torch.device,
) -> torch.nn.Module:
with torch.no_grad():
new_conv_in = torch.nn.Conv2d(
8,
original_conv.out_channels,
original_conv.kernel_size,
original_conv.stride,
original_conv.padding,
)
new_conv_in.weight.zero_()
new_conv_in.weight[:, :4, :, :].copy_(original_conv.weight)
new_conv_in.bias = original_conv.bias
return new_conv_in.to(dtype=dtype, device=device)
def generate_lighting_image(self, original_image, direction):
_, image_height, image_width, _ = original_image.shape
match direction:
case 'Left Light':
gradient = np.linspace(255, 0, image_width)
image = np.tile(gradient, (image_height, 1))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Right Light':
gradient = np.linspace(0, 255, image_width)
image = np.tile(gradient, (image_height, 1))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Top Light':
gradient = np.linspace(255, 0, image_height)[:, None]
image = np.tile(gradient, (1, image_width))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Bottom Light':
gradient = np.linspace(0, 255, image_height)[:, None]
image = np.tile(gradient, (1, image_width))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Circle Light':
x = np.linspace(-1, 1, image_width)
y = np.linspace(-1, 1, image_height)
x, y = np.meshgrid(x, y)
r = np.sqrt(x ** 2 + y ** 2)
r = r / r.max()
color1 = np.array([0, 0, 0])[np.newaxis, np.newaxis, :]
color2 = np.array([255, 255, 255])[np.newaxis, np.newaxis, :]
gradient = (color1 * r[..., np.newaxis] + color2 * (1 - r)[..., np.newaxis]).astype(np.uint8)
image = pil2tensor(Image.fromarray(gradient))
return image
case _:
image = pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
return image
def generate_source_image(self, original_image, source):
batch_size, image_height, image_width, _ = original_image.shape
match source:
case 'Use Flipped Background Image':
if batch_size < 2:
raise ValueError('Must be at least 2 image to use flipped background image.')
original_image = [img.unsqueeze(0) for img in original_image]
image = torch.flip(original_image[1], [2])
return image
case 'Ambient':
input_bg = np.zeros(shape=(image_height, image_width, 3), dtype=np.uint8) + 64
return np2tensor(input_bg)
case 'Left Light':
gradient = np.linspace(224, 32, image_width)
image = np.tile(gradient, (image_height, 1))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Right Light':
gradient = np.linspace(32, 224, image_width)
image = np.tile(gradient, (image_height, 1))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Top Light':
gradient = np.linspace(224, 32, image_height)[:, None]
image = np.tile(gradient, (1, image_width))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case 'Bottom Light':
gradient = np.linspace(32, 224, image_height)[:, None]
image = np.tile(gradient, (1, image_width))
input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8)
return np2tensor(input_bg)
case _:
image = pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
return image
def apply(self, ic_model_path, model: ModelPatcher, c_concat: dict, ic_model=None) -> Tuple[ModelPatcher]:
try:
ModelPatcher.calculate_weight = calculate_weight_adjust_channel(ModelPatcher.calculate_weight)
except:
pass
device = comfy.model_management.get_torch_device()
dtype = comfy.model_management.unet_dtype()
work_model = model.clone()
# Apply scale factor.
base_model: BaseModel = work_model.model
scale_factor = base_model.model_config.latent_format.scale_factor
# [B, 4, H, W]
concat_conds: torch.Tensor = c_concat["samples"] * scale_factor
# [1, 4 * B, H, W]
concat_conds = torch.cat([c[None, ...] for c in concat_conds], dim=1)
def unet_dummy_apply(unet_apply: Callable, params: UnetParams):
"""A dummy unet apply wrapper serving as the endpoint of wrapper
chain."""
return unet_apply(x=params["input"], t=params["timestep"], **params["c"])
existing_wrapper = work_model.model_options.get(
"model_function_wrapper", unet_dummy_apply
)
def wrapper_func(unet_apply: Callable, params: UnetParams):
return existing_wrapper(unet_apply, params=self.apply_c_concat(params, concat_conds))
work_model.set_model_unet_function_wrapper(wrapper_func)
if not ic_model:
ic_model = load_unet(ic_model_path)
ic_model_state_dict = ic_model.model.diffusion_model.state_dict()
work_model.add_patches(
patches={
("diffusion_model." + key): (value.to(dtype=dtype, device=device),)
for key, value in ic_model_state_dict.items()
}
)
return (work_model, ic_model)
+537 -43
View File
@@ -1,20 +1,42 @@
from PIL import Image
import os
import hashlib
import folder_paths
import torch
import numpy as np
import comfy.utils
import comfy.model_management
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from server import PromptServer
from nodes import MAX_RESOLUTION
from PIL import Image, ImageDraw, ImageFilter
from torchvision.transforms import Resize, CenterCrop, GaussianBlur
from torchvision.transforms.functional import to_pil_image
from .log import log_node_info
from .libs.log import log_node_info
from .libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask
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
# 图像数量
class imageCount:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
}
}
CATEGORY = "EasyUse/Image"
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("count",)
FUNCTION = "get_count"
def get_count(self, images):
return (images.size(0),)
# 图像裁切
class imageInsetCrop:
@@ -519,6 +541,87 @@ class imageSplitList:
new_images[1].append(img)
return new_images
class imageSplitGrid:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"row": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}),
"column": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Image"
def crop(self, image, width, height, x, y):
x = min(x, image.shape[2] - 1)
y = min(y, image.shape[1] - 1)
to_x = width + x
to_y = height + y
img = image[:, y:to_y, x:to_x, :]
return img
def doit(self, images, row, column):
_, height, width, _ = images.shape
sub_width = width // column
sub_height = height // row
new_images = []
for i in range(row):
for j in range(column):
new_images.append(self.crop(images, sub_width, sub_height, j * sub_width, i * sub_height))
return (torch.cat(new_images, dim=0),)
class imagesSplitImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE")
RETURN_NAMES = ("image1", "image2", "image3", "image4", "image5")
FUNCTION = "split"
CATEGORY = "EasyUse/Image"
def split(self, images,):
new_images = torch.chunk(images, len(images), dim=0)
return new_images
class imageConcat:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
"direction": (['right','down','left','up',],{"default": 'right'}),
"match_image_size": ("BOOLEAN", {"default": False}),
}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "concat"
CATEGORY = "EasyUse/Image"
def concat(self, image1, image2, direction, match_image_size):
if match_image_size:
image2 = torch.nn.functional.interpolate(image2, size=(image1.shape[2], image1.shape[3]), mode="bilinear")
if direction == 'right':
row = torch.cat((image1, image2), dim=2)
elif direction == 'down':
row = torch.cat((image1, image2), dim=1)
elif direction == 'left':
row = torch.cat((image2, image1), dim=2)
elif direction == 'up':
row = torch.cat((image2, image1), dim=1)
return (row,)
# 图片背景移除
from .briaai.rembg import BriaRMBG, preprocess_image, postprocess_image
from .libs.utils import get_local_filepath, easySave, install_package
@@ -594,12 +697,12 @@ class imageChooser(PreviewImage):
def INPUT_TYPES(self):
return {
"required":{
"mode": (['Always Pause', 'Keep Last Selection'], {"default": "Always Pause"}),
},
"optional": {
"images": ("IMAGE",),
},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("IMAGE",)
@@ -622,8 +725,7 @@ class imageChooser(PreviewImage):
else:
return None
def chooser(self, prompt=None, my_unique_id=None, **kwargs):
def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
id = my_unique_id[0]
if id not in ChooserMessage.stash:
ChooserMessage.stash[id] = {}
@@ -646,9 +748,19 @@ class imageChooser(PreviewImage):
images = result['ui']['images']
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
# 获取上次选择
mode = kwargs.pop('mode', 'Always Pause')
last_choosen = None
if mode == 'Keep Last Selection':
if id and extra_pnginfo[0] and "workflow" in extra_pnginfo[0]:
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)
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()
@@ -663,7 +775,7 @@ class imageColorMatch(PreviewImage):
"required": {
"image_ref": ("IMAGE",),
"image_target": ("IMAGE",),
"method": (['mkl', 'hm', 'reinhard', 'mvgd', 'hm-mvgd-hm', 'hm-mkl-hm'],),
"method": (['wavelet', 'adain', 'mkl', 'hm', 'reinhard', 'mvgd', 'hm-mvgd-hm', 'hm-mkl-hm'],),
"image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -678,36 +790,38 @@ class imageColorMatch(PreviewImage):
FUNCTION = "color_match"
def color_match(self, image_ref, image_target, method, image_output, save_prefix, prompt=None, extra_pnginfo=None):
try:
from color_matcher import ColorMatcher
except:
install_package("color-matcher")
from color_matcher import ColorMatcher
cm = ColorMatcher()
image_ref = image_ref.cpu()
image_target = image_target.cpu()
batch_size = image_target.size(0)
out = []
images_target = image_target.squeeze()
images_ref = image_ref.squeeze()
image_ref_np = images_ref.numpy()
images_target_np = images_target.numpy()
if image_ref.size(0) > 1 and image_ref.size(0) != batch_size:
raise ValueError("ColorMatch: Use either single reference image or a matching batch of reference images.")
for i in range(batch_size):
image_target_np = images_target_np if batch_size == 1 else images_target[i].numpy()
image_ref_np_i = image_ref_np if image_ref.size(0) == 1 else images_ref[i].numpy()
if method in ["wavelet", "adain"]:
result_images = wavelet_color_fix(tensor2pil(image_target), tensor2pil(image_ref)) if method == 'wavelet' else adain_color_fix(tensor2pil(image_target), tensor2pil(image_ref))
new_images = pil2tensor(result_images)
else:
try:
image_result = cm.transfer(src=image_target_np, ref=image_ref_np_i, method=method)
except BaseException as e:
print(f"Error occurred during transfer: {e}")
break
out.append(torch.from_numpy(image_result))
from color_matcher import ColorMatcher
except:
install_package("color-matcher")
from color_matcher import ColorMatcher
image_ref = image_ref.cpu()
image_target = image_target.cpu()
batch_size = image_target.size(0)
out = []
images_target = image_target.squeeze()
images_ref = image_ref.squeeze()
new_images = torch.stack(out, dim=0).to(torch.float32)
image_ref_np = images_ref.numpy()
images_target_np = images_target.numpy()
if image_ref.size(0) > 1 and image_ref.size(0) != batch_size:
raise ValueError("ColorMatch: Use either single reference image or a matching batch of reference images.")
cm = ColorMatcher()
for i in range(batch_size):
image_target_np = images_target_np if batch_size == 1 else images_target[i].numpy()
image_ref_np_i = image_ref_np if image_ref.size(0) == 1 else images_ref[i].numpy()
try:
image_result = cm.transfer(src=image_target_np, ref=image_ref_np_i, method=method)
except BaseException as e:
print(f"Error occurred during transfer: {e}")
break
out.append(torch.from_numpy(image_result))
new_images = torch.stack(out, dim=0).to(torch.float32)
results = easySave(new_images, save_prefix, image_output, prompt, extra_pnginfo)
@@ -718,6 +832,94 @@ class imageColorMatch(PreviewImage):
return {"ui": {"images": results},
"result": (new_images,)}
class imageDetailTransfer:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"target": ("IMAGE",),
"source": ("IMAGE",),
"mode": (["add", "multiply", "screen", "overlay", "soft_light", "hard_light", "color_dodge", "color_burn", "difference", "exclusion", "divide",],{"default": "add"}),
"blur_sigma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 100.0, "step": 0.01}),
"blend_factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.001, "round": 0.001}),
"image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"mask": ("MASK",),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
OUTPUT_NODE = True
FUNCTION = "transfer"
CATEGORY = "EasyUse/Image"
def transfer(self, target, source, mode, blur_sigma, blend_factor, image_output, save_prefix, mask=None, prompt=None, extra_pnginfo=None):
batch_size, height, width, _ = target.shape
device = comfy.model_management.get_torch_device()
target_tensor = target.permute(0, 3, 1, 2).clone().to(device)
source_tensor = source.permute(0, 3, 1, 2).clone().to(device)
if target.shape[1:] != source.shape[1:]:
source_tensor = comfy.utils.common_upscale(source_tensor, width, height, "bilinear", "disabled")
if source.shape[0] < batch_size:
source = source[0].unsqueeze(0).repeat(batch_size, 1, 1, 1)
kernel_size = int(6 * int(blur_sigma) + 1)
gaussian_blur = GaussianBlur(kernel_size=(kernel_size, kernel_size), sigma=(blur_sigma, blur_sigma))
blurred_target = gaussian_blur(target_tensor)
blurred_source = gaussian_blur(source_tensor)
if mode == "add":
new_image = (source_tensor - blurred_source) + blurred_target
elif mode == "multiply":
new_image = source_tensor * blurred_target
elif mode == "screen":
new_image = 1 - (1 - source_tensor) * (1 - blurred_target)
elif mode == "overlay":
new_image = torch.where(blurred_target < 0.5, 2 * source_tensor * blurred_target,
1 - 2 * (1 - source_tensor) * (1 - blurred_target))
elif mode == "soft_light":
new_image = (1 - 2 * blurred_target) * source_tensor ** 2 + 2 * blurred_target * source_tensor
elif mode == "hard_light":
new_image = torch.where(source_tensor < 0.5, 2 * source_tensor * blurred_target,
1 - 2 * (1 - source_tensor) * (1 - blurred_target))
elif mode == "difference":
new_image = torch.abs(blurred_target - source_tensor)
elif mode == "exclusion":
new_image = 0.5 - 2 * (blurred_target - 0.5) * (source_tensor - 0.5)
elif mode == "color_dodge":
new_image = blurred_target / (1 - source_tensor)
elif mode == "color_burn":
new_image = 1 - (1 - blurred_target) / source_tensor
elif mode == "divide":
new_image = (source_tensor / blurred_source) * blurred_target
else:
new_image = source_tensor
new_image = torch.lerp(target_tensor, new_image, blend_factor)
if mask is not None:
mask = mask.to(device)
new_image = torch.lerp(target_tensor, new_image, mask)
new_image = torch.clamp(new_image, 0, 1)
new_image = new_image.permute(0, 2, 3, 1).cpu().float()
results = easySave(new_image, save_prefix, image_output, prompt, extra_pnginfo)
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": (new_image,)}
return {"ui": {"images": results},
"result": (new_image,)}
# 图像反推
from .libs.image import ci
@@ -754,6 +956,7 @@ class humanSegmentation:
"image": ("IMAGE",),
"method": (["selfie_multiclass_256x256", "human_parsing_lip"],),
"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},),
},
"hidden": {
"prompt": "PROMPT",
@@ -761,8 +964,8 @@ class humanSegmentation:
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask",)
RETURN_TYPES = ("IMAGE", "MASK", "BBOX")
RETURN_NAMES = ("image", "mask", "bbox")
FUNCTION = "parsing"
CATEGORY = "EasyUse/Segmentation"
@@ -779,7 +982,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, prompt=None, my_unique_id=None):
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']:
@@ -879,7 +1082,253 @@ class humanSegmentation:
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
return (output_image, mask)
# use crop
bbox = [[0, 0, 0, 0]]
if crop_multi > 0.0:
output_image, mask, bbox = imageCropFromMask().crop(output_image, mask, crop_multi, crop_multi, 1.0)
return (output_image, mask, bbox)
class imageCropFromMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"image_crop_multi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}),
"mask_crop_multi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}),
"bbox_smooth_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "BBOX",)
RETURN_NAMES = ("crop_image", "crop_mask", "bbox",)
FUNCTION = "crop"
CATEGORY = "EasyUse/Image"
def smooth_bbox_size(self, prev_bbox_size, curr_bbox_size, alpha):
if alpha == 0:
return prev_bbox_size
return round(alpha * curr_bbox_size + (1 - alpha) * prev_bbox_size)
def smooth_center(self, prev_center, curr_center, alpha=0.5):
if alpha == 0:
return prev_center
return (
round(alpha * curr_center[0] + (1 - alpha) * prev_center[0]),
round(alpha * curr_center[1] + (1 - alpha) * prev_center[1])
)
def image2mask(self, image):
return image[:, :, :, 0]
def mask2image(self, mask):
return mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
def cropimage(self, original_images, masks, crop_size_mult, bbox_smooth_alpha):
bounding_boxes = []
cropped_images = []
self.max_bbox_width = 0
self.max_bbox_height = 0
# First, calculate the maximum bounding box size across all masks
curr_max_bbox_width = 0
curr_max_bbox_height = 0
for mask in masks:
_mask = tensor2pil(mask)
non_zero_indices = np.nonzero(np.array(_mask))
min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1])
min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0])
width = max_x - min_x
height = max_y - min_y
curr_max_bbox_width = max(curr_max_bbox_width, width)
curr_max_bbox_height = max(curr_max_bbox_height, height)
# Smooth the changes in the bounding box size
self.max_bbox_width = self.smooth_bbox_size(self.max_bbox_width, curr_max_bbox_width, bbox_smooth_alpha)
self.max_bbox_height = self.smooth_bbox_size(self.max_bbox_height, curr_max_bbox_height, bbox_smooth_alpha)
# Apply the crop size multiplier
self.max_bbox_width = round(self.max_bbox_width * crop_size_mult)
self.max_bbox_height = round(self.max_bbox_height * crop_size_mult)
bbox_aspect_ratio = self.max_bbox_width / self.max_bbox_height
# Then, for each mask and corresponding image...
for i, (mask, img) in enumerate(zip(masks, original_images)):
_mask = tensor2pil(mask)
non_zero_indices = np.nonzero(np.array(_mask))
min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1])
min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0])
# Calculate center of bounding box
center_x = np.mean(non_zero_indices[1])
center_y = np.mean(non_zero_indices[0])
curr_center = (round(center_x), round(center_y))
# If this is the first frame, initialize prev_center with curr_center
if not hasattr(self, 'prev_center'):
self.prev_center = curr_center
# Smooth the changes in the center coordinates from the second frame onwards
if i > 0:
center = self.smooth_center(self.prev_center, curr_center, bbox_smooth_alpha)
else:
center = curr_center
# Update prev_center for the next frame
self.prev_center = center
# Create bounding box using max_bbox_width and max_bbox_height
half_box_width = round(self.max_bbox_width / 2)
half_box_height = round(self.max_bbox_height / 2)
min_x = max(0, center[0] - half_box_width)
max_x = min(img.shape[1], center[0] + half_box_width)
min_y = max(0, center[1] - half_box_height)
max_y = min(img.shape[0], center[1] + half_box_height)
# Append bounding box coordinates
bounding_boxes.append((min_x, min_y, max_x - min_x, max_y - min_y))
# Crop the image from the bounding box
cropped_img = img[min_y:max_y, min_x:max_x, :]
# Calculate the new dimensions while maintaining the aspect ratio
new_height = min(cropped_img.shape[0], self.max_bbox_height)
new_width = round(new_height * bbox_aspect_ratio)
# Resize the image
resize_transform = Resize((new_height, new_width))
resized_img = resize_transform(cropped_img.permute(2, 0, 1))
# Perform the center crop to the desired size
crop_transform = CenterCrop((self.max_bbox_height, self.max_bbox_width)) # swap the order here if necessary
cropped_resized_img = crop_transform(resized_img)
cropped_images.append(cropped_resized_img.permute(1, 2, 0))
return cropped_images, bounding_boxes
def crop(self, image, mask, image_crop_multi, mask_crop_multi, bbox_smooth_alpha):
cropped_images, bounding_boxes = self.cropimage(image, mask, image_crop_multi, bbox_smooth_alpha)
cropped_mask_image, _ = self.cropimage(self.mask2image(mask), mask, mask_crop_multi, bbox_smooth_alpha)
cropped_image_out = torch.stack(cropped_images, dim=0)
cropped_mask_out = torch.stack(cropped_mask_image, dim=0)
return (cropped_image_out, cropped_mask_out[:, :, :, 0], bounding_boxes)
class imageUncropFromBBOX:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"original_image": ("IMAGE",),
"crop_image": ("IMAGE",),
"bbox": ("BBOX",),
"border_blending": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01},),
"use_square_mask": ("BOOLEAN", {"default": True}),
},
"optional":{
"optional_mask": ("MASK",)
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "uncrop"
CATEGORY = "EasyUse/Image"
def bbox_check(self, bbox, target_size=None):
if not target_size:
return bbox
new_bbox = (
bbox[0],
bbox[1],
min(target_size[0] - bbox[0], bbox[2]),
min(target_size[1] - bbox[1], bbox[3]),
)
return new_bbox
def bbox_to_region(self, bbox, target_size=None):
bbox = self.bbox_check(bbox, target_size)
return (bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3])
def uncrop(self, original_image, crop_image, bbox, border_blending, use_square_mask, optional_mask=None):
def inset_border(image, border_width=20, border_color=(0)):
width, height = image.size
bordered_image = Image.new(image.mode, (width, height), border_color)
bordered_image.paste(image, (0, 0))
draw = ImageDraw.Draw(bordered_image)
draw.rectangle((0, 0, width - 1, height - 1), outline=border_color, width=border_width)
return bordered_image
if len(original_image) != len(crop_image):
raise ValueError(
f"The number of original_images ({len(original_image)}) and cropped_images ({len(crop_image)}) should be the same")
# Ensure there are enough bboxes, but drop the excess if there are more bboxes than images
if len(bbox) > len(original_image):
print(f"Warning: Dropping excess bounding boxes. Expected {len(original_image)}, but got {len(bbox)}")
bbox = bbox[:len(original_image)]
elif len(bbox) < len(original_image):
raise ValueError("There should be at least as many bboxes as there are original and cropped images")
out_images = []
for i in range(len(original_image)):
img = tensor2pil(original_image[i])
crop = tensor2pil(crop_image[i])
_bbox = bbox[i]
bb_x, bb_y, bb_width, bb_height = _bbox
paste_region = self.bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size)
# rescale the crop image to fit the paste_region
crop = crop.resize((round(paste_region[2] - paste_region[0]), round(paste_region[3] - paste_region[1])))
crop_img = crop.convert("RGB")
# border blending
if border_blending > 1.0:
border_blending = 1.0
elif border_blending < 0.0:
border_blending = 0.0
blend_ratio = (max(crop_img.size) / 2) * float(border_blending)
blend = img.convert("RGBA")
if use_square_mask:
mask = Image.new("L", img.size, 0)
mask_block = Image.new("L", (paste_region[2] - paste_region[0], paste_region[3] - paste_region[1]), 255)
mask_block = inset_border(mask_block, round(blend_ratio / 2), (0))
mask.paste(mask_block, paste_region)
else:
if optional_mask is None:
raise ValueError("optional_mask is required when use_square_mask is False")
original_mask = tensor2pil(optional_mask)
original_mask = original_mask.resize((paste_region[2] - paste_region[0], paste_region[3] - paste_region[1]))
mask = Image.new("L", img.size, 0)
mask.paste(original_mask, paste_region)
mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4))
mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4))
blend.paste(crop_img, paste_region)
blend.putalpha(mask)
img = Image.alpha_composite(img.convert("RGBA"), blend)
out_images.append(img.convert("RGB"))
output_images = torch.cat([pil2tensor(img) for img in out_images], dim=0)
return (output_images,)
import cv2
import base64
@@ -901,7 +1350,7 @@ class loadImageBase64:
RETURN_TYPES = ("IMAGE", "MASK")
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "image"
CATEGORY = "EasyUse/Image/LoadImage"
def convert_color(self, image,):
if len(image.shape) > 2 and image.shape[2] >= 4:
@@ -961,6 +1410,35 @@ class imageToBase64:
base64_str = base64.b64encode(image_bytes).decode("utf-8")
return {"result": (base64_str,)}
class removeLocalImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"file_name": ("STRING",{"default":""}),
},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "remove"
CATEGORY = "EasyUse/Image"
def remove(self, file_name):
hasFile = False
for file in os.listdir(folder_paths.input_directory):
name_without_extension, file_extension = os.path.splitext(file)
if name_without_extension == file_name or file == file_name:
os.remove(os.path.join(folder_paths.input_directory, file))
hasFile = True
break
if hasFile:
PromptServer.instance.send_sync("easyuse-toast", {"content": "Removed SuccessFully", "type":'success'})
else:
PromptServer.instance.send_sync("easyuse-toast", {"content": "Removed Failed", "type": 'error'})
return ()
# 姿势编辑器
class poseEditor:
@classmethod
@@ -1005,6 +1483,7 @@ class poseEditor:
NODE_CLASS_MAPPINGS = {
"easy imageInsetCrop": imageInsetCrop,
"easy imageCount": imageCount,
"easy imageSize": imageSize,
"easy imageSizeBySide": imageSizeBySide,
"easy imageSizeByLongerSide": imageSizeByLongerSide,
@@ -1014,21 +1493,29 @@ NODE_CLASS_MAPPINGS = {
"easy imageScaleDownToSize": imageScaleDownToSize,
"easy imageRatio": imageRatio,
"easy imageToMask": imageToMask,
"easy imageConcat": imageConcat,
"easy imageSplitList": imageSplitList,
"easy imageSplitGrid": imageSplitGrid,
"easy imagesSplitImage": imagesSplitImage,
"easy imageCropFromMask": imageCropFromMask,
"easy imageUncropFromBBOX": imageUncropFromBBOX,
"easy imageSave": imageSaveSimple,
"easy imageRemBg": imageRemBg,
"easy imageChooser": imageChooser,
"easy imageColorMatch": imageColorMatch,
"easy imageDetailTransfer": imageDetailTransfer,
"easy imageInterrogator": imageInterrogator,
"easy joinImageBatch": JoinImageBatch,
"easy loadImageBase64": loadImageBase64,
"easy imageToBase64": imageToBase64,
"easy joinImageBatch": JoinImageBatch,
"easy humanSegmentation": humanSegmentation,
"easy removeLocalImage": removeLocalImage,
"easy poseEditor": poseEditor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy imageInsetCrop": "ImageInsetCrop",
"easy imageCount": "ImageCount",
"easy imageSize": "ImageSize",
"easy imageSizeBySide": "ImageSize (Side)",
"easy imageSizeByLongerSide": "ImageSize (LongerSide)",
@@ -1039,15 +1526,22 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy imageRatio": "ImageRatio",
"easy imageToMask": "ImageToMask",
"easy imageHSVMask": "ImageHSVMask",
"easy imageConcat": "imageConcat",
"easy imageSplitList": "imageSplitList",
"easy imageSplitGrid": "imageSplitGrid",
"easy imagesSplitImage": "imagesSplitImage",
"easy imageCropFromMask": "imageCropFromMask",
"easy imageUncropFromBBOX": "imageUncropFromBBOX",
"easy imageSave": "SaveImage (Simple)",
"easy imageRemBg": "Image Remove Bg",
"easy imageChooser": "Image Chooser",
"easy imageColorMatch": "Image Color Match",
"easy imageDetailTransfer": "Image Detail Transfer",
"easy imageInterrogator": "Image To Prompt",
"easy joinImageBatch": "JoinImageBatch",
"easy loadImageBase64": "LoadImage (Base64)",
"easy loadImageBase64": "Load Image (Base64)",
"easy imageToBase64": "Image To Base64",
"easy humanSegmentation": "Human Segmentation",
"easy removeLocalImage": "Remove Local Image",
"easy poseEditor": "PoseEditor",
}
+10 -7
View File
@@ -53,20 +53,23 @@ class LayerDiffuse:
sd_version = get_sd_version(model)
model_url = LAYER_DIFFUSION[method.value][sd_version]["model_url"]
if image is not None:
image = image.movedim(-1, 1)
try:
ModelPatcher.calculate_weight = calculate_weight_adjust_channel(ModelPatcher.calculate_weight)
except:
pass
if method in [LayerMethod.FG_ONLY_CONV, LayerMethod.FG_ONLY_ATTN] and sd_version == 'sd15':
if method in [LayerMethod.FG_ONLY_CONV, LayerMethod.FG_ONLY_ATTN] and sd_version == 'sd1':
self.frames = 1
elif method in [LayerMethod.BG_TO_BLEND, LayerMethod.FG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG, LayerMethod.FG_BLEND_TO_BG] and sd_version == 'sd15':
elif method in [LayerMethod.BG_TO_BLEND, LayerMethod.FG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG, LayerMethod.FG_BLEND_TO_BG] and sd_version == 'sd1':
self.frames = 2
batch_size, _, height, width = samples['samples'].shape
if batch_size % 2 != 0:
raise Exception(f"The batch size should be a multiple of 2. 批次大小需为2的倍数")
control_img = image
elif method == LayerMethod.EVERYTHING and sd_version == 'sd15':
elif method == LayerMethod.EVERYTHING and sd_version == 'sd1':
batch_size, _, height, width = samples['samples'].shape
self.frames = 3
if batch_size % 3 != 0:
@@ -77,7 +80,7 @@ class LayerDiffuse:
model_path = get_local_filepath(model_url, LAYER_DIFFUSION_DIR)
layer_lora_state_dict = load_layer_model_state_dict(model_path)
work_model = model.clone()
if sd_version == 'sd15':
if sd_version == 'sd1':
patcher = AttentionSharingPatcher(
work_model, self.frames, use_control=control_img is not None
)
@@ -97,7 +100,7 @@ class LayerDiffuse:
else:
c_concat = model.model.latent_format.process_in(torch.cat([samples["samples"], blend_samples["samples"]], dim=1))
samp_model, positive, negative = (work_model,) + self.apply_layer_c_concat(positive, negative, c_concat)
elif sd_version == 'sd15':
elif sd_version == 'sd1':
if method in [LayerMethod.BG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG]:
additional_cond = (additional_cond[0], None)
elif method in [LayerMethod.FG_TO_BLEND, LayerMethod.FG_BLEND_TO_BG]:
@@ -166,10 +169,10 @@ class LayerDiffuse:
alpha = []
if layer_diffusion_method is not None:
sd_version = get_sd_version(model)
if sd_version not in ['sdxl', 'sd15']:
if sd_version not in ['sdxl', 'sd1']:
raise Exception(f"Only SDXL and SD1.5 model supported for Layer Diffusion")
method = self.get_layer_diffusion_method(layer_diffusion_method, blend_samples is not None)
sd15_allow = True if sd_version == 'sd15' and method in [LayerMethod.FG_ONLY_ATTN, LayerMethod.EVERYTHING, LayerMethod.BG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG] else False
sd15_allow = True if sd_version == 'sd1' and method in [LayerMethod.FG_ONLY_ATTN, LayerMethod.EVERYTHING, LayerMethod.BG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG] else False
sdxl_allow = True if sd_version == 'sdxl' and method in [LayerMethod.FG_ONLY_CONV, LayerMethod.FG_ONLY_ATTN, LayerMethod.BG_BLEND_TO_FG] else False
if sdxl_allow or sd15_allow:
if self.vae_transparent_decoder is None:
+1 -1
View File
@@ -17,7 +17,7 @@ try:
from diffusers.models.modeling_utils import ModelMixin
from diffusers import __version__
if __version__:
if version.parse(__version__) < version.parse("0.27.0"):
if version.parse(__version__) < version.parse("0.26.0"):
from diffusers.models.unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block
else:
from diffusers.models.unets.unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block
+9 -11
View File
@@ -4,9 +4,7 @@ import itertools
from comfy import model_management
from comfy.sdxl_clip import SDXLClipModel, SDXLRefinerClipModel, SDXLClipG
from nodes import NODE_CLASS_MAPPINGS, ConditioningConcat, CLIPTextEncode
from .libs.utils import compare_revision
from nodes import NODE_CLASS_MAPPINGS, ConditioningConcat
def _grouper(n, iterable):
it = iter(iterable)
@@ -237,17 +235,17 @@ def encode_token_weights_g(model, token_weight_pairs):
def encode_token_weights_l(model, token_weight_pairs):
l_out, _ = model.clip_l.encode_token_weights(token_weight_pairs)
return l_out, None
l_out, pooled = model.clip_l.encode_token_weights(token_weight_pairs)
return l_out, pooled
def encode_token_weights(model, token_weight_pairs, encode_func):
if model.layer_idx is not None:
# 2016 [c2cb8e88] 及以上版本去除了sdxl clip的clip_layer方法
if compare_revision(2016):
model.cond_stage_model.set_clip_options({'layer': model.layer_idx})
else:
model.cond_stage_model.clip_layer(model.layer_idx)
# if compare_revision(2016):
model.cond_stage_model.set_clip_options({'layer': model.layer_idx})
# else:
# model.cond_stage_model.clip_layer(model.layer_idx)
model_management.load_model_gpu(model.patcher)
return encode_func(model.cond_stage_model, token_weight_pairs)
@@ -316,8 +314,8 @@ def advanced_encode(clip, text, token_normalization, weight_interpretation, w_ma
embeddings_final, pooled = advanced_encode_from_tokens(tokenized['l'],
token_normalization,
weight_interpretation,
lambda x: (clip.encode_from_tokens({'l': x}), None),
w_max=w_max)
lambda x: encode_token_weights(clip, x, encode_token_weights_l),
w_max=w_max,return_pooled=True,)
cond = [[embeddings_final, {"pooled_output": pooled}]]
if conditioning is not None:
+115
View File
@@ -0,0 +1,115 @@
import torch
from PIL import Image
from torch import Tensor
from torch.nn import functional as F
from torchvision.transforms import ToTensor, ToPILImage
def adain_color_fix(target: Image, source: Image):
# Convert images to tensors
to_tensor = ToTensor()
target_tensor = to_tensor(target).unsqueeze(0)
source_tensor = to_tensor(source).unsqueeze(0)
# Apply adaptive instance normalization
result_tensor = adaptive_instance_normalization(target_tensor, source_tensor)
# Convert tensor back to image
to_image = ToPILImage()
result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0))
return result_image
def wavelet_color_fix(target: Image, source: Image):
source = source.resize(target.size, resample=Image.Resampling.LANCZOS)
# Convert images to tensors
to_tensor = ToTensor()
target_tensor = to_tensor(target).unsqueeze(0)
source_tensor = to_tensor(source).unsqueeze(0)
# Apply wavelet reconstruction
result_tensor = wavelet_reconstruction(target_tensor, source_tensor)
# Convert tensor back to image
to_image = ToPILImage()
result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0))
return result_image
def calc_mean_std(feat: Tensor, eps=1e-5):
"""Calculate mean and std for adaptive_instance_normalization.
Args:
feat (Tensor): 4D tensor.
eps (float): A small value added to the variance to avoid
divide-by-zero. Default: 1e-5.
"""
size = feat.size()
assert len(size) == 4, 'The input feature should be 4D tensor.'
b, c = size[:2]
feat_var = feat.view(b, c, -1).var(dim=2) + eps
feat_std = feat_var.sqrt().view(b, c, 1, 1)
feat_mean = feat.view(b, c, -1).mean(dim=2).view(b, c, 1, 1)
return feat_mean, feat_std
def adaptive_instance_normalization(content_feat:Tensor, style_feat:Tensor):
"""Adaptive instance normalization.
Adjust the reference features to have the similar color and illuminations
as those in the degradate features.
Args:
content_feat (Tensor): The reference feature.
style_feat (Tensor): The degradate features.
"""
size = content_feat.size()
style_mean, style_std = calc_mean_std(style_feat)
content_mean, content_std = calc_mean_std(content_feat)
normalized_feat = (content_feat - content_mean.expand(size)) / content_std.expand(size)
return normalized_feat * style_std.expand(size) + style_mean.expand(size)
def wavelet_blur(image: Tensor, radius: int):
"""
Apply wavelet blur to the input tensor.
"""
# input shape: (1, 3, H, W)
# convolution kernel
kernel_vals = [
[0.0625, 0.125, 0.0625],
[0.125, 0.25, 0.125],
[0.0625, 0.125, 0.0625],
]
kernel = torch.tensor(kernel_vals, dtype=image.dtype, device=image.device)
# add channel dimensions to the kernel to make it a 4D tensor
kernel = kernel[None, None]
# repeat the kernel across all input channels
kernel = kernel.repeat(3, 1, 1, 1)
image = F.pad(image, (radius, radius, radius, radius), mode='replicate')
# apply convolution
output = F.conv2d(image, kernel, groups=3, dilation=radius)
return output
def wavelet_decomposition(image: Tensor, levels=5):
"""
Apply wavelet decomposition to the input tensor.
This function only returns the low frequency & the high frequency.
"""
high_freq = torch.zeros_like(image)
for i in range(levels):
radius = 2 ** i
low_freq = wavelet_blur(image, radius)
high_freq += (image - low_freq)
image = low_freq
return high_freq, low_freq
def wavelet_reconstruction(content_feat:Tensor, style_feat:Tensor):
"""
Apply wavelet decomposition, so that the content will have the same color as the style.
"""
# calculate the wavelet decomposition of the content feature
content_high_freq, content_low_freq = wavelet_decomposition(content_feat)
del content_low_freq
# calculate the wavelet decomposition of the style feature
style_high_freq, style_low_freq = wavelet_decomposition(style_feat)
del style_high_freq
# reconstruct the content feature with the style's high frequency
return content_high_freq + style_low_freq
+11 -5
View File
@@ -1,14 +1,20 @@
from .utils import find_wildcards_seed, find_nearest_steps, is_linked_styles_selector
from ..log import log_node_warn
from ..adv_encode import advanced_encode
from ..wildcards import process_with_loras
from .log import log_node_warn
from .translate import zh_to_en, has_chinese
from .wildcards import process_with_loras
from .adv_encode import advanced_encode
from nodes import ConditioningConcat, ConditioningCombine, ConditioningAverage, ConditioningSetTimestepRange
def prompt_to_cond(type, model, clip, clip_skip, lora_stack, text, prompt_token_normalization, prompt_weight_interpretation, a1111_prompt_style ,my_unique_id, prompt, easyCache, can_load_lora=True, steps=None):
styles_selector = is_linked_styles_selector(prompt, my_unique_id, type)
title = "正面提示词" if type == 'positive' else "负面提示词"
log_node_warn("正在处理" + title + "...")
log_node_warn("正在进行" + title + "...")
# Translate cn to en
if has_chinese(text):
text = zh_to_en([text])[0]
positive_seed = find_wildcards_seed(my_unique_id, text, prompt)
model, clip, text, cond_decode, show_prompt, pipe_lora_stack = process_with_loras(
text, model, clip, type, positive_seed, can_load_lora, lora_stack, easyCache)
@@ -18,7 +24,7 @@ def prompt_to_cond(type, model, clip, clip_skip, lora_stack, text, prompt_token_
if clip_skip != 0:
clipped.clip_layer(clip_skip)
log_node_warn("正在处理" + title + "编码...")
log_node_warn("正在进行" + title + "编码...")
steps = steps if steps is not None else find_nearest_steps(my_unique_id, prompt)
return (advanced_encode(clipped, text, prompt_token_normalization,
prompt_weight_interpretation, w_max=1.0,
+3 -17
View File
@@ -7,26 +7,12 @@ class easyControlnet:
def __init__(self):
pass
def load_controlnet(self, control_net_name, control_net, scale_soft_weights):
if control_net is None:
if scale_soft_weights < 1:
if "ScaledSoftControlNetWeights" in NODE_CLASS_MAPPINGS:
soft_weight_cls = NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights']
(weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False)
cn_adv_cls = NODE_CLASS_MAPPINGS['ControlNetLoaderAdvanced']
control_net, = cn_adv_cls().load_controlnet(control_net_name, timestep_keyframe)
else:
raise Exception(f"[Advanced-ControlNet Not Found] you need to install 'COMFYUI-Advanced-ControlNet'")
else:
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
control_net = comfy.controlnet.load_controlnet(controlnet_path)
return control_net
def apply(self, control_net_name, image, positive, negative, strength, start_percent=0, end_percent=1, control_net=None, scale_soft_weights=1, mask=None):
def apply(self, control_net_name, image, positive, negative, strength, start_percent=0, end_percent=1, control_net=None, scale_soft_weights=1, mask=None, easyCache=None, use_cache=True):
if strength == 0:
return (positive, negative)
control_net = self.load_controlnet(control_net_name, control_net, scale_soft_weights)
if control_net is None:
control_net = easyCache.load_controlnet(control_net_name, scale_soft_weights, use_cache)
if mask is not None:
mask = mask.to(self.device)
+2
View File
@@ -19,6 +19,8 @@ class InpaintWorker:
def __init__(self, node_name):
self.node_name = node_name if node_name is not None else ""
self.original_calculate_weight = ModelPatcher.calculate_weight
if not hasattr(ModelPatcher, "original_calculate_weight"):
ModelPatcher.original_calculate_weight = self.original_calculate_weight
self.injected_model_patcher_calculate_weight = False
def load_fooocus_patch(self, lora: dict, to_load: dict):
+72 -15
View File
@@ -5,6 +5,7 @@ import numpy as np
from enum import Enum
from PIL import Image
from io import BytesIO
from typing import List, Union
import folder_paths
from .utils import install_package
@@ -15,6 +16,17 @@ def pil2tensor(image):
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# np to Tensor
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
if isinstance(img_np, list):
return torch.cat([np2tensor(img) for img in img_np], dim=0)
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
# Tensor to np
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
if len(tensor.shape) == 3: # Single image
return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
else: # Batch of images
return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor]
def pil2byte(pil_image, format='PNG'):
byte_arr = BytesIO()
@@ -48,6 +60,52 @@ def image2mask(image: Image) -> torch.Tensor:
ret_mask = torch.tensor([pil2tensor(_image)[0, :, :, 3].tolist()])
return ret_mask
def mask2image(mask: torch.Tensor) -> Image:
masks = tensor2np(mask)
for m in masks:
_mask = Image.fromarray(m).convert("L")
_image = Image.new("RGBA", _mask.size, color='white')
_image = Image.composite(
_image, Image.new("RGBA", _mask.size, color='black'), _mask)
return _image
# 图像融合
class blendImage:
def g(self, x):
return torch.where(x <= 0.25, ((16 * x - 12) * x + 4) * x, torch.sqrt(x))
def blend_mode(self, img1, img2, mode):
if mode == "normal":
return img2
elif mode == "multiply":
return img1 * img2
elif mode == "screen":
return 1 - (1 - img1) * (1 - img2)
elif mode == "overlay":
return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2))
elif mode == "soft_light":
return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1),
img1 + (2 * img2 - 1) * (self.g(img1) - img1))
elif mode == "difference":
return img1 - img2
else:
raise ValueError(f"Unsupported blend mode: {mode}")
def blend_images(self, image1: torch.Tensor, image2: torch.Tensor, blend_factor: float, blend_mode: str = 'normal'):
image2 = image2.to(image1.device)
if image1.shape != image2.shape:
image2 = image2.permute(0, 3, 1, 2)
image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic',
crop='center')
image2 = image2.permute(0, 2, 3, 1)
blended_image = self.blend_mode(image1, image2, blend_mode)
blended_image = image1 * (1 - blend_factor) + blended_image * blend_factor
blended_image = torch.clamp(blended_image, 0, 1)
return blended_image
class ResizeMode(Enum):
RESIZE = "Just Resize"
@@ -111,27 +169,26 @@ class CI_Inference:
def image_to_prompt(self, image, mode, model_name='ViT-L-14/openai', low_vram=False):
try:
install_package("clip_interrogator", "0.6.0")
from clip_interrogator import Config, Interrogator
global Config, Interrogator
except:
install_package("clip_interrogator", "0.6.0")
from clip_interrogator import Config, Interrogator
pbar = comfy.utils.ProgressBar(len(image))
pbar = comfy.utils.ProgressBar(len(image))
self._load_model(model_name, low_vram)
prompt = []
for i in range(len(image)):
im = image[i]
self._load_model(model_name, low_vram)
prompt = []
for i in range(len(image)):
im = image[i]
im = tensor2pil(im)
im = im.convert('RGB')
im = tensor2pil(im)
im = im.convert('RGB')
_prompt = self._interrogate(im, mode)
pbar.update(1)
prompt.append(_prompt)
_prompt = self._interrogate(im, mode)
pbar.update(1)
prompt.append(_prompt)
return prompt
except Exception as e:
print(e)
return [""]
return prompt
ci = CI_Inference()
View File
+116 -12
View File
@@ -1,16 +1,21 @@
import time, os, psutil
import folder_paths
import comfy.utils
import comfy.sd
import folder_paths
import comfy.controlnet
from comfy.model_patcher import ModelPatcher
from nodes import NODE_CLASS_MAPPINGS
from collections import defaultdict
from ..log import log_node_info, log_node_error
from .log import log_node_info, log_node_error
stable_diffusion_loaders = ["easy a1111Loader", "easy comfyLoader", "easy zero123Loader", "easy svdLoader"]
stable_diffusion_loaders = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader", "easy zero123Loader", "easy svdLoader"]
stable_cascade_loaders = ["easy cascadeLoader"]
controlnet_loaders = ["easy controlnetLoader", "easy controlnetLoaderADV"]
instant_loaders = ["easy instantIDApply", "easy instantIDApplyADV"]
cascade_vae_node = ["easy preSamplingCascade", "easy fullCascadeKSampler"]
model_merge_node = ["easy XYInputs: ModelMergeBlocks"]
lora_widget = ["easy a1111Loader", "easy comfyLoader"]
lora_widget = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader"]
class easyLoader:
def __init__(self):
@@ -22,6 +27,7 @@ class easyLoader:
"bvae": defaultdict(tuple),
"vae": defaultdict(object),
"lora": defaultdict(dict), # {lora_name: {UID: (model_lora, clip_lora)}}
"controlnet": defaultdict(dict),
}
self.memory_threshold = self.determine_memory_threshold(0.7)
self.lora_name_cache = []
@@ -50,9 +56,17 @@ class easyLoader:
for key in keys - desired_names:
del self.loaded_objects[object_type][key]
def get_input_value(self, entry, key):
def get_input_value(self, entry, key, prompt=None):
val = entry["inputs"][key]
return val if isinstance(val, str) else val[0]
if isinstance(val, str):
return val
elif isinstance(val, list):
if prompt is not None and val[0]:
return prompt[val[0]]['inputs'][key]
else:
return val[0]
else:
return str(val)
def process_pipe_loader(self, entry, desired_ckpt_names, desired_vae_names, desired_lora_names, desired_lora_settings, num_loras=3, suffix=""):
for idx in range(1, num_loras + 1):
@@ -71,10 +85,10 @@ class easyLoader:
desired_vae_names = set()
desired_lora_names = set()
desired_lora_settings = set()
desired_controlnet_names = set()
for entry in prompt.values():
class_type = entry["class_type"]
if class_type in lora_widget:
lora_name = self.get_input_value(entry, "lora_name")
desired_lora_names.add(lora_name)
@@ -82,7 +96,7 @@ class easyLoader:
desired_lora_settings.add(setting)
if class_type in stable_diffusion_loaders:
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name"))
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name", prompt))
desired_vae_names.add(self.get_input_value(entry, "vae_name"))
elif class_type in stable_cascade_loaders:
@@ -99,6 +113,16 @@ class easyLoader:
if decode_vae_name and decode_vae_name != 'None':
desired_vae_names.add(decode_vae_name)
elif class_type in controlnet_loaders:
control_net_name = self.get_input_value(entry, "control_net_name", prompt)
scale_soft_weights = self.get_input_value(entry, "scale_soft_weights")
desired_controlnet_names.add(f'{control_net_name};{scale_soft_weights}')
elif class_type in instant_loaders:
control_net_name = self.get_input_value(entry, "control_net_name", prompt)
scale_soft_weights = self.get_input_value(entry, "cn_soft_weights")
desired_controlnet_names.add(f'{control_net_name};{scale_soft_weights}')
elif class_type in model_merge_node:
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_1"))
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_2"))
@@ -106,7 +130,7 @@ class easyLoader:
if vae_use != 'Use Model 1' and vae_use != 'Use Model 2':
desired_vae_names.add(vae_use)
object_types = ["ckpt", "unet", "clip", "bvae", "vae", "lora"]
object_types = ["ckpt", "unet", "clip", "bvae", "vae", "lora", "controlnet"]
for object_type in object_types:
if object_type == 'unet':
desired_names = desired_unet_names
@@ -117,6 +141,8 @@ class easyLoader:
desired_names = desired_ckpt_names
elif object_type == "vae":
desired_names = desired_vae_names
elif object_type == "controlnet":
desired_names = desired_controlnet_names
else:
desired_names = desired_lora_names
self.clear_unused_objects(desired_names, object_type)
@@ -155,7 +181,7 @@ class easyLoader:
current_memory = self.get_memory_usage()
if current_memory < self.memory_threshold:
return
eviction_order = ["vae", "lora", "bvae", "clip", "ckpt"]
eviction_order = ["vae", "lora", "bvae", "clip", "ckpt", "controlnet"]
for obj_type in eviction_order:
if current_memory < self.memory_threshold:
break
@@ -225,7 +251,27 @@ class easyLoader:
return model
def load_clip(self, clip_name, type='stable_diffusion'):
def load_controlnet(self, control_net_name, scale_soft_weights=1, use_cache=True):
unique_id = f'{control_net_name};{str(scale_soft_weights)}'
if use_cache and unique_id in self.loaded_objects["controlnet"]:
return self.loaded_objects["controlnet"][unique_id][0]
if scale_soft_weights < 1:
if "ScaledSoftControlNetWeights" in NODE_CLASS_MAPPINGS:
soft_weight_cls = NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights']
(weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False)
cn_adv_cls = NODE_CLASS_MAPPINGS['ControlNetLoaderAdvanced']
control_net, = cn_adv_cls().load_controlnet(control_net_name, timestep_keyframe)
else:
raise Exception(
f"[Advanced-ControlNet Not Found] you need to install 'COMFYUI-Advanced-ControlNet'")
else:
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
control_net = comfy.controlnet.load_controlnet(controlnet_path)
if use_cache:
self.add_to_cache("controlnet", unique_id, control_net)
self.eviction_based_on_memory()
return control_net
def load_clip(self, clip_name, type='stable_diffusion', load_clip=None):
if type == 'stable_diffusion':
clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION
else:
@@ -330,4 +376,62 @@ class easyLoader:
self.lora_name_cache.append(x)
return x
return None
return None
def load_main(self, ckpt_name, config_name, vae_name, lora_name, lora_model_strength, lora_clip_strength, optional_lora_stack, model_override, clip_override, vae_override, prompt):
model: ModelPatcher | None = None
clip: comfy.sd.CLIP | None = None
vae: comfy.sd.VAE | None = None
clip_vision = None
lora_stack = []
can_load_lora = True
# 判断是否存在 模型或Lora叠加xyplot, 若存在优先缓存第一个模型
xy_model_id = next((x for x in prompt if str(prompt[x]["class_type"]) in ["easy XYInputs: ModelMergeBlocks",
"easy XYInputs: Checkpoint"]), None)
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
if xy_model_id is not None:
node = prompt[xy_model_id]
if "ckpt_name_1" in node["inputs"]:
ckpt_name_1 = node["inputs"]["ckpt_name_1"]
model, clip, vae, clip_vision = self.load_checkpoint(ckpt_name_1)
can_load_lora = False
# Load models
elif model_override is not None and clip_override is not None and vae_override is not None:
model = model_override
clip = clip_override
vae = vae_override
elif model_override is not None:
raise Exception(f"[ERROR] clip or vae is missing")
elif vae_override is not None:
raise Exception(f"[ERROR] model or clip is missing")
elif clip_override is not None:
raise Exception(f"[ERROR] model or vae is missing")
else:
model, clip, vae, clip_vision = self.load_checkpoint(ckpt_name, config_name)
if optional_lora_stack is not None and can_load_lora:
for lora in optional_lora_stack:
lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1],
"clip_strength": lora[2]}
model, clip = self.load_lora(lora)
lora['model'] = model
lora['clip'] = clip
lora_stack.append(lora)
if lora_name != "None" and can_load_lora:
lora = {"lora_name": lora_name, "model": model, "clip": clip, "model_strength": lora_model_strength,
"clip_strength": lora_clip_strength}
model, clip = self.load_lora(lora)
lora_stack.append(lora)
# Check for custom VAE
if vae_name not in ["Baked VAE", "Baked-VAE"]:
vae = self.load_vae(vae_name)
# CLIP skip
if not clip:
raise Exception("No CLIP found")
return model, clip, vae, clip_vision, lora_stack
View File
+154 -25
View File
@@ -5,13 +5,14 @@ import latent_preview
from nodes import MAX_RESOLUTION
from PIL import Image
from typing import Dict, List, Optional, Tuple, Union, Any
from .utils import get_sd_version
class easySampler:
def __init__(self):
self.last_helds: dict[str, list] = {
"results": [],
"pipe_line": [],
}
self.device = comfy.model_management.intermediate_device()
@staticmethod
def tensor2pil(image: torch.Tensor) -> Image.Image:
@@ -47,9 +48,38 @@ class easySampler:
parts.append('None')
return parts
def emptyLatent(self, resolution, empty_latent_width, empty_latent_height, batch_size=1, compression=0):
if resolution != "自定义 x 自定义":
try:
width, height = map(int, resolution.split(' x '))
empty_latent_width = width
empty_latent_height = height
except ValueError:
raise ValueError("Invalid base_resolution format.")
if compression == 0:
latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8], device=self.device)
samples = {"samples": latent}
else:
latent_c = torch.zeros(
[batch_size, 16, empty_latent_height // compression, empty_latent_width // compression])
latent_b = torch.zeros([batch_size, 4, empty_latent_height // 4, empty_latent_width // 4])
samples = ({"samples": latent_c}, {"samples": latent_b})
return samples
def add_model_patch_option(self, model):
if 'transformer_options' not in model.model_options:
model.model_options['transformer_options'] = {}
to = model.model_options['transformer_options']
if "model_patch" not in to:
to["model_patch"] = {}
return to
def common_ksampler(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0,
disable_noise=False, start_step=None, last_step=None, force_full_denoise=False,
preview_latent=True, disable_pbar=False, custom=None):
preview_latent=True, disable_pbar=False):
device = comfy.model_management.get_torch_device()
latent_image = latent["samples"]
@@ -74,29 +104,40 @@ class easySampler:
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
pbar.update_absolute(step + 1, total_steps, preview_bytes)
if custom is not None:
guider = custom['guider'] if 'guider' in custom else None
sampler = custom['sampler'] if 'sampler' in custom else None
sigmas = custom['sigmas'] if 'sigmas' in custom else None
noise = custom['noise'] if 'noise' in custom else None
samples = guider.sample(noise.generate_noise(latent), latent_image, sampler, sigmas,
denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar,
seed=noise.seed)
samples = samples.to(comfy.model_management.intermediate_device())
else:
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout,
device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative,
latent_image,
denoise=denoise, disable_noise=disable_noise, start_step=start_step,
last_step=last_step,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback,
disable_pbar=disable_pbar, seed=seed)
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout,
device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
#######################################################################################
# brushnet
transformer_options = model.model_options['transformer_options'] if "transformer_options" in model.model_options else {}
if 'model_patch' in transformer_options and 'brushnet' in transformer_options['model_patch']:
to = self.add_model_patch_option(model)
mp = to['model_patch']
if isinstance(model.model.model_config, comfy.supported_models.SD15):
mp['SDXL'] = False
elif isinstance(model.model.model_config, comfy.supported_models.SDXL):
mp['SDXL'] = True
else:
print('Base model type: ', type(model.model.model_config))
raise Exception("Unsupported model type: ", type(model.model.model_config))
mp['unet'] = model.model.diffusion_model
mp['step'] = 0
mp['total_steps'] = 1
#
#######################################################################################
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative,
latent_image,
denoise=denoise, disable_noise=disable_noise, start_step=start_step,
last_step=last_step,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback,
disable_pbar=disable_pbar, seed=seed)
out = latent.copy()
out["samples"] = samples
@@ -129,8 +170,31 @@ class easySampler:
pbar = comfy.utils.ProgressBar(steps)
#######################################################################################
# brushnet
to = None
transformer_options = model.model_options['transformer_options'] if "transformer_options" in model.model_options else {}
if 'model_patch' in transformer_options and 'brushnet' in transformer_options['model_patch']:
to = self.add_model_patch_option(model)
mp = to['model_patch']
if isinstance(model.model.model_config, comfy.supported_models.SD15):
mp['SDXL'] = False
elif isinstance(model.model.model_config, comfy.supported_models.SDXL):
mp['SDXL'] = True
else:
print('Base model type: ', type(model.model.model_config))
raise Exception("Unsupported model type: ", type(model.model.model_config))
mp['unet'] = model.model.diffusion_model
mp['step'] = 0
mp['total_steps'] = 1
#
#######################################################################################
def callback(step, x0, x, total_steps):
preview_bytes = None
if to is not None and "model_patch" in to:
to['model_patch']['step'] = step + 1
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
pbar.update_absolute(step + 1, total_steps, preview_bytes)
@@ -143,6 +207,32 @@ class easySampler:
out["samples"] = samples
return out
def custom_advanced_ksampler(self, noise, guider, sampler, sigmas, latent_image):
latent = latent_image
latent_image = latent["samples"]
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
x0_output = {}
callback = latent_preview.prepare_callback(guider.model_patcher, sigmas.shape[-1] - 1, x0_output)
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
samples = guider.sample(noise.generate_noise(latent), latent_image, sampler, sigmas, denoise_mask=noise_mask,
callback=callback, disable_pbar=disable_pbar, seed=noise.seed)
samples = samples.to(comfy.model_management.intermediate_device())
out = latent.copy()
out["samples"] = samples
if "x0" in x0_output:
out_denoised = latent.copy()
out_denoised["samples"] = guider.model_patcher.model.process_latent_out(x0_output["x0"].cpu())
else:
out_denoised = out
return (out, out_denoised)
def get_value_by_id(self, key: str, my_unique_id: Any) -> Optional[Any]:
"""Retrieve value by its associated ID."""
try:
@@ -220,4 +310,43 @@ class easySampler:
sdxl_pipe.get("clip"),
sdxl_pipe.get("images"),
sdxl_pipe.get("seed")
)
)
class alignYourStepsScheduler:
NOISE_LEVELS = {
"SD1": [14.6146412293, 6.4745760956, 3.8636745985, 2.6946151520, 1.8841921177, 1.3943805092, 0.9642583904,
0.6523686016, 0.3977456272, 0.1515232662, 0.0291671582],
"SDXL": [14.6146412293, 6.3184485287, 3.7681790315, 2.1811480769, 1.3405244945, 0.8620721141, 0.5550693289,
0.3798540708, 0.2332364134, 0.1114188177, 0.0291671582],
"SVD": [700.00, 54.5, 15.886, 7.977, 4.248, 1.789, 0.981, 0.403, 0.173, 0.034, 0.002]}
def loglinear_interp(self, t_steps, num_steps):
"""
Performs log-linear interpolation of a given array of decreasing numbers.
"""
xs = np.linspace(0, 1, len(t_steps))
ys = np.log(t_steps[::-1])
new_xs = np.linspace(0, 1, num_steps)
new_ys = np.interp(new_xs, xs, ys)
interped_ys = np.exp(new_ys)[::-1].copy()
return interped_ys
def get_sigmas(self, model_type, steps, denoise):
total_steps = steps
if denoise < 1.0:
if denoise <= 0.0:
return (torch.FloatTensor([]),)
total_steps = round(steps * denoise)
sigmas = self.NOISE_LEVELS[model_type][:]
if (steps + 1) != len(sigmas):
sigmas = self.loglinear_interp(sigmas, steps + 1)
sigmas = sigmas[-(total_steps + 1):]
sigmas[-1] = 0
return (torch.FloatTensor(sigmas),)
+1 -1
View File
@@ -6,7 +6,7 @@ import pathlib
from aiohttp import web
from server import PromptServer
from .image import tensor2pil, pil2tensor, image2base64, pil2byte
from ..log import log_node_error
from .log import log_node_error
root_path = pathlib.Path(__file__).parent.parent.parent
+148
View File
@@ -0,0 +1,148 @@
import torch
import torch.nn as nn
from comfy.model_patcher import ModelPatcher
from typing import Union
T = torch.Tensor
def exists(val):
return val is not None
def default(val, d):
if exists(val):
return val
return d
class StyleAlignedArgs:
def __init__(self, share_attn: str) -> None:
self.adain_keys = "k" in share_attn
self.adain_values = "v" in share_attn
self.adain_queries = "q" in share_attn
share_attention: bool = True
adain_queries: bool = True
adain_keys: bool = True
adain_values: bool = True
def expand_first(
feat: T,
scale=1.0,
) -> T:
"""
Expand the first element so it has the same shape as the rest of the batch.
"""
b = feat.shape[0]
feat_style = torch.stack((feat[0], feat[b // 2])).unsqueeze(1)
if scale == 1:
feat_style = feat_style.expand(2, b // 2, *feat.shape[1:])
else:
feat_style = feat_style.repeat(1, b // 2, 1, 1, 1)
feat_style = torch.cat([feat_style[:, :1], scale * feat_style[:, 1:]], dim=1)
return feat_style.reshape(*feat.shape)
def concat_first(feat: T, dim=2, scale=1.0) -> T:
"""
concat the the feature and the style feature expanded above
"""
feat_style = expand_first(feat, scale=scale)
return torch.cat((feat, feat_style), dim=dim)
def calc_mean_std(feat, eps: float = 1e-5) -> "tuple[T, T]":
feat_std = (feat.var(dim=-2, keepdims=True) + eps).sqrt()
feat_mean = feat.mean(dim=-2, keepdims=True)
return feat_mean, feat_std
def adain(feat: T) -> T:
feat_mean, feat_std = calc_mean_std(feat)
feat_style_mean = expand_first(feat_mean)
feat_style_std = expand_first(feat_std)
feat = (feat - feat_mean) / feat_std
feat = feat * feat_style_std + feat_style_mean
return feat
class SharedAttentionProcessor:
def __init__(self, args: StyleAlignedArgs, scale: float):
self.args = args
self.scale = scale
def __call__(self, q, k, v, extra_options):
if self.args.adain_queries:
q = adain(q)
if self.args.adain_keys:
k = adain(k)
if self.args.adain_values:
v = adain(v)
if self.args.share_attention:
k = concat_first(k, -2, scale=self.scale)
v = concat_first(v, -2)
return q, k, v
def get_norm_layers(
layer: nn.Module,
norm_layers_: "dict[str, list[Union[nn.GroupNorm, nn.LayerNorm]]]",
share_layer_norm: bool,
share_group_norm: bool,
):
if isinstance(layer, nn.LayerNorm) and share_layer_norm:
norm_layers_["layer"].append(layer)
if isinstance(layer, nn.GroupNorm) and share_group_norm:
norm_layers_["group"].append(layer)
else:
for child_layer in layer.children():
get_norm_layers(
child_layer, norm_layers_, share_layer_norm, share_group_norm
)
def register_norm_forward(
norm_layer: Union[nn.GroupNorm, nn.LayerNorm],
) -> Union[nn.GroupNorm, nn.LayerNorm]:
if not hasattr(norm_layer, "orig_forward"):
setattr(norm_layer, "orig_forward", norm_layer.forward)
orig_forward = norm_layer.orig_forward
def forward_(hidden_states: T) -> T:
n = hidden_states.shape[-2]
hidden_states = concat_first(hidden_states, dim=-2)
hidden_states = orig_forward(hidden_states) # type: ignore
return hidden_states[..., :n, :]
norm_layer.forward = forward_ # type: ignore
return norm_layer
def register_shared_norm(
model: ModelPatcher,
share_group_norm: bool = True,
share_layer_norm: bool = True,
):
norm_layers = {"group": [], "layer": []}
get_norm_layers(model.model, norm_layers, share_layer_norm, share_group_norm)
print(
f"Patching {len(norm_layers['group'])} group norms, {len(norm_layers['layer'])} layer norms."
)
return [register_norm_forward(layer) for layer in norm_layers["group"]] + [
register_norm_forward(layer) for layer in norm_layers["layer"]
]
SHARE_NORM_OPTIONS = ["both", "group", "layer", "disabled"]
SHARE_ATTN_OPTIONS = ["q+k", "q+k+v", "disabled"]
def styleAlignBatch(model, share_norm, share_attn, scale=1.0):
m = model.clone()
share_group_norm = share_norm in ["group", "both"]
share_layer_norm = share_norm in ["layer", "both"]
register_shared_norm(model, share_group_norm, share_layer_norm)
args = StyleAlignedArgs(share_attn)
m.set_model_attn1_patch(SharedAttentionProcessor(args, scale))
return m
+238
View File
@@ -0,0 +1,238 @@
import re
import os
import folder_paths
import comfy.utils
import torch
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
from .utils import install_package
try:
from lark import Lark, Transformer, v_args
except:
print('install lark-parser...')
install_package('lark-parser')
from lark import Lark, Transformer, v_args
model_path = os.path.join(folder_paths.models_dir, 'prompt_generator')
zh_en_model_path = os.path.join(model_path, 'opus-mt-zh-en')
zh_en_model, zh_en_tokenizer = None, None
def correct_prompt_syntax(prompt=""):
# print("input prompt",prompt)
corrected_elements = []
# 处理成统一的英文标点
prompt = prompt.replace('(', '(').replace(')', ')').replace(',', ',').replace(';', ',').replace('。', '.').replace(':',':')
# 删除多余的空格
prompt = re.sub(r'\s+', ' ', prompt).strip()
prompt = prompt.replace("< ","<").replace(" >",">").replace("( ","(").replace(" )",")").replace("[ ","[").replace(' ]',']')
# 分词
prompt_elements = prompt.split(',')
def balance_brackets(element, open_bracket, close_bracket):
open_brackets_count = element.count(open_bracket)
close_brackets_count = element.count(close_bracket)
return element + close_bracket * (open_brackets_count - close_brackets_count)
for element in prompt_elements:
element = element.strip()
# 处理空元素
if not element:
continue
# 检查并处理圆括号、方括号、尖括号
if element[0] in '([':
corrected_element = balance_brackets(element, '(', ')') if element[0] == '(' else balance_brackets(element, '[', ']')
elif element[0] == '<':
corrected_element = balance_brackets(element, '<', '>')
else:
# 删除开头的右括号或右方括号
corrected_element = element.lstrip(')]')
corrected_elements.append(corrected_element)
# 重组修正后的prompt
return ','.join(corrected_elements)
def detect_language(input_str):
# 统计中文和英文字符的数量
count_cn = count_en = 0
for char in input_str:
if '\u4e00' <= char <= '\u9fff':
count_cn += 1
elif char.isalpha():
count_en += 1
# 根据统计的字符数量判断主要语言
if count_cn > count_en:
return "cn"
elif count_en > count_cn:
return "en"
else:
return "unknow"
def has_chinese(text):
has_cn = False
_text = text
_text = re.sub(r'<.*?>', '', _text)
_text = re.sub(r'__.*?__', '', _text)
_text = re.sub(r'embedding:.*?(\d+)?', '', _text)
for char in _text:
if '\u4e00' <= char <= '\u9fff':
has_cn = True
break
elif char.isalpha():
continue
return has_cn
def translate(text):
global zh_en_model_path, zh_en_model, zh_en_tokenizer
if not os.path.exists(zh_en_model_path):
zh_en_model_path = 'Helsinki-NLP/opus-mt-zh-en'
if zh_en_model is None:
zh_en_model = AutoModelForSeq2SeqLM.from_pretrained(zh_en_model_path).eval()
zh_en_tokenizer = AutoTokenizer.from_pretrained(zh_en_model_path, padding=True, truncation=True)
zh_en_model.to("cuda" if torch.cuda.is_available() else "cpu")
with torch.no_grad():
encoded = zh_en_tokenizer([text], return_tensors="pt")
encoded.to(zh_en_model.device)
sequences = zh_en_model.generate(**encoded)
return zh_en_tokenizer.batch_decode(sequences, skip_special_tokens=True)[0]
@v_args(inline=True) # Decorator to flatten the tree directly into the function arguments
class ChinesePromptTranslate(Transformer):
def sentence(self, *args):
return ", ".join(args)
def phrase(self, *args):
return "".join(args)
def emphasis(self, *args):
# Reconstruct the emphasis with translated content
return "(" + "".join(args) + ")"
def weak_emphasis(self, *args):
print('weak_emphasis:', args)
return "[" + "".join(args) + "]"
def embedding(self, *args):
print('prompt embedding', args[0])
if len(args) == 1:
# print('prompt embedding',str(args[0]))
# 只传递了一个参数,意味着只有embedding名称没有数字
embedding_name = str(args[0])
return f"embedding:{embedding_name}"
elif len(args) > 1:
embedding_name, *numbers = args
if len(numbers) == 2:
return f"embedding:{embedding_name}:{numbers[0]}:{numbers[1]}"
elif len(numbers) == 1:
return f"embedding:{embedding_name}:{numbers[0]}"
else:
return f"embedding:{embedding_name}"
def lora(self, *args):
if len(args) == 1:
return f"<lora:{args[0]}>"
elif len(args) > 1:
# print('lora', args)
_, loar_name, *numbers = args
loar_name = str(loar_name).strip()
if len(numbers) == 2:
return f"<lora:{loar_name}:{numbers[0]}:{numbers[1]}>"
elif len(numbers) == 1:
return f"<lora:{loar_name}:{numbers[0]}>"
else:
return f"<lora:{loar_name}>"
def weight(self, word, number):
translated_word = translate(str(word)).rstrip('.')
return f"({translated_word}:{str(number).strip()})"
def schedule(self, *args):
print('prompt schedule', args)
data = [str(arg).strip() for arg in args]
return f"[{':'.join(data)}]"
def word(self, word):
# Translate each word using the dictionary
if re.search(r'__.*?__', str(word)):
return str(word).rstrip('.')
elif re.search(r'@.*?@', str(word)):
return str(word).replace('@', '').rstrip('.')
elif detect_language(str(word)) == "cn":
return translate(str(word)).rstrip('.')
else:
return str(word).rstrip('.')
#定义Prompt文法
grammar = """
start: sentence
sentence: phrase ("," phrase)*
phrase: emphasis | weight | word | lora | embedding | schedule
emphasis: "(" sentence ")" -> emphasis
| "[" sentence "]" -> weak_emphasis
weight: "(" word ":" NUMBER ")"
schedule: "[" word ":" word ":" NUMBER "]"
lora: "<" WORD ":" WORD (":" NUMBER)? (":" NUMBER)? ">"
embedding: "embedding" ":" WORD (":" NUMBER)? (":" NUMBER)?
word: WORD
NUMBER: /\s*-?\d+(\.\d+)?\s*/
WORD: /[^,:\(\)\[\]<>]+/
"""
def zh_to_en(text):
global zh_en_model_path, zh_en_model, zh_en_tokenizer
# 进度条
pbar = comfy.utils.ProgressBar(len(text) + 1)
texts = [correct_prompt_syntax(t) for t in text]
install_package('sentencepiece', '0.2.0')
if not os.path.exists(zh_en_model_path):
zh_en_model_path = 'Helsinki-NLP/opus-mt-zh-en'
if zh_en_model is None:
zh_en_model = AutoModelForSeq2SeqLM.from_pretrained(zh_en_model_path).eval()
zh_en_tokenizer = AutoTokenizer.from_pretrained(zh_en_model_path, padding=True, truncation=True)
zh_en_model.to("cuda" if torch.cuda.is_available() else "cpu")
prompt_result = []
en_texts = []
for t in texts:
if t:
# translated_text = translated_word = translate(zh_en_tokenizer,zh_en_model,str(t))
parser = Lark(grammar, start="start", parser="lalr", transformer=ChinesePromptTranslate())
# print('t',t)
result = parser.parse(t).children
# print('en_result',result)
# en_text=translate(zh_en_tokenizer,zh_en_model,text_without_syntax)
en_texts.append(result[0])
zh_en_model.to('cpu')
# print("test en_text", en_texts)
# en_text.to("cuda" if torch.cuda.is_available() else "cpu")
pbar.update(1)
for t in en_texts:
prompt_result.append(t)
pbar.update(1)
# print('prompt_result', prompt_result, )
if len(prompt_result) == 0:
prompt_result = [""]
return prompt_result
+11 -17
View File
@@ -21,6 +21,8 @@ def get_comfyui_revision():
import sys
import importlib.util
import importlib.metadata
import comfy.model_management as mm
import gc
from packaging import version
from server import PromptServer
def is_package_installed(package):
@@ -81,16 +83,6 @@ def find_tags(string: str, sep="/") -> list[str]:
return string.split(sep)[:-1]
return []
import folder_paths
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
for full_folder_path in full_folder_paths:
folder_paths.add_model_folder_path(folder_name, full_folder_path)
if folder_name in folder_paths.folder_names_and_paths:
current_paths, current_extensions = folder_paths.folder_names_and_paths[folder_name]
updated_extensions = current_extensions | extensions
folder_paths.folder_names_and_paths[folder_name] = (current_paths, updated_extensions)
else:
folder_paths.folder_names_and_paths[folder_name] = (full_folder_paths, extensions)
from comfy.model_base import BaseModel
import comfy.supported_models
@@ -103,7 +95,11 @@ def get_sd_version(model):
elif isinstance(
model_config, (comfy.supported_models.SD15, comfy.supported_models.SD20)
):
return 'sd15'
return 'sd1'
elif isinstance(
model_config, (comfy.supported_models.SVD_img2vid)
):
return 'svd'
else:
return 'unknown'
@@ -231,7 +227,7 @@ def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=No
from nodes import PreviewImage, SaveImage
if output_type == "Hide":
return list()
if output_type == "Preview":
if output_type in ["Preview", "Preview&Choose"]:
filename_prefix = 'easyPreview'
results = PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
return results['ui']['images']
@@ -255,8 +251,6 @@ def getMetadata(filepath):
return header
def cleanGPUUsedForce():
import torch.cuda
import comfy.model_management
if torch.cuda.is_available():
torch.cuda.empty_cache()
comfy.model_management.unload_all_models()
gc.collect()
mm.unload_all_models()
mm.soft_empty_cache()
+4 -3
View File
@@ -2,11 +2,12 @@ import os, torch
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from .utils import easySave
from ..config import RESOURCES_DIR
from ..log import log_node_warn
from ..adv_encode import advanced_encode
from .adv_encode import advanced_encode
from .controlnet import easyControlnet
from .log import log_node_warn
from ..layer_diffuse.func import LayerDiffuse
from ..config import RESOURCES_DIR
class easyXYPlot():
def __init__(self, xyPlotData, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id, sampler, easyCache):
+31 -1
View File
@@ -2,6 +2,7 @@ from typing import Iterator, List, Tuple, Dict, Any, Union, Optional
from _decimal import Context, getcontext
from decimal import Decimal
from .libs.utils import AlwaysEqualProxy, cleanGPUUsedForce
from .libs.cache import remove_cache
import numpy as np
import json
@@ -277,6 +278,30 @@ class imageSwitch:
else:
return (image_b, )
class textSwitch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("INT", {"default": 1, "min": 1, "max": 2}),
},
"optional": {
"text1": ("STRING", {"forceInput": True}),
"text2": ("STRING", {"forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
CATEGORY = "EasyUse/Logic/Switch"
FUNCTION = "switch"
def switch(self, input, text1=None, text2=None,):
if input == 1:
return (text1,)
else:
return (text2,)
# ---------------------------------------------------------------运算 开始----------------------------------------------------------------------#
COMPARE_FUNCTIONS = {
@@ -507,9 +532,9 @@ class cleanGPUUsed:
def empty_cache(self, anything, unique_id=None, extra_pnginfo=None):
cleanGPUUsedForce()
remove_cache('*')
return ()
from .libs.cache import remove_cache
class clearCacheKey:
@classmethod
def INPUT_TYPES(s):
@@ -549,6 +574,9 @@ class clearCacheAll:
remove_cache('*')
return ()
NODE_CLASS_MAPPINGS = {
"easy string": String,
"easy int": Int,
@@ -558,6 +586,7 @@ NODE_CLASS_MAPPINGS = {
"easy boolean": Boolean,
"easy compare": Compare,
"easy imageSwitch": imageSwitch,
"easy textSwitch": textSwitch,
"easy if": If,
"easy isSDXL": isSDXL,
"easy xyAny": xyAny,
@@ -577,6 +606,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy boolean": "Boolean",
"easy compare": "Compare",
"easy imageSwitch": "Image Switch",
"easy textSwitch": "Text Switch",
"easy if": "If",
"easy isSDXL": "Is SDXL",
"easy xyAny": "XYAny",
-38
View File
@@ -2,9 +2,6 @@ import random
import server
from enum import Enum
seed_nodes = ["easy wildcards","easy preSampling","easy preSamplingAdvanced","easy preSamplingSdTurbo","easy preSamplingDynamicCFG","easy preSamplingLayerDiffusion","easy preSamplingCascade","easy fullCascadeKSampler","easy fullkSampler","easy seed","easy latentNoisy", "easy preSamplingNoiseIn"]
class SGmode(Enum):
FIX = 1
INCR = 2
@@ -123,41 +120,6 @@ def prompt_seed_update(json_data):
# control after generated
if mode is not None and not mode:
control_seed(node[1], action, seed_is_global)
# else:
# prompts = json_data['prompt'].items()
# for k, v in prompts:
# if 'class_type' not in v:
# continue
# cls = v['class_type']
# if cls in seed_nodes:
# extra_data = next((x for x in workflow["nodes"] if str(x["id"]) == k), None)
# if extra_data is not None:
# inputs = extra_data.get('inputs')
# widgets_value = extra_data.get('widgets_values')
# widgets_length = len(widgets_value)
# if "disable" in widgets_value:
# break
# if inputs is not None and inputs != []:
# seed_num_input = next((x for x in inputs if x['name'] == 'seed_num' and x['type'] == 'INT'), None)
# if seed_num_input is not None:
# action = 'fixed'
# else:
# action = widgets_value[widgets_length - 1]
# else:
# control_index = widgets_length - 2 if cls == 'easy seed' else widgets_length - 1
# action = widgets_value[control_index]
#
# # print(action)
# node = k, v
# value = control_seed(node[1], action, False)
#
# if k not in seed_widget_map:
# continue
#
# if 'seed_num' in v['inputs']:
# if isinstance(v['inputs']['seed_num'], int):
# v['inputs']['seed_num'] = value
return value is not None
+15
View File
@@ -0,0 +1,15 @@
[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.1.8"
license = "LICENSE"
dependencies = ["diffusers>=0.25.0", "clip_interrogator>=0.6.0", "sentencepiece==0.2.0", "lark-parser", "onnxruntime"]
[project.urls]
Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "yolain"
DisplayName = "ComfyUI-Easy-Use"
Icon = ""
+2 -1
View File
@@ -1,4 +1,5 @@
diffusers>=0.25.0
clip_interrogator>=0.6.0
sentencepiece==0.2.0
lark-parser
onnxruntime
aiohttp
+35
View File
@@ -0,0 +1,35 @@
.easyuse-chooser-dialog{
max-width: 600px;
}
.easyuse-chooser-dialog-title{
font-size: 18px;
font-weight: 700;
text-align: center;
color:var(--input-text);
margin:0;
}
.easyuse-chooser-dialog-images{
margin-top:10px;
display: flex;
flex-wrap: wrap;
width: 100%;
box-sizing: border-box;
}
.easyuse-chooser-dialog-images img{
width: 50%;
height: auto;
cursor: pointer;
box-sizing: border-box;
filter:brightness(80%);
}
.easyuse-chooser-dialog-images img:hover{
filter:brightness(100%);
}
.easyuse-chooser-dialog-images img.selected{
border: 4px solid var(--success-color);
}
.easyuse-chooser-hidden{
display: none;
height:0;
}
+19
View File
@@ -10,4 +10,23 @@
background-color: var(--comfy-menu-bg);
padding: 10px 4px;
border: 1px solid var(--border-color);z-index: 999999999;padding-top: 0;
}
#easyuse_groups_map .icon{
width: 12px;
height:12px;
}
#easyuse_groups_map .closeBtn{
float: right;
color: var(--input-text);
border-radius:30px;
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
cursor: pointer;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
}
#easyuse_groups_map .closeBtn:hover{
filter:brightness(120%);
}
+3 -1
View File
@@ -5,4 +5,6 @@
@import "contextmenu.css";
@import "modelinfo.css";
@import "toast.css";
@import "account.css";
@import "account.css";
@import "chooser.css";
@import "toolbar.css";
+1 -1
View File
@@ -1,6 +1,6 @@
:root {
--theme-color:#3f3eed;
--theme-color-light:#006691;
--theme-color-light: #008ecb;
--success-color: #52c41a;
--error-color: #ff4d4f;
--warning-color: #faad14;
+213
View File
@@ -0,0 +1,213 @@
.easyuse-toolbar{
background: rgba(15,15,15,.5);
backdrop-filter: blur(4px) brightness(120%);
border-radius:0 12px 12px 0;
min-width:50px;
height:24px;
position: fixed;
bottom:85px;
left:0px;
display: flex;
align-items: center;
z-index:10000;
}
.easyuse-toolbar.disable-render-info{
bottom: 55px;
}
.easyuse-toolbar-item{
border-radius:20px;
height: 20px;
width:20px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease-in-out;
margin-left:2.5px;
}
.easyuse-toolbar-icon{
width: 14px;
height: 14px;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color:white;
transition: all 0.3s ease-in-out;
}
.easyuse-toolbar-tips{
visibility: hidden;
opacity: 0;
position: absolute;
top: -25px;
left: 0;
color: var(--descrip-text);
padding: 2px 5px;
border-radius: 5px;
font-size: 11px;
min-width:100px;
transition: all 0.3s ease-in-out;
}
.easyuse-toolbar-item:hover{
background:rgba(12,12,12,1);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-tips{
opacity: 1;
visibility: visible;
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.group{
color:var(--warning-color);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.rocket{
color:var(--theme-color-light);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.question{
color:var(--success-color);
}
.easyuse-guide-dialog{
max-width: 300px;
font-family: var(--font-family);
position: absolute;
z-index:100;
left:0;
bottom:140px;
background: rgba(25,25,25,.85);
backdrop-filter: blur(8px) brightness(120%);
border-radius:0 12px 12px 0;
padding:10px;
transition: .5s all ease-in-out;
visibility: visible;
opacity: 1;
transform: translateX(0%);
}
.easyuse-guide-dialog.disable-render-info{
bottom:110px;
}
.easyuse-guide-dialog-top{
display: flex;
justify-content: space-between;
align-items: center;
}
.easyuse-guide-dialog-top .icon{
width: 12px;
height:12px;
}
.easyuse-guide-dialog.hidden{
opacity: 0;
transform: translateX(-50%);
visibility: hidden;
}
.easyuse-guide-dialog .closeBtn{
float: right;
color: var(--input-text);
border-radius:30px;
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
cursor: pointer;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
}
.easyuse-guide-dialog .closeBtn:hover{
filter:brightness(120%);
}
.easyuse-guide-dialog-title{
color:var(--input-text);
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.easyuse-guide-dialog-remark{
color: var(--input-text);
font-size: 12px;
margin-top: 5px;
}
.easyuse-guide-dialog-content{
max-height: 600px;
overflow: auto;
}
.easyuse-guide-dialog a, .easyuse-guide-dialog a:visited{
color: var(--theme-color-light);
cursor: pointer;
}
.easyuse-guide-dialog-note{
margin-top: 20px;
color:white;
}
.easyuse-guide-dialog p{
margin:4px 0;
font-size: 12px;
font-weight: 300;
}
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin-top: 12px;
margin-bottom: 8px;
font-weight: 600;
line-height: 1.25;
padding-bottom: 5px;
border-bottom: 1px solid var(--border-color);
color: var(--input-text);
}
.markdown-body h1{
font-size: 18px;
}
.markdown-body h2{
font-size: 16px;
}
.markdown-body h3{
font-size: 14px;
}
.markdown-body h4{
font-size: 13px;
}
.markdown-body table {
display: block;
/*width: 100%;*/
/*width: max-content;*/
max-width: 300px;
overflow: auto;
color:var(--input-text);
box-sizing: border-box;
border: 1px solid var(--border-color);
text-align: left;
width: 100%;
}
.markdown-body table th, .markdown-body table td {
padding: 6px 13px;
font-size: 12px;
margin:0;
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.markdown-body table td {
font-size: 12px;
}
.markdown-body table th:last-child, .markdown-body table td:last-child{
border-right: none;
}
.markdown-body table tr:last-child td{
border-bottom: none;
}
.markdown-body table th{
font-weight: bold;
width: auto;
min-width: 70px;
}
.markdown-body table th:last-child{
width:100%;
}
.markdown-body .warning{
color:var(--warning-color)
}
.markdown-body .error{
color:var(--error-color)
}
.markdown-body .success{
color:var(--success-color)
}
.markdown-body .link{
color:var(--theme-color-light)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { app } from "/scripts/app.js";
import { app } from "../../../scripts/app.js";
app.registerExtension({
+14 -3
View File
@@ -2,6 +2,9 @@ import {getLocale} from './utils.js'
const locale = getLocale()
const zhCN = {
"Workflow created by": "工作流创建者",
"Watch more video content": "观看更多视频内容",
"Workflow Guide":"工作流指南",
// ExtraMenu
"💎 View Checkpoint Info...": "💎 查看 Checkpoint 信息...",
"💎 View Lora Info...": "💎 查看 Lora 信息...",
@@ -14,11 +17,16 @@ const zhCN = {
"No notes": "当前还没有备注内容",
"Saving Notes...": "正在保存备注...",
"Type your notes here":"在这里输入备注内容",
"ModelName":"模型名称",
"Models Required":"所需模型",
"Download Model": "下载模型",
"Source Url": "模型源地址",
"Notes": "备注",
"Type": "类型",
"Trained Words": "训练词",
"BaseModel": "基础算法",
"Details": "详情",
"Description": "描述",
"Download": "下载量",
"Source": "来源",
"Saving Preview...": "正在保存预览图...",
@@ -30,9 +38,8 @@ const zhCN = {
"Reboot ComfyUI":"重启ComfyUI",
"Are you sure you'd like to reboot the server?": "是否要重启ComfyUI?",
// GroupMap
"Groups Map (EasyUse)": "管理组 (EasyUse)",
"Reboot ComfyUI (EasyUse)": "重启服务 (EasyUse)",
"Forced Cleanup Of GPU Usage (EasyUse)": "强制清理GPU占用 (EasyUse)",
"Groups Map": "管理组",
"Cleanup Of GPU Usage": "清理GPU占用",
"Please stop all running tasks before cleaning GPU": "请在清理GPU之前停止所有运行中的任务",
"Always": "启用中",
"Bypass": "已忽略",
@@ -43,6 +50,7 @@ const zhCN = {
"Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)": "启用ALT1~9从节点模板粘贴到工作流 (ComfyUI-Easy-Use)",
"Enable process bar in queue button (ComfyUI-Easy-Use)": "启用提示词队列进度显示条 (ComfyUI-Easy-Use)",
"Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)": "启用上下文菜单自动嵌套子目录 (ComfyUI-Easy-Use)",
"Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)": "启用工具栏固定在左下角 (ComfyUI-Easy-Use)",
"Too many thumbnails, have closed the display": "模型缩略图太多啦,为您关闭了显示",
// selector
"Empty All": "清空所有",
@@ -59,6 +67,9 @@ const zhCN = {
"APIKEY is not Empty": "APIKEY 不能为空",
"Add Account": "添加账号",
"Getting Your APIKEY": "获取您的APIKEY",
// choosers
"Choose Selected Images": "选择选中的图片",
"Choose images to continue": "选择图片以继续",
// seg
"Background": "背景",
"Hat": "帽子",
+5
View File
@@ -0,0 +1,5 @@
export const quesitonIcon = `<svg t="1714564780771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1489" width="200" height="200" data-spm-anchor-id="a313x.search_index.0.i2.5a663a81pw6qup"><path d="M514.048 54.272q95.232 0 178.688 36.352t145.92 98.304 98.304 145.408 35.84 178.688-35.84 178.176-98.304 145.408-145.92 98.304-178.688 35.84-178.176-35.84-145.408-98.304-98.304-145.408-35.84-178.176 35.84-178.688 98.304-145.408 145.408-98.304 178.176-36.352zM515.072 826.368q26.624 0 44.544-17.92t17.92-43.52q0-26.624-17.92-44.544t-44.544-17.92-44.544 17.92-17.92 44.544q0 25.6 17.92 43.52t44.544 17.92zM567.296 574.464q-1.024-16.384 20.48-34.816t48.128-40.96 49.152-50.688 24.576-65.024q2.048-39.936-8.192-74.752t-33.792-59.904-60.928-39.936-87.552-14.848q-62.464 0-103.936 22.016t-67.072 53.248-35.84 64.512-9.216 55.808q1.024 26.624 16.896 38.912t34.304 12.8 33.792-10.24 15.36-31.232q0-12.288 7.68-30.208t20.992-34.304 32.256-27.648 42.496-11.264q46.08 0 73.728 23.04t25.6 57.856q0 17.408-10.24 32.256t-26.112 28.672-33.792 27.648-33.792 28.672-26.624 32.256-11.776 37.888l1.024 38.912q0 15.36 14.336 29.184t37.888 14.848q23.552-1.024 37.376-15.36t12.8-32.768l0-24.576z" p-id="1490" fill="currentColor"></path></svg>`
export const rocketIcon = `<svg t="1714565020764" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7999" width="200" height="200"><path d="M810.438503 379.664884l-71.187166-12.777183C737.426025 180.705882 542.117647 14.602496 532.991087 7.301248c-12.777184-10.951872-32.855615-10.951872-47.45811 0-9.12656 7.301248-204.434938 175.229947-206.26025 359.586453l-67.536542 10.951871c-18.253119 3.650624-31.030303 18.253119-31.030303 36.506239v189.832442c0 10.951872 5.475936 21.903743 12.777184 27.379679 7.301248 5.475936 14.602496 9.12656 23.729055 9.12656h5.475936l133.247772-23.729055c40.156863 47.458111 91.265597 73.012478 151.500891 73.012477 60.235294 0 111.344029-27.379679 151.500891-74.837789l136.898396 23.729055h5.475936c9.12656 0 16.427807-3.650624 23.729055-9.12656 9.12656-7.301248 12.777184-16.427807 12.777184-27.379679V412.520499c1.825312-14.602496-10.951872-29.204991-27.379679-32.855615zM620.606061 766.631016H401.568627c-20.078431 0-36.506239 16.427807-36.506238 36.506239v109.518716c0 14.602496 9.12656 29.204991 23.729055 34.680927 14.602496 5.475936 31.030303 1.825312 40.156863-9.126559l16.427807-18.25312 32.855615 80.313726c5.475936 14.602496 18.253119 23.729055 34.680927 23.729055 16.427807 0 27.379679-9.12656 34.680927-23.729055l32.855615-80.313726 16.427807 18.25312c10.951872 10.951872 25.554367 14.602496 40.156863 9.126559 14.602496-5.475936 23.729055-18.253119 23.729055-34.680927v-109.518716c-3.650624-20.078431-20.078431-36.506239-40.156862-36.506239z" fill="currentColor" p-id="8000"></path></svg>`
export const groupIcon = `<svg t="1714565543756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22538" width="200" height="200"><path d="M871.616 64H152.384c-31.488 0-60.416 25.28-60.416 58.24v779.52c0 32.896 26.24 58.24 60.352 58.24h719.232c34.112 0 60.352-25.344 60.352-58.24V122.24c0.128-32.96-28.8-58.24-60.288-58.24zM286.272 512c-23.616 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008A43.328 43.328 0 0 1 286.272 512z m0-202.496c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008a43.456 43.456 0 0 1-44.608 43.008zM737.728 512H435.904c-23.68 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.752 42.752 0 0 1-41.984 43.008z m0-202.496H435.904c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.88 42.88 0 0 1-42.048 43.008z" p-id="22539" fill="currentColor"></path></svg>`
export const rebootIcon = `<svg t="1714568501931" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4275" width="200" height="200"><path d="M511.721751 0.000278a511.999861 511.999861 0 1 0 512.277971 511.721751A511.721751 511.721751 0 0 0 511.721751 0.000278zM184.386696 511.722029A36.988583 36.988583 0 0 1 222.487718 475.011556h92.888622a36.710473 36.710473 0 0 1 0 73.420947H222.487718a36.710473 36.710473 0 0 1-38.101022-36.710474z m201.351385 158.522499l-65.911986 65.911987a36.988583 36.988583 0 0 1-62.852781-25.864197 38.101022 38.101022 0 0 1 10.846276-26.142307L333.731577 618.238024a36.710473 36.710473 0 1 1 52.006504 52.006504z m29.201513-256.138985a36.710473 36.710473 0 0 1-52.006504 0l-65.633877-65.633877a36.988583 36.988583 0 0 1 26.142307-62.85278 36.154254 36.154254 0 0 1 25.864197 10.846276L414.939594 361.54282a36.988583 36.988583 0 0 1 0 52.562723z m135.439398 373.779366a37.266693 37.266693 0 0 1-36.988583 36.988583 36.988583 36.988583 0 0 1-36.710473-36.988583V695.274397a36.988583 36.988583 0 0 1 36.710473-36.988583A37.266693 37.266693 0 0 1 550.378992 695.274397z m0-459.437137a37.266693 37.266693 0 0 1-36.988583 36.988583 36.988583 36.988583 0 0 1-36.710473-36.988583V235.559149a36.988583 36.988583 0 0 1 36.710473-36.988583 37.544802 37.544802 0 0 1 36.988583 36.988583z m63.965219 15.85225L679.978088 278.109926a36.710473 36.710473 0 0 1 52.006504 51.728394L667.463154 396.584635a37.544802 37.544802 0 0 1-52.284614 0 36.988583 36.988583 0 0 1-10.568166-26.142306 36.432364 36.432364 0 0 1 9.733837-26.142307z m122.090135 397.974905a37.544802 37.544802 0 0 1-52.284613 0l-65.355767-65.911986a36.154254 36.154254 0 0 1 0-51.728395 36.710473 36.710473 0 0 1 25.864197-10.846276 35.876145 35.876145 0 0 1 25.864197 10.846276l65.911986 65.633877a36.988583 36.988583 0 0 1 0 52.006504z m66.468206-194.676753h-92.888622a36.710473 36.710473 0 0 1 0-73.420947h92.888622a36.710473 36.710473 0 0 1 0 73.420947z" fill="currentColor" p-id="4276"></path></svg>`
export const closeIcon = `<svg t="1714965640187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4264" width="200" height="200"><path d="M597.795527 511.488347 813.564755 295.718095c23.833825-23.833825 23.833825-62.47489 0.001023-86.307691-23.832801-23.832801-62.47489-23.833825-86.307691 0L511.487835 425.180656 295.717583 209.410404c-23.833825-23.833825-62.475913-23.833825-86.307691 0-23.832801 23.832801-23.833825 62.47489 0 86.308715l215.769228 215.769228L209.410915 727.258599c-23.833825 23.833825-23.833825 62.47489 0 86.307691 23.832801 23.833825 62.473867 23.833825 86.307691 0l215.768205-215.768205 215.769228 215.769228c23.834848 23.833825 62.475913 23.832801 86.308715 0 23.833825-23.833825 23.833825-62.47489 0-86.307691L597.795527 511.488347z" fill="currentColor" p-id="4265"></path></svg>`
+2 -2
View File
@@ -1,5 +1,5 @@
import { $el, ComfyDialog } from "/scripts/ui.js";
import { api } from "/scripts/api.js";
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../../scripts/api.js";
import {formatTime} from './utils.js';
import {$t} from "./i18n.js";
import {toast} from "./toast.js";
+9
View File
@@ -1,4 +1,5 @@
import {sleep} from "./utils.js";
import {$t} from "./i18n.js";
class Toast{
@@ -68,6 +69,14 @@ class Toast{
container && (container.innerHTML = "");
}
async copyright(duration = 5000, actions = []) {
this.showToast({
id: `toast-info`,
content: `${this.info_icon} ${$t('Workflow created by')} <a href="https://github.com/yolain/">Yolain</a> , ${$t('Watch more video content')} <a href="https://space.bilibili.com/1840885116">B站乱乱呀</a>`,
duration,
actions
});
}
async info(content, duration = 3000, actions = []) {
this.showToast({
id: `toast-info`,
+467 -253
View File
@@ -1,8 +1,11 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import {deepEqual, addCss, isLocalNetwork} from "../common/utils.js";
import {quesitonIcon, rocketIcon, groupIcon, rebootIcon, closeIcon} from "../common/icon.js";
import {$t} from '../common/i18n.js';
import {toast} from "../common/toast.js";
import {$el, ComfyDialog} from "../../../../scripts/ui.js";
addCss('css/index.css')
@@ -21,6 +24,396 @@ api.addEventListener("easyuse-toast",event=>{
})
}
})
let draggerEl = null
let isGroupMapcanMove = true
function createGroupMap(){
let div = document.querySelector('#easyuse_groups_map')
if(div){
div.style.display = div.style.display == 'none' ? 'flex' : 'none'
return
}
let groups = app.canvas.graph._groups
let nodes = app.canvas.graph._nodes
let old_nodes = groups.length
div = document.createElement('div')
div.id = 'easyuse_groups_map'
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 8px);
justify-content: space-between;
align-items: center;
padding: 0 6px;
height: 44px;`
let hideBtn = $el('button.closeBtn',{
innerHTML:closeIcon,
onclick:_=>div.style.display = 'none'
})
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(hideBtn)
textB.style.fontSize = '11px'
textB.innerHTML = `<b>${$t('Groups Map')} (EasyUse)</b>`
div.appendChild(btn)
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
if(isGroupMapcanMove){
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
}
})
function updateGroups(groups, groupsDiv, autoSortDiv){
if(groups.length>0){
autoSortDiv.style.display = 'block'
}else autoSortDiv.style.display = 'none'
for (let index in groups) {
const group = groups[index]
const title = group.title
const show_text = $t('Always')
const hide_text = $t('Bypass')
const mute_text = $t('Never')
let group_item = document.createElement('div')
let group_item_style = `justify-content: space-between;display:flex;background-color: var(--comfy-input-bg);border-radius: 5px;border:1px solid var(--border-color);margin-top:5px;`
group_item.addEventListener("mouseover",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1.2);"
})
group_item.addEventListener("mouseleave",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1);"
})
group_item.addEventListener("dragstart",e=>{
draggerEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setDragImage(emptyImg, 0, 0);
})
group_item.addEventListener("dragend",e=>{
e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable");
document.querySelectorAll('.easyuse-group-item').forEach((el,i) => {
var prev_i = el.dataset.id;
if (el == draggerEl && prev_i != i ) {
groups.splice(i, 0, groups.splice(prev_i, 1)[0]);
}
el.dataset.id = i;
});
isGroupMapcanMove = true
})
group_item.addEventListener("dragover",e=>{
e.preventDefault();
if (e.currentTarget == draggerEl) return;
let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget.nextSibling);
} else {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget);
}
isGroupMapcanMove = true
})
group_item.setAttribute('data-id',index)
group_item.className = 'easyuse-group-item'
group_item.style = group_item_style
// 标题
let text_group_title = document.createElement('div')
text_group_title.style = `flex:1;font-size:12px;color:var(--input-text);padding:4px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;cursor:pointer`
text_group_title.innerHTML = `${title}`
text_group_title.addEventListener('mousedown',e=>{
isGroupMapcanMove = false
e.currentTarget.parentNode.draggable = 'true';
})
text_group_title.addEventListener('mouseleave',e=>{
setTimeout(_=>{
isGroupMapcanMove = true
},150)
})
group_item.append(text_group_title)
// 按钮组
let buttons = document.createElement('div')
group.recomputeInsideNodes();
const nodesInGroup = group._nodes;
let isGroupShow = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 0
let isGroupMute = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 2
let go_btn = document.createElement('button')
go_btn.style = "margin-right:6px;cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;"
go_btn.innerText = "Go"
go_btn.addEventListener('click', () => {
app.canvas.ds.offset[0] = -group.pos[0] - group.size[0] * 0.5 + (app.canvas.canvas.width * 0.5) / app.canvas.ds.scale;
app.canvas.ds.offset[1] = -group.pos[1] - group.size[1] * 0.5 + (app.canvas.canvas.height * 0.5) / app.canvas.ds.scale;
app.canvas.setDirty(true, true);
app.canvas.setZoom(1)
})
buttons.append(go_btn)
let see_btn = document.createElement('button')
let defaultStyle = `cursor:pointer;font-size:10px;;padding:2px;border: 1px solid var(--border-color);border-radius:4px;width:36px;`
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
let pressTimer
let firstTime =0, lastTime =0
let isHolding = false
see_btn.addEventListener('click', () => {
if(isHolding){
isHolding = false
return
}
for (const node of nodesInGroup) {
node.mode = isGroupShow ? 4 : 0;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
})
see_btn.addEventListener('mousedown', () => {
firstTime = new Date().getTime();
clearTimeout(pressTimer);
pressTimer = setTimeout(_=>{
for (const node of nodesInGroup) {
node.mode = isGroupMute ? 0 : 2;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
},500)
})
see_btn.addEventListener('mouseup', () => {
lastTime = new Date().getTime();
if(lastTime - firstTime > 500) isHolding = true
clearTimeout(pressTimer);
})
buttons.append(see_btn)
group_item.append(buttons)
groupsDiv.append(group_item)
}
}
let groupsDiv = document.createElement('div')
groupsDiv.id = 'easyuse-groups-items'
groupsDiv.style = `overflow-y: auto;max-height: 400px;height:100%;width: 100%;`
let autoSortDiv = document.createElement('button')
autoSortDiv.style = `cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;`
autoSortDiv.innerText = $t('Auto Sorting')
autoSortDiv.addEventListener('click',e=>{
e.preventDefault()
groupsDiv.innerHTML = ``
let new_groups = groups.sort((a,b)=> a['pos'][0] - b['pos'][0]).sort((a,b)=> a['pos'][1] - b['pos'][1])
updateGroups(new_groups, groupsDiv, autoSortDiv)
})
updateGroups(groups, groupsDiv, autoSortDiv)
div.appendChild(groupsDiv)
let remarkDiv = document.createElement('p')
remarkDiv.style = `text-align:center; font-size:10px; padding:0 10px;color:var(--descrip-text)`
remarkDiv.innerText = $t('Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never')
div.appendChild(groupsDiv)
div.appendChild(remarkDiv)
div.appendChild(autoSortDiv)
let graphDiv = document.getElementById("graph-canvas")
graphDiv.addEventListener('mouseover', async () => {
groupsDiv.innerHTML = ``
let new_groups = app.canvas.graph._groups
updateGroups(new_groups, groupsDiv, autoSortDiv)
old_nodes = nodes
})
if (!document.querySelector('#easyuse_groups_map')){
document.body.appendChild(div)
}else{
div.style.display = 'flex'
}
}
async function cleanup(){
try {
const {Running, Pending} = await api.getQueue()
if(Running.length>0 || Pending.length>0){
toast.error($t("Clean Failed")+ ":"+ $t("Please stop all running tasks before cleaning GPU"))
return
}
api.fetchApi("/easyuse/cleangpu",{
method:"POST"
}).then(res=>{
if(res.status == 200){
toast.success($t("Clean SuccessFully"))
}else{
toast.error($t("Clean Failed"))
}
})
} catch (exception) {}
}
let guideDialog = null
let isDownloading = false
function download_model(url,local_dir){
if(isDownloading || !url || !local_dir) return
isDownloading = true
let body = new FormData();
body.append('url', url);
body.append('local_dir', local_dir);
api.fetchApi("/easyuse/model/download",{
method:"POST",
body
}).then(res=>{
if(res.status == 200){
toast.success($t("Download SuccessFully"))
}else{
toast.error($t("Download Failed"))
}
isDownloading = false
})
}
class GuideDialog {
constructor(note, need_models){
this.dialogDiv = null
this.modelsDiv = null
if(need_models?.length>0){
let tbody = []
for(let i=0;i<need_models.length;i++){
tbody.push($el('tr',[
$el('td',{innerHTML:need_models[i].title || need_models[i].name || ''}),
$el('td',[
need_models[i]['download_url'] ? $el('a',{onclick:_=>download_model(need_models[i]['download_url'],need_models[i]['local_dir']), target:"_blank", textContent:$t('Download Model')}) : '',
need_models[i]['source_url'] ? $el('a',{href:need_models[i]['source_url'], target:"_blank", textContent:$t('Source Url')}) : '',
need_models[i]['desciption'] ? $el('span',{textContent:need_models[i]['desciption']}) : '',
]),
]))
}
this.modelsDiv = $el('div.easyuse-guide-dialog-models.markdown-body',[
$el('h3',{textContent:$t('Models Required')}),
$el('table',{cellpadding:0,cellspacing:0},[
$el('thead',[
$el('tr',[
$el('th',{innerHTML:$t('ModelName')}),
$el('th',{innerHTML:$t('Description')}),
])
]),
$el('tbody',tbody)
])
])
}
this.dialogDiv = $el('div.easyuse-guide-dialog.hidden',[
$el('div.easyuse-guide-dialog-header',[
$el('div.easyuse-guide-dialog-top',[
$el('div.easyuse-guide-dialog-title',{
innerHTML:$t('Workflow Guide')
}),
$el('button.closeBtn',{innerHTML:closeIcon,onclick:_=>this.close()})
]),
$el('div.easyuse-guide-dialog-remark',{
innerHTML:`${$t('Workflow created by')} <a href="https://github.com/yolain/" target="_blank">Yolain</a> , ${$t('Watch more video content')} <a href="https://space.bilibili.com/1840885116" target="_blank">B站乱乱呀</a>`
})
]),
$el('div.easyuse-guide-dialog-content.markdown-body',[
$el('div.easyuse-guide-dialog-note',{
innerHTML:note
}),
...this.modelsDiv ? [this.modelsDiv] : []
])
])
if(disableRenderInfo){
this.dialogDiv.classList.add('disable-render-info')
}
document.body.appendChild(this.dialogDiv)
}
show(){
if(this.dialogDiv) this.dialogDiv.classList.remove('hidden')
}
close(){
if(this.dialogDiv){
this.dialogDiv.classList.add('hidden')
}
}
toggle(){
if(this.dialogDiv){
if(this.dialogDiv.classList.contains('hidden')){
this.show()
}else{
this.close()
}
}
}
remove(){
if(this.dialogDiv) document.body.removeChild(this.dialogDiv)
}
}
const getEnableToolBar = _ => app.ui.settings.getSettingValue(toolBarId, true)
const toolBarId = "Comfy.EasyUse.toolBar"
let enableToolBar = getEnableToolBar()
let disableRenderInfo = localStorage['Comfy.Settings.Comfy.EasyUse.disableRenderInfo'] ? true : false
export function addToolBar(app) {
app.ui.settings.addSetting({
id: toolBarId,
name: $t("Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)"),
type: "boolean",
defaultValue: enableToolBar,
onChange(value) {
enableToolBar = !!value;
if(enableToolBar){
showToolBar()
}else hideToolBar()
},
});
}
let note = null
let toolbar = null
function showToolBar(){
toolbar.style.display = 'flex'
}
function hideToolBar(){
toolbar.style.display = 'none'
}
app.registerExtension({
name: "comfy.easyUse",
init() {
@@ -28,273 +421,27 @@ app.registerExtension({
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = getCanvasMenuOptions.apply(this, arguments);
let draggerEl = null
let isGroupMapcanMove = true
let old_groups = []
let emptyImg = new Image()
emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
options.push(null,
// Groups Map
{
content: '📜 '+ $t('Groups Map (EasyUse)'),
content: groupIcon.replace('currentColor','var(--warning-color)') + ' '+ $t('Groups Map') + ' (EasyUse)',
callback: async() => {
let groups = app.canvas.graph._groups
let nodes = app.canvas.graph._nodes
let old_nodes = groups.length
let div =
document.querySelector('#easyuse_groups_map') ||
document.createElement('div')
div.id = 'easyuse_groups_map'
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 8px);
justify-content: space-between;
align-items: center;
padding: 0 6px;
height: 44px;`
let hideBtn = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(hideBtn)
textB.style.fontSize = '11px'
textB.innerHTML = `<b>${$t('Groups Map (EasyUse)')}</b>`
hideBtn.style = `float: right;color: var(--input-text);border-radius:6px;font-size:9px;
background-color: var(--comfy-input-bg); border: 1px solid var(--border-color);cursor: pointer;padding: 5px;aspect-ratio: 1 / 1;`
hideBtn.addEventListener('click', () => {
div.style.display = 'none'
})
hideBtn.innerText = '❌'
div.appendChild(btn)
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
if(isGroupMapcanMove){
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
}
})
function updateGroups(groups, groupsDiv, autoSortDiv){
if(groups.length>0){
autoSortDiv.style.display = 'block'
}else autoSortDiv.style.display = 'none'
for (let index in groups) {
const group = groups[index]
const title = group.title
const show_text = $t('Always')
const hide_text = $t('Bypass')
const mute_text = $t('Never')
let group_item = document.createElement('div')
let group_item_style = `justify-content: space-between;display:flex;background-color: var(--comfy-input-bg);border-radius: 5px;border:1px solid var(--border-color);margin-top:5px;`
group_item.addEventListener("mouseover",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1.2);"
})
group_item.addEventListener("mouseleave",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1);"
})
group_item.addEventListener("dragstart",e=>{
draggerEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setDragImage(emptyImg, 0, 0);
})
group_item.addEventListener("dragend",e=>{
e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable");
document.querySelectorAll('.easyuse-group-item').forEach((el,i) => {
var prev_i = el.dataset.id;
if (el == draggerEl && prev_i != i ) {
groups.splice(i, 0, groups.splice(prev_i, 1)[0]);
}
el.dataset.id = i;
});
isGroupMapcanMove = true
})
group_item.addEventListener("dragover",e=>{
e.preventDefault();
if (e.currentTarget == draggerEl) return;
let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget.nextSibling);
} else {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget);
}
isGroupMapcanMove = true
})
group_item.setAttribute('data-id',index)
group_item.className = 'easyuse-group-item'
group_item.style = group_item_style
// 标题
let text_group_title = document.createElement('div')
text_group_title.style = `flex:1;font-size:12px;color:var(--input-text);padding:4px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;cursor:pointer`
text_group_title.innerHTML = `${title}`
text_group_title.addEventListener('mousedown',e=>{
isGroupMapcanMove = false
e.currentTarget.parentNode.draggable = 'true';
})
text_group_title.addEventListener('mouseleave',e=>{
setTimeout(_=>{
isGroupMapcanMove = true
},150)
})
group_item.append(text_group_title)
// 按钮组
let buttons = document.createElement('div')
group.recomputeInsideNodes();
const nodesInGroup = group._nodes;
let isGroupShow = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 0
let isGroupMute = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 2
let go_btn = document.createElement('button')
go_btn.style = "margin-right:6px;cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;"
go_btn.innerText = "Go"
go_btn.addEventListener('click', () => {
app.canvas.ds.offset[0] = -group.pos[0] - group.size[0] * 0.5 + (app.canvas.canvas.width * 0.5) / app.canvas.ds.scale;
app.canvas.ds.offset[1] = -group.pos[1] - group.size[1] * 0.5 + (app.canvas.canvas.height * 0.5) / app.canvas.ds.scale;
app.canvas.setDirty(true, true);
app.canvas.setZoom(1)
})
buttons.append(go_btn)
let see_btn = document.createElement('button')
let defaultStyle = `cursor:pointer;font-size:10px;;padding:2px;border: 1px solid var(--border-color);border-radius:4px;width:36px;`
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
let pressTimer
let firstTime =0, lastTime =0
let isHolding = false
see_btn.addEventListener('click', () => {
if(isHolding){
isHolding = false
return
}
for (const node of nodesInGroup) {
node.mode = isGroupShow ? 4 : 0;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
})
see_btn.addEventListener('mousedown', () => {
firstTime = new Date().getTime();
clearTimeout(pressTimer);
pressTimer = setTimeout(_=>{
for (const node of nodesInGroup) {
node.mode = isGroupMute ? 0 : 2;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
},500)
})
see_btn.addEventListener('mouseup', () => {
lastTime = new Date().getTime();
if(lastTime - firstTime > 500) isHolding = true
clearTimeout(pressTimer);
})
buttons.append(see_btn)
group_item.append(buttons)
groupsDiv.append(group_item)
}
}
let groupsDiv = document.createElement('div')
groupsDiv.id = 'easyuse-groups-items'
groupsDiv.style = `overflow-y: auto;max-height: 400px;height:100%;width: 100%;`
let autoSortDiv = document.createElement('button')
autoSortDiv.style = `cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;`
autoSortDiv.innerText = $t('Auto Sorting')
autoSortDiv.addEventListener('click',e=>{
e.preventDefault()
groupsDiv.innerHTML = ``
let new_groups = groups.sort((a,b)=> a['pos'][0] - b['pos'][0]).sort((a,b)=> a['pos'][1] - b['pos'][1])
updateGroups(new_groups, groupsDiv, autoSortDiv)
})
updateGroups(groups, groupsDiv, autoSortDiv)
div.appendChild(groupsDiv)
let remarkDiv = document.createElement('p')
remarkDiv.style = `text-align:center; font-size:10px; padding:0 10px;color:var(--descrip-text)`
remarkDiv.innerText = $t('Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never')
div.appendChild(groupsDiv)
div.appendChild(remarkDiv)
div.appendChild(autoSortDiv)
let graphDiv = document.getElementById("graph-canvas")
graphDiv.addEventListener('mouseover', async () => {
groupsDiv.innerHTML = ``
let new_groups = app.canvas.graph._groups
updateGroups(new_groups, groupsDiv, autoSortDiv)
old_nodes = nodes
})
if (!document.querySelector('#easyuse_groups_map')){
document.body.appendChild(div)
}else{
div.style.display = 'flex'
}
createGroupMap()
}
},
// Force clean ComfyUI GPU Used 强制卸载模型GPU占用
{
content: '🚀 '+ $t('Forced Cleanup Of GPU Usage (EasyUse)'),
content: rocketIcon.replace('currentColor','var(--theme-color-light)') + ' '+ $t('Cleanup Of GPU Usage') + ' (EasyUse)',
callback: async() =>{
try {
const {Running, Pending} = await api.getQueue()
if(Running.length>0 || Pending.length>0){
toast.error($t("Clean Failed")+ ":"+ $t("Please stop all running tasks before cleaning GPU"))
return
}
api.fetchApi("/easyuse/cleangpu",{
method:"POST"
}).then(res=>{
if(res.status == 200){
toast.success($t("Clean SuccessFully"))
}else{
toast.error($t("Clean Failed"))
}
})
} catch (exception) {}
await cleanup()
}
},
// Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务
isLocalNetwork(window.location.host) ? {
content: '🔴 '+ $t('Reboot ComfyUI (EasyUse)'),
content: rebootIcon.replace('currentColor','var(--error-color)') + ' '+ $t('Reboot ComfyUI') + ' (EasyUse)',
callback: _ =>{
if (confirm($t("Are you sure you'd like to reboot the server?"))){
try {
@@ -306,6 +453,73 @@ app.registerExtension({
);
return options;
};
let renderInfoEvent = LGraphCanvas.prototype.renderInfo
if(disableRenderInfo){
LGraphCanvas.prototype.renderInfo = function (ctx, x, y) {}
}
if(!toolbar){
toolbar = $el('div.easyuse-toolbar',[
$el('div.easyuse-toolbar-item',{
onclick:_=>{
createGroupMap()
}
},[
$el('div.easyuse-toolbar-icon.group', {innerHTML:groupIcon}),
$el('div.easyuse-toolbar-tips',$t('Groups Map'))
]),
$el('div.easyuse-toolbar-item',{
onclick:async()=>{
await cleanup()
}
},[
$el('div.easyuse-toolbar-icon.rocket',{innerHTML:rocketIcon}),
$el('div.easyuse-toolbar-tips',$t('Cleanup Of GPU Usage'))
]),
])
if(disableRenderInfo){
toolbar.classList.add('disable-render-info')
}else{
toolbar.classList.remove('disable-render-info')
}
document.body.appendChild(toolbar)
}
// rewrite handleFile
let loadGraphDataEvent = app.loadGraphData
app.loadGraphData = async function (data, clean=true) {
// if(data?.extra?.cpr){
// toast.copyright()
// }
if(data?.extra?.note){
if(guideDialog) {
guideDialog.remove()
guideDialog = null
}
if(note && toolbar) toolbar.removeChild(note)
const need_models = data.extra?.need_models || null
guideDialog = new GuideDialog(data.extra.note, need_models)
note = $el('div.easyuse-toolbar-item',{
onclick:async()=>{
guideDialog.toggle()
}
},[
$el('div.easyuse-toolbar-icon.question',{innerHTML:quesitonIcon}),
$el('div.easyuse-toolbar-tips',$t('Workflow Guide'))
])
if(toolbar) toolbar.insertBefore(note, toolbar.firstChild)
}
else{
if(note) {
toolbar.removeChild(note)
note = null
}
}
return await loadGraphDataEvent.apply(this, [...arguments])
}
addToolBar(app)
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name.startsWith("easy")) {
+3 -3
View File
@@ -1,6 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { $el, ComfyDialog } from "/scripts/ui.js";
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { $t } from '../common/i18n.js'
import { toast } from "../common/toast.js";
import {sleep, accSub} from "../common/utils.js";
+5 -4
View File
@@ -1,6 +1,6 @@
import {app} from "/scripts/app.js";
import {api} from "/scripts/api.js";
import {$el} from "/scripts/ui.js";
import {app} from "../../../../scripts/app.js";
import {api} from "../../../../scripts/api.js";
import {$el} from "../../../../scripts/ui.js";
import {$t} from "../common/i18n.js";
import {getExtension, spliceExtension} from '../common/utils.js'
import {toast} from "../common/toast.js";
@@ -23,6 +23,7 @@ export function addMenuNestSubSetting(app) {
const getEnableMenuNestSub = _ => app.ui.settings.getSettingValue(setting_id, enableMenuNestSub)
const Loaders = ['easy fullLoader','easy a1111Loader','easy comfyLoader']
app.registerExtension({
name:"comfy.easyUse.contextMenu",
@@ -39,7 +40,7 @@ app.registerExtension({
}
const existingContextMenu = LiteGraph.ContextMenu;
LiteGraph.ContextMenu = function(values,options){
const threshold = 15;
const threshold = 10;
const enabled = getEnableMenuNestSub();
if(!enabled || (values?.length || 0) <= threshold || !(options?.callback) || values.some(i => typeof i !== 'string')){
if(enabled){
+77 -31
View File
@@ -1,6 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { ComfyWidgets } from "/scripts/widgets.js";
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { ComfyWidgets } from "../../../../scripts/widgets.js";
import { toast} from "../common/toast.js";
import { $t } from '../common/i18n.js';
@@ -51,18 +51,18 @@ function widgetLogic(node, widget) {
updateNodeHeight(node)
}
if (widget.name === 'image_output') {
if (widget.value === 'Sender' || widget.value === 'Sender/Save'){
if (widget.value === 'Sender' || widget.value === 'Sender&Save'){
toggleWidget(node, findWidgetByName(node, 'link_id'), true)
}else {
toggleWidget(node, findWidgetByName(node, 'link_id'))
}
if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'PreviewChooser' || widget.value === 'Sender') {
if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'Preview&Choose' || widget.value === 'Sender') {
toggleWidget(node, findWidgetByName(node, 'save_prefix'))
toggleWidget(node, findWidgetByName(node, 'output_path'))
toggleWidget(node, findWidgetByName(node, 'embed_workflow'))
toggleWidget(node, findWidgetByName(node, 'number_padding'))
toggleWidget(node, findWidgetByName(node, 'overwrite_existing'))
} else if (widget.value === 'Save' || widget.value === 'Hide/Save' || widget.value === 'Sender/Save') {
} else if (widget.value === 'Save' || widget.value === 'Hide&Save' || widget.value === 'Sender&Save') {
toggleWidget(node, findWidgetByName(node, 'save_prefix'), true)
toggleWidget(node, findWidgetByName(node, 'output_path'), true)
toggleWidget(node, findWidgetByName(node, 'embed_workflow'), true)
@@ -70,7 +70,7 @@ function widgetLogic(node, widget) {
toggleWidget(node, findWidgetByName(node, 'overwrite_existing'), true)
}
if(widget.value === 'Hide' || widget.value === 'Hide/Save'){
if(widget.value === 'Hide' || widget.value === 'Hide&Save'){
toggleWidget(node, findWidgetByName(node, 'decode_vae_name'))
}else{
toggleWidget(node, findWidgetByName(node, 'decode_vae_name'), true)
@@ -110,21 +110,72 @@ function widgetLogic(node, widget) {
}
updateNodeHeight(node)
}
if (widget.name === 'mode') {
let number_to_show = findWidgetByName(node, 'num_loras').value + 1
if (widget.name === 'num_controlnet') {
let number_to_show = widget.value + 1
for (let i = 0; i < number_to_show; i++) {
if (widget.value === "simple") {
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
toggleWidget(node, findWidgetByName(node, 'controlnet_'+i), true)
toggleWidget(node, findWidgetByName(node, 'controlnet_'+i+'_strength'), true)
toggleWidget(node, findWidgetByName(node, 'scale_soft_weight_'+i),true)
if (findWidgetByName(node, 'mode').value === "simple") {
toggleWidget(node, findWidgetByName(node, 'start_percent_'+i))
toggleWidget(node, findWidgetByName(node, 'end_percent_'+i))
} else {
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)}
toggleWidget(node, findWidgetByName(node, 'start_percent_'+i),true)
toggleWidget(node, findWidgetByName(node, 'end_percent_'+i), true)
}
}
for (let i = number_to_show; i < 10; i++) {
toggleWidget(node, findWidgetByName(node, 'controlnet_'+i))
toggleWidget(node, findWidgetByName(node, 'controlnet_'+i+'_strength'))
toggleWidget(node, findWidgetByName(node, 'start_percent_'+i))
toggleWidget(node, findWidgetByName(node, 'end_percent_'+i))
toggleWidget(node, findWidgetByName(node, 'scale_soft_weight_'+i))
}
updateNodeHeight(node)
}
if (widget.name === 'mode') {
switch (node.comfyClass) {
case 'easy loraStack':
for (let i = 0; i < (findWidgetByName(node, 'num_loras').value + 1); i++) {
if (widget.value === "simple") {
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
} else {
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)}
}
updateNodeHeight(node)
break
case 'easy controlnetStack':
for (let i = 0; i < (findWidgetByName(node, 'num_controlnet').value + 1); i++) {
if (widget.value === "simple") {
toggleWidget(node, findWidgetByName(node, 'start_percent_'+i))
toggleWidget(node, findWidgetByName(node, 'end_percent_'+i))
} else {
toggleWidget(node, findWidgetByName(node, 'start_percent_' + i), true)
toggleWidget(node, findWidgetByName(node, 'end_percent_' + i), true)
}
}
updateNodeHeight(node)
break
case 'easy icLightApply':
if (widget.value === "Foreground") {
toggleWidget(node, findWidgetByName(node, 'lighting'), true)
toggleWidget(node, findWidgetByName(node, 'remove_bg'), true)
toggleWidget(node, findWidgetByName(node, 'source'))
} else {
toggleWidget(node, findWidgetByName(node, 'lighting'))
toggleWidget(node, findWidgetByName(node, 'source'), true)
toggleWidget(node, findWidgetByName(node, 'remove_bg'))
}
updateNodeHeight(node)
break
}
}
if (widget.name === 'resolution') {
if (widget.value === "自定义 x 自定义") {
toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), true)
@@ -133,7 +184,6 @@ function widgetLogic(node, widget) {
toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), false)
toggleWidget(node, findWidgetByName(node, 'empty_latent_height'), false)
}
updateNodeHeight(node)
}
if (widget.name === 'downscale_mode') {
const widget_names = ['block_number', 'downscale_factor', 'start_percent', 'end_percent', 'downscale_after_skip', 'downscale_method', 'upscale_method']
@@ -558,7 +608,9 @@ app.registerExtension({
case "easy svdLoader":
case "easy dynamiCrafterLoader":
case "easy loraStack":
case "easy controlnetStack":
case "easy latentNoisy":
case "easy preSampling":
case "easy preSamplingAdvanced":
case "easy preSamplingNoiseIn":
case "easy preSamplingCustom":
@@ -578,6 +630,7 @@ app.registerExtension({
case "easy detailerFix":
case "easy imageRemBg":
case "easy imageColorMatch":
case "easy imageDetailTransfer":
case "easy loadImageBase64":
case "easy XYInputs: Steps":
case "easy XYInputs: Sampler/Scheduler":
@@ -589,6 +642,7 @@ app.registerExtension({
case "easy rangeFloat":
case 'easy latentCompositeMaskedWithCond':
case 'easy pipeEdit':
case 'easy icLightApply':
case 'easy ipadapterApply':
case 'easy ipadapterApplyADV':
case 'easy ipadapterApplyEncoder':
@@ -806,6 +860,7 @@ app.registerExtension({
const pos = this.widgets.findIndex((w) => w.name === "spent_time");
if (pos !== -1 && this.widgets[pos]) {
const w = this.widgets[pos]
console.log(text)
w.value = text;
}
}
@@ -929,24 +984,15 @@ app.registerExtension({
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
// const values = ["randomize", "fixed", "increment", "decrement"]
// const seed_widget = this.widgets.find(w => w.name == 'seed_num')
// const seed_control = this.addWidget("combo", "control_before_generate", values[0], () => {
// }, {
// values,
// serialize: false
// })
// seed_widget.linkedWidgets = [seed_control]
const seed_widget = this.widgets.find(w => ['seed_num','seed'].includes(w.name))
const seed_control = this.widgets.find(w=> ['control_before_generate','control_after_generate'].includes(w.name))
if(nodeData.name == 'easy seed'){
this.addWidget("button", "🎲 Manual Random Seed", null, _=>{
if(seed_control.value != 'fixed'){
seed_control.value = 'fixed'
}
const randomSeedButton = this.addWidget("button", "🎲 Manual Random Seed", null, _=>{
if(seed_control.value != 'fixed') seed_control.value = 'fixed'
seed_widget.value = Math.floor(Math.random() * 1125899906842624)
app.queuePrompt(0, 1)
})
},{ serialize:false})
seed_widget.linkedWidgets = [randomSeedButton, seed_control];
}
}
const onAdded = nodeType.prototype.onAdded;
@@ -1101,7 +1147,7 @@ const getSetWidgets = ['rescale_after_model', 'rescale',
'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method',
'image_output', 'add_noise', 'info', 'sampler_name',
'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name',
'num_loras', 'mode', 'toggle', 'resolution', 'target_parameter',
'num_loras', 'num_controlnet', 'mode', 'toggle', 'resolution', 'target_parameter',
'input_count', 'replace_count', 'downscale_mode', 'range_mode','text_combine_mode', 'input_mode',
'lora_count','ckpt_count', 'conditioning_mode', 'preset', 'use_tiled', 'use_batch', 'num_embeds',
"easing_mode", "guider", "scheduler"
+34 -3
View File
@@ -1,4 +1,4 @@
import {app} from "/scripts/app.js";
import {app} from "../../../../scripts/app.js";
import {$t} from '../common/i18n.js'
import {CheckpointInfoDialog, LoraInfoDialog} from "../common/model.js";
@@ -9,6 +9,7 @@ const controlnet = ['easy controlnetLoader', 'easy controlnetLoaderADV', 'easy i
const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV', 'easy ipadapterStyleComposition', 'easy ipadapterApplyFromParams']
const positive_prompt = ['easy positive', 'easy wildcards']
const imageNode = ['easy loadImageBase64', 'LoadImage', 'LoadImageMask']
const brushnet = ['easy applyBrushNet', 'easy applyPowerPaint']
const widgetMapping = {
"positive_prompt":{
"text": "positive",
@@ -64,6 +65,12 @@ const widgetMapping = {
"image":"image",
"base64_data":"base64_data",
"channel": "channel"
},
"brushnet":{
"dtype": "dtype",
"scale": "scale",
"start_at": "start_at",
"end_at": "end_at"
}
}
const inputMapping = {
@@ -99,6 +106,11 @@ const inputMapping = {
"image_style": "image",
"attn_mask":"attn_mask",
"optional_ipadapter":"optional_ipadapter"
},
"brushnet":{
"pipe": "pipe",
"image": "image",
"mask": "mask"
}
};
@@ -138,6 +150,9 @@ const outputMapping = {
"masks":"masks",
"ipadapter":"ipadapter"
},
"brushnet":{
"pipe": "pipe",
}
};
// 替换节点
@@ -273,11 +288,11 @@ const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => {
has_submenu: has_submenu,
callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include)
})
if(type == 'loaders'){
if(type == 'loaders') {
options.unshift({
content: $t("💎 View Lora Info..."),
callback: (value, options, e, menu, node) => {
const widget = node.widgets.find(cate=> cate.name == 'lora_name')
const widget = node.widgets.find(cate => cate.name == 'lora_name')
let name = widget.value;
if (!name || name == 'None') return
new LoraInfoDialog(name).show('loras', name);
@@ -522,7 +537,19 @@ app.registerExtension({
}
}
})
// ckptNames
if(nodeData.name == 'easy ckptNames'){
options.unshift({
content: $t("💎 View Checkpoint Info..."),
callback: (value, options, e, menu, node) => {
let name = node.widgets[0].value;
if (!name || name == 'None') return
new CheckpointInfoDialog(name).show('checkpoints', name);
}
})
}
})
// Swap提示词
if (positive_prompt.includes(nodeData.name)) {
addMenu("↪️ Swap EasyPrompt", 'positive_prompt', positive_prompt, nodeType)
@@ -551,6 +578,10 @@ app.registerExtension({
if (imageNode.includes(nodeData.name)) {
addMenu("↪️ Swap LoadImage", 'load_image', imageNode, nodeType)
}
// Swap Brushnet
if (brushnet.includes(nodeData.name)) {
addMenu("↪️ Swap BrushNet", 'brushnet', brushnet, nodeType)
}
}
});
+8 -4
View File
@@ -1,17 +1,18 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { $el } from "/scripts/ui.js";
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el } from "../../../../scripts/ui.js";
import {addPreconnect, addCss} from "../common/utils.js";
const locale = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US'
const customThemeColor = "#3f3eed"
const customThemeColorLight = "#006691"
const customThemeColorLight = "#008ecb"
// 增加Slot颜色
const customPipeLineLink = "#7737AA"
const customPipeLineSDXLLink = "#7737AA"
const customIntLink = "#29699C"
const customXYPlotLink = "#74DA5D"
const customLoraStackLink = "#94dccd"
const customXYLink = "#38291f"
var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
@@ -20,6 +21,8 @@ if (!customLinkColors["PIPE_LINE_SDXL"] || !LGraphCanvas.link_type_colors["PIPE_
if (!customLinkColors["INT"] || !LGraphCanvas.link_type_colors["INT"]) {customLinkColors["INT"] = customIntLink;}
if (!customLinkColors["XYPLOT"] || !LGraphCanvas.link_type_colors["XYPLOT"]) {customLinkColors["XYPLOT"] = customXYPlotLink;}
if (!customLinkColors["X_Y"] || !LGraphCanvas.link_type_colors["X_Y"]) {customLinkColors["X_Y"] = customXYLink;}
if (!customLinkColors["LORA_STACK"] || !LGraphCanvas.link_type_colors["LORA_STACK"]) {customLinkColors["LORA_STACK"] = customLoraStackLink;}
if (!customLinkColors["CONTROL_NET_STACK"] || !LGraphCanvas.link_type_colors["CONTROL_NET_STACK"]) {customLinkColors["CONTROL_NET_STACK"] = customLoraStackLink;}
localStorage.setItem('Comfy.Settings.easyUse.customLinkColors', JSON.stringify(customLinkColors));
@@ -747,6 +750,7 @@ const NODE_COLORS = {
"easy promptReplace":"cyan",
"easy XYInputs: Seeds++ Batch": customXYLink,
"easy XYInputs: ModelMergeBlocks": customXYLink,
'easy textSwitch': "pale_blue"
}
function setNodeColors(node, theme) {
+3 -3
View File
@@ -1,7 +1,7 @@
// 1.0.2
import { app } from "/scripts/app.js";
import { GroupNodeConfig } from "/extensions/core/groupNode.js";
import { api } from "/scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { GroupNodeConfig } from "../../../../extensions/core/groupNode.js";
import { api } from "../../../../scripts/api.js";
import { $t } from "../common/i18n.js"
const nodeTemplateShortcutId = "Comfy.EasyUse.NodeTemplateShortcut"
+2 -2
View File
@@ -1,5 +1,5 @@
import { app } from "/scripts/app.js";
import { applyTextReplacements } from "/scripts/utils.js";
import { app } from "../../../../scripts/app.js";
import { applyTextReplacements } from "../../../../scripts/utils.js";
const extraNodes = ["easy imageSave", "easy fullkSampler", "easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo","easy detailerFix"]
+6 -6
View File
@@ -1,5 +1,5 @@
import {app} from "/scripts/app.js";
import {$el} from "/scripts/ui.js";
import {app} from "../../../../scripts/app.js";
import {$el} from "../../../../scripts/ui.js";
import {$t} from "../common/i18n.js";
import {findWidgetByName, toggleWidget} from "../common/utils.js";
@@ -63,10 +63,10 @@ app.registerExtension({
selector.element.children[0].innerHTML = ''
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 200]);
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 400]);
this.setSize([300, 500]);
}
let list = getTagList(tags[method_values]);
selector.element.children[0].append(...list)
@@ -122,10 +122,10 @@ app.registerExtension({
}
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 200]);
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 420]);
this.setSize([300, 500]);
}
},1)
+8 -7
View File
@@ -1,7 +1,7 @@
// 1.0.3
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { $el } from "/scripts/ui.js";
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el } from "../../../../scripts/ui.js";
import { $t } from "../common/i18n.js";
// 获取风格列表
@@ -92,10 +92,11 @@ async function displayImage(imgName, styleName) {
img.src = empty_img
}
}
var x = e.pageX-pxy.x-100;
var y = e.pageY-pxy.y+25;
img.style.left = x+"px";
img.style.top = y+"px";
var scale = app?.canvas?.ds?.scale || 1;
var x = (e.pageX-pxy.x-100)/scale;
var y = (e.pageY-pxy.y+25)/scale;
img.style.left = x+"px";
img.style.top = y+"px";
img.style.display = "block";
img.style.borderRadius = "10px";
img.style.borderColor = "var(--fg-color)"
+4 -4
View File
@@ -1,10 +1,10 @@
import {app} from "/scripts/app.js";
import {api} from "/scripts/api.js";
import {$el} from "/scripts/ui.js";
import {app} from "../../../../scripts/app.js";
import {api} from "../../../../scripts/api.js";
import {$el} from "../../../../scripts/ui.js";
const propmts = ["easy wildcards", "easy positive", "easy negative", "easy stylesSelector", "easy promptConcat", "easy promptReplace"]
const loaders = ["easy a1111Loader", "easy comfyLoader", "easy fullLoader", "easy svdLoader", "easy cascadeLoader", "easy sv3dLoader"]
const preSamplingNodes = ["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "preSamplingCustom", "easy preSamplingDynamicCFG","easy preSamplingSdTurbo", "easy preSamplingLayerDiffusion"]
const preSamplingNodes = ["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingCustom", "easy preSamplingDynamicCFG","easy preSamplingSdTurbo", "easy preSamplingLayerDiffusion"]
const kSampler = ["easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo"]
const controlNetNodes = ["easy controlnetLoader", "easy controlnetLoaderADV"]
const instantIDNodes = ["easy instantIDApply", "easy instantIDApplyADV"]
+2 -3
View File
@@ -1,5 +1,5 @@
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
import { app } from "../../../../scripts/app.js";
import { ComfyWidgets } from "../../../../scripts/widgets.js";
const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 };
const WIDGET_GAP = -4;
@@ -150,7 +150,6 @@ const cssCode = `
border-radius: 7px;
text-align: center;
text-wrap: balance;
text-transform: uppercase;
}
.hideInfo-dropdown {
position: absolute;
+1 -1
View File
@@ -1,4 +1,4 @@
import { app } from "/scripts/app.js";
import { app } from "../../../../scripts/app.js";
import {removeDropdown, createDropdown} from "../common/dropdown.js";
function generateNumList(dictionary) {
-1
View File
@@ -1,5 +1,4 @@
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from '/scripts/widgets.js'
// Node that allows you to tunnel connections for cleaner graphs
+119 -15
View File
@@ -1,15 +1,81 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { ComfyDialog, $el } from "../../../../scripts/ui.js";
import { restart_from_here } from "./prompt.js";
import { FlowState } from "./state.js";
import { hud, FlowState } from "./state.js";
import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./messaging.js";
import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./preview.js";
import {toggleWidget} from "../common/utils.js";
import {$t} from "../common/i18n.js";
class chooserImageDialog extends ComfyDialog {
constructor() {
super();
this.node = null
this.select_index = []
this.dialog_div = null
}
show(image,node){
this.select_index = []
this.node = node
const images_div = image.map((img, index) => {
const imgEl = $el('img', {
src: img.src,
onclick: _ => {
if(this.select_index.includes(index)){
this.select_index = this.select_index.filter(i => i !== index)
imgEl.classList.remove('selected')
} else {
this.select_index.push(index)
imgEl.classList.add('selected')
}
if (node.selected.has(index)) node.selected.delete(index);
else node.selected.add(index);
}
})
return imgEl
})
super.show($el('div.easyuse-chooser-dialog',[
$el('h5.easyuse-chooser-dialog-title', $t('Choose images to continue')),
$el('div.easyuse-chooser-dialog-images',images_div)
]))
}
createButtons() {
const btns = super.createButtons();
btns[0].onclick = _ => {
if (FlowState.running()) { send_cancel();}
super.close()
}
btns.unshift($el('button', {
type: 'button',
textContent: $t('Choose Selected Images'),
onclick: _ => {
if (FlowState.paused()) {
send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]);
}
if (FlowState.idle()) {
skip_next_restart_message();
restart_from_here(this.node.id).then(() => { send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]); });
}
super.close()
}
}))
return btns
}
}
function progressButtonPressed() {
const node = app.graph._nodes_by_id[this.node_id];
if (node) {
const selected = [...node.selected]
if(selected?.length>0){
node.setProperty('values',selected)
}
if (FlowState.paused()) {
send_message(node.id, [...node.selected, -1, ...node.anti_selected]);
}
@@ -20,7 +86,22 @@ function progressButtonPressed() {
}
}
function cancelButtonPressed() { if (FlowState.running()) { send_cancel(); } }
function cancelButtonPressed() {
if (FlowState.running()) { send_cancel();}
}
function enable_disabling(button) {
Object.defineProperty(button, 'clicked', {
get : function() { return this._clicked; },
set : function(v) { this._clicked = (v && this.name!=''); }
})
}
function disable_serialize(widget) {
if (!widget.options) widget.options = { };
widget.options.serialize = false;
}
app.registerExtension({
name:'comfy.easyuse.imageChooser',
@@ -28,8 +109,22 @@ app.registerExtension({
window.addEventListener("beforeunload", send_cancel, true);
},
setup(app) {
const draw = LGraphCanvas.prototype.draw;
LGraphCanvas.prototype.draw = function() {
if (hud.update()) {
app.graph._nodes.forEach((node)=> { if (node.update) { node.update(); } })
}
draw.apply(this,arguments);
}
function easyuseImageChooser(event) {
display_preview_images(event);
const {node,image,isKSampler} = display_preview_images(event);
if(isKSampler) {
const dialog = new chooserImageDialog();
dialog.show(image,node)
}
}
api.addEventListener("easyuse-image-choose", easyuseImageChooser);
@@ -60,11 +155,9 @@ app.registerExtension({
},
async nodeCreated(node, app) {
if(node.comfyClass == 'easy imageChooser'){
node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed, {serialize: false});
node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed, {serialize: false});
/* Capture clicks */
const org_onMouseDown = node.onMouseDown;
node.setProperty('values',[])
/* A property defining the top of the image when there is just one */
if(node?.imageIndex === undefined){
@@ -80,6 +173,8 @@ app.registerExtension({
})
}
/* Capture clicks */
const org_onMouseDown = node.onMouseDown;
node.onMouseDown = function( e, pos, canvas ) {
if (e.isPrimary) {
const i = click_is_in_image(node, pos);
@@ -88,6 +183,13 @@ app.registerExtension({
return (org_onMouseDown && org_onMouseDown.apply(this, arguments));
}
node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed);
node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed);
enable_disabling(node.cancel_button_widget);
enable_disabling(node.send_button_widget);
disable_serialize(node.cancel_button_widget);
disable_serialize(node.send_button_widget);
}
},
@@ -101,9 +203,11 @@ app.registerExtension({
}
nodeType.prototype.imageClicked = function (imageIndex) {
if (this.selected.has(imageIndex)) this.selected.delete(imageIndex);
else this.selected.add(imageIndex);
this.update();
if (nodeType?.comfyClass==="easy imageChooser") {
if (this.selected.has(imageIndex)) this.selected.delete(imageIndex);
else this.selected.add(imageIndex);
this.update();
}
}
const update = nodeType.prototype.update;
@@ -112,10 +216,10 @@ app.registerExtension({
if (this.send_button_widget) {
this.send_button_widget.node_id = this.id;
const selection = ( this.selected ? this.selected.size : 0 ) + ( this.anti_selected ? this.anti_selected.size : 0 )
const maxlength = this.imgs.length;
const maxlength = this.imgs?.length || 0;
if (FlowState.paused_here(this.id) && selection>0) {
this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image";
} else if (FlowState.idle() && selection>0) {
} else if (selection>0) {
this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image as restart";
}
else {
+4 -4
View File
@@ -1,4 +1,4 @@
import { api } from "/scripts/api.js";
import { api } from "../../../../scripts/api.js";
import { FlowState } from "./state.js";
function send_message_from_pausing_node(message) {
@@ -15,9 +15,9 @@ function send_message(id, message) {
function send_cancel() {
send_message(-1,'__cancel__');
//FlowState.cancelling = true;
//api.interrupt();
//FlowState.cancelling = false;
FlowState.cancelling = true;
api.interrupt();
FlowState.cancelling = false;
}
var skip_next = 0;
+6 -2
View File
@@ -1,11 +1,14 @@
import { app } from "/scripts/app.js";
import { app } from "../../../../scripts/app.js";
const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy fullkSampler']
function display_preview_images(event) {
const node = app.graph._nodes_by_id[event.detail.id];
if (node) {
node.selected = new Set();
node.anti_selected = new Set();
showImages(node, event.detail.urls);
const image = showImages(node, event.detail.urls);
return {node,image,isKSampler:kSampler.includes(node.type)}
} else {
console.log(`Image Chooser Preview - failed to find ${event.detail.id}`)
}
@@ -20,6 +23,7 @@ function showImages(node, urls) {
img.src = `/view?filename=${encodeURIComponent(u.filename)}&type=temp&subfolder=${app.getPreviewFormatParam()}`
})
node.setSizeForImage?.();
return node.imgs
}
function drawRect(node, s, ctx) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { app } from "/scripts/app.js";
import { app } from "../../../../scripts/app.js";
function links_with(p, node_id, down, up) {
const links_with = [];
+32 -3
View File
@@ -1,6 +1,33 @@
import { app } from "/scripts/app.js";
import { app } from "../../../../scripts/app.js";
export class FlowState {
class HUD {
constructor() {
this.current_node_id = undefined;
this.class_of_current_node = null;
this.current_node_is_chooser = false;
}
update() {
if (app.runningNodeId==this.current_node_id) return false;
this.current_node_id = app.runningNodeId;
if (this.current_node_id) {
this.class_of_current_node = app.graph?._nodes_by_id[app.runningNodeId.toString()]?.comfyClass;
this.current_node_is_chooser = this.class_of_current_node === "easy imageChooser"
} else {
this.class_of_current_node = undefined;
this.current_node_is_chooser = false;
}
return true;
}
}
const hud = new HUD();
class FlowState {
constructor(){}
static idle() {
return (!app.runningNodeId);
@@ -23,4 +50,6 @@ export class FlowState {
return "Idle";
}
static cancelling = false;
}
}
export { hud, FlowState}