diff --git a/CHANGELOG.FEAT.md b/CHANGELOG.FEAT.md deleted file mode 100644 index 94d2da9..0000000 --- a/CHANGELOG.FEAT.md +++ /dev/null @@ -1,7 +0,0 @@ -- [x] feat(core): add feat that noted in issue 1 -- [x] feat(core): add paths when it does not exits -- [x] feat(core): out info when text is empty -- [x] feat(core): use prefix as file name when overwrite_mode is 'prefix_as_filename' -- [x] feat(core): use the 2th result is filename without ext -- [x] feat(core): use the 3th result is ext -- [x] feat(core): rename 'Save Text File' to io-text-save for issue 3 diff --git a/README.04-GIT.md b/README.04-GIT.md deleted file mode 100644 index c9bd668..0000000 --- a/README.04-GIT.md +++ /dev/null @@ -1,260 +0,0 @@ -## basic do - -- [x] set git default branch -```bash -git config --global init.defaultBranch main -``` - -- [x] ini git repo -```bash -[ ! -e .git ] && git init; -``` - -- [x] set user name and email -```bash -git config --global user.name "yemiancheng" ;git config --global user.email "ymc.github@gmail.com"; -``` - -- [ ] change the master bransh to "main" from "master" -```bash -git branch -M main -``` - -- [x] add github repo (in plan) -```bash -# copy token files to current project -# cp /d/code/shell/github-cli/secrets/* ./secrets - - -workspace=$(pwd); -cd /d/code/shell/github-cli;shfile=./index.sh; -repourl="ymc-github/ymc-node-suite-comfyui";repodesc="custom nodes for comfyui,like AI painting in comfyui"; - -# add repo with default desc -$shfile $repourl add # (in plan) -# put repo desc -# $shfile "$repourl" put "$repodesc" # (process) -# $shfile $repourl del - -cd "$workspace" -``` - - - -- [x] add remote repo url -```bash -# git remote add origin https://github.com/YMC-GitHub/ymc-node-suite-comfyui.git -git remote add origin git@github.com:YMC-GitHub/ymc-node-suite-comfyui.git -``` - -- [x] set gitignore and commit it -```bash -git add .gitignore;git commit -m "build(core): add gitignore"; -git add .gitignore;git commit -m "build(core): set note for go allow list"; -git add .gitignore;git commit -m "build(core): put gitignore to design project dirs"; -``` - -[fefer: some gitignore template](https://github.com/github/gitignore) - - -- [x] set edifotconfig and commit it -```bash -git add .editorconfig ; git commit -m "build(core): add editor config"; -``` - -- [x] set license and commit it -```bash -git add LICENSE ; git commit -m "build(core): add license"; -``` - -- [x] set readmd and commit it -```bash -git add README.md ; git commit -m "docs(core): add readme"; -git add README.md ; git commit -m "docs(core): put readme to readable"; -``` - -- [ ] get time with format -```bash -format="+%Y-%m-%d %H:%M:%S";time="2021-05-18 13:12:19";date -d "$time" "$format"; -``` - -- [ ] git commit files with custom date -```bash -git commit -m "chore(core): update readme in github action" --date "$(date "+%Y-%m-%d %H:%M:%S" -d "+8 hour") +0800" -``` - -- [ ] design project dirs -```bash -git add release_win/.gitignore;git commit -m "build(core): add gitignore to design project dirs"; -git add release_win/README.md;git commit -m "docs(core): output *.exe files here"; -git add release_win/*.ico;git commit -m "build(core): output icon files here"; - - -git add icons/.gitignore;git commit -m "build(core): add gitignore to design project dirs"; -git add icons/README.md;git commit -m "docs(core): put icon files here as resources"; -git add icons/*.ico;git commit -m "build(core): add resources files icon"; - -git add .vscode/.gitignore;git commit -m "build(core): add gitignore to design project dirs"; -git add .vscode/README.md;git commit -m "docs(core): put vscode editor config files of project level here"; -``` - -- [ ] add docs about how to setup this -```bash -git add README.01-SETUP.md ; git commit -m "docs(core): add readme to setup this"; -``` - -- [ ] add docs about how to code before coding -```bash -git add README.02-BEFORE-CODE.md ; git commit -m "docs(core): add readme to code this"; -``` - -- [ ] add docs about some libs i noted -```bash -git add README.03-LIB-NOTE.md ; git commit -m "docs(core): add readme to note libs"; -``` - -- [ ] add go code snippets in project level -```bash -git add .vscode/.gitignore;git commit -m "build(core): add go code snippets in project level"; -git add .vscode/go.code-snippets;git commit -m "docs(core): add go code snippets"; -``` - -- [ ] add hello world in root dirs -```bash -git add main.go;git commit -m "build(core): add hello world in root dirs"; -``` - -- [ ] add go mod and workspace -```bash -git add go.mod go.sum go.work.sum;git commit -m "build(core): add mod and workspace"; -``` - -- [ ] add mod chapter01 -```bash -git add src/chapter01/higo;git commit -m "build(core): add mod chapter01"; -``` - -- [x] push to github repo (the first) -```bash -git push -u origin main -``` - -- [ ] replace git protocol with https when failed -```bash -# set -git config --global url."https://".insteadof git:// -# unset -# git config --global --unset url."https://".insteadof -``` - -- [x] rename github repo remote name from "origin" to "github" -```bash -git remote rename origin github -``` - -- [ ] remove github repo remote -```bash -git remote remove github -``` - -- [ ] set remote repo url from https to git -```bash -git remote set-url origin git@github.com:YMC-GitHub/ymc-node-suite-comfyui.git -# git remote set-url github git@github.com:YMC-GitHub/ymc-node-suite-comfyui.git -``` - - -- [x] speedup github for `git clone` -```bash -# set -git config --global url."https://ghproxy.com/https://github.com".insteadOf "https://github.com" -# unset -# git config --global --unset url."https://ghproxy.com/https://github.com".insteadOf -# get -# git config --global url."https://ghproxy.com/https://github.com".insteadOf -``` - -- [x] unset speedup github for `git clone` when pushing -```bash -# unset -git config --global --unset url."https://ghproxy.com/https://github.com".insteadOf -``` - - -- [x] mgnt git credential -```powershell -# git config --global credential.helper manager -# git config --global credential.helper cache -# git config --global --unset credential.helper -# https://zhuanlan.zhihu.com/p/157751660 -``` - -- [x] add docs about using git for this repo i noted -```bash -git add README.04-GIT.md ; git commit -m "docs(core): add readme to note git for this repo"; -git add README.04-GIT.md ; git commit -m "docs(core): put readme to note git for this repo"; - -``` - - -- [ ] add docs about using ssh in github i noted -```bash -git add README.05-SSH.md ; git commit -m "docs(core): add readme to note ssh"; -``` - -- [x] set pull action for this repo -```bash -# zero:knowledge:s:pull-action -# you must know . -# git config pull.rebase false # merge -git config pull.rebase true # rebase -# git config pull.ff only # fast-forward only -# zero:knowledge:e:pull-action -``` - -- [x] keep workspace clean before pulling -```bash -# git stash -``` - -- [x] pull remote github repo before pushing again -```bash -git pull origin main -# git pull github main -``` - -- [x] push remote github repo (no the first time) -```bash -git push origin main -``` - - -- [x] add requirements -```bash -git add requirements.txt ; git commit -m "build(core): add requirements nodes"; -``` - -- [x] add current custom nodes -```bash -git add __init__.py YMC_Node_Suite.py; git commit -m "feat(core): add custom nodes"; -``` - -- [x] add changelog for this repo -```bash -git add README.md CHANGELOG*; git commit -m "docs(core): add changelog info"; -``` - -- [x] add tag -```bash -git log --oneline -git tag v1.0.0 -m "version 1.0.0" 4ce981e -git tag v1.1.0 -m "version 1.1.0" 28b0da0 -git tag v2.0.0 -m "version 2.0.0" 44d9b66 -git push origin main --tags -``` - -- [x] get feat or build note from code - -build(core): get feat in bash `cat YMC_Node_Suite.py | grep "^ *# feat.*" | sed "s/ *#/- [x]/g" > CHANGELOG.FEAT.md` - -build(core): get build in bash `cat YMC_Node_Suite.py | grep "^ *# build.*" | sed "s/ *#/- [x]/g" > CHANGELOG.BUILD.md` diff --git a/README.md b/README.md index 6b5c0f7..ad77221 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,31 @@ ymc 's nodes for comfyui +## Usage + +- you can find it in search box : double click + typing keywords( eg. `ymc suite` ,`text` ,`cutoff`,`region`) +- you can find it in right mouse menu : `ymc suite` +- ~~you can find it in right mouse menu : `ymc/utils` (as alias)~~ + +## Based-on + +- pypi package [yors_comfyui_node_setup](https://pypi.org/project/yors_comfyui_node_setup/) - setup comfyui custom nodes easily + +- pypi package [yors_pano_ansi_color](https://pypi.org/project/yors_pano_ansi_color/) - info msg in console with color in your comfyui custom nodes easily + +- pypi package [yors_pano_zero_field](https://pypi.org/project/yors_pano_zero_field/) - set nodes input field to be HQ in your comfyui custom nodes easily + +- pypi package [yors_pano_list_util](https://pypi.org/project/yors_pano_list_util/) - enjoy python list for python starter in your comfyui custom nodes easily. + + +## Published to Comfy registry + +- get more details in [publish_to_comfy.yml](.github/workflows/publish_to_comfy.yml) + +- [docs for publishing to comfy registey](https://docs.comfy.org/registry/overview) + +- installed with comfy-cli ? `comfy node registry-install ymc-node-suite-comfyui` + ## Author name|email|desciption @@ -15,6 +40,10 @@ MIT ## Changelog +### 3.0.0 + +- [x] refator(core): rename project dirs and all nodes name for hight qulity nodes. + ### 2.0.0 - [x] feat(core): rename 'Save Text File' to io-text-save for issue 3 diff --git a/__init__.py b/__init__.py index 418dba1..3bd2177 100644 --- a/__init__.py +++ b/__init__.py @@ -7,25 +7,65 @@ # # export comfyui node vars # __all__,NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = entry(__name__,__file__) -# v2 -from yors_comfyui_node_setup import node_install_requirements,entry_pre_import,entry_import,get_all_classs_in_sys,register_node_list_local +# v4 +import os +from yors_comfyui_node_setup import custom_nodes_get_path,custom_nodes_add_to_sys,import_custom_node_module,node_install_requirements,get_module_name_contains_x_in_sys,get_node_class_in_sys_modules,register_node_list_local +from yors_pano_ansi_color import info_status,info_step,msg_padd,log_msg +import importlib -# install requirements -node_install_requirements(__file__) +info_step("add comfui/custom_nodes to sys.path") +custom_nodes_path = [custom_nodes_get_path(__file__,'../../custom_nodes')] +custom_nodes_add_to_sys(custom_nodes_path) -# gen __all__ -__all__ = entry_pre_import(__name__,__file__) +info_step("import modules in sub diretory modules") +import_custom_node_module(os.path.dirname(__file__),__name__,"modules") -# import moudle with __all__ -entry_import(__name__,__all__) +# info_step(f"install deps (not installed) in requirements.txt if file exits") +# node_install_requirements(__file__) + +# info_step(f"read name in sys.modules if name including name") +# - custom_node_module_class_name_list = read_module_class_name(__name__) +# custom_node_module_class_name_list = get_module_name_contains_x_in_sys(__name__) + +info_step(f"read valid node class if key in sys.modules including this module name") +# - NodeClassList,NodeClassNameList = read_module_valid_node_class(__name__) +NodeClassList,NodeClassNameList = get_node_class_in_sys_modules(__name__) + +info_step(f"make node_class_mappings and node_display_name_mappings {__name__}") +NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list_local(NodeClassList) + +info_step(f"print node name of {__name__}") +NODE_MENU_NAMES.sort() +# print("\n".join(NODE_MENU_NAMES)) -# get class after importing moudle with __all__ -this_module_all_classes = get_all_classs_in_sys(__name__) # register node with default category -# NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list(this_module_all_classes,False) +# - NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list(this_module_all_classes,False) +# NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list_local(this_module_all_classes,False) # addtional register node with custom category -NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list_local(this_module_all_classes,True,"ymc/suite") +# NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = register_node_list_local(this_module_all_classes,True,"ymc/suite") # print(NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES) -print("\n".join(NODE_MENU_NAMES)) \ No newline at end of file +# print("\n".join(NODE_MENU_NAMES)) + +# info_step(f"define __all__ (not web ext) {__name__}") +# __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] + +info_step(f"define __all__ (support web ext) {__name__}") +WEB_DIRECTORY = "./web" +# docs(core): export NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,WEB_DIRECTORY +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] + +# get version from pyproject.toml +# ... +version="3.0.0" + +log_msg(msg_padd("=",60,"=")) +log_msg(msg_padd("welocme to ymc_node_suite_comfyui",60,"=")) +log_msg(f'version: {version}') +log_msg(f'node counts:{len(NODE_MENU_NAMES)}') +log_msg(f'node menu names:') +for node_name in NODE_MENU_NAMES: + # log_msg(f'node name:{node_name}') + info_status(f'{node_name}',0) +log_msg(msg_padd("=",60,"=")) \ No newline at end of file diff --git a/conf.py b/conf.py deleted file mode 100644 index 15897b3..0000000 --- a/conf.py +++ /dev/null @@ -1,2 +0,0 @@ -CURRENT_CATEGORY="ymc/suite" -CURRENT_FUNCTION="exec" \ No newline at end of file diff --git a/docs/ask.for.ai.node-01-the-old-nodes.md b/docs/ask.for.ai.node-01-the-old-nodes.md new file mode 100644 index 0000000..7542ede --- /dev/null +++ b/docs/ask.for.ai.node-01-the-old-nodes.md @@ -0,0 +1,141 @@ +以下是代码中定义的 ComfyUI 节点(Nodes)及其功能的解读: + +--- + +### **1. IO 相关节点** +- **`io-text-save` (save text file)** + - **功能**:将文本保存到文件中,支持路径和文件名生成规则(如时间戳、自定义前缀等)。 + - **输入**:文本内容、路径、文件名前缀、扩展名等。 + - **输出**:保存的文本内容、文件名(不含扩展名)、扩展名。 + - **特点**:支持历史记录管理,路径自动创建。 + +- **`io-util-file-list-get`** + - **功能**:获取指定目录下匹配特定模式的文件列表(如 `*.txt`)。 + - **输入**:目录路径、文件匹配模式(如 `*.txt`)。 + - **输出**:文件列表(每行一个文件名或绝对路径)。 + +- **`io-util-file-list-get-text`** + - **功能**:读取指定目录下所有匹配文件的文本内容并合并。 + - **输入**:目录路径、文件匹配模式。 + - **输出**:合并后的多行文本。 + +--- + +### **2. 文本处理节点** +- **`text-util-join-text`** + - **功能**:将多个输入文本用分隔符连接成一个字符串。 + - **输入**:分隔符和最多 7 个文本字段。 + - **输出**:合并后的字符串。 + +- **`text-util-search-text`** + - **功能**:在文本中搜索并替换内容。 + - **输入**:原始文本、搜索字符串、替换字符串。 + - **输出**:替换后的文本。 + +- **`text-util-random-text`** + - **功能**:从分隔符(如 `|`)分隔的文本中随机选择一项。 + - **输入**:文本、随机种子、分隔符。 + - **输出**:随机选择的文本和种子。 + +- **`text-util-loop-text`** + - **功能**:按顺序循环选择分隔符分隔的文本项(基于种子)。 + - **输入**:文本、种子、分隔符。 + - **输出**:选择的文本、种子、当前索引。 + +- **`text-util-switch-text`** + - **功能**:根据布尔值切换输出两个文本之一。 + - **输入**:两个文本、布尔值(0 或 1)。 + - **输出**:选择的文本。 + +--- + +### **3. 提示词(Prompt)处理节点** +- **`text-util-prompt-join`** + - **功能**:合并多个提示词,自动去重和标准化格式。 + - **输入**:最多 7 个提示词字段。 + - **输出**:合并后的提示词。 + +- **`text-util-prompt-del`** + - **功能**:从提示词中删除指定内容。 + - **输入**:原始提示词、待删除内容。 + - **输出**:清理后的提示词。 + +- **`text-util-prompt-std`** + - **功能**:标准化提示词(去除多余空格、换行等)。 + - **输入**:原始提示词。 + - **输出**:标准化后的提示词。 + +- **`text-util-prompt-shuffle`** + - **功能**:随机打乱提示词顺序(可选保留前 N 项)。 + - **输入**:提示词、随机种子、保留项数。 + - **输出**:打乱后的提示词。 + +--- + +### **4. 区域(Region)工具节点** +- **`region-util-get-by-center-and-size`** + - **功能**:根据中心点坐标和宽高计算裁剪区域的边界(左、上、右、下)。 + - **输入**:中心点坐标 `(cx, cy)`、宽 `w`、高 `h`。 + - **输出**:边界坐标和中心点信息。 + +- **`region-util-get-by-lt`** + - **功能**:根据左上角坐标和宽高计算裁剪区域的边界。 + - **输入**:左上角坐标 `(left, top)`、宽 `w`、高 `h`。 + - **输出**:边界坐标和中心点信息。 + +- **`region-util-get-pad-out-location-by-size`** + - **功能**:根据宽高计算填充(Padding)后的边界坐标(用于图像扩展)。 + - **输入**:宽 `w`、高 `h`。 + - **输出**:填充后的边界坐标。 + +--- + +### **5. 画布(Canvas)工具节点** +- **`canvas-util-cal-size`** + - **功能**:根据宽高比例(如 `1:1`、`9:16`)调整画布尺寸。 + - **输入**:原始宽高、比例、锁定模式(固定宽或高)。 + - **输出**:调整后的宽高。 + +--- + +### **6. 图像处理节点** +- **`img-util-get-image-size`** + - **功能**:获取图像的宽度和高度。 + - **输入**:图像张量。 + - **输出**:宽、高。 + +- **`img-util-switch-input-image`** + - **功能**:根据布尔值切换输出两张图像之一。 + - **输入**:两张图像、布尔值。 + - **输出**:选择的图像。 + +--- + +### **7. 条件(Conditioning)工具节点** +- **`conditioning-util-input-switch`** + - **功能**:根据布尔值切换输出两个条件(Conditioning)之一。 + - **输入**:两个条件、布尔值。 + - **输出**:选择的条件。 + +--- + +### **8. 其他工具节点** +- **`number-util-random-num`** + - **功能**:生成随机数(整数、浮点数或布尔值)。 + - **输入**:范围、随机种子。 + - **输出**:随机数。 + +- **`xyz-util-txt-to-int`** + - **功能**:将文本描述的数值范围(如 `1-10[5]`)解析为整数或浮点数列表。 + - **输入**:文本范围、键名(如 `Steps`)。 + - **输出**:解析后的数值列表和网格标注文本。 + +- **`pipe-util-to-basic-pipe`** + - **功能**:将模型、CLIP、VAE、条件等组合成基础管道(Basic Pipe)。 + - **输入**:模型、CLIP、VAE、正负条件。 + - **输出**:基础管道及各组件。 + +--- + +### **总结** +这些节点覆盖了文本处理、图像操作、区域计算、条件控制、随机生成等常见需求,适合在 ComfyUI 工作流中实现复杂的逻辑组合。每个节点均支持 ComfyUI 的输入输出规范,可直接与其他节点连接使用。 \ No newline at end of file diff --git a/CHANGELOG.BUILD.md b/docs/ask.for.ai.node-02-refactor-nodes.md similarity index 100% rename from CHANGELOG.BUILD.md rename to docs/ask.for.ai.node-02-refactor-nodes.md diff --git a/docs/ask.for.ai.py.faqs.01.md b/docs/ask.for.ai.py.faqs.01.md new file mode 100644 index 0000000..7b5d365 --- /dev/null +++ b/docs/ask.for.ai.py.faqs.01.md @@ -0,0 +1,33 @@ +## attempted relative import with no known parent package + +### 终端输出内容 +```terminal +Traceback (most recent call last): + File "D:\code\python\panz\comfyui-nodes\ymc-node-suite-comfyui\node_demo.py", line 1, in + from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION +ImportError: attempted relative import with no known parent package +``` + +### 错误解释 +- `Traceback (most recent call last)`:这是 Python 错误堆栈跟踪的起始行,表明 Python 开始回溯错误发生的位置。 +- `File "D:\code\python\panz\comfyui-nodes\ymc-node-suite-comfyui\node_demo.py", line 1, in `:指出错误发生在 `node_demo.py` 文件的第 1 行。 +- `from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION`:这是出错的代码行,尝试使用相对导入从 `conf` 模块导入 `CURRENT_CATEGORY` 和 `CURRENT_FUNCTION`。 +- `ImportError: attempted relative import with no known parent package`:错误类型为 `ImportError`,提示使用相对导入时没有找到已知的父包。相对导入需要在包的上下文中使用,当直接运行脚本时,Python 不会将其视为包的一部分,所以会报错。 + +### 修复建议 +#### 方法一:将项目作为包运行 +如果你想使用相对导入,可以将项目作为包运行。在项目根目录下使用 `-m` 选项: +```powershell +python -m ymc-node-suite-comfyui.node_demo +``` + +#### 方法二:使用绝对导入 +修改 `node_demo.py` 文件,将相对导入改为绝对导入: +```python:d:\code\python\panz\comfyui-nodes\ymc-node-suite-comfyui\node_demo.py +# 修改前 +# from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION + +# 修改后 +from ymc_node_suite_comfyui.conf import CURRENT_CATEGORY,CURRENT_FUNCTION +``` +请根据实际的包名调整绝对导入路径。 \ No newline at end of file diff --git a/node_demo.py b/docs/node_demo.py.md similarity index 89% rename from node_demo.py rename to docs/node_demo.py.md index 8816ef7..4c26d69 100644 --- a/node_demo.py +++ b/docs/node_demo.py.md @@ -1,4 +1,6 @@ -from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION +CURRENT_CATEGORY="ymc/suite" +CURRENT_FUNCTION="exec" + class TextInput: @classmethod def INPUT_TYPES(s): diff --git a/docs/opv.this.repo.md b/docs/opv.this.repo.md new file mode 100644 index 0000000..bee9a01 --- /dev/null +++ b/docs/opv.this.repo.md @@ -0,0 +1,113 @@ +```powershell +# 1 +python __init__.py + +python nodes.py + + +# +python -m main; # run module main done +python main.py; # run module main done + +# python node_demo.py + +python -m ymc_node_suite_comfyui.node_demo + +# yours touch docs/ask.for.ai.py.faqs.01.md +# yours touch docs/ask.for.ai.py.faqs.02.md + +yours touch docs/ask.for.ai.node-01-the-old-nodes.md +yours touch docs/ask.for.ai.node-02-refactor-nodes.md + +yours touch nodes/text/__init__.py +yours touch nodes/text/conf.py +yours touch nodes/text/join.py +yours touch nodes/text/search.py +yours touch nodes/text/random.py +yours touch nodes/text/loop.py +yours touch nodes/text/switch.py +yours touch nodes/text/prompts.py #join,std,del,dup,shuffle +yours touch nodes/text/input.py + +yours touch nodes/area/__init__.py +yours touch nodes/area/conf.py +yours touch nodes/area/from_center_and_size.py +yours touch nodes/area/from_lefttop_and_size.py +yours touch nodes/area/padd.py + +# canvas cal size + +yours touch nodes/image/__init__.py +yours touch nodes/image/conf.py +yours touch nodes/image/get_image_size.py +yours touch nodes/image/switch_image.py + + +yours touch nodes/utils/__init__.py +yours touch nodes/utils/conf.py +yours touch nodes/utils/random_num.py +yours touch nodes/utils/text_to_int.py +yours touch nodes/utils/basic_pipe.py + +yours touch nodes/file/__init__.py +yours touch nodes/file/conf.py +yours touch nodes/file/get_file_list.py +yours touch nodes/file/get_text_of_file_list.py + + +yours touch nodes/cutoff/__init__.py +sh -c "cp nodes/text/__init__.py nodes/cutoff/__init__.py" +sh -c "cp nodes/text/conf.py nodes/cutoff/conf.py" + +yours touch nodes/cutoff/text_from.py +yours touch nodes/cutoff/text_list_color.py + +python -c "import sys;print(sys.path)" + +# sh -c "rm -rf nodes/text/{input,join,loop,random,search,switch}.py" +# sh -c "rm -rf nodes/cutoff" + +# sh -c "rm -rf nodes/cutoff" + +``` +## fags +- ImportError: attempted relative import with no known parent package + + +## depoy to comfyui (local + debug) +```bash +# del local nodes in comfyui custom_nodes +sh -c "rm -rf /o//app/ComfyUI/custom_nodes/ymc-node-suite-comfyui" +# copy local nodes to comfyui custom_nodes +sh -c "cp -r /d/code/python/panz/comfyui-nodes/ymc-node-suite-comfyui /o/app/ComfyUI/custom_nodes/" + + +# conda activate sdwa;cd O:\app\ComfyUI;python main.py; +``` + +## opv - docs +```bash +git add README.md ; git commit -m "docs(core): put usage" + + +git add . ; git commit -m "refactor(core): reset dirs" +git add . ; git commit -m "refactor(core): rename nodes map" +git add . ; git commit -m "refactor(core): rename nodes display name" + +git add . ; git commit -m "refactor(core): put dirs and category" + +# get git msg log with oneline and short hash and msg head and time +git log --pretty=format:"%h %ad %s" --date=short -n 5 + +git log --oneline -n 5 + +git rm CHANGELOG.*.md +git rm conf.py +git mv node_demo.py docs/node_demo.py.md +git rm README.*.md +``` + +## opv - shotscreen +```bash + +``` diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..c934dbe --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,5 @@ +from yors_comfyui_node_setup import entry,node_install_requirements +node_install_requirements(__file__) +__all__,NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = entry(__name__,__file__,False) + +# from . import * \ No newline at end of file diff --git a/modules/text/__init__.py b/modules/text/__init__.py new file mode 100644 index 0000000..c934dbe --- /dev/null +++ b/modules/text/__init__.py @@ -0,0 +1,5 @@ +from yors_comfyui_node_setup import entry,node_install_requirements +node_install_requirements(__file__) +__all__,NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,NODE_MENU_NAMES = entry(__name__,__file__,False) + +# from . import * \ No newline at end of file diff --git a/modules/text/conf.py b/modules/text/conf.py new file mode 100644 index 0000000..0f1bd6f --- /dev/null +++ b/modules/text/conf.py @@ -0,0 +1,2 @@ +CURRENT_CATEGORY="ymc suite/text" +CURRENT_FUNCTION="exec" \ No newline at end of file diff --git a/modules/text/node_cutoff_region.py b/modules/text/node_cutoff_region.py new file mode 100644 index 0000000..79db611 --- /dev/null +++ b/modules/text/node_cutoff_region.py @@ -0,0 +1,101 @@ +# import random +from yors_pano_list_util import ListDelExclude,ListFillOne,ListToTupe,ListShuffle + +# from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION + +CURRENT_CATEGORY="ymc suite/text" +CURRENT_FUNCTION="exec" + +def StrSetUndefinedAsEmpty(text,deft:str=""): + """ + target_text=StrSetUndefinedAsEmpty(target_text) + + when text is "undefined", return "" + """ + if text == "undefined": + return deft + return text + +# code(core): about shift + tap and tap in vscode + +def CutoffRegionTextBurn(main_text, target_text, weight): + """ + CutoffRegionTextBurn(main_text,target_text,weight) + + burn region text + """ + # Converted inputs are sent as the string of 'undefined' if not connected + target_text = StrSetUndefinedAsEmpty(target_text) + main_text = StrSetUndefinedAsEmpty(main_text) + + region_text = "" + if weight is None or weight == "" or weight == "1.0": + region_text = (" ").join(filter(None, [target_text, main_text])) + else: + # region_text = (" ").join(filter(None, ["(",target_text, main_text,":",weight,")"])) + region_text = (" ").join(filter(None, [target_text, main_text])) + region_text = ("").join(filter(None, ["(", region_text, ":", weight, ")"])) + # return {"ui": {"text": (region_text,)}, "result": (region_text,)} + return region_text, target_text, weight + + +# feat(core): NodeCutoffRegionTextBurn - cutoff - region text burn +class NodeCutoffRegionTextBurn: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "main_text": ("STRING", {"default": "dress", "multiline": True}), + "target_text":("STRING", {"default": "white", "multiline": False}), + "weight": ("STRING", {"default": "1.0", "multiline": False}), + } + } + + RETURN_TYPES = ("STRING","STRING","STRING",) + RETURN_NAMES = ('region_text','target_text','prompt_weight') + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "cutoff - region text burn" + + OUTPUT_NODE = True + + def exec(self, main_text,target_text,weight): + return CutoffRegionTextBurn(main_text,target_text,weight) + + +TEXT_BASE_COLORS=['red','orange','yellow','green','cyan','blue','purple','black','white','pink','custom'] +TEXT_BASE_COLORS_COUNT=len(TEXT_BASE_COLORS) + + +def CutoffColorTextPreset(custom:str="sliver"): + """ + CutoffColorTextList(custom) + + color text preset will update the last one will custom + """ + # return red,orange,yellow,green,cyan,blue,purple,black,white,pink,custom + TEXT_BASE_COLORS_VALUE = TEXT_BASE_COLORS.copy() + TEXT_BASE_COLORS_VALUE[TEXT_BASE_COLORS_COUNT-1]=custom + return ListToTupe(TEXT_BASE_COLORS_VALUE) + +# feat(core): NodeCutoffRegionColorTextPreset - cutoff - color preset of region text +class NodeCutoffRegionColorTextPreset: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "custom": ("STRING", {"default": "red", "multiline": False}) + } + } + + # RETURN_TYPES = ("STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING",) + RETURN_TYPES = ListToTupe(ListFillOne('STRING',TEXT_BASE_COLORS_COUNT)) + RETURN_NAMES = ListToTupe(TEXT_BASE_COLORS) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "cutoff - color preset of region text" + OUTPUT_NODE = True + def exec(self, custom): + # Converted inputs are sent as the string of 'undefined' if not connected + custom=StrSetUndefinedAsEmpty(custom,"sliver") + return CutoffColorTextPreset(custom) \ No newline at end of file diff --git a/modules/text/node_plain.py b/modules/text/node_plain.py new file mode 100644 index 0000000..2a1841a --- /dev/null +++ b/modules/text/node_plain.py @@ -0,0 +1,503 @@ +import random +from yors_pano_list_util import ListDelExclude,ListFillOne,ListToTupe,ListShuffle + +from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION + +def TextListRandom(text:str, seed,char='|'): + """ + item,seed = TextListRandom(text,seed,'|') + + TextListRandom(text,seed,',') + """ + iteml = text.split(char) + random.seed(seed) + choice = random.choice(iteml) + return ListToTupe([choice,seed]) + + +def range_safe(i:int,s:int,e:int): + ''' + ensure i is in [s,e] + ''' + res=i + if res e: + res=e + return res + +def range_safe_loop(i:int,s:int,e:int): + ''' + ensure i is in [s,e] for loop + ''' + res=i + if res =e: + res = res % e + res = s + res + return res + +def TextListLoop(text, seed,char='|'): + iteml = text.split(char) + # get loop index with de/cr seed + length=len(iteml) + index=range_safe_loop(seed-1,0,length) + choice = iteml[index] + # print(maxindex,seed,index,choice) + return ListToTupe([choice,seed,str(index+1),length]) + + + +def TextReplace(text, find, replace): + """ + text replace find with re.sub + """ + import re + result = re.sub(find, replace, text) + return result + + + +TEXT_LIST_RETURN_NAMES=['a','b','c','d','e','f','g'] +TEXT_LIST_COUNT=len(TEXT_LIST_RETURN_NAMES) +TEXT_LIST_ADV_RETURN_NAMES=TEXT_LIST_RETURN_NAMES.copy() +TEXT_LIST_ADV_RETURN_NAMES.insert(0,"prefix") +# feat(core): NodePlainTextList - input text preset +class NodePlainTextList: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "a": ("STRING",{"forceInput":False}), + "b": ("STRING",{"forceInput":False}), + "c": ("STRING",{"forceInput":False}), + "d": ("STRING",{"forceInput":False}), + "e": ("STRING",{"forceInput":False}), + "f": ("STRING",{"forceInput":False}), + "g": ("STRING",{"forceInput":False}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('STRING',TEXT_LIST_COUNT)) + RETURN_NAMES = ListToTupe(TEXT_LIST_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - input text preset" + OUTPUT_NODE = True + + def exec(self, a,b,c,d,e,f,g): + return ListToTupe([a,b,c,d,e,f,g]) + +# feat(core): NodePlainTextListAdv - input text preset - adv +class NodePlainTextListAdv: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prefix": ("STRING",{"forceInput":False}), + "a": ("STRING",{"forceInput":False}), + "b": ("STRING",{"forceInput":False}), + "c": ("STRING",{"forceInput":False}), + "d": ("STRING",{"forceInput":False}), + "e": ("STRING",{"forceInput":False}), + "f": ("STRING",{"forceInput":False}), + "g": ("STRING",{"forceInput":False}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('STRING',TEXT_LIST_COUNT+1)) + RETURN_NAMES = ListToTupe(TEXT_LIST_ADV_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - input text preset - adv" + OUTPUT_NODE = True + + def exec(self,prefix, a,b,c,d,e,f,g): + joined = list(map(lambda x: prefix+x,[a,b,c,d,e,f,g])) + import datetime + # import time + joined.insert(0,prefix) + joined = list(map(lambda x: datetime.datetime.now().strftime(x),joined)) # replace time exp like %Y %M %D + return ListToTupe(joined) + +# feat(core): NodePlainTextListJoinTextAdv - join text to text preset - adv +class NodePlainTextListJoinTextAdv: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + + }, + "optional": { + "text": ("STRING",{"forceInput": True}), + "a": ("STRING",{"forceInput":True}), + "b": ("STRING",{"forceInput":True}), + "c": ("STRING",{"forceInput":True}), + "d": ("STRING",{"forceInput":True}), + "e": ("STRING",{"forceInput":True}), + "f": ("STRING",{"forceInput":True}), + "g": ("STRING",{"forceInput":True}), + "action": (["none","text-as-head","text-as-tail"],), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('STRING',TEXT_LIST_COUNT+1)) + RETURN_NAMES = ListToTupe(['text','a','b','c','d','e','f','g']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - join text to text preset - adv" + OUTPUT_NODE = True + + def exec(self,text, a,b,c,d,e,f,g,action="none"): + joined=[] + if "none" in action: + joined = [a,b,c,d,e,f,g] + elif "text-as-head" in action: + joined = list(map(lambda x: text+x,[a,b,c,d,e,f,g])) + elif "text-as-tail" in action: + joined = list(map(lambda x: x+text,[a,b,c,d,e,f,g])) + import datetime + # import time + joined.insert(0,text) + joined = list(map(lambda x: datetime.datetime.now().strftime(x),joined)) # replace time exp like %Y %M %D + return ListToTupe(joined) + +# feat(core): NodePlainTextJoin - join text +class NodePlainTextJoin: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "splitchar": ("STRING", {"default": "_", "multiline": False}), + "a": ("STRING",{"forceInput":False}), + "b": ("STRING",{"forceInput":False}), + "c": ("STRING",{"forceInput":False}), + "d": ("STRING",{"forceInput":False}), + "e": ("STRING",{"forceInput":False}), + "f": ("STRING",{"forceInput":False}), + "g": ("STRING",{"forceInput":False}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('STRING',1)) + RETURN_NAMES = ListToTupe(['text']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - join text" + + + OUTPUT_NODE = True + + def exec(self, splitchar,a,b,c,d,e,f,g): + # args to list + ls=[a,b,c,d,e,f,g] + # ignore none + ls = list(filter(None, ls)) + # join with splitChar + joinedtxt=splitchar.join(ls) + return ListToTupe([joinedtxt]) + +# feat(core): NodePlainTextRandom - random text in list +class NodePlainTextRandom: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": '', "multiline": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "char": ("STRING", {"default": '|', "multiline": False}), + } + } + + RETURN_TYPES = ("STRING",'INT',) + RETURN_NAMES = ListToTupe(['text','seed']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - random text in list" + def exec(self, text, seed,char='|'): + return TextListRandom(text,seed,char) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + +# feat(core): NodePlainTextLoop - get text in list (loop) +class NodePlainTextLoop: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": '', "multiline": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "char": ("STRING", {"default": '|', "multiline": False}), + } + } + + RETURN_TYPES = ("STRING",'INT','STRING','INT') + RETURN_NAMES = ListToTupe(['text','seed','index','count']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - get text in list (loop)" + + def exec(self, text, seed,char='|'): + return TextListLoop(text, seed,char) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + +# feat(core): NodePlainTextReplace - replace text +class NodePlainTextReplace: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": '', "multiline": False}), + "find": ("STRING", {"default": '', "multiline": False}), + "replace": ("STRING", {"default": '', "multiline": False}), + "action": (["yes",'no'],), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ListToTupe(['text']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - replace" + + + def exec(self, text, find, replace,action='yes'): + # feat(core): do nothing when action no + if 'no' in action: + return ListToTupe([text]) + # feat(core): replace text + return ListToTupe([TextReplace(text, find, replace)]) + + +def NumStrToNumInt(str:str="1",roundit:bool=True): + """ + NumStrToNumInt(0.5) + NumStrToNumInt(float(str)) + """ + if roundit == True: + return int(round(str)) + return int(str) + +def NumStrIsRoundToA(a='1', isRoundTo='1'): + """ + NumStrIsRoundToA("0.8","1") + NumStrIsRoundToA("0.4","1") + """ + if NumStrToNumInt(a) == NumStrToNumInt(isRoundTo): + return True + else: + return False + + +def BoolStr2BoolNum(str:str="true"): + """ + BoolStr2BoolNum("TRUE") + BoolStr2BoolNum("false") + """ + # true,TRUE + if str=="true": + return 1 + else: + return 0 + +# feat(core): NodeNumStrToNum - text to number +class NodeNumStrToNum: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": '', "multiline": False}), + } + } + + # RETURN_TYPES = ('NUMBER',) + RETURN_TYPES = ("NUMBER", "FLOAT", "INT") + RETURN_NAMES = ListToTupe(["number", "float", "roundInt"]) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - to number" + + def exec(self, text=''): + strInt = int(text) + strFlo = float(text) + strRoundInt = int(round(strFlo)) + return ListToTupe([strInt,strFlo,strRoundInt]) + +# feat(core): NodeNumStrToBool - text to bool +class NodeNumStrToBool: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": '1', "multiline": False}), + "isRoundTo":("STRING", {"default": '1', "multiline": False}), + } + } + + RETURN_TYPES = ('STRING',) + RETURN_NAMES = ListToTupe(["boolean"]) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - is round to x" + + def exec(self, a='1', isRoundTo='1'): + return ListToTupe([NumStrIsRoundToA(a,isRoundTo)]) + +# feat(core): NodePlainTextSwitchWithBool - text switch with bool +class NodePlainTextSwitchWithBool: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "a": ("STRING", {"default": '', "multiline": False}), + "b": ("STRING", {"default": '', "multiline": False}), + "boolean": ("BOOL",), + } + } + + RETURN_TYPES = ('STRING',) + RETURN_NAMES = ListToTupe(["text"]) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - switch a and b" + + def exec(self, a='', b='', boolean:bool=True): + if boolean==True: + return (a, ) + else: + return (b, ) + +# feat(core): NodePlainTextSwitchWithText - text switch with text +class NodePlainTextSwitchWithText: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "a": ("STRING", {"default": '', "multiline": False}), + "b": ("STRING", {"default": '', "multiline": False}), + "text": ("STRING", {"default": '1', "multiline": False}), + "isRoundTo":("STRING", {"default": '1', "multiline": False}), + } + } + + RETURN_TYPES = ('STRING',) + RETURN_NAMES = ListToTupe(["text"]) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - switch a and b (isRoundTo)" + def exec(self, a='', b='', text='1',isRoundTo='1'): + if NumStrIsRoundToA(text,isRoundTo): + return (a, ) + else: + return (b, ) + +# class NodeTextShow: +# @classmethod +# def INPUT_TYPES(s): +# return {"required": { +# "text": ("STRING", {"forceInput": True}), +# }} + +# # INPUT_IS_LIST = True +# FUNCTION = CURRENT_FUNCTION +# CATEGORY = CURRENT_CATEGORY +# NODE_DESC = "plain - show text" +# OUTPUT_NODE = True +# RETURN_TYPES = ('STRING',) +# # OUTPUT_IS_LIST = (True,) + +# def exec(self, text): +# return {"ui": {"text": text}, "result": (text,)} +# refer: pythongosssss /ComfyUI-Custom-Scripts/py/show_text.py + +# feat(core): NodeTextShow - show text +class NodeTextShow: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING", {"forceInput": True}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - show text" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + def exec(self, text, unique_id = None, extra_pnginfo=None): + if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]: + workflow = extra_pnginfo[0]["workflow"] + node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None) + if node: + node["widgets_values"] = [text] + return {"ui": {"text": text}, "result": (text,)} + +# feat(core): NodeTextCalcSize - cacluate width and height +class NodeTextCalcSize: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "w": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}), + "h": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}), + "lockoff": (["false", "w","h"],), + # "rate": (["1:1", "2:3","9:16"],), + "rate": ("STRING", {"default": "1:1", "multiline": False}), + "reverse": (["false", "true"],), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('INT',2)) + RETURN_NAMES = ListToTupe(['w','h']) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "plain - cacluate width and height" + OUTPUT_NODE = True + + def exec(self,w=512, h=512,lockoff='false',rate='1:1',reverse='false'): + rw=w + rh=h + # get h or w with lockoff + if lockoff != "false": + ratel=list(map(int,rate.split(":"))) + raten=(ratel[0]/ratel[1]) + if lockoff == "w": + rh=int(w/raten) + if lockoff == "h": + rw=int(h*raten) + + floatl=[rw,rh] + # reverse result of w h + if reverse == "true": + floatl=[rh,rw] + intl=list(map(int,floatl)) + return ListToTupe(intl) \ No newline at end of file diff --git a/modules/text/node_prompt.py b/modules/text/node_prompt.py new file mode 100644 index 0000000..a70a194 --- /dev/null +++ b/modules/text/node_prompt.py @@ -0,0 +1,615 @@ +import re +import random + +from yors_pano_list_util import ListDelExclude,ListFillOne,ListToTupe,ListShuffle + +# use current_category and current_function from con.py in current directory +# from .conf import CURRENT_CATEGORY,CURRENT_FUNCTION + +# docs(core): use current_category and current_function in this file +CURRENT_CATEGORY="ymc suite/text" +CURRENT_FUNCTION="exec" + +def tags_listify(s:str): + ls = s.split(",") + ls = list(filter(None, ls)) + return ls + +def tags_strify(l:list[str]): + return ','.join(l) + +def tag_trim(s:str): + trimedtag=s + trimedtag=re.sub(r'^ *', '', trimedtag,0) + trimedtag=re.sub(r' *$', '', trimedtag,0) + return trimedtag + +def tag_ms2os(s:str): + return re.sub(re.compile(r' {1,}'), ' ', s,0) + +def tag_unweight(s:str): + # unweight,trim, + unwdtag=s.replace("(","").replace(")","") + unwdtag=re.sub(r':\d.*', '', unwdtag,0) + unwdtag=tag_trim(unwdtag) + unwdtag=tag_ms2os(unwdtag) + return unwdtag + +def tags_unweight(s:str): + ls=tags_listify(s) + nls=[] + for tag in ls: + stdedtag=tag_unweight(tag) + if stdedtag !="": + nls.append(stdedtag) + return ",".join(nls) + +def tags_onelineify(s:str): + return re.sub(r'\n', ',', s,0) + +def tag_std(s:str): + # trim,ms2os + stdedtag=s + stdedtag=tag_trim(stdedtag) + stdedtag=tag_ms2os(stdedtag) + return stdedtag + + +def tags_dup(s:str): + nls=tags_listify(s) + nls=list(dict.fromkeys(nls)) + return ",".join(nls) + +def tags_std(s:str): + ls=tags_listify(s) + nls=[] + for tag in ls: + stdedtag=tag_std(tag) + if stdedtag !="": + nls.append(stdedtag) + return ",".join(nls) + +def tags_ignore(s:str,i:str): + ls=tags_listify(s) + li=tags_listify(i) + + nls=[] + for tag in ls: + stdedtag=tag_unweight(tag) + if stdedtag !="" and not(stdedtag in li): + nls.append(tag) + return ",".join(nls) + +def tags_replace(s:str,i:str,r:str): + ls=tags_listify(s) + li=tags_listify(i) + # lr=tags_listify(r) + + nls=[] + for tag in ls: + stdedtag=tag_unweight(tag) + if stdedtag !="": + if stdedtag in li: + # replace found with r + nls.append(r) + else: + nls.append(tag) + return ",".join(nls) + +def tags_search(s:str,i:str): + ls=tags_listify(s) + li=tags_listify(i) + nls=[] + for tag in ls: + stdedtag=tag_unweight(tag) + if stdedtag !="" : + for pattern in li: + if pattern in stdedtag: + nls.append(tag) + return ",".join(nls) + +def tags_head(s:str,i:str): + """ + move some tags to head + """ + head=tags_listify(i) + tails=tags_listify(tags_ignore(s,i)) + return ",".join(head+tails) + +def tags_tail(s:str,i:str): + """ + move some tags to head + """ + tails=tags_listify(i) + head=tags_listify(tags_ignore(s,i)) + return ",".join(head+tails) + + +# feat(core): NodePromptAdd - prompt add some text +class NodePromptAdd: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + "toadd": ("STRING",{"forceInput":False}), + "action": (["add2head",'add2tail','del|add2head','del|add2tail','none'],), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - add" + + OUTPUT_NODE = True + def exec(self, text='',toadd='',action='add2head'): + # if action =="add2head": + # return list_2_tupe([tags_head(text,prev)]) + # if action =="add2tail": + # return list_2_tupe([tags_tail(text,prev)]) + if 'none' in action: + return ListToTupe([text]) + prev=toadd + if 'del' in action: + prev=tags_ignore(text,toadd) + if 'add2head' in action: + prev=tags_head(text,prev) + if 'add2tail' in action: + prev=tags_tail(text,prev) + return ListToTupe([prev]) + +# feat(core): NodePromptAdvAdd - prompt add some text (adv) +class NodePromptAdvSearch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + "search": ("STRING",{"forceInput":False}), + "action": (['none',"del", "move2head",'move2tail',"replace","search"],), + "replace": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - search (adv)" + + OUTPUT_NODE = True + def exec(self, text='',search='',action='none',replace=''): + if action == "none": + return ListToTupe([text]) + + if action == "search": + return ListToTupe([tags_search(text,search)]) + searchedtags=tags_search(text,search) + if action =="replace": + return ListToTupe([tags_replace(text,searchedtags,replace)]) + if action =="del": + return ListToTupe([tags_ignore(text,searchedtags)]) + if action =="move2head": + return ListToTupe([tags_head(text,searchedtags)]) + if action =="move2tail": + return ListToTupe([tags_tail(text,searchedtags)]) + +# feat(core): NodePromptSearch - prompt search some text +class NodePromptSearch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + "search": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - search" + + OUTPUT_NODE = True + def exec(self, text='',search=''): + return ListToTupe([tags_search(text,search)]) + +# feat(core): NodePromptDup - prompt dup some text +class NodePromptDup: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - dup" + + OUTPUT_NODE = True + def exec(self, text): + return ListToTupe([tags_dup(text)]) + +# feat(core): NodePromptAdvDup - prompt dup some text (adv) +class NodePromptAdvDup: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + # "mode": (["alline", "in-each-line"],), + } + } + + RETURN_TYPES = ("STRING","STRING",) + RETURN_NAMES = ('STRING',"STRING",) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - dup (adv)" + + OUTPUT_NODE = True + def exec(self, text): + a='' + res=[] + textl=text.split("\n") + for line in textl: + res.append(tags_dup(line)) + a='\n'.join(res) + + tags_dup(tags_onelineify(text)) + return ListToTupe([tags_dup(tags_onelineify(a)),a]) + +# feat(core): NodePromptShuffle - prompt shuffle some text +class NodePromptShuffle: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + "keep_n_token": ("STRING", {"default": "0", "multiline": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - shuffle" + + OUTPUT_NODE = True + + def exec(self, text='',keep_n_token='0',seed=0): + random.seed(seed) + textl=tags_listify(tags_onelineify(text)) + el=ListShuffle(textl,int(keep_n_token)) + return ListToTupe([','.join(el)]) + +# feat(core): NodePromptDel - prompt del some text +class NodePromptDel: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "search": ("STRING",{"forceInput":False}), + "todel": ("STRING",{"forceInput":False}), + "action": (["yes",'no'],), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - del" + + OUTPUT_NODE = True + + def exec(self, search='',todel='',action='yes'): + # https://www.geeksforgeeks.org/python-remove-empty-strings-from-list-of-strings/ + if 'no' in action: + return ListToTupe([search]) + ca=tags_ignore(search,tags_unweight(todel)) + return ListToTupe([ca]) + +# feat(core): NodePromptStd - prompt std +class NodePromptStd: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - std" + + OUTPUT_NODE = True + + def exec(self, text): + return ListToTupe([tags_std(text)]) + +# feat(core): NodePromptInput - prompt input +class NodePromptInput: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"default": "","multiline":True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('text',) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - input" + + OUTPUT_NODE = True + + def exec(self, text=''): + return ListToTupe([tags_std(text)]) + +# feat(core): NodePromptUnweight - prompt unweight +class NodePromptUnweight: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - unweight" + + OUTPUT_NODE = True + + def exec(self, text): + return ListToTupe([tags_unweight(text)]) + +# feat(core): NodePromptJoin - prompt join +class NodePromptJoin: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "a": ("STRING",{"forceInput":False}), + "b": ("STRING",{"forceInput":False}), + "c": ("STRING",{"forceInput":False}), + "d": ("STRING",{"forceInput":False}), + "e": ("STRING",{"forceInput":False}), + "f": ("STRING",{"forceInput":False}), + "g": ("STRING",{"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - join" + + OUTPUT_NODE = True + + def exec(self, a,b,c,d,e,f,g): + vl=[a,b,c,d,e,f,g] + vs = (",").join(vl) + vs=tags_std(vs) + # vs = vs.replace(" ", " ").replace(" ,", ",").replace(", ", ",").replace(",,", ",").replace(",,", ",") + return {"ui": {"text": (vs,)}, "result": (vs,)} + + + +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + + def __ne__(self, __value: object) -> bool: + return False + +any_type = AnyType("*") + + +default_prompt1='''Swing + Slide + Climbing frame + Sandbox + See-saw + Merry-go-round + Jungle gym + Trampoline + Monkey bars + Rocking horse + Playhouse + Hopscotch + Balance beam + Spring rider + Water play area + Ball pit + Tunnel + Zip line + Basketball hoop + Bicycle rack + Spinner + Climbing wall + Rope ladder + Tetherball + Flying fox + Swinging bridge + Spiral slide + Water sprinkler + Pedal go-kart + Miniature golf course + ''' +default_prompt1="\n".join([p.strip() for p in default_prompt1.split('\n') if p.strip()!='']) + +# refer comfyui-mixlab-nodes +# feat(core): NodePromptRandom - prompt random +class NodePromptRandom: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "max_count": ("INT", {"default": 9, "min": 1, "max": 1000}), + # "image_field": ("IMAGE",), + "mutable_prompt": ("STRING", + { + "multiline": True, + "default": default_prompt1 + }), + "immutable_prompt": ("STRING", + { + "multiline": True, + "default": 'sticker, Cartoon, ``' + }), + "random_sample": (["enable", "disable"],), + # "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}), + }, + "optional":{ + "seed": (any_type, {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - random" + + + RETURN_TYPES = ("STRING",) + + # CATEGORY = "♾️Mixlab/Prompt" + + OUTPUT_IS_LIST = (True,) + OUTPUT_NODE = True + + def exec(self,max_count,mutable_prompt,immutable_prompt,random_sample,seed=0): + + # Split the text into an array of words + words1 = mutable_prompt.split("\n") + + # Split the text into an array of words + words2 = immutable_prompt.split("\n") + + # progress bar + # pbar = comfy.utils.ProgressBar(len(words1)*len(words2)) + + # Select a random word from the array + # random_word = random.choice(words) + + prompts=[] + for w1 in words1: + w1=w1.strip() + for w2 in words2: + w2=w2.strip() + if '``' not in w2: + if w2=="": + w2='``' + else: + w2=w2+',``' + if w1!='' and w2!='': + prompts.append(w2.replace('``', w1)) + # pbar.update(1) + + if len(prompts)==0: + prompts.append(immutable_prompt) + + if random_sample=='enable': + # random - get x-count element in array + prompts = random.sample(prompts, min(max_count,len(prompts))) + else: + prompts = prompts[:min(max_count,len(prompts))] + + prompts= [elem.strip() for elem in prompts if elem.strip()] + + # return (new_prompt) + return {"ui": {"prompts": prompts}, "result": (prompts,)} + +def addWeight(text, weight=1): + if weight == 1: + return text + else: + return f"({text}:{round(weight,3)})" + +# feat(core): NodePromptWeight - prompt add weight +class NodePromptWeight: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + + "prompt_keyword": ("STRING", + { + "multiline": False, + "default": '', + "dynamicPrompts": False + }), + + "weight":("FLOAT", {"default": 1, "min": -3,"max": 3,"step": 0.01,"display": "slider"}), + + # "min_value":("FLOAT", { + # "default": -2, + # "min": -10, + # "max": 0xffffffffffffffff, + # "step": 0.01, + # "display": "number" + # }), + # "max_value":("FLOAT", { + # "default": 2, + # "min": -10, + # "max": 0xffffffffffffffff, + # "step": 0.01, + # "display": "number" + # }), + + } + } + + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "prompt - add weight" + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("prompt",) + + # FUNCTION = "run" + # CATEGORY = "♾️Mixlab/Prompt" + + INPUT_IS_LIST = False + OUTPUT_IS_LIST = (False,) + OUTPUT_NODE = False + + def exec(self,prompt_keyword,weight): + # if weight < min_value: + # weight= min_value + # elif weight > max_value: + # weight= max_value + p=addWeight(prompt_keyword,weight) + return (p,) \ No newline at end of file diff --git a/modules/text/node_pyio.py b/modules/text/node_pyio.py new file mode 100644 index 0000000..4f14cd0 --- /dev/null +++ b/modules/text/node_pyio.py @@ -0,0 +1,160 @@ +import glob +import hashlib +# import json +# import numpy as np +# from numba import jit +import os +from pathlib import Path +from yors_pano_list_util import ListDelExclude,ListFillOne,ListToTupe,ListShuffle +CURRENT_CATEGORY="ymc suite/text" +CURRENT_FUNCTION="exec" + +def pyioReadDirectory(dir='',pattern='*.txt'): + """ + pyio - get path at dir and pattern + """ + res=[] + for filename in glob.glob(os.path.join(dir,pattern)): + res.append(filename) + return res + +def pyioReadAsText(loc): + """ + pyio - read text at location + """ + res='' + with open(loc) as f: + res = f.read() + f.close() + return res + +def pyioGetFilesInDir(input_dir): + """ + files = pyioGetFilesInDir(input_dir) + """ + return [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] + + +def pyioParseFilename(filename:str): + """ + pyioParseFilename('config.txt') + """ + name, suffix = os.path.splitext(filename) + return (name, suffix) + +def pyioGetFilename(filename:str): + """ + pyioGetFilename('config.txt') + """ + name, suffix = os.path.splitext(filename) + return name + +def pyioPlibParseFilename(filename:str): + """ + pyioPlibParseFilename('config.txt') + """ + p=Path(filename) + return (p.suffix, p.stem) + +def StrmFromList(txtl:list[str],eof:str='\n'): + """ + strm - from text list + + StrmFromList(fileAbsPathList) + """ + return eof.join(txtl) + + +# feat(core): NodePyioFilenameListDetect - pyio - detect filename list +class NodePyioFilenameListDetect: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "path": ("STRING", {"default": "", "multiline": False}), + "pattern": ("STRING", {"default": "*.txt", "multiline": False}), + # "mode": ("STRING", {"default": "filename", "multiline": False}), + "mode": (["filename", "abspath","filename-no-ext"],), + } + } + + RETURN_TYPES = ("STRING",'INT') + RETURN_NAMES = ('STRING','count') + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "pyio - detect filename list" + + OUTPUT_NODE = True + + def exec(self, path='',pattern='*.txt',mode='filename'): + + fileAbsPathList=pyioReadDirectory(path,pattern) + resl = fileAbsPathList + if mode=='filename': + fileNameList=list(map(os.path.basename,fileAbsPathList)) + resl=fileNameList + if mode=='filename-no-ext': + filenameNoExtList=list(map(pyioGetFilename,fileNameList)) + resl=filenameNoExtList + + + # resl='\n'.join(resl) + # fileAbsPathListStrm = StrmFromList(fileAbsPathList) + # fileNameListStrm = StrmFromList(fileNameList) + # filenmeWithoutSuffixList=StrmFromList(filenameNoExtList) + return ListToTupe([StrmFromList(resl),len(resl)]) + +# feat(core): NodePyioFilenameListOutmDetect - pyio - detect filename list (outm) +class NodePyioFilenameListOutmDetect: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "path": ("STRING", {"default": "", "multiline": False}), + "pattern": ("STRING", {"default": "*.txt", "multiline": False}), + } + } + + RETURN_TYPES = ("STRING","STRING","STRING","INT") + RETURN_NAMES = ('absPath','name','nameNoExt','count') + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "pyio - detect filename list (outm)" + + OUTPUT_NODE = True + + def exec(self, path='',pattern='*.txt'): + + absPathList=pyioReadDirectory(path,pattern) + + nameList=list(map(os.path.basename,absPathList)) + + nameNoExtList=list(map(pyioGetFilename,nameList)) + + return ListToTupe([StrmFromList(absPathList),StrmFromList(nameList),StrmFromList(nameNoExtList),len(absPathList)]) + +# feat(core): IoUtilFileListGetText - io - get file list and read as text +class IoUtilFileListGetText: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "path": ("STRING", {"default": "", "multiline": False}), + "pattern": ("STRING", {"default": "*.txt", "multiline": False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ('STRING',) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + OUTPUT_NODE = True + + def exec(self, path='',pattern='*.txt'): + plist=pyioReadDirectory(path,pattern) + text=[] + for loc in plist: + text.append(pyioReadAsText(loc)) + # to multi line + text='\n'.join(text) + return ListToTupe([text]) \ No newline at end of file diff --git a/modules/text/node_region.py b/modules/text/node_region.py new file mode 100644 index 0000000..8668de4 --- /dev/null +++ b/modules/text/node_region.py @@ -0,0 +1,149 @@ + +from yors_pano_list_util import ListDelExclude,ListFillOne,ListToTupe,ListShuffle +# CURRENT_CATEGORY="Ymc Suite/region-util" +CURRENT_CATEGORY="ymc suite/text" +CURRENT_FUNCTION="exec" + +def region_strify(l:list): + tl=list(map(str,l)) + tl=list(filter(None, tl)) + return ','.join(tl) + +def region_listfy(s:str): + tl=s.split(",") + tl=list(filter(None, tl)) + return tl + +def region_intify(l:list): + tl=list(map(int,l)) + return tl + +def region_get_location_from_center_size(cx=128, cy=128, w=256, h=256): + left=cx-w/2 + top=cy-h/2 + right=cx+w/2 + bottom=cy+h/2 + floatl=[left,top,right,bottom,cx,cy,w,h] + intl=list(map(int,floatl)) + return intl + +def region_burn_corp_location_as_square(w=256, h=256): + c=0 + if w=h: + c=1 + top=c*(w-h)/2 + bottom=top + floatl=[left,top,right,bottom] + intl=list(map(int,floatl)) + return ListToTupe(intl) + +def region_burn_corp_location_from_lt_and_size(left=0, top=0, w=256, h=256): + cx=left+w/2 + cy=top+h/2 + + right=cx+w/2 + bottom=cy+h/2 + floatl=[left,top,right,bottom,cx,cy,w,h] + intl=list(map(int,floatl)) + return ListToTupe(intl) + + +REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h'] +REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) +# feat(core): NodeRegionCropLocationBurnNumz - num - burn crop location (center & size) +class NodeRegionCropLocationBurnNumz: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "cx": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}), + "cy": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}), + "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT)) + RETURN_NAMES = ListToTupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "num - burn crop location (center & size)" + OUTPUT_NODE = True + + def exec(self, cx=128, cy=128, w=256, h=256): + intl=region_get_location_from_center_size(cx,cy,w,h) + return ListToTupe(intl) + +# feat(core): NodeRegionCropLocationBurnTxtz - txt - burn crop location (center & size) +class NodeRegionCropLocationBurnTxtz: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "cxcy": ("STRING", {"default": '128,128', "multiline": False}), + "size": ("STRING", {"default": '256,256', "multiline": False}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT)) + RETURN_NAMES = ListToTupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "txt - burn crop location (cxcy & size)" + OUTPUT_NODE = True + + def exec(self, cxcy='128,128',size='256,256'): + cx,cy=region_intify(region_listfy(cxcy)) + w,h=region_intify(region_listfy(size)) + intl=region_get_location_from_center_size(cx,cy,w,h) + return ListToTupe(intl) + +REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h'] +REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES) +# feat(core): NodeRegionCropLocationBurnLtwh - int - burn crop location (lt & size) +class NodeRegionCropLocationBurnLtwh: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "left": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}), + "top": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}), + "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('INT',REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT)) + RETURN_NAMES = ListToTupe(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "int - burn crop location (lt & size)" + + OUTPUT_NODE = True + + def exec(self, left=0, top=0, w=256, h=256): + return region_burn_corp_location_from_lt_and_size(left,top,w,h) + +REGION_LOCATION_RETURN_NAMES=['left','top','right','bottom'] +# feat(core): NodeRegionPaddingSquareLocationBurn - int - burn padding location as square (size) +class NodeRegionPaddingSquareLocationBurn: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), + } + } + RETURN_TYPES = ListToTupe(ListFillOne('INT',len(REGION_LOCATION_RETURN_NAMES))) + RETURN_NAMES = ListToTupe(REGION_LOCATION_RETURN_NAMES) + FUNCTION = CURRENT_FUNCTION + CATEGORY = CURRENT_CATEGORY + NODE_DESC = "int - burn padding location as square (size)" + OUTPUT_NODE = True + + def exec(self, w=256, h=256): + return region_burn_corp_location_as_square(w,h) diff --git a/nodes.py b/nodes.py deleted file mode 100644 index 4210796..0000000 --- a/nodes.py +++ /dev/null @@ -1,2127 +0,0 @@ - -# [save text file of was node suite](D:\code\python\comfyui\custom_nodes\was-node-suite-comfyui\WAS_Node_Suite.py) - -# from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops, ImageFont -# from PIL.PngImagePlugin import PngInfo -# from io import BytesIO -# from typing import Optional -from typing import Callable, List, Dict, Any, Union, Tuple, cast,Optional -from urllib.request import urlopen -# import comfy.diffusers_convert -# import comfy.samplers -# import comfy.sd -# import comfy.utils -# import comfy.clip_vision -# import model_management -import folder_paths as comfy_paths -# from comfy_extras.chainner_models import model_loading -import glob -import hashlib -import json -# import nodes -# import math -# import numpy as np -from numba import jit -import os -import random -import re -# import requests -import socket -# import subprocess -import sys -import time -# import torch -# from tqdm import tqdm - -# define manifest about this project in dict -MANIFEST = { - "name": "ymc node suite", - "version": (1,0,0), - "author": "ymc-github", - "project": "https://github.com/ymc-github/ymc-node-suite-comfyui", - "description": "an extensive node suite for comfyui", -} - -#! SET SYS PATH -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "ymc_node_suite_comfyui")) -sys.path.append(comfy_paths.base_path) - - -#! SYSTEM HOOKS -"""color str""" -class cstr(str): - class color: - END = '\33[0m' - BOLD = '\33[1m' - ITALIC = '\33[3m' - UNDERLINE = '\33[4m' - BLINK = '\33[5m' - BLINK2 = '\33[6m' - SELECTED = '\33[7m' - - BLACK = '\33[30m' - RED = '\33[31m' - GREEN = '\33[32m' - YELLOW = '\33[33m' - BLUE = '\33[34m' - VIOLET = '\33[35m' - BEIGE = '\33[36m' - WHITE = '\33[37m' - - BLACKBG = '\33[40m' - REDBG = '\33[41m' - GREENBG = '\33[42m' - YELLOWBG = '\33[43m' - BLUEBG = '\33[44m' - VIOLETBG = '\33[45m' - BEIGEBG = '\33[46m' - WHITEBG = '\33[47m' - - GREY = '\33[90m' - LIGHTRED = '\33[91m' - LIGHTGREEN = '\33[92m' - LIGHTYELLOW = '\33[93m' - LIGHTBLUE = '\33[94m' - LIGHTVIOLET = '\33[95m' - LIGHTBEIGE = '\33[96m' - LIGHTWHITE = '\33[97m' - - GREYBG = '\33[100m' - LIGHTREDBG = '\33[101m' - LIGHTGREENBG = '\33[102m' - LIGHTYELLOWBG = '\33[103m' - LIGHTBLUEBG = '\33[104m' - LIGHTVIOLETBG = '\33[105m' - LIGHTBEIGEBG = '\33[106m' - LIGHTWHITEBG = '\33[107m' - - @staticmethod - def add_code(name, code): - if not hasattr(cstr.color, name.upper()): - setattr(cstr.color, name.upper(), code) - else: - raise ValueError(f"'cstr' object already contains a code with the name '{name}'.") - - def __new__(cls, text): - return super().__new__(cls, text) - - def __getattr__(self, attr): - if attr.lower().startswith("_cstr"): - code = getattr(self.color, attr.upper().lstrip("_cstr")) - modified_text = self.replace(f"__{attr[1:]}__", f"{code}") - return cstr(modified_text) - elif attr.upper() in dir(self.color): - code = getattr(self.color, attr.upper()) - modified_text = f"{code}{self}{self.color.END}" - return cstr(modified_text) - elif attr.lower() in dir(cstr): - return getattr(cstr, attr.lower()) - else: - raise AttributeError(f"'cstr' object has no attribute '{attr}'") - - def print(self, **kwargs): - print(self, **kwargs) - -#! MESSAGE TEMPLATES -cstr.color.add_code("msg", f"{cstr.color.BLUE}Ymc Node Suite: {cstr.color.END}") -cstr.color.add_code("warning", f"{cstr.color.BLUE}Ymc Node Suite {cstr.color.LIGHTYELLOW}Warning: {cstr.color.END}") -cstr.color.add_code("error", f"{cstr.color.RED}Ymc Node Suite {cstr.color.END}Error: {cstr.color.END}") - - - -#! GLOBALS -NODE_FILE = os.path.abspath(__file__) -MIDAS_INSTALLED = False -CUSTOM_NODES_DIR = comfy_paths.folder_names_and_paths["custom_nodes"][0][0] -MODELS_DIR = comfy_paths.models_dir -WAS_SUITE_ROOT = os.path.dirname(NODE_FILE) -WAS_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_suite_settings.json') -WAS_HISTORY_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_history.json') -WAS_CONFIG_FILE = os.path.join(WAS_SUITE_ROOT, 'was_suite_config.json') -STYLES_PATH = os.path.join(WAS_SUITE_ROOT, 'styles.json') -ALLOWED_EXT = ('.jpeg', '.jpg', '.png', - '.tiff', '.gif', '.bmp', '.webp') - - - -#! INSTALLATION CLEANUP - -# Delete legacy nodes -legacy_was_nodes = ['fDOF_WAS.py', 'Image_Blank_WAS.py', 'Image_Blend_WAS.py', 'Image_Canny_Filter_WAS.py', 'Canny_Filter_WAS.py', 'Image_Combine_WAS.py', 'Image_Edge_Detection_WAS.py', 'Image_Film_Grain_WAS.py', 'Image_Filters_WAS.py', - 'Image_Flip_WAS.py', 'Image_Nova_Filter_WAS.py', 'Image_Rotate_WAS.py', 'Image_Style_Filter_WAS.py', 'Latent_Noise_Injection_WAS.py', 'Latent_Upscale_WAS.py', 'MiDaS_Depth_Approx_WAS.py', 'NSP_CLIPTextEncoder.py', 'Samplers_WAS.py'] -legacy_was_nodes_found = [] - -WAS_NODES_DIR_NAME="ymc-node-suite-comfyui" -WAS_NODES_INDEX="Ymc_Node_Suite.py" -if os.path.basename(CUSTOM_NODES_DIR) == WAS_NODES_DIR_NAME: - legacy_was_nodes.append(WAS_NODES_INDEX) - -f_disp = False -node_path_dir = os.getcwd()+os.sep+'ComfyUI'+os.sep+'custom_nodes'+os.sep -for f in legacy_was_nodes: - file = f'{node_path_dir}{f}' - if os.path.exists(file): - if not f_disp: - cstr("Found legacy nodes. Archiving legacy nodes...").msg.print() - f_disp = True - legacy_was_nodes_found.append(file) - -if legacy_was_nodes_found: - import zipfile - from os.path import basename - archive = zipfile.ZipFile( - f'{node_path_dir}WAS_Legacy_Nodes_Backup_{round(time.time())}.zip', "w") - for f in legacy_was_nodes_found: - archive.write(f, basename(f)) - try: - os.remove(f) - except OSError: - pass - archive.close() - -if f_disp: - cstr("Legacy cleanup complete.").msg.print() - -#! WAS SUITE CONFIG - -was_conf_template = { - "run_requirements": True, - "suppress_uncomfy_warnings": True, - "show_startup_junk": True, - "show_inspiration_quote": True, - "text_nodes_type": "STRING", - "webui_styles": None, - "webui_styles_persistent_update": True, - "blip_model_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_capfilt_large.pth", - "blip_model_vqa_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_vqa_capfilt_large.pth", - "sam_model_vith_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth", - "sam_model_vitl_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth", - "sam_model_vitb_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", - "history_display_limit": 36, - "use_legacy_ascii_text": False, - "ffmpeg_bin_path": "/path/to/ffmpeg", - "ffmpeg_extra_codecs": { - "avc1": ".mp4", - "h264": ".mkv", - }, - "wildcards_path": os.path.join(WAS_SUITE_ROOT, "wildcards"), - "wildcard_api": True, - } - -# Create, Load, or Update Config -def getSuiteConfig(): - global was_conf_template - try: - with open(WAS_CONFIG_FILE, "r") as f: - was_config = json.load(f) - except OSError as e: - cstr(f"Unable to load conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print() - return was_conf_template - except Exception as e: - cstr(f"Unable to load conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print() - return was_conf_template - return was_config - -def updateSuiteConfig(conf): - try: - with open(WAS_CONFIG_FILE, "w", encoding='utf-8') as f: - json.dump(conf, f, indent=4) - except OSError as e: - print(e) - return False - except Exception as e: - print(e) - return False - return True - -if not os.path.exists(WAS_CONFIG_FILE): - if updateSuiteConfig(was_conf_template): - cstr(f'Created default conf file at `{WAS_CONFIG_FILE}`.').msg.print() - was_config = getSuiteConfig() - else: - cstr(f"Unable to create default conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print() - was_config = was_conf_template - -else: - was_config = getSuiteConfig() - - update_config = False - for sett_ in was_conf_template.keys(): - if not was_config.__contains__(sett_): - was_config.update({sett_: was_conf_template[sett_]}) - update_config = True - - if update_config: - updateSuiteConfig(was_config) - - -# WAS SETTINGS MANAGER - -class WASDatabase: - """ - The WAS Suite Database Class provides a simple key-value database that stores - data in a flatfile using the JSON format. Each key-value pair is associated with - a category. - - Attributes: - filepath (str): The path to the JSON file where the data is stored. - data (dict): The dictionary that holds the data read from the JSON file. - - Methods: - insert(category, key, value): Inserts a key-value pair into the database - under the specified category. - get(category, key): Retrieves the value associated with the specified - key and category from the database. - update(category, key): Update a value associated with the specified - key and category from the database. - delete(category, key): Deletes the key-value pair associated with the - specified key and category from the database. - _save(): Saves the current state of the database to the JSON file. - """ - def __init__(self, filepath): - self.filepath = filepath - try: - with open(filepath, 'r') as f: - self.data = json.load(f) - except FileNotFoundError: - self.data = {} - - def catExists(self, category): - return self.data.__contains__(category) - - def keyExists(self, category, key): - return self.data[category].__contains__(key) - - def insert(self, category, key, value): - if category not in self.data: - self.data[category] = {} - self.data[category][key] = value - self._save() - - def update(self, category, key, value): - if category in self.data and key in self.data[category]: - self.data[category][key] = value - self._save() - - def updateCat(self, category, dictionary): - self.data[category].update(dictionary) - self._save() - - def get(self, category, key): - return self.data.get(category, {}).get(key, None) - - def getDB(self): - return self.data - - def insertCat(self, category): - if self.data.__contains__(category): - cstr(f"The database category `{category}` already exists!").error.print() - return - self.data[category] = {} - self._save() - - def getDict(self, category): - if not self.data.__contains__(category): - cstr(f"\033[34mYmc Node Suite\033[0m Error: The database category `{category}` does not exist!").error.print() - return self.data[category] - - def delete(self, category, key): - if category in self.data and key in self.data[category]: - del self.data[category][key] - self._save() - - def _save(self): - try: - with open(self.filepath, 'w') as f: - json.dump(self.data, f, indent=4) - except FileNotFoundError: - cstr(f"Cannot save database to file '{self.filepath}'." - " Storing the data in the object instead. Does the folder and node file have write permissions?").warning.print() - -# Initialize the settings database -WDB = WASDatabase(WAS_DATABASE) - -# WAS Token Class - -class TextTokens: - def __init__(self): - self.WDB = WDB - if not self.WDB.getDB().__contains__('custom_tokens'): - self.WDB.insertCat('custom_tokens') - self.custom_tokens = self.WDB.getDict('custom_tokens') - - self.tokens = { - '[time]': str(time.time()).replace('.','_'), - '[hostname]': socket.gethostname(), - } - - if '.' in self.tokens['[time]']: - self.tokens['[time]'] = self.tokens['[time]'].split('.')[0] - - try: - self.tokens['[user]'] = ( os.getlogin() if os.getlogin() else 'null' ) - except Exception: - self.tokens['[user]'] = 'null' - - def addToken(self, name, value): - self.custom_tokens.update({name: value}) - self._update() - - def removeToken (self, name): - self.custom_tokens.pop(name) - self._update() - - def format_time(self, format_code): - return time.strftime(format_code, time.localtime(time.time())) - - def parseTokens(self, text): - tokens = self.tokens.copy() - - if self.custom_tokens: - tokens.update(self.custom_tokens) - - # Update time - tokens['[time]'] = str(time.time()) - if '.' in tokens['[time]']: - tokens['[time]'] = tokens['[time]'].split('.')[0] - - for token, value in tokens.items(): - if token.startswith('[time('): - continue - text = text.replace(token, value) - - def replace_custom_time(match): - format_code = match.group(1) - return self.format_time(format_code) - - text = re.sub(r'\[time\((.*?)\)\]', replace_custom_time, text) - - return text - - def _update(self): - self.WDB.updateCat('custom_tokens', self.custom_tokens) - - -# Update text file history - -def update_history_text_files(new_paths): - HDB = WASDatabase(WAS_HISTORY_DATABASE) - if HDB.catExists("History") and HDB.keyExists("History", "TextFiles"): - saved_paths = HDB.get("History", "TextFiles") - for path_ in saved_paths: - if not os.path.exists(path_): - saved_paths.remove(path_) - if isinstance(new_paths, str): - if new_paths in saved_paths: - saved_paths.remove(new_paths) - saved_paths.append(new_paths) - elif isinstance(new_paths, list): - for path_ in new_paths: - if path_ in saved_paths: - saved_paths.remove(path_) - saved_paths.append(path_) - HDB.update("History", "TextFiles", saved_paths) - else: - if not HDB.catExists("History"): - HDB.insertCat("History") - if isinstance(new_paths, str): - HDB.insert("History", "TextFiles", [new_paths]) - elif isinstance(new_paths, list): - HDB.insert("History", "TextFiles", new_paths) - -# Update output image history - -# def update_history_output_images(new_paths): -# HDB = WASDatabase(WAS_HISTORY_DATABASE) -# category = "Output_Images" -# if HDB.catExists("History") and HDB.keyExists("History", category): -# saved_paths = HDB.get("History", category) -# for path_ in saved_paths: -# if not os.path.exists(path_): -# saved_paths.remove(path_) -# if isinstance(new_paths, str): -# if new_paths in saved_paths: -# saved_paths.remove(new_paths) -# saved_paths.append(new_paths) -# elif isinstance(new_paths, list): -# for path_ in new_paths: -# if path_ in saved_paths: -# saved_paths.remove(path_) -# saved_paths.append(path_) -# HDB.update("History", category, saved_paths) -# else: -# if not HDB.catExists("History"): -# HDB.insertCat("History") -# if isinstance(new_paths, str): -# HDB.insert("History", category, [new_paths]) -# elif isinstance(new_paths, list): -# HDB.insert("History", category, new_paths) - -# set INPUT_TYPES -class WAS_Text_Save: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"forceInput": True}), - "path": ("STRING", {"default": './ComfyUI/output/[time(%Y-%m-%d)]', "multiline": False}), - "filename_prefix": ("STRING", {"default": "ComfyUI"}), - "filename_delimiter": ("STRING", {"default":"_"}), - "filename_number_padding": ("INT", {"default":4, "min":2, "max":9, "step":1}), - "overwrite_mode": (["false", "prefix_as_filename"],), - "ext": ("STRING", {"default":".txt"}), - - } - } - - OUTPUT_NODE = True - # feat(core): add feat that noted in issue 1 - # https://github.com/YMC-GitHub/ymc-node-suite-comfyui/issues/1 - RETURN_TYPES = ("STRING", "STRING","STRING") - RETURN_NAMES = ('content','filenameWithoutExt','ext') - FUNCTION = "save_text_file" - CATEGORY = "Ymc Suite/IO" - - def save_text_file(self, text, path, filename_prefix='ComfyUI', filename_delimiter='_', filename_number_padding=4,overwrite_mode='false',ext='.txt'): - - tokens = TextTokens() - path = tokens.parseTokens(path) - filename_prefix = tokens.parseTokens(filename_prefix) - - # feat(core): add paths when it does not exits - if not os.path.exists(path): - cstr(f"The path `{path}` doesn't exist! Creating it...").warning.print() - try: - os.makedirs(path, exist_ok=True) - except OSError as e: - cstr(f"The path `{path}` could not be created! Is there write access?\n{e}").error.print() - - # feat(core): out info when text is empty - if text.strip() == '': - cstr(f"There is no text specified to save! Text is empty.").error.print() - - - delimiter = filename_delimiter - number_padding = int(filename_number_padding) - # file_extension = '.txt' - file_extension = ext - - filename ,counter= self.generate_filename_data(path, filename_prefix, delimiter, number_padding, file_extension) - - filenameWithoutExt = f"{filename_prefix}{delimiter}{counter:0{number_padding}}" - - # feat(core): use prefix as file name when overwrite_mode is 'prefix_as_filename' - if overwrite_mode == 'prefix_as_filename': - filename = f"{filename_prefix}{file_extension}" - filenameWithoutExt = f"{filename_prefix}" - - fileLocation = os.path.join(path, filename) - - self.writeTextFile(fileLocation, text) - - update_history_text_files(fileLocation) - # filenameWithoutExt,ext = os.path.splitext(filename) - - # return (text, { "ui": { "string": text } } ) - # feat(core): use the 2th result is filename without ext - # feat(core): use the 3th result is ext - return text, filenameWithoutExt,file_extension - - def generate_filename_data(self, path, prefix, delimiter, number_padding, extension): - pattern = f"{re.escape(prefix)}{re.escape(delimiter)}(\\d{{{number_padding}}})" - existing_counters = [ - int(re.search(pattern, filename).group(1)) - for filename in os.listdir(path) - if re.match(pattern, filename) - ] - existing_counters.sort(reverse=True) - - if existing_counters: - counter = existing_counters[0] + 1 - else: - counter = 1 - - filename = f"{prefix}{delimiter}{counter:0{number_padding}}{extension}" - while os.path.exists(os.path.join(path, filename)): - counter += 1 - filename = f"{prefix}{delimiter}{counter:0{number_padding}}{extension}" - - return filename,counter - - def writeTextFile(self, file, content): - try: - with open(file, 'w', encoding='utf-8', newline='\n') as f: - f.write(content) - except OSError: - cstr(f"Unable to save file `{file}`").error.print() - -# Image Save (NSP Compatible) -# Originally From ComfyUI/nodes.py - -# class WAS_Image_Save: -# def __init__(self): -# self.output_dir = comfy_paths.output_directory -# self.type = 'output' -# @classmethod -# def INPUT_TYPES(cls): -# return { -# "required": { -# "images": ("IMAGE", ), -# "output_path": ("STRING", {"default": '[time(%Y-%m-%d)]', "multiline": False}), -# "filename_prefix": ("STRING", {"default": "ComfyUI"}), -# "filename_delimiter": ("STRING", {"default":"_"}), -# "filename_number_padding": ("INT", {"default":4, "min":1, "max":9, "step":1}), -# "filename_number_start": (["false", "true"],), -# "extension": (['png', 'jpeg', 'gif', 'tiff', 'webp'], ), -# "quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}), -# "lossless_webp": (["false", "true"],), -# "overwrite_mode": (["false", "prefix_as_filename"],), -# "show_history": (["false", "true"],), -# "show_history_by_prefix": (["true", "false"],), -# "embed_workflow": (["true", "false"],), -# "show_previews": (["true", "false"],), -# }, -# "hidden": { -# "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" -# }, -# } - -# RETURN_TYPES = () -# FUNCTION = "was_save_images" - -# OUTPUT_NODE = True - -# CATEGORY = "WAS Suite/IO" - -# def was_save_images(self, images, output_path='', filename_prefix="ComfyUI", filename_delimiter='_', -# extension='png', quality=100, lossless_webp="false", prompt=None, extra_pnginfo=None, -# overwrite_mode='false', filename_number_padding=4, filename_number_start='false', -# show_history='false', show_history_by_prefix="true", embed_workflow="true", -# show_previews="true"): - -# delimiter = filename_delimiter -# number_padding = filename_number_padding -# lossless_webp = (lossless_webp == "true") - -# # Define token system -# tokens = TextTokens() - -# original_output = self.output_dir -# # Parse prefix tokens -# filename_prefix = tokens.parseTokens(filename_prefix) - -# # Setup output path -# if output_path in [None, '', "none", "."]: -# output_path = self.output_dir -# else: -# output_path = tokens.parseTokens(output_path) -# if not os.path.isabs(output_path): -# output_path = os.path.join(self.output_dir, output_path) -# base_output = os.path.basename(output_path) -# if output_path.endswith("ComfyUI/output") or output_path.endswith("ComfyUI\output"): -# base_output = "" - -# # Check output destination -# if output_path.strip() != '': -# if not os.path.isabs(output_path): -# output_path = os.path.join(comfy_paths.output_directory, output_path) -# if not os.path.exists(output_path.strip()): -# cstr(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.').warning.print() -# os.makedirs(output_path, exist_ok=True) - -# # Find existing counter values -# if filename_number_start == 'true': -# pattern = f"(\\d{{{filename_number_padding}}}){re.escape(delimiter)}{re.escape(filename_prefix)}" -# else: -# pattern = f"{re.escape(filename_prefix)}{re.escape(delimiter)}(\\d{{{filename_number_padding}}})" -# existing_counters = [ -# int(re.search(pattern, filename).group(1)) -# for filename in os.listdir(output_path) -# if re.match(pattern, os.path.basename(filename)) -# ] -# existing_counters.sort(reverse=True) - -# # Set initial counter value -# if existing_counters: -# counter = existing_counters[0] + 1 -# else: -# counter = 1 - -# # Set initial counter value -# if existing_counters: -# counter = existing_counters[0] + 1 -# else: -# counter = 1 - -# # Set Extension -# file_extension = '.' + extension -# if file_extension not in ALLOWED_EXT: -# cstr(f"The extension `{extension}` is not valid. The valid formats are: {', '.join(sorted(ALLOWED_EXT))}").error.print() -# file_extension = "png" - -# results = list() -# for image in images: -# i = 255. * image.cpu().numpy() -# img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - -# metadata = PngInfo() -# if embed_workflow == 'true': -# if prompt is not None: -# metadata.add_text("prompt", json.dumps(prompt)) -# if extra_pnginfo is not None: -# for x in extra_pnginfo: -# metadata.add_text(x, json.dumps(extra_pnginfo[x])) - -# if overwrite_mode == 'prefix_as_filename': -# file = f"{filename_prefix}{file_extension}" -# else: -# if filename_number_start == 'true': -# file = f"{counter:0{number_padding}}{delimiter}{filename_prefix}{file_extension}" -# else: -# file = f"{filename_prefix}{delimiter}{counter:0{number_padding}}{file_extension}" -# if os.path.exists(os.path.join(output_path, file)): -# counter += 1 -# try: -# output_file = os.path.abspath(os.path.join(output_path, file)) -# if extension == 'png': -# img.save(output_file, -# pnginfo=metadata, optimize=True) -# elif extension == 'webp': -# img.save(output_file, quality=quality) -# elif extension == 'jpeg': -# img.save(output_file, -# quality=quality, optimize=True) -# elif extension == 'tiff': -# img.save(output_file, -# quality=quality, optimize=True) -# elif extension == 'webp': -# img.save(output_file, quality=quality, -# lossless=lossless_webp, exif=metadata) - -# cstr(f"Image file saved to: {output_file}").msg.print() - -# if show_history != 'true' and show_previews == 'true': -# subfolder = self.get_subfolder_path(output_file, original_output) -# results.append({ -# "filename": file, -# "subfolder": subfolder, -# "type": self.type -# }) - -# # Update the output image history -# update_history_output_images(output_file) - -# except OSError as e: -# cstr(f'Unable to save file to: {output_file}').error.print() -# print(e) -# except Exception as e: -# cstr('Unable to save file due to the to the following error:').error.print() -# print(e) - -# if overwrite_mode == 'false': -# counter += 1 - -# filtered_paths = [] -# if show_history == 'true' and show_previews == 'true': -# HDB = WASDatabase(WAS_HISTORY_DATABASE) -# conf = getSuiteConfig() -# if HDB.catExists("History") and HDB.keyExists("History", "Output_Images"): -# history_paths = HDB.get("History", "Output_Images") -# else: -# history_paths = None - -# if history_paths: - -# for image_path in history_paths: -# image_subdir = self.get_subfolder_path(image_path, self.output_dir) -# current_subdir = self.get_subfolder_path(output_file, self.output_dir) -# if not os.path.exists(image_path): -# continue -# if show_history_by_prefix == 'true' and image_subdir != current_subdir: -# continue -# if show_history_by_prefix == 'true' and not os.path.basename(image_path).startswith(filename_prefix): -# continue -# filtered_paths.append(image_path) - -# if conf.__contains__('history_display_limit'): -# filtered_paths = filtered_paths[-conf['history_display_limit']:] - -# filtered_paths.reverse() - -# if filtered_paths: -# for image_path in filtered_paths: -# subfolder = self.get_subfolder_path(image_path, self.output_dir) -# image_data = { -# "filename": os.path.basename(image_path), -# "subfolder": subfolder, -# "type": self.type -# } -# results.append(image_data) - -# if show_previews == 'true': -# return {"ui": {"images": results}} -# else: -# return {"ui": {"images": []}} - - -# def get_subfolder_path(self, image_path, output_path): -# output_parts = output_path.strip(os.sep).split(os.sep) -# image_parts = image_path.strip(os.sep).split(os.sep) -# common_parts = os.path.commonprefix([output_parts, image_parts]) -# subfolder_parts = image_parts[len(common_parts):] -# subfolder_path = os.sep.join(subfolder_parts[:-1]) -# return subfolder_path - -"""python-list-util""" -def list_del_exculde(start,oldlist,exclist,newlist): - if start==len(oldlist):return newlist #base condition - if not(exclist[start] in oldlist): #checking if - newlist.append(oldlist[start]) - return list_del_exculde(start+1,oldlist,exclist,newlist) -def list_fill_one(t,n=20): - return [t] * n -def list_2_tupe(l): - return tuple(l) - -def list_shuffle(l:list,keep_n=0): - """ - list shuffle with keep n token - - return a new list - """ - el=l.copy() - index=keep_n - if index >=1: - el=l[index:] - random.shuffle(el) - if index >=1: - el=l[0:index]+el - return el - -"""cutoff-region-util""" -# cutoff region util -class CutoffRegionUtil: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "main_text": ("STRING", {"default": "dress", "multiline": True}), - "target_text":("STRING", {"default": "white", "multiline": False}), - "weight": ("STRING", {"default": "1.0", "multiline": False}), - } - } - - RETURN_TYPES = ("STRING","STRING","STRING",) - RETURN_NAMES = ('region_text','target_text','prompt_weight') - FUNCTION = "exec" - CATEGORY = "Ymc Suite/cutoff-region-util" - OUTPUT_NODE = True - - def exec(self, main_text,target_text,weight): - # Converted inputs are sent as the string of 'undefined' if not connected - if target_text == "undefined": - target_text = "" - if main_text == "undefined": - main_text = "" - region_text = "" - if weight is None or weight=='' or weight=='1.0': - region_text = (" ").join(filter(None, [target_text, main_text])) - else: - # region_text = (" ").join(filter(None, ["(",target_text, main_text,":",weight,")"])) - region_text = (" ").join(filter(None, [target_text, main_text])) - region_text = ("").join(filter(None, ["(",region_text,":",weight,")"])) - # return {"ui": {"text": (region_text,)}, "result": (region_text,)} - return region_text,target_text,weight - -# cutoff region util - - - -TEXT_BASE_COLORS=['red','orange','yellow','green','cyan','blue','purple','black','white','pink','custom'] -TEXT_BASE_COLORS_COUNT=len(TEXT_BASE_COLORS) - -class CutoffRegionUtilColorTextList: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "custom": ("STRING", {"default": "red", "multiline": False}) - } - } - - # RETURN_TYPES = ("STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING",) - RETURN_TYPES = list_2_tupe(list_fill_one('STRING',TEXT_BASE_COLORS_COUNT)) - RETURN_NAMES = list_2_tupe(TEXT_BASE_COLORS) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/cutoff-region-util" - OUTPUT_NODE = True - - def exec(self, custom): - # Converted inputs are sent as the string of 'undefined' if not connected - if custom == "undefined": - custom = "sliver" - # return red,orange,yellow,green,cyan,blue,purple,black,white,pink,custom - TEXT_BASE_COLORS_VALUE = TEXT_BASE_COLORS.copy() - TEXT_BASE_COLORS_VALUE[TEXT_BASE_COLORS_COUNT-1]=custom - return list_2_tupe(TEXT_BASE_COLORS_VALUE) - -TEXT_LIST_RETURN_NAMES=['a','b','c','d','e','f','g'] -TEXT_LIST_COUNT=len(TEXT_LIST_RETURN_NAMES) -class TextUtilPathList: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "a": ("STRING",{"forceInput":False}), - "b": ("STRING",{"forceInput":False}), - "c": ("STRING",{"forceInput":False}), - "d": ("STRING",{"forceInput":False}), - "e": ("STRING",{"forceInput":False}), - "f": ("STRING",{"forceInput":False}), - "g": ("STRING",{"forceInput":False}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('STRING',TEXT_LIST_COUNT)) - RETURN_NAMES = list_2_tupe(TEXT_LIST_RETURN_NAMES) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, a,b,c,d,e,f,g): - return list_2_tupe([a,b,c,d,e,f,g]) - -# "text": (TEXT_TYPE, {"forceInput": (True if TEXT_TYPE == 'STRING' else False)}), -class TextUtilJoinText: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "splitchar": ("STRING", {"default": "_", "multiline": False}), - "a": ("STRING",{"forceInput":False}), - "b": ("STRING",{"forceInput":False}), - "c": ("STRING",{"forceInput":False}), - "d": ("STRING",{"forceInput":False}), - "e": ("STRING",{"forceInput":False}), - "f": ("STRING",{"forceInput":False}), - "g": ("STRING",{"forceInput":False}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('STRING',1)) - RETURN_NAMES = list_2_tupe(['text']) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, splitchar,a,b,c,d,e,f,g): - ls=[a,b,c,d,e,f,g] - ls = list(filter(None, ls)) - joinedtxt=splitchar.join(ls) - return list_2_tupe([joinedtxt]) - - -class TextUtilRandomText: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": '', "multiline": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "char": ("STRING", {"default": '|', "multiline": False}), - } - } - - RETURN_TYPES = ("STRING",'INT',) - RETURN_NAMES = list_2_tupe(['text','seed']) - FUNCTION = "exec" - - CATEGORY = "Ymc Suite/text-util" - - def exec(self, text, seed,char='|'): - iteml = text.split(char) - random.seed(seed) - choice = random.choice(iteml) - return list_2_tupe([choice,seed]) - - @classmethod - def IS_CHANGED(cls, **kwargs): - return float("NaN") - -class TextUtilLoopText: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": '', "multiline": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "char": ("STRING", {"default": '|', "multiline": False}), - } - } - - RETURN_TYPES = ("STRING",'INT','STRING') - RETURN_NAMES = list_2_tupe(['text','seed','index']) - FUNCTION = "exec" - - CATEGORY = "Ymc Suite/text-util" - - def exec(self, text, seed,char='|'): - iteml = text.split(char) - random.seed(seed) - # get random index in range - # index = random.randint(0, len(iteml)-1) - - # get loop index with de/cr seed - length=len(iteml) - maxindex=(length-1) - minindex=0 - index=minindex - if seed == 0: - index=0 - else: - if maxindex > seed: - index=seed - else: - index = seed % maxindex - if index <=0: - index=0 - if index >length: - index=maxindex - - choice = iteml[index] - # print(maxindex,seed,index,choice) - return list_2_tupe([choice,seed,str(index)]) - - @classmethod - def IS_CHANGED(cls, **kwargs): - return float("NaN") - -class TextUtilSearchText: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": '', "multiline": False}), - "find": ("STRING", {"default": '', "multiline": False}), - "replace": ("STRING", {"default": '', "multiline": False}), - "action": (["yes",'no'],), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = list_2_tupe(['text']) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - - def exec(self, text, find, replace,action='yes'): - if 'no' in action: - return list_2_tupe([text]) - return (self.replace_substring(text, find, replace), ) - - def replace_substring(self, text, find, replace): - import re - text = re.sub(find, replace, text) - return text - -class TextUtilSwitchText: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "a": ("STRING", {"default": '', "multiline": False}), - "b": ("STRING", {"default": '', "multiline": False}), - "boolean_number": ("NUMBER",), - } - } - - RETURN_TYPES = ('STRING',) - FUNCTION = "exec" - - CATEGORY = "Ymc Suite/text-util" - def exec(self, a='', b='', boolean_number=1): - - if int(round(boolean_number)) == 1: - return (a, ) - else: - return (b, ) - - -# D:\code\python\comfyui\nodes.py -# class ImageInvert: - -# @classmethod -# def INPUT_TYPES(s): -# return {"required": { "image": ("IMAGE",)}} - -# RETURN_TYPES = ("IMAGE",) -# FUNCTION = "invert" - -# CATEGORY = "image" - -# def invert(self, image): -# s = 1.0 - image -# return (s,) - -"""region-util""" - -def region_strify(l:list): - tl=list(map(str,l)) - tl=list(filter(None, tl)) - return ','.join(tl) - -def region_listfy(s:str): - tl=s.split(",") - tl=list(filter(None, tl)) - return tl - -def region_intify(l:list): - tl=list(map(int,l)) - return tl - -def region_get_location_from_center_size(cx=128, cy=128, w=256, h=256): - left=cx-w/2 - top=cy-h/2 - right=cx+w/2 - bottom=cy+h/2 - floatl=[left,top,right,bottom,cx,cy,w,h] - intl=list(map(int,floatl)) - return intl - -REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h'] -REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) -class RegionUtilGetCropLocationByCenter: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "cx": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}), - "cy": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}), - "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT)) - RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/region-util" - OUTPUT_NODE = True - - def exec(self, cx=128, cy=128, w=256, h=256): - intl=region_get_location_from_center_size(cx,cy,w,h) - return list_2_tupe(intl) - -class RegionUtilGetCropLocationFromCenterSizeText: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING", {"default": '128,128,256,256', "multiline": False}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT)) - RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/region-util" - OUTPUT_NODE = True - - def exec(self, text='128,128,256,256'): - tl=region_intify(region_listfy(text)) - cx,cy,w,h=tl - intl=region_get_location_from_center_size(cx,cy,w,h) - return list_2_tupe(intl) - -REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h'] -REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES) -class RegionUtilGetCropLocationByLT: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "left": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}), - "top": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}), - "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT)) - RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/region-util" - OUTPUT_NODE = True - - def exec(self, left=0, top=0, w=256, h=256): - cx=left+w/2 - cy=top+h/2 - - right=cx+w/2 - bottom=cy+h/2 - floatl=[left,top,right,bottom,cx,cy,w,h] - intl=list(map(int,floatl)) - return list_2_tupe(intl) - -REGION_LOCATION_RETURN_NAMES=['left','top','right','bottom'] -class RegionUtilGetPadOutLocationBySize: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - "h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',len(REGION_LOCATION_RETURN_NAMES))) - RETURN_NAMES = list_2_tupe(REGION_LOCATION_RETURN_NAMES) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/region-util" - OUTPUT_NODE = True - - def exec(self, w=256, h=256): - c=0 - if w=h: - c=1 - top=c*(w-h)/2 - bottom=top - floatl=[left,top,right,bottom] - intl=list(map(int,floatl)) - return list_2_tupe(intl) - - -"""canvas-util""" -class CanvasUtilCalSize: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "w": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}), - "h": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}), - "lockoff": (["false", "w","h"],), - # "rate": (["1:1", "2:3","9:16"],), - "rate": ("STRING", {"default": "1:1", "multiline": False}), - "reverse": (["false", "true"],), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',2)) - RETURN_NAMES = list_2_tupe(['w','h']) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/canvas-util" - OUTPUT_NODE = True - - def exec(self,w=512, h=512,lockoff='false',rate='1:1',reverse='false'): - rw=w - rh=h - # get h or w with lockoff - if lockoff != "false": - ratel=list(map(int,rate.split(":"))) - raten=(ratel[0]/ratel[1]) - if lockoff == "w": - rh=int(w/raten) - if lockoff == "h": - rw=int(h*raten) - - floatl=[rw,rh] - # reverse result of w h - if reverse == "true": - floatl=[rh,rw] - intl=list(map(int,floatl)) - return list_2_tupe(intl) - -"""hks-util""" -class HksUtilCalDenoiseStep: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "steps": ("INT", {"default":20, "max": 50, "min":0, "step":1}), - "denoise": ("FLOAT", {"min": 0, "max": 1, "step": 0.05, "default": 0.3}), - } - } - RETURN_TYPES = list_2_tupe(list_fill_one('INT',2)) - RETURN_NAMES = list_2_tupe(['steps','step_at_start']) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/hks-util" - OUTPUT_NODE = True - - def exec(self,steps=20, denoise=0.3): - floatl=[steps,steps*(1-denoise)] - intl=list(map(int,floatl)) - return list_2_tupe(intl) - -# text-util -def tags_listify(s:str): - ls = s.split(",") - ls = list(filter(None, ls)) - return ls - -def tags_strify(l:list[str]): - return ','.join(l) - -def tag_trim(s:str): - trimedtag=s - trimedtag=re.sub(r'^ *', '', trimedtag,0) - trimedtag=re.sub(r' *$', '', trimedtag,0) - return trimedtag - -def tag_ms2os(s:str): - return re.sub(re.compile(r' {1,}'), ' ', s,0) - -def tag_unweight(s:str): - # unweight,trim, - unwdtag=s.replace("(","").replace(")","") - unwdtag=re.sub(r':\d.*', '', unwdtag,0) - unwdtag=tag_trim(unwdtag) - unwdtag=tag_ms2os(unwdtag) - return unwdtag - -def tags_unweight(s:str): - ls=tags_listify(s) - nls=[] - for tag in ls: - stdedtag=tag_unweight(tag) - if stdedtag !="": - nls.append(stdedtag) - return ",".join(nls) - -def tags_onelineify(s:str): - return re.sub(r'\n', ',', s,0) - -def tag_std(s:str): - # trim,ms2os - stdedtag=s - stdedtag=tag_trim(stdedtag) - stdedtag=tag_ms2os(stdedtag) - return stdedtag - - -def tags_dup(s:str): - nls=tags_listify(s) - nls=list(dict.fromkeys(nls)) - return ",".join(nls) - -def tags_std(s:str): - ls=tags_listify(s) - nls=[] - for tag in ls: - stdedtag=tag_std(tag) - if stdedtag !="": - nls.append(stdedtag) - return ",".join(nls) - -def tags_ignore(s:str,i:str): - ls=tags_listify(s) - li=tags_listify(i) - - nls=[] - for tag in ls: - stdedtag=tag_unweight(tag) - if stdedtag !="" and not(stdedtag in li): - nls.append(tag) - return ",".join(nls) - -def tags_replace(s:str,i:str,r:str): - ls=tags_listify(s) - li=tags_listify(i) - # lr=tags_listify(r) - - nls=[] - for tag in ls: - stdedtag=tag_unweight(tag) - if stdedtag !="": - if stdedtag in li: - # replace found with r - nls.append(r) - else: - nls.append(tag) - return ",".join(nls) - -def tags_search(s:str,i:str): - ls=tags_listify(s) - li=tags_listify(i) - nls=[] - for tag in ls: - stdedtag=tag_unweight(tag) - if stdedtag !="" : - for pattern in li: - if pattern in stdedtag: - nls.append(tag) - return ",".join(nls) - -def tags_head(s:str,i:str): - """ - move some tags to head - """ - head=tags_listify(i) - tails=tags_listify(tags_ignore(s,i)) - return ",".join(head+tails) - -def tags_tail(s:str,i:str): - """ - move some tags to head - """ - tails=tags_listify(i) - head=tags_listify(tags_ignore(s,i)) - return ",".join(head+tails) - - -class TextUtilPromptAdd: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - "toadd": ("STRING",{"forceInput":False}), - "action": (["add2head",'add2tail','del|add2head','del|add2tail','none'],), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text='',toadd='',action='add2head'): - # if action =="add2head": - # return list_2_tupe([tags_head(text,prev)]) - # if action =="add2tail": - # return list_2_tupe([tags_tail(text,prev)]) - if 'none' in action: - return list_2_tupe([text]) - prev=toadd - if 'del' in action: - prev=tags_ignore(text,toadd) - if 'add2head' in action: - prev=tags_head(text,prev) - if 'add2tail' in action: - prev=tags_tail(text,prev) - return list_2_tupe([prev]) - -class TextUtilPromptAdvSearch: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - "search": ("STRING",{"forceInput":False}), - "action": (['none',"del", "move2head",'move2tail',"replace","search"],), - "replace": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text='',search='',action='none',replace=''): - if action == "none": - return list_2_tupe([text]) - - if action == "search": - return list_2_tupe([tags_search(text,search)]) - searchedtags=tags_search(text,search) - if action =="replace": - return list_2_tupe([tags_replace(text,searchedtags,replace)]) - if action =="del": - return list_2_tupe([tags_ignore(text,searchedtags)]) - if action =="move2head": - return list_2_tupe([tags_head(text,searchedtags)]) - if action =="move2tail": - return list_2_tupe([tags_tail(text,searchedtags)]) - - -class TextUtilPromptSearch: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - "search": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text='',search=''): - return list_2_tupe([tags_search(text,search)]) - -class TextUtilPromptDup: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text): - return list_2_tupe([tags_dup(text)]) - -# tags_onelineify -class TextUtilPromptAdvDup: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - # "mode": (["alline", "in-each-line"],), - } - } - - RETURN_TYPES = ("STRING","STRING",) - RETURN_NAMES = ('STRING',"STRING",) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text): - a='' - res=[] - textl=text.split("\n") - for line in textl: - res.append(tags_dup(line)) - a='\n'.join(res) - - tags_dup(tags_onelineify(text)) - return list_2_tupe([tags_dup(tags_onelineify(a)),a]) - - -class TextUtilPromptShuffle: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - "keep_n_token": ("STRING", {"default": "0", "multiline": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text='',keep_n_token='0',seed=0): - random.seed(seed) - textl=tags_listify(tags_onelineify(text)) - el=list_shuffle(textl,int(keep_n_token)) - return list_2_tupe([','.join(el)]) - -class TextUtilPromptDel: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "search": ("STRING",{"forceInput":False}), - "todel": ("STRING",{"forceInput":False}), - "action": (["yes",'no'],), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, search='',todel='',action='yes'): - # https://www.geeksforgeeks.org/python-remove-empty-strings-from-list-of-strings/ - if 'no' in action: - return list_2_tupe([search]) - ca=tags_ignore(search,tags_unweight(todel)) - return list_2_tupe([ca]) - -class TextUtilPromptStd: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text): - return list_2_tupe([tags_std(text)]) - -class TextUtilPromptUnweight: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "text": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, text): - return list_2_tupe([tags_unweight(text)]) - -class TextUtilPromptJoin: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "a": ("STRING",{"forceInput":False}), - "b": ("STRING",{"forceInput":False}), - "c": ("STRING",{"forceInput":False}), - "d": ("STRING",{"forceInput":False}), - "e": ("STRING",{"forceInput":False}), - "f": ("STRING",{"forceInput":False}), - "g": ("STRING",{"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/text-util" - OUTPUT_NODE = True - - def exec(self, a,b,c,d,e,f,g): - vl=[a,b,c,d,e,f,g] - vs = (",").join(vl) - vs=tags_std(vs) - # vs = vs.replace(" ", " ").replace(" ,", ",").replace(", ", ",").replace(",,", ",").replace(",,", ",") - return {"ui": {"text": (vs,)}, "result": (vs,)} - -# class TextUtilTextShow: -# @classmethod -# def INPUT_TYPES(s): -# return {"required": { -# "text": ("STRING", {"forceInput": True}), -# }} - -# # INPUT_IS_LIST = True -# RETURN_TYPES = ("STRING",) -# FUNCTION = "notify" -# OUTPUT_NODE = True -# # OUTPUT_IS_LIST = (True,) - -# CATEGORY = "Ymc Suite/text-util" - -# def notify(self, text): -# return {"ui": {"text": text}, "result": (text,)} - -class NumberUtilRandomNum: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number_type": (["integer", "float", "bool"],), - "minimum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}), - "maximum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - - RETURN_TYPES = ("NUMBER", "FLOAT", "INT") - FUNCTION = "exec" - - CATEGORY = "WAS Suite/Number" - - def exec(self, minimum, maximum, seed, number_type='integer'): - - # Set Generator Seed - random.seed(seed) - - # Return random number - if number_type: - if number_type == 'integer': - number = random.randint(minimum, maximum) - elif number_type == 'float': - number = random.uniform(minimum, maximum) - elif number_type == 'bool': - number = random.random() - else: - return - - # Return number - return (number, float(number), int(number)) - - @classmethod - def IS_CHANGED(cls, seed, **kwargs): - m = hashlib.sha256() - m.update(seed) - return m.digest().hex() - -def io_file_list(dir='',pattern='*.txt'): - res=[] - for filename in glob.glob(os.path.join(dir,pattern)): - res.append(filename) - return res -def io_file_get_txt(loc): - res='' - with open(loc) as f: - res = f.read() - f.close() - return res - -class IoUtilFileListGet: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "path": ("STRING", {"default": "", "multiline": False}), - "pattern": ("STRING", {"default": "*.txt", "multiline": False}), - # "mode": ("STRING", {"default": "filename", "multiline": False}), - "mode": (["filename", "abspath"],), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/IO" - OUTPUT_NODE = True - - def exec(self, path='',pattern='*.txt',mode='filename'): - text=io_file_list(path,pattern) - if mode=='filename': - text=list(map(os.path.basename,text)) - text='\n'.join(text) - return list_2_tupe([text]) - -class IoUtilFileListGetText: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "path": ("STRING", {"default": "", "multiline": False}), - "pattern": ("STRING", {"default": "*.txt", "multiline": False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ('STRING',) - FUNCTION = "exec" - CATEGORY = "Ymc Suite/IO" - OUTPUT_NODE = True - - def exec(self, path='',pattern='*.txt'): - plist=io_file_list(path,pattern) - text=[] - for loc in plist: - text.append(io_file_get_txt(loc)) - # to multi line - text='\n'.join(text) - return list_2_tupe([text]) - - -"""pipe-util""" -class PipeUtilToBasicPipe: - @classmethod - def INPUT_TYPES(s): - return {"required": { - "model": ("MODEL",), - "clip": ("CLIP",), - "vae": ("VAE",), - "positive": ("CONDITIONING",), - "negative": ("CONDITIONING",), - }, - } - - RETURN_TYPES = ("BASIC_PIPE", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING") - RETURN_NAMES = ("basic_pipe", "model", "clip", "vae", "positive", "negative") - FUNCTION = "doit" - - CATEGORY = "Ymc Suite/pipe-util" - - def doit(self, model, clip, vae, positive, negative): - basic_pipe = (model, clip, vae, positive, negative) - return basic_pipe,model, clip, vae, positive, negative - -"""img-util""" -class ImgUtilGetImageSize: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - } - } - - RETURN_TYPES = ("INT", "INT") - RETURN_NAMES = ("width", "height") - FUNCTION = "get_size" - CATEGORY = "Ymc Suite/img-util" - - def get_size(self, image): - _, height, width, _ = image.shape - return (width, height) - -class ImgUtilSwitchInputImage: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image_a": ("IMAGE",), - "image_b": ("IMAGE",), - "boolean_number": ("NUMBER",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_input_switch" - CATEGORY = "Ymc Suite/img-util" - - def image_input_switch(self, image_a, image_b, boolean_number=1): - - if int(round(boolean_number)) == 1: - return (image_a, ) - else: - return (image_b, ) - -"""conditioning-util""" -class ConditioningUtilInputSwitch: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "conditioning_a": ("CONDITIONING",), - "conditioning_b": ("CONDITIONING",), - "boolean_number": ("NUMBER",), - } - } - - RETURN_TYPES = ("CONDITIONING",) - FUNCTION = "conditioning_input_switch" - - CATEGORY = "Ymc Suite/conditioning-util" - - def conditioning_input_switch(self, conditioning_a, conditioning_b, boolean_number=1): - - if int(round(boolean_number)) == 1: - return (conditioning_a, ) - else: - return (conditioning_b, ) - -"""xyz-util""" -re_int = re.compile(r"\s*([+-]?\s*\d+)\s*") -re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") -re_range2 = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[([+-]\d+)\s*\])?\s*") - -re_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*") -re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") -re_range_float2 = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[([+-]\d+)\s*\])?\s*") - -def xyz_generate_floats(batch_count, first_float, last_float): - if batch_count > 1: - interval = (last_float - first_float) / (batch_count - 1) - return [round(first_float + i * interval, 3) for i in range(batch_count)] - else: - return [first_float] if batch_count == 1 else [] - -def xyz_generate_ints(batch_count, first_int, last_int): - if batch_count > 1: - interval = (last_int - first_int) / (batch_count - 1) - values = [int(first_int + i * interval) for i in range(batch_count)] - else: - values = [first_int] if batch_count == 1 else [] - values = list(set(values)) # Remove duplicates - values.sort() # Sort in ascending order - return values - -def xyz_gen_frange(start, end, step): - x = float(start) - end = float(end+0.1) - step = float(step) - while x < end: - yield x - x += step - -def xyz_parse_int(input: str): - m = re_int.fullmatch(input) - if m is not None: - return int(m.group(1)) - - m = re_range.fullmatch(input) - if m is not None: - start, end, step = m.group(1), m.group(2), m.group(3) - if step is None: - step = 1 - return list(range(int(start), int(end) + 1, int(step))) - - m = re_range2.fullmatch(input) - if m is not None: - start, end, count = m.group(1), m.group(2), m.group(3) - if count is None: - count = 1 - return xyz_generate_ints(int(count),int(start),int(end)) - if m is None: - raise ValueError(f'failed to process: {input}') - -def xyz_parse_float(input: str): - m = re_float.fullmatch(input) - if m is not None: - return float(m.group(1)) - - m = re_range_float.fullmatch(input) - if m is not None: - start, end, step = m.group(1), m.group(2), m.group(3) - if step is None: - step = 1.0 - return list(xyz_gen_frange(float(start), float(end), float(step))) - - m = re_range_float2.fullmatch(input) - if m is not None: - start, end, count = m.group(1), m.group(2), m.group(3) - if count is None: - count = 1 - return xyz_generate_floats(int(count),float(start),float(end)) - - if m is None: - raise ValueError(f'failed to process: {input}') - - -def xyz_parse(input: str, cont: Union[Callable[[str],Any],None]): - vs = [ x.strip() for x in input.split(',') ] - if cont is not None: - new_vs = [] - for v in vs: - new_v = cont(v) - if isinstance(new_v, list): - new_vs += new_v - else: - new_vs.append(new_v) - vs = new_vs - return vs - -def xyz_float2str(v,): - return '{:.2f}'.format(v) - -def xyz_get_n_float(s,n=0): - fstr=str(s) - a,b,c=fstr.partition('.') - if n==0: - return ''.join([a]) - c=(c+"0"*n)[:n] - return ".".join([a,c]) - -class XyzUtilKVTxtToOther: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "v": ("STRING", {"default": "", "multiline": False}), - # "name": ("STRING", {"default": "steps", "multiline": False}), - "k": ("STRING", {"default": "Steps", "multiline": False}), - "namepreset": (["none","no-shortname","empty-shortname","Steps", "CFG Scale","Denoise"],), - "shortname": ("STRING", {"default": "", "multiline": False}), - # "keepndecimal": ("STRING", {"default": "0", "multiline": False}), - "keepndecimal": ("INT", {"default": 1, "min": 0, "max": 8}), - } - } - - # RETURN_TYPES = ("INT","INT","INT","STRING","XY") - # RETURN_NAMES = ('s','e','count','gridAnnotion','X or Y') - RETURN_TYPES = ("INT","STRING","XY") - RETURN_NAMES = ('count','row of col text','X or Y') - FUNCTION = "exec" - CATEGORY = "Ymc Suite/xyz-util" - OUTPUT_NODE = True - - def exec(self, v='',k='Steps',namepreset='none',shortname='',keepndecimal=1): - intkl=['Steps','Clip Skip','custom'] - floatkl=['CFG Scale','Denoise',"custom-float"] - # use name preset - name=namepreset if not (namepreset in ['none','no-shortname',"empty-shortname"]) else k - # name="Steps" if name=='' else name - - - txtl=[] - il=[] - tl=[] - if name in intkl: - tl=xyz_parse(v,xyz_parse_int) - txtl=list(map(str,tl)) - il=list(map(int,tl)) - - if name in floatkl: - tl=xyz_parse(v,xyz_parse_float) - txtl=list(map(str,tl)) - # - # if keepndecimal!='': - # dc=int(keepndecimal) - - if keepndecimal>0: - for index,value in enumerate(txtl): - txtl[index]=xyz_get_n_float(value,keepndecimal) - il=list(map(float,txtl)) - - print(tl) - print(il) - print(txtl) - - # gen x or y key for image grid - kname=name - if shortname !="" and not(namepreset in ['none','no-shortname','empty-shortname']): - kname=shortname - if (namepreset in ["empty-shortname"]): - kname="" - if kname !="": - kname=kname+':' - rowOfColTxt=''+kname+';'.join(txtl) - # s=il[0] - # e=il[-1] - count=len(il) - - dset=(name, il) - # return list_2_tupe([s,e,count,gridAnnotion,dset]) - return list_2_tupe([count,rowOfColTxt,dset]) - -# class XyzUtilTxtToFloat: -# @classmethod -# def INPUT_TYPES(s): -# return { -# "required": { -# "text": ("STRING", {"default": "", "multiline": False}), -# "name": ("STRING", {"default": "steps", "multiline": False}), -# } -# } - -# RETURN_TYPES = ("FLOAT","FLOAT","FLOAT","STRING","DICT") -# RETURN_NAMES = ('s','e','count','gridAnnotion','DICT') -# FUNCTION = "exec" -# CATEGORY = "Ymc Suite/xyz-util" -# OUTPUT_NODE = True - -# def exec(self, text='',name='steps'): -# tl=xyz_parse(text,xyz_parse_float) -# il=list(map(float,tl.copy())) -# # tl=list(map(str,tl)) -# gridAnnotion=name+':'+';'.join(tl) -# s=il[0] -# e=il[-1] -# count=len(il) -# dset=(name, il) -# return list_2_tupe([s,e,count,gridAnnotion,dset]) - -# feat(core): rename 'Save Text File' to io-text-save for issue 3 - # https://github.com/YMC-GitHub/ymc-node-suite-comfyui/issues/3 - -# NODE MAPPING -NODE_CLASS_MAPPINGS = { - "io-text-save": WAS_Text_Save, - # "io-image-save": WAS_Image_Save, - "cutoff-region-util": CutoffRegionUtil, - "pipe-util-to-basic-pipe": PipeUtilToBasicPipe, - "canvas-util-cal-size": CanvasUtilCalSize, - # "img-util-get-image-size": ImgUtilGetImageSize, - # "img-util-switch-input-image": ImgUtilSwitchInputImage, - # "conditioning-util-input-switch": ConditioningUtilInputSwitch, - "hks-util-cal-denoise-step": HksUtilCalDenoiseStep, - "io-util-file-list-get": IoUtilFileListGet, - "io-util-file-list-get-text": IoUtilFileListGetText, - "text-preset-colors": CutoffRegionUtilColorTextList, - "text-util-prompt-join": TextUtilPromptJoin, - "text-util-prompt-del":TextUtilPromptDel, - "text-util-prompt-std":TextUtilPromptStd, - "text-util-prompt-unweight":TextUtilPromptUnweight, - "text-util-prompt-dup":TextUtilPromptDup, - "text-util-prompt-adv-dup":TextUtilPromptAdvDup, - "text-util-prompt-shuffle":TextUtilPromptShuffle, - "text-util-prompt-search":TextUtilPromptSearch, - "text-util-prompt-adv-search":TextUtilPromptAdvSearch, - "text-util-prompt-add-prompt":TextUtilPromptAdd, - "text-util-path-list": TextUtilPathList, - "text-util-join-text": TextUtilJoinText, - "text-util-search-text": TextUtilSearchText, - "text-util-random-text": TextUtilRandomText, - "text-util-loop-text": TextUtilLoopText, - "text-util-switch-text":TextUtilSwitchText, - # "text-util-show-text": TextUtilTextShow, - "xyz-util-txt-to-int": XyzUtilKVTxtToOther, - "number-util-random-num": NumberUtilRandomNum, - "region-util-get-pad-out-location-by-size": RegionUtilGetPadOutLocationBySize, - "region-util-get-crop-location-from-center-size-text": RegionUtilGetCropLocationFromCenterSizeText, - "region-util-get-by-center-and-size": RegionUtilGetCropLocationByCenter, - "region-util-get-by-lt": RegionUtilGetCropLocationByLT -} -NODE_DISPLAY_NAME_MAPPINGS = { - "io-text-save": 'save text file', - # "io-image-save": WAS_Image_Save, - "pipe-util-to-basic-pipe": 'to basic pipe', - "canvas-util-cal-size": 'cal size', - # "img-util-get-image-size": 'get image size', - # "img-util-switch-input-image": 'switch input img', - # "conditioning-util-input-switch": 'switch input conditioning', - "hks-util-cal-denoise-step": 'cal denoise step', - "cutoff-region-util": 'gen region text', - "io-util-file-list-get-text": 'get multi line text of file list', - "io-util-file-list-get": 'get file list', - "text-preset-colors": 'base colors', - "text-util-prompt-join": 'join prompt', - "text-util-prompt-del": 'del prompt', - "text-util-prompt-std": 'std prompt', - "text-util-prompt-unweight": 'unweight prompt', - "text-util-prompt-dup": 'dup prompt', - "text-util-prompt-adv-dup":'dup prompt (adv)', - "text-util-prompt-shuffle":'shuffle prompt', - "text-util-prompt-search":'search propmt', - "text-util-prompt-adv-search":'search prompt (adv)', - "text-util-prompt-add-prompt":'add prompt', - "text-util-path-list": 'path preset list', - "text-util-join-text": 'join text', - "text-util-search-text": 'search text and replace', - "text-util-random-text": 'get random text', - "text-util-loop-text": 'get random or loop text', - "text-util-switch-text":'switch text', - # "text-util-show-text": 'show text', - "xyz-util-txt-to-int": 'txt to int', - "number-util-random-num": 'random number', - "region-util-get-pad-out-location-by-size": 'get pad out location by size', - "region-util-get-crop-location-from-center-size-text": 'get region by center and size (from text)', - "region-util-get-by-center-and-size": 'get region by center and size', - "region-util-get-by-lt": 'get region by left top' -} diff --git a/requirements.txt b/requirements.txt index bffd8c7..2acd51a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,4 @@ yors_comfyui_node_setup -cmake -fairscale>=0.4.4 -git+https://github.com/WASasquatch/img2texture.git -gitpython -imageio -joblib -matplotlib -numba -numpy<1.24>=1.18 -opencv-python-headless[ffmpeg]<=4.7.0.72 -pilgram -git+https://github.com/WASasquatch/ffmpy.git -rembg -scikit-image==0.20.0 -scikit-learn -scipy -timm>=0.4.12 -tqdm -transformers==4.26.1 \ No newline at end of file +yors_pano_ansi_color +yors_pano_zero_field +yors_pano_list_util \ No newline at end of file