diff --git a/__dev__.py b/__dev__.py index 3aab20d..c07d27d 100644 --- a/__dev__.py +++ b/__dev__.py @@ -1,16 +1,18 @@ import subprocess import os import shutil +import glob THIS_DIR=os.path.dirname(os.path.abspath(__file__)) -DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js') -DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') +DIR_DEV=os.path.abspath(f'{THIS_DIR}/web') +DIR_WEB=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy') -if os.path.exists(DIR_DEV_JS): - shutil.rmtree(DIR_DEV_JS) +js_files = glob.glob(os.path.join(THIS_DIR, '*.js')) +for file in js_files: + os.remove(file) subprocess.run(["./node_modules/typescript/bin/tsc"]) -if os.path.exists(DIR_WEB_JS): - shutil.rmtree(DIR_WEB_JS) -shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) \ No newline at end of file +if os.path.exists(DIR_WEB): + shutil.rmtree(DIR_WEB) +shutil.copytree(DIR_DEV, DIR_WEB, dirs_exist_ok=True) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 483e522..23f1618 100644 --- a/__init__.py +++ b/__init__.py @@ -10,7 +10,7 @@ import inspect import os import shutil -from server import PromptServer +# from .server import server from .py.log import log_welcome from .py.context import RgthreeContext @@ -45,21 +45,32 @@ NODE_CLASS_MAPPINGS = { RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, } -THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -DIR_DEV_JS = os.path.abspath(f'{THIS_DIR}/js') -DIR_PY = os.path.abspath(f'{THIS_DIR}/py') -DIR_WEB_JS = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') -if not os.path.exists(DIR_WEB_JS): - os.makedirs(DIR_WEB_JS) -shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) +# This doesn't import correctly.. +# WEB_DIRECTORY = "./web" + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +DIR_DEV_WEB = os.path.abspath(f'{THIS_DIR}/web/') +DIR_PY = os.path.abspath(f'{THIS_DIR}/py') + +# remove old directory. +OLD_DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') +if os.path.exists(OLD_DIR_WEB): + shutil.rmtree(OLD_DIR_WEB) + +DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy') +if os.path.exists(DIR_WEB): + shutil.rmtree(DIR_WEB) +os.makedirs(DIR_WEB) + +shutil.copytree(DIR_DEV_WEB, DIR_WEB, dirs_exist_ok=True) NOT_NODES = ['constants', 'log', 'utils', 'rgthree'] __all__ = ['NODE_CLASS_MAPPINGS'] nodes = [] -for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=DIR_DEV_JS): +for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=os.path.join(DIR_DEV_WEB, 'js')): name = os.path.splitext(file)[0] if name not in nodes and name not in NOT_NODES and not name.startswith( '_') and not name.startswith('base') and not 'utils' in name: diff --git a/ts/html/links.ts b/ts/html/links.ts new file mode 100644 index 0000000..98432c3 --- /dev/null +++ b/ts/html/links.ts @@ -0,0 +1,616 @@ +// @ts-ignore +import { getPngMetadata } from "/scripts/pnginfo.js"; + +type SerializedLink = [ + number, // this.id, + number, // this.origin_id, + number, // this.origin_slot, + number, // this.target_id, + number, // this.target_slot, + number, // this.type +]; + +interface SerializedNodeInput { + name: string; + type: string; + link: number; +} +interface SerializedNodeOutput { + name: string; + type: string; + link: number; + slot_index: number; + links: number[]; +} +interface SerializedNode { + id: number; + inputs: SerializedNodeInput[]; + outputs: SerializedNodeOutput[]; + mode: number; + order: number; + pos: [number, number]; + properties: any; + size: [number, number]; + type: string; + widgets_values: Array; +} + +interface SerializedGraph { + config: any; + extra: any; + groups: any; + last_link_id: number; + last_node_id: number; + links: SerializedLink[]; + nodes: SerializedNode[]; +} + +enum IoDirection { + INPUT, + OUTPUT, +} + +interface BadLinksData { + fixed: boolean; + graph: SerializedGraph; + patched: number; + deleted: number; +} + +function wait(ms = 16, value?: any) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); +} + +const logger = { + logTo: console as Console | HTMLElement, + log: (...args: any[]) => { + logger.logTo === console + ? console.log(...args) + : ((logger.logTo as HTMLElement).innerText += args.join(",") + "\n"); + }, +}; + +const findBadLinksLogger = { + log: async (...args: any[]) => { + logger.log(...args); + // await wait(48); + }, +}; + +class LinkPage { + private containerEl: HTMLDivElement; + private figcaptionEl: HTMLElement; + private btnFix: HTMLButtonElement; + private outputeMessageEl: HTMLDivElement; + private outputImageEl: HTMLImageElement; + + private file?: File | Blob; + private graph?: SerializedGraph; + private graphResults?: BadLinksData; + private graphFinalResults?: BadLinksData; + + constructor() { + // const consoleEl = document.getElementById("console")!; + this.containerEl = document.querySelector(".box")!; + this.figcaptionEl = document.querySelector("figcaption")!; + this.outputeMessageEl = document.querySelector(".output")!; + this.outputImageEl = document.querySelector(".output-image")!; + this.btnFix = document.querySelector(".btn-fix")!; + + // Need to prevent on dragover to allow drop... + document.addEventListener( + "dragover", + (e) => { + e.preventDefault(); + }, + false, + ); + document.addEventListener("drop", (e) => { + this.onDrop(e); + }); + this.btnFix.addEventListener("click", (e) => { + this.onFixClick(e); + }); + } + + private async onFixClick(e: MouseEvent) { + if (!this.graphResults || !this.graph) { + this.updateUi("⛔ Fix button click without results."); + return; + } + // Fix + let graphFinalResults = await fixBadLinks(this.graph, true); + // Confirm + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + // This should have happened, but try to run it through again if there's till an issue. + if (graphFinalResults.patched || graphFinalResults.deleted) { + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + } + // Final Confirm + if (graphFinalResults.patched || graphFinalResults.deleted) { + this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow."); + return + } + this.graphFinalResults = graphFinalResults; + this.updateUi("✅ Workflow fixed."); + this.saveFixedWorkflow(); + + } + + private async onDrop(event: DragEvent) { + if (!event.dataTransfer) { + return; + } + this.reset(); + + event.preventDefault(); + event.stopPropagation(); + + // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that + if (event.dataTransfer.files.length && event.dataTransfer.files?.[0]?.type !== "image/bmp") { + await this.handleFile(event.dataTransfer.files[0]!); + return; + } + + // Try loading the first URI in the transfer list + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v)); + if (match) { + const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } + + reset() { + this.file = undefined; + this.graph = undefined; + this.graphResults = undefined; + this.graphFinalResults = undefined; + this.updateUi(); + } + + private updateUi(msg?: string) { + this.outputeMessageEl.innerHTML = ""; + if (this.file && !this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.add("-has-file"); + this.figcaptionEl.innerHTML = (this.file as File).name || this.file.type; + if (this.file.type === "application/json") { + this.outputImageEl.src = "icon_file_json.png"; + } else { + const reader = new FileReader(); + reader.onload = () => (this.outputImageEl.src = reader.result as string); + reader.readAsDataURL(this.file); + } + } else if (!this.file && this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.remove("-has-file"); + this.outputImageEl.src = ""; + this.outputImageEl.removeAttribute("src"); + } + + if (this.graphResults) { + this.containerEl.classList.add("-has-results"); + if (!this.graphResults.patched && !this.graphResults.deleted) { + this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; + } else { + this.outputeMessageEl.innerHTML = `⚠️ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; + } + } else { + this.containerEl.classList.remove("-has-results"); + } + + if (msg) { + this.outputeMessageEl.innerHTML = msg; + } + } + + private async handleFile(file: File | Blob) { + this.file = file; + this.updateUi(); + + let workflow: string | null = null; + if (file.type.startsWith("image/")) { + const pngInfo = await getPngMetadata(file); + workflow = pngInfo?.workflow; + } else if ( + file.type === "application/json" || + (file instanceof File && file.name.endsWith(".json")) + ) { + workflow = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsText(file); + }); + } + if (!workflow) { + this.updateUi("⛔ No workflow found in dropped item."); + } else { + try { + this.graph = JSON.parse(workflow); + } catch (e) { + this.graph = undefined; + } + if (!this.graph) { + this.updateUi("⛔ Invalid workflow found in dropped item."); + } else { + this.loadGraphData(this.graph); + } + } + } + + private async loadGraphData(graphData: SerializedGraph) { + this.graphResults = await fixBadLinks(graphData); + this.updateUi(); + } + + private async saveFixedWorkflow() { + if (!this.graphFinalResults) { + this.updateUi("⛔ Save w/o final graph patched."); + return; + } + + let filename: string|null = (this.file as File).name || 'workflow.json'; + let filenames = filename.split('.'); + filenames.pop(); + filename = filenames.join('.'); + filename += '_fixed.json'; + filename = prompt("Save workflow as:", filename); + if (!filename) return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + const json = JSON.stringify(this.graphFinalResults.graph, null, 2); + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.download = filename; + anchor.href = url; + anchor.style.display = 'none'; + document.body.appendChild(anchor); + await wait(); + anchor.click(); + await wait(); + anchor.remove(); + window.URL.revokeObjectURL(url); + } +} + +new LinkPage(); + +function getNodeById(graph: SerializedGraph, id: number) { + 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 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. + */ +async function fixBadLinks(graph: SerializedGraph, fix = false): Promise { + const patchedNodeSlots: { + [nodeId: string]: { + inputs?: { [slot: number]: number | null }; + outputs?: { + [slots: number]: { + links: number[]; + changes: { [linkId: number]: "ADD" | "REMOVE" }; + }; + }; + }; + } = {}; + // const findBadLinksLogger = this.newLogSession("[findBadLinks]"); + const data: { patchedNodes: SerializedNode[]; 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, + 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) { + await findBadLinksLogger.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) { + await findBadLinksLogger.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) { + await findBadLinksLogger.log( + ` > Hmmm.. asked to add ${linkId} but it is already in list...`, + ); + return false; + } + patchedNode["outputs"]![slot]!["links"].push(linkId); + if (fix) { + node.outputs[slot]!.links?.push(linkId); + } + } else { + let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); + if (linkIdIndex === -1) { + await findBadLinksLogger.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, 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, 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; + } + + const linksReverse = [...graph.links]; + linksReverse.reverse(); + for (let l of linksReverse) { + if (!l) continue; + const link = extendLink(l); + + 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) { + await findBadLinksLogger.log( + `Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`, + ); + } else if (!originNode) { + await findBadLinksLogger.log( + `Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`, + ); + if (targetHasLink()) { + await findBadLinksLogger.log( + ` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`, + ); + patchTarget("REMOVE", -1); + } + } else if (!targetNode) { + await findBadLinksLogger.log( + `Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`, + ); + if (originHasLink()) { + await findBadLinksLogger.log( + ` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`, + ); + patchOrigin("REMOVE"); + } + } + continue; + } + + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + await findBadLinksLogger.log( + `${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`, + ); + await findBadLinksLogger.log( + ` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`, + ); + patchOrigin("ADD"); + } else if (!targetHasLink()) { + await findBadLinksLogger.log( + `${link.id} is funky... ${targetLog} is NOT correct (is ${ + targetNode.inputs[link.target_slot]!.link + }), but ${originLog} contains it`, + ); + if (!targetHasAnyLink()) { + await findBadLinksLogger.log( + ` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`, + ); + let patched = patchTarget("ADD"); + if (!patched) { + await findBadLinksLogger.log( + ` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`, + ); + patched = patchOrigin("REMOVE"); + } + } else { + await findBadLinksLogger.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 = extendLink(l); + 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)) + ) { + await findBadLinksLogger.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 ${ + link.target_id ? "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--) { + await findBadLinksLogger.log(`Deleting link #${data.deletedLinks[i]}.`); + // graph.links[data.deletedLinks[i]!]; + const idx = graph.links.findIndex((l) => l[0] === data.deletedLinks[i]); + if (idx === -1) { + await findBadLinksLogger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`); + } + graph.links.splice(idx, 1); + } + graph.links = graph.links.filter((l) => !!l); + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + await findBadLinksLogger.log(`No bad links detected.`); + return { + fixed: false, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; + } + await findBadLinksLogger.log( + `${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${ + data.deletedLinks.length || "no" + } stale link removals.`, + ); + return { + fixed: fix, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; +} diff --git a/tsconfig.json b/tsconfig.json index c4e2d45..87ebc34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "paths": { "*": ["ts/typings/*"], }, - "outDir": "js/", + "outDir": "web/", "removeComments": true, "strict": true, "noImplicitAny": true, @@ -32,7 +32,7 @@ "skipLibCheck": true, }, "include": [ - "ts/*.ts", "ts/typings/index.d.ts", + "ts/*.ts", "ts/**/*.ts", "ts/typings/index.d.ts", ], "exclude": [ "**/*.spec.ts", diff --git a/js/base_any_input_connected_node.js b/web/base_any_input_connected_node.js similarity index 100% rename from js/base_any_input_connected_node.js rename to web/base_any_input_connected_node.js diff --git a/js/base_node.js b/web/base_node.js similarity index 100% rename from js/base_node.js rename to web/base_node.js diff --git a/js/base_node_collector.js b/web/base_node_collector.js similarity index 100% rename from js/base_node_collector.js rename to web/base_node_collector.js diff --git a/js/base_node_mode_changer.js b/web/base_node_mode_changer.js similarity index 100% rename from js/base_node_mode_changer.js rename to web/base_node_mode_changer.js diff --git a/js/base_power_prompt.js b/web/base_power_prompt.js similarity index 100% rename from js/base_power_prompt.js rename to web/base_power_prompt.js diff --git a/js/bypasser.js b/web/bypasser.js similarity index 100% rename from js/bypasser.js rename to web/bypasser.js diff --git a/js/constants.js b/web/constants.js similarity index 100% rename from js/constants.js rename to web/constants.js diff --git a/js/context.js b/web/context.js similarity index 100% rename from js/context.js rename to web/context.js diff --git a/js/display_any.js b/web/display_any.js similarity index 100% rename from js/display_any.js rename to web/display_any.js diff --git a/js/fast_actions_button.js b/web/fast_actions_button.js similarity index 100% rename from js/fast_actions_button.js rename to web/fast_actions_button.js diff --git a/web/html/icon_file_json.png b/web/html/icon_file_json.png new file mode 100644 index 0000000..ad3a1cb Binary files /dev/null and b/web/html/icon_file_json.png differ diff --git a/web/html/links.html b/web/html/links.html new file mode 100644 index 0000000..7ab1f4f --- /dev/null +++ b/web/html/links.html @@ -0,0 +1,122 @@ + + + + rgthree's comfy: Workflow Link Fixer + + + + +
+

