{
  "name": "AI News Video - COMPLETE WITH GOOGLE TTS (JWT token flow)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ai-news-google-tts-complete",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        250,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Generate 3 AI news items\nconst articles = [\n  { title: 'OpenAI Unveils GPT-5 with Revolutionary Reasoning Capabilities', snippet: 'OpenAI announces GPT-5 featuring breakthrough reasoning abilities and unprecedented accuracy', index: 0 },\n  { title: 'Google Achieves Major Quantum AI Computing Breakthrough', snippet: 'Google reveals groundbreaking advancement in quantum computing integrated with AI', index: 1 },\n  { title: 'Microsoft Copilot Receives Enterprise-Grade AI Upgrade', snippet: 'Microsoft enhances Copilot with advanced AI capabilities and enterprise security', index: 2 }\n];\nreturn articles.map(a => ({ json: a }));"
      },
      "id": "generate-news",
      "name": "Generate News",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        450,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Generate script\nconst title = String($json.title || 'AI News');\nconst snippet = String($json.snippet || '');\nconst index = Number($json.index || 0);\nconst templates = [\n  `Breaking AI news: ${title}. ${snippet}. This marks a major milestone in artificial intelligence.`,\n  `Major update: ${title}. ${snippet}. Industry experts call this a game-changing advancement.`,\n  `Latest AI: ${title}. ${snippet}. This innovation promises to revolutionize technology.`\n];\nlet script = templates[index % templates.length];\nif (script.length > 180) script = script.substring(0, 177) + '...';\nreturn { json: { title, snippet, script, index } };\n"
      },
      "id": "generate-script",
      "name": "Generate Script",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        650,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Create JWT and request OAuth access token using service account JSON\n// NOTE: this node contains your service account json. If you want it to be stored elsewhere, replace SA_JSON.\nconst SA_JSON = {\n  \"type\": \"service_account\",\n  \"project_id\": \"lateral-goods-477606-i1\",\n  \"private_key_id\": \"5c6d4ca06ed90142ec2a6e8f37d2b59e6d823277\",\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCi7LsMHfCAow3r\\nHt6soc0ktc2HNKBXeNmbb9TdxJ4lKo3vkdqnA5kcC+Cext9g6JL67iJLFCtm7b7U\\nzSzTCld0KBWVymQk1wjNI//qgBHRHVc9MIorwA+XAhVsU2olhC6YXwUrc3+GY4ai\\nUR2AlJq+ooXqTAcikMQWhpZVpl8zOUoUxGsZR1m6R4Wxo8wgHX3f7p/SPnq3sg8S\\nVKX3G31qVCuwwlJO4FyYWSzIMDpqDT/0M8xoFXQa99jbdTX/QDQCBpcGnGHWArvS\\nh92Dz1rshlojl3P0pK42tBJ4Qp0WDOLEqJesOAXs32jfeeXSLfCMIGlwUYnEyc/v\\nBxcpcY57AgMBAAECggEAAzTsqOuYLVuDHNwASTMA9RAEHdQwyDYMPfC5te8+NpIt\\nnDNxHxfH1kKaETQMtLLzqfbzX3HPdvY1Dt0dhbQY9x63+ZA49Imh2MulPOQwvUOo\\nLTLc0iViuIchprwD+Jck9jJ6gX2NwX4dl/Oj97vqzoXBMNnF6i2xLlij9xkLDGf7\\n4GZbT4gx/ZzpzwalUTjt1QMYChgohBZbbMK7SaXXhWvd5A3UCvvxBhxKyKYAlTDO\\nPenxS1b8hLvkzc8ECbzmIPQIp1YJrzz+QCe2Vkzmdi5nmQq8nD1WCoLEn7bCWNoZ\\nrwUIhlk9MOaiOiW+AYR9aub32LNHypgXRph/6u9GgQKBgQDc3MmF004PuNaED9UV\\nCQah4u6JG4w5ioqLjujQ793SbLdaC/Iaxu0JuX9Y7zgU6zXJKz1zYOf/T5voqbar\\nYkkko9kq/lsQ5l422gTCkrdAGmj0fLDfjr1FG6BkSfmeOQmoZ5Uf6GVAjQP5I6c9\\nr1e51o12pxmxAZLAG1fQja89MwKBgQC82EZunX2HFzAnFPvleiDkBJCE4tvHWfXi\\nSIjIODw6skFIKkRqv2VZtAH6PeryT0C2vMuzXhBl1IypPFqogLVrX1YAixY+7rLM\\niddcn0nnBZtECgSDY/5ULbbwsGBWpRGV6Pv/MOWxQyeYu1ImaNXKO0bUHTCi4px+\\nZo8ujlQZmQKBgQCNMrG7Vq2vK3IpF54YRp7w3A23pd7t4n5UXlbFTLQ5lLtbXAu5\\nxrc/4lFh3/2wkfbe10AABVIMTS7VfbqEst8kB4QNEnPRyBUvaA5m/jkdSEUVGKpT\\nIgQqrFDMDOcCmmBsQ1x4+6/PpteFbZ+7td+VtW7XDllEakcRfemUMSB5NQKBgQCZ\\n3oLs6Ef6hYtHnNJuNSeNgqaakBnRgdxWBxHkSeXRUaLdgQsEC3UyNPiThFXmH2s0\\nOfqj6JXl0tzVnAamW1D27tQtVybGGkn3XKzsnCFkKm5LbvokcJouzpzL2np0vsTo\\nZ9DEKnxNBdHCoYabIzpnMAtTE4GohopKd5hcr72YqQKBgCudbVkwO57mXQ0ligDp\\nP9rOLTuH4YVXNcj3yVLxLdp7wALzGwUVShjwigWoXzUWef2pxDfwWAPDhQ1dt0sq\\n+qcyzQdw/rGkQ+FHeoq+GrLr19u8ef/uWFgtcoDksbntQvVkM2JwrSmefWSZ4Atm\\nVvZ/xk5KOreIjUfFmMuuUAxL\\n-----END PRIVATE KEY-----\\n\",\n  \"client_email\": \"vertex-ai-n8n@lateral-goods-477606-i1.iam.gserviceaccount.com\",\n  \"client_id\": \"104790218756773876337\",\n  \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n  \"token_uri\": \"https://oauth2.googleapis.com/token\",\n  \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n  \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-n8n%40lateral-goods-477606-i1.iam.gserviceaccount.com\",\n  \"universe_domain\": \"googleapis.com\"\n};\n\nconst crypto = require('crypto');\nconst https = require('https');\n\nfunction base64url(input) {\n  return input.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nasync function postJson(url, body, headers = {}) {\n  return new Promise((resolve, reject) => {\n    const u = new URL(url);\n    const data = JSON.stringify(body);\n    const opts = {\n      method: 'POST',\n      hostname: u.hostname,\n      path: u.pathname + (u.search || ''),\n      headers: Object.assign({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, headers)\n    };\n    const req = https.request(opts, (res) => {\n      let out = '';\n      res.on('data', (d) => out += d);\n      res.on('end', () => {\n        try { resolve(JSON.parse(out)); } catch (e) { reject(new Error('Invalid JSON response: ' + out)); }\n      });\n    });\n    req.on('error', reject);\n    req.write(data);\n    req.end();\n  });\n}\n\n(async () => {\n  try {\n    const iat = Math.floor(Date.now() / 1000);\n    const exp = iat + 3600;\n    const header = { alg: 'RS256', typ: 'JWT' };\n    const scope = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/cloud-platform.read-only';\n    const payload = {\n      iss: SA_JSON.client_email,\n      sub: SA_JSON.client_email,\n      scope: scope,\n      aud: SA_JSON.token_uri,\n      iat: iat,\n      exp: exp\n    };\n    const signingInput = base64url(Buffer.from(JSON.stringify(header))) + '.' + base64url(Buffer.from(JSON.stringify(payload)));\n    const signer = crypto.createSign('RSA-SHA256');\n    signer.update(signingInput);\n    signer.end();\n    const signature = signer.sign(SA_JSON.private_key);\n    const jwt = signingInput + '.' + base64url(signature);\n\n    // Exchange JWT for access token\n    const tokenResp = await postJson(SA_JSON.token_uri, {\n      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n      assertion: jwt\n    }, { 'Content-Type': 'application/x-www-form-urlencoded' });\n\n    if (!tokenResp.access_token) throw new Error('Token response missing access_token: ' + JSON.stringify(tokenResp));\n\n    return [{ json: { access_token: tokenResp.access_token, expires_in: tokenResp.expires_in, token_type: tokenResp.token_type } }];\n  } catch (err) {\n    throw new Error('Failed to obtain access token: ' + err.message);\n  }\n})();\n"
      },
      "id": "get-access-token",
      "name": "Create JWT & Get Access Token",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        850,
        480
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "multiplex",
        "options": {}
      },
      "id": "merge-data",
      "name": "Merge Script + Token",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2.1,
      "position": [
        1050,
        480
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://texttospeech.googleapis.com/v1/text:synthesize",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $node['Create JWT & Get Access Token'].json.access_token }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"input\": { \"text\": {{ JSON.stringify($json.script) }} },\n  \"voice\": { \"languageCode\": \"en-US\", \"name\": \"en-US-Neural2-J\", \"ssmlGender\": \"MALE\" },\n  \"audioConfig\": { \"audioEncoding\": \"MP3\", \"speakingRate\": 1.0, \"pitch\": 0.0, \"volumeGainDb\": 0.0, \"sampleRateHertz\": 24000 }\n}",
        "options": {}
      },
      "id": "gemini-tts-call",
      "name": "Google Cloud TTS API Call",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1250,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Convert base64 audio from Google TTS API response to binary\nconst title = String($input.item.json.title || 'AI News');\nconst snippet = String($input.item.json.snippet || '');\nconst script = String($input.item.json.script || '');\nconst index = Number($input.item.json.index || 0);\n\nconst audioContent = $input.item.json.audioContent || $input.item.json.audioContent;\nif (!audioContent) {\n  throw new Error('No audio content received from Google TTS API');\n}\n\nconst audioBuffer = Buffer.from(audioContent, 'base64');\nreturn {\n  json: { title, snippet, script, index, voiceFile: `voice_${index}.mp3` },\n  binary: { audio: { data: audioBuffer.toString('base64'), mimeType: 'audio/mpeg', fileName: `voice_${index}.mp3`, fileExtension: 'mp3' } }\n};"
      },
      "id": "prepare-audio-data",
      "name": "Prepare Audio Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1450,
        480
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "=voice_{{ $json.index }}.mp3",
        "dataPropertyName": "audio",
        "options": {
          "folderPath": "d:/AI-AGENT/output"
        }
      },
      "id": "save-voice",
      "name": "Save Voice File",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        1650,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Generate subtitles\nconst script = String($input.item.json.script || '');\nconst index = Number($input.item.json.index || 0);\nconst words = script.split(/\\s+/);\nconst chunks = [];\nlet currentChunk = [];\nlet chunkLength = 0;\nfor (const word of words) {\n  currentChunk.push(word);\n  chunkLength += word.length + 1;\n  if (chunkLength > 55 || /[.!?,]$/.test(word)) { chunks.push(currentChunk.join(' ')); currentChunk = []; chunkLength = 0; }\n}\nif (currentChunk.length > 0) chunks.push(currentChunk.join(' '));\nif (chunks.length === 0) chunks.push(script);\nlet srtContent = '';\nlet startTime = 0;\nchunks.forEach((chunk, i) => {\n  const duration = Math.max(2, Math.ceil(chunk.length / 12));\n  const endTime = startTime + duration;\n  const formatTime = (sec) => {\n    const h = String(Math.floor(sec / 3600)).padStart(2, '0');\n    const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');\n    const s = String(Math.floor(sec % 60)).padStart(2, '0');\n    return `${h}:${m}:${s},000`;\n  };\n  srtContent += `${i + 1}\\n${formatTime(startTime)} --> ${formatTime(endTime)}\\n${chunk.trim()}\\n\\n`;\n  startTime = endTime;\n});\nconst buffer = Buffer.from(srtContent, 'utf-8');\nreturn { json: { title: $input.item.json.title, script: script, index: index, voiceFile: `voice_${index}.mp3`, subtitleFile: `subtitles_${index}.srt`, duration: startTime }, binary: { data: { data: buffer.toString('base64'), mimeType: 'text/plain', fileName: `subtitles_${index}.srt`, fileExtension: 'srt' } } };\n"
      },
      "id": "generate-subtitles",
      "name": "Generate Subtitles",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1850,
        480
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.subtitleFile }}",
        "dataPropertyName": "data",
        "options": {
          "folderPath": "d:/AI-AGENT/output"
        }
      },
      "id": "save-subtitles",
      "name": "Save Subtitles",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        2050,
        480
      ]
    },
    {
      "parameters": {
        "command": "=cd d:\\AI-AGENT\\output && ffmpeg -y -loop 1 -i \"d:\\AI-AGENT\\ai_background.jpg\" -i \"{{ $json.voiceFile }}\" -vf \"scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,subtitles={{ $json.subtitleFile }}:force_style='FontName=Arial Bold,FontSize=28,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,BorderStyle=3,Outline=3,Shadow=2,MarginV=60,Alignment=2'\" -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest -t {{ $json.duration }} \"video_{{ $json.index }}.mp4\"",
        "options": {}
      },
      "id": "create-video",
      "name": "Create Video",
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        2050,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Aggregate results\nconst allItems = $input.all();\nconst videos = [];\nfor (const item of allItems) {\n  const index = item.json.index || 0;\n  videos.push({ index: index, title: item.json.title || 'Untitled', videoFile: `video_${index}.mp4`, voiceFile: `voice_${index}.mp3`, subtitleFile: `subtitles_${index}.srt`, duration: item.json.duration || 20 });\n}\nvideos.sort((a,b)=>a.index-b.index);\nconst fileListContent = videos.map(v => `file 'video_${v.index}.mp4'`).join('\\n');\nconst fileListBuffer = Buffer.from(fileListContent,'utf-8');\nconst totalDuration = videos.reduce((sum,v)=>sum+v.duration,0);\nconst summary = `\u2705 SUCCESS: ${videos.length} videos created with Google Cloud TTS!\\n\\nTotal Duration: ${totalDuration}s\\n\\nFiles:\\n${videos.map((v,i)=>`${i+1}. ${v.videoFile}`).join('\\n')}\\n\\nFinal: AI_NEWS_FINAL_COMPLETE.mp4`;\nconst summaryBuffer = Buffer.from(summary,'utf-8');\nreturn [{ json: { success: true, totalVideos: videos.length, totalDuration: totalDuration, videos: videos, outputFolder: 'd:/AI-AGENT/output/' }, binary: { summary: { data: summaryBuffer.toString('base64'), mimeType: 'text/plain', fileName: 'SUMMARY.txt', fileExtension: 'txt' }, filelist: { data: fileListBuffer.toString('base64'), mimeType: 'text/plain', fileName: 'filelist.txt', fileExtension: 'txt' } } }];"
      },
      "id": "aggregate",
      "name": "Aggregate Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2250,
        480
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "filelist.txt",
        "dataPropertyName": "filelist",
        "options": {
          "folderPath": "d:/AI-AGENT/output"
        }
      },
      "id": "save-filelist",
      "name": "Save Filelist",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        2450,
        480
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "SUMMARY.txt",
        "dataPropertyName": "summary",
        "options": {
          "folderPath": "d:/AI-AGENT/output"
        }
      },
      "id": "save-summary",
      "name": "Save Summary",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        2650,
        480
      ]
    },
    {
      "parameters": {
        "command": "cd d:\\AI-AGENT\\output && ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy AI_NEWS_FINAL_COMPLETE.mp4",
        "options": {}
      },
      "id": "final-video",
      "name": "Create Final Video",
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        2850,
        480
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { success: true, message: '\ud83c\udf89 Video Ready with Google Cloud TTS!', finalVideo: 'd:/AI-AGENT/output/AI_NEWS_FINAL_COMPLETE.mp4', videos: $json.videos, totalDuration: $json.totalDuration } }}"
      },
      "id": "response",
      "name": "Send Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        3050,
        480
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Generate News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate News": {
      "main": [
        [
          {
            "node": "Generate Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Script": {
      "main": [
        [
          {
            "node": "Create JWT & Get Access Token",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Script + Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create JWT & Get Access Token": {
      "main": [
        [
          {
            "node": "Merge Script + Token",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Script + Token": {
      "main": [
        [
          {
            "node": "Google Cloud TTS API Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Cloud TTS API Call": {
      "main": [
        [
          {
            "node": "Prepare Audio Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Audio Data": {
      "main": [
        [
          {
            "node": "Save Voice File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Voice File": {
      "main": [
        [
          {
            "node": "Generate Subtitles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Subtitles": {
      "main": [
        [
          {
            "node": "Save Subtitles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Subtitles": {
      "main": [
        [
          {
            "node": "Create Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Video": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Results": {
      "main": [
        [
          {
            "node": "Save Filelist",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Filelist": {
      "main": [
        [
          {
            "node": "Create Final Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Summary": {
      "main": [
        [
          {
            "node": "Create Final Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Final Video": {
      "main": [
        [
          {
            "node": "Send Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}