Spaces:
Sleeping
Sleeping
import type { BadLinksData, SerializedGraph, SerializedLink, SerializedNode } from "typings/index.js"; | |
import type { LGraph, LGraphNode, LLink, serializedLGraph } from "typings/litegraph.js"; | |
enum IoDirection { | |
INPUT, | |
OUTPUT, | |
} | |
function getNodeById(graph: SerializedGraph | LGraph | serializedLGraph, id: number) { | |
if ((graph as LGraph).getNodeById) { | |
return (graph as LGraph).getNodeById(id); | |
} | |
graph = graph as SerializedGraph; | |
return graph.nodes.find((n) => n.id === id)!; | |
} | |
function extendLink(link: SerializedLink) { | |
return { | |
link: link, | |
id: link[0], | |
origin_id: link[1], | |
origin_slot: link[2], | |
target_id: link[3], | |
target_slot: link[4], | |
type: link[5], | |
}; | |
} | |
/** | |
* Takes a SerializedGraph or live LGraph and inspects the links and nodes to ensure the linking | |
* makes logical sense. Can apply fixes when passed the `fix` argument as true. | |
* | |
* Note that fixes are a best-effort attempt. Seems to get it correct in most cases, but there is a | |
* chance it correct an anomoly that results in placing an incorrect link (say, if there were two | |
* links in the data). Users should take care to not overwrite work until manually checking the | |
* result. | |
*/ | |
export function fixBadLinks( | |
graph: SerializedGraph | LGraph, | |
fix = false, | |
silent = false, | |
logger: { log: (...args: any[]) => void } = console, | |
): BadLinksData { | |
const patchedNodeSlots: { | |
[nodeId: string]: { | |
inputs?: { [slot: number]: number | null }; | |
outputs?: { | |
[slots: number]: { | |
links: number[]; | |
changes: { [linkId: number]: "ADD" | "REMOVE" }; | |
}; | |
}; | |
}; | |
} = {}; | |
// const logger = this.newLogSession("[findBadLinks]"); | |
const data: { patchedNodes: Array<SerializedNode | LGraphNode>; deletedLinks: number[] } = { | |
patchedNodes: [], | |
deletedLinks: [], | |
}; | |
/** | |
* Internal patch node. We keep track of changes in patchedNodeSlots in case we're in a dry run. | |
*/ | |
async function patchNodeSlot( | |
node: SerializedNode | LGraphNode, | |
ioDir: IoDirection, | |
slot: number, | |
linkId: number, | |
op: "ADD" | "REMOVE", | |
) { | |
patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; | |
const patchedNode = patchedNodeSlots[node.id]!; | |
if (ioDir == IoDirection.INPUT) { | |
patchedNode["inputs"] = patchedNode["inputs"] || {}; | |
// We can set to null (delete), so undefined means we haven't set it at all. | |
if (patchedNode["inputs"]![slot] !== undefined) { | |
!silent && | |
logger.log( | |
` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"]![ | |
slot | |
]!} Skipping.`, | |
); | |
return false; | |
} | |
let linkIdToSet = op === "REMOVE" ? null : linkId; | |
patchedNode["inputs"]![slot] = linkIdToSet; | |
if (fix) { | |
// node.inputs[slot]!.link = linkIdToSet; | |
} | |
} else { | |
patchedNode["outputs"] = patchedNode["outputs"] || {}; | |
patchedNode["outputs"]![slot] = patchedNode["outputs"]![slot] || { | |
links: [...(node.outputs?.[slot]?.links || [])], | |
changes: {}, | |
}; | |
if (patchedNode["outputs"]![slot]!["changes"]![linkId] !== undefined) { | |
!silent && | |
logger.log( | |
` > Already set ${node.id}.outputs[${slot}] to ${ | |
patchedNode["inputs"]![slot] | |
}! Skipping.`, | |
); | |
return false; | |
} | |
patchedNode["outputs"]![slot]!["changes"]![linkId] = op; | |
if (op === "ADD") { | |
let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); | |
if (linkIdIndex !== -1) { | |
!silent && logger.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`); | |
return false; | |
} | |
patchedNode["outputs"]![slot]!["links"].push(linkId); | |
if (fix) { | |
node.outputs = node.outputs || []; | |
node.outputs[slot] = node.outputs[slot] || ({} as any); | |
node.outputs[slot]!.links = node.outputs[slot]!.links || []; | |
node.outputs[slot]!.links!.push(linkId); | |
} | |
} else { | |
let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); | |
if (linkIdIndex === -1) { | |
!silent && logger.log(` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`); | |
return false; | |
} | |
patchedNode["outputs"]![slot]!["links"].splice(linkIdIndex, 1); | |
if (fix) { | |
node.outputs?.[slot]!.links!.splice(linkIdIndex, 1); | |
} | |
} | |
} | |
data.patchedNodes.push(node); | |
return true; | |
} | |
/** | |
* Internal to check if a node (or patched data) has a linkId. | |
*/ | |
function nodeHasLinkId( | |
node: SerializedNode | LGraphNode, | |
ioDir: IoDirection, | |
slot: number, | |
linkId: number, | |
) { | |
// Patched data should be canonical. We can double check if fixing too. | |
let has = false; | |
if (ioDir === IoDirection.INPUT) { | |
let nodeHasIt = node.inputs?.[slot]?.link === linkId; | |
if (patchedNodeSlots[node.id]?.["inputs"]) { | |
let patchedHasIt = patchedNodeSlots[node.id]!["inputs"]![slot] === linkId; | |
// If we're fixing, double check that node matches. | |
if (fix && nodeHasIt !== patchedHasIt) { | |
throw Error("Error. Expected node to match patched data."); | |
} | |
has = patchedHasIt; | |
} else { | |
has = !!nodeHasIt; | |
} | |
} else { | |
let nodeHasIt = node.outputs?.[slot]?.links?.includes(linkId); | |
if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"][linkId]) { | |
let patchedHasIt = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.includes(linkId); | |
// If we're fixing, double check that node matches. | |
if (fix && nodeHasIt !== patchedHasIt) { | |
throw Error("Error. Expected node to match patched data."); | |
} | |
has = !!patchedHasIt; | |
} else { | |
has = !!nodeHasIt; | |
} | |
} | |
return has; | |
} | |
/** | |
* Internal to check if a node (or patched data) has a linkId. | |
*/ | |
function nodeHasAnyLink(node: SerializedNode | LGraphNode, ioDir: IoDirection, slot: number) { | |
// Patched data should be canonical. We can double check if fixing too. | |
let hasAny = false; | |
if (ioDir === IoDirection.INPUT) { | |
let nodeHasAny = node.inputs?.[slot]?.link != null; | |
if (patchedNodeSlots[node.id]?.["inputs"]) { | |
let patchedHasAny = patchedNodeSlots[node.id]!["inputs"]![slot] != null; | |
// If we're fixing, double check that node matches. | |
if (fix && nodeHasAny !== patchedHasAny) { | |
throw Error("Error. Expected node to match patched data."); | |
} | |
hasAny = patchedHasAny; | |
} else { | |
hasAny = !!nodeHasAny; | |
} | |
} else { | |
let nodeHasAny = node.outputs?.[slot]?.links?.length; | |
if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"]) { | |
let patchedHasAny = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.length; | |
// If we're fixing, double check that node matches. | |
if (fix && nodeHasAny !== patchedHasAny) { | |
throw Error("Error. Expected node to match patched data."); | |
} | |
hasAny = !!patchedHasAny; | |
} else { | |
hasAny = !!nodeHasAny; | |
} | |
} | |
return hasAny; | |
} | |
let links: Array<SerializedLink | LLink> = []; | |
if (!Array.isArray(graph.links)) { | |
Object.values(graph.links).reduce((acc, v) => { | |
acc[v.id] = v; | |
return acc; | |
}, links); | |
} else { | |
links = graph.links; | |
} | |
const linksReverse = [...links]; | |
linksReverse.reverse(); | |
for (let l of linksReverse) { | |
if (!l) continue; | |
const link = (l as LLink).origin_slot != null ? (l as LLink) : extendLink(l as SerializedLink); | |
const originNode = getNodeById(graph, link.origin_id); | |
const originHasLink = () => | |
nodeHasLinkId(originNode!, IoDirection.OUTPUT, link.origin_slot, link.id); | |
const patchOrigin = (op: "ADD" | "REMOVE", id = link.id) => | |
patchNodeSlot(originNode!, IoDirection.OUTPUT, link.origin_slot, id, op); | |
const targetNode = getNodeById(graph, link.target_id); | |
const targetHasLink = () => | |
nodeHasLinkId(targetNode!, IoDirection.INPUT, link.target_slot, link.id); | |
const targetHasAnyLink = () => nodeHasAnyLink(targetNode!, IoDirection.INPUT, link.target_slot); | |
const patchTarget = (op: "ADD" | "REMOVE", id = link.id) => | |
patchNodeSlot(targetNode!, IoDirection.INPUT, link.target_slot, id, op); | |
const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`; | |
const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`; | |
if (!originNode || !targetNode) { | |
if (!originNode && !targetNode) { | |
!silent && | |
logger.log( | |
`Link ${link.id} is invalid, ` + | |
`both origin ${link.origin_id} and target ${link.target_id} do not exist`, | |
); | |
} else if (!originNode) { | |
!silent && | |
logger.log( | |
`Link ${link.id} is funky... ` + | |
`origin ${link.origin_id} does not exist, but target ${link.target_id} does.`, | |
); | |
if (targetHasLink()) { | |
!silent && | |
logger.log( | |
` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`, | |
); | |
patchTarget("REMOVE", -1); | |
} | |
} else if (!targetNode) { | |
!silent && | |
logger.log( | |
`Link ${link.id} is funky... ` + | |
`target ${link.target_id} does not exist, but origin ${link.origin_id} does.`, | |
); | |
if (originHasLink()) { | |
!silent && | |
logger.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`); | |
patchOrigin("REMOVE"); | |
} | |
} | |
continue; | |
} | |
if (targetHasLink() || originHasLink()) { | |
if (!originHasLink()) { | |
!silent && | |
logger.log( | |
`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`, | |
); | |
!silent && | |
logger.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`); | |
patchOrigin("ADD"); | |
} else if (!targetHasLink()) { | |
!silent && | |
logger.log( | |
`${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode.inputs?.[ | |
link.target_slot | |
]?.link}), but ${originLog} contains it`, | |
); | |
if (!targetHasAnyLink()) { | |
!silent && logger.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`); | |
let patched = patchTarget("ADD"); | |
if (!patched) { | |
!silent && | |
logger.log( | |
` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`, | |
); | |
patched = patchOrigin("REMOVE"); | |
} | |
} else { | |
!silent && | |
logger.log( | |
` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`, | |
); | |
patchOrigin("REMOVE"); | |
} | |
} | |
} | |
} | |
// Now that we've cleaned up the inputs, outputs, run through it looking for dangling links., | |
for (let l of linksReverse) { | |
if (!l) continue; | |
const link = (l as LLink).origin_slot != null ? (l as LLink) : extendLink(l as SerializedLink); | |
const originNode = getNodeById(graph, link.origin_id); | |
const targetNode = getNodeById(graph, link.target_id); | |
// Now that we've manipulated the linking, check again if they both exist. | |
if ( | |
(!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && | |
(!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id)) | |
) { | |
!silent && | |
logger.log( | |
`${link.id} is def invalid; BOTH origin node ${link.origin_id} ${ | |
!originNode ? "is removed" : `doesn\'t have ${link.id}` | |
} and ${link.origin_id} target node ${ | |
!targetNode ? "is removed" : `doesn\'t have ${link.id}` | |
}.`, | |
); | |
data.deletedLinks.push(link.id); | |
continue; | |
} | |
} | |
// If we're fixing, then we've been patching along the way. Now go through and actually delete | |
// the zombie links from `app.graph.links` | |
if (fix) { | |
for (let i = data.deletedLinks.length - 1; i >= 0; i--) { | |
!silent && logger.log(`Deleting link #${data.deletedLinks[i]}.`); | |
if ((graph as LGraph).getNodeById) { | |
delete graph.links[data.deletedLinks[i]!]; | |
} else { | |
graph = graph as SerializedGraph; | |
// Sometimes we got objects for links if passed after ComfyUI's loadGraphData modifies the | |
// data. We make a copy now, but can handle the bastardized objects just in case. | |
const idx = graph.links.findIndex( | |
(l) => l && (l[0] === data.deletedLinks[i] || (l as any).id === data.deletedLinks[i]), | |
); | |
if (idx === -1) { | |
logger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`); | |
} | |
logger.log(`splicing ${idx} from links`); | |
graph.links.splice(idx, 1); | |
} | |
} | |
// If we're a serialized graph, we can filter out the links because it's just an array. | |
if (!(graph as LGraph).getNodeById) { | |
graph.links = (graph as SerializedGraph).links.filter((l) => !!l); | |
} | |
} | |
if (!data.patchedNodes.length && !data.deletedLinks.length) { | |
return { | |
hasBadLinks: false, | |
fixed: false, | |
graph, | |
patched: data.patchedNodes.length, | |
deleted: data.deletedLinks.length, | |
}; | |
} | |
!silent && | |
logger.log( | |
`${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${ | |
data.deletedLinks.length || "no" | |
} stale link removals.`, | |
); | |
let hasBadLinks: boolean = !!(data.patchedNodes.length || data.deletedLinks.length); | |
// If we're fixing, then let's run it again to see if there are no more bad links. | |
if (fix && !silent) { | |
const rerun = fixBadLinks(graph, false, true); | |
hasBadLinks = rerun.hasBadLinks; | |
} | |
return { | |
hasBadLinks, | |
fixed: !!hasBadLinks && fix, | |
graph, | |
patched: data.patchedNodes.length, | |
deleted: data.deletedLinks.length, | |
}; | |
} | |