UNPKG

n8n-nodes-gemini-ai

Version:

n8n community node for Google Gemini AI integration with text generation, file upload & analysis, and TTS (Text-to-Speech) support

805 lines 39.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Gemini = void 0; const n8n_workflow_1 = require("n8n-workflow"); class Gemini { constructor() { this.description = { displayName: 'Gemini', name: 'gemini', icon: 'file:gemini.svg', group: ['transform'], version: 2, subtitle: '={{$parameter["operation"]}}', description: 'Interact with Google Gemini AI', defaults: { name: 'Gemini', }, inputs: ["main"], outputs: ["main"], credentials: [ { name: 'geminiApi', required: true, }, ], usableAsTool: true, properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Analyze File', value: 'analyzeFile', description: 'Analyze an uploaded file with text prompt', action: 'Analyze file', }, { name: 'Generate Text', value: 'generateText', description: 'Generate text using Gemini AI', action: 'Generate text', }, { name: 'Generate TTS', value: 'generateTTS', description: 'Generate audio from text using Text-to-Speech', action: 'Generate audio', }, { name: 'Upload File', value: 'uploadFile', description: 'Upload a file to Gemini for processing', action: 'Upload file', }, ], default: 'generateText', }, { displayName: 'Binary Property', name: 'binaryProperty', type: 'string', displayOptions: { show: { operation: ['uploadFile'], }, }, default: 'data', placeholder: 'data', description: 'Name of the binary property that contains the file to upload', }, { displayName: 'Display Name', name: 'displayName', type: 'string', displayOptions: { show: { operation: ['uploadFile'], }, }, default: '', placeholder: 'My Audio File', description: 'A name for the uploaded file (will use original filename if empty)', }, { displayName: 'File URI', name: 'fileUri', type: 'string', displayOptions: { show: { operation: ['analyzeFile'], }, }, default: '', placeholder: 'files/your-file-ID', description: 'URI of the uploaded file (from Upload File operation)', required: true, }, { displayName: 'File MIME Type', name: 'fileMimeType', type: 'string', displayOptions: { show: { operation: ['analyzeFile'], }, }, default: 'audio/mp3', placeholder: 'audio/mp3, image/jpeg, video/mp4, etc.', description: 'MIME type of the file (you can type any MIME type)', required: true, }, { displayName: 'Prompt', name: 'promptForFile', type: 'string', displayOptions: { show: { operation: ['analyzeFile'], }, }, typeOptions: { rows: 4, }, default: 'Describe this file', placeholder: 'What do you see in this image? Or: Transcribe this audio...', description: 'What you want to ask about the file', required: true, }, { displayName: 'Model', name: 'modelForFile', type: 'options', displayOptions: { show: { operation: ['analyzeFile'], }, }, options: [ { name: 'Gemini 1.5 Flash', value: 'gemini-1.5-flash', description: 'Fast and efficient model', }, { name: 'Gemini 1.5 Flash-8B', value: 'gemini-1.5-flash-8b', description: 'High-volume tasks with lower intelligence', }, { name: 'Gemini 1.5 Pro', value: 'gemini-1.5-pro', description: 'Complex reasoning tasks requiring more intelligence', }, { name: 'Gemini 2.0 Flash', value: 'gemini-2.0-flash', description: 'Next-generation features, speed and real-time streaming', }, { name: 'Gemini 2.0 Flash-Lite', value: 'gemini-2.0-flash-lite', description: 'Cost-effective with short latency', }, { name: 'Gemini 2.5 Flash Preview', value: 'gemini-2.5-flash-preview-05-20', description: 'Latest Gemini 2.5 Flash model with dynamic thinking', }, { name: 'Gemini 2.5 Pro Preview', value: 'gemini-2.5-pro-preview-05-06', description: 'Enhanced reasoning and multimodal understanding', }, ], default: 'gemini-2.5-flash-preview-05-20', description: 'The Gemini model to use for file analysis', }, { displayName: 'Model', name: 'model', type: 'options', displayOptions: { show: { operation: ['generateText'], }, }, options: [ { name: 'Gemini 1.5 Flash', value: 'gemini-1.5-flash', description: 'Fast and efficient model', }, { name: 'Gemini 1.5 Flash-8B', value: 'gemini-1.5-flash-8b', description: 'High-volume tasks with lower intelligence', }, { name: 'Gemini 1.5 Pro', value: 'gemini-1.5-pro', description: 'Complex reasoning tasks requiring more intelligence', }, { name: 'Gemini 2.0 Flash', value: 'gemini-2.0-flash', description: 'Next-generation features, speed and real-time streaming', }, { name: 'Gemini 2.0 Flash-Lite', value: 'gemini-2.0-flash-lite', description: 'Cost-effective with short latency', }, { name: 'Gemini 2.5 Flash Preview', value: 'gemini-2.5-flash-preview-05-20', description: 'Latest Gemini 2.5 Flash model with dynamic thinking', }, { name: 'Gemini 2.5 Pro Preview', value: 'gemini-2.5-pro-preview-05-06', description: 'Enhanced reasoning and multimodal understanding', }, ], default: 'gemini-2.5-flash-preview-05-20', description: 'The Gemini model to use', }, { displayName: 'Prompt', name: 'prompt', type: 'string', displayOptions: { show: { operation: ['generateText'], }, }, typeOptions: { rows: 4, }, default: '', placeholder: 'Enter your prompt here...', description: 'The text prompt to send to Gemini', required: true, }, { displayName: 'System Prompt', name: 'systemPrompt', type: 'string', displayOptions: { show: { operation: ['generateText'], }, }, typeOptions: { rows: 3, }, default: '', placeholder: 'You are a helpful assistant...', description: 'System instructions for the AI (optional)', }, { displayName: 'Response Format', name: 'responseFormat', type: 'options', displayOptions: { show: { operation: ['generateText'], }, }, options: [ { name: 'Text', value: 'text/plain', description: 'Plain text response', }, { name: 'JSON', value: 'application/json', description: 'JSON formatted response', }, ], default: 'text/plain', description: 'The format of the response', }, { displayName: 'Temperature', name: 'temperature', type: 'number', displayOptions: { show: { operation: ['generateText'], }, }, typeOptions: { minValue: 0, maxValue: 2, numberPrecision: 1, }, default: 1, description: 'Controls randomness in responses (0-2)', }, { displayName: 'Max Output Tokens', name: 'maxOutputTokens', type: 'number', displayOptions: { show: { operation: ['generateText'], }, }, typeOptions: { minValue: 1, maxValue: 8192, }, default: 2048, description: 'Maximum number of tokens to generate', }, { displayName: 'Text to Speak', name: 'textToSpeak', type: 'string', displayOptions: { show: { operation: ['generateTTS'], }, }, typeOptions: { rows: 4, }, default: '', placeholder: 'Say cheerfully: Have a wonderful day!', description: 'The text that will be converted to audio', required: true, }, { displayName: 'TTS Model', name: 'ttsModel', type: 'options', displayOptions: { show: { operation: ['generateTTS'], }, }, options: [ { name: 'Gemini 2.5 Flash Preview TTS', value: 'gemini-2.5-flash-preview-tts', description: 'Fast TTS with short latency', }, { name: 'Gemini 2.5 Pro Preview TTS', value: 'gemini-2.5-pro-preview-tts', description: 'High-quality TTS with enhanced control', }, ], default: 'gemini-2.5-flash-preview-tts', description: 'The Gemini TTS model to use', }, { displayName: 'Voice', name: 'voice', type: 'options', displayOptions: { show: { operation: ['generateTTS'], }, }, options: [ { name: 'Achernar (Soft)', value: 'Achernar', description: 'Soft voice' }, { name: 'Achird (Friendly)', value: 'Achird', description: 'Friendly voice' }, { name: 'Algenib (Gravelly)', value: 'Algenib', description: 'Gravelly voice' }, { name: 'Algieba (Smooth)', value: 'Algieba', description: 'Smooth voice' }, { name: 'Alnilam (Firm)', value: 'Alnilam', description: 'Firm voice' }, { name: 'Aoede (Breezy)', value: 'Aoede', description: 'Breezy voice' }, { name: 'Autonoe (Bright)', value: 'Autonoe', description: 'Bright voice' }, { name: 'Callirrhoe (Light)', value: 'Callirrhoe', description: 'Light voice' }, { name: 'Charon (Informative)', value: 'Charon', description: 'Informative voice' }, { name: 'Despina (Smooth)', value: 'Despina', description: 'Smooth voice' }, { name: 'Enceladus (Breathy)', value: 'Enceladus', description: 'Breathy voice' }, { name: 'Erinome (Clean)', value: 'Erinome', description: 'Clean voice' }, { name: 'Fenrir (Enthusiastic)', value: 'Fenrir', description: 'Enthusiastic voice' }, { name: 'Gacrux (Mature)', value: 'Gacrux', description: 'Mature voice' }, { name: 'Iapetus (Clear)', value: 'Iapetus', description: 'Clear voice' }, { name: 'Kore (Firm)', value: 'Kore', description: 'Firm voice' }, { name: 'Laomedeia (Upbeat)', value: 'Laomedeia', description: 'Upbeat voice' }, { name: 'Leda (Young)', value: 'Leda', description: 'Young voice' }, { name: 'Orus (Friendly)', value: 'Orus', description: 'Friendly voice' }, { name: 'Puck (Upbeat)', value: 'Puck', description: 'Upbeat voice' }, { name: 'Pulcherrima (Expressive)', value: 'Pulcherrima', description: 'Expressive voice' }, { name: 'Rasalgethi (Informative)', value: 'Rasalgethi', description: 'Informative voice' }, { name: 'Sadachbia (Energetic)', value: 'Sadachbia', description: 'Energetic voice' }, { name: 'Sadaltager (Expert)', value: 'Sadaltager', description: 'Expert voice' }, { name: 'Schedar (Even)', value: 'Schedar', description: 'Even voice' }, { name: 'Sulafat (Warm)', value: 'Sulafat', description: 'Warm voice' }, { name: 'Umbriel (Calm)', value: 'Umbriel', description: 'Calm voice' }, { name: 'Vindemiatrix (Gentle)', value: 'Vindemiatrix', description: 'Gentle voice' }, { name: 'Zephyr (Bright)', value: 'Zephyr', description: 'Bright voice' }, { name: 'Zubenelgenubi (Casual)', value: 'Zubenelgenubi', description: 'Casual voice' }, ], default: 'Kore', description: 'The voice to use for text-to-speech', }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); if (operation === 'uploadFile') { const binaryProperty = this.getNodeParameter('binaryProperty', i); let displayName = this.getNodeParameter('displayName', i); try { const binaryData = items[i].binary; if (!binaryData || !binaryData[binaryProperty]) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No binary data found in property '${binaryProperty}'. Make sure the previous node outputs binary data.`, { itemIndex: i, }); } const binaryItem = binaryData[binaryProperty]; let fileBuffer; if (binaryItem.data) { fileBuffer = Buffer.from(binaryItem.data, 'base64'); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Binary data in property '${binaryProperty}' does not contain valid data`, { itemIndex: i, }); } const fileSizeBytes = fileBuffer.length; let mimeType = binaryItem.mimeType || 'application/octet-stream'; const fileName = binaryItem.fileName || 'uploaded_file'; if (!displayName && fileName) { displayName = fileName.replace(/\.[^/.]+$/, ""); } if (!displayName) { displayName = 'Uploaded File'; } if (mimeType === 'application/octet-stream' && fileName) { const extension = fileName.toLowerCase().split('.').pop(); switch (extension) { case 'mp3': mimeType = 'audio/mp3'; break; case 'ogg': mimeType = 'audio/ogg'; break; case 'oga': mimeType = 'audio/oga'; break; case 'wav': mimeType = 'audio/wav'; break; case 'jpg': case 'jpeg': mimeType = 'image/jpeg'; break; case 'png': mimeType = 'image/png'; break; case 'pdf': mimeType = 'application/pdf'; break; case 'mp4': mimeType = 'video/mp4'; break; case 'mov': mimeType = 'video/quicktime'; break; } } const initOptions = { method: 'POST', headers: { 'X-Goog-Upload-Protocol': 'resumable', 'X-Goog-Upload-Command': 'start', 'X-Goog-Upload-Header-Content-Length': fileSizeBytes.toString(), 'X-Goog-Upload-Header-Content-Type': mimeType, 'Content-Type': 'application/json', }, body: { file: { display_name: displayName, }, }, url: 'https://generativelanguage.googleapis.com/upload/v1beta/files', json: true, resolveWithFullResponse: true, }; const initResponse = await this.helpers.requestWithAuthentication.call(this, 'geminiApi', initOptions); const uploadUrl = initResponse.headers['x-goog-upload-url']; if (!uploadUrl) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to get upload URL from Gemini API', { itemIndex: i, }); } const uploadOptions = { method: 'POST', headers: { 'Content-Length': fileSizeBytes.toString(), 'X-Goog-Upload-Offset': '0', 'X-Goog-Upload-Command': 'upload, finalize', }, body: fileBuffer, url: uploadUrl, json: true, }; const uploadResponse = await this.helpers.request(uploadOptions); if (!uploadResponse.file || !uploadResponse.file.uri) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to upload file to Gemini API', { itemIndex: i, }); } returnData.push({ json: { fileUri: uploadResponse.file.uri, fileName: uploadResponse.file.name, displayName: uploadResponse.file.displayName, mimeType: uploadResponse.file.mimeType, sizeBytes: uploadResponse.file.sizeBytes, createTime: uploadResponse.file.createTime, originalFileName: fileName, originalMimeType: mimeType, ...items[i].json, }, pairedItem: { item: i }, }); } catch (fileError) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `File upload error: ${fileError.message}`, { itemIndex: i, }); } } else if (operation === 'generateText') { const model = this.getNodeParameter('model', i); const prompt = this.getNodeParameter('prompt', i); const systemPrompt = this.getNodeParameter('systemPrompt', i); const responseFormat = this.getNodeParameter('responseFormat', i); const temperature = this.getNodeParameter('temperature', i); const maxOutputTokens = this.getNodeParameter('maxOutputTokens', i); const contents = []; if (systemPrompt) { contents.push({ role: 'user', parts: [{ text: systemPrompt }], }); } contents.push({ role: 'user', parts: [{ text: prompt }], }); const requestBody = { contents, generationConfig: { responseMimeType: responseFormat, temperature, maxOutputTokens, }, }; const options = { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: requestBody, url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, json: true, }; try { const apiResponse = await this.helpers.requestWithAuthentication.call(this, 'geminiApi', options); let response = ''; if (apiResponse.candidates && apiResponse.candidates.length > 0) { const candidate = apiResponse.candidates[0]; if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) { response = candidate.content.parts[0].text || ''; } } if (!response) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid response received from Gemini API', { itemIndex: i, }); } returnData.push({ json: { response, model, prompt, ...items[i].json, }, pairedItem: { item: i }, }); } catch (apiError) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Gemini API Error: ${apiError.message}`, { itemIndex: i, }); } } else if (operation === 'analyzeFile') { const fileUri = this.getNodeParameter('fileUri', i); const fileMimeType = this.getNodeParameter('fileMimeType', i); const promptForFile = this.getNodeParameter('promptForFile', i); const modelForFile = this.getNodeParameter('modelForFile', i); try { const contents = [{ role: 'user', parts: [ { text: promptForFile }, { file_data: { mime_type: fileMimeType, file_uri: fileUri } } ], }]; const requestBody = { contents, generationConfig: { temperature: 1, maxOutputTokens: 8192, }, }; const options = { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: requestBody, url: `https://generativelanguage.googleapis.com/v1beta/models/${modelForFile}:generateContent`, json: true, }; const apiResponse = await this.helpers.requestWithAuthentication.call(this, 'geminiApi', options); let response = ''; if (apiResponse.candidates && apiResponse.candidates.length > 0) { const candidate = apiResponse.candidates[0]; if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) { response = candidate.content.parts[0].text || ''; } } if (!response) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid response received from Gemini API', { itemIndex: i, }); } returnData.push({ json: { response, model: modelForFile, prompt: promptForFile, fileUri, fileMimeType, ...items[i].json, }, pairedItem: { item: i }, }); } catch (apiError) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Gemini API Error: ${apiError.message}`, { itemIndex: i, }); } } else if (operation === 'generateTTS') { const textToSpeak = this.getNodeParameter('textToSpeak', i); const ttsModel = this.getNodeParameter('ttsModel', i); const voice = this.getNodeParameter('voice', i); try { const contents = [{ parts: [{ text: textToSpeak }], }]; const requestBody = { contents, generationConfig: { responseModalities: ['AUDIO'], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice, }, }, }, }, }; const options = { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: requestBody, url: `https://generativelanguage.googleapis.com/v1beta/models/${ttsModel}:generateContent`, json: true, }; const apiResponse = await this.helpers.requestWithAuthentication.call(this, 'geminiApi', options); if (!apiResponse.candidates || apiResponse.candidates.length === 0) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No candidates returned from Gemini TTS API. Response: ${JSON.stringify(apiResponse)}`, { itemIndex: i, }); } const candidate = apiResponse.candidates[0]; if (candidate.finishReason && candidate.finishReason !== 'STOP') { let errorMessage = `TTS generation failed. Finish reason: ${candidate.finishReason}`; if (candidate.finishReason === 'SAFETY') { errorMessage += '. The text was blocked due to safety concerns. Try modifying your text.'; } else if (candidate.finishReason === 'RECITATION') { errorMessage += '. The text was blocked due to recitation concerns. Try modifying your text.'; } else if (candidate.finishReason === 'OTHER') { errorMessage += '. There was an issue with the request. Check your text length and voice selection.'; } throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { itemIndex: i, }); } let audioData = ''; if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) { const part = candidate.content.parts[0]; if (part.inlineData && part.inlineData.data) { audioData = part.inlineData.data; } } if (!audioData) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No audio data found in response. Response structure: ${JSON.stringify(apiResponse, null, 2)}`, { itemIndex: i, }); } const pcmBuffer = Buffer.from(audioData, 'base64'); const createWavHeader = (dataLength, sampleRate, channels, bitsPerSample) => { const header = Buffer.alloc(44); const byteRate = (sampleRate * channels * bitsPerSample) / 8; const blockAlign = (channels * bitsPerSample) / 8; header.write('RIFF', 0); header.writeUInt32LE(36 + dataLength, 4); header.write('WAVE', 8); header.write('fmt ', 12); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(sampleRate, 24); header.writeUInt32LE(byteRate, 28); header.writeUInt16LE(blockAlign, 32); header.writeUInt16LE(bitsPerSample, 34); header.write('data', 36); header.writeUInt32LE(dataLength, 40); return header; }; const wavHeader = createWavHeader(pcmBuffer.length, 24000, 1, 16); const wavBuffer = Buffer.concat([wavHeader, pcmBuffer]); const wavBase64 = wavBuffer.toString('base64'); const fileName = `tts_output_${Date.now()}.wav`; returnData.push({ json: { textToSpeak, ttsModel, voice, audioFileName: fileName, audioSize: wavBuffer.length, originalPcmSize: pcmBuffer.length, ...items[i].json, }, binary: { data: { data: wavBase64, mimeType: 'audio/wav', fileName: fileName, }, }, pairedItem: { item: i }, }); } catch (apiError) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Gemini TTS API Error: ${apiError.message}`, { itemIndex: i, }); } } } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error.message, ...items[i].json, }, pairedItem: { item: i }, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i, }); } } } return [returnData]; } } exports.Gemini = Gemini; //# sourceMappingURL=Gemini.node.js.map