UNPKG

n8n-nodes-comfyui-image-to-image

Version:

n8n nodes to integrate with ComfyUI for image transformations, dual image processing, and image+video processing using stable diffusion workflows

453 lines 23 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComfyuiDualImageToImage = void 0; const n8n_workflow_1 = require("n8n-workflow"); const form_data_1 = __importDefault(require("form-data")); class ComfyuiDualImageToImage { constructor() { this.description = { displayName: 'ComfyUI Dual Image Transformer', name: 'comfyuiDualImageToImage', icon: 'file:comfyui.svg', group: ['transform'], version: 1, description: '🖼️+🖼️ Transform two input images into one output image using ComfyUI workflows (image blending, comparison, fusion)', defaults: { name: 'ComfyUI Dual Image Transformer', }, credentials: [ { name: 'comfyUIApi', required: true, }, ], inputs: ['main'], outputs: ['main'], properties: [ { displayName: 'Workflow JSON', name: 'workflow', type: 'string', typeOptions: { rows: 10, }, default: '', required: true, description: 'The ComfyUI workflow in JSON format (must contain two LoadImage nodes)', }, { displayName: 'First Image Input Type', name: 'firstInputType', type: 'options', options: [ { name: 'URL', value: 'url' }, { name: 'Base64', value: 'base64' }, { name: 'Binary', value: 'binary' } ], default: 'url', required: true, }, { displayName: 'First Input Image', name: 'firstInputImage', type: 'string', default: '', required: true, displayOptions: { show: { firstInputType: ['url', 'base64'], }, }, description: 'URL or base64 data of the first input image', }, { displayName: 'First Image Binary Property', name: 'firstBinaryPropertyName', type: 'string', default: 'data', required: true, displayOptions: { show: { firstInputType: ['binary'], }, }, description: 'Name of the binary property containing the first image', }, { displayName: 'Second Image Input Type', name: 'secondInputType', type: 'options', options: [ { name: 'URL', value: 'url' }, { name: 'Base64', value: 'base64' }, { name: 'Binary', value: 'binary' } ], default: 'url', required: true, }, { displayName: 'Second Input Image', name: 'secondInputImage', type: 'string', default: '', required: true, displayOptions: { show: { secondInputType: ['url', 'base64'], }, }, description: 'URL or base64 data of the second input image', }, { displayName: 'Second Image Binary Property', name: 'secondBinaryPropertyName', type: 'string', default: 'data2', required: true, displayOptions: { show: { secondInputType: ['binary'], }, }, description: 'Name of the binary property containing the second image', }, { displayName: 'First Image Node ID', name: 'firstImageNodeId', type: 'string', default: 'load_image_1', required: true, description: 'Node ID in workflow for the first LoadImage node', }, { displayName: 'Second Image Node ID', name: 'secondImageNodeId', type: 'string', default: 'load_image_2', required: true, description: 'Node ID in workflow for the second LoadImage node', }, { displayName: 'Timeout', name: 'timeout', type: 'number', default: 30, description: 'Maximum time in minutes to wait for image generation', }, ], }; } async execute() { var _a, _b, _c, _d; const credentials = await this.getCredentials('comfyUIApi'); const workflow = this.getNodeParameter('workflow', 0); const firstInputType = this.getNodeParameter('firstInputType', 0); const secondInputType = this.getNodeParameter('secondInputType', 0); const firstImageNodeId = this.getNodeParameter('firstImageNodeId', 0); const secondImageNodeId = this.getNodeParameter('secondImageNodeId', 0); const timeout = this.getNodeParameter('timeout', 0); const apiUrl = credentials.apiUrl; const apiKey = credentials.apiKey; console.log('[ComfyUI Dual] Executing dual image transformation with API URL:', apiUrl); const headers = { 'Content-Type': 'application/json', }; if (apiKey) { console.log('[ComfyUI Dual] Using API key authentication'); headers['Authorization'] = `Bearer ${apiKey}`; } try { console.log('[ComfyUI Dual] Checking API connection...'); await this.helpers.request({ method: 'GET', url: `${apiUrl}/system_stats`, headers, json: true, }); let firstImageBuffer; if (firstInputType === 'url') { const firstInputImage = this.getNodeParameter('firstInputImage', 0); console.log('[ComfyUI Dual] Downloading first image from URL:', firstInputImage); const response = await this.helpers.request({ method: 'GET', url: firstInputImage, encoding: null, }); firstImageBuffer = Buffer.from(response); } else if (firstInputType === 'binary') { console.log('[ComfyUI Dual] Getting first binary data from input'); const firstBinaryPropertyName = this.getNodeParameter('firstBinaryPropertyName', 0); console.log('[ComfyUI Dual] Looking for first binary property:', firstBinaryPropertyName); const items = this.getInputData(); const binaryProperties = Object.keys(items[0].binary || {}); console.log('[ComfyUI Dual] Available binary properties:', binaryProperties); let actualFirstPropertyName = firstBinaryPropertyName; if (!((_a = items[0].binary) === null || _a === void 0 ? void 0 : _a[firstBinaryPropertyName])) { console.log(`[ComfyUI Dual] Binary property "${firstBinaryPropertyName}" not found, searching for alternatives...`); const imageProperty = binaryProperties.find(key => { var _a; return (_a = items[0].binary[key].mimeType) === null || _a === void 0 ? void 0 : _a.startsWith('image/'); }); if (imageProperty) { console.log(`[ComfyUI Dual] Found alternative first image property: "${imageProperty}"`); actualFirstPropertyName = imageProperty; } else { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `No binary data found in property "${firstBinaryPropertyName}" and no image alternatives found for first image` }); } } firstImageBuffer = await this.helpers.getBinaryDataBuffer(0, actualFirstPropertyName); console.log('[ComfyUI Dual] Got first binary data, size:', firstImageBuffer.length, 'bytes'); const mimeType = items[0].binary[actualFirstPropertyName].mimeType; if (!mimeType || !mimeType.startsWith('image/')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Invalid media type for first image: ${mimeType}. Only images are supported.` }); } } else { const firstInputImage = this.getNodeParameter('firstInputImage', 0); firstImageBuffer = Buffer.from(firstInputImage, 'base64'); } let secondImageBuffer; if (secondInputType === 'url') { const secondInputImage = this.getNodeParameter('secondInputImage', 0); console.log('[ComfyUI Dual] Downloading second image from URL:', secondInputImage); const response = await this.helpers.request({ method: 'GET', url: secondInputImage, encoding: null, }); secondImageBuffer = Buffer.from(response); } else if (secondInputType === 'binary') { console.log('[ComfyUI Dual] Getting second binary data from input'); const secondBinaryPropertyName = this.getNodeParameter('secondBinaryPropertyName', 0); console.log('[ComfyUI Dual] Looking for second binary property:', secondBinaryPropertyName); const items = this.getInputData(); const binaryProperties = Object.keys(items[0].binary || {}); let actualSecondPropertyName = secondBinaryPropertyName; if (!((_b = items[0].binary) === null || _b === void 0 ? void 0 : _b[secondBinaryPropertyName])) { console.log(`[ComfyUI Dual] Binary property "${secondBinaryPropertyName}" not found, searching for alternatives...`); const imageProperty = binaryProperties.find(key => { var _a; return ((_a = items[0].binary[key].mimeType) === null || _a === void 0 ? void 0 : _a.startsWith('image/')) && key !== (firstInputType === 'binary' ? this.getNodeParameter('firstBinaryPropertyName', 0) : ''); }); if (imageProperty) { console.log(`[ComfyUI Dual] Found alternative second image property: "${imageProperty}"`); actualSecondPropertyName = imageProperty; } else { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `No binary data found in property "${secondBinaryPropertyName}" and no image alternatives found for second image` }); } } secondImageBuffer = await this.helpers.getBinaryDataBuffer(0, actualSecondPropertyName); console.log('[ComfyUI Dual] Got second binary data, size:', secondImageBuffer.length, 'bytes'); const mimeType = items[0].binary[actualSecondPropertyName].mimeType; if (!mimeType || !mimeType.startsWith('image/')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Invalid media type for second image: ${mimeType}. Only images are supported.` }); } } else { const secondInputImage = this.getNodeParameter('secondInputImage', 0); secondImageBuffer = Buffer.from(secondInputImage, 'base64'); } console.log('[ComfyUI Dual] Uploading first image...'); const firstFormData = new form_data_1.default(); firstFormData.append('image', firstImageBuffer, 'first_input.png'); firstFormData.append('subfolder', ''); firstFormData.append('overwrite', 'true'); const firstUploadResponse = await this.helpers.request({ method: 'POST', url: `${apiUrl}/upload/image`, headers: { ...headers, ...firstFormData.getHeaders(), }, body: firstFormData, }); const firstImageInfo = JSON.parse(firstUploadResponse); console.log('[ComfyUI Dual] First image uploaded:', firstImageInfo); console.log('[ComfyUI Dual] Uploading second image...'); const secondFormData = new form_data_1.default(); secondFormData.append('image', secondImageBuffer, 'second_input.png'); secondFormData.append('subfolder', ''); secondFormData.append('overwrite', 'true'); const secondUploadResponse = await this.helpers.request({ method: 'POST', url: `${apiUrl}/upload/image`, headers: { ...headers, ...secondFormData.getHeaders(), }, body: secondFormData, }); const secondImageInfo = JSON.parse(secondUploadResponse); console.log('[ComfyUI Dual] Second image uploaded:', secondImageInfo); let workflowData; try { workflowData = JSON.parse(workflow); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid workflow JSON. Please check the JSON syntax and try again.', description: error.message }); } if (typeof workflowData !== 'object' || workflowData === null) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid workflow structure. The workflow must be a valid JSON object.' }); } if (workflowData[firstImageNodeId]) { if (workflowData[firstImageNodeId].class_type === 'LoadImage') { workflowData[firstImageNodeId].inputs.image = firstImageInfo.name; console.log(`[ComfyUI Dual] Updated first LoadImage node "${firstImageNodeId}" with image: ${firstImageInfo.name}`); } else { console.warn(`[ComfyUI Dual] Node "${firstImageNodeId}" is not a LoadImage node`); } } else { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `First LoadImage node with ID "${firstImageNodeId}" not found in workflow` }); } if (workflowData[secondImageNodeId]) { if (workflowData[secondImageNodeId].class_type === 'LoadImage') { workflowData[secondImageNodeId].inputs.image = secondImageInfo.name; console.log(`[ComfyUI Dual] Updated second LoadImage node "${secondImageNodeId}" with image: ${secondImageInfo.name}`); } else { console.warn(`[ComfyUI Dual] Node "${secondImageNodeId}" is not a LoadImage node`); } } else { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Second LoadImage node with ID "${secondImageNodeId}" not found in workflow` }); } console.log('[ComfyUI Dual] LoadImage nodes updated via specific node IDs'); console.log('[ComfyUI Dual] Queueing image generation...'); const response = await this.helpers.request({ method: 'POST', url: `${apiUrl}/prompt`, headers, body: { prompt: workflowData, }, json: true, }); if (!response.prompt_id) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Failed to get prompt ID from ComfyUI' }); } const promptId = response.prompt_id; console.log('[ComfyUI Dual] Image generation queued with ID:', promptId); let attempts = 0; const maxAttempts = 60 * timeout; await new Promise(resolve => setTimeout(resolve, 5000)); while (attempts < maxAttempts) { console.log(`[ComfyUI Dual] Checking image generation status (attempt ${attempts + 1}/${maxAttempts})...`); await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; const history = await this.helpers.request({ method: 'GET', url: `${apiUrl}/history/${promptId}`, headers, json: true, }); const promptResult = history[promptId]; if (!promptResult) { console.log('[ComfyUI Dual] Prompt not found in history'); continue; } if (promptResult.status === undefined) { console.log('[ComfyUI Dual] Execution status not found'); continue; } if ((_c = promptResult.status) === null || _c === void 0 ? void 0 : _c.completed) { console.log('[ComfyUI Dual] Image generation completed'); if (((_d = promptResult.status) === null || _d === void 0 ? void 0 : _d.status_str) === 'error') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI Dual] Image generation failed' }); } console.log('[ComfyUI Dual] Raw outputs structure:', JSON.stringify(promptResult.outputs, null, 2)); const imageOutputs = Object.values(promptResult.outputs) .flatMap((nodeOutput) => nodeOutput.images || []) .filter((image) => image.type === 'output' || image.type === 'temp') .map((img) => ({ ...img, url: `${apiUrl}/view?filename=${img.filename}&subfolder=${img.subfolder || ''}&type=${img.type}` })); console.log('[ComfyUI Dual] Found image outputs:', imageOutputs); if (imageOutputs.length === 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI Dual] No image outputs found in results' }); } const imageOutput = imageOutputs[0]; const imageResponse = await this.helpers.request({ method: 'GET', url: imageOutput.url, encoding: null, resolveWithFullResponse: true }); if (imageResponse.statusCode === 404) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Image file not found at ${imageOutput.url}` }); } console.log('[ComfyUI Dual] Using media directly from ComfyUI'); const buffer = Buffer.from(imageResponse.body); const base64Data = buffer.toString('base64'); const fileSize = Math.round(buffer.length / 1024 * 10) / 10 + " kB"; let mimeType = 'image/png'; let fileExtension = 'png'; if (imageOutput.filename.endsWith('.jpg') || imageOutput.filename.endsWith('.jpeg')) { mimeType = 'image/jpeg'; fileExtension = imageOutput.filename.endsWith('.jpg') ? 'jpg' : 'jpeg'; } else if (imageOutput.filename.endsWith('.webp')) { mimeType = 'image/webp'; fileExtension = 'webp'; } return [[{ json: { mimeType, fileName: imageOutput.filename, data: base64Data, status: promptResult.status, inputImages: { first: firstImageInfo.name, second: secondImageInfo.name } }, binary: { data: { fileName: imageOutput.filename, data: base64Data, fileType: 'image', fileSize, fileExtension, mimeType } } }]]; } } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Image generation timeout after ${timeout} minutes` }); } catch (error) { console.error('[ComfyUI Dual] Image generation error:', error); throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `ComfyUI Dual API Error: ${error.message}`, description: error.description || '' }); } } } exports.ComfyuiDualImageToImage = ComfyuiDualImageToImage; //# sourceMappingURL=ComfyuiDualImageToImage.node.js.map