{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        2000,
        1408
      ],
      "id": "484ca989-a86d-4479-a08f-877415229a79",
      "name": "Manual Test Trigger"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "phase3-start",
        "options": {
          "responseData": "{\"ok\":true}"
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        1984,
        1600
      ],
      "id": "62118ed1-25e5-4cd3-a16a-5bbf0aa60d7c",
      "name": "Phase 3 Start Webhook"
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2256,
        1408
      ],
      "id": "e9666cce-99d9-47d6-a239-fc82642dd6c1",
      "name": "Read Threads Sheet",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "function getWebhookPayload() {\n  try {\n    const webhookItem = $('Phase 3 Start Webhook').first();\n    return webhookItem.json.body || webhookItem.json || {};\n  } catch (error) {\n    return {};\n  }\n}\n\nconst payload = getWebhookPayload();\nconst targetId = String(payload.id || payload.ID || '').trim();\nconst targetStatus = String(payload.status || payload.Status || '').trim().toLowerCase();\nconst targetScript = String(payload.narrator_script || payload.Narrator_Script || '').trim();\nconst targetAudio = String(payload.audio_path || payload.Audio_Path || '').trim();\n\nif (targetId && targetStatus === 'in progress' && targetScript && !targetAudio) {\n  return [{\n    json: {\n      ID: targetId,\n      Narrator_Script: payload.Narrator_Script || payload.narrator_script || '',\n      Extracted_Content: payload.Extracted_Content || payload.extracted_content || '',\n      Status: payload.Status || payload.status || 'In Progress',\n      Note: payload.Note || payload.note || '',\n    },\n  }];\n}\n\nconst rows = $input.all().filter(item => {\n  const id = String(item.json.ID || '').trim();\n  const status = String(item.json.Status || '').trim().toLowerCase();\n  const hasScript = String(item.json.Narrator_Script || '').trim();\n  const hasAudio = String(item.json.Audio_Path || '').trim();\n  if (targetId && id !== targetId) return false;\n  return status === 'in progress' && hasScript && !hasAudio;\n});\n\nreturn rows.slice(0, 1);"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2512,
        1168
      ],
      "id": "80855357-a1e8-4f8b-8021-7fbe5242f106",
      "name": "Pick Voice Row"
    },
    {
      "parameters": {
        "jsCode": "function getTargetId() {\n  try {\n    const webhookItem = $('Phase 3 Start Webhook').first();\n    const payload = webhookItem.json.body || webhookItem.json || {};\n    return String(payload.id || '').trim();\n  } catch (error) {\n    return '';\n  }\n}\n\nconst targetId = getTargetId();\nconst rows = $input.all().filter(item => {\n  const id = String(item.json.ID || '').trim();\n  const status = String(item.json.Status || '').trim().toLowerCase();\n  const hasScreenshots = String(item.json.Screenshots || '').trim();\n  const hasAudio = String(item.json.Audio_Path || '').trim();\n  const hasTiming = String(item.json.Audio_Timing || '').trim();\n  const hasVisual = String(item.json.Visual_Video_Path || '').trim();\n  if (targetId && id !== targetId) return false;\n  return status === 'in progress' && hasScreenshots && hasAudio && hasTiming && !hasVisual;\n});\n\nreturn rows.slice(0, 1);"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2512,
        1328
      ],
      "id": "859890d7-e640-49d8-8bac-23a20b419231",
      "name": "Pick Visual Row"
    },
    {
      "parameters": {
        "jsCode": "function getTargetId() {\n  try {\n    const webhookItem = $('Phase 3 Start Webhook').first();\n    const payload = webhookItem.json.body || webhookItem.json || {};\n    return String(payload.id || '').trim();\n  } catch (error) {\n    return '';\n  }\n}\n\nconst targetId = getTargetId();\nconst rows = $input.all().filter(item => {\n  const id = String(item.json.ID || '').trim();\n  const status = String(item.json.Status || '').trim().toLowerCase();\n  const audioPath = String(item.json.Audio_Path || '').trim();\n  const visualPath = String(item.json.Visual_Video_Path || '').trim();\n  const finalPath = String(item.json.Video_Path || '').trim();\n  if (targetId && id !== targetId) return false;\n  return status === 'in progress' && audioPath && visualPath && !finalPath;\n});\n\nreturn rows.slice(0, 1);"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3360,
        1600
      ],
      "id": "b618de55-5d33-450e-81e2-884fc68b7968",
      "name": "Pick Merge Row"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:8000/phase3/voice",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { id: $json.ID, script: $json.Narrator_Script, extracted_content: $json.Extracted_Content || '' } }}",
        "options": {
          "timeout": 900000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2768,
        1168
      ],
      "id": "6509f73e-66e7-42aa-8d99-bf4f648ac36b",
      "name": "Run Voice Generator",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 30000
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:8000/phase3/visual",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { id: $json.ID, screenshots: $json.Screenshots, script: $json.Narrator_Script || '', audio_path: $json.Audio_Path || '', audio_timing: $json.Audio_Timing || '', extracted_content: $json.Extracted_Content || '' } }}",
        "options": {
          "timeout": 900000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2768,
        1328
      ],
      "id": "f2a50f98-89f4-425a-9bbe-2dcf1af831de",
      "name": "Run Visual Builder",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 30000
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:8000/phase3/merge",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { id: $json.ID, audio_path: $json.Audio_Path, visual_path: $json.Visual_Video_Path, script: $json.Narrator_Script || '', extracted_content: $json.Extracted_Content || '' } }}",
        "options": {
          "timeout": 1200000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3600,
        1600
      ],
      "id": "3c2a893c-cfd2-41ff-a6fe-8886138a13f7",
      "name": "Run Merge Final",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 30000
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "ID": "={{ $json.ID }}",
            "Audio_Path": "={{ $json.Audio_Path }}",
            "Audio_Timing": "={{ $json.Audio_Timing }}",
            "Status": "={{ $json.Status }}",
            "Note": "={{ $json.Note }}"
          },
          "matchingColumns": [
            "ID"
          ],
          "schema": [
            {
              "id": "ID",
              "displayName": "ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Source_URL",
              "displayName": "Source_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Collected_At",
              "displayName": "Collected_At",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Screenshots",
              "displayName": "Screenshots",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Extracted_Content",
              "displayName": "Extracted_Content",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Narrator_Script",
              "displayName": "Narrator_Script",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Audio_Path",
              "displayName": "Audio_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Audio_Timing",
              "displayName": "Audio_Timing",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Visual_Video_Path",
              "displayName": "Visual_Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Video_Path",
              "displayName": "Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Caption",
              "displayName": "Caption",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Admin_Decision",
              "displayName": "Admin_Decision",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "TikTok_Publish_ID",
              "displayName": "TikTok_Publish_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Published_URL",
              "displayName": "Published_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Note",
              "displayName": "Note",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft_Video_URL",
              "displayName": "Draft_Video_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Draft_Drive_File_ID",
              "displayName": "Draft_Drive_File_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        3024,
        1168
      ],
      "id": "16232393-d513-44c2-a56b-519bc5330f1e",
      "name": "Update Voice Row",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "ID": "={{ $json.ID }}",
            "Visual_Video_Path": "={{ $json.Visual_Video_Path }}",
            "Status": "={{ $json.Status }}",
            "Note": "={{ $json.Note }}"
          },
          "matchingColumns": [
            "ID"
          ],
          "schema": [
            {
              "id": "ID",
              "displayName": "ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Source_URL",
              "displayName": "Source_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Collected_At",
              "displayName": "Collected_At",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Screenshots",
              "displayName": "Screenshots",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Extracted_Content",
              "displayName": "Extracted_Content",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Narrator_Script",
              "displayName": "Narrator_Script",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Audio_Path",
              "displayName": "Audio_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Audio_Timing",
              "displayName": "Audio_Timing",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Visual_Video_Path",
              "displayName": "Visual_Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Video_Path",
              "displayName": "Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Caption",
              "displayName": "Caption",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Admin_Decision",
              "displayName": "Admin_Decision",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "TikTok_Publish_ID",
              "displayName": "TikTok_Publish_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Published_URL",
              "displayName": "Published_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Note",
              "displayName": "Note",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft_Video_URL",
              "displayName": "Draft_Video_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Draft_Drive_File_ID",
              "displayName": "Draft_Drive_File_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2976,
        1328
      ],
      "id": "e253b616-537b-48c1-b69d-05bcb4061dd3",
      "name": "Update Visual Row",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "ID": "={{ $json.ID }}",
            "Video_Path": "={{ $json.Video_Path }}",
            "Caption": "={{ $json.Caption }}",
            "Status": "={{ $json.Status }}",
            "Note": "={{ $json.Note }}"
          },
          "matchingColumns": [
            "ID"
          ],
          "schema": [
            {
              "id": "ID",
              "displayName": "ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Source_URL",
              "displayName": "Source_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Collected_At",
              "displayName": "Collected_At",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Screenshots",
              "displayName": "Screenshots",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Extracted_Content",
              "displayName": "Extracted_Content",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Narrator_Script",
              "displayName": "Narrator_Script",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Audio_Path",
              "displayName": "Audio_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Audio_Timing",
              "displayName": "Audio_Timing",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Visual_Video_Path",
              "displayName": "Visual_Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Video_Path",
              "displayName": "Video_Path",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Caption",
              "displayName": "Caption",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Admin_Decision",
              "displayName": "Admin_Decision",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "TikTok_Publish_ID",
              "displayName": "TikTok_Publish_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Published_URL",
              "displayName": "Published_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Note",
              "displayName": "Note",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft_Video_URL",
              "displayName": "Draft_Video_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Draft_Drive_File_ID",
              "displayName": "Draft_Drive_File_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        4672,
        1584
      ],
      "id": "c5a26d52-8da3-4d56-86ac-d65fcea68f3c",
      "name": "Update Merge Row",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_PHASE4_START_URL || 'http://n8n:5678/webhook/phase4-start' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { id: $json.ID, video_path: $json.Video_Path || '', caption: $json.Caption || '', status: $json.Status, note: $json.Note || '', draft_video_url: $json.Draft_Video_URL || '', tiktok_publish_id: $json.TikTok_Publish_ID || '' } }}",
        "options": {
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4880,
        1584
      ],
      "id": "f03ad121-8d53-4f00-ada2-e3c9251c93d5",
      "name": "Trigger Phase 4",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        3136,
        1600
      ],
      "id": "33cbcf97-c1e1-47bb-a6d0-df7daede6dbc",
      "name": "Read Threads Sheet For Merge",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
          "mode": "list",
          "cachedResultName": "GOOGLE_SHEET_ID",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Threads",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        3136,
        1456
      ],
      "id": "7a8a34aa-1b95-4ad0-bf78-81c1135ea9ea",
      "name": "Read Threads Sheet For Merge After Visual",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const env = (typeof process !== 'undefined' && process.env) ? process.env : {};\nconst enabledRaw = String(env.AI_CAPTION_ENABLED || env.CAPTION_REWRITE_ENABLED || 'true').trim().toLowerCase();\nconst enabled = ['1', 'true', 'yes', 'on'].includes(enabledRaw);\nconst model = String(env.AI_CAPTION_MODEL || env.SCRIPT_REWRITE_MODEL || 'gemini-2.5-flash-lite').trim();\nfunction cleanText(value) { return String(value || '').replace(/\\s+/g, ' ').trim(); }\nfunction parseJsonObject(value) { try { const parsed = JSON.parse(String(value || '').trim() || '{}'); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch (error) { return {}; } }\nfunction takeTexts(values, limit = 8) { const output = []; const seen = new Set(); for (const item of Array.isArray(values) ? values : []) { const raw = typeof item === 'string' ? item : (item && item.text); const text = cleanText(raw).slice(0, 500); const key = text.toLowerCase().replace(/[^0-9a-z\\u00c0-\\u1ef9]+/g, ''); if (!text || !key || seen.has(key)) continue; seen.add(key); output.push(text); if (output.length >= limit) break; } return output; }\nfor (const item of $input.all()) { const source = item.json; const extracted = parseJsonObject(source.Extracted_Content || ''); const script = cleanText(source.Narrator_Script || ''); const post = cleanText(extracted.post_text || ''); const segments = takeTexts(extracted.segments || [], 10); const comments = takeTexts(extracted.comments || [], 5); const fallbackCaption = cleanText(source.Caption || '') || 'Cau chuyen nay dang duoc ban luan tren Threads\\n\\n#threads #storytime #chuyencuocsong #xuhuong'; source._caption = { enabled, model, prompt: ['You are a Vietnamese TikTok caption strategist.', 'Read the narrator script and source context, then write a natural TikTok caption that teases the story without copying a random script segment.', 'The caption must be in Vietnamese, conversational, concise, and suitable for a story/discussion video sourced from Threads.', 'Do not invent facts. Do not mention AI, automation, screenshots, or internal workflow details.', 'Avoid generic ending questions like Ban nghi sao unless it truly fits the content.', 'Return valid JSON only with this schema: {\"caption\":\"...\",\"hashtags\":[\"#...\",\"#...\"]}.', 'Caption target: 1 to 3 short sentences, max 260 characters before hashtags.', 'Hashtags target: 4 to 7 hashtags. Use specific topic hashtags when clear, then add generic ones such as #threads #storytime #chuyencuocsong #xuhuong.'].join(' '), script, post, segments, comments, fallback_caption: fallbackCaption }; }\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3824,
        1600
      ],
      "id": "a3007880-3d07-46a6-a110-694f31e77f07",
      "name": "Prepare AI Caption"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json._caption.model ? 'models/' + $json._caption.model.replace(/^models\\//, '') : 'models/gemini-2.5-flash-lite' }}"
        },
        "messages": {
          "values": [
            {
              "content": "={{ $json._caption.prompt + '\\n\\nNARRATOR_SCRIPT:\\n' + ($json._caption.script || '(none)') + '\\n\\nPOST_TEXT:\\n' + ($json._caption.post || '(none)') + '\\n\\nSOURCE_SEGMENTS:\\n- ' + ((($json._caption.segments || []).length ? $json._caption.segments : ['(none)']).join('\\n- ')) + '\\n\\nCOMMENTS:\\n- ' + ((($json._caption.comments || []).length ? $json._caption.comments : ['(none)']).join('\\n- ')) + '\\n\\nFALLBACK_CAPTION:\\n' + ($json._caption.fallback_caption || '') + '\\n\\nReturn JSON only.' }}"
            }
          ]
        },
        "simplify": false,
        "jsonOutput": true,
        "builtInTools": {},
        "options": {
          "maxOutputTokens": 800,
          "temperature": 0.65,
          "topP": 0.9
        }
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1.1,
      "position": [
        4000,
        1408
      ],
      "id": "65eebbe5-adea-406d-aac4-b57c1a6c855b",
      "name": "Gemini AI Caption Request",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        4272,
        1584
      ],
      "id": "c3f2c053-50fe-48d4-be2d-2bf89ddbdde4",
      "name": "Merge AI Caption Result"
    },
    {
      "parameters": {
        "jsCode": "const DEFAULT_HASHTAGS = ['#threads', '#storytime', '#chuyencuocsong', '#xuhuong'];\nfunction cleanText(value) { return String(value || '').replace(/\\s+/g, ' ').trim(); }\nfunction stripMarkdown(value) { const fence = String.fromCharCode(96).repeat(3); let text = cleanText(value); if (text.toLowerCase().startsWith(fence + 'json')) text = text.slice(7); else if (text.startsWith(fence)) text = text.slice(3); if (text.endsWith(fence)) text = text.slice(0, -3); return text.trim(); }\nfunction parseJsonCandidate(text) { const cleaned = stripMarkdown(text); if (!cleaned) return null; const candidates = [cleaned]; const braceMatches = cleaned.match(/\\{[\\s\\S]*\\}/g) || []; candidates.push(...braceMatches); for (const candidate of candidates) { try { const parsed = JSON.parse(candidate); if (parsed && typeof parsed === 'object') return parsed; } catch (error) {} } return null; }\nfunction extractTextDeep(value, bucket = []) { if (typeof value === 'string') { const text = cleanText(value); if (text) bucket.push(text); return bucket; } if (!value || typeof value !== 'object') return bucket; if (Array.isArray(value)) { value.forEach((entry) => extractTextDeep(entry, bucket)); return bucket; } for (const key of ['caption', 'text', 'output', 'generatedText', 'content', 'message']) { if (typeof value[key] === 'string') bucket.push(value[key]); } Object.values(value).forEach((entry) => { if (entry && typeof entry === 'object') extractTextDeep(entry, bucket); }); return bucket; }\nfunction extractCaptionPayload(data) { const direct = data && typeof data === 'object' ? data : {}; if (typeof direct.caption === 'string') return direct; for (const text of extractTextDeep(direct, [])) { const parsed = parseJsonCandidate(text); if (parsed && typeof parsed.caption === 'string') return parsed; } return {}; }\nfunction normalizeCaption(value) { let text = cleanText(value).replace(/^caption\\s*:\\s*/i, '').replace(/[\"'_*~|]+/g, ' ').replace(/(^|\\s)#[^\\s#]+/g, ' '); text = cleanText(text); if (text.length > 320) text = cleanText(text.slice(0, 320).replace(/\\s+\\S*$/, '')); return text; }\nfunction normalizeHashtag(value) { let tag = cleanText(value).toLowerCase(); tag = tag.replace(/[.,;:!?()[\\]{}\"'_*~|]/g, ''); tag = tag.replace(/\\s+/g, ''); if (!tag) return ''; if (!tag.startsWith('#')) tag = '#' + tag; tag = tag.replace(/[^#0-9a-z_\\u00c0-\\u1ef9]/g, ''); return tag.length > 1 ? tag : ''; }\nfunction normalizeHashtags(values) { const raw = Array.isArray(values) ? values : String(values || '').split(/\\s+/); const result = []; const seen = new Set(); for (const value of [...raw, ...DEFAULT_HASHTAGS]) { const tag = normalizeHashtag(value); if (!tag || seen.has(tag)) continue; seen.add(tag); result.push(tag); if (result.length >= 7) break; } return result; }\nfor (const item of $input.all()) { const meta = item.json._caption || {}; const note = cleanText(item.json.Note || ''); const fallback = cleanText(meta.fallback_caption || item.json.Caption || ''); const disabled = !meta.enabled; const payload = extractCaptionPayload(item.json); const generatedCaption = normalizeCaption(payload.caption || ''); const hashtags = normalizeHashtags(payload.hashtags || payload.hashtag || ''); if (disabled) { item.json.Caption = fallback; item.json.Note = cleanText(note + ' | AI caption skipped: disabled by AI_CAPTION_ENABLED'); } else if (generatedCaption && generatedCaption.length >= 20) { item.json.Caption = (generatedCaption + '\\n\\n' + hashtags.join(' ')).slice(0, 900).trim(); item.json.Note = cleanText(note + ' | AI caption ready gemini=' + (meta.model || 'default')); } else { item.json.Caption = fallback; item.json.Note = cleanText(note + ' | AI caption fallback: empty Gemini caption gemini=' + (meta.model || 'default')); } delete item.json._caption; }\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4480,
        1584
      ],
      "id": "2e49b8dd-0562-4a2c-b145-992404e05b5d",
      "name": "Finalize AI Caption"
    }
  ],
  "connections": {
    "Manual Test Trigger": {
      "main": [
        [
          {
            "node": "Read Threads Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Phase 3 Start Webhook": {
      "main": [
        [
          {
            "node": "Read Threads Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Threads Sheet": {
      "main": [
        [
          {
            "node": "Pick Voice Row",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read Threads Sheet For Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Voice Row": {
      "main": [
        [
          {
            "node": "Run Voice Generator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Visual Row": {
      "main": [
        [
          {
            "node": "Run Visual Builder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Merge Row": {
      "main": [
        [
          {
            "node": "Run Merge Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Voice Generator": {
      "main": [
        [
          {
            "node": "Update Voice Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Visual Builder": {
      "main": [
        [
          {
            "node": "Update Visual Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Merge Final": {
      "main": [
        [
          {
            "node": "Prepare AI Caption",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Voice Row": {
      "main": [
        [
          {
            "node": "Read Threads Sheet For Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Visual Row": {
      "main": [
        [
          {
            "node": "Read Threads Sheet For Merge After Visual",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Merge Row": {
      "main": [
        [
          {
            "node": "Trigger Phase 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Threads Sheet For Merge": {
      "main": [
        [
          {
            "node": "Pick Visual Row",
            "type": "main",
            "index": 0
          },
          {
            "node": "Pick Merge Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Threads Sheet For Merge After Visual": {
      "main": [
        [
          {
            "node": "Pick Merge Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare AI Caption": {
      "main": [
        [
          {
            "node": "Gemini AI Caption Request",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge AI Caption Result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Gemini AI Caption Request": {
      "main": [
        [
          {
            "node": "Merge AI Caption Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge AI Caption Result": {
      "main": [
        [
          {
            "node": "Finalize AI Caption",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finalize AI Caption": {
      "main": [
        [
          {
            "node": "Update Merge Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}