Files
Duanyll-comfyui_functional/comfyui_functional/utils.py
T
2025-11-13 04:29:15 +00:00

257 lines
8.8 KiB
Python

from typing import TypedDict
import copy
import json
import io
import base64
import numpy as np
import torch
from PIL import Image
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
class ContainsDynamicDict(dict):
"""
A custom dictionary that dynamically returns values for keys based on a pattern.
- If a key in the passed dictionary has a value with `{"_dynamic": "number"}` in the tuple's second position,
then any other key starting with the same string and ending with a number will return that value.
- For other keys, normal dictionary lookup behavior applies.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Store prefixes associated with `_dynamic` values for efficient lookup
self._dynamic_prefixes = {
key.rstrip("0123456789"): value
for key, value in self.items()
if isinstance(value, tuple)
and len(value) > 1
and value[1].get("_dynamic") == "number"
}
def __contains__(self, key):
# Check if key matches a dynamically handled prefix or exists normally
return any(
key.startswith(prefix) and key[len(prefix) :].isdigit()
for prefix in self._dynamic_prefixes
) or super().__contains__(key)
def __getitem__(self, key):
# Dynamically return the value for keys matching a `prefix<number>` pattern
for prefix, value in self._dynamic_prefixes.items():
if key.startswith(prefix) and key[len(prefix) :].isdigit():
return value
# Fallback to normal dictionary behavior for other keys
return super().__getitem__(key)
class Closure(TypedDict):
body: dict
captures: list
output: list
def create_graph_from_closure(closure: Closure, params, caller_unique_id=None):
body = copy.deepcopy(closure["body"])
graph = {}
def transform_input(spec):
if not isinstance(spec, list):
return spec
# Return normal spec if not a capture or param
if spec[0] != "__capture" and spec[0] != "__param":
return spec
if spec[0] == "__param":
if spec[1] >= len(params):
raise IndexError(
f"Parameter index {spec[1]} out of range for function call. "
f"Only {len(params)} parameters were provided."
)
value = params[spec[1]]
else:
value = closure["captures"][spec[1]]
if isinstance(value, list):
prefix = f"{caller_unique_id}_" if caller_unique_id is not None else ""
recover_id = f"{prefix}{spec[0]}_{spec[1]}"
if recover_id not in graph:
graph[recover_id] = {
"inputs": {
# Wrap the list in a tuple so it's treated as a literal, not a link
"values": (value,),
},
"class_type": "__RecoverList__",
}
return [recover_id, 0]
else:
return value
for node_id, node_data in body.items():
inputs = node_data["inputs"]
for key in inputs.keys():
inputs[key] = transform_input(inputs[key])
node_data["override_display_id"] = node_id
graph[f"{caller_unique_id}_{node_id}"] = node_data
output = transform_input(closure["output"])
return graph, output
def create_remote_workflow_from_closure(closure, params):
body = copy.deepcopy(closure["body"])
graph = {}
def transform_input(spec):
if not isinstance(spec, list):
return spec
# Return normal spec if not a capture or param
if spec[0] != "__capture" and spec[0] != "__param":
return spec
# Check if we have already created a deserialize node for this spec
deserialize_node_id = f"deserialize_{spec[0]}_{spec[1]}"
if deserialize_node_id in graph:
return [deserialize_node_id, 0]
# Try to create a deserialize node
if spec[0] == "__param":
if spec[1] >= len(params):
raise IndexError(
f"Parameter index {spec[1]} out of range for function call. "
f"Only {len(params)} parameters were provided."
)
value = params[spec[1]]
else:
value = closure["captures"][spec[1]]
try:
serialized_value = serialize(value)
graph[deserialize_node_id] = {
"inputs": {
"data": serialized_value,
},
"class_type": "__Deserialize__",
}
return [deserialize_node_id, 0]
except TypeError as e:
if spec[0] == "__capture":
raise ValueError(
f"Cannot serialize captured value at index {spec[1]}. "
"Captured values must be serializable to be used in remote execution. "
"Consider disabling capture for this function to initialize the value remotely. "
f"Error: {e}"
)
else:
raise ValueError(
f"Cannot serialize parameter at index {spec[1]}. "
"Parameters must be serializable to be used in remote execution. "
f"Error: {e}"
)
for node_id, node_data in body.items():
inputs = node_data["inputs"]
for key in inputs.keys():
inputs[key] = transform_input(inputs[key])
graph[node_id] = node_data
output = transform_input(closure["output"])
graph["__output__"] = {
"inputs": {
"data": output,
},
"class_type": "__Serialize__",
"_meta": {
"title": "%__output__%"
}
}
return graph
class WhitelistEncoder(json.JSONEncoder):
def default(self, obj):
# 区分 set 和 tuple
if isinstance(obj, set):
return {'__type__': 'set', 'data': list(obj)}
if isinstance(obj, tuple):
return {'__type__': 'tuple', 'data': list(obj)}
if isinstance(obj, bytes):
return {'__type__': 'bytes', 'data': base64.b64encode(obj).decode('utf-8')}
if isinstance(obj, np.ndarray):
buffer = io.BytesIO()
np.save(buffer, obj, allow_pickle=False)
return {
'__type__': 'numpy.ndarray',
'data': base64.b64encode(buffer.getvalue()).decode('utf-8')
}
if isinstance(obj, torch.Tensor):
buffer = io.BytesIO()
torch.save(obj.cpu(), buffer)
return {
'__type__': 'torch.Tensor',
'data': base64.b64encode(buffer.getvalue()).decode('utf-8')
}
if isinstance(obj, Image.Image):
buffer = io.BytesIO()
obj.save(buffer, format='PNG')
return {
'__type__': 'PIL.Image',
'data': base64.b64encode(buffer.getvalue()).decode('utf-8')
}
raise TypeError(
f"Object of type '{type(obj).__name__}' is not serializable. "
"Only basic types, set, tuple, bytes, numpy.ndarray, "
"torch.Tensor, and PIL.Image are allowed."
)
# 2. 自定义解码器 (Deserialization)
def whitelist_decoder(dct):
"""
用于 json.load/loads 的 object_hook,用于恢复自定义类型。
"""
if '__type__' in dct:
obj_type = dct['__type__']
data = dct['data']
if obj_type == 'set':
return set(data)
if obj_type == 'tuple':
return tuple(data)
if obj_type == 'bytes':
return base64.b64decode(data)
if obj_type == 'numpy.ndarray':
buffer = io.BytesIO(base64.b64decode(data))
# allow_pickle=True 是为了恢复可能包含的复杂dtype
return np.load(buffer, allow_pickle=True)
if obj_type == 'torch.Tensor':
buffer = io.BytesIO(base64.b64decode(data))
return torch.load(buffer)
if obj_type == 'PIL.Image':
buffer = io.BytesIO(base64.b64decode(data))
img = Image.open(buffer)
# .copy() 是个好习惯,确保数据已从缓冲区完全加载
return img.copy()
return dct
def serialize(obj):
"""
将对象序列化为 JSON 字符串。
仅支持基本类型、set、tuple、bytes、numpy.ndarray、torch.Tensor 和 PIL.Image。
"""
return json.dumps(obj, cls=WhitelistEncoder)
def deserialize(json_str):
"""
从 JSON 字符串反序列化对象。
"""
return json.loads(json_str, object_hook=whitelist_decoder)