├── .github
└── workflows
│ └── publish_action.yml
├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── auth_unit.py
├── docs
├── menu.png
├── output.png
├── web.png
├── wechat.jpg
└── workflow.png
├── encrypt_node.py
├── example_workflows
├── simple_workflow.json
└── simple_workflow.png
├── input_node.py
├── install.bat
├── install.sh
├── js
├── riceconfig.js
└── riceround.js
├── message_holder.py
├── output_node.py
├── publish.py
├── pyproject.toml
├── pyproject_cloud.toml
├── requirements.txt
├── rice_def.py
├── rice_install_client.py
├── rice_prompt_handler.py
├── rice_prompt_info.py
├── rice_url_config.py
├── rice_websocket.py
├── static
├── dialog-lib.umd.cjs
└── login-dialog.umd.cjs
├── utils.py
└── version.json
/.github/workflows/publish_action.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Comfy registry
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "pyproject.toml"
9 |
10 | jobs:
11 | publish-node:
12 | name: Publish Custom Node to registry
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out code
16 | uses: actions/checkout@v4
17 | - name: Publish Custom Node
18 | uses: Comfy-Org/publish-node-action@main
19 | with:
20 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /__pycache__
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 RiceRound
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ComfyUI_RiceRound
2 |
3 | [](https://github.com/RiceRound/ComfyUI_RiceRound/stargazing)
4 |
5 | > **📚 详细文档**: 访问 [https://help.riceround.online/](https://help.riceround.online/) 获取完整使用指南
6 |
7 | > **⭐ 喜欢这个项目?** 请给我们的 [GitHub 仓库](https://github.com/RiceRound/ComfyUI_RiceRound) 点个星标支持我们!
8 |
9 | ## 简介
10 |
11 | RiceRound 是一个开源项目,旨在将本地 AI 工作流(通过 ComfyUI 创建)转移到云端,支持分布式部署。创作者可以轻松设计并运行工作流,实时监控任务进度与节点状态。平台支持一键生成云节点与在线页面,创作者可直接通过单次付费进行操作,收入全部归创作者。
12 |
13 | 一键部署,同时生成在线页面和 ComfyUI 云端节点。
14 |
15 | ## 快速启动
16 |
17 | ### 1. 安装节点
18 |
19 | #### 方法一:通过 ComfyUI-Manager 安装(推荐)
20 | 1. 打开 ComfyUI-Manager
21 | 2. 在搜索框中输入 "RiceRound"
22 | 3. 点击安装按钮完成安装
23 |
24 | #### 方法二:通过 Git 克隆安装
25 | ```bash
26 | cd ComfyUI/custom_nodes/
27 | git clone https://github.com/RiceRound/ComfyUI_RiceRound
28 | ```
29 |
30 | #### 方法三:手动下载安装
31 | 1. 下载 [ComfyUI_RiceRound 压缩包](https://github.com/RiceRound/ComfyUI_RiceRound/archive/refs/heads/main.zip)
32 | 2. 解压文件到 `ComfyUI/custom_nodes/` 目录
33 | 3. 重启 ComfyUI 使节点生效
34 |
35 | ### 2. 搭建工作流
36 |
37 | ⚠️ **重要提示**: 只使用 RiceRound -> input 里面的节点,Output 节点是用来排查部署问题使用的。
38 |
39 | 
40 |
41 | ### 3. 发布工作流
42 |
43 | 在工作流尾部加上 RiceRound Publish 节点用于发布,然后点击运行。
44 |
45 | 
46 |
47 | ### 4. 查看结果
48 |
49 | 会在 output 文件夹生成一些加密工作流文件,其中 workflow.json 就是你拿去发布的加密工作流。
50 |
51 | 
52 |
53 | ### 5. 管理工作流
54 |
55 | 在 [https://www.riceround.online/](https://www.riceround.online/) 可以管理你的工作流,也可以看见工作流生成的页面。
56 |
57 | 
58 |
59 | ## 客户端节点部署
60 |
61 | ### Windows 平台
62 | - 下载 [share_client_windows_setup.exe](https://github.com/RiceRound/ComfyUI_RiceRound/releases) 全量安装包(带图形界面和启停管理)
63 | - 或使用命令行绿色工具(将 share_client_windows.exe 重命名为 riceround-client)
64 |
65 | ### Linux/MacOS 平台
66 | - 下载对应平台的客户端
67 | - 运行命令:`./riceround-client`
68 |
69 | 更多详细安装和配置说明,请访问 [https://help.riceround.online/](https://help.riceround.online/)
70 |
71 | ## 核心功能
72 |
73 | - 🚀 **云节点与页面生成**: 一键生成云节点,自动展示在线页面
74 | - 🌐 **分布式部署与自动扩展**: 支持多机器部署,智能化资源调度
75 | - 💰 **收入分配透明**: 支持单次付费模式,创作者直接获得收入
76 | - 🔓 **开源计划**: 逐步开放源代码,支持社区共建发展
77 |
78 | ## 常见问题
79 |
80 | 如遇问题,可查看日志文件进行排查:
81 |
82 | - Windows: `C:\Users\<用户名>\RiceRound\logs\`
83 | - macOS: `/Users/<用户名>/RiceRound/logs/`
84 | - Linux: `/home/<用户名>/RiceRound\logs\`
85 |
86 | ## 持续更新中
87 |
88 | 有时候教程、演示文件没有来得及更新,请联系我微信。
89 |
90 | 
91 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import platform
3 | import random
4 | import sys
5 | from urllib.parse import unquote
6 | import asyncio
7 | import aiohttp
8 | from .utils import get_local_app_setting_path, restart_comfyui
9 | from server import PromptServer
10 | from .input_node import *
11 | from .output_node import *
12 | from .encrypt_node import *
13 | from .auth_unit import AuthUnit
14 | from aiohttp import web
15 | from functools import partial
16 | from .rice_prompt_handler import RiceRoundPromptHandler
17 | from .rice_url_config import RiceUrlConfig
18 | from .rice_prompt_info import RiceEnvConfig, RicePromptInfo
19 |
20 |
21 | def create_dynamic_nodes(base_class):
22 | rice_prompt_info = RicePromptInfo()
23 | dynamic_classes = {}
24 | for node_name, info in rice_prompt_info.choice_classname_map.items():
25 | class_name = node_name
26 | category = "RiceRound/Advanced/Choice"
27 | dynamic_class = type(
28 | class_name,
29 | (base_class,),
30 | {"__node_name__": node_name, "CATEGORY": category},
31 | )
32 | dynamic_classes[class_name] = {
33 | "dynamic_class": dynamic_class,
34 | "display_name": info.get("display_name", class_name),
35 | }
36 | return dynamic_classes
37 |
38 |
39 | dynamic_choice_nodes = create_dynamic_nodes(RiceRoundBaseChoiceNode)
40 | NODE_CLASS_MAPPINGS = {
41 | "RiceRoundSimpleChoiceNode": RiceRoundSimpleChoiceNode,
42 | "RiceRoundAdvancedChoiceNode": RiceRoundAdvancedChoiceNode,
43 | "RiceRoundImageBridgeNode": RiceRoundImageBridgeNode,
44 | "RiceRoundSimpleImageNode": RiceRoundSimpleImageNode,
45 | "RiceRoundImageNode": RiceRoundImageNode,
46 | "RiceRoundDownloadImageAndMaskNode": RiceRoundDownloadImageAndMaskNode,
47 | "RiceRoundDownloadImageNode": RiceRoundDownloadImageNode,
48 | "RiceRoundRandomSeedNode": RiceRoundRandomSeedNode,
49 | "RiceRoundInputTextNode": RiceRoundInputTextNode,
50 | "RiceRoundMaskBridgeNode": RiceRoundMaskBridgeNode,
51 | "RiceRoundDownloadMaskNode": RiceRoundDownloadMaskNode,
52 | "RiceRoundIntNode": RiceRoundIntNode,
53 | "RiceRoundFloatNode": RiceRoundFloatNode,
54 | "RiceRoundBooleanNode": RiceRoundBooleanNode,
55 | "RiceRoundStrToIntNode": RiceRoundStrToIntNode,
56 | "RiceRoundStrToFloatNode": RiceRoundStrToFloatNode,
57 | "RiceRoundStrToBooleanNode": RiceRoundStrToBooleanNode,
58 | "RiceRoundDecryptNode": RiceRoundDecryptNode,
59 | "RiceRoundOutputImageBridgeNode": RiceRoundOutputImageBridgeNode,
60 | "RiceRoundImageUrlNode": RiceRoundImageUrlNode,
61 | "RiceRoundUploadImageNode": RiceRoundUploadImageNode,
62 | "RiceRoundOutputMaskBridgeNode": RiceRoundOutputMaskBridgeNode,
63 | "RiceRoundOutputIntNode": RiceRoundOutputIntNode,
64 | "RiceRoundOutputFloatNode": RiceRoundOutputFloatNode,
65 | "RiceRoundOutputBooleanNode": RiceRoundOutputBooleanNode,
66 | "RiceRoundOutputTextNode": RiceRoundOutputTextNode,
67 | "RiceRoundEncryptNode": RiceRoundEncryptNode,
68 | "RiceRoundOutputImageNode": RiceRoundOutputImageNode,
69 | **{name: cls["dynamic_class"] for (name, cls) in dynamic_choice_nodes.items()},
70 | }
71 | NODE_DISPLAY_NAME_MAPPINGS = {
72 | "RiceRoundSimpleChoiceNode": "Simple Choice",
73 | "RiceRoundAdvancedChoiceNode": "Advanced Choice",
74 | "RiceRoundImageBridgeNode": "Image Bridge",
75 | "RiceRoundSimpleImageNode": "Simple Image",
76 | "RiceRoundImageNode": "Image & Mask",
77 | "RiceRoundDownloadImageAndMaskNode": "Download Image&Mask",
78 | "RiceRoundDownloadImageNode": "Download Image",
79 | "RiceRoundRandomSeedNode": "Random Seed",
80 | "RiceRoundInputTextNode": "Input Text",
81 | "RiceRoundMaskBridgeNode": "Mask Bridge",
82 | "RiceRoundDownloadMaskNode": "Download Mask",
83 | "RiceRoundIntNode": "RiceRound Int",
84 | "RiceRoundFloatNode": "RiceRound Float",
85 | "RiceRoundBooleanNode": "RiceRound Boolean",
86 | "RiceRoundStrToIntNode": "RiceRound Str To Int",
87 | "RiceRoundStrToFloatNode": "RiceRound Str To Float",
88 | "RiceRoundStrToBooleanNode": "RiceRound Str To Boolean",
89 | "RiceRoundDecryptNode": "RiceRound Cloud",
90 | "RiceRoundOutputImageBridgeNode": "Output Image Bridge",
91 | "RiceRoundImageUrlNode": "Image URL",
92 | "RiceRoundUploadImageNode": "Upload Image",
93 | "RiceRoundOutputMaskBridgeNode": "Output Mask Bridge",
94 | "RiceRoundOutputIntNode": "Output Int",
95 | "RiceRoundOutputFloatNode": "Output Float",
96 | "RiceRoundOutputBooleanNode": "Output Boolean",
97 | "RiceRoundOutputTextNode": "Output Text",
98 | "RiceRoundEncryptNode": "RiceRound Publish",
99 | "RiceRoundOutputImageNode": "Output Image",
100 | **{name: cls["display_name"] for (name, cls) in dynamic_choice_nodes.items()},
101 | }
102 | WEB_DIRECTORY = "./js"
103 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAMES_MAPPINGS", "WEB_DIRECTORY"]
104 | handler_instance = RiceRoundPromptHandler()
105 | onprompt_callback = partial(handler_instance.onprompt_handler)
106 | PromptServer.instance.add_on_prompt_handler(onprompt_callback)
107 | routes = PromptServer.instance.routes
108 | url_config = RiceUrlConfig()
109 | workspace_path = os.path.join(os.path.dirname(__file__))
110 | dist_path = os.path.join(workspace_path, "static")
111 | if os.path.exists(dist_path):
112 | PromptServer.instance.app.add_routes(
113 | [aiohttp.web.static("/riceround/static", dist_path)]
114 | )
115 |
116 |
117 | @routes.post("/riceround/auth_callback")
118 | async def auth_callback(request):
119 | auth_query = await request.json()
120 | token = auth_query.get("token", "")
121 | client_key = auth_query.get("client_key", "")
122 | logging.info(f"### auth_callback: {token} {client_key}")
123 | if token and client_key:
124 | token = unquote(token)
125 | client_key = unquote(client_key)
126 | AuthUnit().set_user_token(token, client_key)
127 | return aiohttp.web.json_response({"status": "success"}, status=200)
128 |
129 |
130 | @routes.post("/riceround/set_long_token")
131 | async def set_long_token(request):
132 | data = await request.json()
133 | long_token = data.get("long_token", "")
134 | if long_token:
135 | AuthUnit().save_user_token(long_token)
136 | return aiohttp.web.json_response({"status": "success"}, status=200)
137 |
138 |
139 | @routes.post("/riceround/set_node_additional_info")
140 | async def set_node_additional_info(request):
141 | additional_info = await request.json()
142 | RicePromptInfo().set_node_additional_info(additional_info)
143 | return web.json_response({}, status=200)
144 |
145 |
146 | @routes.get("/riceround/open_folder")
147 | async def open_folder(request):
148 | if request.remote not in ("127.0.0.1", "::1"):
149 | return web.json_response({"error": "Unauthorized access"}, status=403)
150 | id = request.query.get("id", "")
151 | folder = None
152 | if id == "1":
153 | folder = get_local_app_setting_path()
154 | if not folder.exists():
155 | return web.json_response({"error": "Folder does not exist"}, status=404)
156 | elif id == "2":
157 | folder = RicePromptInfo().get_choice_server_folder()
158 | if not folder.exists():
159 | return web.json_response({"error": "Folder does not exist"}, status=404)
160 | if folder:
161 | system = platform.system()
162 | try:
163 | if system == "Windows":
164 | os.startfile(folder)
165 | return web.json_response({"status": "success"}, status=200)
166 | except Exception as e:
167 | return web.json_response({"error": str(e)}, status=500)
168 |
169 |
170 | @routes.get("/riceround/get_current_env_config")
171 | async def save_current_env_config(request):
172 | if request.remote not in ("127.0.0.1", "::1"):
173 | return web.json_response({"error": "Unauthorized access"}, status=403)
174 | env_info = RiceEnvConfig().read_env()
175 | return web.json_response(env_info, status=200)
176 |
177 |
178 | @routes.get("/riceround/logout")
179 | async def logout(request):
180 | AuthUnit().clear_user_token()
181 | return aiohttp.web.json_response({"status": "success"}, status=200)
182 |
183 |
184 | @routes.post("/riceround/set_auto_overwrite")
185 | async def set_auto_overwrite(request):
186 | data = await request.json()
187 | auto_overwrite = data.get("auto_overwrite")
188 | RicePromptInfo().set_auto_overwrite(auto_overwrite)
189 | return web.json_response({"status": "success"}, status=200)
190 |
191 |
192 | @routes.post("/riceround/set_auto_publish")
193 | async def set_auto_publish(request):
194 | data = await request.json()
195 | auto_publish = data.get("auto_publish")
196 | RicePromptInfo().set_auto_publish(auto_publish)
197 | return web.json_response({"status": "success"}, status=200)
198 |
199 |
200 | @routes.post("/riceround/set_wait_time")
201 | async def set_wait_time(request):
202 | data = await request.json()
203 | wait_time = data.get("wait_time")
204 | RicePromptInfo().set_wait_time(wait_time)
205 | return web.json_response({"status": "success"}, status=200)
206 |
207 |
208 | @routes.post("/riceround/install_choice_node")
209 | async def install_choice_node(request):
210 | async def delayed_restart():
211 | await asyncio.sleep(3)
212 | restart_comfyui()
213 |
214 | data = await request.json()
215 | template_id = data.get("template_id")
216 | need_reboot = data.get("need_reboot", False)
217 | if not template_id:
218 | return aiohttp.web.json_response(
219 | {"status": "failed", "message": "template_id is required"}, status=400
220 | )
221 | if RicePromptInfo().install_choice_node(template_id):
222 | if need_reboot:
223 | asyncio.create_task(delayed_restart())
224 | return aiohttp.web.json_response(
225 | {
226 | "status": "success",
227 | "message": "Installation successful, server will restart in 3 seconds",
228 | },
229 | status=200,
230 | )
231 | return aiohttp.web.json_response(
232 | {"status": "failed", "message": "Installation failed"}, status=400
233 | )
234 |
235 |
236 | @routes.post("/riceround/export_toml")
237 | async def export_toml(request):
238 | data = await request.json()
239 | secret_token = data.get("secret_token")
240 | if not secret_token:
241 | return aiohttp.web.json_response(
242 | {"status": "failed", "message": "secret_token is required"}, status=400
243 | )
244 | try:
245 | from .rice_install_client import RiceInstallClient
246 |
247 | toml_content = RiceInstallClient().export_toml(secret_token)
248 | return aiohttp.web.Response(
249 | body=toml_content.encode("utf-8"),
250 | headers={
251 | "Content-Type": "application/toml",
252 | "Content-Disposition": 'attachment; filename="client.toml"',
253 | },
254 | )
255 | except Exception as e:
256 | return aiohttp.web.json_response(
257 | {"status": "failed", "message": str(e)}, status=400
258 | )
259 |
260 |
261 | @routes.get("/riceround/fix_toml")
262 | async def fix_toml(request):
263 | try:
264 | from .rice_install_client import RiceInstallClient
265 |
266 | RiceInstallClient().auto_fix_toml()
267 | return aiohttp.web.json_response({"status": "success"}, status=200)
268 | except Exception as e:
269 | return aiohttp.web.json_response(
270 | {"status": "failed", "message": str(e)}, status=400
271 | )
272 |
273 |
274 | is_on_riceround = False
275 | client_random = None
276 | if os.getenv("RICE_ROUND_SERVER") == "true":
277 | is_on_riceround = True
278 | if is_on_riceround:
279 | client_random = os.getenv("RICE_ROUND_CLIENT_RANDOM")
280 |
281 |
282 | @web.middleware
283 | async def check_login_status(request, handler):
284 | if is_on_riceround:
285 | if request.path.startswith("/internal/"):
286 | return await handler(request)
287 | if (
288 | request.headers.get("owner") == "share_client"
289 | and request.headers.get("client_random") == client_random
290 | ):
291 | return await handler(request)
292 | else:
293 | try:
294 | headers = dict(request.headers)
295 | logging.warn(f"### check_login_status failed - Headers: {headers}")
296 | except Exception as e:
297 | logging.warn(
298 | f"### check_login_status failed - Error parsing request: {str(e)}"
299 | )
300 | return web.json_response({"error": "Access denied"}, status=403)
301 | return await handler(request)
302 |
303 |
304 | if is_on_riceround == True:
305 | PromptServer.instance.app.middlewares.append(check_login_status)
306 |
--------------------------------------------------------------------------------
/auth_unit.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import time
4 | import requests
5 | import configparser
6 | from .rice_def import RiceRoundErrorDef
7 | from .utils import get_local_app_setting_path, get_machine_id, generate_random_string
8 | from .rice_url_config import RiceUrlConfig
9 | from server import PromptServer
10 |
11 |
12 | class AuthUnit:
13 | _instance = None
14 |
15 | def __new__(cls, *args, **kwargs):
16 | if cls._instance is None:
17 | cls._instance = super(AuthUnit, cls).__new__(cls)
18 | return cls._instance
19 |
20 | def __init__(self):
21 | if not hasattr(self, "initialized"):
22 | self.machine_id = get_machine_id()
23 | local_app_path = get_local_app_setting_path()
24 | local_app_path.mkdir(parents=True, exist_ok=True)
25 | self.config_path = local_app_path / "config.ini"
26 | self.last_check_time = 0
27 | self.initialized = True
28 | self.user_id = 0
29 |
30 | def empty_token(self, need_clear=False):
31 | self.token = ""
32 | self.last_check_time = 0
33 | if need_clear:
34 | self.clear_user_token()
35 |
36 | def get_user_token(self):
37 | self.token = self.read_user_token()
38 | if (
39 | time.time() - self.last_check_time > 120
40 | and self.token
41 | and len(self.token) > 50
42 | ):
43 | try:
44 | headers = {
45 | "Content-Type": "application/json",
46 | "Authorization": f"Bearer {self.token}",
47 | }
48 | response = requests.get(
49 | RiceUrlConfig().get_info_url, headers=headers, timeout=10
50 | )
51 | if response.status_code == 200:
52 | user_info = response.json()
53 | try:
54 | self.user_id = int(user_info.get("user_id", 0) or 0)
55 | except (ValueError, TypeError):
56 | self.user_id = 0
57 | self.last_check_time = time.time()
58 | return self.token, "", RiceRoundErrorDef.SUCCESS
59 | else:
60 | logging.warn(
61 | f"get_user_token failed, {response.status_code} {response.text}"
62 | )
63 | error_message = "登录结果错误"
64 | error_code = RiceRoundErrorDef.UNKNOWN_ERROR
65 | try:
66 | response_data = response.json()
67 | if "message" in response_data:
68 | error_message = response_data["message"]
69 | except ValueError:
70 | pass
71 | if response.status_code == 401:
72 | error_message = "登录已过期,请重新登录"
73 | error_code = RiceRoundErrorDef.HTTP_UNAUTHORIZED
74 | elif response.status_code == 500:
75 | error_message = "服务器内部错误,请稍后重试"
76 | error_code = RiceRoundErrorDef.HTTP_INTERNAL_ERROR
77 | elif response.status_code == 503:
78 | error_message = "服务不可用,请稍后重试"
79 | error_code = RiceRoundErrorDef.HTTP_SERVICE_UNAVAILABLE
80 | self.empty_token(response.status_code == 401)
81 | return None, error_message, error_code
82 | except requests.exceptions.Timeout:
83 | self.empty_token()
84 | return None, "请求超时,请检查网络连接", RiceRoundErrorDef.HTTP_TIMEOUT
85 | except requests.exceptions.ConnectionError:
86 | self.empty_token()
87 | return None, "网络连接失败,请检查网络", RiceRoundErrorDef.NETWORK_ERROR
88 | except requests.exceptions.RequestException as e:
89 | self.empty_token()
90 | return None, f"请求失败: {str(e)}", RiceRoundErrorDef.REQUEST_ERROR
91 | if self.token and len(self.token) > 50:
92 | return self.token, "", RiceRoundErrorDef.SUCCESS
93 | return None, "未读取到有效的token,请重新登录", RiceRoundErrorDef.NO_TOKEN_ERROR
94 |
95 | def get_user_info(self):
96 | _, error_message, error_code = self.get_user_token()
97 | if error_code == RiceRoundErrorDef.SUCCESS and self.user_id:
98 | return error_code, self.user_id
99 | else:
100 | return error_code, error_message
101 |
102 | def login_dialog(self, title=""):
103 | self.client_key = generate_random_string(8)
104 | PromptServer.instance.send_sync(
105 | "riceround_login_dialog",
106 | {
107 | "machine_id": self.machine_id,
108 | "client_key": self.client_key,
109 | "title": title,
110 | },
111 | )
112 |
113 | def read_user_token(self):
114 | if not os.path.exists(self.config_path):
115 | return ""
116 | try:
117 | config = configparser.ConfigParser()
118 | config.read(self.config_path, encoding="utf-8")
119 | return config.get("Auth", "user_token", fallback="")
120 | except Exception as e:
121 | print(f"Error reading token: {e}")
122 | return ""
123 |
124 | def set_user_token(self, user_token, client_key):
125 | if not client_key or self.client_key != client_key:
126 | logging.warn(f"client_key is not match, {self.client_key} != {client_key}")
127 | return
128 | if not user_token:
129 | user_token = ""
130 | print("user_token is empty")
131 | self.save_user_token(user_token)
132 |
133 | def save_user_token(self, user_token):
134 | try:
135 | config = configparser.ConfigParser()
136 | try:
137 | if os.path.exists(self.config_path):
138 | config.read(self.config_path, encoding="utf-8")
139 | except Exception as read_error:
140 | print(f"Warning: Error reading existing config: {read_error}")
141 | if "Auth" not in config:
142 | config.add_section("Auth")
143 | config["Auth"]["user_token"] = user_token
144 | with open(self.config_path, "w", encoding="utf-8") as f:
145 | config.write(f)
146 | except Exception as e:
147 | print(f"Error saving token: {e}")
148 | raise RuntimeError(f"Failed to save token: {e}")
149 |
150 | def set_long_token(self, long_token):
151 | if not long_token:
152 | return
153 | self.save_user_token(long_token)
154 | self.client_key = ""
155 |
156 | def clear_user_token(self):
157 | PromptServer.instance.send_sync(
158 | "riceround_clear_user_info", {"clear_key": "all"}
159 | )
160 | if os.path.exists(self.config_path):
161 | try:
162 | config = configparser.ConfigParser()
163 | config.read(self.config_path, encoding="utf-8")
164 | if "Auth" not in config:
165 | return
166 | if "user_token" not in config["Auth"]:
167 | return
168 | config["Auth"]["user_token"] = ""
169 | with open(self.config_path, "w", encoding="utf-8") as f:
170 | config.write(f)
171 | except Exception as e:
172 | print(f"Error clearing token: {e}")
173 | raise RuntimeError(f"Failed to clear token: {e}")
174 |
--------------------------------------------------------------------------------
/docs/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/docs/menu.png
--------------------------------------------------------------------------------
/docs/output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/docs/output.png
--------------------------------------------------------------------------------
/docs/web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/docs/web.png
--------------------------------------------------------------------------------
/docs/wechat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/docs/wechat.jpg
--------------------------------------------------------------------------------
/docs/workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/docs/workflow.png
--------------------------------------------------------------------------------
/encrypt_node.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | import copy
3 | from io import BytesIO
4 | import json
5 | import logging
6 | import os
7 | import random
8 | import shutil
9 | import uuid
10 | import numpy as np
11 | import comfy.utils
12 | import time
13 | from PIL import Image
14 | from .rice_def import RiceRoundErrorDef
15 | from .auth_unit import AuthUnit
16 | from .publish import Publish
17 | from .utils import combine_files
18 | from .rice_url_config import machine_upload_image
19 | import folder_paths
20 | from server import PromptServer
21 | from .rice_url_config import RiceUrlConfig
22 | from .rice_prompt_info import RicePromptInfo
23 |
24 | output_project_folder = folder_paths.output_directory
25 | INPUT_NODE_TYPES = [
26 | "RiceRoundSimpleChoiceNode",
27 | "RiceRoundAdvancedChoiceNode",
28 | "RiceRoundSimpleImageNode",
29 | "RiceRoundImageNode",
30 | "RiceRoundDownloadImageNode",
31 | "RiceRoundImageBridgeNode",
32 | "RiceRoundInputTextNode",
33 | "RiceRoundMaskBridgeNode",
34 | "RiceRoundDownloadMaskNode",
35 | "RiceRoundIntNode",
36 | "RiceRoundFloatNode",
37 | "RiceRoundStrToIntNode",
38 | "RiceRoundStrToFloatNode",
39 | "RiceRoundBooleanNode",
40 | "RiceRoundStrToBooleanNode",
41 | ]
42 |
43 |
44 | class RiceRoundEncryptNode:
45 | def __init__(self):
46 | self.template_id = uuid.uuid4().hex
47 | self.output_dir = folder_paths.get_temp_directory()
48 | self.type = "temp"
49 | self.prefix_append = "_temp_" + "".join(
50 | random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)
51 | )
52 | self.compress_level = 4
53 |
54 | @classmethod
55 | def INPUT_TYPES(s):
56 | return {
57 | "required": {
58 | "project_name": ("STRING", {"default": "my_project"}),
59 | "template_id": ("STRING", {"default": uuid.uuid4().hex}),
60 | "images": ("IMAGE",),
61 | },
62 | "hidden": {
63 | "unique_id": "UNIQUE_ID",
64 | "prompt": "PROMPT",
65 | "extra_pnginfo": "EXTRA_PNGINFO",
66 | },
67 | }
68 |
69 | @classmethod
70 | def IS_CHANGED(cls, **kwargs):
71 | return float("NaN")
72 |
73 | RETURN_TYPES = ()
74 | OUTPUT_NODE = True
75 | FUNCTION = "encrypt"
76 | CATEGORY = "RiceRound"
77 |
78 | def encrypt(self, project_name, template_id, images, **kwargs):
79 | unique_id = kwargs.pop("unique_id", None)
80 | extra_pnginfo = kwargs.pop("extra_pnginfo", None)
81 | prompt = kwargs.pop("prompt", None)
82 | encrypt = Encrypt(extra_pnginfo["workflow"], prompt, project_name, template_id)
83 | publish_folder = encrypt.do_encrypt()
84 | filename_prefix = "rice_round"
85 | filename_prefix += self.prefix_append
86 | (
87 | full_output_folder,
88 | filename,
89 | counter,
90 | subfolder,
91 | filename_prefix,
92 | ) = folder_paths.get_save_image_path(
93 | filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
94 | )
95 | results = list()
96 | preview_path = None
97 | for batch_number, image in enumerate(images):
98 | i = 255.0 * image.cpu().numpy()
99 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
100 | if batch_number == 0:
101 | preview_path = os.path.join(publish_folder, "preview.png")
102 | img.save(preview_path)
103 | filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
104 | file = f"{filename_with_batch_num}_{counter:05}_.png"
105 | img.save(
106 | os.path.join(full_output_folder, file),
107 | compress_level=self.compress_level,
108 | )
109 | results.append(
110 | {"filename": file, "subfolder": subfolder, "type": self.type}
111 | )
112 | counter += 1
113 | auto_publish = RicePromptInfo().get_auto_publish()
114 | if auto_publish:
115 | publish = Publish(publish_folder)
116 | user_token, error_msg, error_code = AuthUnit().get_user_token()
117 | if not user_token:
118 | print(f"riceround get user token failed, {error_msg}")
119 | if (
120 | error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED
121 | or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR
122 | ):
123 | AuthUnit().login_dialog("安装节点需要先完成登录")
124 | else:
125 | PromptServer.instance.send_sync(
126 | "riceround_toast",
127 | {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"},
128 | )
129 | return {}
130 | else:
131 | publish_result = publish.publish(
132 | user_token,
133 | template_id,
134 | project_name,
135 | preview_path,
136 | os.path.join(publish_folder, f"{template_id}.bin"),
137 | )
138 | if not publish_result:
139 | PromptServer.instance.send_sync(
140 | "riceround_toast", {"content": "发布失败,请检查发布步骤", "type": "error"}
141 | )
142 | else:
143 | PromptServer.instance.send_sync(
144 | "riceround_toast",
145 | {"content": "发布成功,请启动client", "type": "success"},
146 | )
147 | try:
148 | from .rice_install_client import RiceInstallClient
149 |
150 | is_installed = RiceInstallClient().is_client_installed()
151 | is_running = RiceInstallClient().is_client_running()
152 | PromptServer.instance.send_sync(
153 | "riceround_client_install_dialog",
154 | {"is_installed": is_installed, "is_running": is_running},
155 | )
156 | except Exception as e:
157 | logging.error(f"repair client error: {e}")
158 | logging.info(f"Debug - results length: {len(results)}, images: {results}")
159 | return {"ui": {"images": results}}
160 |
161 |
162 | class RiceRoundOutputImageNode:
163 | def __init__(self):
164 | self.url_config = RiceUrlConfig()
165 |
166 | @classmethod
167 | def INPUT_TYPES(s):
168 | return {
169 | "required": {"images": ("IMAGE",), "task_id": ("STRING", {"default": ""})},
170 | "optional": {"template_id": ("STRING", {"default": ""})},
171 | "hidden": {
172 | "unique_id": "UNIQUE_ID",
173 | "prompt": "PROMPT",
174 | "extra_pnginfo": "EXTRA_PNGINFO",
175 | },
176 | }
177 |
178 | RETURN_TYPES = ()
179 | OUTPUT_NODE = True
180 | FUNCTION = "load"
181 | CATEGORY = "__hidden__"
182 |
183 | def load(self, images, task_id, template_id, **kwargs):
184 | unique_id = kwargs.pop("unique_id", None)
185 | prompt = kwargs.pop("prompt", None)
186 | extra_pnginfo = kwargs.pop("extra_pnginfo", None)
187 | client_id = PromptServer.instance.client_id
188 | prompt_id = ""
189 | if (
190 | hasattr(PromptServer.instance, "last_prompt_id")
191 | and PromptServer.instance.last_prompt_id
192 | ):
193 | prompt_id = PromptServer.instance.last_prompt_id
194 | if unique_id is None:
195 | raise Exception("Warning: 'unique_id' is missing.")
196 | if prompt is None:
197 | raise Exception("Warning: 'prompt' is missing.")
198 | if not task_id:
199 | raise Exception("Warning: 'task_id' is missing.")
200 | else:
201 | print(f"RiceRoundOutputImageNode task_id: {task_id}")
202 | if images.shape[0] > 5:
203 | raise ValueError("Error: Cannot upload more than 5 images.")
204 | image_results = []
205 | for image in images:
206 | download_url = machine_upload_image(image, task_id)
207 | if not download_url:
208 | raise ValueError("Error: Failed to upload image.")
209 | image_results.append(download_url)
210 | result_data = {"image_type": "PNG", "image_results": image_results}
211 | result_info = {
212 | "task_id": task_id,
213 | "unique_id": unique_id,
214 | "client_id": client_id,
215 | "prompt_id": prompt_id,
216 | "timestamp": int(time.time() * 1000),
217 | "image_type": "PNG",
218 | "result_data": result_data,
219 | }
220 | PromptServer.instance.send_sync(
221 | "rice_round_done", result_info, sid=client_id
222 | )
223 | return {}
224 |
225 |
226 | class Encrypt:
227 | def __init__(self, workflow, prompt, project_name, template_id):
228 | self.original_workflow = workflow
229 | self.original_prompt = prompt
230 | self.template_id = template_id
231 | self.project_name = project_name
232 | self.project_folder = os.path.join(
233 | output_project_folder, self.project_name, self.template_id
234 | )
235 | if not os.path.exists(self.project_folder):
236 | os.makedirs(self.project_folder)
237 | self.output_folder = os.path.join(self.project_folder, "output")
238 | if not os.path.exists(self.output_folder):
239 | os.makedirs(self.output_folder)
240 | self.publish_folder = os.path.join(self.project_folder, "publish")
241 | if not os.path.exists(self.publish_folder):
242 | os.makedirs(self.publish_folder)
243 | self.last_node_id = 0
244 | self.last_link_id = 0
245 | self.link_owner_map = defaultdict(dict)
246 | self.workflow_nodes_dict = {}
247 | self.node_prompt_map = {}
248 | self.input_node_map = {}
249 | self.related_node_ids = set()
250 |
251 | def do_encrypt(self):
252 | self.load_workflow()
253 | self.load_prompt()
254 | self.analyze_input_from_workflow()
255 | self.assemble_new_workflow()
256 | self.output_template_json_file()
257 | self.assemble_new_prompt()
258 | self.output_file(self.original_workflow, f"original_workflow")
259 | self.output_file(self.original_prompt, f"original_prompt")
260 | self.save_rice_zip()
261 | self.clear()
262 | return self.publish_folder
263 |
264 | def clear(self):
265 | self.original_workflow = None
266 | self.original_prompt = None
267 | self.template_id = None
268 | self.project_name = None
269 | self.project_folder = None
270 | self.last_node_id = 0
271 | self.last_link_id = 0
272 | RicePromptInfo().clear()
273 |
274 | def load_workflow(self):
275 | simplify_workflow = copy.deepcopy(self.original_workflow)
276 | self.workflow_nodes_dict = {
277 | int(node["id"]): node for node in simplify_workflow["nodes"]
278 | }
279 | for node in simplify_workflow["nodes"]:
280 | output_nodes = node.get("outputs", [])
281 | if not output_nodes:
282 | continue
283 | for output in output_nodes:
284 | links = output.get("links", [])
285 | if not links:
286 | continue
287 | for link in links:
288 | link = int(link)
289 | self.link_owner_map[link]["links"] = copy.deepcopy(links)
290 | self.link_owner_map[link]["slot_index"] = output.get(
291 | "slot_index", 0
292 | )
293 | self.link_owner_map[link]["owner_id"] = int(node["id"])
294 | self.link_owner_map[link]["type"] = output.get("type", "")
295 | self.last_node_id = int(simplify_workflow["last_node_id"])
296 | self.last_link_id = int(simplify_workflow["last_link_id"])
297 |
298 | def load_prompt(self):
299 | simplify_prompt = copy.deepcopy(self.original_prompt)
300 | self.node_prompt_map = {
301 | int(node_id): node for (node_id, node) in simplify_prompt.items()
302 | }
303 |
304 | def analyze_input_from_workflow(self):
305 | for id, node in self.workflow_nodes_dict.items():
306 | class_type = node.get("type", "")
307 | if class_type in INPUT_NODE_TYPES:
308 | self.input_node_map[id] = copy.deepcopy(node)
309 | output_nodes = node.get("outputs", [])
310 | if not output_nodes:
311 | continue
312 | links = output_nodes[0].get("links", [])
313 | if not links:
314 | continue
315 | link_id = int(links[0])
316 | self.input_node_map[id]["main_link_id"] = link_id
317 | self.input_node_map[id]["main_link_type"] = output_nodes[0].get(
318 | "type", "STRING"
319 | )
320 | self.input_node_map = {
321 | k: v for (k, v) in sorted(self.input_node_map.items(), key=lambda x: x[0])
322 | }
323 |
324 | def assemble_new_workflow(self):
325 | input_node_ids = list(self.input_node_map.keys())
326 | new_simplify_workflow = copy.deepcopy(self.original_workflow)
327 | self.related_node_ids = self.find_workflow_related_nodes(
328 | new_simplify_workflow["links"], input_node_ids
329 | )
330 | new_simplify_workflow["nodes"] = [
331 | node
332 | for node in new_simplify_workflow["nodes"]
333 | if int(node["id"]) in self.related_node_ids
334 | ]
335 | self.invalid_new_workflow(new_simplify_workflow)
336 | new_node_ids = self.add_decrypt_node(new_simplify_workflow)
337 | self.remove_redundant_links(new_simplify_workflow)
338 | self.remove_unrelated_nodes(
339 | new_simplify_workflow, self.related_node_ids, new_node_ids
340 | )
341 | self.replace_choice_template(new_simplify_workflow)
342 | self.replace_workflow_node(new_simplify_workflow)
343 | self.output_file(new_simplify_workflow, f"{self.template_id}_workflow")
344 |
345 | def output_template_json_file(self):
346 | system_default_title = set()
347 | try:
348 | from nodes import NODE_DISPLAY_NAME_MAPPINGS
349 |
350 | for k, v in NODE_DISPLAY_NAME_MAPPINGS.items():
351 | system_default_title.add(k)
352 | system_default_title.add(v)
353 | except ImportError:
354 | print("Warning: Could not import NODE_DISPLAY_NAME_MAPPINGS")
355 | rice_prompt_info = RicePromptInfo()
356 | elements = []
357 | for node_id, node in self.input_node_map.items():
358 | input_number = node["input_anything"]
359 | owner_node_type = self.workflow_nodes_dict[node_id]["type"]
360 | node_prompt_inputs = self.node_prompt_map[node_id].get("inputs", {})
361 | label_name = str(node_prompt_inputs.get("name", ""))
362 | if not label_name:
363 | label_name = (
364 | self.node_prompt_map[node_id].get("_meta", {}).get("title", "")
365 | )
366 | item = {
367 | "id": str(input_number),
368 | "type": "",
369 | "describe": "输入组件",
370 | "node_id": str(node_id),
371 | "settings": {},
372 | }
373 | if owner_node_type in [
374 | "RiceRoundSimpleImageNode",
375 | "RiceRoundDownloadImageNode",
376 | "RiceRoundImageBridgeNode",
377 | ]:
378 | item["type"] = "image_upload"
379 | item["describe"] = "请上传图片"
380 | item["settings"] = {
381 | "accept": "image/*",
382 | "max_size": 500000,
383 | "tip": "请上传不超过500KB的图片",
384 | }
385 | elif owner_node_type == "RiceRoundImageNode":
386 | item["type"] = "mask_image_upload"
387 | item["describe"] = "请上传图片并编辑蒙版"
388 | item["settings"] = {
389 | "accept": "image/*",
390 | "max_size": 500000,
391 | "tip": "请上传不超过500KB的图片",
392 | "mask": True,
393 | }
394 | elif owner_node_type in [
395 | "RiceRoundMaskBridgeNode",
396 | "RiceRoundDownloadMaskNode",
397 | ]:
398 | item["type"] = "mask_upload"
399 | item["describe"] = "请上传蒙版"
400 | item["settings"] = {
401 | "accept": "image/*",
402 | "max_size": 50000,
403 | "tip": "请上传不超过50KB的图片",
404 | }
405 | elif owner_node_type == "RiceRoundInputTextNode":
406 | item["type"] = "text"
407 | item["describe"] = "提示词"
408 | item["settings"] = {"placeholder": "请描述图片内容", "multiline": True}
409 | elif (
410 | owner_node_type == "RiceRoundSimpleChoiceNode"
411 | or owner_node_type == "RiceRoundAdvancedChoiceNode"
412 | ):
413 | item["type"] = "choice"
414 | item["describe"] = "模型选择"
415 | item["settings"] = {
416 | "options": rice_prompt_info.get_choice_value(node_id),
417 | "default": node_prompt_inputs.get("default", ""),
418 | }
419 | item["addition"] = rice_prompt_info.get_choice_node_addition(node_id)
420 | elif (
421 | owner_node_type == "RiceRoundIntNode"
422 | or owner_node_type == "RiceRoundStrToIntNode"
423 | ):
424 | item["type"] = "number_int"
425 | item["describe"] = "数值"
426 | item["settings"] = {
427 | "min": node_prompt_inputs.get("min", 0),
428 | "max": node_prompt_inputs.get("max", 1000),
429 | "number": node_prompt_inputs.get("number", 0),
430 | }
431 | elif (
432 | owner_node_type == "RiceRoundFloatNode"
433 | or owner_node_type == "RiceRoundStrToFloatNode"
434 | ):
435 | item["type"] = "number_float"
436 | item["describe"] = "数值"
437 | item["settings"] = {
438 | "min": node_prompt_inputs.get("min", 0.0),
439 | "max": node_prompt_inputs.get("max", 1e3),
440 | "number": node_prompt_inputs.get("number", 0.0),
441 | }
442 | elif (
443 | owner_node_type == "RiceRoundBooleanNode"
444 | or owner_node_type == "RiceRoundStrToBooleanNode"
445 | ):
446 | item["type"] = "switch"
447 | item["describe"] = "开关"
448 | item["settings"] = {"default": node_prompt_inputs.get("value", False)}
449 | else:
450 | raise ValueError(
451 | f"Error: The node {node_id} is not a valid RiceRound node."
452 | )
453 | if label_name and label_name not in system_default_title:
454 | item["describe"] = label_name
455 | elements.append(item)
456 | json_dict = {"template_id": self.template_id, "elements": elements}
457 | self.output_file(json_dict, f"{self.template_id}_template")
458 |
459 | def assemble_new_prompt(self):
460 | new_prompt = self._create_filtered_prompt()
461 | self._replace_encrypt_node(new_prompt)
462 | self._transform_node_types(new_prompt)
463 | self.output_file(new_prompt, f"{self.template_id}_job")
464 |
465 | def _create_filtered_prompt(self):
466 | new_prompt = copy.deepcopy(self.original_prompt)
467 | exclude_node_ids = self._get_exclude_node_ids(new_prompt)
468 | for node_id in exclude_node_ids:
469 | new_prompt.pop(str(node_id), None)
470 | return new_prompt
471 |
472 | def _replace_encrypt_node(self, new_prompt):
473 | for node_id, node in new_prompt.items():
474 | class_type = node.get("class_type", "")
475 | print(f"class_type: {class_type}")
476 | if class_type == "RiceRoundEncryptNode":
477 | node["class_type"] = "RiceRoundOutputImageNode"
478 | node["inputs"]["task_id"] = ""
479 | node["inputs"].pop("project_name", None)
480 | if "_meta" in node and "title" in node["_meta"]:
481 | node["_meta"]["title"] = "RiceRoundOutputImageNode"
482 |
483 | def save_rice_zip(self):
484 | import pyzipper
485 |
486 | try:
487 | files_to_zip = []
488 | for i, file in enumerate(
489 | [
490 | f"{self.template_id}_job.json",
491 | f"{self.template_id}_template.json",
492 | f"{self.template_id}_workflow.json",
493 | "original_workflow.json",
494 | "original_prompt.json",
495 | ]
496 | ):
497 | src_path = os.path.join(self.output_folder, file)
498 | files_to_zip.append((src_path, f"{i}.bin"))
499 | zip_file_path = os.path.join(self.publish_folder, f"{self.template_id}.bin")
500 | with pyzipper.AESZipFile(
501 | zip_file_path,
502 | "w",
503 | compression=pyzipper.ZIP_DEFLATED,
504 | encryption=pyzipper.WZ_AES,
505 | ) as zipf:
506 | zipf.setpassword(self.template_id.encode())
507 | for file_path, arcname in files_to_zip:
508 | zipf.write(file_path, arcname)
509 | shutil.copy2(
510 | os.path.join(self.output_folder, f"{self.template_id}_template.json"),
511 | os.path.join(self.publish_folder, "template.json"),
512 | )
513 | shutil.copy2(
514 | os.path.join(self.output_folder, f"{self.template_id}_workflow.json"),
515 | os.path.join(self.project_folder, "workflow.json"),
516 | )
517 | except Exception as e:
518 | print(f"Error creating zip: {str(e)}")
519 | raise
520 |
521 | def _get_exclude_node_ids(self, prompt):
522 | EXCLUDE_NODE_TYPES = {"RiceRoundDecryptNode"}
523 | exclude_ids = self.related_node_ids.difference(set(self.input_node_map.keys()))
524 | for node_id, node in prompt.items():
525 | if node.get("class_type", "") in EXCLUDE_NODE_TYPES:
526 | exclude_ids.add(int(node_id))
527 | return exclude_ids
528 |
529 | def _transform_node_types(self, prompt):
530 | NODE_TYPE_MAPPING = {
531 | "RiceRoundImageBridgeNode": {
532 | "new_type": "RiceRoundDownloadImageNode",
533 | "new_inputs": {"image_url": ""},
534 | },
535 | "RiceRoundSimpleImageNode": {
536 | "new_type": "RiceRoundDownloadImageNode",
537 | "new_inputs": {"image_url": ""},
538 | },
539 | "RiceRoundImageNode": {
540 | "new_type": "RiceRoundDownloadImageAndMaskNode",
541 | "new_inputs": {"image_url": ""},
542 | },
543 | "RiceRoundMaskBridgeNode": {
544 | "new_type": "RiceRoundDownloadMaskNode",
545 | "new_inputs": {"mask_url": ""},
546 | },
547 | "RiceRoundIntNode": {
548 | "new_type": "RiceRoundStrToIntNode",
549 | "new_inputs": {"str": ""},
550 | },
551 | "RiceRoundFloatNode": {
552 | "new_type": "RiceRoundStrToFloatNode",
553 | "new_inputs": {"str": ""},
554 | },
555 | "RiceRoundBooleanNode": {
556 | "new_type": "RiceRoundStrToBooleanNode",
557 | "new_inputs": {"str": ""},
558 | },
559 | }
560 | for node_id, node in prompt.items():
561 | node.pop("is_changed", None)
562 | node_type = node.get("class_type", "")
563 | node_inputs = node.get("inputs", {})
564 | if not node_inputs:
565 | continue
566 | label_name = node_inputs.get("name", "")
567 | if node_type in NODE_TYPE_MAPPING:
568 | mapping = NODE_TYPE_MAPPING[node_type]
569 | node["class_type"] = mapping["new_type"]
570 | node["inputs"] = mapping["new_inputs"].copy()
571 | if label_name:
572 | node["inputs"]["name"] = label_name
573 |
574 | def add_decrypt_node(self, workflow):
575 | new_node_ids = set()
576 | self.last_node_id += 1
577 | decrypt_node_id = self.last_node_id
578 | decrypt_to_save_link_id = self.last_link_id + 1
579 | encrypt_node = {
580 | "id": decrypt_node_id,
581 | "type": "RiceRoundDecryptNode",
582 | "pos": [420, 0],
583 | "size": [500, 150],
584 | "flags": {},
585 | "mode": 0,
586 | "order": 20,
587 | "inputs": [],
588 | "outputs": [
589 | {
590 | "name": "IMAGE",
591 | "type": "IMAGE",
592 | "links": [],
593 | "label": "IMAGE",
594 | "slot_index": 0,
595 | }
596 | ],
597 | "properties": {
598 | "Node name for S&R": "RiceRoundDecryptNode",
599 | "cnr_id": "comfyui_riceround",
600 | },
601 | "widgets_values": [str(self.template_id), 735127949069071, "randomize"],
602 | }
603 | for idx, (owner_id, owner_node) in enumerate(self.input_node_map.items()):
604 | link_id = owner_node["main_link_id"]
605 | link_type = owner_node["main_link_type"]
606 | owner_node["input_anything"] = idx
607 | input_entry = {
608 | "name": f"input_anything{idx if idx>0 else''} ({owner_id})",
609 | "type": "*",
610 | "link": link_id,
611 | "label": f"input_anything{idx if idx>0 else''} ({owner_id})",
612 | }
613 | if idx == 0:
614 | input_entry["shape"] = 7
615 | encrypt_node["inputs"].append(input_entry)
616 | if link_type not in ["IMAGE", "STRING"]:
617 | link_type = "STRING"
618 | links = [link_id, owner_id, 0, decrypt_node_id, idx, link_type]
619 | workflow["links"].append(links)
620 | self.last_node_id += 1
621 | save_image_node = {
622 | "id": self.last_node_id,
623 | "type": "SaveImage",
624 | "pos": [982, 5],
625 | "size": [315, 58],
626 | "flags": {},
627 | "order": 21,
628 | "mode": 0,
629 | "inputs": [
630 | {
631 | "name": "images",
632 | "type": "IMAGE",
633 | "link": decrypt_to_save_link_id,
634 | "label": "图像",
635 | }
636 | ],
637 | "outputs": [],
638 | "properties": {"Node name for S&R": "SaveImage", "cnr_id": "comfy-core"},
639 | "widgets_values": ["ComfyUI"],
640 | }
641 | encrypt_node["outputs"][0]["links"] = [decrypt_to_save_link_id]
642 | workflow["links"].append(
643 | [decrypt_to_save_link_id, decrypt_node_id, 0, self.last_node_id, 0, "IMAGE"]
644 | )
645 | new_node_ids.add(decrypt_node_id)
646 | new_node_ids.add(self.last_node_id)
647 | workflow["nodes"].extend([encrypt_node, save_image_node])
648 | workflow["last_node_id"] = self.last_node_id
649 | workflow["last_link_id"] = decrypt_to_save_link_id
650 | return new_node_ids
651 |
652 | def output_file(self, workflow, prefix):
653 | json_file_path = os.path.join(self.output_folder, f"{prefix}.json")
654 | with open(json_file_path, "w", encoding="utf-8") as f:
655 | json.dump(workflow, f, ensure_ascii=False, indent=4)
656 |
657 | def remove_redundant_links(self, workflow):
658 | delete_links = set()
659 | for node in workflow["nodes"]:
660 | node_id = int(node["id"])
661 | if node_id in self.input_node_map:
662 | main_link_id = self.input_node_map[node_id]["main_link_id"]
663 | outputs = node.get("outputs", [])
664 | if not outputs:
665 | continue
666 | for output in outputs:
667 | links = output.get("links", [])
668 | if not links:
669 | continue
670 | for link in links:
671 | if link != main_link_id:
672 | delete_links.add(link)
673 | outputs[0]["links"] = [main_link_id]
674 | workflow["links"] = [
675 | link
676 | for link in workflow["links"]
677 | if isinstance(link, list) and len(link) == 6 and link[0] not in delete_links
678 | ]
679 |
680 | def replace_choice_template(self, workflow):
681 | rice_prompt_info = RicePromptInfo()
682 | for node in workflow["nodes"]:
683 | node_id = int(node["id"])
684 | if node.get("type", "") == "RiceRoundAdvancedChoiceNode":
685 | new_node_type = rice_prompt_info.get_choice_classname(node_id)
686 | if new_node_type:
687 | node["type"] = new_node_type
688 | else:
689 | print(
690 | f"Warning: The node {node_id} is not a valid RiceRound Choice node."
691 | )
692 | choice_node_map = {}
693 | for node_id, node in self.input_node_map.items():
694 | if node.get("type", "") == "RiceRoundSimpleChoiceNode":
695 | choice_value = rice_prompt_info.get_choice_value(node_id)
696 | choice_node_map[node_id] = choice_value
697 | if "extra" not in workflow:
698 | workflow["extra"] = {}
699 | workflow["extra"]["choice_node_map"] = choice_node_map
700 |
701 | def replace_workflow_node(self, workflow):
702 | NODE_TYPE_MAPPING = {
703 | "RiceRoundImageBridgeNode": ("RiceRoundOutputImageBridgeNode", ""),
704 | "RiceRoundSimpleImageNode": ("RiceRoundUploadImageNode", ""),
705 | "RiceRoundImageNode": ("RiceRoundUploadImageNode", "Image&Mask"),
706 | "RiceRoundDownloadImageNode": ("RiceRoundImageUrlNode", ""),
707 | "RiceRoundMaskBridgeNode": ("RiceRoundOutputMaskBridgeNode", ""),
708 | "RiceRoundDownloadMaskNode": ("RiceRoundMaskUrlNode", ""),
709 | "RiceRoundIntNode": ("RiceRoundOutputIntNode", ""),
710 | "RiceRoundFloatNode": ("RiceRoundOutputFloatNode", ""),
711 | "RiceRoundBooleanNode": ("RiceRoundOutputBooleanNode", ""),
712 | "RiceRoundStrToBooleanNode": ("RiceRoundOutputTextNode", ""),
713 | "RiceRoundStrToIntNode": ("RiceRoundOutputTextNode", ""),
714 | "RiceRoundStrToFloatNode": ("RiceRoundOutputTextNode", ""),
715 | }
716 | replace_node_ids = set()
717 | for node in workflow["nodes"]:
718 | node_type = node.get("type", "")
719 | if node_type in NODE_TYPE_MAPPING:
720 | if "outputs" not in node:
721 | raise ValueError(f"Node {node.get('id','unknown')} missing outputs")
722 | if not node["outputs"] or not isinstance(node["outputs"], list):
723 | raise ValueError(
724 | f"Invalid outputs format in node {node.get('id','unknown')}"
725 | )
726 | new_type = NODE_TYPE_MAPPING[node_type][0]
727 | new_name = (
728 | new_type
729 | if NODE_TYPE_MAPPING[node_type][1] == ""
730 | else NODE_TYPE_MAPPING[node_type][1]
731 | )
732 | node.update(
733 | {
734 | "name": new_name,
735 | "type": new_type,
736 | "outputs": [{"type": "STRING", **node["outputs"][0]}],
737 | "properties": {"Node name for S&R": new_name},
738 | }
739 | )
740 | replace_node_ids.add(int(node["id"]))
741 | for link in workflow["links"]:
742 | if len(link) == 6 and link[1] in replace_node_ids:
743 | link[5] = "STRING"
744 |
745 | def remove_unrelated_nodes(self, workflow, related_node_ids, new_node_ids):
746 | links = []
747 | combined_node_ids = related_node_ids.union(new_node_ids)
748 | for link in workflow["links"]:
749 | if len(link) == 6:
750 | if link[1] in combined_node_ids and link[3] in combined_node_ids:
751 | links.append(link)
752 | workflow["links"] = links
753 |
754 | def invalid_new_workflow(self, workflow):
755 | for node in workflow["nodes"]:
756 | inputs = node.get("inputs", [])
757 | for input in inputs:
758 | link = int(input.get("link", 0))
759 | owner_id = self.link_owner_map[link]["owner_id"]
760 | owner_node_type = self.workflow_nodes_dict[owner_id]["type"]
761 | if owner_node_type in INPUT_NODE_TYPES:
762 | raise ValueError(
763 | f"Error: The node {node['id']} may have circular references, generation failed."
764 | )
765 |
766 | def find_workflow_related_nodes(self, links, input_ids):
767 | found_ids = set(input_ids)
768 | stack = list(input_ids)
769 | while stack:
770 | current_id = stack.pop()
771 | for link in links:
772 | if len(link) == 6 and link[3] == current_id:
773 | source_id = link[1]
774 | if source_id not in found_ids:
775 | if source_id in self.workflow_nodes_dict:
776 | found_ids.add(source_id)
777 | stack.append(source_id)
778 | return found_ids
779 |
--------------------------------------------------------------------------------
/example_workflows/simple_workflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "last_node_id": 14,
3 | "last_link_id": 13,
4 | "nodes": [
5 | {
6 | "id": 7,
7 | "type": "CLIPTextEncode",
8 | "pos": [
9 | 413,
10 | 389
11 | ],
12 | "size": [
13 | 425.27801513671875,
14 | 180.6060791015625
15 | ],
16 | "flags": {},
17 | "order": 6,
18 | "mode": 0,
19 | "inputs": [
20 | {
21 | "name": "clip",
22 | "label": "CLIP",
23 | "type": "CLIP",
24 | "link": 5
25 | }
26 | ],
27 | "outputs": [
28 | {
29 | "name": "CONDITIONING",
30 | "label": "条件",
31 | "type": "CONDITIONING",
32 | "links": [
33 | 6
34 | ],
35 | "slot_index": 0
36 | }
37 | ],
38 | "properties": {
39 | "cnr_id": "comfy-core",
40 | "ver": "0.3.15",
41 | "Node name for S&R": "CLIPTextEncode"
42 | },
43 | "widgets_values": [
44 | "text, watermark",
45 | [
46 | false,
47 | true
48 | ]
49 | ]
50 | },
51 | {
52 | "id": 3,
53 | "type": "KSampler",
54 | "pos": [
55 | 863,
56 | 186
57 | ],
58 | "size": [
59 | 315,
60 | 474
61 | ],
62 | "flags": {},
63 | "order": 7,
64 | "mode": 0,
65 | "inputs": [
66 | {
67 | "name": "model",
68 | "label": "模型",
69 | "type": "MODEL",
70 | "link": 1
71 | },
72 | {
73 | "name": "positive",
74 | "label": "正面条件",
75 | "type": "CONDITIONING",
76 | "link": 4
77 | },
78 | {
79 | "name": "negative",
80 | "label": "负面条件",
81 | "type": "CONDITIONING",
82 | "link": 6
83 | },
84 | {
85 | "name": "latent_image",
86 | "label": "Latent",
87 | "type": "LATENT",
88 | "link": 2
89 | },
90 | {
91 | "name": "seed",
92 | "label": "随机种",
93 | "type": "INT",
94 | "widget": {
95 | "name": "seed"
96 | },
97 | "link": 13
98 | }
99 | ],
100 | "outputs": [
101 | {
102 | "name": "LATENT",
103 | "label": "Latent",
104 | "type": "LATENT",
105 | "links": [
106 | 7
107 | ],
108 | "slot_index": 0
109 | }
110 | ],
111 | "properties": {
112 | "cnr_id": "comfy-core",
113 | "ver": "0.3.15",
114 | "Node name for S&R": "KSampler"
115 | },
116 | "widgets_values": [
117 | 53308676468181,
118 | "randomize",
119 | 20,
120 | 8,
121 | "euler",
122 | "normal",
123 | 1
124 | ]
125 | },
126 | {
127 | "id": 6,
128 | "type": "CLIPTextEncode",
129 | "pos": [
130 | 415,
131 | 186
132 | ],
133 | "size": [
134 | 422.84503173828125,
135 | 164.31304931640625
136 | ],
137 | "flags": {},
138 | "order": 5,
139 | "mode": 0,
140 | "inputs": [
141 | {
142 | "name": "clip",
143 | "label": "CLIP",
144 | "type": "CLIP",
145 | "link": 3
146 | },
147 | {
148 | "name": "text",
149 | "label": "文本",
150 | "type": "STRING",
151 | "widget": {
152 | "name": "text"
153 | },
154 | "link": 10
155 | }
156 | ],
157 | "outputs": [
158 | {
159 | "name": "CONDITIONING",
160 | "label": "条件",
161 | "type": "CONDITIONING",
162 | "links": [
163 | 4
164 | ],
165 | "slot_index": 0
166 | }
167 | ],
168 | "properties": {
169 | "cnr_id": "comfy-core",
170 | "ver": "0.3.15",
171 | "Node name for S&R": "CLIPTextEncode"
172 | },
173 | "widgets_values": [
174 | "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
175 | [
176 | false,
177 | true
178 | ]
179 | ]
180 | },
181 | {
182 | "id": 4,
183 | "type": "CheckpointLoaderSimple",
184 | "pos": [
185 | -16.503997802734375,
186 | 240.74636840820312
187 | ],
188 | "size": [
189 | 315,
190 | 98
191 | ],
192 | "flags": {},
193 | "order": 4,
194 | "mode": 0,
195 | "inputs": [
196 | {
197 | "name": "ckpt_name",
198 | "label": "Checkpoint名称",
199 | "type": "COMBO",
200 | "widget": {
201 | "name": "ckpt_name"
202 | },
203 | "link": 11
204 | }
205 | ],
206 | "outputs": [
207 | {
208 | "name": "MODEL",
209 | "label": "模型",
210 | "type": "MODEL",
211 | "links": [
212 | 1
213 | ],
214 | "slot_index": 0
215 | },
216 | {
217 | "name": "CLIP",
218 | "label": "CLIP",
219 | "type": "CLIP",
220 | "links": [
221 | 3,
222 | 5
223 | ],
224 | "slot_index": 1
225 | },
226 | {
227 | "name": "VAE",
228 | "label": "VAE",
229 | "type": "VAE",
230 | "links": [
231 | 8
232 | ],
233 | "slot_index": 2
234 | }
235 | ],
236 | "properties": {
237 | "cnr_id": "comfy-core",
238 | "ver": "0.3.15",
239 | "Node name for S&R": "CheckpointLoaderSimple"
240 | },
241 | "widgets_values": [
242 | "v1-5-pruned-emaonly.ckpt"
243 | ]
244 | },
245 | {
246 | "id": 8,
247 | "type": "VAEDecode",
248 | "pos": [
249 | 1209,
250 | 188
251 | ],
252 | "size": [
253 | 210,
254 | 46
255 | ],
256 | "flags": {},
257 | "order": 8,
258 | "mode": 0,
259 | "inputs": [
260 | {
261 | "name": "samples",
262 | "label": "Latent",
263 | "type": "LATENT",
264 | "link": 7
265 | },
266 | {
267 | "name": "vae",
268 | "label": "VAE",
269 | "type": "VAE",
270 | "link": 8
271 | }
272 | ],
273 | "outputs": [
274 | {
275 | "name": "IMAGE",
276 | "label": "图像",
277 | "type": "IMAGE",
278 | "links": [
279 | 12
280 | ],
281 | "slot_index": 0
282 | }
283 | ],
284 | "properties": {
285 | "cnr_id": "comfy-core",
286 | "ver": "0.3.15",
287 | "Node name for S&R": "VAEDecode"
288 | },
289 | "widgets_values": []
290 | },
291 | {
292 | "id": 5,
293 | "type": "EmptyLatentImage",
294 | "pos": [
295 | 473,
296 | 609
297 | ],
298 | "size": [
299 | 315,
300 | 106
301 | ],
302 | "flags": {},
303 | "order": 0,
304 | "mode": 0,
305 | "inputs": [],
306 | "outputs": [
307 | {
308 | "name": "LATENT",
309 | "label": "Latent",
310 | "type": "LATENT",
311 | "links": [
312 | 2
313 | ],
314 | "slot_index": 0
315 | }
316 | ],
317 | "properties": {
318 | "cnr_id": "comfy-core",
319 | "ver": "0.3.15",
320 | "Node name for S&R": "EmptyLatentImage"
321 | },
322 | "widgets_values": [
323 | 512,
324 | 512,
325 | 1
326 | ]
327 | },
328 | {
329 | "id": 11,
330 | "type": "RiceRoundInputTextNode",
331 | "pos": [
332 | -192.8919677734375,
333 | -94.64482116699219
334 | ],
335 | "size": [
336 | 400,
337 | 200
338 | ],
339 | "flags": {},
340 | "order": 1,
341 | "mode": 0,
342 | "inputs": [],
343 | "outputs": [
344 | {
345 | "name": "STRING",
346 | "label": "STRING",
347 | "type": "STRING",
348 | "links": [
349 | 10
350 | ],
351 | "slot_index": 0
352 | }
353 | ],
354 | "properties": {
355 | "cnr_id": "comfyui_riceround",
356 | "ver": "4f9bb568efd29815148c54edf41eb1e5f040098b",
357 | "Node name for S&R": "RiceRoundInputTextNode"
358 | },
359 | "widgets_values": [
360 | "1 dog",
361 | [
362 | false,
363 | true
364 | ]
365 | ]
366 | },
367 | {
368 | "id": 12,
369 | "type": "RiceRoundSimpleChoiceNode",
370 | "pos": [
371 | -410.595458984375,
372 | 246.42388916015625
373 | ],
374 | "size": [
375 | 315,
376 | 82
377 | ],
378 | "flags": {},
379 | "order": 2,
380 | "mode": 0,
381 | "inputs": [],
382 | "outputs": [
383 | {
384 | "name": "value",
385 | "label": "value",
386 | "type": "COMBO",
387 | "links": [
388 | 11
389 | ],
390 | "slot_index": 0
391 | }
392 | ],
393 | "properties": {
394 | "cnr_id": "comfyui_riceround",
395 | "ver": "4f9bb568efd29815148c54edf41eb1e5f040098b",
396 | "Node name for S&R": "RiceRoundSimpleChoiceNode"
397 | },
398 | "widgets_values": [
399 | "ckpt_name",
400 | "1.5\\dreamshaper_8_v8.safetensors"
401 | ]
402 | },
403 | {
404 | "id": 13,
405 | "type": "RiceRoundEncryptNode",
406 | "pos": [
407 | 1510.3780517578125,
408 | 289.9645690917969
409 | ],
410 | "size": [
411 | 400,
412 | 318
413 | ],
414 | "flags": {},
415 | "order": 9,
416 | "mode": 0,
417 | "inputs": [
418 | {
419 | "name": "images",
420 | "label": "images",
421 | "type": "IMAGE",
422 | "link": 12
423 | }
424 | ],
425 | "outputs": [],
426 | "properties": {
427 | "cnr_id": "comfyui_riceround",
428 | "ver": "4f9bb568efd29815148c54edf41eb1e5f040098b",
429 | "Node name for S&R": "RiceRoundEncryptNode"
430 | },
431 | "widgets_values": [
432 | "Unsaved Workflow - ComfyUI",
433 | "7bd02cb89b0fdeb142dcadc58dcc7ba9",
434 | null
435 | ]
436 | },
437 | {
438 | "id": 14,
439 | "type": "RiceRoundRandomSeedNode",
440 | "pos": [
441 | 552.6851196289062,
442 | 88.35224914550781
443 | ],
444 | "size": [
445 | 210,
446 | 26
447 | ],
448 | "flags": {},
449 | "order": 3,
450 | "mode": 0,
451 | "inputs": [],
452 | "outputs": [
453 | {
454 | "name": "INT",
455 | "label": "INT",
456 | "type": "INT",
457 | "links": [
458 | 13
459 | ],
460 | "slot_index": 0
461 | }
462 | ],
463 | "properties": {
464 | "cnr_id": "comfyui_riceround",
465 | "ver": "4f9bb568efd29815148c54edf41eb1e5f040098b",
466 | "Node name for S&R": "RiceRoundRandomSeedNode"
467 | }
468 | }
469 | ],
470 | "links": [
471 | [
472 | 1,
473 | 4,
474 | 0,
475 | 3,
476 | 0,
477 | "MODEL"
478 | ],
479 | [
480 | 2,
481 | 5,
482 | 0,
483 | 3,
484 | 3,
485 | "LATENT"
486 | ],
487 | [
488 | 3,
489 | 4,
490 | 1,
491 | 6,
492 | 0,
493 | "CLIP"
494 | ],
495 | [
496 | 4,
497 | 6,
498 | 0,
499 | 3,
500 | 1,
501 | "CONDITIONING"
502 | ],
503 | [
504 | 5,
505 | 4,
506 | 1,
507 | 7,
508 | 0,
509 | "CLIP"
510 | ],
511 | [
512 | 6,
513 | 7,
514 | 0,
515 | 3,
516 | 2,
517 | "CONDITIONING"
518 | ],
519 | [
520 | 7,
521 | 3,
522 | 0,
523 | 8,
524 | 0,
525 | "LATENT"
526 | ],
527 | [
528 | 8,
529 | 4,
530 | 2,
531 | 8,
532 | 1,
533 | "VAE"
534 | ],
535 | [
536 | 10,
537 | 11,
538 | 0,
539 | 6,
540 | 1,
541 | "STRING"
542 | ],
543 | [
544 | 11,
545 | 12,
546 | 0,
547 | 4,
548 | 0,
549 | "COMBO"
550 | ],
551 | [
552 | 12,
553 | 8,
554 | 0,
555 | 13,
556 | 0,
557 | "IMAGE"
558 | ],
559 | [
560 | 13,
561 | 14,
562 | 0,
563 | 3,
564 | 4,
565 | "INT"
566 | ]
567 | ],
568 | "groups": [],
569 | "config": {},
570 | "extra": {
571 | "ds": {
572 | "scale": 0.7972024500000006,
573 | "offset": [
574 | 1381.5789126523111,
575 | 375.7707577101947
576 | ]
577 | },
578 | "node_versions": {
579 | "comfy-core": "0.3.12",
580 | "ComfyUI_RiceRound": "d3b0a8353c18e926eaaeb27f72a72c46d8b13caa"
581 | }
582 | },
583 | "version": 0.4
584 | }
--------------------------------------------------------------------------------
/example_workflows/simple_workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RiceRound/ComfyUI_RiceRound/1be37e81400514ecbc795634f11786cd38579af8/example_workflows/simple_workflow.png
--------------------------------------------------------------------------------
/input_node.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import os
4 | import re
5 | import time
6 | import random
7 | from PIL import Image, ImageOps, ImageSequence
8 | import numpy as np
9 | import torch
10 | import node_helpers
11 | from .rice_prompt_info import RicePromptInfo
12 | from nodes import LoadImage
13 | import requests
14 | from .utils import pil2tensor
15 |
16 |
17 | class _BasicTypes(str):
18 | basic_types = ["STRING"]
19 |
20 | def __eq__(self, other):
21 | return other in self.basic_types or isinstance(other, (list, _BasicTypes))
22 |
23 | def __ne__(self, other):
24 | return not self.__eq__(other)
25 |
26 |
27 | BasicTypes = _BasicTypes("BASIC")
28 |
29 |
30 | class RiceRoundSimpleChoiceNode:
31 | def __init__(self):
32 | self.prompt_info = RicePromptInfo()
33 |
34 | @classmethod
35 | def INPUT_TYPES(cls):
36 | return {
37 | "required": {
38 | "name": ("STRING", {"default": "Parameter"}),
39 | "default": ("STRING", {"default": ""}),
40 | },
41 | "optional": {},
42 | "hidden": {
43 | "unique_id": "UNIQUE_ID",
44 | "prompt": "PROMPT",
45 | "extra_pnginfo": "EXTRA_PNGINFO",
46 | },
47 | }
48 |
49 | RETURN_TYPES = (BasicTypes,)
50 | RETURN_NAMES = ("value",)
51 | FUNCTION = "placeholder"
52 | CATEGORY = "RiceRound/Input"
53 |
54 | def placeholder(self, name, default, **kwargs):
55 | unique_id = int(kwargs.pop("unique_id", 0))
56 | prompt = kwargs.pop("prompt", None)
57 | need_wait = True
58 | if prompt:
59 | for _, node in prompt.items():
60 | if node.get("class_type", "") == "RiceRoundDecryptNode":
61 | need_wait = False
62 | break
63 | if need_wait:
64 | for i in range(10):
65 | if unique_id in self.prompt_info.choice_node_map:
66 | break
67 | time.sleep(1)
68 | if unique_id not in self.prompt_info.choice_node_map:
69 | print(
70 | f"Warning: RiceRoundSimpleChoiceNode {unique_id} not found in prompt_info.choice_node_map"
71 | )
72 | return (default,)
73 |
74 |
75 | class RiceRoundAdvancedChoiceNode(RiceRoundSimpleChoiceNode):
76 | def __init__(self):
77 | super().__init__()
78 |
79 | CATEGORY = "RiceRound/Advanced"
80 |
81 |
82 | class RiceRoundSimpleImageNode(LoadImage):
83 | def __init__(self):
84 | super().__init__()
85 |
86 | RETURN_TYPES = ("IMAGE",)
87 | RETURN_NAMES = ("value",)
88 | CATEGORY = "RiceRound/Input"
89 | FUNCTION = "load_image"
90 |
91 | def load_image(self, image):
92 | output_image, _ = super().load_image(image)
93 | return (output_image,)
94 |
95 |
96 | class RiceRoundImageNode(LoadImage):
97 | def __init__(self):
98 | super().__init__()
99 |
100 | RETURN_TYPES = "IMAGE", "MASK"
101 | RETURN_NAMES = "image", "mask"
102 | CATEGORY = "RiceRound/Input"
103 | FUNCTION = "load_image"
104 |
105 | def load_image(self, image):
106 | return super().load_image(image)
107 |
108 |
109 | class RiceRoundDownloadImageNode:
110 | def __init__(self):
111 | 0
112 |
113 | @classmethod
114 | def INPUT_TYPES(s):
115 | return {
116 | "required": {"image_url": ("STRING", {"default": ""})},
117 | "optional": {},
118 | "hidden": {},
119 | }
120 |
121 | RETURN_TYPES = ("IMAGE",)
122 | RETURN_NAMES = ("value",)
123 | FUNCTION = "load_image"
124 | CATEGORY = "RiceRound/Input"
125 |
126 | def load_image(self, image_url, **kwargs):
127 | image = Image.open(requests.get(image_url, stream=True).raw)
128 | image = ImageOps.exif_transpose(image)
129 | return (pil2tensor(image),)
130 |
131 |
132 | class RiceRoundDownloadImageAndMaskNode:
133 | def __init__(self):
134 | 0
135 |
136 | @classmethod
137 | def INPUT_TYPES(s):
138 | return {"required": {"image_url": ("STRING", {"default": ""})}}
139 |
140 | RETURN_TYPES = "IMAGE", "MASK"
141 | RETURN_NAMES = "image", "mask"
142 | FUNCTION = "load_image"
143 | CATEGORY = "RiceRound/Input"
144 |
145 | def load_image(self, image_url, **kwargs):
146 | img = Image.open(requests.get(image_url, stream=True).raw)
147 | img = ImageOps.exif_transpose(img)
148 | output_images = []
149 | output_masks = []
150 | w, h = None, None
151 | excluded_formats = ["MPO"]
152 | for i in ImageSequence.Iterator(img):
153 | i = node_helpers.pillow(ImageOps.exif_transpose, i)
154 | if i.mode == "I":
155 | i = i.point(lambda i: i * (1 / 255))
156 | image = i.convert("RGB")
157 | if len(output_images) == 0:
158 | w = image.size[0]
159 | h = image.size[1]
160 | if image.size[0] != w or image.size[1] != h:
161 | continue
162 | image = np.array(image).astype(np.float32) / 255.0
163 | image = torch.from_numpy(image)[None,]
164 | if "A" in i.getbands():
165 | mask = np.array(i.getchannel("A")).astype(np.float32) / 255.0
166 | mask = 1.0 - torch.from_numpy(mask)
167 | else:
168 | mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
169 | output_images.append(image)
170 | output_masks.append(mask.unsqueeze(0))
171 | if len(output_images) > 1 and img.format not in excluded_formats:
172 | output_image = torch.cat(output_images, dim=0)
173 | output_mask = torch.cat(output_masks, dim=0)
174 | else:
175 | output_image = output_images[0]
176 | output_mask = output_masks[0]
177 | return output_image, output_mask
178 |
179 |
180 | class RiceRoundImageBridgeNode:
181 | def __init__(self):
182 | 0
183 |
184 | @classmethod
185 | def INPUT_TYPES(s):
186 | return {
187 | "required": {"images": ("IMAGE", {"tooltip": "only image."})},
188 | "optional": {},
189 | }
190 |
191 | RETURN_TYPES = ("IMAGE",)
192 | RETURN_NAMES = ("value",)
193 | FUNCTION = "bridge"
194 | CATEGORY = "RiceRound/Input"
195 |
196 | def bridge(self, images, **kwargs):
197 | return (images,)
198 |
199 |
200 | class RiceRoundMaskBridgeNode:
201 | def __init__(self):
202 | 0
203 |
204 | @classmethod
205 | def INPUT_TYPES(s):
206 | return {"required": {"mask": ("MASK", {"tooltip": "only image."})}}
207 |
208 | RETURN_TYPES = ("MASK",)
209 | RETURN_NAMES = ("value",)
210 | FUNCTION = "bridge"
211 | CATEGORY = "RiceRound/Input"
212 |
213 | def bridge(self, mask, **kwargs):
214 | return (mask,)
215 |
216 |
217 | class RiceRoundDownloadMaskNode:
218 | def __init__(self):
219 | 0
220 |
221 | @classmethod
222 | def INPUT_TYPES(s):
223 | return {"required": {"mask_url": ("STRING", {"default": ""})}}
224 |
225 | RETURN_TYPES = ("MASK",)
226 | RETURN_NAMES = ("value",)
227 | FUNCTION = "load_mask"
228 | CATEGORY = "RiceRound/Input"
229 |
230 | def load_mask(self, mask_url, **kwargs):
231 | try:
232 | response = requests.get(mask_url, stream=True, timeout=10)
233 | response.raise_for_status()
234 | mask = Image.open(response.raw)
235 | if mask.mode != "L":
236 | mask = mask.convert("L")
237 | return (pil2tensor(mask),)
238 | except requests.exceptions.RequestException as e:
239 | print(f"Error downloading mask from {mask_url}: {str(e)}")
240 | raise
241 | except Exception as e:
242 | print(f"Error processing mask: {str(e)}")
243 | raise
244 |
245 |
246 | class RiceRoundIntNode:
247 | def __init__(self):
248 | 0
249 |
250 | @classmethod
251 | def INPUT_TYPES(s):
252 | return {
253 | "required": {
254 | "name": ("STRING", {"default": "数值"}),
255 | "number": ("INT", {"default": 0}),
256 | "min": ("INT", {"default": 0}),
257 | "max": ("INT", {"default": 100}),
258 | }
259 | }
260 |
261 | RETURN_TYPES = ("INT",)
262 | RETURN_NAMES = ("value",)
263 | FUNCTION = "load"
264 | CATEGORY = "RiceRound/Input"
265 |
266 | def load(self, name, number, min, max, **kwargs):
267 | return (number,)
268 |
269 |
270 | class RiceRoundStrToIntNode:
271 | def __init__(self):
272 | 0
273 |
274 | @classmethod
275 | def INPUT_TYPES(s):
276 | return {"required": {"name": ("STRING", {"default": "数值"}), "str": ("STRING",)}}
277 |
278 | RETURN_TYPES = ("INT",)
279 | RETURN_NAMES = ("value",)
280 | OUTPUT_NODE = True
281 | FUNCTION = "load"
282 | CATEGORY = "RiceRound/Input"
283 |
284 | def load(self, name, str, **kwargs):
285 | return (int(str),)
286 |
287 |
288 | class RiceRoundFloatNode:
289 | def __init__(self):
290 | 0
291 |
292 | @classmethod
293 | def INPUT_TYPES(s):
294 | return {
295 | "required": {
296 | "name": ("STRING", {"default": "数值"}),
297 | "number": ("FLOAT", {"default": 0.0}),
298 | "min": ("FLOAT", {"default": 0.0}),
299 | "max": ("FLOAT", {"default": 1e2}),
300 | }
301 | }
302 |
303 | RETURN_TYPES = ("FLOAT",)
304 | RETURN_NAMES = ("value",)
305 | FUNCTION = "load"
306 | CATEGORY = "RiceRound/Input"
307 |
308 | def load(self, name, number, min, max, **kwargs):
309 | return (number,)
310 |
311 |
312 | class RiceRoundStrToFloatNode:
313 | def __init__(self):
314 | 0
315 |
316 | @classmethod
317 | def INPUT_TYPES(s):
318 | return {"required": {"name": ("STRING", {"default": "数值"}), "str": ("STRING",)}}
319 |
320 | RETURN_TYPES = ("FLOAT",)
321 | RETURN_NAMES = ("value",)
322 | FUNCTION = "load"
323 | CATEGORY = "RiceRound/Input"
324 |
325 | def load(self, name, str, **kwargs):
326 | return (float(str),)
327 |
328 |
329 | class RiceRoundBooleanNode:
330 | @classmethod
331 | def INPUT_TYPES(s):
332 | return {
333 | "required": {
334 | "name": ("STRING", {"default": "开关"}),
335 | "value": ("BOOLEAN", {"default": False}),
336 | }
337 | }
338 |
339 | RETURN_TYPES = ("BOOLEAN",)
340 | RETURN_NAMES = ("value",)
341 | FUNCTION = "execute"
342 | CATEGORY = "RiceRound/Input"
343 |
344 | def execute(self, name, value):
345 | return (value,)
346 |
347 |
348 | class RiceRoundStrToBooleanNode:
349 | def __init__(self):
350 | 0
351 |
352 | @classmethod
353 | def INPUT_TYPES(s):
354 | return {"required": {"name": ("STRING", {"default": "开关"}), "str": ("STRING",)}}
355 |
356 | RETURN_TYPES = ("BOOLEAN",)
357 | RETURN_NAMES = ("value",)
358 | FUNCTION = "load"
359 | CATEGORY = "RiceRound/Input"
360 |
361 | def load(self, name, str, **kwargs):
362 | return (str.lower() == "true",)
363 |
364 |
365 | class RiceRoundInputTextNode:
366 | def __init__(self):
367 | 0
368 |
369 | @classmethod
370 | def INPUT_TYPES(s):
371 | return {
372 | "required": {
373 | "text_info": (
374 | "STRING",
375 | {"multiline": True, "tooltip": "The text to be encoded."},
376 | )
377 | }
378 | }
379 |
380 | RETURN_TYPES = ("STRING",)
381 | FUNCTION = "load"
382 | CATEGORY = "RiceRound/Input"
383 |
384 | def load(self, text_info, **kwargs):
385 | text = ""
386 | try:
387 | json_data = json.loads(text_info)
388 | text = json_data.get("content", "")
389 | except json.JSONDecodeError:
390 | text = text_info
391 | return (text,)
392 |
393 |
394 | class RiceRoundRandomSeedNode:
395 | def __init__(self):
396 | 0
397 |
398 | @classmethod
399 | def INPUT_TYPES(s):
400 | return {"required": {}, "optional": {}, "hidden": {}}
401 |
402 | RETURN_TYPES = ("INT",)
403 | FUNCTION = "random"
404 | CATEGORY = "RiceRound/Input"
405 |
406 | @classmethod
407 | def IS_CHANGED(s):
408 | return random.randint(0, 999999)
409 |
410 | def random(self):
411 | r = random.randint(0, 999999)
412 | print("产生随机数 ", r)
413 | return (r,)
414 |
--------------------------------------------------------------------------------
/install.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | set "requirements_txt=%~dp0\requirements.txt"
4 | set "requirements_repair_txt=%~dp0\repair_dependency_list.txt"
5 | set "python_exec=..\..\..\python_embeded\python.exe"
6 | set "aki_python_exec=..\..\python\python.exe"
7 |
8 | echo Installing EasyUse Requirements...
9 |
10 | if exist "%python_exec%" (
11 | echo Installing with ComfyUI Portable
12 | "%python_exec%" -s -m pip install -r "%requirements_txt%"
13 | )^
14 | else if exist "%aki_python_exec%" (
15 | echo Installing with ComfyUI Aki
16 | "%aki_python_exec%" -s -m pip install -r "%requirements_txt%"
17 | for /f "delims=" %%i in (%requirements_repair_txt%) do (
18 | %aki_python_exec% -s -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "%%i"
19 | )
20 | )^
21 | else (
22 | echo Installing with system Python
23 | pip install -r "%requirements_txt%"
24 | )
25 |
26 | pause
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Define paths relative to the current directory for requirements files
4 | requirements_txt="$(dirname "$0")/requirements.txt"
5 | requirements_repair_txt="$(dirname "$0")/repair_dependency_list.txt"
6 |
7 | # Define Python executables for different environments
8 | python_exec="../../../python_embeded/python3"
9 | aki_python_exec="../../python/python3"
10 |
11 | echo "Installing EasyUse Requirements..."
12 |
13 | # Check if the ComfyUI Portable Python exists
14 | if [ -f "$python_exec" ]; then
15 | echo "Installing with ComfyUI Portable"
16 | "$python_exec" -m pip install --upgrade pip
17 | "$python_exec" -m pip install -r "$requirements_txt"
18 |
19 | # Check if the ComfyUI Aki Python exists
20 | elif [ -f "$aki_python_exec" ]; then
21 | echo "Installing with ComfyUI Aki"
22 | "$aki_python_exec" -m pip install --upgrade pip
23 | "$aki_python_exec" -m pip install -r "$requirements_txt"
24 |
25 | # Attempt to install missing dependencies from the repair list
26 | while IFS= read -r line; do
27 | "$aki_python_exec" -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "$line"
28 | done < "$requirements_repair_txt"
29 |
30 | # Fall back to system Python if neither of the above are found
31 | else
32 | echo "Installing with system Python"
33 | python3 -m pip install --upgrade pip
34 | python3 -m pip install -r "$requirements_txt"
35 | fi
36 |
37 | # Wait for the user to acknowledge completion
38 | echo "Installation completed. Press any key to continue..."
39 | read -n 1 -s
40 |
--------------------------------------------------------------------------------
/js/riceconfig.js:
--------------------------------------------------------------------------------
1 | import { api } from "../../../scripts/api.js";
2 |
3 | import { ComfyApp, app } from "../../../scripts/app.js";
4 |
5 | import { showToast } from "./riceround.js";
6 |
7 | const UserTokenKey = "riceround_user_token";
8 |
9 | function isValidJWTFormat(e) {
10 | if ("string" != typeof e) return !1;
11 | if (e.length < 50) return !1;
12 | const t = e.split(".");
13 | if (3 !== t.length) return !1;
14 | const n = /^[A-Za-z0-9_-]+$/;
15 | return t.every((e => e.length > 0 && n.test(e)));
16 | }
17 |
18 | async function exportTomlMessageBox(e) {
19 | const t = document.createElement("div");
20 | document.body.appendChild(t);
21 | const {createApp: n, ref: o} = Vue, i = n({
22 | template: '\n
检测到高级选择节点,是否需要安装相关组件?
\n