{
  "id": "Aam0MiNTRlIk0307",
  "name": "Automatic Video Subtitles",
  "tags": [],
  "nodes": [
    {
      "id": "25815aa9-f6f8-436b-8672-c55035889687",
      "name": "Listen to new file",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        -400,
        -48
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "1H47ZBdBi-qmNiC83IuZjYjG_4kLQuSp0",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1H47ZBdBi-qmNiC83IuZjYjG_4kLQuSp0",
          "cachedResultName": "videos"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "936fdf71-65bd-4995-ab57-97ec1d2ddc68",
      "name": "Download file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        64,
        -64
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {},
        "operation": "download"
      },
      "typeVersion": 3
    },
    {
      "id": "4b1b5cbc-04fc-4992-9475-b6f7d85b2f5d",
      "name": "Check if new file is audio",
      "type": "n8n-nodes-base.if",
      "position": [
        -192,
        -48
      ],
      "parameters": {
        "options": {
          "ignoreCase": true
        },
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cddb00f9-2237-4e8b-bad7-8bd7f5d7524c",
              "operator": {
                "type": "string",
                "operation": "regex"
              },
              "leftValue": "={{ $json.fileExtension }}",
              "rightValue": "^(mp3|wav|aac|ogg|flac|m4a|wma)$"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f394bf23-4eb3-4821-a902-b771713c9440",
      "name": "Transcribe audio",
      "type": "n8n-nodes-smallestai.smallestai",
      "position": [
        272,
        -64
      ],
      "parameters": {
        "resource": "stt",
        "additionalOptions": {}
      },
      "credentials": {},
      "typeVersion": 1
    },
    {
      "id": "b07e86d1-0a38-4ea3-bf2d-a5eaeab2077f",
      "name": "Generate subtitles",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        -64
      ],
      "parameters": {
        "jsCode": "const NL = String.fromCharCode(10);\nconst root = $input.first().json;\n\nconst TEXT_KEYS = ['transcript', 'transcription', 'text', 'result', 'output', 'content'];\nfunction findText(o) {\n  let best = '';\n  (function walk(x) {\n    if (x == null) return;\n    if (Array.isArray(x)) { x.forEach(walk); return; }\n    if (typeof x === 'object') {\n      for (const k of Object.keys(x)) {\n        const v = x[k];\n        if (typeof v === 'string' && TEXT_KEYS.indexOf(k.toLowerCase()) !== -1 && v.trim().length > best.length) best = v;\n        walk(v);\n      }\n    }\n  })(o);\n  if (best && best.trim()) return best.trim();\n  (function walk2(x) {\n    if (x == null) return;\n    if (typeof x === 'string') { if (x.trim().length > best.length) best = x; return; }\n    if (Array.isArray(x)) { x.forEach(walk2); return; }\n    if (typeof x === 'object') Object.keys(x).forEach(k => walk2(x[k]));\n  })(o);\n  return (best || '').trim();\n}\n\nfunction findSegments(o) {\n  let found = null;\n  (function walk(x) {\n    if (found) return;\n    if (Array.isArray(x)) {\n      if (x.length && x[0] && typeof x[0] === 'object' && x[0].start != null) { found = x; return; }\n      x.forEach(walk); return;\n    }\n    if (x && typeof x === 'object') Object.keys(x).forEach(k => walk(x[k]));\n  })(o);\n  return found;\n}\n\nfunction findDuration(o) {\n  let d = null;\n  (function walk(x) {\n    if (d != null) return;\n    if (Array.isArray(x)) { x.forEach(walk); return; }\n    if (x && typeof x === 'object') {\n      for (const k of Object.keys(x)) {\n        if (/duration/i.test(k) && typeof x[k] === 'number' && x[k] > 0) { d = x[k]; return; }\n        walk(x[k]);\n      }\n    }\n  })(o);\n  return d;\n}\n\nconst transcriptText = findText(root);\nconst segs = findSegments(root);\nconst duration = findDuration(root);\n\nfunction p(n, l) { l = l || 2; return String(n).padStart(l, '0'); }\nfunction fmt(sec) { if (sec < 0) sec = 0; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); const ms = Math.round((sec - Math.floor(sec)) * 1000); return p(h) + ':' + p(m) + ':' + p(s) + ',' + p(ms, 3); }\n\nlet cues = [];\nlet estimated = true;\nif (segs && segs.length && segs[0].start != null) {\n  if (segs[0].text != null) {\n    cues = segs.map(s => ({ start: s.start, end: (s.end != null ? s.end : s.start + 2), text: (s.text || '').toString().trim() }));\n    estimated = false;\n  } else {\n    const g = 8;\n    for (let i = 0; i < segs.length; i += g) {\n      const c = segs.slice(i, i + g);\n      cues.push({ start: c[0].start, end: (c[c.length - 1].end != null ? c[c.length - 1].end : c[c.length - 1].start + 1), text: c.map(w => (w.word || w.text || '').toString()).join(' ').trim() });\n    }\n    estimated = false;\n  }\n}\nif (!cues.length) {\n  const words = transcriptText.split(/\\s+/).filter(Boolean);\n  const per = 8;\n  const total = Math.max(1, Math.ceil(words.length / per));\n  const dur = (duration && duration > 0) ? duration : (words.length / 2.5);\n  const cd = total > 0 ? dur / total : 0;\n  for (let i = 0; i < total; i++) {\n    cues.push({ start: i * cd, end: (i + 1) * cd, text: words.slice(i * per, (i + 1) * per).join(' ') });\n  }\n  estimated = true;\n}\n\nlet srt = '';\ncues.forEach((c, i) => { srt += (i + 1) + NL + fmt(c.start) + ' --> ' + fmt(c.end) + NL + c.text + NL + NL; });\nsrt = srt.trim() + NL;\n\nreturn [{ json: { cueCount: cues.length, estimatedTiming: estimated, transcriptFound: transcriptText.slice(0, 80), responseTopLevelKeys: Object.keys(root || {}), srt: srt } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f4bf664d-c103-4781-ae25-da4f67eb169f",
      "name": "Prepare binary for Drive File",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        -64
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfor (const item of items) {\n  const originalName = $('Listen to new file').first().json.name;\n  item.json.srtFileName = originalName.replace(/\\.[^/.]+$/, \".srt\");\n  \n  item.binary = {\n    data: {\n      data: Buffer.from(item.json.srt, 'utf8').toString('base64'),\n      mimeType: 'text/plain',\n      fileName: item.json.srtFileName\n    }\n  };\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "f88e76cd-c70f-472d-9ea4-fdee2ac578a5",
      "name": "Upload subtitles file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        896,
        -64
      ],
      "parameters": {
        "name": "={{ $json.srtFileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": "1H47ZBdBi-qmNiC83IuZjYjG_4kLQuSp0",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1H47ZBdBi-qmNiC83IuZjYjG_4kLQuSp0",
          "cachedResultName": "videos"
        },
        "inputDataFieldName": "=data"
      },
      "typeVersion": 3
    },
    {
      "id": "b204909c-3e97-4a39-acc9-e77ee7e6b96e",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        -224
      ],
      "parameters": {
        "width": 464,
        "height": 624,
        "content": "## Automatic Video Subtitles Generator\n\n### How it works\n\nThis workflow automatically listens for new files added to a specific Google Drive folder, verifies if they match supported audio/video formats, and downloads the content. It then transcribes the audio using Smallest.ai, processes the transcription timestamps into standard SRT subtitle format via a custom JavaScript node, and uploads the final `.srt` subtitle file back to Google Drive.\n\n### Setup steps\n\n- Connect your Google Drive account credentials in the **Listen to new file**, **Download file**, and **Upload subtitles file** nodes.\n- Update the target Folder ID in both the trigger node and the final upload node to point to your specific folder.\n- Connect your Smallest.ai credentials in the **Transcribe audio** node to enable speech-to-text parsing.\n\n### Customization\n\nAdjust the regular expression in the format validation node if you want to support or restrict different extensions. You can also customize the **Generate subtitles** Code node logic to change word grouping limits, styling, or timestamp spacing defaults."
      },
      "typeVersion": 1
    },
    {
      "id": "370419a7-2325-47a1-a6a1-8f4fd2f1353c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 336,
        "content": "## Detect & Download Media\n\nTriggers on new files added to the monitored folder, validates the extension against media formats, and pulls the binary data."
      },
      "typeVersion": 1
    },
    {
      "id": "3d18a360-bd1d-4cec-9b5f-4a2ca098bb4e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 336,
        "content": "## Save & Export SRT\n\nConverts the text-based SRT output back into a binary file stream and pushes the complete subtitle document right back to your Google Drive directory."
      },
      "typeVersion": 1
    },
    {
      "id": "6dfed1f8-3713-46c2-92cf-c6ba9b7523cf",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        48,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 336,
        "content": "## Transcribe & Process\n\nSends the downloaded media file to Smallest.ai for Speech-to-Text translation and formats the payload into valid timestamped SRT subtitle blocks."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "a921835b-b5c5-468e-8186-38a636d12a64",
  "nodeGroups": [],
  "connections": {
    "Download file": {
      "main": [
        [
          {
            "node": "Transcribe audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe audio": {
      "main": [
        [
          {
            "node": "Generate subtitles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate subtitles": {
      "main": [
        [
          {
            "node": "Prepare binary for Drive File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listen to new file": {
      "main": [
        [
          {
            "node": "Check if new file is audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if new file is audio": {
      "main": [
        [
          {
            "node": "Download file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare binary for Drive File": {
      "main": [
        [
          {
            "node": "Upload subtitles file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}