{
  "name": "bryom_content_produce_lite",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "node-cron-15m",
      "name": "Cron 15min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -700,
        0
      ]
    },
    {
      "parameters": {
        "operation": "search",
        "base": {
          "__rl": true,
          "value": "appQttEoMG3Ksijak",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tbltu9XcHeG0q1PuF",
          "mode": "id"
        },
        "filterByFormula": "AND({Approved} = TRUE(), {Status} = 'Approved', OR({Job Status} = 'pending', {Job Status} = BLANK()))",
        "returnAll": false,
        "limit": 5,
        "options": {}
      },
      "id": "node-airtable-search",
      "name": "Airtable: Approved Queue",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        -480,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{ $items().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "node-if-approved",
      "name": "IF approved rows exist",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -260,
        0
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "node-batch",
      "name": "Loop One Row",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -40,
        -100
      ]
    },
    {
      "parameters": {
        "language": "javaScript",
        "jsCode": "// Strip SSML tags from voiceover text \u2014 ElevenLabs free/starter does not always parse <break> reliably; safer to remove and let pacing be natural.\nconst row = items[0].json;\nconst rawVo = row.fields?.['Voiceover Text'] || row.fields?.Script || '';\nconst clean = rawVo.replace(/<break[^>]*\\/?>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nreturn [{\n  json: {\n    record_id: row.id,\n    record_fields: row.fields,\n    voice_text: clean.slice(0, 4500),\n    pillar: row.fields?.['Content Pillar'] || '',\n    hook: row.fields?.Hook || '',\n    script: row.fields?.Script || '',\n    caption: row.fields?.Caption || '',\n    hashtags: row.fields?.Hashtags || '',\n    mascot_pose: row.fields?.['Mascot Pose'] || '',\n    on_screen: row.fields?.['On-screen Text'] || '',\n    animation_prompt: row.fields?.['Animation Prompt'] || '',\n  },\n}];"
      },
      "id": "node-prep",
      "name": "Function: Prep TTS Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        180,
        -100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.elevenlabs.io/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "xi-api-key",
              "value": "={{ $env.ELEVENLABS_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "audio/mpeg"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": {{ JSON.stringify($json.voice_text) }},\n  \"model_id\": {{ JSON.stringify($env.ELEVENLABS_MODEL_ID) }},\n  \"voice_settings\": { \"stability\": 0.55, \"similarity_boost\": 0.75 }\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          },
          "timeout": 90000,
          "retry": {
            "enabled": true,
            "maxRetries": 2,
            "waitBetween": 5000
          }
        }
      },
      "id": "node-elevenlabs",
      "name": "ElevenLabs: TTS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        400,
        -100
      ]
    },
    {
      "parameters": {
        "language": "javaScript",
        "jsCode": "// Rename binary key + add filename for Discord multipart upload.\nconst item = items[0];\nconst recId = $('Function: Prep TTS Input').item.json.record_id;\nconst pillar = $('Function: Prep TTS Input').item.json.pillar.toLowerCase().replace(/[^a-z0-9]+/g,'_');\nconst filename = `bryom_${pillar}_${recId.slice(-6)}.mp3`;\nconst binary = item.binary?.data;\nif (!binary) throw new Error('ElevenLabs returned no audio binary');\nbinary.fileName = filename;\nbinary.mimeType = 'audio/mpeg';\nreturn [{ json: { ...$('Function: Prep TTS Input').item.json, filename }, binary: { voiceover: binary } }];"
      },
      "id": "node-rename",
      "name": "Function: Tag Audio File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        620,
        -100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.DISCORD_WEBHOOK_URL }}",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "file",
              "inputDataFieldName": "voiceover"
            },
            {
              "name": "payload_json",
              "value": "={\n  \"username\": \"Bryom Content Bot\",\n  \"content\": \":microphone2: **Voiceover ready** for `{{ $json.filename }}`\\nPillar: **{{ $json.pillar }}**\\nMascot pose: `{{ $json.mascot_pose }}`\\n\\n**Hook:** {{ $json.hook }}\\n\\n**Script:**\\n{{ $json.script }}\\n\\n**Caption:**\\n{{ $json.caption }}\\n\\n**Hashtags:**\\n{{ $json.hashtags }}\\n\\nDownload the MP3, drop into CapCut, post to IG Reels + TikTok. Then flip Status to `Posted` in Airtable.\"\n}"
            }
          ]
        },
        "options": {
          "timeout": 30000,
          "retry": {
            "enabled": true,
            "maxRetries": 2
          }
        }
      },
      "id": "node-discord",
      "name": "Discord: Voiceover + Brief",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        840,
        -100
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "base": {
          "__rl": true,
          "value": "appQttEoMG3Ksijak",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tbltu9XcHeG0q1PuF",
          "mode": "id"
        },
        "id": "={{ $('Function: Prep TTS Input').item.json.record_id }}",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Job Status": "tts_done",
            "Status": "Scheduled"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [],
          "attemptToConvertTypes": true,
          "convertFieldsToString": true
        }
      },
      "id": "node-airtable-update",
      "name": "Airtable: Mark TTS Done",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        1060,
        -100
      ]
    },
    {
      "parameters": {
        "amount": 2,
        "unit": "seconds"
      },
      "id": "node-wait",
      "name": "Throttle 2s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1280,
        -100
      ]
    }
  ],
  "connections": {
    "Cron 15min": {
      "main": [
        [
          {
            "node": "Airtable: Approved Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable: Approved Queue": {
      "main": [
        [
          {
            "node": "IF approved rows exist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF approved rows exist": {
      "main": [
        [
          {
            "node": "Loop One Row",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Loop One Row": {
      "main": [
        [],
        [
          {
            "node": "Function: Prep TTS Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function: Prep TTS Input": {
      "main": [
        [
          {
            "node": "ElevenLabs: TTS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ElevenLabs: TTS": {
      "main": [
        [
          {
            "node": "Function: Tag Audio File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function: Tag Audio File": {
      "main": [
        [
          {
            "node": "Discord: Voiceover + Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord: Voiceover + Brief": {
      "main": [
        [
          {
            "node": "Airtable: Mark TTS Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable: Mark TTS Done": {
      "main": [
        [
          {
            "node": "Throttle 2s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Throttle 2s": {
      "main": [
        [
          {
            "node": "Loop One Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Helsinki",
    "errorWorkflow": ""
  },
  "tags": [
    {
      "name": "bryom"
    },
    {
      "name": "content"
    },
    {
      "name": "b-lite"
    }
  ],
  "meta": {
    "templateCredsSetupCompleted": false
  }
}