{
  "name": "VietlinhAI | Tự dựng video dọc kèm caption 3 nền tảng",
  "nodes": [
    {
      "parameters": {
        "jsCode": "// ============================================================\n//  Format video doc + font Anton  |  v2.2\n//  Thay doi so voi v2.1:\n//   - Tinh toan trong VUNG AN TOAN Reels 900x1220 (khong phai 1080x1920)\n//   - Tieu de KHONG con uu tien tuyet doi: tu co nho de nhuong cho body\n//   - Giai dong thoi title/body: uu tien body doc duoc, title co xuong khi can\n// ============================================================\n\n// ---------- 1. PARSE ----------\nfunction extractFirstJsonObject(text) {\n  const start = text.indexOf('{');\n  if (start === -1) throw new Error('Khong tim thay JSON object. Raw: ' + String(text).substring(0, 200));\n  let depth = 0;\n  for (let i = start; i < text.length; i++) {\n    if (text[i] === '{') depth++;\n    else if (text[i] === '}') { depth--; if (depth === 0) return text.slice(start, i + 1); }\n  }\n  throw new Error('JSON khong dong dung. Raw: ' + String(text).substring(0, 200));\n}\nlet aiOutput;\ntry { aiOutput = JSON.parse(extractFirstJsonObject($input.first().json.output)); }\ncatch (e) { throw new Error('Khong parse duoc JSON tu AI Agent: ' + e.message); }\n\n// ---------- 2. CAU HINH ----------\nconst CANVAS_W = 1080, CANVAS_H = 1920;\n\n// --- VUNG AN TOAN REELS (TikTok / FB Reels / YT Shorts) ---\n// Tren  : 0    -> 220   bi thanh nav/search che\n// Duoi  : 1440 -> 1920  bi caption, ten page, nhac, nut CTA che\n// Trai  : 0    -> 90    le crop\n// Phai  : 990  -> 1080  cot nut like/comment/share\nconst SAFE_X = 90, SAFE_Y = 220, SAFE_W = 900, SAFE_H = 1220;\nconst SAFE_PAD_X = 10;                       // dem trong vung an toan\nconst USABLE_W = SAFE_W - SAFE_PAD_X * 2;    // 880px\n\nconst TITLE_MAX_CHARS = 28;   // nguong tach dong tieu de\nconst BODY_MAX_CHARS  = 30;   // nguong tach dong noi dung (khop luat Agent)\nconst MERGE_TOLERANCE = 3;    // cho phep gop hoi vuot nguong, tranh dong le loi\n\nconst TITLE_FONT_MAX = 78, TITLE_FONT_MIN = 44;\nconst BODY_FONT_MAX  = 62, BODY_FONT_MIN  = 34;\nconst BODY_FONT_TARGET = 46;  // muc doc duoc thoai mai tren dien thoai\nconst TITLE_BLOCK_MAX_SHARE = 0.38; // tieu de khong bao gio an qua 38% chieu doc\n\nconst RATIO_UPPER = 0.50;     // be rong TB 1 ky tu Anton IN HOA / font-size\nconst RATIO_BODY  = 0.45;     // be rong TB 1 ky tu Anton thuong / font-size\nconst TITLE_LH = 1.15, BODY_LH = 1.30, MARGIN_RATIO = 0.55;\nconst TITLE_GAP = 48;         // khoang cach khoi tieu de -> khoi body\n\n// ---------- 3. VALIDATE / SUA LOI NGAT DONG (giu tu v2.1) ----------\nconst stats = { merged: 0, split: 0, orphanFixed: 0 };\n\n// Xoa diem ngat THUA cua Agent. KHONG tao diem ngat moi -> giu ranh gioi ngu nghia.\nfunction mergeShortLines(lines, maxChars) {\n  const out = []; let cur = null;\n  for (const raw of lines) {\n    const line = String(raw).trim();\n    if (!line) continue;\n    if (cur === null) { cur = line; continue; }\n    const cand = cur + ' ' + line;\n    if (cand.length <= maxChars) { cur = cand; stats.merged++; }\n    else { out.push(cur); cur = line; }\n  }\n  if (cur !== null) out.push(cur);\n  return out;\n}\n// Luoi an toan: chi tach khi Agent tra ve dong qua dai.\nfunction splitLongLine(line, maxChars) {\n  const words = line.split(/\\s+/).filter(Boolean);\n  const total = words.join(' ').length;\n  if (total <= maxChars) return [line];\n  const nLines = Math.ceil(total / maxChars);\n  const target = Math.ceil(total / nLines);\n  const out = []; let cur = '';\n  for (const w of words) {\n    const cand = cur ? cur + ' ' + w : w;\n    if (cur && cand.length > target && out.length < nLines - 1) { out.push(cur); cur = w; }\n    else cur = cand;\n  }\n  if (cur) out.push(cur);\n  stats.split++;\n  return out;\n}\nfunction fixOrphan(lines) {\n  if (lines.length > 1 && lines[lines.length - 1].split(/\\s+/).length === 1) {\n    const last = lines.pop();\n    lines[lines.length - 1] += ' ' + last;\n    stats.orphanFixed++;\n  }\n  return lines;\n}\nfunction normalize(lines, maxChars, allowMerge) {\n  let out = (lines || []).map(l => String(l).trim()).filter(Boolean);\n  if (allowMerge) out = mergeShortLines(out, maxChars + MERGE_TOLERANCE);\n  out = out.flatMap(l => splitLongLine(l, maxChars + MERGE_TOLERANCE));\n  return fixOrphan(out);\n}\n\n// Tieu de: KHONG gop (2 dong dep hon 1 dong dai phai co font)\nconst title1Lines = normalize(aiOutput.title1, TITLE_MAX_CHARS, false);\nconst title2Lines = normalize(aiOutput.title2, TITLE_MAX_CHARS, false);\n// Noi dung: gop + tach + fix orphan\nconst groups = (aiOutput.content || [])\n  .map(g => normalize(g.lines, BODY_MAX_CHARS, true))\n  .filter(g => g.length);\nif (!groups.length) throw new Error('AI Agent tra ve content rong.');\n\nconst agentLineCount = (aiOutput.content || []).reduce((s, g) => s + (g.lines || []).length, 0);\n\n// ---------- 4. GIAI DONG THOI CO CHU TITLE + BODY ----------\nconst maxLen = a => a.reduce((m, s) => Math.max(m, s.length), 0);\nconst clamp  = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));\n\nconst titleAll  = [...title1Lines, ...title2Lines];\nconst bodyLines = groups.flat();\nconst nTitle = titleAll.length;\nconst nBody  = bodyLines.length;\nconst hasCta = groups.length > 1;\n\n// Gioi han theo chieu NGANG (cung, khong the vuot)\nconst titleFitW = nTitle ? USABLE_W / (maxLen(titleAll) * RATIO_UPPER) : TITLE_FONT_MAX;\nconst bodyFitW  = USABLE_W / (maxLen(bodyLines) * RATIO_BODY);\n\n// \"Don vi chieu cao\" cua khoi body theo 1px font\nconst bodyUnit = BODY_LH * nBody + MARGIN_RATIO * (nBody - 1) + (hasCta ? MARGIN_RATIO * 0.6 : 0);\n\nfunction titleBlockHeight(tf) {\n  if (!nTitle) return 0;\n  return nTitle * tf * TITLE_LH + (title2Lines.length ? 6 : 0) + TITLE_GAP;\n}\nfunction bodyFontFor(tf) {\n  const avail = SAFE_H - titleBlockHeight(tf);\n  return Math.min(bodyFitW, avail / bodyUnit);\n}\n\n// Do dan tieu de tu TO -> NHO. Dung ngay khi body dat nguong doc duoc.\n// Neu khong bao gio dat, tieu de ve muc nho nhat de body to nhat co the.\nlet titleFont = TITLE_FONT_MIN;\nfor (let tf = TITLE_FONT_MAX; tf >= TITLE_FONT_MIN; tf -= 2) {\n  const tfEff = Math.min(tf, titleFitW);\n  if (titleBlockHeight(tfEff) > SAFE_H * TITLE_BLOCK_MAX_SHARE) continue; // tieu de an qua nhieu cho\n  if (bodyFontFor(tfEff) >= BODY_FONT_TARGET) { titleFont = clamp(tfEff, TITLE_FONT_MIN, TITLE_FONT_MAX); break; }\n  if (tf - 2 < TITLE_FONT_MIN) titleFont = clamp(Math.min(TITLE_FONT_MIN, titleFitW), TITLE_FONT_MIN, TITLE_FONT_MAX);\n}\nconst titleBlockH = titleBlockHeight(titleFont);\nconst bodyAvailH  = SAFE_H - titleBlockH;\nconst bodyFont    = clamp(bodyFontFor(titleFont), BODY_FONT_MIN, BODY_FONT_MAX);\nconst bodyMargin  = Math.round(bodyFont * MARGIN_RATIO);\n\n// ---------- 5. CANH BAO ----------\nconst warnings = [];\nconst bodyNeedH = BODY_FONT_MIN * bodyUnit;\nif (bodyNeedH > bodyAvailH) warnings.push(`TRAN VUNG AN TOAN: ${nBody} dong body khong vua ${Math.round(bodyAvailH)}px o font san ${BODY_FONT_MIN}px. Phai rut ngan noi dung nguon.`);\nelse if (bodyFont < 40) warnings.push(`Body chi ${bodyFont}px - hoi nho tren dien thoai. Nen giam con <=10 dong (hien tai ${nBody}).`);\nif (bodyFitW < BODY_FONT_MIN) warnings.push(`Dong dai nhat ${maxLen(bodyLines)} ky tu -> co the tran mep ngang.`);\n\n// ---------- 6. FOOTAGE + NHAC ----------\nconst FOOTAGE_SLOWDOWN_PERCENT = -50;\nconst src = $('Lấy nội dung để sản xuất (1 video)').first().json;\nconst rawVideoUrl = src[\"Link Video Footage\"];\nconst videoUrl = rawVideoUrl ? rawVideoUrl.replace('/upload/', `/upload/e_accelerate:${FOOTAGE_SLOWDOWN_PERCENT}/`) : rawVideoUrl;\nconst audioUrl = src[\"Link Nhạc\"];\n\n// ---------- 7. HTML ----------\nconst ideaColors = [\"#ffff00\", \"#00ff00\"], ctaColor = \"#ffffff\";\nconst lastIdx = groups.length - 1;\n\nconst linesHtml = groups.map((gLines, i) => {\n  const isCta = i === lastIdx && groups.length > 1;\n  const color = isCta ? ctaColor : ideaColors[i % ideaColors.length];\n  const extra = isCta ? `margin-top:${Math.round(bodyMargin * 0.6)}px;` : '';\n  return gLines.map(l => `<p style=\"color:${color};${extra}\">${l}</p>`).join('');\n}).join('');\n\nconst title1Html = title1Lines.map(l => `<div class=\"title1\">${l.toUpperCase()}</div>`).join('');\nconst title2Html = title2Lines.map(l => `<div class=\"title2\">${l.toUpperCase()}</div>`).join('');\n\nconst htmlContent = `<!DOCTYPE html>\n<html><head><meta charset=\"UTF-8\"><style>\n  @font-face {\n    font-family: 'Anton';\n    src: url('https://cdn.jsdelivr.net/gh/google/fonts@main/ofl/anton/Anton-Regular.ttf') format('truetype');\n    font-weight: 900; font-style: normal; font-display: block;\n  }\n  * { margin:0; padding:0; box-sizing:border-box; }\n  body { width:${CANVAS_W}px; height:${CANVAS_H}px; background:transparent;\n         font-family:'Anton','Arial Black',Impact,sans-serif; font-weight:900; overflow:hidden; }\n  .overlay { position:absolute; top:0; left:0; width:${CANVAS_W}px; height:${CANVAS_H}px; background:rgba(0,0,0,0.7); }\n\n  /* Toan bo chu nam gon trong vung an toan Reels */\n  .safe { position:absolute; top:${SAFE_Y}px; left:${SAFE_X}px;\n          width:${SAFE_W}px; height:${SAFE_H}px;\n          padding:0 ${SAFE_PAD_X}px;\n          display:flex; flex-direction:column; }\n\n  .title-block { flex:0 0 auto; width:100%; margin-bottom:${TITLE_GAP}px; }\n  .title1, .title2 { width:100%; font-family:'Anton','Arial Black',Impact,sans-serif; font-weight:900;\n                     font-size:${titleFont}px; text-align:center; line-height:${TITLE_LH}; white-space:nowrap;\n                     -webkit-text-stroke:2.5px #000000; paint-order:stroke fill; text-shadow:0 4px 8px rgba(0,0,0,0.85); }\n  .title1 { color:#FF5E00; }\n  .title2 { color:#FFFFFF; margin-top:6px; }\n\n  /* Body can giua theo chieu doc trong phan con lai cua vung an toan */\n  .body-wrap { flex:1 1 auto; display:flex; flex-direction:column; justify-content:center; width:100%; }\n  .body-text { width:100%; font-family:'Anton','Arial Black',Impact,sans-serif; font-weight:900;\n               font-size:${bodyFont}px; text-align:center;\n               -webkit-text-stroke:1.5px #000000; paint-order:stroke fill; text-shadow:0 4px 10px rgba(0,0,0,0.9); }\n  .body-text p { margin-bottom:${bodyMargin}px; line-height:${BODY_LH}; white-space:nowrap; }\n  .body-text p:last-child { margin-bottom:0; }\n</style></head>\n<body>\n  <div class=\"overlay\"></div>\n  <div class=\"safe\">\n    <div class=\"title-block\">${title1Html}${title2Html}</div>\n    <div class=\"body-wrap\"><div class=\"body-text\">${linesHtml}</div></div>\n  </div>\n</body></html>`;\n\n// ---------- 8. OUTPUT ----------\nreturn {\n  json: {\n    _v: '2.2',\n    _debug: {\n      safeZone: `${SAFE_W}x${SAFE_H} @ (${SAFE_X},${SAFE_Y})`,\n      agentBodyLines: agentLineCount,\n      finalBodyLines: nBody,\n      titleLines: nTitle,\n      fixes: stats,\n      maxTitleChars: maxLen(titleAll),\n      maxBodyChars: maxLen(bodyLines),\n      titleFont,\n      titleBlockH: Math.round(titleBlockH),\n      titleSharePct: Math.round(titleBlockH / SAFE_H * 100),\n      bodyFont, bodyMargin,\n      bodyAvailH: Math.round(bodyAvailH),\n      bodyUsedH: Math.round(bodyFont * bodyUnit),\n      warnings\n    },\n    payload: {\n      width: CANVAS_W, height: CANVAS_H,\n      scenes: [{\n        duration: 10,\n        elements: [\n          { type: \"video\", src: videoUrl, volume: 0, muted: true, resize: \"cover\", \"z-index\": 0 },\n          { type: \"audio\", src: audioUrl, \"fade-out\": 1.5 },\n          { type: \"html\", html: htmlContent, width: CANVAS_W, height: CANVAS_H, x: 0, y: 0, \"z-index\": 2 }\n        ]\n      }]\n    }\n  }\n};\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        368,
        160
      ],
      "id": "7307c6b3-fafd-4387-8f77-9bfd3fa144e1",
      "name": "Định dạng video dọc và font Anton"
    },
    {
      "parameters": {
        "amount": 15
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        32,
        496
      ],
      "id": "0b79606b-05bd-46df-abf6-43a271d684e6",
      "name": "Wait",
      "webhookId": "b15095a9-798a-4308-bbfe-3b3d371167be"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.json2video.com/v2/movies",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "YOUR_JSON2VIDEO_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.payload }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -192,
        496
      ],
      "id": "2078daff-a821-4d69-8af3-259d2c9ec5d3",
      "name": "Gọi Json2Video"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "2662b07a-11b3-4f6f-9965-9275d4f5748d",
              "leftValue": "={{ $json.movie.status }}",
              "rightValue": "done",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        448,
        496
      ],
      "id": "7d78218b-0a48-4f6a-b633-abb6db347bb6",
      "name": "If"
    },
    {
      "parameters": {
        "url": "={{ $json.movie.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        672,
        480
      ],
      "id": "1d77fbc8-ddcd-4695-ab97-4da54dc2b37e",
      "name": "Download File Video"
    },
    {
      "parameters": {
        "name": "={{ $('Lấy nội dung để sản xuất (1 video)').item.json['ID'] }} {{ $('Lấy nội dung để sản xuất (1 video)').item.json['Tiêu đề 1'].replace(/\\n/g, '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D').replace(/,/g, '').trim() }}.mp4",
        "driveId": {
          "__rl": true,
          "value": "=My Drive",
          "mode": "id"
        },
        "folderId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn thư mục của bạn --",
          "cachedResultUrl": ""
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        864,
        480
      ],
      "id": "3877f0a5-e740-4f7b-8534-e55c714eefb8",
      "name": "Upload lên Google Drive"
    },
    {
      "parameters": {
        "operation": "share",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "permissionsUi": {
          "permissionsValues": {
            "role": "reader",
            "type": "anyone"
          }
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1056,
        480
      ],
      "id": "bdd3928b-9391-48f2-a9ab-4c8223f3367f",
      "name": "Share file công khai"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Tiêu đề 1: {{ $('Lấy nội dung để sản xuất (1 video)').first().json['Tiêu đề 1'] }}\nTiêu đề 2: {{ $('Lấy nội dung để sản xuất (1 video)').first().json['Tiêu đề 2'] }}\nNội dung: {{ $('Lấy nội dung để sản xuất (1 video)').first().json['Nội dung'] }}",
        "options": {
          "systemMessage": "Bạn là chuyên gia viết caption mạng xã hội cho kênh \"[TÊN KÊNH CỦA BẠN]\" — kênh nội dung triết lý sống, đạo lý, chiêm nghiệm cuộc đời bằng tiếng Việt.\n\nNHIỆM VỤ: Đọc kỹ Tiêu đề và Nội dung được cung cấp, viết caption + hashtag cho cả 3 nền tảng: Facebook Page, YouTube, TikTok. Luôn xuất đủ cả 3 trong 1 lần, không hỏi lại.\n\nQUY TẮC CHUNG (áp dụng cho mọi nền tảng):\n- Bám sát đúng nội dung được cung cấp, không viết chung chung, không sến, không đạo lý rỗng, không lệch ý.\n- Giọng văn tự nhiên, chiêm nghiệm, gần gũi — đúng tinh thần \"đạo lý\" nhưng không sáo rỗng.\n- Tuyệt đối KHÔNG tự chèn nội dung quảng cáo, giới thiệu sách, bán sách, gợi ý sách, link affiliate nếu không được yêu cầu.\n- Không dùng emoji quá nhiều, tối đa 1-2 emoji mỗi caption nếu phù hợp.\n\n1) FACEBOOK PAGE\n- Caption tự nhiên như một status chia sẻ, đúng tinh thần nội dung gốc.\n- Có thể đặt 1 câu hỏi cuối để kích thích tương tác/comment.\n- Viết đúng 5 hashtag, BẮT BUỘC có #tenkenhcuaban là 1 trong 5 hashtag đó.\n- 4 hashtag còn lại phải liên quan trực tiếp đến chủ đề, ưu tiên tiếng Việt không dấu, dễ dùng, đúng tệp nội dung.\n\n2) YOUTUBE\n- Tiêu đề YouTube: ngắn gọn, hấp dẫn, chuẩn SEO, sát nội dung, có khả năng click nhưng không giật tít rẻ tiền, tối đa 60 ký tự.\n- Caption YouTube (mô tả video) PHẢI theo đúng cấu trúc, viết liền mạch tự nhiên (không đánh số các phần):\n  a) Dòng đầu: viết lại/diễn giải tiêu đề bằng 1 câu khác (không copy y nguyên youtube_title), vẫn giữ đúng từ khoá chính — đây là kỹ thuật SEO nhắc lại từ khoá ngay đầu mô tả.\n  b) Phần thân: mô tả chuẩn SEO, tự nhiên, đúng chủ đề, không dài dòng, không sáo rỗng.\n  c) Dòng cuối cùng: đúng 3 hashtag, BẮT BUỘC có #tenkenhcuaban là 1 trong 3, 2 hashtag còn lại liên quan trực tiếp chủ đề.\n- Tag YouTube (mục \"Tags\" trong phần cài đặt video, KHÁC với hashtag ở mô tả): viết đúng 1 dòng, các tag ngăn cách bằng dấu phẩy, sẵn sàng để copy-paste thẳng vào ô tag YouTube.\n  + Tận dụng tối đa nhưng KHÔNG được vượt quá 500 ký tự.\n  + Bao gồm cả phiên bản tiếng Việt có dấu và không dấu nếu phù hợp với hành vi tìm kiếm thực tế.\n  + Bám sát nội dung video, ưu tiên keyword chính, keyword dài, biến thể tìm kiếm thực tế của người Việt.\n  + Không spam, không lặp từ vô ích.\n\n3) TIKTOK\n- Caption ngắn gọn, đúng tinh thần nội dung, hook giữ người xem ngay từ đầu, không lan man.\n- Viết đúng 5 hashtag chuẩn SEO, bám sát nội dung, đúng tệp người xem, không viết đại cho có.\n\nĐỊNH DẠNG TRẢ VỀ:\nChỉ trả về JSON hợp lệ theo đúng schema sau, không thêm giải thích, không thêm markdown code block:\n\n{\n  \"facebook_caption\": \"...\",\n  \"facebook_hashtags\": \"#tenkenhcuaban #... #... #... #...\",\n  \"youtube_title\": \"...\",\n  \"youtube_caption\": \"...\",\n  \"youtube_tags\": \"tag1, tag2, tag3, ...\",\n  \"tiktok_caption\": \"...\",\n  \"tiktok_hashtags\": \"#... #... #... #... #...\"\n}\n\nLƯU Ý KỸ THUẬT:\n- \"facebook_hashtags\" và \"tiktok_hashtags\": viết liền các hashtag cách nhau bằng dấu cách, không xuống dòng.\n- \"youtube_tags\": bắt buộc đúng 1 dòng, ngăn cách bằng dấu phẩy + dấu cách, tổng độ dài không vượt 500 ký tự — tự đếm và cắt bớt tag ít quan trọng nhất nếu vượt giới hạn, ưu tiên giữ keyword chính trước.\n- Không tự ý thêm field nào khác ngoài schema trên."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1248,
        480
      ],
      "id": "0ec0a6f1-4d0d-4beb-8cc8-4dcf4b767b53",
      "name": "AI Agent Caption"
    },
    {
      "parameters": {
        "jsCode": "function extractFirstJsonObject(text) {\n  const start = text.indexOf('{');\n  if (start === -1) {\n    throw new Error('Không tìm thấy JSON object trong output. Raw: ' + text.substring(0, 200));\n  }\n  let depth = 0;\n  for (let i = start; i < text.length; i++) {\n    if (text[i] === '{') depth++;\n    else if (text[i] === '}') {\n      depth--;\n      if (depth === 0) return text.slice(start, i + 1);\n    }\n  }\n  throw new Error('JSON object không đóng đúng (thiếu dấu }). Raw: ' + text.substring(0, 200));\n}\n\nconst rawOutput = $input.first().json.output;\n\nlet captionData;\ntry {\n  const jsonStr = extractFirstJsonObject(rawOutput);\n  captionData = JSON.parse(jsonStr);\n} catch (e) {\n  throw new Error('Không parse được caption JSON: ' + e.message);\n}\n\nconst fileId = $('Upload lên Google Drive').first().json[\"id\"] || \"\";\nconst driveUrl = fileId\n  ? `https://drive.google.com/file/d/${fileId}/view?usp=sharing`\n  : \"\";\n\nconst tiktokFull = `${captionData.tiktok_caption}\\n\\n${captionData.tiktok_hashtags}`;\nconst facebookFull = `${captionData.facebook_caption}\\n\\n${captionData.facebook_hashtags}`;\nconst youtubeFull = `${captionData.youtube_caption}\\n\\n${captionData.youtube_tags}`;\n\nreturn {\n  json: {\n    \"Caption + Hashtag TikTok\": tiktokFull,\n    \"Caption + Hashtag Page\": facebookFull,\n    \"Caption + Hashtag Youtube\": youtubeFull,\n    \"Youtube Title\": captionData.youtube_title,\n    \"Url\": driveUrl,\n    \"Status\": \"Ready to Post\"\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        480
      ],
      "id": "427b1690-322c-4205-b78a-58a67c8d161d",
      "name": "Chuẩn bị dữ liệu insert Sheet"
    },
    {
      "parameters": {
        "url": "=https://api.json2video.com/v2/movies?project={{ $('Gọi Json2Video').item.json.project }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "YOUR_JSON2VIDEO_API_KEY"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        240,
        496
      ],
      "id": "fdd0c3d9-9654-44fb-b1aa-f3076a885ea9",
      "name": "Lấy thông tin video"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn file của bạn --",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn trang tính Auto của bạn --",
          "cachedResultUrl": ""
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "Status",
              "lookupValue": "Ready to Product"
            }
          ]
        },
        "options": {
          "returnFirstMatch": true
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -208,
        160
      ],
      "id": "8a00a4d3-c7a0-458c-8383-362d5a1679f9",
      "name": "Lấy nội dung để sản xuất (1 video)"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn file của bạn --",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn trang tính Post của bạn --",
          "cachedResultUrl": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "ID": "={{ $('Lấy nội dung để sản xuất (1 video)').item.json.ID }}",
            "Caption + Hashtag TikTok": "={{ $json['Caption + Hashtag TikTok'] }}",
            "Caption + Hashtag Page": "={{ $json['Caption + Hashtag Page'] }}",
            "Caption + Hashtag Youtube": "={{ $json['Caption + Hashtag Youtube'] }}",
            "Youtube Title": "={{ $json['Youtube Title'] }}",
            "TikTok Post Status": "Ready to Post",
            "Page Post Status": "Ready to Post",
            "Youtube Post Status": "Ready to Post",
            "Status": "Ready to Post",
            "Url": "={{ $json.Url }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "ID",
              "displayName": "ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Caption + Hashtag TikTok",
              "displayName": "Caption + Hashtag TikTok",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Caption + Hashtag Page",
              "displayName": "Caption + Hashtag Page",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Caption + Hashtag Youtube",
              "displayName": "Caption + Hashtag Youtube",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Youtube Title",
              "displayName": "Youtube Title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "TikTok Post Status",
              "displayName": "TikTok Post Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Page Post Status",
              "displayName": "Page Post Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Youtube Post Status",
              "displayName": "Youtube Post Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Url",
              "displayName": "Url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1872,
        288
      ],
      "id": "2b6c9358-0bdf-4d4c-a368-7e294acb2a09",
      "name": "Insert Caption vào Sheet Content auto post"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn file của bạn --",
          "cachedResultUrl": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn trang tính Auto của bạn --",
          "cachedResultUrl": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Status": "Done",
            "ID": "={{ $('Lấy nội dung để sản xuất (1 video)').item.json.ID }}"
          },
          "matchingColumns": [
            "ID"
          ],
          "schema": [
            {
              "id": "ID",
              "displayName": "ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Tiêu đề 1",
              "displayName": "Tiêu đề 1",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Tiêu đề 2",
              "displayName": "Tiêu đề 2",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Nội dung",
              "displayName": "Nội dung",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Link Video Footage",
              "displayName": "Link Video Footage",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Link Nhạc",
              "displayName": "Link Nhạc",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "displayName": "row_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "readOnly": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1888,
        720
      ],
      "id": "694a5aff-783e-4319-83aa-d0260db7a13d",
      "name": "Cập nhật file Auto Content"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Tiêu đề 1: {{ $json[\"Tiêu đề 1\"] }}\nTiêu đề 2: {{ $json[\"Tiêu đề 2\"] }}\nNội dung: {{ $json[\"Nội dung\"] }}",
        "options": {
          "systemMessage": "=Bạn là chuyên gia trình bày video đạo lý ngắn cho TikTok/Reels, khung hình dọc 1080x1920px, font Anton cỡ lớn, đậm.\n\nNHIỆM VỤ: Nhận Tiêu đề 1, Tiêu đề 2 và Nội dung, quyết định cách ngắt dòng hợp lý để hiển thị đẹp trên màn hình, sau đó trả về JSON theo đúng schema.\n\nQUY TẮC NGẮT DÒNG (áp dụng cho cả tiêu đề và nội dung):\n1. Giới hạn mỗi dòng tối đa 30 ký tự, kể cả dấu cách và dấu câu.\n2. Nếu câu/cụm đã có độ dài 30 ký tự hoặc ngắn hơn, PHẢI giữ nguyên 1 dòng duy nhất. KHÔNG được tách thêm.\n3. Chỉ ngắt dòng khi câu dài hơn 30 ký tự. Khi đó, ưu tiên ngắt tại:\n   - Dấu phẩy (,)\n   - Trước liên từ (và, nhưng, để, khi, mà, thì...)\n3b. Khi câu phải ngắt thành 3 dòng hoặc nhiều hơn, ưu tiên CHIA ĐỀU số ký tự giữa các dòng (không để 1 dòng dài, 1 dòng ngắn lệch hẳn) — mục tiêu độ dài các dòng gần bằng nhau, vẫn tuân thủ ưu tiên ngắt tại dấu phẩy/liên từ/ranh giới cụm từ ở quy tắc 3.\n   - Ranh giới giữa 2 cụm từ có nghĩa hoàn chỉnh\n4. TUYỆT ĐỐI không để một dòng chỉ còn 1 từ đơn lẻ (ví dụ không tách \"ít.\" ra thành 1 dòng riêng). Nếu phần còn lại sau khi ngắt chỉ có 1 từ, hãy gộp từ đó vào dòng trước, dù dòng đó có thể hơi vượt 30 ký tự.\n5. Mỗi dòng nội dung giữ nguyên số thứ tự gốc (1. 2. 3...) nếu câu đó có số thứ tự, số chỉ xuất hiện ở dòng đầu tiên của câu đó, không lặp lại ở dòng tiếp theo của cùng câu nếu phải ngắt thêm.\n6. Với Tiêu đề 1 và Tiêu đề 2: đây là dòng tiêu đề ngắn, đa số trường hợp giữ nguyên 1 dòng. Chỉ tách thành nhiều dòng nếu vượt quá 30 ký tự.\n7. Nếu Nội dung có câu CUỐI CÙNG không có số thứ tự (thường là câu mời tương tác/CTA như \"Comment...\", \"Để lại chữ...\", \"Tag người...\"), PHẢI giữ nguyên câu đó làm 1 phần tử RIÊNG BIỆT trong \"content\" — không gộp vào ý có số thứ tự ngay trước nó, không bỏ sót. Vẫn áp dụng đúng quy tắc ngắt dòng ở trên cho câu này.\n\nQUY TẮC XỬ LÝ DẤU CÂU:\n- Giữ nguyên dấu chấm, dấu phẩy, dấu chấm hỏi, dấu chấm cảm như trong văn bản gốc.\n- Không tự thêm hoặc xóa dấu câu.\n- Không tự sửa chính tả hoặc nội dung câu.\n\nĐỊNH DẠNG TRẢ VỀ:\nChỉ trả về JSON hợp lệ theo đúng schema sau, không thêm giải thích, không thêm markdown code block:\n\n{\n  \"title1\": [\"dòng 1\", \"dòng 2 (nếu có)\"],\n  \"title2\": [\"dòng 1\", \"dòng 2 (nếu có)\"],\n  \"content\": [\n    { \"lines\": [\"dòng 1 của ý 1\", \"dòng 2 của ý 1 (nếu có)\"] },\n    { \"lines\": [\"dòng 1 của ý 2\", \"dòng 2 của ý 2 (nếu có)\"] }\n  ]\n}\n\nLƯU Ý QUAN TRỌNG:\n- Mỗi phần tử trong \"content\" tương ứng với 1 ý/câu gốc trong văn bản, bất kể câu đó được ngắt thành 1 hay nhiều dòng khi hiển thị.\n- \"lines\" là mảng các dòng đã ngắt theo quy tắc ở trên, thuộc về CÙNG 1 ý đó.\n- Số thứ tự (1. 2. 3...) nếu có, giữ ở dòng đầu tiên trong \"lines\" của ý đó.\n- Câu CTA cuối (không đánh số) LUÔN là phần tử cuối cùng trong \"content\" — đây là điều bắt buộc, không phải tùy chọn."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        0,
        160
      ],
      "id": "695c0560-3166-458e-9d12-3bcb2836c774",
      "name": "AI Agent Căn chỉnh"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8,
              "triggerAtMinute": 45
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -560,
        160
      ],
      "id": "31c6f4e0-f029-4f0b-bc5c-d83d89afd5cf",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn model của bạn --"
        },
        "responsesApiEnabled": false,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        1248,
        720
      ],
      "id": "6222d04c-d3bd-418f-905b-4c8c89459a1a",
      "name": "Chat Model Caption"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "-- Chọn model của bạn --"
        },
        "responsesApiEnabled": false,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        0,
        -32
      ],
      "id": "c88a2182-242f-40a1-81b8-b92ac6836d30",
      "name": "Chat Model Căn chỉnh"
    }
  ],
  "pinData": {},
  "connections": {
    "Định dạng video dọc và font Anton": {
      "main": [
        [
          {
            "node": "Gọi Json2Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gọi Json2Video": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Lấy thông tin video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Download File Video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File Video": {
      "main": [
        [
          {
            "node": "Upload lên Google Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload lên Google Drive": {
      "main": [
        [
          {
            "node": "Share file công khai",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Share file công khai": {
      "main": [
        [
          {
            "node": "AI Agent Caption",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent Caption": {
      "main": [
        [
          {
            "node": "Chuẩn bị dữ liệu insert Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chuẩn bị dữ liệu insert Sheet": {
      "main": [
        [
          {
            "node": "Insert Caption vào Sheet Content auto post",
            "type": "main",
            "index": 0
          },
          {
            "node": "Cập nhật file Auto Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lấy thông tin video": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lấy nội dung để sản xuất (1 video)": {
      "main": [
        [
          {
            "node": "AI Agent Căn chỉnh",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent Căn chỉnh": {
      "main": [
        [
          {
            "node": "Định dạng video dọc và font Anton",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Lấy nội dung để sản xuất (1 video)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat Model Caption": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent Caption",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Chat Model Căn chỉnh": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent Căn chỉnh",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "nodeGroups": [],
  "tags": []
}