n8n-nodes-comfyui
Version:
n8n node to integrate with ComfyUI stable diffusion workflows
329 lines • 17.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Comfyui = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const sharp_1 = __importDefault(require("sharp"));
class Comfyui {
constructor() {
this.description = {
displayName: 'ComfyUI',
name: 'comfyui',
icon: 'file:comfyui.svg',
group: ['transform'],
version: 1,
description: 'Execute ComfyUI workflows',
defaults: {
name: 'ComfyUI',
},
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: 'Output Format',
name: 'outputFormat',
type: 'options',
options: [
{
name: 'JPEG',
value: 'jpeg',
},
{
name: 'PNG',
value: 'png',
},
{
name: 'WebP',
value: 'webp',
},
{
name: 'Raw (Original)',
value: 'raw',
},
],
default: 'jpeg',
description: 'The format of the output images. Raw downloads files as-is without conversion.',
},
{
displayName: 'JPEG Quality',
name: 'jpegQuality',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 100
},
default: 80,
description: 'Quality of JPEG output (1-100)',
displayOptions: {
show: {
outputFormat: ['jpeg'],
},
},
},
{
displayName: 'WebP Quality',
name: 'webpQuality',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 100
},
default: 80,
description: 'Quality of WebP output (1-100)',
displayOptions: {
show: {
outputFormat: ['webp'],
},
},
},
{
displayName: 'Timeout',
name: 'timeout',
type: 'number',
default: 30,
description: 'Maximum time in minutes to wait for workflow completion',
},
],
};
}
async execute() {
var _a, _b, _c, _d;
const credentials = await this.getCredentials('comfyUIApi');
const items = this.getInputData();
const apiUrl = credentials.apiUrl.trim().replace(/\/+$/, '');
const apiKey = (_a = credentials.apiKey) === null || _a === void 0 ? void 0 : _a.trim();
console.log('[ComfyUI] Executing with API URL:', apiUrl);
const headers = {
'Content-Type': 'application/json',
};
if (apiKey) {
console.log('[ComfyUI] Using API key authentication');
headers['Authorization'] = `Bearer ${apiKey}`;
}
const isInQueue = (queue, promptId) => {
for (const item of queue) {
if (item.length > 1 && item[1] === promptId) {
return true;
}
}
return false;
};
try {
console.log('[ComfyUI] Checking API connection...');
await this.helpers.request({
method: 'GET',
url: `${apiUrl}/system_stats`,
headers,
json: true,
});
const allOutputs = [];
for (let i = 0; i < items.length; i++) {
const workflow = this.getNodeParameter('workflow', i);
const timeout = this.getNodeParameter('timeout', i);
const outputFormat = this.getNodeParameter('outputFormat', i);
let jpegQuality;
if (outputFormat === 'jpeg') {
jpegQuality = this.getNodeParameter('jpegQuality', i);
}
let webpQuality;
if (outputFormat === 'webp') {
webpQuality = this.getNodeParameter('webpQuality', i);
}
console.log(`[ComfyUI] Queueing prompt for item ${i}...`);
const response = await this.helpers.request({
method: 'POST',
url: `${apiUrl}/prompt`,
headers,
body: {
prompt: JSON.parse(workflow),
},
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] Prompt queued with ID:', promptId);
let attempts = 0;
const maxAttempts = 60 * timeout;
await new Promise(resolve => setTimeout(resolve, 5000));
let completed = false;
while (attempts < maxAttempts) {
console.log(`[ComfyUI] Checking execution status (attempt ${attempts + 1}/${maxAttempts})...`);
await new Promise(resolve => setTimeout(resolve, 1000));
attempts++;
const queueStatus = await this.helpers.request({
method: 'GET',
url: `${apiUrl}/queue`,
headers,
json: true,
});
const isRunning = isInQueue(queueStatus.queue_running || [], promptId);
const isPending = isInQueue(queueStatus.queue_pending || [], promptId);
if (isRunning) {
console.log('[ComfyUI] Prompt is currently running');
continue;
}
if (isPending) {
console.log('[ComfyUI] Prompt is pending in queue');
continue;
}
console.log('[ComfyUI] Prompt has left the queue, checking history...');
const history = await this.helpers.request({
method: 'GET',
url: `${apiUrl}/history/${promptId}`,
headers,
json: true,
});
const promptResult = history[promptId];
if (!promptResult) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
message: '[ComfyUI] Workflow execution failed: prompt disappeared from queue but is not in history. This usually indicates a server crash or prompt parsing error.'
});
}
if (promptResult.status === undefined) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI] Workflow execution failed: prompt contains no status' });
}
if (((_b = promptResult.status) === null || _b === void 0 ? void 0 : _b.status_str) === 'error') {
const errorMessages = ((_c = promptResult.status) === null || _c === void 0 ? void 0 : _c.messages) || [];
const executionError = errorMessages.find((msg) => msg[0] === 'execution_error');
let errorDetails = '[ComfyUI] Workflow execution failed';
if (executionError && executionError[1]) {
const errorInfo = executionError[1];
errorDetails = `[ComfyUI] Workflow execution failed in node ${errorInfo.node_id} (${errorInfo.node_type}): ${errorInfo.exception_message}`;
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: errorDetails });
}
if ((_d = promptResult.status) === null || _d === void 0 ? void 0 : _d.completed) {
console.log('[ComfyUI] Execution completed');
if (!promptResult.outputs) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: '[ComfyUI] No outputs found in workflow result' });
}
const nodeOutputValues = Object.values(promptResult.outputs);
const imageFiles = nodeOutputValues
.flatMap((nodeOutput) => nodeOutput.images || [])
.filter((f) => f.type === 'output' || f.type === 'temp');
const videoFiles = nodeOutputValues
.flatMap((nodeOutput) => nodeOutput.videos || [])
.filter((f) => f.type === 'output' || f.type === 'temp');
const VIDEO_MIME = {
mp4: 'video/mp4',
webm: 'video/webm',
mov: 'video/quicktime',
avi: 'video/x-msvideo',
gif: 'image/gif',
};
const outputs = await Promise.all([
...imageFiles.map(async (file) => {
var _a;
console.log(`[ComfyUI] Downloading image:`, file.filename);
const fileUrl = `${apiUrl}/view?filename=${file.filename}&subfolder=${file.subfolder || ''}&type=${file.type || ''}`;
try {
const rawData = await this.helpers.request({ method: 'GET', url: fileUrl, encoding: null, headers });
let outputBuffer;
let fileExtension;
let mimeType;
if (outputFormat === 'raw') {
outputBuffer = Buffer.from(rawData, 'base64');
fileExtension = ((_a = file.filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || 'png';
mimeType = `image/${fileExtension}`;
}
else {
const imageInput = (0, sharp_1.default)(Buffer.from(rawData, 'base64'));
if (outputFormat === 'jpeg') {
outputBuffer = await imageInput.jpeg({ quality: jpegQuality }).toBuffer();
}
else if (outputFormat === 'webp') {
outputBuffer = await imageInput.webp({ quality: webpQuality }).toBuffer();
}
else {
outputBuffer = await imageInput.png().toBuffer();
}
fileExtension = outputFormat;
mimeType = `image/${outputFormat}`;
}
const outputBase64 = outputBuffer.toString('base64');
return {
json: { filename: file.filename, type: file.type, subfolder: file.subfolder || '', mediaType: 'image' },
binary: { data: {
fileName: file.filename,
data: outputBase64,
fileType: 'image',
fileSize: Math.round(outputBuffer.length / 1024 * 10) / 10 + " kB",
fileExtension,
mimeType,
} }
};
}
catch (error) {
console.error(`[ComfyUI] Failed to download image ${file.filename}:`, error);
return { json: { filename: file.filename, type: file.type, subfolder: file.subfolder || '', error: error.message } };
}
}),
...videoFiles.map(async (file) => {
var _a;
console.log(`[ComfyUI] Downloading video:`, file.filename);
const ext = ((_a = file.filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || 'mp4';
const mimeType = file.format || VIDEO_MIME[ext] || `video/${ext}`;
const fileUrl = `${apiUrl}/view?filename=${file.filename}&subfolder=${file.subfolder || ''}&type=${file.type || ''}`;
try {
const rawData = await this.helpers.request({ method: 'GET', url: fileUrl, encoding: null, headers });
const outputBuffer = Buffer.from(rawData, 'base64');
const outputBase64 = outputBuffer.toString('base64');
return {
json: { filename: file.filename, type: file.type, subfolder: file.subfolder || '', mediaType: 'video' },
binary: { data: {
fileName: file.filename,
data: outputBase64,
fileType: 'video',
fileSize: Math.round(outputBuffer.length / 1024 * 10) / 10 + " kB",
fileExtension: ext,
mimeType,
} }
};
}
catch (error) {
console.error(`[ComfyUI] Failed to download video ${file.filename}:`, error);
return { json: { filename: file.filename, type: file.type, subfolder: file.subfolder || '', error: error.message } };
}
}),
]);
console.log(`[ComfyUI] All images downloaded for item ${i}`);
allOutputs.push(...outputs);
completed = true;
break;
}
}
if (!completed) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Execution timeout after ${timeout} minutes` });
}
}
return [allOutputs];
}
catch (error) {
console.error('[ComfyUI] Execution error:', error);
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `ComfyUI API Error: ${error.message}` });
}
}
}
exports.Comfyui = Comfyui;
//# sourceMappingURL=Comfyui.node.js.map