Files
MontagenAI-ComfyUI-Montagen/server/LGraph.py
T
igrzf12 f47a356b1e refactor(server): optimize workflow synchronization and material handling
- Update workflow synchronization logic to handle version conflicts
- Refactor material handling to support parent-child relationships
- Improve clip creation and updating process
- Optimize workflow initialization and saving
2025-03-20 18:31:16 +08:00

378 lines
13 KiB
Python

from .LLink import LLink
from .LGraphNode import LGraphNode
from .Utils import to_base36_random, MONTAGENPROJ
from datetime import datetime
class LGraph:
def __init__(self, data=None):
self.state = {
"lastGroupId": 0,
"lastNodeId": 0,
"lastLinkId": 0,
"lastRerouteId": 0,
}
if data:
if "workflow" in data:
data = data["workflow"]
self.data = data
else:
self.data = LGraph.create_empty_workflow()
self.configure(data)
@staticmethod
def create_empty_workflow(
user_id=None, project_id=None, workflow_id=None, workflow_name=None
):
return {
"last_node_id": 0,
"last_link_id": 0,
"nodes": [],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [100, 300],
},
MONTAGENPROJ: {
"userId": user_id,
"projectId": project_id,
"workflowId": workflow_id,
"workflowName": workflow_name,
"version": 0,
"modifyTime": datetime.now().isoformat(),
},
},
"version": 0.4,
}
@property
def extra(self):
if "extra" not in self.data:
self.data["extra"] = {}
return self.data.get("extra")
@property
def montagenInfo(self):
return self.extra.get(MONTAGENPROJ, {})
@montagenInfo.setter
def montagenInfo(self, value):
self.extra[MONTAGENPROJ] = value
@property
def nodes(self):
if "nodes" not in self.data:
self.data["nodes"] = []
return self.data.get("nodes", [])
@property
def graphNodes(self):
return [LGraphNode(self, node) for node in self.data.get("nodes", [])]
@property
def links(self):
if "links" not in self.data:
self.data["links"] = []
return self.data.get("links", [])
@property
def montagenName(self):
return self.montagenInfo.get("workflowName")
@montagenName.setter
def montagenName(self, value):
if value:
self.montagenInfo["workflowName"] = value
@property
def version(self):
return self.montagenInfo.get("version")
@version.setter
def version(self, value):
self.montagenInfo["version"] = value
@property
def montagenWorkflowId(self):
return self.montagenInfo.get("workflowId")
@property
def montagenModifyTime(self):
return datetime.fromisoformat(
self.montagenInfo.get("modifyTime", datetime.now().isoformat())
)
@montagenModifyTime.setter
def montagenModifyTime(self, value):
self.montagenInfo["modifyTime"] = value.isoformat()
def increace_version(self):
self.version += 1
# def addEmptyNode(self, clipId, name, type, tag=None):
# state = self.state
# node = LGraphNode.CreateNode(self, clipId, name, type, tag)
# node.id = state["lastNodeId"] + 1
# if state["lastNodeId"] < node.id:
# state["lastNodeId"] = node.id
# self.nodes.append(node.serialize())
# self.serialize()
def setWorkflowInfo(self, user_id, project_id, workflow_id, workflow_name):
montagen_workflow_info = {
"userId": user_id,
"projectId": project_id,
"workflowId": workflow_id,
"workflowName": workflow_name or self.montagenName,
}
self.extra[MONTAGENPROJ] = montagen_workflow_info
def deleteNode(self, clipId):
for i, node in enumerate(self.nodes):
lGraphNode = LGraphNode(self, node)
if lGraphNode.clipId == clipId:
if hasattr(lGraphNode, "inputs"):
for j, slot in enumerate(lGraphNode.inputs):
if slot.link is not None:
self.disconnectInput(lGraphNode, slot.link)
if hasattr(lGraphNode, "outputs"):
for k, slot in enumerate(lGraphNode.outputs):
if slot.links and len(slot.links):
self.disconnectOutput(lGraphNode, slot.links)
self.nodes.pop(i)
break
def renameNode(self, clipId, name):
for i, node in enumerate(self.nodes):
lGraphNode = LGraphNode(self, node)
if lGraphNode.clipId == clipId:
lGraphNode.clipName = name
break
def disconnectInput(self, nodeInput: LGraphNode, linkId):
for i, link in enumerate(self.links):
lLink = LLink.create_from_array(link)
if lLink.id == linkId:
self.links.pop(i)
if lLink.origin_id:
for j, node in enumerate(self.nodes):
lGraphNode = LGraphNode(self, node)
if lGraphNode.id == lLink.origin_id:
for k, lslot in enumerate(lGraphNode.outputs):
if lslot.links:
for l, llink in enumerate(lslot.links):
if llink == linkId:
lslot.links.pop(l)
if len(lslot.links) == 0:
lslot.links = None
break
break
def disconnectOutput(self, nodeOutput: LGraphNode, links):
for i, linkId in enumerate(links):
for j, link in enumerate(self.links):
lLink = LLink.create_from_array(link)
if lLink.id == linkId:
self.links.pop(j)
if lLink.target_id:
for k, node in enumerate(self.nodes):
lGraphNode = LGraphNode(self, node)
if lGraphNode.id == lLink.target_id:
for l, lslot in enumerate(lGraphNode.inputs):
if linkId == lslot.link:
lslot.link = None
break
break
def hasNode(self, clipId):
for node in self.nodes:
lGraphNode = LGraphNode(self, node)
if lGraphNode.clipId == clipId:
return True
return False
def getNodeById(self, nodeId):
for node in self.nodes:
lGraphNode = LGraphNode(self, node)
if str(lGraphNode.id) == str(nodeId):
return lGraphNode
return None
def isOldStyleClipId(self, clipId):
return "_" in clipId
def exportNode(self, clipId):
nodes_to_copy = []
links_to_copy = []
def collect_connected_nodes(node_id, direction="both"):
for link in self.links:
llink = LLink.create_from_array(link)
if direction in ["both", "input"] and llink.target_id == node_id:
source_node = next(
(
n
for n in self.nodes
if LGraphNode(self, n).id == llink.origin_id
),
None,
)
if source_node and source_node not in nodes_to_copy:
nodes_to_copy.append(source_node)
collect_connected_nodes(llink.origin_id, "input")
if link not in links_to_copy:
links_to_copy.append(link)
if direction in ["both", "output"] and llink.origin_id == node_id:
target_node = next(
(
n
for n in self.nodes
if LGraphNode(self, n).id == llink.target_id
),
None,
)
if target_node and target_node not in nodes_to_copy:
nodes_to_copy.append(target_node)
collect_connected_nodes(llink.target_id, "output")
if link not in links_to_copy:
links_to_copy.append(link)
# Find the source node and its connected nodes
source_node = next(
(n for n in self.nodes if LGraphNode(self, n).clipId == clipId), None
)
if not source_node:
return None
nodes_to_copy.append(source_node)
collect_connected_nodes(LGraphNode(self, source_node).id)
# Prepare exported data
exported_nodes = []
exported_links = []
# Create new nodes with sequential IDs
for node in nodes_to_copy:
original_node = LGraphNode(self, node)
new_node = original_node.clone()
exported_nodes.append(new_node)
# Create new links with sequential IDs
for link in links_to_copy:
original_link = LLink.create_from_array(link)
new_link = [
original_link.id,
original_link.origin_id,
original_link.origin_slot,
original_link.target_id,
original_link.target_slot,
original_link.type,
]
exported_links.append(new_link)
return {"nodes": exported_nodes, "links": exported_links}
def importNode(self, imported_data):
"""Import nodes and links from another graph while handling ID mapping"""
if (
not imported_data
or "nodes" not in imported_data
or "links" not in imported_data
):
return None
# Create new ID mappings
old_to_new_ids = {}
old_to_new_link_ids = {}
imported_nodes = []
imported_links = []
# Process nodes
for node in imported_data["nodes"]:
new_id = self.state["lastNodeId"] + 1
self.state["lastNodeId"] = new_id
old_to_new_ids[node["id"]] = new_id
node["id"] = new_id
imported_nodes.append(node)
lNode = LGraphNode(self, node)
if lNode.isMontagenNode:
lNode.clipId = to_base36_random()
# Process links
for link in imported_data["links"]:
new_link_id = self.state["lastLinkId"] + 1
self.state["lastLinkId"] = new_link_id
# Map old IDs to new IDs for the link
new_link = [
new_link_id,
old_to_new_ids[link[1]], # origin_id
link[2], # origin_slot
old_to_new_ids[link[3]], # target_id
link[4], # target_slot
link[5], # type
]
old_to_new_link_ids[link[0]] = new_link_id
imported_links.append(new_link)
for node in imported_nodes:
for slot in node["inputs"]:
if slot["link"]:
slot["link"] = old_to_new_link_ids.get(slot["link"], None)
for slot in node["outputs"]:
if slot["links"]:
newlinks = []
for link in slot["links"]:
newlink = old_to_new_link_ids.get(link, None)
if newlink:
newlinks.append(newlink)
slot["links"] = None if len(newlinks) == 0 else newlinks
# Add imported nodes and links to the graph
self.nodes.extend(imported_nodes)
self.links.extend(imported_links)
self.serialize()
return {"nodes": imported_nodes, "links": imported_links}
def serialize(self):
data = self.data
if data.get("version") == 0.4:
data["last_node_id"] = self.state["lastNodeId"]
data["last_link_id"] = self.state["lastLinkId"]
else:
if data.get("state"):
state = data["state"]
state["lastGroupId"] = self.state["lastGroupId"]
state["lastLinkId"] = self.state["lastLinkId"]
state["lastNodeId"] = self.state["lastNodeId"]
state["lastRerouteId"] = self.state["lastRerouteId"]
return self.data
def configure(self, data):
if not data:
return
if data.get("version") == 0.4:
if "last_node_id" in data:
self.state["lastNodeId"] = data["last_node_id"]
if "last_link_id" in data:
self.state["lastLinkId"] = data["last_link_id"]
else:
if data.get("state"):
state = data["state"]
if "lastGroupId" in state:
self.state["lastGroupId"] = state["lastGroupId"]
if "lastLinkId" in state:
self.state["lastLinkId"] = state["lastLinkId"]
if "lastNodeId" in state:
self.state["lastNodeId"] = state["lastNodeId"]
if "lastRerouteId" in state:
self.state["lastRerouteId"] = state["lastRerouteId"]