UNPKG

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
"use strict"; 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