From ab4783c2775a75d8fe9f366cb0fd8df1a17f3386 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:15:12 -0400 Subject: [PATCH] Adds a link fixer html --- __dev__.py | 16 +- __init__.py | 29 +- ts/html/links.ts | 616 +++++++++++++++++++ tsconfig.json | 4 +- {js => web}/base_any_input_connected_node.js | 0 {js => web}/base_node.js | 0 {js => web}/base_node_collector.js | 0 {js => web}/base_node_mode_changer.js | 0 {js => web}/base_power_prompt.js | 0 {js => web}/bypasser.js | 0 {js => web}/constants.js | 0 {js => web}/context.js | 0 {js => web}/display_any.js | 0 {js => web}/fast_actions_button.js | 0 web/html/icon_file_json.png | Bin 0 -> 7213 bytes web/html/links.html | 122 ++++ web/html/links.js | 432 +++++++++++++ {js => web}/image_inset_crop.js | 0 {js => web}/muter.js | 0 {js => web}/node_collector.js | 0 {js => web}/node_mode_relay.js | 0 {js => web}/node_mode_repeater.js | 0 {js => web}/power_prompt.js | 0 {js => web}/reroute.js | 0 {js => web}/rgthree.js | 0 {js => web}/seed.js | 0 {js => web}/utils.js | 0 27 files changed, 1201 insertions(+), 18 deletions(-) create mode 100644 ts/html/links.ts rename {js => web}/base_any_input_connected_node.js (100%) rename {js => web}/base_node.js (100%) rename {js => web}/base_node_collector.js (100%) rename {js => web}/base_node_mode_changer.js (100%) rename {js => web}/base_power_prompt.js (100%) rename {js => web}/bypasser.js (100%) rename {js => web}/constants.js (100%) rename {js => web}/context.js (100%) rename {js => web}/display_any.js (100%) rename {js => web}/fast_actions_button.js (100%) create mode 100644 web/html/icon_file_json.png create mode 100644 web/html/links.html create mode 100644 web/html/links.js rename {js => web}/image_inset_crop.js (100%) rename {js => web}/muter.js (100%) rename {js => web}/node_collector.js (100%) rename {js => web}/node_mode_relay.js (100%) rename {js => web}/node_mode_repeater.js (100%) rename {js => web}/power_prompt.js (100%) rename {js => web}/reroute.js (100%) rename {js => web}/rgthree.js (100%) rename {js => web}/seed.js (100%) rename {js => web}/utils.js (100%) 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 0000000000000000000000000000000000000000..ad3a1cb2b89a2051010d53d69b12cca8735af353 GIT binary patch literal 7213 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 literal 0 HcmV?d00001 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