Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67afdc8204 | ||
|
|
8a0f2fc412 | ||
|
|
22145befb3 | ||
|
|
8730ffd140 | ||
|
|
32931f09a7 | ||
|
|
86873e7bda | ||
|
|
375f3b77e0 | ||
|
|
450b1ce4ce | ||
|
|
457b3a81e8 | ||
|
|
271685698b | ||
|
|
859af7e7b4 | ||
|
|
8b522f121d | ||
|
|
5b469409bc | ||
|
|
80e1261b1c | ||
|
|
005c57839c | ||
|
|
cf15032ab6 | ||
|
|
ca93381de8 | ||
|
|
58e077a743 | ||
|
|
4de1ab3b66 | ||
|
|
595e0738a9 | ||
|
|
7535cd0dfd | ||
|
|
960862223b | ||
|
|
54d080bf6a | ||
|
|
625efbfa2f | ||
|
|
5618a748c1 | ||
|
|
130c1b5796 | ||
|
|
3cf9ab4e63 | ||
|
|
ff5e3a34fc | ||
|
|
ec4ca6717f | ||
|
|
b82bb48948 | ||
|
|
d08eedabd3 | ||
|
|
8ba21d0b44 | ||
|
|
337a03bb19 | ||
|
|
aef19b8772 | ||
|
|
d60b61d575 | ||
|
|
a3f051f0c3 | ||
|
|
8ca6ace667 | ||
|
|
81c510c06e | ||
|
|
7601371923 | ||
|
|
b11c634872 | ||
|
|
7c470c67d6 | ||
|
|
b5865efd16 | ||
|
|
5ec3b5ef86 | ||
|
|
b5e31ef12a | ||
|
|
21b3c15040 | ||
|
|
5dfcbcf51d | ||
|
|
070001b36b | ||
|
|
6b4c89adc4 | ||
|
|
32ad26f0e1 | ||
|
|
d9c2072a2d | ||
|
|
e94405e610 | ||
|
|
03d5a4cf12 | ||
|
|
ad43ed3154 | ||
|
|
5cc1f8535a | ||
|
|
9f42ead9db | ||
|
|
23d9c365bd | ||
|
|
7a17ad010d | ||
|
|
3b38a5ae60 | ||
|
|
3b224fbccd | ||
|
|
00c69fc816 | ||
|
|
d39b5e13e2 | ||
|
|
e900c2ca9c | ||
|
|
9947b8be70 | ||
|
|
bc4cec287e | ||
|
|
84a4348bc5 | ||
|
|
29cca677c5 | ||
|
|
76b5896f08 | ||
|
|
be70a8671a | ||
|
|
0f6cc6958a | ||
|
|
499ed4eafc | ||
|
|
125b3a4905 | ||
|
|
a1b402b4a9 | ||
|
|
755723ce64 | ||
|
|
737feef400 | ||
|
|
8ecc929cd4 | ||
|
|
58e9d594cd | ||
|
|
b6deb5f515 | ||
|
|
1148fbe5fe | ||
|
|
5026d36489 | ||
|
|
fe0cad5981 | ||
|
|
3f37ba9491 | ||
|
|
a72a4dc40f | ||
|
|
11794f7d71 | ||
|
|
74226d8b2a | ||
|
|
f550ce83a8 | ||
|
|
34882bca10 | ||
|
|
43b94be806 | ||
|
|
0349d81694 | ||
|
|
93254a4c07 | ||
|
|
8485447325 | ||
|
|
717092a3ce | ||
|
|
14a1121860 | ||
|
|
8c1eec2858 | ||
|
|
b6bb4a3055 | ||
|
|
6873492872 | ||
|
|
2d71b3e647 | ||
|
|
e7320ec0c4 | ||
|
|
560be6aee7 | ||
|
|
54614079ca | ||
|
|
b0cd0bcb5b | ||
|
|
e46f8a45d0 |
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
@@ -18,6 +19,22 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Extract version from pyproject.toml
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep -E '^\s*version\s*=' pyproject.toml | head -1 | sed -E 's/.*version\s*=\s*"([^"]+)".*/\1/')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "ERROR: Could not extract version from pyproject.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Extracted version: $VERSION"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
generate_release_notes: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
|
||||
+4
-2
@@ -9,13 +9,15 @@ workflow/**
|
||||
autocomplete/**
|
||||
web_beta/**
|
||||
web_version/dev/**
|
||||
ComfyUI-Easy-Use-Frontend/
|
||||
docs/**
|
||||
.vscode/
|
||||
.vs/
|
||||
.idea/
|
||||
.claude/**
|
||||
mmb-preset.custom.txt
|
||||
config.yaml
|
||||
node.tar.gz
|
||||
.codex
|
||||
|
||||
.cursorrules
|
||||
tools/ComfyUI-Easy-Use.json
|
||||
tools/ComfyUI-Easy-Use.json
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "ComfyUI-Easy-Use-Frontend"]
|
||||
path = ComfyUI-Easy-Use-Frontend
|
||||
url = https://github.com/yolain/ComfyUI-Easy-Use-Frontend.git
|
||||
branch = main
|
||||
Submodule
+1
Submodule ComfyUI-Easy-Use-Frontend added at 656ae09121
+78
-1
@@ -19,7 +19,7 @@
|
||||
- 增加了预采样参数配置的节点,可与采样节点分离,更方便预览。
|
||||
- 支持通配符与Lora的提示词节点,如需使用Lora Block Weight用法,需先保证自定义节点包中安装了 [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
|
||||
- 可多选的风格化提示词选择器,默认是Fooocus的样式json,可自定义json放在styles底下,samples文件夹里可放预览图(名称和name一致,图片文件名如有空格需转为下划线'_')
|
||||
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像,需先安装 [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes)
|
||||
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像
|
||||
- 可使用`easy latentNoisy`或`easy preSamplingNoiseIn`节点实现对潜空间的噪声注入
|
||||
- 简化 SD1.x、SD2.x、SDXL、SVD、Zero123等流程
|
||||
- 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#1-13-stable-cascade)
|
||||
@@ -39,6 +39,7 @@
|
||||
- 支持 kolors 模型
|
||||
- 支持 flux 模型
|
||||
- 支持 惰性条件判断(ifElse)和 for循环
|
||||
- 支持 Anima 与 Krea2 diffusion 模型,可通过 `easy diffusionModelLoader` 加载(需显式选择文本编码器与 VAE),并使用 `easy XYInputs: DiffusionModel` 进行 XY 对比
|
||||
|
||||
## 👨🏻🔧 安装
|
||||
|
||||
@@ -52,6 +53,82 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
**v1.4.1**
|
||||
|
||||
- 修复 `easy saveText` 将文本输出限制在输出目录 #1032
|
||||
|
||||
**v1.4.0**
|
||||
|
||||
- 添加 `easy tableEditor` 节点 - 用于编辑和显示表格数据的节点
|
||||
- 修复 `easy showAnything` 在最新版 ComfyUI 前端无法工作的问题
|
||||
- 修复 `easy multiAnglePrompt` 设置保存失败的问题
|
||||
- 修复 `easy detailer` 在子图中无法工作的问题
|
||||
- 修复新版 ComfyUI 前端中"刷新节点"功能连接线丢失的问题
|
||||
- 使用原生 `VAEDecodeTiled` 进行分块解码(支持 Qwen Image VAE)
|
||||
- 修复 `easy forLoopStart` - 允许 `total=0` 以防止不必要的循环执行
|
||||
- 修复 `easy preSampling` - `samplerCustomSettings.ip2p` 中 `vae`/`pixels` 现在是可选的
|
||||
- 修复 `easy pixart` ControlNet 包装器 - 使用 `pe_interpolation` 替代已移除的 `lewei_scale`
|
||||
- 修复 `easy promptConcat` - 当输入为列表时的 TypeError 问题
|
||||
- 使用专用 RNG 进行全局种子生成
|
||||
- 修复 `easy imageDetailTransfer` - 多帧蒙版在通道广播时崩溃
|
||||
- 修复 Windows 环境下 PrimeVue 对话框遮罩未清除的问题
|
||||
- 修复 `LockedMeta` 对象 TypeError(`object of type 'LockedMeta' has no len()`)
|
||||
- 增强 `easy simpleMath` `evaluate_formula` 以处理列表输入
|
||||
- 修复 `loraStack`/`controlnetStack` - 禁用时不再清除上游堆栈
|
||||
- 修复 XYPlot 在 `Seeds++ Batch` 和元组 X/Y 输入时崩溃的问题
|
||||
- 回滚循环节点到 v1 版本
|
||||
- 修复 `easy NodesMap` - 避免递归组引用
|
||||
- 修复 `easy CleanVRAM` 清理顺序并正确清除 Easy-Use 缓存
|
||||
- 指定文件读取的 UTF-8 编码
|
||||
- 移除不必要的 print 语句
|
||||
|
||||
**v1.3.6**
|
||||
|
||||
- 恢复 `easy showAnything` 对于列表类型的支持(但一些情况下展示庞大数据时仍会导致ComfyUI崩溃)
|
||||
- 修复自定义小部件以支持子图和 Nodes 2.0 #942
|
||||
- 添加 `easy multiAngle` 节点
|
||||
- 将 `prompt.py` 转换为 V3 Schema
|
||||
- 修复 `easy humanSegmentation` 错误
|
||||
- 添加 `easy stringJoinLines`、`easy stringToIntList`、`easy simpleMath`
|
||||
- 修复 `easy ifElse` 和 `easy anythingIndexSwitch` 在某些环境下失败的问题
|
||||
|
||||
**v1.3.5**
|
||||
|
||||
- 修复`isNone`
|
||||
- 将`preview_rescale`添加到`easy imageChooser`
|
||||
- 修复小部件隐藏#910
|
||||
- 将 max 参数添加到 `wildcardsPromptMatrix` 偏移量 #909
|
||||
- 修复子图节点上的标题框样式
|
||||
- 在 `easypromptLine` 上添加 `remove_empty_lines`
|
||||
|
||||
**v1.3.4**
|
||||
|
||||
- 修复 `easy seedList` 最大值 #879
|
||||
- 为xyplot添加controlnet input #877
|
||||
- 为 `easy indexAnything` 支持 `反向索引`
|
||||
|
||||
**v1.3.3**
|
||||
|
||||
- 删除CSS类名称`gird-cols-1` #859
|
||||
- 修复锁定种子在 `easy promptAwait` 中不起作用
|
||||
- 重命名节点图
|
||||
- 修复`easy ImageChooser`输出错误类型 #845
|
||||
|
||||
**v1.3.2**
|
||||
|
||||
- 改造 `easy imageChooser` 节点以兼容 frontend>=v1.24.2, 解决方案参考自 [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
|
||||
- 改造 `easy stylesSelector` 节点, 你可在 [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) 下载到 `styles` 文件夹下
|
||||
- 改造 `easy humanSegmentation` 节点
|
||||
- 修复 `easy makeImageForICLora` 节点.
|
||||
- 添加 `easy joycaption3API` 节点
|
||||
- 添加 `easy promptAwait` 节点
|
||||
|
||||
**v1.3.1**
|
||||
|
||||
- 重写 drawNodeWidget 修复组节点预览的问题.
|
||||
- 更新了一些 XYPlot 的功能 by [mekinney](https://github.com/mekinney)
|
||||
- 添加 `easy seedList` 节点 (它对循环节点有用)
|
||||
|
||||
**v1.3.0**
|
||||
|
||||
- 将循环节点设置为最大输入和输出数量为20
|
||||
|
||||
@@ -12,29 +12,30 @@
|
||||
|
||||
**ComfyUI-Easy-Use** is an efficiency custom nodes integration package, which is extended on the basis of [TinyTerraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes). It has been integrated and optimized for many popular awesome custom nodes to achieve the purpose of faster and more convenient use of ComfyUI. While ensuring the degree of freedom, it restores the ultimate smooth image production experience that belongs to Stable Diffusion.
|
||||
|
||||
## 👨🏻🎨 Introduce
|
||||
## 👨🏻🎨 Introduction
|
||||
|
||||
- Inspire by [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes), which greatly reduces the time cost of tossing workflows。
|
||||
- Inspired by [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes), which greatly reduces the time cost of tossing workflows。
|
||||
- UI interface beautification, the first time you install the user, if you need to use the UI theme, please switch the theme in Settings -> Color Palette and refresh page.
|
||||
- Added a node for pre-sampling parameter configuration, which can be separated from the sampling node for easier previewing
|
||||
- Wildcards and lora's are supported, for Lora Block Weight usage, ensure that the custom node package has the [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
|
||||
- Multi-selectable styled cue word selector, default is Fooocus style json, custom json can be placed under styles, samples folder can be placed in the preview image (name and name consistent, image file name such as spaces need to be converted to underscores '_')
|
||||
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui, and needs to be installed [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes) first.
|
||||
- Noise injection into the latent space can be achieved using the `easy latentNoisy` or `easy preSamplingNoiseIn` node
|
||||
- Simplified processes for SD1.x, SD2.x, SDXL, SVD, Zero123, etc. [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion)
|
||||
- Simplified Stable Cascade [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade)
|
||||
- Added a node for pre-sampling parameter configuration, which can be separated from the sampling node for easier previewing.
|
||||
- Wildcards and lora's are supported, for Lora Block Weight usage, ensure that the custom node package has the [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack).
|
||||
- Multi-selectable styled cue word selector, default is Fooocus style json, custom json can be placed under styles, samples folder can be placed in the preview image (name and name consistent, image file name such as spaces need to be converted to underscores '_').
|
||||
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui.
|
||||
- Noise injection into the latent space can be achieved using the `easy latentNoisy` or `easy preSamplingNoiseIn` node.
|
||||
- Simplified processes for SD1.x, SD2.x, SDXL, SVD, Zero123, etc. [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion).
|
||||
- Simplified Stable Cascade [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade).
|
||||
- Simplified Layer Diffuse [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#LayerDiffusion),The first time you use it you may need to run `pip install -r requirements.txt` to install the required dependencies.
|
||||
- Simplified InstantID [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), You need to make sure that the custom node package has the [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID)
|
||||
- Extending the usability of XYplot
|
||||
- Fooocus Inpaint integration
|
||||
- Simplified InstantID [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), You need to make sure that the custom node package has the [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID).
|
||||
- Extending the usability of XYplot.
|
||||
- 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
|
||||
- Support SD3's model
|
||||
- Support Kolors‘s model
|
||||
- Support Flux's model
|
||||
- Support lazy if else and for loops
|
||||
- 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.
|
||||
- Support SD3's model.
|
||||
- Support Kolors‘s model.
|
||||
- Support Flux's model.
|
||||
- Support lazy if else and for loops.
|
||||
- Support Anima and Krea2 diffusion models with `easy diffusionModelLoader` and `easy XYInputs: DiffusionModel`. The loader requires an explicit text encoder and VAE selection.
|
||||
|
||||
## 👨🏻🔧 Installation
|
||||
Clone the repo into the **custom_nodes** directory and install the requirements:
|
||||
@@ -47,6 +48,76 @@ Double-click install.bat to install the required dependencies
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v1.4.1**
|
||||
|
||||
- Fix `easy saveText` to confine text outputs to output directory #1032
|
||||
|
||||
**v1.4.0**
|
||||
|
||||
- Add `easy tableEditor` node - A node for editing and displaying table data
|
||||
- Fix `easy showAnything` not working on latest ComfyUI frontend
|
||||
- Fix `easy multiAnglePrompt` settings failed to save
|
||||
- Fix `easy detailer` not working in subgraphs
|
||||
- Fix connection lines missing from "Refresh Nodes" feature in new ComfyUI frontend
|
||||
- Use native `VAEDecodeTiled` for tiled decode (Qwen Image VAE support)
|
||||
- Fix `easy forLoopStart` - allow `total=0` to prevent unnecessary loop execution
|
||||
- Fix `easy preSampling` - `vae`/`pixels` now optional on `samplerCustomSettings.ip2p`
|
||||
- Fix `easy pixart` ControlNet wrappers - use `pe_interpolation` instead of removed `lewei_scale`
|
||||
- Fix `easy promptConcat` - TypeError when an input is a list
|
||||
- Use a dedicated RNG for global seed generation
|
||||
- Fix `easy imageDetailTransfer` - multi-frame masks crash on channel broadcast
|
||||
- Fix PrimeVue dialog overlay not being cleared in Windows environment
|
||||
- Fix `LockedMeta` object TypeError (`object of type 'LockedMeta' has no len()`)
|
||||
- Enhance `easy simpleMath` `evaluate_formula` to handle list inputs
|
||||
- Fix `loraStack`/`controlnetStack` - disable no longer erases upstream stack
|
||||
- Fix XYPlot crashes in `Seeds++ Batch` and tuple X/Y inputs
|
||||
- Rollback cycle node to version v1
|
||||
- Fix `easy NodesMap` - avoid recursive group refs
|
||||
- Fix `easy CleanVRAM` teardown ordering and clear Easy-Use cache properly
|
||||
- Specify UTF-8 encoding for file reads
|
||||
- Remove unnecessary print statements
|
||||
|
||||
**v1.3.6**
|
||||
|
||||
- Restored `easy showAnything` support for list types (but displaying large data in some cases may still cause ComfyUI to crash)
|
||||
- Fix custom widgets to support subgraph and Nodes 2.0 #942
|
||||
- Add `easy multiAngle` node
|
||||
- Convert `prompt.py` to V3 Schema
|
||||
- Fix `easy humanSegmentation` error
|
||||
- Add `easy stringJoinLines`,`easy stringToIntList`, `easy simpleMath`
|
||||
- Fix `easy ifElse` and `easy anythingIndexSwitch` fails in certain environments
|
||||
|
||||
**v1.3.5**
|
||||
|
||||
- Fix `isNone`
|
||||
- Add `preview_rescale` to `easy imageChooser`
|
||||
- Fix widget hidden #910
|
||||
- Add max parameter to `wildcardsPromptMatrix` offset #909
|
||||
- Fix title box style on subgraph node
|
||||
- Add `remove_empty_lines` on `easy promptLine`
|
||||
|
||||
**v1.3.4**
|
||||
|
||||
- Fix `easy seedList` max_num #879
|
||||
- Add controlnet input to xyplot #877
|
||||
- Support `nagative indexing` for `easy indexAnything`
|
||||
|
||||
**v1.3.3**
|
||||
|
||||
- Removed the definition of the CSS class name gird-cols-1 #859
|
||||
- Fix lock seed not working in `easy promptAwait`
|
||||
- Rename the nodes map
|
||||
- Fix `easy imageChooser` output error type #845
|
||||
|
||||
**v1.3.2**
|
||||
|
||||
- Revamp `easy imageChooser` node to adapt frontend>=v1.24.2, solution referenced from [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
|
||||
- Revamp `easy stylesSelector` node, and you can download [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) to the `styles` folder
|
||||
- Revamp `easy humanSegmentation` node
|
||||
- Fix `easy makeImageForICLora` node issue, that occurred when the heights of two images were the same during image stitching on.
|
||||
- Add `easy joycaption3API` node
|
||||
- Add `easy promptAwait` node
|
||||
|
||||
**v1.3.1**
|
||||
|
||||
- Rewrite drawNodeWidget and fix the GroupNode preview issue.
|
||||
@@ -512,3 +583,4 @@ If my custom nodes has added value to your day, consider indulging in a coffee t
|
||||
My gratitude extends to the generous souls who bestow a star. Your support is much appreciated!
|
||||
|
||||
[](https://github.com/yolain/ComfyUI-Easy-Use/stargazers)
|
||||
|
||||
|
||||
+13
-2
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.3.1"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
import yaml
|
||||
import json
|
||||
@@ -12,10 +12,21 @@ comfy_path = folder_paths.base_path
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
try:
|
||||
import comfy.supported_models as _supported_models
|
||||
_HAS_DIFFUSION_XY_SUPPORT = (
|
||||
hasattr(_supported_models, "Anima")
|
||||
and hasattr(_supported_models, "Krea2")
|
||||
)
|
||||
except Exception:
|
||||
_HAS_DIFFUSION_XY_SUPPORT = False
|
||||
|
||||
if not _HAS_DIFFUSION_XY_SUPPORT:
|
||||
print("[ComfyUI-Easy-Use] Anima/Krea2 XY nodes need comfy.supported_models.Anima and Krea2")
|
||||
|
||||
importlib.import_module('.py.routes', __name__)
|
||||
importlib.import_module('.py.server', __name__)
|
||||
nodes_list = ["util", "seed", "prompt", "loaders", "adapter", "inpaint", "preSampling", "samplers", "fix", "pipe", "xyplot", "image", "logic", "api", "deprecated"]
|
||||
# locale = {}
|
||||
for module_name in nodes_list:
|
||||
imported_module = importlib.import_module(".py.nodes.{}".format(module_name), __name__)
|
||||
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"settingsCategories": {
|
||||
"Hotkeys": "Hotkeys",
|
||||
"Nodes": "Nodes",
|
||||
"NodesMap": "NodesMap"
|
||||
"NodesMap": "NodesMap",
|
||||
"StylesSelector": "StylesSelector"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "Util",
|
||||
|
||||
@@ -250,6 +250,9 @@
|
||||
},
|
||||
"max_rows": {
|
||||
"name": "max_rows"
|
||||
},
|
||||
"remove_empty_lines":{
|
||||
"name": "remove_empty_lines"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -466,6 +469,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy multiAngle":{
|
||||
"display_name": "Multi Angle Prompt",
|
||||
"inputs": {
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"1":{
|
||||
"name": "params"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy fullLoader": {
|
||||
"display_name": "EasyLoader (Full)",
|
||||
"inputs": {
|
||||
@@ -1186,6 +1202,173 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy diffusionModelLoader": {
|
||||
"display_name": "EasyDiffusionModelLoader",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
"vae_name": {
|
||||
"name": "vae_name"
|
||||
},
|
||||
"clip_name": {
|
||||
"name": "clip_name"
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"empty_latent_width": {
|
||||
"name": "empty_latent_width"
|
||||
},
|
||||
"empty_latent_height": {
|
||||
"name": "empty_latent_height"
|
||||
},
|
||||
"positive": {
|
||||
"name": "positive"
|
||||
},
|
||||
"negative": {
|
||||
"name": "negative"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"model_override": {
|
||||
"name": "model_override"
|
||||
},
|
||||
"clip_override": {
|
||||
"name": "clip_override"
|
||||
},
|
||||
"vae_override": {
|
||||
"name": "vae_override"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "pipe"
|
||||
},
|
||||
"1": {
|
||||
"name": "model"
|
||||
},
|
||||
"2": {
|
||||
"name": "vae"
|
||||
},
|
||||
"3": {
|
||||
"name": "clip"
|
||||
},
|
||||
"4": {
|
||||
"name": "positive"
|
||||
},
|
||||
"5": {
|
||||
"name": "negative"
|
||||
},
|
||||
"6": {
|
||||
"name": "latent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy XYInputs: DiffusionModel": {
|
||||
"display_name": "XY Inputs: Diffusion Model //EasyUse",
|
||||
"inputs": {
|
||||
"model_count": {
|
||||
"name": "model_count"
|
||||
},
|
||||
"model_name_1": {
|
||||
"name": "model_name_1"
|
||||
},
|
||||
"clip_name_1": {
|
||||
"name": "clip_name_1"
|
||||
},
|
||||
"vae_name_1": {
|
||||
"name": "vae_name_1"
|
||||
},
|
||||
"model_name_2": {
|
||||
"name": "model_name_2"
|
||||
},
|
||||
"clip_name_2": {
|
||||
"name": "clip_name_2"
|
||||
},
|
||||
"vae_name_2": {
|
||||
"name": "vae_name_2"
|
||||
},
|
||||
"model_name_3": {
|
||||
"name": "model_name_3"
|
||||
},
|
||||
"clip_name_3": {
|
||||
"name": "clip_name_3"
|
||||
},
|
||||
"vae_name_3": {
|
||||
"name": "vae_name_3"
|
||||
},
|
||||
"model_name_4": {
|
||||
"name": "model_name_4"
|
||||
},
|
||||
"clip_name_4": {
|
||||
"name": "clip_name_4"
|
||||
},
|
||||
"vae_name_4": {
|
||||
"name": "vae_name_4"
|
||||
},
|
||||
"model_name_5": {
|
||||
"name": "model_name_5"
|
||||
},
|
||||
"clip_name_5": {
|
||||
"name": "clip_name_5"
|
||||
},
|
||||
"vae_name_5": {
|
||||
"name": "vae_name_5"
|
||||
},
|
||||
"model_name_6": {
|
||||
"name": "model_name_6"
|
||||
},
|
||||
"clip_name_6": {
|
||||
"name": "clip_name_6"
|
||||
},
|
||||
"vae_name_6": {
|
||||
"name": "vae_name_6"
|
||||
},
|
||||
"model_name_7": {
|
||||
"name": "model_name_7"
|
||||
},
|
||||
"clip_name_7": {
|
||||
"name": "clip_name_7"
|
||||
},
|
||||
"vae_name_7": {
|
||||
"name": "vae_name_7"
|
||||
},
|
||||
"model_name_8": {
|
||||
"name": "model_name_8"
|
||||
},
|
||||
"clip_name_8": {
|
||||
"name": "clip_name_8"
|
||||
},
|
||||
"vae_name_8": {
|
||||
"name": "vae_name_8"
|
||||
},
|
||||
"model_name_9": {
|
||||
"name": "model_name_9"
|
||||
},
|
||||
"clip_name_9": {
|
||||
"name": "clip_name_9"
|
||||
},
|
||||
"vae_name_9": {
|
||||
"name": "vae_name_9"
|
||||
},
|
||||
"model_name_10": {
|
||||
"name": "model_name_10"
|
||||
},
|
||||
"clip_name_10": {
|
||||
"name": "clip_name_10"
|
||||
},
|
||||
"vae_name_10": {
|
||||
"name": "vae_name_10"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "X or Y"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy loraStack": {
|
||||
"display_name": "EasyLoraStack",
|
||||
"inputs": {
|
||||
@@ -6255,6 +6438,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy PassOrNone": {
|
||||
"display_name": "Pass or None",
|
||||
"inputs": {
|
||||
"any": {
|
||||
"name": "anything"
|
||||
},
|
||||
"default": {
|
||||
"name": "default"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "output"
|
||||
},
|
||||
"1": {
|
||||
"name": "is_none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy isNone": {
|
||||
"display_name": "Is None",
|
||||
"inputs": {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"settingsCategories": {
|
||||
"Hotkeys": "快捷键",
|
||||
"Nodes": "节点相关",
|
||||
"NodesMap": "管理节点组"
|
||||
"NodesMap": "管理节点组",
|
||||
"StylesSelector": "样式选择器",
|
||||
"MultiAngle": "摄影机多角度提示词"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "工具",
|
||||
|
||||
+339
-124
@@ -154,6 +154,9 @@
|
||||
},
|
||||
"max_rows": {
|
||||
"name": "最大行数"
|
||||
},
|
||||
"remove_empty_lines":{
|
||||
"name": "去除空行"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -165,6 +168,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy promptAwait": {
|
||||
"display_name": "提示词等待",
|
||||
"inputs": {
|
||||
"now": {
|
||||
"name": "当前"
|
||||
},
|
||||
"prev": {
|
||||
"name": "上一步"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "提示词",
|
||||
"placeholder": "输入提示词或使用语音输入转文字"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "输出"
|
||||
},
|
||||
"1": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"2": {
|
||||
"name": "继续"
|
||||
},
|
||||
"3": {
|
||||
"name": "随机种"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy promptConcat": {
|
||||
"display_name": "提示词联结",
|
||||
"inputs": {
|
||||
@@ -361,6 +393,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy multiAngle":{
|
||||
"display_name": "多视角提示词",
|
||||
"inputs": {
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"1":{
|
||||
"name": "参数"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy fullLoader": {
|
||||
"display_name": "简易加载器 (完整版)",
|
||||
"inputs": {
|
||||
@@ -859,6 +904,173 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy diffusionModelLoader": {
|
||||
"display_name": "简易加载器(扩散模型)",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "扩散模型"
|
||||
},
|
||||
"vae_name": {
|
||||
"name": "VAE"
|
||||
},
|
||||
"clip_name": {
|
||||
"name": "文本编码器"
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"empty_latent_width": {
|
||||
"name": "宽度"
|
||||
},
|
||||
"empty_latent_height": {
|
||||
"name": "高度"
|
||||
},
|
||||
"positive": {
|
||||
"name": "正面提示词"
|
||||
},
|
||||
"negative": {
|
||||
"name": "负面提示词"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "批次大小"
|
||||
},
|
||||
"model_override": {
|
||||
"name": "模型(可选)"
|
||||
},
|
||||
"clip_override": {
|
||||
"name": "CLIP(可选)"
|
||||
},
|
||||
"vae_override": {
|
||||
"name": "VAE(可选)"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "节点束"
|
||||
},
|
||||
"1": {
|
||||
"name": "模型"
|
||||
},
|
||||
"2": {
|
||||
"name": "VAE"
|
||||
},
|
||||
"3": {
|
||||
"name": "CLIP"
|
||||
},
|
||||
"4": {
|
||||
"name": "正面提示词"
|
||||
},
|
||||
"5": {
|
||||
"name": "负面提示词"
|
||||
},
|
||||
"6": {
|
||||
"name": "潜空间"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy XYInputs: DiffusionModel": {
|
||||
"display_name": "XY输入: Diffusion Model",
|
||||
"inputs": {
|
||||
"model_count": {
|
||||
"name": "模型数量"
|
||||
},
|
||||
"model_name_1": {
|
||||
"name": "扩散模型1"
|
||||
},
|
||||
"clip_name_1": {
|
||||
"name": "文本编码器1"
|
||||
},
|
||||
"vae_name_1": {
|
||||
"name": "VAE1"
|
||||
},
|
||||
"model_name_2": {
|
||||
"name": "扩散模型2"
|
||||
},
|
||||
"clip_name_2": {
|
||||
"name": "文本编码器2"
|
||||
},
|
||||
"vae_name_2": {
|
||||
"name": "VAE2"
|
||||
},
|
||||
"model_name_3": {
|
||||
"name": "扩散模型3"
|
||||
},
|
||||
"clip_name_3": {
|
||||
"name": "文本编码器3"
|
||||
},
|
||||
"vae_name_3": {
|
||||
"name": "VAE3"
|
||||
},
|
||||
"model_name_4": {
|
||||
"name": "扩散模型4"
|
||||
},
|
||||
"clip_name_4": {
|
||||
"name": "文本编码器4"
|
||||
},
|
||||
"vae_name_4": {
|
||||
"name": "VAE4"
|
||||
},
|
||||
"model_name_5": {
|
||||
"name": "扩散模型5"
|
||||
},
|
||||
"clip_name_5": {
|
||||
"name": "文本编码器5"
|
||||
},
|
||||
"vae_name_5": {
|
||||
"name": "VAE5"
|
||||
},
|
||||
"model_name_6": {
|
||||
"name": "扩散模型6"
|
||||
},
|
||||
"clip_name_6": {
|
||||
"name": "文本编码器6"
|
||||
},
|
||||
"vae_name_6": {
|
||||
"name": "VAE6"
|
||||
},
|
||||
"model_name_7": {
|
||||
"name": "扩散模型7"
|
||||
},
|
||||
"clip_name_7": {
|
||||
"name": "文本编码器7"
|
||||
},
|
||||
"vae_name_7": {
|
||||
"name": "VAE7"
|
||||
},
|
||||
"model_name_8": {
|
||||
"name": "扩散模型8"
|
||||
},
|
||||
"clip_name_8": {
|
||||
"name": "文本编码器8"
|
||||
},
|
||||
"vae_name_8": {
|
||||
"name": "VAE8"
|
||||
},
|
||||
"model_name_9": {
|
||||
"name": "扩散模型9"
|
||||
},
|
||||
"clip_name_9": {
|
||||
"name": "文本编码器9"
|
||||
},
|
||||
"vae_name_9": {
|
||||
"name": "VAE9"
|
||||
},
|
||||
"model_name_10": {
|
||||
"name": "扩散模型10"
|
||||
},
|
||||
"clip_name_10": {
|
||||
"name": "文本编码器10"
|
||||
},
|
||||
"vae_name_10": {
|
||||
"name": "VAE10"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "X或Y"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy zero123Loader": {
|
||||
"display_name": "简易加载器(Zero123)",
|
||||
"inputs": {
|
||||
@@ -1125,6 +1337,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy loraSwitcher": {
|
||||
"display_name": "简易Lora切换器",
|
||||
"inputs": {
|
||||
"optional_lora_stack": {
|
||||
"name": "LoRA堆(可选)"
|
||||
},
|
||||
"toggle": {
|
||||
"name": "开关"
|
||||
},
|
||||
"select": {
|
||||
"name": "选择项"
|
||||
},
|
||||
"num_loras": {
|
||||
"name": "LoRA数量"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "LoRA堆"
|
||||
},
|
||||
"1": {
|
||||
"name": "LoRA名称"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy controlnetStack": {
|
||||
"display_name": "简易 ControlNet 堆",
|
||||
"inputs": {
|
||||
@@ -4274,6 +4511,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy loraPromptApply":{
|
||||
"display_name": "应用提示词LoRA",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "模型"
|
||||
},
|
||||
"clip": {
|
||||
"name": "CLIP"
|
||||
},
|
||||
"positive":{
|
||||
"name": "正面提示词"
|
||||
},
|
||||
"negative":{
|
||||
"name": "负面提示词"
|
||||
}
|
||||
},
|
||||
"outputs":{
|
||||
"0": {
|
||||
"name": "模型"
|
||||
},
|
||||
"1": {
|
||||
"name": "CLIP"
|
||||
},
|
||||
"2": {
|
||||
"name": "正面提示词"
|
||||
},
|
||||
"3": {
|
||||
"name": "负面提示词"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy loraStackApply": {
|
||||
"display_name": "应用LoRA堆",
|
||||
"inputs": {
|
||||
@@ -5987,7 +6255,7 @@
|
||||
"display_name": "ckpt名称列表",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "模型名称"
|
||||
"name": "模型名称"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -6009,6 +6277,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy tableEditor": {
|
||||
"display_name": "表格编辑器",
|
||||
"description": "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串及渲染图像。",
|
||||
"inputs": {
|
||||
"table_data": {
|
||||
"name": "表格数据"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Markdown"
|
||||
},
|
||||
"1": {
|
||||
"name": "图像"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy string": {
|
||||
"display_name": "字符串",
|
||||
"inputs": {
|
||||
@@ -6113,73 +6398,13 @@
|
||||
"easy whileLoopStart": {
|
||||
"display_name": "While循环-开始",
|
||||
"inputs": {
|
||||
"initial_value0": {
|
||||
"name": "初始值0"
|
||||
},
|
||||
"initial_value1": {
|
||||
"name": "初始值1"
|
||||
},
|
||||
"initial_value2": {
|
||||
"name": "初始值2"
|
||||
},
|
||||
"initial_value3": {
|
||||
"name": "初始值3"
|
||||
},
|
||||
"initial_value4": {
|
||||
"name": "初始值4"
|
||||
},
|
||||
"initial_value5": {
|
||||
"name": "初始值5"
|
||||
},
|
||||
"initial_value6": {
|
||||
"name": "初始值6"
|
||||
},
|
||||
"initial_value7": {
|
||||
"name": "初始值7"
|
||||
},
|
||||
"initial_value8": {
|
||||
"name": "初始值8"
|
||||
},
|
||||
"initial_value9": {
|
||||
"name": "初始值9"
|
||||
},
|
||||
"condition": {
|
||||
"name": "条件"
|
||||
"name": "开始循环"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "开始"
|
||||
},
|
||||
"1": {
|
||||
"name": "值0"
|
||||
},
|
||||
"2": {
|
||||
"name": "值1"
|
||||
},
|
||||
"3": {
|
||||
"name": "值2"
|
||||
},
|
||||
"4": {
|
||||
"name": "值3"
|
||||
},
|
||||
"5": {
|
||||
"name": "值4"
|
||||
},
|
||||
"6": {
|
||||
"name": "值5"
|
||||
},
|
||||
"7": {
|
||||
"name": "值6"
|
||||
},
|
||||
"8": {
|
||||
"name": "值7"
|
||||
},
|
||||
"9": {
|
||||
"name": "值8"
|
||||
},
|
||||
"10": {
|
||||
"name": "值9"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6189,71 +6414,11 @@
|
||||
"flow": {
|
||||
"name": "结束"
|
||||
},
|
||||
"initial_value0": {
|
||||
"name": "初始值0"
|
||||
},
|
||||
"initial_value1": {
|
||||
"name": "初始值1"
|
||||
},
|
||||
"initial_value2": {
|
||||
"name": "初始值2"
|
||||
},
|
||||
"initial_value3": {
|
||||
"name": "初始值3"
|
||||
},
|
||||
"initial_value4": {
|
||||
"name": "初始值4"
|
||||
},
|
||||
"initial_value5": {
|
||||
"name": "初始值5"
|
||||
},
|
||||
"initial_value6": {
|
||||
"name": "初始值6"
|
||||
},
|
||||
"initial_value7": {
|
||||
"name": "初始值7"
|
||||
},
|
||||
"initial_value8": {
|
||||
"name": "初始值8"
|
||||
},
|
||||
"initial_value9": {
|
||||
"name": "初始值9"
|
||||
},
|
||||
"condition": {
|
||||
"name": "条件"
|
||||
"name": "继续循环"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "值0"
|
||||
},
|
||||
"1": {
|
||||
"name": "值1"
|
||||
},
|
||||
"2": {
|
||||
"name": "值2"
|
||||
},
|
||||
"3": {
|
||||
"name": "值3"
|
||||
},
|
||||
"4": {
|
||||
"name": "值4"
|
||||
},
|
||||
"5": {
|
||||
"name": "值5"
|
||||
},
|
||||
"6": {
|
||||
"name": "值6"
|
||||
},
|
||||
"7": {
|
||||
"name": "值7"
|
||||
},
|
||||
"8": {
|
||||
"name": "值8"
|
||||
},
|
||||
"9": {
|
||||
"name": "值9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy forLoopStart": {
|
||||
@@ -6323,6 +6488,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy PassOrNone": {
|
||||
"display_name": "传递或为空",
|
||||
"inputs": {
|
||||
"any": {
|
||||
"name": "任何"
|
||||
},
|
||||
"default": {
|
||||
"name": "默认值"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "输出"
|
||||
},
|
||||
"1": {
|
||||
"name": "是否为空"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy isNone": {
|
||||
"display_name": "是否为空",
|
||||
"inputs": {
|
||||
@@ -6711,7 +6895,38 @@
|
||||
"name": "温度"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "最大词令牌数"
|
||||
"name": "最大词元数"
|
||||
},
|
||||
"caption_type": {
|
||||
"name": "提示词类型"
|
||||
},
|
||||
"caption_length": {
|
||||
"name": "提示词长度"
|
||||
},
|
||||
"name_input": {
|
||||
"name": "名称输入"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "提示词"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy joyCaption3API": {
|
||||
"display_name": "JoyCaption3(硅基流动)",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "图像"
|
||||
},
|
||||
"do_sample": {
|
||||
"name": "执行采样"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "温度"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "最大词元数"
|
||||
},
|
||||
"caption_type": {
|
||||
"name": "提示词类型"
|
||||
|
||||
@@ -63,5 +63,24 @@
|
||||
"EasyUse_NodesMap_Enable": {
|
||||
"name": "启用管理节点组",
|
||||
"tooltip": "您需要刷新页面以成功更新"
|
||||
},
|
||||
"EasyUse_StylesSelector_DisplayType": {
|
||||
"name": "样式选择器显示类型",
|
||||
"tooltip": "样式选择器显示类型,如果设置为“网格”,则显示为网格,如果设置为“列表”,则显示为列表",
|
||||
"options": {
|
||||
"Grid": "网格",
|
||||
"List": "列表"
|
||||
}
|
||||
},
|
||||
"EasyUse_MultiAngle_InvertRotate": {
|
||||
"name": "启用反转旋转模式",
|
||||
"tooltip": "在多角度节点中启用反转旋转模式,使旋转方向与大多数3D软件一致"
|
||||
},
|
||||
"EasyUse_MultiAngle_HollowMode": {
|
||||
"name": "启用多角度镂空展示模式",
|
||||
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
|
||||
},
|
||||
"EasyUse_MultiAngle_AddAnglePrompt": {
|
||||
"name": "启用添加多角度提示词"
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,15 @@ HUMANPARSING_MODELS = {
|
||||
},
|
||||
"human-parts":{
|
||||
"model_url":"https://huggingface.co/Metal3d/deeplabv3p-resnet50-human/resolve/main/deeplabv3p-resnet50-human.onnx",
|
||||
},
|
||||
"segformer_b3_clothes":{
|
||||
"model_name": "sayeed99/segformer_b3_clothes",
|
||||
},
|
||||
"segformer_b3_fashion":{
|
||||
"model_name": "sayeed99/segformer-b3-fashion",
|
||||
},
|
||||
"face_parsing":{
|
||||
"model_name": "jonathandinu/face-parsing"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,3 +405,21 @@ PROMPT_TEMPLATE = {
|
||||
}
|
||||
|
||||
NEW_SCHEDULERS = ['align_your_steps', 'gits']
|
||||
|
||||
DIFFUSION_MODEL_XY_DEFAULTS = {
|
||||
"anima": {
|
||||
"clip_name": "qwen_3_06b_base.safetensors",
|
||||
"clip_type": "anima",
|
||||
"vae_name": "qwen_image_vae.safetensors",
|
||||
},
|
||||
"krea2": {
|
||||
"clip_name": "Huihui-Qwen3-VL-4B-Instruct-abliterated-fp8_scaled.safetensors",
|
||||
"clip_type": "krea2",
|
||||
"vae_name": "qwen_image_vae.safetensors",
|
||||
},
|
||||
}
|
||||
|
||||
DIFFUSION_MODEL_CLIP_TYPES = {
|
||||
"anima": "anima",
|
||||
"krea2": "krea2",
|
||||
}
|
||||
|
||||
@@ -56,13 +56,14 @@ class BizyAIRAPI:
|
||||
f"Failed to connect to the server: {e}, if you have no key, "
|
||||
)
|
||||
|
||||
# joycaptionTwo
|
||||
def joyCaption2(self, payload, image, apikey_override=None):
|
||||
# joycaption
|
||||
def joyCaption(self, payload, image, apikey_override=None, API_URL='/supernode/joycaption2'):
|
||||
if apikey_override is not None:
|
||||
api_key = apikey_override
|
||||
else:
|
||||
api_key = self.getAPIKey()
|
||||
url = f"{self.base_url}/supernode/joycaption2"
|
||||
url = f"{self.base_url}{API_URL}"
|
||||
print('Sending request to:', url)
|
||||
auth = f"Bearer {api_key}"
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
|
||||
+4
-3
@@ -77,10 +77,11 @@ def update_cache(k, tag, v):
|
||||
else:
|
||||
cache_count[k] += 1
|
||||
def remove_cache(key):
|
||||
global cache
|
||||
if key == '*':
|
||||
cache = TaggedCache(cache_settings)
|
||||
cache.clear()
|
||||
cache_count.clear()
|
||||
elif key in cache:
|
||||
del cache[key]
|
||||
cache_count.pop(key, None)
|
||||
else:
|
||||
print(f"invalid {key}")
|
||||
print(f"invalid {key}")
|
||||
|
||||
+139
-38
@@ -1,52 +1,153 @@
|
||||
from threading import Event
|
||||
|
||||
import torch
|
||||
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
from comfy import model_management as mm
|
||||
from comfy_execution.graph import ExecutionBlocker
|
||||
import time
|
||||
|
||||
class ChooserCancelled(Exception):
|
||||
pass
|
||||
|
||||
class ChooserMessage:
|
||||
stash = {}
|
||||
messages = {}
|
||||
cancelled = False
|
||||
def get_chooser_cache():
|
||||
"""获取选择器缓存"""
|
||||
if not hasattr(PromptServer.instance, '_easyuse_chooser_node'):
|
||||
PromptServer.instance._easyuse_chooser_node = {}
|
||||
return PromptServer.instance._easyuse_chooser_node
|
||||
|
||||
@classmethod
|
||||
def addMessage(cls, id, message):
|
||||
if message == '__cancel__':
|
||||
cls.messages = {}
|
||||
cls.cancelled = True
|
||||
elif message == '__start__':
|
||||
cls.messages = {}
|
||||
cls.stash = {}
|
||||
cls.cancelled = False
|
||||
else:
|
||||
cls.messages[str(id)] = message
|
||||
def cleanup_session_data(node_id):
|
||||
"""清理会话数据"""
|
||||
node_data = get_chooser_cache()
|
||||
if node_id in node_data:
|
||||
session_keys = ["event", "selected", "images", "total_count", "cancelled"]
|
||||
for key in session_keys:
|
||||
if key in node_data[node_id]:
|
||||
del node_data[node_id][key]
|
||||
|
||||
def wait_for_chooser(id, images, mode, period=0.1):
|
||||
try:
|
||||
node_data = get_chooser_cache()
|
||||
images = [images[i:i + 1, ...] for i in range(images.shape[0])]
|
||||
if mode == "Keep Last Selection":
|
||||
if id in node_data and "last_selection" in node_data[id]:
|
||||
last_selection = node_data[id]["last_selection"]
|
||||
if last_selection and len(last_selection) > 0:
|
||||
valid_indices = [idx for idx in last_selection if 0 <= idx < len(images)]
|
||||
if valid_indices:
|
||||
try:
|
||||
PromptServer.instance.send_sync("easyuse-image-keep-selection", {
|
||||
"id": id,
|
||||
"selected": valid_indices
|
||||
})
|
||||
except Exception as e:
|
||||
pass
|
||||
cleanup_session_data(id)
|
||||
indices_str = ','.join(str(i) for i in valid_indices)
|
||||
images = [images[idx] for idx in valid_indices]
|
||||
images = torch.cat(images, dim=0)
|
||||
return {"result": (images,)}
|
||||
|
||||
if id in node_data:
|
||||
del node_data[id]
|
||||
|
||||
event = Event()
|
||||
node_data[id] = {
|
||||
"event": event,
|
||||
"images": images,
|
||||
"selected": None,
|
||||
"total_count": len(images),
|
||||
"cancelled": False,
|
||||
}
|
||||
|
||||
while id in node_data:
|
||||
node_info = node_data[id]
|
||||
if node_info.get("cancelled", False):
|
||||
cleanup_session_data(id)
|
||||
raise ChooserCancelled("Manual selection cancelled")
|
||||
|
||||
if "selected" in node_info and node_info["selected"] is not None:
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def waitForMessage(cls, id, period=0.1, asList=False):
|
||||
sid = str(id)
|
||||
while not (sid in cls.messages) and not ("-1" in cls.messages):
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise ChooserCancelled()
|
||||
time.sleep(period)
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise ChooserCancelled()
|
||||
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
|
||||
try:
|
||||
if asList:
|
||||
return [int(x.strip()) for x in message.split(",")]
|
||||
|
||||
if id in node_data:
|
||||
node_info = node_data[id]
|
||||
selected_indices = node_info.get("selected")
|
||||
|
||||
if selected_indices is not None and len(selected_indices) > 0:
|
||||
valid_indices = [idx for idx in selected_indices if 0 <= idx < len(images)]
|
||||
if valid_indices:
|
||||
selected_images = [images[idx] for idx in valid_indices]
|
||||
|
||||
if id not in node_data:
|
||||
node_data[id] = {}
|
||||
node_data[id]["last_selection"] = valid_indices
|
||||
cleanup_session_data(id)
|
||||
selected_images = torch.cat(selected_images, dim=0)
|
||||
return {"result": (selected_images,)}
|
||||
else:
|
||||
cleanup_session_data(id)
|
||||
return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
|
||||
else:
|
||||
return int(message.strip())
|
||||
except ValueError:
|
||||
print(
|
||||
f"ERROR IN IMAGE_CHOOSER - failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}")
|
||||
return [1] if asList else 1
|
||||
cleanup_session_data(id)
|
||||
return {
|
||||
"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
|
||||
else:
|
||||
return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
|
||||
|
||||
except ChooserCancelled:
|
||||
raise mm.InterruptProcessingException()
|
||||
except Exception as e:
|
||||
node_data = get_chooser_cache()
|
||||
if id in node_data:
|
||||
cleanup_session_data(id)
|
||||
if 'image_list' in locals() and len(images) > 0:
|
||||
return {"result": (images[0])}
|
||||
else:
|
||||
return {"result": (ExecutionBlocker(None),)}
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post('/easyuse/image_chooser_message')
|
||||
async def make_image_selection(request):
|
||||
post = await request.post()
|
||||
ChooserMessage.addMessage(post.get("id"), post.get("message"))
|
||||
return web.json_response({})
|
||||
async def handle_image_selection(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
node_id = data.get("node_id")
|
||||
selected = data.get("selected", [])
|
||||
action = data.get("action")
|
||||
|
||||
node_data = get_chooser_cache()
|
||||
|
||||
if node_id not in node_data:
|
||||
return web.json_response({"code": -1, "error": "Node data does not exist"})
|
||||
|
||||
try:
|
||||
node_info = node_data[node_id]
|
||||
|
||||
if "total_count" not in node_info:
|
||||
return web.json_response({"code": -1, "error": "The node has been processed"})
|
||||
|
||||
if action == "cancel":
|
||||
node_info["cancelled"] = True
|
||||
node_info["selected"] = []
|
||||
elif action == "select" and isinstance(selected, list):
|
||||
valid_indices = [idx for idx in selected if isinstance(idx, int) and 0 <= idx < node_info["total_count"]]
|
||||
if valid_indices:
|
||||
node_info["selected"] = valid_indices
|
||||
node_info["cancelled"] = False
|
||||
else:
|
||||
return web.json_response({"code": -1, "error": "Invalid Selection Index"})
|
||||
else:
|
||||
return web.json_response({"code": -1, "error": "Invalid operation"})
|
||||
|
||||
node_info["event"].set()
|
||||
return web.json_response({"code": 1})
|
||||
|
||||
except Exception as e:
|
||||
if node_id in node_data and "event" in node_data[node_id]:
|
||||
node_data[node_id]["event"].set()
|
||||
return web.json_response({"code": -1, "message": "Processing Failed"})
|
||||
|
||||
except Exception as e:
|
||||
return web.json_response({"code": -1, "message": "Request Failed"})
|
||||
|
||||
@@ -14,7 +14,7 @@ def prompt_to_cond(type, model, clip, clip_skip, lora_stack, text, prompt_token_
|
||||
if model_type not in ['hydit'] and text is not None and has_chinese(text):
|
||||
text = zh_to_en([text])[0]
|
||||
|
||||
if model_type in ['hydit', 'flux', 'mochi']:
|
||||
if model_type in ['hydit', 'flux', 'mochi', 'anima', 'krea2']:
|
||||
log_node_warn(title + "...")
|
||||
embeddings_final, = CLIPTextEncode().encode(clip, text) if text is not None else (None,)
|
||||
|
||||
|
||||
+83
-3
@@ -8,6 +8,8 @@ from comfy.model_patcher import ModelPatcher
|
||||
from nodes import NODE_CLASS_MAPPINGS
|
||||
from collections import defaultdict
|
||||
from .log import log_node_info, log_node_error
|
||||
from .utils import get_sd_version
|
||||
from ..config import DIFFUSION_MODEL_XY_DEFAULTS, DIFFUSION_MODEL_CLIP_TYPES
|
||||
from ..modules.dit.pixArt.loader import load_pixart
|
||||
|
||||
diffusion_loaders = ["easy fullLoader", "easy a1111Loader", "easy fluxLoader", "easy comfyLoader", "easy hunyuanDiTLoader", "easy zero123Loader", "easy svdLoader"]
|
||||
@@ -145,6 +147,28 @@ class easyLoader:
|
||||
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 == "easy diffusionModelLoader":
|
||||
desired_unet_names.add(self.get_input_value(entry, "model_name", prompt))
|
||||
clip_name = self.get_input_value(entry, "clip_name", prompt)
|
||||
vae_name = self.get_input_value(entry, "vae_name", prompt)
|
||||
if clip_name not in ("None", "Auto"):
|
||||
desired_clip_names.add(clip_name)
|
||||
if vae_name not in ("None", "Auto"):
|
||||
desired_vae_names.add(vae_name)
|
||||
|
||||
elif class_type == "easy XYInputs: DiffusionModel":
|
||||
model_count = int(self.get_input_value(entry, "model_count", prompt) or 0)
|
||||
for i in range(1, model_count + 1):
|
||||
model_name = self.get_input_value(entry, f"model_name_{i}", prompt)
|
||||
if model_name and model_name != "None":
|
||||
desired_unet_names.add(model_name)
|
||||
clip_name = self.get_input_value(entry, f"clip_name_{i}", prompt)
|
||||
if clip_name not in ("None", "Auto"):
|
||||
desired_clip_names.add(clip_name)
|
||||
vae_name = self.get_input_value(entry, f"vae_name_{i}", prompt)
|
||||
if vae_name not in ("None", "Auto"):
|
||||
desired_vae_names.add(vae_name)
|
||||
|
||||
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"))
|
||||
@@ -282,6 +306,57 @@ class easyLoader:
|
||||
|
||||
return model
|
||||
|
||||
def load_diffusion_model(self, model_name):
|
||||
if model_name in self.loaded_objects["unet"]:
|
||||
log_node_info("Load Diffusion Model", f"{model_name} cached")
|
||||
return self.loaded_objects["unet"][model_name][0]
|
||||
|
||||
model_path = folder_paths.get_full_path("diffusion_models", model_name)
|
||||
if not model_path:
|
||||
raise FileNotFoundError(f"[EasyUse] diffusion model not found: {model_name}")
|
||||
|
||||
model = comfy.sd.load_diffusion_model(model_path)
|
||||
self.add_to_cache("unet", model_name, model)
|
||||
self.eviction_based_on_memory()
|
||||
|
||||
return model
|
||||
|
||||
def load_diffusion_xy_model(self, model_name, clip_name, vae_name):
|
||||
model = self.load_diffusion_model(model_name)
|
||||
family = get_sd_version(model)
|
||||
|
||||
defaults = DIFFUSION_MODEL_XY_DEFAULTS.get(family)
|
||||
if defaults is None:
|
||||
raise RuntimeError(f"[EasyUse] unsupported diffusion model family: {family}")
|
||||
|
||||
if clip_name in ("Auto", None):
|
||||
clip_name = defaults["clip_name"]
|
||||
if vae_name in ("Auto", None):
|
||||
vae_name = defaults["vae_name"]
|
||||
|
||||
clip = self.load_clip(clip_name, type=defaults["clip_type"])
|
||||
vae = self.load_vae(vae_name)
|
||||
|
||||
return model, clip, vae, family
|
||||
|
||||
def load_diffusion_model_required(self, model_name, clip_name, vae_name):
|
||||
if clip_name in ("None", None):
|
||||
raise RuntimeError("[EasyUse] clip_name is required: please select a text encoder")
|
||||
if vae_name in ("None", None):
|
||||
raise RuntimeError("[EasyUse] vae_name is required: please select a VAE")
|
||||
|
||||
model = self.load_diffusion_model(model_name)
|
||||
family = get_sd_version(model)
|
||||
|
||||
clip_type = DIFFUSION_MODEL_CLIP_TYPES.get(family)
|
||||
if clip_type is None:
|
||||
raise RuntimeError(f"[EasyUse] unsupported diffusion model family: {family}")
|
||||
|
||||
clip = self.load_clip(clip_name, type=clip_type)
|
||||
vae = self.load_vae(vae_name)
|
||||
|
||||
return model, clip, vae, family
|
||||
|
||||
def load_controlnet(self, control_net_name, scale_soft_weights=1, use_cache=True):
|
||||
unique_id = f'{control_net_name};{str(scale_soft_weights)}'
|
||||
if use_cache and unique_id in self.loaded_objects["controlnet"]:
|
||||
@@ -303,8 +378,9 @@ class easyLoader:
|
||||
|
||||
return control_net
|
||||
def load_clip(self, clip_name, type='stable_diffusion', load_clip=None):
|
||||
if clip_name in self.loaded_objects["clip"]:
|
||||
return self.loaded_objects["clip"][clip_name][0]
|
||||
cache_key = f"{clip_name}::{type}"
|
||||
if cache_key in self.loaded_objects["clip"]:
|
||||
return self.loaded_objects["clip"][cache_key][0]
|
||||
|
||||
if type == 'stable_diffusion':
|
||||
clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION
|
||||
@@ -316,9 +392,13 @@ class easyLoader:
|
||||
clip_type = comfy.sd.CLIPType.FLUX
|
||||
elif type == 'stable_audio':
|
||||
clip_type = comfy.sd.CLIPType.STABLE_AUDIO
|
||||
elif type == 'krea2':
|
||||
clip_type = comfy.sd.CLIPType.KREA2
|
||||
elif type == 'anima':
|
||||
clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION
|
||||
clip_path = folder_paths.get_full_path("clip", clip_name)
|
||||
load_clip = comfy.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type)
|
||||
self.add_to_cache("clip", clip_name, load_clip)
|
||||
self.add_to_cache("clip", cache_key, load_clip)
|
||||
self.eviction_based_on_memory()
|
||||
|
||||
return load_clip
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Math utility functions for formula evaluation
|
||||
"""
|
||||
import math
|
||||
import re
|
||||
|
||||
def evaluate_formula(formula: str, a=0, b=0, c=0, d=0):
|
||||
"""
|
||||
计算字符串数学公式
|
||||
|
||||
支持的运算符和函数:
|
||||
- 基本运算:+, -, *, /, //, %, **
|
||||
- 比较运算:>, <, >=, <=, ==, !=
|
||||
- 数学函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10
|
||||
- 三角函数:sin, cos, tan, asin, acos, atan
|
||||
- 常量:pi, e
|
||||
|
||||
Args:
|
||||
formula: 数学公式字符串,可以使用变量a、b、c、d
|
||||
a: 变量a的值
|
||||
b: 变量b的值
|
||||
c: 变量c的值
|
||||
d: 变量d的值
|
||||
|
||||
Returns:
|
||||
如果任意输入为list则返回list[float],否则返回float
|
||||
|
||||
Examples:
|
||||
>>> evaluate_formula("a + b", 1, 2)
|
||||
3.0
|
||||
>>> evaluate_formula("pow(a, 2)", 5)
|
||||
25.0
|
||||
>>> evaluate_formula("ceil(a / b)", 5, 2)
|
||||
3.0
|
||||
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 5, 3)
|
||||
3.0
|
||||
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 2, 3)
|
||||
2.0
|
||||
"""
|
||||
# 安全的数学函数白名单
|
||||
safe_dict = {
|
||||
# 基本运算
|
||||
'abs': abs,
|
||||
'pow': pow,
|
||||
'round': round,
|
||||
# 数学函数
|
||||
'ceil': math.ceil,
|
||||
'floor': math.floor,
|
||||
'sqrt': math.sqrt,
|
||||
'exp': math.exp,
|
||||
'log': math.log,
|
||||
'log10': math.log10,
|
||||
# 三角函数
|
||||
'sin': math.sin,
|
||||
'cos': math.cos,
|
||||
'tan': math.tan,
|
||||
'asin': math.asin,
|
||||
'acos': math.acos,
|
||||
'atan': math.atan,
|
||||
# 常量
|
||||
'pi': math.pi,
|
||||
'e': math.e,
|
||||
}
|
||||
|
||||
# 判断是否有 list 输入
|
||||
list_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if isinstance(v, (list, tuple))}
|
||||
scalar_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if not isinstance(v, (list, tuple))}
|
||||
|
||||
def _eval_single(vals: dict) -> float:
|
||||
env = dict(safe_dict)
|
||||
env.update({k: float(v) for k, v in vals.items()})
|
||||
try:
|
||||
result = eval(formula, {"__builtins__": {}}, env)
|
||||
return float(result)
|
||||
except Exception as e:
|
||||
raise ValueError(f"公式计算错误: {str(e)}")
|
||||
|
||||
if not list_inputs:
|
||||
# 全是标量
|
||||
return _eval_single({k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items()})
|
||||
|
||||
# 有 list 输入,逐元素计算
|
||||
max_len = max(len(v) for v in list_inputs.values())
|
||||
results = []
|
||||
for i in range(max_len):
|
||||
vals = {k: float(v) for k, v in scalar_inputs.items()}
|
||||
for k, v in list_inputs.items():
|
||||
vals[k] = float(v[i] if i < len(v) else v[-1])
|
||||
results.append(_eval_single(vals))
|
||||
return results
|
||||
|
||||
|
||||
def ceil_value(value: float) -> int:
|
||||
"""向上取整"""
|
||||
return math.ceil(value)
|
||||
|
||||
|
||||
def floor_value(value: float) -> int:
|
||||
"""向下取整"""
|
||||
return math.floor(value)
|
||||
|
||||
|
||||
def round_value(value: float, decimals: int = 0) -> float:
|
||||
"""
|
||||
四舍五入
|
||||
|
||||
Args:
|
||||
value: 要取整的值
|
||||
decimals: 保留小数位数
|
||||
|
||||
Returns:
|
||||
四舍五入后的值
|
||||
"""
|
||||
return round(value, decimals)
|
||||
|
||||
|
||||
def power(base: float, exponent: float) -> float:
|
||||
"""计算幂运算"""
|
||||
return math.pow(base, exponent)
|
||||
|
||||
|
||||
def sqrt_value(value: float) -> float:
|
||||
"""计算平方根"""
|
||||
if value < 0:
|
||||
raise ValueError("不能对负数求平方根")
|
||||
return math.sqrt(value)
|
||||
|
||||
|
||||
def add(a: float, b: float) -> float:
|
||||
"""加法"""
|
||||
return a + b
|
||||
|
||||
|
||||
def subtract(a: float, b: float) -> float:
|
||||
"""减法"""
|
||||
return a - b
|
||||
|
||||
|
||||
def multiply(a: float, b: float) -> float:
|
||||
"""乘法"""
|
||||
return a * b
|
||||
|
||||
|
||||
def divide(a: float, b: float) -> float:
|
||||
"""除法"""
|
||||
if b == 0:
|
||||
raise ValueError("除数不能为零")
|
||||
return a / b
|
||||
@@ -0,0 +1,55 @@
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
import time
|
||||
import json
|
||||
|
||||
class MessageCancelled(Exception):
|
||||
pass
|
||||
|
||||
class Message:
|
||||
stash = {}
|
||||
messages = {}
|
||||
cancelled = False
|
||||
|
||||
@classmethod
|
||||
def addMessage(cls, id, message):
|
||||
if message == '__cancel__':
|
||||
cls.messages = {}
|
||||
cls.cancelled = True
|
||||
elif message == '__start__':
|
||||
cls.messages = {}
|
||||
cls.stash = {}
|
||||
cls.cancelled = False
|
||||
else:
|
||||
cls.messages[str(id)] = message
|
||||
|
||||
@classmethod
|
||||
def waitForMessage(cls, id, period=0.1, asList=False):
|
||||
sid = str(id)
|
||||
while not (sid in cls.messages) and not ("-1" in cls.messages):
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise MessageCancelled()
|
||||
time.sleep(period)
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise MessageCancelled()
|
||||
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
|
||||
try:
|
||||
if asList:
|
||||
return [str(x.strip()) for x in message.split(",")]
|
||||
else:
|
||||
try:
|
||||
return json.loads(message)
|
||||
except ValueError:
|
||||
return message
|
||||
except ValueError:
|
||||
print( f"ERROR IN MESSAGE - failed to parse '${message}' as ${'comma separated list of strings' if asList else 'string'}")
|
||||
return [message] if asList else message
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post('/easyuse/message_callback')
|
||||
async def message_callback(request):
|
||||
post = await request.post()
|
||||
Message.addMessage(post.get("id"), post.get("message"))
|
||||
return web.json_response({})
|
||||
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
|
||||
def resolve_output_file_path(output_root, output_file_path, file_name, file_extension):
|
||||
"""Resolve a workflow-provided output path beneath ``output_root``.
|
||||
|
||||
Relative output directories remain supported, but are interpreted relative
|
||||
to ComfyUI's configured output directory rather than the process working
|
||||
directory. Resolving both paths prevents ``..`` components and existing
|
||||
symlinks from escaping the allowed root.
|
||||
"""
|
||||
output_root = os.path.realpath(output_root)
|
||||
requested_directory = output_file_path
|
||||
if not os.path.isabs(requested_directory):
|
||||
requested_directory = os.path.join(output_root, requested_directory)
|
||||
|
||||
candidate = os.path.realpath(
|
||||
os.path.join(requested_directory, f"{file_name}.{file_extension}")
|
||||
)
|
||||
try:
|
||||
is_within_output = os.path.commonpath((output_root, candidate)) == output_root
|
||||
except ValueError:
|
||||
# Different Windows drives and paths containing null bytes are unsafe.
|
||||
is_within_output = False
|
||||
|
||||
if not is_within_output:
|
||||
raise ValueError("Saving outside the ComfyUI output directory is not allowed")
|
||||
|
||||
return candidate
|
||||
+5
-2
@@ -65,6 +65,9 @@ class easySampler:
|
||||
elif model_type == 'mochi':
|
||||
latent = torch.zeros([batch_size, 12, ((video_length - 1) // 6) + 1, empty_latent_height // 8, empty_latent_width // 8], device=self.device)
|
||||
samples = {"samples": latent}
|
||||
elif model_type in ("anima", "krea2"):
|
||||
latent = torch.zeros([batch_size, 16, 1, empty_latent_height // 8, empty_latent_width // 8], device=self.device)
|
||||
samples = {"samples": latent}
|
||||
elif compression == 0:
|
||||
latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8], device=self.device)
|
||||
samples = {"samples": latent}
|
||||
@@ -84,7 +87,7 @@ class easySampler:
|
||||
"""
|
||||
|
||||
latent_size = latent_image.size()
|
||||
latent_size_1batch = [1, latent_size[1], latent_size[2], latent_size[3]]
|
||||
latent_size_1batch = [1] + list(latent_size[1:])
|
||||
|
||||
if variation_strength is not None and variation_strength > 0 or incremental_seed_mode.startswith(
|
||||
"variation str inc"):
|
||||
@@ -108,7 +111,7 @@ class easySampler:
|
||||
if strength_up is not None:
|
||||
strength += strength_up
|
||||
|
||||
variation_noise = variation_latent.expand(input_latent.size()[0], -1, -1, -1)
|
||||
variation_noise = variation_latent.expand(input_latent.size()[0], *([-1] * (variation_latent.dim() - 1)))
|
||||
result = (1 - strength) * input_latent + strength * variation_noise
|
||||
return result
|
||||
|
||||
|
||||
@@ -35,9 +35,13 @@ import sys
|
||||
import importlib.util
|
||||
import importlib.metadata
|
||||
import comfy.model_management as mm
|
||||
import logging
|
||||
import gc
|
||||
from packaging import version
|
||||
from server import PromptServer
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
def is_package_installed(package):
|
||||
try:
|
||||
module = importlib.util.find_spec(package)
|
||||
@@ -124,6 +128,10 @@ def get_sd_version(model):
|
||||
return 'flux'
|
||||
elif isinstance(model_config, comfy.supported_models.GenmoMochi):
|
||||
return 'mochi'
|
||||
elif isinstance(model_config, comfy.supported_models.Anima):
|
||||
return 'anima'
|
||||
elif isinstance(model_config, comfy.supported_models.Krea2):
|
||||
return 'krea2'
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
@@ -277,6 +285,20 @@ def getMetadata(filepath):
|
||||
return header
|
||||
|
||||
def cleanGPUUsedForce():
|
||||
from .cache import remove_cache
|
||||
|
||||
remove_cache("*")
|
||||
gc.collect()
|
||||
try:
|
||||
import torch
|
||||
except (ImportError, OSError, RuntimeError) as exc:
|
||||
LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: torch import failed: %s", exc)
|
||||
else:
|
||||
try:
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
except (AttributeError, OSError, RuntimeError) as exc:
|
||||
LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: %s", exc)
|
||||
|
||||
mm.unload_all_models()
|
||||
mm.soft_empty_cache()
|
||||
|
||||
@@ -47,7 +47,7 @@ def read_wildcard_dict(wildcard_path):
|
||||
easy_wildcard_dict[key] = lines
|
||||
elif file.endswith('.yaml'):
|
||||
file_path = os.path.join(root, file)
|
||||
with open(file_path, 'r') as f:
|
||||
with open(file_path, 'r', encoding="utf-8") as f:
|
||||
yaml_data = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
for k, v in yaml_data.items():
|
||||
@@ -55,7 +55,7 @@ def read_wildcard_dict(wildcard_path):
|
||||
elif file.endswith('.json'):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
with open(file_path, 'r', encoding="utf-8") as f:
|
||||
json_data = json.load(f)
|
||||
for key, value in json_data.items():
|
||||
key = wildcard_normalize(key)
|
||||
|
||||
+102
-27
@@ -63,10 +63,14 @@ class easyXYPlot():
|
||||
if value_type in ['Lora', 'Checkpoint']:
|
||||
arr = value.split(',')
|
||||
model_name = os.path.basename(os.path.splitext(arr[0])[0])
|
||||
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr[3]) > 2 else ''
|
||||
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr) > 3 and len(arr[3]) > 2 else ''
|
||||
lora_weight = float(arr[1]) if value_type == 'Lora' and len(arr) > 1 else 0
|
||||
lora_weight_desc = f"({lora_weight:.2f})" if lora_weight > 0 else ''
|
||||
value_label = f"{model_name[:30]}{lora_weight_desc} {trigger_words}"
|
||||
lora_weight_desc = f" w:{lora_weight:.2f}" if value_type == 'Lora' and lora_weight != 1.0 else ''
|
||||
value_label = f"{model_name[:25]}{lora_weight_desc}{trigger_words}"
|
||||
|
||||
if value_type == "DiffusionModel":
|
||||
model_name = os.path.basename(os.path.splitext(value.split(",")[0])[0])
|
||||
value_label = model_name[:25]
|
||||
|
||||
if value_type in ["ModelMergeBlocks"]:
|
||||
if ":" in value:
|
||||
@@ -98,6 +102,32 @@ class easyXYPlot():
|
||||
|
||||
return plot_image_vars, value_label
|
||||
|
||||
@staticmethod
|
||||
def _ensure_latent_for_model(model, vae, samples, plot_image_vars):
|
||||
fmt = model.model.latent_format
|
||||
x = samples["samples"]
|
||||
expected_ndim = 2 + fmt.latent_dimensions
|
||||
|
||||
if x.ndim == expected_ndim and x.shape[1] == fmt.latent_channels:
|
||||
return samples
|
||||
|
||||
if fmt.latent_dimensions == 3 and x.ndim == 4:
|
||||
if x.count_nonzero() == 0:
|
||||
x = torch.zeros(
|
||||
[x.shape[0], fmt.latent_channels, 1, x.shape[2], x.shape[3]],
|
||||
dtype=x.dtype, device=x.device)
|
||||
elif plot_image_vars.get("images") is not None:
|
||||
x = vae.encode(plot_image_vars["images"][..., :3])
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Switching to a 3D-latent model requires an input image "
|
||||
"or an empty latent"
|
||||
)
|
||||
|
||||
return {**samples, "samples": x}
|
||||
|
||||
return samples
|
||||
|
||||
@staticmethod
|
||||
def get_font(font_size, font_path=None):
|
||||
if font_path is None:
|
||||
@@ -362,14 +392,46 @@ class easyXYPlot():
|
||||
if "negative_cond" in plot_image_vars:
|
||||
negative = negative + plot_image_vars["negative_cond"]
|
||||
|
||||
# DiffusionModel
|
||||
if self.x_type == "DiffusionModel" or self.y_type == "DiffusionModel":
|
||||
xy_values = x_value if self.x_type == "DiffusionModel" else y_value
|
||||
model_name, clip_name, vae_name = xy_values.split(",")
|
||||
model, clip, vae, family = self.easyCache.load_diffusion_xy_model(
|
||||
model_name.replace("*", ","),
|
||||
clip_name.replace("*", ","),
|
||||
vae_name.replace("*", ","),
|
||||
)
|
||||
sd_version = family
|
||||
|
||||
positive = plot_image_vars["positive"]
|
||||
negative = plot_image_vars["negative"]
|
||||
if positive is not None:
|
||||
positive, = CLIPTextEncode().encode(clip, positive)
|
||||
if negative is not None:
|
||||
negative, = CLIPTextEncode().encode(clip, negative)
|
||||
|
||||
samples = self._ensure_latent_for_model(
|
||||
model, vae, samples, plot_image_vars
|
||||
)
|
||||
|
||||
# Lora
|
||||
if self.x_type == "Lora" or self.y_type == "Lora":
|
||||
# print(f"Lora: {x_value} {y_value}")
|
||||
model = model if model is not None else plot_image_vars["model"]
|
||||
clip = clip if clip is not None else plot_image_vars["clip"]
|
||||
xy_values = x_value if self.x_type == "Lora" else y_value
|
||||
lora_name, lora_model_strength, lora_clip_strength, _ = xy_values.split(",")
|
||||
lora_stack = [{"lora_name": lora_name, "model": model, "clip" :clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}]
|
||||
|
||||
# Build lora_stack from both X and Y axes if both are LoRA types
|
||||
lora_stack = []
|
||||
|
||||
# Add X axis LoRA if present
|
||||
if self.x_type == "Lora":
|
||||
lora_name, lora_model_strength, lora_clip_strength, _ = x_value.split(",")
|
||||
lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)})
|
||||
|
||||
# Add Y axis LoRA if present
|
||||
if self.y_type == "Lora":
|
||||
lora_name, lora_model_strength, lora_clip_strength, _ = y_value.split(",")
|
||||
lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)})
|
||||
|
||||
# print(f"new_lora_stack: {new_lora_stack}")
|
||||
|
||||
@@ -389,7 +451,7 @@ class easyXYPlot():
|
||||
if self.x_type == 'Positive Prompt S/R' or self.y_type == 'Positive Prompt S/R':
|
||||
positive = x_value if self.x_type == "Positive Prompt S/R" else y_value
|
||||
|
||||
if sd_version == 'flux':
|
||||
if sd_version in ("flux", "anima", "krea2"):
|
||||
positive, = CLIPTextEncode().encode(clip, positive)
|
||||
else:
|
||||
positive = advanced_encode(clip, positive,
|
||||
@@ -405,7 +467,7 @@ class easyXYPlot():
|
||||
if self.x_type == 'Negative Prompt S/R' or self.y_type == 'Negative Prompt S/R':
|
||||
negative = x_value if self.x_type == "Negative Prompt S/R" else y_value
|
||||
|
||||
if sd_version == 'flux':
|
||||
if sd_version in ("flux", "anima", "krea2"):
|
||||
negative, = CLIPTextEncode().encode(clip, negative)
|
||||
else:
|
||||
negative = advanced_encode(clip, negative,
|
||||
@@ -430,7 +492,8 @@ class easyXYPlot():
|
||||
strength = item[2]
|
||||
start_percent = item[3]
|
||||
end_percent = item[4]
|
||||
positive, negative = easyControlnet().apply(control_net_name, image, positive, negative, strength, start_percent, end_percent, None, 1)
|
||||
provided_control_net = item[5] if len(item) > 5 else None
|
||||
positive, negative = easyControlnet().apply(control_net_name, image, positive, negative, strength, start_percent, end_percent, provided_control_net, 1)
|
||||
# Flux guidance
|
||||
if self.x_type == "Flux Guidance" or self.y_type == "Flux Guidance":
|
||||
positive = plot_image_vars["positive_cond"] if "positive" in plot_image_vars else None
|
||||
@@ -472,7 +535,7 @@ class easyXYPlot():
|
||||
clip = clip.clone()
|
||||
clip.clip_layer(plot_image_vars['clip_skip'])
|
||||
|
||||
if sd_version == 'flux':
|
||||
if sd_version in ("flux", "anima", "krea2"):
|
||||
positive, = CLIPTextEncode().encode(clip, positive)
|
||||
else:
|
||||
positive = advanced_encode(clip, plot_image_vars['positive'],
|
||||
@@ -480,7 +543,7 @@ class easyXYPlot():
|
||||
plot_image_vars['positive_weight_interpretation'], w_max=1.0,
|
||||
apply_to_pooled="enable",a1111_prompt_style=a1111_prompt_style, steps=steps)
|
||||
|
||||
if sd_version == 'flux':
|
||||
if sd_version in ("flux", "anima", "krea2"):
|
||||
negative, = CLIPTextEncode().encode(clip, negative)
|
||||
else:
|
||||
negative = advanced_encode(clip, plot_image_vars['negative'],
|
||||
@@ -576,28 +639,40 @@ class easyXYPlot():
|
||||
|
||||
def get_labels_and_sample(self, plot_image_vars, latent_image, preview_latent, start_step, last_step,
|
||||
force_full_denoise, disable_noise):
|
||||
for x_index, x_value in enumerate(self.x_values):
|
||||
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value,
|
||||
x_index)
|
||||
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
|
||||
if self.y_type != 'None':
|
||||
# Handle X-only variation (Y is "None")
|
||||
if self.y_type == 'None':
|
||||
for x_index, x_value in enumerate(self.x_values):
|
||||
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index)
|
||||
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
|
||||
|
||||
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
|
||||
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
|
||||
disable_noise, start_step, last_step, force_full_denoise, x_value)
|
||||
self.num += 1
|
||||
# Handle Y-only variation (X is "None")
|
||||
elif self.x_type == 'None':
|
||||
for y_index, y_value in enumerate(self.y_values):
|
||||
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index)
|
||||
self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
|
||||
|
||||
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
|
||||
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
|
||||
disable_noise, start_step, last_step, force_full_denoise, y_value=y_value)
|
||||
self.num += 1
|
||||
# Handle both X and Y variation
|
||||
else:
|
||||
for x_index, x_value in enumerate(self.x_values):
|
||||
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index)
|
||||
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
|
||||
|
||||
for y_index, y_value in enumerate(self.y_values):
|
||||
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value,
|
||||
y_index)
|
||||
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index)
|
||||
self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
|
||||
# ttNl(f'{CC.GREY}X: {x_value_label}, Y: {y_value_label}').t(
|
||||
# f'Plot Values {self.num}/{self.total} ->').p()
|
||||
|
||||
|
||||
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
|
||||
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
|
||||
disable_noise, start_step, last_step, force_full_denoise, x_value, y_value)
|
||||
self.num += 1
|
||||
else:
|
||||
# ttNl(f'{CC.GREY}X: {x_value_label}').t(f'Plot Values {self.num}/{self.total} ->').p()
|
||||
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
|
||||
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, disable_noise,
|
||||
start_step, last_step, force_full_denoise, x_value)
|
||||
self.num += 1
|
||||
|
||||
# Rearrange latent array to match preview image grid
|
||||
self.latents_plot = self.rearrange_tensors(self.latents_plot, self.num_cols, self.num_rows)
|
||||
|
||||
@@ -77,7 +77,7 @@ class ControlPixArtHalf(Module):
|
||||
|
||||
def forward_c(self, c):
|
||||
self.h, self.w = c.shape[-2]//self.patch_size, c.shape[-1]//self.patch_size
|
||||
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(c.device).to(self.dtype)
|
||||
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), pe_interpolation=self.pe_interpolation, base_size=self.base_size)).unsqueeze(0).to(c.device).to(self.dtype)
|
||||
return self.x_embedder(c) + pos_embed if c is not None else c
|
||||
|
||||
# def forward(self, x, t, c, **kwargs):
|
||||
@@ -225,7 +225,7 @@ class ControlPixArtMSHalf(ControlPixArtHalf):
|
||||
c_size, ar = data_info['img_hw'].to(self.dtype), data_info['aspect_ratio'].to(self.dtype)
|
||||
self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size
|
||||
|
||||
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(x.device).to(self.dtype)
|
||||
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), pe_interpolation=self.pe_interpolation, base_size=self.base_size)).unsqueeze(0).to(x.device).to(self.dtype)
|
||||
x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2
|
||||
t = self.t_embedder(timestep) # (N, D)
|
||||
csize = self.csize_embedder(c_size, bs) # (N, D)
|
||||
|
||||
@@ -28,7 +28,7 @@ class DoubleStreamBlockIPA(nn.Module):
|
||||
|
||||
self.txt_norm2 = original_block.txt_norm2
|
||||
self.txt_mlp = original_block.txt_mlp
|
||||
self.flipped_img_txt = original_block.flipped_img_txt
|
||||
self.flipped_img_txt = getattr(original_block, 'flipped_img_txt', False)
|
||||
|
||||
self.ip_adapter = ip_adapter
|
||||
self.image_emb = image_emb
|
||||
|
||||
@@ -150,7 +150,10 @@ class LayerDiffuse:
|
||||
alpha = pixel_with_alpha[..., 0]
|
||||
|
||||
alpha = 1.0 - alpha
|
||||
new_images, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
|
||||
try:
|
||||
new_images, = JoinImageWithAlpha().execute(image, alpha)
|
||||
except:
|
||||
new_images, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
|
||||
return new_images, alpha
|
||||
|
||||
def make_3d_mask(self, mask):
|
||||
|
||||
+38
-2
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
import torch
|
||||
import folder_paths
|
||||
import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models
|
||||
from comfy_extras.nodes_compositing import JoinImageWithAlpha
|
||||
from comfy.clip_vision import load as load_clip_vision
|
||||
@@ -9,12 +10,39 @@ from ..config import *
|
||||
|
||||
from ..libs.log import log_node_info, log_node_warn
|
||||
from ..libs.utils import get_local_filepath, get_sd_version
|
||||
from ..libs.wildcards import process_with_loras
|
||||
from ..libs.controlnet import easyControlnet
|
||||
from ..libs.conditioning import prompt_to_cond
|
||||
from ..libs import cache as backend_cache
|
||||
|
||||
from .. import easyCache
|
||||
|
||||
class applyLoraPrompt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
"positive": ("STRING", {"default": "", "forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"negative": ("STRING", {"default": "", "forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING")
|
||||
RETURN_NAMES = ("model", "clip", "positive", "negative")
|
||||
CATEGORY = "EasyUse/Adapter"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, clip, positive, negative=None):
|
||||
model, clip, positive, _, _, _ = process_with_loras(positive, model, clip, 'Positive', easyCache=easyCache)
|
||||
if negative is not None:
|
||||
model, clip, negative, _, _, _ = process_with_loras(negative, model, clip, 'Negative', easyCache=easyCache)
|
||||
|
||||
return (model, clip, positive, negative if negative is not None else "")
|
||||
|
||||
class applyLoraStack:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -153,7 +181,10 @@ class icLightApply:
|
||||
image = self.removebg(image)
|
||||
else:
|
||||
mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu")
|
||||
image, = JoinImageWithAlpha().join_image_with_alpha(image, mask)
|
||||
try:
|
||||
image, = JoinImageWithAlpha().execute(image, mask)
|
||||
except:
|
||||
image, = JoinImageWithAlpha().join_image_with_alpha(image, mask)
|
||||
|
||||
iclight = ICLight()
|
||||
if mode == 'Foreground':
|
||||
@@ -163,7 +194,10 @@ class icLightApply:
|
||||
if source not in ['Use Background Image', 'Use Flipped Background Image']:
|
||||
_, height, width, _ = lighting_image.shape
|
||||
mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu")
|
||||
lighting_image, = JoinImageWithAlpha().join_image_with_alpha(lighting_image, mask)
|
||||
try:
|
||||
lighting_image, = JoinImageWithAlpha().execute(lighting_image, mask)
|
||||
except:
|
||||
lighting_image, = JoinImageWithAlpha().join_image_with_alpha(lighting_image, mask)
|
||||
if batch_size < 2:
|
||||
image = self.batch(image, lighting_image)
|
||||
else:
|
||||
@@ -1284,6 +1318,7 @@ class applyPulIDADV(applyPulID):
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy loraPromptApply": applyLoraPrompt,
|
||||
"easy loraStackApply": applyLoraStack,
|
||||
"easy controlnetStackApply": applyControlnetStack,
|
||||
"easy ipadapterApply": ipadapterApply,
|
||||
@@ -1303,6 +1338,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy loraPromptApply": "Easy Apply LoraPrompt",
|
||||
"easy loraStackApply": "Easy Apply LoraStack",
|
||||
"easy controlnetStackApply": "Easy Apply CnetStack",
|
||||
"easy ipadapterApply": "Easy Apply IPAdapter",
|
||||
|
||||
+9
-35
@@ -3,37 +3,8 @@ from ..libs.api.fluxai import fluxaiAPI
|
||||
from ..libs.api.bizyair import bizyairAPI, encode_data
|
||||
from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
|
||||
|
||||
class fluxPromptGenAPI:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"describe": ("STRING", {"default": "", "placeholder": "Describe your image idea (you can use any language)", "multiline": True}),
|
||||
},
|
||||
"optional": {
|
||||
"cookie_override": ("STRING", {"default": "", "forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
|
||||
FUNCTION = "generate"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
CATEGORY = "EasyUse/API"
|
||||
|
||||
def generate(self, describe, cookie_override=None, prompt=None, unique_id=None, extra_pnginfo=None):
|
||||
prompt = fluxaiAPI.promptGenerate(describe, cookie_override)
|
||||
return (prompt,)
|
||||
|
||||
class joyCaption2API:
|
||||
API_URL = f"/supernode/joycaption2"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -110,12 +81,12 @@ class joyCaption2API:
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("caption",)
|
||||
|
||||
FUNCTION = "joycaption2"
|
||||
FUNCTION = "joycaption"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
CATEGORY = "EasyUse/API"
|
||||
|
||||
def joycaption2(
|
||||
def joycaption(
|
||||
self,
|
||||
image,
|
||||
do_sample,
|
||||
@@ -149,17 +120,20 @@ class joyCaption2API:
|
||||
}
|
||||
|
||||
pbar.update_absolute(30)
|
||||
caption = bizyairAPI.joyCaption2(payload, image, apikey_override)
|
||||
caption = bizyairAPI.joyCaption(payload, image, apikey_override, API_URL=self.API_URL)
|
||||
|
||||
pbar.update_absolute(100)
|
||||
return (caption,)
|
||||
|
||||
class joyCaption3API(joyCaption2API):
|
||||
API_URL = f"/supernode/joycaption3"
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy fluxPromptGenAPI": fluxPromptGenAPI,
|
||||
"easy joyCaption2API": joyCaption2API,
|
||||
"easy joyCaption3API": joyCaption3API,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy fluxPromptGenAPI": "Prompt Gen (FluxAI)",
|
||||
"easy joyCaption2API": "JoyCaption2 (BizyAIR)",
|
||||
"easy joyCaption3API": "JoyCaption3 (BizyAIR)",
|
||||
}
|
||||
+6
-2
@@ -438,7 +438,11 @@ class detailerFix:
|
||||
# Clean loaded_objects
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
my_unique_id = int(my_unique_id)
|
||||
# my_unique_id can be a composite ID (e.g. `101:134`) when put inside a sub-graph; this fixes it so that it can support sub-graphs
|
||||
try:
|
||||
my_unique_id = int(my_unique_id)
|
||||
except (ValueError, TypeError):
|
||||
my_unique_id = int(str(my_unique_id).split(':')[-1])
|
||||
|
||||
model = model or (pipe["model"] if "model" in pipe else None)
|
||||
if model is None:
|
||||
@@ -640,4 +644,4 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy ultralyticsDetectorPipe": "UltralyticsDetector (Pipe)",
|
||||
"easy samLoaderPipe": "SAMLoader (Pipe)",
|
||||
"easy detailerFix": "DetailerFix",
|
||||
}
|
||||
}
|
||||
|
||||
+158
-90
@@ -4,19 +4,19 @@ import torch
|
||||
import numpy as np
|
||||
import comfy.utils
|
||||
import comfy.model_management
|
||||
import shutil
|
||||
from comfy_extras.nodes_compositing import JoinImageWithAlpha
|
||||
from server import PromptServer
|
||||
from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageOps
|
||||
import torch.nn.functional as F
|
||||
from torchvision.transforms import Resize, CenterCrop, GaussianBlur
|
||||
from torchvision.transforms import Resize, CenterCrop, GaussianBlur, ToPILImage
|
||||
from torchvision.transforms.functional import to_pil_image
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple
|
||||
from ..libs.cache import cache, update_cache, remove_cache
|
||||
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image, fit_resize_image
|
||||
from ..libs.colorfix import adain_color_fix, wavelet_color_fix
|
||||
from ..libs.chooser import ChooserMessage, ChooserCancelled
|
||||
from ..config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR
|
||||
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
@@ -1005,12 +1005,14 @@ class imageRemBg:
|
||||
"result": (new_images, masks)}
|
||||
|
||||
# 图像选择器
|
||||
from ..libs.chooser import wait_for_chooser
|
||||
class imageChooser(PreviewImage):
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
return {
|
||||
"required":{
|
||||
"mode": (['Always Pause', 'Keep Last Selection'], {"default": "Always Pause"}),
|
||||
"preview_rescale": ("FLOAT", {"default": 1.0, "min": 0.05, "max": 1.0, "step": 0.05}),
|
||||
},
|
||||
"optional": {
|
||||
"images": ("IMAGE",),
|
||||
@@ -1041,55 +1043,36 @@ class imageChooser(PreviewImage):
|
||||
def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
|
||||
id = my_unique_id[0]
|
||||
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
|
||||
if id not in ChooserMessage.stash:
|
||||
ChooserMessage.stash[id] = {}
|
||||
my_stash = ChooserMessage.stash[id]
|
||||
|
||||
# enable stashing. If images is None, we are operating in read-from-stash mode
|
||||
if 'images' in kwargs:
|
||||
my_stash['images'] = kwargs['images']
|
||||
else:
|
||||
kwargs['images'] = my_stash.get('images', None)
|
||||
|
||||
if (kwargs['images'] is None):
|
||||
return (None, None, None, "")
|
||||
if (kwargs.get('images') is None):
|
||||
return (torch.zeros(1, 1, 1, 3),)
|
||||
|
||||
images_in = torch.cat(kwargs.pop('images'))
|
||||
self.batch = images_in.shape[0]
|
||||
for x in kwargs: kwargs[x] = kwargs[x][0]
|
||||
|
||||
try:
|
||||
pnginfo = extra_pnginfo[0]
|
||||
except:
|
||||
pnginfo = None
|
||||
result = self.save_images(images=images_in, prompt=prompt, extra_pnginfo=pnginfo)
|
||||
|
||||
images = result['ui']['images']
|
||||
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
|
||||
preview_rescale = kwargs.pop('preview_rescale', 1.0)
|
||||
if preview_rescale < 1.0:
|
||||
images_preview, = imageScaleDownBy().image_scale_down_by(images_in, preview_rescale)
|
||||
else:
|
||||
images_preview = images_in
|
||||
result = self.save_images(images=images_preview, prompt=prompt, extra_pnginfo=pnginfo)
|
||||
if "ui" in result and "images" in result['ui']:
|
||||
images = result["ui"]["images"]
|
||||
else:
|
||||
images = []
|
||||
try:
|
||||
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# 获取上次选择
|
||||
mode = kwargs.pop('mode', 'Always Pause')
|
||||
last_choosen = None
|
||||
if mode == 'Keep Last Selection':
|
||||
if not extra_pnginfo:
|
||||
print("Error: extra_pnginfo is empty")
|
||||
elif (not isinstance(extra_pnginfo[0], dict) or "workflow" not in extra_pnginfo[0]):
|
||||
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
|
||||
else:
|
||||
workflow = extra_pnginfo[0]["workflow"]
|
||||
node = next((x for x in workflow["nodes"] if str(x["id"]) == id), None)
|
||||
if node:
|
||||
last_choosen = node['properties']['values']
|
||||
|
||||
# wait for selection
|
||||
try:
|
||||
selections = ChooserMessage.waitForMessage(id, asList=True) if last_choosen is None or len(last_choosen)<1 else last_choosen
|
||||
choosen = [x for x in selections if x >= 0] if len(selections)>1 else [0]
|
||||
except ChooserCancelled:
|
||||
raise comfy.model_management.InterruptProcessingException()
|
||||
|
||||
return {"ui": {"images": images},
|
||||
"result": (self.tensor_bundle(images_in, choosen),)}
|
||||
return wait_for_chooser(id, images_in, mode)
|
||||
|
||||
class imageColorMatch(PreviewImage):
|
||||
@classmethod
|
||||
@@ -1235,6 +1218,8 @@ class imageDetailTransfer:
|
||||
new_image = torch.lerp(target_tensor, new_image, blend_factor)
|
||||
if mask is not None:
|
||||
mask = mask.to(device)
|
||||
if mask.dim() == 3: # (B,H,W) batch/video mask -> (B,1,H,W) so it broadcasts over channels
|
||||
mask = mask.unsqueeze(1)
|
||||
new_image = torch.lerp(target_tensor, new_image, mask)
|
||||
new_image = torch.clamp(new_image, 0, 1)
|
||||
new_image = new_image.permute(0, 2, 3, 1).cpu().float()
|
||||
@@ -1277,13 +1262,22 @@ class humanSegmentation:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
return {
|
||||
"required":{
|
||||
"image": ("IMAGE",),
|
||||
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)"],),
|
||||
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)", "segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"],),
|
||||
"confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},),
|
||||
"crop_multi": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.001},),
|
||||
"mask_components":(
|
||||
"EASY_COMBO",{
|
||||
"options": [{'label':'Background','value':0}],
|
||||
"multi_select": {
|
||||
"placeholder": "select mask components",
|
||||
"chip": True,
|
||||
"max_selected_labels": 4,
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
@@ -1309,12 +1303,12 @@ class humanSegmentation:
|
||||
numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB)
|
||||
return mp.Image(image_format=image_format, data=numpy_image)
|
||||
|
||||
def parsing(self, image, confidence, method, crop_multi, prompt=None, my_unique_id=None):
|
||||
mask_components = []
|
||||
if my_unique_id in prompt:
|
||||
if prompt[my_unique_id]["inputs"]['mask_components']:
|
||||
mask_components = prompt[my_unique_id]["inputs"]['mask_components'].split(',')
|
||||
mask_components = list(map(int, mask_components))
|
||||
def parsing(self, image, confidence, method, crop_multi, mask_components, prompt=None, my_unique_id=None):
|
||||
if isinstance(mask_components, str):
|
||||
mask_components = [int(x) for x in mask_components.split(',') if x]
|
||||
else:
|
||||
mask_components = mask_components if mask_components else []
|
||||
|
||||
if method == 'selfie_multiclass_256x256':
|
||||
try:
|
||||
import mediapipe as mp
|
||||
@@ -1341,6 +1335,9 @@ class humanSegmentation:
|
||||
ret_images = []
|
||||
ret_masks = []
|
||||
|
||||
if len(mask_components) == 0:
|
||||
return (image, torch.zeros_like(image[:, :, :, 0:1]), torch.tensor([0,0,0,0]))
|
||||
|
||||
with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter:
|
||||
for img in image:
|
||||
_image = torch.unsqueeze(img, 0)
|
||||
@@ -1374,7 +1371,14 @@ class humanSegmentation:
|
||||
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_2d = mask.numpy_view()
|
||||
if mask_2d.ndim == 3 and mask_2d.shape[2] == 1:
|
||||
mask_2d = mask_2d.squeeze(axis=2)
|
||||
elif mask_2d.ndim != 2:
|
||||
raise ValueError(f"Unexpected mask shape: {mask_2d.shape}")
|
||||
condition = np.stack((mask_2d,) * image_shape[-1], axis=-1) > confidence
|
||||
if condition.ndim == 4 and condition.shape[2] == 1:
|
||||
condition = condition.squeeze(2)
|
||||
mask_array = np.where(condition, mask_foreground_array, mask_background_array)
|
||||
mask_arrays.append(mask_array)
|
||||
# Merge our masks taking the maximum from each
|
||||
@@ -1416,7 +1420,11 @@ class humanSegmentation:
|
||||
|
||||
alpha = 1.0 - mask
|
||||
|
||||
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
|
||||
try:
|
||||
output_image, = JoinImageWithAlpha().execute(image, alpha)
|
||||
except:
|
||||
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
|
||||
|
||||
|
||||
elif method == "human_parts (deeplabv3p)":
|
||||
if method in cache:
|
||||
@@ -1442,6 +1450,107 @@ class humanSegmentation:
|
||||
output_image = torch.cat(ret_images, dim=0)
|
||||
mask = torch.cat(ret_masks, dim=0)
|
||||
|
||||
elif method in ["segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"]:
|
||||
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
|
||||
|
||||
# 分割
|
||||
def get_segmentation_from_model(tensor_image, model, processor):
|
||||
cloth = tensor2pil(tensor_image)
|
||||
inputs = processor(images=cloth, return_tensors="pt")
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.logits.cpu()
|
||||
upsampled_logits = F.interpolate(logits, size=cloth.size[::-1], mode="bilinear",
|
||||
align_corners=False)
|
||||
pred_seg = upsampled_logits.argmax(dim=1)[0].numpy()
|
||||
return pred_seg, cloth
|
||||
|
||||
|
||||
if method in cache:
|
||||
_, (processor, model) = cache[method][1]
|
||||
else:
|
||||
model_folder_path = os.path.join(folder_paths.models_dir, method)
|
||||
if os.path.exists(model_folder_path):
|
||||
print(f"Start to load existing model...")
|
||||
else:
|
||||
from huggingface_hub import snapshot_download
|
||||
PromptServer.instance.send_sync("easyuse-toast", {"content": f"Model not found locally. Downloading {method}...", "type": 'loading', "duration": 10000})
|
||||
print(f"Model not found locally. Downloading {method}...")
|
||||
model_path_cache = os.path.join(folder_paths.models_dir, "cache-"+method)
|
||||
snapshot_download(
|
||||
repo_id=HUMANPARSING_MODELS[method]['model_name'],
|
||||
local_dir=model_path_cache,
|
||||
local_dir_use_symlinks=False,
|
||||
resume_download=True
|
||||
)
|
||||
shutil.move(model_path_cache, model_folder_path)
|
||||
print(f"Model downloaded to {model_folder_path}...")
|
||||
try:
|
||||
model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[method][0][0])
|
||||
except:
|
||||
pass
|
||||
|
||||
processor = SegformerImageProcessor.from_pretrained(model_folder_path)
|
||||
model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path)
|
||||
update_cache(method, 'human_segmentation', (False, (processor, model)))
|
||||
|
||||
ret_images = []
|
||||
ret_masks = []
|
||||
|
||||
if method == "face_parsing":
|
||||
import matplotlib
|
||||
import torchvision.transforms as T
|
||||
transform = ToPILImage()
|
||||
colormap = matplotlib.colormaps['viridis']
|
||||
device = model.device
|
||||
results = []
|
||||
images = []
|
||||
for img in image:
|
||||
size = img.shape[:2]
|
||||
inputs = processor(images=transform(img.permute(2, 0, 1)), return_tensors="pt")
|
||||
inputs = {k: v.to(device) for k, v in inputs.items()}
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.logits
|
||||
upsampled_logits = F.interpolate(
|
||||
logits,
|
||||
size=size,
|
||||
mode="bilinear",
|
||||
align_corners=False)
|
||||
|
||||
pred_seg = upsampled_logits.argmax(dim=1)[0]
|
||||
pred_seg_np = pred_seg.cpu().detach().numpy().astype(np.uint8)
|
||||
results.append(torch.tensor(pred_seg_np))
|
||||
|
||||
results_out = torch.stack(results, dim=0)
|
||||
for img, result_item in zip(image, results_out):
|
||||
mask = torch.zeros(result_item.shape, dtype=torch.uint8)
|
||||
for i in mask_components:
|
||||
mask = mask | torch.where(result_item == i, 1, 0)
|
||||
|
||||
# 将mask转换为numpy数组,并确保数据类型正确
|
||||
mask_np = (mask * 255).numpy().astype(np.uint8)
|
||||
_mask = Image.fromarray(mask_np)
|
||||
|
||||
# 处理图像输出
|
||||
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
|
||||
ret_images.append(pil2tensor(ret_image))
|
||||
ret_masks.append(image2mask(_mask))
|
||||
|
||||
else:
|
||||
for img in image:
|
||||
pred_seg, cloth = get_segmentation_from_model(img, model, processor)
|
||||
i = torch.unsqueeze(img, 0)
|
||||
i = pil2tensor(tensor2pil(i).convert('RGB'))
|
||||
|
||||
mask = np.isin(pred_seg, mask_components).astype(np.uint8)
|
||||
_mask = Image.fromarray(mask * 255)
|
||||
|
||||
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
|
||||
ret_images.append(pil2tensor(ret_image))
|
||||
ret_masks.append(image2mask(_mask))
|
||||
|
||||
output_image = torch.cat(ret_images, dim=0)
|
||||
mask = torch.cat(ret_masks, dim=0)
|
||||
|
||||
# use crop
|
||||
bbox = [[0, 0, 0, 0]]
|
||||
if crop_multi > 0.0:
|
||||
@@ -1892,47 +2001,6 @@ class loadImagesForLoop:
|
||||
"result": tuple(["stub", index, image, mask, name] + outputs),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
# 姿势编辑器
|
||||
# class poseEditor:
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(self):
|
||||
# temp_dir = folder_paths.get_temp_directory()
|
||||
#
|
||||
# if not os.path.isdir(temp_dir):
|
||||
# os.makedirs(temp_dir)
|
||||
#
|
||||
# temp_dir = folder_paths.get_temp_directory()
|
||||
#
|
||||
# return {"required":
|
||||
# {"image": (sorted(os.listdir(temp_dir)),)},
|
||||
# }
|
||||
#
|
||||
# RETURN_TYPES = ("IMAGE",)
|
||||
# FUNCTION = "output_pose"
|
||||
#
|
||||
# CATEGORY = "EasyUse/🚫 Deprecated"
|
||||
#
|
||||
# def output_pose(self, image):
|
||||
# image_path = os.path.join(folder_paths.get_temp_directory(), image)
|
||||
# # print(f"Create: {image_path}")
|
||||
#
|
||||
# i = Image.open(image_path)
|
||||
# image = i.convert("RGB")
|
||||
# image = np.array(image).astype(np.float32) / 255.0
|
||||
# image = torch.from_numpy(image)[None,]
|
||||
#
|
||||
# return (image,)
|
||||
#
|
||||
# @classmethod
|
||||
# def IS_CHANGED(self, image):
|
||||
# image_path = os.path.join(
|
||||
# folder_paths.get_temp_directory(), image)
|
||||
# # print(f'Change: {image_path}')
|
||||
#
|
||||
# m = hashlib.sha256()
|
||||
# with open(image_path, 'rb') as f:
|
||||
# m.update(f.read())
|
||||
# return m.digest().hex()
|
||||
|
||||
class makeImageForICRepaint:
|
||||
@classmethod
|
||||
@@ -2022,7 +2090,7 @@ class makeImageForICRepaint:
|
||||
image, mask, context_mask = None, None, None
|
||||
|
||||
# resize
|
||||
if img1_h != img2_h or img1_w != img2_w:
|
||||
if img1_h != img2_h and img1_w != img2_w:
|
||||
width, height = img2_w, img2_h
|
||||
fit = 'crop'
|
||||
if method != 'uniform width':
|
||||
|
||||
+4
-1
@@ -331,7 +331,10 @@ class applyInpaint:
|
||||
new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, grow_mask_by, noise_mask=noise_mask)
|
||||
cls = ALL_NODE_CLASS_MAPPINGS['DifferentialDiffusion']
|
||||
if cls is not None:
|
||||
model, = cls().apply(new_pipe['model'])
|
||||
try:
|
||||
model, = cls().execute(new_pipe['model'])
|
||||
except Exception:
|
||||
model, = cls().apply(new_pipe['model'])
|
||||
new_pipe['model'] = model
|
||||
else:
|
||||
raise Exception("Differential Diffusion not found,please update comfyui")
|
||||
|
||||
+152
-4
@@ -8,7 +8,7 @@ from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
|
||||
|
||||
from ..libs.log import log_node_info, log_node_error, log_node_warn
|
||||
from ..libs.wildcards import process_with_loras
|
||||
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version
|
||||
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version, AlwaysEqualProxy
|
||||
from ..libs.sampler import easySampler
|
||||
from ..libs.controlnet import easyControlnet, union_controlnet_types
|
||||
from ..libs.conditioning import prompt_to_cond
|
||||
@@ -19,6 +19,7 @@ from ..config import *
|
||||
|
||||
from .. import easyCache, sampler
|
||||
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
# 简易加载器完整
|
||||
resolution_strings = [f"{width} x {height} (custom)" if width == 'width' and height == 'height' else f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
|
||||
class fullLoader:
|
||||
@@ -1145,7 +1146,144 @@ class mochiLoader(fullLoader):
|
||||
batch_size, model_override, clip_override, vae_override, a1111_prompt_style=False, video_length=length, prompt=prompt,
|
||||
my_unique_id=my_unique_id
|
||||
)
|
||||
# Diffusion model loader
|
||||
class diffusionModelLoader:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (folder_paths.get_filename_list("diffusion_models"),),
|
||||
"vae_name": (["None"] + folder_paths.get_filename_list("vae"), {"default": "None"}),
|
||||
"clip_name": (["None"] + folder_paths.get_filename_list("text_encoders"), {"default": "None"}),
|
||||
"resolution": (resolution_strings, {"default": "1024 x 1024"}),
|
||||
"empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
|
||||
"empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
|
||||
"positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"negative": ("STRING", {"default": "", "multiline": True}),
|
||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
|
||||
},
|
||||
"optional": {
|
||||
"model_override": ("MODEL",),
|
||||
"clip_override": ("CLIP",),
|
||||
"vae_override": ("VAE",),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE", "CLIP", "CONDITIONING", "CONDITIONING", "LATENT")
|
||||
RETURN_NAMES = ("pipe", "model", "vae", "clip", "positive", "negative", "latent")
|
||||
FUNCTION = "adv_pipeloader"
|
||||
CATEGORY = "EasyUse/Loaders"
|
||||
|
||||
def adv_pipeloader(self, model_name, vae_name, clip_name, resolution,
|
||||
empty_latent_width, empty_latent_height, positive, negative,
|
||||
batch_size, model_override=None, clip_override=None,
|
||||
vae_override=None, prompt=None, my_unique_id=None):
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
model, clip, vae, family = easyCache.load_diffusion_model_required(
|
||||
model_name, clip_name, vae_name
|
||||
)
|
||||
|
||||
if model_override is not None:
|
||||
model = model_override
|
||||
if clip_override is not None:
|
||||
clip = clip_override
|
||||
if vae_override is not None:
|
||||
vae = vae_override
|
||||
|
||||
samples = sampler.emptyLatent(resolution, empty_latent_width,
|
||||
empty_latent_height, batch_size,
|
||||
model_type=family)
|
||||
|
||||
positive_cond, positive_wildcard, model, clip = prompt_to_cond(
|
||||
"positive", model, clip, 0, [], positive, "none", "comfy",
|
||||
False, my_unique_id, prompt, easyCache, model_type=family)
|
||||
negative_cond, negative_wildcard, model, clip = prompt_to_cond(
|
||||
"negative", model, clip, 0, [], negative, "none", "comfy",
|
||||
False, my_unique_id, prompt, easyCache, model_type=family)
|
||||
|
||||
if negative_cond is None:
|
||||
negative_cond, = ConditioningZeroOut().zero_out(positive_cond)
|
||||
|
||||
pipe = {
|
||||
"model": model,
|
||||
"positive": positive_cond,
|
||||
"negative": negative_cond,
|
||||
"vae": vae,
|
||||
"clip": clip,
|
||||
"samples": samples,
|
||||
"images": None,
|
||||
"loader_settings": {
|
||||
"model_name": model_name,
|
||||
"clip_name": clip_name,
|
||||
"vae_name": vae_name,
|
||||
"model_type": family,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
"resolution": resolution,
|
||||
"empty_latent_width": empty_latent_width,
|
||||
"empty_latent_height": empty_latent_height,
|
||||
"batch_size": batch_size,
|
||||
},
|
||||
}
|
||||
|
||||
return pipe, model, vae, clip, positive_cond, negative_cond, samples
|
||||
|
||||
# lora
|
||||
class loraSwitcher:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
max_lora_num = 50
|
||||
inputs = {
|
||||
"required": {
|
||||
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
|
||||
"select": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
|
||||
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
|
||||
"lora_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
|
||||
},
|
||||
"optional": {
|
||||
"optional_lora_stack": ("LORA_STACK",),
|
||||
},
|
||||
}
|
||||
|
||||
for i in range(1, max_lora_num + 1):
|
||||
inputs["optional"][f"lora_{i}_name"] = (
|
||||
["None"] + folder_paths.get_filename_list("loras"), {"default": "None"})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("LORA_STACK", any_type)
|
||||
RETURN_NAMES = ("lora_stack", "lora_name")
|
||||
FUNCTION = "stack"
|
||||
|
||||
CATEGORY = "EasyUse/Loaders"
|
||||
|
||||
def stack(self, toggle, select,num_loras, lora_strength, optional_lora_stack=None, **kwargs):
|
||||
if toggle in [False, None, "False"]:
|
||||
return (optional_lora_stack, '')
|
||||
|
||||
if not kwargs and optional_lora_stack is None:
|
||||
return (None, '')
|
||||
|
||||
loras = []
|
||||
|
||||
# Import Stack values
|
||||
if optional_lora_stack is not None:
|
||||
loras.extend([l for l in optional_lora_stack if l[0] != "None"])
|
||||
|
||||
# Import Lora values
|
||||
lora_name = kwargs.get(f"lora_{select}_name")
|
||||
|
||||
if not lora_name or lora_name == "None":
|
||||
return (None,'')
|
||||
|
||||
loras.append((lora_name, lora_strength, lora_strength))
|
||||
|
||||
name = os.path.splitext(os.path.basename(str(lora_name)))[0]
|
||||
return (loras, name)
|
||||
|
||||
|
||||
class loraStack:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -1155,7 +1293,7 @@ class loraStack:
|
||||
max_lora_num = 10
|
||||
inputs = {
|
||||
"required": {
|
||||
"toggle": ("BOOLEAN", {"label_on": "enabled", "label_off": "disabled"}),
|
||||
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
|
||||
"mode": (["simple", "advanced"],),
|
||||
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
|
||||
},
|
||||
@@ -1183,7 +1321,10 @@ class loraStack:
|
||||
CATEGORY = "EasyUse/Loaders"
|
||||
|
||||
def stack(self, toggle, mode, num_loras, optional_lora_stack=None, **kwargs):
|
||||
if (toggle in [False, None, "False"]) or not kwargs:
|
||||
if toggle in [False, None, "False"]:
|
||||
return (optional_lora_stack,)
|
||||
|
||||
if not kwargs and optional_lora_stack is None:
|
||||
return (None,)
|
||||
|
||||
loras = []
|
||||
@@ -1240,7 +1381,10 @@ class controlnetStack:
|
||||
CATEGORY = "EasyUse/Loaders"
|
||||
|
||||
def stack(self, toggle, mode, num_controlnet, optional_controlnet_stack=None, **kwargs):
|
||||
if (toggle in [False, None, "False"]) or not kwargs:
|
||||
if toggle in [False, None, "False"]:
|
||||
return (optional_controlnet_stack,)
|
||||
|
||||
if not kwargs and optional_controlnet_stack is None:
|
||||
return (None,)
|
||||
|
||||
controlnets = []
|
||||
@@ -1482,6 +1626,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy hunyuanDiTLoader": hunyuanDiTLoader,
|
||||
"easy pixArtLoader": pixArtLoader,
|
||||
"easy mochiLoader": mochiLoader,
|
||||
"easy diffusionModelLoader": diffusionModelLoader,
|
||||
"easy loraSwitcher": loraSwitcher,
|
||||
"easy loraStack": loraStack,
|
||||
"easy controlnetStack": controlnetStack,
|
||||
"easy controlnetLoader": controlnetSimple,
|
||||
@@ -1503,6 +1649,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy hunyuanDiTLoader": "EasyLoader (HunyuanDiT)",
|
||||
"easy pixArtLoader": "EasyLoader (PixArt)",
|
||||
"easy mochiLoader": "EasyLoader (Mochi)",
|
||||
"easy diffusionModelLoader": "EasyDiffusionModelLoader",
|
||||
"easy loraSwitcher": "EasyLoraSwitcher",
|
||||
"easy loraStack": "EasyLoraStack",
|
||||
"easy controlnetStack": "EasyControlnetStack",
|
||||
"easy controlnetLoader": "EasyControlnet",
|
||||
|
||||
+1057
-1045
File diff suppressed because it is too large
Load Diff
+24
-14
@@ -554,13 +554,17 @@ class pipeXYPlotAdvanced:
|
||||
if font_path and not os.path.exists(font_path):
|
||||
font_path = os.path.join(self.user_font_dir, font)
|
||||
|
||||
if X != None:
|
||||
if X is not None:
|
||||
if isinstance(X, tuple):
|
||||
X = X[0]
|
||||
x_axis = X.get('axis')
|
||||
x_values = X.get('values')
|
||||
else:
|
||||
x_axis = "Nothing"
|
||||
x_values = [""]
|
||||
if Y != None:
|
||||
if Y is not None:
|
||||
if isinstance(Y, tuple):
|
||||
Y = Y[0]
|
||||
y_axis = Y.get('axis')
|
||||
y_values = Y.get('values')
|
||||
else:
|
||||
@@ -625,20 +629,26 @@ class pipeXYPlotAdvanced:
|
||||
"lora_stack": lora_stack,
|
||||
}
|
||||
|
||||
if x_axis == "advanced: DiffusionModel":
|
||||
x_values = "; ".join(x_values)
|
||||
|
||||
if y_axis == "advanced: DiffusionModel":
|
||||
y_values = "; ".join(y_values)
|
||||
|
||||
if x_axis == 'advanced: Seeds++ Batch':
|
||||
if new_pipe['seed']:
|
||||
value = x_values
|
||||
x_values = []
|
||||
for index in range(value):
|
||||
x_values.append(str(new_pipe['seed'] + index))
|
||||
x_values = "; ".join(x_values)
|
||||
seed = new_pipe.get('seed') or 0
|
||||
value = x_values
|
||||
x_values = []
|
||||
for index in range(value):
|
||||
x_values.append(str(seed + index))
|
||||
x_values = "; ".join(x_values)
|
||||
if y_axis == 'advanced: Seeds++ Batch':
|
||||
if new_pipe['seed']:
|
||||
value = y_values
|
||||
y_values = []
|
||||
for index in range(value):
|
||||
y_values.append(str(new_pipe['seed'] + index))
|
||||
y_values = "; ".join(y_values)
|
||||
seed = new_pipe.get('seed') or 0
|
||||
value = y_values
|
||||
y_values = []
|
||||
for index in range(value):
|
||||
y_values.append(str(seed + index))
|
||||
y_values = "; ".join(y_values)
|
||||
|
||||
if x_axis == 'advanced: Positive Prompt S/R':
|
||||
if positive:
|
||||
|
||||
@@ -262,15 +262,16 @@ class samplerSettingsNoiseIn:
|
||||
model = pipe["model"]
|
||||
|
||||
# generate base noise
|
||||
batch_size, _, height, width = latent["samples"].shape
|
||||
sample_shape = latent["samples"].shape
|
||||
batch_size = sample_shape[0]
|
||||
generator = torch.manual_seed(seed)
|
||||
base_noise = torch.randn((1, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).repeat(batch_size, 1, 1, 1).cpu()
|
||||
base_noise = torch.randn((1, *sample_shape[1:]), dtype=torch.float32, device="cpu", generator=generator).repeat(batch_size, *([1] * (len(sample_shape) - 1))).cpu()
|
||||
|
||||
# generate variation noise
|
||||
if optional_noise_seed is None or optional_noise_seed == seed:
|
||||
optional_noise_seed = seed+1
|
||||
generator = torch.manual_seed(optional_noise_seed)
|
||||
variation_noise = torch.randn((batch_size, 4, height, width), dtype=torch.float32, device="cpu",
|
||||
variation_noise = torch.randn(sample_shape, dtype=torch.float32, device="cpu",
|
||||
generator=generator).cpu()
|
||||
|
||||
slerp_noise = self.slerp(factor, base_noise, variation_noise)
|
||||
@@ -367,7 +368,7 @@ class samplerCustomSettings:
|
||||
FUNCTION = "settings"
|
||||
CATEGORY = "EasyUse/PreSampling"
|
||||
|
||||
def ip2p(self, positive, negative, vae, pixels, latent=None):
|
||||
def ip2p(self, positive, negative, vae=None, pixels=None, latent=None):
|
||||
if latent is not None:
|
||||
concat_latent = latent
|
||||
else:
|
||||
|
||||
+489
-279
@@ -1,185 +1,186 @@
|
||||
import json
|
||||
import os
|
||||
from urllib.request import urlopen
|
||||
|
||||
import folder_paths
|
||||
|
||||
from .. import easyCache
|
||||
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
|
||||
|
||||
from comfy_api.latest import io
|
||||
|
||||
|
||||
# 正面提示词
|
||||
class positivePrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class positivePrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"positive": ("STRING", {"default": "", "multiline": True, "placeholder": "Positive"}),}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy positive",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("positive", default="", multiline=True, placeholder="Positive"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_positive", display_name="positive"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("positive",)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
@staticmethod
|
||||
def main(positive):
|
||||
return positive,
|
||||
@classmethod
|
||||
def execute(cls, positive):
|
||||
return io.NodeOutput(positive)
|
||||
|
||||
# 通配符提示词
|
||||
class wildcardsPrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class wildcardsPrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
|
||||
"multiline_mode": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
return io.Schema(
|
||||
node_id="easy wildcards",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support wildcard)"),
|
||||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||||
io.Int.Input("seed", default=0, min=0, max=MAX_SEED_NUM),
|
||||
io.Boolean.Input("multiline_mode", default=False),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_text", display_name="text", is_output_list=True),
|
||||
io.String.Output(id="populated_text", display_name="populated_text", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("text", "populated_text")
|
||||
OUTPUT_IS_LIST = (True, True)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
seed = kwargs["seed"]
|
||||
@classmethod
|
||||
def execute(cls, text, seed, multiline_mode, **kwargs):
|
||||
prompt = cls.hidden.prompt
|
||||
|
||||
# Clean loaded_objects
|
||||
if prompt:
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
text = kwargs['text']
|
||||
if "multiline_mode" in kwargs and kwargs["multiline_mode"]:
|
||||
if multiline_mode:
|
||||
populated_text = []
|
||||
_text = []
|
||||
text = text.split("\n")
|
||||
for t in text:
|
||||
text_lines = text.split("\n")
|
||||
for t in text_lines:
|
||||
_text.append(t)
|
||||
populated_text.append(process(t, seed))
|
||||
text = _text
|
||||
else:
|
||||
populated_text = [process(text, seed)]
|
||||
text = [text]
|
||||
return {"ui": {"value": [seed]}, "result": (text, populated_text)}
|
||||
return io.NodeOutput(text, populated_text, ui={"value": [seed]})
|
||||
|
||||
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
|
||||
class wildcardsPromptMatrix:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class wildcardsPromptMatrix(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"offset": ("INT", {"default": 0, "min": 0, "step": 1, "control_after_generate": True}),
|
||||
},
|
||||
"optional":{
|
||||
"output_limit": ("INT", {"default": 1, "min": -1, "step": 1, "tooltip": "Output All Probilities"})
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
return io.Schema(
|
||||
node_id="easy wildcardsMatrix",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support Lora Block Weight and wildcard)"),
|
||||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||||
io.Int.Input("offset", default=0, min=0, max=MAX_SEED_NUM, step=1, control_after_generate=True),
|
||||
io.Int.Input("output_limit", default=1, min=-1, step=1, tooltip="Output All Probilities", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("populated_text", is_output_list=True),
|
||||
io.Int.Output("total"),
|
||||
io.Int.Output("factors", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "INT")
|
||||
RETURN_NAMES = ("populated_text", "total", "factors")
|
||||
OUTPUT_IS_LIST = (True, False, True)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
offset = kwargs["offset"]
|
||||
output_limit = kwargs.get("output_limit", 1)
|
||||
@classmethod
|
||||
def execute(cls, text, offset, output_limit=1, **kwargs):
|
||||
prompt = cls.hidden.prompt
|
||||
# Clean loaded_objects
|
||||
if prompt:
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
text = kwargs['text']
|
||||
p = WildcardProcessor(text)
|
||||
total = p.total()
|
||||
limit = total if output_limit > total or output_limit == -1 else output_limit
|
||||
offset = 0 if output_limit == -1 else offset
|
||||
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
|
||||
return {"ui": {"value": [offset]}, "result": (populated_text, p.total(), list(p.placeholder_choices.values()))}
|
||||
return io.NodeOutput(populated_text, p.total(), list(p.placeholder_choices.values()), ui={"value": [offset]})
|
||||
|
||||
# 负面提示词
|
||||
class negativePrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class negativePrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"negative": ("STRING", {"default": "", "multiline": True, "placeholder": "Negative"}),}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy negative",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("negative", default="", multiline=True, placeholder="Negative"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_negative", display_name="negative"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("negative",)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
@staticmethod
|
||||
def main(negative):
|
||||
return negative,
|
||||
@classmethod
|
||||
def execute(cls, negative):
|
||||
return io.NodeOutput(negative)
|
||||
|
||||
# 风格提示词选择器
|
||||
class stylesPromptSelector:
|
||||
class stylesPromptSelector(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
styles = ["fooocus_styles"]
|
||||
styles_dir = FOOOCUS_STYLES_DIR
|
||||
for file_name in os.listdir(styles_dir):
|
||||
file = os.path.join(styles_dir, file_name)
|
||||
if os.path.isfile(file) and file_name.endswith(".json"):
|
||||
styles.append(file_name.split(".")[0])
|
||||
return {
|
||||
"required": {
|
||||
"styles": (styles, {"default": "fooocus_styles"}),
|
||||
},
|
||||
"optional": {
|
||||
"positive": ("STRING", {"forceInput": True}),
|
||||
"negative": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
if file_name != "fooocus_styles.json":
|
||||
styles.append(file_name.split(".")[0])
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("positive", "negative",)
|
||||
return io.Schema(
|
||||
node_id="easy stylesSelector",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.Combo.Input("styles", options=styles, default="fooocus_styles"),
|
||||
io.String.Input("positive", default="", force_input=True, optional=True),
|
||||
io.String.Input("negative", default="", force_input=True, optional=True),
|
||||
io.Custom(io_type="EASY_PROMPT_STYLES").Input("select_styles", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_positive", display_name="positive"),
|
||||
io.String.Output(id="output_negative", display_name="negative"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
CATEGORY = 'EasyUse/Prompt'
|
||||
FUNCTION = 'run'
|
||||
|
||||
def run(self, styles, positive='', negative='', prompt=None, extra_pnginfo=None, my_unique_id=None):
|
||||
@classmethod
|
||||
def execute(cls, styles, positive='', negative='', select_styles=None, **kwargs):
|
||||
values = []
|
||||
all_styles = {}
|
||||
positive_prompt, negative_prompt = '', negative
|
||||
if styles == "fooocus_styles":
|
||||
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
|
||||
if styles == "fooocus_styles" and not os.path.exists(fooocus_custom_dir):
|
||||
file = os.path.join(RESOURCES_DIR, styles + '.json')
|
||||
else:
|
||||
file = os.path.join(FOOOCUS_STYLES_DIR, styles + '.json')
|
||||
@@ -188,117 +189,135 @@ class stylesPromptSelector:
|
||||
f.close()
|
||||
for d in data:
|
||||
all_styles[d['name']] = d
|
||||
if my_unique_id in prompt:
|
||||
if prompt[my_unique_id]["inputs"]['select_styles']:
|
||||
values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
|
||||
# if my_unique_id in prompt:
|
||||
# if prompt[my_unique_id]["inputs"]['select_styles']:
|
||||
# values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
|
||||
|
||||
if isinstance(select_styles, str):
|
||||
values = select_styles.split(',')
|
||||
else:
|
||||
values = select_styles if select_styles else []
|
||||
|
||||
has_prompt = False
|
||||
if len(values) == 0:
|
||||
return (positive, negative)
|
||||
return io.NodeOutput(positive, negative)
|
||||
|
||||
for index, val in enumerate(values):
|
||||
if val not in all_styles:
|
||||
continue
|
||||
if 'prompt' in all_styles[val]:
|
||||
if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False:
|
||||
positive_prompt = all_styles[val]['prompt'].replace('{prompt}', positive)
|
||||
has_prompt = True
|
||||
else:
|
||||
elif "{prompt}" in all_styles[val]['prompt']:
|
||||
positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '')
|
||||
else:
|
||||
positive_prompt = all_styles[val]['prompt'] if positive_prompt == '' else positive_prompt + ', ' + all_styles[val]['prompt']
|
||||
if 'negative_prompt' in all_styles[val]:
|
||||
negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt']
|
||||
|
||||
if has_prompt == False and positive:
|
||||
positive_prompt = positive + positive_prompt + ', '
|
||||
|
||||
return (positive_prompt, negative_prompt)
|
||||
return io.NodeOutput(positive_prompt, negative_prompt)
|
||||
|
||||
#prompt
|
||||
class prompt:
|
||||
class prompt(io.ComfyNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "placeholder": "Prompt"}),
|
||||
"prefix": (["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], {"default": "Select the prefix add to the text"}),
|
||||
"subject": (["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], {"default": "👤Select the subject add to the text"}),
|
||||
"action": (["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], {"default": "🎬Select the action add to the text"}),
|
||||
"clothes": (["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], {"default": "👚Select the clothes add to the text"}),
|
||||
"environment": (["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], {"default": "☀️Select the illumination environment add to the text"}),
|
||||
"background": (["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], {"default": "🎞️Select the background add to the text"}),
|
||||
"nsfw": (["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], {"default": "🔞️Select the nsfw add to the text"}),
|
||||
},"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy prompt",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, placeholder="Prompt"),
|
||||
io.Combo.Input("prefix", options=["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], default="Select the prefix add to the text"),
|
||||
io.Combo.Input("subject", options=["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], default="👤Select the subject add to the text"),
|
||||
io.Combo.Input("action", options=["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], default="🎬Select the action add to the text"),
|
||||
io.Combo.Input("clothes", options=["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], default="👚Select the clothes add to the text"),
|
||||
io.Combo.Input("environment", options=["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], default="☀️Select the illumination environment add to the text"),
|
||||
io.Combo.Input("background", options=["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], default="🎞️Select the background add to the text"),
|
||||
io.Combo.Input("nsfw", options=["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], default="🔞️Select the nsfw add to the text"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def doit(self, *args, **kwargs):
|
||||
text = kwargs['text']
|
||||
return (text,)
|
||||
@classmethod
|
||||
def execute(cls, text, **kwargs):
|
||||
return io.NodeOutput(text)
|
||||
|
||||
#promptList
|
||||
class promptList:
|
||||
class promptList(io.ComfyNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {
|
||||
"prompt_1": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_2": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_3": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_4": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_5": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"optional_prompt_list": ("LIST",)
|
||||
}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptList",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt_1", multiline=True, default=""),
|
||||
io.String.Input("prompt_2", multiline=True, default=""),
|
||||
io.String.Input("prompt_3", multiline=True, default=""),
|
||||
io.String.Input("prompt_4", multiline=True, default=""),
|
||||
io.String.Input("prompt_5", multiline=True, default=""),
|
||||
io.Custom(io_type="LIST").Input("optional_prompt_list", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom(io_type="LIST").Output("prompt_list"),
|
||||
io.String.Output("prompt_strings", is_output_list=True),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("LIST", "STRING")
|
||||
RETURN_NAMES = ("prompt_list", "prompt_strings")
|
||||
OUTPUT_IS_LIST = (False, True)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def run(self, **kwargs):
|
||||
@classmethod
|
||||
def execute(cls, prompt_1="", prompt_2="", prompt_3="", prompt_4="", prompt_5="", optional_prompt_list=None, **kwargs):
|
||||
prompts = []
|
||||
|
||||
if "optional_prompt_list" in kwargs:
|
||||
for l in kwargs["optional_prompt_list"]:
|
||||
if optional_prompt_list:
|
||||
for l in optional_prompt_list:
|
||||
prompts.append(l)
|
||||
|
||||
# Iterate over the received inputs in sorted order.
|
||||
for k in sorted(kwargs.keys()):
|
||||
v = kwargs[k]
|
||||
# Add individual prompts
|
||||
for p in [prompt_1, prompt_2, prompt_3, prompt_4, prompt_5]:
|
||||
if isinstance(p, str) and p != '':
|
||||
prompts.append(p)
|
||||
|
||||
# Only process string input ports.
|
||||
if isinstance(v, str) and v != '':
|
||||
prompts.append(v)
|
||||
|
||||
return (prompts, prompts)
|
||||
return io.NodeOutput(prompts, prompts)
|
||||
|
||||
#promptLine
|
||||
class promptLine:
|
||||
class promptLine(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "text"}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "max": 9999}),
|
||||
"max_rows": ("INT", {"default": 1000, "min": 1, "max": 9999}),
|
||||
},
|
||||
"hidden":{
|
||||
"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptLine",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt", multiline=True, default="text"),
|
||||
io.Int.Input("start_index", default=0, min=0, max=9999),
|
||||
io.Int.Input("max_rows", default=1000, min=1, max=9999),
|
||||
io.Boolean.Input("remove_empty_lines", default=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("STRING", is_output_list=True),
|
||||
io.Combo.Output("COMBO", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", AlwaysEqualProxy('*'))
|
||||
RETURN_NAMES = ("STRING", "COMBO")
|
||||
OUTPUT_IS_LIST = (True, True)
|
||||
FUNCTION = "generate_strings"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def generate_strings(self, prompt, start_index, max_rows, workflow_prompt=None, my_unique_id=None):
|
||||
@classmethod
|
||||
def execute(cls, prompt, start_index, max_rows, remove_empty_lines=True, **kwargs):
|
||||
lines = prompt.split('\n')
|
||||
# lines = [zh_to_en([v])[0] if has_chinese(v) else v for v in lines if v]
|
||||
|
||||
if remove_empty_lines:
|
||||
lines = [line for line in lines if line.strip()]
|
||||
|
||||
start_index = max(0, min(start_index, len(lines) - 1))
|
||||
|
||||
@@ -306,58 +325,112 @@ class promptLine:
|
||||
|
||||
rows = lines[start_index:end_index]
|
||||
|
||||
return (rows, rows)
|
||||
return io.NodeOutput(rows, rows)
|
||||
|
||||
class promptConcat:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {
|
||||
},
|
||||
"optional": {
|
||||
"prompt1": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
|
||||
"prompt2": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
|
||||
"separator": ("STRING", {"multiline": False, "default": ""}),
|
||||
},
|
||||
}
|
||||
RETURN_TYPES = ("STRING", )
|
||||
RETURN_NAMES = ("prompt", )
|
||||
FUNCTION = "concat_text"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def concat_text(self, prompt1="", prompt2="", separator=""):
|
||||
|
||||
return (prompt1 + separator + prompt2,)
|
||||
|
||||
class promptReplace:
|
||||
import comfy.utils
|
||||
from server import PromptServer
|
||||
from ..libs.messages import MessageCancelled, Message
|
||||
class promptAwait(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"find1": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace1": ("STRING", {"multiline": False, "default": ""}),
|
||||
"find2": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace2": ("STRING", {"multiline": False, "default": ""}),
|
||||
"find3": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace3": ("STRING", {"multiline": False, "default": ""}),
|
||||
},
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptAwait",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.AnyType.Input("now"),
|
||||
io.String.Input("prompt", multiline=True, default="", placeholder="Enter a prompt or use voice to enter to text"),
|
||||
io.Custom(io_type="EASY_PROMPT_AWAIT_BAR").Input("toolbar"),
|
||||
io.AnyType.Input("prev", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.AnyType.Output(id="output", display_name="output"),
|
||||
io.String.Output(id="output_prompt", display_name="prompt"),
|
||||
io.Boolean.Output("continue"),
|
||||
io.Int.Output("seed"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.unique_id,
|
||||
io.Hidden.extra_pnginfo,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "replace_text"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
@classmethod
|
||||
def execute(cls, now, prompt, toolbar, prev=None, **kwargs):
|
||||
id = cls.hidden.unique_id
|
||||
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
|
||||
if ":" in id:
|
||||
id = id.split(":")[0]
|
||||
pbar = comfy.utils.ProgressBar(100)
|
||||
pbar.update_absolute(30)
|
||||
PromptServer.instance.send_sync('easyuse_prompt_await', {"id": id})
|
||||
try:
|
||||
res = Message.waitForMessage(id, asList=False)
|
||||
if res is None or res == "-1":
|
||||
result = (now, prompt, False, 0)
|
||||
else:
|
||||
input = now if res['select'] == 'now' or prev is None else prev
|
||||
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
|
||||
pbar.update_absolute(100)
|
||||
return io.NodeOutput(*result)
|
||||
except MessageCancelled:
|
||||
pbar.update_absolute(100)
|
||||
raise comfy.model_management.InterruptProcessingException()
|
||||
|
||||
def replace_text(self, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||||
class promptConcat(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptConcat",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt1", multiline=False, default="", force_input=True, optional=True),
|
||||
io.String.Input("prompt2", multiline=False, default="", force_input=True, optional=True),
|
||||
io.String.Input("separator", multiline=False, default="", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, prompt1="", prompt2="", separator=""):
|
||||
def to_string(value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return ", ".join(to_string(v) for v in value)
|
||||
return str(value)
|
||||
|
||||
return io.NodeOutput(to_string(prompt1) + to_string(separator) + to_string(prompt2))
|
||||
|
||||
class promptReplace(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptReplace",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt", multiline=True, default="", force_input=True),
|
||||
io.String.Input("find1", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace1", multiline=False, default="", optional=True),
|
||||
io.String.Input("find2", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace2", multiline=False, default="", optional=True),
|
||||
io.String.Input("find3", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace3", multiline=False, default="", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_prompt",display_name="prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||||
prompt = prompt.replace(find1, replace1)
|
||||
prompt = prompt.replace(find2, replace2)
|
||||
prompt = prompt.replace(find3, replace3)
|
||||
|
||||
return (prompt,)
|
||||
return io.NodeOutput(prompt)
|
||||
|
||||
|
||||
# 肖像大师
|
||||
@@ -365,10 +438,10 @@ class promptReplace:
|
||||
# Version: 2.2
|
||||
# https://stefanoflore.it
|
||||
# https://ai-wiz.art
|
||||
class portraitMaster:
|
||||
class portraitMaster(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
max_float_value = 1.95
|
||||
prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json')
|
||||
if not os.path.exists(prompt_path):
|
||||
@@ -380,50 +453,72 @@ class portraitMaster:
|
||||
del response, temp_prompt
|
||||
# Load local
|
||||
with open(prompt_path, 'r') as f:
|
||||
list = json.load(f)
|
||||
keys = [
|
||||
['shot', 'COMBO', {"key": "shot_list"}], ['shot_weight', 'FLOAT'],
|
||||
['gender', 'COMBO', {"default": "Woman", "key": "gender_list"}], ['age', 'INT', {"default": 30, "min": 18, "max": 90, "step": 1, "display": "slider"}],
|
||||
['nationality_1', 'COMBO', {"default": "Chinese", "key": "nationality_list"}], ['nationality_2', 'COMBO', {"key": "nationality_list"}], ['nationality_mix', 'FLOAT'],
|
||||
['body_type', 'COMBO', {"key": "body_type_list"}], ['body_type_weight', 'FLOAT'], ['model_pose', 'COMBO', {"key": "model_pose_list"}], ['eyes_color', 'COMBO', {"key": "eyes_color_list"}],
|
||||
['facial_expression', 'COMBO', {"key": "face_expression_list"}], ['facial_expression_weight', 'FLOAT'], ['face_shape', 'COMBO', {"key": "face_shape_list"}], ['face_shape_weight', 'FLOAT'], ['facial_asymmetry', 'FLOAT'],
|
||||
['hair_style', 'COMBO', {"key": "hair_style_list"}], ['hair_color', 'COMBO', {"key": "hair_color_list"}], ['disheveled', 'FLOAT'], ['beard', 'COMBO', {"key": "beard_list"}],
|
||||
['skin_details', 'FLOAT'], ['skin_pores', 'FLOAT'], ['dimples', 'FLOAT'], ['freckles', 'FLOAT'],
|
||||
['moles', 'FLOAT'], ['skin_imperfections', 'FLOAT'], ['skin_acne', 'FLOAT'], ['tanned_skin', 'FLOAT'],
|
||||
['eyes_details', 'FLOAT'], ['iris_details', 'FLOAT'], ['circular_iris', 'FLOAT'], ['circular_pupil', 'FLOAT'],
|
||||
['light_type', 'COMBO', {"key": "light_type_list"}], ['light_direction', 'COMBO', {"key": "light_direction_list"}], ['light_weight', 'FLOAT']
|
||||
]
|
||||
widgets = {}
|
||||
for i, obj in enumerate(keys):
|
||||
if obj[1] == 'COMBO':
|
||||
key = obj[2]['key'] if obj[2] and 'key' in obj[2] else obj[0]
|
||||
_list = list[key].copy()
|
||||
_list.insert(0, '-')
|
||||
widgets[obj[0]] = (_list, {**obj[2]})
|
||||
elif obj[1] == 'FLOAT':
|
||||
widgets[obj[0]] = ("FLOAT", {"default": 0, "step": 0.05, "min": 0, "max": max_float_value, "display": "slider",})
|
||||
elif obj[1] == 'INT':
|
||||
widgets[obj[0]] = (obj[1], obj[2])
|
||||
del list
|
||||
return {
|
||||
"required": {
|
||||
**widgets,
|
||||
"photorealism_improvement": (["enable", "disable"],),
|
||||
"prompt_start": ("STRING", {"multiline": True, "default": "raw photo, (realistic:1.5)"}),
|
||||
"prompt_additional": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_end": ("STRING", {"multiline": True, "default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
data = json.load(f)
|
||||
|
||||
inputs = []
|
||||
# Shot
|
||||
inputs.append(io.Combo.Input("shot", options=['-'] + data['shot_list']))
|
||||
inputs.append(io.Float.Input("shot_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Gender and age
|
||||
inputs.append(io.Combo.Input("gender", options=['-'] + data['gender_list'], default="Woman"))
|
||||
inputs.append(io.Int.Input("age", default=30, min=18, max=90, step=1, display_mode=io.NumberDisplay.slider))
|
||||
# Nationality
|
||||
inputs.append(io.Combo.Input("nationality_1", options=['-'] + data['nationality_list'], default="Chinese"))
|
||||
inputs.append(io.Combo.Input("nationality_2", options=['-'] + data['nationality_list']))
|
||||
inputs.append(io.Float.Input("nationality_mix", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Body
|
||||
inputs.append(io.Combo.Input("body_type", options=['-'] + data['body_type_list']))
|
||||
inputs.append(io.Float.Input("body_type_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("model_pose", options=['-'] + data['model_pose_list']))
|
||||
inputs.append(io.Combo.Input("eyes_color", options=['-'] + data['eyes_color_list']))
|
||||
# Face
|
||||
inputs.append(io.Combo.Input("facial_expression", options=['-'] + data['face_expression_list']))
|
||||
inputs.append(io.Float.Input("facial_expression_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("face_shape", options=['-'] + data['face_shape_list']))
|
||||
inputs.append(io.Float.Input("face_shape_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("facial_asymmetry", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Hair
|
||||
inputs.append(io.Combo.Input("hair_style", options=['-'] + data['hair_style_list']))
|
||||
inputs.append(io.Combo.Input("hair_color", options=['-'] + data['hair_color_list']))
|
||||
inputs.append(io.Float.Input("disheveled", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("beard", options=['-'] + data['beard_list']))
|
||||
# Skin details
|
||||
inputs.append(io.Float.Input("skin_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_pores", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("dimples", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("freckles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("moles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_imperfections", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_acne", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("tanned_skin", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Eyes
|
||||
inputs.append(io.Float.Input("eyes_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("iris_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("circular_iris", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("circular_pupil", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Light
|
||||
inputs.append(io.Combo.Input("light_type", options=['-'] + data['light_type_list']))
|
||||
inputs.append(io.Combo.Input("light_direction", options=['-'] + data['light_direction_list']))
|
||||
inputs.append(io.Float.Input("light_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Additional
|
||||
inputs.append(io.Combo.Input("photorealism_improvement", options=["enable", "disable"]))
|
||||
inputs.append(io.String.Input("prompt_start", multiline=True, default="raw photo, (realistic:1.5)"))
|
||||
inputs.append(io.String.Input("prompt_additional", multiline=True, default=""))
|
||||
inputs.append(io.String.Input("prompt_end", multiline=True, default=""))
|
||||
inputs.append(io.String.Input("negative_prompt", multiline=True, default=""))
|
||||
|
||||
return io.Schema(
|
||||
node_id="easy portraitMaster",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=inputs,
|
||||
outputs=[
|
||||
io.String.Output("positive"),
|
||||
io.String.Output("negative"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("positive", "negative",)
|
||||
|
||||
FUNCTION = "pm"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def pm(self, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
|
||||
@classmethod
|
||||
def execute(cls, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
|
||||
facial_expression="-", facial_expression_weight=0, face_shape="-", face_shape_weight=0,
|
||||
nationality_1="-", nationality_2="-", nationality_mix=0.5, age=30, hair_style="-", hair_color="-",
|
||||
disheveled=0, dimples=0, freckles=0, skin_pores=0, skin_details=0, moles=0, skin_imperfections=0,
|
||||
@@ -553,7 +648,118 @@ class portraitMaster:
|
||||
|
||||
log_node_info("Portrait Master as generate the prompt:", prompt)
|
||||
|
||||
return (prompt, negative_prompt,)
|
||||
return io.NodeOutput(prompt, negative_prompt)
|
||||
|
||||
# 多角度
|
||||
class multiAngle(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy multiAngle",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.Custom(io_type="EASY_MULTI_ANGLE").Input("multi_angle", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt", is_output_list=True),
|
||||
io.Custom(io_type="EASY_MULTI_ANGLE").Output("params"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, multi_angle=None, **kwargs):
|
||||
if multi_angle is None:
|
||||
return io.NodeOutput([""])
|
||||
|
||||
if isinstance(multi_angle, str):
|
||||
try:
|
||||
multi_angle = json.loads(multi_angle)
|
||||
except:
|
||||
raise Exception(f"Invalid multi angle: {multi_angle}")
|
||||
|
||||
prompts = []
|
||||
for angle_data in multi_angle:
|
||||
rotate = angle_data.get("rotate", 0)
|
||||
vertical = angle_data.get("vertical", 0)
|
||||
zoom = angle_data.get("zoom", 5)
|
||||
add_angle_prompt = angle_data.get("add_angle_prompt", True)
|
||||
|
||||
# Validate input ranges
|
||||
rotate = max(0, min(360, int(rotate)))
|
||||
vertical = max(-90, min(90, int(vertical)))
|
||||
zoom = max(0.0, min(10.0, float(zoom)))
|
||||
|
||||
h_angle = rotate % 360
|
||||
|
||||
# Horizontal direction mapping
|
||||
h_suffix = "" if add_angle_prompt else " quarter"
|
||||
if h_angle < 22.5 or h_angle >= 337.5: h_direction = "front view"
|
||||
elif h_angle < 67.5: h_direction = f"front-right{h_suffix} view"
|
||||
elif h_angle < 112.5: h_direction = "right side view"
|
||||
elif h_angle < 157.5: h_direction = f"back-right{h_suffix} view"
|
||||
elif h_angle < 202.5: h_direction = "back view"
|
||||
elif h_angle < 247.5: h_direction = f"back-left{h_suffix} view"
|
||||
elif h_angle < 292.5: h_direction = "left side view"
|
||||
else: h_direction = f"front-left{h_suffix} view"
|
||||
|
||||
# Vertical direction mapping
|
||||
if add_angle_prompt:
|
||||
if vertical == -90:
|
||||
v_direction = "bottom-looking-up perspective, extreme worm's eye view, focus subject bottom"
|
||||
elif vertical < -75:
|
||||
v_direction = "bottom-looking-up perspective, extreme worm's eye view"
|
||||
elif vertical < -45:
|
||||
v_direction = "ultra-low angle"
|
||||
elif vertical < -15:
|
||||
v_direction = "low angle"
|
||||
elif vertical < 15:
|
||||
v_direction = "eye level"
|
||||
elif vertical < 45:
|
||||
v_direction = "high angle"
|
||||
elif vertical < 75:
|
||||
v_direction = "bird's eye view"
|
||||
elif vertical < 90:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||||
else:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||||
else:
|
||||
if vertical < -15:
|
||||
v_direction = "low-angle shot"
|
||||
elif vertical < 15:
|
||||
v_direction = "eye-level shot"
|
||||
elif vertical < 45:
|
||||
v_direction = "elevated shot"
|
||||
elif vertical < 75:
|
||||
v_direction = "high-angle shot"
|
||||
elif vertical < 90:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||||
else:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||||
|
||||
# Distance/zoom mapping
|
||||
if add_angle_prompt:
|
||||
if zoom < 2: distance = "extreme wide shot"
|
||||
elif zoom < 4: distance = "wide shot"
|
||||
elif zoom < 6: distance = "medium shot"
|
||||
elif zoom < 8: distance = "close-up"
|
||||
else: distance = "extreme close-up"
|
||||
else:
|
||||
if zoom < 2: distance = "extreme wide shot"
|
||||
elif zoom < 4: distance = "wide shot"
|
||||
elif zoom < 6: distance = "medium shot"
|
||||
elif zoom < 8: distance = "close-up"
|
||||
else: distance = "extreme close-up"
|
||||
|
||||
# Build prompt
|
||||
if add_angle_prompt:
|
||||
prompt = f"{h_direction}, {v_direction}, {distance} (horizontal: {rotate}, vertical: {vertical}, zoom: {zoom:.1f})"
|
||||
else:
|
||||
prompt = f"{h_direction} {v_direction} {distance}"
|
||||
|
||||
prompts.append(prompt)
|
||||
|
||||
return io.NodeOutput(prompts, multi_angle)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -564,10 +770,12 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy prompt": prompt,
|
||||
"easy promptList": promptList,
|
||||
"easy promptLine": promptLine,
|
||||
"easy promptAwait": promptAwait,
|
||||
"easy promptConcat": promptConcat,
|
||||
"easy promptReplace": promptReplace,
|
||||
"easy stylesSelector": stylesPromptSelector,
|
||||
"easy portraitMaster": portraitMaster,
|
||||
"easy multiAngle": multiAngle,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -578,8 +786,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy prompt": "Prompt",
|
||||
"easy promptList": "PromptList",
|
||||
"easy promptLine": "PromptLine",
|
||||
"easy promptAwait": "PromptAwait",
|
||||
"easy promptConcat": "PromptConcat",
|
||||
"easy promptReplace": "PromptReplace",
|
||||
"easy stylesSelector": "Styles Selector",
|
||||
"easy portraitMaster": "Portrait Master",
|
||||
}
|
||||
"easy multiAngle": "Multi Angle",
|
||||
}
|
||||
|
||||
+39
-58
@@ -1,13 +1,14 @@
|
||||
import sys, re, time
|
||||
import torch
|
||||
import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models
|
||||
import folder_paths
|
||||
from comfy.model_patcher import ModelPatcher
|
||||
from comfy_extras.nodes_mask import GrowMask
|
||||
import comfy_extras.nodes_custom_sampler as custom_samplers
|
||||
from tqdm import trange
|
||||
|
||||
from server import PromptServer
|
||||
from nodes import RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, VAEEncodeForInpaint, InpaintModelConditioning
|
||||
from nodes import RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, VAEEncodeForInpaint, InpaintModelConditioning, VAEDecodeTiled
|
||||
from ..modules.layer_diffuse import LayerDiffuse
|
||||
from ..config import *
|
||||
|
||||
@@ -15,7 +16,6 @@ from ..libs.log import log_node_warn
|
||||
from ..libs.utils import easySave, get_local_filepath, get_sd_version
|
||||
from ..libs.sampler import alignYourStepsScheduler, gitsScheduler
|
||||
from ..libs.xyplot import easyXYPlot
|
||||
from ..libs.chooser import ChooserMessage, ChooserCancelled
|
||||
|
||||
from .. import easyCache, sampler
|
||||
|
||||
@@ -30,7 +30,7 @@ class samplerFull:
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,),
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
|
||||
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
|
||||
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
|
||||
"save_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
@@ -119,7 +119,14 @@ class samplerFull:
|
||||
def get_custom_cls(self, sampler_name):
|
||||
try:
|
||||
cls = custom_samplers.__dict__[sampler_name]
|
||||
return cls()
|
||||
cls = cls()
|
||||
if hasattr(cls, "get_sigmas"):
|
||||
cls.execute = cls.get_sigmas
|
||||
elif hasattr(cls, "get_guider"):
|
||||
cls.execute = cls.get_guider
|
||||
elif hasattr(cls, "get_sampler"):
|
||||
cls.execute = cls.get_sampler
|
||||
return cls
|
||||
except:
|
||||
raise Exception(f"Custom sampler {sampler_name} not found, Please updated your ComfyUI")
|
||||
|
||||
@@ -131,6 +138,14 @@ class samplerFull:
|
||||
to["model_patch"] = {}
|
||||
return to
|
||||
|
||||
def get_align_your_steps_sigmas(self, model, steps, denoise):
|
||||
model_type = get_sd_version(model)
|
||||
# Anima/Krea2 have no dedicated AYS table; keep the SDXL table they used before these families were recognized.
|
||||
if model_type in ("anima", "krea2", "unknown"):
|
||||
model_type = "sdxl"
|
||||
sigmas, = alignYourStepsScheduler().get_sigmas(model_type.upper(), steps, denoise)
|
||||
return sigmas
|
||||
|
||||
def get_sampler_custom(self, model, positive, negative, loader_settings):
|
||||
_guider = None
|
||||
middle = loader_settings['middle'] if "middle" in loader_settings else negative
|
||||
@@ -157,28 +172,25 @@ class samplerFull:
|
||||
sigmas = optional_sigmas
|
||||
else:
|
||||
if scheduler == 'vp':
|
||||
sigmas, = self.get_custom_cls('VPScheduler').get_sigmas(steps, beta_d, beta_min, eps_s)
|
||||
sigmas, = self.get_custom_cls('VPScheduler').execute(steps, beta_d, beta_min, eps_s)
|
||||
elif scheduler == 'karrasADV':
|
||||
sigmas, = self.get_custom_cls('KarrasScheduler').get_sigmas(steps, sigma_max, sigma_min, rho)
|
||||
sigmas, = self.get_custom_cls('KarrasScheduler').execute(steps, sigma_max, sigma_min, rho)
|
||||
elif scheduler == 'exponentialADV':
|
||||
sigmas, = self.get_custom_cls('ExponentialScheduler').get_sigmas(steps, sigma_max, sigma_min)
|
||||
sigmas, = self.get_custom_cls('ExponentialScheduler').execute(steps, sigma_max, sigma_min)
|
||||
elif scheduler == 'polyExponential':
|
||||
sigmas, = self.get_custom_cls('PolyexponentialScheduler').get_sigmas(steps, sigma_max, sigma_min, rho)
|
||||
sigmas, = self.get_custom_cls('PolyexponentialScheduler').execute(steps, sigma_max, sigma_min, rho)
|
||||
elif scheduler == 'sdturbo':
|
||||
sigmas, = self.get_custom_cls('SDTurboScheduler').get_sigmas(model, steps, denoise)
|
||||
sigmas, = self.get_custom_cls('SDTurboScheduler').execute(model, steps, denoise)
|
||||
elif scheduler == 'alignYourSteps':
|
||||
model_type = get_sd_version(model)
|
||||
if model_type == 'unknown':
|
||||
model_type = 'sdxl'
|
||||
sigmas, = alignYourStepsScheduler().get_sigmas(model_type.upper(), steps, denoise)
|
||||
sigmas = self.get_align_your_steps_sigmas(model, steps, denoise)
|
||||
elif scheduler == 'gits':
|
||||
sigmas, = gitsScheduler().get_sigmas(coeff, steps, denoise)
|
||||
else:
|
||||
sigmas, = self.get_custom_cls('BasicScheduler').get_sigmas(model, scheduler, steps, denoise)
|
||||
sigmas, = self.get_custom_cls('BasicScheduler').execute(model, scheduler, steps, denoise)
|
||||
|
||||
# filp_sigmas
|
||||
if flip_sigmas:
|
||||
sigmas, = self.get_custom_cls('FlipSigmas').get_sigmas(sigmas)
|
||||
sigmas, = self.get_custom_cls('FlipSigmas').execute(sigmas)
|
||||
|
||||
#######################################################################################
|
||||
# brushnet
|
||||
@@ -210,12 +222,12 @@ class samplerFull:
|
||||
positive = c
|
||||
|
||||
if guider in ['CFG', 'IP2P+CFG']:
|
||||
_guider, = self.get_custom_cls('CFGGuider').get_guider(model, positive, negative, cfg)
|
||||
_guider, = self.get_custom_cls('CFGGuider').execute(model, positive, negative, cfg)
|
||||
elif guider in ['DualCFG', 'IP2P+DualCFG']:
|
||||
_guider, = self.get_custom_cls('DualCFGGuider').get_guider(model, positive, middle,
|
||||
_guider, = self.get_custom_cls('DualCFGGuider').execute(model, positive, middle,
|
||||
negative, cfg, cfg_negative)
|
||||
else:
|
||||
_guider, = self.get_custom_cls('BasicGuider').get_guider(model, positive)
|
||||
_guider, = self.get_custom_cls('BasicGuider').execute(model, positive)
|
||||
|
||||
# sampler
|
||||
if optional_sampler:
|
||||
@@ -224,7 +236,7 @@ class samplerFull:
|
||||
if sampler_name == 'inversed_euler':
|
||||
_sampler, = self.get_inversed_euler_sampler()
|
||||
else:
|
||||
_sampler, = self.get_custom_cls('KSamplerSelect').get_sampler(sampler_name)
|
||||
_sampler, = self.get_custom_cls('KSamplerSelect').execute(sampler_name)
|
||||
|
||||
|
||||
return (_guider, _sampler, sigmas)
|
||||
@@ -278,18 +290,18 @@ class samplerFull:
|
||||
if width_downscale_factor > 1.75:
|
||||
log_node_warn("Patch model unet add downscale...")
|
||||
log_node_warn("Downscale factor:" + str(width_downscale_factor))
|
||||
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic",
|
||||
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic",
|
||||
"bicubic")
|
||||
elif height_downscale_factor > 1.25:
|
||||
log_node_warn("Patch model unet add downscale....")
|
||||
log_node_warn("Downscale factor:" + str(height_downscale_factor))
|
||||
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic",
|
||||
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic",
|
||||
"bicubic")
|
||||
else:
|
||||
cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale']
|
||||
log_node_warn("Patch model unet add downscale....")
|
||||
log_node_warn("Downscale factor:" + str(downscale_options['downscale_factor']))
|
||||
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method'])
|
||||
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method'])
|
||||
return samp_model
|
||||
|
||||
def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive,
|
||||
@@ -333,10 +345,7 @@ class samplerFull:
|
||||
_guider, _sampler, sigmas = self.get_sampler_custom(samp_model, samp_positive, samp_negative, samp_custom)
|
||||
samp_samples, samp_blend_samples = sampler.custom_advanced_ksampler(_guider, _sampler, sigmas, samp_samples, add_noise, samp_seed, preview_latent=preview_latent)
|
||||
elif scheduler == 'align_your_steps':
|
||||
model_type = get_sd_version(samp_model)
|
||||
if model_type == 'unknown':
|
||||
model_type = 'sdxl'
|
||||
sigmas, = alignYourStepsScheduler().get_sigmas(model_type.upper(), steps, denoise)
|
||||
sigmas = self.get_align_your_steps_sigmas(samp_model, steps, denoise)
|
||||
_sampler = comfy.samplers.sampler_object(sampler_name)
|
||||
samp_samples = sampler.custom_ksampler(samp_model, samp_seed, steps, cfg, _sampler, sigmas, samp_positive, samp_negative, samp_samples, disable_noise=disable_noise, preview_latent=preview_latent, noise_device=noise_device)
|
||||
elif scheduler == 'gits':
|
||||
@@ -355,7 +364,7 @@ class samplerFull:
|
||||
spent_time = 'Diffusion:' + str((end_time - start_time) / 1000) + '″'
|
||||
else:
|
||||
if tile_size is not None:
|
||||
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
|
||||
samp_images, = VAEDecodeTiled().decode(samp_vae, {"samples": latent}, tile_size)
|
||||
else:
|
||||
samp_images = samp_vae.decode(latent).cpu()
|
||||
if len(samp_images.shape) == 5: # Combine batches
|
||||
@@ -401,34 +410,6 @@ class samplerFull:
|
||||
|
||||
del pipe
|
||||
|
||||
if image_output == 'Preview&Choose':
|
||||
if my_unique_id not in ChooserMessage.stash:
|
||||
ChooserMessage.stash[my_unique_id] = {}
|
||||
my_stash = ChooserMessage.stash[my_unique_id]
|
||||
|
||||
PromptServer.instance.send_sync("easyuse-image-choose", {"id": my_unique_id, "urls": results})
|
||||
# wait for selection
|
||||
try:
|
||||
selections = ChooserMessage.waitForMessage(my_unique_id, asList=True)
|
||||
samples = samp_samples['samples']
|
||||
samples = [samples[x] for x in selections if x >= 0] if len(selections) > 1 else [samples[0]]
|
||||
new_images = [new_images[x] for x in selections if x >= 0] if len(selections) > 1 else [new_images[0]]
|
||||
samp_images = [samp_images[x] for x in selections if x >= 0] if len(selections) > 1 else [samp_images[0]]
|
||||
new_images = torch.stack(new_images, dim=0)
|
||||
samp_images = torch.stack(samp_images, dim=0)
|
||||
samples = torch.stack(samples, dim=0)
|
||||
samp_samples = {"samples": samples}
|
||||
new_pipe['samples'] = samp_samples
|
||||
new_pipe['loader_settings']['batch_size'] = len(new_images)
|
||||
except ChooserCancelled:
|
||||
raise comfy.model_management.InterruptProcessingException()
|
||||
|
||||
new_pipe['images'] = new_images
|
||||
new_pipe['samp_images'] = samp_images
|
||||
|
||||
return {"ui": {"images": results},
|
||||
"result": sampler.get_output(new_pipe,)}
|
||||
|
||||
if image_output in ("Hide", "Hide&Save", "None"):
|
||||
return {"ui":{}, "result":sampler.get_output(new_pipe,)}
|
||||
|
||||
@@ -594,7 +575,7 @@ class samplerSimple(samplerFull):
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required":
|
||||
{"pipe": ("PIPE_LINE",),
|
||||
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
|
||||
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
|
||||
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
|
||||
"save_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
@@ -626,7 +607,7 @@ class samplerSimpleCustom(samplerFull):
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required":
|
||||
{"pipe": ("PIPE_LINE",),
|
||||
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
|
||||
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
|
||||
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
|
||||
"save_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
@@ -1001,7 +982,7 @@ class samplerSDTurbo:
|
||||
|
||||
# 解码图片
|
||||
if tile_size is not None:
|
||||
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
|
||||
samp_images, = VAEDecodeTiled().decode(samp_vae, {"samples": latent}, tile_size)
|
||||
else:
|
||||
samp_images = samp_vae.decode(latent).cpu()
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class seedList:
|
||||
return {
|
||||
"required": {
|
||||
"min_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
|
||||
"max_num": ("INT", {"default": MAX_SEED_NUM, "min": 0 }),
|
||||
"max_num": ("INT", {"default": MAX_SEED_NUM, "max": MAX_SEED_NUM, "min": 0 }),
|
||||
"method": (["random", "increment", "decrement"], {"default": "random"}),
|
||||
"total": ("INT", {"default": 1, "min": 1, "max": 100000}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM,}),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import folder_paths
|
||||
import json
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
|
||||
class showLoaderSettingsNames:
|
||||
@@ -125,12 +127,291 @@ class setLoraName:
|
||||
return (lora_name,)
|
||||
|
||||
|
||||
def _markdown_table_to_image(markdown: str, font_path: str):
|
||||
"""将 Markdown 表格字符串渲染为 PIL.Image(RGB),支持单元格自动换行。"""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# 解析行,过滤分隔行(如 |---|---|)
|
||||
lines = [l for l in (markdown or '').strip().splitlines() if l.strip()]
|
||||
table_rows = []
|
||||
for line in lines:
|
||||
if re.match(r'^\|[\s\-:|]+\|$', line.strip()):
|
||||
continue
|
||||
cells = [re.sub(r'\*\*(.+?)\*\*', lambda m: '\x01' + m.group(1) + '\x02',
|
||||
re.sub(r'<br\s*/?>', '\n', c.strip(), flags=re.IGNORECASE))
|
||||
for c in line.strip().strip('|').split('|')]
|
||||
table_rows.append(cells)
|
||||
|
||||
if not table_rows:
|
||||
return Image.new("RGB", (400, 80), (255, 255, 255))
|
||||
|
||||
num_cols = max(len(r) for r in table_rows)
|
||||
table_rows = [r + [''] * (num_cols - len(r)) for r in table_rows]
|
||||
|
||||
# 加载字体
|
||||
font_size = 16
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
pad_x, pad_y = 14, 10
|
||||
border = 1
|
||||
max_cell_text_width = 200 # 单元格文字区域最大宽度(像素)
|
||||
|
||||
def get_text_width(text):
|
||||
clean = re.sub('[\x01\x02]', '', text)
|
||||
try:
|
||||
bbox = font.getbbox(clean)
|
||||
return bbox[2] - bbox[0]
|
||||
except Exception:
|
||||
return len(clean) * 9
|
||||
|
||||
def get_line_height():
|
||||
try:
|
||||
bbox = font.getbbox('Ag\u4e2d')
|
||||
return bbox[3] - bbox[1]
|
||||
except Exception:
|
||||
return font_size + 2
|
||||
|
||||
def wrap_text(text, max_width):
|
||||
"""换行:先按 \\n 切段,每段再按英文单词边界 / CJK 字符换行。"""
|
||||
if not text:
|
||||
return ['']
|
||||
# 先按显式换行符切段,再对每段分别软换行
|
||||
hard_lines = text.split('\n')
|
||||
if len(hard_lines) > 1:
|
||||
result = []
|
||||
for hl in hard_lines:
|
||||
result.extend(wrap_text(hl, max_width))
|
||||
return result if result else ['']
|
||||
|
||||
# 将文本拆分为:CJK 单字符 / 空白序列 / 非CJK非空白序列(英文单词/标点等)
|
||||
tokens = re.findall(
|
||||
r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]'
|
||||
r'|[ \t]+'
|
||||
r'|[^ \t\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+',
|
||||
text
|
||||
)
|
||||
|
||||
result, current = [], ''
|
||||
for token in tokens:
|
||||
is_space = token.strip() == ''
|
||||
test = current + token
|
||||
if get_text_width(test) <= max_width:
|
||||
if is_space and not current:
|
||||
continue # 跳过行首空格
|
||||
current = test
|
||||
else:
|
||||
if is_space:
|
||||
# 空白处换行,丢弃该空白
|
||||
if current:
|
||||
result.append(current)
|
||||
current = ''
|
||||
elif get_text_width(token) <= max_width:
|
||||
# 整个 token 能放一行,整体移到下一行
|
||||
if current:
|
||||
result.append(current)
|
||||
current = token
|
||||
else:
|
||||
# token 本身超宽(极长单词),逐字符强拆
|
||||
for char in token:
|
||||
if get_text_width(current + char) <= max_width:
|
||||
current += char
|
||||
else:
|
||||
if current:
|
||||
result.append(current)
|
||||
current = char
|
||||
if current:
|
||||
result.append(current)
|
||||
return result if result else ['']
|
||||
|
||||
line_h = get_line_height()
|
||||
|
||||
def parse_line_segments(line):
|
||||
"""将含 \\x01..\\x02 粗体标记的行拆分为 (text, is_bold) 片段列表。"""
|
||||
result, bold = [], False
|
||||
for part in re.split('([\x01\x02])', line):
|
||||
if part == '\x01':
|
||||
bold = True
|
||||
elif part == '\x02':
|
||||
bold = False
|
||||
elif part:
|
||||
result.append((part, bold))
|
||||
return result or [('', False)]
|
||||
|
||||
def balance_bold_markers(lines):
|
||||
"""确保每行粗体标记自成一对:跨行时在行首补开、行尾补关标记。"""
|
||||
result, in_bold = [], False
|
||||
for line in lines:
|
||||
if in_bold:
|
||||
line = '\x01' + line
|
||||
for ch in line:
|
||||
if ch == '\x01': in_bold = True
|
||||
elif ch == '\x02': in_bold = False
|
||||
if in_bold:
|
||||
line = line + '\x02'
|
||||
result.append(line)
|
||||
return result
|
||||
|
||||
# 第一遍:计算各列宽度(不超过 max_cell_text_width,按子行分别测量)
|
||||
col_text_widths = []
|
||||
for col_idx in range(num_cols):
|
||||
max_w = 0
|
||||
for row in table_rows:
|
||||
cell = row[col_idx] if col_idx < len(row) else ''
|
||||
for seg_line in cell.split('\n'):
|
||||
max_w = max(max_w, min(get_text_width(seg_line), max_cell_text_width))
|
||||
col_text_widths.append(max_w)
|
||||
col_widths = [w + pad_x * 2 for w in col_text_widths]
|
||||
|
||||
# 第二遍:对每行每格换行,计算各行高度
|
||||
wrapped_rows = []
|
||||
row_heights = []
|
||||
for row in table_rows:
|
||||
wrapped_cells = []
|
||||
max_lines = 1
|
||||
for col_idx in range(num_cols):
|
||||
cell = row[col_idx] if col_idx < len(row) else ''
|
||||
wrapped = balance_bold_markers(wrap_text(cell, col_text_widths[col_idx]))
|
||||
wrapped_cells.append(wrapped)
|
||||
max_lines = max(max_lines, len(wrapped))
|
||||
wrapped_rows.append(wrapped_cells)
|
||||
row_heights.append(max_lines * line_h + pad_y * 2)
|
||||
|
||||
# 每列左边缘 x 坐标(每列前留 1px 边框)
|
||||
col_x = [border]
|
||||
for cw in col_widths:
|
||||
col_x.append(col_x[-1] + cw + border)
|
||||
|
||||
total_width = col_x[-1]
|
||||
total_height = border + sum(rh + border for rh in row_heights)
|
||||
|
||||
# 配色
|
||||
header_bg = (52, 73, 94)
|
||||
header_fg = (255, 255, 255)
|
||||
even_bg = (248, 249, 252)
|
||||
odd_bg = (255, 255, 255)
|
||||
border_color = (180, 185, 195)
|
||||
text_color = (50, 54, 62)
|
||||
|
||||
# 以边框色填充整张图,格线自然显现
|
||||
img = Image.new("RGB", (total_width, total_height), border_color)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
def render_line(x, y, line, fg):
|
||||
"""逐片段渲染一行文字;粗体通过向右偏移 1px 再描一遍来模拟加粗。"""
|
||||
try:
|
||||
text_offset = -font.getbbox(re.sub('[\x01\x02]', '', line) or 'A')[1]
|
||||
except Exception:
|
||||
text_offset = 0
|
||||
sx = x
|
||||
for seg, is_bold in parse_line_segments(line):
|
||||
draw.text((sx, y + text_offset), seg, font=font, fill=fg)
|
||||
if is_bold:
|
||||
draw.text((sx + 1, y + text_offset), seg, font=font, fill=fg)
|
||||
try:
|
||||
w = font.getbbox(seg)[2] - font.getbbox(seg)[0]
|
||||
except Exception:
|
||||
w = len(seg) * 9
|
||||
sx += w + (1 if is_bold else 0)
|
||||
|
||||
row_y = border
|
||||
for row_idx, (wrapped_cells, rh) in enumerate(zip(wrapped_rows, row_heights)):
|
||||
is_header = row_idx == 0
|
||||
bg = header_bg if is_header else (odd_bg if row_idx % 2 == 1 else even_bg)
|
||||
fg = header_fg if is_header else text_color
|
||||
|
||||
for col_idx in range(num_cols):
|
||||
cx, cw = col_x[col_idx], col_widths[col_idx]
|
||||
# 填充单元格背景
|
||||
draw.rectangle([cx, row_y, cx + cw - 1, row_y + rh - 1], fill=bg)
|
||||
|
||||
cell_lines = wrapped_cells[col_idx] if col_idx < len(wrapped_cells) else ['']
|
||||
total_text_h = len(cell_lines) * line_h
|
||||
ty = row_y + (rh - total_text_h) // 2 # 垂直居中起点
|
||||
for line_text in cell_lines:
|
||||
render_line(cx + pad_x, ty, line_text, fg)
|
||||
ty += line_h
|
||||
|
||||
row_y += rh + border
|
||||
|
||||
# 最长边不小于 1280,等比放大
|
||||
min_long_side = 1280
|
||||
long_side = max(img.width, img.height)
|
||||
if long_side < min_long_side:
|
||||
scale = min_long_side / long_side
|
||||
new_w = round(img.width * scale)
|
||||
new_h = round(img.height * scale)
|
||||
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
class tableEditor:
|
||||
"""表格编辑器节点 —— 通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 字符串。"""
|
||||
|
||||
CATEGORY = "EasyUse/Util"
|
||||
|
||||
RETURN_TYPES = ("STRING", "IMAGE")
|
||||
RETURN_NAMES = ("markdown", "image")
|
||||
FUNCTION = "execute"
|
||||
|
||||
DESCRIPTION = "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串。"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"table_data": ("EASY_TABLE_EDITOR",),
|
||||
},
|
||||
}
|
||||
|
||||
def execute(self, table_data):
|
||||
# 表格数据可能是纯 Markdown 字符串,也可能是序列化后的 JSON
|
||||
if isinstance(table_data, str) and table_data.strip().startswith('{'):
|
||||
try:
|
||||
obj = json.loads(table_data)
|
||||
markdown = obj.get('markdown', '')
|
||||
if not markdown:
|
||||
# 重新从 headers/rows 生成
|
||||
headers = obj.get('headers', [])
|
||||
rows = obj.get('rows', [])
|
||||
col_widths = [max(len(str(h)), 3) for h in headers]
|
||||
for row in rows:
|
||||
for i, cell in enumerate(row):
|
||||
if i < len(col_widths):
|
||||
col_widths[i] = max(col_widths[i], len(str(cell)))
|
||||
header_line = '| ' + ' | '.join(str(h).ljust(col_widths[i]) for i, h in enumerate(headers)) + ' |'
|
||||
sep_line = '| ' + ' | '.join('-' * w for w in col_widths) + ' |'
|
||||
row_lines = [
|
||||
'| ' + ' | '.join(str(row[i] if i < len(row) else '').ljust(col_widths[i]) for i in range(len(headers))) + ' |'
|
||||
for row in rows
|
||||
]
|
||||
markdown = '\n'.join([header_line, sep_line] + row_lines)
|
||||
except Exception:
|
||||
markdown = table_data
|
||||
else:
|
||||
markdown = table_data
|
||||
|
||||
# 将 Markdown 表格渲染为图像
|
||||
font_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||
'resources', 'wenquan.ttf'
|
||||
)
|
||||
from ..libs.image import pil2tensor
|
||||
img_tensor = pil2tensor(_markdown_table_to_image(markdown, font_path).convert("RGB"))
|
||||
|
||||
return (markdown, img_tensor)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy showLoaderSettingsNames": showLoaderSettingsNames,
|
||||
"easy sliderControl": sliderControl,
|
||||
"easy ckptNames": setCkptName,
|
||||
"easy controlnetNames": setControlName,
|
||||
"easy loraNames": setLoraName,
|
||||
"easy tableEditor": tableEditor,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -139,4 +420,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy ckptNames": "Ckpt Names",
|
||||
"easy controlnetNames": "ControlNet Names",
|
||||
"easy loraNames": "Lora Names",
|
||||
"easy tableEditor": "Table Editor",
|
||||
}
|
||||
|
||||
+54
-10
@@ -413,6 +413,9 @@ class XYplot_Control_Net:
|
||||
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
|
||||
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"control_net": ("CONTROL_NET",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("X_Y",)
|
||||
@@ -421,7 +424,7 @@ class XYplot_Control_Net:
|
||||
CATEGORY = "EasyUse/XY Inputs"
|
||||
|
||||
def xy_value(self, control_net_name, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent,
|
||||
last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent):
|
||||
last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent, control_net=None):
|
||||
|
||||
axis, = None,
|
||||
|
||||
@@ -430,38 +433,38 @@ class XYplot_Control_Net:
|
||||
if target_parameter == "strength":
|
||||
axis = "advanced: ControlNetStrength"
|
||||
|
||||
values.append([(control_net_name, image, first_strength, start_percent, end_percent)])
|
||||
values.append([(control_net_name, image, first_strength, start_percent, end_percent, control_net)])
|
||||
strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0
|
||||
for i in range(1, batch_count - 1):
|
||||
values.append([(control_net_name, image, first_strength + i * strength_increment, start_percent,
|
||||
end_percent)])
|
||||
end_percent, control_net)])
|
||||
if batch_count > 1:
|
||||
values.append([(control_net_name, image, last_strength, start_percent, end_percent)])
|
||||
values.append([(control_net_name, image, last_strength, start_percent, end_percent, control_net)])
|
||||
|
||||
elif target_parameter == "start_percent":
|
||||
axis = "advanced: ControlNetStart%"
|
||||
|
||||
percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0
|
||||
values.append([(control_net_name, image, strength, first_start_percent, end_percent)])
|
||||
values.append([(control_net_name, image, strength, first_start_percent, end_percent, control_net)])
|
||||
for i in range(1, batch_count - 1):
|
||||
values.append([(control_net_name, image, strength, first_start_percent + i * percent_increment,
|
||||
end_percent)])
|
||||
end_percent, control_net)])
|
||||
|
||||
# Always add the last start_percent if batch_count is more than 1.
|
||||
if batch_count > 1:
|
||||
values.append((control_net_name, image, strength, last_start_percent, end_percent))
|
||||
values.append([(control_net_name, image, strength, last_start_percent, end_percent, control_net)])
|
||||
|
||||
elif target_parameter == "end_percent":
|
||||
axis = "advanced: ControlNetEnd%"
|
||||
|
||||
percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0
|
||||
values.append([(control_net_name, image, image, strength, start_percent, first_end_percent)])
|
||||
values.append([(control_net_name, image, strength, start_percent, first_end_percent, control_net)])
|
||||
for i in range(1, batch_count - 1):
|
||||
values.append([(control_net_name, image, strength, start_percent,
|
||||
first_end_percent + i * percent_increment)])
|
||||
first_end_percent + i * percent_increment, control_net)])
|
||||
|
||||
if batch_count > 1:
|
||||
values.append([(control_net_name, image, strength, start_percent, last_end_percent)])
|
||||
values.append([(control_net_name, image, strength, start_percent, last_end_percent, control_net)])
|
||||
|
||||
|
||||
return ({"axis": axis, "values": values},)
|
||||
@@ -525,6 +528,45 @@ class XYplot_Checkpoint:
|
||||
xy_values = {"axis": axis, "values": values, "lora_stack": optional_lora_stack}
|
||||
return (xy_values,)
|
||||
|
||||
# Diffusion Models
|
||||
class XYplot_DiffusionModel:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
models = ["None"] + folder_paths.get_filename_list("diffusion_models")
|
||||
clips = ["Auto"] + folder_paths.get_filename_list("text_encoders")
|
||||
vaes = ["Auto"] + folder_paths.get_filename_list("vae")
|
||||
|
||||
inputs = {
|
||||
"required": {
|
||||
"model_count": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}),
|
||||
}
|
||||
}
|
||||
for i in range(1, 11):
|
||||
inputs["required"][f"model_name_{i}"] = (models,)
|
||||
inputs["required"][f"clip_name_{i}"] = (clips, {"default": "Auto"})
|
||||
inputs["required"][f"vae_name_{i}"] = (vaes, {"default": "Auto"})
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("X_Y",)
|
||||
RETURN_NAMES = ("X or Y",)
|
||||
FUNCTION = "xy_value"
|
||||
CATEGORY = "EasyUse/XY Inputs"
|
||||
|
||||
def xy_value(self, model_count, **kwargs):
|
||||
values = []
|
||||
for i in range(1, model_count + 1):
|
||||
model_name = kwargs.get(f"model_name_{i}")
|
||||
if not model_name or model_name == "None":
|
||||
continue
|
||||
clip_name = kwargs.get(f"clip_name_{i}", "Auto")
|
||||
vae_name = kwargs.get(f"vae_name_{i}", "Auto")
|
||||
values.append(
|
||||
model_name.replace(",", "*") + ","
|
||||
+ clip_name.replace(",", "*") + ","
|
||||
+ vae_name.replace(",", "*")
|
||||
)
|
||||
return ({"axis": "advanced: DiffusionModel", "values": values},)
|
||||
|
||||
#Loras
|
||||
class XYplot_Lora:
|
||||
|
||||
@@ -667,6 +709,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy XYInputs: Sampler/Scheduler": XYplot_Sampler_Scheduler,
|
||||
"easy XYInputs: Denoise": XYplot_Denoise,
|
||||
"easy XYInputs: Checkpoint": XYplot_Checkpoint,
|
||||
"easy XYInputs: DiffusionModel": XYplot_DiffusionModel,
|
||||
"easy XYInputs: Lora": XYplot_Lora,
|
||||
"easy XYInputs: ModelMergeBlocks": XYplot_ModelMergeBlocks,
|
||||
"easy XYInputs: PromptSR": XYplot_PromptSR,
|
||||
@@ -685,6 +728,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy XYInputs: Sampler/Scheduler": "XY Inputs: Sampler/Scheduler //EasyUse",
|
||||
"easy XYInputs: Denoise": "XY Inputs: Denoise //EasyUse",
|
||||
"easy XYInputs: Checkpoint": "XY Inputs: Checkpoint //EasyUse",
|
||||
"easy XYInputs: DiffusionModel": "XY Inputs: Diffusion Model //EasyUse",
|
||||
"easy XYInputs: Lora": "XY Inputs: Lora //EasyUse",
|
||||
"easy XYInputs: ModelMergeBlocks": "XY Inputs: ModelMergeBlocks //EasyUse",
|
||||
"easy XYInputs: PromptSR": "XY Inputs: PromptSR //EasyUse",
|
||||
|
||||
+153
-48
@@ -1,10 +1,16 @@
|
||||
import os
|
||||
import hashlib
|
||||
import hmac
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import secrets
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
from urllib.parse import urlsplit
|
||||
import folder_paths
|
||||
from aiohttp import web
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from server import PromptServer
|
||||
from .config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_STYLES_SAMPLES
|
||||
from .libs.model import easyModelManager
|
||||
@@ -25,7 +31,6 @@ def get_version(request):
|
||||
def cleanGPU(request):
|
||||
try:
|
||||
cleanGPUUsedForce()
|
||||
remove_cache('*')
|
||||
return web.Response(status=200)
|
||||
except Exception as e:
|
||||
return web.Response(status=500)
|
||||
@@ -51,8 +56,30 @@ async def translate(request):
|
||||
else:
|
||||
return web.json_response({"text": text})
|
||||
|
||||
@PromptServer.instance.routes.get("/easyuse/reboot")
|
||||
def reboot(request):
|
||||
_reboot_token = secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def _same_origin_request(request):
|
||||
fetch_site = request.headers.get("Sec-Fetch-Site")
|
||||
if fetch_site and fetch_site not in ("same-origin", "none"):
|
||||
return False
|
||||
origin = request.headers.get("Origin")
|
||||
return not origin or urlsplit(origin).netloc == request.host
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/easyuse/reboot-token")
|
||||
async def get_reboot_token(request):
|
||||
if not _same_origin_request(request):
|
||||
return web.Response(status=403)
|
||||
return web.json_response({"token": _reboot_token}, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/easyuse/reboot")
|
||||
async def reboot(request):
|
||||
token = request.headers.get("X-EasyUse-Reboot-Token", "")
|
||||
if not _same_origin_request(request) or not hmac.compare_digest(token, _reboot_token):
|
||||
return web.Response(status=403)
|
||||
|
||||
try:
|
||||
sys.stdout.close_log()
|
||||
except Exception as e:
|
||||
@@ -78,13 +105,14 @@ async def parse_csv(request):
|
||||
@PromptServer.instance.routes.get("/easyuse/prompt/styles")
|
||||
async def getStylesList(request):
|
||||
if "name" in request.rel_url.query:
|
||||
name = request.rel_url.query["name"]
|
||||
if name == 'fooocus_styles':
|
||||
file = os.path.join(RESOURCES_DIR, name+'.json')
|
||||
cn_file = os.path.join(RESOURCES_DIR, name + '_cn.json')
|
||||
style_name = request.rel_url.query["name"]
|
||||
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
|
||||
if style_name == 'fooocus_styles' and not os.path.exists(fooocus_custom_dir):
|
||||
file = os.path.join(RESOURCES_DIR, style_name+'.json')
|
||||
cn_file = os.path.join(RESOURCES_DIR, style_name + '_cn.json')
|
||||
else:
|
||||
file = os.path.join(FOOOCUS_STYLES_DIR, name+'.json')
|
||||
cn_file = os.path.join(FOOOCUS_STYLES_DIR, name + '_cn.json')
|
||||
file = os.path.join(FOOOCUS_STYLES_DIR, style_name+'.json')
|
||||
cn_file = os.path.join(FOOOCUS_STYLES_DIR, style_name + '_cn.json')
|
||||
cn_data = None
|
||||
if os.path.isfile(cn_file):
|
||||
f = open(cn_file, 'r', encoding='utf-8')
|
||||
@@ -103,13 +131,25 @@ async def getStylesList(request):
|
||||
key = ' '.join(
|
||||
word.upper() if word.lower() in ['mre', 'sai', '3d'] else word.capitalize() for word in
|
||||
words)
|
||||
img_name = '_'.join(words).lower()
|
||||
if "name_cn" in d:
|
||||
nd['name_cn'] = d['name_cn']
|
||||
elif cn_data:
|
||||
nd['name_cn'] = cn_data[key] if key in cn_data else key
|
||||
nd["name"] = d['name']
|
||||
nd['imgName'] = img_name
|
||||
if "thumbnail" in d:
|
||||
thumbnail = d['thumbnail']
|
||||
if isinstance(d['thumbnail'], str):
|
||||
nd['thumbnail'] = thumbnail if "http" in thumbnail else f'/easyuse/prompt/styles/image?path={thumbnail}'
|
||||
elif isinstance(d['thumbnail'], list):
|
||||
nd['thumbnail'] = [thumb if "http" in thumb else f'/easyuse/prompt/styles/image?path={thumb}' for thumb in thumbnail]
|
||||
else:
|
||||
nd['thumbnail'] = f'/easyuse/prompt/styles/image?name={name}&styles_name={style_name}'
|
||||
if "thumbnail_variant" in d:
|
||||
nd['thumbnailVariant'] = d['thumbnail_variant']
|
||||
if "media_type" in d:
|
||||
nd['mediaType'] = d['media_type']
|
||||
if "media_subtype" in d:
|
||||
nd['mediaSubtype'] = d['media_subtype']
|
||||
if "prompt" in d:
|
||||
nd['prompt'] = d['prompt']
|
||||
if "negative_prompt" in d:
|
||||
@@ -122,7 +162,15 @@ async def getStylesList(request):
|
||||
@PromptServer.instance.routes.get("/easyuse/prompt/styles/image")
|
||||
async def getStylesImage(request):
|
||||
styles_name = request.rel_url.query["styles_name"] if "styles_name" in request.rel_url.query else None
|
||||
if "name" in request.rel_url.query:
|
||||
if "path" in request.rel_url.query:
|
||||
path = request.rel_url.query["path"]
|
||||
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', path)
|
||||
parent_file = os.path.join(FOOOCUS_STYLES_DIR, path)
|
||||
if os.path.isfile(file):
|
||||
return web.FileResponse(file)
|
||||
elif os.path.isfile(parent_file):
|
||||
return web.FileResponse(parent_file)
|
||||
elif "name" in request.rel_url.query:
|
||||
name = request.rel_url.query["name"]
|
||||
if os.path.exists(os.path.join(FOOOCUS_STYLES_DIR, 'samples')):
|
||||
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', name + '.jpg')
|
||||
@@ -149,9 +197,9 @@ async def getModelsList(request):
|
||||
@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}")
|
||||
async def save_notes(request):
|
||||
name = request.match_info["name"]
|
||||
pos = name.index("/")
|
||||
type = name[0:pos]
|
||||
name = name[pos+1:]
|
||||
type, separator, name = name.partition("/")
|
||||
if not separator or type not in ("checkpoints", "loras", "embeddings"):
|
||||
return web.Response(status=400)
|
||||
|
||||
file_path = None
|
||||
if type == "embeddings" or type == "loras":
|
||||
@@ -169,24 +217,34 @@ async def save_notes(request):
|
||||
if file_path is not None:
|
||||
break
|
||||
else:
|
||||
file_path = folder_paths.get_full_path(
|
||||
type, name)
|
||||
if name in folder_paths.get_filename_list(type):
|
||||
file_path = folder_paths.get_full_path(type, name)
|
||||
if not file_path:
|
||||
return web.Response(status=404)
|
||||
|
||||
file_no_ext = os.path.splitext(file_path)[0]
|
||||
info_file = file_no_ext + ".txt"
|
||||
with open(info_file, "w") as f:
|
||||
f.write(await request.text())
|
||||
staged_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", encoding="utf-8", dir=os.path.dirname(info_file),
|
||||
prefix=".easyuse-notes-", delete=False
|
||||
) as staged:
|
||||
staged_path = staged.name
|
||||
staged.write(await request.text())
|
||||
os.replace(staged_path, info_file)
|
||||
finally:
|
||||
if staged_path and os.path.exists(staged_path):
|
||||
os.unlink(staged_path)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@PromptServer.instance.routes.get("/easyuse/metadata/{name}")
|
||||
async def load_metadata(request):
|
||||
name = request.match_info["name"]
|
||||
pos = name.index("/")
|
||||
type = name[0:pos]
|
||||
name = name[pos+1:]
|
||||
type, separator, name = name.partition("/")
|
||||
if not separator or type not in ("checkpoints", "loras", "embeddings"):
|
||||
return web.Response(status=400)
|
||||
|
||||
file_path = None
|
||||
if type == "embeddings":
|
||||
@@ -204,7 +262,8 @@ async def load_metadata(request):
|
||||
if file_path is not None:
|
||||
break
|
||||
else:
|
||||
file_path = folder_paths.get_full_path(type, name)
|
||||
if name in folder_paths.get_filename_list(type):
|
||||
file_path = folder_paths.get_full_path(type, name)
|
||||
if not file_path:
|
||||
return web.Response(status=404)
|
||||
|
||||
@@ -221,47 +280,93 @@ async def load_metadata(request):
|
||||
file_no_ext = os.path.splitext(file_path)[0]
|
||||
|
||||
info_file = file_no_ext + ".txt"
|
||||
if os.path.isfile(info_file):
|
||||
if os.path.isfile(info_file) and not os.path.islink(info_file):
|
||||
with open(info_file, "r") as f:
|
||||
meta["easyuse.notes"] = f.read()
|
||||
|
||||
hash_file = file_no_ext + ".sha256"
|
||||
if os.path.isfile(hash_file):
|
||||
with open(hash_file, "rt") as f:
|
||||
meta["easyuse.sha256"] = f.read()
|
||||
else:
|
||||
with open(file_path, "rb") as f:
|
||||
meta["easyuse.sha256"] = hashlib.sha256(f.read()).hexdigest()
|
||||
with open(hash_file, "wt") as f:
|
||||
f.write(meta["easyuse.sha256"])
|
||||
# Sidecar hashes are user-controlled; never use them as proof of the model's hash.
|
||||
stat = os.stat(file_path)
|
||||
meta["easyuse.sha256"] = _model_sha256(
|
||||
file_path, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns
|
||||
)
|
||||
|
||||
return web.json_response(meta)
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _model_sha256(path, size, mtime_ns, ctime_ns):
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as model:
|
||||
for chunk in iter(lambda: model.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
_PREVIEW_FORMATS = {
|
||||
".png": "PNG",
|
||||
".jpg": "JPEG",
|
||||
".jpeg": "JPEG",
|
||||
".webp": "WEBP",
|
||||
".gif": "GIF",
|
||||
}
|
||||
|
||||
@PromptServer.instance.routes.post("/easyuse/save/{name}")
|
||||
async def save_preview(request):
|
||||
name = request.match_info["name"]
|
||||
pos = name.index("/")
|
||||
type = name[0:pos]
|
||||
name = name[pos+1:]
|
||||
model_type, separator, model_name = name.partition("/")
|
||||
if not separator or model_type not in ("checkpoints", "loras"):
|
||||
return web.Response(status=400)
|
||||
if model_name not in folder_paths.get_filename_list(model_type):
|
||||
return web.Response(status=404)
|
||||
|
||||
model_path = folder_paths.get_full_path(model_type, model_name)
|
||||
if not model_path:
|
||||
return web.Response(status=404)
|
||||
|
||||
body = await request.json()
|
||||
|
||||
dir = folder_paths.get_directory_by_type(body.get("type", "output"))
|
||||
subfolder = body.get("subfolder", "")
|
||||
full_output_folder = os.path.join(dir, os.path.normpath(subfolder))
|
||||
|
||||
if os.path.commonpath((dir, os.path.abspath(full_output_folder))) != dir:
|
||||
filename = body.get("filename")
|
||||
if (body.get("type") != "temp" or body.get("subfolder", "") != ""
|
||||
or not isinstance(filename, str) or not filename
|
||||
or os.path.basename(filename) != filename or filename in (".", "..")):
|
||||
return web.Response(status=400)
|
||||
|
||||
filepath = os.path.join(full_output_folder, body.get("filename", ""))
|
||||
image_path = folder_paths.get_full_path(type, name)
|
||||
image_path = os.path.splitext(
|
||||
image_path)[0] + os.path.splitext(filepath)[1]
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
if extension not in _PREVIEW_FORMATS:
|
||||
return web.Response(status=400)
|
||||
|
||||
shutil.copyfile(filepath, image_path)
|
||||
temp_dir = folder_paths.get_directory_by_type("temp")
|
||||
filepath = os.path.join(temp_dir, filename)
|
||||
if (os.path.commonpath((os.path.realpath(temp_dir), os.path.realpath(filepath)))
|
||||
!= os.path.realpath(temp_dir) or not os.path.isfile(filepath)):
|
||||
return web.Response(status=400)
|
||||
|
||||
image_path = os.path.splitext(model_path)[0] + extension
|
||||
if (os.path.islink(image_path)
|
||||
or os.path.realpath(os.path.dirname(image_path))
|
||||
!= os.path.realpath(os.path.dirname(model_path))):
|
||||
return web.Response(status=400)
|
||||
|
||||
staged_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
dir=os.path.dirname(image_path), prefix=".easyuse-preview-", delete=False
|
||||
) as staged:
|
||||
staged_path = staged.name
|
||||
with open(filepath, "rb") as source:
|
||||
shutil.copyfileobj(source, staged)
|
||||
with Image.open(staged_path) as image:
|
||||
if image.format != _PREVIEW_FORMATS[extension]:
|
||||
return web.Response(status=400)
|
||||
image.verify()
|
||||
os.replace(staged_path, image_path)
|
||||
except (OSError, ValueError, UnidentifiedImageError):
|
||||
return web.Response(status=400)
|
||||
finally:
|
||||
if staged_path and os.path.exists(staged_path):
|
||||
os.unlink(staged_path)
|
||||
|
||||
return web.json_response({
|
||||
"image": type + "/" + os.path.basename(image_path)
|
||||
"image": model_type + "/" + os.path.basename(image_path)
|
||||
})
|
||||
|
||||
@PromptServer.instance.routes.post("/easyuse/model/download")
|
||||
|
||||
+7
-3
@@ -2,6 +2,10 @@ import random
|
||||
import server
|
||||
from enum import Enum
|
||||
|
||||
# Dedicated RNG for seed generation. Many custom nodes call random.seed() while they run,
|
||||
# which resets the global RNG and makes the generated seeds repeat.
|
||||
_seed_rng = random.Random()
|
||||
|
||||
class SGmode(Enum):
|
||||
FIX = 1
|
||||
INCR = 2
|
||||
@@ -34,7 +38,7 @@ class SeedGenerator:
|
||||
if self.base_value < 0:
|
||||
self.base_value = 1125899906842624
|
||||
elif self.action == SGmode.RAND:
|
||||
self.base_value = random.randint(0, 1125899906842624)
|
||||
self.base_value = _seed_rng.randint(0, 1125899906842624)
|
||||
|
||||
return seed
|
||||
|
||||
@@ -52,7 +56,7 @@ def control_seed(v, action, seed_is_global):
|
||||
if value < 0:
|
||||
value = 1125899906842624
|
||||
elif action == 'randomize' or action == 'randomize for each node':
|
||||
value = random.randint(0, 1125899906842624)
|
||||
value = _seed_rng.randint(0, 1125899906842624)
|
||||
if seed_is_global:
|
||||
v['inputs']['value'] = value
|
||||
|
||||
@@ -163,4 +167,4 @@ def onprompt(json_data):
|
||||
|
||||
return json_data
|
||||
|
||||
server.PromptServer.instance.add_on_prompt_handler(onprompt)
|
||||
server.PromptServer.instance.add_on_prompt_handler(onprompt)
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
[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.3.1"
|
||||
version = "1.4.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["diffusers", "accelerate", "clip_interrogator>=0.6.0", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python", "matplotlib", "peft"]
|
||||
dependencies = ["diffusers", "accelerate", "clip_interrogator", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python-headless", "matplotlib", "peft"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
|
||||
@@ -12,4 +12,4 @@ Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
|
||||
[tool.comfy]
|
||||
PublisherId = "yolain"
|
||||
DisplayName = "ComfyUI-Easy-Use"
|
||||
Icon = ""
|
||||
Icon = "https://mintlify.s3.us-west-1.amazonaws.com/yolain/images/logo.svg"
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ accelerate
|
||||
clip_interrogator>=0.6.0
|
||||
lark
|
||||
onnxruntime
|
||||
opencv-python
|
||||
opencv-python-headless
|
||||
sentencepiece
|
||||
spandrel
|
||||
matplotlib
|
||||
|
||||
+1923
-1373
File diff suppressed because it is too large
Load Diff
@@ -1,279 +0,0 @@
|
||||
{
|
||||
"Fooocus V2": "Fooocus V2扩展词",
|
||||
"Default (Slightly Cinematic)": "默认(轻微的电影感)",
|
||||
"Fooocus Enhance": "Fooocus-优化增强",
|
||||
"Fooocus Cinematic": "Fooocus-电影感",
|
||||
"Fooocus Sharp": "Fooocus-锐化",
|
||||
"Fooocus Masterpiece": "Fooocus-杰作",
|
||||
"Fooocus Photograph": "Fooocus-照片",
|
||||
"Fooocus Negative": "Fooocus-反向提示词",
|
||||
"SAI 3D Model": "SAI-3D模型",
|
||||
"SAI Analog Film": "SAI-模拟电影",
|
||||
"SAI Anime": "SAI-动漫",
|
||||
"SAI Cinematic": "SAI-电影片段",
|
||||
"SAI Comic Book": "SAI-漫画",
|
||||
"SAI Craft Clay": "SAI-工艺粘土",
|
||||
"SAI Digital Art": "SAI-数字艺术",
|
||||
"SAI Enhance": "SAI-增强",
|
||||
"SAI Fantasy Art": "SAI-奇幻艺术",
|
||||
"SAI Isometric": "SAI-等距风格",
|
||||
"SAI Line Art": "SAI-线条艺术",
|
||||
"SAI Lowpoly": "SAI-低多边形",
|
||||
"SAI Neonpunk": "SAI-霓虹朋克",
|
||||
"SAI Origami": "SAI-折纸",
|
||||
"SAI Photographic": "SAI-摄影",
|
||||
"SAI Pixel Art": "SAI-像素艺术",
|
||||
"SAI Texture": "SAI-纹理",
|
||||
"MRE Cinematic Dynamic": "MRE-史诗电影",
|
||||
"MRE Spontaneous Picture": "MRE-自然的抓拍照片",
|
||||
"MRE Artistic Vision": "MRE-艺术视觉",
|
||||
"MRE Dark Dream": "MRE-黑暗梦境",
|
||||
"MRE Gloomy Art": "MRE-阴郁艺术",
|
||||
"MRE Bad Dream": "MRE-噩梦",
|
||||
"MRE Underground": "MRE-阴森地下",
|
||||
"MRE Surreal Painting": "MRE-超现实主义绘画",
|
||||
"MRE Dynamic Illustration": "MRE-动态插画",
|
||||
"MRE Undead Art": "MRE-遗忘艺术家作品",
|
||||
"MRE Elemental Art": "MRE-元素艺术",
|
||||
"MRE Space Art": "MRE-空间艺术",
|
||||
"MRE Ancient Illustration": "MRE-古代插图",
|
||||
"MRE Brave Art": "MRE-勇敢艺术",
|
||||
"MRE Heroic Fantasy": "MRE-英雄幻想",
|
||||
"MRE Dark Cyberpunk": "MRE-黑暗赛博朋克",
|
||||
"MRE Lyrical Geometry": "MRE-抒情几何抽象画",
|
||||
"MRE Sumi E Symbolic": "MRE-墨绘长笔画",
|
||||
"MRE Sumi E Detailed": "MRE-精细墨绘画",
|
||||
"MRE Manga": "MRE-日本漫画",
|
||||
"MRE Anime": "MRE-日本动画片",
|
||||
"MRE Comic": "MRE-成人漫画书插画",
|
||||
"Ads Advertising": "广告-广告",
|
||||
"Ads Automotive": "广告-汽车",
|
||||
"Ads Corporate": "广告-企业品牌",
|
||||
"Ads Fashion Editorial": "广告-时尚编辑",
|
||||
"Ads Food Photography": "广告-食品摄影",
|
||||
"Ads Gourmet Food Photography": "广告-顶级美食摄影",
|
||||
"Ads Luxury": "广告-奢侈品",
|
||||
"Ads Real Estate": "广告-房地产",
|
||||
"Ads Retail": "广告-零售",
|
||||
"Artstyle Abstract": "艺术风格-抽象",
|
||||
"Artstyle Abstract Expressionism": "艺术风格-抽象表现主义",
|
||||
"Artstyle Art Deco": "艺术风格-装饰艺术",
|
||||
"Artstyle Art Nouveau": "艺术风格-新艺术",
|
||||
"Artstyle Constructivist": "艺术风格-构造主义",
|
||||
"Artstyle Cubist": "艺术风格-立体主义",
|
||||
"Artstyle Expressionist": "艺术风格-表现主义",
|
||||
"Artstyle Graffiti": "艺术风格-涂鸦",
|
||||
"Artstyle Hyperrealism": "艺术风格-超写实主义",
|
||||
"Artstyle Impressionist": "艺术风格-印象派",
|
||||
"Artstyle Pointillism": "艺术风格-点彩派",
|
||||
"Artstyle Pop Art": "艺术风格-波普艺术",
|
||||
"Artstyle Psychedelic": "艺术风格-迷幻",
|
||||
"Artstyle Renaissance": "艺术风格-文艺复兴",
|
||||
"Artstyle Steampunk": "艺术风格-蒸汽朋克",
|
||||
"Artstyle Surrealist": "艺术风格-超现实主义",
|
||||
"Artstyle Typography": "艺术风格-字体设计",
|
||||
"Artstyle Watercolor": "艺术风格-水彩",
|
||||
"Futuristic Biomechanical": "未来主义-生物机械",
|
||||
"Futuristic Biomechanical Cyberpunk": "未来主义-生物机械-赛博朋克",
|
||||
"Futuristic Cybernetic": "未来主义-人机融合",
|
||||
"Futuristic Cybernetic Robot": "未来主义-人机融合-机器人",
|
||||
"Futuristic Cyberpunk Cityscape": "未来主义-赛博朋克城市",
|
||||
"Futuristic Futuristic": "未来主义-未来主义",
|
||||
"Futuristic Retro Cyberpunk": "未来主义-复古赛博朋克",
|
||||
"Futuristic Retro Futurism": "未来主义-复古未来主义",
|
||||
"Futuristic Sci Fi": "未来主义-科幻",
|
||||
"Futuristic Vaporwave": "未来主义-蒸汽波",
|
||||
"Game Bubble Bobble": "游戏-泡泡龙",
|
||||
"Game Cyberpunk Game": "游戏-赛博朋克游戏",
|
||||
"Game Fighting Game": "游戏-格斗游戏",
|
||||
"Game Gta": "游戏-侠盗猎车手",
|
||||
"Game Mario": "游戏-马里奥",
|
||||
"Game Minecraft": "游戏-我的世界",
|
||||
"Game Pokemon": "游戏-宝可梦",
|
||||
"Game Retro Arcade": "游戏-复古街机",
|
||||
"Game Retro Game": "游戏-复古游戏",
|
||||
"Game Rpg Fantasy Game": "游戏-角色扮演幻想游戏",
|
||||
"Game Strategy Game": "游戏-策略游戏",
|
||||
"Game Streetfighter": "游戏-街头霸王",
|
||||
"Game Zelda": "游戏-塞尔达传说",
|
||||
"Misc Architectural": "其他-建筑",
|
||||
"Misc Disco": "其他-迪斯科",
|
||||
"Misc Dreamscape": "其他-梦境",
|
||||
"Misc Dystopian": "其他-反乌托邦",
|
||||
"Misc Fairy Tale": "其他-童话故事",
|
||||
"Misc Gothic": "其他-哥特风",
|
||||
"Misc Grunge": "其他-垮掉的",
|
||||
"Misc Horror": "其他-恐怖",
|
||||
"Misc Kawaii": "其他-可爱",
|
||||
"Misc Lovecraftian": "其他-洛夫克拉夫特",
|
||||
"Misc Macabre": "其他-恐怖",
|
||||
"Misc Manga": "其他-漫画",
|
||||
"Misc Metropolis": "其他-大都市",
|
||||
"Misc Minimalist": "其他-极简主义",
|
||||
"Misc Monochrome": "其他-单色",
|
||||
"Misc Nautical": "其他-航海",
|
||||
"Misc Space": "其他-太空",
|
||||
"Misc Stained Glass": "其他-彩色玻璃",
|
||||
"Misc Techwear Fashion": "其他-科技时尚",
|
||||
"Misc Tribal": "其他-部落",
|
||||
"Misc Zentangle": "其他-禅绕画",
|
||||
"Papercraft Collage": "手工艺-拼贴",
|
||||
"Papercraft Flat Papercut": "手工艺-平面剪纸",
|
||||
"Papercraft Kirigami": "手工艺-切纸",
|
||||
"Papercraft Paper Mache": "手工艺-纸浆塑造",
|
||||
"Papercraft Paper Quilling": "手工艺-纸艺卷轴",
|
||||
"Papercraft Papercut Collage": "手工艺-剪纸拼贴",
|
||||
"Papercraft Papercut Shadow Box": "手工艺-剪纸影箱",
|
||||
"Papercraft Stacked Papercut": "手工艺-层叠剪纸",
|
||||
"Papercraft Thick Layered Papercut": "手工艺-厚层剪纸",
|
||||
"Photo Alien": "摄影-外星人",
|
||||
"Photo Film Noir": "摄影-黑色电影",
|
||||
"Photo Glamour": "摄影-魅力",
|
||||
"Photo Hdr": "摄影-高动态范围",
|
||||
"Photo Iphone Photographic": "摄影-苹果手机摄影",
|
||||
"Photo Long Exposure": "摄影-长曝光",
|
||||
"Photo Neon Noir": "摄影-霓虹黑色",
|
||||
"Photo Silhouette": "摄影-轮廓",
|
||||
"Photo Tilt Shift": "摄影-移轴",
|
||||
"Cinematic Diva": "电影女主角",
|
||||
"Abstract Expressionism": "抽象表现主义",
|
||||
"Academia": "学术",
|
||||
"Action Figure": "动作人偶",
|
||||
"Adorable 3D Character": "可爱的3D角色",
|
||||
"Adorable Kawaii": "可爱的卡哇伊",
|
||||
"Art Deco": "装饰艺术",
|
||||
"Art Nouveau": "新艺术,美丽艺术",
|
||||
"Astral Aura": "星体光环",
|
||||
"Avant Garde": "前卫",
|
||||
"Baroque": "巴洛克",
|
||||
"Bauhaus Style Poster": "包豪斯风格海报",
|
||||
"Blueprint Schematic Drawing": "蓝图示意图",
|
||||
"Caricature": "漫画",
|
||||
"Cel Shaded Art": "卡通渲染",
|
||||
"Character Design Sheet": "角色设计表",
|
||||
"Classicism Art": "古典主义艺术",
|
||||
"Color Field Painting": "色彩领域绘画",
|
||||
"Colored Pencil Art": "彩色铅笔艺术",
|
||||
"Conceptual Art": "概念艺术",
|
||||
"Constructivism": "建构主义",
|
||||
"Cubism": "立体主义",
|
||||
"Dadaism": "达达主义",
|
||||
"Dark Fantasy": "黑暗奇幻",
|
||||
"Dark Moody Atmosphere": "黑暗忧郁气氛",
|
||||
"Dmt Art Style": "迷幻艺术风格",
|
||||
"Doodle Art": "涂鸦艺术",
|
||||
"Double Exposure": "双重曝光",
|
||||
"Dripping Paint Splatter Art": "滴漆飞溅艺术",
|
||||
"Expressionism": "表现主义",
|
||||
"Faded Polaroid Photo": "褪色的宝丽来照片",
|
||||
"Fauvism": "野兽派",
|
||||
"Flat 2d Art": "平面 2D 艺术",
|
||||
"Fortnite Art Style": "堡垒之夜艺术风格",
|
||||
"Futurism": "未来派",
|
||||
"Glitchcore": "故障核心",
|
||||
"Glo Fi": "光明高保真",
|
||||
"Googie Art Style": "古吉艺术风格",
|
||||
"Graffiti Art": "涂鸦艺术",
|
||||
"Harlem Renaissance Art": "哈莱姆文艺复兴艺术",
|
||||
"High Fashion": "高级时装",
|
||||
"Idyllic": "田园诗般",
|
||||
"Impressionism": "印象派",
|
||||
"Infographic Drawing": "信息图表绘图",
|
||||
"Ink Dripping Drawing": "滴墨绘画",
|
||||
"Japanese Ink Drawing": "日式水墨画",
|
||||
"Knolling Photography": "规律摆放摄影",
|
||||
"Light Cheery Atmosphere": "轻松愉快的气氛",
|
||||
"Logo Design": "标志设计",
|
||||
"Luxurious Elegance": "奢华优雅",
|
||||
"Macro Photography": "微距摄影",
|
||||
"Mandola Art": "曼陀罗艺术",
|
||||
"Marker Drawing": "马克笔绘图",
|
||||
"Medievalism": "中世纪主义",
|
||||
"Minimalism": "极简主义",
|
||||
"Neo Baroque": "新巴洛克",
|
||||
"Neo Byzantine": "新拜占庭",
|
||||
"Neo Futurism": "新未来派",
|
||||
"Neo Impressionism": "新印象派",
|
||||
"Neo Rococo": "新洛可可",
|
||||
"Neoclassicism": "新古典主义",
|
||||
"Op Art": "欧普艺术",
|
||||
"Ornate And Intricate": "华丽而复杂",
|
||||
"Pencil Sketch Drawing": "铅笔素描",
|
||||
"Pop Art 2": "流行艺术2",
|
||||
"Rococo": "洛可可",
|
||||
"Silhouette Art": "剪影艺术",
|
||||
"Simple Vector Art": "简单矢量艺术",
|
||||
"Sketchup": "草图",
|
||||
"Steampunk 2": "赛博朋克2",
|
||||
"Surrealism": "超现实主义",
|
||||
"Suprematism": "至上主义",
|
||||
"Terragen": "地表风景",
|
||||
"Tranquil Relaxing Atmosphere": "宁静轻松的氛围",
|
||||
"Sticker Designs": "贴纸设计",
|
||||
"Vibrant Rim Light": "生动的边缘光",
|
||||
"Volumetric Lighting": "体积照明",
|
||||
"Watercolor 2": "水彩2",
|
||||
"Whimsical And Playful": "异想天开、俏皮",
|
||||
"Mk Chromolithography": "MK 色彩版画",
|
||||
"Mk Cross Processing Print": "MK 交叉过程打印",
|
||||
"Mk Dufaycolor Photograph": "MK 杜法色彩照片",
|
||||
"Mk Herbarium": "MK 植物标本馆",
|
||||
"Mk Punk Collage": "MK 朋克拼贴画",
|
||||
"Mk Mosaic": "MK 镶嵌图",
|
||||
"Mk Van Gogh": "MK 梵高",
|
||||
"Mk Coloring Book": "MK 色彩书",
|
||||
"Mk Singer Sargent": "MK 辛格 · 萨尔生特",
|
||||
"Mk Pollock": "MK 波洛克",
|
||||
"Mk Basquiat": "MK 巴斯奎特",
|
||||
"Mk Andy Warhol": "MK 安迪 · 沃霍尔",
|
||||
"Mk Halftone Print": "MK 半色版画",
|
||||
"Mk Gond Painting": "MK 贡德绘画",
|
||||
"Mk Albumen Print": "MK 白蛋清印刷",
|
||||
"Mk Aquatint Print": "MK 水蚀刻印刷",
|
||||
"Mk Anthotype Print": "MK 花纹版画",
|
||||
"Mk Inuit Carving": "MK 因纽特雕塑",
|
||||
"Mk Bromoil Print": "MK 溴油印刷",
|
||||
"Mk Calotype Print": "MK 卡洛雅图印刷",
|
||||
"Mk Color Sketchnote": "MK色彩素描笔记",
|
||||
"Mk Cibulak Porcelain": "MK 西布拉瓷器",
|
||||
"Mk Alcohol Ink Art": "MK 酒精水彩艺术",
|
||||
"Mk One Line Art": "MK 一线画",
|
||||
"Mk Blacklight Paint": "MK 黑光油漆",
|
||||
"Mk Carnival Glass": "MK 嘉年华玻璃",
|
||||
"Mk Cyanotype Print": "MK 青色版画",
|
||||
"Mk Cross Stitching": "MK 交叉针织",
|
||||
"Mk Encaustic Paint": "MK 蜡漆",
|
||||
"Mk Embroidery": "MK 刺绣",
|
||||
"Mk Gyotaku": "MK 鱼拓版画",
|
||||
"Mk Luminogram": "MK 光感影像",
|
||||
"Mk Lite Brite Art": "MK 彩色灯泡艺术",
|
||||
"Mk Mokume Gane": "MK 木金工艺",
|
||||
"Pebble Art": "MK 鹅卵石艺术",
|
||||
"Mk Palekh": "MK 帕列赫",
|
||||
"Mk Suminagashi": "MK 澄洗画",
|
||||
"Mk Scrimshaw": "MK 丝线绣",
|
||||
"Mk Shibori": "MK 湿布雕版印刷",
|
||||
"Mk Vitreous Enamel": "MK 玻璃珐琅",
|
||||
"Mk Ukiyo E": "MK 浮世绘",
|
||||
"Mk Vintage Airline Poster": "MK 古董航空公司海报",
|
||||
"Mk Vintage Travel Poster": "MK 古董旅行海报",
|
||||
"Mk Bauhaus Style": "Mk 包豪斯风格",
|
||||
"Mk Afrofuturism": "Mk 非洲未来主义",
|
||||
"Mk Atompunk": "Mk 原子朋克",
|
||||
"Mk Constructivism": "Mk 构成派",
|
||||
"Mk Chicano Art": "Mk 西班牙裔美国艺术",
|
||||
"Mk De Stijl": "Mk 去风格派",
|
||||
"Mk Dayak Art": "Mk 达雅克艺术",
|
||||
"Mk Fayum Portrait": "Mk 法尤姆肖像画",
|
||||
"Mk Illuminated Manuscript": "Mk 彩绘手稿",
|
||||
"Mk Kalighat Painting": "Mk 卡利加特绘画",
|
||||
"Mk Madhubani Painting": "Mk 马杜班尼绘画",
|
||||
"Mk Pictorialism": "Mk 描绘主义",
|
||||
"Mk Pichwai Painting": "Mk 皮奇瓦伊绘画",
|
||||
"Mk Patachitra Painting": "Mk 帕塔基特拉绘画",
|
||||
"Mk Samoan Art Inspired": "Mk 萨莫亚艺术启发的",
|
||||
"Mk Tlingit Art": "Mk 特林吉特艺术",
|
||||
"Mk Adnate Style": "Mk 阿达内特风格",
|
||||
"Mk Ron English Style": "Mk 罗恩英国风格",
|
||||
"Mk Shepard Fairey Style": "Mk 舒帕德 · 费尔利风格"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,275 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).parents[1]
|
||||
UTILS_PATH = PLUGIN_ROOT / "py" / "libs" / "utils.py"
|
||||
CONFIG_PATH = PLUGIN_ROOT / "py" / "config.py"
|
||||
LOADER_PATH = PLUGIN_ROOT / "py" / "libs" / "loader.py"
|
||||
XYPLOT_NODE_PATH = PLUGIN_ROOT / "py" / "nodes" / "xyplot.py"
|
||||
XYPLOT_LIB_PATH = PLUGIN_ROOT / "py" / "libs" / "xyplot.py"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def installed_modules(modules):
|
||||
added = []
|
||||
for name, module in modules.items():
|
||||
if name not in sys.modules:
|
||||
added.append(name)
|
||||
sys.modules[name] = module
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for name in added:
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
|
||||
def load_module(name, path, package=None):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
if package is not None:
|
||||
module.__package__ = package
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def make_package(name, **attrs):
|
||||
package = types.ModuleType(name)
|
||||
package.__path__ = []
|
||||
for key, value in attrs.items():
|
||||
setattr(package, key, value)
|
||||
return package
|
||||
|
||||
|
||||
def comfy_stubs():
|
||||
model_management = types.ModuleType("comfy.model_management")
|
||||
model_base = types.ModuleType("comfy.model_base")
|
||||
model_base.BaseModel = object
|
||||
supported_models_base = types.ModuleType("comfy.supported_models_base")
|
||||
supported_models_base.BASE = object
|
||||
supported_models = types.ModuleType("comfy.supported_models")
|
||||
supported_models.supported_models_base = supported_models_base
|
||||
comfy = make_package(
|
||||
"comfy",
|
||||
model_management=model_management,
|
||||
model_base=model_base,
|
||||
supported_models_base=supported_models_base,
|
||||
supported_models=supported_models,
|
||||
)
|
||||
for name in (
|
||||
"SDXL", "SDXLRefiner", "SD15", "SD20", "SVD_img2vid", "SD3",
|
||||
"HunyuanDiT", "Flux", "GenmoMochi", "Anima", "Krea2",
|
||||
):
|
||||
setattr(supported_models, name, type(name, (), {}))
|
||||
server = types.ModuleType("server")
|
||||
server.PromptServer = object
|
||||
return {
|
||||
"comfy": comfy,
|
||||
"comfy.model_management": model_management,
|
||||
"comfy.model_base": model_base,
|
||||
"comfy.supported_models_base": supported_models_base,
|
||||
"comfy.supported_models": supported_models,
|
||||
"server": server,
|
||||
}
|
||||
|
||||
|
||||
def xyplot_node_stubs():
|
||||
folder_paths = types.ModuleType("folder_paths")
|
||||
folder_paths.get_filename_list = lambda folder: []
|
||||
return {
|
||||
"comfy": make_package("comfy"),
|
||||
"folder_paths": folder_paths,
|
||||
"fake_py": make_package("fake_py"),
|
||||
"fake_py.nodes": make_package("fake_py.nodes"),
|
||||
"fake_py.libs": make_package("fake_py.libs"),
|
||||
"fake_py.config": make_package("fake_py.config", RESOURCES_DIR="resources"),
|
||||
"fake_py.libs.utils": make_package("fake_py.libs.utils", getMetadata=lambda *args, **kwargs: None),
|
||||
}
|
||||
|
||||
|
||||
def xyplot_lib_stubs():
|
||||
fake_utils = make_package("fake_py.utils", easySave=object, get_sd_version=lambda model: "unknown")
|
||||
fake_adv_encode = make_package("fake_py.libs.adv_encode", advanced_encode=object)
|
||||
fake_controlnet = make_package("fake_py.libs.controlnet", easyControlnet=object)
|
||||
fake_log = make_package("fake_py.libs.log", log_node_warn=lambda *args, **kwargs: None)
|
||||
return {
|
||||
"nodes": make_package("nodes", CLIPTextEncode=object),
|
||||
"fake_py": make_package("fake_py"),
|
||||
"fake_py.libs": make_package("fake_py.libs"),
|
||||
"fake_py.modules": make_package("fake_py.modules"),
|
||||
"fake_py.utils": fake_utils,
|
||||
"fake_py.libs.utils": fake_utils,
|
||||
"fake_py.libs.adv_encode": fake_adv_encode,
|
||||
"fake_py.libs.controlnet": fake_controlnet,
|
||||
"fake_py.libs.log": fake_log,
|
||||
"fake_py.modules.layer_diffuse": make_package("fake_py.modules.layer_diffuse", LayerDiffuse=object),
|
||||
"fake_py.config": make_package("fake_py.config", RESOURCES_DIR="resources"),
|
||||
}
|
||||
|
||||
|
||||
def loader_stubs():
|
||||
comfy = make_package("comfy")
|
||||
comfy.utils = make_package("comfy.utils")
|
||||
comfy.sd = make_package("comfy.sd")
|
||||
comfy.controlnet = make_package("comfy.controlnet")
|
||||
comfy.model_patcher = make_package("comfy.model_patcher", ModelPatcher=type("ModelPatcher", (), {}))
|
||||
folder_paths = make_package("folder_paths")
|
||||
folder_paths.get_full_path = lambda folder, name: None
|
||||
folder_paths.get_folder_paths = lambda folder: []
|
||||
folder_paths.get_filename_list = lambda folder: []
|
||||
fake_log = make_package("fake_py.libs.log", log_node_info=lambda *args, **kwargs: None, log_node_error=lambda *args, **kwargs: None)
|
||||
fake_utils = make_package("fake_py.libs.utils", get_sd_version=lambda model: "unknown")
|
||||
fake_config = make_package(
|
||||
"fake_py.config",
|
||||
DIFFUSION_MODEL_XY_DEFAULTS={},
|
||||
DIFFUSION_MODEL_CLIP_TYPES={"anima": "anima", "krea2": "krea2"},
|
||||
)
|
||||
fake_pixart = make_package("fake_py.modules.dit.pixArt.loader", load_pixart=object)
|
||||
return {
|
||||
"comfy": comfy,
|
||||
"comfy.utils": comfy.utils,
|
||||
"comfy.sd": comfy.sd,
|
||||
"comfy.controlnet": comfy.controlnet,
|
||||
"comfy.model_patcher": comfy.model_patcher,
|
||||
"folder_paths": folder_paths,
|
||||
"nodes": make_package("nodes", NODE_CLASS_MAPPINGS={}),
|
||||
"fake_py": make_package("fake_py"),
|
||||
"fake_py.libs": make_package("fake_py.libs"),
|
||||
"fake_py.modules": make_package("fake_py.modules"),
|
||||
"fake_py.modules.dit": make_package("fake_py.modules.dit"),
|
||||
"fake_py.modules.dit.pixArt": make_package("fake_py.modules.dit.pixArt"),
|
||||
"fake_py.libs.log": fake_log,
|
||||
"fake_py.libs.utils": fake_utils,
|
||||
"fake_py.config": fake_config,
|
||||
"fake_py.modules.dit.pixArt.loader": fake_pixart,
|
||||
}
|
||||
|
||||
|
||||
class FakeModelPatcher:
|
||||
def __init__(self, model_config=None, latent_format=None):
|
||||
self.model = types.SimpleNamespace(model_config=model_config, latent_format=latent_format)
|
||||
|
||||
|
||||
class FakeLatentFormat:
|
||||
latent_dimensions = 3
|
||||
latent_channels = 16
|
||||
|
||||
|
||||
class DiffusionXYHelperTests(unittest.TestCase):
|
||||
def test_get_sd_version_anima_and_krea2(self):
|
||||
with installed_modules(comfy_stubs()):
|
||||
utils = load_module("diffusion_xy_test_utils", UTILS_PATH)
|
||||
|
||||
anima_config = utils.comfy.supported_models.Anima()
|
||||
self.assertEqual(utils.get_sd_version(FakeModelPatcher(anima_config)), "anima")
|
||||
|
||||
krea2_config = utils.comfy.supported_models.Krea2()
|
||||
self.assertEqual(utils.get_sd_version(FakeModelPatcher(krea2_config)), "krea2")
|
||||
|
||||
def test_diffusion_model_xy_defaults_are_complete(self):
|
||||
with tempfile.TemporaryDirectory() as models_dir:
|
||||
folder_paths = types.ModuleType("folder_paths")
|
||||
folder_paths.models_dir = models_dir
|
||||
with installed_modules({"folder_paths": folder_paths}):
|
||||
config = load_module("diffusion_xy_test_config", CONFIG_PATH)
|
||||
|
||||
for family in ("anima", "krea2"):
|
||||
defaults = config.DIFFUSION_MODEL_XY_DEFAULTS[family]
|
||||
self.assertTrue(defaults["clip_name"])
|
||||
self.assertTrue(defaults["clip_type"])
|
||||
self.assertTrue(defaults["vae_name"])
|
||||
self.assertEqual(config.DIFFUSION_MODEL_CLIP_TYPES[family], family)
|
||||
|
||||
def test_load_diffusion_model_required_rejects_missing_clip_and_vae(self):
|
||||
with installed_modules(loader_stubs()):
|
||||
loader_module = load_module("fake_py.libs.loader", LOADER_PATH, "fake_py.libs")
|
||||
loader = loader_module.easyLoader.__new__(loader_module.easyLoader)
|
||||
loader.load_diffusion_model = lambda model_name: ("model", model_name)
|
||||
loader.load_clip = lambda clip_name, type='stable_diffusion': ("clip", clip_name, type)
|
||||
loader.load_vae = lambda vae_name: ("vae", vae_name)
|
||||
loader_module.get_sd_version = lambda model: "krea2"
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "clip_name is required"):
|
||||
loader.load_diffusion_model_required("model.safetensors", "None", "vae.safetensors")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "vae_name is required"):
|
||||
loader.load_diffusion_model_required("model.safetensors", "clip.safetensors", None)
|
||||
|
||||
model, clip, vae, family = loader.load_diffusion_model_required(
|
||||
"model.safetensors", "clip.safetensors", "vae.safetensors"
|
||||
)
|
||||
self.assertEqual(family, "krea2")
|
||||
self.assertEqual(clip[2], "krea2")
|
||||
|
||||
loader_module.get_sd_version = lambda model: "flux"
|
||||
with self.assertRaisesRegex(RuntimeError, "unsupported diffusion model family: flux"):
|
||||
loader.load_diffusion_model_required("model.safetensors", "clip.safetensors", "vae.safetensors")
|
||||
|
||||
def test_xyplot_diffusion_model_value_format(self):
|
||||
with installed_modules(xyplot_node_stubs()):
|
||||
node_module = load_module("fake_py.nodes.xyplot", XYPLOT_NODE_PATH, "fake_py.nodes")
|
||||
node = node_module.XYplot_DiffusionModel()
|
||||
|
||||
result = node.xy_value(
|
||||
2,
|
||||
model_name_1="waiANIMA_v10Base10.safetensors",
|
||||
clip_name_1="qwen_3_06b_base.safetensors",
|
||||
vae_name_1="qwen_image_vae.safetensors",
|
||||
model_name_2="moodyKrea2Mix,v70.safetensors",
|
||||
clip_name_2="Auto",
|
||||
vae_name_2="Auto",
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]["axis"], "advanced: DiffusionModel")
|
||||
self.assertEqual(
|
||||
result[0]["values"],
|
||||
[
|
||||
"waiANIMA_v10Base10.safetensors,qwen_3_06b_base.safetensors,qwen_image_vae.safetensors",
|
||||
"moodyKrea2Mix*v70.safetensors,Auto,Auto",
|
||||
],
|
||||
)
|
||||
|
||||
model_name, clip_name, vae_name = result[0]["values"][0].split(",")
|
||||
self.assertEqual(model_name.replace("*", ","), "waiANIMA_v10Base10.safetensors")
|
||||
self.assertEqual(clip_name.replace("*", ","), "qwen_3_06b_base.safetensors")
|
||||
self.assertEqual(vae_name.replace("*", ","), "qwen_image_vae.safetensors")
|
||||
|
||||
model_name, clip_name, vae_name = result[0]["values"][1].split(",")
|
||||
self.assertEqual(model_name.replace("*", ","), "moodyKrea2Mix,v70.safetensors")
|
||||
self.assertEqual(clip_name, "Auto")
|
||||
self.assertEqual(vae_name, "Auto")
|
||||
|
||||
def test_ensure_latent_raises_for_nonempty_4d_latent_without_image(self):
|
||||
with installed_modules(xyplot_lib_stubs()):
|
||||
xyplot_module = load_module("fake_py.libs.xyplot", XYPLOT_LIB_PATH, "fake_py.libs")
|
||||
|
||||
model = FakeModelPatcher(latent_format=FakeLatentFormat())
|
||||
vae = types.SimpleNamespace(encode=lambda pixels: torch.zeros([pixels.shape[0], 16, 1, pixels.shape[2], pixels.shape[3]]))
|
||||
samples = {"samples": torch.ones([1, 4, 64, 64])}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "requires an input image"):
|
||||
xyplot_module.easyXYPlot._ensure_latent_for_model(model, vae, samples, {})
|
||||
|
||||
def test_ensure_latent_expands_empty_4d_latent_to_5d(self):
|
||||
with installed_modules(xyplot_lib_stubs()):
|
||||
xyplot_module = load_module("fake_py.libs.xyplot", XYPLOT_LIB_PATH, "fake_py.libs")
|
||||
|
||||
model = FakeModelPatcher(latent_format=FakeLatentFormat())
|
||||
vae = types.SimpleNamespace(encode=lambda pixels: torch.zeros([pixels.shape[0], 16, 1, pixels.shape[2], pixels.shape[3]]))
|
||||
samples = {"samples": torch.zeros([1, 4, 64, 64])}
|
||||
|
||||
result = xyplot_module.easyXYPlot._ensure_latent_for_model(model, vae, samples, {})
|
||||
|
||||
self.assertEqual(result["samples"].shape, torch.Size([1, 16, 1, 64, 64]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).parents[1] / "py" / "libs" / "path_utils.py"
|
||||
SPEC = importlib.util.spec_from_file_location("easyuse_path_utils", MODULE_PATH)
|
||||
path_utils = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(path_utils)
|
||||
|
||||
|
||||
class ResolveOutputFilePathTests(unittest.TestCase):
|
||||
def test_relative_subdirectory_is_resolved_under_output_root(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
result = path_utils.resolve_output_file_path(
|
||||
output_root, "metadata", "prompt", "txt"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
os.path.join(os.path.realpath(output_root), "metadata", "prompt.txt"),
|
||||
)
|
||||
|
||||
def test_absolute_directory_inside_output_root_is_allowed(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
inside = os.path.join(output_root, "metadata")
|
||||
|
||||
result = path_utils.resolve_output_file_path(
|
||||
output_root, inside, "prompt", "txt"
|
||||
)
|
||||
|
||||
self.assertEqual(result, os.path.join(inside, "prompt.txt"))
|
||||
|
||||
def test_absolute_directory_outside_output_root_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
with tempfile.TemporaryDirectory() as outside:
|
||||
with self.assertRaises(ValueError):
|
||||
path_utils.resolve_output_file_path(
|
||||
output_root, outside, "marker", "txt"
|
||||
)
|
||||
|
||||
def test_output_directory_traversal_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
with self.assertRaises(ValueError):
|
||||
path_utils.resolve_output_file_path(
|
||||
output_root, "../outside", "marker", "txt"
|
||||
)
|
||||
|
||||
def test_file_name_traversal_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
with self.assertRaises(ValueError):
|
||||
path_utils.resolve_output_file_path(
|
||||
output_root, ".", "../../marker", "txt"
|
||||
)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
|
||||
def test_symlink_escape_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as output_root:
|
||||
with tempfile.TemporaryDirectory() as outside:
|
||||
os.symlink(outside, os.path.join(output_root, "linked"))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
path_utils.resolve_output_file_path(
|
||||
output_root, "linked", "marker", "txt"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,174 @@
|
||||
import ast
|
||||
import asyncio
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from functools import lru_cache
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import urlsplit
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
|
||||
ROUTES_PATH = Path(__file__).parents[1] / "py" / "routes.py"
|
||||
|
||||
|
||||
def load_handlers(folder_paths, get_metadata):
|
||||
"""Load the actual handlers without importing ComfyUI's GPU dependencies."""
|
||||
names = {
|
||||
"_same_origin_request", "get_reboot_token", "reboot", "_model_sha256",
|
||||
"load_metadata", "save_notes", "save_preview",
|
||||
}
|
||||
tree = ast.parse(ROUTES_PATH.read_text())
|
||||
functions = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names:
|
||||
node.decorator_list = []
|
||||
functions.append(node)
|
||||
namespace = {
|
||||
"os": os, "sys": sys, "hashlib": hashlib, "hmac": __import__("hmac"),
|
||||
"json": json, "shutil": shutil, "tempfile": tempfile,
|
||||
"lru_cache": lru_cache, "urlsplit": urlsplit, "web": web,
|
||||
"Image": Image, "UnidentifiedImageError": UnidentifiedImageError,
|
||||
"folder_paths": folder_paths, "getMetadata": get_metadata,
|
||||
"_reboot_token": "test-reboot-token",
|
||||
"_PREVIEW_FORMATS": {
|
||||
".png": "PNG", ".jpg": "JPEG", ".jpeg": "JPEG",
|
||||
".webp": "WEBP", ".gif": "GIF",
|
||||
},
|
||||
}
|
||||
exec(compile(ast.Module(body=functions, type_ignores=[]), str(ROUTES_PATH), "exec"), namespace)
|
||||
return SimpleNamespace(**namespace)
|
||||
|
||||
|
||||
class SecurityRouteTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.workspace = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.workspace.cleanup)
|
||||
self.root = Path(self.workspace.name)
|
||||
self.model_dir = self.root / "models"
|
||||
self.temp_dir = self.root / "temp"
|
||||
self.model_dir.mkdir()
|
||||
self.temp_dir.mkdir()
|
||||
self.model_path = self.model_dir / "sample.safetensors"
|
||||
self.model_path.write_bytes(b"model data")
|
||||
paths = SimpleNamespace(
|
||||
get_filename_list=lambda kind: [self.model_path.name],
|
||||
get_full_path=lambda kind, name: str(self.model_path),
|
||||
get_directory_by_type=lambda kind: str(self.temp_dir),
|
||||
)
|
||||
self.handlers = load_handlers(
|
||||
paths,
|
||||
lambda path: json.dumps({"__metadata__": {"easyuse.notes": "<img onerror=alert(1)>"}}),
|
||||
)
|
||||
|
||||
def request(self, name="loras/sample.safetensors", filename="preview.png", **body):
|
||||
payload = {"type": "temp", "filename": filename, **body}
|
||||
return SimpleNamespace(
|
||||
match_info={"name": name},
|
||||
json=lambda: asyncio.sleep(0, result=payload),
|
||||
headers={}, host="localhost:8188",
|
||||
)
|
||||
|
||||
def test_save_rejects_script_and_custom_node_target(self):
|
||||
(self.temp_dir / "payload.py").write_text("print('sentinel')")
|
||||
response = asyncio.run(self.handlers.save_preview(self.request(filename="payload.py")))
|
||||
self.assertEqual(response.status, 400)
|
||||
response = asyncio.run(self.handlers.save_preview(
|
||||
self.request(name="custom_nodes/package/__init__.py", filename="payload.py")
|
||||
))
|
||||
self.assertEqual(response.status, 400)
|
||||
|
||||
def test_save_accepts_real_image_and_rejects_disguised_script(self):
|
||||
Image.new("RGB", (1, 1)).save(self.temp_dir / "preview.png")
|
||||
response = asyncio.run(self.handlers.save_preview(self.request()))
|
||||
self.assertEqual(response.status, 200)
|
||||
with Image.open(self.model_dir / "sample.png") as saved:
|
||||
self.assertEqual(saved.format, "PNG")
|
||||
|
||||
(self.temp_dir / "preview.png").write_text("print('sentinel')")
|
||||
response = asyncio.run(self.handlers.save_preview(self.request()))
|
||||
self.assertEqual(response.status, 400)
|
||||
with Image.open(self.model_dir / "sample.png") as saved:
|
||||
self.assertEqual(saved.format, "PNG")
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
|
||||
def test_save_does_not_follow_preview_symlink(self):
|
||||
Image.new("RGB", (1, 1)).save(self.temp_dir / "preview.png")
|
||||
protected = self.root / "protected.txt"
|
||||
protected.write_text("untouched")
|
||||
os.symlink(protected, self.model_dir / "sample.png")
|
||||
response = asyncio.run(self.handlers.save_preview(self.request()))
|
||||
self.assertEqual(response.status, 400)
|
||||
self.assertEqual(protected.read_text(), "untouched")
|
||||
|
||||
def test_metadata_ignores_forged_hash_sidecar(self):
|
||||
(self.model_dir / "sample.sha256").write_text("0" * 64)
|
||||
response = asyncio.run(self.handlers.load_metadata(self.request()))
|
||||
self.assertEqual(
|
||||
json.loads(response.text)["easyuse.sha256"],
|
||||
hashlib.sha256(self.model_path.read_bytes()).hexdigest(),
|
||||
)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
|
||||
def test_notes_reject_custom_nodes_and_do_not_follow_symlinks(self):
|
||||
request = self.request(name="custom_nodes/package/__init__.py")
|
||||
request.text = lambda: asyncio.sleep(0, result="new notes")
|
||||
self.assertEqual(asyncio.run(self.handlers.save_notes(request)).status, 400)
|
||||
|
||||
protected = self.root / "protected.txt"
|
||||
protected.write_text("untouched")
|
||||
os.symlink(protected, self.model_dir / "sample.txt")
|
||||
request = self.request()
|
||||
request.text = lambda: asyncio.sleep(0, result="new notes")
|
||||
self.assertEqual(asyncio.run(self.handlers.save_notes(request)).status, 200)
|
||||
self.assertEqual(protected.read_text(), "untouched")
|
||||
self.assertEqual((self.model_dir / "sample.txt").read_text(), "new notes")
|
||||
|
||||
def test_reboot_requires_token_and_same_origin(self):
|
||||
self.assertTrue(inspect.iscoroutinefunction(self.handlers.get_reboot_token))
|
||||
self.assertTrue(inspect.iscoroutinefunction(self.handlers.reboot))
|
||||
request = self.request()
|
||||
request.headers = {"Sec-Fetch-Site": "cross-site"}
|
||||
self.assertEqual(asyncio.run(self.handlers.get_reboot_token(request)).status, 403)
|
||||
request.headers = {"Sec-Fetch-Site": "same-origin"}
|
||||
self.assertEqual(json.loads(asyncio.run(self.handlers.get_reboot_token(request)).text)["token"], "test-reboot-token")
|
||||
request.headers = {}
|
||||
with patch.object(self.handlers.os, "execv", return_value="restarted") as restart:
|
||||
self.assertEqual(asyncio.run(self.handlers.reboot(request)).status, 403)
|
||||
request.headers = {"X-EasyUse-Reboot-Token": "test-reboot-token", "Origin": "http://other.test"}
|
||||
self.assertEqual(asyncio.run(self.handlers.reboot(request)).status, 403)
|
||||
restart.assert_not_called()
|
||||
request.headers["Origin"] = "http://localhost:8188"
|
||||
request.headers["Sec-Fetch-Site"] = "same-site"
|
||||
self.assertEqual(asyncio.run(self.handlers.reboot(request)).status, 403)
|
||||
request.headers["Sec-Fetch-Site"] = "same-origin"
|
||||
self.assertEqual(asyncio.run(self.handlers.reboot(request)), "restarted")
|
||||
restart.assert_called_once()
|
||||
|
||||
def test_reboot_routes_return_http_responses(self):
|
||||
async def exercise_routes():
|
||||
app = web.Application()
|
||||
app.router.add_get("/easyuse/reboot-token", self.handlers.get_reboot_token)
|
||||
app.router.add_post("/easyuse/reboot", self.handlers.reboot)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
token_response = await client.get("/easyuse/reboot-token")
|
||||
self.assertEqual(token_response.status, 200)
|
||||
self.assertEqual((await token_response.json())["token"], "test-reboot-token")
|
||||
reboot_response = await client.post("/easyuse/reboot")
|
||||
self.assertEqual(reboot_response.status, 403)
|
||||
|
||||
asyncio.run(exercise_routes())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,50 +0,0 @@
|
||||
# 开发人员使用(请勿运行)
|
||||
# 将 https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Translation 的翻译文件转换格式以适配 ComfyUI locales
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
old_json_path = 'ComfyUI-Easy-Use.json'
|
||||
root_path = pathlib.Path(__file__).parent.parent
|
||||
new_json_path = os.path.join(root_path,'locales/zh/nodeDefs.json')
|
||||
|
||||
def transform_dict(data):
|
||||
new_dict = {}
|
||||
for k, v in data.items():
|
||||
new_dict[k] = {
|
||||
"display_name": "",
|
||||
"inputs": {}
|
||||
}
|
||||
if isinstance(v, dict):
|
||||
for key, value in v.items():
|
||||
if key == 'title':
|
||||
new_dict[k]['display_name'] = value
|
||||
elif key in ['inputs','widgets']:
|
||||
for _key, _value in value.items():
|
||||
new_dict[k]['inputs'] = {
|
||||
**new_dict[k]['inputs'],
|
||||
_key: {"name": _value}
|
||||
}
|
||||
elif key == 'outputs':
|
||||
if not new_dict[k].get('outputs'):
|
||||
new_dict[k]['outputs'] = {}
|
||||
for idx, (out_key, out_value) in enumerate(value.items()):
|
||||
new_dict[k]['outputs'][idx] = {"name": out_value}
|
||||
return new_dict
|
||||
|
||||
def main():
|
||||
|
||||
# 读取原始JSON文件
|
||||
with open(old_json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 转换数据
|
||||
transformed_data = transform_dict(data)
|
||||
|
||||
# 写入新的JSON文件
|
||||
with open(new_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(transformed_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -114,10 +114,10 @@ export class ModelInfoDialog extends ComfyDialog {
|
||||
|
||||
let pre = this.customNotes.substring(end, pos);
|
||||
if (pre) {
|
||||
pre = pre.replaceAll("\n", "<br>");
|
||||
notes.push(
|
||||
$el("span", {
|
||||
innerHTML: pre,
|
||||
textContent: pre,
|
||||
style: { whiteSpace: "pre-line" },
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -127,6 +127,7 @@ export class ModelInfoDialog extends ComfyDialog {
|
||||
href: m[0],
|
||||
textContent: m[0],
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -335,7 +336,9 @@ export class ModelInfoDialog extends ComfyDialog {
|
||||
const blob = await (await fetch(cate.url)).blob();
|
||||
|
||||
// Store it in temp
|
||||
const name = "temp_preview." + new URL(cate.url).pathname.split(".")[1];
|
||||
const extension = ({"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif"})[blob.type]
|
||||
|| new URL(cate.url).pathname.split(".").pop().toLowerCase();
|
||||
const name = "temp_preview." + extension;
|
||||
const body = new FormData();
|
||||
body.append("image", new File([blob], name));
|
||||
body.append("overwrite", "true");
|
||||
@@ -365,10 +368,13 @@ export class ModelInfoDialog extends ComfyDialog {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}).then(_=>{
|
||||
toast.success($t('Saving Succeed'))
|
||||
toast.hideLoading()
|
||||
});
|
||||
}).then(response => {
|
||||
if (!response.ok) throw new Error(`Error saving preview (${response.status})`);
|
||||
toast.success($t('Saving Succeed'));
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
toast.error($t('Saving Failed'));
|
||||
}).finally(() => toast.hideLoading());
|
||||
this.isSaving = false
|
||||
app.refreshComboInNodes();
|
||||
},
|
||||
@@ -680,4 +686,4 @@ export class LoraInfoDialog extends ModelInfoDialog {
|
||||
|
||||
return btns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,10 +486,16 @@ app.registerExtension({
|
||||
// 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: _ =>{
|
||||
callback: async _ =>{
|
||||
if (confirm($t("Are you sure you'd like to reboot the server?"))){
|
||||
try {
|
||||
api.fetchApi("/easyuse/reboot");
|
||||
const tokenResponse = await api.fetchApi("/easyuse/reboot-token");
|
||||
if (!tokenResponse.ok) throw new Error("Could not get reboot token");
|
||||
const {token} = await tokenResponse.json();
|
||||
await api.fetchApi("/easyuse/reboot", {
|
||||
method: "POST",
|
||||
headers: {"X-EasyUse-Reboot-Token": token},
|
||||
});
|
||||
} catch (exception) {}
|
||||
}
|
||||
}
|
||||
@@ -607,4 +613,4 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1149,7 +1149,7 @@ app.registerExtension({
|
||||
|
||||
for (const list of text) {
|
||||
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
// w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.6;
|
||||
w.value = list;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
// 全局Seed
|
||||
function globalSeedHandler(event) {
|
||||
@@ -27,21 +28,31 @@ function globalSeedHandler(event) {
|
||||
|
||||
api.addEventListener("easyuse-global-seed", globalSeedHandler);
|
||||
|
||||
const original_queuePrompt = api.queuePrompt;
|
||||
async function queuePrompt_with_seed(number, { output, workflow }) {
|
||||
function addSeedWidgetsToWorkflow(prompt, graph) {
|
||||
const workflow = prompt?.workflow;
|
||||
if (!workflow || typeof workflow !== 'object') return prompt;
|
||||
|
||||
workflow.seed_widgets = {};
|
||||
|
||||
for(let i in app.graph._nodes_by_id) {
|
||||
let widgets = app.graph._nodes_by_id[i].widgets;
|
||||
const nodes = graph?._nodes_by_id || {};
|
||||
for(let i in nodes) {
|
||||
let widgets = nodes[i].widgets;
|
||||
if(widgets) {
|
||||
for(let j in widgets) {
|
||||
if((widgets[j].name == 'seed_num' || widgets[j].name == 'seed' || widgets[j].name == 'noise_seed') && widgets[j].type != 'converted-widget')
|
||||
workflow.seed_widgets[i] = parseInt(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await original_queuePrompt.call(api, number, { output, workflow });
|
||||
return prompt;
|
||||
}
|
||||
|
||||
api.queuePrompt = queuePrompt_with_seed;
|
||||
// Keep ComfyUI's queuePrompt untouched so its validation errors and evolving
|
||||
// execution options remain owned by the core frontend.
|
||||
const original_graphToPrompt = app.graphToPrompt;
|
||||
app.graphToPrompt = function graphToPrompt_with_seed(...args) {
|
||||
const graph = args[0] || app.rootGraph || app.graph;
|
||||
return Promise.resolve(original_graphToPrompt.apply(this, args))
|
||||
.then(prompt => addSeedWidgetsToWorkflow(prompt, graph));
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user