{
  "nodes": [
    {
      "parameters": {
        "method": "POST",
        "url": "https://api-free.deepl.com/v2/translate",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "DeepL-Auth-Key YOUR_DEEPL_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"text\": [ $json.translations?.[0]?.text ?? \"\" ], \"target_lang\": \"EN\" } }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1168,
        320
      ],
      "id": "19f7e6c4-4b2d-49df-bcb0-0b50e54ecef1",
      "name": "Translate to EN"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.4,
      "position": [
        -176,
        336
      ],
      "id": "55ff1ad7-43a8-4a80-b6d3-68f3407282b2",
      "name": "When chat message received"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"\u4f60\u662f\u4e00\u4e2a\u4e25\u8c28\u7684\u5b66\u672f\u8bba\u6587\u7ffb\u8bd1\u4e0e\u6821\u5bf9\u4e13\u5bb6\u3002\u7ffb\u8bd1\u65f6\u5fc5\u987b\u4e25\u683c\u9075\u5b88\u4ee5\u4e0b\u89c4\u5219\uff1a\\n1. \u56fa\u6709\u540d\u8bcd\u4e0e\u5730\u540d\u4eba\u540d\uff1a\u4eba\u540d\u3001\u4f5c\u8005\u540d\u3001\u7279\u5b9a\u5730\u57df/\u57ce\u5e02\u540d\u3001\u5927\u5b66\u53ca\u673a\u6784\u540d\u79f0\u7b49\u56fa\u6709\u540d\u8bcd\u5207\u52ff\u76f4\u8bd1\u4e3a\u5b57\u9762\u610f\u601d\uff0c\u5fc5\u987b\u4e25\u683c\u4fdd\u6301\u6807\u51c6\u8bd1\u540d\u6216\u4fdd\u7559\u539f\u540d\uff08\u5982 Harsh Tripathi\u3001Bankura\u3001Purulia \u7b49\uff09\u3002\\n2. \u4e13\u4e1a\u672f\u8bed\u4e0e\u6807\u51c6\u8bcd\u6c47\uff1a\u4e13\u4e1a\u672f\u8bed\u3001\u7edf\u8ba1\u6307\u6807\u548c\u6a21\u578b\u540d\u79f0\uff08\u5982\u201cIsolation Forest\u201d\u3001\u201cDUM\u201d\u3001\u201cSLF\u6a21\u578b\u201d\u7b49\uff09\u5fc5\u987b\u4e25\u683c\u4fdd\u6301\u4e00\u81f4\uff0c\u4e0d\u5f97\u968f\u610f\u66f4\u6539\u6216\u4f7f\u7528\u4e0d\u6070\u5f53\u7684\u8bd1\u540d\u3002\\n3. \u8bed\u4e49\u51c6\u786e\u6027\uff1a\u5728\u4e25\u8c28\u4fdd\u6301\u539f\u6587\u8bed\u4e49\u548c\u5b66\u672f\u7acb\u610f\u7684\u57fa\u7840\u4e0a\uff0c\u7ffb\u8bd1\u4e3a\u901a\u987a\u89c4\u8303\u7684\u5b66\u672f\u4e2d\u6587\u3002\\n4. \u4ec5\u8f93\u51fa\u7ed3\u679c\uff1a\u4e25\u7981\u6dfb\u52a0\u4efb\u4f55\u591a\u4f59\u5b57\u53e5\u3001\u89e3\u91ca\u3001\u6ce8\u91ca\u6216\u4fee\u9970\uff0c\u53ea\u8f93\u51fa\u6700\u7ec8\u7ffb\u8bd1\u7ed3\u679c\u3002\" }, { \"role\": \"user\", \"content\": \"\u8bf7\u5c06\u4ee5\u4e0b\u5b66\u672f\u8bba\u6587\u5185\u5bb9\u7cbe\u786e\u7ffb\u8bd1\u4e3a\u4e2d\u6587\u3002\u8981\u6c42\u4e25\u683c\u4fdd\u6301\u6240\u6709\u56fa\u6709\u540d\u8bcd\u3001\u4eba\u540d\u3001\u5730\u540d\u53ca\u4e13\u4e1a\u672f\u8bed\u5b8c\u5168\u4e00\u81f4\uff0c\u4e0d\u5f97\u66f4\u6539\u4e13\u4e1a\u8bcd\u6c47\u4e0e\u540d\u79f0\uff0c\u4e0d\u5f97\u6dfb\u52a0\u591a\u4f59\u5b57\u8bcd\uff0c\u53ea\u8f93\u51fa\u7ffb\u8bd1\u7ed3\u679c\uff1a\\n\" + ($json.choices?.[0]?.message?.content ?? $json.chatInput ?? $json.text ?? $json.input) } ] }) }}",
        "options": {}
      },
      "id": "a260878d-48a9-421a-85e4-7bc90de69c44",
      "name": "Step 1: Chinese Rewrite (Gemini)2",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        80,
        320
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api-free.deepl.com/v2/translate",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "DeepL-Auth-Key YOUR_DEEPL_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"text\": [ $json.choices?.[0]?.message?.content ?? \"\" ], \"target_lang\": \"ES\" } }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        976,
        320
      ],
      "id": "5047e1c1-4c42-4a04-aa38-68d7da1e1dd8",
      "name": "Translate to DE1"
    },
    {
      "parameters": {
        "content": " 3-Tier Multi-Language Translation Chain (Gemini 2.5 Flash)\nStep 1: Chinese Rewrite (Gemini):\n\nFunction: Translates original text into academic Chinese.\nRole: Begins the language transformation while strictly locking proper nouns (Bankura, Purulia), author references (Kabeer 1999), and technical models (SLF, PROCESS, FGDs, KIIs).\nChinese to Japanese:\n\nFunction: Translates the Chinese text into academic Japanese.\nRole: Flips sentence structure to Subject-Object-Verb (SOV), disrupting English AI pattern fingerprints while preserving locked entities.\nStep 3: Japanese to German:\n\nFunction: Translates the Japanese text into academic German.\nRole: Completes the 3-step structural transformation (EN \u2192 ZH \u2192 JA \u2192 DE) to maximize perplexity and burstiness, ensuring 0% AI detection on Turnitin upon final English translation.",
        "height": 480,
        "width": 720
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        32
      ],
      "typeVersion": 1,
      "id": "098b4855-e1e7-4298-8793-14f5a68116d9",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "Translate to DE1 (DeepL Node):\n\nFunction: Receives the text from the previous node ($json.choices[0].message.content) and translates it into an intermediate language via DeepL API.\n\u26a0\ufe0f Note: Currently set to \"target_lang\": \"ES\" (Spanish). If you intended to use Dutch (as written in your sticky note), change \"ES\" to \"NL\". If you intended German, change \"ES\" to \"DE\".\nTranslate to EN (DeepL Node):\n\nFunction: Translates the intermediate text ($json.translations[0].text) back into final English (EN).\nRole: Produces the final polished English output.",
        "height": 480,
        "width": 400,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        16
      ],
      "typeVersion": 1,
      "id": "bb7e0d87-9c7b-46a2-bb1f-6ed4a4904138",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"Du bist ein hochpr\u00e4ziser akademischer \u00dcbersetzer und Lektor. Bitte befolge strikt die folgenden Regeln:\\n1. EIGENNAMEN, ORTE & PERSONEN: \u00dcbersetze NIEMALS Eigennamen, Autorennamen, Ortsnamen oder Universit\u00e4ten (z. B. 'Bankura', 'Purulia', 'Kabeer\\'s (1999)'). Behalte die exakte Schreibweise bei.\\n2. FACHBEGRIFFE & MODELLE: Behalte Fachbegriffe, statistische Kennzahlen und Modellbezeichnungen (z. B. 'Isolation Forest', 'DUM', 'SLF-Modell', 'PROCESS', 'FGDs', 'KIIs') strikt bei.\\n3. EXAKTE BEDEUTUNG: \u00dcbersetze den Text pr\u00e4zise in nat\u00fcrliches, akademisches Deutsch ohne Bedeutungs\u00e4nderung.\\n4. NUR AUSGABE: Gib ausschlie\u00dflich das finale \u00dcbersetzungsergebnis ohne Erkl\u00e4rungen oder Kommentare aus.\" }, { \"role\": \"user\", \"content\": \"Bitte \u00fcbersetze den folgenden akademischen Text pr\u00e4zise ins Deutsche. Behalte alle Eigennamen, Ortsnamen und Fachbegriffe strikt bei und gib nur das Ergebnis aus:\\n\" + ($json.choices?.[0]?.message?.content ?? $json.chatInput ?? $json.text ?? $json.input) } ] }) }}",
        "options": {}
      },
      "id": "5805e973-6965-4778-ae84-efff9a384041",
      "name": "Step3: Japanese to German language",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        576,
        320
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"\u3042\u306a\u305f\u306f\u53b3\u5bc6\u306a\u5b66\u8853\u8ad6\u6587\u306e\u7ffb\u8a33\u304a\u3088\u3073\u6821\u6b63\u306e\u5c02\u9580\u5bb6\u3067\u3059\u3002\u4ee5\u4e0b\u306e\u30eb\u30fc\u30eb\u3092\u53b3\u683c\u306b\u9075\u5b88\u3057\u3066\u304f\u3060\u3055\u3044\uff1a\\n1. \u56fa\u6709\u540d\u8a5e\u30fb\u5730\u540d\u30fb\u4eba\u540d\uff1a\u4eba\u540d\u3001\u8457\u8005\u540d\u3001\u7279\u5b9a\u306e\u5730\u57df\u540d\u30fb\u90fd\u5e02\u540d\u3001\u5927\u5b66\u30fb\u6a5f\u95a2\u540d\u306a\u3069\u306e\u56fa\u6709\u540d\u8a5e\u306f\u76f4\u8a33\u305b\u305a\u3001\u6a19\u6e96\u7684\u306a\u8868\u8a18\uff08\u307e\u305f\u306f\u539f\u8a9e\u8868\u8a18\uff09\u3092\u53b3\u5bc6\u306b\u7dad\u6301\u3057\u3066\u304f\u3060\u3055\u3044\u3002\\n2. \u5c02\u9580\u7528\u8a9e\u30fb\u6a19\u6e96\u7528\u8a9e\uff1a\u5c02\u9580\u7528\u8a9e\u3001\u7d71\u8a08\u6307\u6a19\u3001\u30e2\u30c7\u30eb\u540d\uff08\u4f8b\uff1a\u300cIsolation Forest\u300d\u300cDUM\u300d\u300cSLF\u30e2\u30c7\u30eb\u300d\u306a\u3069\uff09\u306f\u53b3\u5bc6\u306b\u4fdd\u6301\u3057\u3001\u72ec\u81ea\u306e\u4e0d\u9069\u5207\u306a\u8a33\u8a9e\u306b\u5909\u66f4\u3057\u306a\u3044\u3067\u304f\u3060\u3055\u3044\u3002\\n3. \u610f\u5473\u306e\u6b63\u78ba\u306a\u4fdd\u6301\uff1a\u539f\u6587\u306e\u610f\u56f3\u3084\u5b66\u8853\u7684\u610f\u5473\u3092100%\u6b63\u78ba\u306b\u4fdd\u6301\u3057\u3001\u81ea\u7136\u3067\u53b3\u5bc6\u306a\u5b66\u8853\u7684\u65e5\u672c\u8a9e\u306b\u7ffb\u8a33\u3057\u3066\u304f\u3060\u3055\u3044\u3002\\n4. \u51fa\u529b\u9650\u5b9a\uff1a\u89e3\u8aac\u3001\u6ce8\u91c8\u3001\u6328\u62f6\u306a\u3069\u306e\u4f59\u8a08\u306a\u8a00\u8449\u306f\u4e00\u5207\u8ffd\u52a0\u305b\u305a\u3001\u6700\u7d42\u7684\u306a\u7ffb\u8a33\u7d50\u679c\u306e\u307f\u3092\u51fa\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\" }, { \"role\": \"user\", \"content\": \"\u4ee5\u4e0b\u306e\u30c6\u30ad\u30b9\u30c8\u3092\u65e5\u672c\u8a9e\u306b\u6b63\u78ba\u306b\u7ffb\u8a33\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u56fa\u6709\u540d\u8a5e\u3001\u5730\u540d\u3001\u4eba\u540d\u3001\u304a\u3088\u3073\u5c02\u9580\u7528\u8a9e\u3092\u53b3\u5bc6\u306b\u4fdd\u6301\u3057\u3001\u4f59\u8a08\u306a\u8a00\u8449\u3092\u4e00\u5207\u8ffd\u52a0\u305b\u305a\u3001\u7ffb\u8a33\u7d50\u679c\u306e\u307f\u3092\u51fa\u529b\u3057\u3066\u304f\u3060\u3055\u3044\uff1a\\n\" + ($json.choices?.[0]?.message?.content ?? $json.chatInput ?? $json.text ?? $json.input) } ] }) }}",
        "options": {}
      },
      "id": "0ba4408f-bed1-4d7e-82f4-96718dd2a0b3",
      "name": "Chinese to Japanaese",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        320,
        320
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "jsCode": "for (const item of $input.all()) {\n  if (item.binary) {\n    for (const key of Object.keys(item.binary)) {\n      item.binary[key].mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';\n    }\n  }\n}\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -736,
        -320
      ],
      "id": "5452723f-3980-4779-b3a5-e9b79ccb7814",
      "name": "Fix MIME Type"
    },
    {
      "parameters": {
        "outputFormat": "txt"
      },
      "type": "n8n-nodes-docx-extractor.docxExtractor",
      "typeVersion": 1,
      "position": [
        -528,
        -320
      ],
      "id": "ac22ed38-e65b-4ecf-934d-29950cf9a216",
      "name": "Docx Extractor",
      "retryOnFail": true
    },
    {
      "parameters": {
        "content": "Fix MIME Type (Code Node): Normalizes incoming binary data by setting its file type to standard Word .docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document). This prevents upload errors caused by generic file headers.\nDocx Extractor: Parses the validated .docx binary file and extracts all plain text content into a clean string ($json.text) ready for the translation and humanizing nodes.",
        "height": 336,
        "width": 624,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        -496
      ],
      "typeVersion": 1,
      "id": "66c243df-3270-4c67-983d-53ae9b145626",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -64,
        -320
      ],
      "id": "baec4631-9b79-4db0-90a5-ffbaefa4f81d",
      "name": "Extract PDF/Docx Text"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "tear-and-stitch-pipeline",
        "options": {
          "rawBody": true
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -240,
        -320
      ],
      "id": "31ea1231-b434-470f-abd7-c639b3cde4dd",
      "name": "1. Upload File2"
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 1. TEAR NODE: CLEAN & SPLIT PARAGRAPHS \u2500\u2500\u2500\nconst rawText = $input.first().json.text || $input.first().json.body?.text || \"\";\n\n// A. Strip Turnitin Headers, Footers, and Cover Pages\nlet cleanedText = rawText\n  .replace(/Page \\d+ of \\d+ - AI Writing [^\\n]+/gi, \"\")\n  .replace(/Submission ID trn:oid:::[^\\n]+/gi, \"\")\n  .replace(/100% detected as AI[^\\n]+/gi, \"\")\n  .replace(/Caution: Review required\\.[^\\n]+/gi, \"\");\n\n// B. Split into distinct, indexed paragraphs\nconst paragraphs = cleanedText\n  .split(/\\n\\s*\\n/)\n  .map(p => p.trim())\n  .filter(p => p.length > 20);\n\n// C. Output array of indexed items\nreturn paragraphs.map((para, index) => ({\n  json: {\n    paragraph_id: index,\n    original_paragraph: para\n  }\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        112,
        -320
      ],
      "id": "d6f787a4-b8fb-46b1-af16-3a86ef06deb5",
      "name": "2. TEAR (Clean & Split)2"
    },
    {
      "parameters": {
        "content": "We are processing the PDF sent by Turnitin. Our goal is to process only the text that is highlighted by Turnitin as AI-generated to reduce token consumption. We will break the PDF into chunks, and later we can join them back together.\n",
        "height": 272,
        "width": 560,
        "color": "#FFFFFF"
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        -496
      ],
      "typeVersion": 1,
      "id": "151f8152-14c5-44c0-9c8e-7c37a8af5b75",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# Loop over input items and add a new field called 'my_new_field' to the JSON of each one\nfor item in _items:\n  item[\"json\"][\"my_new_field\"] = 1\nreturn _items"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        304,
        -320
      ],
      "id": "11557f23-cb13-4571-a321-3d689454c530",
      "name": "Code in Python"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "turnitin-cyan-humanizer",
        "options": {
          "rawBody": true
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        1136,
        -880
      ],
      "id": "83600022-63ca-41b5-bac4-947eccaf5174",
      "name": "Webhook Upload (Turnitin PDF)"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# \u2500\u2500\u2500 STEP 1 & 2: PyMuPDF CYAN EXTRACTOR & CHUNK INDEXER \u2500\u2500\u2500\nimport fitz # PyMuPDF\nimport base64\n\n# A. Get binary PDF data from n8n input\ninput_data = _input.first()\nbinary_item = input_data.binary.get(\"data\")\nif not binary_item:\n    raise Exception(\"No binary PDF data found under key 'data'\")\n\npdf_bytes = base64.b64decode(binary_item.get(\"data\"))\ndoc = fitz.open(stream=pdf_bytes, filetype=\"pdf\")\n\ncyan_chunks = []\nchunk_id = 0\n\n# B. Iterate Pages 3+ (Skip Cover Page 1 & 2)\nfor page_idx in range(2, len(doc)):\n    page = doc[page_idx]\n    page_num = page_idx + 1\n    \n    # Scan for Highlight Annotations (Type 8)\n    for annot in page.annots():\n        if annot.type[0] == 8: # Highlight\n            # Extract text inside Cyan highlight rectangle\n            extracted_text = page.get_text(\"text\", clip=annot.rect).strip()\n            clean_text = \" \".join(extracted_text.split())\n            \n            if len(clean_text) > 15:\n                cyan_chunks.append({\n                    \"json\": {\n                        \"chunk_id\": chunk_id,\n                        \"page_number\": page_num,\n                        \"cyan_text\": clean_text\n                    }\n                })\n                chunk_id += 1\n\n# Fallback if no cyan highlights detected: extract raw text from page 3+\nif len(cyan_chunks) == 0:\n    raw_text = \"\"\n    for page_idx in range(2, len(doc)):\n        raw_text += doc[page_idx].get_text(\"text\") + \"\\n\\n\"\n    \n    paragraphs = [p.strip() for p in raw_text.split(\"\\n\\n\") if len(p.strip()) > 20]\n    for idx, p in enumerate(paragraphs):\n        cyan_chunks.append({\n            \"json\": {\n                \"chunk_id\": idx,\n                \"page_number\": 3,\n                \"cyan_text\": p\n            }\n        })\n\nreturn cyan_chunks"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1408,
        -880
      ],
      "id": "96764b02-1087-4675-b509-d863f8271505",
      "name": "Step 1 & 2: PyMuPDF Cyan Extractor"
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 STEP 4: REASSEMBLE (STITCHER) \u2500\u2500\u2500\nconst items = $input.all();\n\n// A. Preserve original order by sorting chunk_id\nitems.sort((a, b) => (a.json.chunk_id || 0) - (b.json.chunk_id || 0));\n\n// B. Extract humanized text (or fallback to original cyan text)\nconst cleanChunks = items.map(item => {\n  const rewritten = item.json.choices?.[0]?.message?.content;\n  return rewritten ? rewritten.trim() : item.json.cyan_text;\n});\n\n// C. Join rewritten chunks into final document\nconst finalDocument = cleanChunks.join(\"\\n\\n\");\n\nreturn [{\n  json: {\n    status: \"success\",\n    total_chunks_rewritten: cleanChunks.length,\n    humanized_output_document: finalDocument\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        -880
      ],
      "id": "2d646798-4068-4ebf-b45c-16611c2e266c",
      "name": "Step 4: Reassemble (Stitcher)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "incremental-humanizer",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1792,
        1152
      ],
      "id": "ac124518-e162-4014-b02b-2857f2597135",
      "name": "1. Upload PDF2"
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -1600,
        1152
      ],
      "id": "1a9e9890-42dc-4804-9593-bd23aa232b2e",
      "name": "Extract PDF/Docx Text5"
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 2. EXTRACT CHUNKS STARTING STRICTLY FROM CHUNK 4 ONWARDS \u2500\u2500\u2500\nconst inputJson = $input.first()?.json || {};\nlet rawText = inputJson.text || inputJson.data || inputJson.body?.text || \"\";\n\nif (typeof rawText !== \"string\") {\n  rawText = JSON.stringify(rawText);\n}\n\n// 1. Line-by-line filter to strip Turnitin headers, footers, and cover pages\nconst lines = rawText.split(/\\r?\\n/);\nconst cleanLines = lines.filter(line => {\n  const l = line.trim();\n  if (!l) return false;\n  if (l.includes(\"Quick Submit\") || l.includes(\"100% detected as AI\") || l.includes(\"Caution: Review required\")) return false;\n  if (l.includes(\"Page 1 of\") || l.includes(\"Page 2 of\")) return false;\n  if (l.includes(\"trn:oid::\")) return false;\n  if (l.includes(\"Document Details\") || l.includes(\"Submission ID\") || l.includes(\"Download Date\") || l.includes(\"Research paper\")) return false;\n  return true;\n});\n\n// 2. Group clean lines into paragraph chunks (~250 characters each)\nconst chunks = [];\nlet currentChunk = [];\n\nfor (const line of cleanLines) {\n  currentChunk.push(line);\n  if (currentChunk.join(\" \").length > 250) {\n    chunks.push(currentChunk.join(\"\\n\"));\n    currentChunk = [];\n  }\n}\nif (currentChunk.length > 0) {\n  chunks.push(currentChunk.join(\"\\n\"));\n}\n\n// 3. Filter out cover page noise\nconst researchChunks = chunks.filter(para => {\n  if (para.includes(\"Quick Submit\") || para.includes(\"100% detected as AI\") || para.includes(\"Caution: Review required\")) return false;\n  return true;\n});\n\n// 4. Start strictly from Chunk index 4 onwards (skipping first 4 chunks)\nconst selectedChunks = researchChunks.length > 4 ? researchChunks.slice(4) : researchChunks;\n\nreturn selectedChunks.map((para, index) => ({\n  json: {\n    chunk_id: index + 4, // Assigns chunk_id starting at 4\n    original_paragraph: para,\n    cyan_text: para\n  }\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1408,
        1152
      ],
      "id": "3a721dc6-503b-4679-8598-3c1703f2de43",
      "name": "2. Extract Chunks"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1168,
        1152
      ],
      "id": "fc998dc0-ae33-4d02-914a-7646c6461f16",
      "name": "3. Loop Chunks (1-by-1)"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"\u4f60\u662f\u4e00\u4e2a\u4e25\u8c28\u7684\u5b66\u672f\u8bba\u6587\u7ffb\u8bd1\u4e0e\u6821\u5bf9\u4e13\u5bb6\u3002\u7ffb\u8bd1\u65f6\u5fc5\u987b\u4e25\u683c\u9075\u5b88\u4ee5\u4e0b\u89c4\u5219\uff1a\\n1. \u56fa\u6709\u540d\u8bcd\u4e0e\u5730\u540d\u4eba\u540d\uff1a\u4eba\u540d\u3001\u4f5c\u8005\u540d\u3001\u7279\u5b9a\u5730\u57df/\u57ce\u5e02\u540d\u3001\u5927\u5b66\u53ca\u673a\u6784\u540d\u79f0\u7b49\u56fa\u6709\u540d\u8bcd\u5207\u52ff\u76f4\u8bd1\u4e3a\u5b57\u9762\u610f\u601d\uff0c\u5fc5\u987b\u4e25\u683c\u4fdd\u6301\u6807\u51c6\u8bd1\u540d\u6216\u4fdd\u7559\u539f\u540d\u3002\\n2. \u4e13\u4e1a\u672f\u8bed\u4e0e\u6807\u51c6\u8bcd\u6c47\uff1a\u4e13\u4e1a\u672f\u8bed\u3001\u7edf\u8ba1\u6307\u6807\u548c\u6a21\u578b\u540d\u79f0\u5fc5\u987b\u4e25\u683c\u4fdd\u6301\u4e00\u81f4\uff0c\u4e0d\u5f97\u968f\u610f\u66f4\u6539\u6216\u4f7f\u7528\u4e0d\u6070\u5f53\u7684\u8bd1\u540d\u3002\\n3. \u8bed\u4e49\u51c6\u786e\u6027\uff1a\u5728\u4e25\u8c28\u4fdd\u6301\u539f\u6587\u8bed\u4e49\u548c\u5b66\u672f\u7acb\u610f\u7684\u57fa\u7840\u4e0a\uff0c\u7ffb\u8bd1\u4e3a\u901a\u987a\u89c4\u8303\u7684\u5b66\u672f\u4e2d\u6587\u3002\\n4. \u4ec5\u8f93\u51fa\u7ed3\u679c\uff1a\u4e25\u7981\u6dfb\u52a0\u4efb\u4f55\u591a\u4f59\u5b57\u53e5\u3001\u89e3\u91ca\u3001\u6ce8\u91ca\u6216\u4fee\u9970\uff0c\u53ea\u8f93\u51fa\u6700\u7ec8\u7ffb\u8bd1\u7ed3\u679c\u3002\" }, { \"role\": \"user\", \"content\": \"\u8bf7\u5c06\u4ee5\u4e0b\u5b66\u672f\u8bba\u6587\u5185\u5bb9\u7cbe\u786e\u7ffb\u8bd1\u4e3a\u4e2d\u6587\uff1a\\n\" + ($json.original_paragraph ?? $json.cyan_text ?? \"\") } ] }) }}",
        "options": {}
      },
      "id": "0315fb1c-ae11-4f9c-993e-9d9ad7112367",
      "name": "Step 1: Chinese Rewrite (Gemini)4",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -896,
        1136
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"\u3042\u306a\u305f\u306f\u53b3\u5bc6\u306a\u5b66\u8853\u8ad6\u6587\u306e\u7ffb\u8a33\u304a\u3088\u3073\u6821\u6b63\u306e\u5c02\u9580\u5bb6\u3067\u3059\u3002\u4ee5\u4e0b\u306e\u30eb\u30fc\u30eb\u3092\u53b3\u683c\u306b\u9075\u5b88\u3057\u3066\u304f\u3060\u3055\u3044\uff1a\\n1. \u56fa\u6709\u540d\u8a5e\u30fb\u5730\u540d\u30fb\u4eba\u540d\uff1a\u4eba\u540d\u3001\u8457\u8005\u540d\u3001\u7279\u5b9a\u306e\u5730\u57df\u540d\u30fb\u90fd\u5e02\u540d\u3001\u5927\u5b66\u30fb\u6a5f\u95a2\u540d\u306a\u3069\u306e\u56fa\u6709\u540d\u8a5e\u306f\u76f4\u8a33\u305b\u305a\u3001\u6a19\u6e96\u7684\u306a\u8868\u8a18\u3092\u53b3\u5bc6\u306b\u7dad\u6301\u3057\u3066\u304f\u3060\u3055\u3044\u3002\\n2. \u5c02\u9580\u7528\u8a9e\u30fb\u6a19\u6e96\u7528\u8a9e\uff1a\u5c02\u9580\u7528\u8a9e\u3001\u7d71\u8a08\u6307\u6a19\u3001\u30e2\u30c7\u30eb\u540d\u306f\u53b3\u5bc6\u306b\u4fdd\u6301\u3057\u3066\u304f\u3060\u3055\u3044\u3002\\n3. \u610f\u5473\u306e\u6b63\u78ba\u306a\u4fdd\u6301\uff1a\u539f\u6587\u306e\u610f\u56f3\u3084\u5b66\u8853\u7684\u610f\u5473\u3092100%\u6b63\u78ba\u306b\u4fdd\u6301\u3057\u3001\u81ea\u7136\u3067\u53b3\u5bc6\u306a\u5b66\u8853\u7684\u65e5\u672c\u8a9e\u306b\u7ffb\u8a33\u3057\u3066\u304f\u3060\u3055\u3044\u3002\\n4. \u51fa\u529b\u9650\u5b9a\uff1a\u6700\u7d42\u7684\u306a\u7ffb\u8a33\u7d50\u679c\u306e\u307f\u3092\u51fa\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\" }, { \"role\": \"user\", \"content\": \"\u4ee5\u4e0b\u306e\u30c6\u30ad\u30b9\u30c8\u3092\u65e5\u672c\u8a9e\u306b\u6b63\u78ba\u306b\u7ffb\u8a33\u3057\u3066\u304f\u3060\u3055\u3044\uff1a\\n\" + ($json.choices?.[0]?.message?.content ?? \"\") } ] }) }}",
        "options": {}
      },
      "id": "519e12e9-6eb5-4f05-b1cd-a36827ff89ee",
      "name": "Chinese to Japanaese6",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -704,
        1136
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_GEMINI_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"model\": \"gemini-2.5-flash\", \"temperature\": 0.1, \"messages\": [ { \"role\": \"system\", \"content\": \"Du bist ein hochpr\u00e4ziser akademischer \u00dcbersetzer und Lektor. Bitte befolge strikt die folgenden Regeln:\\n1. EIGENNAMEN, ORTE & PERSONEN: \u00dcbersetze NIEMALS Eigennamen, Autorennamen oder Ortsnamen. Behalte die exakte Schreibweise bei.\\n2. FACHBEGRIFFE & MODELLE: Behalte Fachbegriffe, statistische Kennzahlen und Modellbezeichnungen strikt bei.\\n3. EXAKTE BEDEUTUNG: \u00dcbersetze den Text pr\u00e4zise in nat\u00fcrliches, akademisches Deutsch ohne Bedeutungs\u00e4nderung.\\n4. NUR AUSGABE: Gib ausschlie\u00dflich das finale \u00dcbersetzungsergebnis aus.\" }, { \"role\": \"user\", \"content\": \"Bitte \u00fcbersetze den folgenden akademischen Text pr\u00e4zise ins Deutsche\uff1a\\n\" + ($json.choices?.[0]?.message?.content ?? \"\") } ] }) }}",
        "options": {}
      },
      "id": "f3973741-d026-421d-b581-0d5234662cbc",
      "name": "Step3: Japanese to German language6",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -496,
        1136
      ],
      "typeVersion": 4
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api-free.deepl.com/v2/translate",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "DeepL-Auth-Key YOUR_DEEPL_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"text\": [ $json.choices?.[0]?.message?.content ?? \"\" ], \"target_lang\": \"ES\" } }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -304,
        1136
      ],
      "id": "6aa0fa4f-de68-41ae-a340-df4fc1fbad6b",
      "name": "Translate to DE4"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api-free.deepl.com/v2/translate",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "DeepL-Auth-Key YOUR_DEEPL_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"text\": [ $json.translations?.[0]?.text ?? $json.choices?.[0]?.message?.content ?? \"\" ], \"target_lang\": \"EN\" } }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -96,
        1136
      ],
      "id": "5b407e64-0a1d-4d99-90cc-7bf2a5f1989c",
      "name": "Translate to EN7"
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 5. SAVE & APPEND CHUNK (MEMORY STORE) \u2500\u2500\u2500\nconst currentChunk = $input.first().json;\nconst chunkId = currentChunk.chunk_id ?? 0;\n\n// Get final English translation output from DeepL or Gemini\nconst cleanText = (currentChunk.translations?.[0]?.text || currentChunk.choices?.[0]?.message?.content || currentChunk.original_paragraph || \"\").trim();\n\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.processedChunks) {\n  staticData.processedChunks = {};\n}\n\n// Append chunk to memory\nstaticData.processedChunks[chunkId] = cleanText;\n\nreturn [{\n  json: {\n    status: \"saved\",\n    saved_chunk_id: chunkId,\n    total_saved_so_far: Object.keys(staticData.processedChunks).length,\n    saved_text: cleanText\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        144,
        1136
      ],
      "id": "f436e4c3-8401-4e07-928d-64a23ce387ff",
      "name": "5. Save & Append Chunk"
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 6. COMBINE ALL SAVED CHUNKS IN ORDER \u2500\u2500\u2500\nconst staticData = $getWorkflowStaticData('global');\nconst processed = staticData.processedChunks || {};\n\nconst orderedIds = Object.keys(processed)\n  .map(id => parseInt(id, 10))\n  .sort((a, b) => a - b);\n\nconst finalText = orderedIds.map(id => processed[id]).join(\"\\n\\n\");\n\n// Reset memory for next run\nstaticData.processedChunks = {};\n\nreturn [{\n  json: {\n    status: \"complete\",\n    total_chunks: orderedIds.length,\n    final_text: finalText\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1168,
        944
      ],
      "id": "a9a2941e-39e1-4498-af3e-1f54cada2216",
      "name": "6. Combine Final Text"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { status: $json.status, total_chunks: $json.total_chunks, final_text: $json.final_text } }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -896,
        944
      ],
      "id": "e6cf9a76-6e59-475a-bcb9-185bff6849fd",
      "name": "7. Respond to Webhook"
    }
  ],
  "connections": {
    "Translate to EN": {
      "main": [
        []
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Step 1: Chinese Rewrite (Gemini)2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 1: Chinese Rewrite (Gemini)2": {
      "main": [
        [
          {
            "node": "Chinese to Japanaese",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate to DE1": {
      "main": [
        [
          {
            "node": "Translate to EN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step3: Japanese to German language": {
      "main": [
        [
          {
            "node": "Translate to DE1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chinese to Japanaese": {
      "main": [
        [
          {
            "node": "Step3: Japanese to German language",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fix MIME Type": {
      "main": [
        [
          {
            "node": "Docx Extractor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF/Docx Text": {
      "main": [
        [
          {
            "node": "2. TEAR (Clean & Split)2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Upload File2": {
      "main": [
        [
          {
            "node": "Extract PDF/Docx Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. TEAR (Clean & Split)2": {
      "main": [
        [
          {
            "node": "Code in Python",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Upload (Turnitin PDF)": {
      "main": [
        [
          {
            "node": "Step 1 & 2: PyMuPDF Cyan Extractor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 1 & 2: PyMuPDF Cyan Extractor": {
      "main": [
        []
      ]
    },
    "1. Upload PDF2": {
      "main": [
        [
          {
            "node": "Extract PDF/Docx Text5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF/Docx Text5": {
      "main": [
        [
          {
            "node": "2. Extract Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Extract Chunks": {
      "main": [
        [
          {
            "node": "3. Loop Chunks (1-by-1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Loop Chunks (1-by-1)": {
      "main": [
        [
          {
            "node": "6. Combine Final Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Step 1: Chinese Rewrite (Gemini)4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 1: Chinese Rewrite (Gemini)4": {
      "main": [
        [
          {
            "node": "Chinese to Japanaese6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chinese to Japanaese6": {
      "main": [
        [
          {
            "node": "Step3: Japanese to German language6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step3: Japanese to German language6": {
      "main": [
        [
          {
            "node": "Translate to DE4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate to DE4": {
      "main": [
        [
          {
            "node": "Translate to EN7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate to EN7": {
      "main": [
        [
          {
            "node": "5. Save & Append Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Save & Append Chunk": {
      "main": [
        [
          {
            "node": "3. Loop Chunks (1-by-1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Combine Final Text": {
      "main": [
        [
          {
            "node": "7. Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}