{
  "name": "SRT Translator",
  "nodes": [
    {
      "parameters": {
        "formTitle": "SRT Translator",
        "formDescription": "Translate SRT subtitle files to any language",
        "formFields": {
          "values": [
            {
              "fieldLabel": "srt_file",
              "fieldType": "file",
              "multipleFiles": false,
              "acceptFileTypes": ".srt",
              "requiredField": true
            },
            {
              "fieldLabel": "target_lang",
              "placeholder": "Spanish",
              "requiredField": true
            },
            {
              "fieldLabel": "source_lang",
              "placeholder": "English (default)"
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {}
      },
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [
        -6224,
        3248
      ],
      "id": "bb55a87c-b8d7-43f0-aa1e-23eca9a371a1",
      "name": "On form submission"
    },
    {
      "parameters": {
        "jsCode": "/**\n * N8N Code Box: Subtitle Array Batch Splitter\n * Purpose: Splits subtitle arrays into smaller batches of N items for processing\n * Input: Items with 'subtitles' array property\n * Output: Multiple items, each containing a batch of subtitles with metadata\n */\nconst BATCH_SIZE = $input.item.json.BATCH_SIZE || 50;\nconst outputItems = [];\n\nfor (const item of $input.all()) {\n  const subtitles = item.json.subtitles || [];\n\n  if (subtitles.length === 0) {\n    const { subtitles: _, ...restJson } = item.json;\n    outputItems.push({\n      json: {\n        ...restJson,\n        batch_subtitles: [],\n        batch_number: 0,\n        batch_size: 0,\n        total_subtitles: 0,\n        total_batches: 0\n      }\n    });\n    continue;\n  }\n\n  const totalBatches = Math.ceil(subtitles.length / BATCH_SIZE);\n  const { subtitles: _, ...restJson } = item.json;\n\n  for (let i = 0; i < subtitles.length; i += BATCH_SIZE) {\n    const batchNumber = Math.floor(i / BATCH_SIZE) + 1;\n    const batch = subtitles.slice(i, i + BATCH_SIZE);\n\n    outputItems.push({\n      json: {\n        ...restJson,\n        batch_subtitles: batch,\n        batch_number: batchNumber,\n        batch_size: batch.length,\n        total_subtitles: subtitles.length,\n        total_batches: totalBatches,\n        batch_start_index: i + 1,\n        batch_end_index: Math.min(i + BATCH_SIZE, subtitles.length)\n      }\n    });\n  }\n}\n\nreturn outputItems;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -5344,
        3008
      ],
      "id": "6d6bc8cf-2a59-4cdb-a331-675eab6a4917",
      "name": "Split in batches"
    },
    {
      "parameters": {
        "jsCode": "/**\n * N8N Code Box: Translation Batch Merger and SRT Data Combiner\n * Purpose: Collects all translated batches from input, deduplicates by index,\n *          and combines in a single list\n * Input: All translated batch items\n * Output: Single item with all translations sorted by index\n */\nconst allTranslations = [];\n\nfor (const item of $input.all()) {\n  const output = item.json.output || [];\n  for (const t of output) {\n    allTranslations.push(t);\n  }\n}\n\n// Deduplicate by index, last write wins (handles retries), then sort ascending\nconst map = {};\nfor (const t of allTranslations) map[t.index] = t;\nconst sorted = Object.values(map).sort((a, b) => a.index - b.index);\n\nreturn [{\n  json: {\n    subtitles_translated: sorted,\n    total_translations: sorted.length\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -4832,
        2992
      ],
      "id": "ea1c7c47-fddc-423d-8580-ed7138f0cfb3",
      "name": "Join batches"
    },
    {
      "parameters": {
        "content": "## Translate SRT\nTranslate the SRT lines in batches",
        "height": 496,
        "width": 868,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5376,
        2880
      ],
      "typeVersion": 1,
      "id": "e02a2753-ad7d-412a-9db9-a50a2fec103a",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Parse SRT\nReturns SRT contents as a JSON array",
        "height": 240,
        "width": 384,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5824,
        2864
      ],
      "typeVersion": 1,
      "id": "fee91d46-95f7-4117-8125-ae2463476f47",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "jsCode": "/**\n * N8N Code Box: SRT Subtitle Parser to JSON\n * Purpose: Parses SRT subtitle content and converts it to structured JSON format\n * Input: Items with 'file_content' property containing SRT formatted text\n * Output: Items with parsed subtitles array and count added to JSON\n * Throws: Error if no subtitles are found in the content\n */\n\n/**\n * Sanitizes subtitle text by replacing problematic characters\n * that can break JSON parsing or downstream processing\n */\nfunction sanitizeText(text) {\n  return text\n    // Curly/smart quotes \u2192 straight quotes\n    .replace(/[\\u201C\\u201D\\u201E\\u201F\\u275D\\u275E]/g, '\"')  // \" \" \u201e \u201f \u275d \u275e \u2192 \"\n    .replace(/[\\u2018\\u2019\\u201A\\u201B\\u275B\\u275C]/g, \"'\")  // ' ' \u201a \u201b \u275b \u275c \u2192 '\n    // Dashes: em/en dash \u2192 hyphen\n    .replace(/[\\u2013\\u2014]/g, '-')\n    // Ellipsis character \u2192 three dots\n    .replace(/\\u2026/g, '...')\n    // Non-breaking space \u2192 regular space\n    .replace(/\\u00A0/g, ' ')\n    // Zero-width characters\n    .replace(/[\\u200B\\u200C\\u200D\\uFEFF]/g, '')\n    // Trim stray whitespace\n    .trim();\n}\n\nfor (const item of $input.all()) {\n  // Get SRT content and clean it\n  let srtContent = item.json.file_content || '';\n  \n  // Remove BOM if present\n  srtContent = srtContent.replace(/^\\uFEFF/, '');\n  \n  // Remove carriage returns and normalize line breaks\n  srtContent = srtContent.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n  \n  const subtitlesList = [];\n  \n  if (srtContent) {\n    // Split into blocks by double line break\n    const blocks = srtContent.split(/\\n\\s*\\n+/);\n    \n    for (const block of blocks) {\n      if (!block.trim()) continue;\n      \n      // Split into lines and clean\n      const lines = block.trim().split('\\n').map(line => line.trim()).filter(line => line);\n      \n      // Verify we have at least 3 lines (index, timestamp, text)\n      if (lines.length < 3) continue;\n      \n      // Extract index\n      const index = parseInt(lines[0]);\n      if (isNaN(index)) continue;\n      \n      // Extract timestamps\n      const timestampLine = lines[1];\n      if (!timestampLine.includes('-->')) continue;\n      \n      const timeParts = timestampLine.split('-->');\n      if (timeParts.length !== 2) continue;\n      \n      const startTime = timeParts[0].trim();\n      const endTime = timeParts[1].trim();\n      \n      // Extract text (can be multiple lines) and sanitize\n      const text = sanitizeText(lines.slice(2).join(' '));\n      \n      // Create subtitle object\n      const subtitle = {\n        index: index,\n        start: startTime,\n        end: endTime,\n        text: text\n      };\n      \n      subtitlesList.push(subtitle);\n    }\n  }\n  \n  // Throw error if no subtitles were parsed\n  if (subtitlesList.length === 0) {\n    throw new Error('No subtitles found in SRT content');\n  }\n  \n  // Add parsed data to item's JSON\n  item.json.subtitles = subtitlesList;\n  item.json.subtitles_count = subtitlesList.length;\n  \n  // Remove file_content to free up memory\n  delete item.json.file_content;\n}\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -5568,
        2944
      ],
      "id": "a7e7a690-6643-4bfa-a336-524b03892b06",
      "name": "Parse SRT",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "content": "## Generate SRT\nRetrieve the translated lines and creates a new SRT file",
        "height": 240,
        "width": 400,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4464,
        2848
      ],
      "typeVersion": 1,
      "id": "d2215c6f-16c4-4630-ad7e-1ccae494b91d",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "jsCode": "/**\n * N8N Code Box: Original and Translated Subtitle Merger\n * Purpose: Merges original subtitles with translations based on index, preserving timing data\n * Input: Single item with 'subtitles' (original) and 'subtitles_translated' arrays\n * Output: Items with merged subtitles using original timing and translated text\n */\n\nconst results = [];\n\nfor (const item of $input.all()) {\n  const originalSubtitles = item.json.subtitles || [];\n  const translatedSubtitles = item.json.subtitles_translated || [];\n\n  const translationsMap = {};\n  translatedSubtitles.forEach(t => {\n    translationsMap[t.index] = t.text;\n  });\n\n  const mergedSubtitles = originalSubtitles.map(subtitle => ({\n    index: subtitle.index,\n    start: subtitle.start,\n    end: subtitle.end,\n    text: translationsMap[subtitle.index] || subtitle.text\n  }));\n\n  results.push({\n    json: {\n      merged_subtitles: mergedSubtitles,\n      target_lang: item.json.target_lang,\n      srt_file: item.json.srt_file,\n      total_subtitles: mergedSubtitles.length\n    }\n  });\n}\n\nreturn results;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -4416,
        2928
      ],
      "id": "62730897-10c4-4530-a6c5-32c994210882",
      "name": "Merge Original and Translated Subtitles"
    },
    {
      "parameters": {
        "jsCode": "/**\n * N8N Code Box: SRT File Generator from Merged Subtitles\n * Purpose: Converts merged subtitle data back to SRT format and creates binary file\n * Input: Items with 'merged_subtitles' array containing subtitle objects\n * Output: Items with SRT content as text and binary file, plus metadata (lines, size)\n */\n\n// Generate SRT file from merged subtitles\nfor (const item of $input.all()) {\n  const mergedSubtitles = item.json.merged_subtitles || [];\n  \n  if (mergedSubtitles.length === 0) {\n    item.json.srt_content = '';\n    item.json.srt_lines = 0;\n    // Remove merged_subtitles to free up memory\n    delete item.json.merged_subtitles;\n    continue;\n  }\n  \n  // Sort by index to ensure correct order\n  const sortedSubtitles = mergedSubtitles.sort((a, b) => a.index - b.index);\n  \n  // Generate SRT content\n  const srtBlocks = [];\n  \n  for (const subtitle of sortedSubtitles) {\n    // SRT format:\n    // Subtitle number\n    // start_timestamp --> end_timestamp\n    // Subtitle text\n    // Empty line\n    \n    const srtBlock = `${subtitle.index}\n${subtitle.start} --> ${subtitle.end}\n${subtitle.text}`;\n    \n    srtBlocks.push(srtBlock);\n  }\n  \n  // Join blocks with double line break\n  const srtContent = srtBlocks.join('\\n\\n');\n  \n  // Add SRT content to item\n  // item.json.srt_content = srtContent;\n  item.json.srt_lines = srtBlocks.length;\n  item.json.srt_size = srtContent.length;\n  \n  // Create binary SRT file\n  const originalFileName = item.json.srt_file.file_name || 'subtitles.srt';\n  const targetLang = item.json.target_lang || 'translated';\n  \n  // Generate new filename: originalname.TARGET_LANG.srt\n  const fileNameWithoutExt = originalFileName.replace(/\\.srt$/i, '');\n  const newFileName = `${fileNameWithoutExt}.${targetLang}.srt`;\n  \n  // Convert to Base64 for binary storage\n  const buffer = Buffer.from(srtContent, 'utf-8');\n  const base64Content = buffer.toString('base64');\n  \n  // Add as binary file\n  item.binary = item.binary || {};\n  item.binary['translated_srt'] = {\n    data: base64Content,\n    fileName: newFileName,\n    mimeType: 'text/plain'\n  };\n  \n  // Remove merged_subtitles to free up memory\n  delete item.json.merged_subtitles;\n}\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -4192,
        2928
      ],
      "id": "b418dd80-8342-41a7-a136-d553feaed2f8",
      "name": "Generate SRT file"
    },
    {
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "srt_file",
              "type": "object"
            },
            {
              "name": "target_lang"
            },
            {
              "name": "source_lang"
            },
            {
              "name": "binary_file_name"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -6224,
        2944
      ],
      "id": "30a5c581-8caf-4a12-8fea-b82a4eab6389",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "={{ $workflow.id }}",
          "mode": "id",
          "cachedResultUrl": "/workflow/=%7B%7B%20$workflow.id%20%7D%7D"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "srt_file": "={{ $json.srt_file }}",
            "target_lang": "={{ $json.target_lang }}",
            "source_lang": "={{ $json.source_lang }}",
            "binary_file_name": "srt_file"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "srt_file",
              "displayName": "srt_file",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "object",
              "removed": false
            },
            {
              "id": "target_lang",
              "displayName": "target_lang",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "source_lang",
              "displayName": "source_lang",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "binary_file_name",
              "displayName": "binary_file_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "mode": "each",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        -6000,
        3248
      ],
      "id": "90419f6b-f9fe-43f8-ba0b-0a222ee5d8de",
      "name": "Execute SRT Translate",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "completion",
        "respondWith": "returnBinary",
        "completionTitle": "Translation Complete \u2705",
        "completionMessage": "Your subtitle file has been successfully translated and is ready for download.",
        "inputDataFieldName": "translated_srt",
        "limitWaitTime": true,
        "resumeAmount": 5,
        "resumeUnit": "minutes",
        "options": {}
      },
      "type": "n8n-nodes-base.form",
      "typeVersion": 1,
      "position": [
        -5776,
        3152
      ],
      "id": "958e365a-2ef5-4856-a39c-64e5c511c50c",
      "name": "Form OK"
    },
    {
      "parameters": {
        "operation": "completion",
        "completionTitle": "Translation Failed \u274c",
        "completionMessage": "There was an error processing your subtitle file. Please try again",
        "options": {}
      },
      "type": "n8n-nodes-base.form",
      "typeVersion": 1,
      "position": [
        -5776,
        3344
      ],
      "id": "b50c953b-3950-4eb5-aaee-ff0beab8c067",
      "name": "Form ERROR"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "4ff759d9-1b87-4861-9dbd-e9dabd0358eb",
              "leftValue": "={{ $json.total_subtitles }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            },
            {
              "id": "63467ef4-24f3-4129-a5ad-cf08214527cd",
              "leftValue": "={{ $json.srt_lines }}",
              "rightValue": "={{ $json.total_subtitles }}",
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -4416,
        3248
      ],
      "id": "d959b077-4d57-4307-965a-cb8ea81bade3",
      "name": "Translation OK?"
    },
    {
      "parameters": {
        "content": "### Return result\nOK - with file\nERROR - if something failed\n",
        "height": 336,
        "width": 384,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4448,
        3136
      ],
      "typeVersion": 1,
      "id": "1b385e4f-1bde-4fab-86ba-316af82d8d7e",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "download": true,
          "userIds": "1613101109"
        }
      },
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        -6224,
        3696
      ],
      "id": "d6c61452-b415-48d9-b922-b40ca278b24c",
      "name": "Telegram Trigger",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "text": "=Oops \ud83d\ude05 Something went wrong.\nPlease try again with your .SRT file and target language.\n",
        "additionalFields": {
          "appendAttribution": false,
          "reply_to_message_id": "={{ $('Telegram Trigger').item.json.message.message_id }}"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -4752,
        3824
      ],
      "id": "a45c9fd5-ff12-4186-9cd8-4b0516a2f1ce",
      "name": "Reply with error",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.message.document }}",
                    "rightValue": "",
                    "operator": {
                      "type": "object",
                      "operation": "notExists",
                      "singleValue": true
                    },
                    "id": "f207c61b-d43f-41ae-8ff4-756809a9f96f"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "NO File"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "d19f0484-1c91-4d19-84b1-c79bc21c29bd",
                    "leftValue": "={{ $json.message.document.file_name.toLowerCase() }}",
                    "rightValue": "srt",
                    "operator": {
                      "type": "string",
                      "operation": "notEndsWith"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "File not a SRT"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "ee6b6cf3-5e43-4b83-9b06-e3e858e840bf",
                    "leftValue": "={{ $json.message.caption }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "empty",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "No target lang"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        -6000,
        3664
      ],
      "id": "8b74a7a0-3cb2-4f99-8374-486f5d38ed31",
      "name": "Check Message File"
    },
    {
      "parameters": {
        "operation": "sendDocument",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "binaryData": true,
        "binaryPropertyName": "translated_srt",
        "additionalFields": {
          "caption": "=File translated to {{ $json.target_lang }} \u2705",
          "reply_to_message_id": "={{ $('Telegram Trigger').item.json.message.message_id }}"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -4976,
        3712
      ],
      "id": "cc176474-6e22-4024-8470-ee77afa61406",
      "name": "Send Translated SRT",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "text": "={{ $json.message.caption }}",
        "attributes": {
          "attributes": [
            {
              "name": "target_lang",
              "description": "Target Language",
              "required": true
            },
            {
              "name": "source_lang",
              "description": "Source Language"
            }
          ]
        },
        "options": {
          "systemPromptTemplate": "=# Caption Language Extractor\n\n## Role\nYou are a language extraction parser. Extract translation intent from a Telegram caption and return a JSON object.\n\n## Extraction Rules\n- **target_lang** (mandatory): language to translate INTO. Normalize to full English name. If not found, return `\"unknown\"`.\n- **source_lang** (optional): language of the original subtitles, only if explicitly mentioned. If absent, return `\"\"`.\n\n## Output Format\nReturn ONLY valid JSON. No text outside it.\n```json\n{\"target_lang\": \"string\", \"source_lang\": \"string\"}\n```\n\n## Examples\n| Caption | target_lang | source_lang |\n|---|---|---|\n| `ES` | `\"Spanish\"` | `\"\"` |\n| `traduce a espa\u00f1ol` | `\"Spanish\"` | `\"\"` |\n| `from English to Spanish` | `\"Spanish\"` | `\"English\"` |\n| `traduce de ingl\u00e9s a espa\u00f1ol` | `\"Spanish\"` | `\"English\"` |"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "typeVersion": 1.2,
      "position": [
        -5776,
        3696
      ],
      "id": "319a11e9-abee-4e02-8ffc-d4cda36c8a79",
      "name": "Extract Source & Target Lang",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "text": "=File received. Translating to '{{ $json.output.target_lang }}'... \u23f3",
        "additionalFields": {
          "appendAttribution": false,
          "disable_notification": true,
          "reply_to_message_id": "={{ $('Telegram Trigger').item.json.message.message_id }}"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -5424,
        3632
      ],
      "id": "32621e68-9325-4597-b5bc-042a0c4791f1",
      "name": "Send 'In Progress' message",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"json_schema\": {\n    \"name\": \"translation_batch\",\n    \"schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"output\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"index\": {\n                \"type\": \"integer\"\n              },\n              \"text\": {\n                \"type\": \"string\"\n              }\n            },\n            \"required\": [\n              \"index\",\n              \"text\"\n            ]\n          }\n        }\n      },\n      \"required\": [\n        \"output\"\n      ]\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        -4960,
        3200
      ],
      "id": "7212ca16-59ae-4e8e-9517-bfc6102f4e43",
      "name": "SRT indexed array format",
      "onError": "continueRegularOutput",
      "notes": "Sample:\n\n[\n  {\n    \"index\": 1,\n    \"text\": \"Este es un archivo SRT de ejemplo.\"\n  },\n  {\n    \"index\": 2,\n    \"text\": \"Este es un archivo SRT de ejemplo.\"\n  }\n]"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -4192,
        3152
      ],
      "id": "ebf9aca6-88c3-4e39-b52f-4b962c50fdf1",
      "name": "Return OK"
    },
    {
      "parameters": {
        "errorMessage": " There was an error processing the subtitle file"
      },
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        -4192,
        3344
      ],
      "id": "c745d72a-8267-478e-aa7d-b453e1d0f33b",
      "name": "Return ERROR"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "10c0668b-1098-479f-806c-dcca01dc9f54",
              "name": "BATCH_SIZE",
              "value": 150,
              "type": "number"
            },
            {
              "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
              "name": "LLM_PARALLEL_COUNT",
              "value": 10,
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -6016,
        2944
      ],
      "id": "c2c911de-3187-4fb0-8673-be7daa61d335",
      "name": "CONFIG"
    },
    {
      "parameters": {
        "operation": "text",
        "binaryPropertyName": "={{ $json.binary_file_name }}",
        "destinationKey": "file_content",
        "options": {
          "encoding": "utf8",
          "stripBOM": true,
          "keepSource": "json"
        }
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        -5792,
        2944
      ],
      "id": "eb29e1f8-2df8-43c1-baf3-ceb418bbaab1",
      "name": "Read file contents",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "model": "openai/gpt-4.1-nano",
        "options": {
          "responseFormat": "json_object"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -5296,
        3216
      ],
      "id": "6e530003-2fdc-42cc-b75b-1a84f717859b",
      "name": "OpenRouter - GPT 4.1 Nano",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": "google/gemini-2.5-flash-lite",
        "options": {
          "responseFormat": "json_object"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -5168,
        3248
      ],
      "id": "d8c17d48-ea50-46a4-b215-ec2fc16a699f",
      "name": "OpenRouter - Gemini 2.5 Flash Lite",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": "google/gemini-2.5-flash-lite",
        "options": {
          "responseFormat": "json_object"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -5776,
        3904
      ],
      "id": "5380a2af-4400-4e29-af68-156cdf484a18",
      "name": "OpenRouter - Gemini 2.5 Flash Lite (target extractor)",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $workflow.id }}",
          "cachedResultName": "={{ $workflow.id }}"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "srt_file": "={{ $json.message.document }}",
            "target_lang": "={{ $json.output.target_lang }}",
            "source_lang": "={{ $json.output.source_lang || 'auto-infer the source language' }}",
            "binary_file_name": "data"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "srt_file",
              "displayName": "srt_file",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "object"
            },
            {
              "id": "target_lang",
              "displayName": "target_lang",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "source_lang",
              "displayName": "source_lang",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "binary_file_name",
              "displayName": "binary_file_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        -5200,
        3808
      ],
      "id": "19ba0f93-c088-435c-b9d9-bfa18832b275",
      "name": "Execute SRT Translate workflow",
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -4640,
        2944
      ],
      "id": "dc167dbf-0ead-40fe-8ef7-60a7823df5b8",
      "name": "Aggregate SRT lines"
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -5424,
        3824
      ],
      "id": "919ffbcf-6640-4ae7-8aca-a092148945f4",
      "name": "Recover context"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=INPUT -> {{ JSON.stringify($json.batch_subtitles.map(sub => ({ index: sub.index, text: sub.text }))) }}\n\n___",
        "hasOutputParser": true,
        "needsFallback": true,
        "messages": {
          "messageValues": [
            {
              "message": "=# Professional Subtitle Translation System\n\n## Role\nYou are a professional audiovisual subtitle translator. Your sole task is to translate subtitle entries from '{{ $json.source_lang || 'english' }}' to '{{ $json.target_lang }}' with linguistic precision and narrative consistency.\n\n## Input Format\nA JSON array of subtitle entries, each containing:\n- `index`: sequential identifier (integer)\n- `text`: subtitle text to translate\n\n## Translation Rules\n\n### Rule 1 \u2014 Context-First Analysis\nBefore translating any entry, read the entire batch. Identify:\n- Character genders and relationships\n- Speaker identity and emotional tone\n- Technical terms, proper nouns, recurring vocabulary\n- Formality register (tuteo vs. usted, t\u00fa vs. vous, etc.)\n\nUse this context to resolve every ambiguity consistently across the batch.\n\n### Rule 2 \u2014 Language Handling\n- If a line is already in {{ $json.target_lang }}: return it unchanged\n- If a line is in a language other than {{ $json.source_lang || 'english' }} but identifiable: translate it to {{ $json.target_lang }}\n- If a line is in an unidentifiable language: return it unchanged\n- If a line is empty, whitespace-only, or contains only symbols/numbers: return it unchanged\n\n### Rule 3 \u2014 Translation Quality\n- PRESERVE: meaning, tone, style, punctuation, and capitalization patterns\n- PRESERVE: similar text length to respect subtitle timing constraints\n- ENSURE: natural, idiomatic flow in {{ $json.target_lang }}\n- APPLY: cultural adaptation only when literal translation would impede comprehension\n- MAINTAIN: all terminology and character name decisions consistently across the batch\n\n## Output Format\nReturn ONLY a valid JSON object. No explanations, no comments, no markdown, no text outside the JSON.\n```json\n{\n  \"output\": [\n    {\"index\": int, \"text\": \"string\"},\n    ...\n  ]\n}\n```\n\n### Constraints\n- MUST contain exactly the same number of entries as the input\n- MUST NOT add, remove, or reorder entries\n- MUST NOT include any content outside the JSON object\n\n## Example\n\n**Input:**\n```json\n[\n  {\"index\": 47, \"text\": \"Maria walked into the room.\"},\n  {\"index\": 48, \"text\": \"She looked tired.\"},\n  {\"index\": 49, \"text\": \"Good morning, doctor.\"}\n]\n```\n\n**Output (English \u2192 Spanish):**\n```json\n{\n  \"output\": [\n    {\"index\": 47, \"text\": \"Mar\u00eda entr\u00f3 en la habitaci\u00f3n.\"},\n    {\"index\": 48, \"text\": \"Se ve\u00eda cansada.\"},\n    {\"index\": 49, \"text\": \"Buenos d\u00edas, doctora.\"}\n  ]\n}\n```\n"
            }
          ]
        },
        "batching": {
          "batchSize": "={{ $('CONFIG').item.json.LLM_PARALLEL_COUNT }}"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        -5152,
        2992
      ],
      "id": "664ef063-b24f-4b0d-adcd-6cd5011524cf",
      "name": "Translate lines",
      "retryOnFail": true,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "chatId": "={{ $json.message.chat.id }}",
        "text": "=Hi \ud83d\udc4b Please send me:  \n- Your .SRT file  \n- The target language (e.g. English, French...) in the file description ",
        "additionalFields": {
          "appendAttribution": false,
          "disable_notification": true,
          "reply_to_message_id": "={{ $json.message.message_id }}"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -5712,
        3536
      ],
      "id": "bad9382c-e4fe-4a3e-ae47-c8b31258c77d",
      "name": "Send instructions",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "On form submission": {
      "main": [
        [
          {
            "node": "Execute SRT Translate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split in batches": {
      "main": [
        [
          {
            "node": "Translate lines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Join batches": {
      "main": [
        [
          {
            "node": "Aggregate SRT lines",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse SRT": {
      "main": [
        [
          {
            "node": "Split in batches",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate SRT lines",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return ERROR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Original and Translated Subtitles": {
      "main": [
        [
          {
            "node": "Generate SRT file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate SRT file": {
      "main": [
        [
          {
            "node": "Translation OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "CONFIG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute SRT Translate": {
      "main": [
        [
          {
            "node": "Form OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Form ERROR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translation OK?": {
      "main": [
        [
          {
            "node": "Return OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return ERROR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Check Message File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Message File": {
      "main": [
        [
          {
            "node": "Send instructions",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send instructions",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send instructions",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract Source & Target Lang",
            "type": "main",
            "index": 0
          },
          {
            "node": "Recover context",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Extract Source & Target Lang": {
      "main": [
        [
          {
            "node": "Send 'In Progress' message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Recover context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SRT indexed array format": {
      "ai_outputParser": [
        [
          {
            "node": "Translate lines",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "CONFIG": {
      "main": [
        [
          {
            "node": "Read file contents",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read file contents": {
      "main": [
        [
          {
            "node": "Parse SRT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter - GPT 4.1 Nano": {
      "ai_languageModel": [
        [
          {
            "node": "Translate lines",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter - Gemini 2.5 Flash Lite": {
      "ai_languageModel": [
        [
          {
            "node": "Translate lines",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    },
    "OpenRouter - Gemini 2.5 Flash Lite (target extractor)": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Source & Target Lang",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Execute SRT Translate workflow": {
      "main": [
        [
          {
            "node": "Send Translated SRT",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply with error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate SRT lines": {
      "main": [
        [
          {
            "node": "Merge Original and Translated Subtitles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Translated SRT": {
      "main": [
        [],
        [
          {
            "node": "Reply with error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recover context": {
      "main": [
        [
          {
            "node": "Execute SRT Translate workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate lines": {
      "main": [
        [
          {
            "node": "Join batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "0665c796-7d0d-442e-814c-856acbf20117",
  "id": "fdSpvUNx0vyf0kMJ",
  "tags": [
    {
      "updatedAt": "2025-08-20T18:31:53.488Z",
      "createdAt": "2025-08-20T18:31:53.488Z",
      "id": "MYQWvz4yQohlX8Dp",
      "name": "telegram"
    },
    {
      "updatedAt": "2025-08-15T08:45:03.140Z",
      "createdAt": "2025-08-15T08:45:03.140Z",
      "id": "iiq7yealSFabpVUg",
      "name": "ai"
    },
    {
      "updatedAt": "2025-08-20T20:35:44.435Z",
      "createdAt": "2025-08-20T20:35:44.435Z",
      "id": "syw5irhKWlVWyLTC",
      "name": "utils"
    }
  ]
}