submit project with version 0.0.1
This commit is contained in:
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+12
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+7
@@ -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>
|
||||
Generated
+8
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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>.
|
||||
|
||||
@@ -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 提供以下节点
|
||||
|
||||

|
||||
|
||||
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 的中的所有节点。首先加载一张图片,然后使用图片描述节点(自定义),生成图片中文、英文描述。然后使用翻译节点,将图片中文描述翻译为英文,英文描述翻译为中文。最后,使用预览文本节点,展示翻译后内容。适用场景:图生图,提取图片中主要特征,生成图片描述,对描述进行局部调整,再二次渲染生图。
|
||||

|
||||
---
|
||||
|
||||
## 如何安装
|
||||
|
||||
### **推荐方式**
|
||||
* 通过 [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
@@ -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']
|
||||
@@ -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)}")
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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 = ""
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()))
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 545 KiB |
@@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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": "唯一编号"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)}")
|
||||
@@ -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
|
||||
@@ -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,)}
|
||||
@@ -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,)
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -0,0 +1,9 @@
|
||||
fastapi
|
||||
huggingface_hub
|
||||
langdetect
|
||||
Pillow
|
||||
pydantic
|
||||
Requests
|
||||
torch
|
||||
torchvision
|
||||
transformers
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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
@@ -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)
|
||||
@@ -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]}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user