submit project with version 0.0.1

This commit is contained in:
aicoder-max
2025-06-05 16:13:37 +08:00
parent 5be3d9fc00
commit 04b0f03150
41 changed files with 2553 additions and 66 deletions
+8
View File
@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.11 (Pillar_For_ComfyUI)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>
+51
View File
@@ -0,0 +1,51 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="72" name="Python" />
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyArgumentListInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyPep8Inspection" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E302" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="ConnectionError" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PySimplifyBooleanCheckInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyTypeCheckerInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignoredIdentifiers">
<list>
<option value="folder_paths" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (Pillar_For_ComfyUI)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Pillar_For_ComfyUI)" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Pillar_For_ComfyUI.iml" filepath="$PROJECT_DIR$/.idea/Pillar_For_ComfyUI.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+78 -65
View File
@@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@@ -7,15 +7,17 @@
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -60,7 +72,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+72 -1
View File
@@ -1 +1,72 @@
init
## Pillar for ComfyUI
Pillar 是一款 ComfyUI 扩展插件,为 ComfyUI 提供调用本地分布式服务的能力。目前集成[llama-joycaption-beta-one-hf-llava](https://huggingface.co/fancyfeast/llama-joycaption-beta-one-hf-llava)模型,提供分布式部署方案,解决模型占用过多资源,影响生图效率的问题。
---
## Pillar 提供以下节点
![Nodes.png](images/Nodes.png)
1. **预览文本节点**
展示文本信息。
2. **翻译节点**
中文和英文两种语言互译,自动检测输入语言类型,当输入语言为中文时,则翻译为英文,反之,则翻译为中文。
* **服务器/本地**: 本地:当前物理机调用模型。远程:通过Http协议调用模型分布式服务。
3. **图片描述节点**
根据输入提示词选项,输出图片描述。
* **服务器/本地**: 本地:当前物理机调用模型。远程:通过Http协议调用模型分布式服务。
* **服务器IP:端口**: 本地模式下该参数不起作用。远程模式下,该参数填写远程服务器IP:端口,填写格式为:192.168.1.100:8000
* **模型加载方式**:只对本地模式下有效。选项:最大节省 (4-bit)、平衡 (8-bit)、默认模式 ,内存占用分别约为:4.2G、8.5G、17G
* **描述类型**: 让模型按照选定类型输出图片描述。支持选项:详细描述、详细描述(随意)、直接描述、Stable Diffusion 提示、MidJourney 提示、Danbooru 标签列表、e621 标签列表、Rule34 标签列表、Booru-like 标签列表、艺术评论家、产品列表、社交媒体帖子
* **描述长度**: 限制模型输出长度。支持选项:任意、非常短、短、中等长度、长、非常长、指定token长度(20、30、...)
* **附加选项1**: 进步提示模型应该如何生成图片描述,支持选项:如果图片中有人物 / 角色,你必须用 {name} 来称呼他们。、不要包含无法改变的信息(如种族、性别等),但仍应包含可改变的属性(如发型)。、包含关于照明信息。略...
* **附加选项2**: 同上。
* **附加选项3**: 同上。
* **人名**: 与附加选项中:如果图片中有人物 / 角色,你必须用 {name} 来称呼他们。配合使用,这里的人名将替换前面的{name}。
* **最大token数**: 限制模型计算规模,token越大,模型计算越耗时越长。
* **温度**: 调整生成文本的随机性和创造性,取值范围:通常为 0.0~2.0,默认值约 0.7。值越小(接近 0):输出更确定性、聚焦,倾向于选择概率最高的词,生成内容更保守、准确,但可能更刻板。
值越大(如 1.0 以上):输出更随机、多样,允许模型探索低概率词,生成内容更有创造性,但可能更偏离主题或出现逻辑错误。应用场景: 需精确答案时(如数学计算、事实陈述):用低温(0.2~0.5)。 需创意内容时(如故事写作、诗歌生成):用高温(0.7~1.0)。
* **系数P**: Top-P Sampling(Nucleus Sampling,核采样)作用:动态选择候选词,使累积概率超过阈值 P(如 0.9)的词进入候选集。取值:P 为概率值(如 P=0.9)。 P 越小:候选词越少,生成越确定性。 P 越大:候选词越多,接近随机采样。优势:自适应调整候选词数量,避免高质量但低概率的词被完全排除(对比 Top-K)。
应用场景: 平衡多样性与合理性:常用 P=0.8~0.95。
* **系数K**: Top-K Sampling(Top-K 采样)作用:限制模型在生成下一个词时的候选词范围,只从概率最高的 K 个词中选择。取值:K 为正整数(如 K=40)。 K 越小:候选词越少,生成越聚焦,但可能导致重复或刻板表达。 K 越大:候选词越多,生成更灵活,但可能引入无关词汇。应用场景:
防止模型生成低质量词汇:设置适当的 K(如 50~100)。 需严格控制内容时:用较小的 K(如 20~30)。
4. **图片描述(自定义)**
自定义提示词,更灵活,对图片描述节点的扩充。支持中文、英文两种提示,输出图片描述。
* **服务器/本地**: 本地:当前物理机调用模型。远程:通过Http协议调用模型分布式服务。
* **模型加载方式**:只对本地模式下有效。选项:最大节省 (4-bit)、平衡 (8-bit)、默认模式 ,内存占用分别约为:4.2G、8.5G、17G
* **系统提示词**:自定义系统提示词,告诉模型它的角色。
* **用户提示词**:用户提示词,告诉模型应该如何对图片进行描述。
* **最大token数**: 同图片描述节点,详情参见图片描述节点。
* **温度**: 同图片描述节点,详情参见图片描述节点。
* **系数P**: 同图片描述节点,详情参见图片描述节点。
* **系数K**: 同图片描述节点,详情参见图片描述节点。
---
## **示例工作流**
这个工作流展示了如何使用 Pillar 的中的所有节点。首先加载一张图片,然后使用图片描述节点(自定义),生成图片中文、英文描述。然后使用翻译节点,将图片中文描述翻译为英文,英文描述翻译为中文。最后,使用预览文本节点,展示翻译后内容。适用场景:图生图,提取图片中主要特征,生成图片描述,对描述进行局部调整,再二次渲染生图。
![Pillar_Example_Wrokflow.png](images/Pillar_Example_Wrokflow.png)
---
## 如何安装
### **推荐方式**
* 通过 [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager).
### **手动安装**
* 在终端(cmd) 导航至`ComfyUI/custom_nodes` 目录.
* 在 `custom_nodes` 目录下使用以下命令克隆仓库:
```
git clone https://github.com/aicoder-max/Pillar_For_ComfyUI
cd Pillar_For_ComfyUI
```
* 在 Python 环境中安装依赖项:.
* Windows Portable 在 `ComfyUI\custom_nodes\Pillar_For_ComfyUI`目录内运行以下命令:
```
..\..\..\python_embeded\python.exe -m pip install -r requirements.txt
```
* 若使用虚拟环境(venv)或 Conda:先激活 Python 环境,再运行:
```
pip install -r requirements.txt
```
## Piller Service GitHub
[GitHub: ](https://github.com/aicoder-max/Pillar_Service)https://github.com/aicoder-max/Pillar_Service
+21
View File
@@ -0,0 +1,21 @@
"""
▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄
▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀█░▌ ▀▀▀▀█░█▀▀▀▀ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌
▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌
▐░█▄▄▄▄▄▄▄█░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄█░▌
▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀█░█▀▀
▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌
▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌▐░▌ ▐░▌
▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░▌
▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀
### Pillar Extention For ComfyUI
### Brought to you by aicoder-max
### https://github.com/aicoder-max/Pillar_For_ComfyUI/
"""
from .pillar_plus import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, WEB_DIRECTORY
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
+243
View File
@@ -0,0 +1,243 @@
"""
Base client class for the ComfyUI Extension Service.
This class provides the foundation for service-specific clients
with common functionality for API communication.
"""
import json
import logging
import socket
import uuid
from enum import Enum
from typing import Dict, Any
import requests
from .exceptions import APIError, RateLimitError, ServiceUnavailableError, ValidationError
logger = logging.getLogger(__name__)
class HttpMethod(str, Enum):
"""HTTP request methods."""
GET = "GET"
POST = "POST"
class Endpoint(str, Enum):
"""Common API endpoints."""
HEALTH_CHECK = "health/direct"
CLEAR_CACHE = "admin/clear-cache"
MEMORY_CLEANUP = "admin/cleanup-memory"
class AuthenticationError:
pass
class BaseClient:
"""
Base client class for the ComfyUI Extension Service.
This class provides the foundation for service-specific clients
with common functionality for API communication.
"""
DEFAULT_TIMEOUT = 60
DEFAULT_USERNAME = "anonymous"
DEFAULT_IP = "127.0.0.1"
DEFAULT_HOSTNAME = "localhost"
CONTENT_TYPE_JSON = "application/json"
# Map HTTP status codes to exception classes
ERROR_STATUS_MAP = {
401: AuthenticationError,
422: ValidationError,
429: RateLimitError,
}
def __init__(
self,
username: str = None,
timeout: int = DEFAULT_TIMEOUT,
headers: Dict[str, str] = None,
):
"""
Initialize the base client.
Args:
username: The username to use for API calls
timeout: Request timeout in seconds
headers: Additional headers to include in requests
"""
self.username = username or self.DEFAULT_USERNAME
self.timeout = timeout
self.headers = headers or {}
self.headers.update({"Content-Type": self.CONTENT_TYPE_JSON})
# Get client IP address
self._setup_client_info()
def _ensure_url_prefix(self,url: str) -> str:
if not url:
return url
# Remove leading/trailing whitespace
url = url.strip()
# If URL doesn't start with http:// or https://, add http://
if not url.startswith("http://") and not url.startswith("https://"):
url = "http://" + url
# Convert https:// to http:// for consistency
if url.startswith("https://"):
url = "http://" + url[8:]
return url
def _setup_client_info(self) -> None:
"""Set up client hostname and IP address information."""
try:
self.hostname = socket.gethostname()
self.ip_address = socket.gethostbyname(self.hostname)
except Exception as e:
logger.warning(f"Failed to get client IP address: {e}")
self.ip_address = self.DEFAULT_IP
self.hostname = self.DEFAULT_HOSTNAME
@staticmethod
def _build_url(base_url: str, endpoint: str) -> str:
"""
Build a complete URL from .e base URL and endpoint.
Args:
base_url: The base URL of the API server
endpoint: The API endpoint
Returns:
The complete URL
"""
return f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
def _prepare_request_data(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Prepare request data by adding required fields.
Args:
request_data: The request data
Returns:
The prepared request data
"""
# Create a new dictionary to avoid modifying the original
prepared_data = request_data.copy()
# Add required fields if not already present
if "req_id" not in prepared_data:
prepared_data["req_id"] = str(uuid.uuid4())
if "user_name" not in prepared_data:
prepared_data["user_name"] = self.username
if "ip_address" not in prepared_data:
prepared_data["ip_address"] = self.ip_address
return prepared_data
def _handle_error_status(self, response: requests.Response) -> None:
"""
Handle error status codes from the API.
Args:
response: The response object
Raises:
APIError: If the API returns an error
ValidationError: If the response cannot be parsed
"""
try:
data = response.json()
except json.JSONDecodeError:
raise ValidationError(f"Invalid JSON response: {response.text}")
# Check if the response is a BaseResponse
if isinstance(data, dict) and "success" in data:
if not data["success"]:
raise APIError(
status_code=response.status_code,
message=data.get("msg", "Unknown error"),
response=data,
)
# Check HTTP status
if response.status_code >= 400:
error_message = data.get("detail", "Unknown error")
# Use the error status map to get the appropriate exception class
exception_class = self.ERROR_STATUS_MAP.get(
response.status_code,
ServiceUnavailableError if response.status_code >= 500 else APIError
)
if exception_class == APIError:
raise exception_class(
status_code=response.status_code,
message=error_message,
response=data,
)
else:
raise exception_class(f"{exception_class.__name__}: {error_message}")
def _request(
self,
base_url: str,
method: HttpMethod,
endpoint: str,
data: Dict[str, Any] = None,
params: Dict[str, Any] = None,
files: Dict[str, Any] = None,
headers: Dict[str, str] = None,
) -> Dict[str, Any]:
logger.debug(f"base_url:{base_url}")
url = self._ensure_url_prefix(base_url)
logger.debug(f"_ensure_url_prefix:{url}")
url = self._build_url(url, endpoint)
logger.debug(f"_build_url:{url}")
request_headers = self.headers.copy()
if headers:
request_headers.update(headers)
# Prepare request data if it's a dict and not a file upload
if data and isinstance(data, dict) and not files:
data = self._prepare_request_data(data)
try:
kwargs = {
"method": method.value,
"url": url,
"params": params,
"headers": request_headers,
"timeout": self.timeout,
}
if files:
if "Content-Type" in request_headers:
del request_headers["Content-Type"]
kwargs["data"] = data
kwargs["files"] = files
else:
kwargs["json"] = data if data else None
response = requests.request(**kwargs)
# Log request details before sending
logger.info(f"Sending {method.value} request to {url}")
logger.debug(f"Request headers: {request_headers}")
if data and not files: # Only log data for non-file requests
logger.debug(f"Request data: {data}")
if files:
logger.debug(f"Files to upload: {list(files.keys())}")
# Log response details before returning
logger.info(f"Received response with status {response.status_code}")
logger.debug(f"Response headers: {response.headers}")
logger.debug(f"Response content: {response.text[:1000]}...") # Limit long responses
# Handle error status codes
self._handle_error_status(response)
return response.json()
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Connection error: {str(e)}")
+53
View File
@@ -0,0 +1,53 @@
from typing import Dict, Any
class ClientException(Exception):
"""
Base exception for all client exceptions.
All custom exceptions in the ComfyUI Extension Service client inherit from this class.
"""
pass
class APIError(ClientException):
"""
Exception raised when the API returns an error.
Attributes:
status_code (int): HTTP status code returned by the API
message (str): Error message from the API
response (Dict[str, Any]): Full response from the API
"""
def __init__(self, status_code: int = None, message: str = None, response: Dict[str, Any] = None):
self.status_code = status_code
self.message = message
self.response = response
super().__init__(f"API Error: {status_code} - {message}")
class ConnectionError(ClientException):
"""Exception raised when the client fails to connect to the server."""
pass
class AuthenticationError(ClientException):
"""Exception raised when authentication fails."""
pass
class ValidationError(ClientException):
"""Exception raised when request validation fails."""
pass
class RateLimitError(ClientException):
"""Exception raised when the client hits rate limits."""
pass
class ServiceUnavailableError(ClientException):
"""
Exception raised when a service is unavailable.
This indicates temporary server issues or maintenance.
"""
pass
+83
View File
@@ -0,0 +1,83 @@
from .base_client import logger, HttpMethod
from typing import Dict
from .base_client import BaseClient
from ..dto.joy_caption_dto import JoyCaptionRequest
from ..dto.translate_dto import TranslationRequest
class JoyCaptionServiceClient(BaseClient):
"""
Client for the JoyCaption service that generates captions for images.
Inherits common functionality from BaseClient.
"""
def generate_caption(self, base_url: str, request: JoyCaptionRequest) -> Dict[str, str]:
try:
data = {
"system_prompt": request.system_prompt,
"prompt": request.prompt,
"max_new_tokens": str(request.max_new_tokens),
"temperature": str(request.temperature),
"top_p": str(request.top_p),
"top_k": str(request.top_k),
"user_name": self.username # Use username from client
}
files = {
"image_file": ("image.jpg", request.image_file, "image/jpeg")
}
# Make request using base client's _request method
response = self._request(
base_url=base_url,
method=HttpMethod.POST,
endpoint="joycaption/generate",
data=data,
files=files
)
# Extract caption data
return {
"enCaption": response.get("enCaption", ""),
"cnCaption": response.get("cnCaption", "")
}
except Exception as e:
logger.error(f"Error in generate_caption: {str(e)}", exc_info=True)
# Re-raise the exception with original context
raise e from e
def translate(self, base_url: str, request: TranslationRequest) -> str:
"""
Translate text between languages using the translation service.
Args:
base_url: The base URL of the API server
request: The translation request containing text and parameters
Returns:
The translated text
Raises:
ConnectionError: If there's a network error communicating with the service
ValueError: If the response format is invalid
"""
# Build request data using fields from request object
request_data = {
"text": request.text,
}
# Make request using base client's _request method
response = self._request(
base_url=base_url,
method=HttpMethod.POST,
endpoint="translate",
data=request_data
)
# Parse response
if isinstance(response, dict) and "translated_text" in response:
return response["translated_text"]
else:
raise ValueError("Invalid response format: missing translated_text field")
View File
+41
View File
@@ -0,0 +1,41 @@
import uuid
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
class BaseRequest(BaseModel):
user_name: str = "anonymous"
ip_address: str = "anonymous"
req_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
class BaseResponse(BaseModel):
res_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
rel_req_id: str
res_time: str = Field(default_factory=lambda: datetime.now().isoformat())
success: bool = True
msg: str = "Request processed successfully"
# Cache management specific models
class CacheClearRequest(BaseRequest):
service: Optional[str] = None # If None, clear all caches
class CacheClearResponse(BaseResponse):
cleared_count: int = 0
cleared_services: List[str] = []
# Memory management specific models
class MemoryCleanupRequest(BaseRequest):
service: str # Required, must specify which service to clean up
class MemoryCleanupResponse(BaseResponse):
freed_memory: bool = False
cleanup_details: str = ""
+44
View File
@@ -0,0 +1,44 @@
from fastapi import UploadFile, File, Form
from ..dto.base_dto import BaseRequest, BaseResponse
from ..util.constants import DEFAULT_TEMPERATURE, DEFAULT_MAX_NEW_TOKENS, DEFAULT_TOP_P, \
DEFAULT_SYSTEM_PROMPT, DEFAULT_TOP_K
class JoyCaptionResponse(BaseResponse):
"""Response model for JoyCaption API"""
enCaption: str = ""
cnCaption: str = ""
class JoyCaptionRequest(BaseRequest):
"""Request model for generate_caption API with Form fields"""
image_file: bytes
system_prompt: str = DEFAULT_SYSTEM_PROMPT
prompt: str
max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS
temperature: float = DEFAULT_TEMPERATURE
top_p: float = DEFAULT_TOP_P
top_k: int = DEFAULT_TOP_K
@classmethod
def as_form(
cls,
image_file: bytes,
system_prompt: str = Form(DEFAULT_SYSTEM_PROMPT),
prompt: str = Form("Describe this image"),
max_new_tokens: int = Form(DEFAULT_MAX_NEW_TOKENS),
temperature: float = Form(DEFAULT_TEMPERATURE),
top_p: float = Form(DEFAULT_TOP_P),
top_k: int = Form(DEFAULT_TOP_K),
):
"""Factory method to create GenCaptionRequest from .rm fields"""
return cls(
image_file=image_file,
system_prompt=system_prompt,
prompt=prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
user_name="testUser"
)
+21
View File
@@ -0,0 +1,21 @@
import uuid
from pydantic import Field
from ..dto.base_dto import BaseRequest, BaseResponse
class TranslationRequest(BaseRequest):
"""Request model for translation API"""
text: str
user_name: str = "anonymous"
ip_address: str = "anonymous"
req_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
class TranslationResponse(BaseResponse):
"""Response model for translation API"""
translated_text: str = ""
original_text: str = ""
success: bool = True
req_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
execution_time: float = 0.0
rel_req_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

+82
View File
@@ -0,0 +1,82 @@
import {app} from "../../scripts/app.js";
import {ComfyWidgets} from "../../scripts/widgets.js";
app.registerExtension({
name: "Comfy.Pillar.TextMultLine",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "Pillar_TextMultLine") return;
const resize = function () {
// auto resize
const sz = this.computeSize();
this.size = [
Math.max(this.size[0], sz[0]),
Math.max(this.size[1], sz[1])
];
requestAnimationFrame(() => {
this.onResize?.(this.size);
app.graph.setDirtyCanvas(true, false);
});
};
const refresh = function (values) {
//console.info("node refresh: ", this.type, this.id, values);
if (values) {
const w = this?.widgets?.find(
(v) => v.type === "customtext" && v.name === "__preview"
);
if (w) {
let text = "";
if (typeof values === "string") text = values;
else if (Array.isArray(values)) text = values[0];
w.value = text;
app.graph.setDirtyCanvas(true, false);
}
}
// auto resize
resize.call(this);
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
// add preview widget
const previewer = ComfyWidgets.STRING(
this,
"__preview",
[
"STRING",
{
default: "",
placeholder: "预览文本...",
multiline: true,
},
],
app
);
previewer.widget.inputEl.readOnly = true;
app.graph.setDirtyCanvas(true, false);
resize.call(this);
onNodeCreated?.apply(this, arguments);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (w) {
onConfigure?.apply(this, arguments);
if (w?.widgets_values?.length > 0) refresh.call(this, w.widgets_values);
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (output) {
onExecuted?.apply(this, arguments);
refresh.call(this, output?.string);
};
},
});
+147
View File
@@ -0,0 +1,147 @@
{
"Pillar_JoyCaption": {
"display_name": "图片描述",
"inputs": {
"exec_opt": {
"name": "服务器/本地"
},
"base_url": {
"name": "服务器IP:端口"
},
"image": {
"name": "图片"
},
"memory_mode": {
"name": "模型加载方式"
},
"caption_type": {
"name": "描述类型"
},
"caption_length": {
"name": "描述长度"
},
"extra_option1": {
"name": "附加选项1"
},
"extra_option2": {
"name": "附加选项2"
},
"extra_option3": {
"name": "附加选项3"
},
"person_name": {
"name": "人名"
},
"max_new_tokens": {
"name": "最大token数"
},
"temperature": {
"name": "温度"
},
"top_p": {
"name": "系数p"
},
"top_k": {
"name": "系数k"
}
},
"outputs": {
"0": {
"name": "模型输入提示词"
},
"1": {
"name": "英文描述"
},
"2": {
"name": "中文描述"
}
}
},
"Pillar_JoyCaptionCustom": {
"display_name": "图片描述(自定义)",
"inputs": {
"exec_opt": {
"name": "服务器/本地"
},
"base_url": {
"name": "服务器IP:端口"
},
"image": {
"name": "图片"
},
"memory_mode": {
"name": "模型加载方式"
},
"system_prompt": {
"name": "系统提示"
},
"user_query": {
"name": "自定义提示词"
},
"max_new_tokens": {
"name": "最大token数"
},
"temperature": {
"name": "温度"
},
"top_p": {
"name": "系数p"
},
"top_k": {
"name": "系数k"
}
},
"outputs": {
"0": {
"name": "模型输入提示词"
},
"1": {
"name": "英文描述"
},
"2": {
"name": "中文描述"
}
}
},
"Pillar_Translation": {
"display_name": "翻译",
"inputs": {
"exec_opt": {
"name": "服务器/本地"
},
"base_url": {
"name": "服务器IP:端口"
},
"source": {
"name": "源语言"
},
"target": {
"name": "目标语言"
},
"text": {
"name": "源语言文本"
}
},
"outputs": {
"0": {
"name": "目标语言"
}
}
},
"Pillar_TextMultLine": {
"display_name": "预览文本",
"inputs": {
"text": {
"name": "输入文本"
}
},
"outputs": {
"0": {
"name": "输出文本"
},
"1": {
"name": "唯一编号"
}
}
}
}
View File
+65
View File
@@ -0,0 +1,65 @@
import re
import folder_paths
from comfy.comfy_types import ComfyNodeABC
from pathlib import Path
from typing import Dict, Any, Tuple, ClassVar
from ..util.pyproject import CATEGORY_NAME
from ..util import log
class ExtensionNode(ComfyNodeABC):
RETURN_TYPES: ClassVar[Tuple[str, ...]] = ()
RETURN_NAMES: ClassVar[Tuple[str, ...]] = ()
FUNCTION: ClassVar[str] = ""
CATEGORY: ClassVar[str] = CATEGORY_NAME
OUTPUT_NODE = True
DESCRIPTION: ClassVar[str] = ""
_log = None
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {"required": {}, "optional": {}}
def __init__(self):
self._log = log
@classmethod
def get_node_name(cls) -> str:
return f"{cls.CATEGORY}_{cls.__name__}"
@classmethod
def get_dispay_name(cls) -> str:
return f"Pillar{cls.__name__}"
def _download_model_from_hf(self, repo_id: str, folder_name: str, force_download: bool = False,
local_files_only: bool = False) -> Path:
try:
model_save_path = Path(folder_paths.models_dir) / folder_name / Path(repo_id).stem
if not model_save_path.exists() or force_download:
try:
from huggingface_hub import snapshot_download
log.log_node_info(self.get_node_name(), f"Downloading model from {repo_id} to {model_save_path}...")
snapshot_download(
repo_id=repo_id,
local_dir=str(model_save_path),
force_download=force_download,
local_files_only=local_files_only
)
self._log.log_node_info(self.get_node_name(),
f"Model successfully downloaded to {model_save_path}.")
except FileNotFoundError:
error_msg = f"File not found during download of {repo_id}."
self._log.log_node_warn(self.get_node_name(), error_msg)
raise RuntimeError(error_msg)
except PermissionError:
error_msg = f"Permission denied when trying to download {repo_id} to {model_save_path}."
self._log.log_node_warn(self.get_node_name(), error_msg)
raise RuntimeError(error_msg)
except Exception as e:
error_msg = f"Unexpected error downloading model {repo_id}: {str(e)}"
self._log.log_node_warn(self.get_node_name(), error_msg)
raise RuntimeError(error_msg)
return model_save_path
except Exception as e:
self._log.log_node_warn(self.get_node_name(), f"Unexpected error processing model {repo_id}: {str(e)}")
raise RuntimeError(f"Failed to process model {repo_id}: {str(e)}")
+268
View File
@@ -0,0 +1,268 @@
import io
from typing import Any, Dict
from torchvision.utils import save_image
from PIL import Image
from .extension_node import ExtensionNode
from ..client.joy_caption_service_client import JoyCaptionServiceClient
from ..util.constants import CAPTION_LENGTH_CHOICES, CAPTION_TYPE, DEFAULT_BASE_URL, DEFAULT_MAX_NEW_TOKENS, \
DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, DEFAULT_TOP_K, DEFAULT_TOP_P, EXEC_OPTIONS, EXTRA_OPTIONS, \
MEMORY_MODE, MIN_TEMPERATURE, MIN_TOKENS, MIN_TOP_K, MIN_TOP_P, \
TEMPERATURE_STEP, TOP_P_STEP, MAX_TOKENS, MAX_TEMPERATURE, MAX_TOP_P, MAX_TOP_K
def build_prompt(caption_type: str, caption_length: str | int, extra_options: list[str], name_input: str) -> tuple[
str, str]:
caption_type_code = CAPTION_TYPE.get_by_label(caption_type)
caption_length_code = CAPTION_LENGTH_CHOICES.get_by_label(caption_length)
caption_templates = CAPTION_TYPE.get_by_code(caption_type_code)
code = caption_length_code if caption_length_code else "any"
if code == "any":
map_idx = 0
elif isinstance(code, str) and code.isdigit():
map_idx = 1
else:
map_idx = 2
prompt_code = caption_templates[map_idx]
prompt_label = f"{caption_type}, {caption_length}"
# 添加额外选项
extra_options_codes = []
extra_options_labels = []
if extra_options:
for opt in extra_options:
if opt:
code = EXTRA_OPTIONS.get_by_label(opt)
if code:
extra_options_codes.append(code)
extra_options_labels.append(opt)
if extra_options_codes:
prompt_code += " " + " ".join(extra_options_codes)
if extra_options_labels:
prompt_label += "\n"
for option in extra_options_labels:
prompt_label += f"- {option}\n"
prompt_code = prompt_code.format(
name=name_input or "{NAME}",
length=caption_length,
word_count=caption_length,
)
if name_input:
prompt_label += f"- Name: {name_input}"
return prompt_code, prompt_label
# Shared client instance
_joy_caption_client = None
def _validate_image_tensor(image_tensor):
"""
Validate and prepare image tensor for processing.
Args:
image_tensor: Input image tensor to validate
Returns:
Validated tensor image
Raises:
ValueError: If the image tensor is invalid
"""
if image_tensor is None or not hasattr(image_tensor, "shape"):
raise ValueError("Invalid image tensor: missing shape attribute")
if len(image_tensor.shape) != 4:
raise ValueError(f"Expected 4D image tensor, got shape {image_tensor.shape}")
if image_tensor.shape[0] == 0:
raise ValueError("Empty image tensor")
return image_tensor[0].permute(2, 0, 1)
def tensor_to_bytes(image_tensor) -> bytes:
"""
Convert a PyTorch image tensor to JPEG bytes.
Args:
image_tensor: Input image tensor (batch_size, height, width, channels)
Returns:
Bytes of the converted image
Raises:
ValueError: If the image tensor is invalid
"""
# Validate input
tensor_image = _validate_image_tensor(image_tensor)
buffer = io.BytesIO()
save_image(tensor_image, buffer, "JPEG")
buffer.seek(0)
return buffer.getvalue()
def _get_joy_caption_client() -> JoyCaptionServiceClient:
global _joy_caption_client
if _joy_caption_client is None:
_joy_caption_client = JoyCaptionServiceClient()
return _joy_caption_client
from ..dto.joy_caption_dto import JoyCaptionRequest
def _process_remote_request(self,base_url: str, image: Any, system_prompt: str, prompt: str,
max_new_tokens: int, temperature: float, top_p: float,
top_k: int) -> Dict[str, str]:
if not base_url or base_url == DEFAULT_BASE_URL:
error_msg = "Error: Please provide a valid base_url for remote execution"
return {
"enCaption": error_msg,
"cnCaption": error_msg
}
try:
client = _get_joy_caption_client()
request = JoyCaptionRequest.as_form(tensor_to_bytes(image),system_prompt,prompt,max_new_tokens,temperature,top_p,top_k)
return client.generate_caption(base_url=base_url, request=request)
except Exception as e:
import traceback
traceback.format_exc()
self._log.log_node_warn(self.get_node_name(),f"Error in remote caption generation: {str(e)}")
error_msg = f"Error generating caption: {str(e)}"
return {
"enCaption": error_msg,
"cnCaption": error_msg
}
from ..service.joy_caption_service import JoyCaptionService
def _process_local_request(self, image: Any, system_prompt: str, prompt: str, memory_mode: str,
max_new_tokens: int, temperature: float, top_p: float,
top_k: int):
try:
checkpoint_path = self._download_model_from_hf("fancyfeast/llama-joycaption-beta-one-hf-llava",
"LLavacheckpoints", False, False)
memory_mode_code = MEMORY_MODE.get_by_label(memory_mode)
service = JoyCaptionService(str(checkpoint_path), memory_mode_code)
image = Image.open(io.BytesIO(tensor_to_bytes(image)))
en_caption, cn_caption = service.generate(image, system_prompt, prompt, max_new_tokens, temperature, top_p,
top_k)
return en_caption, cn_caption
except Exception as e:
self._log.log_node_warn(self.get_node_name(),f"Error in local caption generation: {str(e)}")
error_msg = f"Error generating caption: {str(e)}"
return error_msg, error_msg
class JoyCaption(ExtensionNode):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"exec_opt": (EXEC_OPTIONS.labels(),),
"base_url": ("STRING", {"multiline": False, "default": DEFAULT_BASE_URL}),
"image": ("IMAGE",),
"memory_mode": (MEMORY_MODE.labels(),),
"caption_type": (CAPTION_TYPE.labels(),),
"caption_length": (CAPTION_LENGTH_CHOICES.labels(),),
"extra_option1": (EXTRA_OPTIONS.labels(),),
"extra_option2": (EXTRA_OPTIONS.labels(),),
"extra_option3": (EXTRA_OPTIONS.labels(),),
"person_name": ("STRING", {"default": "", "multiline": False,
"placeholder": "only needed if you use the 'If there is a person/character in the image you must refer to them as {name}.' extra option."}),
"max_new_tokens": ("INT", {"default": DEFAULT_MAX_NEW_TOKENS, "min": MIN_TOKENS, "max": MAX_TOKENS}),
"temperature": ("FLOAT",
{"default": DEFAULT_TEMPERATURE, "min": MIN_TEMPERATURE, "max": MAX_TEMPERATURE,
"step": TEMPERATURE_STEP}),
"top_p": ("FLOAT", {"default": DEFAULT_TOP_P, "min": MIN_TOP_P, "max": MAX_TOP_P, "step": TOP_P_STEP}),
"top_k": ("INT", {"default": DEFAULT_TOP_K, "min": MIN_TOP_K, "max": MAX_TOP_K}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("query", "en_caption", "cn_caption")
DESCRIPTION = "JoyCaption生成图片描述"
FUNCTION = "generate"
def generate(self, exec_opt, base_url, image, memory_mode, caption_type, caption_length, extra_option1,
extra_option2, extra_option3, person_name, max_new_tokens, temperature, top_p, top_k):
extras = [extra_option1, extra_option2, extra_option3]
extras = [extra for extra in extras if extra]
system_prompt = DEFAULT_SYSTEM_PROMPT
exec_mode = EXEC_OPTIONS.get_by_label(exec_opt)
prompt_code, prompt_label = build_prompt(caption_type, caption_length, extras, person_name)
if exec_mode == "remote":
caption_result = _process_remote_request(self,base_url, image, system_prompt, prompt_code, max_new_tokens,
temperature, top_p, top_k)
en_caption = caption_result.get("enCaption", "")
cn_caption = caption_result.get("cnCaption", "")
else:
en_caption, cn_caption = _process_local_request(self, image, system_prompt, prompt_code, memory_mode,
max_new_tokens, temperature, top_p, top_k)
return prompt_label, en_caption, cn_caption
class JoyCaptionCustom(ExtensionNode):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"exec_opt": (EXEC_OPTIONS.labels(),),
"base_url": ("STRING", {"multiline": False, "default": DEFAULT_BASE_URL}),
"image": ("IMAGE",),
"memory_mode": (MEMORY_MODE.labels(),),
"system_prompt": ("STRING", {"multiline": False,"default": DEFAULT_SYSTEM_PROMPT}),
"user_query": ("STRING", {"multiline": True, "default": "Write a detailed description for this image."}),
# generation params
"max_new_tokens": ("INT", {"default": DEFAULT_MAX_NEW_TOKENS, "min": MIN_TOKENS, "max": MAX_TOKENS}),
"temperature": ("FLOAT",
{"default": DEFAULT_TEMPERATURE, "min": MIN_TEMPERATURE, "max": MAX_TEMPERATURE,
"step": TEMPERATURE_STEP}),
"top_p": ("FLOAT", {"default": DEFAULT_TOP_P, "min": MIN_TOP_P, "max": MAX_TOP_P, "step": TOP_P_STEP}),
"top_k": ("INT", {"default": DEFAULT_TOP_K, "min": MIN_TOP_K, "max": MAX_TOP_K}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("query", "en_caption", "cn_caption")
DESCRIPTION = "JoyCaption生成图片描述,自定义提示词."
FUNCTION = "generate"
def generate(self, exec_opt, base_url, image, memory_mode, system_prompt, user_query, max_new_tokens, temperature,
top_p, top_k):
exec_mode = EXEC_OPTIONS.get_by_label(exec_opt)
if exec_mode == "remote":
caption_result = _process_remote_request(self,base_url, image, system_prompt, user_query, max_new_tokens,
temperature, top_p, top_k)
en_caption = caption_result.get("enCaption", "")
cn_caption = caption_result.get("cnCaption", "")
else:
en_caption, cn_caption = _process_local_request(self, image, system_prompt, user_query, memory_mode,
max_new_tokens, temperature, top_p, top_k)
return user_query, en_caption, cn_caption
+21
View File
@@ -0,0 +1,21 @@
from .extension_node import ExtensionNode
class TextMultLine(ExtensionNode):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"placeholder": "请输入文本...","multiline": False})
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
OUTPUT_NODE = True
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("text", "unique_id")
DESCRIPTION = "多行文本预览"
FUNCTION = "doit"
@staticmethod
def doit(text, prompt=None, extra_pnginfo=None, unique_id=None):
return {"ui": {"string": [text, unique_id, ]}, "result": (text, unique_id,)}
+67
View File
@@ -0,0 +1,67 @@
from .extension_node import ExtensionNode
from ..dto.translate_dto import TranslationRequest
from ..util.constants import DEFAULT_BASE_URL, EXEC_OPTIONS, MEMORY_MODE
DEFAULT_USER = "anonymous"
ERROR_INVALID_BASE_URL = "Error: Please provide a valid base_url for remote execution"
class Translation(ExtensionNode):
from typing import Tuple, Dict, Any
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"exec_opt": (EXEC_OPTIONS.labels(),),
"base_url": ("STRING", {"default": DEFAULT_BASE_URL, "multiline": False, "placeholder": ""}),
"text": ("STRING", {"multiline": True, "placeholder": "请输入要翻译的内容..."}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "translate_text"
DESCRIPTION = "JoyCaption模型翻译"
def _remote_translate(self, base_url: str, text: str) -> str:
if not base_url or base_url == DEFAULT_BASE_URL:
self._log.log_node_warn(self.get_node_name(),self.ERROR_INVALID_BASE_URL)
return text
from ..client.joy_caption_service_client import JoyCaptionServiceClient
client = JoyCaptionServiceClient()
request = TranslationRequest(
text=text,
)
return client.translate(base_url, request)
def _local_translate(self, text: str) -> str:
check_path = self._download_model_from_hf(
"fancyfeast/llama-joycaption-beta-one-hf-llava",
"LLavacheckpoints", False, False
)
from ..service.joy_caption_service import JoyCaptionService
# Use an instance variable to cache the service
if not hasattr(self, "_joy_caption_service") or self._joy_caption_service is None:
self._joy_caption_service = JoyCaptionService(str(check_path), "Maximum Savings (4-bit)")
service = self._joy_caption_service
return service.tranlation(text)
def translate_text(self, **kwargs) -> Tuple[str]:
exec_mode = EXEC_OPTIONS.get_by_label(kwargs["exec_opt"])
text = kwargs["text"]
try:
if exec_mode == "remote":
text_translated = self._remote_translate(kwargs["base_url"],text)
else:
text_translated = self._local_translate(text)
except Exception as e:
self._log.log_node_warn(self.get_node_name(),f"Translation error ({exec_mode}): {str(e)}")
text_translated = text
return (text_translated,)
+42
View File
@@ -0,0 +1,42 @@
import logging
from .util.log import log
from .util.pyproject import NAME
from .util.pyproject import VERSION
IS_COMFYUI_ENVIRONMENT = False
NODE_CLASS_MAPPINGS = dict()
NODE_DISPLAY_NAME_MAPPINGS = dict()
WEB_DIRECTORY = "./js"
try:
import folder_paths
IS_COMFYUI_ENVIRONMENT = True
logger = logging.getLogger(NAME)
except ImportError:
IS_COMFYUI_ENVIRONMENT = False
if IS_COMFYUI_ENVIRONMENT:
try:
from .nodes.text_mult_line import TextMultLine
from .nodes.translation import Translation
from .nodes.joy_caption import JoyCaption
from .nodes.joy_caption import JoyCaptionCustom
NODE_CLASS_MAPPINGS = {
TextMultLine.get_node_name(): TextMultLine,
Translation.get_node_name(): Translation,
JoyCaption.get_node_name(): JoyCaption,
JoyCaptionCustom.get_node_name(): JoyCaptionCustom,
}
NODE_DISPLAY_NAME_MAPPINGS = {
TextMultLine.get_node_name(): TextMultLine.get_dispay_name(),
Translation.get_node_name(): Translation.get_dispay_name(),
JoyCaption.get_node_name(): JoyCaption.get_dispay_name(),
JoyCaptionCustom.get_node_name(): JoyCaptionCustom.get_dispay_name(),
}
log(f"version:{VERSION} start successfully. load node count: {len(NODE_CLASS_MAPPINGS)}.🚀🚀🚀", "CYAN")
except Exception as e:
log(f"Error loading {NAME} : {e}", "RED")
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "pillar_for_comfyui"
version = "0.0.1"
description = ""
license = {file = "LICENSE"}
requires-python = ">=3.11"
[project.urls]
Repository = "https://github.com/aicoder-max/Pillar_For_ComfyUI"
Documentation = "https://github.com/aicoder-max/Pillar_For_ComfyUI/tree/main?tab=readme-ov-file"
"Bug Tracker" = "https://github.com/aicoder-max/Pillar_For_ComfyUI/issues"
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[tool.comfy]
PublisherId = "aicoder-max"
DisplayName = "Pillar_For_ComfyUI"
Icon = "https://raw.githubusercontent.com/username/super-resolution-node/main/icon.png"
+9
View File
@@ -0,0 +1,9 @@
fastapi
huggingface_hub
langdetect
Pillow
pydantic
Requests
torch
torchvision
transformers
View File
+59
View File
@@ -0,0 +1,59 @@
import threading
from abc import abstractmethod
import gc
import torch
from ..pillar_plus import IS_COMFYUI_ENVIRONMENT
if not IS_COMFYUI_ENVIRONMENT:
from server import logger
else:
import logging
logger = logging.getLogger(__name__)
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class BaseService(metaclass=SingletonMeta):
def __init__(self, model_path):
self.logger = logger
self.model_path = model_path
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model = None
@classmethod
def get_name(cls) -> str:
return cls.__name__
@classmethod
@abstractmethod
def get_model_name(cls):
pass
@abstractmethod
def cleanup(self):
pass
def _free_memory(self):
if hasattr(self, 'model') and self.model is not None:
# Move model to CPU first if it was on GPU
if self.device != "cpu":
self.model.to("cpu")
# Delete model and explicitly call garbage collector
del self.model
self.model = None
# Force CUDA memory cleanup if available
if torch.cuda.is_available():
torch.cuda.empty_cache()
# Run garbage collector
gc.collect()
+205
View File
@@ -0,0 +1,205 @@
# Configure logging
import re
import threading
import torch
from PIL import Image
from transformers import AutoProcessor, LlavaForConditionalGeneration, BitsAndBytesConfig
from langdetect import detect
from .base_service import BaseService
from ..util.constants import DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, DEFAULT_TOP_K, DEFAULT_TOP_P, MAX_TOKENS, MEMORY_MODE
BILINGUAL_SUFFIX = "Please reply in both Chinese and English according to this format **English:**English Description**Chinese:**Chinese Description"
class JoyCaptionService(BaseService):
"""
A singleton service for generating captions for images using the Llava model.
"""
_lock = threading.Lock() # Class-level lock for thread safety
@classmethod
def get_model_name(cls):
return "llama-joycaption-beta-one-hf-llava"
def __init__(self, model_path: str, memory_mode: str):
# Prevent re-initialization
if not hasattr(self, '_initialized'):
# Initialize the base class
super().__init__(model_path)
try:
# Check if GPU is available
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.logger.info(f"Using device: {self.device}")
self.processor = AutoProcessor.from_pretrained(model_path)
if memory_mode == "Default":
self.model = LlavaForConditionalGeneration.from_pretrained(model_path,
torch_dtype="bfloat16",
device_map="auto")
else:
# Configure quantization based on memory mode
quantization_config_params = MEMORY_MODE.get_by_code(memory_mode)
quantization_config = BitsAndBytesConfig(
**quantization_config_params,
llm_int8_skip_modules=["vision_tower", "multi_modal_projector"],
# Transformer's Siglip implementation has bugs when quantized, so skip those.
)
self.model = LlavaForConditionalGeneration.from_pretrained(str(model_path), torch_dtype="auto",
device_map="auto",
quantization_config=quantization_config)
self.logger.info(f"Loaded model {model_path} with memory mode {memory_mode}")
self.model.eval()
self._initialized = True
self.logger.info(f"Model loaded with 4-bit quantization and ready for inference")
except Exception as e:
self.logger.error(f"Error loading model: {str(e)}")
raise
def cleanup(self):
# Only clean up if initialized
if hasattr(self, '_initialized') and self._initialized:
self.logger.info("Starting cleanup of JoyCaptionService resources...")
# Clean up processor
if hasattr(self, 'processor') and self.processor is not None:
self.logger.debug("Cleaning up processor...")
del self.processor
self.processor = None
self.logger.debug("Cleaning up model and memory...")
self._free_memory()
# Mark as uninitialized
self._initialized = False
self.logger.info("Cleaned up model resources for JoyCaptionService")
@staticmethod
def extract_section(caption: str, markers: list, other_markers: list):
for marker in markers:
if marker in caption:
parts = caption.split(marker, 1)
if len(parts) > 1:
text = parts[1].strip()
for other_marker in other_markers:
if other_marker in text:
text = text.split(other_marker, 1)[0].strip()
return text
return ""
@staticmethod
def parse_bilingual_caption(caption: str):
caption = caption.strip()
en_markers = ["**English Description:**", "**英文描述:**", "**English:**", "English Description:"]
cn_markers = ["**Chinese Description:**", "**中文描述:**", "**Chinese:**", "Chinese Description:"]
en_caption = JoyCaptionService.extract_section(caption, en_markers, cn_markers)
cn_caption = JoyCaptionService.extract_section(caption, cn_markers, en_markers)
if not en_caption and not cn_caption:
lines = caption.split('\n')
english_lines = []
chinese_lines = []
chinese_char_pattern = re.compile(r'[\u4e00-\u9fff]')
current_section = "unknown"
for line in lines:
line = line.strip()
if not line:
continue
if chinese_char_pattern.search(line):
if current_section != "chinese":
current_section = "chinese"
chinese_lines.append(line)
else:
if current_section != "english":
current_section = "english"
english_lines.append(line)
en_caption = " ".join(english_lines).strip() if english_lines else ""
cn_caption = " ".join(chinese_lines).strip() if chinese_lines else ""
return (en_caption, cn_caption) if en_caption or cn_caption else (caption, caption)
@torch.inference_mode()
def generate(self, image: Image.Image, system: str, prompt: str, max_new_tokens: int, temperature: float,
top_p: float, top_k: int):
# Limit max_new_tokens not to exceed MAX_TOKENS
max_new_tokens = min(max_new_tokens, MAX_TOKENS)
prompt = f"{prompt.strip()} {BILINGUAL_SUFFIX}"
convo = [
{"role": "system", "content": system.strip()},
{"role": "user", "content": prompt}
]
# Acquire lock to ensure thread safety
with self._lock:
convo_string = self.processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
# Use self.device to maintain device consistency
inputs = self.processor(text=[convo_string], images=[image], return_tensors="pt").to(self.device)
# Use bfloat16 for pixel_values to save memory
if torch.cuda.is_available():
inputs['pixel_values'] = inputs['pixel_values'].to(torch.bfloat16)
generate_ids = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=True if temperature > 0 else False,
suppress_tokens=None,
use_cache=True,
temperature=temperature,
top_k=None if top_k == 0 else top_k,
top_p=top_p,
)[0]
generate_ids = generate_ids[inputs['input_ids'].shape[1]:]
caption = self.processor.tokenizer.decode(generate_ids, skip_special_tokens=True,
clean_up_tokenization_spaces=False).strip()
en_caption, cn_caption = JoyCaptionService.parse_bilingual_caption(caption)
return en_caption, cn_caption
@torch.inference_mode()
def tranlation(self, prompt: str):
lang = detect(prompt)
if lang == "zh-cn":
lang = "English"
else:
lang = "Chinese"
prompt = f"translate this passage into{lang}: {prompt.strip()} "
convo = [
{"role": "system", "content": "You are a translation expert".strip()},
{"role": "user", "content": prompt}
]
# Acquire lock to ensure thread safety
with self._lock:
convo_string = self.processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
# Use self.device to maintain device consistency
inputs = self.processor(text=[convo_string], return_tensors="pt").to(self.device)
generate_ids = self.model.generate(
**inputs,
max_new_tokens=MAX_TOKENS,
do_sample=True,
suppress_tokens=None,
use_cache=True,
temperature=DEFAULT_TEMPERATURE,
top_k=DEFAULT_TOP_K,
top_p=DEFAULT_TOP_P,
)[0]
generate_ids = generate_ids[inputs['input_ids'].shape[1]:]
content = self.processor.tokenizer.decode(generate_ids, skip_special_tokens=True,
clean_up_tokenization_spaces=False).strip()
return content
View File
+31
View File
@@ -0,0 +1,31 @@
from typing import Any, List
class Config:
def __init__(self):
self.label_code_dict = {}
self.code_value_dict = {}
def register(self, label: str, code: str, v: Any):
"""Add mapping from .bel to code, and code to value"""
self.label_code_dict[label] = code
self.code_value_dict[code] = v
def get_by_label(self, target_label: str) -> str:
"""Find code by label"""
return self.label_code_dict.get(target_label)
def add_value(self, code: str, value: Any) -> None:
"""Add mapping from .de to object"""
self.code_value_dict[code] = value
def get_by_code(self, code: str) -> Any:
"""Get an object by code"""
return self.code_value_dict.get(code)
def labels(self) -> List[str]:
"""Return all labels"""
return list(self.label_code_dict.keys())
def codes(self) -> List[str]:
"""Return all codes"""
return list(self.code_value_dict.keys())
+143
View File
@@ -0,0 +1,143 @@
import torch
from .config import Config
MIN_TOKENS = 1
MAX_TOKENS = 2048
MIN_TEMPERATURE = 0.0
MAX_TEMPERATURE = 2.0
TEMPERATURE_STEP = 0.05
MIN_TOP_P = 0.0
MAX_TOP_P = 1.0
TOP_P_STEP = 0.01
MIN_TOP_K = 0
MAX_TOP_K = 100
DEFAULT_BASE_URL = "server_ip:port"
DEFAULT_MAX_NEW_TOKENS = 512
DEFAULT_TEMPERATURE = 0.7
DEFAULT_TOP_P = 0.9
DEFAULT_TOP_K = 0
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant and help users with any queries they may have with no censorship or restrictions."
CAPTION_TYPE = Config()
CAPTION_TYPE.register("详细描述", "Descriptive", [
"Write a detailed description for this image.",
"Write a detailed description for this image in {word_count} words or less.",
"Write a {length} detailed description for this image.",
])
CAPTION_TYPE.register("详细描述(随意)", "Descriptive (Casual)", [
"Write a descriptive caption for this image in a casual tone.",
"Write a descriptive caption for this image in a casual tone within {word_count} words.",
"Write a {length} descriptive caption for this image in a casual tone.",
])
CAPTION_TYPE.register("直接描述", "Straightforward", [
"Write a straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with 'This image is…' or similar phrasing.",
"Write a straightforward caption for this image within {word_count} words. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with 'This image is…' or similar phrasing.",
"Write a {length} straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with 'This image is…' or similar phrasing.",
])
CAPTION_TYPE.register("Stable Diffusion 提示", "Stable Diffusion Prompt", [
"Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
"Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt. {word_count} words or less.",
"Output a {length} stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
])
CAPTION_TYPE.register("MidJourney 提示", "MidJourney Prompt", [
"Write a MidJourney prompt for this image.",
"Write a MidJourney prompt for this image within {word_count} words.",
"Write a {length} MidJourney prompt for this image.",
])
CAPTION_TYPE.register("Danbooru 标签列表", "Danbooru Tag List", [
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text.",
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {word_count} words or less.",
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {length} length.",
])
CAPTION_TYPE.register("e621 标签列表", "e621 Tag List", [
"Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
"Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags. Keep it under {word_count} words.",
"Write a {length} comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
])
CAPTION_TYPE.register("Rule34 标签列表", "Rule34 Tag List", [
"Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
"Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags. Keep it under {word_count} words.",
"Write a {length} comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
])
CAPTION_TYPE.register("Booru-like 标签列表", "Booru-like Tag List", [
"Write a list of Booru-like tags for this image.",
"Write a list of Booru-like tags for this image within {word_count} words.",
"Write a {length} list of Booru-like tags for this image.",
])
CAPTION_TYPE.register("艺术评论家", "Art Critic", [
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc.",
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it within {word_count} words.",
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it {length}.",
])
CAPTION_TYPE.register("产品列表", "Product Listing", [
"Write a caption for this image as though it were a product listing.",
"Write a caption for this image as though it were a product listing. Keep it under {word_count} words.",
"Write a {length} caption for this image as though it were a product listing.",
])
CAPTION_TYPE.register("社交媒体帖子", "Social Media Post", [
"Write a caption for this image as if it were being used for a social media post.",
"Write a caption for this image as if it were being used for a social media post. Limit the caption to {word_count} words.",
"Write a {length} caption for this image as if it were being used for a social media post.",
])
EXTRA_OPTIONS = Config()
EXTRA_OPTIONS.register("", "", None)
EXTRA_OPTIONS.register("如果图片中有人物 / 角色,你必须用 {name} 来称呼他们。","If there is a person/character in the image you must refer to them as {name}.", None)
EXTRA_OPTIONS.register("不要包含无法改变的信息(如种族、性别等),但仍应包含可改变的属性(如发型)。","Do NOT include information about people/characters that cannot be changed (like ethnicity, gender, etc), but do still include changeable attributes (like hair style).",None)
EXTRA_OPTIONS.register("包含关于照明信息。", "Include information about lighting.", None)
EXTRA_OPTIONS.register("包含关于相机角度的信息。", "Include information about camera angle.", None)
EXTRA_OPTIONS.register("包含关于是否存在水印的信息。","Include information about whether there is a watermark or not.", None)
EXTRA_OPTIONS.register("包含关于是否存在JPEG伪影的信息。","Include information about whether there are JPEG artifacts or not.", None)
EXTRA_OPTIONS.register("如果这是一张照片,你必须包含关于相机使用情况的信息以及光圈、快门速度、ISO等细节。","If it is a photo you MUST include information about what camera was likely used and details such as aperture, shutter speed, ISO, etc.",None)
EXTRA_OPTIONS.register("不要包含任何色情内容;保持它PG级。", "Do NOT include anything sexual; keep it PG.", None)
EXTRA_OPTIONS.register("不要提及图片的分辨率。", "Do NOT mention the image's resolution.", None)
EXTRA_OPTIONS.register("你必须包含关于图片主观审美质量的信息,从低到非常高。","You MUST include information about the subjective aesthetic quality of the image from low to very high.",None)
EXTRA_OPTIONS.register("包含关于图片构图风格的信息,如引导线、三分法则或对称性。","Include information on the image's composition style, such as leading lines, rule of thirds, or symmetry.",None)
EXTRA_OPTIONS.register("不要提及图片中的任何文本。", "Do NOT mention any text that is in the image.", None)
EXTRA_OPTIONS.register("指定景深和背景是否聚焦或模糊。","Specify the depth of field and whether the background is in focus or blurred.", None)
EXTRA_OPTIONS.register("如果适用,提及可能使用的人工或自然光源。","If applicable, mention the likely use of artificial or natural lighting sources.", None)
EXTRA_OPTIONS.register("不要使用任何模糊的语言。", "Do NOT use any ambiguous language.", None)
EXTRA_OPTIONS.register("包含关于图片是否是SFW、暗示性或NSFW的信息。","Include whether the image is sfw, suggestive, or nsfw.", None)
EXTRA_OPTIONS.register("只描述图片最重要的元素。", "ONLY describe the most important elements of the image.", None)
EXTRA_OPTIONS.register("如果这是一件艺术品,不要包含艺术家或作品的名称。","If it is a work of art, do not include the artist's name or the title of the work.", None)
EXTRA_OPTIONS.register("识别图片的方向(肖像、风景或正方形)和明显的宽高比。","Identify the image orientation (portrait, landscape, or square) and aspect ratio if obvious.",None)
EXTRA_OPTIONS.register("""使用粗俗俚语和脏话,如(但不限于)"fucking"、"slut"、"cock" 等。""","""Use vulgar slang and profanity, such as (but not limited to) "fucking," "slut," "cock," etc.""",None)
EXTRA_OPTIONS.register("不要使用礼貌的委婉语——倾向于直接、随意的表达。","Do NOT use polite euphemisms—lean into blunt, casual phrasing.", None)
EXTRA_OPTIONS.register("当适用时,包含关于人物/角色的年龄信息。","Include information about the ages of any people/characters when applicable.", None)
EXTRA_OPTIONS.register("提及图片是否描绘了极端特写、特写、中特写、中景、牛仔镜头、中远景、远景或极端远景。","Mention whether the image depicts an extreme close-up, close-up, medium close-up, medium shot, cowboy shot, medium wide shot, wide shot, or extreme wide shot.",None)
EXTRA_OPTIONS.register("不要提及图片的氛围/感觉/等。", "Do not mention the mood/feeling/etc of the image.", None)
EXTRA_OPTIONS.register("明确指定视角高度(眼睛水平、低角度虫眼、鸟瞰、无人机、屋顶等)。","Explicitly specify the vantage height (eye-level, low-angle worm's-eye, bird's-eye, drone, rooftop, etc.).",None)
EXTRA_OPTIONS.register("如果存在水印,你必须提及它。", "If there is a watermark, you must mention it.", None)
EXTRA_OPTIONS.register("""你的回答将被用于一个文本到图像模型,所以避免使用像"这张图片展示了…"、"你在看…"等无用的元短语。""","""Your response will be used by a text-to-image model, so avoid useless meta phrases like "This image shows…", "You are looking at...", etc.""",None)
CAPTION_LENGTH_CHOICES = Config()
# 字幕长度选项
CAPTION_LENGTH_CHOICES.register("任意", "any", None)
CAPTION_LENGTH_CHOICES.register("非常短", "very short", None)
CAPTION_LENGTH_CHOICES.register("短", "short", None)
CAPTION_LENGTH_CHOICES.register("中等长度", "medium-length", None)
CAPTION_LENGTH_CHOICES.register("长", "long", None)
CAPTION_LENGTH_CHOICES.register("非常长", "very long", None)
# 添加数字选项
for i in range(20, 261, 10):
CAPTION_LENGTH_CHOICES.register(str(i), str(i), None)
MEMORY_MODE = Config()
MEMORY_MODE.register("最大节省 (4-bit)", "Maximum Savings (4-bit)", {
"load_in_4bit": True,
"bnb_4bit_quant_type": "nf4",
"bnb_4bit_compute_dtype": torch.bfloat16,
"bnb_4bit_use_double_quant": True,
})
MEMORY_MODE.register("平衡 (8-bit)", "Balanced (8-bit)", {"load_in_8bit": True})
MEMORY_MODE.register("默认模式", "Default", {})
EXEC_OPTIONS = Config()
EXEC_OPTIONS.register("远程", "remote", None)
EXEC_OPTIONS.register("本地", "local", None)
+83
View File
@@ -0,0 +1,83 @@
from .pyproject import DISPAY_NAME
# https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
# https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
COLORS = {
'BLACK': '\33[30m',
'RED': '\33[31m',
'GREEN': '\33[32m',
'YELLOW': '\33[33m',
'BLUE': '\33[34m',
'MAGENTA': '\33[35m',
'CYAN': '\33[36m',
'WHITE': '\33[37m',
'GREY': '\33[90m',
'BRIGHT_RED': '\33[91m',
'BRIGHT_GREEN': '\33[92m',
'BRIGHT_YELLOW': '\33[93m',
'BRIGHT_BLUE': '\33[94m',
'BRIGHT_MAGENTA': '\33[95m',
'BRIGHT_CYAN': '\33[96m',
'BRIGHT_WHITE': '\33[97m',
# Styles.
'RESET': '\33[0m', # Note, Portainer doesn't like 00 here, so we'll use 0. Should be fine...
'BOLD': '\33[01m',
'NORMAL': '\33[22m',
'ITALIC': '\33[03m',
'UNDERLINE': '\33[04m',
'BLINK': '\33[05m',
'BLINK2': '\33[06m',
'SELECTED': '\33[07m',
# Backgrounds
'BG_BLACK': '\33[40m',
'BG_RED': '\33[41m',
'BG_GREEN': '\33[42m',
'BG_YELLOW': '\33[43m',
'BG_BLUE': '\33[44m',
'BG_MAGENTA': '\33[45m',
'BG_CYAN': '\33[46m',
'BG_WHITE': '\33[47m',
'BG_GREY': '\33[100m',
'BG_BRIGHT_RED': '\33[101m',
'BG_BRIGHT_GREEN': '\33[102m',
'BG_BRIGHT_YELLOW': '\33[103m',
'BG_BRIGHT_BLUE': '\33[104m',
'BG_BRIGHT_MAGENTA': '\33[105m',
'BG_BRIGHT_CYAN': '\33[106m',
'BG_BRIGHT_WHITE': '\33[107m',
}
def log_node_success(node_name, message, msg_color='RESET'):
"""Logs a success message."""
_log_node("BRIGHT_GREEN", node_name, message, msg_color=msg_color)
def log_node_info(node_name, message, msg_color='RESET'):
"""Logs an info message."""
_log_node("CYAN", node_name, message, msg_color=msg_color)
def log_node_warn(node_name, message, msg_color='RESET'):
"""Logs an warn message."""
_log_node("YELLOW", node_name, message, msg_color=msg_color)
def log_node(node_name, message, msg_color='RESET'):
"""Logs a message."""
_log_node("CYAN", node_name, message, msg_color=msg_color)
def _log_node(color, node_name, message, msg_color='RESET'):
"""Logs for a node message."""
log(message, color=color, prefix=node_name.replace(" (rgthree)", ""), msg_color=msg_color)
def log(message, color=None, msg_color=None, prefix=None):
"""Basic logging."""
color = COLORS[color] if color is not None and color in COLORS else COLORS["BRIGHT_GREEN"]
msg_color = COLORS[msg_color] if msg_color is not None and msg_color in COLORS else ''
prefix = f'[{prefix}]' if prefix is not None else ''
msg = f'{color}[{DISPAY_NAME}]{prefix}'
msg += f'{msg_color} {message}{COLORS["RESET"]}'
print(msg)
+41
View File
@@ -0,0 +1,41 @@
import os
import re
import json
from .utils import set_dict_value
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_FILE_PY_PROJECT = os.path.join(_THIS_DIR, '..', 'pyproject.toml')
def read_pyproject():
"""Reads the pyproject.toml file"""
data = {}
last_key = ''
lines = []
# I'd like to use tomllib/tomli, but I'd much rather not introduce dependencies since I've yet to
# need to and not everyone may have 3.11. We've got a controlled config file anyway.
with open(_FILE_PY_PROJECT, "r", encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
line = line.strip()
if re.match(r'\[([^\]]+)\]$', line):
last_key = line[1:-1]
set_dict_value(data, last_key, data[last_key] if last_key in data else {})
continue
value_matches = re.match(r'^([^\s\=]+)\s*=\s*(.*)$', line)
if value_matches:
try:
set_dict_value(data, f'{last_key}.{value_matches[1]}', json.loads(value_matches[2]))
except json.decoder.JSONDecodeError:
# We don't handle multiline arrays or curly brackets; that's ok, we know the file.
pass
return data
_DATA = read_pyproject()
VERSION: str = _DATA['project']['version']
NAME: str = _DATA['project']['name']
DISPAY_NAME: str = _DATA['tool']['comfy']['DisplayName']
parts = DISPAY_NAME.split("_")
CATEGORY_NAME = f"{parts[0]}"
+14
View File
@@ -0,0 +1,14 @@
def set_dict_value(data: dict, dict_key: str, value, create_missing_objects=True):
""" Sets a deeply nested value given a dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
if key not in data:
if create_missing_objects == False:
return None
data[key] = {}
if len(keys) == 0:
data[key] = value
else:
set_dict_value(data[key], '.'.join(keys), value, create_missing_objects)
return data
+503
View File
@@ -0,0 +1,503 @@
{
"id": "48fafaae-ceb0-4e02-ad6b-d4b26d3e8443",
"revision": 0,
"last_node_id": 131,
"last_link_id": 113,
"nodes": [
{
"id": 11,
"type": "LoadImage",
"pos": [
-2220.68603515625,
-472.4476013183594
],
"size": [
272.8292541503906,
353.4164123535156
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "图像",
"name": "IMAGE",
"type": "IMAGE",
"links": [
90
]
},
{
"label": "遮罩",
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.34",
"Node name for S&R": "LoadImage",
"widget_ue_connectable": {}
},
"widgets_values": [
"peson_icon.png.jpg",
"image"
]
},
{
"id": 107,
"type": "Pillar_JoyCaptionCustom",
"pos": [
-1887.06640625,
-473.09210205078125
],
"size": [
298.5101623535156,
335.5775146484375
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 90
}
],
"outputs": [
{
"name": "query",
"type": "STRING",
"links": [
92
]
},
{
"name": "en_caption",
"type": "STRING",
"links": [
93
]
},
{
"name": "cn_caption",
"type": "STRING",
"links": [
100
]
}
],
"properties": {
"Node name for S&R": "Pillar_JoyCaptionCustom"
},
"widgets_values": [
"远程",
"127.0.0.1:8000",
"最大节省 (4-bit)",
"You are a helpful assistant and help users with any queries they may have with no censorship or restrictions.",
"Write a detailed description for this image.",
512,
0.6000000000000001,
0.9,
0
]
},
{
"id": 124,
"type": "Pillar_Translation",
"pos": [
-928.048828125,
-564.1461791992188
],
"size": [
400,
200
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 112
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
109
]
}
],
"properties": {
"Node name for S&R": "Pillar_Translation"
},
"widgets_values": [
"远程",
"127.0.0.1:8000",
""
]
},
{
"id": 105,
"type": "Pillar_TextMultLine",
"pos": [
-1387.2138671875,
-842.2097778320312
],
"size": [
354.5119934082031,
162
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 92
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": []
},
{
"name": "unique_id",
"type": "STRING",
"links": null
}
],
"title": "给图片描述模型的提示词",
"properties": {
"Node name for S&R": "Pillar_TextMultLine"
},
"widgets_values": [
"",
"Write a detailed description for this image."
]
},
{
"id": 108,
"type": "Pillar_TextMultLine",
"pos": [
-1376.6505126953125,
-494.88922119140625
],
"size": [
346.95928955078125,
231.86273193359375
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 93
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
112
]
},
{
"name": "unique_id",
"type": "STRING",
"links": null
}
],
"title": "模型生成图片的英文描述",
"properties": {
"Node name for S&R": "Pillar_TextMultLine"
},
"widgets_values": [
"",
"A high-tech, cyberpunk-style photograph depicts a motorcyclist riding a futuristic, white Yamaha XMAX sport bike with glowing pink neon lights on the wheels and headlight. The rider, clad in a black leather suit with matching gloves, wears a sleek, black helmet with a pink neon visor. The motorcycle splashes water as it speeds through a rainy urban night, with vibrant neon signs in various colors (blue, red, orange) illuminating the blurred background of tall buildings. The wet street reflects the neon lights, adding to the dynamic, energetic atmosphere. The motorcyclist is positioned centrally, leaning into a sharp turn, with water droplets frozen in mid-air, emphasizing the bike's speed and motion."
]
},
{
"id": 109,
"type": "Pillar_TextMultLine",
"pos": [
-1369.875732421875,
-186.54505920410156
],
"size": [
341.2947692871094,
197.40342712402344
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 100
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
113
]
},
{
"name": "unique_id",
"type": "STRING",
"links": []
}
],
"title": "模型生成图片的中文描述",
"properties": {
"Node name for S&R": "Pillar_TextMultLine"
},
"widgets_values": [
"",
"一个高科技, 科幻风格的摄影作品,描绘了一辆未来化的白色雅马哈XMAX运动摩托车,摩托车的轮子和前灯都有亮着的粉色 Neon 灯。骑士穿着一套黑色皮衣和配套黑色手套,戴着一款黑色头盔,头盔上有粉色 Neon visor。摩托车在雨中快速行驶,水花在摩托车的后方飞溅。背景中有多种颜色的 Neon 照明灯(蓝色、红色、橙色),高楼建筑形成了一个模糊的背景。摩托车的轮子和前灯都有水滴被捕捉在空中,摩托车的速度和运动感被突显。骑士位于中央,摩托车在一个急转中,水滴被捕捉在空中,整个场景充满了动态和高能量。"
]
},
{
"id": 106,
"type": "Pillar_Translation",
"pos": [
-928.1884765625,
-257.7129821777344
],
"size": [
400,
200
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 113
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
97
]
}
],
"properties": {
"Node name for S&R": "Pillar_Translation"
},
"widgets_values": [
"本地",
"127.0.0.1:8000",
""
]
},
{
"id": 112,
"type": "Pillar_TextMultLine",
"pos": [
-406.6347351074219,
-302.3141784667969
],
"size": [
639.7739868164062,
265.66192626953125
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 97
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": null
},
{
"name": "unique_id",
"type": "STRING",
"links": null
}
],
"title": "翻译后的英文描述",
"properties": {
"Node name for S&R": "Pillar_TextMultLine"
},
"widgets_values": [
"",
"This is a photograph with a futuristic, sci-fi style, depicting a high-tech, modified white Yamaha XMAX motorcycle. The motorcycle has glowing pink neon lights on both the wheels and the front. The rider is dressed in black leather clothing and matching gloves, with a black helmet featuring a pink neon visor. The motorcycle is moving rapidly through rain, with water droplets flying behind it. The background includes various neon lights in colors like blue, red, and orange, and features a blurred cityscape. The motorcycle's wheels and front lights are splashed with water droplets, emphasizing the speed and motion. The rider is centered, with the motorcycle in a sharp turn, and water droplets are captured in mid-air, creating a dynamic and energetic scene."
]
},
{
"id": 125,
"type": "Pillar_TextMultLine",
"pos": [
-423.5297546386719,
-605.3947143554688
],
"size": [
657.0283813476562,
183.14300537109375
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 109
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": null
},
{
"name": "unique_id",
"type": "STRING",
"links": null
}
],
"title": "翻译后的中文描述",
"properties": {
"Node name for S&R": "Pillar_TextMultLine"
},
"widgets_values": [
"",
"一张高科技,赛博朋克风格的摄影作品,描绘着一名摩托骑手驾驶着未来风格的白色雅马哈XMAX运动摩托车,摩托车的轮子和前灯都有闪亮的粉色 neon灯。骑手穿着黑色皮衣和配套的手套,戴着一顶黑色 helmet,visor上也有粉色 neon灯。摩托车快速通过雨夜的城市,各种 neon色灯(蓝色,红色,橙色)闪亮,背景是高楼的模糊影像。湿润的路面反射 neon灯,增添了运动摩托车的动感,激动的氛围。摩托车骑手位于中心,正在打一个急转弯,水滴在空中被冻结,突出了摩托车的速度和运动。"
]
}
],
"links": [
[
90,
11,
0,
107,
0,
"IMAGE"
],
[
92,
107,
0,
105,
0,
"STRING"
],
[
93,
107,
1,
108,
0,
"STRING"
],
[
97,
106,
0,
112,
0,
"STRING"
],
[
100,
107,
2,
109,
0,
"STRING"
],
[
109,
124,
0,
125,
0,
"STRING"
],
[
112,
108,
0,
124,
0,
"STRING"
],
[
113,
109,
0,
106,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.4122927695244514,
"offset": [
2280.7654048896793,
1116.4455084735107
]
},
"frontendVersion": "1.18.10",
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}