{
  "name": "GEMINI 3.1 GENERATE v2",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        28992,
        20176
      ],
      "id": "9181a364-0ab8-4fff-9b66-3cd6e9adba90",
      "name": "When clicking \u2018Execute workflow\u2019"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "porto-tools-image",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        28992,
        20416
      ],
      "id": "74055aca-c88a-47bf-9bb1-e5a5d79b7863",
      "name": "Webhook (PORTO)",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "projectId",
              "name": "projectId",
              "value": "={{ $env.GCP_PROJECT_ID || 'project-8fa51551-4f6c-4ff6-8b7' }}",
              "type": "string"
            },
            {
              "id": "accessToken",
              "name": "accessToken",
              "value": "={{ $env.GCP_ACCESS_TOKEN }}",
              "type": "string"
            },
            {
              "id": "prompt",
              "name": "prompt",
              "value": "Generate a premium product image on a clean studio background. High detail, realistic lighting, sharp focus.",
              "type": "string"
            },
            {
              "id": "aspectRatio",
              "name": "aspectRatio",
              "value": "1:1",
              "type": "string"
            },
            {
              "id": "source",
              "name": "source",
              "value": "manual",
              "type": "string"
            },
            {
              "id": "requestId",
              "name": "requestId",
              "value": "={{ $now.toISO() }}",
              "type": "string"
            },
            {
              "id": "references",
              "name": "references",
              "value": "={{ [] }}",
              "type": "array"
            },
            {
              "id": "referenceMapping",
              "name": "referenceMapping",
              "value": "={{ ({}) }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "5138585b-c3ae-4387-aa82-db02b30067b7",
      "name": "Manual Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        29312,
        20176
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "projectId",
              "name": "projectId",
              "value": "={{ $env.GCP_PROJECT_ID || 'project-8fa51551-4f6c-4ff6-8b7' }}",
              "type": "string"
            },
            {
              "id": "accessToken",
              "name": "accessToken",
              "value": "={{ $env.GCP_ACCESS_TOKEN }}",
              "type": "string"
            },
            {
              "id": "prompt",
              "name": "prompt",
              "value": "={{ $json.body.prompt }}",
              "type": "string"
            },
            {
              "id": "aspectRatio",
              "name": "aspectRatio",
              "value": "={{ $json.body.aspectRatio || '1:1' }}",
              "type": "string"
            },
            {
              "id": "source",
              "name": "source",
              "value": "porto-web",
              "type": "string"
            },
            {
              "id": "requestId",
              "name": "requestId",
              "value": "={{ $json.body.requestId }}",
              "type": "string"
            },
            {
              "id": "userEmail",
              "name": "userEmail",
              "value": "={{ $json.body.userEmail }}",
              "type": "string"
            },
            {
              "id": "references",
              "name": "references",
              "value": "={{ $json.body.references || [] }}",
              "type": "array"
            },
            {
              "id": "referenceMapping",
              "name": "referenceMapping",
              "value": "={{ $json.body.referenceMapping || {} }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "99f16994-e25c-4d6e-9041-f7adeefddabc",
      "name": "Webhook Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        29312,
        20416
      ]
    },
    {
      "parameters": {
        "jsCode": "const refs = Array.isArray($json.references) ? $json.references : [];\nconst downloaded = [];\nfor (const r of refs) {\n  if (!r || typeof r.url !== 'string') continue;\n  const buf = await this.helpers.httpRequest({\n    method: 'GET',\n    url: r.url,\n    encoding: 'arraybuffer',\n    returnFullResponse: false,\n  });\n  const base64 = Buffer.from(buf).toString('base64');\n  downloaded.push({ mimeType: r.mimeType || 'image/png', data: base64 });\n}\nreturn [{ json: { ...$json, refsBase64: downloaded } }];"
      },
      "id": "c3a91e2f-6b7d-4e8a-9c1f-2d4e5f6a7b8c",
      "name": "Download References",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        29504,
        20304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://aiplatform.googleapis.com/v1/projects/{{$json.projectId}}/locations/global/publishers/google/models/gemini-3.1-flash-image-preview:generateContent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{$json.accessToken}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ (() => { const refs = Array.isArray($json.refsBase64) ? $json.refsBase64 : []; let p = $json.prompt || ''; const map = $json.referenceMapping || {}; for (const [token, idx] of Object.entries(map)) { const num = Number(idx) + 1; p = p.split(token).join('image #' + num); } const parts = refs.map(r => ({ inlineData: { mimeType: r.mimeType, data: r.data } })); parts.push({ text: p }); return { contents: [{ role: 'user', parts }], generationConfig: { responseModalities: ['TEXT', 'IMAGE'], imageConfig: { aspectRatio: $json.aspectRatio } } }; })() }}",
        "options": {}
      },
      "id": "a184784f-f933-4b71-979e-5a95e7d88d76",
      "name": "Vertex Gemini 3.1 Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        29632,
        20304
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const upstream = $('Vertex Gemini 3.1 Image').first().json;\n\nfunction readConfigNode(nodeName) {\n  try {\n    const item = $(nodeName).first().json;\n    return item?.source ? item : null;\n  } catch (error) {\n    return null;\n  }\n}\n\nfunction asObject(value) {\n  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;\n}\n\nfunction asArray(value) {\n  return Array.isArray(value) ? value : [];\n}\n\nfunction firstString(...values) {\n  for (const value of values) {\n    if (typeof value === 'string' && value.trim()) return value.trim();\n  }\n  return '';\n}\n\nfunction collectParts(response) {\n  const candidates = asArray(response?.candidates);\n  const parts = [];\n\n  for (const candidate of candidates) {\n    parts.push(...asArray(candidate?.content?.parts));\n    parts.push(...asArray(candidate?.content?.role?.parts));\n  }\n\n  parts.push(...asArray(response?.content?.parts));\n  parts.push(...asArray(response?.parts));\n\n  return parts.filter(Boolean);\n}\n\nfunction readInlineData(part) {\n  const inlineData = asObject(part?.inlineData) || asObject(part?.inline_data);\n  if (!inlineData) return null;\n\n  const rawData = firstString(inlineData.data, inlineData.imageBase64, inlineData.base64);\n  if (!rawData) return null;\n\n  const dataUrlMatch = rawData.match(/^data:([^;]+);base64,(.+)$/);\n  const mimeType = firstString(\n    inlineData.mimeType,\n    inlineData.mime_type,\n    dataUrlMatch?.[1],\n    'image/png',\n  );\n  const imageBase64 = (dataUrlMatch?.[2] || rawData).replace(/\\s+/g, '');\n\n  return { mimeType, imageBase64 };\n}\n\nfunction isProbablyBase64(value) {\n  return /^[A-Za-z0-9+/]+={0,2}$/.test(value) && value.length % 4 === 0;\n}\n\nfunction extensionFromMime(mimeType) {\n  if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'jpg';\n  if (mimeType.includes('webp')) return 'webp';\n  return 'png';\n}\n\nconst sourceItem = readConfigNode('Webhook Config') || readConfigNode('Manual Config') || {};\nconst parts = collectParts(upstream);\nconst imagePart = parts.find((part) => readInlineData(part));\nconst textPart = parts.find((part) => typeof part?.text === 'string' && part.text.trim());\n\nif (!imagePart) {\n  const candidateCount = asArray(upstream?.candidates).length;\n  const partKeys = parts.map((part) => Object.keys(asObject(part) || {}).join('|')).filter(Boolean);\n  throw new Error(\n    'Tidak ada inline image dari Gemini response. candidates=' + candidateCount +\n    '; parts=' + parts.length +\n    '; partKeys=' + (partKeys.join(', ') || 'none') +\n    '. Pastikan responseModalities berisi IMAGE dan model mendukung image output.'\n  );\n}\n\nconst inline = readInlineData(imagePart);\nif (!inline?.imageBase64) {\n  throw new Error('Inline image ditemukan, tetapi field data/base64 kosong.');\n}\n\nif (!isProbablyBase64(inline.imageBase64)) {\n  throw new Error('Inline image data bukan base64 valid. Cek output Gemini/HTTP node.');\n}\n\nconst imageBytes = Buffer.from(inline.imageBase64, 'base64');\nif (!imageBytes.length) {\n  throw new Error('Inline image base64 valid tetapi menghasilkan file kosong.');\n}\n\nconst mimeType = inline.mimeType;\nconst extension = extensionFromMime(mimeType);\nconst requestId = firstString(sourceItem.requestId, $execution.id, Date.now().toString());\n\nreturn [{\n  json: {\n    ok: true,\n    model: 'gemini-3.1-flash-image-preview',\n    mimeType,\n    imageBase64: inline.imageBase64,\n    imageSizeBytes: imageBytes.length,\n    text: textPart?.text ?? '',\n    source: sourceItem.source || 'manual',\n    requestId,\n    aspectRatio: sourceItem.aspectRatio || '1:1',\n    userEmail: sourceItem.userEmail || null,\n  },\n  binary: {\n    image: {\n      data: inline.imageBase64,\n      mimeType,\n      fileName: 'gemini-3-1-output-' + requestId + '.' + extension,\n      fileExtension: extension,\n      fileSize: imageBytes.length,\n    },\n  },\n}];"
      },
      "id": "b7a545d7-1ae8-49af-a4f8-67bf068f0e08",
      "name": "Convert Image to Binary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        29888,
        20304
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "source-porto-web",
                    "leftValue": "={{ $json.source }}",
                    "rightValue": "porto-web",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "porto-web"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "manual"
        }
      },
      "id": "8811a762-1864-4cb7-aeba-181387af6499",
      "name": "Route by source",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        30144,
        20304
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, source: $json.source, requestId: $json.requestId, model: $json.model, aspectRatio: $json.aspectRatio, mimeType: $json.mimeType, imageBase64: $json.imageBase64, imageSizeBytes: $json.imageSizeBytes, text: $json.text || '' } }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "ce409c00-0980-4c50-bafd-65119501b3cc",
      "name": "Respond to Webhook (PORTO)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        30432,
        20176
      ]
    },
    {
      "parameters": {
        "operation": "sendPhoto",
        "chatId": "-1003986112540",
        "binaryData": true,
        "binaryPropertyName": "image",
        "additionalFields": {
          "caption": "={{ $json.text || 'Generated with Gemini 3.1 Flash Image Preview' }}"
        }
      },
      "id": "91654968-dfe5-4f36-bdfb-dc233d37f5f0",
      "name": "Send Image to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        30432,
        20416
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Manual Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook (PORTO)": {
      "main": [
        [
          {
            "node": "Webhook Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Config": {
      "main": [
        [
          {
            "node": "Download References",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Config": {
      "main": [
        [
          {
            "node": "Download References",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download References": {
      "main": [
        [
          {
            "node": "Vertex Gemini 3.1 Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vertex Gemini 3.1 Image": {
      "main": [
        [
          {
            "node": "Convert Image to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Image to Binary": {
      "main": [
        [
          {
            "node": "Route by source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by source": {
      "main": [
        [
          {
            "node": "Respond to Webhook (PORTO)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Image to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a70e7b37-37e8-4c67-baaa-8cfdcd53073f",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "I7lkeFnnbHg0nPGP",
  "tags": []
}