n8n-nodes-comfyui-image-to-image
Version:
n8n nodes to integrate with ComfyUI for image transformations, dual image processing, image+video processing, and text-to-video generation using stable diffusion workflows
486 lines • 25.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComfyuiDualImageVideoGenerator = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const form_data_1 = __importDefault(require("form-data"));
class ComfyuiDualImageVideoGenerator {
constructor() {
this.description = {
displayName: 'ComfyUI Dual Image Video Generator',
name: 'comfyuiDualImageVideoGenerator',
icon: 'file:comfyui.svg',
group: ['transform'],
version: 1,
description: '🖼️🖼️➡️🎬 Generate video from two image inputs using ComfyUI workflows (AnimateDiff, motion generation, interpolation)',
defaults: {
name: 'ComfyUI Dual Image Video Generator',
},
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 for video generation)',
},
{
displayName: 'First Image Input Type',
name: 'firstImageInputType',
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: {
firstImageInputType: ['url', 'base64'],
},
},
description: 'URL or base64 data of the first input image',
},
{
displayName: 'First Image Binary Property',
name: 'firstImageBinaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
firstImageInputType: ['binary'],
},
},
description: 'Name of the binary property containing the first image',
},
{
displayName: 'Second Image Input Type',
name: 'secondImageInputType',
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: {
secondImageInputType: ['url', 'base64'],
},
},
description: 'URL or base64 data of the second input image',
},
{
displayName: 'Second Image Binary Property',
name: 'secondImageBinaryPropertyName',
type: 'string',
default: 'data2',
required: true,
displayOptions: {
show: {
secondImageInputType: ['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: 'Video Frame Count',
name: 'frameCount',
type: 'number',
default: 16,
description: 'Number of frames to generate for the video',
},
{
displayName: 'Video Frame Rate',
name: 'frameRate',
type: 'number',
default: 8,
description: 'Frame rate for the output video',
},
{
displayName: 'Timeout',
name: 'timeout',
type: 'number',
default: 60,
description: 'Maximum time in minutes to wait for video generation',
},
],
};
}
async execute() {
var _a, _b;
const credentials = await this.getCredentials('comfyUIApi');
const workflow = this.getNodeParameter('workflow', 0);
const firstImageInputType = this.getNodeParameter('firstImageInputType', 0);
const secondImageInputType = this.getNodeParameter('secondImageInputType', 0);
const firstImageNodeId = this.getNodeParameter('firstImageNodeId', 0);
const secondImageNodeId = this.getNodeParameter('secondImageNodeId', 0);
const frameCount = this.getNodeParameter('frameCount', 0);
const frameRate = this.getNodeParameter('frameRate', 0);
const timeout = this.getNodeParameter('timeout', 0);
const apiUrl = credentials.apiUrl;
const apiKey = credentials.apiKey;
console.log('[ComfyUI DualImageVideo] Executing dual image-to-video generation with API URL:', apiUrl);
const headers = {
'Content-Type': 'application/json',
};
if (apiKey) {
console.log('[ComfyUI DualImageVideo] Using API key authentication');
headers['Authorization'] = `Bearer ${apiKey}`;
}
try {
console.log('[ComfyUI DualImageVideo] Checking API connection...');
await this.helpers.request({
method: 'GET',
url: `${apiUrl}/system_stats`,
headers,
json: true,
});
const getImageBuffer = async (inputType, imageParam, binaryParam, imageIndex = 0) => {
var _a, _b;
if (inputType === 'url') {
const imageUrl = this.getNodeParameter(imageParam, 0);
console.log(`[ComfyUI DualImageVideo] Downloading image ${imageIndex + 1} from URL: ${imageUrl}`);
const response = await this.helpers.request({
method: 'GET',
url: imageUrl,
encoding: null,
});
return Buffer.from(response);
}
else if (inputType === 'binary') {
console.log(`[ComfyUI DualImageVideo] Getting binary data for image ${imageIndex + 1}`);
const binaryPropertyName = this.getNodeParameter(binaryParam, 0);
const items = this.getInputData();
const binaryProperties = Object.keys(items[0].binary || {});
let actualPropertyName = binaryPropertyName;
let targetItem = items[0];
if (imageIndex === 1 && items.length > 1) {
targetItem = items[1];
console.log(`[ComfyUI DualImageVideo] Using input item ${imageIndex + 1} for second image`);
}
if (!((_a = targetItem.binary) === null || _a === void 0 ? void 0 : _a[binaryPropertyName])) {
console.log(`[ComfyUI DualImageVideo] Binary property "${binaryPropertyName}" not found for image ${imageIndex + 1}, searching alternatives...`);
if (imageIndex === 1) {
const secondImageProps = ['data2', 'image2', 'second_image', 'secondImage'];
const foundSecondProp = secondImageProps.find(prop => { var _a, _b, _c; return (_c = (_b = (_a = targetItem.binary) === null || _a === void 0 ? void 0 : _a[prop]) === null || _b === void 0 ? void 0 : _b.mimeType) === null || _c === void 0 ? void 0 : _c.startsWith('image/'); });
if (foundSecondProp) {
console.log(`[ComfyUI DualImageVideo] Found second image property: "${foundSecondProp}"`);
actualPropertyName = foundSecondProp;
}
}
if (!((_b = targetItem.binary) === null || _b === void 0 ? void 0 : _b[actualPropertyName])) {
const imageProperty = Object.keys(targetItem.binary || {}).find(key => { var _a; return (_a = targetItem.binary[key].mimeType) === null || _a === void 0 ? void 0 : _a.startsWith('image/'); });
if (imageProperty) {
console.log(`[ComfyUI DualImageVideo] Found alternative image property: "${imageProperty}"`);
actualPropertyName = imageProperty;
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `No image binary data found for image ${imageIndex + 1} in property "${binaryPropertyName}"`
});
}
}
}
const itemIndex = imageIndex === 1 && items.length > 1 ? 1 : 0;
const imageBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, actualPropertyName);
console.log(`[ComfyUI DualImageVideo] Got binary data for image ${imageIndex + 1}, size: ${imageBuffer.length} bytes`);
const mimeType = targetItem.binary[actualPropertyName].mimeType;
if (!mimeType || !mimeType.startsWith('image/')) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `Invalid media type for image ${imageIndex + 1}: ${mimeType}. Only images are supported.`
});
}
return imageBuffer;
}
else {
const imageData = this.getNodeParameter(imageParam, 0);
return Buffer.from(imageData, 'base64');
}
};
const firstImageBuffer = await getImageBuffer(firstImageInputType, 'firstInputImage', 'firstImageBinaryPropertyName', 0);
const secondImageBuffer = await getImageBuffer(secondImageInputType, 'secondInputImage', 'secondImageBinaryPropertyName', 1);
console.log('[ComfyUI DualImageVideo] Uploading first image...');
const firstImageFormData = new form_data_1.default();
firstImageFormData.append('image', firstImageBuffer, 'first_input.png');
firstImageFormData.append('subfolder', '');
firstImageFormData.append('overwrite', 'true');
const firstImageUploadResponse = await this.helpers.request({
method: 'POST',
url: `${apiUrl}/upload/image`,
headers: {
...headers,
...firstImageFormData.getHeaders(),
},
body: firstImageFormData,
});
const firstImageInfo = JSON.parse(firstImageUploadResponse);
console.log('[ComfyUI DualImageVideo] First image uploaded:', firstImageInfo);
console.log('[ComfyUI DualImageVideo] Uploading second image...');
const secondImageFormData = new form_data_1.default();
secondImageFormData.append('image', secondImageBuffer, 'second_input.png');
secondImageFormData.append('subfolder', '');
secondImageFormData.append('overwrite', 'true');
const secondImageUploadResponse = await this.helpers.request({
method: 'POST',
url: `${apiUrl}/upload/image`,
headers: {
...headers,
...secondImageFormData.getHeaders(),
},
body: secondImageFormData,
});
const secondImageInfo = JSON.parse(secondImageUploadResponse);
console.log('[ComfyUI DualImageVideo] 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 DualImageVideo] Updated first LoadImage node "${firstImageNodeId}" with image: ${firstImageInfo.name}`);
}
else {
console.warn(`[ComfyUI DualImageVideo] 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 DualImageVideo] Updated second LoadImage node "${secondImageNodeId}" with image: ${secondImageInfo.name}`);
}
else {
console.warn(`[ComfyUI DualImageVideo] 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`
});
}
const videoNodeTypes = [
'AnimateDiffSampler',
'SVD_img2vid_Conditioning',
'VideoHelperSuite',
'VHS_VideoCombine',
'AnimateDiffCombine'
];
Object.values(workflowData).forEach((node) => {
if (videoNodeTypes.includes(node.class_type)) {
if (node.inputs.frame_count !== undefined) {
node.inputs.frame_count = frameCount;
}
if (node.inputs.frames !== undefined) {
node.inputs.frames = frameCount;
}
if (node.inputs.frame_rate !== undefined) {
node.inputs.frame_rate = frameRate;
}
if (node.inputs.fps !== undefined) {
node.inputs.fps = frameRate;
}
console.log(`[ComfyUI DualImageVideo] Updated ${node.class_type} with frameCount: ${frameCount}, frameRate: ${frameRate}`);
}
});
console.log(`[ComfyUI DualImageVideo] Final workflow LoadImage nodes:`, {
[firstImageNodeId]: firstImageInfo.name,
[secondImageNodeId]: secondImageInfo.name
});
console.log('[ComfyUI DualImageVideo] Queueing video 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 DualImageVideo] Video generation queued with ID:', promptId);
let attempts = 0;
const maxAttempts = 60 * timeout;
await new Promise(resolve => setTimeout(resolve, 10000));
while (attempts < maxAttempts) {
console.log(`[ComfyUI DualImageVideo] Checking generation status (attempt ${attempts + 1}/${maxAttempts})...`);
await new Promise(resolve => setTimeout(resolve, 5000));
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 DualImageVideo] Prompt not found in history');
continue;
}
if (promptResult.status === undefined) {
console.log('[ComfyUI DualImageVideo] Execution status not found');
continue;
}
if ((_a = promptResult.status) === null || _a === void 0 ? void 0 : _a.completed) {
console.log('[ComfyUI DualImageVideo] Video generation completed');
if (((_b = promptResult.status) === null || _b === void 0 ? void 0 : _b.status_str) === 'error') {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI DualImageVideo] Video generation failed' });
}
console.log('[ComfyUI DualImageVideo] Raw outputs structure:', JSON.stringify(promptResult.outputs, null, 2));
const videoOutputs = Object.values(promptResult.outputs)
.flatMap((nodeOutput) => [
...(nodeOutput.gifs || []),
...(nodeOutput.videos || []),
...(nodeOutput.images || []).filter((img) => {
var _a, _b, _c, _d;
return ((_a = img.filename) === null || _a === void 0 ? void 0 : _a.endsWith('.gif')) ||
((_b = img.filename) === null || _b === void 0 ? void 0 : _b.endsWith('.mp4')) ||
((_c = img.filename) === null || _c === void 0 ? void 0 : _c.endsWith('.webm')) ||
((_d = img.filename) === null || _d === void 0 ? void 0 : _d.endsWith('.mov'));
})
])
.filter((output) => output.type === 'output' || output.type === 'temp')
.map((video) => ({
...video,
url: `${apiUrl}/view?filename=${video.filename}&subfolder=${video.subfolder || ''}&type=${video.type}`
}));
console.log('[ComfyUI DualImageVideo] Found video outputs:', videoOutputs);
if (videoOutputs.length === 0) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI DualImageVideo] No video outputs found in results' });
}
const videoOutput = videoOutputs[0];
const videoResponse = await this.helpers.request({
method: 'GET',
url: videoOutput.url,
encoding: null,
resolveWithFullResponse: true
});
if (videoResponse.statusCode === 404) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Video file not found at ${videoOutput.url}` });
}
console.log('[ComfyUI DualImageVideo] Using video directly from ComfyUI');
const buffer = Buffer.from(videoResponse.body);
const base64Data = buffer.toString('base64');
const fileSize = Math.round(buffer.length / 1024 * 10) / 10 + " kB";
let mimeType = 'video/mp4';
let fileExtension = 'mp4';
let fileType = 'video';
if (videoOutput.filename.endsWith('.gif')) {
mimeType = 'image/gif';
fileExtension = 'gif';
fileType = 'image';
}
else if (videoOutput.filename.endsWith('.webm')) {
mimeType = 'video/webm';
fileExtension = 'webm';
}
else if (videoOutput.filename.endsWith('.mov')) {
mimeType = 'video/quicktime';
fileExtension = 'mov';
}
return [[{
json: {
mimeType,
fileName: videoOutput.filename,
data: base64Data,
status: promptResult.status,
frameCount,
frameRate,
duration: frameCount / frameRate,
inputImages: {
first: firstImageInfo.name,
second: secondImageInfo.name
}
},
binary: {
data: {
fileName: videoOutput.filename,
data: base64Data,
fileType: fileType,
fileSize,
fileExtension,
mimeType
}
}
}]];
}
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Video generation timeout after ${timeout} minutes` });
}
catch (error) {
console.error('[ComfyUI DualImageVideo] Video generation error:', error);
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `ComfyUI API Error: ${error.message}`,
description: error.description || ''
});
}
}
}
exports.ComfyuiDualImageVideoGenerator = ComfyuiDualImageVideoGenerator;
//# sourceMappingURL=ComfyuiDualImageVideoGenerator.node.js.map