UNPKG

n8n-nodes-comfyui

Version:

n8n node to integrate with ComfyUI stable diffusion workflows

329 lines 17.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.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