├── .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 | [![Star on GitHub](https://img.shields.io/github/stars/RiceRound/ComfyUI_RiceRound.svg?style=social)](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 | ![image](docs/menu.png) 40 | 41 | ### 3. 发布工作流 42 | 43 | 在工作流尾部加上 RiceRound Publish 节点用于发布,然后点击运行。 44 | 45 | ![image](docs/workflow.png) 46 | 47 | ### 4. 查看结果 48 | 49 | 会在 output 文件夹生成一些加密工作流文件,其中 workflow.json 就是你拿去发布的加密工作流。 50 | 51 | ![image](docs/output.png) 52 | 53 | ### 5. 管理工作流 54 | 55 | 在 [https://www.riceround.online/](https://www.riceround.online/) 可以管理你的工作流,也可以看见工作流生成的页面。 56 | 57 | ![image](docs/web.png) 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 | ![image](docs/wechat.jpg) 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 \n \n \n \n \n \n \n ', 23 | setup() { 24 | const e = o(!0), n = o(!1), l = o({ 25 | secretToken: "" 26 | }), a = () => { 27 | document.body.removeChild(t), i.unmount(); 28 | }; 29 | return { 30 | dialogVisible: e, 31 | loading: n, 32 | form: l, 33 | handleClose: a, 34 | handleGenerate: async () => { 35 | if (l.value.secretToken) { 36 | n.value = !0; 37 | try { 38 | const e = await api.fetchApi("/riceround/export_toml", { 39 | method: "POST", 40 | headers: { 41 | "Content-Type": "application/json" 42 | }, 43 | body: JSON.stringify({ 44 | secret_token: l.value.secretToken 45 | }) 46 | }); 47 | if (e.ok) { 48 | const t = await e.blob(), n = window.URL.createObjectURL(t), o = document.createElement("a"); 49 | o.href = n, o.download = "client.toml", document.body.appendChild(o), o.click(), 50 | window.URL.revokeObjectURL(n), document.body.removeChild(o), ElementPlus.ElMessage.success("配置文件生成成功"), 51 | a(); 52 | } else { 53 | const t = await e.json(); 54 | ElementPlus.ElMessage.error(t.message || "生成配置文件失败"); 55 | } 56 | } catch (e) { 57 | ElementPlus.ElMessage.error("生成配置文件失败"); 58 | } finally { 59 | n.value = !1; 60 | } 61 | } else ElementPlus.ElMessage.warning("请输入机器码"); 62 | }, 63 | openHelp: () => { 64 | window.open("https://help.riceround.online/#/install?id=client-node-deployment", "_blank"); 65 | } 66 | }; 67 | } 68 | }); 69 | i.use(ElementPlus), i.mount(t); 70 | } 71 | 72 | app.registerExtension({ 73 | name: "riceround.config", 74 | async setup() { 75 | app.ui.settings.addSetting({ 76 | id: "RiceRound.Advanced.setting", 77 | name: "模型列表存放位置,手动清理或安装高级节点", 78 | type: () => { 79 | const e = document.createElement("tr"), t = document.createElement("td"), n = document.createElement("input"); 80 | return n.type = "button", n.value = "打开文件夹", n.style.borderRadius = "8px", n.style.padding = "8px 16px", 81 | n.style.fontSize = "14px", n.style.cursor = "pointer", n.style.border = "1px solid #666", 82 | n.style.backgroundColor = "#444", n.style.color = "#fff", n.onmouseover = () => { 83 | n.style.backgroundColor = "#555"; 84 | }, n.onmouseout = () => { 85 | n.style.backgroundColor = "#444"; 86 | }, n.onclick = () => { 87 | api.fetchApi("/riceround/open_folder?id=2", { 88 | method: "GET", 89 | headers: { 90 | "Content-Type": "application/json" 91 | } 92 | }); 93 | }, t.appendChild(n), e.appendChild(t), e; 94 | } 95 | }), app.ui.settings.addSetting({ 96 | id: "RiceRound.User.logout", 97 | name: "登出当前用户", 98 | type: () => { 99 | const e = document.createElement("tr"), t = document.createElement("td"), n = document.createElement("input"); 100 | return n.type = "button", n.value = "登出", n.style.borderRadius = "8px", n.style.padding = "8px 16px", 101 | n.style.fontSize = "14px", n.style.cursor = "pointer", n.style.border = "1px solid #666", 102 | n.style.backgroundColor = "#444", n.style.color = "#fff", n.onclick = async () => { 103 | localStorage.removeItem("Comfy.Settings.RiceRound.User.long_token"), localStorage.removeItem(UserTokenKey), 104 | app.ui.settings.setSettingValue("RiceRound.User.long_token", ""), await api.fetchApi("/riceround/logout"), 105 | showToast("登出成功"); 106 | }, t.appendChild(n), e.appendChild(t), e; 107 | } 108 | }), app.ui.settings.addSetting({ 109 | id: "RiceRound.User.long_token", 110 | name: "设置长效token", 111 | type: "text", 112 | textType: "password", 113 | defaultValue: "", 114 | tooltip: "用于非本机授权登录情况,请勿泄露!提倡使用本机登录授权更安全!", 115 | onChange: async function(e) { 116 | isValidJWTFormat(e) && await api.fetchApi("/riceround/set_long_token", { 117 | method: "POST", 118 | headers: { 119 | "Content-Type": "application/json" 120 | }, 121 | body: JSON.stringify({ 122 | long_token: e 123 | }) 124 | }); 125 | } 126 | }), app.ui.settings.addSetting({ 127 | id: "RiceRound.Setting.wait-time", 128 | name: "任务排队等待时间(秒)", 129 | tooltip: "不建议设置太短,否则可能等不到运行结果就退出了", 130 | type: "slider", 131 | attrs: { 132 | min: 30, 133 | max: 7200, 134 | step: 10 135 | }, 136 | defaultValue: 600, 137 | onChange: e => { 138 | api.fetchApi("/riceround/set_wait_time", { 139 | method: "POST", 140 | headers: { 141 | "Content-Type": "application/json" 142 | }, 143 | body: JSON.stringify({ 144 | wait_time: e 145 | }) 146 | }); 147 | } 148 | }), app.ui.settings.addSetting({ 149 | id: "RiceRound.Publish", 150 | name: "自动发布工作流", 151 | type: "boolean", 152 | defaultValue: !0, 153 | tooltip: "设置为true时,会自动发布工作流", 154 | onChange: function(e) { 155 | api.fetchApi("/riceround/set_auto_publish", { 156 | method: "POST", 157 | headers: { 158 | "Content-Type": "application/json" 159 | }, 160 | body: JSON.stringify({ 161 | auto_publish: e 162 | }) 163 | }), e || localStorage.setItem("RiceRound.Setting.auto_overwrite", e); 164 | } 165 | }), app.ui.settings.addSetting({ 166 | id: "RiceRound.Cloud.export", 167 | name: "生成云机器配置", 168 | type: () => { 169 | const e = document.createElement("tr"), t = document.createElement("td"), n = document.createElement("input"); 170 | return n.type = "button", n.value = "导出", n.style.borderRadius = "8px", n.style.padding = "8px 16px", 171 | n.style.fontSize = "14px", n.style.cursor = "pointer", n.style.border = "1px solid #666", 172 | n.style.backgroundColor = "#444", n.style.color = "#fff", n.onclick = async () => { 173 | exportTomlMessageBox("生成云机器配置"); 174 | }, t.appendChild(n), e.appendChild(t), e; 175 | } 176 | }), app.ui.settings.addSetting({ 177 | id: "RiceRound.Cloud.fix_toml", 178 | name: "修复本机ComfyUI环境配置", 179 | tooltip: "注意请明确当前ComfyUI环境是否正常,修复后最好手动查看一下client.toml文件是否修复成功", 180 | type: () => { 181 | const e = document.createElement("tr"), t = document.createElement("td"), n = document.createElement("input"); 182 | return n.type = "button", n.value = "修复", n.style.borderRadius = "8px", n.style.padding = "8px 16px", 183 | n.style.fontSize = "14px", n.style.cursor = "pointer", n.style.border = "1px solid #666", 184 | n.style.backgroundColor = "#444", n.style.color = "#fff", n.onclick = async () => { 185 | const e = await api.fetchApi("/riceround/fix_toml", { 186 | method: "GET", 187 | headers: { 188 | "Content-Type": "application/json" 189 | } 190 | }); 191 | 200 == e.status ? showToast("修复成功") : showToast("修复失败 " + e?.message || ""); 192 | }, t.appendChild(n), e.appendChild(t), e; 193 | } 194 | }); 195 | } 196 | }); -------------------------------------------------------------------------------- /js/riceround.js: -------------------------------------------------------------------------------- 1 | import { api } from "../../../scripts/api.js"; 2 | 3 | import { ComfyApp, app } from "../../../scripts/app.js"; 4 | 5 | const style = document.createElement("style"); 6 | 7 | style.textContent = "\n .riceround-swal-top-container {\n z-index: 99999 !important;\n }\n", 8 | document.head.appendChild(style); 9 | 10 | export async function loadResource(e, t = "") { 11 | if (!document.querySelector(`script[src="${e}"]`)) { 12 | const t = document.createElement("script"); 13 | t.src = e, document.head.appendChild(t); 14 | try { 15 | await new Promise(((o, n) => { 16 | t.onload = o, t.onerror = () => n(new Error(`Failed to load script: ${e}`)); 17 | })); 18 | } catch (e) {} 19 | } 20 | if (t) { 21 | if (!document.querySelector(`link[href="${t}"]`)) { 22 | const e = document.createElement("link"); 23 | e.rel = "stylesheet", e.href = t, document.head.appendChild(e); 24 | try { 25 | await new Promise(((o, n) => { 26 | e.onload = o, e.onerror = () => n(new Error(`Failed to load stylesheet: ${t}`)); 27 | })); 28 | } catch (e) {} 29 | } 30 | } 31 | } 32 | 33 | let toastHasLoaded = !1; 34 | 35 | async function loadToast() { 36 | if (!toastHasLoaded) { 37 | const e = "https://cdn.jsdelivr.net/npm/toastify-js", t = "https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"; 38 | await loadResource(e, t), toastHasLoaded = !0; 39 | } 40 | } 41 | 42 | export async function showToast(e, t = "info", o = 3e3) { 43 | await loadToast(), "info" == t ? Toastify({ 44 | text: e, 45 | duration: o, 46 | close: !1, 47 | gravity: "top", 48 | position: "center", 49 | backgroundColor: "#3498db", 50 | stopOnFocus: !1 51 | }).showToast() : "error" == t ? Toastify({ 52 | text: e, 53 | duration: o, 54 | close: !0, 55 | gravity: "top", 56 | position: "center", 57 | backgroundColor: "#FF4444", 58 | stopOnFocus: !0 59 | }).showToast() : "warning" == t && Toastify({ 60 | text: e, 61 | duration: o, 62 | close: !0, 63 | gravity: "top", 64 | position: "center", 65 | backgroundColor: "#FFA500", 66 | stopOnFocus: !0 67 | }).showToast(); 68 | } 69 | 70 | let messageBoxHasLoaded = !1; 71 | 72 | export async function loadMessageBox() { 73 | messageBoxHasLoaded || (await loadResource("https://cdn.jsdelivr.net/npm/sweetalert2@11", "https://cdn.jsdelivr.net/npm/@sweetalert2/theme-bootstrap-4/bootstrap-4.css"), 74 | messageBoxHasLoaded = !0); 75 | } 76 | 77 | async function showClientInstallMessageBox(e, t) { 78 | const o = localStorage.getItem("riceround_client_dontshow"); 79 | if (!(o && Number(o) > Date.now())) if (await loadMessageBox(), e) { 80 | if (!t) { 81 | const e = await Swal.fire({ 82 | title: "Client未启动", 83 | html: '本机Client似乎没有启动,这不影响发布,但发布后可能没有算力可用,可以点击这里寻求帮助,也可以尝试修复本机配置或打开Client文件夹查看详情。', 84 | icon: "warning", 85 | showCancelButton: !0, 86 | showDenyButton: !0, 87 | confirmButtonText: "修复配置", 88 | denyButtonText: "打开文件夹", 89 | cancelButtonText: "不再提示", 90 | heightAuto: !1, 91 | customClass: { 92 | container: "riceround-swal-top-container" 93 | } 94 | }); 95 | if (e.isConfirmed) await api.fetchApi("/riceround/fix_toml", { 96 | method: "Get" 97 | }); else if (e.isDenied) await api.fetchApi("/riceround/open_folder?id=1", { 98 | method: "GET" 99 | }); else if (e.dismiss === Swal.DismissReason.cancel) { 100 | const e = Date.now() + 2592e5; 101 | localStorage.setItem("riceround_client_dontshow", e.toString()); 102 | } 103 | } 104 | } else { 105 | const e = await Swal.fire({ 106 | title: "未安装Client", 107 | html: "您似乎没有安装Client,这样即使完成发布后,仍然可能没有算力支撑导致无法使用,详情请点击下方按钮查看并安装", 108 | icon: "warning", 109 | confirmButtonText: "查看安装说明", 110 | showCancelButton: !0, 111 | cancelButtonText: "不再提示", 112 | heightAuto: !1, 113 | customClass: { 114 | container: "riceround-swal-top-container" 115 | } 116 | }); 117 | if (e.isConfirmed) window.open("https://help.riceround.online", "_blank"); else if (e.dismiss === Swal.DismissReason.cancel) { 118 | const e = Date.now() + 2592e5; 119 | localStorage.setItem("riceround_client_dontshow", e.toString()); 120 | } 121 | } 122 | } 123 | 124 | function send_message(e, t) { 125 | api.fetchApi("/riceround/message", { 126 | method: "POST", 127 | headers: { 128 | "Content-Type": "application/json" 129 | }, 130 | body: JSON.stringify({ 131 | id: e, 132 | message: t 133 | }) 134 | }); 135 | } 136 | 137 | var skip_next = 0; 138 | 139 | function send_onstart() { 140 | return skip_next > 0 ? (skip_next -= 1, !1) : (send_message(-1, "__start__"), !0); 141 | } 142 | 143 | async function serverShowMessageBox(e, t) { 144 | await loadMessageBox(); 145 | const o = { 146 | ...e, 147 | heightAuto: !1 148 | }; 149 | try { 150 | const e = await Swal.fire(o), n = { 151 | confirmed: e.isConfirmed ? 1 : 0, 152 | value: e.value, 153 | dismiss: e.dismiss 154 | }; 155 | api.fetchApi("/riceround/message", { 156 | method: "POST", 157 | headers: { 158 | "Content-Type": "application/json" 159 | }, 160 | body: JSON.stringify({ 161 | id: t, 162 | message: n 163 | }) 164 | }); 165 | } catch (e) { 166 | api.fetchApi("/riceround/message", { 167 | method: "POST", 168 | headers: { 169 | "Content-Type": "application/json" 170 | }, 171 | body: JSON.stringify({ 172 | id: t, 173 | message: { 174 | confirmed: 0, 175 | error: e.message 176 | } 177 | }) 178 | }); 179 | } 180 | window.addEventListener("beforeunload", (function() { 181 | fetch("/riceround/message", { 182 | method: "POST", 183 | headers: { 184 | "Content-Type": "application/json" 185 | }, 186 | body: JSON.stringify({ 187 | id: t, 188 | message: "__cancel__" 189 | }), 190 | keepalive: !0 191 | }); 192 | }), { 193 | once: !0 194 | }); 195 | } 196 | 197 | api.addEventListener("riceround_toast", (e => { 198 | showToast(e.detail.content, e.detail.type, e.detail.duration); 199 | })), api.addEventListener("riceround_server_dialog", (e => { 200 | serverShowMessageBox(JSON.parse(e.detail.json_content), e.detail.id); 201 | })), api.addEventListener("riceround_client_install_dialog", (e => { 202 | showClientInstallMessageBox(e.detail.is_installed, e.detail.is_running); 203 | })); 204 | 205 | let dialogLibHasLoaded = !1; 206 | 207 | async function waitForObject(e, t, o = 5e3) { 208 | return new Promise(((n, i) => { 209 | const a = Date.now(), s = () => { 210 | Date.now() - a > o ? i(new Error(`Timeout waiting for ${t} to load`)) : e() ? n() : setTimeout(s, 50); 211 | }; 212 | s(); 213 | })); 214 | } 215 | 216 | export async function initDialogLib(e = !1) { 217 | if (!dialogLibHasLoaded) try { 218 | const e = "https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.global.js"; 219 | await loadResource(e, ""), await waitForObject((() => window.Vue), "vue"); 220 | const t = "https://cdn.jsdelivr.net/npm/element-plus", o = "https://cdn.jsdelivr.net/npm/element-plus/dist/index.css"; 221 | if (await loadResource(t, o), await waitForObject((() => window.ElementPlus), "element-plus"), 222 | null == window.DialogLib) { 223 | const e = "riceround/static/dialog-lib.umd.cjs"; 224 | await loadResource(e, ""), await waitForObject((() => window.DialogLib), "showLoginDialog"); 225 | } 226 | dialogLibHasLoaded = !0; 227 | } catch (e) { 228 | throw e; 229 | } 230 | } 231 | 232 | async function setNodeAdditionalInfo(e) { 233 | try { 234 | const t = { 235 | method: "POST", 236 | headers: { 237 | "Content-Type": "application/json" 238 | }, 239 | body: JSON.stringify(e) 240 | }, o = await api.fetchApi("/riceround/set_node_additional_info", t); 241 | await o.json(); 242 | } catch (e) {} 243 | } 244 | 245 | function changeWidget(e, t, o, n) { 246 | e.type = t, e.value = o, e.options = n; 247 | } 248 | 249 | function changeWidgets(e, t, o, n) { 250 | "customtext" === t && (t = "text"); 251 | const i = n.options; 252 | var a = e.widgets[1].value; 253 | i?.values?.includes(a) || (a = n.value), "RiceRoundAdvancedChoiceNode" == e.comfyClass || "RiceRoundSimpleChoiceNode" == e.comfyClass ? "combo" === t ? setupComboWidget(e.widgets[1], e, 1, i.values) : changeWidget(e.widgets[1], t, a, i) : "RiceRoundIntNode" != e.comfyClass && "RiceRoundFloatNode" != e.comfyClass || 4 == e.widgets.length && (changeWidget(e.widgets[1], "number", a, i), 254 | changeWidget(e.widgets[2], "number", i?.min ?? 0, i), changeWidget(e.widgets[3], "number", i?.max ?? 100, i)); 255 | } 256 | 257 | function adaptWidgetsBasedOnConnection(e, t, o, n) { 258 | if (e.outputs[0].type = o.type, "name" === e.widgets[0].label) { 259 | ([ "数值", "文本", "列表", "参数", "Parameter" ].includes(e.widgets[0].value) || "" == e.widgets[0].value) && (e.widgets[0].value = o.label ? o.label : o.name); 260 | } 261 | const i = o.widget?.name ? o.widget?.name : o.name, a = t.widgets.find((e => e.name === i)); 262 | if (!a) return; 263 | changeWidgets(e, a.origType ?? a.type, t, a); 264 | } 265 | 266 | api.addEventListener("riceround_login_dialog", (e => { 267 | const t = e.detail.client_key, o = e.detail.title; 268 | window.DialogLib.showLoginDialog({ 269 | title: o, 270 | spyNetworkError: !0, 271 | mainKey: "riceround" 272 | }).then((e => { 273 | api.fetchApi("/riceround/auth_callback", { 274 | method: "POST", 275 | headers: { 276 | "Content-Type": "application/json" 277 | }, 278 | body: JSON.stringify({ 279 | token: e, 280 | client_key: t 281 | }) 282 | }), showToast("登录成功"); 283 | })).catch((e => { 284 | showToast("登录失败", "error"); 285 | })); 286 | })), api.addEventListener("riceround_show_workflow_payment_dialog", (e => { 287 | const t = e.detail.title ?? "支付", o = e.detail.template_id; 288 | o ? window.DialogLib.showWorkflowQRPaymentDialog({ 289 | title: t, 290 | template_id: o 291 | }).then((({success: e, msg: t}) => { 292 | showToast(t, e ? "info" : "error"); 293 | })).catch((e => { 294 | showToast("取消支付", "error"); 295 | })) : showToast("模板ID不能为空", "error"); 296 | })), api.addEventListener("riceround_clear_user_info", (async e => { 297 | const t = e.detail.clear_key; 298 | "all" == t ? (localStorage.removeItem("Comfy.Settings.RiceRound.User.long_token"), 299 | localStorage.removeItem("riceround_user_token"), app.ui.settings.setSettingValue("RiceRound.User.long_token", "")) : "long_token" == t ? (localStorage.removeItem("Comfy.Settings.RiceRound.User.long_token"), 300 | app.ui.settings.setSettingValue("RiceRound.User.long_token", "")) : "user_token" == t && localStorage.removeItem("riceround_user_token"); 301 | })), api.addEventListener("execution_start", (async ({detail: e}) => { 302 | let t = ""; 303 | const o = {}; 304 | for (const e of app.graph.nodes) { 305 | if ("RiceRoundDecryptNode" === e.type) return; 306 | if ("RiceRoundEncryptNode" === e.type) { 307 | const o = e.widgets?.find((e => "template_id" === e.name && e.value)); 308 | if (o) { 309 | if (t) return; 310 | t = o.value; 311 | } 312 | } else if ("RiceRoundAdvancedChoiceNode" === e.type || "RiceRoundSimpleChoiceNode" === e.type) { 313 | if (1 === !e.outputs?.[0]?.links?.length) continue; 314 | const t = e.widgets[1].options?.values ?? []; 315 | if (!t.length) continue; 316 | const n = e.graph.links[e.outputs[0].links[0]]; 317 | if (!n) continue; 318 | const i = e.graph.getNodeById(n.target_id); 319 | if (!i?.inputs || "RiceRoundDecryptNode" === i.comfyClass || "RiceRoundEncryptNode" === i.comfyClass) continue; 320 | const a = i.inputs[n.target_slot]; 321 | if (!a || !i.widgets) continue; 322 | const s = a.widget?.name || a.name; 323 | if (!s) continue; 324 | const d = `${i.comfyClass}.${s}`; 325 | o[e.id] = { 326 | class_name: d, 327 | options_value: t, 328 | node_type: e.type 329 | }; 330 | } 331 | } 332 | t && Object.keys(o).length > 0 && await setNodeAdditionalInfo({ 333 | choice_node_map: o, 334 | template_id: t 335 | }), send_onstart(); 336 | })); 337 | 338 | const nodeTimersMap = new Map; 339 | 340 | function setupComboWidget(e, t, o, n) { 341 | e.type = "combo", e.options = { 342 | values: n 343 | }, n.includes(e.value) || (e.value = n[0]); 344 | const i = t.id; 345 | e.callback = function(e) { 346 | const t = app.graph._nodes_by_id[i]; 347 | t?.widgets?.[o] && (t.widgets[o].value = e, t.setDirtyCanvas(!0)); 348 | }; 349 | } 350 | 351 | function applySimpleChoiceNodeExtraLogic(e, t) { 352 | if (!e?.id) return; 353 | nodeTimersMap.has(e.id) && (clearTimeout(nodeTimersMap.get(e.id)), nodeTimersMap.delete(e.id)); 354 | const o = setTimeout((() => { 355 | nodeTimersMap.delete(e.id); 356 | const o = t.graph.extra?.choice_node_map; 357 | 2 === e?.widgets?.length && o?.[e.id] && (setupComboWidget(e.widgets[1], e, 1, o[e.id]), 358 | e.setDirtyCanvas(!0)); 359 | }), 200); 360 | nodeTimersMap.set(e.id, o); 361 | } 362 | 363 | function adaptWidgetsToConnection(e) { 364 | if (!e.outputs || 0 === e.outputs.length) return; 365 | const t = e.outputs[0].links; 366 | if (t && 1 === t.length) { 367 | const o = e.graph.links[t[0]]; 368 | if (!o) return; 369 | const n = e.graph.getNodeById(o.target_id); 370 | if (!n || !n.inputs) return; 371 | if ("RiceRoundDecryptNode" == n.comfyClass && "RiceRoundSimpleChoiceNode" == e.comfyClass) return void applySimpleChoiceNodeExtraLogic(e, app); 372 | if ("RiceRoundEncryptNode" == n.comfyClass || "RiceRoundDecryptNode" == n.comfyClass) return; 373 | const i = n.inputs[o.target_slot]; 374 | if (!i || void 0 === n.widgets) return; 375 | adaptWidgetsBasedOnConnection(e, n, i, app); 376 | } else t && 0 !== t.length || ("RiceRoundAdvancedChoiceNode" == e.comfyClass || "RiceRoundSimpleChoiceNode" == e.comfyClass ? (e.widgets[0].value = "Parameter", 377 | e.outputs[0].type = "*") : "RiceRoundIntNode" == e.comfyClass ? 4 == e.widgets.length && (e.widgets[0].value = "数值", 378 | e.widgets[1].value = 0, e.widgets[2].value = 0, e.widgets[3].value = 100) : "RiceRoundFloatNode" == e.comfyClass && 4 == e.widgets.length && (e.widgets[0].value = "数值", 379 | e.widgets[1].value = 0, e.widgets[2].value = 0, e.widgets[3].value = 100)); 380 | } 381 | 382 | function setupParameterNode(e) { 383 | const t = e.prototype.onAdded; 384 | e.prototype.onAdded = function() { 385 | t?.apply(this, arguments), adaptWidgetsToConnection(this); 386 | }; 387 | const o = e.prototype.onAfterGraphConfigured; 388 | e.prototype.onAfterGraphConfigured = function() { 389 | o?.apply(this, arguments), adaptWidgetsToConnection(this); 390 | }; 391 | const n = e.prototype.onConnectOutput; 392 | e.prototype.onConnectOutput = function(e, t, o, i, a) { 393 | if (!(o.widget || [ "STRING", "COMBO", "combo" ].includes(o.type) || o.type.includes("*"))) return !1; 394 | if (n) { 395 | return n.apply(this, arguments); 396 | } 397 | return !0; 398 | }; 399 | const i = e.prototype.onConnectionsChange; 400 | e.prototype.onConnectionsChange = function(e, t, o, n, a) { 401 | return 2 != e || o || !this?.type || "RiceRoundAdvancedChoiceNode" != this.type && "RiceRoundSimpleChoiceNode" != this.type || (this.widgets[0].value = "Parameter"), 402 | app.configuringGraph || adaptWidgetsToConnection(this), i?.apply(this, arguments); 403 | }; 404 | } 405 | 406 | function generateUUID() { 407 | let e = ""; 408 | for (let t = 0; t < 32; t++) { 409 | e += Math.floor(16 * Math.random()).toString(16); 410 | } 411 | return e; 412 | } 413 | 414 | function rebootAPI() { 415 | if ("electronAPI" in window) return window.electronAPI.restartApp(), !0; 416 | api.fetchApi("/manager/reboot"); 417 | } 418 | 419 | app.registerExtension({ 420 | name: "riceround.custom", 421 | setup() { 422 | initDialogLib(); 423 | }, 424 | async beforeRegisterNodeDef(e, t, o) { 425 | if ([ "RiceRoundAdvancedChoiceNode", "RiceRoundSimpleChoiceNode", "RiceRoundIntNode", "RiceRoundFloatNode" ].includes(t.name) && setupParameterNode(e), 426 | "RiceRoundEncryptNode" == t.name) { 427 | const t = 400, o = 120, n = e.prototype.onNodeCreated; 428 | e.prototype.onNodeCreated = function() { 429 | const e = n ? n.apply(this) : void 0; 430 | return void 0 !== this.size?.[0] && (this.size[0] = t), void 0 !== this.size?.[1] && (this.size[1] = Math.max(o, this.size[1])), 431 | e; 432 | }, e.prototype.onResize = function(e) { 433 | return void 0 !== e?.[0] && (e[0] = t), void 0 !== e?.[1] && (e[1] = Math.max(o, e[1])), 434 | e; 435 | }; 436 | } 437 | }, 438 | loadedGraphNode: async e => { 439 | if (!e.title) { 440 | const t = e.type; 441 | if (t && t.includes("RiceRoundAdvancedChoiceNode")) { 442 | const e = t.match(/RiceRoundAdvancedChoiceNode_([^_]+)_/); 443 | if (!e) return; 444 | const o = e[1], n = localStorage.getItem("riceround_choice_dontshow"); 445 | if (n && Number(n) > Date.now()) return; 446 | await loadMessageBox(); 447 | const i = await Swal.fire({ 448 | title: "高级选择节点安装确认", 449 | html: '\n
\n

检测到高级选择节点,是否需要安装相关组件?

\n
\n \n \n
\n
\n ', 450 | icon: "question", 451 | showCancelButton: !0, 452 | confirmButtonText: "安装", 453 | cancelButtonText: "不再提示", 454 | heightAuto: !1, 455 | backdrop: !0, 456 | allowOutsideClick: !1, 457 | customClass: { 458 | container: "riceround-swal-top-container" 459 | }, 460 | preConfirm: () => ({ 461 | needReboot: document.getElementById("swal-restart-checkbox").checked 462 | }) 463 | }); 464 | if (i.dismiss === Swal.DismissReason.cancel) { 465 | const e = Date.now() + 2592e5; 466 | return void localStorage.setItem("riceround_choice_dontshow", e.toString()); 467 | } 468 | if (i.isConfirmed) try { 469 | if (!(await api.fetchApi("/riceround/install_choice_node", { 470 | method: "POST", 471 | headers: { 472 | "Content-Type": "application/json" 473 | }, 474 | body: JSON.stringify({ 475 | template_id: o, 476 | need_reboot: i.value.needReboot 477 | }) 478 | })).ok) throw new Error("Installation failed"); 479 | await Swal.fire({ 480 | title: "安装成功", 481 | text: i.value.needReboot ? "组件已安装,服务即将重启" : "组件已安装完成", 482 | icon: "success", 483 | heightAuto: !1, 484 | customClass: { 485 | container: "riceround-swal-top-container" 486 | } 487 | }); 488 | } catch (e) { 489 | await Swal.fire({ 490 | title: "安装失败", 491 | text: "组件安装过程中出现错误", 492 | icon: "error", 493 | heightAuto: !1, 494 | customClass: { 495 | container: "riceround-swal-top-container" 496 | } 497 | }); 498 | } 499 | } 500 | } 501 | }, 502 | nodeCreated(e, t) { 503 | if ("RiceRoundEncryptNode" == e.comfyClass && e.widgets && e.widgets.length > 0) { 504 | const t = window.document.title; 505 | if (t || (t = localStorage.getItem("Comfy.PreviousWorkflow")), t) { 506 | let o = t.replace(/[<>:"/\\|?*]/g, " ").replace(/^\s+|\s+$/g, ""); 507 | for (let t = 0; t < e.widgets.length; t++) { 508 | let n = e.widgets[t]; 509 | if ("project_name" == n.name && ("" == n.value || null == n.value || "my_project" == n.value)) { 510 | n.value = o; 511 | break; 512 | } 513 | } 514 | } 515 | e.widgets.push({ 516 | name: "generate_uuid", 517 | type: "button", 518 | label: "Generate UUID", 519 | callback: () => { 520 | const t = generateUUID(); 521 | for (let o = 0; o < e.widgets.length; o++) { 522 | let n = e.widgets[o]; 523 | if ("template_id" == n.name) { 524 | n.value = t; 525 | break; 526 | } 527 | } 528 | } 529 | }); 530 | const o = document.getElementById(e.id); 531 | o && o.appendChild(button); 532 | } 533 | } 534 | }); -------------------------------------------------------------------------------- /message_holder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import requests 5 | from server import PromptServer 6 | from aiohttp import web 7 | 8 | 9 | class Cancelled(Exception): 10 | 0 11 | 12 | 13 | class MessageHolder: 14 | messages = {} 15 | cancelled = False 16 | 17 | @classmethod 18 | def addMessage(cls, id, message): 19 | if message == "__cancel__": 20 | cls.messages = {} 21 | cls.cancelled = True 22 | elif message == "__start__": 23 | cls.messages = {} 24 | cls.cancelled = False 25 | else: 26 | cls.messages[str(id)] = message 27 | 28 | @classmethod 29 | def waitForMessage(cls, id, period=0.1, timeout=60): 30 | sid = str(id) 31 | cls.messages.clear() 32 | cls.cancelled = False 33 | start_time = time.time() 34 | while sid not in cls.messages: 35 | if cls.cancelled: 36 | cls.cancelled = False 37 | raise Cancelled() 38 | if time.time() - start_time > timeout: 39 | raise Cancelled("Operation timed out") 40 | time.sleep(period) 41 | if cls.cancelled: 42 | cls.cancelled = False 43 | raise Cancelled() 44 | return cls.messages.pop(sid) 45 | 46 | 47 | routes = PromptServer.instance.routes 48 | 49 | 50 | @routes.post("/riceround/message") 51 | async def message_handler(request): 52 | data = await request.json() 53 | MessageHolder.addMessage(data["id"], data["message"]) 54 | return web.json_response({}) 55 | -------------------------------------------------------------------------------- /output_node.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import json 3 | import os 4 | import re 5 | from pathlib import Path 6 | from PIL import Image, ImageOps 7 | from PIL.PngImagePlugin import PngInfo 8 | import torch 9 | from comfy import model_management 10 | import requests 11 | import numpy as np 12 | import folder_paths 13 | from nodes import LoadImage 14 | from comfy.utils import ProgressBar 15 | from server import PromptServer 16 | from .rice_def import RiceRoundErrorDef, RiceTaskErrorDef 17 | from .rice_url_config import RiceUrlConfig, user_upload_image, user_upload_imagefile 18 | from .utils import get_machine_id, pil2tensor 19 | from .auth_unit import AuthUnit 20 | from .rice_prompt_info import RicePromptInfo 21 | from .rice_websocket import ( 22 | TaskInfo, 23 | TaskStatus, 24 | TaskWebSocket, 25 | start_and_wait_task_done, 26 | ) 27 | 28 | 29 | class RiceRoundDecryptNode: 30 | def __init__(self): 31 | self.auth_unit = AuthUnit() 32 | self.machine_id = get_machine_id() 33 | self.url_config = RiceUrlConfig() 34 | self.pbar = None 35 | self.last_progress = 0 36 | self.user_token = None 37 | 38 | @classmethod 39 | def INPUT_TYPES(s): 40 | return { 41 | "required": { 42 | "rice_template_id": ("STRING", {"default": ""}), 43 | "seed": ( 44 | "INT", 45 | { 46 | "default": 0, 47 | "min": 0, 48 | "max": 0xFFFFFFFFFFFFFFFF, 49 | "tooltip": "The random seed used for creating the noise.", 50 | }, 51 | ), 52 | }, 53 | "optional": {"input_anything": ("*", {})}, 54 | "hidden": { 55 | "unique_id": "UNIQUE_ID", 56 | "prompt": "PROMPT", 57 | "extra_pnginfo": "EXTRA_PNGINFO", 58 | }, 59 | } 60 | 61 | @classmethod 62 | def VALIDATE_INPUTS(s, input_types): 63 | for key, value in input_types.items(): 64 | if key.startswith("input_anything"): 65 | if value not in ("STRING", "TEXT", "PROMPT"): 66 | return f"{key} must be of string type" 67 | return True 68 | 69 | RETURN_TYPES = ("IMAGE",) 70 | OUTPUT_NODE = True 71 | FUNCTION = "execute" 72 | CATEGORY = "RiceRound/Output" 73 | 74 | def progress_callback(self, task_uuid, progress_text, progress, preview_refreshed): 75 | if not self.pbar: 76 | return 77 | if preview_refreshed: 78 | url = self.url_config.workflow_preview_url + "?task_uuid=" + task_uuid 79 | try: 80 | headers = {"Authorization": f"Bearer {self.user_token}"} 81 | response = requests.get(url, stream=True, headers=headers) 82 | response.raise_for_status() 83 | preview_image = Image.open(BytesIO(response.content)) 84 | self.pbar.update_absolute( 85 | self.last_progress, preview=("PNG", preview_image, None) 86 | ) 87 | except Exception as e: 88 | print(f"Failed to load preview image: {str(e)}") 89 | self.pbar.update_absolute(self.last_progress) 90 | else: 91 | self.last_progress = progress 92 | self.pbar.update_absolute(progress) 93 | 94 | def execute(self, rice_template_id, **kwargs): 95 | self.pbar = ProgressBar(100) 96 | self.user_token, error_msg, error_code = self.auth_unit.get_user_token() 97 | if not self.user_token: 98 | if ( 99 | error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED 100 | or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR 101 | ): 102 | AuthUnit().login_dialog("运行云节点需要先完成登录") 103 | else: 104 | PromptServer.instance.send_sync( 105 | "riceround_toast", 106 | {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"}, 107 | ) 108 | raise ValueError(error_msg) 109 | index_dict = {} 110 | for k, v in kwargs.items(): 111 | if k.startswith("input_anything"): 112 | suffix = k[len("input_anything") :] 113 | suffix = re.sub("\\s*\\([^)]*\\)", "", suffix) 114 | index = 0 if suffix == "" else int(suffix) 115 | if index in index_dict: 116 | raise ValueError(f"Duplicate input_anything index: {index}") 117 | if isinstance(v, str): 118 | index_dict[str(index)] = v 119 | else: 120 | raise ValueError(f"Invalid input type: {type(v)}") 121 | if not index_dict: 122 | return (torch.zeros(1, 1, 1, 3),) 123 | task_info = self.create_task(index_dict, rice_template_id, self.user_token) 124 | if not task_info or not task_info.task_uuid: 125 | raise ValueError("Failed to create task") 126 | start_and_wait_task_done( 127 | self.url_config.task_ws_url, 128 | self.user_token, 129 | self.machine_id, 130 | task_info, 131 | self.progress_callback, 132 | RicePromptInfo().get_wait_time(), 133 | ) 134 | model_management.throw_exception_if_processing_interrupted() 135 | result_data = task_info.result_data 136 | if not result_data: 137 | if task_info.progress_text and task_info.state > TaskStatus.FINISHED: 138 | raise ValueError(task_info.progress_text) 139 | else: 140 | raise ValueError("websocket failed") 141 | image_results = result_data.get("image_results", []) 142 | if not image_results: 143 | raise ValueError("Failed to get image results") 144 | images = [] 145 | for image_url in image_results: 146 | image = Image.open(requests.get(image_url, stream=True).raw) 147 | image = ImageOps.exif_transpose(image) 148 | images.append(pil2tensor(image)) 149 | image_tensor = torch.cat(images, dim=0) 150 | self.pbar = None 151 | return (image_tensor,) 152 | 153 | def create_task(self, input_data, template_id, user_token): 154 | task_url = self.url_config.prompt_task_url 155 | headers = { 156 | "Authorization": f"Bearer {user_token}", 157 | "Content-Type": "application/json", 158 | } 159 | request_data = { 160 | "taskData": json.dumps(input_data), 161 | "workData": json.dumps({"template_id": template_id}), 162 | } 163 | response = requests.post(task_url, json=request_data, headers=headers) 164 | if response.status_code == 200: 165 | response_data = response.json() 166 | if response_data.get("code") == 0 and "data" in response_data: 167 | task_info = TaskInfo(response_data.get("data", {})) 168 | if task_info.task_uuid: 169 | return task_info 170 | else: 171 | raise ValueError("No task UUID in response") 172 | else: 173 | raise ValueError( 174 | f"API error: {response_data.get('message','Unknown error')}" 175 | ) 176 | elif response.status_code == RiceRoundErrorDef.HTTP_INTERNAL_ERROR: 177 | response_data = response.json() 178 | if ( 179 | response_data.get("code") 180 | == RiceTaskErrorDef.ERROR_INSUFFICIENT_PERMISSION_INSUFFICIENT_BALANCE 181 | ): 182 | PromptServer.instance.send_sync( 183 | "riceround_show_workflow_payment_dialog", 184 | {"template_id": template_id, "title": "余额不足,请充值"}, 185 | ) 186 | raise ValueError(f"余额不足,运行失败,请完成支付后重试!") 187 | else: 188 | raise ValueError( 189 | f"API error: {response_data.get('message','Unknown error')}" 190 | ) 191 | else: 192 | raise ValueError(f"HTTP error {response.status_code}: {response.text}") 193 | 194 | 195 | class RiceRoundBaseChoiceNode: 196 | def __init__(self): 197 | 0 198 | 199 | @classmethod 200 | def INPUT_TYPES(cls): 201 | node_name = getattr(cls, "__node_name__", None) 202 | options = ( 203 | RicePromptInfo().get_choice_node_options(node_name) if node_name else [] 204 | ) 205 | return { 206 | "required": { 207 | "name": ("STRING", {"default": "Parameter"}), 208 | "default": (options,), 209 | }, 210 | "optional": {}, 211 | "hidden": {}, 212 | } 213 | 214 | RETURN_TYPES = ("STRING",) 215 | RETURN_NAMES = ("value",) 216 | FUNCTION = "placeholder" 217 | CATEGORY = "__hidden__" 218 | 219 | def placeholder(self, default, **kwargs): 220 | return (default,) 221 | 222 | 223 | def upload_imagefile(image_path): 224 | user_token, error_msg, error_code = AuthUnit().get_user_token() 225 | if not user_token: 226 | if ( 227 | error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED 228 | or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR 229 | ): 230 | AuthUnit().login_dialog("运行云节点需要先完成登录") 231 | else: 232 | PromptServer.instance.send_sync( 233 | "riceround_toast", {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"} 234 | ) 235 | raise ValueError(error_msg) 236 | return user_upload_imagefile(image_path, user_token) 237 | 238 | 239 | def upload_image(image): 240 | user_token, error_msg, error_code = AuthUnit().get_user_token() 241 | if not user_token: 242 | if ( 243 | error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED 244 | or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR 245 | ): 246 | AuthUnit().login_dialog("运行云节点需要先完成登录") 247 | else: 248 | PromptServer.instance.send_sync( 249 | "riceround_toast", {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"} 250 | ) 251 | raise ValueError(error_msg) 252 | return user_upload_image(image, user_token) 253 | 254 | 255 | class RiceRoundImageUrlNode: 256 | def __init__(self): 257 | 0 258 | 259 | @classmethod 260 | def INPUT_TYPES(s): 261 | return {"required": {"image_url": ("STRING",)}} 262 | 263 | RETURN_TYPES = ("STRING",) 264 | RETURN_NAMES = ("value",) 265 | OUTPUT_NODE = True 266 | FUNCTION = "load_image" 267 | CATEGORY = "RiceRound/Output" 268 | 269 | def load_image(self, image_url, **kwargs): 270 | return (image_url,) 271 | 272 | 273 | class RiceRoundUploadImageNode(LoadImage): 274 | def __init__(self): 275 | super().__init__() 276 | 277 | @classmethod 278 | def INPUT_TYPES(s): 279 | input_dir = folder_paths.get_input_directory() 280 | files = [ 281 | f 282 | for f in os.listdir(input_dir) 283 | if os.path.isfile(os.path.join(input_dir, f)) 284 | ] 285 | return {"required": {"image": (sorted(files), {"image_upload": True})}} 286 | 287 | RETURN_TYPES = ("STRING",) 288 | RETURN_NAMES = ("value",) 289 | OUTPUT_NODE = True 290 | FUNCTION = "load_image" 291 | CATEGORY = "RiceRound/Output" 292 | 293 | def load_image(self, image, **kwargs): 294 | image_path = folder_paths.get_annotated_filepath(image) 295 | download_url = upload_imagefile(image_path) 296 | return (download_url,) 297 | 298 | 299 | class RiceRoundOutputImageBridgeNode: 300 | def __init__(self): 301 | 0 302 | 303 | @classmethod 304 | def INPUT_TYPES(s): 305 | return { 306 | "required": {"images": ("IMAGE", {"tooltip": "only image."})}, 307 | "optional": {}, 308 | } 309 | 310 | RETURN_TYPES = ("STRING",) 311 | RETURN_NAMES = ("value",) 312 | OUTPUT_NODE = True 313 | FUNCTION = "bridge" 314 | CATEGORY = "RiceRound/Output" 315 | 316 | def bridge(self, images, **kwargs): 317 | return upload_image(images) 318 | 319 | 320 | class RiceRoundOutputMaskBridgeNode: 321 | def __init__(self): 322 | 0 323 | 324 | @classmethod 325 | def INPUT_TYPES(s): 326 | return {"required": {"mask": ("MASK",)}} 327 | 328 | RETURN_TYPES = ("STRING",) 329 | RETURN_NAMES = ("value",) 330 | OUTPUT_NODE = True 331 | FUNCTION = "bridge" 332 | CATEGORY = "RiceRound/Output" 333 | 334 | def bridge(self, mask, **kwargs): 335 | mask_np = mask.cpu().numpy() 336 | mask_np = (mask_np * 255).astype(np.uint8) 337 | mask_image = Image.fromarray(mask_np) 338 | image_url = upload_image(mask_image) 339 | return (image_url,) 340 | 341 | 342 | class RiceRoundMaskUrlNode: 343 | def __init__(self): 344 | 0 345 | 346 | @classmethod 347 | def INPUT_TYPES(s): 348 | return {"required": {"mask_url": ("STRING",)}} 349 | 350 | RETURN_TYPES = ("STRING",) 351 | RETURN_NAMES = ("value",) 352 | OUTPUT_NODE = True 353 | FUNCTION = "load_image" 354 | CATEGORY = "RiceRound/Output" 355 | 356 | def load_image(self, mask_url, **kwargs): 357 | return (mask_url,) 358 | 359 | 360 | class RiceRoundOutputIntNode: 361 | def __init__(self): 362 | 0 363 | 364 | @classmethod 365 | def INPUT_TYPES(s): 366 | return { 367 | "required": { 368 | "name": ("STRING", {"default": "数值"}), 369 | "number": ("INT",), 370 | "min": ("INT", {"default": 0}), 371 | "max": ("INT", {"default": 1000000}), 372 | } 373 | } 374 | 375 | RETURN_TYPES = ("STRING",) 376 | RETURN_NAMES = ("value",) 377 | OUTPUT_NODE = True 378 | FUNCTION = "bridge" 379 | CATEGORY = "RiceRound/Output" 380 | 381 | def bridge(self, name, number, min, max, **kwargs): 382 | return (str(number),) 383 | 384 | 385 | class RiceRoundOutputFloatNode: 386 | def __init__(self): 387 | 0 388 | 389 | @classmethod 390 | def INPUT_TYPES(s): 391 | return { 392 | "required": { 393 | "name": ("STRING", {"default": "数值"}), 394 | "number": ("FLOAT",), 395 | "min": ("FLOAT", {"default": 0.0}), 396 | "max": ("FLOAT", {"default": 1e6}), 397 | } 398 | } 399 | 400 | RETURN_TYPES = ("STRING",) 401 | RETURN_NAMES = ("value",) 402 | OUTPUT_NODE = True 403 | FUNCTION = "bridge" 404 | CATEGORY = "RiceRound/Output" 405 | 406 | def bridge(self, name, number, min, max, **kwargs): 407 | return (str(number),) 408 | 409 | 410 | class RiceRoundOutputBooleanNode: 411 | def __init__(self): 412 | 0 413 | 414 | @classmethod 415 | def INPUT_TYPES(s): 416 | return { 417 | "required": { 418 | "name": ("STRING", {"default": "开关"}), 419 | "value": ("BOOLEAN", {"default": False}), 420 | } 421 | } 422 | 423 | RETURN_TYPES = ("STRING",) 424 | RETURN_NAMES = ("value",) 425 | OUTPUT_NODE = True 426 | FUNCTION = "bridge" 427 | CATEGORY = "RiceRound/Output" 428 | 429 | def bridge(self, name, value, **kwargs): 430 | str_value = "true" if value else "false" 431 | return (str_value,) 432 | 433 | 434 | class RiceRoundOutputTextNode: 435 | def __init__(self): 436 | 0 437 | 438 | @classmethod 439 | def INPUT_TYPES(s): 440 | return {"required": {"name": ("STRING", {"default": "文本"}), "str": ("STRING",)}} 441 | 442 | RETURN_TYPES = ("STRING",) 443 | RETURN_NAMES = ("value",) 444 | OUTPUT_NODE = True 445 | FUNCTION = "bridge" 446 | CATEGORY = "RiceRound/Output" 447 | 448 | def bridge(self, name, str, **kwargs): 449 | return (str,) 450 | -------------------------------------------------------------------------------- /publish.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import requests 5 | from .utils import generate_random_string 6 | from .rice_prompt_info import RicePromptInfo 7 | from .rice_url_config import RiceUrlConfig, user_upload_imagefile 8 | from server import PromptServer 9 | from aiohttp import web 10 | import time 11 | from .message_holder import MessageHolder 12 | 13 | 14 | class Publish: 15 | def __init__(self, publish_folder): 16 | self.publish_folder = publish_folder 17 | 18 | def publish( 19 | self, user_token, template_id, project_name, preview_path, publish_file 20 | ): 21 | if not os.path.exists(publish_file): 22 | raise ValueError(f"Publish file not found: {publish_file}") 23 | overwrite = False 24 | error_code, error_msg = self._check_workflow(user_token, template_id) 25 | if error_code == 1: 26 | overwrite = True 27 | auto_overwrite = False 28 | print("总是有人覆盖导致工作流丢失故障,先关闭这个功能,必须弹窗提示!后继看看能不能尽量智能检测") 29 | if not auto_overwrite: 30 | json_content = { 31 | "title": "已经存在相同template_id的数据,是否覆盖?注意,如果不是同一个工作流相互覆盖,会造成严重后果!", 32 | "icon": "info", 33 | "confirmButtonText": "覆盖后手动调整template", 34 | "cancelButtonText": "取消", 35 | "showCancelButton": True, 36 | "timer": 50000, 37 | } 38 | message_id = generate_random_string(10) 39 | PromptServer.instance.send_sync( 40 | "riceround_server_dialog", 41 | {"json_content": json.dumps(json_content), "id": message_id}, 42 | ) 43 | msg_result = MessageHolder.waitForMessage(message_id, timeout=60000) 44 | if isinstance(msg_result, str): 45 | result = json.loads(msg_result) 46 | else: 47 | result = msg_result 48 | if result.get("confirmed", 0) != 1: 49 | print("riceround upload cancel: User rejected overwrite") 50 | return False 51 | elif error_code != 0: 52 | print(f"riceround upload failed: {error_msg}") 53 | PromptServer.instance.send_sync( 54 | "riceround_toast", {"content": f"异常情况,{error_msg}", "type": "error"} 55 | ) 56 | return False 57 | preview_image_url = None 58 | if not overwrite: 59 | if os.path.exists(preview_path): 60 | preview_image_url = user_upload_imagefile(preview_path, user_token) 61 | success, message = self._upload_workflow( 62 | user_token, template_id, project_name, preview_image_url, publish_file 63 | ) 64 | if success: 65 | PromptServer.instance.send_sync( 66 | "riceround_toast", {"content": "上传成功", "type": "info", "duration": 5000} 67 | ) 68 | else: 69 | PromptServer.instance.send_sync( 70 | "riceround_toast", 71 | {"content": f"上传失败: {message}", "type": "error", "duration": 5000}, 72 | ) 73 | return success 74 | 75 | def _check_workflow(self, user_token, template_id): 76 | headers = {"Authorization": f"Bearer {user_token}"} 77 | params = {"id": template_id, "action": "check"} 78 | try: 79 | response = requests.get( 80 | RiceUrlConfig().publisher_workflow_url, params=params, headers=headers 81 | ) 82 | if response.status_code == 200: 83 | response_data = response.json() 84 | error_code = response_data.get("code") 85 | error_msg = response_data.get("message") 86 | return error_code, error_msg 87 | else: 88 | return -1, "" 89 | except Exception as e: 90 | return -1, str(e) 91 | 92 | def _upload_workflow( 93 | self, user_token, template_id, project_name, preview_image_url, publish_file 94 | ): 95 | try: 96 | headers = {"Authorization": f"Bearer {user_token}"} 97 | json_data = { 98 | "template_id": template_id, 99 | "title": project_name, 100 | "main_image_url": preview_image_url or "", 101 | } 102 | with open(publish_file, "rb") as f: 103 | files = {"workflow_file": ("workflow", f, "application/octet-stream")} 104 | form_data = {"data": json.dumps(json_data), "source": "comfyui"} 105 | response = requests.put( 106 | RiceUrlConfig().publisher_workflow_url, 107 | headers=headers, 108 | files=files, 109 | data=form_data, 110 | ) 111 | if response.status_code == 200: 112 | response_data = response.json() 113 | if response_data.get("code") == 0: 114 | return True, "Success" 115 | else: 116 | return False, response_data.get("message", "Unknown error") 117 | else: 118 | logging.error(f"Server returned status: {response}") 119 | return False, f"Server returned status code: {response.status_code}" 120 | except Exception as e: 121 | return False, str(e) 122 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui_riceround" 3 | description = "RiceRound is an open-source project that migrates local AI workflows (built with ComfyUI) to the cloud, enabling distributed deployment. Creators can design and run workflows while monitoring progress and node status in real-time. The platform supports one-click cloud node generation and online page creation." 4 | version = "1.2.0" 5 | license = {file = "LICENSE"} 6 | dependencies = ["aiohttp", "pyzipper", "websockets", "portalocker", "tomlkit"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/RiceRound/ComfyUI_RiceRound" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "riceround" 14 | DisplayName = "ComfyUI_RiceRound" 15 | Icon = "https://www.riceround.online/RiceRound.ico" 16 | -------------------------------------------------------------------------------- /pyproject_cloud.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "riceround_cloud" 3 | description = "A tool for generating encrypted workflows and commercializing AI nodes for ComfyUI." 4 | version = "1.1.1" 5 | license = {file = "LICENSE"} 6 | dependencies = ["aiohttp", "pyzipper", "websockets", "portalocker", "tomlkit"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/RiceRound/ComfyUI_RiceRound_Cloud" 10 | 11 | [tool.comfy] 12 | PublisherId = "riceround" 13 | DisplayName = "ComfyUI_RiceRoundCloud" 14 | Icon = "https://www.riceround.online/RiceRound.ico" 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | pyzipper 3 | websockets 4 | portalocker 5 | tomlkit -------------------------------------------------------------------------------- /rice_def.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class RiceRoundErrorDef(IntEnum): 5 | SUCCESS = 0 6 | HTTP_OK = 200 7 | HTTP_UNAUTHORIZED = 401 8 | HTTP_INTERNAL_ERROR = 500 9 | HTTP_SERVICE_UNAVAILABLE = 503 10 | HTTP_TIMEOUT = 408 11 | NETWORK_ERROR = 1 12 | REQUEST_ERROR = 2 13 | NO_TOKEN_ERROR = 3 14 | UNKNOWN_ERROR = 600 15 | OVER_MAX_ERROR = 998 16 | ERROR_EXECUTABLE_NOT_FOUND = 1001 17 | ERROR_START_EXCEPTION = 1002 18 | ERROR_PROCESS_EXIT = 1003 19 | ERROR_RESTART_EXCEPTION = 1004 20 | ERROR_MACHINE_CODE_BASE = 1000 21 | ERROR_SECRET_TOKEN_BASE = 1100 22 | ERROR_SECRET_TOKEN = 1101 23 | ERROR_INSTALL_CLIENT_TOML = 1102 24 | 25 | @staticmethod 26 | def calc_error_code(base_value, error_code): 27 | max_value = 1000 28 | if error_code < max_value: 29 | if error_code != 0: 30 | return base_value + error_code 31 | else: 32 | return base_value + RiceRoundErrorDef.UNKNOWN_ERROR 33 | else: 34 | return base_value + RiceRoundErrorDef.OVER_MAX_ERROR 35 | 36 | 37 | class RiceTaskErrorDef(IntEnum): 38 | ERROR_FAILED_TO_FETCH_TASK_TEMPLATE = 2001 39 | ERROR_INVALID_TASK_TEMPLATE = 2002 40 | ERROR_FAILED_TO_GET_QUEUE_COUNT = 2003 41 | ERROR_QUEUE_FULL = 2004 42 | ERROR_INSUFFICIENT_PERMISSION = 2005 43 | ERROR_NO_AVAILABLE_MACHINE = 2006 44 | ERROR_FAILED_TO_DEDUCT_BALANCE = 2007 45 | ERROR_FAILED_TO_ADD_TASK_TO_QUEUE = 2008 46 | ERROR_FAILED_TO_SAVE_TASK_RECORD = 2009 47 | ERROR_INSUFFICIENT_PERMISSION_PAYER_ID_INVALID = 2051 48 | ERROR_INSUFFICIENT_PERMISSION_PROVIDER_ID_INVALID = 2052 49 | ERROR_INSUFFICIENT_PERMISSION_BALANCE_FETCH_FAILED = 2053 50 | ERROR_INSUFFICIENT_PERMISSION_INSUFFICIENT_BALANCE = 2054 51 | -------------------------------------------------------------------------------- /rice_install_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | import requests 5 | from .rice_def import RiceRoundErrorDef 6 | from .rice_url_config import RiceUrlConfig 7 | from .auth_unit import AuthUnit 8 | from .rice_prompt_info import RiceEnvConfig 9 | from .utils import get_local_app_setting_path 10 | 11 | 12 | class RiceInstallClient: 13 | def __init__(self): 14 | self.current_path = os.path.dirname(os.path.abspath(__file__)) 15 | self.app_path = get_local_app_setting_path() 16 | ( 17 | self.source_executable_filename, 18 | self.executable_filename, 19 | ) = self._get_platform_executables() 20 | 21 | def _get_platform_executables(self): 22 | if sys.platform == "win32": 23 | return "share_client_windows.exe", "share_client.exe" 24 | elif sys.platform == "darwin": 25 | return "share_client_mac", "share_client" 26 | elif sys.platform == "linux": 27 | return "share_client_linux", "share_client" 28 | else: 29 | raise OSError(f"Unsupported platform: {sys.platform}") 30 | 31 | def is_client_running(self): 32 | if not self.is_client_installed(): 33 | return False 34 | lock_path = os.path.join(tempfile.gettempdir(), "rice_client.lock") 35 | if not os.path.exists(lock_path): 36 | return False 37 | try: 38 | import portalocker 39 | 40 | with open(lock_path, "w") as f: 41 | portalocker.lock(f, portalocker.LOCK_EX | portalocker.LOCK_NB) 42 | portalocker.unlock(f) 43 | return False 44 | except portalocker.LockException: 45 | return True 46 | 47 | def is_client_installed(self): 48 | if not self.app_path.exists(): 49 | return False 50 | executable_path = self.app_path / self.executable_filename 51 | if not executable_path.exists(): 52 | return False 53 | toml_path = self.app_path / "client.toml" 54 | if not toml_path.exists(): 55 | return False 56 | return True 57 | 58 | def repair_client_toml(self, client_toml_path): 59 | if not client_toml_path.exists(): 60 | return False 61 | try: 62 | import tomlkit 63 | from tomlkit import dumps 64 | 65 | env_config = RiceEnvConfig().read_env() 66 | if not env_config: 67 | print("Error: Failed to read environment config") 68 | return False 69 | with open(client_toml_path, "r", encoding="utf-8") as f: 70 | toml_data = tomlkit.load(f) 71 | comfyui_config = toml_data.get("ComfyUI") 72 | if not comfyui_config or not isinstance(comfyui_config, dict): 73 | return False 74 | comfyui_config["PythonPath"] = env_config["PythonPath"] 75 | comfyui_config["WorkingDirectory"] = env_config["WorkingDirectory"] 76 | comfyui_config["ComfyuiScriptName"] = env_config["ScriptName"] 77 | with open(client_toml_path, "w", encoding="utf-8") as f: 78 | f.write(dumps(toml_data)) 79 | return True 80 | except Exception as e: 81 | print(f"Error repairing client.toml: {str(e)}") 82 | return False 83 | 84 | def _generate_toml_config( 85 | self, secret_token, comfyui_port=6607, local_server_port=6608 86 | ): 87 | try: 88 | import tomlkit 89 | from tomlkit import comment, table, dumps 90 | 91 | env_config = RiceEnvConfig().read_env() 92 | config = tomlkit.document() 93 | config.add(comment("日志级别设置")) 94 | config.add(comment("可选值: 'debug', 'info', 'warn', 'error'")) 95 | config["LogLevel"] = "info" 96 | config.add(comment("机器码,非常重要,用于登录鉴权")) 97 | config.add(comment("在官网可以获取自己的机器码,普通用户也可以由管理员授予")) 98 | config["SecretToken"] = secret_token 99 | config.add(comment("本地服务端口")) 100 | config.add(comment("用于本地服务端口,通常为 6608")) 101 | config["Port"] = local_server_port 102 | comfyui_table = table() 103 | comfyui_table.add(comment("ComfyUI 监听的端口")) 104 | comfyui_table.add(comment("端口号,默认为 6607")) 105 | comfyui_table["Port"] = comfyui_port 106 | comfyui_table.add(comment("Python 解释器路径")) 107 | comfyui_table.add(comment("这里填写你安装的 Python 解释器路径,确保 Python 环境已经配置好")) 108 | comfyui_table["PythonPath"] = str(env_config["PythonPath"]) 109 | comfyui_table.add(comment("ComfyUI 脚本的文件名")) 110 | comfyui_table.add(comment("这里填写 ComfyUI 的启动脚本名,通常是 'main.py'")) 111 | comfyui_table["ComfyuiScriptName"] = env_config["ScriptName"] 112 | comfyui_table.add(comment("ComfyUI 工作目录")) 113 | comfyui_table.add(comment("这里填写 ComfyUI 所在的目录路径")) 114 | comfyui_table["WorkingDirectory"] = str(env_config["WorkingDirectory"]) 115 | comfyui_table.add(comment("环境命令,用于激活相关环境")) 116 | comfyui_table.add(comment("例如可以填写 conda 环境的激活命令 conda activate comfyui")) 117 | comfyui_table["EnvCmd"] = "" 118 | comfyui_table.add(comment("启动时附加的命令行参数")) 119 | comfyui_table.add(comment("可根据需要添加,常用的如 '--disable-metadata'")) 120 | comfyui_table["AddCmd"] = env_config["AddCmd"] 121 | config["ComfyUI"] = comfyui_table 122 | return dumps(config) 123 | except Exception as e: 124 | print(f"Error generating TOML content: {str(e)}") 125 | raise e 126 | 127 | def install_client_toml(self, comfyui_port, local_server_port, secret_token): 128 | try: 129 | os.makedirs(self.app_path, exist_ok=True) 130 | toml_content = self._generate_toml_config( 131 | secret_token, comfyui_port, local_server_port 132 | ) 133 | client_toml_path = self.app_path / "client.toml" 134 | with open(client_toml_path, "w", encoding="utf-8") as f: 135 | f.write(toml_content) 136 | return True 137 | except Exception as e: 138 | print(f"Error writing client.toml: {str(e)}") 139 | return False 140 | 141 | def export_toml(self, secret_token): 142 | return self._generate_toml_config(secret_token) 143 | 144 | def auto_fix_toml(self, comfyui_port=6607, local_server_port=8689): 145 | toml_path = self.app_path / "client.toml" 146 | if not toml_path.exists(): 147 | secret_token, error_message, error_code = self.get_secret_token() 148 | if not secret_token: 149 | return ( 150 | error_code 151 | if error_code != RiceRoundErrorDef.SUCCESS 152 | else RiceRoundErrorDef.ERROR_SECRET_TOKEN, 153 | error_message, 154 | ) 155 | if not self.install_client_toml( 156 | comfyui_port, local_server_port, secret_token 157 | ): 158 | return RiceRoundErrorDef.ERROR_INSTALL_CLIENT_TOML, "安装client.toml失败" 159 | else: 160 | self.repair_client_toml(toml_path) 161 | 162 | def get_secret_token(self): 163 | token, error_message, error_code = AuthUnit().get_user_token() 164 | if not token: 165 | return None, error_message, error_code 166 | headers = { 167 | "Content-Type": "application/json", 168 | "Authorization": f"Bearer {token}", 169 | } 170 | try: 171 | response = requests.get( 172 | RiceUrlConfig().machine_bind_key_url, headers=headers, timeout=10 173 | ) 174 | if response.status_code != 200: 175 | error_code = response.status_code 176 | return ( 177 | None, 178 | "获取密钥失败", 179 | RiceRoundErrorDef.calc_error_code( 180 | RiceRoundErrorDef.ERROR_MACHINE_CODE_BASE, error_code 181 | ), 182 | ) 183 | response_data = response.json() 184 | if response_data.get("code") != 0: 185 | return None, "获取密钥失败: 响应码不为0", RiceRoundErrorDef.ERROR_SECRET_TOKEN 186 | secret_token = response_data.get("data", {}).get("key") 187 | if not secret_token: 188 | return None, "获取密钥失败: 密钥为空", RiceRoundErrorDef.ERROR_SECRET_TOKEN 189 | return secret_token, "", 0 190 | except Exception as e: 191 | return None, "获取密钥失败" + str(e), RiceRoundErrorDef.ERROR_SECRET_TOKEN 192 | -------------------------------------------------------------------------------- /rice_prompt_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import tempfile 5 | import time 6 | from .rice_def import RiceRoundErrorDef 7 | from server import PromptServer 8 | from .auth_unit import AuthUnit 9 | from .utils import get_local_app_setting_path 10 | from .rice_prompt_info import RicePromptInfo 11 | 12 | 13 | class RiceRoundPromptHandler: 14 | _instance = None 15 | _initialized = False 16 | 17 | def __new__(cls, *args, **kwargs): 18 | if cls._instance is None: 19 | cls._instance = super(RiceRoundPromptHandler, cls).__new__(cls) 20 | return cls._instance 21 | 22 | def __init__(self): 23 | if not self._initialized: 24 | self.client_id = "" 25 | self.task_uuid = "" 26 | self._initialized = True 27 | 28 | def onprompt_handler(self, json_data): 29 | RicePromptInfo().clear() 30 | if "prompt" not in json_data: 31 | return json_data 32 | has_rice_component = False 33 | prompt = json_data["prompt"] 34 | for node in prompt.values(): 35 | class_type = node.get("class_type") 36 | if class_type in ["RiceRoundEncryptNode", "RiceRoundDecryptNode"]: 37 | has_rice_component = True 38 | break 39 | if has_rice_component: 40 | user_token, error_msg, error_code = AuthUnit().get_user_token() 41 | if not user_token: 42 | if ( 43 | error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED 44 | or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR 45 | ): 46 | AuthUnit().login_dialog("RiceRound云节点,请先完成登录") 47 | json_data["prompt"] = {} 48 | return json_data 49 | else: 50 | PromptServer.instance.send_sync( 51 | "riceround_toast", 52 | {"content": f"无法完成鉴权登录,{error_msg}", "type": "error"}, 53 | ) 54 | return json_data 55 | if "client_id" not in json_data: 56 | return json_data 57 | self.client_id = json_data["client_id"] 58 | if "task_uuid" not in json_data: 59 | return json_data 60 | self.task_uuid = json_data["task_uuid"] 61 | if "template" not in json_data: 62 | raise Exception("Warning: 'template' is missing.") 63 | print( 64 | f"RiceRoundPromptHandler self.client_id={self.client_id!r}{ self.task_uuid=}" 65 | ) 66 | input_data = json_data["input"] if "input" in json_data else {} 67 | prompt_data = json_data["prompt"] 68 | prompt_data = self.replace_output_prompt(prompt_data) 69 | id_type_map, node_id_map = self.parse_template(json_data["template"]) 70 | prompt_data = self.replace_input_prompt( 71 | prompt_data, input_data, id_type_map, node_id_map 72 | ) 73 | print(f"RiceRoundPromptHandler prompt_data={prompt_data!r}") 74 | json_data["prompt"] = prompt_data 75 | return json_data 76 | 77 | def parse_template(self, template_data): 78 | id_type_map = {} 79 | node_id_map = {} 80 | elements = template_data["elements"] 81 | for element in elements: 82 | id = element["id"] 83 | node_id_map[id] = element["node_id"] 84 | id_type_map[id] = element["type"] 85 | return id_type_map, node_id_map 86 | 87 | def replace_output_prompt(self, prompt_data): 88 | for node_id, node in prompt_data.items(): 89 | if node.get("class_type") == "RiceRoundOutputImageNode": 90 | node["inputs"]["task_id"] = self.task_uuid 91 | elif node.get("class_type") == "RiceRoundRandomSeedNode": 92 | node["inputs"]["seed"] = random.randint(0, 999999) 93 | return prompt_data 94 | 95 | def replace_input_prompt(self, prompt_data, input_data, id_type_map, node_id_map): 96 | INPUT_TYPE_MAPPING = { 97 | "text": "text_info", 98 | "image_upload": "image_url", 99 | "mask_image_upload": "image_url", 100 | "mask_upload": "mask_url", 101 | "number_int": "str", 102 | "number_float": "str", 103 | "choice": "default", 104 | "switch": "str", 105 | } 106 | for input_id, value in input_data.items(): 107 | input_type = id_type_map.get(input_id, "") 108 | input_field = INPUT_TYPE_MAPPING.get(input_type) 109 | if not input_field: 110 | print( 111 | f"RiceRoundPromptHandler replace_input_prompt unknown input_type {input_type}" 112 | ) 113 | continue 114 | node = prompt_data[node_id_map[input_id]] 115 | node["inputs"][input_field] = str(value) 116 | if os.environ.get("RICEROUND_DEBUG_SAVE_PROMPT") == "true": 117 | temp_dir = tempfile.gettempdir() 118 | with open(f"{temp_dir}//{self.task_uuid}_prompt_data.json", "w") as f: 119 | json.dump(prompt_data, f, indent=4) 120 | return prompt_data 121 | -------------------------------------------------------------------------------- /rice_prompt_info.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import copy 3 | import hashlib 4 | import json 5 | import os 6 | from pathlib import Path 7 | import sys 8 | from .auth_unit import AuthUnit 9 | from .rice_url_config import download_template 10 | from server import PromptServer 11 | import re 12 | from .utils import get_local_app_setting_path 13 | 14 | 15 | class RicePromptInfo: 16 | _instance = None 17 | _initialized = False 18 | 19 | def __new__(cls): 20 | if cls._instance is None: 21 | cls._instance = super(RicePromptInfo, cls).__new__(cls) 22 | return cls._instance 23 | 24 | def __init__(self): 25 | if RicePromptInfo._initialized: 26 | return 27 | local_app_path = get_local_app_setting_path() 28 | local_app_path.mkdir(parents=True, exist_ok=True) 29 | self.config_path = local_app_path / "config.ini" 30 | self.choice_node_map = {} 31 | self.auto_overwrite = self._read_config_bool( 32 | "Settings", "auto_overwrite", False 33 | ) 34 | self.auto_publish = self._read_config_bool("Settings", "auto_publish", True) 35 | self.run_client = self._read_config_bool("Settings", "run_client", False) 36 | self.wait_time = self._read_config_int("Settings", "wait_time", 600) 37 | self.choice_classname_map = {} 38 | self.load_choice_node_map() 39 | RicePromptInfo._initialized = True 40 | 41 | def _read_config_bool(self, section, key, default=False): 42 | try: 43 | config = configparser.ConfigParser() 44 | config.read(self.config_path, encoding="utf-8") 45 | return config.getboolean(section, key, fallback=default) 46 | except Exception as e: 47 | print(f"Error reading config {section}.{key}: {e}") 48 | return default 49 | 50 | def _read_config_int(self, section, key, default=0): 51 | try: 52 | config = configparser.ConfigParser() 53 | config.read(self.config_path, encoding="utf-8") 54 | return config.getint(section, key, fallback=default) 55 | except Exception as e: 56 | print(f"Error reading config {section}.{key}: {e}") 57 | return default 58 | 59 | def _write_config_bool(self, section, key, value): 60 | try: 61 | config = configparser.ConfigParser() 62 | config.read(self.config_path, encoding="utf-8") 63 | if not config.has_section(section): 64 | config.add_section(section) 65 | config.set(section, key, str(value).lower()) 66 | with open(self.config_path, "w", encoding="utf-8") as f: 67 | config.write(f) 68 | return True 69 | except Exception as e: 70 | print(f"Error writing config {section}.{key}: {e}") 71 | return False 72 | 73 | def _write_config_int(self, section, key, value): 74 | try: 75 | config = configparser.ConfigParser() 76 | config.read(self.config_path, encoding="utf-8") 77 | if not config.has_section(section): 78 | config.add_section(section) 79 | config.set(section, key, str(value)) 80 | with open(self.config_path, "w", encoding="utf-8") as f: 81 | config.write(f) 82 | return True 83 | except Exception as e: 84 | print(f"Error writing config {section}.{key}: {e}") 85 | return False 86 | 87 | def set_auto_overwrite(self, auto_overwrite): 88 | self.auto_overwrite = auto_overwrite 89 | self._write_config_bool("Settings", "auto_overwrite", auto_overwrite) 90 | 91 | def get_auto_overwrite(self): 92 | return self.auto_overwrite 93 | 94 | def set_auto_publish(self, auto_publish): 95 | self.auto_publish = auto_publish 96 | self._write_config_bool("Settings", "auto_publish", auto_publish) 97 | 98 | def get_auto_publish(self): 99 | return self.auto_publish 100 | 101 | def set_wait_time(self, wait_time): 102 | self.wait_time = wait_time 103 | self._write_config_int("Settings", "wait_time", wait_time) 104 | 105 | def get_wait_time(self): 106 | return max(self.wait_time, 10) 107 | 108 | def clear(self): 109 | self.choice_node_map.clear() 110 | 111 | def get_choice_server_folder(self): 112 | choice_server_folder = get_local_app_setting_path() / "choice_node" 113 | if not choice_server_folder.exists(): 114 | choice_server_folder.mkdir(parents=True) 115 | return choice_server_folder 116 | 117 | def load_choice_node_map(self): 118 | choice_server_folder = self.get_choice_server_folder() 119 | for file in choice_server_folder.glob("*.json"): 120 | try: 121 | with open(file, "r", encoding="utf-8") as f: 122 | data = json.load(f) 123 | if not isinstance(data, dict): 124 | print(f"Warning: Invalid JSON structure in file: {file}") 125 | continue 126 | elements = data.get("elements", []) 127 | if not isinstance(elements, list): 128 | print(f"Warning: 'elements' is not a list in file: {file}") 129 | continue 130 | for element in elements: 131 | if not isinstance(element, dict): 132 | continue 133 | if element.get("type") != "choice": 134 | continue 135 | addition = element.get("addition", {}) 136 | if not addition or not isinstance(addition, dict): 137 | continue 138 | if addition.get("node_type") != "RiceRoundAdvancedChoiceNode": 139 | continue 140 | settings = element.get("settings", {}) 141 | options = settings.get("options", []) 142 | python_class_name = addition.get("python_class_name") 143 | if python_class_name and isinstance(options, list): 144 | info = copy.deepcopy(addition) 145 | info["options_value"] = options 146 | self.choice_classname_map[python_class_name] = info 147 | except json.JSONDecodeError as e: 148 | print(f"Error parsing JSON from file {file}: {str(e)}") 149 | except Exception as e: 150 | print(f"Unexpected error processing file {file}: {str(e)}") 151 | continue 152 | 153 | def install_choice_node(self, template_id): 154 | user_token, error_msg, error_code = AuthUnit().get_user_token() 155 | template_file_path = self.get_choice_server_folder() / f"{template_id}.json" 156 | try: 157 | download_template(template_id, user_token, template_file_path) 158 | except Exception as e: 159 | print(f"failed to download template, {e}") 160 | return False 161 | return True 162 | 163 | def get_choice_node_addition(self, node_id): 164 | info = copy.deepcopy(self.choice_node_map.get(node_id, {})) 165 | if info and isinstance(info, dict): 166 | info.pop("options_value", None) 167 | return info 168 | return {} 169 | 170 | def get_choice_node_options(self, node_class_name): 171 | return self.choice_classname_map.get(node_class_name, {}).get( 172 | "options_value", [] 173 | ) 174 | 175 | def get_choice_classname(self, node_id): 176 | return self.choice_node_map.get(node_id, {}).get("python_class_name", "") 177 | 178 | def get_choice_value(self, node_id): 179 | return self.choice_node_map.get(node_id, {}).get("options_value", []) 180 | 181 | def set_node_additional_info(self, node_additional_info): 182 | if node_additional_info and isinstance(node_additional_info, dict): 183 | self.template_id = node_additional_info.get("template_id", "") 184 | choice_node_map = node_additional_info.get("choice_node_map", {}) 185 | for node_id, info in choice_node_map.items(): 186 | node_id = int(node_id) 187 | class_name = info.get("class_name", "") 188 | info["template_id"] = self.template_id 189 | info["display_name"] = class_name 190 | node_type = info.get("node_type", "") 191 | if node_type == "RiceRoundAdvancedChoiceNode": 192 | python_class_name = ( 193 | f"RiceRoundAdvancedChoiceNode_{self.template_id}_{node_id}" 194 | ) 195 | info["python_class_name"] = python_class_name 196 | self.choice_node_map[node_id] = info 197 | 198 | 199 | class RiceEnvConfig: 200 | def __init__(self): 201 | 0 202 | 203 | def filter_add_cmd(self, add_cmd): 204 | filtered_add_cmd = [] 205 | skip_next = False 206 | if not add_cmd: 207 | return "" 208 | try: 209 | for arg in add_cmd.split(): 210 | if skip_next: 211 | skip_next = False 212 | continue 213 | if arg in ["--listen", "--port"]: 214 | skip_next = True 215 | continue 216 | filtered_add_cmd.append(arg) 217 | except Exception as e: 218 | print(f"Error processing add_cmd: {e}") 219 | return "" 220 | return " ".join(filtered_add_cmd) 221 | 222 | def _get_python_path(self): 223 | try: 224 | return sys.executable.replace("\\", "/").strip("\"'") 225 | except Exception as e: 226 | print(f"Error getting Python path: {e}") 227 | return "" 228 | 229 | def _get_working_directory(self): 230 | try: 231 | script_abs_path = os.path.abspath(sys.argv[0]).replace("\\", "/") 232 | script_dir = os.path.dirname(script_abs_path) 233 | working_directory = script_dir 234 | custom_nodes_pattern = re.compile( 235 | "[/\\\\]custom_nodes[/\\\\]", re.IGNORECASE 236 | ) 237 | match = custom_nodes_pattern.search(script_dir) 238 | if match: 239 | custom_nodes_pos = match.start() 240 | potential_parent_dir = script_dir[:custom_nodes_pos] 241 | custom_nodes_dir = os.path.join(potential_parent_dir, "custom_nodes") 242 | if os.path.isdir(custom_nodes_dir) or os.path.isdir( 243 | os.path.join(potential_parent_dir, "Custom_Nodes") 244 | ): 245 | working_directory = potential_parent_dir 246 | return working_directory.strip("\"'") 247 | except Exception as e: 248 | print(f"Error getting working directory: {e}") 249 | try: 250 | return os.getcwd().replace("\\", "/").strip("\"'") 251 | except: 252 | return "" 253 | 254 | def _get_add_cmd(self): 255 | try: 256 | cmd_args = " ".join(sys.argv[1:]) 257 | return self.filter_add_cmd(cmd_args).strip() 258 | except Exception as e: 259 | print(f"Error getting command arguments: {e}") 260 | return "" 261 | 262 | def _get_script_name(self): 263 | try: 264 | py_file_pattern = re.compile("[/\\\\]([^/\\\\]+\\.py)", re.IGNORECASE) 265 | match = py_file_pattern.search(sys.argv[0]) 266 | if match: 267 | return match.group(1) 268 | basename = os.path.basename(sys.argv[0]).replace("\\", "/").strip("\"'") 269 | if re.search("\\.py$", basename, re.IGNORECASE): 270 | return basename 271 | else: 272 | return "main.py" 273 | except Exception as e: 274 | print(f"Error getting script name: {e}") 275 | return "main.py" 276 | 277 | def read_env(self): 278 | python_path = self._get_python_path() 279 | working_directory = self._get_working_directory() 280 | add_cmd = self._get_add_cmd() 281 | script_name = self._get_script_name() 282 | return { 283 | "PythonPath": python_path, 284 | "WorkingDirectory": working_directory, 285 | "AddCmd": add_cmd, 286 | "ScriptName": script_name, 287 | } 288 | -------------------------------------------------------------------------------- /rice_url_config.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | import json 3 | import os 4 | from PIL import Image 5 | from io import BytesIO 6 | import numpy as np 7 | import requests 8 | from urllib.parse import urljoin 9 | from .utils import get_local_app_setting_path 10 | 11 | DEFAULT_SUBDOMAIN = "api" if os.getenv("RICE_ROUND_DEBUG") != "true" else "test" 12 | _URL_PREFIX = os.getenv("RICE_ROUND_URL_PREFIX", "") 13 | DEFAULT_URL_PREFIX = ( 14 | _URL_PREFIX 15 | if _URL_PREFIX and len(_URL_PREFIX) > 10 16 | else f"https://{DEFAULT_SUBDOMAIN}.riceround.online" 17 | ) 18 | DEFAULT_WS_PREFIX = f"wss://{DEFAULT_SUBDOMAIN}.riceround.online" 19 | 20 | 21 | class UploadType(IntEnum): 22 | TEMPLATE_PUBLISH_IMAGE = 1 23 | USER_UPLOAD_TASK_IMAGE = 2 24 | MACHINE_TASK_RESULT = 1000 25 | 26 | 27 | class RiceUrlConfig: 28 | _instance = None 29 | _initialized = False 30 | 31 | def __new__(cls, *args, **kwargs): 32 | if cls._instance is None: 33 | cls._instance = super(RiceUrlConfig, cls).__new__(cls) 34 | return cls._instance 35 | 36 | def __init__(self): 37 | if not self._initialized: 38 | self._initialized = True 39 | 40 | def get_server_url(self, url_path): 41 | return urljoin(DEFAULT_URL_PREFIX, url_path) 42 | 43 | def get_ws_url(self, url_path): 44 | return urljoin(DEFAULT_WS_PREFIX, url_path) 45 | 46 | @property 47 | def machine_upload_sign_url(self): 48 | return self.get_server_url("/api/machine_client/upload_image_sign_url") 49 | 50 | @property 51 | def user_upload_sign_url(self): 52 | return self.get_server_url("/api/user/upload_sign_url") 53 | 54 | @property 55 | def prompt_task_url(self): 56 | return self.get_server_url("/api/workflow/add_task") 57 | 58 | @property 59 | def preview_refresh_url(self): 60 | return self.get_server_url("/api/workflow/refresh_preview") 61 | 62 | @property 63 | def task_ws_url(self): 64 | return self.get_ws_url("/api/workflow/task_websocket") 65 | 66 | @property 67 | def workflow_preview_url(self): 68 | return self.get_server_url("/api/workflow/refresh_preview") 69 | 70 | @property 71 | def get_info_url(self): 72 | return self.get_server_url("/api/workflow/get_info") 73 | 74 | @property 75 | def machine_bind_key_url(self): 76 | return self.get_server_url("/api/machine_bind/key") 77 | 78 | @property 79 | def workflow_template_url(self): 80 | return self.get_server_url("/api/workflow/get_template") 81 | 82 | @property 83 | def publisher_workflow_url(self): 84 | return self.get_server_url("/api/publisher/workflow") 85 | 86 | 87 | def user_upload_imagefile(image_file_path, user_token): 88 | if not os.path.exists(image_file_path): 89 | raise ValueError(f"Image file not found: {image_file_path}") 90 | content_types = { 91 | ".png": "image/png", 92 | ".jpg": "image/jpeg", 93 | ".jpeg": "image/jpeg", 94 | ".webp": "image/webp", 95 | ".bmp": "image/bmp", 96 | } 97 | file_extension = os.path.splitext(image_file_path)[1].lower() 98 | if file_extension not in content_types: 99 | raise ValueError( 100 | f"Unsupported image format: {file_extension}. Supported formats: {', '.join(content_types.keys())}" 101 | ) 102 | content_type = content_types[file_extension] 103 | upload_sign_url = RiceUrlConfig().user_upload_sign_url 104 | headers = {"Authorization": f"Bearer {user_token}"} 105 | params = { 106 | "upload_type": UploadType.USER_UPLOAD_TASK_IMAGE.value, 107 | "file_type": content_type, 108 | } 109 | response = requests.get(upload_sign_url, headers=headers, params=params) 110 | upload_url = "" 111 | download_url = "" 112 | if response.status_code == 200: 113 | response_data = response.json() 114 | if response_data.get("code") == 0: 115 | upload_url = response_data.get("data", {}).get("upload_sign_url", "") 116 | download_url = response_data.get("data", {}).get("download_url", "") 117 | else: 118 | raise ValueError( 119 | f"Failed to get upload URL. Status code: {response.status_code}" 120 | ) 121 | if not upload_url or not download_url: 122 | raise ValueError("Failed to get upload URL. Upload sign URL is empty") 123 | try: 124 | with open(image_file_path, "rb") as f: 125 | image_data = f.read() 126 | response = requests.put( 127 | upload_url, data=image_data, headers={"Content-Type": content_type} 128 | ) 129 | if response.status_code == 200: 130 | return download_url 131 | else: 132 | raise ValueError( 133 | f"Failed to upload image. Status code: {response.status_code}" 134 | ) 135 | except IOError as e: 136 | raise ValueError(f"Failed to read image file: {str(e)}") 137 | 138 | 139 | def user_upload_image(image, user_token): 140 | upload_sign_url = RiceUrlConfig().user_upload_sign_url 141 | headers = {"Authorization": f"Bearer {user_token}"} 142 | params = { 143 | "upload_type": UploadType.USER_UPLOAD_TASK_IMAGE.value, 144 | "file_type": "image/png", 145 | } 146 | response = requests.get(upload_sign_url, headers=headers, params=params) 147 | upload_url = "" 148 | download_url = "" 149 | if response.status_code == 200: 150 | response_data = response.json() 151 | if response_data.get("code") == 0: 152 | upload_url = response_data.get("data", {}).get("upload_sign_url", "") 153 | download_url = response_data.get("data", {}).get("download_url", "") 154 | else: 155 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 156 | if not upload_url or not download_url: 157 | raise ValueError(f"failed to upload image. upload_sign_url is empty") 158 | i = 255.0 * image.cpu().numpy() 159 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 160 | bytesIO = BytesIO() 161 | img.save(bytesIO, format="PNG", quality=95, compress_level=1) 162 | send_bytes = bytesIO.getvalue() 163 | response = requests.put( 164 | upload_sign_url, data=send_bytes, headers={"Content-Type": "image/png"} 165 | ) 166 | if response.status_code == 200: 167 | return download_url 168 | else: 169 | print(f"failed to upload image. Status code: {response.status_code}") 170 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 171 | 172 | 173 | def machine_upload_image(image, task_id): 174 | upload_image_sign_url = RiceUrlConfig().machine_upload_sign_url 175 | print(f"upload_image_sign_url: {upload_image_sign_url}") 176 | i = 255.0 * image.cpu().numpy() 177 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 178 | bytesIO = BytesIO() 179 | img.save(bytesIO, format="PNG", quality=95, compress_level=1) 180 | send_bytes = bytesIO.getvalue() 181 | upload_url = "" 182 | download_url = "" 183 | params = { 184 | "upload_type": UploadType.MACHINE_TASK_RESULT.value, 185 | "file_type": "image/png", 186 | "task_id": task_id, 187 | } 188 | response = requests.get(upload_image_sign_url, params=params) 189 | if response.status_code == 200: 190 | response_data = response.json() 191 | if response_data.get("code") == 0: 192 | upload_url = response_data.get("data", {}).get("upload_sign_url", "") 193 | download_url = response_data.get("data", {}).get("download_url", "") 194 | else: 195 | print( 196 | f"failed to upload image. Status code: {response.status_code}, Response: {response.text}" 197 | ) 198 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 199 | if not upload_url or not download_url: 200 | raise ValueError(f"failed to upload image. upload_sign_url is empty") 201 | response = requests.put( 202 | upload_url, data=send_bytes, headers={"Content-Type": "image/png"} 203 | ) 204 | if response.status_code == 200: 205 | return download_url 206 | else: 207 | print( 208 | f"failed to upload image. Status code: {response.status_code}, Response: {response.text}" 209 | ) 210 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 211 | 212 | 213 | def download_template(template_id, user_token, save_path): 214 | workflow_template_url = RiceUrlConfig().workflow_template_url 215 | headers = {"Authorization": f"Bearer {user_token}"} if user_token else {} 216 | params = {"template_id": template_id} 217 | response = requests.get(workflow_template_url, headers=headers, params=params) 218 | if response.status_code != 200: 219 | raise ValueError(f"Failed to get template. Status code: {response.status_code}") 220 | response_data = response.json() 221 | if response_data.get("code") != 0: 222 | raise ValueError(f"Failed to get template. Error: {response_data.get('msg')}") 223 | download_url = response_data.get("data", {}).get("download_url") 224 | if not download_url: 225 | raise ValueError("Template download URL is empty") 226 | template_response = requests.get(download_url) 227 | if template_response.status_code != 200: 228 | raise ValueError( 229 | f"Failed to download template. Status code: {template_response.status_code}" 230 | ) 231 | try: 232 | template_data = template_response.json() 233 | if template_data.get("template_id") != template_id: 234 | raise ValueError( 235 | f"Template ID mismatch. Expected: {template_id}, Got: {template_data.get('template_id')}" 236 | ) 237 | with open(save_path, "wb") as file: 238 | file.write(template_response.content) 239 | return template_data 240 | except json.JSONDecodeError: 241 | raise ValueError("Failed to parse template JSON data") 242 | -------------------------------------------------------------------------------- /rice_websocket.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | import json 4 | import time 5 | import threading 6 | from typing import Any, Callable, Optional 7 | import asyncio 8 | import websockets 9 | from websockets.exceptions import ConnectionClosedError 10 | import comfy.model_management as model_management 11 | 12 | COMMAND_TYPE_USER_SERVER_TASK_PROGRESS = 5004 13 | COMMAND_TYPE_USER_CLIENT_WEB_COMMAND_CANCEL_TASK = 4002 14 | 15 | 16 | class TaskStatus(Enum): 17 | CREATED = 0 18 | PENDING = 1 19 | IN_PROGRESS = 2 20 | FINISHED = 3 21 | FAILED = 4 22 | CANCELLED = 5 23 | 24 | def __lt__(self, other): 25 | if self.__class__ is other.__class__: 26 | return self.value < other.value 27 | return NotImplemented 28 | 29 | def __le__(self, other): 30 | if self.__class__ is other.__class__: 31 | return self.value <= other.value 32 | return NotImplemented 33 | 34 | def __gt__(self, other): 35 | if self.__class__ is other.__class__: 36 | return self.value > other.value 37 | return NotImplemented 38 | 39 | def __ge__(self, other): 40 | if self.__class__ is other.__class__: 41 | return self.value >= other.value 42 | return NotImplemented 43 | 44 | 45 | class TaskInfo: 46 | def __init__(self, json_data): 47 | self.task_uuid = json_data.get("task_uuid", "") 48 | self.state = TaskStatus(json_data.get("state", 0)) 49 | self.progress = json_data.get("progress", 0) 50 | self.progress_text = json_data.get("progress_text", "") 51 | self.thumbnail = json_data.get("thumbnail", "") 52 | self.prompt = json_data.get("prompt", "") 53 | self.create_time = json_data.get("create_time", "") 54 | self.update_time = json_data.get("update_time", "") 55 | self.template_id = json_data.get("template_id", "") 56 | self.template_description = json_data.get("template_description", "") 57 | self.template_name = json_data.get("template_name", "") 58 | self.result_data = json_data.get("result_data", None) 59 | self.lock = threading.Lock() 60 | self.preview_refreshed = False 61 | 62 | def to_dict(self): 63 | return { 64 | "task_uuid": self.task_uuid, 65 | "state": self.state.value, 66 | "progress": self.progress, 67 | "progress_text": self.progress_text, 68 | "thumbnail": self.thumbnail, 69 | "prompt": self.prompt, 70 | "create_time": self.create_time, 71 | "update_time": self.update_time, 72 | "template_id": self.template_id, 73 | "template_description": self.template_description, 74 | "template_name": self.template_name, 75 | } 76 | 77 | def update_progress(self, json_data): 78 | with self.lock: 79 | if json_data.get("task_uuid", "") != self.task_uuid: 80 | return False 81 | state = TaskStatus(json_data.get("state", 0)) 82 | if state < self.state: 83 | return False 84 | self.state = state 85 | progress = json_data.get("progress", 0) 86 | progress_text = json_data.get("progress_text", "") 87 | if progress == 0 and progress_text == "preview_refreshed": 88 | print(f"Task {self.task_uuid} preview_refreshed") 89 | self.preview_refreshed = True 90 | else: 91 | self.preview_refreshed = False 92 | if progress > self.progress: 93 | self.progress = progress 94 | self.progress_text = progress_text 95 | elif state == TaskStatus.FAILED: 96 | self.progress_text = ( 97 | progress_text if progress_text else "task failed" 98 | ) 99 | elif state == TaskStatus.CANCELLED: 100 | self.progress_text = ( 101 | progress_text if progress_text else "task cancelled" 102 | ) 103 | else: 104 | return False 105 | result_data = json_data.get("result_data", None) 106 | if result_data: 107 | self.result_data = result_data 108 | elif self.state == TaskStatus.IN_PROGRESS: 109 | self.result_data = None 110 | return True 111 | 112 | def is_task_done(self): 113 | with self.lock: 114 | return self.state >= TaskStatus.FINISHED 115 | 116 | def __str__(self): 117 | return f"Task {self.task_uuid}: {self.state.name} ({self.progress}%) - {self.progress_text}" 118 | 119 | 120 | class PackageMessage: 121 | def __init__(self, CommandType, Message): 122 | self.CommandType = CommandType 123 | self.Message = Message 124 | 125 | def to_json(self): 126 | return json.dumps({"CommandType": self.CommandType, "Message": self.Message}) 127 | 128 | @classmethod 129 | def from_json(cls, data): 130 | parsed = json.loads(data) 131 | return cls(parsed["CommandType"], parsed["Message"]) 132 | 133 | 134 | class TaskWebSocket: 135 | def __init__( 136 | self, url, token, machine_id, task_info, progress_callback, timeout=600 137 | ): 138 | self.url = f"{url}?machine_id={machine_id}" 139 | self.token = token 140 | self.task_info = task_info 141 | self.stop_event = asyncio.Event() 142 | self.timeout = timeout 143 | self.progress_callback = progress_callback 144 | self.last_progress_time = None 145 | self.message_timeout = timeout - 3 if timeout < 600 else 600 146 | self.websocket = None 147 | self.task = None 148 | 149 | async def connect(self): 150 | try: 151 | ws_url = f"{self.url}&token={self.token}" 152 | async with websockets.connect(ws_url) as websocket: 153 | self.websocket = websocket 154 | await self.on_connection_open() 155 | await self.run_tasks() 156 | except Exception as e: 157 | print(f"Connection error: {e}") 158 | 159 | async def run_tasks(self): 160 | try: 161 | receive_task = asyncio.create_task(self.on_receive()) 162 | monitor_task = asyncio.create_task(self.monitor_progress_timeout()) 163 | done, pending = await asyncio.wait( 164 | [receive_task, monitor_task], 165 | timeout=self.timeout, 166 | return_when=asyncio.FIRST_COMPLETED, 167 | ) 168 | self.stop_event.set() 169 | for task in pending: 170 | print(f"cancel task {task}") 171 | task.cancel() 172 | try: 173 | await task 174 | except asyncio.CancelledError: 175 | pass 176 | finally: 177 | self.stop_event.set() 178 | 179 | async def on_receive(self): 180 | try: 181 | if not self.websocket or self.stop_event.is_set(): 182 | return 183 | async for message in self.websocket: 184 | if self.stop_event.is_set(): 185 | break 186 | await self.on_message(message) 187 | except Exception as e: 188 | print(f"Error while listening to messages: {e}") 189 | 190 | async def monitor_progress_timeout(self): 191 | while not self.stop_event.is_set(): 192 | await asyncio.sleep(5) 193 | try: 194 | model_management.throw_exception_if_processing_interrupted() 195 | except Exception as e: 196 | print(f"Processing interrupted during progress monitoring: {e}") 197 | self.stop_event.set() 198 | cancel_message = PackageMessage( 199 | CommandType=COMMAND_TYPE_USER_CLIENT_WEB_COMMAND_CANCEL_TASK, 200 | Message={"task_uuid": self.task_info.task_uuid}, 201 | ) 202 | try: 203 | await self.send_message(cancel_message) 204 | except Exception as send_err: 205 | print(f"Failed to send cancel notification: {send_err}") 206 | break 207 | current_time = asyncio.get_event_loop().time() 208 | if ( 209 | self.last_progress_time 210 | and current_time - self.last_progress_time > self.message_timeout 211 | ): 212 | print( 213 | f"No task progress received within {self.message_timeout} seconds, disconnecting..." 214 | ) 215 | self.stop_event.set() 216 | break 217 | 218 | async def on_message(self, message): 219 | try: 220 | package = PackageMessage.from_json(message) 221 | if package.CommandType == COMMAND_TYPE_USER_SERVER_TASK_PROGRESS: 222 | await self.handle_task_progress(package) 223 | else: 224 | print(f"Unknown message type: {package.CommandType}") 225 | except Exception as e: 226 | print(f"Message unpacking error: {e}") 227 | 228 | async def on_connection_open(self): 229 | print("WebSocket connection connected") 230 | 231 | async def send_message(self, message): 232 | try: 233 | if self.websocket: 234 | await self.websocket.send(message.to_json()) 235 | except Exception as e: 236 | print(f"Error sending message: {e}") 237 | 238 | async def handle_task_progress(self, package): 239 | if not self.task_info or self.stop_event.is_set(): 240 | return 241 | loop = asyncio.get_event_loop() 242 | self.last_progress_time = loop.time() 243 | if self.task_info.update_progress(package.Message): 244 | print(f"Task progress updated: {self.task_info}") 245 | if self.progress_callback: 246 | self.progress_callback( 247 | self.task_info.task_uuid, 248 | self.task_info.progress_text, 249 | self.task_info.progress, 250 | self.task_info.preview_refreshed, 251 | ) 252 | if self.task_info.is_task_done(): 253 | print("task is done") 254 | self.stop_event.set() 255 | 256 | async def shutdown(self): 257 | print("shutdown websocket") 258 | self.stop_event.set() 259 | self.progress_callback = None 260 | if self.websocket: 261 | try: 262 | await self.websocket.close() 263 | except websockets.ConnectionClosed: 264 | pass 265 | finally: 266 | self.websocket = None 267 | 268 | 269 | def start_and_wait_task_done( 270 | task_ws_url, user_token, machine_id, task_info, progress_callback, timeout=7200 271 | ): 272 | async def main(): 273 | task_ws = TaskWebSocket( 274 | task_ws_url, user_token, machine_id, task_info, progress_callback, timeout 275 | ) 276 | try: 277 | await task_ws.connect() 278 | except asyncio.CancelledError: 279 | print("Task cancelled") 280 | finally: 281 | await task_ws.shutdown() 282 | 283 | asyncio.run(main()) 284 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import hashlib 3 | from io import BytesIO 4 | import os 5 | from pathlib import Path 6 | import random 7 | import string 8 | import sys 9 | import uuid 10 | import torch 11 | import numpy as np 12 | from PIL import Image 13 | import platform 14 | import subprocess 15 | 16 | 17 | def pil2tensor(images): 18 | def single_pil2tensor(image): 19 | np_image = np.array(image).astype(np.float32) / 255.0 20 | if np_image.ndim == 2: 21 | return torch.from_numpy(np_image).unsqueeze(0) 22 | else: 23 | return torch.from_numpy(np_image).unsqueeze(0) 24 | 25 | if isinstance(images, Image.Image): 26 | return single_pil2tensor(images) 27 | else: 28 | return torch.cat([single_pil2tensor(img) for img in images], dim=0) 29 | 30 | 31 | def calculate_machine_id(): 32 | return str(uuid.getnode()) 33 | 34 | 35 | def normalize_machine_id(machine_id): 36 | salt = "RiceRound" 37 | trimmed_id = machine_id.strip() 38 | lowercase_id = trimmed_id.lower() 39 | salted_id = lowercase_id + salt 40 | hash_obj = hashlib.md5(salted_id.encode("utf-8")) 41 | return hash_obj.hexdigest() 42 | 43 | 44 | def get_local_app_setting_path(): 45 | home = Path.home() 46 | config_dir = home / "RiceRound" 47 | return config_dir 48 | 49 | 50 | def get_machine_id(): 51 | config_dir = get_local_app_setting_path() 52 | config_file = config_dir / "machine.ini" 53 | try: 54 | config_dir.mkdir(parents=True, exist_ok=True) 55 | except Exception as e: 56 | print(f"Error creating directory '{config_dir}': {e}") 57 | return "" 58 | config = configparser.ConfigParser() 59 | try: 60 | if config_file.exists(): 61 | config.read(config_file, encoding="utf-8") 62 | if "Machine" in config and "machine_id" in config["Machine"]: 63 | return config["Machine"]["machine_id"] 64 | original_host_id = calculate_machine_id() 65 | machine_id = normalize_machine_id(original_host_id) 66 | if "Machine" not in config: 67 | config.add_section("Machine") 68 | config.set("Machine", "machine_id", machine_id) 69 | with open(config_file, "w", encoding="utf-8") as file: 70 | config.write(file) 71 | return machine_id 72 | except Exception as e: 73 | print(f"Error handling machine ID in '{config_file}': {e}") 74 | return "" 75 | 76 | 77 | def restart_comfyui(): 78 | try: 79 | sys.stdout.close_log() 80 | except Exception: 81 | pass 82 | if "__COMFY_CLI_SESSION__" in os.environ: 83 | with open(os.path.join(os.environ["__COMFY_CLI_SESSION__"] + ".reboot"), "w"): 84 | 0 85 | print("Restarting...\n\n") 86 | exit(0) 87 | sys_argv = sys.argv.copy() 88 | if "--windows-standalone-build" in sys_argv: 89 | sys_argv.remove("--windows-standalone-build") 90 | if sys.platform.startswith("win32"): 91 | cmds = ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:] 92 | else: 93 | cmds = [sys.executable] + sys_argv 94 | print(f"Command: {cmds}", flush=True) 95 | return os.execv(sys.executable, cmds) 96 | 97 | 98 | def combine_files(files, password, zip_file_path): 99 | import pyzipper 100 | 101 | for file_path in files: 102 | if not os.path.exists(file_path): 103 | raise FileNotFoundError(f"file not found: {file_path}") 104 | try: 105 | with pyzipper.AESZipFile( 106 | zip_file_path, 107 | "w", 108 | compression=pyzipper.ZIP_DEFLATED, 109 | encryption=pyzipper.WZ_AES, 110 | ) as zipf: 111 | if isinstance(password, str): 112 | password = password.encode("utf-8") 113 | zipf.setpassword(password) 114 | for index, file_path in enumerate(files, start=1): 115 | arcname = f"{index}.bin" 116 | zipf.write(file_path, arcname) 117 | return True 118 | except Exception as e: 119 | print(f"Error creating zip: {str(e)}") 120 | return False 121 | 122 | 123 | def generate_random_string(length): 124 | random.seed() 125 | letters = string.ascii_letters 126 | return "".join(random.choice(letters) for _ in range(length)) 127 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"version":"1.0.0","main_download_link":"https://www.riceround.online/node_download/ComfyUI_RiceRound.7z","backup_download_link":"https://download.riceround.online/node_download/ComfyUI_RiceRound.7z"} --------------------------------------------------------------------------------