Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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...9e34002f61
+55
-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)
|
||||
@@ -52,6 +52,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,29 @@
|
||||
|
||||
**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.
|
||||
|
||||
## 👨🏻🔧 Installation
|
||||
Clone the repo into the **custom_nodes** directory and install the requirements:
|
||||
@@ -47,6 +47,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 +582,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)
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.3.3"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
import yaml
|
||||
import json
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"Hotkeys": "快捷键",
|
||||
"Nodes": "节点相关",
|
||||
"NodesMap": "管理节点组",
|
||||
"StylesSelector": "样式选择器"
|
||||
"StylesSelector": "样式选择器",
|
||||
"MultiAngle": "摄影机多角度提示词"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "工具",
|
||||
|
||||
@@ -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": {
|
||||
@@ -6072,7 +6088,7 @@
|
||||
"display_name": "ckpt名称列表",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "模型名称"
|
||||
"name": "模型名称"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -6094,6 +6110,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy tableEditor": {
|
||||
"display_name": "表格编辑器",
|
||||
"description": "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串及渲染图像。",
|
||||
"inputs": {
|
||||
"table_data": {
|
||||
"name": "表格数据"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Markdown"
|
||||
},
|
||||
"1": {
|
||||
"name": "图像"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy string": {
|
||||
"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": "启用添加多角度提示词"
|
||||
}
|
||||
}
|
||||
+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}")
|
||||
|
||||
+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
|
||||
@@ -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)
|
||||
@@ -277,6 +281,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)
|
||||
|
||||
+46
-23
@@ -63,10 +63,10 @@ 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 in ["ModelMergeBlocks"]:
|
||||
if ":" in value:
|
||||
@@ -367,9 +367,19 @@ class easyXYPlot():
|
||||
# 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}")
|
||||
|
||||
@@ -430,7 +440,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
|
||||
@@ -576,28 +587,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")
|
||||
|
||||
+14
-5
@@ -1175,9 +1175,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 +1237,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 +1297,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 = []
|
||||
|
||||
+1009
-1047
File diff suppressed because it is too large
Load Diff
+18
-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:
|
||||
@@ -626,19 +630,19 @@ class pipeXYPlotAdvanced:
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
@@ -367,7 +367,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",
|
||||
}
|
||||
|
||||
+26
-18
@@ -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")
|
||||
|
||||
@@ -156,15 +164,15 @@ 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':
|
||||
@@ -173,11 +181,11 @@ class samplerFull:
|
||||
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 +217,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 +231,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 +285,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,
|
||||
@@ -354,7 +362,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 +980,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",
|
||||
}
|
||||
|
||||
+13
-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},)
|
||||
|
||||
@@ -25,7 +25,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)
|
||||
|
||||
+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,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()
|
||||
@@ -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()
|
||||
@@ -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