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
JavaScript
"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