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
JavaScript
;
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