1218 lines
49 KiB
Python
1218 lines
49 KiB
Python
# ComfyUI remote node execution nodes.
|
|
# Copyright (C) 2024 LatentRat
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
# TODO: option to actually run IS_CHANGED checks remotely
|
|
# TODO: option to lazily serialize/send inputs only when needed remotely? and option to cache them remotely too
|
|
# TODO: torch weights_only option
|
|
# TODO: clear remote history item afterwards option
|
|
|
|
import base64
|
|
import collections
|
|
import copy
|
|
import enum
|
|
import functools
|
|
import gzip
|
|
import math
|
|
import threading
|
|
import time
|
|
import os
|
|
from io import BytesIO
|
|
|
|
from uuid import uuid4
|
|
from zipfile import ZipFile
|
|
|
|
import execution
|
|
from comfy_execution.graph import DynamicPrompt
|
|
|
|
import requests
|
|
import torch
|
|
import websockets.sync.client
|
|
|
|
from .libs.safe_load import torch_safe_load_dict
|
|
from .libs.fancy_safetensors import torch as fst_torch
|
|
|
|
import json
|
|
|
|
import logging as _logging
|
|
|
|
from comfy.comfy_types import IO as CoIO
|
|
|
|
logger = _logging.getLogger(__name__)
|
|
|
|
jdumps = lambda data: json.dumps(data).replace("\n", "")
|
|
|
|
_NUM_OUTPUTS = int(os.environ.get("RAT_REMOTE_RUN_NUM_OUTPUTS") or 10)
|
|
logger.info("%s", f"RemoteRun nodes using _NUM_OUTPUTS={_NUM_OUTPUTS}")
|
|
|
|
TOGGLE_CHOICES = ("only_locally", "only_remotely")
|
|
INPUTS_OFF_OPTIONS = ["lazy", "disconnected"]
|
|
BLOCK_OPTIONS = ["error", "block_execution_silent", "block_execution_verbose", "passthrough", "return_none"]
|
|
|
|
|
|
class BinaryResponseMessageID(enum.IntEnum):
|
|
BINARY_RESPONSE_MESSAGE_ID = 12341119
|
|
GZIPPED_BINARY_RESPONSE_MESSAGE_ID = 12341120
|
|
|
|
|
|
def serialization_load_input(name = "serialization"):
|
|
return { name: (["safe_torch_pt", "unsafe_torch_pt", "fancy_safetensors"], { "default": "fancy_safetensors" }) }
|
|
|
|
|
|
def response_input(name = "response"):
|
|
return { name: (["base64_result", "binary"], { "default": "base64_result" }) }
|
|
|
|
|
|
def ws_settings_inputs():
|
|
return {
|
|
"ws_compression": ("BOOLEAN", { "default": False }),
|
|
# strings to make it easier to add more in case of wanting a partial option without breaking backwards compatibility
|
|
"forward_progress_messages": (["on", "off"], { "default": "on" }),
|
|
}
|
|
|
|
|
|
def shared_input_settings():
|
|
return {
|
|
"max_size_mb": ("INT", { "default": 32, "min": 0, "max": 8 * 1024, "step": 1 }),
|
|
"gzip_compression": ("BOOLEAN", { "default": False }),
|
|
"gzip_level": ("INT", { "default": 9, "min": 1, "max": 9, "step": 1 }),
|
|
}
|
|
|
|
|
|
class RemoteRunSerializerOutNode():
|
|
TYPE_NAME = "RAT_RemoteRunSerializerOut"
|
|
DISPLAY_NAME = "ReRu Serializer Output (Internal)"
|
|
|
|
CATEGORY = "Remote Run/__Internal__/"
|
|
FUNCTION = "run"
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, is_changed: bool = False, **_kwargs):
|
|
return time.time_ns() if is_changed else None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
**serialization_load_input(),
|
|
},
|
|
"optional": {
|
|
**dict((f"input_{num}", (CoIO.ANY, { })) for num in range(_NUM_OUTPUTS)),
|
|
**shared_input_settings(),
|
|
**response_input(),
|
|
"is_changed": ("BOOLEAN", { "default": False }),
|
|
},
|
|
}
|
|
return inputs
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
|
|
def run(self,
|
|
serialization: str,
|
|
max_size_mb: int | None = None, gzip_compression: bool = False, gzip_level: int = 9, response = None,
|
|
**kwargs,
|
|
):
|
|
inputs = { k: v for k, v in kwargs.items() if k.startswith("input_") }
|
|
|
|
data = serialize_obj(serialization, inputs)
|
|
if response == "binary":
|
|
import server
|
|
instance = server.PromptServer.instance
|
|
client_id = instance.client_id
|
|
if not client_id:
|
|
raise ValueError("No client connected to send binary response to")
|
|
|
|
data_type = BinaryResponseMessageID.BINARY_RESPONSE_MESSAGE_ID.value
|
|
if gzip_compression:
|
|
start = time.monotonic_ns()
|
|
comp = gzip.compress(data, compresslevel = gzip_level)
|
|
end = time.monotonic_ns()
|
|
took_ns = (end - start)
|
|
mb_s = (len(data) * 1e9) / (took_ns * 1024 * 1024)
|
|
took_sec = took_ns / 1e9
|
|
factor = len(comp) / len(data)
|
|
if factor < 0.99:
|
|
logger.info("%s", f"RemoteRunSerializerOutNode.run serialization={serialization!r} {response=!r} "
|
|
f"gzip compressed {len(data)} to {len(comp)} bytes, factor {factor:.3f}, "
|
|
f"took {took_sec:.3f} sec, {mb_s:.2f} MB/s")
|
|
data = comp
|
|
data_type = BinaryResponseMessageID.GZIPPED_BINARY_RESPONSE_MESSAGE_ID.value
|
|
else:
|
|
logger.info("%s", f"RemoteRunSerializerOutNode.run serialization={serialization!r} {response=!r} "
|
|
f"gzip compression not effective, kept original {len(data)} bytes "
|
|
f"over compressed {len(comp)} bytes, factor {factor:.3f}, "
|
|
f"took {took_sec:.3f} sec, {mb_s:.2f} MB/s")
|
|
|
|
logger.info("%s", f"RemoteRunSerializerOutNode.run serialization={serialization!r} {response=!r} "
|
|
f"data_size={len(data)}, {max_size_mb=} {client_id=!r}")
|
|
|
|
if max_size_mb and len(data) > max_size_mb * 1024 * 1024:
|
|
raise ValueError(f"Serialized data too large: {len(data)} bytes, max_size_mb={max_size_mb}")
|
|
|
|
instance.send_sync(data_type, data, client_id)
|
|
return ("",)
|
|
|
|
results = base64.b64encode(data).decode()
|
|
logger.info("%s", f"RemoteRunSerializerOutNode.run serialization={serialization!r} {response=!r} data_size={len(data)}, "
|
|
f"b64encode_size={len(results)}, {max_size_mb=}")
|
|
if max_size_mb and len(results) > max_size_mb * 1024 * 1024:
|
|
raise ValueError(f"Serialized b64 data too large: {len(results)} bytes, max_size_mb={max_size_mb}")
|
|
|
|
results = (results,)
|
|
return {
|
|
"results": results,
|
|
"ui": { "results": results }
|
|
}
|
|
|
|
|
|
class RemoteRunDeserializerOutNode():
|
|
TYPE_NAME = "RAT_RemoteRunDeserializerOut"
|
|
DISPLAY_NAME = "ReRu Deserializer Output (Internal)"
|
|
|
|
CATEGORY = "Remote Run/__Internal__/"
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
**serialization_load_input(),
|
|
"data": ("STRING", { }),
|
|
},
|
|
"optional": {
|
|
"is_changed": ("BOOLEAN", { "default": False }),
|
|
},
|
|
}
|
|
return inputs
|
|
|
|
RETURN_TYPES = tuple([CoIO.ANY] * _NUM_OUTPUTS)
|
|
|
|
def run(self, serialization: str, data: str, **_kwargs):
|
|
obj = deserialize_response(serialization, data)
|
|
return tuple(obj.get(i) for i in range(_NUM_OUTPUTS))
|
|
|
|
|
|
class RemoteRunTogglerNode():
|
|
TYPE_NAME = "RAT_RemoteRunToggler"
|
|
DISPLAY_NAME = "ReRu Toggle"
|
|
|
|
"""
|
|
This is a Input Toggle Switch that can be used to enable/disable certain parts of a graph
|
|
in the exact opposite way between the local and remote side using lazy evaluation or prompt preprocessing.
|
|
|
|
If the switch run_side is set to enabled it will run all inputs, otherwise set them all to off/lazy
|
|
or disconnected before prompt execution, depending on [inputs_when_off] setting.
|
|
When a RemoteRun runner node encounters one of these switches in a prompt it just toggles
|
|
the run_side value so the remote side will do the exact opposite of the local one,
|
|
without even knowing it's the remote side.
|
|
"""
|
|
CATEGORY = "Remote Run"
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
"enabled": (TOGGLE_CHOICES, { }),
|
|
"inputs_when_off": (INPUTS_OFF_OPTIONS, { "default": "lazy" }),
|
|
"when_off": (BLOCK_OPTIONS, { "default": "passthrough" }),
|
|
},
|
|
"optional": {
|
|
**dict((f"input_{num}", (CoIO.ANY, { "lazy": True })) for num in range(_NUM_OUTPUTS)),
|
|
"_ignore_": ("INTERNAL", { "default": "" }),
|
|
},
|
|
# "hidden": { "own_id": "UNIQUE_ID" },
|
|
# "hidden": { "prompt": "PROMPT" },
|
|
"hidden": {
|
|
"own_id": "UNIQUE_ID",
|
|
# "prompt": "PROMPT",
|
|
}
|
|
}
|
|
return inputs
|
|
|
|
RETURN_TYPES = tuple([CoIO.ANY] * _NUM_OUTPUTS)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(self, **_kwargs):
|
|
return None
|
|
|
|
def check_lazy_status(self, enabled: str, own_id = None, **kwargs):
|
|
if enabled not in TOGGLE_CHOICES:
|
|
raise ValueError("Invalid enabled value", own_id, enabled, TOGGLE_CHOICES)
|
|
|
|
if enabled == TOGGLE_CHOICES[0]:
|
|
inputs_with_connections = [i for i in kwargs.keys() if i.startswith("input_")]
|
|
# print("RemoteRunToggler.check_lazy_status", enabled, own_id, inputs_with_connections, "all:", { k: (v is not None) for k, v in kwargs.items() })
|
|
return inputs_with_connections
|
|
|
|
# print("RemoteRunToggler.check_lazy_status", enabled, own_id, [], "all:", { k: (v is not None) for k, v in kwargs.items() })
|
|
return []
|
|
|
|
def run(self, enabled: str, when_off: str, _ignore_ = None, own_id: str = None, **kwargs):
|
|
disconnected_input_ids = _ignore_
|
|
# print(own_id, enabled, _ignore_)
|
|
print(f"{self.__class__.__name__}.run {own_id=} {enabled=} {when_off=} {disconnected_input_ids=}")
|
|
# print("RemoteRunToggler.prompt", prompt)
|
|
if enabled == TOGGLE_CHOICES[1]:
|
|
error_msg = f"{self.DISPLAY_NAME!r} node output is off and set to 'error'"
|
|
return block_option_return(when_off or "passthrough", kwargs, _NUM_OUTPUTS, error_msg, "RemoteRunToggler")
|
|
|
|
return tuple(kwargs.get(f"input_{num}", None) for num in range(_NUM_OUTPUTS))
|
|
|
|
|
|
def _shared_input_types():
|
|
return {
|
|
"remote_url": ("STRING", {
|
|
"default": "http://127.0.0.1:8189/",
|
|
"tooltip": "ComfyUI instance URL",
|
|
}),
|
|
"total_timeout": ("FLOAT", {
|
|
"tooltip": "timeout in seconds",
|
|
"default": 90.0, "min": 0.1, "max": 24 * 3600.0, "step": 0.1
|
|
}),
|
|
"request_timeout": ("FLOAT", {
|
|
"tooltip": "short timeout in seconds",
|
|
"default": 10.0, "min": 0.1, "max": 24 * 3600.0, "step": 0.1
|
|
}),
|
|
**serialization_load_input(),
|
|
# **output_nodes_input(),
|
|
}
|
|
|
|
|
|
def _get_max_size(max_size_mb: int | None):
|
|
if not max_size_mb or max_size_mb <= 0:
|
|
return None, None
|
|
|
|
max_bytes = max_size_mb * 1024 * 1024
|
|
ws = math.ceil(max_bytes * 1.1)
|
|
|
|
return max_bytes, ws
|
|
|
|
|
|
class RemoteRunInputNode():
|
|
TYPE_NAME = "RAT_RemoteRunInput"
|
|
DISPLAY_NAME = "ReRu Input Graph(s)"
|
|
|
|
NUM_OUTPUTS = _NUM_OUTPUTS
|
|
CATEGORY = "Remote Run"
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, is_changed: bool = False, **_kwargs):
|
|
return time.time_ns() if is_changed else None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
**_shared_input_types(),
|
|
},
|
|
"optional": {
|
|
**dict((f"input_{num}", (CoIO.ANY, { "lazy": True })) for num in range(_NUM_OUTPUTS)),
|
|
**shared_input_settings(),
|
|
**response_input(),
|
|
**ws_settings_inputs(),
|
|
"run_outputs_connected_to_inputs": (["ignore", "run"], { "default": "ignore" }),
|
|
"inputs_when_local": (INPUTS_OFF_OPTIONS, { "default": "disconnected" }),
|
|
"is_changed": ("BOOLEAN", { "default": False }),
|
|
"_ignore_": ("INTERNAL", { "default": "" }),
|
|
},
|
|
"hidden": {
|
|
"dynprompt": "DYNPROMPT",
|
|
"own_id": "UNIQUE_ID",
|
|
}
|
|
}
|
|
return inputs
|
|
|
|
RETURN_TYPES = tuple([CoIO.ANY] * _NUM_OUTPUTS)
|
|
|
|
def check_lazy_status(self, **_kwargs):
|
|
return []
|
|
|
|
def run(self,
|
|
remote_url: str, total_timeout: float, request_timeout: float, serialization: str,
|
|
max_size_mb: int | None = None, gzip_compression: bool = False, gzip_level: int = 9, response = None,
|
|
run_outputs_connected_to_inputs = "disconnected", is_changed: bool = False,
|
|
ws_compression: bool = False, forward_progress_messages: str = "off",
|
|
dynprompt: DynamicPrompt = None, own_id = None, _ignore_ = None,
|
|
**_kwargs,
|
|
):
|
|
prompt = { i: dynprompt.get_node(i) for i in dynprompt.all_node_ids() }
|
|
return partial_json_expansion(
|
|
prompt, own_id, remote_url, serialization,
|
|
total_timeout, request_timeout,
|
|
max_size_mb, gzip_compression, gzip_level,
|
|
ws_compression, forward_progress_messages, is_changed, response,
|
|
run_outputs_connected_to_inputs,
|
|
)
|
|
|
|
|
|
class RemoteRunInputOutputNode(RemoteRunInputNode):
|
|
TYPE_NAME = "RAT_RemoteRunInputOutput"
|
|
DISPLAY_NAME = "ReRu Input Graph(s) Output"
|
|
OUTPUT_NODE = True
|
|
|
|
|
|
class RemoteRunJsonNode():
|
|
TYPE_NAME = "RAT_RemoteRunJson"
|
|
DISPLAY_NAME = "ReRu JSON"
|
|
|
|
NUM_OUTPUTS = _NUM_OUTPUTS
|
|
CATEGORY = "Remote Run"
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, is_changed: bool = False, **_kwargs):
|
|
return time.time_ns() if is_changed else None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
**_shared_input_types(),
|
|
"JSON": ("STRING", { }),
|
|
},
|
|
"optional": {
|
|
**dict((f"input_{num}", (CoIO.ANY, { })) for num in range(_NUM_OUTPUTS)),
|
|
"max_size_mb": shared_input_settings()["max_size_mb"],
|
|
**response_input(),
|
|
**ws_settings_inputs(),
|
|
"is_changed": ("BOOLEAN", { "default": False }),
|
|
},
|
|
}
|
|
return inputs
|
|
|
|
RETURN_TYPES = tuple([CoIO.ANY] * _NUM_OUTPUTS)
|
|
|
|
def run(self,
|
|
remote_url: str, total_timeout: float, request_timeout: float, JSON: str, serialization: str,
|
|
max_size_mb: int | None = None, response = None,
|
|
ws_compression: bool = False, forward_progress_messages: str = "off",
|
|
**kwargs,
|
|
):
|
|
max_size_mb = max_size_mb or None
|
|
run_obj = json.loads(JSON)
|
|
extra_output_ids = run_obj.get("extra_output_ids") or None
|
|
run_prompt = run_obj["prompt"]
|
|
data_total = 0
|
|
for node_id, node in run_prompt.items():
|
|
# serialize needed inputs to this node and set up
|
|
# Deserializer data output nodes with the serialized data in the remote prompt
|
|
if node["class_type"] == RemoteRunDeserializerOutNode.TYPE_NAME:
|
|
config = node.pop("deserializer_config")
|
|
outputs = config["outputs"]
|
|
|
|
data = { }
|
|
for output_name, output_data in outputs.items():
|
|
if not isinstance(output_data, list) or not len(output_data) == 2:
|
|
raise ValueError("Invalid output data", output_data, config)
|
|
|
|
if not output_name.startswith("output_") and output_name[7:].isdigit():
|
|
raise ValueError("Invalid output data", output_data, config)
|
|
|
|
output_num = int(output_name[7:])
|
|
|
|
output_data_type, output_data_value = output_data
|
|
if output_data_type == "RunJsonInput":
|
|
if not output_data_value.startswith("input_") and output_data_value[6:].isdigit():
|
|
raise ValueError("Invalid output data", output_data, config)
|
|
|
|
local_input_name = output_data_value
|
|
data[output_num] = kwargs[local_input_name]
|
|
elif output_data_type == "CONSTANT":
|
|
data[output_num] = output_data_value
|
|
else:
|
|
raise ValueError("Invalid output data", output_data, config)
|
|
|
|
data = serialize_obj(serialization, data)
|
|
data = base64.b64encode(data).decode()
|
|
|
|
data_total += len(data)
|
|
if max_size_mb and data_total > max_size_mb * 1024 * 1024:
|
|
raise ValueError(f"Serialized data too large: {data_total} bytes, max_size_mb={max_size_mb}")
|
|
|
|
node["inputs"] = {
|
|
"serialization": serialization,
|
|
"data": data,
|
|
}
|
|
|
|
serializers = { k: v for k, v in run_prompt.items() if v["class_type"] == RemoteRunSerializerOutNode.TYPE_NAME }
|
|
if len(serializers) != 1:
|
|
raise ValueError(f"expected exactly one {RemoteRunSerializerOutNode.TYPE_NAME!r} serializer node, got {len(serializers)}",
|
|
serializers, run_prompt)
|
|
serialized_id = list(serializers.keys())[0]
|
|
|
|
post_max, ws_max = _get_max_size(max_size_mb)
|
|
binary_response = response == "binary"
|
|
raw_result = remote_execute_prompt(
|
|
remote_url, run_prompt, serialized_id, extra_output_ids = extra_output_ids,
|
|
ws_compression = ws_compression, forward_progress_messages = forward_progress_messages,
|
|
binary_response = binary_response,
|
|
ws_max_size = ws_max, post_max_size = post_max,
|
|
total_timeout = total_timeout, short_timeout = request_timeout,
|
|
)
|
|
obj = deserialize_response(serialization, raw_result)
|
|
|
|
result = tuple(obj.get(f"input_{num}", None) for num in range(_NUM_OUTPUTS))
|
|
return result
|
|
|
|
|
|
def block_option_return(option: str, kwargs, num_outputs, error_msg, block_msg):
|
|
option = option.lower()
|
|
if option == "passthrough":
|
|
return tuple(kwargs.get(f"input_{num}", None) for num in range(num_outputs))
|
|
elif option == "block_execution_silent":
|
|
return tuple(execution.ExecutionBlocker(None) for _ in range(num_outputs))
|
|
elif option == "block_execution_verbose":
|
|
return tuple(execution.ExecutionBlocker(f"{block_msg} out={num}") for num in range(num_outputs))
|
|
elif option == "error":
|
|
raise ValueError(error_msg)
|
|
elif option == "return_none":
|
|
return tuple(None for _ in range(num_outputs))
|
|
else:
|
|
raise ValueError(f"Invalid option value: {option!r}")
|
|
|
|
|
|
class RemoteRunStartNode():
|
|
TYPE_NAME = "RAT_RemoteRunStart"
|
|
DISPLAY_NAME = "ReRu Start From Here ->"
|
|
|
|
CATEGORY = "Remote Run"
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"optional": {
|
|
**dict((f"input_{num}", (CoIO.ANY, { })) for num in range(_NUM_OUTPUTS)),
|
|
"remote_run_dependent_outputs": (["ignore", "run"], { "default": "ignore" }),
|
|
"inputs_when_local": (INPUTS_OFF_OPTIONS, { "default": "disconnected" }),
|
|
"outputs_when_local": (BLOCK_OPTIONS, { "default": "error" }),
|
|
},
|
|
}
|
|
return inputs
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, outputs_when_local: str = None, remote_run_dependent_outputs: str = None, *_args, **_kwargs):
|
|
outputs_when_local = outputs_when_local or "error"
|
|
if outputs_when_local.lower() not in BLOCK_OPTIONS:
|
|
return f"Invalid outputs_when_local value: {outputs_when_local!r}"
|
|
|
|
remote_run_dependent_outputs = remote_run_dependent_outputs or "ignore"
|
|
if remote_run_dependent_outputs.lower() not in ("ignore", "run"):
|
|
return f"Invalid remote_run_dependent_outputs value: {remote_run_dependent_outputs!r}"
|
|
|
|
return True
|
|
|
|
RETURN_TYPES = tuple([CoIO.ANY] * _NUM_OUTPUTS)
|
|
|
|
def run(self, outputs_when_local: str = None, **kwargs):
|
|
error_msg = f"{self.DISPLAY_NAME!r} node output used locally, but outputs_when_local is set to 'error'"
|
|
return block_option_return(outputs_when_local or "error", kwargs, _NUM_OUTPUTS, error_msg = error_msg, block_msg = "RemoteRunStartNode")
|
|
|
|
|
|
def _max_id(prompt):
|
|
def toint(i):
|
|
try:
|
|
return int(i)
|
|
except ValueError:
|
|
return 0
|
|
|
|
if not len(prompt):
|
|
return 0
|
|
|
|
return max(toint(i) for i in prompt.keys())
|
|
|
|
|
|
def make_counter(start: int, map = None):
|
|
def get():
|
|
nonlocal start
|
|
ret = start
|
|
start += 1
|
|
return map(ret) if map is not None else ret
|
|
|
|
return get
|
|
|
|
|
|
def get_input_graph_nodes(prompt: dict, root_node_id: str) -> tuple[set[str], set[str]]:
|
|
start_nodes = set()
|
|
input_nodes = set()
|
|
|
|
def _nodes(node_id: str, seen):
|
|
if node_id in seen:
|
|
return
|
|
seen.add(node_id)
|
|
|
|
node = prompt[node_id]
|
|
if node.get("class_type") == RemoteRunStartNode.TYPE_NAME:
|
|
start_nodes.add(node_id)
|
|
return
|
|
|
|
inputs = node["inputs"]
|
|
for inp in inputs.values():
|
|
if isinstance(inp, list):
|
|
input_nodes.add(inp[0])
|
|
_nodes(inp[0], seen)
|
|
|
|
_nodes(root_node_id, set())
|
|
|
|
return start_nodes, input_nodes
|
|
|
|
|
|
def get_full_dependents_of_outputs(prompt: dict, of_main_node_ids: set[str], stop_at_node_ids = None) -> dict[str, set[str]]:
|
|
# For each main_node_id get all nodes that are directly or indirectly linked to its outputs
|
|
# but stop going down the graph when hitting another stop_at_node_ids (or main_node_id if stop_at_node_ids not given).
|
|
of_main_node_ids = set(of_main_node_ids)
|
|
stop_at_node_ids = set(stop_at_node_ids if stop_at_node_ids is not None else of_main_node_ids)
|
|
|
|
inputs_map = { }
|
|
for nid, node in prompt.items():
|
|
inputs = set()
|
|
for v in node["inputs"].values():
|
|
if isinstance(v, list) and len(v) == 2:
|
|
inputs.add(v[0])
|
|
inputs_map[nid] = inputs
|
|
|
|
@functools.cache
|
|
def get_all_parents(nid):
|
|
if nid in stop_at_node_ids:
|
|
return set()
|
|
|
|
parents = set()
|
|
for inp in inputs_map.get(nid) or []:
|
|
parents.add(inp)
|
|
parents.update(get_all_parents(inp))
|
|
return parents
|
|
|
|
res = { i: set() for i in of_main_node_ids }
|
|
for nid in prompt.keys():
|
|
all_parents_until_parent_node_id_or_end = get_all_parents(nid)
|
|
important_parents = all_parents_until_parent_node_id_or_end & of_main_node_ids
|
|
for p in important_parents:
|
|
res[p].add(nid)
|
|
|
|
return res
|
|
|
|
|
|
def get_extra_output_ids_dependent_on(prompt: dict, node_ids: set[str], stop_at_node_ids = None):
|
|
# get all nodes that are directly or indirectly linked to the outputs of the given node_ids
|
|
# (generally all RemoteRunStartNodes), stop going down the chain when hitting another remote_run
|
|
# start or input/json node.
|
|
|
|
node_ids = set(node_ids)
|
|
stop_at_node_ids = set(stop_at_node_ids if stop_at_node_ids is not None else node_ids)
|
|
deps = get_full_dependents_of_outputs(prompt, node_ids, stop_at_node_ids)
|
|
|
|
extra_output_ids = set()
|
|
for parent_id, all_deps in deps.items():
|
|
if not all_deps or parent_id not in node_ids:
|
|
continue
|
|
extra_output_ids.update(all_deps)
|
|
|
|
return sorted(extra_output_ids)
|
|
|
|
|
|
def partial_json_expansion(
|
|
prompt: dict, root_node_id: str, remote_url: str, serialization: str,
|
|
total_timeout: float, request_timeout: float,
|
|
max_size_mb: int | None, gzip_compression: bool, gzip_level: int,
|
|
ws_compression: bool, forward_progress_messages: str, is_changed: bool, response: str | None,
|
|
run_outputs_connected_to_inputs: str,
|
|
):
|
|
"""
|
|
When there are RemoteStart nodes then go from:
|
|
A -> B -> C -> RemoteStart -> D -> E -> F -> RemoteRunInput -> G -> H -> I
|
|
to
|
|
A -> B -> C -> RemoteRunJson(run_remotely="D -> E -> F") -> G -> H -> I
|
|
|
|
Build an expanded graph to replace the RemoteRunInput node with a RemoteRunJson node copying
|
|
the whole input graph going to the RemoteRunInput node up until RemoteStart nodes are hit,
|
|
and replace all those nodes with a RemoteRunJson node that will run that part as prompt remotely instead.
|
|
The RemoteRunJson gets linked to all the inputs of the RemoteStart nodes if any so it can serialize
|
|
them and send them to the remote instance as part of the remote prompt.
|
|
Optionally if remote_run_dependent_outputs==run then also include all other nodes that are connected to
|
|
the outputs of any input graph nodes but don't lead to the RemoteRunInput node and run outputs in those too,
|
|
see below for example, this can be set either for everything if the RemoteRunInput node has it set
|
|
or per RemoteRunStart node for anything going off a node in between that and the RemoteRunInput node.
|
|
|
|
If there are no RemoteStart nodes then take all the nodes going into RemoteRunInput.
|
|
Optionally also all nodes that go off those inputs nodes too if run_outputs_connected_to_inputs is set to "run",
|
|
so if there are any outputs as part of the input graph but that doesn't lead to RemoteRunInput those get run too.
|
|
So from:
|
|
A -> B -> C -> D -> RemoteRunInput -> G -> H -> I
|
|
A -> SaveImage[F]
|
|
to:
|
|
RemoteRunJson(run_remotely="A -> B -> C -> D") -> G -> H -> I
|
|
or with run_outputs_connected_to_inputs = "run" to:
|
|
RemoteRunJson(run_remotely="A -> SaveImage[F]; A -> B -> C -> D", run_extra_outputs=["F"]) -> G -> H -> I
|
|
|
|
"""
|
|
remote_prompt = copy.deepcopy(prompt)
|
|
next_node_id = make_counter(_max_id(remote_prompt) + 1, str)
|
|
rcon, dscon = update_toggles_inplace(remote_prompt, True, True, "remote_run_input_toggle", False)
|
|
|
|
add_outputs_of_inputs = (run_outputs_connected_to_inputs or "").lower() == "run"
|
|
|
|
if rcon or dscon:
|
|
logger.info("%s", f"partial_json_expansion {root_node_id=} toggled {rcon + dscon} nodes, "
|
|
f"reconnected {rcon}, disconnected {dscon}")
|
|
|
|
extra_output_ids = None
|
|
local_expanded_inputs = { }
|
|
remote_run_dependent_outputs_of_nodes = []
|
|
start_node_ids, other_node_ids = get_input_graph_nodes(remote_prompt, root_node_id)
|
|
|
|
# The D -> E -> F part(s), anything before the RemoteRunInput node back up to any RemoteRunStart nodes
|
|
# and anything starting on its own.
|
|
small_prompt = { i: remote_prompt[i] for i in (start_node_ids | other_node_ids | { root_node_id }) }
|
|
if start_node_ids:
|
|
# For the remote prompt: just replace each RemoteStart node with a Deserializer node that outputs
|
|
# the serialized and sent over inputs.
|
|
# On the local side as there can be multiple RemoteStart nodes all the inputs to the RemoteStart nodes have to also
|
|
# be linked up to the newly created/expanded RemoteRunJson node and kept track of which serialized inputs goes to which
|
|
# Deserializer node output on the remote side that replace the RemoteStart nodes in the remote prompts.
|
|
|
|
next_json_input_num = make_counter(0)
|
|
|
|
# On remote prompt change all RemoteRunStart nodes to Deserializer nodes
|
|
for start_id in start_node_ids:
|
|
# TODO: ignore inputs for outputs that aren't used, currently serialized/sent for nothing
|
|
|
|
start_node = small_prompt[start_id]
|
|
start_inputs = start_node["inputs"]
|
|
node_remote_run_dependent_outputs = start_inputs.get("remote_run_dependent_outputs")
|
|
if (node_remote_run_dependent_outputs or "").lower() == "run":
|
|
remote_run_dependent_outputs_of_nodes.append(start_id)
|
|
|
|
des = { }
|
|
for input_name, input_value in start_inputs.items():
|
|
if not input_name.startswith("input_"):
|
|
continue
|
|
|
|
output_name = "output_" + input_name[6:]
|
|
if isinstance(input_value, list):
|
|
local_input_name = f"input_{next_json_input_num()}"
|
|
local_expanded_inputs[local_input_name] = input_value
|
|
des[output_name] = ("RunJsonInput", local_input_name)
|
|
else:
|
|
des[output_name] = ("CONSTANT", input_value)
|
|
|
|
# The deserializer_config part will be removed by the RemoteRunJson node and rewritten
|
|
# to use the serialized inputs as given.
|
|
small_prompt[start_id] = {
|
|
"class_type": RemoteRunDeserializerOutNode.TYPE_NAME,
|
|
"deserializer_config": {
|
|
"outputs": des,
|
|
},
|
|
}
|
|
|
|
if add_outputs_of_inputs:
|
|
input_node_ids = start_node_ids | other_node_ids
|
|
stop_at = start_node_ids | { root_node_id }
|
|
extra_output_ids = get_extra_output_ids_dependent_on(remote_prompt, input_node_ids, stop_at_node_ids = stop_at)
|
|
logger.info("partial_json_expansion %s: add_outputs_of_inputs -> extra_output_ids=%s",
|
|
root_node_id, (len(extra_output_ids), extra_output_ids))
|
|
elif remote_run_dependent_outputs_of_nodes:
|
|
# Add all output nodes that go off away from the input graph, so between a start or StartNode to RemoteRunInput node,
|
|
# but don't themselves lead to the RemoteRunInput node.
|
|
extra_output_ids = get_extra_output_ids_dependent_on(remote_prompt, set(remote_run_dependent_outputs_of_nodes), stop_at_node_ids = { root_node_id })
|
|
logger.info("partial_json_expansion %s: remote_run_dependent_outputs_of_nodes=%s -> extra_output_ids=%s",
|
|
root_node_id, remote_run_dependent_outputs_of_nodes, (len(extra_output_ids), extra_output_ids))
|
|
|
|
# This is really all dependent nodes not just Output ones but no need to limit to only output ones since any normal non output
|
|
# nodes given will just be ignored by comfy prompt handling (currently).
|
|
extra_output_ids = list(extra_output_ids) if extra_output_ids else None
|
|
for i in extra_output_ids or []:
|
|
if i not in small_prompt:
|
|
small_prompt[i] = remote_prompt[i]
|
|
|
|
remote_prompt = small_prompt
|
|
root_node = remote_prompt.pop(root_node_id)
|
|
root_inputs = { k: v for k, v in root_node["inputs"].items() if isinstance(v, list) }
|
|
remote_prompt[root_node_id] = {
|
|
"class_type": RemoteRunSerializerOutNode.TYPE_NAME,
|
|
"inputs": {
|
|
"serialization": serialization,
|
|
"max_size_mb": max_size_mb,
|
|
"is_changed": is_changed,
|
|
**root_inputs,
|
|
},
|
|
}
|
|
|
|
remote_obj = dict(
|
|
prompt = remote_prompt,
|
|
extra_output_ids = extra_output_ids,
|
|
)
|
|
json_node_id_str = next_node_id()
|
|
new_graph = {
|
|
json_node_id_str: {
|
|
"class_type": RemoteRunJsonNode.TYPE_NAME,
|
|
"inputs": dict(
|
|
JSON = json.dumps(remote_obj),
|
|
remote_url = remote_url,
|
|
total_timeout = total_timeout,
|
|
request_timeout = request_timeout,
|
|
serialization = serialization,
|
|
max_size_mb = max_size_mb,
|
|
gzip_compression = gzip_compression,
|
|
gzip_level = gzip_level,
|
|
ws_compression = ws_compression,
|
|
forward_progress_messages = forward_progress_messages,
|
|
is_changed = is_changed,
|
|
response = response,
|
|
**local_expanded_inputs,
|
|
),
|
|
}
|
|
}
|
|
# print("new_graph", json.dumps(new_graph, indent = 4))
|
|
|
|
replaced_output_ids = tuple([json_node_id_str, i] for i in range(_NUM_OUTPUTS))
|
|
return {
|
|
"result": replaced_output_ids,
|
|
"expand": new_graph,
|
|
}
|
|
|
|
|
|
def serialize_obj(serialization: str, obj):
|
|
if serialization in ("torch_pt", "unsafe_torch_pt", "safe_torch_pt"):
|
|
buf = BytesIO()
|
|
torch.save(obj, f = buf)
|
|
buf = buf.getvalue()
|
|
elif serialization == "fancy_safetensors":
|
|
buf = fst_torch.save_bytes(obj, transform_tensor_fn = lambda t: t.detach().clone().contiguous())
|
|
else:
|
|
raise ValueError("Unknown serialization method", serialization)
|
|
return buf
|
|
|
|
|
|
def deserialize_response(serialization: str, result: dict | str | bytes):
|
|
if isinstance(result, dict):
|
|
result_str = result["results"][0]
|
|
data = base64.b64decode(result_str)
|
|
elif isinstance(result, str):
|
|
data = base64.b64decode(result)
|
|
else:
|
|
data = result
|
|
return deserialize_data(serialization, data)
|
|
|
|
|
|
def deserialize_data(serialization: str, data: bytes):
|
|
if serialization == "unsafe_torch_pt":
|
|
return torch.load(BytesIO(data))
|
|
elif serialization == "safe_torch_pt":
|
|
return torch_safe_load_dict(ZipFile(BytesIO(data), mode = "r"))
|
|
elif serialization == "fancy_safetensors":
|
|
return fst_torch.load(data)
|
|
else:
|
|
raise ValueError("Unknown serialization method", serialization)
|
|
|
|
|
|
def output_nodes_transform(prompt: dict, output_nodes: str, keep_nodes: set[str] = None) -> dict:
|
|
if output_nodes == "Remove":
|
|
output_ids = set(prompt_outputs(prompt)) - (keep_nodes or set())
|
|
return { k: v for k, v in prompt.items() if k not in output_ids }
|
|
|
|
return prompt
|
|
|
|
|
|
def update_toggles_inplace(prompt: dict, toggle: bool, reconnect: bool, label: str, raise_on_existing: bool, input_node_is_local: bool = True):
|
|
to_switch = {
|
|
RemoteRunTogglerNode.TYPE_NAME: "inputs_when_off",
|
|
RemoteRunInputNode.TYPE_NAME: "inputs_when_local",
|
|
}
|
|
reconnected_cnt = disconnected_cnt = 0
|
|
for node_id, node in prompt.items():
|
|
class_type = node["class_type"]
|
|
input_name = to_switch.get(class_type)
|
|
if not input_name:
|
|
continue
|
|
|
|
inputs = node.get("inputs")
|
|
if not inputs:
|
|
continue
|
|
|
|
if class_type == RemoteRunTogglerNode.TYPE_NAME:
|
|
enabled = inputs.get("enabled")
|
|
if enabled not in TOGGLE_CHOICES:
|
|
raise ValueError("Invalid enabled value", node_id, enabled, TOGGLE_CHOICES)
|
|
if toggle:
|
|
enabled = inputs["enabled"] = TOGGLE_CHOICES[1] if enabled == TOGGLE_CHOICES[0] else TOGGLE_CHOICES[0]
|
|
|
|
enabled = enabled == "only_locally"
|
|
else:
|
|
enabled = input_node_is_local
|
|
|
|
if (
|
|
reconnect
|
|
and enabled
|
|
and inputs.get(input_name) == "disconnected"
|
|
and (disconnected_inputs := inputs.get("_ignore_"))
|
|
):
|
|
# restore original disconnected inputs for remote side
|
|
try:
|
|
disconnected_inputs = json.loads(disconnected_inputs)
|
|
except Exception as e:
|
|
logger.error("%s RemoteRunToggler.run invalid disconnected_inputs %r: %s", label, disconnected_inputs, e)
|
|
raise ValueError("Invalid disconnected_inputs", node_id, disconnected_inputs, e)
|
|
|
|
if disconnected_inputs:
|
|
for inp_name, inp_val in disconnected_inputs.items():
|
|
if inp_name in inputs:
|
|
raise ValueError("RemoteRunToggler.run disconnected_inputs input already set", node_id, inp_name, inp_val)
|
|
inputs[inp_name] = inp_val
|
|
inputs.pop("_ignore_")
|
|
logger.debug("%s reconnected %s RemoteRunToggler inputs on node %r: %s",
|
|
label, len(disconnected_inputs), node_id, disconnected_inputs)
|
|
reconnected_cnt += 1
|
|
|
|
elif not enabled and inputs.get(input_name) == "disconnected":
|
|
given_inputs = { k: v for k, v in inputs.items() if k.startswith("input_") }
|
|
for k in given_inputs.keys():
|
|
inputs.pop(k)
|
|
|
|
if not given_inputs:
|
|
continue
|
|
|
|
cur = inputs.get("_ignore_")
|
|
if cur:
|
|
if raise_on_existing:
|
|
raise ValueError("remote_run update_toggles_inplace overwriting existing _ignore_", node_id, cur, given_inputs)
|
|
else:
|
|
logger.warning("%s overwriting existing _ignore_ on node_id=%r, cur=%r, new=%r",
|
|
label, node_id, cur, given_inputs)
|
|
|
|
inputs["_ignore_"] = json.dumps(given_inputs)
|
|
logger.debug("%s disconnected inputs for node_id=%r, inputs=%s",
|
|
label, node_id, given_inputs)
|
|
disconnected_cnt += 1
|
|
|
|
return reconnected_cnt, disconnected_cnt
|
|
|
|
|
|
def prompt_outputs(prompt: dict) -> list[str]:
|
|
valid = execution.validate_prompt(prompt)
|
|
if not valid[0]:
|
|
if isinstance(valid[1], dict) and valid[1].get("type") == "prompt_no_outputs":
|
|
return []
|
|
raise Exception(f"Prompt validation failed, couldn't get output node ids: {valid}, {prompt}")
|
|
outputs = valid[2]
|
|
return outputs
|
|
|
|
|
|
def retry_dec(total_tries: int = 1, log_exc = True, sleep = None, stop_on = None):
|
|
if isinstance(stop_on, type):
|
|
stop_on = (stop_on,)
|
|
stop_on = stop_on or []
|
|
|
|
def make_dec(func):
|
|
def wrapper(*args, **kwargs):
|
|
for i in range(total_tries):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
if log_exc:
|
|
logger.exception("function error: fn:%r, retrying %d", func, i)
|
|
|
|
if i == total_tries - 1 or (stop_on and type(e) in stop_on):
|
|
raise e
|
|
|
|
if sleep:
|
|
time.sleep(sleep)
|
|
|
|
return wrapper
|
|
|
|
return make_dec
|
|
|
|
|
|
def remote_execute_prompt(
|
|
base_url: str, prompt: dict,
|
|
own_id: str, extra_output_ids: list[str] | None,
|
|
ws_compression: bool, forward_progress_messages: str,
|
|
binary_response: bool, ws_max_size: int | None, post_max_size: int | None,
|
|
total_timeout: float, short_timeout: float,
|
|
total_tries = 3,
|
|
):
|
|
ws_max_size = ws_max_size or 64 * 1024 * 1024
|
|
|
|
total_timeout = total_timeout or 10 # expected to always be set, so default to low ones in case of unset
|
|
short_timeout = short_timeout or 5
|
|
|
|
base_url = base_url.rstrip("/")
|
|
base_ws_url = base_url.replace("http://", "ws://")
|
|
|
|
client_id = str(uuid4())
|
|
post_nonce = str(uuid4())
|
|
|
|
def _make_adjusted_timeout(total_timeout: float):
|
|
# ensure current timeout wouldn't exceed total_timeout
|
|
start_ts = time.monotonic()
|
|
|
|
def get(wanted_timeout: float, raise_on_timeout = False):
|
|
left_secs = total_timeout - (time.monotonic() - start_ts)
|
|
if raise_on_timeout and left_secs <= 0:
|
|
raise TimeoutError("total timeout reached", total_timeout)
|
|
return min(left_secs, wanted_timeout)
|
|
|
|
return get
|
|
|
|
adjusted_timeout_fn = _make_adjusted_timeout(total_timeout)
|
|
|
|
@retry_dec(total_tries, stop_on = TimeoutError)
|
|
def post(timeout):
|
|
post_url = base_url + "/api/prompt"
|
|
apicall = dict(
|
|
client_id = client_id,
|
|
prompt = prompt,
|
|
nonce = post_nonce, # for safe retry, assumes nonce duplicate node/extension installed
|
|
partial_execution_targets = list(extra_output_ids or []) + [own_id],
|
|
)
|
|
|
|
if os.environ.get("DEV") == "1":
|
|
from pathlib import Path
|
|
Path(f"./dev/jsons/post_prompt__{time.time_ns()}.json").write_text(json.dumps(prompt))
|
|
|
|
body = json.dumps(apicall)
|
|
if isinstance(body, str):
|
|
body = body.encode()
|
|
if post_max_size and len(body) > post_max_size:
|
|
raise ValueError(f"Post body too large: {len(body)} bytes, post_max_size={post_max_size}")
|
|
|
|
logger.debug("Posting prompt to url=%r, size=%d, timeout=%.1f", post_url, len(body), timeout)
|
|
|
|
headers = { "Content-Type": "application/json" }
|
|
resp = requests.post(post_url, data = body, headers = headers, timeout = adjusted_timeout_fn(timeout, True))
|
|
if resp.status_code != 200:
|
|
raise ValueError("Failed to post prompt", post_url, resp.status_code, resp.content, apicall)
|
|
|
|
obj = resp.json()
|
|
return obj["prompt_id"]
|
|
|
|
ws_url = base_ws_url + f"/ws?clientId={client_id}"
|
|
|
|
websocket = None
|
|
# create websocket connection first, then send prompt POST and wait for result/error/timeout on ws
|
|
try:
|
|
@retry_dec(total_tries, stop_on = TimeoutError)
|
|
def connect():
|
|
return websockets.sync.client.connect(
|
|
ws_url,
|
|
open_timeout = adjusted_timeout_fn(short_timeout), close_timeout = 5,
|
|
max_size = ws_max_size,
|
|
compression = "deflate" if ws_compression else None,
|
|
)
|
|
|
|
logger.debug("Connecting to websocket url=%r", ws_url)
|
|
websocket = connect()
|
|
prompt_id = post(short_timeout)
|
|
|
|
forward_ws_messages = forward_progress_messages == "on"
|
|
forwarded_types = { "executing", "progress_state" }
|
|
|
|
while True:
|
|
message = websocket.recv(timeout = adjusted_timeout_fn(total_timeout, True))
|
|
if isinstance(message, bytes):
|
|
if not binary_response:
|
|
logger.warning("Ignoring binary websocket message, expected text: size=%d", len(message))
|
|
else:
|
|
if len(message) < 4:
|
|
logger.warning("Ignoring too short binary websocket message, expected longer: size=%d", len(message))
|
|
continue
|
|
message_type = int.from_bytes(message[:4], "big")
|
|
if message_type == BinaryResponseMessageID.BINARY_RESPONSE_MESSAGE_ID.value:
|
|
logger.info("Got binary response websocket message: size=%d", len(message))
|
|
rest = message[4:]
|
|
return rest
|
|
elif message_type == BinaryResponseMessageID.GZIPPED_BINARY_RESPONSE_MESSAGE_ID.value:
|
|
logger.info("Got gzipped binary response websocket message: size=%d", len(message))
|
|
comp = message[4:]
|
|
gzrest = gzip.decompress(comp)
|
|
return gzrest
|
|
else:
|
|
logger.warning("Ignoring unknown binary websocket message type: type=%d, size=%d", message_type, len(message))
|
|
continue
|
|
continue
|
|
|
|
message_len = len(message or "")
|
|
message_obj = json.loads(message)
|
|
|
|
own_prompt_id = message_obj.get("data", { }).get("prompt_id") == prompt_id
|
|
if own_prompt_id:
|
|
if (
|
|
forward_ws_messages
|
|
and (msg_type := message_obj.get("type")) in forwarded_types
|
|
and (data := message_obj.get("data"))
|
|
):
|
|
node = data.get("node")
|
|
ignore = msg_type == "executing" and node == own_id
|
|
if not ignore:
|
|
try:
|
|
import server
|
|
instance = server.PromptServer.instance
|
|
client_id = instance.client_id
|
|
logger.debug("Forwarding remote prompt server message type=%r, node=%r, client_id=%r, size=%d",
|
|
msg_type, node, client_id, message_len)
|
|
instance.send_sync(msg_type, data, client_id)
|
|
except Exception as e:
|
|
logger.exception("Failed to forward remote prompt server message type=%r, node=%r, client_id=%r",
|
|
msg_type, node, client_id)
|
|
|
|
if message_obj.get("type") == "executed":
|
|
logger.debug("got executed message for own prompt_id=%r, size=%d", prompt_id, message_len)
|
|
data = message_obj["data"]
|
|
if data["node"] == own_id:
|
|
result = data["output"]
|
|
return result
|
|
elif own_prompt_id and message_obj.get("type") == "execution_error":
|
|
raise ValueError("remote comfy prompt execution error", message_obj, base_url, prompt)
|
|
except TimeoutError:
|
|
# TODO: interrupt execution of that prompt in case of timeout?? always or option
|
|
# also not sure if specific prompt interrupt is easily possible
|
|
raise
|
|
|
|
finally:
|
|
if websocket is not None:
|
|
threading.Thread(target = websocket.close, daemon = True).start()
|
|
|
|
|
|
class RemoteRunSetNumOutputsNode():
|
|
DISPLAY_NAME = "ReRu Set Num Outputs"
|
|
|
|
CATEGORY = "Remote Run"
|
|
FUNCTION = "run"
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"num": ("INT", { "default": 10, "min": 1, "max": 100, "step": 1 }),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
|
|
def run(self, num: int):
|
|
global _NUM_OUTPUTS
|
|
logger.info("Changing RemoteRun number of inputs/outputs from %d to %d", _NUM_OUTPUTS, num)
|
|
_NUM_OUTPUTS = num
|
|
return { }
|
|
|
|
|
|
def add_server_prompt_nonce_handler():
|
|
logger.info("Adding promptserver remote_run nonce no duplicate prompt handler")
|
|
import server
|
|
|
|
nonce_ordered = collections.deque()
|
|
nonce_set = set()
|
|
NONCE_MAX = 1000
|
|
|
|
def prompt_handler(original_request: dict):
|
|
nonce = original_request.get("nonce")
|
|
if nonce is None:
|
|
return original_request
|
|
|
|
if nonce in nonce_set:
|
|
logger.info("Duplicate nonce found, ignoring prompt: %r", nonce)
|
|
return { }
|
|
|
|
nonce_set.add(nonce)
|
|
while len(nonce_set) > NONCE_MAX:
|
|
oldest = nonce_ordered.popleft()
|
|
nonce_set.remove(oldest)
|
|
return original_request
|
|
|
|
if os.environ.get("HOTRELOAD"):
|
|
to_del = [i for i in server.PromptServer.instance.on_prompt_handlers if getattr(i, "rr_nonce_nodup_clearable", False)]
|
|
for i in to_del:
|
|
print("hotreload clearing old remote run nonce prompt handler", i)
|
|
server.PromptServer.instance.on_prompt_handlers.remove(i)
|
|
prompt_handler.rr_nonce_nodup_clearable = True
|
|
|
|
server.PromptServer.instance.add_on_prompt_handler(prompt_handler)
|
|
|
|
|
|
def add_server_prompt_input_disconnect_handler():
|
|
logger.info("Adding promptserver remote_run input disconnect handler")
|
|
import server
|
|
|
|
def prompt_handler(original_request: dict):
|
|
if not isinstance(original_request.get("prompt"), dict):
|
|
return original_request
|
|
|
|
original_request = copy.deepcopy(original_request)
|
|
prompt = original_request["prompt"]
|
|
rcon, dscon = update_toggles_inplace(prompt, False, False, "input_disconnect_handler", raise_on_existing = True)
|
|
if rcon or dscon:
|
|
logger.info("remote_run input disconnect handler, reconnected %d, disconnected %d nodes", rcon, dscon)
|
|
return original_request
|
|
|
|
if os.environ.get("HOTRELOAD"):
|
|
to_del = [i for i in server.PromptServer.instance.on_prompt_handlers if getattr(i, "rr_input_discon_clearable", False)]
|
|
for i in to_del:
|
|
print("hotreload clearing old remote run input disconnect prompt handler", i)
|
|
server.PromptServer.instance.on_prompt_handlers.remove(i)
|
|
prompt_handler.rr_input_discon_clearable = True
|
|
server.PromptServer.instance.on_prompt_handlers.insert(0, prompt_handler)
|
|
else:
|
|
server.PromptServer.instance.add_on_prompt_handler(prompt_handler)
|
|
|
|
|
|
if os.environ.get("ADD_REMOTE_RUN_NONCE_HANDLER") == "1":
|
|
logger.info("ADD_REMOTE_RUN_NONCE_HANDLER=1, adding remote_run no duplicate nonce handler!")
|
|
try:
|
|
add_server_prompt_nonce_handler()
|
|
except Exception as e:
|
|
logger.exception("Failed to add remote_run no duplicate nonce handler")
|
|
else:
|
|
logger.info("skipping remote run_no duplicate nonce handler, ADD_REMOTE_RUN_NONCE_HANDLER=1 to enable.")
|
|
|
|
if os.environ.get("SKIP_REMOTE_RUN_INPUT_DISCONNECT_HANDLER") != "1":
|
|
logger.info("SKIP_REMOTE_RUN_INPUT_DISCONNECT_HANDLER=1 not given, adding remote_run input disconnect handler!")
|
|
try:
|
|
add_server_prompt_input_disconnect_handler()
|
|
except Exception as e:
|
|
logger.exception("Failed to add remote_run input disconnect handler")
|
|
else:
|
|
logger.info("skipping remote_run input disconnect handler due to env var.")
|
|
|
|
|
|
def node_mappings(classes):
|
|
make_type_name = lambda cn: f"RAT_{cn.removesuffix('Node')}"
|
|
|
|
class_map, name_map = { }, { }
|
|
for i in classes:
|
|
type_name = getattr(i, "TYPE_NAME", make_type_name(i.__name__))
|
|
class_map[type_name] = i
|
|
name_map[type_name] = getattr(i, "DISPLAY_NAME", type_name)
|
|
|
|
return class_map, name_map
|
|
|
|
|
|
REMOTE_RUN_RUN_NODES = [
|
|
RemoteRunInputNode,
|
|
RemoteRunInputOutputNode,
|
|
RemoteRunJsonNode,
|
|
]
|
|
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = node_mappings(
|
|
(
|
|
RemoteRunStartNode,
|
|
*REMOTE_RUN_RUN_NODES,
|
|
RemoteRunTogglerNode,
|
|
RemoteRunSerializerOutNode, RemoteRunDeserializerOutNode,
|
|
RemoteRunSetNumOutputsNode,
|
|
)
|
|
)
|