{
  "name": "n8n workflow",
  "nodes": [
    {
      "id": "37a9addb-a90e-4491-8b7a-a42fad57329a",
      "name": "🎬 Start: Upload PDF File",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -340,
        -60
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "dd475138-a5fa-4818-aeaa-27e8e0969367",
      "name": "📄 Extract Text from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -120,
        -60
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "data0"
      },
      "typeVersion": 1
    },
    {
      "id": "9a24fae8-ac8c-48a0-a985-69e0b4a3e0b8",
      "name": "🤖 Generate Podcast Script",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        100,
        -160
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "=Create an engaging podcast script based on the following content. The script should be exactly 5-7 minutes when read aloud (approximately 750-1000 words).  CONTENT TO DISCUSS: {content}  STYLE: {style}  PODCAST SCRIPT REQUIREMENTS: 1. Start with: \"Welcome to our AI-generated podcast. Today we're diving into...\" 2. Structure: Introduction, 2-3 main discussion points, conclusion 3. Use natural conversational language 4. Include smooth transitions between topics 5. Add brief pauses with periods and commas for natural speech 6. Avoid complex punctuation that might confuse TTS 7. End with: \"Thanks for listening! This has been your AI podcast.\" 8. Write ONLY the spoken content (no stage directions) 9. Keep sentences moderate length for clear speech 10. Use engaging storytelling techniques"
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "87891658-777b-47bf-9a86-9c23076a7477",
      "name": "Google Gemini Flash 2.0",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        188,
        60
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.0-flash-001"
      },
      "typeVersion": 1
    },
    {
      "id": "8ebba81a-a42e-48cf-aeaa-1263fe59ed90",
      "name": "⚙️ Prepare TTS Request",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        -60
      ],
      "parameters": {
        "jsCode": "const script = $input.first().json.text;\nconst voiceName = $input.first().json.voiceName || \"Kore\";\n\nreturn {\n  json: {\n    requestBody: {\n      contents: [{\n        parts: [{\n          text: script\n        }]\n      }],\n      generationConfig: {\n        responseModalities: [\"AUDIO\"],\n        speechConfig: {\n          voiceConfig: {\n            prebuiltVoiceConfig: {\n              voiceName: voiceName\n            }\n          }\n        }\n      }\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "78e3f968-ec21-4d85-a802-688f1d33b48c",
      "name": "🎙️ Convert Text to Speech With Gemini",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        696,
        -60
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "googlePalmApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "1baddfca-84c4-4ffd-beb8-5acb919112fe",
      "name": "🔧 Process Audio Response",
      "type": "n8n-nodes-base.code",
      "position": [
        916,
        -60
      ],
      "parameters": {
        "jsCode": "const response = $input.all()[0].json;\n\nconsole.log('=== GEMINI TTS DEBUG ===');\nconsole.log('Response status:', response.candidates?.[0]?.finishReason);\n\nif (response.candidates?.[0]?.finishReason === 'STOP') {\n  try {\n    // Get the base64 audio data\n    const base64AudioData = response.candidates[0].content.parts[0].inlineData.data;\n    const mimeType = response.candidates[0].content.parts[0].inlineData.mimeType;\n    \n    console.log('Audio data length:', base64AudioData.length);\n    console.log('Mime type:', mimeType);\n    \n    // CRITICAL FIX: Properly decode base64 to actual audio bytes\n    const actualAudioBytes = Buffer.from(base64AudioData, 'base64');\n    \n    console.log('Decoded audio size:', actualAudioBytes.length);\n    \n    // Create WAV file with proper PCM16 format\n    const sampleRate = 24000;\n    const channels = 1;\n    const bitsPerSample = 16;\n    \n    // Create WAV header\n    const wavHeader = createWavHeader(actualAudioBytes.length, sampleRate, channels, bitsPerSample);\n    const wavFile = Buffer.concat([wavHeader, actualAudioBytes]);\n    \n    const filename = `tts_fixed_${Date.now()}.wav`;\n    \n    return {\n      json: {\n        success: true,\n        filename: filename,\n        audioSize: wavFile.length,\n        audioSizeMB: (wavFile.length / 1024 / 1024).toFixed(2),\n        originalBase64Length: base64AudioData.length,\n        decodedAudioLength: actualAudioBytes.length\n      },\n      binary: {\n        audio: {\n          data: wavFile,\n          mimeType: 'audio/wav',\n          fileName: filename\n        }\n      }\n    };\n    \n  } catch (error) {\n    return {\n      json: {\n        success: false,\n        error: error.message,\n        step: 'audio_processing'\n      }\n    };\n  }\n} else {\n  return {\n    json: {\n      success: false,\n      error: 'TTS generation failed',\n      finishReason: response.candidates?.[0]?.finishReason || 'unknown',\n      response: response\n    }\n  };\n}\n\n// Helper function to create WAV header\nfunction createWavHeader(dataLength, sampleRate, channels, bitsPerSample) {\n  const byteRate = sampleRate * channels * bitsPerSample / 8;\n  const blockAlign = channels * bitsPerSample / 8;\n  const header = Buffer.alloc(44);\n  \n  // \"RIFF\" chunk descriptor\n  header.write('RIFF', 0);\n  header.writeUInt32LE(36 + dataLength, 4);\n  header.write('WAVE', 8);\n  \n  // \"fmt \" sub-chunk\n  header.write('fmt ', 12);\n  header.writeUInt32LE(16, 16); // Sub-chunk size\n  header.writeUInt16LE(1, 20);  // Audio format (PCM)\n  header.writeUInt16LE(channels, 22);\n  header.writeUInt32LE(sampleRate, 24);\n  header.writeUInt32LE(byteRate, 28);\n  header.writeUInt16LE(blockAlign, 32);\n  header.writeUInt16LE(bitsPerSample, 34);\n  \n  // \"data\" sub-chunk\n  header.write('data', 36);\n  header.writeUInt32LE(dataLength, 40);\n  \n  return header;\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "36978e98-5d80-4859-868f-5f8e6ddcb569",
      "name": "💾 Save Podcast Audio",
      "type": "n8n-nodes-base.writeBinaryFile",
      "position": [
        1136,
        -60
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.filename }}",
        "dataPropertyName": "audio"
      },
      "typeVersion": 1
    },
    {
      "id": "0deff03c-60f7-4fd0-84e2-96527caba405",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -360,
        -680
      ],
      "parameters": {
        "width": 320,
        "height": 400,
        "content": "## 🎯 Main Template Overview\nWhat this workflow does:\n- Extracts text from uploaded PDF files\n- Generates conversational podcast script using AI\n- Converts script to natural-sounding audio\n- Saves final podcast as WAV file\n\nSetup required:\n- Google Gemini API key (for AI script generation)\n- Google PaLM API credentials (for TTS)\n- File upload capability"
      },
      "typeVersion": 1
    },
    {
      "id": "b867911c-8add-450c-89d8-906cace883ad",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -20,
        -680
      ],
      "parameters": {
        "color": 5,
        "width": 300,
        "height": 400,
        "content": "## 🔧 Setup Instructions\nBefore You Start:\n\nGet API Keys:\n- Gemini API: https://aistudio.google.com/\n- Add to n8n credentials as \"Google Gemini(PaLM) Api account\"\n\n\nTest the workflow:\n- Use the manual trigger\n- Upload a PDF file\n- Check that audio file is generated"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Google Gemini Flash 2.0": {
      "ai_languageModel": [
        [
          {
            "node": "🤖 Generate Podcast Script",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "⚙️ Prepare TTS Request": {
      "main": [
        [
          {
            "node": "🎙️ Convert Text to Speech With Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "📄 Extract Text from PDF": {
      "main": [
        [
          {
            "node": "🤖 Generate Podcast Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🎬 Start: Upload PDF File": {
      "main": [
        [
          {
            "node": "📄 Extract Text from PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🔧 Process Audio Response": {
      "main": [
        [
          {
            "node": "💾 Save Podcast Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🤖 Generate Podcast Script": {
      "main": [
        [
          {
            "node": "⚙️ Prepare TTS Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🎙️ Convert Text to Speech With Gemini": {
      "main": [
        [
          {
            "node": "🔧 Process Audio Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {},
  "active": false
}
