n8n-nodes-ksc-comfyui
Version:
n8n node to integrate with ComfyUI stable diffusion workflows for image transformations
314 lines • 15.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComfyuiImageToVideo = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const form_data_1 = __importDefault(require("form-data"));
class ComfyuiImageToVideo {
constructor() {
this.description = {
displayName: 'ComfyUI Image to Video',
name: 'comfyuiImageToVideo',
icon: 'file:comfyui.svg',
group: ['transform'],
version: 1,
description: 'Convert images to videos using ComfyUI workflow',
defaults: {
name: 'ComfyUI 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',
},
{
displayName: 'Input Type',
name: 'inputType',
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: {
inputType: ['url', 'base64'],
},
},
description: 'URL or base64 data of the input image',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
inputType: ['binary'],
},
},
description: 'Name of the binary property containing the image',
},
{
displayName: 'Timeout',
name: 'timeout',
type: 'number',
default: 30,
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 inputType = this.getNodeParameter('inputType', 0);
const timeout = this.getNodeParameter('timeout', 0);
const apiUrl = credentials.apiUrl;
const apiKey = credentials.apiKey;
console.log('[ComfyUI] Executing image to video conversion with API URL:', apiUrl);
const headers = {
'Content-Type': 'application/json',
};
if (apiKey) {
console.log('[ComfyUI] Using API key authentication');
headers['Authorization'] = `Bearer ${apiKey}`;
}
try {
console.log('[ComfyUI] Checking API connection...');
await this.helpers.request({
method: 'GET',
url: `${apiUrl}/system_stats`,
headers,
json: true,
});
let imageBuffer;
if (inputType === 'url') {
const inputImage = this.getNodeParameter('inputImage', 0);
console.log('[ComfyUI] Downloading image from URL:', inputImage);
const response = await this.helpers.request({
method: 'GET',
url: inputImage,
encoding: null,
});
imageBuffer = Buffer.from(response);
}
else if (inputType === 'binary') {
console.log('[ComfyUI] Getting binary data from input');
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
console.log('[ComfyUI] Looking for binary property:', binaryPropertyName);
const items = this.getInputData();
const binaryProperties = Object.keys(items[0].binary || {});
console.log('[ComfyUI] Available binary properties:', binaryProperties);
let actualPropertyName = binaryPropertyName;
if (!((_a = items[0].binary) === null || _a === void 0 ? void 0 : _a[binaryPropertyName])) {
console.log(`[ComfyUI] Binary property "${binaryPropertyName}" 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] Found alternative image property: "${imageProperty}"`);
actualPropertyName = imageProperty;
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `No binary data found in property "${binaryPropertyName}" and no image alternatives found`
});
}
}
imageBuffer = await this.helpers.getBinaryDataBuffer(0, actualPropertyName);
console.log('[ComfyUI] Got binary data, size:', imageBuffer.length, 'bytes');
const mimeType = items[0].binary[actualPropertyName].mimeType;
console.log('[ComfyUI] Binary data mime type:', mimeType);
if (!mimeType || !mimeType.startsWith('image/')) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `Invalid media type: ${mimeType}. Only images are supported.`
});
}
}
else {
const inputImage = this.getNodeParameter('inputImage', 0);
imageBuffer = Buffer.from(inputImage, 'base64');
}
console.log('[ComfyUI] Uploading image...');
const formData = new form_data_1.default();
formData.append('image', imageBuffer, 'input.png');
formData.append('subfolder', '');
formData.append('overwrite', 'true');
const uploadResponse = await this.helpers.request({
method: 'POST',
url: `${apiUrl}/upload/image`,
headers: {
...headers,
...formData.getHeaders(),
},
body: formData,
});
const imageInfo = JSON.parse(uploadResponse);
console.log('[ComfyUI] 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.'
});
}
const loadImageNode = Object.values(workflowData).find((node) => node.class_type === 'LoadImage' && node.inputs && node.inputs.image !== undefined);
if (!loadImageNode) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: 'No LoadImage node found in the workflow. The workflow must contain a LoadImage node with an image input.'
});
}
loadImageNode.inputs.image = imageInfo.name;
console.log('[ComfyUI] 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] 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] Checking video 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] Prompt not found in history');
continue;
}
if (promptResult.status === undefined) {
console.log('[ComfyUI] Execution status not found');
continue;
}
if ((_b = promptResult.status) === null || _b === void 0 ? void 0 : _b.completed) {
console.log('[ComfyUI] 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] Video generation failed' });
}
console.log('[ComfyUI] Raw outputs structure:', JSON.stringify(promptResult.outputs, null, 2));
const mediaOutputs = Object.values(promptResult.outputs)
.flatMap((nodeOutput) => nodeOutput.images || nodeOutput.gifs || [])
.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] Found media outputs:', mediaOutputs);
if (mediaOutputs.length === 0) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI] No media outputs found in results' });
}
const videoOutputs = mediaOutputs.filter(output => output.filename.endsWith('.webp') ||
output.filename.endsWith('.mp4') ||
output.filename.endsWith('.gif'));
if (videoOutputs.length === 0) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI] No video outputs found in results' });
}
console.log('[ComfyUI] Found video outputs:', videoOutputs);
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] Using media 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 = 'image/webp';
let fileExtension = 'webp';
if (videoOutput.filename.endsWith('.mp4')) {
mimeType = 'video/mp4';
fileExtension = 'mp4';
}
else if (videoOutput.filename.endsWith('.gif')) {
mimeType = 'image/gif';
fileExtension = 'gif';
}
return [[{
json: {
mimeType,
fileName: videoOutput.filename,
data: base64Data,
status: promptResult.status,
},
binary: {
data: {
fileName: videoOutput.filename,
data: base64Data,
fileType: 'video',
fileSize,
fileExtension,
mimeType
}
}
}]];
}
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Video generation timeout after ${timeout} minutes` });
}
catch (error) {
console.error('[ComfyUI] Video generation error:', error);
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: `ComfyUI API Error: ${error.message}`,
description: error.description || ''
});
}
}
}
exports.ComfyuiImageToVideo = ComfyuiImageToVideo;
//# sourceMappingURL=ComfyuiImageToVideo.node.js.map