{
  "id": "ZJq4JKOm81uu2cLw",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Video Translation and Subtitle Rendering Template",
  "tags": [],
  "nodes": [
    {
      "id": "5ddfca70-4e28-4b96-aa14-9e16f7077018",
      "name": "Transcribe: Submit to AssemblyAI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1872,
        112
      ],
      "parameters": {
        "url": "https://api.assemblyai.com/v2/transcript",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"audio_url\": \"{{ $('Prepare: Convert Drive Share URL to Download URL').first().json.download_url }}\",\n  \"language_code\": \"en\",\n  \"speech_models\": [\"universal-3-pro\", \"universal-2\"],\n  \"punctuate\": false,\n  \"format_text\": false,\n  \"speech_understanding\": {\n    \"request\": {\n      \"translation\": {\n        \"target_languages\": [\"ja\"]\n      }\n    }\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c501325f-bbfd-4d1f-a571-b54ca74f7723",
      "name": "Wait: AssemblyAI Processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        2096,
        112
      ],
      "parameters": {
        "amount": 300
      },
      "typeVersion": 1.1
    },
    {
      "id": "cc7b089b-de50-4df6-9594-f452866c2992",
      "name": "Check: AssemblyAI Transcript Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2320,
        32
      ],
      "parameters": {
        "url": "=https://api.assemblyai.com/v2/transcript/{{ $('Transcribe: Submit to AssemblyAI').item.json.id }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "dbd0dad5-9e59-4d17-bcac-7759da9c1d16",
      "name": "Wait: Creatomate Rendering",
      "type": "n8n-nodes-base.wait",
      "position": [
        3568,
        112
      ],
      "parameters": {
        "amount": 300
      },
      "typeVersion": 1.1
    },
    {
      "id": "f383bd1d-b065-4bdf-be32-945eccea948e",
      "name": "Check: Creatomate Render Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3792,
        32
      ],
      "parameters": {
        "url": "=https://api.creatomate.com/v2/renders/{{ $json.id }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c6af0c1a-76ff-4904-8550-13b97c62b867",
      "name": "Check: Render Completed?",
      "type": "n8n-nodes-base.if",
      "position": [
        4016,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "18c3fa02-29d7-4f53-af46-12758cc14326",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "succeeded"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "d172bebe-9f9f-421b-9f9e-2f40f1b7ce14",
      "name": "Extract: Parse Latest Video URL",
      "type": "n8n-nodes-base.code",
      "position": [
        1200,
        112
      ],
      "parameters": {
        "jsCode": "const rows = $input.all();\n\nif (rows.length === 0) {\n  return [];\n}\n\n// row_number \u304c\u4e00\u756a\u5927\u304d\u3044\u884c\u3092\u53d6\u5f97\nconst lastRow = rows.sort((a, b) => {\n  return Number(a.json.row_number) - Number(b.json.row_number);\n}).at(-1);\n\n// row_number \u4ee5\u5916\u306e\u5024\u3092\u53d6\u308a\u51fa\u3059\nconst values = Object.entries(lastRow.json)\n  .filter(([key, value]) => key !== 'row_number')\n  .map(([key, value]) => value)\n  .filter(value => value !== null && value !== undefined && String(value).trim() !== '');\n\n// URL\u3089\u3057\u3044\u5024\u3092\u53d6\u5f97\nconst url = values.find(value => {\n  return String(value).startsWith('http://') || String(value).startsWith('https://');\n});\n\nreturn [\n  {\n    json: {\n      row_number: lastRow.json.row_number,\n      url: url || ''\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "6dc76f32-17bd-4072-8015-706f2a474db6",
      "name": "Trigger: Watch New Video Rows",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "position": [
        976,
        112
      ],
      "parameters": {
        "event": "rowAdded",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iJ5_iwna9y0J10wXIUEcGFpQ1cdF08BqCy0tsOxZM9s/edit#gid=0",
          "cachedResultName": "URL\u8cbc\u308a\u4ed8\u3051"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1iJ5_iwna9y0J10wXIUEcGFpQ1cdF08BqCy0tsOxZM9s",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iJ5_iwna9y0J10wXIUEcGFpQ1cdF08BqCy0tsOxZM9s/edit?usp=drivesdk",
          "cachedResultName": "\u3010n8n\u3011\u52d5\u753b\u30c6\u30ed\u30c3\u30d7\u81ea\u52d5\u5316\u30b7\u30b9\u30c6\u30e0"
        }
      },
      "credentials": {
        "googleSheetsTriggerOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "db3d03cf-3ef8-4dcc-8a12-3ad721fc7fe9",
      "name": "Model: Gemini Translation",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        2768,
        256
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-3-flash-preview"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "dfcc1196-9877-4fe1-af13-69b0000d232a",
      "name": "Fetch: Video Metadata",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1648,
        112
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/drive/v3/files/{{ $json.download_url.match(/[?&]id=([^&]+)/)[1] }}?fields=id,name,mimeType,videoMediaMetadata(width,height,durationMillis)&supportsAllDrives=true",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "6c0e83c9-95d0-4ccd-b58d-667462dccb10",
      "name": "WORKFLOW OVERVIEW",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -160
      ],
      "parameters": {
        "width": 920,
        "height": 1288,
        "content": "## AUTOMATE ENGLISH-TO-JAPANESE VIDEO TRANSCRIPTION, TRANSLATION, AND RENDERING WITH ASSEMBLYAI AND CREATOMATE\n\n## Who\u2019s it for\nCreators, educators, marketers, and small teams who use n8n Cloud to automatically turn uploaded English videos into Japanese-subtitled videos without manually transcribing, translating, timing subtitles, or rendering the final file.\n\n## What it does\nThis workflow watches a Google Sheet for a new video URL, converts the Google Drive sharing link into a usable download URL, reads the video metadata, sends the audio/video to AssemblyAI for English transcription and Japanese translation, improves the Japanese text with Gemini, builds timed subtitles and a Creatomate render script, renders the subtitled video, checks the render status, and posts the completed video URL to Slack.\n\n## How it works\n1. A Google Sheets trigger starts the workflow when a new row is added.\n2. The latest video URL is extracted and normalized.\n3. Google Drive metadata is fetched to determine whether the video is portrait or landscape.\n4. AssemblyAI generates English transcription and Japanese translation.\n5. The workflow automatically checks the status and loops until processing is complete.\n6. Gemini polishes the Japanese subtitle text without changing the meaning.\n7. Code node builds subtitle segments and the Creatomate render payload.\n8. Creatomate renders the final subtitled video.\n9. The workflow loops until rendering succeeds, then sends the result to Slack.\n\n## Requirements\n- Google Sheets OAuth2 credential\n- Google Drive OAuth2 credential\n- AssemblyAI HTTP Header Auth credential\n- Google Gemini API credential\n- Creatomate HTTP Header Auth credential\n- Slack OAuth2 credential\n- A Google Sheet containing video URLs\n- A Slack channel for the final video link\n\n## How to set up\n\n1. Download the MP4 video file that you want to translate.\n2. Upload the MP4 file to Google Drive.\n3. Open the file's sharing settings and change the access from **Restricted** to **Anyone with the link**.\n4. Copy the Google Drive sharing link.\n5. Add your API credentials in n8n for Google Sheets, Google Drive, AssemblyAI, Gemini, Creatomate, and Slack.\n6. Update the Google Sheets and Slack node settings to match your own environment and resources.\n7. Confirm that the input column contains a valid Google Drive video URL.\n8. Activate the workflow.\n9. Paste the sharing link into the designated input column in your Google Sheet.\n\nOnce the workflow is activated, simply paste a new Google Drive video URL into the configured Google Sheet. The workflow automatically detects the new row, starts the transcription and translation process, generates Japanese subtitles, renders the final subtitled video, and sends the completed video link to Slack without any manual intervention.\n\n\n## How to customize\n- Change the source from Google Sheets to another trigger.\n- Edit the Gemini prompt for a different subtitle style.\n- Update the Creatomate template or render settings.\n- Send the final result to email, Notion, Google Drive, or another destination instead of Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "299025e1-160b-4b3d-81c3-5828f75d22eb",
      "name": "STEP1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 484,
        "content": "## STEP1: Video URL Intake\nWatch a Google Sheet for a newly added row, parse the latest row, and extract the usable video URL from the row data."
      },
      "typeVersion": 1
    },
    {
      "id": "9c2c2628-1cbe-463f-8272-4b975329689c",
      "name": "STEP2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 484,
        "content": "## STEP2: Prepare Download URL and Retrieve Video Metadata\nNormalize the Google Drive sharing link into a direct download URL, then fetch video metadata such as width and height for subtitle layout decisions."
      },
      "typeVersion": 1
    },
    {
      "id": "3604e79c-fa7f-4b6d-b221-d4ae837f9f2a",
      "name": "STEP3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 912,
        "height": 484,
        "content": "## STEP3: Transcription & Translation\nSubmit the video URL to AssemblyAI, wait for processing, and check the transcript status to retrieve Japanese translation data.\n\nThe If node verifies whether transcription has completed. If processing is still running, the workflow waits and checks again until the result is ready."
      },
      "typeVersion": 1
    },
    {
      "id": "8eef2907-2fa9-42fc-a149-896dc9a84855",
      "name": "STEP4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 568,
        "height": 580,
        "content": "## STEP4: Generate Natural Japanese Subtitles and Render Data\nConvert the translated text into natural subtitle-friendly Japanese, create subtitle timing information, and generate the render payload used to produce the final subtitled video."
      },
      "typeVersion": 1
    },
    {
      "id": "fe6ec697-17e9-4701-805d-28b1a9598bd3",
      "name": "STEP6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3296,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 440,
        "height": 516,
        "content": "## STEP5: Create Subtitled Video\nSend the final subtitle data and video source to Creatomate.\nCreatomate combines the video and subtitles into a single rendered video.\nThe workflow waits until the finished video is ready for download."
      },
      "typeVersion": 1
    },
    {
      "id": "c3d402ef-6d65-438e-8ab7-dd6758bf8c8c",
      "name": "STEP7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3728,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 728,
        "height": 516,
        "content": "## STEP6: Render Status Check & Notification\nCheck whether Creatomate rendering has succeeded. If not, wait and retry. Once complete, send the final video URL to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "3fc5729c-4edb-4284-950e-c2de87b925cc",
      "name": "Prepare: Convert Drive Share URL to Download URL",
      "type": "n8n-nodes-base.set",
      "position": [
        1424,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a7a39887-e994-42f3-bc11-a1a7c808ae38",
              "name": "download_url",
              "type": "string",
              "value": "={{ \"https://drive.google.com/uc?export=download&id=\" + $json.url.match(/\\/d\\/([^/]+)/)[1] }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "eccb7c0b-832d-4c8d-8c62-9a058d58573c",
      "name": "Generate: Natural Subtitle Timeline and Creatomate Render Script",
      "type": "n8n-nodes-base.code",
      "position": [
        3120,
        112
      ],
      "parameters": {
        "jsCode": "// ======================================================\n// AI: Polish Japanese Translation\u3067\u6574\u3048\u305f\u65e5\u672c\u8a9e text \u3068\n// AssemblyAI \u306e words \u304b\u3089\n// \u65e5\u672c\u8a9e\u5b57\u5e55JSON\u914d\u5217\u3068 Creatomate\u7528RenderScript\u3092\u4f5c\u6210\u3059\u308b Code \u30ce\u30fc\u30c9\n//\n// \u5bfe\u5fdc\u5185\u5bb9\uff1a\n// - \u5b57\u5e55\u306f\u57fa\u672c\u7684\u306b1\u884c\u3067\u8868\u793a\u3059\u308b\n// - \u9577\u3044\u5b57\u5e55\u306f\u81ea\u7136\u306a\u6587\u7ae0\u533a\u5207\u308a\u3067\u5206\u5272\u3059\u308b\n// - \u6700\u5f8c\u306e\u5b57\u5e55\u306f\u5fc5\u305a AssemblyAI words \u306e\u6700\u5f8c\u306e end \u3067\u7d42\u308f\u308b\n// ======================================================\n\n\n// ======================================================\n// 1. AI: Polish Japanese Translation \u306e\u51fa\u529b\u3092\u53d6\u5f97\n// ======================================================\n\nconst llmInput = items[0].json;\n\n\n// ======================================================\n// 2. AssemblyAI GET\u7d50\u679c\u306e\u5143\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\n// ======================================================\n\nconst originalNodeName = 'Check: AssemblyAI Transcript Status';\n\nlet originalInput;\n\ntry {\n  originalInput = $(originalNodeName).item.json;\n} catch (error) {\n  originalInput = $(originalNodeName).first().json;\n}\n\nif (!originalInput || Object.keys(originalInput).length === 0) {\n  throw new Error(\n    `\u5143\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30ce\u30fc\u30c9\u540d\u300c${originalNodeName}\u300d\u304c\u6b63\u3057\u3044\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002`\n  );\n}\n\n\n// ======================================================\n// 3. AI: Polish Japanese Translation \u306e\u51fa\u529b\u304b\u3089\u6574\u5f62\u6e08\u307f text \u3092\u53d6\u5f97\n// ======================================================\n\nfunction pickTextFromObject(obj) {\n  if (!obj || typeof obj !== 'object') {\n    return '';\n  }\n\n  const directKeys = [\n    'text',\n    'output',\n    'response',\n    'result',\n    'content',\n    'chat_message',\n    'completion'\n  ];\n\n  for (const key of directKeys) {\n    if (typeof obj[key] === 'string' && obj[key].trim() !== '') {\n      return obj[key];\n    }\n  }\n\n  if (\n    obj.message &&\n    typeof obj.message.content === 'string' &&\n    obj.message.content.trim() !== ''\n  ) {\n    return obj.message.content;\n  }\n\n  if (obj.data && typeof obj.data === 'object') {\n    const textFromData = pickTextFromObject(obj.data);\n\n    if (textFromData) {\n      return textFromData;\n    }\n  }\n\n  if (obj.output && typeof obj.output === 'object') {\n    const textFromOutput = pickTextFromObject(obj.output);\n\n    if (textFromOutput) {\n      return textFromOutput;\n    }\n  }\n\n  if (Array.isArray(obj.generations)) {\n    const firstGeneration = obj.generations[0];\n\n    if (firstGeneration) {\n      if (\n        typeof firstGeneration.text === 'string' &&\n        firstGeneration.text.trim() !== ''\n      ) {\n        return firstGeneration.text;\n      }\n\n      if (\n        firstGeneration.message &&\n        typeof firstGeneration.message.content === 'string' &&\n        firstGeneration.message.content.trim() !== ''\n      ) {\n        return firstGeneration.message.content;\n      }\n    }\n  }\n\n  return '';\n}\n\nlet formattedText = pickTextFromObject(llmInput);\n\n\n// ======================================================\n// 4. AI: Polish Japanese Translation \u306e\u51fa\u529b\u304c\u53d6\u308c\u306a\u3044\u5834\u5408\u306f AssemblyAI \u7ffb\u8a33\u3092\u88dc\u52a9\u7684\u306b\u4f7f\u3046\n// ======================================================\n\nif (\n  !formattedText &&\n  originalInput.translated_texts &&\n  typeof originalInput.translated_texts.ja === 'string' &&\n  originalInput.translated_texts.ja.trim() !== ''\n) {\n  formattedText = originalInput.translated_texts.ja;\n}\n\nformattedText = String(formattedText || '').trim();\n\nformattedText = formattedText\n  .replace(/^\u6574\u5f62\u5f8c\u306e\u672c\u6587[:\uff1a]\\s*/g, '')\n  .replace(/^\u51fa\u529b[:\uff1a]\\s*/g, '')\n  .replace(/^\u672c\u6587[:\uff1a]\\s*/g, '')\n  .replace(/^\u7d50\u679c[:\uff1a]\\s*/g, '')\n  .trim();\n\nif (!formattedText) {\n  throw new Error(\n    [\n      'AI: Polish Japanese Translation\u306e\u6574\u5f62\u6e08\u307ftext\u304c\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002',\n      '\u307e\u305f\u3001AssemblyAI\u306e translated_texts.ja \u3082\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002',\n      'AI: Polish Japanese Translation\u306e\u5165\u529b\u304c {{ $json.translated_texts.ja }} \u306b\u306a\u3063\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002',\n      '',\n      '\u73fe\u5728\u306eAI: Polish Japanese Translation\u51fa\u529b:',\n      JSON.stringify(llmInput, null, 2),\n      '',\n      '\u73fe\u5728\u306eAssemblyAI\u51fa\u529b:',\n      JSON.stringify(originalInput, null, 2)\n    ].join('\\n')\n  );\n}\n\n\n// ======================================================\n// 5. text\u3060\u3051 AI: Polish Japanese Translation \u306e\u51fa\u529b\u306b\u5dee\u3057\u66ff\u3048\u308b\n// ======================================================\n\nconst input = {\n  ...originalInput,\n  text: formattedText,\n  original_text_before_llm: originalInput.text || '',\n  translated_ja_before_llm: originalInput.translated_texts?.ja || '',\n  text_source: 'AI: Polish Japanese Translation or AssemblyAI translated_texts.ja',\n  original_data_source: originalNodeName\n};\n\n\n// ======================================================\n// 6. AssemblyAI \u306e\u51e6\u7406\u5b8c\u4e86\u30c1\u30a7\u30c3\u30af\n// ======================================================\n\nif (input.status !== 'completed') {\n  throw new Error(\n    `AssemblyAI\u306e\u6587\u5b57\u8d77\u3053\u3057\u304c\u307e\u3060\u5b8c\u4e86\u3057\u3066\u3044\u307e\u305b\u3093\u3002\u73fe\u5728\u306estatus: ${input.status}`\n  );\n}\n\n\n// ======================================================\n// 7. \u5b57\u5e55\u7528\u306e\u65e5\u672c\u8a9e text \u3092\u53d6\u5f97\n// ======================================================\n\nconst textForSubtitle = String(input.text || '').trim();\n\nif (!textForSubtitle) {\n  throw new Error(\n    'input.text \u304c\u7a7a\u3067\u3059\u3002\u65e5\u672c\u8a9e\u5b57\u5e55\u7528\u30c6\u30ad\u30b9\u30c8\u304c\u53d6\u5f97\u3067\u304d\u3066\u3044\u307e\u305b\u3093\u3002'\n  );\n}\n\n\n// ======================================================\n// 9. AssemblyAI \u306e words \u3092\u53d6\u5f97\n// ======================================================\n\nconst words = input.words;\n\nif (!Array.isArray(words) || words.length === 0) {\n  throw new Error(\n    'AssemblyAI\u306e\u51fa\u529b\u306b words \u304c\u3042\u308a\u307e\u305b\u3093\u3002\u300cCheck: AssemblyAI Transcript Status\u300d\u306eOutput\u3067 words \u304c\u914d\u5217\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\n\n// ======================================================\n// 10. \u52d5\u753bURL\u3092\u53d6\u5f97\n// ======================================================\n\nconst videoUrl =\n  input.audio_url ||\n  input.video_url ||\n  input.videoUrl ||\n  input['\u52d5\u753bURL'];\n\nif (!videoUrl) {\n  throw new Error(\n    '\u52d5\u753bURL\u304c\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002audio_url / video_url / videoUrl / \u52d5\u753bURL \u306e\u3044\u305a\u308c\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\n\n// ======================================================\n// 11. \u8a18\u53f7\u30fb\u6587\u5b57\u5217\u51e6\u7406\u7528\u306e\u8a2d\u5b9a\n// ======================================================\n\nconst punctuationRegex = /[\u3001\u3002,.!?\uff01\uff1f\u300c\u300d\u300e\u300f\uff08\uff09()\\[\\]\u3010\u3011]/g;\n\nfunction normalizeText(text) {\n  return String(text || '')\n    .replace(punctuationRegex, '')\n    .replace(/\\s+/g, '')\n    .trim();\n}\n\nfunction cleanDisplayText(text) {\n  return String(text || '')\n    .replace(punctuationRegex, '')\n    .replace(/\\s+/g, '')\n    .trim();\n}\n\nfunction keepBreakText(text) {\n  return String(text || '')\n    .replace(/\\s+/g, '')\n    .trim();\n}\n\n\n// ======================================================\n// 12. \u6587\u5b57\u7a2e\u5224\u5b9a\n// ======================================================\n\nfunction isAsciiAlphaNumeric(char) {\n  return /[A-Za-z0-9]/.test(char || '');\n}\n\nfunction isJapaneseChar(char) {\n  return /[\\u3040-\\u30ff\\u3400-\\u9fff]/.test(char || '');\n}\n\nfunction isKatakanaChar(char) {\n  return /[\\u30a0-\\u30ff\u30fc]/.test(char || '');\n}\n\nfunction isDigitChar(char) {\n  return /[0-9\uff10-\uff19]/.test(char || '');\n}\n\nfunction isJapaneseNumberUnitChar(char) {\n  return /[\u5341\u767e\u5343\u4e07\u5104\u5146]/.test(char || '');\n}\n\nfunction isBadJapaneseContinuation(previous, next) {\n  const pair = `${previous || ''}${next || ''}`;\n\n  const badPairs = [\n    '\u3067\u304d',\n    '\u304d\u306a',\n    '\u306a\u304f',\n    '\u304f\u306a',\n    '\u306a\u308a',\n    '\u306a\u308b',\n    '\u306e\u3067',\n    '\u3067\u3059',\n    '\u3059\u304c',\n    '\u307e\u3057',\n    '\u3057\u305f',\n    '\u3057\u3066',\n    '\u3044\u308b',\n    '\u3042\u308a',\n    '\u307e\u305b',\n    '\u305b\u3093',\n    '\u308f\u304b',\n    '\u5206\u304b',\n    '\u304b\u308a',\n    '\u308a\u307e',\n    '\u307e\u3059',\n    '\u3053\u3068',\n    '\u3068\u306f',\n    '\u306b\u306f',\n    '\u3067\u306f'\n  ];\n\n  return badPairs.includes(pair);\n}\n\n\n// ======================================================\n// 13. \u5b57\u5e55\u306e\u8abf\u6574\u5024\n// ======================================================\n//\n// \u3053\u3053\u3067\u306f\u3001\u52d5\u753b\u306e\u5411\u304d\u306b\u5408\u308f\u305b\u3066\n// \u300c\u5b57\u5e551\u56de\u3042\u305f\u308a\u306b\u8868\u793a\u3059\u308b\u6587\u5b57\u6570\u300d\u3092\u81ea\u52d5\u3067\u5207\u308a\u66ff\u3048\u308b\u3002\n//\n// \u7406\u7531\uff1a\n// \u6a2a\u9577\u52d5\u753b\u306a\u3089\u3001\u753b\u9762\u306e\u6a2a\u5e45\u304c\u5e83\u3044\u305f\u3081\u5c11\u3057\u9577\u3081\u306e\u5b57\u5e55\u3067\u3082\u8aad\u307f\u3084\u3059\u3044\u3002\n// \u4e00\u65b9\u3067\u3001\u7e26\u9577\u52d5\u753b\u306f\u6a2a\u5e45\u304c\u72ed\u3044\u305f\u3081\u3001\u9577\u3044\u5b57\u5e55\u3092\u51fa\u3059\u3068\n// \u753b\u9762\u4e0a\u3067\u6298\u308a\u8fd4\u3055\u308c\u305f\u308a\u3001\u6587\u5b57\u304c\u8a70\u307e\u3063\u3066\u898b\u3048\u305f\u308a\u3057\u3084\u3059\u3044\u3002\n//\n// \u305d\u306e\u305f\u3081\u3001Fetch: Video Metadata\u30ce\u30fc\u30c9\u306e\n// videoMediaMetadata.width\n// videoMediaMetadata.height\n// \u3092\u78ba\u8a8d\u3057\u3001height \u304c width \u3088\u308a\u5927\u304d\u3044\u5834\u5408\u306f\u300c\u7e26\u52d5\u753b\u300d\u3068\u5224\u65ad\u3059\u308b\u3002\n//\n// \u7e26\u52d5\u753b\u306e\u5834\u5408\uff1a\n// - \u6700\u4f4e\u6587\u5b57\u6570\uff1a6\n// - \u7406\u60f3\u6587\u5b57\u6570\uff1a16\n// - \u901a\u5e38\u4e0a\u9650\uff1a16\n// - \u7d76\u5bfe\u4e0a\u9650\uff1a16\n//\n// \u6a2a\u52d5\u753b\u30fb\u307e\u305f\u306f\u30b5\u30a4\u30ba\u53d6\u5f97\u3067\u304d\u306a\u3044\u5834\u5408\uff1a\n// - \u3053\u308c\u307e\u3067\u901a\u308a\u306e\u8a2d\u5b9a\u3092\u4f7f\u3046\n// - \u6700\u4f4e\u6587\u5b57\u6570\uff1a6\n// - \u7406\u60f3\u6587\u5b57\u6570\uff1a16\n// - \u901a\u5e38\u4e0a\u9650\uff1a22\n// - \u7d76\u5bfe\u4e0a\u9650\uff1a28\n//\n// \u203b const \u3067\u306f\u306a\u304f let \u306b\u3057\u3066\u3044\u308b\u7406\u7531\uff1a\n// \u52d5\u753b\u306e\u5411\u304d\u306b\u3088\u3063\u3066\u3001\u3042\u3068\u304b\u3089\u5024\u3092\u5165\u308c\u66ff\u3048\u308b\u5fc5\u8981\u304c\u3042\u308b\u305f\u3081\u3002\n// ======================================================\n\nconst videoMetadataNodeName = 'Fetch: Video Metadata';\n\nlet videoMetadataInput;\n\ntry {\n  videoMetadataInput = $(videoMetadataNodeName).item.json;\n} catch (error) {\n  videoMetadataInput = $(videoMetadataNodeName).first().json;\n}\n\nconst videoWidth = Number(videoMetadataInput?.videoMediaMetadata?.width || 0);\nconst videoHeight = Number(videoMetadataInput?.videoMediaMetadata?.height || 0);\n\nconst isPortraitVideo =\n  Number.isFinite(videoWidth) &&\n  Number.isFinite(videoHeight) &&\n  videoWidth > 0 &&\n  videoHeight > 0 &&\n  videoHeight > videoWidth;\n\nlet minCharsPerSubtitle = 6;\nlet idealCharsPerSubtitle = 16;\nlet maxCharsPerSubtitle = 22;\nlet hardMaxCharsPerSubtitle = 28;\n\nif (isPortraitVideo) {\n  minCharsPerSubtitle = 6;\n  idealCharsPerSubtitle = 16;\n  maxCharsPerSubtitle = 16;\n  hardMaxCharsPerSubtitle = 16;\n}\n\n\n// ======================================================\n// 14. \u9014\u4e2d\u3067\u5207\u308a\u305f\u304f\u306a\u3044\u8868\u73fe\n// ======================================================\n\nconst protectedPhrases = [\n  '\u7406\u89e3\u3067\u304d\u306a\u3044',\n  '\u5168\u304f\u7406\u89e3\u3067\u304d\u306a\u3044',\n  '\u3067\u304d\u306a\u3044',\n  '\u3067\u304d\u306a\u304f',\n  '\u3067\u304d\u306a\u304f\u306a\u308b',\n  '\u3067\u304d\u306a\u3044\u3067\u3057\u3087\u3046',\n  '\u3067\u304d\u306a\u3044\u3067\u3057\u3087\u3046\u304b\u3089',\n  '\u3067\u3057\u3087\u3046\u304b\u3089',\n  '\u3067\u3057\u3087\u3046',\n  '\u3067\u3057\u3087\u3046\u304b',\n  '\u3042\u308a\u307e\u305b\u3093',\n  '\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f',\n  '\u3057\u3066\u3044\u306a\u3044',\n  '\u3057\u3066\u3044\u306a\u3044\u3067\u3057\u3087\u3046',\n  '\u6301\u3063\u3066\u3044\u306a\u3044',\n  '\u6301\u3063\u3066\u3044\u306a\u3044\u3067\u3057\u3087\u3046',\n  '\u59cb\u3081\u305f\u306e\u3067\u3059\u304c',\n  '\u30c6\u30b9\u30c8\u3092\u59cb\u3081\u305f\u306e\u3067\u3059\u304c',\n  '\u5206\u304b\u308a\u307e\u3059',\n  '\u308f\u304b\u308a\u307e\u3059',\n  '\u898b\u821e\u308f\u308c\u305f\u3053\u3068',\n  '\u898b\u821e\u308f\u308c\u305f\u3053\u3068\u304c\u5206\u304b\u308a\u307e\u3059',\n  '\u6587\u5b57\u8d77\u3053\u3057',\n  '\u975e\u5e38\u306b\u6642\u9593\u304c\u304b\u304b\u3063\u3066\u3044\u307e\u3057\u305f',\n  '\u305d\u3093\u306a\u3053\u3068',\n  '\u8d77\u3053\u308a\u307e\u305b\u3093',\n  '\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9',\n  '\u30a6\u30a7\u30d6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3',\n  '\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb',\n  '\u30c8\u30e9\u30d5\u30a3\u30c3\u30af',\n  '\u6b63\u898f\u306e\u30e6\u30fc\u30b6\u30fc',\n  '\u30ea\u30af\u30a8\u30b9\u30c8',\n  '\u6bce\u52061\u4e072\u5343\u4ee5\u4e0a',\n  '1\u4e072\u5343\u4ee5\u4e0a',\n  '\u4f55\u5104\u3082\u306e\u30ea\u30af\u30a8\u30b9\u30c8',\n  'BridgeVoice',\n  'BridgeVoice\u3067\u306f',\n  '\u30b9\u30ad\u30eb\u30bb\u30c3\u30c8',\n  '\u4e2d\u5c0f\u4f01\u696d',\n  '\u5927\u4f01\u696d',\n  '\u4e00\u90e8\u306e\u5927\u4f01\u696d',\n  '\u30d3\u30b8\u30cd\u30b9\u30de\u30f3',\n  'AI\u526f\u696d',\n  '\u4eba\u5de5\u77e5\u80fd',\n  '\u52d5\u753b\u7de8\u96c6',\n  '\u81ea\u52d5\u5316',\n  '\u97f3\u58f0\u8a8d\u8b58',\n  '\u751f\u6210AI',\n  'Google\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8',\n  'Google\u30b9\u30e9\u30a4\u30c9',\n  'Google\u30c9\u30e9\u30a4\u30d6',\n  'n8n',\n  'Creatomate',\n  'AssemblyAI'\n];\n\nfunction isInsideProtectedPhrase(text, index) {\n  if (index <= 0 || index >= text.length) {\n    return false;\n  }\n\n  for (const phrase of protectedPhrases) {\n    let searchStart = 0;\n\n    while (true) {\n      const foundIndex = text.indexOf(phrase, searchStart);\n\n      if (foundIndex === -1) {\n        break;\n      }\n\n      const phraseStart = foundIndex;\n      const phraseEnd = foundIndex + phrase.length;\n\n      if (index > phraseStart && index < phraseEnd) {\n        return true;\n      }\n\n      searchStart = foundIndex + 1;\n    }\n  }\n\n  return false;\n}\n\nfunction isBadSplitPosition(text, index) {\n  if (index <= 0 || index >= text.length) {\n    return false;\n  }\n\n  const prev = text[index - 1];\n  const next = text[index];\n\n  const before = cleanDisplayText(text.slice(0, index));\n  const after = cleanDisplayText(text.slice(index));\n\n  if (\n    (before.endsWith('\u30a6\u30a7\u30d6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3') && after.startsWith('\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb')) ||\n    (before.endsWith('BridgeVoice\u3067\u306f') && after.startsWith('\u305d\u3093\u306a\u3053\u3068')) ||\n    (before.endsWith('\u30a2\u30af\u30bb\u30b9') && after.startsWith('\u3067\u304d\u306a\u304f'))\n  ) {\n    return false;\n  }\n\n  if (isAsciiAlphaNumeric(prev) && isAsciiAlphaNumeric(next)) {\n    return true;\n  }\n\n  if (isKatakanaChar(prev) && isKatakanaChar(next)) {\n    return true;\n  }\n\n  if (isDigitChar(prev) && isJapaneseNumberUnitChar(next)) {\n    return true;\n  }\n\n  if (isJapaneseNumberUnitChar(prev) && isDigitChar(next)) {\n    return true;\n  }\n\n  if (isBadJapaneseContinuation(prev, next)) {\n    return true;\n  }\n\n  if (isInsideProtectedPhrase(text, index)) {\n    return true;\n  }\n\n  return false;\n}\n\n\n// ======================================================\n// 15. \u5206\u5272\u5019\u88dc\u306e\u8a55\u4fa1\n// ======================================================\n\nconst goodEndingPatterns = [\n  '\u3067\u3059',\n  '\u307e\u3059',\n  '\u3067\u3057\u305f',\n  '\u307e\u3057\u305f',\n  '\u3067\u3057\u3087\u3046',\n  '\u304f\u3060\u3055\u3044',\n  '\u3067\u304d\u307e\u3059',\n  '\u3067\u304d\u307e\u305b\u3093',\n  '\u3042\u308a\u307e\u305b\u3093',\n  '\u3057\u3066\u3044\u307e\u3059',\n  '\u3057\u3066\u3044\u308b',\n  '\u3057\u3066\u3044\u306a\u3044',\n  '\u306b\u306a\u308a\u307e\u3059',\n  '\u3068\u3044\u3046\u3053\u3068\u3067\u3059',\n  '\u3068\u3044\u3046\u3053\u3068',\n  '\u3068\u3044\u3046',\n  '\u305f\u3081',\n  '\u304b\u3089',\n  '\u306a\u3089',\n  '\u306b\u306f',\n  '\u3067\u306f',\n  '\u3068\u306f',\n  '\u3053\u305d',\n  '\u307e\u3067',\n  '\u307b\u3069',\n  '\u3060\u3051',\n  '\u306a\u3069',\n  '\u3082\u306e'\n];\n\nconst badEndingPatterns = [\n  '\u7406\u89e3\u3067',\n  '\u5168\u304f\u7406\u89e3\u3067',\n  '\u3067\u304d',\n  '\u3067\u304d\u306a',\n  '\u3067\u304d\u306a\u3044\u3067',\n  '\u3067\u3057',\n  '\u306e\u3067',\n  '\u3087\u3046',\n  '\u3057\u3087\u3046',\n  '\u30c0\u30c3\u30b7\u30e5\u30dc',\n  '\u30a6\u30a7\u30d6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30d5\u30a1',\n  '\u30d5\u30a1',\n  '\u30d5\u30a1\u30a4',\n  '\u30d5\u30a1\u30a4\u30a2',\n  '\u30d5\u30a1\u30a4\u30a2\u30a6',\n  '\u30a2\u30af\u30bb\u30b9\u3067',\n  '1\u4e072',\n  '\u6bce\u52061\u4e072',\n  '\u30b9\u30ad\u30eb',\n  '\u30bb\u30c3\u30c8',\n  '\u305d\u3046\u3044\u3063\u305f\u3053\u3068\u306e',\n  '\u305d\u3046\u3044\u3063\u305f\u3053\u3068\u3092',\n  '\u4e00\u90e8\u306e',\n  '\u305d\u306e\u3088\u3046\u306a',\n  '\u3069\u308c\u3082',\n  '\u5168\u304f'\n];\n\nconst badStartingPatterns = [\n  '\u30fc\u30c9',\n  '\u304d\u306a\u3044',\n  '\u304d\u306a\u304f',\n  '\u304d\u306a\u304f\u306a\u308b',\n  '\u5343\u4ee5\u4e0a',\n  '\u304c\u5206\u304b\u308a\u307e\u3059',\n  '\u304c\u308f\u304b\u308a\u307e\u3059',\n  '\u3059\u304c',\n  '\u306a\u3044\u3067\u3057\u3087\u3046',\n  '\u3067\u3057\u3087\u3046\u304b\u3089',\n  '\u3057\u3087\u3046\u304b\u3089',\n  '\u30bb\u30c3\u30c8',\n  '\u4f01\u696d\u3067\u306f',\n  '\u90e8\u306e\u5927\u4f01\u696d',\n  '\u3046\u3044\u3063\u305f',\n  '\u308c\u3082',\n  '\u89e3\u3067\u304d\u306a\u3044',\n  '\u7406\u89e3\u3067\u304d\u306a\u3044'\n];\n\nconst connectorStartingPatterns = [\n  '\u5b9f\u306f',\n  '\u7279\u306b',\n  '\u7d50\u8ad6\u304b\u3089',\n  '\u3067\u306f',\n  '\u306a\u305c',\n  '\u305d\u306e\u7406\u7531',\n  '\u4e00\u3064\u76ee',\n  '\u4e8c\u3064\u76ee',\n  '\u4e09\u3064\u76ee',\n  '\u307e\u305a',\n  '\u6b21\u306b',\n  '\u3055\u3089\u306b',\n  '\u305d\u3057\u3066',\n  '\u3057\u304b\u3057',\n  '\u4f8b\u3048\u3070',\n  '\u4eca\u56de',\n  '\u4eca\u3059\u3050',\n  '\u3053\u308c\u306b\u3088\u308a',\n  '\u3064\u307e\u308a',\n  '\u305d\u306e\u305f\u3081',\n  '\u5927\u5207\u306a\u306e\u306f',\n  '\u3053\u3053\u3067',\n  '\u3053\u306e\u3088\u3046\u306b',\n  '\u3060\u304b\u3089',\n  '\u4e00\u65b9\u3067'\n];\n\nconst preferredStartingPatterns = [\n  '\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9',\n  '\u898b\u821e\u308f\u308c\u305f',\n  '\u898b\u821e\u308f\u308c\u305f\u3053\u3068',\n  '\u6587\u5b57\u8d77\u3053\u3057',\n  'BridgeVoice\u3067\u306f',\n  '\u305d\u3093\u306a\u3053\u3068',\n  '\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb',\n  '\u30c8\u30e9\u30d5\u30a3\u30c3\u30af',\n  '\u3067\u304d\u306a\u304f',\n  '\u3067\u304d\u306a\u304f\u306a\u308b',\n  '\u30ea\u30af\u30a8\u30b9\u30c8',\n  '\u6628\u65e5',\n  '\u5b9f\u969b\u306b\u306f',\n  '\u3053\u306e\u653b\u6483',\n  '\u6b63\u898f\u306e\u30e6\u30fc\u30b6\u30fc',\n  '\u3059\u3050\u306b'\n];\n\nconst preferredEndingPatterns = [\n  '\u7c21\u5358\u306a',\n  '\u30ea\u30af\u30a8\u30b9\u30c8\u306b',\n  '\u59cb\u3081\u305f\u306e\u3067\u3059\u304c',\n  'BridgeVoice\u3067\u306f',\n  '\u30a6\u30a7\u30d6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3',\n  '\u30a2\u30af\u30bb\u30b9',\n  '1\u4e072\u5343\u4ee5\u4e0a\u306e',\n  '\u6bce\u52061\u4e072\u5343\u4ee5\u4e0a\u306e'\n];\n\nfunction endsWithAny(text, patterns) {\n  return patterns.some((pattern) => text.endsWith(pattern));\n}\n\nfunction startsWithAny(text, patterns) {\n  return patterns.some((pattern) => text.startsWith(pattern));\n}\n\nfunction getSplitScore(text, index) {\n  if (index <= 0 || index >= text.length) {\n    return -9999;\n  }\n\n  if (isBadSplitPosition(text, index)) {\n    return -9999;\n  }\n\n  const beforeRaw = text.slice(0, index);\n  const afterRaw = text.slice(index);\n\n  const before = cleanDisplayText(beforeRaw);\n  const after = cleanDisplayText(afterRaw);\n\n  if (before.length < minCharsPerSubtitle || after.length < minCharsPerSubtitle) {\n    return -200;\n  }\n\n  if (endsWithAny(before, badEndingPatterns)) {\n    return -800;\n  }\n\n  if (startsWithAny(after, badStartingPatterns)) {\n    return -800;\n  }\n\n  let score = 0;\n\n  const prev = text[index - 1];\n  const next = text[index];\n\n  if (\n    prev === '\u3002' ||\n    prev === '\uff01' ||\n    prev === '\uff1f' ||\n    prev === '!' ||\n    prev === '?'\n  ) {\n    score += 120;\n  }\n\n  if (\n    prev === '\u3001' ||\n    prev === ',' ||\n    prev === ' ' ||\n    prev === '\u3000'\n  ) {\n    score += 100;\n  }\n\n  if (startsWithAny(after, preferredStartingPatterns)) {\n    score += 150;\n  }\n\n  if (endsWithAny(before, preferredEndingPatterns)) {\n    score += 150;\n  }\n\n  if (startsWithAny(afterRaw, connectorStartingPatterns)) {\n    score += 80;\n  }\n\n  if (endsWithAny(before, goodEndingPatterns)) {\n    score += 70;\n  }\n\n  if (\n    prev === '\u306f' ||\n    prev === '\u304c' ||\n    prev === '\u3092' ||\n    prev === '\u306b' ||\n    prev === '\u3067' ||\n    prev === '\u3078' ||\n    prev === '\u3082' ||\n    prev === '\u3068' ||\n    prev === '\u3084'\n  ) {\n    score += 35;\n  }\n\n  if (prev === '\u306e') {\n    score += 10;\n  }\n\n  if (prev === '\u306a' && startsWithAny(after, preferredStartingPatterns)) {\n    score += 60;\n  }\n\n  if (isAsciiAlphaNumeric(prev) && isJapaneseChar(next)) {\n    score += 20;\n  }\n\n  if (isJapaneseChar(prev) && isAsciiAlphaNumeric(next)) {\n    score += 20;\n  }\n\n  const distanceFromIdeal = Math.abs(before.length - idealCharsPerSubtitle);\n  score -= distanceFromIdeal * 2;\n\n  if (before.length > maxCharsPerSubtitle) {\n    score -= 50;\n  }\n\n  return score;\n}\n\n\n// ======================================================\n// 16. \u5b89\u5168\u3067\u81ea\u7136\u306a\u5206\u5272\u4f4d\u7f6e\u3092\u63a2\u3059\n// ======================================================\n\nfunction findNaturalSplitIndex(text, preferredIndex = idealCharsPerSubtitle) {\n  const rawText = String(text || '');\n\n  if (cleanDisplayText(rawText).length <= maxCharsPerSubtitle) {\n    return rawText.length;\n  }\n\n  const length = rawText.length;\n  let bestIndex = -1;\n  let bestScore = -9999;\n\n  const searchStart = Math.max(minCharsPerSubtitle, preferredIndex - 10);\n  const searchEnd = Math.min(length - minCharsPerSubtitle, hardMaxCharsPerSubtitle + 8);\n\n  for (let i = searchStart; i <= searchEnd; i++) {\n    const score = getSplitScore(rawText, i);\n\n    if (score > bestScore) {\n      bestScore = score;\n      bestIndex = i;\n    }\n  }\n\n  if (bestIndex !== -1 && bestScore > -100) {\n    return bestIndex;\n  }\n\n  for (\n    let i = Math.min(length - minCharsPerSubtitle, maxCharsPerSubtitle);\n    i >= minCharsPerSubtitle;\n    i--\n  ) {\n    const score = getSplitScore(rawText, i);\n\n    if (score > -100) {\n      return i;\n    }\n  }\n\n  for (\n    let i = maxCharsPerSubtitle + 1;\n    i <= Math.min(length - minCharsPerSubtitle, hardMaxCharsPerSubtitle + 8);\n    i++\n  ) {\n    const score = getSplitScore(rawText, i);\n\n    if (score > -100) {\n      return i;\n    }\n  }\n\n  let fallbackIndex = Math.min(maxCharsPerSubtitle, length - minCharsPerSubtitle);\n\n  while (\n    fallbackIndex > minCharsPerSubtitle &&\n    isBadSplitPosition(rawText, fallbackIndex)\n  ) {\n    fallbackIndex--;\n  }\n\n  if (fallbackIndex <= minCharsPerSubtitle) {\n    fallbackIndex = Math.min(maxCharsPerSubtitle, length - minCharsPerSubtitle);\n\n    while (\n      fallbackIndex < length - minCharsPerSubtitle &&\n      isBadSplitPosition(rawText, fallbackIndex)\n    ) {\n      fallbackIndex++;\n    }\n  }\n\n  return fallbackIndex;\n}\n\n\n// ======================================================\n// 17. LLM\u6574\u5f62\u6e08\u307ftext\u3092\u3001\u53e5\u8aad\u70b9\u30d9\u30fc\u30b9\u3067\u5927\u304d\u304f\u5206\u3051\u308b\n// ======================================================\n\nfunction splitFormattedTextIntoSegments(text) {\n  const segments = [];\n  let buffer = '';\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i];\n\n    buffer += char;\n\n    const isStrongBreak =\n      char === '\u3002' ||\n      char === '\uff01' ||\n      char === '\uff1f' ||\n      char === '!' ||\n      char === '?';\n\n    const isWeakBreak =\n      char === '\u3001' ||\n      char === ',';\n\n    const currentText = keepBreakText(buffer);\n    const cleanedBuffer = cleanDisplayText(buffer);\n\n    if (isStrongBreak) {\n      if (cleanedBuffer.length > 0) {\n        segments.push({\n          text: currentText,\n          break_type: 'strong'\n        });\n      }\n\n      buffer = '';\n      continue;\n    }\n\n    if (isWeakBreak && cleanedBuffer.length >= idealCharsPerSubtitle) {\n      segments.push({\n        text: currentText,\n        break_type: 'weak'\n      });\n\n      buffer = '';\n      continue;\n    }\n\n    if (cleanedBuffer.length >= hardMaxCharsPerSubtitle + 6) {\n      const splitIndex = findNaturalSplitIndex(currentText, idealCharsPerSubtitle);\n      const before = currentText.slice(0, splitIndex);\n      const after = currentText.slice(splitIndex);\n\n      if (cleanDisplayText(before).length > 0) {\n        segments.push({\n          text: before,\n          break_type: 'length'\n        });\n      }\n\n      buffer = after;\n      continue;\n    }\n  }\n\n  const rest = keepBreakText(buffer);\n\n  if (cleanDisplayText(rest).length > 0) {\n    segments.push({\n      text: rest,\n      break_type: 'end'\n    });\n  }\n\n  return segments;\n}\n\n\n// ======================================================\n// 18. \u9577\u3044\u30bb\u30b0\u30e1\u30f3\u30c8\u3092\u81ea\u7136\u306a1\u884c\u5b57\u5e55\u306b\u5206\u5272\u3059\u308b\n// ======================================================\n\nfunction splitLongSegment(segmentText) {\n  let current = String(segmentText || '');\n  const result = [];\n\n  if (cleanDisplayText(current).length <= maxCharsPerSubtitle) {\n    const cleaned = cleanDisplayText(current);\n\n    if (cleaned) {\n      return [cleaned];\n    }\n\n    return [];\n  }\n\n  let safetyCounter = 0;\n\n  while (cleanDisplayText(current).length > maxCharsPerSubtitle && safetyCounter < 100) {\n    safetyCounter++;\n\n    const splitIndex = findNaturalSplitIndex(current, idealCharsPerSubtitle);\n\n    if (splitIndex <= 0 || splitIndex >= current.length) {\n      break;\n    }\n\n    const before = cleanDisplayText(current.slice(0, splitIndex));\n    const after = keepBreakText(current.slice(splitIndex));\n\n    if (before.length > 0) {\n      result.push(before);\n    }\n\n    current = after;\n\n    if (!current) {\n      break;\n    }\n  }\n\n  const rest = cleanDisplayText(current);\n\n  if (rest.length > 0) {\n    result.push(rest);\n  }\n\n  return result;\n}\n\n\n// ======================================================\n// 19. \u4e0d\u81ea\u7136\u306a\u5206\u5272\u3092\u5f8c\u51e6\u7406\u3067\u4fee\u5fa9\u3059\u308b\n// ======================================================\n\nfunction shouldMergeBecauseUnnatural(previous, current) {\n  if (!previous || !current) {\n    return false;\n  }\n\n  const merged = previous + current;\n\n  if (merged.length > hardMaxCharsPerSubtitle) {\n    return false;\n  }\n\n  if (endsWithAny(previous, badEndingPatterns)) {\n    return true;\n  }\n\n  if (startsWithAny(current, badStartingPatterns)) {\n    return true;\n  }\n\n  if (isAsciiAlphaNumeric(previous[previous.length - 1]) && isAsciiAlphaNumeric(current[0])) {\n    return true;\n  }\n\n  if (isKatakanaChar(previous[previous.length - 1]) && isKatakanaChar(current[0])) {\n    return true;\n  }\n\n  if (isDigitChar(previous[previous.length - 1]) && isJapaneseNumberUnitChar(current[0])) {\n    return true;\n  }\n\n  if (previous.endsWith('\u7406\u89e3\u3067') && current.startsWith('\u304d\u306a\u3044')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u3067\u304d') && current.startsWith('\u306a\u3044')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u3067\u304d') && current.startsWith('\u306a\u304f')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u3067') && current.startsWith('\u304d\u306a\u304f')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u306e\u3067') && current.startsWith('\u3059\u304c')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u3053\u3068') && current.startsWith('\u304c\u5206\u304b\u308a\u307e\u3059')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u30c0\u30c3\u30b7\u30e5\u30dc') && current.startsWith('\u30fc\u30c9')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u30d5\u30a1') && current.startsWith('\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb')) {\n    return true;\n  }\n\n  if (previous.endsWith('1\u4e072') && current.startsWith('\u5343\u4ee5\u4e0a')) {\n    return true;\n  }\n\n  if (previous.endsWith('\u30b9\u30ad\u30eb') && current.startsWith('\u30bb\u30c3\u30c8')) {\n    return true;\n  }\n\n  return false;\n}\n\nfunction repairUnnaturalSplits(texts) {\n  const repaired = [];\n\n  for (const rawText of texts) {\n    const text = cleanDisplayText(rawText);\n\n    if (!text) {\n      continue;\n    }\n\n    if (repaired.length === 0) {\n      repaired.push(text);\n      continue;\n    }\n\n    const previous = repaired[repaired.length - 1];\n\n    if (shouldMergeBecauseUnnatural(previous, text)) {\n      repaired[repaired.length - 1] = previous + text;\n    } else {\n      repaired.push(text);\n    }\n  }\n\n  return repaired;\n}\n\n\n// ======================================================\n// 20. \u77ed\u3059\u304e\u308b\u5b57\u5e55\u3092\u81ea\u7136\u306a\u7bc4\u56f2\u3067\u7d50\u5408\u3059\u308b\n// ======================================================\n\nfunction mergeTooShortSegments(texts) {\n  const result = [];\n\n  for (const rawText of texts) {\n    const text = cleanDisplayText(rawText);\n\n    if (!text) {\n      continue;\n    }\n\n    if (result.length === 0) {\n      result.push(text);\n      continue;\n    }\n\n    const previous = result[result.length - 1];\n    const merged = previous + text;\n\n    if (text.length < minCharsPerSubtitle && merged.length <= maxCharsPerSubtitle) {\n      result[result.length - 1] = merged;\n      continue;\n    }\n\n    if (\n      previous.length < minCharsPerSubtitle &&\n      merged.length <= maxCharsPerSubtitle\n    ) {\n      result[result.length - 1] = merged;\n      continue;\n    }\n\n    result.push(text);\n  }\n\n  return result;\n}\n\n\n// ======================================================\n// 21. 1\u884c\u306b\u53ce\u307e\u308b\u3088\u3046\u306b\u6700\u7d42\u8abf\u6574\u3059\u308b\n// ======================================================\n\nfunction forceOneLineButNatural(texts) {\n  const result = [];\n\n  for (const rawText of texts) {\n    let current = cleanDisplayText(rawText);\n\n    if (!current) {\n      continue;\n    }\n\n    if (current.length <= hardMaxCharsPerSubtitle) {\n      result.push(current);\n      continue;\n    }\n\n    let safetyCounter = 0;\n\n    while (current.length > hardMaxCharsPerSubtitle && safetyCounter < 100) {\n      safetyCounter++;\n\n      const splitIndex = findNaturalSplitIndex(current, idealCharsPerSubtitle);\n\n      if (splitIndex <= 0 || splitIndex >= current.length) {\n        break;\n      }\n\n      const before = cleanDisplayText(current.slice(0, splitIndex));\n      const after = cleanDisplayText(current.slice(splitIndex));\n\n      if (before.length > 0) {\n        result.push(before);\n      }\n\n      current = after;\n\n      if (!current) {\n        break;\n      }\n    }\n\n    if (current.length > 0) {\n      result.push(current);\n    }\n  }\n\n  return result;\n}\n\n\n// ======================================================\n// 22. \u65e5\u672c\u8a9etext\u304b\u3089\u5b57\u5e55\u30c6\u30ad\u30b9\u30c8\u914d\u5217\u3092\u4f5c\u308b\n// ======================================================\n\nconst rawSegments = splitFormattedTextIntoSegments(textForSubtitle);\n\nlet targetSubtitleTexts = [];\n\nfor (const segment of rawSegments) {\n  const parts = splitLongSegment(segment.text);\n\n  for (const part of parts) {\n    const cleaned = cleanDisplayText(part);\n\n    if (cleaned.length > 0) {\n      targetSubtitleTexts.push(cleaned);\n    }\n  }\n}\n\ntargetSubtitleTexts = repairUnnaturalSplits(targetSubtitleTexts);\ntargetSubtitleTexts = mergeTooShortSegments(targetSubtitleTexts);\ntargetSubtitleTexts = forceOneLineButNatural(targetSubtitleTexts);\ntargetSubtitleTexts = repairUnnaturalSplits(targetSubtitleTexts);\ntargetSubtitleTexts = mergeTooShortSegments(targetSubtitleTexts);\n\nif (targetSubtitleTexts.length === 0) {\n  throw new Error(\n    '\u65e5\u672c\u8a9etext\u304b\u3089\u5b57\u5e55\u533a\u5207\u308a\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002text\u306e\u5185\u5bb9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\n\n// ======================================================\n// 23. AssemblyAI words \u304b\u3089\u5168\u4f53\u6642\u9593\u3060\u3051\u3092\u53d6\u5f97\u3059\u308b\n// ======================================================\n//\n// \u3053\u3053\u3067\u306f words \u306e text \u306f\u5b57\u5e55\u672c\u6587\u3068\u3057\u3066\u4f7f\u308f\u306a\u3044\u3002\n// words \u306f\u300c\u958b\u59cb\u6642\u523b\u300d\u3068\u300c\u7d42\u4e86\u6642\u523b\u300d\u3092\u53d6\u308b\u305f\u3081\u3060\u3051\u306b\u4f7f\u3046\u3002\n// ======================================================\n\nconst cleanWords = [];\n\nfor (const word of words) {\n  const cleanWord = normalizeText(word.text);\n\n  if (!cleanWord) {\n    continue;\n  }\n\n  if (\n    typeof word.start !== 'number' ||\n    typeof word.end !== 'number'\n  ) {\n    continue;\n  }\n\n  cleanWords.push({\n    text: cleanWord,\n    start: word.start,\n    end: word.end\n  });\n}\n\nif (cleanWords.length === 0) {\n  throw new Error(\n    'AssemblyAI\u306ewords\u3092\u30af\u30ea\u30fc\u30f3\u5316\u3057\u305f\u7d50\u679c\u3001\u4f7f\u7528\u3067\u304d\u308b\u5358\u8a9e\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002'\n  );\n}\n\n\n// ======================================================\n// 24. \u65e5\u672c\u8a9e\u5b57\u5e55\u3092\u52d5\u753b\u5168\u4f53\u306e\u6642\u9593\u306b\u5408\u308f\u305b\u3066\u914d\u7f6e\u3059\u308b\n// ======================================================\n\nconst subtitles = [];\n\nconst totalStartMs = cleanWords[0].start || 0;\nconst totalEndMs = cleanWords[cleanWords.length - 1].end;\n\nif (!Number.isFinite(totalEndMs) || totalEndMs <= totalStartMs) {\n  throw new Error(\n    `\u5b57\u5e55\u306e\u5168\u4f53\u6642\u9593\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002totalStartMs=${totalStartMs}, totalEndMs=${totalEndMs}`\n  );\n}\n\nconst totalDurationMs = totalEndMs - totalStartMs;\n\nconst displayTexts = targetSubtitleTexts\n  .map((text) => cleanDisplayText(text))\n  .filter((text) => text && text.length > 0);\n\nif (displayTexts.length === 0) {\n  throw new Error(\n    '\u65e5\u672c\u8a9e\u5b57\u5e55\u30c6\u30ad\u30b9\u30c8\u304c\u7a7a\u3067\u3059\u3002targetSubtitleTexts \u306e\u4e2d\u8eab\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\nconst totalChars = displayTexts.join('').length;\n\nif (totalChars === 0) {\n  throw new Error(\n    '\u65e5\u672c\u8a9e\u5b57\u5e55\u306e\u5408\u8a08\u6587\u5b57\u6570\u304c0\u3067\u3059\u3002AI: Polish Japanese Translation\u306e\u51fa\u529b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\nconst preferredMinDurationMs = 550;\nconst safeMinDurationMs = Math.min(\n  preferredMinDurationMs,\n  Math.floor(totalDurationMs / displayTexts.length)\n);\n\nlet currentMs = totalStartMs;\n\nfor (let i = 0; i < displayTexts.length; i++) {\n  const displayText = displayTexts[i];\n\n  let startMs = currentMs;\n  let endMs;\n\n  if (i === displayTexts.length - 1) {\n    endMs = totalEndMs;\n  } else {\n    const ratio = displayText.length / totalChars;\n    let durationMs = totalDurationMs * ratio;\n\n    durationMs = Math.max(durationMs, safeMinDurationMs);\n\n    endMs = startMs + durationMs;\n\n    const remainingCount = displayTexts.length - i - 1;\n    const minimumRemainingMs = remainingCount * safeMinDurationMs;\n\n    if (endMs > totalEndMs - minimumRemainingMs) {\n      endMs = totalEndMs - minimumRemainingMs;\n    }\n  }\n\n  if (endMs <= startMs) {\n    endMs = Math.min(startMs + safeMinDurationMs, totalEndMs);\n  }\n\n  const durationMs = endMs - startMs;\n\n  if (durationMs > 0) {\n    subtitles.push({\n      time: Number((startMs / 1000).toFixed(2)),\n      duration: Number((durationMs / 1000).toFixed(2)),\n      value: displayText\n    });\n  }\n\n  currentMs = endMs;\n}\n\n\n// ======================================================\n// 25. \u6700\u5f8c\u306e\u5b57\u5e55\u304c\u5fc5\u305a totalEndMs \u3067\u7d42\u308f\u308b\u3088\u3046\u306b\u5fae\u8abf\u6574\n// ======================================================\n\nif (subtitles.length > 0) {\n  const last = subtitles[subtitles.length - 1];\n\n  const lastStartMs = Math.round(last.time * 1000);\n  const fixedLastDurationMs = totalEndMs - lastStartMs;\n\n  if (fixedLastDurationMs > 0) {\n    last.duration = Number((fixedLastDurationMs / 1000).toFixed(2));\n  }\n}\n\n\n// ======================================================\n// 26. \u5b57\u5e55\u304c\u4f5c\u6210\u3067\u304d\u306a\u304b\u3063\u305f\u5834\u5408\u306f\u30a8\u30e9\u30fc\n// ======================================================\n\nif (subtitles.length === 0) {\n  throw new Error(\n    '\u5b57\u5e55\u30c7\u30fc\u30bf\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002targetSubtitleTexts \u307e\u305f\u306f words \u306e\u4e2d\u8eab\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002'\n  );\n}\n\n\n// ======================================================\n// 27. Creatomate\u7528\u306e\u5b57\u5e55\u30c6\u30ad\u30b9\u30c8\u8981\u7d20\u3092\u4f5c\u6210\n// ======================================================\n//\n// 1\u884c\u8868\u793a\u3092\u512a\u5148\u3059\u308b\u3002\n// height\u3092\u5c0f\u3055\u304f\u3057\u3059\u304e\u308b\u3068\u4e0a\u4e0b\u304c\u7aae\u5c48\u306b\u306a\u308b\u305f\u3081\u300110%\u306b\u3059\u308b\u3002\n// \u6587\u5b57\u30b5\u30a4\u30ba\u306f\u5927\u304d\u3059\u304e\u308b\u3068\u6298\u308a\u8fd4\u3057\u3055\u308c\u3084\u3059\u3044\u306e\u30675.5vmin\u306b\u8abf\u6574\u3002\n// ======================================================\n\nconst subtitleElements = subtitles.map((subtitle) => {\n  return {\n    type: 'text',\n\n    time: subtitle.time,\n    duration: subtitle.duration,\n    text: subtitle.value,\n\n    x: '50%',\n    y: '82%',\n\n    width: '94%',\n    height: '10%',\n\n    x_alignment: '50%',\n    y_alignment: '50%',\n\n    fill_color: '#ffffff',\n    stroke_color: '#000000',\n    stroke_width: '1.1 vmin',\n\n    font_family: 'Noto Sans JP',\n    font_weight: 700,\n    font_size: '5.5 vmin',\n\n    background_color: 'rgba(0,0,0,0)',\n\n    z_index: 10\n  };\n});\n\n\n// ======================================================\n// 28. Creatomate API\u306b\u9001\u308bBody\u3092\u4f5c\u6210\n// ======================================================\n//\n// width / height \u3092\u6307\u5b9a\u3057\u306a\u3044\u3053\u3068\u3067\u3001\u5143\u52d5\u753b\u30b5\u30a4\u30ba\u306e\u307e\u307e\u51fa\u529b\u3059\u308b\u3002\n// ======================================================\n\nconst creatomateBody = {\n  output_format: 'mp4',\n\n  elements: [\n    {\n      type: 'video',\n      source: videoUrl,\n\n      x: '50%',\n      y: '50%',\n      width: '100%',\n      height: '100%',\n\n      fit: 'contain',\n\n      z_index: 1\n    },\n\n    ...subtitleElements\n  ]\n};\n\n\n// ======================================================\n// 29. \u6b21\u306eHTTP Request\u30ce\u30fc\u30c9\u306b\u6e21\u3059\n// ======================================================\n\nreturn [\n  {\n    json: {\n      video_url: videoUrl,\n\n      subtitle_count: subtitles.length,\n      subtitles,\n\n      subtitle_timing_debug: {\n        total_start_ms: totalStartMs,\n        total_end_ms: totalEndMs,\n        total_duration_sec: Number((totalDurationMs / 1000).toFixed(2)),\n        min_chars_per_subtitle: minCharsPerSubtitle,\n        ideal_chars_per_subtitle: idealCharsPerSubtitle,\n        max_chars_per_subtitle: maxCharsPerSubtitle,\n        hard_max_chars_per_subtitle: hardMaxCharsPerSubtitle,\n        video_width: videoWidth,\n        video_height: videoHeight,\n        is_portrait_video: isPortraitVideo,\n        last_subtitle_end_sec: Number(\n          (\n            subtitles[subtitles.length - 1].time +\n            subtitles[subtitles.length - 1].duration\n          ).toFixed(2)\n        )\n      },\n\n      normalized_text_debug: {\n        original_text: input.text,\n        normalized_text: textForSubtitle\n      },\n\n      creatomate_body: creatomateBody\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "63c589da-6961-4eeb-990c-16576b57e2eb",
      "name": "Render: Combine Video and Subtitle Timeline",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3344,
        112
      ],
      "parameters": {
        "url": "https://api.creatomate.com/v2/renders",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.creatomate_body }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "b3b74bf5-5b32-4cd7-89f1-2dc807368304",
      "name": "Check: Transcription Completed?",
      "type": "n8n-nodes-base.if",
      "position": [
        2544,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f28d7e48-44d1-4e94-a5b5-bbdcfd063186",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "2e18d225-cb52-4bff-8aea-4bc7cffd292a",
      "name": "AI: Add Natural Punctuation for Subtitle Generation",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2768,
        112
      ],
      "parameters": {
        "text": "=\u3042\u306a\u305f\u306f\u65e5\u672c\u8a9e\u5b57\u5e55\u7528\u30c6\u30ad\u30b9\u30c8\u3092\u6574\u3048\u308b\u5c02\u9580\u30a2\u30b7\u30b9\u30bf\u30f3\u30c8\u3067\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u6587\u5b57\u8d77\u3053\u3057\u30c6\u30ad\u30b9\u30c8\u3092\u3001\u5b57\u5e55\u3068\u3057\u3066\u81ea\u7136\u306b\u5206\u5272\u3057\u3084\u3059\u3044\u6587\u7ae0\u306b\u6574\u3048\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u76ee\u7684:\n\u5f8c\u7d9a\u306eCode\u30ce\u30fc\u30c9\u3067\u300c\u3002\u300d\u300c\u3001\u300d\u3092\u76ee\u5370\u306b\u3057\u3066\u3001Vrew\u306e\u3088\u3046\u306a\u81ea\u7136\u306a\u5b57\u5e55\u533a\u5207\u308a\u3092\u4f5c\u308b\u3053\u3068\u3002\n\n\u5fc5\u305a\u5b88\u308b\u30eb\u30fc\u30eb:\n\n* \u5143\u306e\u610f\u5473\u3092\u5909\u3048\u306a\u3044\n* \u8981\u7d04\u3057\u306a\u3044\n* \u8a00\u3044\u63db\u3048\u306a\u3044\n* \u65b0\u3057\u3044\u60c5\u5831\u3092\u8ffd\u52a0\u3057\u306a\u3044\n* \u6587\u7ae0\u3092\u81ea\u7136\u306a\u5358\u4f4d\u306b\u5206\u3051\u308b\n* \u9577\u3059\u304e\u308b\u6587\u306f\u81ea\u7136\u306a\u4f4d\u7f6e\u3067\u5206\u3051\u308b\n* \u300c\u3001\u300d\u300c\u3002\u300d\u3092\u81ea\u7136\u306b\u8ffd\u52a0\u3059\u308b\n* \u63a5\u7d9a\u304c\u4e0d\u81ea\u7136\u306a\u90e8\u5206\u306b\u306f\u300c\u3002\u300d\u3092\u5165\u308c\u3066\u6587\u3092\u5206\u3051\u308b\n* \u5b57\u5e55\u3067\u8aad\u307f\u3084\u3059\u3044\u3088\u3046\u306b\u30011\u6587\u304c\u9577\u304f\u306a\u308a\u3059\u304e\u306a\u3044\u3088\u3046\u306b\u3059\u308b\n* \u51fa\u529b\u306f\u6574\u5f62\u5f8c\u306e\u672c\u6587\u3060\u3051\u306b\u3059\u308b\n* \u7b87\u6761\u66f8\u304d\u306b\u3057\u306a\u3044\n* \u89e3\u8aac\u3084\u524d\u7f6e\u304d\u306f\u51fa\u529b\u3057\u306a\u3044\n\n\u7279\u306b\u610f\u8b58\u3059\u308b\u3053\u3068:\n\n* \u300c\u3053\u306e\u52d5\u753b\u306f\u5fc5\u898b\u3067\u3059\u3002\u5b9f\u306f\u3001AI\u3092\u4f7f\u3063\u3066\u301c\u300d\u306e\u3088\u3046\u306b\u3001\u8a71\u984c\u304c\u5207\u308a\u66ff\u308f\u308b\u5834\u6240\u3067\u306f\u6587\u3092\u5206\u3051\u308b\n* \u300c\u7d50\u8ad6\u304b\u3089\u304a\u4f1d\u3048\u3059\u308b\u3068\u3001\u301c\u300d\u306e\u3088\u3046\u306a\u5c0e\u5165\u8868\u73fe\u306e\u5f8c\u306b\u306f\u8aad\u70b9\u3092\u5165\u308c\u308b\n* \u300c\u3067\u306f\u3001\u306a\u305c\u301c\u3067\u3057\u3087\u3046\u304b\u3002\u300d\u306e\u3088\u3046\u306a\u554f\u3044\u304b\u3051\u306f1\u6587\u3068\u3057\u3066\u6574\u3048\u308b\n* \u300c\u4e00\u3064\u76ee\u306f\u3001\u301c\u3067\u3059\u3002\u300d\u306e\u3088\u3046\u306a\u8aac\u660e\u6587\u306f\u81ea\u7136\u306b\u533a\u5207\u308b\n\n\u5c02\u9580\u7528\u8a9e\u30fb\u6280\u8853\u7528\u8a9e\u30fb\u6570\u5b66\u7528\u8a9e\u306e\u8868\u8a18\u30eb\u30fc\u30eb:\n\n* \u5b57\u5e55\u3068\u3057\u3066\u8868\u793a\u3057\u305f\u3068\u304d\u306b\u4e0d\u81ea\u7136\u306b\u306a\u308b\u97f3\u8a33\u8868\u8a18\u3001\u30ab\u30bf\u30ab\u30ca\u8868\u8a18\u3001\u3072\u3089\u304c\u306a\u8868\u8a18\u306f\u3001\u4e00\u822c\u7684\u306b\u4f7f\u308f\u308c\u308b\u81ea\u7136\u306a\u8868\u8a18\u3078\u5909\u63db\u3059\u308b\n* \u5143\u306e\u610f\u5473\u306f\u7d76\u5bfe\u306b\u5909\u3048\u306a\u3044\n* \u5b57\u5e55\u3068\u3057\u3066\u81ea\u7136\u3067\u3001\u8996\u8074\u8005\u304c\u4e00\u77ac\u3067\u7406\u89e3\u3067\u304d\u308b\u8868\u8a18\u3092\u512a\u5148\u3059\u308b\n* \u6570\u5b66\u3001IT\u3001\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0\u3001AI\u3001\u30af\u30e9\u30a6\u30c9\u3001Web\u3001\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3001\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3001\u30d3\u30b8\u30cd\u30b9\u3001\u79d1\u5b66\u3001\u5de5\u5b66\u306a\u3069\u5e45\u5e83\u3044\u5c02\u9580\u7528\u8a9e\u306b\u5bfe\u5fdc\u3059\u308b\n* \u97f3\u3060\u3051\u3067\u8868\u73fe\u3055\u308c\u305f\u5c02\u9580\u7528\u8a9e\u306f\u3001\u4e00\u822c\u7684\u306a\u8868\u8a18\u304c\u660e\u78ba\u306a\u5834\u5408\u306e\u307f\u5909\u63db\u3059\u308b\n* \u88fd\u54c1\u540d\u3001\u30b5\u30fc\u30d3\u30b9\u540d\u3001\u6280\u8853\u540d\u3001\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0\u8a00\u8a9e\u540d\u3001\u30e9\u30a4\u30d6\u30e9\u30ea\u540d\u3001\u30d5\u30ec\u30fc\u30e0\u30ef\u30fc\u30af\u540d\u306f\u4e00\u822c\u7684\u306a\u6b63\u5f0f\u8868\u8a18\u3078\u5909\u63db\u3059\u308b\n* \u6570\u5f0f\u3084\u6570\u5b66\u7528\u8a9e\u306f\u4e00\u822c\u7684\u306a\u6570\u5f0f\u8868\u8a18\u3078\u5909\u63db\u3059\u308b\n* \u5909\u63db\u5f8c\u3082\u6587\u7ae0\u5168\u4f53\u306e\u610f\u5473\u3084\u6587\u8108\u3092\u5909\u3048\u306a\u3044\n* \u4e0d\u78ba\u5b9f\u306a\u5834\u5408\u306f\u7121\u7406\u306b\u5909\u63db\u3057\u306a\u3044\n\n\u5909\u63db\u4f8b:\n\n\u30b5\u30a4\u30f3 \u2192 sin\n\u30b3\u30b5\u30a4\u30f3 \u2192 cos\n\u30bf\u30f3\u30b8\u30a7\u30f3\u30c8 \u2192 tan\n\u30d1\u30a4 \u2192 \u03c0\n\u30b7\u30fc\u30bf \u2192 \u03b8\n\n\u30b8\u30a7\u30a4\u30bd\u30f3 \u2192 JSON\n\u30a8\u30fc\u30d4\u30fc\u30a2\u30a4 \u2192 API\n\u30a8\u30b9\u30ad\u30e5\u30fc\u30a8\u30eb \u2192 SQL\n\u30b7\u30fc\u30a8\u30b9\u30d6\u30a4 \u2192 CSV\n\u30a8\u30a4\u30c1\u30c6\u30a3\u30fc\u30c6\u30a3\u30fc\u30d4\u30fc \u2192 HTTP\n\u30a8\u30a4\u30c1\u30c6\u30a3\u30fc\u30c6\u30a3\u30fc\u30d4\u30fc\u30a8\u30b9 \u2192 HTTPS\n\n\u30b8\u30fc\u30d4\u30fc\u30c6\u30a3\u30fc \u2192 GPT\n\u30a8\u30eb\u30a8\u30eb\u30a8\u30e0 \u2192 LLM\n\u30b8\u30a7\u30df\u30cb \u2192 Gemini\n\u30af\u30ed\u30fc\u30c9 \u2192 Claude\n\n\u30b8\u30e3\u30d0\u30b9\u30af\u30ea\u30d7\u30c8 \u2192 JavaScript\n\u30bf\u30a4\u30d7\u30b9\u30af\u30ea\u30d7\u30c8 \u2192 TypeScript\n\u30ce\u30fc\u30c9\u30b8\u30a7\u30a4\u30a8\u30b9 \u2192 Node.js\n\u30ea\u30a2\u30af\u30c8 \u2192 React\n\u30cd\u30af\u30b9\u30c8\u30b8\u30a7\u30a4\u30a8\u30b9 \u2192 Next.js\n\u30e9\u30e9\u30d9\u30eb \u2192 Laravel\n\u30d3\u30e5\u30fc \u2192 Vue\n\u30d1\u30a4\u30bd\u30f3 \u2192 Python\n\n\u30af\u30e9\u30a6\u30c9\u30b3\u30fc\u30c9 \u2192 Cloud Code\n\u30b0\u30fc\u30b0\u30eb\u30c9\u30e9\u30a4\u30d6 \u2192 Google Drive\n\u30b0\u30fc\u30b0\u30eb\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8 \u2192 Google Sheets\n\u30b0\u30fc\u30b0\u30eb\u30b9\u30e9\u30a4\u30c9 \u2192 Google Slides\n\n\u91cd\u8981:\n\n* \u4e0a\u8a18\u306f\u4f8b\u3067\u3042\u308a\u3001\u540c\u69d8\u306e\u5c02\u9580\u7528\u8a9e\u5168\u822c\u306b\u5bfe\u5fdc\u3059\u308b\n* \u4e00\u822c\u7684\u306a\u82f1\u5b57\u8868\u8a18\u30fb\u6570\u5f0f\u8868\u8a18\u30fb\u88fd\u54c1\u540d\u8868\u8a18\u304c\u5b58\u5728\u3059\u308b\u5834\u5408\u306f\u3001\u305d\u306e\u8868\u8a18\u3092\u512a\u5148\u3059\u308b\n* \u5b57\u5e55\u3068\u3057\u3066\u753b\u9762\u306b\u8868\u793a\u3055\u308c\u305f\u969b\u306e\u81ea\u7136\u3055\u3092\u6700\u512a\u5148\u3059\u308b\n* \u82f1\u5b57\u8868\u8a18\u304c\u4e00\u822c\u7684\u306a\u7528\u8a9e\u306f\u82f1\u5b57\u8868\u8a18\u3092\u512a\u5148\u3059\u308b\n* \u6570\u5b66\u8868\u73fe\u306f\u53ef\u80fd\u306a\u9650\u308a\u81ea\u7136\u306a\u6570\u5f0f\u8868\u8a18\u3092\u512a\u5148\u3059\u308b\n\n\u51fa\u529b\u524d\u30c1\u30a7\u30c3\u30af:\n\n* \u5c02\u9580\u7528\u8a9e\u304c\u4e0d\u81ea\u7136\u306a\u30ab\u30bf\u30ab\u30ca\u306e\u307e\u307e\u6b8b\u3063\u3066\u3044\u306a\u3044\u304b\u78ba\u8a8d\u3059\u308b\n* \u6570\u5b66\u7528\u8a9e\u306f\u53ef\u80fd\u306a\u9650\u308a\u4e00\u822c\u7684\u306a\u6570\u5f0f\u8868\u8a18\u3078\u5909\u63db\u3059\u308b\n* IT\u7528\u8a9e\u306f\u53ef\u80fd\u306a\u9650\u308a\u4e00\u822c\u7684\u306a\u88fd\u54c1\u540d\u30fb\u6280\u8853\u540d\u8868\u8a18\u3078\u5909\u63db\u3059\u308b\n* \u7565\u8a9e\u3068\u3057\u3066\u5e83\u304f\u8a8d\u77e5\u3055\u308c\u3066\u3044\u308b\u3082\u306e\u306f\u7565\u8a9e\u8868\u8a18\u3092\u512a\u5148\u3059\u308b\n* \u5b57\u5e55\u3068\u3057\u3066\u753b\u9762\u306b\u8868\u793a\u3055\u308c\u305f\u969b\u306e\u81ea\u7136\u3055\u3092\u6700\u512a\u5148\u3059\u308b\n* \u51fa\u529b\u306f\u6574\u5f62\u5f8c\u306e\u672c\u6587\u306e\u307f\u3068\u3059\u308b\n* \u89e3\u8aac\u3001\u524d\u7f6e\u304d\u3001\u88dc\u8db3\u3001\u6ce8\u91c8\u306f\u51fa\u529b\u3057\u306a\u3044\n\n\u5165\u529b\u30c6\u30ad\u30b9\u30c8:\n\n{{ $json.translated_texts.ja }}\n",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "b501b205-23ae-4e45-857b-01f0a6f8529f",
      "name": "Notify: Send Video Link to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        4208,
        112
      ],
      "parameters": {
        "text": "={{ $json.url }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0AFRAJ1RTM",
          "cachedResultName": "gmail\u30a2\u30e9\u30fc\u30c82"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "7861bfa9-2211-432e-a2b0-c206cba44c48",
  "connections": {
    "Fetch: Video Metadata": {
      "main": [
        [
          {
            "node": "Transcribe: Submit to AssemblyAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check: Render Completed?": {
      "main": [
        [
          {
            "node": "Notify: Send Video Link to Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait: Creatomate Rendering",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Model: Gemini Translation": {
      "ai_languageModel": [
        [
          {
            "node": "AI: Add Natural Punctuation for Subtitle Generation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Wait: Creatomate Rendering": {
      "main": [
        [
          {
            "node": "Check: Creatomate Render Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait: AssemblyAI Processing": {
      "main": [
        [
          {
            "node": "Check: AssemblyAI Transcript Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger: Watch New Video Rows": {
      "main": [
        [
          {
            "node": "Extract: Parse Latest Video URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check: Creatomate Render Status": {
      "main": [
        [
          {
            "node": "Check: Render Completed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check: Transcription Completed?": {
      "main": [
        [
          {
            "node": "AI: Add Natural Punctuation for Subtitle Generation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait: AssemblyAI Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract: Parse Latest Video URL": {
      "main": [
        [
          {
            "node": "Prepare: Convert Drive Share URL to Download URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe: Submit to AssemblyAI": {
      "main": [
        [
          {
            "node": "Wait: AssemblyAI Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check: AssemblyAI Transcript Status": {
      "main": [
        [
          {
            "node": "Check: Transcription Completed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Render: Combine Video and Subtitle Timeline": {
      "main": [
        [
          {
            "node": "Wait: Creatomate Rendering",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare: Convert Drive Share URL to Download URL": {
      "main": [
        [
          {
            "node": "Fetch: Video Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Add Natural Punctuation for Subtitle Generation": {
      "main": [
        [
          {
            "node": "Generate: Natural Subtitle Timeline and Creatomate Render Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate: Natural Subtitle Timeline and Creatomate Render Script": {
      "main": [
        [
          {
            "node": "Render: Combine Video and Subtitle Timeline",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}