multimodalart's picture
Squashing commit
4450790 verified
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,
};
}