Compare commits

..
47 Commits
Author SHA1 Message Date
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
yolain 79b81100ff fix:easy ipadapterApply some changes 2024-04-26 09:19:03 +08:00
yolain c885fd9bcf fix:easy ipadapterApply bug 2024-04-25 11:02:20 +08:00
yolain c46aaa6084 fix:incorrect string preview when refresh page 2024-04-25 00:19:37 +08:00
yolain 0562d4eb0e add:easy humanSegmentation 2024-04-24 22:47:33 +08:00
yolain cfa56d36d7 add:easy imageColorMatch 2024-04-24 17:37:48 +08:00
yolain 913cfe73ae rewrite:easy cleanGPUUsed can force cleanup of models gpu usage 2024-04-24 16:56:27 +08:00
yolain 590161c560 add:easy ipadadpterApplyFromParams 2024-04-23 14:39:38 +08:00
yolain d674313240 fix:api-key placeholder 2024-04-19 12:19:26 +08:00
yolain 44258090cd fix:deduct credit when request successful in easy stablediffusion3API 2024-04-19 11:53:49 +08:00
yolain 1222799e8f add:stableDiffusion3 API node 2024-04-19 11:20:15 +08:00
63 changed files with 4953 additions and 716 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
@@ -10,4 +10,5 @@ autocomplete/**
docs/**
.vscode/
.idea/
mmb-preset.custom.txt
mmb-preset.custom.txt
config.yaml
+64 -17
View File
@@ -28,10 +28,43 @@
- Fooocus Inpaint integration
- Integration of common logical calculations, conversion of types, display of all types, etc.
- 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.4 (2024/4/10)**
**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
- Added `easy imageColorMatch`
- Added `easy ipadapterApplyRegional`
- Added `easy ipadapterApplyFromParams`
- Added `easy imageInterrogator` - Image To Prompt
- Added `easy stableDiffusion3API` - Easy Stable Diffusion 3 Multiple accounts API Node
**v1.1.4**
- Added `easy preSamplingCustom` - Custom-PreSampling, can be supported cosXL-edit
- Added `easy ipadapterStyleComposition`
@@ -39,7 +72,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
@@ -48,7 +81,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
@@ -64,7 +97,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**
@@ -74,7 +107,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
@@ -90,15 +123,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
@@ -108,8 +144,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
@@ -117,22 +155,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
@@ -152,9 +196,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`
@@ -172,9 +217,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)
@@ -191,7 +237,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>
@@ -286,6 +332,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
+62 -46
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,27 +23,65 @@
- 可多选的风格化提示词选择器,默认是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)
## 更新日志
**v1.1.4 (2024/4/13)**
**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` - 多类分割、人像分割
- 增加 `easy imageColorMatch`
- 增加 `easy ipadapterApplyRegional`
- 增加 `easy ipadapterApplyFromParams`
- 增加 `easy imageInterrogator` - 图像反推
- 增加 `easy stableDiffusion3API` - 简易的Stable Diffusion 3 多账号API节点
**v1.1.4**
- 增加 `easy imageChooser` - 从[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker)简化的图片选择器
- 增加 `easy preSamplingCustom` - 自定义预采样,可支持cosXL-edit
- 增加 `easy ipadapterStyleComposition`
- 增加 在Loaders上右键菜单可查看 checkpoints、lora 信息
- 修复 `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模型 的加载支持
@@ -53,7 +91,7 @@
- `easy wildcards` 增加 **multiline_mode**属性
- 增加 当节点需要下载模型时,若huggingface连接超时,会切换至镜像地址下载模型
**v1.1.2 (39c5ccf)**
**v1.1.2**
- 改写 EasyUse 相关节点的部分插槽推荐节点
- 增加 **启用上下文菜单自动嵌套子目录** 设置项,默认为启用状态,可分类子目录及checkpoints、loras预览图
@@ -69,7 +107,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**
@@ -80,7 +118,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等
@@ -94,15 +132,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` 搜索框修改为不区分大小写匹配
@@ -116,13 +157,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>
@@ -297,38 +341,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
@@ -355,3 +369,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 内补节点
+3 -1
View File
@@ -1,3 +1,5 @@
__version__ = "1.1.7"
import os
import glob
import folder_paths
@@ -85,4 +87,4 @@ WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"]
print('\033[34mComfy-Easy-Use (v1.1.4): \033[92mLoaded\033[0m')
print(f'\033[34mComfy-Easy-Use v{__version__}: \033[92mLoaded\033[0m')
+33
View File
@@ -0,0 +1,33 @@
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("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)
+8 -8
View File
@@ -237,17 +237,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 +316,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:
+26 -3
View File
@@ -9,7 +9,7 @@ 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
from .libs.utils import getMetadata, cleanGPUUsedForce, get_local_filepath
try:
import aiohttp
@@ -19,8 +19,17 @@ except ImportError:
print("pip install aiohttp")
sys.exit()
@PromptServer.instance.routes.post("/easyuse/cleangpu")
def cleanGPU(request):
try:
cleanGPUUsedForce()
return web.Response(status=200)
except Exception as e:
return web.Response(status=500)
pass
@PromptServer.instance.routes.get("/easyuse/reboot")
def reboot(self):
def reboot(request):
try:
sys.stdout.close_log()
except Exception as e:
@@ -129,7 +138,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))
@@ -259,5 +268,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 = {}
+65 -11
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,24 @@ 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"
}
}
}
# layerDiffuse
LAYER_DIFFUSION_DIR = os.path.join(folder_paths.models_dir, "layer_model")
@@ -68,7 +86,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 +96,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 +107,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 +120,7 @@ LAYER_DIFFUSION = {
}
},
"Foreground": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_fg2bg.safetensors"
},
"sdxl": {
@@ -110,7 +128,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 +136,7 @@ LAYER_DIFFUSION = {
}
},
"Background": {
"sd15": {
"sd1": {
"model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_sd15_bg2fg.safetensors"
},
"sdxl": {
@@ -126,7 +144,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 +153,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 = {
@@ -159,7 +198,7 @@ IPADAPTER_MODELS = {
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15.safetensors"
},
"sdxl": {
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl.safetensors"
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl_vit-h.safetensors"
}
},
"VIT-G (medium strength)": {
@@ -167,7 +206,7 @@ IPADAPTER_MODELS = {
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_vit-G.safetensors"
},
"sdxl": {
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl_vit-h.safetensors"
"model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl.safetensors"
}
},
"PLUS (high strength)": {
@@ -260,4 +299,19 @@ DYNAMICRAFTER_MODELS = {
"dynamicrafter_unet_256 (2.98GB)": {
"model_url": "https://huggingface.co/ExponentialML/DynamiCrafterUNet/resolve/main/dynamicrafter_unet_256.safetensors"
},
}
#humanParsing
HUMANPARSING_MODELS = {
"parsing_lip": {
"model_url": "https://huggingface.co/levihsu/OOTDiffusion/resolve/main/checkpoints/humanparsing/parsing_lip.onnx",
},
}
#mediapipe
MEDIAPIPE_DIR = os.path.join(folder_paths.models_dir, "mediapipe")
MEDIAPIPE_MODELS = {
"selfie_multiclass_256x256": {
"model_url": "https://huggingface.co/yolain/selfie_multiclass_256x256/resolve/main/selfie_multiclass_256x256.tflite"
}
}
+796 -228
View File
File diff suppressed because it is too large Load Diff
+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):
View File
+156
View File
@@ -0,0 +1,156 @@
import torch
import numpy as np
import cv2
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from .simple_extractor_dataset import SimpleFolderDataset
from .transforms import transform_logits
from tqdm import tqdm
from PIL import Image
def get_palette(num_cls):
""" Returns the color map for visualizing the segmentation mask.
Args:
num_cls: Number of classes
Returns:
The color map
"""
n = num_cls
palette = [0] * (n * 3)
for j in range(0, n):
lab = j
palette[j * 3 + 0] = 0
palette[j * 3 + 1] = 0
palette[j * 3 + 2] = 0
i = 0
while lab:
palette[j * 3 + 0] |= (((lab >> 0) & 1) << (7 - i))
palette[j * 3 + 1] |= (((lab >> 1) & 1) << (7 - i))
palette[j * 3 + 2] |= (((lab >> 2) & 1) << (7 - i))
i += 1
lab >>= 3
return palette
def delete_irregular(logits_result):
parsing_result = np.argmax(logits_result, axis=2)
upper_cloth = np.where(parsing_result == 4, 255, 0)
contours, hierarchy = cv2.findContours(upper_cloth.astype(np.uint8),
cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1)
area = []
for i in range(len(contours)):
a = cv2.contourArea(contours[i], True)
area.append(abs(a))
if len(area) != 0:
top = area.index(max(area))
M = cv2.moments(contours[top])
cY = int(M["m01"] / M["m00"])
dresses = np.where(parsing_result == 7, 255, 0)
contours_dress, hierarchy_dress = cv2.findContours(dresses.astype(np.uint8),
cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1)
area_dress = []
for j in range(len(contours_dress)):
a_d = cv2.contourArea(contours_dress[j], True)
area_dress.append(abs(a_d))
if len(area_dress) != 0:
top_dress = area_dress.index(max(area_dress))
M_dress = cv2.moments(contours_dress[top_dress])
cY_dress = int(M_dress["m01"] / M_dress["m00"])
wear_type = "dresses"
if len(area) != 0:
if len(area_dress) != 0 and cY_dress > cY:
irregular_list = np.array([4, 5, 6])
logits_result[:, :, irregular_list] = -1
else:
irregular_list = np.array([5, 6, 7, 8, 9, 10, 12, 13])
logits_result[:cY, :, irregular_list] = -1
wear_type = "cloth_pant"
parsing_result = np.argmax(logits_result, axis=2)
# pad border
parsing_result = np.pad(parsing_result, pad_width=1, mode='constant', constant_values=0)
return parsing_result, wear_type
def hole_fill(img):
img_copy = img.copy()
mask = np.zeros((img.shape[0] + 2, img.shape[1] + 2), dtype=np.uint8)
cv2.floodFill(img, mask, (0, 0), 255)
img_inverse = cv2.bitwise_not(img)
dst = cv2.bitwise_or(img_copy, img_inverse)
return dst
def refine_mask(mask):
contours, hierarchy = cv2.findContours(mask.astype(np.uint8),
cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1)
area = []
for j in range(len(contours)):
a_d = cv2.contourArea(contours[j], True)
area.append(abs(a_d))
refine_mask = np.zeros_like(mask).astype(np.uint8)
if len(area) != 0:
i = area.index(max(area))
cv2.drawContours(refine_mask, contours, i, color=255, thickness=-1)
# keep large area in skin case
for j in range(len(area)):
if j != i and area[i] > 2000:
cv2.drawContours(refine_mask, contours, j, color=255, thickness=-1)
return refine_mask
def refine_hole(parsing_result_filled, parsing_result, arm_mask):
filled_hole = cv2.bitwise_and(np.where(parsing_result_filled == 4, 255, 0),
np.where(parsing_result != 4, 255, 0)) - arm_mask * 255
contours, hierarchy = cv2.findContours(filled_hole, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1)
refine_hole_mask = np.zeros_like(parsing_result).astype(np.uint8)
for i in range(len(contours)):
a = cv2.contourArea(contours[i], True)
# keep hole > 2000 pixels
if abs(a) > 2000:
cv2.drawContours(refine_hole_mask, contours, i, color=255, thickness=-1)
return refine_hole_mask + arm_mask
def onnx_inference(lip_session, input_dir, mask_components=[0]):
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.406, 0.456, 0.485], std=[0.225, 0.224, 0.229])
])
input_size = [473, 473]
dataset_lip = SimpleFolderDataset(root=input_dir, input_size=input_size, transform=transform)
dataloader_lip = DataLoader(dataset_lip)
palette = get_palette(20)
with torch.no_grad():
for _, batch in enumerate(tqdm(dataloader_lip)):
image, meta = batch
c = meta['center'].numpy()[0]
s = meta['scale'].numpy()[0]
w = meta['width'].numpy()[0]
h = meta['height'].numpy()[0]
output = lip_session.run(None, {"input.1": image.numpy().astype(np.float32)})
upsample = torch.nn.Upsample(size=input_size, mode='bilinear', align_corners=True)
upsample_output = upsample(torch.from_numpy(output[1][0]).unsqueeze(0))
upsample_output = upsample_output.squeeze()
upsample_output = upsample_output.permute(1, 2, 0) # CHW -> HWC
logits_result_lip = transform_logits(upsample_output.data.cpu().numpy(), c, s, w, h,
input_size=input_size)
parsing_result = np.argmax(logits_result_lip, axis=2)
output_img = Image.fromarray(np.asarray(parsing_result, dtype=np.uint8))
output_img.putpalette(palette)
mask = np.isin(output_img, mask_components).astype(np.uint8)
mask_image = Image.fromarray(mask * 255)
mask_image = mask_image.convert("RGB")
mask_image = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0)
output_img = output_img.convert('RGB')
output_img = torch.from_numpy(np.array(output_img).astype(np.float32) / 255.0).unsqueeze(0)
return output_img, mask_image
+23
View File
@@ -0,0 +1,23 @@
from .parsing_api import onnx_inference
from ..libs.utils import install_package
class HumanParsing:
def __init__(self, model_path):
self.model_path = model_path
self.session = None
def __call__(self, input_image, mask_components):
if self.session is None:
install_package('onnxruntime')
import onnxruntime as ort
session_options = ort.SessionOptions()
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
# session_options.add_session_config_entry('gpu_id', str(gpu_id))
self.session = ort.InferenceSession(self.model_path, sess_options=session_options,
providers=['CPUExecutionProvider'])
parsed_image, mask = onnx_inference(self.session, input_image, mask_components)
return parsed_image, mask
@@ -0,0 +1,88 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@Author : Peike Li
@Contact : peike.li@yahoo.com
@File : dataset.py
@Time : 8/30/19 9:12 PM
@Desc : Dataset Definition
@License : This source code is licensed under the license found in the
LICENSE file in the root directory of this source tree.
"""
import os
import cv2
import numpy as np
from PIL import Image
from torch.utils import data
from .transforms import get_affine_transform
class SimpleFolderDataset(data.Dataset):
def __init__(self, root, input_size=[512, 512], transform=None):
self.root = root
self.input_size = input_size
self.transform = transform
self.aspect_ratio = input_size[1] * 1.0 / input_size[0]
self.input_size = np.asarray(input_size)
self.is_pil_image = False
if isinstance(root, Image.Image):
self.file_list = [root]
self.is_pil_image = True
elif os.path.isfile(root):
self.file_list = [os.path.basename(root)]
self.root = os.path.dirname(root)
else:
self.file_list = os.listdir(self.root)
def __len__(self):
return len(self.file_list)
def _box2cs(self, box):
x, y, w, h = box[:4]
return self._xywh2cs(x, y, w, h)
def _xywh2cs(self, x, y, w, h):
center = np.zeros((2), dtype=np.float32)
center[0] = x + w * 0.5
center[1] = y + h * 0.5
if w > self.aspect_ratio * h:
h = w * 1.0 / self.aspect_ratio
elif w < self.aspect_ratio * h:
w = h * self.aspect_ratio
scale = np.array([w, h], dtype=np.float32)
return center, scale
def __getitem__(self, index):
if self.is_pil_image:
img = np.asarray(self.file_list[index])[:, :, [2, 1, 0]]
else:
img_name = self.file_list[index]
img_path = os.path.join(self.root, img_name)
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
h, w, _ = img.shape
# Get person center and scale
person_center, s = self._box2cs([0, 0, w - 1, h - 1])
r = 0
trans = get_affine_transform(person_center, s, r, self.input_size)
input = cv2.warpAffine(
img,
trans,
(int(self.input_size[1]), int(self.input_size[0])),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0, 0, 0))
input = self.transform(input)
meta = {
'center': person_center,
'height': h,
'width': w,
'scale': s,
'rotation': r
}
return input, meta
+167
View File
@@ -0,0 +1,167 @@
# ------------------------------------------------------------------------------
# Copyright (c) Microsoft
# Licensed under the MIT License.
# Written by Bin Xiao (Bin.Xiao@microsoft.com)
# ------------------------------------------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import cv2
import torch
class BRG2Tensor_transform(object):
def __call__(self, pic):
img = torch.from_numpy(pic.transpose((2, 0, 1)))
if isinstance(img, torch.ByteTensor):
return img.float()
else:
return img
class BGR2RGB_transform(object):
def __call__(self, tensor):
return tensor[[2,1,0],:,:]
def flip_back(output_flipped, matched_parts):
'''
ouput_flipped: numpy.ndarray(batch_size, num_joints, height, width)
'''
assert output_flipped.ndim == 4,\
'output_flipped should be [batch_size, num_joints, height, width]'
output_flipped = output_flipped[:, :, :, ::-1]
for pair in matched_parts:
tmp = output_flipped[:, pair[0], :, :].copy()
output_flipped[:, pair[0], :, :] = output_flipped[:, pair[1], :, :]
output_flipped[:, pair[1], :, :] = tmp
return output_flipped
def fliplr_joints(joints, joints_vis, width, matched_parts):
"""
flip coords
"""
# Flip horizontal
joints[:, 0] = width - joints[:, 0] - 1
# Change left-right parts
for pair in matched_parts:
joints[pair[0], :], joints[pair[1], :] = \
joints[pair[1], :], joints[pair[0], :].copy()
joints_vis[pair[0], :], joints_vis[pair[1], :] = \
joints_vis[pair[1], :], joints_vis[pair[0], :].copy()
return joints*joints_vis, joints_vis
def transform_preds(coords, center, scale, input_size):
target_coords = np.zeros(coords.shape)
trans = get_affine_transform(center, scale, 0, input_size, inv=1)
for p in range(coords.shape[0]):
target_coords[p, 0:2] = affine_transform(coords[p, 0:2], trans)
return target_coords
def transform_parsing(pred, center, scale, width, height, input_size):
trans = get_affine_transform(center, scale, 0, input_size, inv=1)
target_pred = cv2.warpAffine(
pred,
trans,
(int(width), int(height)), #(int(width), int(height)),
flags=cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0))
return target_pred
def transform_logits(logits, center, scale, width, height, input_size):
trans = get_affine_transform(center, scale, 0, input_size, inv=1)
channel = logits.shape[2]
target_logits = []
for i in range(channel):
target_logit = cv2.warpAffine(
logits[:,:,i],
trans,
(int(width), int(height)), #(int(width), int(height)),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0))
target_logits.append(target_logit)
target_logits = np.stack(target_logits,axis=2)
return target_logits
def get_affine_transform(center,
scale,
rot,
output_size,
shift=np.array([0, 0], dtype=np.float32),
inv=0):
if not isinstance(scale, np.ndarray) and not isinstance(scale, list):
print(scale)
scale = np.array([scale, scale])
scale_tmp = scale
src_w = scale_tmp[0]
dst_w = output_size[1]
dst_h = output_size[0]
rot_rad = np.pi * rot / 180
src_dir = get_dir([0, src_w * -0.5], rot_rad)
dst_dir = np.array([0, (dst_w-1) * -0.5], np.float32)
src = np.zeros((3, 2), dtype=np.float32)
dst = np.zeros((3, 2), dtype=np.float32)
src[0, :] = center + scale_tmp * shift
src[1, :] = center + src_dir + scale_tmp * shift
dst[0, :] = [(dst_w-1) * 0.5, (dst_h-1) * 0.5]
dst[1, :] = np.array([(dst_w-1) * 0.5, (dst_h-1) * 0.5]) + dst_dir
src[2:, :] = get_3rd_point(src[0, :], src[1, :])
dst[2:, :] = get_3rd_point(dst[0, :], dst[1, :])
if inv:
trans = cv2.getAffineTransform(np.float32(dst), np.float32(src))
else:
trans = cv2.getAffineTransform(np.float32(src), np.float32(dst))
return trans
def affine_transform(pt, t):
new_pt = np.array([pt[0], pt[1], 1.]).T
new_pt = np.dot(t, new_pt)
return new_pt[:2]
def get_3rd_point(a, b):
direct = a - b
return b + np.array([-direct[1], direct[0]], dtype=np.float32)
def get_dir(src_point, rot_rad):
sn, cs = np.sin(rot_rad), np.cos(rot_rad)
src_result = [0, 0]
src_result[0] = src_point[0] * cs - src_point[1] * sn
src_result[1] = src_point[0] * sn + src_point[1] * cs
return src_result
def crop(img, center, scale, output_size, rot=0):
trans = get_affine_transform(center, scale, rot, output_size)
dst_img = cv2.warpAffine(img,
trans,
(int(output_size[1]), int(output_size[0])),
flags=cv2.INTER_LINEAR)
return dst_img
+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)
+731 -11
View File
@@ -1,16 +1,40 @@
from PIL import Image
from PIL import Image, ImageDraw, ImageFilter
import os
import hashlib
import folder_paths
import torch
import numpy as np
import comfy.model_management
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from server import PromptServer
from nodes import MAX_RESOLUTION
from torchvision.transforms import Resize, CenterCrop, InterpolationMode
from torchvision.transforms.functional import to_pil_image
from .log import log_node_info
from .libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds
from .libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, mask2image, blendImage
from .libs.colorfix import adain_color_fix, wavelet_color_fix
from .libs.chooser import ChooserMessage, ChooserCancelled
from nodes import PreviewImage
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:
@@ -516,10 +540,90 @@ 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
from .config import REMBG_DIR, REMBG_MODELS
from .libs.utils import get_local_filepath, easySave, install_package
class imageRemBg:
@classmethod
def INPUT_TYPES(self):
@@ -592,12 +696,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",)
@@ -620,8 +724,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] = {}
@@ -644,9 +747,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()
@@ -654,6 +767,589 @@ class imageChooser(PreviewImage):
return {"ui": {"images": images},
"result": (self.tensor_bundle(images_in, choosen),)}
class imageColorMatch(PreviewImage):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_ref": ("IMAGE",),
"image_target": ("IMAGE",),
"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"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
CATEGORY = "EasyUse/Image"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
OUTPUT_NODE = True
FUNCTION = "color_match"
def color_match(self, image_ref, image_target, method, image_output, save_prefix, prompt=None, extra_pnginfo=None):
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:
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()
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)
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": (new_images,)}
return {"ui": {"images": results},
"result": (new_images,)}
# 图像反推
from .libs.image import ci
class imageInterrogator:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"mode": (['fast','classic','best','negative'],),
"use_lowvram": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "interrogate"
CATEGORY = "EasyUse/Image"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
def interrogate(self, image, mode, use_lowvram=False):
prompt = ci.image_to_prompt(image, mode, low_vram=use_lowvram)
return {"ui":{"text":prompt},"result":(prompt,)}
# 人类分割器
class humanSegmentation:
@classmethod
def INPUT_TYPES(cls):
return {
"required":{
"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",
"my_unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BBOX")
RETURN_NAMES = ("image", "mask", "bbox")
FUNCTION = "parsing"
CATEGORY = "EasyUse/Segmentation"
def get_mediapipe_image(self, image: Image):
import mediapipe as mp
# Convert image to NumPy array
numpy_image = np.asarray(image)
image_format = mp.ImageFormat.SRGB
# Convert BGR to RGB (if necessary)
if numpy_image.shape[-1] == 4:
image_format = mp.ImageFormat.SRGBA
elif numpy_image.shape[-1] == 3:
image_format = mp.ImageFormat.SRGB
numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB)
return mp.Image(image_format=image_format, data=numpy_image)
def parsing(self, image, confidence, method, crop_multi, prompt=None, my_unique_id=None):
mask_components = []
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['mask_components']:
mask_components = prompt[my_unique_id]["inputs"]['mask_components'].split(',')
mask_components = list(map(int, mask_components))
if method == 'selfie_multiclass_256x256':
try:
import mediapipe as mp
except:
install_package("mediapipe")
import mediapipe as mp
from functools import reduce
model_path = get_local_filepath(MEDIAPIPE_MODELS['selfie_multiclass_256x256']['model_url'], MEDIAPIPE_DIR)
model_asset_buffer = None
with open(model_path, "rb") as f:
model_asset_buffer = f.read()
image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=model_asset_buffer)
options = mp.tasks.vision.ImageSegmenterOptions(
base_options=image_segmenter_base_options,
running_mode=mp.tasks.vision.RunningMode.IMAGE,
output_category_mask=True)
# Create the image segmenter
ret_images = []
ret_masks = []
with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter:
for img in image:
_image = torch.unsqueeze(img, 0)
orig_image = tensor2pil(_image).convert('RGB')
# Convert the Tensor to a PIL image
i = 255. * img.cpu().numpy()
image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# create our foreground and background arrays for storing the mask results
mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8)
mask_background_array[:] = (0, 0, 0, 255)
mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8)
mask_foreground_array[:] = (255, 255, 255, 255)
# Retrieve the masks for the segmented image
media_pipe_image = self.get_mediapipe_image(image=image_pil)
segmented_masks = segmenter.segment(media_pipe_image)
masks = []
for i, com in enumerate(mask_components):
masks.append(segmented_masks.confidence_masks[com])
image_data = media_pipe_image.numpy_view()
image_shape = image_data.shape
# convert the image shape from "rgb" to "rgba" aka add the alpha channel
if image_shape[-1] == 3:
image_shape = (image_shape[0], image_shape[1], 4)
mask_background_array = np.zeros(image_shape, dtype=np.uint8)
mask_background_array[:] = (0, 0, 0, 255)
mask_foreground_array = np.zeros(image_shape, dtype=np.uint8)
mask_foreground_array[:] = (255, 255, 255, 255)
mask_arrays = []
if len(masks) == 0:
mask_arrays.append(mask_background_array)
else:
for i, mask in enumerate(masks):
condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence
mask_array = np.where(condition, mask_foreground_array, mask_background_array)
mask_arrays.append(mask_array)
# Merge our masks taking the maximum from each
merged_mask_arrays = reduce(np.maximum, mask_arrays)
# Create the image
mask_image = Image.fromarray(merged_mask_arrays)
# convert PIL image to tensor image
tensor_mask = mask_image.convert("RGB")
tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0
tensor_mask = torch.from_numpy(tensor_mask)[None,]
_mask = tensor_mask.squeeze(3)[..., 0]
_mask = tensor2pil(tensor_mask).convert('L')
ret_image = RGB2RGBA(orig_image, _mask)
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
output_image = torch.cat(ret_images, dim=0)
mask = torch.cat(ret_masks, dim=0)
elif method == "human_parsing_lip":
from .human_parsing.run_parsing import HumanParsing
onnx_path = os.path.join(folder_paths.models_dir, 'onnx')
model_path = get_local_filepath(HUMANPARSING_MODELS['parsing_lip']['model_url'], onnx_path)
parsing = HumanParsing(model_path=model_path)
model_image = image.squeeze(0)
model_image = model_image.permute((2, 0, 1))
model_image = to_pil_image(model_image)
map_image, mask = parsing(model_image, mask_components)
mask = mask[:, :, :, 0]
alpha = 1.0 - mask
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
# 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
class loadImageBase64:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base64_data": ("STRING", {"default": ""}),
"image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("IMAGE", "MASK")
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "EasyUse/Image/LoadImage"
def convert_color(self, image,):
if len(image.shape) > 2 and image.shape[2] >= 4:
return cv2.cvtColor(image, cv2.COLOR_BGRA2RGB)
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
def load_image(self, base64_data, image_output, save_prefix, prompt=None, extra_pnginfo=None):
nparr = np.frombuffer(base64.b64decode(base64_data), np.uint8)
result = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
channels = cv2.split(result)
if len(channels) > 3:
mask = channels[3].astype(np.float32) / 255.0
mask = torch.from_numpy(mask)
else:
mask = torch.ones(channels[0].shape, dtype=torch.float32, device="cpu")
result = self.convert_color(result)
result = result.astype(np.float32) / 255.0
new_images = torch.from_numpy(result)[None,]
results = easySave(new_images, save_prefix, image_output, None, None)
mask = mask.unsqueeze(0)
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": (new_images, mask)}
return {"ui": {"images": results},
"result": (new_images, mask)}
class imageToBase64:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "to_base64"
CATEGORY = "EasyUse/Image"
OUTPUT_NODE = True
def to_base64(self, image, ):
import base64
from io import BytesIO
# 将张量图像转换为PIL图像
pil_image = tensor2pil(image)
buffered = BytesIO()
pil_image.save(buffered, format="JPEG")
image_bytes = buffered.getvalue()
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
@@ -698,6 +1394,7 @@ class poseEditor:
NODE_CLASS_MAPPINGS = {
"easy imageInsetCrop": imageInsetCrop,
"easy imageCount": imageCount,
"easy imageSize": imageSize,
"easy imageSizeBySide": imageSizeBySide,
"easy imageSizeByLongerSide": imageSizeByLongerSide,
@@ -707,16 +1404,28 @@ 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 imageInterrogator": imageInterrogator,
"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)",
@@ -727,10 +1436,21 @@ 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 imageInterrogator": "Image To Prompt",
"easy joinImageBatch": "JoinImageBatch",
"easy poseEditor": "PoseEditor"
"easy loadImageBase64": "Load Image (Base64)",
"easy imageToBase64": "Image To Base64",
"easy humanSegmentation": "Human Segmentation",
"easy removeLocalImage": "Remove Local Image",
"easy poseEditor": "PoseEditor",
}
+7 -7
View File
@@ -58,15 +58,15 @@ class LayerDiffuse:
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 +77,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 +97,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 +166,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:
+5 -6
View File
@@ -7,18 +7,17 @@ import comfy.model_management
from comfy.model_patcher import ModelPatcher
from tqdm import tqdm
from typing import Optional, Tuple
from ..libs.utils import install_package
from packaging import version
try:
install_package("diffusers", "0.27.2", True, "0.25.0")
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.models.modeling_utils import ModelMixin
from diffusers import __version__
if __version__:
try:
diffusers_version = float(__version__.replace('.', '').replace('dev','.'))
except ValueError:
diffusers_version = 270
if diffusers_version < 270:
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
+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
+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):
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)
if mask is not None:
mask = mask.to(self.device)
+159
View File
@@ -1,7 +1,14 @@
import os
import base64
import torch
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
# PIL to Tensor
def pil2tensor(image):
@@ -9,6 +16,28 @@ 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()
pil_image.save(byte_arr, format=format)
byte_arr.seek(0)
return byte_arr
def image2base64(image_base64):
image_bytes = base64.b64decode(image_base64)
image_data = Image.open(BytesIO(image_bytes))
return image_data
# Get new bounds
def get_new_bounds(width, height, left, right, top, bottom):
@@ -19,6 +48,64 @@ def get_new_bounds(width, height, left, right, top, bottom):
bottom = height - bottom
return (left, right, top, bottom)
def RGB2RGBA(image: Image, mask: Image) -> Image:
(R, G, B) = image.convert('RGB').split()
return Image.merge('RGBA', (R, G, B, mask.convert('L')))
def image2mask(image: Image) -> torch.Tensor:
_image = image.convert('RGBA')
alpha = _image.split()[0]
bg = Image.new("L", _image.size)
_image = Image.merge('RGBA', (bg, bg, bg, alpha))
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"
@@ -33,3 +120,75 @@ class ResizeMode(Enum):
return 2
assert False, "NOTREACHED"
# CLIP反推
import comfy.utils
from torchvision import transforms
Config, Interrogator = None, None
class CI_Inference:
ci_model = None
cache_path: str
def __init__(self):
self.ci_model = None
self.low_vram = False
self.cache_path = os.path.join(folder_paths.models_dir, "clip_interrogator")
def _load_model(self, model_name, low_vram=False):
if not (self.ci_model and model_name == self.ci_model.config.clip_model_name and self.low_vram == low_vram):
self.low_vram = low_vram
print(f"Load model: {model_name}")
config = Config(
device="cuda" if torch.cuda.is_available() else "cpu",
download_cache=True,
clip_model_name=model_name,
clip_model_path=self.cache_path,
cache_path=self.cache_path,
caption_model_name='blip-large'
)
if low_vram:
config.apply_low_vram_defaults()
self.ci_model = Interrogator(config)
def _interrogate(self, image, mode, caption=None):
if mode == 'best':
prompt = self.ci_model.interrogate(image, caption=caption)
elif mode == 'classic':
prompt = self.ci_model.interrogate_classic(image, caption=caption)
elif mode == 'fast':
prompt = self.ci_model.interrogate_fast(image, caption=caption)
elif mode == 'negative':
prompt = self.ci_model.interrogate_negative(image)
else:
raise Exception(f"Unknown mode {mode}")
return prompt
def image_to_prompt(self, image, mode, model_name='ViT-L-14/openai', low_vram=False):
try:
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))
self._load_model(model_name, low_vram)
prompt = []
for i in range(len(image)):
im = image[i]
im = tensor2pil(im)
im = im.convert('RGB')
_prompt = self._interrogate(im, mode)
pbar.update(1)
prompt.append(_prompt)
return prompt
ci = CI_Inference()
+52 -9
View File
@@ -1,16 +1,19 @@
import time, os, psutil
import comfy.utils
import comfy.sd
import comfy.controlnet
import folder_paths
from nodes import NODE_CLASS_MAPPINGS
from collections import defaultdict
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,8 +25,9 @@ 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.memory_threshold = self.determine_memory_threshold(0.9)
self.lora_name_cache = []
def clean_values(self, values: str):
@@ -50,9 +54,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 +83,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 +94,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 +111,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 +128,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 +139,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 +179,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,6 +249,25 @@ class easyLoader:
return model
def load_controlnet(self, control_net_name, scale_soft_weights=1):
unique_id = f'{control_net_name};{str(scale_soft_weights)}'
if 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)
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'):
if type == 'stable_diffusion':
clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION
+92 -2
View File
@@ -5,7 +5,7 @@ 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] = {
@@ -47,6 +47,14 @@ class easySampler:
parts.append('None')
return parts
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):
@@ -91,6 +99,26 @@ class easySampler:
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,
@@ -129,8 +157,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_model' 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)
@@ -220,4 +271,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),)
+201
View File
@@ -0,0 +1,201 @@
import json
import os
import yaml
import requests
import pathlib
from aiohttp import web
from server import PromptServer
from .image import tensor2pil, pil2tensor, image2base64, pil2byte
from ..log import log_node_error
root_path = pathlib.Path(__file__).parent.parent.parent
config_path = os.path.join(root_path,'config.yaml')
default_key = [{'name':'Default', 'key':''}]
class StabilityAPI:
def __init__(self):
self.api_url = "https://api.stability.ai"
self.api_keys = None
self.api_current = 0
self.user_info = {}
self.getAPIKeys()
def getErrors(self, code):
errors = {
400: "Bad Request",
403: "ApiKey Forbidden",
413: "Your request was larger than 10MiB.",
429: "You have made more than 150 requests in 10 seconds.",
500: "Internal Server Error",
}
return errors.get(code, "Unknown Error")
def getAPIKeys(self):
if os.path.isfile(config_path):
with open(config_path, 'r') as f:
data = yaml.load(f, Loader=yaml.FullLoader)
if not data:
data = {'STABILITY_API_KEY': default_key, 'STABILITY_API_DEFAULT':0}
with open(config_path, 'w') as f:
yaml.dump(data, f)
if 'STABILITY_API_KEY' not in data:
data['STABILITY_API_KEY'] = default_key
data['STABILITY_API_DEFAULT'] = 0
with open(config_path, 'w') as f:
yaml.dump(data, f)
api_keys = data['STABILITY_API_KEY']
self.api_current = data['STABILITY_API_DEFAULT']
self.api_keys = api_keys
return api_keys
else:
# create a yaml file
with open(config_path, 'w') as f:
data = {'STABILITY_API_KEY': default_key, 'STABILITY_API_DEFAULT':0}
yaml.dump(data, f)
return data['STABILITY_API_KEY']
pass
def setAPIKeys(self, api_keys):
if len(api_keys) > 0:
self.api_keys = api_keys
# load and save the yaml file
with open(config_path, 'r') as f:
data = yaml.load(f, Loader=yaml.FullLoader)
data['STABILITY_API_KEY'] = api_keys
with open(config_path, 'w') as f:
yaml.dump(data, f)
return True
def setAPIDefault(self, current):
if current is not None:
self.api_current = current
# load and save the yaml file
with open(config_path, 'r') as f:
data = yaml.load(f, Loader=yaml.FullLoader)
data['STABILITY_API_DEFAULT'] = current
with open(config_path, 'w') as f:
yaml.dump(data, f)
return True
def generate_sd3_image(self, prompt, negative_prompt, aspect_ratio, model, seed, mode='text-to-image', image=None, strength=1, output_format='png', node_name='easy stableDiffusion3API'):
url = f"{self.api_url}/v2beta/stable-image/generate/sd3"
api_key = self.api_keys[self.api_current]['key']
files = None
data = {
"prompt": prompt,
"mode": mode,
"model": model,
"seed": seed,
"output_format": output_format,
}
if model == 'sd3':
data['negative_prompt'] = negative_prompt
if mode == 'text-to-image':
files = {"none": ''}
data['aspect_ratio'] = aspect_ratio
elif mode == 'image-to-image':
pil_image = tensor2pil(image)
image_byte = pil2byte(pil_image)
files = {"image": ("output.png", image_byte, 'image/png')}
data['strength'] = strength
response = requests.post(url,
headers={"authorization": f"{api_key}", "accept": "application/json"},
files=files,
data=data,
)
if response.status_code == 200:
PromptServer.instance.send_sync('stable-diffusion-api-generate-succeed',{"model":model})
json_data = response.json()
image_base64 = json_data['image']
image_data = image2base64(image_base64)
output_t = pil2tensor(image_data)
return output_t
else:
if 'application/json' in response.headers['Content-Type']:
error_info = response.json()
log_node_error(node_name, error_info.get('name', 'No name provided'))
log_node_error(node_name, error_info.get('errors', ['No details provided']))
error_status_text = self.getErrors(response.status_code)
PromptServer.instance.send_sync('easyuse-toast',{"type": "error", "content": error_status_text})
raise Exception(f"Failed to generate image: {error_status_text}")
# get user account
async def getUserAccount(self, cache=True):
url = f"{self.api_url}/v1/user/account"
api_key = self.api_keys[self.api_current]['key']
name = self.api_keys[self.api_current]['name']
if cache and name in self.user_info:
return self.user_info[name]
else:
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
if response.status_code == 200:
user_info = response.json()
self.user_info[name] = user_info
return user_info
else:
PromptServer.instance.send_sync('easyuse-toast',{'type': 'error', 'content': self.getErrors(response.status_code)})
return None
# get user balance
async def getUserBalance(self):
url = f"{self.api_url}/v1/user/balance"
api_key = self.api_keys[self.api_current]['key']
response = requests.get(url, headers={
"Authorization": f"Bearer {api_key}"
})
if response.status_code == 200:
return response.json()
else:
PromptServer.instance.send_sync('easyuse-toast', {'type': 'error', 'content': self.getErrors(response.status_code)})
return None
stableAPI = StabilityAPI()
@PromptServer.instance.routes.get("/easyuse/stability/api_keys")
async def get_stability_api_keys(request):
stableAPI.getAPIKeys()
return web.json_response({"keys": stableAPI.api_keys, "current": stableAPI.api_current})
@PromptServer.instance.routes.post("/easyuse/stability/set_api_keys")
async def set_stability_api_keys(request):
post = await request.post()
api_keys = post.get("api_keys")
current = post.get('current')
if api_keys is not None:
api_keys = json.loads(api_keys)
stableAPI.setAPIKeys(api_keys)
if current is not None:
print(current)
stableAPI.setAPIDefault(int(current))
account = await stableAPI.getUserAccount()
balance = await stableAPI.getUserBalance()
return web.json_response({'account': account, 'balance': balance})
else:
return web.json_response({'status': 'ok'})
else:
return web.Response(status=400)
@PromptServer.instance.routes.post("/easyuse/stability/set_apikey_default")
async def set_stability_api_default(request):
post = await request.post()
current = post.get("current")
if current is not None and current < len(stableAPI.api_keys):
stableAPI.api_current = current
return web.json_response({'status': 'ok'})
else:
return web.Response(status=400)
@PromptServer.instance.routes.get("/easyuse/stability/user_info")
async def get_account_info(request):
account = await stableAPI.getUserAccount()
balance = await stableAPI.getUserBalance()
return web.json_response({'account': account, 'balance': balance})
@PromptServer.instance.routes.get("/easyuse/stability/balance")
async def get_balance_info(request):
balance = await stableAPI.getUserBalance()
return web.json_response({'balance': balance})
+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
+60 -14
View File
@@ -17,6 +17,53 @@ def get_comfyui_revision():
comfy_ui_revision = "Unknown"
return comfy_ui_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):
try:
module = importlib.util.find_spec(package)
return module is not None
except ImportError as e:
print(e)
return False
def install_package(package, v=None, compare=True, compare_version=None):
run_install = True
if is_package_installed(package):
try:
installed_version = importlib.metadata.version(package)
if v is not None:
if compare_version is None:
compare_version = v
if not compare or version.parse(installed_version) >= version.parse(compare_version):
run_install = False
else:
run_install = False
except:
run_install = False
if run_install:
import subprocess
package_command = package + '==' + v if v is not None else package
PromptServer.instance.send_sync("easyuse-toast", {'content': f"Installing {package_command}...", 'duration': 5000})
result = subprocess.run([sys.executable, '-s', '-m', 'pip', 'install', package_command], capture_output=True, text=True)
if result.returncode == 0:
PromptServer.instance.send_sync("easyuse-toast", {'content': f"{package} installed successfully", 'type': 'success', 'duration': 5000})
print(f"Package {package} installed successfully")
return True
else:
PromptServer.instance.send_sync("easyuse-toast", {'content': f"{package} installed failed", 'type': 'error', 'duration': 5000})
print(f"Package {package} installed failed")
return False
else:
return False
def compare_revision(num):
global comfy_ui_revision
if not comfy_ui_revision:
@@ -36,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
@@ -58,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'
@@ -158,7 +199,7 @@ def get_local_filepath(url, dirname, local_file_name=None):
download_url_to_file(url, destination)
except Exception as err:
PromptServer.instance.send_sync("easyuse-toast",
{'content': f'无法从 {url} 下载模型'}, type='error')
{'content': f'无法从 {url} 下载模型', 'type':'error'})
raise Exception(f'无法从 {url} 下载,错误信息:{str(err.args[0])}')
return destination
@@ -186,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']
@@ -207,4 +248,9 @@ def getMetadata(filepath):
if header_size <= 0:
raise BufferError("Invalid header")
return header
return header
def cleanGPUUsedForce():
gc.collect()
mm.unload_all_models()
mm.soft_empty_cache()
+31 -5
View File
@@ -1,8 +1,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
import torch
from .libs.utils import AlwaysEqualProxy, cleanGPUUsedForce
import numpy as np
import json
@@ -278,6 +277,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 +530,7 @@ class cleanGPUUsed:
CATEGORY = "EasyUse/Logic"
def empty_cache(self, anything, unique_id=None, extra_pnginfo=None):
if torch.cuda.is_available():
torch.cuda.empty_cache()
cleanGPUUsedForce()
return ()
from .libs.cache import remove_cache
@@ -552,6 +573,9 @@ class clearCacheAll:
remove_cache('*')
return ()
NODE_CLASS_MAPPINGS = {
"easy string": String,
"easy int": Int,
@@ -561,6 +585,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,
@@ -580,6 +605,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",
+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.7"
license = "LICENSE"
dependencies = ["diffusers>=0.25.0", "clip_interrogator>=0.6.0", "onnxruntime", "aiohttp"]
[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
View File
@@ -1,2 +1,4 @@
diffusers>=0.25.0
clip_interrogator>=0.6.0
onnxruntime
aiohttp
+93
View File
@@ -0,0 +1,93 @@
.easyuse-account{
}
.easyuse-account-user{
font-size: 10px;
color:var(--descrip-text);
text-align: center;
}
.easyuse-account-user-info{
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom:10px;
cursor: pointer;
}
.easyuse-account-user-info .user{
display: flex;
align-items: center;
}
.easyuse-account-user-info .edit{
padding:5px 10px;
background: var(--comfy-menu-bg);
border-radius:4px;
}
.easyuse-account-user-info:hover{
filter:brightness(110%);
}
.easyuse-account-user-info h5{
margin:0;
font-size: 10px;
text-align: left;
}
.easyuse-account-user-info h6{
margin:0;
font-size: 8px;
text-align: left;
font-weight: 300;
}
.easyuse-account-user-info .remark{
margin-top: 4px;
}
.easyuse-account-user-info .avatar{
width: 36px;
height: 36px;
background: var(--comfy-input-bg);
border-radius: 50%;
margin-right: 5px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
overflow: hidden;
}
.easyuse-account-user-info .avatar img{
width: 100%;
height: 100%;
}
.easyuse-account-dialog{
width: 600px;
}
.easyuse-account-dialog-main a, .easyuse-account-dialog-main a:visited{
font-weight: 400;
color: var(--theme-color-light);
}
.easyuse-account-dialog-item{
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.easyuse-account-dialog-item input{
padding:5px;
margin-right:5px;
}
.easyuse-account-dialog-item input.key{
flex:1;
}
.easyuse-account-dialog-item button{
cursor: pointer;
margin-left:5px!important;
padding:5px!important;
font-size: 16px!important;
}
.easyuse-account-dialog-item button:hover{
filter:brightness(120%);
}
.easyuse-account-dialog-item button.choose {
background: var(--theme-color);
}
.easyuse-account-dialog-item button.delete{
background: var(--error-color);
}
+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%);
}
+4 -1
View File
@@ -4,4 +4,7 @@
@import "groupmap.css";
@import "contextmenu.css";
@import "modelinfo.css";
@import "toast.css";
@import "toast.css";
@import "account.css";
@import "chooser.css";
@import "toolbar.css";
+9 -2
View File
@@ -1,3 +1,6 @@
.easyuse-prompt-styles{
overflow: auto;
}
.easyuse-prompt-styles .tools{
display:flex;
justify-content:space-between;
@@ -41,9 +44,13 @@
min-height: 150px;
height: calc(100% - 40px);
overflow: auto;
// display: flex;
// flex-wrap: wrap;
/*display: flex;*/
/*flex-wrap: wrap;*/
}
.easyuse-prompt-styles-list.no-top{
height: auto;
}
.easyuse-prompt-styles-tag{
display: inline-block;
vertical-align: middle;
+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({
+57 -4
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,22 +17,30 @@ 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...": "正在保存预览图...",
"Saving Succeed":"保存成功",
"Clean SuccessFully":"清理成功",
"Clean Failed": "清理失败",
"Saving Failed":"保存失败",
"No COMBO link": "沒有找到COMBO连接",
"Reboot ComfyUI":"重启ComfyUI",
"Are you sure you'd like to reboot the server?": "是否要重启ComfyUI?",
// GroupMap
"Groups Map (EasyUse)": "管理组 (EasyUse)",
"Reboot ComfyUI (EasyUse)": "重启服务 (EasyUse)",
"Groups Map": "管理组",
"Cleanup Of GPU Usage": "清理GPU占用",
"Please stop all running tasks before cleaning GPU": "请在清理GPU之前停止所有运行中的任务",
"Always": "启用中",
"Bypass": "已忽略",
"Never": "已停用",
@@ -39,9 +50,51 @@ 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)",
"Too many thumbnails, have closed the display": "模型缩略图太多啦,为您关闭了显示"
"Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)": "启用工具栏固定在左下角 (ComfyUI-Easy-Use)",
"Too many thumbnails, have closed the display": "模型缩略图太多啦,为您关闭了显示",
// selector
"Empty All": "清空所有",
"🔎 Type here to search styles ...": "🔎 在此处输入以搜索样式 ...",
// account
"Loading UserInfo...": "正在获取用户信息...",
"Please set the APIKEY first": "请先设置APIKEY",
"Setting APIKEY": "设置APIKEY",
"Save Account Info": "保存账号信息",
"Choose": "选择",
"Delete": "删除",
"Edit": "编辑",
"At least one account is required": "删除失败: 至少需要一个账户",
"APIKEY is not Empty": "APIKEY 不能为空",
"Add Account": "添加账号",
"Getting Your APIKEY": "获取您的APIKEY",
// choosers
"Choose Selected Images": "选择选中的图片",
"Choose images to continue": "选择图片以继续",
// seg
"Background": "背景",
"Hat": "帽子",
"Hair": "头发",
"Body": "身体",
"Face": "脸部",
"Clothes": "衣服",
"Others": "其他",
"Glove": "手套",
"Sunglasses": "太阳镜",
"Upper-clothes": "上衣",
"Dress": "连衣裙",
"Coat": "外套",
"Socks": "袜子",
"Pants": "裤子",
"Jumpsuits": "连体衣",
"Scarf": "围巾",
"Skirt": "裙子",
"Left-arm": "左臂",
"Right-arm": "右臂",
"Left-leg": "左腿",
"Right-leg": "右腿",
"Left-shoe": "左鞋",
"Right-shoe": "右鞋",
}
export const $t = (key) => {
const cn = zhCN[key]
return locale === 'zh-CN' && cn ? cn : key
+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`,
+67
View File
@@ -110,4 +110,71 @@ export function isLocalNetwork(ip) {
return range.test(ip);
}
});
}
/**
* accAdd 高精度加法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accAdd(arg1, arg2) {
let r1, r2, s1, s2,max;
s1 = typeof arg1 == 'string' ? arg1 : arg1.toString()
s2 = typeof arg2 == 'string' ? arg2 : arg2.toString()
try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 }
try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 }
max = Math.pow(10, Math.max(r1, r2))
return (arg1 * max + arg2 * max) / max
}
/**
* accSub 高精度减法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accSub(arg1, arg2) {
let r1, r2, max, min,s1,s2;
s1 = typeof arg1 == 'string' ? arg1 : arg1.toString()
s2 = typeof arg2 == 'string' ? arg2 : arg2.toString()
try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 }
try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 }
max = Math.pow(10, Math.max(r1, r2));
//动态控制精度长度
min = (r1 >= r2) ? r1 : r2;
return ((arg1 * max - arg2 * max) / max).toFixed(min)
}
/**
* accMul 高精度乘法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accMul(arg1, arg2) {
let max = 0, s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString();
try { max += s1.split(".")[1].length } catch (e) { }
try { max += s2.split(".")[1].length } catch (e) { }
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, max)
}
/**
* accDiv 高精度除法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accDiv(arg1, arg2) {
let t1 = 0, t2 = 0, r1, r2,s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString();
try { t1 = s1.toString().split(".")[1].length } catch (e) { }
try { t2 = s2.toString().split(".")[1].length } catch (e) { }
r1 = Number(s1.toString().replace(".", ""))
r2 = Number(s2.toString().replace(".", ""))
return (r1 / r2) * Math.pow(10, t2 - t1)
}
Number.prototype.div = function (arg) {
return accDiv(this, arg);
}
+477 -242
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,252 +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()
}
}
);
// Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务
if(isLocalNetwork(window.location.host)){
options.push(null,{
content: '🔴 '+ $t('Reboot ComfyUI (EasyUse)'),
},
// Force clean ComfyUI GPU Used 强制卸载模型GPU占用
{
content: rocketIcon.replace('currentColor','var(--theme-color-light)') + ' '+ $t('Cleanup Of GPU Usage') + ' (EasyUse)',
callback: async() =>{
await cleanup()
}
},
// Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务
isLocalNetwork(window.location.host) ? {
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 {
@@ -281,10 +449,77 @@ app.registerExtension({
} catch (exception) {}
}
}
})
}
} : null,
);
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 loadGraphDataEvent.apply(this, [...arguments])
}
addToolBar(app)
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name.startsWith("easy")) {
+283
View File
@@ -0,0 +1,283 @@
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";
let api_keys = []
let api_current = 0
let user_info = {}
const api_cost = {
'sd3': 6.5,
'sd3-turbo': 4,
}
class AccountDialog extends ComfyDialog {
constructor() {
super();
this.lists = []
this.dialog_div = null
this.user_div = null
}
addItem(index, user_div){
return $el('div.easyuse-account-dialog-item',[
$el('input',{type:'text',placeholder:'Enter name',oninput: e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
api_keys[dataIndex]['name'] = e.target.value
},value:api_keys[index]['name']}),
$el('input.key',{type:'text',oninput: e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
api_keys[dataIndex]['key'] = e.target.value
},placeholder:'Enter APIKEY', value:api_keys[index]['key']}),
$el('button.choose',{textContent:$t('Choose'),onclick:async(e)=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
let name = api_keys[dataIndex]['name']
let key = api_keys[dataIndex]['key']
if(!name){
toast.error($t('Please enter the account name'))
return
}
else if(!key){
toast.error($t('Please enter the APIKEY'))
return
}
let missing = true
for(let i=0;i<api_keys.length;i++){
if(!api_keys[i].key) {
missing = false
break
}
}
if(!missing){
toast.error($t('APIKEY is not Empty'))
return
}
// 保存记录
api_current = dataIndex
const body = new FormData();
body.append('api_keys', JSON.stringify(api_keys));
body.append('current',api_current)
const res = await api.fetchApi('/easyuse/stability/set_api_keys', {
method: 'POST',
body
})
if (res.status == 200) {
const data = await res.json()
if(data?.account && data?.balance){
const avatar = data.account?.profile_picture || null
const email = data.account?.email || null
const credits = data.balance?.credits || 0
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'),
$el('div.info', [
$el('h5.name', email),
$el('h6.remark','Credits: '+ credits)
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
toast.success($t('Save Succeed'))
}
else toast.success($t('Save Succeed'))
this.close()
} else {
toast.error($t('Save Failed'))
}
}}),
$el('button.delete',{textContent:$t('Delete'),onclick:e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
if(api_keys.length<=1){
toast.error($t('At least one account is required'))
return
}
api_keys.splice(dataIndex,1)
this.dialog_div.removeChild(e.target.parentNode)
}}),
])
}
show(userdiv) {
api_keys.forEach((item,index)=>{
this.lists.push(this.addItem(index,userdiv))
})
this.dialog_div = $el("div.easyuse-account-dialog", this.lists)
super.show(
$el('div.easyuse-account-dialog-main',[
$el('div',[
$el('a',{href:'https://platform.stability.ai/account/keys',target:'_blank',textContent:$t('Getting Your APIKEY')}),
]),
this.dialog_div,
])
);
}
createButtons() {
const btns = super.createButtons();
btns.unshift($el('button',{
type:'button',
textContent:$t('Save Account Info'),
onclick:_=>{
let missing = true
for(let i=0;i<api_keys.length;i++){
if(!api_keys[i].key) {
missing = false
break
}
}
if(!missing){
toast.error($t('APIKEY is not Empty'))
}
else {
const body = new FormData();
body.append('api_keys', JSON.stringify(api_keys));
api.fetchApi('/easyuse/stability/set_api_keys', {
method: 'POST',
body
}).then(res => {
if (res.status == 200) {
toast.success($t('Save Succeed'))
} else {
toast.error($t('Save Failed'))
}
})
}
}
}))
btns.unshift($el('button',{
type:'button',
textContent:$t('Add Account'),
onclick:_=>{
const name = 'Account '+(api_keys.length).toString()
api_keys.push({name,key:''})
const item = this.addItem(api_keys.length - 1)
this.lists.push(item)
this.dialog_div.appendChild(item)
}
}))
return btns
}
}
app.registerExtension({
name: 'comfy.easyUse.account',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == 'easy stableDiffusion3API'){
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function() {
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
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))
let model_widget = this.widgets.find(w => w.name == 'model')
model_widget.callback = value =>{
cost_widget.value = '-'+api_cost[value]
}
const cost_widget = this.addWidget('text', 'cost_credit', '0', _=>{
},{
serialize:false,
})
cost_widget.disabled = true
setTimeout(_=>{
if(seed_control.name == 'control_before_generate' && seed_widget.value === 0){
seed_widget.value = Math.floor(Math.random() * 4294967294)
}
cost_widget.value = '-'+api_cost[model_widget.value]
},100)
let user_div = $el('div.easyuse-account-user', [$t('Loading UserInfo...')])
let account = this.addDOMWidget('account',"btn",$el('div.easyuse-account',user_div));
// 更新balance信息
api.addEventListener('stable-diffusion-api-generate-succeed', async ({detail}) => {
let remarkDiv = user_div.querySelectorAll('.remark')
if(remarkDiv && remarkDiv[0]){
const credits = detail?.model ? api_cost[detail.model] : 0
if(credits) {
let balance = accSub(parseFloat(remarkDiv[0].innerText.replace(/Credits: /g,'')),credits)
if(balance>0){
remarkDiv[0].innerText = 'Credits: '+ balance.toString()
}
}
}
await sleep(10000)
const res = await api.fetchApi('/easyuse/stability/balance')
if(res.status == 200){
const data = await res.json()
if(data?.balance){
const credits = data.balance?.credits || 0
if(remarkDiv && remarkDiv[0]){
remarkDiv[0].innerText = 'Credits: ' + credits
}
}
}
})
// 获取api_keys
const res = await api.fetchApi('/easyuse/stability/api_keys')
if (res.status == 200){
let data = await res.json()
api_keys = data.keys
api_current = data.current
if (api_keys.length > 0 && api_current!==undefined){
const api_key = api_keys[api_current]['key']
const api_name = api_keys[api_current]['name']
if(!api_key){
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', '😀'),
$el('div.info', [
$el('h5.name', api_name),
$el('h6.remark',$t('Click to set the APIKEY first'))
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
}else{
// 获取账号信息
const res = await api.fetchApi('/easyuse/stability/user_info')
if(res.status == 200){
const data = await res.json()
if(data?.account && data?.balance){
const avatar = data.account?.profile_picture || null
const email = data.account?.email || null
const credits = data.balance?.credits || 0
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'),
$el('div.info', [
$el('h5.name', email),
$el('h6.remark','Credits: '+ credits)
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
}
}
}
}
}
}
}
}
})
+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){
+53 -28
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)
@@ -111,16 +111,31 @@ function widgetLogic(node, widget) {
updateNodeHeight(node)
}
if (widget.name === 'mode') {
let number_to_show = findWidgetByName(node, 'num_loras').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'))
} 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)}
switch (node.comfyClass) {
case 'easy loraStack':
let number_to_show = findWidgetByName(node, 'num_loras').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'))
} 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)}
}
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'))
}
break
}
updateNodeHeight(node)
}
@@ -559,6 +574,7 @@ app.registerExtension({
case "easy dynamiCrafterLoader":
case "easy loraStack":
case "easy latentNoisy":
case "easy preSampling":
case "easy preSamplingAdvanced":
case "easy preSamplingNoiseIn":
case "easy preSamplingCustom":
@@ -577,6 +593,8 @@ app.registerExtension({
case "easy hiresFix":
case "easy detailerFix":
case "easy imageRemBg":
case "easy imageColorMatch":
case "easy loadImageBase64":
case "easy XYInputs: Steps":
case "easy XYInputs: Sampler/Scheduler":
case 'easy XYInputs: Checkpoint':
@@ -587,6 +605,7 @@ app.registerExtension({
case "easy rangeFloat":
case 'easy latentCompositeMaskedWithCond':
case 'easy pipeEdit':
case 'easy icLightApply':
case 'easy ipadapterApply':
case 'easy ipadapterApplyADV':
case 'easy ipadapterApplyEncoder':
@@ -804,6 +823,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;
}
}
@@ -943,6 +963,7 @@ app.registerExtension({
seed_control.value = 'fixed'
}
seed_widget.value = Math.floor(Math.random() * 1125899906842624)
app.queuePrompt(0, 1)
})
}
}
@@ -951,9 +972,11 @@ app.registerExtension({
onAdded ? onAdded.apply(this, []) : undefined;
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(seed_control.name == 'control_before_generate' && seed_widget.value === 0){
seed_widget.value = Math.floor(Math.random() * 1125899906842624)
}
setTimeout(_=>{
if(seed_control.name == 'control_before_generate' && seed_widget.value === 0) {
seed_widget.value = Math.floor(Math.random() * 1125899906842624)
}
},1)
}
}
@@ -984,7 +1007,7 @@ app.registerExtension({
}
}
if(['easy showAnything', 'easy showTensorShape'].includes(nodeData.name)){
if(['easy showAnything', 'easy showTensorShape', 'easy imageInterrogator'].includes(nodeData.name)){
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "text");
@@ -1023,13 +1046,15 @@ app.registerExtension({
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
if(!['easy imageInterrogator'].includes(nodeData.name)) {
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
}
}
if(nodeData.name == 'easy convertAnything'){
+27 -5
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";
@@ -6,8 +6,9 @@ const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader']
const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', 'easy preSamplingNoiseIn', 'easy preSamplingCustom', 'easy preSamplingLayerDiffusion', 'easy fullkSampler']
const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy kSamplerInpainting', 'easy kSamplerDownscaleUnet', 'easy kSamplerLayerDiffusion']
const controlnet = ['easy controlnetLoader', 'easy controlnetLoaderADV', 'easy instantIDApply', 'easy instantIDApplyADV']
const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV', 'easy ipadapterStyleComposition']
const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV', 'easy ipadapterStyleComposition', 'easy ipadapterApplyFromParams']
const positive_prompt = ['easy positive', 'easy wildcards']
const imageNode = ['easy loadImageBase64', 'LoadImage', 'LoadImageMask']
const widgetMapping = {
"positive_prompt":{
"text": "positive",
@@ -58,6 +59,11 @@ const widgetMapping = {
"end_at": "end_at",
"cache_mode": "cache_mode",
"use_tiled": "use_tiled",
},
"load_image":{
"image":"image",
"base64_data":"base64_data",
"channel": "channel"
}
}
const inputMapping = {
@@ -131,7 +137,7 @@ const outputMapping = {
"tiles":"tiles",
"masks":"masks",
"ipadapter":"ipadapter"
}
},
};
// 替换节点
@@ -267,11 +273,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);
@@ -516,7 +522,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)
@@ -541,6 +559,10 @@ app.registerExtension({
if (ipadapter.includes(nodeData.name)) {
addMenu("↪️ Swap EasyIPAdapater", 'ipadapter', ipadapter, nodeType)
}
// Swap Image
if (imageNode.includes(nodeData.name)) {
addMenu("↪️ Swap LoadImage", 'load_image', imageNode, nodeType)
}
}
});
+8 -5
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));
@@ -131,7 +134,6 @@ try{
settings["AE.highlight"] = false
}
// 主题设置
console.log(theme_name)
if(!theme_name && _settings['Comfy.ColorPalette']) {
theme_name = `"${_settings['Comfy.ColorPalette']}"`
localStorage.setItem('Comfy.Settings.Comfy.ColorPalette', theme_name)
@@ -748,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"]
+136
View File
@@ -0,0 +1,136 @@
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";
const tags = {
"selfie_multiclass_256x256": ["Background", "Hair", "Body", "Face", "Clothes", "Others",],
"human_parsing_lip":["Background","Hat","Hair","Glove","Sunglasses","Upper-clothes","Dress","Coat","Socks","Pants","Jumpsuits","Scarf","Skirt","Face","Left-arm","Right-arm","Left-leg","Right-leg","Left-shoe","Right-shoe"],
}
function getTagList(tags) {
let rlist=[]
tags.forEach((k,i) => {
rlist.push($el(
"label.easyuse-prompt-styles-tag",
{
dataset: {
tag: i,
name: $t(k),
index: i
},
$: (el) => {
el.children[0].onclick = () => {
el.classList.toggle("easyuse-prompt-styles-tag-selected");
};
},
},
[
$el("input",{
type: 'checkbox',
name: i
}),
$el("span",{
textContent: $t(k),
})
]
))
});
return rlist
}
app.registerExtension({
name: 'comfy.easyUse.seg',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name == 'easy humanSegmentation') {
// 创建时
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
const method = this.widgets.findIndex((w) => w.name == 'method');
const list = $el("ul.easyuse-prompt-styles-list.no-top", []);
let method_values = ''
this.setProperty("values", [])
let selector = this.addDOMWidget('mask_components',"btn",$el('div.easyuse-prompt-styles',[list]))
Object.defineProperty(this.widgets[method],'value',{
set:(value)=>{
method_values = value
if(method_values){
selector.element.children[0].innerHTML = ''
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 500]);
}
let list = getTagList(tags[method_values]);
selector.element.children[0].append(...list)
}
},
get: () => {
return method_values
}
})
let mask_select_values = ''
Object.defineProperty(selector, "value", {
set: (value) => {
setTimeout(_=>{
selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
let arr = value.split(',')
if (arr.includes(el.dataset.tag)) {
el.classList.add("easyuse-prompt-styles-tag-selected");
el.children[0].checked = true
}
})
},100)
},
get: () => {
selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
if(el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")>=0){
if(!this.properties["values"].includes(el.dataset.tag)){
this.properties["values"].push(el.dataset.tag);
}
}else{
if(this.properties["values"].includes(el.dataset.tag)){
this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag);
}
}
});
mask_select_values = this.properties["values"].join(',');
return mask_select_values;
}
});
let old_values = ''
let mask_lists_dom = selector.element.children[0]
// 初始化
setTimeout(_=>{
if(!method_values) {
method_values = 'selfie_multiclass_256x256'
selector.element.children[0].innerHTML = ''
// 重新排序
let list = getTagList(tags[method_values]);
selector.element.children[0].append(...list)
}
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 500]);
}
},1)
return onNodeCreated;
}
}
}
})
+11 -10
View File
@@ -1,7 +1,8 @@
// 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";
// 获取风格列表
let styles_list_cache = {}
@@ -91,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)"
@@ -118,14 +120,13 @@ app.registerExtension({
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
const styles_id = this.widgets.findIndex((w) => w.name == 'styles');
const language = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US'
const list = $el("ul.easyuse-prompt-styles-list",[]);
let styles_values = ''
this.setProperty("values", [])
let selector = this.addDOMWidget('select_styles',"btn",$el('div.easyuse-prompt-styles',[$el('div.tools', [
$el('button.delete',{
textContent: language == 'zh-CN' ? '清空所有' : 'Empty All',
textContent: $t('Empty All'),
style:{},
onclick:()=>{
selector.element.children[0].querySelectorAll(".search").forEach(el=>{
@@ -145,7 +146,7 @@ app.registerExtension({
dir:"ltr",
style:{"overflow-y": "scroll"},
rows:1,
placeholder:language == 'zh-CN' ? "🔎 在此处输入以搜索样式 ..." : "🔎 Type here to search styles ...",
placeholder:$t("🔎 Type here to search styles ..."),
oninput:(e)=>{
let value = e.target.value
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
+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
+95 -6
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 { 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]);
}
@@ -17,10 +83,26 @@ function progressButtonPressed() {
skip_next_restart_message();
restart_from_here(node.id).then(() => { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); });
}
const maxlength = node.imgs.length;
if (FlowState.paused_here(node.id) && selected>0) {
node.send_button_widget.name = (selected>1) ? "Progress selected (" + selected + '/' + maxlength +")" : "Progress selected image";
} else if (FlowState.idle() && selected>0) {
node.send_button_widget.name = (selected>1) ? "Progress selected (" + selected + '/' + maxlength +")" : "Progress selected image as restart";
}
else {
node.send_button_widget.name = "";
}
}
}
function cancelButtonPressed() { if (FlowState.running()) { send_cancel(); } }
function cancelButtonPressed() {
if (FlowState.running()) { send_cancel();}
const node = app.graph._nodes_by_id[this.node_id];
if (node) {
node.send_button_widget.name = "";
node.cancel_button_widget.name = "";
}
}
app.registerExtension({
name:'comfy.easyuse.imageChooser',
@@ -29,7 +111,11 @@ app.registerExtension({
},
setup(app) {
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,9 +146,12 @@ 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});
node.setProperty('values',[])
/* Capture clicks */
const org_onMouseDown = node.onMouseDown;
@@ -112,7 +201,7 @@ 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) {
+1 -1
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) {
+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 = [];
+1 -1
View File
@@ -1,4 +1,4 @@
import { app } from "/scripts/app.js";
import { app } from "../../../../scripts/app.js";
export class FlowState {
constructor(){}