rgthree's Workflow Link Fixer

+

Early versions of the reroute node would occasionally leave behind stale node-linking data in the graph, which could sometimes cause erratic workflow loading. This tool will look at the metadata and attempt to fix these errors.

+

Drag and drop a comfy-generated image or workflow json into this window to check its serialized links and fix.

+ +
+ + + +
+
+ +
+ + + +
+ + + + \ No newline at end of file diff --git a/web/html/links.js b/web/html/links.js new file mode 100644 index 0000000..6635346 --- /dev/null +++ b/web/html/links.js @@ -0,0 +1,432 @@ +import { getPngMetadata } from "/scripts/pnginfo.js"; +var IoDirection; +(function (IoDirection) { + IoDirection[IoDirection["INPUT"] = 0] = "INPUT"; + IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT"; +})(IoDirection || (IoDirection = {})); +function wait(ms = 16, value) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); +} +const logger = { + logTo: console, + log: (...args) => { + logger.logTo === console + ? console.log(...args) + : (logger.logTo.innerText += args.join(",") + "\n"); + }, +}; +const findBadLinksLogger = { + log: async (...args) => { + logger.log(...args); + }, +}; +class LinkPage { + constructor() { + this.containerEl = document.querySelector(".box"); + this.figcaptionEl = document.querySelector("figcaption"); + this.outputeMessageEl = document.querySelector(".output"); + this.outputImageEl = document.querySelector(".output-image"); + this.btnFix = document.querySelector(".btn-fix"); + document.addEventListener("dragover", (e) => { + e.preventDefault(); + }, false); + document.addEventListener("drop", (e) => { + this.onDrop(e); + }); + this.btnFix.addEventListener("click", (e) => { + this.onFixClick(e); + }); + } + async onFixClick(e) { + if (!this.graphResults || !this.graph) { + this.updateUi("⛔ Fix button click without results."); + return; + } + let graphFinalResults = await fixBadLinks(this.graph, true); + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + if (graphFinalResults.patched || graphFinalResults.deleted) { + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + } + if (graphFinalResults.patched || graphFinalResults.deleted) { + this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow."); + return; + } + this.graphFinalResults = graphFinalResults; + this.updateUi("✅ Workflow fixed."); + this.saveFixedWorkflow(); + } + async onDrop(event) { + var _a, _b, _c, _d; + if (!event.dataTransfer) { + return; + } + this.reset(); + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer.files.length && ((_b = (_a = event.dataTransfer.files) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) !== "image/bmp") { + await this.handleFile(event.dataTransfer.files[0]); + return; + } + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v)); + if (match) { + const uri = (_d = (_c = event.dataTransfer.getData(match)) === null || _c === void 0 ? void 0 : _c.split("\n")) === null || _d === void 0 ? void 0 : _d[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } + reset() { + this.file = undefined; + this.graph = undefined; + this.graphResults = undefined; + this.graphFinalResults = undefined; + this.updateUi(); + } + updateUi(msg) { + this.outputeMessageEl.innerHTML = ""; + if (this.file && !this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.add("-has-file"); + this.figcaptionEl.innerHTML = this.file.name || this.file.type; + if (this.file.type === "application/json") { + this.outputImageEl.src = "icon_file_json.png"; + } + else { + const reader = new FileReader(); + reader.onload = () => (this.outputImageEl.src = reader.result); + reader.readAsDataURL(this.file); + } + } + else if (!this.file && this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.remove("-has-file"); + this.outputImageEl.src = ""; + this.outputImageEl.removeAttribute("src"); + } + if (this.graphResults) { + this.containerEl.classList.add("-has-results"); + if (!this.graphResults.patched && !this.graphResults.deleted) { + this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; + } + else { + this.outputeMessageEl.innerHTML = `⚠️ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; + } + } + else { + this.containerEl.classList.remove("-has-results"); + } + if (msg) { + this.outputeMessageEl.innerHTML = msg; + } + } + async handleFile(file) { + this.file = file; + this.updateUi(); + let workflow = null; + if (file.type.startsWith("image/")) { + const pngInfo = await getPngMetadata(file); + workflow = pngInfo === null || pngInfo === void 0 ? void 0 : pngInfo.workflow; + } + else if (file.type === "application/json" || + (file instanceof File && file.name.endsWith(".json"))) { + workflow = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsText(file); + }); + } + if (!workflow) { + this.updateUi("⛔ No workflow found in dropped item."); + } + else { + try { + this.graph = JSON.parse(workflow); + } + catch (e) { + this.graph = undefined; + } + if (!this.graph) { + this.updateUi("⛔ Invalid workflow found in dropped item."); + } + else { + this.loadGraphData(this.graph); + } + } + } + async loadGraphData(graphData) { + this.graphResults = await fixBadLinks(graphData); + this.updateUi(); + } + async saveFixedWorkflow() { + if (!this.graphFinalResults) { + this.updateUi("⛔ Save w/o final graph patched."); + return; + } + let filename = this.file.name || 'workflow.json'; + let filenames = filename.split('.'); + filenames.pop(); + filename = filenames.join('.'); + filename += '_fixed.json'; + filename = prompt("Save workflow as:", filename); + if (!filename) + return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + const json = JSON.stringify(this.graphFinalResults.graph, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.download = filename; + anchor.href = url; + anchor.style.display = 'none'; + document.body.appendChild(anchor); + await wait(); + anchor.click(); + await wait(); + anchor.remove(); + window.URL.revokeObjectURL(url); + } +} +new LinkPage(); +function getNodeById(graph, id) { + return graph.nodes.find((n) => n.id === id); +} +function extendLink(link) { + 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], + }; +} +async function fixBadLinks(graph, fix = false) { + const patchedNodeSlots = {}; + const data = { + patchedNodes: [], + deletedLinks: [], + }; + async function patchNodeSlot(node, ioDir, slot, linkId, op) { + var _a, _b, _c; + patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; + const patchedNode = patchedNodeSlots[node.id]; + if (ioDir == IoDirection.INPUT) { + patchedNode["inputs"] = patchedNode["inputs"] || {}; + if (patchedNode["inputs"][slot] !== undefined) { + await findBadLinksLogger.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) { + } + } + else { + patchedNode["outputs"] = patchedNode["outputs"] || {}; + patchedNode["outputs"][slot] = patchedNode["outputs"][slot] || { + links: [...(((_b = (_a = node.outputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.links) || [])], + changes: {}, + }; + if (patchedNode["outputs"][slot]["changes"][linkId] !== undefined) { + await findBadLinksLogger.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) { + await findBadLinksLogger.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`); + return false; + } + patchedNode["outputs"][slot]["links"].push(linkId); + if (fix) { + (_c = node.outputs[slot].links) === null || _c === void 0 ? void 0 : _c.push(linkId); + } + } + else { + let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId); + if (linkIdIndex === -1) { + await findBadLinksLogger.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; + } + function nodeHasLinkId(node, ioDir, slot, linkId) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let has = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasIt = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) === linkId; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasIt = patchedNodeSlots[node.id]["inputs"][slot] === linkId; + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + else { + let nodeHasIt = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.includes(linkId); + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"][linkId]) { + let patchedHasIt = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.includes(linkId); + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = !!patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + return has; + } + function nodeHasAnyLink(node, ioDir, slot) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let hasAny = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasAny = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) != null; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasAny = patchedNodeSlots[node.id]["inputs"][slot] != null; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + else { + let nodeHasAny = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.length; + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"]) { + let patchedHasAny = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.length; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = !!patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + return hasAny; + } + const linksReverse = [...graph.links]; + linksReverse.reverse(); + for (let l of linksReverse) { + if (!l) + continue; + const link = extendLink(l); + const originNode = getNodeById(graph, link.origin_id); + const originHasLink = () => nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id); + const patchOrigin = (op, 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, 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) { + await findBadLinksLogger.log(`Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`); + } + else if (!originNode) { + await findBadLinksLogger.log(`Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`); + if (targetHasLink()) { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`); + patchTarget("REMOVE", -1); + } + } + else if (!targetNode) { + await findBadLinksLogger.log(`Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`); + if (originHasLink()) { + await findBadLinksLogger.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`); + patchOrigin("REMOVE"); + } + } + continue; + } + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + await findBadLinksLogger.log(`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`); + await findBadLinksLogger.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`); + patchOrigin("ADD"); + } + else if (!targetHasLink()) { + await findBadLinksLogger.log(`${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode.inputs[link.target_slot].link}), but ${originLog} contains it`); + if (!targetHasAnyLink()) { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`); + let patched = patchTarget("ADD"); + if (!patched) { + await findBadLinksLogger.log(` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`); + patched = patchOrigin("REMOVE"); + } + } + else { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`); + patchOrigin("REMOVE"); + } + } + } + } + for (let l of linksReverse) { + if (!l) + continue; + const link = extendLink(l); + const originNode = getNodeById(graph, link.origin_id); + const targetNode = getNodeById(graph, link.target_id); + if ((!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && + (!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))) { + await findBadLinksLogger.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 ${link.target_id ? "is removed" : `doesn\'t have ${link.id}`}.`); + data.deletedLinks.push(link.id); + continue; + } + } + if (fix) { + for (let i = data.deletedLinks.length - 1; i >= 0; i--) { + await findBadLinksLogger.log(`Deleting link #${data.deletedLinks[i]}.`); + const idx = graph.links.findIndex((l) => l[0] === data.deletedLinks[i]); + if (idx === -1) { + await findBadLinksLogger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`); + } + graph.links.splice(idx, 1); + } + graph.links = graph.links.filter((l) => !!l); + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + await findBadLinksLogger.log(`No bad links detected.`); + return { + fixed: false, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; + } + await findBadLinksLogger.log(`${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`); + return { + fixed: fix, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; +} diff --git a/js/image_inset_crop.js b/web/image_inset_crop.js similarity index 100% rename from js/image_inset_crop.js rename to web/image_inset_crop.js diff --git a/js/muter.js b/web/muter.js similarity index 100% rename from js/muter.js rename to web/muter.js diff --git a/js/node_collector.js b/web/node_collector.js similarity index 100% rename from js/node_collector.js rename to web/node_collector.js diff --git a/js/node_mode_relay.js b/web/node_mode_relay.js similarity index 100% rename from js/node_mode_relay.js rename to web/node_mode_relay.js diff --git a/js/node_mode_repeater.js b/web/node_mode_repeater.js similarity index 100% rename from js/node_mode_repeater.js rename to web/node_mode_repeater.js diff --git a/js/power_prompt.js b/web/power_prompt.js similarity index 100% rename from js/power_prompt.js rename to web/power_prompt.js diff --git a/js/reroute.js b/web/reroute.js similarity index 100% rename from js/reroute.js rename to web/reroute.js diff --git a/js/rgthree.js b/web/rgthree.js similarity index 100% rename from js/rgthree.js rename to web/rgthree.js diff --git a/js/seed.js b/web/seed.js similarity index 100% rename from js/seed.js rename to web/seed.js diff --git a/js/utils.js b/web/utils.js similarity index 100% rename from js/utils.js rename to web/utils.js