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 |
@@ -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
-1
@@ -11,10 +11,13 @@ web_beta/**
|
||||
web_version/dev/**
|
||||
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
|
||||
|
||||
Submodule ComfyUI-Easy-Use-Frontend updated: 29e4b02e36...656ae09121
+56
-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,60 @@ 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
|
||||
|
||||
@@ -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,60 @@ 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
|
||||
@@ -528,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
-1
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.3.3"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
import yaml
|
||||
import json
|
||||
@@ -12,6 +12,18 @@ 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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"Hotkeys": "快捷键",
|
||||
"Nodes": "节点相关",
|
||||
"NodesMap": "管理节点组",
|
||||
"StylesSelector": "样式选择器"
|
||||
"StylesSelector": "样式选择器",
|
||||
"MultiAngle": "摄影机多角度提示词"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "工具",
|
||||
|
||||
+220
-1
@@ -154,6 +154,9 @@
|
||||
},
|
||||
"max_rows": {
|
||||
"name": "最大行数"
|
||||
},
|
||||
"remove_empty_lines":{
|
||||
"name": "去除空行"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -390,6 +393,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy multiAngle":{
|
||||
"display_name": "多视角提示词",
|
||||
"inputs": {
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"1":{
|
||||
"name": "参数"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy fullLoader": {
|
||||
"display_name": "简易加载器 (完整版)",
|
||||
"inputs": {
|
||||
@@ -888,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": {
|
||||
@@ -6072,7 +6255,7 @@
|
||||
"display_name": "ckpt名称列表",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "模型名称"
|
||||
"name": "模型名称"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -6094,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": {
|
||||
@@ -6288,6 +6488,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy PassOrNone": {
|
||||
"display_name": "传递或为空",
|
||||
"inputs": {
|
||||
"any": {
|
||||
"name": "任何"
|
||||
},
|
||||
"default": {
|
||||
"name": "默认值"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "输出"
|
||||
},
|
||||
"1": {
|
||||
"name": "是否为空"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy isNone": {
|
||||
"display_name": "是否为空",
|
||||
"inputs": {
|
||||
|
||||
@@ -68,8 +68,19 @@
|
||||
"name": "样式选择器显示类型",
|
||||
"tooltip": "样式选择器显示类型,如果设置为“网格”,则显示为网格,如果设置为“列表”,则显示为列表",
|
||||
"options": {
|
||||
"Gird": "网格",
|
||||
"Grid": "网格",
|
||||
"List": "列表"
|
||||
}
|
||||
},
|
||||
"EasyUse_MultiAngle_InvertRotate": {
|
||||
"name": "启用反转旋转模式",
|
||||
"tooltip": "在多角度节点中启用反转旋转模式,使旋转方向与大多数3D软件一致"
|
||||
},
|
||||
"EasyUse_MultiAngle_HollowMode": {
|
||||
"name": "启用多角度镂空展示模式",
|
||||
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
|
||||
},
|
||||
"EasyUse_MultiAngle_AddAnglePrompt": {
|
||||
"name": "启用添加多角度提示词"
|
||||
}
|
||||
}
|
||||
@@ -405,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",
|
||||
}
|
||||
|
||||
+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}")
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
+9
-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
|
||||
@@ -180,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':
|
||||
@@ -190,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:
|
||||
|
||||
+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",
|
||||
}
|
||||
}
|
||||
|
||||
+33
-5
@@ -1012,6 +1012,7 @@ class imageChooser(PreviewImage):
|
||||
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",),
|
||||
@@ -1043,8 +1044,8 @@ class imageChooser(PreviewImage):
|
||||
id = my_unique_id[0]
|
||||
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
|
||||
|
||||
if (kwargs['images'] is None):
|
||||
return (None,)
|
||||
if (kwargs.get('images') is None):
|
||||
return (torch.zeros(1, 1, 1, 3),)
|
||||
|
||||
images_in = torch.cat(kwargs.pop('images'))
|
||||
for x in kwargs: kwargs[x] = kwargs[x][0]
|
||||
@@ -1053,7 +1054,13 @@ class imageChooser(PreviewImage):
|
||||
pnginfo = extra_pnginfo[0]
|
||||
except:
|
||||
pnginfo = None
|
||||
result = self.save_images(images=images_in, prompt=prompt, extra_pnginfo=pnginfo)
|
||||
|
||||
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:
|
||||
@@ -1211,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()
|
||||
@@ -1295,6 +1304,11 @@ class humanSegmentation:
|
||||
return mp.Image(image_format=image_format, data=numpy_image)
|
||||
|
||||
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
|
||||
@@ -1321,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)
|
||||
@@ -1354,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
|
||||
@@ -1396,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:
|
||||
|
||||
+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")
|
||||
|
||||
+100
-5
@@ -1146,6 +1146,90 @@ 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
|
||||
@@ -1175,9 +1259,12 @@ class loraSwitcher:
|
||||
|
||||
CATEGORY = "EasyUse/Loaders"
|
||||
|
||||
def stack(self, toggle, select,num_loras, lora_strength, optional_lora_stack=None, **kwargs):
|
||||
if (toggle in [False, None, "False"]) or not kwargs:
|
||||
return (None,'')
|
||||
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 = []
|
||||
|
||||
@@ -1234,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 = []
|
||||
@@ -1291,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 = []
|
||||
@@ -1533,6 +1626,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy hunyuanDiTLoader": hunyuanDiTLoader,
|
||||
"easy pixArtLoader": pixArtLoader,
|
||||
"easy mochiLoader": mochiLoader,
|
||||
"easy diffusionModelLoader": diffusionModelLoader,
|
||||
"easy loraSwitcher": loraSwitcher,
|
||||
"easy loraStack": loraStack,
|
||||
"easy controlnetStack": controlnetStack,
|
||||
@@ -1555,6 +1649,7 @@ 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",
|
||||
|
||||
+1057
-1047
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:
|
||||
|
||||
+447
-295
@@ -1,157 +1,151 @@
|
||||
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):
|
||||
@@ -160,25 +154,28 @@ class stylesPromptSelector:
|
||||
if file_name != "fooocus_styles.json":
|
||||
styles.append(file_name.split(".")[0])
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"styles": (styles, {"default": "fooocus_styles"}),
|
||||
},
|
||||
"optional": {
|
||||
"positive": ("STRING", {"forceInput": True}),
|
||||
"negative": ("STRING", {"forceInput": True}),
|
||||
"select_styles": ("EASY_PROMPT_STYLES", {}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
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,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("positive", "negative",)
|
||||
|
||||
CATEGORY = 'EasyUse/Prompt'
|
||||
FUNCTION = 'run'
|
||||
|
||||
def run(self, styles, positive='', negative='', select_styles=None, 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
|
||||
@@ -203,9 +200,11 @@ class stylesPromptSelector:
|
||||
|
||||
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)
|
||||
@@ -220,96 +219,105 @@ class stylesPromptSelector:
|
||||
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))
|
||||
|
||||
@@ -317,35 +325,40 @@ class promptLine:
|
||||
|
||||
rows = lines[start_index:end_index]
|
||||
|
||||
return (rows, rows)
|
||||
return io.NodeOutput(rows, rows)
|
||||
|
||||
import comfy.utils
|
||||
from server import PromptServer
|
||||
from ..libs.messages import MessageCancelled, Message
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
class promptAwait:
|
||||
class promptAwait(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"now": (any_type,),
|
||||
"prompt": ("STRING", {"multiline": True, "default": "", "placeholder":"Enter a prompt or use voice to enter to text"}),
|
||||
"toolbar":("EASY_PROMPT_AWAIT_BAR",),
|
||||
},
|
||||
"optional":{
|
||||
"prev": (any_type,),
|
||||
},
|
||||
"hidden": {"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
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 = (any_type, "STRING", "BOOLEAN", "INT")
|
||||
RETURN_NAMES = ("output", "prompt", "continue", "seed")
|
||||
FUNCTION = "await_select"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def await_select(self, now, prompt, toolbar, prev=None, workflow_prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
|
||||
id = my_unique_id
|
||||
@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]
|
||||
@@ -360,60 +373,64 @@ class promptAwait:
|
||||
input = now if res['select'] == 'now' or prev is None else prev
|
||||
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
|
||||
pbar.update_absolute(100)
|
||||
return result
|
||||
return io.NodeOutput(*result)
|
||||
except MessageCancelled:
|
||||
pbar.update_absolute(100)
|
||||
raise comfy.model_management.InterruptProcessingException()
|
||||
|
||||
class promptConcat:
|
||||
class promptConcat(io.ComfyNode):
|
||||
@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:
|
||||
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 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 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_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "replace_text"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
return io.NodeOutput(to_string(prompt1) + to_string(separator) + to_string(prompt2))
|
||||
|
||||
def replace_text(self, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||||
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)
|
||||
|
||||
|
||||
# 肖像大师
|
||||
@@ -421,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):
|
||||
@@ -436,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,
|
||||
@@ -609,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 = {
|
||||
@@ -625,6 +775,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy promptReplace": promptReplace,
|
||||
"easy stylesSelector": stylesPromptSelector,
|
||||
"easy portraitMaster": portraitMaster,
|
||||
"easy multiAngle": multiAngle,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -640,4 +791,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy promptReplace": "PromptReplace",
|
||||
"easy stylesSelector": "Styles Selector",
|
||||
"easy portraitMaster": "Portrait Master",
|
||||
}
|
||||
"easy multiAngle": "Multi Angle",
|
||||
}
|
||||
|
||||
+36
-26
@@ -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 *
|
||||
|
||||
@@ -118,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")
|
||||
|
||||
@@ -130,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
|
||||
@@ -156,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
|
||||
@@ -209,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:
|
||||
@@ -223,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)
|
||||
@@ -277,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,
|
||||
@@ -332,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':
|
||||
@@ -354,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
|
||||
@@ -972,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",
|
||||
|
||||
+123
-39
@@ -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:
|
||||
@@ -170,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":
|
||||
@@ -190,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":
|
||||
@@ -225,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)
|
||||
|
||||
@@ -242,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.3"
|
||||
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
|
||||
|
||||
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
+1
-1
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