{"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":"Authorization","value":"Bearer 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"},{"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":{}},"id":"node-openai","name":"OpenAI - Gerar Textos","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1888,304]},{"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":183722655}},"meta":null,"pinData":{},"versionId":"2978d144-4376-42af-bd16-08db98cbecd8","activeVersionId":"2978d144-4376-42af-bd16-08db98cbecd8","versionCounter":870,"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":"Authorization","value":"Bearer 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"},{"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":{}},"id":"node-openai","name":"OpenAI - Gerar Textos","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1888,304]},{"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"}]}}