Files
siraxe-ComfyUI-WanVideoWrap…/web/power_spline_editor/graph_query.js
T
2025-10-13 04:52:07 +01:00

646 lines
28 KiB
JavaScript

/**
* Graph Query Utilities for ComfyUI
* Allows querying node connections and extracting image data from connected nodes
*/
import { app } from '../../../scripts/app.js';
/**
* Find the source node connected to a specific input of the current node
* @param {Object} currentNode - The current node object
* @param {string} inputName - Name of the input to find connection for (e.g., "ref_image")
* @returns {Object|null} The source node connected to the input, or null if not connected
*/
function findConnectedSourceNode(currentNode, inputName) {
try {
console.log('Looking for connections for node:', {
id: currentNode.id,
type: currentNode.type,
inputs: currentNode.inputs
});
// Look for connections where this node is the target
const graph = app.graph;
if (!graph || !graph.links) {
console.error('ComfyUI graph or links not available');
return null;
}
// Debug: log all links in the graph
console.log('All graph links:', graph.links);
// Find input index for the given input name
let inputIndex = -1;
let allInputNames = [];
if (currentNode.inputs) {
console.log('Node inputs found:', currentNode.inputs);
for (let i = 0; i < currentNode.inputs.length; i++) {
const input = currentNode.inputs[i];
if (input && input.name) {
allInputNames.push(input.name);
if (input.name === inputName) {
inputIndex = i;
break;
}
}
}
} else {
console.log('No inputs property found on current node');
// Try to get inputs from the node configuration
if (currentNode.constructor?.nodeData?.input) {
const allInputs = {
...currentNode.constructor?.nodeData?.input?.required,
...currentNode.constructor?.nodeData?.input?.optional
};
// Find which position the input is in
let index = 0;
for (const [inputNameDef] of Object.entries(allInputs)) {
allInputNames.push(inputNameDef);
if (inputNameDef === inputName) {
inputIndex = index;
break;
}
index++;
}
}
}
console.log(`All input names:`, allInputNames);
console.log(`Looking for input "${inputName}", found index:`, inputIndex);
console.log(`Input "${inputName}" index:`, inputIndex);
if (inputIndex === -1) {
console.log(`Input "${inputName}" not found on node ${currentNode.type}`);
return null;
}
// Find the link connected to this input (graph.links is a Map, not an array)
let link = null;
if (graph.links && graph.links instanceof Map) {
for (const [linkId, linkObj] of graph.links) {
if (!linkObj) continue;
console.log('Checking link:', {
id: linkId,
origin_id: linkObj.origin_id,
origin_slot: linkObj.origin_slot,
target_id: linkObj.target_id,
target_slot: linkObj.target_slot
});
if (linkObj.target_id === currentNode.id && linkObj.target_slot === inputIndex) {
link = linkObj;
break;
}
}
} else if (Array.isArray(graph.links)) {
// Fallback if links is an array (older format)
link = graph.links.find(linkObj => {
if (!linkObj) return false;
console.log('Checking link:', {
origin_id: linkObj.origin_id,
origin_slot: linkObj.origin_slot,
target_id: linkObj.target_id,
target_slot: linkObj.target_slot
});
return linkObj.target_id === currentNode.id && linkObj.target_slot === inputIndex;
});
} else {
console.error('Graph links is not a Map or Array:', typeof graph.links);
return null;
}
if (!link) {
console.log(`No direct connection found for input "${inputName}" (index: ${inputIndex}) on node ${currentNode.id}`);
// Debug: Check what inputs are connected - iterate through the Map
const connectedLinks = [];
if (graph.links instanceof Map) {
for (const [linkId, linkObj] of graph.links) {
if (linkObj && linkObj.target_id === currentNode.id) {
connectedLinks.push(linkObj);
}
}
} else if (Array.isArray(graph.links)) {
// Fallback for array format
for (const linkObj of graph.links) {
if (linkObj && linkObj.target_id === currentNode.id) {
connectedLinks.push(linkObj);
}
}
}
console.log(`All connected links to this node:`, connectedLinks);
// Alternative approach: try to find any connection that might be the ref_image
if (connectedLinks.length > 0) {
console.log('Found connected links, attempting to match by type or position...');
// If we find connected links, try to match based on common patterns
// ref_image is typically an IMAGE type input
for (const connectedLink of connectedLinks) {
const sourceNode = graph._nodes?.find(node => node.id === connectedLink.origin_id);
if (sourceNode) {
// Check if this is likely an image source by checking the source's output type
const sourceOutput = sourceNode.outputs?.[connectedLink.origin_slot];
if (sourceOutput && (sourceOutput.type === 'IMAGE' || (sourceOutput.name && sourceOutput.name.includes('image')))) {
console.log(`Found possible image source at slot ${connectedLink.target_slot} from node ${sourceNode.type}`);
// Return this connection as a possibility
return {
node: sourceNode,
origin_slot: connectedLink.origin_slot,
target_slot: connectedLink.target_slot
};
}
}
}
// If we still can't find it by output type, just return the first connection as a fallback
// as it might be connected but the type matching isn't working
const firstConnectedLink = connectedLinks[0];
const firstSourceNode = graph._nodes?.find(node => node.id === firstConnectedLink.origin_id);
if (firstSourceNode) {
console.log(`Using fallback: found connection from node ${firstSourceNode.type} at slot ${firstConnectedLink.target_slot}`);
return {
node: firstSourceNode,
origin_slot: firstConnectedLink.origin_slot,
target_slot: firstConnectedLink.target_slot
};
}
}
return null;
} else {
// The direct match was successful
// Find the source node based on the link
const sourceNode = graph._nodes?.find(node => node.id === link.origin_id);
if (!sourceNode) {
console.error(`Source node with id ${link.origin_id} not found`);
return null;
}
console.log(`Found connected source node:`, {
sourceNodeId: sourceNode.id,
sourceNodeType: sourceNode.type,
sourceNodeWidgetValues: sourceNode.widgets ? sourceNode.widgets.map(w => ({name: w.name, type: w.type, value: w.value})) : [],
origin_slot: link.origin_slot,
target_slot: link.target_slot
});
// If the directly connected source node is not an image node, try the deep search approach
if (!isImageNode(sourceNode)) {
console.log('Direct source node is not an image node, trying deep search...');
const deepResult = findDeepSourceNode(currentNode, inputName);
if (deepResult) {
console.log('Deep search found an image node:', {
sourceNodeId: deepResult.node.id,
sourceNodeType: deepResult.node.type
});
return deepResult;
}
}
return {
node: sourceNode,
origin_slot: link.origin_slot,
target_slot: link.target_slot
};
}
// Find the source node based on the link
const sourceNode = graph._nodes?.find(node => node.id === link.origin_id);
if (!sourceNode) {
console.error(`Source node with id ${link.origin_id} not found`);
return null;
}
console.log(`Found connected source node:`, {
sourceNodeId: sourceNode.id,
sourceNodeType: sourceNode.type,
sourceNodeWidgetValues: sourceNode.widgets ? sourceNode.widgets.map(w => ({name: w.name, type: w.type, value: w.value})) : [],
origin_slot: link.origin_slot,
target_slot: link.target_slot
});
return {
node: sourceNode,
origin_slot: link.origin_slot,
target_slot: link.target_slot
};
} catch (error) {
console.error(`Error finding connected source node for "${inputName}":`, error);
console.error(`Stack:`, error.stack);
return null;
}
}
/**
* Extract image data from a source node that may contain image previews
* @param {Object} sourceNodeObj - Object containing the source node and connection info
* @returns {string|null} Base64 image data or URL, or null if no image found
*/
async function extractImageFromSourceNode(sourceNodeObj) {
if (!sourceNodeObj || !sourceNodeObj.node) {
console.error('Invalid source node object');
return null;
}
const sourceNode = sourceNodeObj.node;
console.log(`Attempting to extract image from source node:`, {
id: sourceNode.id,
type: sourceNode.type,
widgets: sourceNode.widgets ? sourceNode.widgets.map(w => ({name: w.name, type: w.type})) : [],
properties: sourceNode.properties
});
try {
// Different node types store image data differently
switch (sourceNode.type) {
case 'LoadImage':
case 'LoadImageUpload':
// These nodes typically store image info in widgets or properties
if (sourceNode.widgets) {
for (const widget of sourceNode.widgets) {
if (widget.name === 'image' && widget.value) {
console.log('Found image in LoadImage widget:', widget.value);
// Handle different formats that LoadImage might use
if (typeof widget.value === 'string') {
// If it's already a URL or data URL, return it
if (widget.value.startsWith('data:') || widget.value.startsWith('http')) {
return widget.value;
}
// If it's a filename, try to construct a full URL
return `/view?filename=${encodeURIComponent(widget.value)}&type=input`;
} else if (typeof widget.value === 'object' && widget.value.filename) {
// Object format with filename property
return `/view?filename=${encodeURIComponent(widget.value.filename)}&type=input`;
}
}
}
}
// Check properties as fallback
if (sourceNode.properties?.value) {
const propValue = sourceNode.properties.value;
if (typeof propValue === 'string') {
if (propValue.startsWith('data:') || propValue.startsWith('http')) {
return propValue;
}
return `/view?filename=${encodeURIComponent(propValue)}&type=input`;
}
}
break;
case 'VAEDecode':
case 'PreviewImage':
case 'SaveImage':
// These nodes display images on the canvas as image widgets
if (sourceNode.widgets) {
for (const widget of sourceNode.widgets) {
// Look for image widgets that contain image data
if (widget.type === 'image') {
if (widget.value) {
if (typeof widget.value === 'string') {
if (widget.value.startsWith('data:') || widget.value.startsWith('http')) {
console.log('Found image in PreviewImage widget:', widget.value);
return widget.value;
}
return `/view?filename=${encodeURIComponent(widget.value)}&type=temp`;
} else if (typeof widget.value === 'object' && widget.value.filename) {
console.log('Found image object in PreviewImage widget:', widget.value);
return `/view?filename=${encodeURIComponent(widget.value.filename)}&type=temp`;
}
}
}
}
}
// Look for DOM elements that might contain image data
// Find image elements in the node's DOM representation
const nodeElements = document.querySelectorAll(`.graphcanvas .node[data-node-id="${sourceNode.id}"] img`);
if (nodeElements.length > 0) {
const imgElement = nodeElements[0];
const src = imgElement.src;
if (src && (src.startsWith('data:') || src.startsWith('http'))) {
console.log('Found image in DOM element:', src);
return src;
}
}
break;
case 'ImageResizeKJv2': // Handle image processing nodes
case 'ImageScale':
case 'ImageScaleBy':
case 'ImageUpscale':
case 'ImageCrop':
// These nodes might pass through image data from their inputs
// Look for image widgets that might contain processed image data
if (sourceNode.widgets) {
for (const widget of sourceNode.widgets) {
if ((widget.type === 'image' || widget.name?.includes('image')) && widget.value) {
console.log(`Found image in ${sourceNode.type} widget:`, widget.value);
if (typeof widget.value === 'string') {
if (widget.value.startsWith('data:') || widget.value.startsWith('http')) {
return widget.value;
}
return `/view?filename=${encodeURIComponent(widget.value)}&type=temp`;
} else if (typeof widget.value === 'object') {
if (widget.value.filename) {
return `/view?filename=${encodeURIComponent(widget.value.filename)}&type=temp`;
}
}
}
}
}
// Even if no direct image found in widgets, the node might still be connected
// to image data that can be accessed through other means
break;
default:
// For other node types, try to find image-related widgets or properties
if (sourceNode.widgets) {
for (const widget of sourceNode.widgets) {
if ((widget.type === 'image' || widget.name?.includes('image') ||
widget.type === 'img' || widget.type === 'file') && widget.value) {
console.log(`Found potential image in ${sourceNode.type} widget:`, widget.value);
if (typeof widget.value === 'string') {
if (widget.value.startsWith('data:') || widget.value.startsWith('http')) {
return widget.value;
}
// Try to construct a URL for ComfyUI image endpoints
return `/view?filename=${encodeURIComponent(widget.value)}&type=temp`;
} else if (typeof widget.value === 'object') {
if (widget.value.filename) {
return `/view?filename=${encodeURIComponent(widget.value.filename)}&type=temp`;
}
}
}
}
}
break;
}
// If no image found in widgets, try to look for image elements in the DOM
// This is a more general approach for nodes that display images directly
const canvasContainer = document.querySelector('.graphcanvas') || document.querySelector('#graph-canvas') || document.querySelector('.comfyui-body');
if (canvasContainer) {
// Look for any image associated with the source node
const nodeElement = canvasContainer.querySelector(`[data-node-id="${sourceNode.id}"]`);
if (nodeElement) {
const imgElements = nodeElement.querySelectorAll('img');
for (const img of imgElements) {
if (img.src && (img.src.startsWith('data:') || img.src.startsWith('http'))) {
console.log('Found image in node DOM element:', img.src);
return img.src;
}
}
}
}
// Debug: log all available information about the source node
console.log(`Debug: No image found in source node ${sourceNode.type}. Available data:`, {
id: sourceNode.id,
type: sourceNode.type,
widgets: sourceNode.widgets?.map(w => ({name: w.name, type: w.type, value: typeof w.value === 'object' ? JSON.stringify(w.value) : w.value})),
properties: sourceNode.properties,
outputs: sourceNode.outputs
});
return null;
} catch (error) {
console.error(`Error extracting image from source node ${sourceNode.type}:`, error);
return null;
}
}
/**
* Load image from URL as base64
* @param {string} imageUrl - URL of the image to load
* @returns {Promise<string|null>} Promise that resolves to base64 image data or null
*/
async function loadImageAsBase64(imageUrl) {
try {
if (!imageUrl) {
return null;
}
// Handle data URLs directly
if (imageUrl.startsWith('data:')) {
return imageUrl;
}
// Add cache-busting parameter to ensure fresh image
let urlToUse = imageUrl;
if (imageUrl.includes('?')) {
urlToUse = `${imageUrl}&t=${Date.now()}`;
} else {
urlToUse = `${imageUrl}?t=${Date.now()}`;
}
const response = await fetch(urlToUse);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Error loading image as base64:', error);
return null;
}
}
/**
* Main function to get reference image from connected node
* @param {Object} currentNode - The Power Spline Editor node
* @returns {Promise<string|null>} Promise that resolves to base64 image data or null
*/
async function getReferenceImageFromConnectedNode(currentNode) {
console.log('Starting reference image query for node:', currentNode);
// Step 1: Find the connected source node for ref_image input
let sourceNodeObj = findConnectedSourceNode(currentNode, 'ref_image');
if (!sourceNodeObj) {
console.log('No source node found for ref_image input, trying deep search...');
// Try deep search as fallback
sourceNodeObj = findDeepSourceNode(currentNode, 'ref_image');
if (sourceNodeObj) {
console.log('Deep search found a potential image source node:', {
sourceNodeId: sourceNodeObj.node.id,
sourceNodeType: sourceNodeObj.node.type
});
}
}
if (!sourceNodeObj) {
console.log('No source node found for ref_image input after deep search');
return null;
}
// Step 2: Extract image data from the source node
let imageDataUrl = await extractImageFromSourceNode(sourceNodeObj);
if (!imageDataUrl) {
console.log('No image data found in source node, trying deep search for original image source...');
// If we couldn't extract image from the found source node, try to find the original image source via deep search
const deepSourceNodeObj = findDeepSourceNode(currentNode, 'ref_image');
if (deepSourceNodeObj && deepSourceNodeObj.node.id !== sourceNodeObj.node.id) {
console.log('Deep search found alternative source node:', {
sourceNodeId: deepSourceNodeObj.node.id,
sourceNodeType: deepSourceNodeObj.node.type
});
imageDataUrl = await extractImageFromSourceNode(deepSourceNodeObj);
if (imageDataUrl) {
sourceNodeObj = deepSourceNodeObj; // Update the source node for potential logging
}
}
}
if (!imageDataUrl) {
console.log('No image data found in source node after trying alternatives');
return null;
}
// Step 3: Load the image as base64
const base64Image = await loadImageAsBase64(imageDataUrl);
if (!base64Image) {
console.error('Failed to load image as base64');
return null;
}
console.log('Successfully retrieved reference image from connected node');
return base64Image;
}
/**
* Recursively find the original source node connected to a specific input
* @param {Object} currentNode - The node to start from
* @param {string} inputName - Name of the input to trace (e.g., "ref_image")
* @param {Set<number>} visited - Internal set to prevent infinite loops
* @returns {Object|null} The ultimate source node or null
*/
function findDeepSourceNode(currentNode, inputName, visited = new Set()) {
const graph = app.graph;
if (!graph || !graph.links) return null;
if (visited.has(currentNode.id)) return null;
visited.add(currentNode.id);
// Get the link object for the input
const inputIndex = currentNode.inputs?.findIndex(i => i.name === inputName) ?? -1;
if (inputIndex < 0) return null;
let link = null;
// Find the link connected to this input (graph.links is a Map, not an array)
if (graph.links && graph.links instanceof Map) {
for (const [linkId, linkObj] of graph.links) {
if (!linkObj) continue;
if (linkObj.target_id === currentNode.id && linkObj.target_slot === inputIndex) {
link = linkObj;
break;
}
}
} else if (Array.isArray(graph.links)) {
// Fallback if links is an array (older format)
link = graph.links.find(linkObj => {
if (!linkObj) return false;
return linkObj.target_id === currentNode.id && linkObj.target_slot === inputIndex;
});
}
if (!link) return null;
const sourceNode = graph._nodes?.find(node => node.id === link.origin_id);
if (!sourceNode) return null;
// Check if the source node contains image data
if (isImageNode(sourceNode)) {
// Prioritize true image loaders over processors
const imageLoaderTypes = ['LoadImage', 'LoadImageUpload'];
if (imageLoaderTypes.some(type => sourceNode.type?.includes(type))) {
return {
node: sourceNode,
origin_slot: link.origin_slot,
target_slot: link.target_slot
};
}
// If this is an image processor, continue looking upstream for the original source
if (sourceNode.inputs) {
for (const input of sourceNode.inputs) {
const deeper = findDeepSourceNode(sourceNode, input.name, new Set(visited)); // Use a copy of visited set for each branch
if (deeper) {
return deeper; // Return the original source found upstream
}
}
}
// If no original source found upstream, return this processor as fallback
return {
node: sourceNode,
origin_slot: link.origin_slot,
target_slot: link.target_slot
};
}
// If not an image node, recursively check each input of the source node for possible image connections
if (sourceNode.inputs) {
for (const input of sourceNode.inputs) {
const deeper = findDeepSourceNode(sourceNode, input.name, new Set(visited)); // Use a copy of visited set for each branch
if (deeper) return deeper;
}
}
return null;
}
/**
* Check if a node is likely to contain image data
* @param {Object} node - The node to check
* @returns {boolean} Whether the node is likely to contain image data
*/
function isImageNode(node) {
// Check node type for common image-related nodes
const imageNodeTypes = [
'LoadImage', 'LoadImageUpload', 'VAEDecode', 'PreviewImage', 'SaveImage',
'ImageScale', 'ImageScaleBy', 'ImageResizeKJv2', 'ImageUpscale', 'ImageCrop',
'ImagePadForOutpaint', 'ImageBatch', 'ImageBlend', 'ImageBlur', 'ImageColorToBW',
'ImageFlip', 'ImageOnlyCheckpointLoader', 'ImageApplyProcessing'
];
if (imageNodeTypes.some(type => node.type && node.type.includes(type))) {
return true;
}
// Check if node has IMAGE type outputs
if (node.outputs) {
for (const output of node.outputs) {
if (output.type === 'IMAGE' || (output.name && (output.name.includes('IMAGE') || output.name.toLowerCase().includes('image')))) {
return true;
}
}
}
// Check if node has image-related widgets
if (node.widgets) {
for (const widget of node.widgets) {
if (widget.type === 'image' || (widget.name && widget.name.toLowerCase().includes('image'))) {
return true;
}
}
}
return false;
}
// Export functions for use in other modules
export { findConnectedSourceNode, findDeepSourceNode, extractImageFromSourceNode, loadImageAsBase64, getReferenceImageFromConnectedNode };