Files

370 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import copy
from collections import deque
from typing import Dict, List, Set, Tuple
from .side_effects import SIDE_EFFECT_NODES
# SIDE_EFFECT_NODES = {}
class WorkflowGraph:
"""一个封装了工作流图结构和操作的类。"""
def __init__(self, workflow: Dict):
self.workflow = workflow
self.adj: Dict[str, List[str]] = {node_id: [] for node_id in workflow}
self.rev_adj: Dict[str, List[str]] = {node_id: [] for node_id in workflow}
self.in_degree: Dict[str, int] = {node_id: 0 for node_id in workflow}
self.out_degree: Dict[str, int] = {node_id: 0 for node_id in workflow}
self._build()
def _build(self):
"""从工作流字典构建图的邻接表和度信息。"""
for node_id, node_data in self.workflow.items():
for input_value in node_data.get("inputs", {}).values():
if isinstance(input_value, list) and len(input_value) == 2:
source_id = str(input_value[0])
if source_id in self.workflow:
self.add_edge(source_id, node_id)
def add_edge(self, source_id: str, dest_id: str):
"""向图中添加一条边并更新所有相关结构。"""
if dest_id not in self.adj.get(source_id, []):
self.adj.setdefault(source_id, []).append(dest_id)
self.rev_adj.setdefault(dest_id, []).append(source_id)
self.out_degree[source_id] = self.out_degree.get(source_id, 0) + 1
self.in_degree[dest_id] = self.in_degree.get(dest_id, 0) + 1
def remove_edge(self, source_id: str, dest_id: str):
"""从图中移除一条边并更新所有相关结构。"""
if source_id in self.adj and dest_id in self.adj[source_id]:
self.adj[source_id].remove(dest_id)
self.out_degree[source_id] -= 1
if dest_id in self.rev_adj and source_id in self.rev_adj[dest_id]:
self.rev_adj[dest_id].remove(source_id)
self.in_degree[dest_id] -= 1
def topological_sort(self) -> Tuple[List[str], bool]:
"""使用 Kahn 算法进行拓扑排序。返回排序后的节点列表和是否成功的标志。"""
sorted_nodes = []
q = deque([node_id for node_id, degree in self.in_degree.items() if degree == 0])
temp_in_degree = self.in_degree.copy()
while q:
u = q.popleft()
sorted_nodes.append(u)
for v in self.adj.get(u, []):
temp_in_degree[v] -= 1
if temp_in_degree[v] == 0:
q.append(v)
is_valid = len(sorted_nodes) == len(self.workflow)
return sorted_nodes, is_valid
def get_leaf_nodes(self) -> Set[str]:
"""获取图中所有的叶子节点(出度为0)。"""
return {node_id for node_id, degree in self.out_degree.items() if degree == 0}
def _find_function_subgraph(end_node_id: str, graph: WorkflowGraph) -> Set[str]:
"""从 __FunctionEnd__ 节点开始后向遍历,找到完整的函数子图。"""
subgraph_nodes = set()
q = deque([end_node_id])
visited = {end_node_id}
while q:
curr_id = q.popleft()
subgraph_nodes.add(curr_id)
for pred_id in graph.rev_adj.get(curr_id, []):
if pred_id not in visited:
visited.add(pred_id)
q.append(pred_id)
return subgraph_nodes
def _find_param_nodes(subgraph_nodes: Set[str], workflow: Dict, graph: WorkflowGraph) -> Tuple[Set[str], Set[str]]:
param_source_nodes = {n for n in subgraph_nodes if workflow[n].get("class_type") == "__FunctionParam__"}
param_nodes = set()
q = deque(list(param_source_nodes))
visited = set(param_source_nodes)
while q:
curr_id = q.popleft()
param_nodes.add(curr_id)
for succ_id in graph.adj.get(curr_id, []):
if succ_id in subgraph_nodes and succ_id not in visited:
visited.add(succ_id)
q.append(succ_id)
return param_nodes
def _transform_single_function(end_node_id: str, workflow: Dict, graph: WorkflowGraph):
"""转换单个 __FunctionEnd__ 节点及其关联的子图。"""
subgraph_nodes = _find_function_subgraph(end_node_id, graph)
original_end_node = workflow[end_node_id]
enable_capture = original_end_node["inputs"].get("capture", True)
if enable_capture:
param_nodes = _find_param_nodes(subgraph_nodes, workflow, graph)
param_nodes.add(end_node_id)
capture_nodes = subgraph_nodes - param_nodes
else:
param_nodes = subgraph_nodes
capture_nodes = set()
# 恢复原始节点类型以便深拷贝
workflow[end_node_id]["class_type"] = original_end_node["class_type"]
# 步骤 4: 创建 __CreateClosure__ 节点
closure_node = copy.deepcopy(original_end_node)
closure_node["class_type"] = "__CreateClosure__"
closure_node["_meta"] = {"title": "Create Closure"}
workflow[end_node_id] = closure_node
# 步骤 5 & 6: 处理捕获边和函数体
has_side_effects = False
param_subgraph_copy = {}
capture_edge_map = {}
capture_counter = 0
for p_node_id in param_nodes:
if workflow[p_node_id]["class_type"] == "__FunctionParam__":
continue
if workflow[p_node_id]["class_type"] in SIDE_EFFECT_NODES:
has_side_effects = True
node_copy = copy.deepcopy(workflow[p_node_id])
for input_name, input_link in list(node_copy.get("inputs", {}).items()):
if not (isinstance(input_link, list) and len(input_link) == 2):
continue
source_id_str = str(input_link[0])
if source_id_str in capture_nodes:
source_id, source_idx = source_id_str, input_link[1]
edge_tuple = (source_id, source_idx)
if edge_tuple not in capture_edge_map:
capture_edge_map[edge_tuple] = capture_counter
closure_node["inputs"][f"capture_{capture_counter}"] = [source_id, source_idx]
graph.add_edge(source_id, end_node_id)
capture_counter += 1
capture_id = capture_edge_map[edge_tuple]
node_copy["inputs"][input_name] = ["__capture", capture_id]
elif workflow.get(source_id_str, {}).get("class_type") == "__FunctionParam__":
param_node = workflow[source_id_str]
param_index = param_node["inputs"].get("index", 0)
node_copy["inputs"][input_name] = ["__param", param_index]
param_subgraph_copy[p_node_id] = node_copy
output_edge = param_subgraph_copy[end_node_id]["inputs"].get("return_value")
closure_node["inputs"]["output"] = json.dumps(output_edge)
del param_subgraph_copy[end_node_id]
closure_node["inputs"]["body"] = json.dumps(param_subgraph_copy)
closure_node["inputs"]["side_effects"] = float("NaN") if has_side_effects else 0.0
if "return_value" in closure_node["inputs"]:
return_link = closure_node["inputs"]["return_value"]
if isinstance(return_link, list) and len(return_link) == 2:
source_id = str(return_link[0])
graph.remove_edge(source_id, end_node_id)
del closure_node["inputs"]["return_value"]
def _prune_unreachable_nodes(workflow: Dict, leaf_nodes: Set[str]) -> Dict:
"""从叶子节点回溯,移除所有不可达的节点。"""
reachable_nodes = set()
q = deque(list(leaf_nodes))
visited = set(leaf_nodes)
while q:
curr_id = q.popleft()
reachable_nodes.add(curr_id)
node_data = workflow.get(curr_id)
if node_data:
for input_value in node_data.get("inputs", {}).values():
if isinstance(input_value, list) and len(input_value) == 2:
pred_id = str(input_value[0])
if pred_id in workflow and pred_id not in visited:
visited.add(pred_id)
q.append(pred_id)
return {node_id: node_data for node_id, node_data in workflow.items() if node_id in reachable_nodes}
def _preprocess_inspect_nodes(workflow: Dict, execute_outputs: list[str]):
"""
预处理 __Inspect__ 节点。
查找所有仅由 __Inspect__ 节点连接的叶子节点,将其"吸收"到 __Inspect__ 节点的 body 中,
然后将 __Inspect__ 节点转换为 __InspectImpl__ 节点。
此函数会直接在传入的 workflow 字典上进行修改。
"""
temp_graph = WorkflowGraph(workflow)
leaf_nodes = temp_graph.get_leaf_nodes()
nodes_to_remove = set()
modified_inspect_nodes: Dict[str, Dict] = {}
for leaf_id in leaf_nodes:
parents = temp_graph.rev_adj.get(leaf_id, [])
# 条件:叶子节点,且只有一个父节点,且父节点类型为 __Inspect__
if len(parents) == 1:
parent_id = parents[0]
parent_node = workflow.get(parent_id)
if not (parent_node and parent_node.get("class_type") == "__Inspect__"):
continue
# 新增检查:确认叶子节点连接的是 __Inspect__ 的 value 输出(索引 1)
is_connected_to_value_output = False
leaf_node = workflow[leaf_id]
for input_link in leaf_node.get("inputs", {}).values():
if isinstance(input_link, list) and str(input_link[0]) == parent_id and input_link[1] == 1:
is_connected_to_value_output = True
break
# 如果不是连接到 value 输出,则跳过此叶子节点
if not is_connected_to_value_output:
continue
# --- 确认无误后,开始转换逻辑 ---
if parent_id not in modified_inspect_nodes:
modified_inspect_nodes[parent_id] = {}
body = modified_inspect_nodes[parent_id]
leaf_node_copy = copy.deepcopy(leaf_node)
# 查找并替换来自父节点的输入边
for input_name, input_link in leaf_node_copy.get("inputs", {}).items():
if isinstance(input_link, list) and str(input_link[0]) == parent_id and input_link[1] == 1:
leaf_node_copy["inputs"][input_name] = ["__value", 0]
break
body[leaf_id] = leaf_node_copy
nodes_to_remove.add(leaf_id)
# 将更改应用到工作流
# 1. 更新被修改的 __Inspect__ 节点
for inspect_id, body_dict in modified_inspect_nodes.items():
inspect_node = workflow[inspect_id]
inspect_node["class_type"] = "__InspectImpl__"
inspect_node["inputs"]["body"] = json.dumps(body_dict)
# 2. 移除被吸收的叶子节点
for node_id in nodes_to_remove:
if node_id in workflow:
del workflow[node_id]
# Remove from execute_outputs if present
if node_id in execute_outputs:
execute_outputs.remove(node_id)
def transform_workflow(workflow: dict, execute_outputs: list[str]) -> tuple[dict, list]:
"""
根据指定算法变换 ComfyUI 工作流。
此算法首先预处理 __Inspect__ 节点以支持动态输出,然后将由
__FunctionParam__ 和 __FunctionEnd__ 定义的函数子图转换为一个 __CreateClosure__ 节点。
Args:
workflow: 代表 ComfyUI 工作流的字典对象。
Returns:
一个元组,包含:
- 变换后的工作流字典。
- 一个包含警告信息的列表(如果有)。
"""
warnings = []
workflow_og = workflow
workflow = copy.deepcopy(workflow_og)
# 步骤 2: 预处理 __Inspect__ 节点
_preprocess_inspect_nodes(workflow, execute_outputs)
# 步骤 3: 从可能已修改的工作流重建图结构以进行后续操作
graph = WorkflowGraph(workflow)
sorted_nodes, is_valid_dag = graph.topological_sort()
if not is_valid_dag:
warnings.append("Warning: The workflow contains cycles and cannot be fully sorted topologically.")
return workflow_og, warnings
# 在函数变换之前,确定最终用于剪枝的叶子节点集合
leaf_nodes_for_pruning = graph.get_leaf_nodes()
# 步骤 4: 按拓扑顺序迭代,转换所有函数定义
for node_id in sorted_nodes:
if workflow[node_id].get("class_type") == "__FunctionEnd__":
_transform_single_function(node_id, workflow, graph)
# 步骤 5: 剪枝,删除不再被任何叶子节点依赖的节点
pruned_workflow = _prune_unreachable_nodes(workflow, leaf_nodes_for_pruning)
# 步骤 6: 最终检查,确保图中不包含 __FunctionParam__ 节点
for node_id, node_data in pruned_workflow.items():
if node_data.get("class_type") == "__FunctionParam__":
warnings.append(
f"Warning: __FunctionParam__ node '{node_id}' remains in the workflow. This indicates incomplete function definitions."
)
return pruned_workflow, warnings
if __name__ == "__main__":
# 示例 ComfyUI 工作流 JSON 对象 (结构简化)
# A -> B, A -> C, B -> C, C -> D, D -> E
# C 是 __FunctionParam__, E 是 __FunctionEnd__
# A 是捕获变量, B/C/D/E 是函数体
example_workflow = {
"1": { # A: Load Checkpoint (capture)
"inputs": {},
"class_type": "CheckpointLoaderSimple·",
"_meta": {"title": "Load Checkpoint"},
},
"2": { # B: CLIP Text Encode (param)
"inputs": {"clip": ["1", 1]},
"class_type": "CLIPTextEncode",
"_meta": {"title": "Prompt"},
},
"3": { # C: __FunctionParam__ (param source)
"inputs": {"index": 0},
"class_type": "__FunctionParam__",
"_meta": {"title": "Function Param: latent_image"},
},
"4": { # D: KSampler (param)
"inputs": {
"model": ["1", 0], # Input from capture node "1"
"positive": ["2", 0],
"latent_image": ["3", 0],
},
"class_type": "KSampler",
"_meta": {"title": "KSampler"},
},
"5": { # E: __FunctionEnd__ -> __CreateClosure__
"inputs": {"return_value": ["4", 0]},
"class_type": "__FunctionEnd__",
"_meta": {"title": "Function End"},
},
"6": { # F: Final Output Node
"inputs": {"images": ["5", 0]},
"class_type": "SaveImage",
"_meta": {"title": "Save Image"},
},
}
# 调用变换函数
transformed_workflow, warnings_list = transform_workflow(example_workflow)
# 打印结果
print("--- Transformed Workflow ---")
print(json.dumps(transformed_workflow, indent=2))
if warnings_list:
print("\n--- Warnings ---")
for warning in warnings_list:
print(warning)