{
  "updatedAt": "2026-07-04T20:25:22.755Z",
  "createdAt": "2026-04-13T14:35:47.015Z",
  "id": "70HVClU6Bm9wyPK7",
  "name": "OOeirense - Reels",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "jsCode": "const response = $input.first().json.body || $input.first().json;\nconst results = response.result || [];\n\nif (!results.length) return [];\n\nconst msg = results[0];\nconst newOffset = msg.update_id + 1;\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.reelsLastUpdateId = newOffset;\n\nif (msg.message?.from?.is_bot) return [];\n\nconst chatId = String(msg.message?.chat?.id || '');\nif (chatId !== '7769073261') return [];\n\nconst text = (msg.message?.text || '').trim();\n\n// Detectar comando: reels <URL>\nconst reelsMatch = text.match(/^reels\\s+(https?:\\/\\/[^\\s]+)/i);\nif (!reelsMatch) return [];\n\nconst url = reelsMatch[1];\nreturn [{ json: { url, messageId: msg.message.message_id, newOffset } }];"
      },
      "id": "node-extrair",
      "name": "Extrair Comando Reels",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        688,
        304
      ]
    },
    {
      "parameters": {
        "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "chat_id",
              "value": "7769073261"
            },
            {
              "name": "text",
              "value": "⏳ Gerando Reels... aguarde."
            }
          ]
        },
        "options": {}
      },
      "id": "node-telegram-recebido",
      "name": "Telegram - Recebido",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1168,
        304
      ]
    },
    {
      "parameters": {
        "url": "=https://r.jina.ai/{{ $('Extrair Comando Reels').item.json.url }}",
        "options": {}
      },
      "id": "node-buscar",
      "name": "Buscar Matéria",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1408,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Input principal: Jina reader\nconst jina = $input.first().json;\nconst body = jina.data || jina.body || '';\n\n// HTML cru: acessar nó diretamente (inputs paralelos não funcionam via $input.all())\nlet htmlRaw = '';\ntry {\n  const h = $('Buscar HTML Direto').first().json;\n  htmlRaw = h.data || h.body || h.text || '';\n} catch(e) {}\n\nconst titleMatch = body.match(/Title:\\s*(.+)/i);\nconst descMatch  = body.match(/Description:\\s*(.+)/i);\nconst ogJina     = (body.match(/Image:\\s*(https?:\\/\\/[^\\s]+)/i) || [])[1] || '';\n\nconst title = titleMatch ? titleMatch[1].trim() : '';\nconst desc  = descMatch  ? descMatch[1].trim()  : '';\n\nconst skipPatterns = /logo|icon|thumb|avatar|banner|placeholder|pixel|1x1|spacer|ads|sponsor|\\/assets\\//i;\n\n// og:image do HTML cru (CANONICAL — imagem principal declarada pelo portal)\nconst ogHtml = (htmlRaw.match(/property=[\"\\']og:image[\"\\'][^>]*content=[\"\\'](https?:\\/\\/[^\"\\'\\s]+)/i)\n             || htmlRaw.match(/content=[\"\\'](https?:\\/\\/[^\"\\'\\s]+)[\"\\'][^>]*property=[\"\\']og:image[\"\\']/i)\n             || [])[1] || '';\n\n// Imagens inline markdown no Jina (fallback — pode pegar \"leia também\")\nconst mdImgs = [...body.matchAll(/!\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+\\.(?:jpg|jpeg|png|webp)[^)\\s]*)\\)/gi)].map(m => m[1]);\n\n// URLs de imagem soltas no Jina\nconst rawImgs = [...body.matchAll(/(https?:\\/\\/[^\\s\"\\')]+\\.(?:jpg|jpeg|png|webp)[^\\s\"\\'\\)]*)/gi)].map(m => m[1]);\n\nconst allImgs = [...new Set([...mdImgs, ...rawImgs])].filter(u => !skipPatterns.test(u));\n\n// PRIORIDADE CORRIGIDA: og:image sempre primeiro (é a imagem canônica da matéria).\n// Inline só como último recurso — Jina às vezes coloca \"leia também\" no topo do body.\n// URLs vindas do HTML cru chegam com entidades escapadas (&amp;) — sem decodificar,\n// o servidor ignora parâmetros como og=1 e serve thumbnail minúscula (64px)\nconst decodeEntities = s => s\n  .replace(/&amp;/gi, '&')\n  .replace(/&#0*38;/g, '&')\n  .replace(/&quot;/gi, '\"')\n  .replace(/&#0*39;/g, \"'\");\n\nconst image = decodeEntities(ogHtml || ogJina || allImgs[0] || '');\n\nconst paraMatch = body.match(/(?:^|\\n)([A-ZÁÉÍÓÚÃÕÇ].{80,})/gm) || [];\nconst bodyText = paraMatch.slice(0, 5).join(' ').replace(/\\s+/g,' ').trim();\n\nreturn [{ json: { title, desc, image, bodyText, url: $('Extrair Comando Reels').item.json.url } }];"
      },
      "id": "node-extrair-dados",
      "name": "Extrair Dados da Matéria",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1648,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ ({ model: 'gpt-4o', max_tokens: 1200, temperature: 0.2, response_format: { type: 'json_object' }, messages: [{ role: 'system', content: 'Você é editor do portal O Oeirense (Oeiras, Piauí). Gere textos jornalísticos fiéis ao fonte.\\n\\nREGRAS INVIOLÁVEIS:\\n1. NUNCA invente datas, dias, horários, nomes, cidades ou números. Copie EXATAMENTE do texto.\\n2. Se não constar no texto, NÃO mencione.\\n3. TÍTULO e DESCRIÇÃO são fontes mais confiáveis que o bodyText.\\n4. Retorne JSON com: chapeu, titulo, script_narracao, legenda.' }, { role: 'user', content: `Gere:\\n- chapeu: máximo 2 palavras\\n- titulo: base no original, máx 80 chars, sem ponto final\\n- script_narracao: narração voz over 30-40s (~85 palavras), linguagem oral jornalística. Gancho → desenvolvimento → fechamento. Sem abreviações; números, horas e siglas por extenso.\\n- legenda: 4 parágrafos (fato, detalhes, impactos, observação). 2-3 frases cada. Sem links/CTA. Termine com pergunta para engajamento.\\n\\nLEMBRETE: NÃO invente nenhum dado.\\n\\nTítulo: ${$json.title}\\nDescrição: ${$json.desc}\\nTexto: ${($json.bodyText || '').substring(0, 3000)}` }] }) }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "id": "node-openai",
      "name": "OpenAI - Gerar Textos",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1888,
        304
      ],
      "credentials": {
        "openAiApi": {
          "id": "kzx5weRZep0izFEn",
          "name": "OpenAI account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Correções para o material gerado pelo OpenAI\n//\n// GRAFIA_FIX — aplica em todos os campos visíveis (chapéu/título/script/legenda).\n// Corrige erros recorrentes do modelo que afetariam a exibição.\nconst GRAFIA_FIX = {\n  'Hamilton': 'Amilton',  // Amilton de Zé Moura (pres. Câmara de Oeiras) — grafia sem H\n};\n\n// TTS_FIX — aplica SÓ no script que vai pro ElevenLabs. Ajusta foneticamente\n// nomes cuja grafia correta tem H mudo em PT mas o TTS multilíngue interpreta\n// como H soprado (inglês).\nconst TTS_FIX = {\n  'Hailton': 'Ailton',    // Dr. Hailton Alves Filho (prefeito) — pronúncia \"Aílton\"\n};\n\nfunction aplicarMapa(texto, mapa) {\n  if (!texto) return texto;\n  let out = texto;\n  for (const [de, para] of Object.entries(mapa)) {\n    out = out.replace(new RegExp('\\\\b' + de + '\\\\b', 'g'), para);\n  }\n  return out;\n}\n\nconst content = $input.first().json.choices[0].message.content.trim();\nconst clean = content.replace(/```json|```/g,'').trim();\nconst parsed = JSON.parse(clean);\n\nconst chapeu = aplicarMapa(parsed.chapeu, GRAFIA_FIX);\nconst titulo = aplicarMapa(parsed.titulo, GRAFIA_FIX);\nconst legenda = aplicarMapa(parsed.legenda || '', GRAFIA_FIX);\nconst scriptGrafia = aplicarMapa(parsed.script_narracao, GRAFIA_FIX);\nconst script = aplicarMapa(scriptGrafia, TTS_FIX);\n\nreturn [{ json: {\n  chapeu,\n  titulo,\n  script,\n  legenda,\n  image: $('Extrair Dados da Matéria').item.json.image\n}}];"
      },
      "id": "node-parsear",
      "name": "Parsear OpenAI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2128,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.elevenlabs.io/v1/text-to-speech/onwK4e9ZLuTAKqWW03F9/with-timestamps",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "xi-api-key",
              "value": "sk_17b648b211092dff78874555e1b21eded7db792734229a29"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ ({ text: $json.script, model_id: 'eleven_multilingual_v2', voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.3, use_speaker_boost: true } }) }}",
        "options": {}
      },
      "id": "node-elevenlabs",
      "name": "ElevenLabs - Narração",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2368,
        304
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst data = $input.first().json;\n\nconst jobId = Date.now();\nconst audioPath = `/tmp/reels_audio_${jobId}.mp3`;\nconst tsPath    = `/tmp/reels_ts_${jobId}.json`;\nconst outputPath = `/tmp/reels_output_${jobId}.mp4`;\n\n// Salvar MP3\nconst audioBuffer = Buffer.from(data.audio_base64, 'base64');\nfs.writeFileSync(audioPath, audioBuffer);\n\n// Salvar timestamps\nfs.writeFileSync(tsPath, JSON.stringify(data.alignment));\n\nreturn [{ json: {\n  jobId,\n  audioPath,\n  tsPath,\n  outputPath,\n  chapeu:  $('Parsear OpenAI').item.json.chapeu,\n  titulo:  $('Parsear OpenAI').item.json.titulo,\n  image:   $('Parsear OpenAI').item.json.image\n}}];"
      },
      "id": "node-salvar-arquivos",
      "name": "Salvar Áudio e Timestamps",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2608,
        304
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.image }}",
        "options": {}
      },
      "id": "node-baixar-img",
      "name": "Baixar Imagem",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2848,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst prev = $('Salvar Áudio e Timestamps').item.json;\n\nconst binaryKey = Object.keys($input.first().binary || {})[0];\nconst binaryMeta = $input.first().binary?.[binaryKey];\n\nlet imgBuffer;\nif (binaryMeta?.data && binaryMeta.data.length > 500) {\n  imgBuffer = Buffer.from(binaryMeta.data, 'base64');\n} else {\n  imgBuffer = await this.helpers.getBinaryDataBuffer(0, binaryKey);\n}\n\nconst fotoPath = `/tmp/reels_foto_${prev.jobId}.jpg`;\nfs.writeFileSync(fotoPath, imgBuffer);\n\nreturn [{ json: { ...prev, fotoPath } }];"
      },
      "id": "node-salvar-img",
      "name": "Salvar Imagem",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3088,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "const { spawn } = require('child_process');\nconst data = $input.first().json;\n\nconst args = [\n  '/Users/mac/ARQUIVOS CLAUDE/gerar-reels.py',\n  '--foto',       data.fotoPath,\n  '--audio',      data.audioPath,\n  '--timestamps', data.tsPath,\n  '--chapeu',     data.chapeu,\n  '--titulo',     data.titulo,\n  '--output',     data.outputPath\n];\n\n// Arquivo de status — quando existir, o vídeo está pronto\nconst statusFile = data.outputPath + '.done';\nconst logFile    = data.outputPath + '.log';\n\nconst fs = require('fs');\n\n// Lançar processo desanexado (não bloqueia o n8n)\nconst child = spawn('/Users/mac/.venv-reels/bin/python', args, {\n  detached: true,\n  stdio: ['ignore',\n    fs.openSync(logFile, 'w'),\n    fs.openSync(logFile, 'a')]\n});\nchild.unref();\n\nreturn [{ json: { ...data, statusFile, logFile, pid: child.pid } }];"
      },
      "id": "node-gerar-reels",
      "name": "Gerar Reels (ffmpeg)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3328,
        304
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst { execFile, spawn } = require('child_process');\n\nconst data = $('Verificar Vídeo Pronto').first().json;\n\nconst IG_USER_ID = '17841477402712235';\nconst ACCESS_TOKEN = 'EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD';\n\nconst caption = String($('Parsear OpenAI').first().json.legenda || '');\nconst videoPath = String(data.outputPath || '');\n\nif (!videoPath) throw new Error('outputPath ausente');\nif (!fs.existsSync(videoPath)) throw new Error('Vídeo não existe: ' + videoPath);\nconst fileSize = fs.statSync(videoPath).size;\n\n// curl assíncrono — execFileSync bloqueava o event loop e o task runner\n// era morto por \"unresponsive\" quando o upload passava de ~40s\nfunction curlAsync(args) {\n  return new Promise((resolve, reject) => {\n    execFile('/usr/bin/curl', args, { maxBuffer: 10 * 1024 * 1024, encoding: 'utf8' },\n      (err, stdout, stderr) => {\n        if (err) reject(new Error('curl falhou: ' + err.message + ' | stderr: ' + String(stderr || '').slice(0, 400)));\n        else resolve(stdout);\n      });\n  });\n}\n\nfunction sleepAsync(secs) {\n  return new Promise(resolve => execFile('/bin/sleep', [String(secs)], () => resolve()));\n}\n\n// Step 1: Init resumable container (POST pequeno e rápido)\nconst initBody = JSON.stringify({\n  media_type: 'REELS',\n  upload_type: 'resumable',\n  caption: caption,\n  access_token: ACCESS_TOKEN,\n});\n\nlet init = null;\nlet lastError = '';\nfor (let attempt = 1; attempt <= 2 && !init; attempt++) {\n  try {\n    const raw = await curlAsync(['-s', '-S', '--max-time', '20', '-X', 'POST',\n      `https://graph.facebook.com/v25.0/${IG_USER_ID}/media`,\n      '-H', 'Content-Type: application/json', '--data-raw', initBody]);\n    let parsed;\n    try { parsed = JSON.parse(raw); } catch (e) {\n      throw new Error('STEP1_INIT JSON invalido: ' + raw.slice(0, 600));\n    }\n    if (parsed.error) {\n      throw new Error('STEP1_INIT erro Meta: ' + (parsed.error.message || JSON.stringify(parsed.error)).slice(0, 400));\n    }\n    if (!parsed.id || !parsed.uri) {\n      throw new Error('STEP1_INIT sem id/uri: ' + raw.slice(0, 600));\n    }\n    init = parsed;\n  } catch (e) {\n    lastError = e.message;\n    if (attempt < 2) await sleepAsync(5);\n  }\n}\nif (!init) throw new Error('Init do container falhou: ' + lastError);\n\nconst containerId = String(init.id);\nconst uploadUri = String(init.uri);\n\n// Step 2: upload binário desanexado (mesmo padrão do nó ffmpeg) — roda fora\n// do task runner; o Wait antes do publish dá tempo de terminar\nconst resultFile = `/tmp/reels_upload_${data.jobId}.json`;\nconst scriptPath = `/tmp/reels_upload_${data.jobId}.sh`;\nconst uploadLog  = `/tmp/reels_upload_${data.jobId}.log`;\n\nconst script = [\n  '#!/bin/bash',\n  'for i in 1 2 3; do',\n  `  /usr/bin/curl -s -S -X POST '${uploadUri}' \\\\`,\n  `    -H 'Authorization: OAuth ${ACCESS_TOKEN}' \\\\`,\n  \"    -H 'offset: 0' \\\\\",\n  `    -H 'file_size: ${fileSize}' \\\\`,\n  \"    -H 'Content-Type: application/octet-stream' \\\\\",\n  `    --data-binary @'${videoPath}' --max-time 150 -o '${resultFile}'`,\n  `  if grep -Eq '\"success\"[[:space:]]*:[[:space:]]*true' '${resultFile}' 2>/dev/null; then exit 0; fi`,\n  '  sleep 20',\n  'done',\n  'exit 1',\n].join('\\n');\n\nfs.writeFileSync(scriptPath, script, { mode: 0o755 });\n\nconst child = spawn('/bin/bash', [scriptPath], {\n  detached: true,\n  stdio: ['ignore', fs.openSync(uploadLog, 'w'), fs.openSync(uploadLog, 'a')]\n});\nchild.unref();\n\nreturn [{ json: {\n  jobId: data.jobId,\n  outputPath: videoPath,\n  chapeu: data.chapeu,\n  titulo: data.titulo,\n  id: containerId,\n  fileSize: fileSize,\n  uploadResultFile: resultFile,\n  uploadPid: child.pid,\n} }];"
      },
      "id": "node-upload-reels",
      "name": "Instagram - Criar Container Reels",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3568,
        304
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "unit": "seconds",
        "amount": 60
      },
      "id": "node-aguardar",
      "name": "Aguardar Geração (10min)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        4048,
        304
      ],
      "webhookId": "d8de132e-c89a-40a9-bda4-8adb18a02eb5",
      "notes": "Loop de verificacao: espera 60s e volta pro \"Verificar Video Pronto\". Teto ~25 tentativas (~25min).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://graph.facebook.com/v25.0/17841477402712235/media_publish",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ ({ creation_id: $input.first().json.id, access_token: 'EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD' }) }}",
        "options": {}
      },
      "id": "node-publicar",
      "name": "Instagram - Publicar Reels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4768,
        304
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "chat_id",
              "value": "7769073261"
            },
            {
              "name": "text",
              "value": "=✅ Reels publicado no @ooeirense!\n\n📰 {{ $('Parsear OpenAI').item.json.titulo }}"
            }
          ]
        },
        "options": {}
      },
      "id": "node-confirmar",
      "name": "Telegram - Confirmar Reels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5008,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst prev = $input.first().json;\n\nconst MAX_ATTEMPTS = 25;              // teto ~25 min (60s por tentativa)\nconst attempt = (prev.attempt || 0) + 1;\n\n// O script so cria o .done (\"ok\") ao terminar com sucesso.\nif (fs.existsSync(prev.statusFile) && fs.existsSync(prev.outputPath)) {\n  try { fs.unlinkSync(prev.statusFile); } catch (e) {}\n  return [{ json: { ...prev, ready: true, attempt } }];\n}\n\nlet log = '';\ntry { log = fs.readFileSync(prev.logFile, 'utf8'); } catch (e) {}\n\n// Falha explicita: traceback no log -> aborta na hora (nao espera o teto)\nif (log.includes('Traceback (most recent call last)')) {\n  throw new Error('Falha ao gerar o video (Python quebrou). Fim do log:\\n' + log.slice(-800));\n}\n\n// Estourou o teto de tempo\nif (attempt >= MAX_ATTEMPTS) {\n  throw new Error('Video nao ficou pronto apos ' + attempt + ' min. Fim do log:\\n' + (log ? log.slice(-800) : 'sem log'));\n}\n\n// Ainda renderizando -> segue no loop\nreturn [{ json: { ...prev, ready: false, attempt } }];"
      },
      "id": "node-check-done",
      "name": "Verificar Vídeo Pronto",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4048,
        640
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "amount": 240
      },
      "id": "node-wait-insta",
      "name": "Aguardar Instagram (3min)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        4528,
        304
      ],
      "webhookId": "445ffba6-a412-40d9-a3e8-130a44fb4c5d"
    },
    {
      "parameters": {
        "url": "=https://r.jina.ai/{{ $('Extrair Comando Reels').item.json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Return-Format",
              "value": "html"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 30000
        }
      },
      "id": "buscar-html-direto",
      "name": "Buscar HTML Direto",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1408,
        528
      ]
    },
    {
      "id": "tg-reels-erro-001",
      "name": "Telegram - Erro Reels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4288,
        560
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ chat_id: '7769073261', text: '❌ <b>Erro ao gerar Reels no @ooeirense</b>\\n\\n' + ($json.error?.message || $json.message || JSON.stringify($json)).substring(0, 300), parse_mode: 'HTML' }) }}",
        "options": {}
      }
    },
    {
      "parameters": {
        "path": "dispatch-reels",
        "httpMethod": "POST",
        "responseMode": "onReceived",
        "options": {}
      },
      "id": "wh-dispatch-reels",
      "name": "Webhook Dispatcher",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        208,
        304
      ],
      "webhookId": "wh-dispatch-reels"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-video-pronto",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.ready }}",
              "rightValue": ""
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4320,
        640
      ],
      "id": "if-video-pronto-2026",
      "name": "Vídeo Pronto?"
    }
  ],
  "connections": {
    "Extrair Comando Reels": {
      "main": [
        [
          {
            "node": "Telegram - Recebido",
            "type": "main",
            "index": 0
          },
          {
            "node": "Buscar HTML Direto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram - Recebido": {
      "main": [
        [
          {
            "node": "Buscar Matéria",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Matéria": {
      "main": [
        [
          {
            "node": "Extrair Dados da Matéria",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair Dados da Matéria": {
      "main": [
        [
          {
            "node": "OpenAI - Gerar Textos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI - Gerar Textos": {
      "main": [
        [
          {
            "node": "Parsear OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsear OpenAI": {
      "main": [
        [
          {
            "node": "ElevenLabs - Narração",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ElevenLabs - Narração": {
      "main": [
        [
          {
            "node": "Salvar Áudio e Timestamps",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram - Erro Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Áudio e Timestamps": {
      "main": [
        [
          {
            "node": "Baixar Imagem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Baixar Imagem": {
      "main": [
        [
          {
            "node": "Salvar Imagem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Imagem": {
      "main": [
        [
          {
            "node": "Gerar Reels (ffmpeg)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gerar Reels (ffmpeg)": {
      "main": [
        [
          {
            "node": "Aguardar Geração (10min)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram - Erro Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram - Criar Container Reels": {
      "main": [
        [
          {
            "node": "Aguardar Instagram (3min)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram - Erro Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram - Publicar Reels": {
      "main": [
        [
          {
            "node": "Telegram - Confirmar Reels",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram - Erro Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verificar Vídeo Pronto": {
      "main": [
        [
          {
            "node": "Vídeo Pronto?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram - Erro Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar HTML Direto": {
      "main": [
        [
          {
            "node": "Extrair Dados da Matéria",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Aguardar Instagram (3min)": {
      "main": [
        [
          {
            "node": "Instagram - Publicar Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aguardar Geração (10min)": {
      "main": [
        [
          {
            "node": "Verificar Vídeo Pronto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Dispatcher": {
      "main": [
        [
          {
            "node": "Extrair Comando Reels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vídeo Pronto?": {
      "main": [
        [
          {
            "node": "Instagram - Criar Container Reels",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aguardar Geração (10min)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "binaryMode": "separate"
  },
  "staticData": {
    "node:Poll Telegram": {
      "recurrenceRules": []
    },
    "global": {
      "reelsLastUpdateId": 183722797
    }
  },
  "meta": null,
  "pinData": {},
  "versionId": "2978d144-4376-42af-bd16-08db98cbecd8",
  "activeVersionId": "2978d144-4376-42af-bd16-08db98cbecd8",
  "versionCounter": 895,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-04-13T14:35:47.020Z",
      "createdAt": "2026-04-13T14:35:47.020Z",
      "role": "workflow:owner",
      "workflowId": "70HVClU6Bm9wyPK7",
      "projectId": "lcNbFUrydUsxxeGp",
      "project": {
        "updatedAt": "2026-05-06T16:07:04.097Z",
        "createdAt": "2026-04-10T15:23:11.499Z",
        "id": "lcNbFUrydUsxxeGp",
        "name": "Ultimo Campos <ultimocampos@gmail.com>",
        "type": "personal",
        "icon": null,
        "description": null,
        "creatorId": "7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"
      }
    }
  ],
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-07-04T20:25:22.765Z",
    "createdAt": "2026-07-04T20:25:22.765Z",
    "versionId": "2978d144-4376-42af-bd16-08db98cbecd8",
    "workflowId": "70HVClU6Bm9wyPK7",
    "nodes": [
      {
        "parameters": {
          "jsCode": "const response = $input.first().json.body || $input.first().json;\nconst results = response.result || [];\n\nif (!results.length) return [];\n\nconst msg = results[0];\nconst newOffset = msg.update_id + 1;\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.reelsLastUpdateId = newOffset;\n\nif (msg.message?.from?.is_bot) return [];\n\nconst chatId = String(msg.message?.chat?.id || '');\nif (chatId !== '7769073261') return [];\n\nconst text = (msg.message?.text || '').trim();\n\n// Detectar comando: reels <URL>\nconst reelsMatch = text.match(/^reels\\s+(https?:\\/\\/[^\\s]+)/i);\nif (!reelsMatch) return [];\n\nconst url = reelsMatch[1];\nreturn [{ json: { url, messageId: msg.message.message_id, newOffset } }];"
        },
        "id": "node-extrair",
        "name": "Extrair Comando Reels",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          688,
          304
        ]
      },
      {
        "parameters": {
          "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
          "sendQuery": true,
          "queryParameters": {
            "parameters": [
              {
                "name": "chat_id",
                "value": "7769073261"
              },
              {
                "name": "text",
                "value": "⏳ Gerando Reels... aguarde."
              }
            ]
          },
          "options": {}
        },
        "id": "node-telegram-recebido",
        "name": "Telegram - Recebido",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1168,
          304
        ]
      },
      {
        "parameters": {
          "url": "=https://r.jina.ai/{{ $('Extrair Comando Reels').item.json.url }}",
          "options": {}
        },
        "id": "node-buscar",
        "name": "Buscar Matéria",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1408,
          304
        ]
      },
      {
        "parameters": {
          "jsCode": "// Input principal: Jina reader\nconst jina = $input.first().json;\nconst body = jina.data || jina.body || '';\n\n// HTML cru: acessar nó diretamente (inputs paralelos não funcionam via $input.all())\nlet htmlRaw = '';\ntry {\n  const h = $('Buscar HTML Direto').first().json;\n  htmlRaw = h.data || h.body || h.text || '';\n} catch(e) {}\n\nconst titleMatch = body.match(/Title:\\s*(.+)/i);\nconst descMatch  = body.match(/Description:\\s*(.+)/i);\nconst ogJina     = (body.match(/Image:\\s*(https?:\\/\\/[^\\s]+)/i) || [])[1] || '';\n\nconst title = titleMatch ? titleMatch[1].trim() : '';\nconst desc  = descMatch  ? descMatch[1].trim()  : '';\n\nconst skipPatterns = /logo|icon|thumb|avatar|banner|placeholder|pixel|1x1|spacer|ads|sponsor|\\/assets\\//i;\n\n// og:image do HTML cru (CANONICAL — imagem principal declarada pelo portal)\nconst ogHtml = (htmlRaw.match(/property=[\"\\']og:image[\"\\'][^>]*content=[\"\\'](https?:\\/\\/[^\"\\'\\s]+)/i)\n             || htmlRaw.match(/content=[\"\\'](https?:\\/\\/[^\"\\'\\s]+)[\"\\'][^>]*property=[\"\\']og:image[\"\\']/i)\n             || [])[1] || '';\n\n// Imagens inline markdown no Jina (fallback — pode pegar \"leia também\")\nconst mdImgs = [...body.matchAll(/!\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+\\.(?:jpg|jpeg|png|webp)[^)\\s]*)\\)/gi)].map(m => m[1]);\n\n// URLs de imagem soltas no Jina\nconst rawImgs = [...body.matchAll(/(https?:\\/\\/[^\\s\"\\')]+\\.(?:jpg|jpeg|png|webp)[^\\s\"\\'\\)]*)/gi)].map(m => m[1]);\n\nconst allImgs = [...new Set([...mdImgs, ...rawImgs])].filter(u => !skipPatterns.test(u));\n\n// PRIORIDADE CORRIGIDA: og:image sempre primeiro (é a imagem canônica da matéria).\n// Inline só como último recurso — Jina às vezes coloca \"leia também\" no topo do body.\n// URLs vindas do HTML cru chegam com entidades escapadas (&amp;) — sem decodificar,\n// o servidor ignora parâmetros como og=1 e serve thumbnail minúscula (64px)\nconst decodeEntities = s => s\n  .replace(/&amp;/gi, '&')\n  .replace(/&#0*38;/g, '&')\n  .replace(/&quot;/gi, '\"')\n  .replace(/&#0*39;/g, \"'\");\n\nconst image = decodeEntities(ogHtml || ogJina || allImgs[0] || '');\n\nconst paraMatch = body.match(/(?:^|\\n)([A-ZÁÉÍÓÚÃÕÇ].{80,})/gm) || [];\nconst bodyText = paraMatch.slice(0, 5).join(' ').replace(/\\s+/g,' ').trim();\n\nreturn [{ json: { title, desc, image, bodyText, url: $('Extrair Comando Reels').item.json.url } }];"
        },
        "id": "node-extrair-dados",
        "name": "Extrair Dados da Matéria",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1648,
          304
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.openai.com/v1/chat/completions",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ ({ model: 'gpt-4o', max_tokens: 1200, temperature: 0.2, response_format: { type: 'json_object' }, messages: [{ role: 'system', content: 'Você é editor do portal O Oeirense (Oeiras, Piauí). Gere textos jornalísticos fiéis ao fonte.\\n\\nREGRAS INVIOLÁVEIS:\\n1. NUNCA invente datas, dias, horários, nomes, cidades ou números. Copie EXATAMENTE do texto.\\n2. Se não constar no texto, NÃO mencione.\\n3. TÍTULO e DESCRIÇÃO são fontes mais confiáveis que o bodyText.\\n4. Retorne JSON com: chapeu, titulo, script_narracao, legenda.' }, { role: 'user', content: `Gere:\\n- chapeu: máximo 2 palavras\\n- titulo: base no original, máx 80 chars, sem ponto final\\n- script_narracao: narração voz over 30-40s (~85 palavras), linguagem oral jornalística. Gancho → desenvolvimento → fechamento. Sem abreviações; números, horas e siglas por extenso.\\n- legenda: 4 parágrafos (fato, detalhes, impactos, observação). 2-3 frases cada. Sem links/CTA. Termine com pergunta para engajamento.\\n\\nLEMBRETE: NÃO invente nenhum dado.\\n\\nTítulo: ${$json.title}\\nDescrição: ${$json.desc}\\nTexto: ${($json.bodyText || '').substring(0, 3000)}` }] }) }}",
          "options": {},
          "authentication": "predefinedCredentialType",
          "nodeCredentialType": "openAiApi"
        },
        "id": "node-openai",
        "name": "OpenAI - Gerar Textos",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1888,
          304
        ],
        "credentials": {
          "openAiApi": {
            "id": "kzx5weRZep0izFEn",
            "name": "OpenAI account"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Correções para o material gerado pelo OpenAI\n//\n// GRAFIA_FIX — aplica em todos os campos visíveis (chapéu/título/script/legenda).\n// Corrige erros recorrentes do modelo que afetariam a exibição.\nconst GRAFIA_FIX = {\n  'Hamilton': 'Amilton',  // Amilton de Zé Moura (pres. Câmara de Oeiras) — grafia sem H\n};\n\n// TTS_FIX — aplica SÓ no script que vai pro ElevenLabs. Ajusta foneticamente\n// nomes cuja grafia correta tem H mudo em PT mas o TTS multilíngue interpreta\n// como H soprado (inglês).\nconst TTS_FIX = {\n  'Hailton': 'Ailton',    // Dr. Hailton Alves Filho (prefeito) — pronúncia \"Aílton\"\n};\n\nfunction aplicarMapa(texto, mapa) {\n  if (!texto) return texto;\n  let out = texto;\n  for (const [de, para] of Object.entries(mapa)) {\n    out = out.replace(new RegExp('\\\\b' + de + '\\\\b', 'g'), para);\n  }\n  return out;\n}\n\nconst content = $input.first().json.choices[0].message.content.trim();\nconst clean = content.replace(/```json|```/g,'').trim();\nconst parsed = JSON.parse(clean);\n\nconst chapeu = aplicarMapa(parsed.chapeu, GRAFIA_FIX);\nconst titulo = aplicarMapa(parsed.titulo, GRAFIA_FIX);\nconst legenda = aplicarMapa(parsed.legenda || '', GRAFIA_FIX);\nconst scriptGrafia = aplicarMapa(parsed.script_narracao, GRAFIA_FIX);\nconst script = aplicarMapa(scriptGrafia, TTS_FIX);\n\nreturn [{ json: {\n  chapeu,\n  titulo,\n  script,\n  legenda,\n  image: $('Extrair Dados da Matéria').item.json.image\n}}];"
        },
        "id": "node-parsear",
        "name": "Parsear OpenAI",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2128,
          304
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.elevenlabs.io/v1/text-to-speech/onwK4e9ZLuTAKqWW03F9/with-timestamps",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "xi-api-key",
                "value": "sk_17b648b211092dff78874555e1b21eded7db792734229a29"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ ({ text: $json.script, model_id: 'eleven_multilingual_v2', voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.3, use_speaker_boost: true } }) }}",
          "options": {}
        },
        "id": "node-elevenlabs",
        "name": "ElevenLabs - Narração",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          2368,
          304
        ],
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "jsCode": "const fs = require('fs');\nconst data = $input.first().json;\n\nconst jobId = Date.now();\nconst audioPath = `/tmp/reels_audio_${jobId}.mp3`;\nconst tsPath    = `/tmp/reels_ts_${jobId}.json`;\nconst outputPath = `/tmp/reels_output_${jobId}.mp4`;\n\n// Salvar MP3\nconst audioBuffer = Buffer.from(data.audio_base64, 'base64');\nfs.writeFileSync(audioPath, audioBuffer);\n\n// Salvar timestamps\nfs.writeFileSync(tsPath, JSON.stringify(data.alignment));\n\nreturn [{ json: {\n  jobId,\n  audioPath,\n  tsPath,\n  outputPath,\n  chapeu:  $('Parsear OpenAI').item.json.chapeu,\n  titulo:  $('Parsear OpenAI').item.json.titulo,\n  image:   $('Parsear OpenAI').item.json.image\n}}];"
        },
        "id": "node-salvar-arquivos",
        "name": "Salvar Áudio e Timestamps",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2608,
          304
        ]
      },
      {
        "parameters": {
          "url": "={{ $json.image }}",
          "options": {}
        },
        "id": "node-baixar-img",
        "name": "Baixar Imagem",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          2848,
          304
        ]
      },
      {
        "parameters": {
          "jsCode": "const fs = require('fs');\nconst prev = $('Salvar Áudio e Timestamps').item.json;\n\nconst binaryKey = Object.keys($input.first().binary || {})[0];\nconst binaryMeta = $input.first().binary?.[binaryKey];\n\nlet imgBuffer;\nif (binaryMeta?.data && binaryMeta.data.length > 500) {\n  imgBuffer = Buffer.from(binaryMeta.data, 'base64');\n} else {\n  imgBuffer = await this.helpers.getBinaryDataBuffer(0, binaryKey);\n}\n\nconst fotoPath = `/tmp/reels_foto_${prev.jobId}.jpg`;\nfs.writeFileSync(fotoPath, imgBuffer);\n\nreturn [{ json: { ...prev, fotoPath } }];"
        },
        "id": "node-salvar-img",
        "name": "Salvar Imagem",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3088,
          304
        ]
      },
      {
        "parameters": {
          "jsCode": "const { spawn } = require('child_process');\nconst data = $input.first().json;\n\nconst args = [\n  '/Users/mac/ARQUIVOS CLAUDE/gerar-reels.py',\n  '--foto',       data.fotoPath,\n  '--audio',      data.audioPath,\n  '--timestamps', data.tsPath,\n  '--chapeu',     data.chapeu,\n  '--titulo',     data.titulo,\n  '--output',     data.outputPath\n];\n\n// Arquivo de status — quando existir, o vídeo está pronto\nconst statusFile = data.outputPath + '.done';\nconst logFile    = data.outputPath + '.log';\n\nconst fs = require('fs');\n\n// Lançar processo desanexado (não bloqueia o n8n)\nconst child = spawn('/Users/mac/.venv-reels/bin/python', args, {\n  detached: true,\n  stdio: ['ignore',\n    fs.openSync(logFile, 'w'),\n    fs.openSync(logFile, 'a')]\n});\nchild.unref();\n\nreturn [{ json: { ...data, statusFile, logFile, pid: child.pid } }];"
        },
        "id": "node-gerar-reels",
        "name": "Gerar Reels (ffmpeg)",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3328,
          304
        ],
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "jsCode": "const fs = require('fs');\nconst { execFile, spawn } = require('child_process');\n\nconst data = $('Verificar Vídeo Pronto').first().json;\n\nconst IG_USER_ID = '17841477402712235';\nconst ACCESS_TOKEN = 'EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD';\n\nconst caption = String($('Parsear OpenAI').first().json.legenda || '');\nconst videoPath = String(data.outputPath || '');\n\nif (!videoPath) throw new Error('outputPath ausente');\nif (!fs.existsSync(videoPath)) throw new Error('Vídeo não existe: ' + videoPath);\nconst fileSize = fs.statSync(videoPath).size;\n\n// curl assíncrono — execFileSync bloqueava o event loop e o task runner\n// era morto por \"unresponsive\" quando o upload passava de ~40s\nfunction curlAsync(args) {\n  return new Promise((resolve, reject) => {\n    execFile('/usr/bin/curl', args, { maxBuffer: 10 * 1024 * 1024, encoding: 'utf8' },\n      (err, stdout, stderr) => {\n        if (err) reject(new Error('curl falhou: ' + err.message + ' | stderr: ' + String(stderr || '').slice(0, 400)));\n        else resolve(stdout);\n      });\n  });\n}\n\nfunction sleepAsync(secs) {\n  return new Promise(resolve => execFile('/bin/sleep', [String(secs)], () => resolve()));\n}\n\n// Step 1: Init resumable container (POST pequeno e rápido)\nconst initBody = JSON.stringify({\n  media_type: 'REELS',\n  upload_type: 'resumable',\n  caption: caption,\n  access_token: ACCESS_TOKEN,\n});\n\nlet init = null;\nlet lastError = '';\nfor (let attempt = 1; attempt <= 2 && !init; attempt++) {\n  try {\n    const raw = await curlAsync(['-s', '-S', '--max-time', '20', '-X', 'POST',\n      `https://graph.facebook.com/v25.0/${IG_USER_ID}/media`,\n      '-H', 'Content-Type: application/json', '--data-raw', initBody]);\n    let parsed;\n    try { parsed = JSON.parse(raw); } catch (e) {\n      throw new Error('STEP1_INIT JSON invalido: ' + raw.slice(0, 600));\n    }\n    if (parsed.error) {\n      throw new Error('STEP1_INIT erro Meta: ' + (parsed.error.message || JSON.stringify(parsed.error)).slice(0, 400));\n    }\n    if (!parsed.id || !parsed.uri) {\n      throw new Error('STEP1_INIT sem id/uri: ' + raw.slice(0, 600));\n    }\n    init = parsed;\n  } catch (e) {\n    lastError = e.message;\n    if (attempt < 2) await sleepAsync(5);\n  }\n}\nif (!init) throw new Error('Init do container falhou: ' + lastError);\n\nconst containerId = String(init.id);\nconst uploadUri = String(init.uri);\n\n// Step 2: upload binário desanexado (mesmo padrão do nó ffmpeg) — roda fora\n// do task runner; o Wait antes do publish dá tempo de terminar\nconst resultFile = `/tmp/reels_upload_${data.jobId}.json`;\nconst scriptPath = `/tmp/reels_upload_${data.jobId}.sh`;\nconst uploadLog  = `/tmp/reels_upload_${data.jobId}.log`;\n\nconst script = [\n  '#!/bin/bash',\n  'for i in 1 2 3; do',\n  `  /usr/bin/curl -s -S -X POST '${uploadUri}' \\\\`,\n  `    -H 'Authorization: OAuth ${ACCESS_TOKEN}' \\\\`,\n  \"    -H 'offset: 0' \\\\\",\n  `    -H 'file_size: ${fileSize}' \\\\`,\n  \"    -H 'Content-Type: application/octet-stream' \\\\\",\n  `    --data-binary @'${videoPath}' --max-time 150 -o '${resultFile}'`,\n  `  if grep -Eq '\"success\"[[:space:]]*:[[:space:]]*true' '${resultFile}' 2>/dev/null; then exit 0; fi`,\n  '  sleep 20',\n  'done',\n  'exit 1',\n].join('\\n');\n\nfs.writeFileSync(scriptPath, script, { mode: 0o755 });\n\nconst child = spawn('/bin/bash', [scriptPath], {\n  detached: true,\n  stdio: ['ignore', fs.openSync(uploadLog, 'w'), fs.openSync(uploadLog, 'a')]\n});\nchild.unref();\n\nreturn [{ json: {\n  jobId: data.jobId,\n  outputPath: videoPath,\n  chapeu: data.chapeu,\n  titulo: data.titulo,\n  id: containerId,\n  fileSize: fileSize,\n  uploadResultFile: resultFile,\n  uploadPid: child.pid,\n} }];"
        },
        "id": "node-upload-reels",
        "name": "Instagram - Criar Container Reels",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3568,
          304
        ],
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "unit": "seconds",
          "amount": 60
        },
        "id": "node-aguardar",
        "name": "Aguardar Geração (10min)",
        "type": "n8n-nodes-base.wait",
        "typeVersion": 1.1,
        "position": [
          4048,
          304
        ],
        "webhookId": "d8de132e-c89a-40a9-bda4-8adb18a02eb5",
        "notes": "Loop de verificacao: espera 60s e volta pro \"Verificar Video Pronto\". Teto ~25 tentativas (~25min).",
        "notesInFlow": true
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://graph.facebook.com/v25.0/17841477402712235/media_publish",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ ({ creation_id: $input.first().json.id, access_token: 'EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD' }) }}",
          "options": {}
        },
        "id": "node-publicar",
        "name": "Instagram - Publicar Reels",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          4768,
          304
        ],
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
          "sendQuery": true,
          "queryParameters": {
            "parameters": [
              {
                "name": "chat_id",
                "value": "7769073261"
              },
              {
                "name": "text",
                "value": "=✅ Reels publicado no @ooeirense!\n\n📰 {{ $('Parsear OpenAI').item.json.titulo }}"
              }
            ]
          },
          "options": {}
        },
        "id": "node-confirmar",
        "name": "Telegram - Confirmar Reels",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          5008,
          304
        ]
      },
      {
        "parameters": {
          "jsCode": "const fs = require('fs');\nconst prev = $input.first().json;\n\nconst MAX_ATTEMPTS = 25;              // teto ~25 min (60s por tentativa)\nconst attempt = (prev.attempt || 0) + 1;\n\n// O script so cria o .done (\"ok\") ao terminar com sucesso.\nif (fs.existsSync(prev.statusFile) && fs.existsSync(prev.outputPath)) {\n  try { fs.unlinkSync(prev.statusFile); } catch (e) {}\n  return [{ json: { ...prev, ready: true, attempt } }];\n}\n\nlet log = '';\ntry { log = fs.readFileSync(prev.logFile, 'utf8'); } catch (e) {}\n\n// Falha explicita: traceback no log -> aborta na hora (nao espera o teto)\nif (log.includes('Traceback (most recent call last)')) {\n  throw new Error('Falha ao gerar o video (Python quebrou). Fim do log:\\n' + log.slice(-800));\n}\n\n// Estourou o teto de tempo\nif (attempt >= MAX_ATTEMPTS) {\n  throw new Error('Video nao ficou pronto apos ' + attempt + ' min. Fim do log:\\n' + (log ? log.slice(-800) : 'sem log'));\n}\n\n// Ainda renderizando -> segue no loop\nreturn [{ json: { ...prev, ready: false, attempt } }];"
        },
        "id": "node-check-done",
        "name": "Verificar Vídeo Pronto",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4048,
          640
        ],
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "amount": 240
        },
        "id": "node-wait-insta",
        "name": "Aguardar Instagram (3min)",
        "type": "n8n-nodes-base.wait",
        "typeVersion": 1.1,
        "position": [
          4528,
          304
        ],
        "webhookId": "445ffba6-a412-40d9-a3e8-130a44fb4c5d"
      },
      {
        "parameters": {
          "url": "=https://r.jina.ai/{{ $('Extrair Comando Reels').item.json.url }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "X-Return-Format",
                "value": "html"
              }
            ]
          },
          "options": {
            "response": {
              "response": {
                "responseFormat": "text"
              }
            },
            "timeout": 30000
          }
        },
        "id": "buscar-html-direto",
        "name": "Buscar HTML Direto",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1408,
          528
        ]
      },
      {
        "id": "tg-reels-erro-001",
        "name": "Telegram - Erro Reels",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          4288,
          560
        ],
        "parameters": {
          "method": "POST",
          "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ chat_id: '7769073261', text: '❌ <b>Erro ao gerar Reels no @ooeirense</b>\\n\\n' + ($json.error?.message || $json.message || JSON.stringify($json)).substring(0, 300), parse_mode: 'HTML' }) }}",
          "options": {}
        }
      },
      {
        "parameters": {
          "path": "dispatch-reels",
          "httpMethod": "POST",
          "responseMode": "onReceived",
          "options": {}
        },
        "id": "wh-dispatch-reels",
        "name": "Webhook Dispatcher",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          208,
          304
        ],
        "webhookId": "wh-dispatch-reels"
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "version": 2,
              "leftValue": "",
              "caseSensitive": true,
              "typeValidation": "strict"
            },
            "combinator": "and",
            "conditions": [
              {
                "id": "cond-video-pronto",
                "operator": {
                  "type": "boolean",
                  "operation": "true",
                  "singleValue": true
                },
                "leftValue": "={{ $json.ready }}",
                "rightValue": ""
              }
            ]
          },
          "options": {}
        },
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.3,
        "position": [
          4320,
          640
        ],
        "id": "if-video-pronto-2026",
        "name": "Vídeo Pronto?"
      }
    ],
    "connections": {
      "Extrair Comando Reels": {
        "main": [
          [
            {
              "node": "Telegram - Recebido",
              "type": "main",
              "index": 0
            },
            {
              "node": "Buscar HTML Direto",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Telegram - Recebido": {
        "main": [
          [
            {
              "node": "Buscar Matéria",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Buscar Matéria": {
        "main": [
          [
            {
              "node": "Extrair Dados da Matéria",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Extrair Dados da Matéria": {
        "main": [
          [
            {
              "node": "OpenAI - Gerar Textos",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "OpenAI - Gerar Textos": {
        "main": [
          [
            {
              "node": "Parsear OpenAI",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parsear OpenAI": {
        "main": [
          [
            {
              "node": "ElevenLabs - Narração",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "ElevenLabs - Narração": {
        "main": [
          [
            {
              "node": "Salvar Áudio e Timestamps",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Telegram - Erro Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Salvar Áudio e Timestamps": {
        "main": [
          [
            {
              "node": "Baixar Imagem",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Baixar Imagem": {
        "main": [
          [
            {
              "node": "Salvar Imagem",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Salvar Imagem": {
        "main": [
          [
            {
              "node": "Gerar Reels (ffmpeg)",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Gerar Reels (ffmpeg)": {
        "main": [
          [
            {
              "node": "Aguardar Geração (10min)",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Telegram - Erro Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Instagram - Criar Container Reels": {
        "main": [
          [
            {
              "node": "Aguardar Instagram (3min)",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Telegram - Erro Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Instagram - Publicar Reels": {
        "main": [
          [
            {
              "node": "Telegram - Confirmar Reels",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Telegram - Erro Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Verificar Vídeo Pronto": {
        "main": [
          [
            {
              "node": "Vídeo Pronto?",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Telegram - Erro Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Buscar HTML Direto": {
        "main": [
          [
            {
              "node": "Extrair Dados da Matéria",
              "type": "main",
              "index": 1
            }
          ]
        ]
      },
      "Aguardar Instagram (3min)": {
        "main": [
          [
            {
              "node": "Instagram - Publicar Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Aguardar Geração (10min)": {
        "main": [
          [
            {
              "node": "Verificar Vídeo Pronto",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Webhook Dispatcher": {
        "main": [
          [
            {
              "node": "Extrair Comando Reels",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Vídeo Pronto?": {
        "main": [
          [
            {
              "node": "Instagram - Criar Container Reels",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Aguardar Geração (10min)",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "Ultimo Campos",
    "name": null,
    "description": null,
    "autosaved": false,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-07-04T20:25:22.888Z",
        "id": 525,
        "workflowId": "70HVClU6Bm9wyPK7",
        "versionId": "2978d144-4376-42af-bd16-08db98cbecd8",
        "event": "activated",
        "userId": "7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"
      },
      {
        "createdAt": "2026-07-04T20:25:22.868Z",
        "id": 524,
        "workflowId": "70HVClU6Bm9wyPK7",
        "versionId": "2978d144-4376-42af-bd16-08db98cbecd8",
        "event": "deactivated",
        "userId": "7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"
      }
    ]
  }
}