Files
RiceRound-ComfyUI_RiceRound/output_node.py
T
2025-02-20 14:54:05 +08:00

451 lines
14 KiB
Python

from io import BytesIO
import json
import os
import re
from pathlib import Path
from PIL import Image, ImageOps
from PIL.PngImagePlugin import PngInfo
import torch
from comfy import model_management
import requests
import numpy as np
import folder_paths
from nodes import LoadImage
from comfy.utils import ProgressBar
from server import PromptServer
from .rice_def import RiceRoundErrorDef, RiceTaskErrorDef
from .rice_url_config import RiceUrlConfig, user_upload_image, user_upload_imagefile
from .utils import get_machine_id, pil2tensor
from .auth_unit import AuthUnit
from .rice_prompt_info import RicePromptInfo
from .rice_websocket import (
TaskInfo,
TaskStatus,
TaskWebSocket,
start_and_wait_task_done,
)
class RiceRoundDecryptNode:
def __init__(self):
self.auth_unit = AuthUnit()
self.machine_id = get_machine_id()
self.url_config = RiceUrlConfig()
self.pbar = None
self.last_progress = 0
self.user_token = None
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"rice_template_id": ("STRING", {"default": ""}),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "The random seed used for creating the noise.",
},
),
},
"optional": {"input_anything": ("*", {})},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
@classmethod
def VALIDATE_INPUTS(s, input_types):
for key, value in input_types.items():
if key.startswith("input_anything"):
if value not in ("STRING", "TEXT", "PROMPT"):
return f"{key} must be of string type"
return True
RETURN_TYPES = ("IMAGE",)
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "RiceRound/Output"
def progress_callback(self, task_uuid, progress_text, progress, preview_refreshed):
if not self.pbar:
return
if preview_refreshed:
url = self.url_config.workflow_preview_url + "?task_uuid=" + task_uuid
try:
headers = {"Authorization": f"Bearer {self.user_token}"}
response = requests.get(url, stream=True, headers=headers)
response.raise_for_status()
preview_image = Image.open(BytesIO(response.content))
self.pbar.update_absolute(
self.last_progress, preview=("PNG", preview_image, None)
)
except Exception as e:
print(f"Failed to load preview image: {str(e)}")
self.pbar.update_absolute(self.last_progress)
else:
self.last_progress = progress
self.pbar.update_absolute(progress)
def execute(self, rice_template_id, **kwargs):
self.pbar = ProgressBar(100)
self.user_token, error_msg, error_code = self.auth_unit.get_user_token()
if not self.user_token:
if (
error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED
or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR
):
AuthUnit().login_dialog("运行云节点需要先完成登录")
else:
PromptServer.instance.send_sync(
"riceround_toast",
{"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"},
)
raise ValueError(error_msg)
index_dict = {}
for k, v in kwargs.items():
if k.startswith("input_anything"):
suffix = k[len("input_anything") :]
suffix = re.sub("\\s*\\([^)]*\\)", "", suffix)
index = 0 if suffix == "" else int(suffix)
if index in index_dict:
raise ValueError(f"Duplicate input_anything index: {index}")
if isinstance(v, str):
index_dict[str(index)] = v
else:
raise ValueError(f"Invalid input type: {type(v)}")
if not index_dict:
return (torch.zeros(1, 1, 1, 3),)
task_info = self.create_task(index_dict, rice_template_id, self.user_token)
if not task_info or not task_info.task_uuid:
raise ValueError("Failed to create task")
start_and_wait_task_done(
self.url_config.task_ws_url,
self.user_token,
self.machine_id,
task_info,
self.progress_callback,
RicePromptInfo().get_wait_time(),
)
model_management.throw_exception_if_processing_interrupted()
result_data = task_info.result_data
if not result_data:
if task_info.progress_text and task_info.state > TaskStatus.FINISHED:
raise ValueError(task_info.progress_text)
else:
raise ValueError("websocket failed")
image_results = result_data.get("image_results", [])
if not image_results:
raise ValueError("Failed to get image results")
images = []
for image_url in image_results:
image = Image.open(requests.get(image_url, stream=True).raw)
image = ImageOps.exif_transpose(image)
images.append(pil2tensor(image))
image_tensor = torch.cat(images, dim=0)
self.pbar = None
return (image_tensor,)
def create_task(self, input_data, template_id, user_token):
"\n Create a task and return the task UUID.\n \n Args:\n task_url (str): The URL to send the task request to\n request_data (dict): The data to send in the request\n headers (dict): The headers to send with the request\n \n Returns:\n str: The task UUID if successful\n \n Raises:\n ValueError: If the request fails or response is invalid\n"
task_url = self.url_config.prompt_task_url
headers = {
"Authorization": f"Bearer {user_token}",
"Content-Type": "application/json",
}
request_data = {
"taskData": json.dumps(input_data),
"workData": json.dumps({"template_id": template_id}),
}
response = requests.post(task_url, json=request_data, headers=headers)
if response.status_code == 200:
response_data = response.json()
if response_data.get("code") == 0 and "data" in response_data:
task_info = TaskInfo(response_data.get("data", {}))
if task_info.task_uuid:
return task_info
else:
raise ValueError("No task UUID in response")
else:
raise ValueError(
f"API error: {response_data.get('message','Unknown error')}"
)
elif response.status_code == RiceRoundErrorDef.HTTP_INTERNAL_ERROR:
response_data = response.json()
if (
response_data.get("code")
== RiceTaskErrorDef.ERROR_INSUFFICIENT_PERMISSION_INSUFFICIENT_BALANCE
):
PromptServer.instance.send_sync(
"riceround_show_workflow_payment_dialog",
{"template_id": template_id, "title": "余额不足,请充值"},
)
raise ValueError(f"余额不足,运行失败,请完成支付后重试!")
else:
raise ValueError(
f"API error: {response_data.get('message','Unknown error')}"
)
else:
raise ValueError(f"HTTP error {response.status_code}: {response.text}")
class RiceRoundBaseChoiceNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(cls):
node_name = getattr(cls, "__node_name__", None)
options = (
RicePromptInfo().get_choice_node_options(node_name) if node_name else []
)
return {
"required": {
"name": ("STRING", {"default": "Parameter"}),
"default": (options,),
},
"optional": {},
"hidden": {},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
FUNCTION = "placeholder"
CATEGORY = "__hidden__"
def placeholder(self, default, **kwargs):
return (default,)
def upload_imagefile(image_path):
user_token, error_msg, error_code = AuthUnit().get_user_token()
if not user_token:
if (
error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED
or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR
):
AuthUnit().login_dialog("运行云节点需要先完成登录")
else:
PromptServer.instance.send_sync(
"riceround_toast", {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"}
)
raise ValueError(error_msg)
return user_upload_imagefile(image_path, user_token)
def upload_image(image):
user_token, error_msg, error_code = AuthUnit().get_user_token()
if not user_token:
if (
error_code == RiceRoundErrorDef.HTTP_UNAUTHORIZED
or error_code == RiceRoundErrorDef.NO_TOKEN_ERROR
):
AuthUnit().login_dialog("运行云节点需要先完成登录")
else:
PromptServer.instance.send_sync(
"riceround_toast", {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"}
)
raise ValueError(error_msg)
return user_upload_image(image, user_token)
class RiceRoundImageUrlNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {"required": {"image_url": ("STRING",)}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "RiceRound/Output"
def load_image(self, image_url, **kwargs):
return (image_url,)
class RiceRoundUploadImageNode(LoadImage):
def __init__(self):
super().__init__()
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [
f
for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))
]
return {"required": {"image": (sorted(files), {"image_upload": True})}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "RiceRound/Output"
def load_image(self, image, **kwargs):
image_path = folder_paths.get_annotated_filepath(image)
download_url = upload_imagefile(image_path)
return (download_url,)
class RiceRoundOutputImageBridgeNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {
"required": {"images": ("IMAGE", {"tooltip": "only image."})},
"optional": {},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, images, **kwargs):
return upload_image(images)
class RiceRoundOutputMaskBridgeNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {"required": {"mask": ("MASK",)}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, mask, **kwargs):
mask_np = mask.cpu().numpy()
mask_np = (mask_np * 255).astype(np.uint8)
mask_image = Image.fromarray(mask_np)
image_url = upload_image(mask_image)
return (image_url,)
class RiceRoundMaskUrlNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {"required": {"mask_url": ("STRING",)}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "RiceRound/Output"
def load_image(self, mask_url, **kwargs):
return (mask_url,)
class RiceRoundOutputIntNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"name": ("STRING", {"default": "数值"}),
"number": ("INT",),
"min": ("INT", {"default": 0}),
"max": ("INT", {"default": 1000000}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, name, number, min, max, **kwargs):
return (str(number),)
class RiceRoundOutputFloatNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"name": ("STRING", {"default": "数值"}),
"number": ("FLOAT",),
"min": ("FLOAT", {"default": 0.0}),
"max": ("FLOAT", {"default": 1e6}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, name, number, min, max, **kwargs):
return (str(number),)
class RiceRoundOutputBooleanNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"name": ("STRING", {"default": "开关"}),
"value": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, name, value, **kwargs):
str_value = "true" if value else "false"
return (str_value,)
class RiceRoundOutputTextNode:
def __init__(self):
0
@classmethod
def INPUT_TYPES(s):
return {"required": {"name": ("STRING", {"default": "文本"}), "str": ("STRING",)}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
FUNCTION = "bridge"
CATEGORY = "RiceRound/Output"
def bridge(self, name, str, **kwargs):
return (str,)