Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cbc2a2a2b | ||
|
|
63811206c9 | ||
|
|
48a4b5bfc6 | ||
|
|
7980f4eef4 | ||
|
|
b19c8b07ea | ||
|
|
3f506d265c | ||
|
|
d8af918e7d | ||
|
|
33566e8474 | ||
|
|
ea0350c2dc | ||
|
|
24526623fb | ||
|
|
7715ebfd06 | ||
|
|
2ba814c131 | ||
|
|
d5ad332666 | ||
|
|
fee7e7bf73 | ||
|
|
01f17ff02b | ||
|
|
da7120219a | ||
|
|
d616d18069 | ||
|
|
dbf76f288c | ||
|
|
05124006ba | ||
|
|
4586af311c | ||
|
|
1cea58c7cf | ||
|
|
a84f7c4a58 | ||
|
|
1d9bf86560 | ||
|
|
8d352b85bc | ||
|
|
403562575c | ||
|
|
dbe2cd6569 | ||
|
|
b3b0a961c5 | ||
|
|
9dfb8b9c15 | ||
|
|
b4ea58946c | ||
|
|
513fc4b67e | ||
|
|
924a16e31c | ||
|
|
ad43f8e0bb | ||
|
|
b7e1ce8a3c | ||
|
|
299090d184 | ||
|
|
3aa7bca86b | ||
|
|
56de6f0bc9 | ||
|
|
f6b5f5c99c | ||
|
|
79b81100ff | ||
|
|
c885fd9bcf | ||
|
|
c46aaa6084 | ||
|
|
0562d4eb0e | ||
|
|
cfa56d36d7 | ||
|
|
913cfe73ae | ||
|
|
590161c560 | ||
|
|
d674313240 | ||
|
|
44258090cd | ||
|
|
1222799e8f |
@@ -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
@@ -10,4 +10,5 @@ autocomplete/**
|
||||
docs/**
|
||||
.vscode/
|
||||
.idea/
|
||||
mmb-preset.custom.txt
|
||||
mmb-preset.custom.txt
|
||||
config.yaml
|
||||
+64
-17
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
[](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
@@ -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')
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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),)
|
||||
@@ -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})
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 = ""
|
||||
@@ -1,2 +1,4 @@
|
||||
diffusers>=0.25.0
|
||||
clip_interrogator>=0.6.0
|
||||
onnxruntime
|
||||
aiohttp
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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";
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
+57
-4
@@ -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
|
||||
|
||||
@@ -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>`
|
||||
@@ -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";
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
@@ -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")) {
|
||||
|
||||
@@ -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')})
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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){
|
||||
|
||||
@@ -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'){
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
|
||||
export class FlowState {
|
||||
constructor(){}
|
||||
|
||||
Reference in New Issue
Block a user