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
380 lines • 18.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComfyuiSingleImageToVideo = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const form_data_1 = __importDefault(require("form-data"));
class ComfyuiSingleImageToVideo {
constructor() {
this.description = {
displayName: 'ComfyUI Single Image To Video',
name: 'comfyuiSingleImageToVideo',
icon: 'file:comfyui.svg',
group: ['transform'],
version: 1,
description: '🖼️➡️🎬 Generate video from a single image input using ComfyUI workflows (image-to-video, animation, motion generation)',
defaults: {
name: 'ComfyUI Single Image To Video',
},
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 LoadImage node for video generation)',
},
{
displayName: 'Image Input Type',
name: 'imageInputType',
type: 'options',
options: [
{ name: 'URL', value: 'url' },
{ name: 'Base64', value: 'base64' },
{ name: 'Binary', value: 'binary' }
],
default: 'url',
required: true,
},
{
displayName: 'Input Image',
name: 'inputImage',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
imageInputType: ['url', 'base64'],
},
},
description: 'URL or base64 data of the input image',
},
{
displayName: 'Image Binary Property',
name: 'imageBinaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
imageInputType: ['binary'],
},
},
description: 'Name of the binary property containing the image',
},
{
displayName: 'Image Node ID',
name: 'imageNodeId',
type: 'string',
default: 'load_image_1',
required: true,
description: 'Node ID in workflow for the 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, _c;
const credentials = await this.getCredentials('comfyUIApi');
const workflow = this.getNodeParameter('workflow', 0);
const imageInputType = this.getNodeParameter('imageInputType', 0);
const imageNodeId = this.getNodeParameter('imageNodeId', 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 ImageVideo] Executing image-to-video generation with API URL:', apiUrl);
const headers = {
'Content-Type': 'application/json',
};
if (apiKey) {
console.log('[ComfyUI ImageVideo] Using API key authentication');
headers['Authorization'] = `Bearer ${apiKey}`;
}
try {
console.log('[ComfyUI ImageVideo] Checking API connection...');
await this.helpers.request({
method: 'GET',
url: `${apiUrl}/system_stats`,
headers,
json: true,
});
let imageBuffer;
if (imageInputType === 'url') {
const inputImage = this.getNodeParameter('inputImage', 0);
console.log('[ComfyUI ImageVideo] Downloading image from URL:', inputImage);
const response = await this.helpers.request({
method: 'GET',
url: inputImage,
encoding: null,
});
imageBuffer = Buffer.from(response);
}
else if (imageInputType === 'binary') {
console.log('[ComfyUI ImageVideo] Getting image binary data from input');
const imageBinaryPropertyName = this.getNodeParameter('imageBinaryPropertyName', 0);
console.log('[ComfyUI ImageVideo] Looking for image binary property:', imageBinaryPropertyName);
const items = this.getInputData();
const binaryProperties = Object.keys(items[0].binary || {});
console.log('[ComfyUI ImageVideo] Available binary properties:', binaryProperties);
let actualImagePropertyName = imageBinaryPropertyName;
if (!((_a = items[0].binary) === null || _a === void 0 ? void 0 : _a[imageBinaryPropertyName])) {
console.log(`[ComfyUI ImageVideo] Binary property "${imageBinaryPropertyName}" 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 ImageVideo] Found alternative image property: "${imageProperty}"`);
actualImagePropertyName = imageProperty;
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `No image binary data found in property "${imageBinaryPropertyName}" and no image alternatives found`
});
}
}
imageBuffer = await this.helpers.getBinaryDataBuffer(0, actualImagePropertyName);
console.log('[ComfyUI ImageVideo] Got image binary data, size:', imageBuffer.length, 'bytes');
const mimeType = items[0].binary[actualImagePropertyName].mimeType;
if (!mimeType || !mimeType.startsWith('image/')) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `Invalid media type for image: ${mimeType}. Only images are supported.`
});
}
}
else {
const inputImage = this.getNodeParameter('inputImage', 0);
imageBuffer = Buffer.from(inputImage, 'base64');
}
const imageFormData = new form_data_1.default();
imageFormData.append('image', imageBuffer, 'input_image.png');
imageFormData.append('subfolder', '');
imageFormData.append('overwrite', 'true');
const imageUploadResponse = await this.helpers.request({
method: 'POST',
url: `${apiUrl}/upload/image`,
headers: {
...headers,
...imageFormData.getHeaders(),
},
body: imageFormData,
});
const imageInfo = JSON.parse(imageUploadResponse);
console.log('[ComfyUI ImageVideo] Image uploaded:', imageInfo);
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[imageNodeId]) {
if (workflowData[imageNodeId].class_type === 'LoadImage') {
workflowData[imageNodeId].inputs.image = imageInfo.name;
console.log(`[ComfyUI ImageVideo] Updated LoadImage node "${imageNodeId}" with image: ${imageInfo.name}`);
}
else {
console.warn(`[ComfyUI ImageVideo] Node "${imageNodeId}" is not a LoadImage node`);
}
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `LoadImage node with ID "${imageNodeId}" 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 ImageVideo] Updated ${node.class_type} with frameCount: ${frameCount}, frameRate: ${frameRate}`);
}
});
console.log('[ComfyUI ImageVideo] 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 ImageVideo] Video 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 ImageVideo] Checking 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 ImageVideo] Prompt not found in history');
continue;
}
if (promptResult.status === undefined) {
console.log('[ComfyUI ImageVideo] Execution status not found');
continue;
}
if ((_b = promptResult.status) === null || _b === void 0 ? void 0 : _b.completed) {
console.log('[ComfyUI ImageVideo] Video generation completed');
if (((_c = promptResult.status) === null || _c === void 0 ? void 0 : _c.status_str) === 'error') {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI ImageVideo] Video generation failed' });
}
console.log('[ComfyUI ImageVideo] 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 ImageVideo] Found video outputs:', videoOutputs);
if (videoOutputs.length === 0) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI ImageVideo] 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 ImageVideo] 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,
},
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 ImageVideo] Video generation error:', error);
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `ComfyUI API Error: ${error.message}`,
description: error.description || ''
});
}
}
}
exports.ComfyuiSingleImageToVideo = ComfyuiSingleImageToVideo;
//# sourceMappingURL=ComfyuiSingleImageToVideo.node.js.map