{
  "name": "Image Generator - Nano Banana Pro + GitHub Storage",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "gerar-imagem",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        250,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"status\": \"received\", \"post_id\": $json.post_id } }}"
      },
      "id": "webhook-response",
      "name": "Responder Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        450,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "resource": "row",
        "operation": "get",
        "tableId": "posts",
        "returnAll": false,
        "limit": 1,
        "filterType": "string",
        "filterString": "=id=eq.{{ $json.post_id }}"
      },
      "id": "get-post",
      "name": "Buscar Post Completo",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        650,
        300
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Extrair tema principal e contexto\nconst post = $json;\n\n// Extrair primeiras palavras-chave (mais relevantes)\nconst palavrasChavePrincipais = (post.palavras_chave || []).slice(0, 3).join(', ');\n\n// Extrair primeiro par\u00e1grafo do conte\u00fado (contexto)\nconst conteudoLimpo = (post.conteudo || '')\n  .replace(/#+\\s/g, '') // Remove markdown headers\n  .replace(/\\*\\*/g, '') // Remove bold\n  .replace(/\\n+/g, ' ') // Remove quebras de linha\n  .trim();\n\nconst primeiroParagrafo = conteudoLimpo.split('.').slice(0, 2).join('.') + '.';\n\nreturn [{\n  json: {\n    post_id: post.id,\n    titulo: post.titulo || '',\n    slug: post.slug || '',\n    plataforma: post.plataforma || 'blog',\n    palavras_chave_principais: palavrasChavePrincipais || 'tecnologia, inova\u00e7\u00e3o',\n    contexto: primeiroParagrafo.substring(0, 300) || post.titulo || 'Artigo sobre tecnologia e inova\u00e7\u00e3o',\n    conteudo_completo: post.conteudo || ''\n  }\n}];"
      },
      "id": "extract-context",
      "name": "Extrair Tema e Contexto",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        850,
        300
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Voc\u00ea \u00e9 um especialista em cria\u00e7\u00e3o de prompts para gera\u00e7\u00e3o de imagens com IA.\n\nBaseado no t\u00edtulo e contexto do artigo abaixo, crie um prompt detalhado e visual para gerar uma imagem de destaque profissional.\n\n**T\u00edtulo do Artigo:**\n{{ $json.titulo }}\n\n**Contexto:**\n{{ $json.contexto }}\n\n**Palavras-chave:**\n{{ $json.palavras_chave_principais }}\n\n**Requisitos do Prompt:**\n- Descrever uma cena visual espec\u00edfica e profissional\n- Incluir elementos relacionados ao tema (tecnologia, neg\u00f3cios, etc)\n- Estilo: moderno, clean, profissional, high-quality\n- Cores: vibrantes mas harmoniosas\n- Composi\u00e7\u00e3o: adequada para imagem de destaque de blog (16:9)\n- Evitar texto na imagem\n- Foco em conceitos visuais abstratos ou metaf\u00f3ricos\n\n**Formato da resposta:**\nRetorne APENAS o prompt em ingl\u00eas, sem introdu\u00e7\u00f5es ou explica\u00e7\u00f5es. O prompt deve ter entre 50-100 palavras.\n\n**Exemplo de bom prompt:**\n\"A modern digital illustration of a bustling Indian marketplace merging with futuristic technology, holographic shopping carts, vibrant colors of orange and blue, professional business atmosphere, high-quality 3D render, clean composition, 16:9 aspect ratio, studio lighting\"",
        "options": {
          "systemMessage": "Voc\u00ea \u00e9 um especialista em cria\u00e7\u00e3o de prompts para gera\u00e7\u00e3o de imagens. Responda apenas com o prompt solicitado, sem explica\u00e7\u00f5es."
        }
      },
      "id": "generate-prompt",
      "name": "IA: Gerar Prompt para Imagem",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.3,
      "position": [
        1050,
        200
      ]
    },
    {
      "parameters": {
        "options": {
          "temperature": 0.7,
          "maxOutputTokens": 300
        }
      },
      "id": "gemini-prompt-model",
      "name": "Gemini - Prompt",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1,
      "position": [
        1050,
        400
      ],
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Traduzir e otimizar prompt\nconst promptNode = $('IA: Gerar Prompt para Imagem').item.json;\nconst contextData = $('Extrair Tema e Contexto').item.json;\n\nlet promptBase = promptNode.output || promptNode.text || promptNode.content || '';\npromptBase = promptBase.trim();\n\n// Garantir que est\u00e1 em ingl\u00eas e adicionar par\u00e2metros de qualidade\nconst promptOtimizado = `${promptBase}, professional photography, high resolution, 4K quality, detailed, sharp focus, vibrant colors, modern aesthetic, no text, no watermark, clean composition, 16:9 aspect ratio`;\n\n// Limitar tamanho (Nano Banana Pro aceita at\u00e9 ~1000 caracteres)\nconst promptFinal = promptOtimizado.substring(0, 900);\n\nconsole.log('\ud83d\udcdd Prompt gerado:', promptFinal);\nconsole.log('\ud83d\udcca Tamanho do prompt:', promptFinal.length, 'caracteres');\n\nreturn [{\n  json: {\n    prompt_imagem: promptFinal,\n    post_id: contextData.post_id,\n    slug: contextData.slug,\n    titulo: contextData.titulo\n  }\n}];"
      },
      "id": "optimize-prompt",
      "name": "Otimizar Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1250,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image:generateContent",
        "authentication": "genericCredentialType",
        "genericAuthType": "queryAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "key",
              "value": "={{ $credentials.googlePalmApi.apiKey }}"
            }
          ]
        },
        "options": {
          "headers": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "bodyParameters": {
            "parameters": [
              {
                "name": "contents",
                "value": "={{ [{ parts: [{ text: $json.prompt_imagem }] }] }}"
              },
              {
                "name": "generationConfig",
                "value": "={{ { temperature: 0.4, candidateCount: 1, maxOutputTokens: 8192, responseMimeType: \"image/png\" } }}"
              }
            ]
          },
          "timeout": 60000
        },
        "sendBody": true,
        "bodyContentType": "json"
      },
      "id": "generate-image",
      "name": "Nano Banana Pro - Gerar Imagem",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1450,
        200
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Extrair imagem Base64 da resposta\nconst resposta = $json;\nconst contextData = $('Otimizar Prompt').item.json;\n\nlet imagemBase64;\n\ntry {\n  if (resposta.candidates && resposta.candidates[0]) {\n    const candidate = resposta.candidates[0];\n    const parts = candidate.content?.parts || [];\n    \n    // Procurar pela parte que cont\u00e9m a imagem\n    for (const part of parts) {\n      if (part.inlineData && part.inlineData.data) {\n        imagemBase64 = part.inlineData.data;\n        break;\n      }\n    }\n  }\n  \n  if (!imagemBase64) {\n    throw new Error('Imagem n\u00e3o encontrada na resposta da API');\n  }\n  \n  const tamanhoMB = (imagemBase64.length * 0.75 / 1024 / 1024).toFixed(2);\n  \n  console.log('\u2705 Imagem gerada com sucesso (base64)');\n  console.log(`\ud83d\udcca Tamanho aproximado: ${tamanhoMB} MB`);\n  \n  return [{\n    json: {\n      imagem_base64: imagemBase64,\n      post_id: contextData.post_id,\n      slug: contextData.slug,\n      prompt_usado: contextData.prompt_imagem,\n      tamanho_bytes: Math.floor(imagemBase64.length * 0.75)\n    }\n  }];\n  \n} catch (error) {\n  console.error('\u274c Erro ao extrair imagem:', error.message);\n  console.error('Resposta recebida:', JSON.stringify(resposta, null, 2));\n  throw error;\n}"
      },
      "id": "extract-image",
      "name": "Extrair Imagem Base64",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1650,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Preparar nome do arquivo e path GitHub\nconst slug = $json.slug || 'imagem-blog';\nconst timestamp = Date.now();\n\n// Sanitizar slug para nome de arquivo\nconst nomeArquivo = `${slug}-${timestamp}.png`\n  .toLowerCase()\n  .replace(/[^a-z0-9-.]/g, '-')\n  .replace(/-+/g, '-')\n  .substring(0, 100);\n\n// Configura\u00e7\u00f5es do GitHub (do .env ou hardcoded)\nconst githubConfig = {\n  owner: 'attivamente',\n  repo: 'imagensBlogELinkedin',\n  path: 'uploads',\n  branch: 'main',\n  token: '<redacted-credential>'\n};\n\n// Path completo no GitHub\nconst pathCompleto = `${githubConfig.path}/${nomeArquivo}`;\n\nconsole.log(`\ud83d\udcc1 Nome do arquivo: ${nomeArquivo}`);\nconsole.log(`\ud83d\udcc2 Path no GitHub: ${pathCompleto}`);\n\nreturn [{\n  json: {\n    imagem_base64: $json.imagem_base64,\n    post_id: $json.post_id,\n    slug: $json.slug,\n    prompt_usado: $json.prompt_usado,\n    nome_arquivo: nomeArquivo,\n    path_completo: pathCompleto,\n    github_config: githubConfig,\n    tamanho_bytes: $json.tamanho_bytes\n  }\n}];"
      },
      "id": "prepare-github",
      "name": "Preparar Nome e Path GitHub",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1850,
        200
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.github.com/repos/attivamente/imagensBlogELinkedin/contents/uploads/{{ $json.nome_arquivo }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "headerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=token {{ $json.github_config.token }}"
            },
            {
              "name": "Accept",
              "value": "application/vnd.github.v3+json"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "check-file",
      "name": "Verificar se Arquivo Existe",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2050,
        200
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Extrair SHA se arquivo existir\nconst verificacao = $json;\nconst githubData = $('Preparar Nome e Path GitHub').item.json;\n\nlet sha = null;\n\n// Se o arquivo j\u00e1 existe, a API retorna um SHA\nif (verificacao.sha) {\n  sha = verificacao.sha;\n  console.log(`\u26a0\ufe0f Arquivo j\u00e1 existe no GitHub. SHA: ${sha}`);\n} else {\n  console.log('\u2705 Arquivo n\u00e3o existe, ser\u00e1 criado novo');\n}\n\nreturn [{\n  json: {\n    ...githubData,\n    sha_existente: sha\n  }\n}];"
      },
      "id": "extract-sha",
      "name": "Extrair SHA se Existir",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2250,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Construir body com SHA se existir\nconst data = $('Extrair SHA se Existir').item.json;\nconst sha = data.sha_existente;\n\nconst body = {\n  message: `Add blog image: ${data.nome_arquivo}`,\n  content: data.imagem_base64,\n  branch: 'main'\n};\n\nif (sha) {\n  body.sha = sha;\n}\n\nreturn [{\n  json: {\n    ...data,\n    upload_body: body\n  }\n}];"
      },
      "id": "prepare-upload-body",
      "name": "Preparar Body Upload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2350,
        200
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.github.com/repos/attivamente/imagensBlogELinkedin/contents/uploads/{{ $json.nome_arquivo }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "headerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=token {{ $json.github_config.token }}"
            },
            {
              "name": "Accept",
              "value": "application/vnd.github.v3+json"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "timeout": 120000
        },
        "sendBody": true,
        "bodyContentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ $json.upload_body }}"
      },
      "id": "upload-github",
      "name": "Upload para GitHub",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2450,
        200
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Extrair URL p\u00fablica da imagem do GitHub\nconst respostaGithub = $json;\nconst githubData = $('Preparar Nome e Path GitHub').item.json;\n\n// Construir URL p\u00fablica raw.githubusercontent.com (recomendado)\nconst owner = githubData.github_config.owner;\nconst repo = githubData.github_config.repo;\nconst branch = githubData.github_config.branch;\nconst path = githubData.github_config.path;\nconst nomeArquivo = githubData.nome_arquivo;\n\nconst urlGithub = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}/${nomeArquivo}`;\n\n// URL alternativa da API (pode ter cache)\nconst downloadUrl = respostaGithub.content?.download_url || urlGithub;\n\nconsole.log('\ud83d\udd17 URL p\u00fablica da imagem:', urlGithub);\nconsole.log('\ud83d\udce5 Download URL (API):', downloadUrl);\n\nreturn [{\n  json: {\n    url_imagem: urlGithub,\n    url_imagem_alternativa: downloadUrl,\n    post_id: githubData.post_id,\n    nome_arquivo: nomeArquivo,\n    prompt_usado: githubData.prompt_usado,\n    github_sha: respostaGithub.content?.sha || respostaGithub.commit?.sha,\n    tamanho_bytes: githubData.tamanho_bytes\n  }\n}];"
      },
      "id": "extract-url",
      "name": "Extrair URL P\u00fablica",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2650,
        200
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "resource": "row",
        "operation": "update",
        "tableId": "posts",
        "filterType": "string",
        "filterString": "=id=eq.{{ $json.post_id }}",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldName": "url_imagem",
              "fieldValue": "={{ $json.url_imagem }}"
            }
          ]
        }
      },
      "id": "update-post",
      "name": "Atualizar Post no Banco",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        2850,
        200
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Log de sucesso\nconst resultado = $('Extrair URL P\u00fablica').item.json;\n\nconsole.log('\u2705 IMAGEM GERADA E SALVA NO GITHUB COM SUCESSO:');\nconsole.log(`\ud83d\udcdd Post ID: ${resultado.post_id}`);\nconsole.log(`\ud83d\uddbc\ufe0f URL da Imagem: ${resultado.url_imagem}`);\nconsole.log(`\ud83d\udcc1 Nome do Arquivo: ${resultado.nome_arquivo}`);\nconsole.log(`\ud83d\udd11 GitHub SHA: ${resultado.github_sha}`);\nconsole.log(`\ud83c\udfa8 Prompt Usado: ${resultado.prompt_usado}`);\nconsole.log(`\ud83d\udcca Tamanho: ${(resultado.tamanho_bytes / 1024 / 1024).toFixed(2)} MB`);\nconsole.log(`\u23f1\ufe0f Timestamp: ${new Date().toISOString()}`);\nconsole.log(`\ud83d\udce6 Reposit\u00f3rio: attivamente/imagensBlogELinkedin`);\n\nreturn [{\n  json: {\n    status: 'sucesso',\n    post_id: resultado.post_id,\n    url_imagem: resultado.url_imagem,\n    github_sha: resultado.github_sha,\n    nome_arquivo: resultado.nome_arquivo,\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "id": "log-success",
      "name": "Log de Sucesso",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3050,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Error Handler\nconst erro = $json.error || {};\nlet contexto = {};\n\ntry {\n  contexto = $('Webhook Trigger').item.json || {};\n} catch (e) {\n  // Fallback\n  contexto = { post_id: 'N/A' };\n}\n\nconsole.error('\u274c ERRO NO WORKFLOW DE GERA\u00c7\u00c3O DE IMAGEM:');\nconsole.error(`Mensagem: ${erro.message || 'Erro desconhecido'}`);\nconsole.error(`Post ID: ${contexto.post_id || 'N/A'}`);\nconsole.error(`Stack: ${erro.stack || 'N/A'}`);\n\n// Usar imagem placeholder do pr\u00f3prio GitHub\nconst imagemPlaceholder = 'https://raw.githubusercontent.com/attivamente/imagensBlogELinkedin/main/uploads/placeholder-blog.png';\n\nreturn [{\n  json: {\n    status: 'erro',\n    post_id: contexto.post_id,\n    url_imagem_fallback: imagemPlaceholder,\n    erro_mensagem: erro.message || 'Erro desconhecido',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "id": "error-handler",
      "name": "Error Handler",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3250,
        400
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Responder Webhook",
            "type": "main",
            "index": 0
          },
          {
            "node": "Buscar Post Completo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Post Completo": {
      "main": [
        [
          {
            "node": "Extrair Tema e Contexto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair Tema e Contexto": {
      "main": [
        [
          {
            "node": "IA: Gerar Prompt para Imagem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IA: Gerar Prompt para Imagem": {
      "main": [
        [
          {
            "node": "Otimizar Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini - Prompt": {
      "ai_languageModel": [
        [
          {
            "node": "IA: Gerar Prompt para Imagem",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Otimizar Prompt": {
      "main": [
        [
          {
            "node": "Nano Banana Pro - Gerar Imagem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Nano Banana Pro - Gerar Imagem": {
      "main": [
        [
          {
            "node": "Extrair Imagem Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair Imagem Base64": {
      "main": [
        [
          {
            "node": "Preparar Nome e Path GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preparar Nome e Path GitHub": {
      "main": [
        [
          {
            "node": "Verificar se Arquivo Existe",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verificar se Arquivo Existe": {
      "main": [
        [
          {
            "node": "Extrair SHA se Existir",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair SHA se Existir": {
      "main": [
        [
          {
            "node": "Preparar Body Upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preparar Body Upload": {
      "main": [
        [
          {
            "node": "Upload para GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload para GitHub": {
      "main": [
        [
          {
            "node": "Extrair URL P\u00fablica",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair URL P\u00fablica": {
      "main": [
        [
          {
            "node": "Atualizar Post no Banco",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Atualizar Post no Banco": {
      "main": [
        [
          {
            "node": "Log de Sucesso",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner"
  },
  "staticData": null,
  "tags": [
    {
      "name": "automation",
      "id": "1"
    },
    {
      "name": "image",
      "id": "2"
    },
    {
      "name": "github",
      "id": "3"
    },
    {
      "name": "gemini",
      "id": "4"
    }
  ],
  "triggerCount": 1,
  "updatedAt": "2025-01-15T14:00:00.000Z",
  "versionId": "1"
}