n8n_3_CAD-BIM-batch-converter-pipeline

Avancé

Ceci est unDocument Extraction, Multimodal AIworkflow d'automatisation du domainecontenant 82 nœuds.Utilise principalement des nœuds comme If, Set, Code, Merge, ManualTrigger. Conversion groupée de fichiers CAD/BIM en XLSX/DAE, avec validation et rapports

Prérequis
  • Clé API OpenAI
  • Clé API Anthropic
  • Clé API Google Gemini
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "id": "u5MdPlYjhriENe3R",
  "meta": {
    "instanceId": "faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"
  },
  "name": "n8n_3_CAD-BIM-Batch-Converter-Pipeline",
  "tags": [],
  "nodes": [
    {
      "id": "be5c0de2-0df1-4c35-8b0e-26f8d1911bda",
      "name": "Note adhésive23",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        224
      ],
      "parameters": {
        "width": 376,
        "height": 368,
        "content": "## 📋 Quick Start Guide\n\n**1️⃣ Configure Settings**\nEdit \"Set Configuration Parameters\":\n- `converter_path`: Path to RvtExporter.exe\n- `source_folder`: Your CAD files location\n- `output_folder`: Where to save results\n- `file_extension`: .rvt, .ifc, .dwg, or .dgn\n\n**2️⃣ Run Pipeline**\nClick \"Execute Workflow\"\n\n**3️⃣ Get Results**\n✅ Excel data files\n✅ 3D DAE models\n✅ HTML report"
      },
      "typeVersion": 1
    },
    {
      "id": "d8a6fce0-af86-4769-8911-afb6c665f38d",
      "name": "Note adhésive25",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        608
      ],
      "parameters": {
        "color": 2,
        "width": 380,
        "height": 224,
        "content": "## 🎓 Video Tutorials\n\n**📹 [CAD-BIM Pipeline Tutorial](https://www.youtube.com/watch?v=PMTZNRFjD6c)**\nComplete walkthrough of CAD-BIM automation\n\n**⚡ [Automated Validation](https://www.youtube.com/watch?v=p84AmP2dcvg)**\nStop manual BIM checking forever\n\n💡 **Pro tip:** Watch before first run!"
      },
      "typeVersion": 1
    },
    {
      "id": "1182ee14-cf89-45ee-96dc-d96a6864dcfa",
      "name": "Note adhésive28",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        1168
      ],
      "parameters": {
        "color": 2,
        "width": 372,
        "height": 244,
        "content": "## 🆘 Support\n\n**Resources:**\n🌐 [DataDrivenConstruction.io](https://datadrivenconstruction.io)\n📧 support@datadrivenconstruction.io\n💬 [Community Forum](https://t.me/datadrivenconstruction)\n\n**Documentation:**\n📚 [n8n Docs](https://docs.n8n.io)\n🎥 [YouTube Channel](https://youtube.com/@datadrivenconstruction)"
      },
      "typeVersion": 1
    },
    {
      "id": "8bd00e43-cb87-489a-87b6-a6258262a5bf",
      "name": "Note adhésive29",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        848
      ],
      "parameters": {
        "color": 2,
        "width": 380,
        "height": 304,
        "content": "## 🎯 Use Cases\n\n**Weekly Reports**\nAutomate Monday conversions\n\n**Quality Control**\nValidate before client delivery\n\n**Archive Projects**\nConvert old files to lightweight formats\n\n**BI Integration**\nFeed Excel data to Power BI"
      },
      "typeVersion": 1
    },
    {
      "id": "3ccefd7c-ade0-4125-952c-928115a49ebf",
      "name": "Note adhésive31",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        112
      ],
      "parameters": {
        "width": 372,
        "height": 100,
        "content": "⭐ **If you find our tools helpful**, please consider starring our repository on [GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto). **Your support helps us** improve and continue developing open solutions for the community!\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c5388249-6dc3-4070-9057-2b5668251832",
      "name": "Note adhésive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -688,
        112
      ],
      "parameters": {
        "width": 520,
        "height": 1940,
        "content": "## 📋 Options Parameter Configuration Guide\n\n## 🎯 Overview\nThe Options parameter allows you to customize the export process with various flags and settings. Multiple options can be combined using spaces.\n\n## 🚀 Available Options\n\n### 📦 Export Modes\n| Mode   | Description |\n|------|-------------|\n| `basic`   |  Minimal export with essential data only |\n| `standard`   | Default export level with standard properties |\n| `complete`   | Full export including all available data |\n| `custom`   | Custom export using category file specification |\n\n### 🛠️ Feature Flags\n| Option | Description |\n|--------|-------------|\n| `bbox` | Add BoundingBox geometry of each element in XLSX |\n| `schedule` | Export all Schedules |\n| `sheets2pdf` | Export all Sheets to PDF format |\n| `[<output file>]` | Specify custom output file path |\n| `[<category file>]` | Text file with category names (required for custom mode) |\n\n### 🚫 Disable Options\n| Option | Description |\n|--------|-------------|\n| `-no-xlsx` | Disable export to .xlsx format |\n| `-no-collada` | Disable export to .dae format |\n\n---\n\n## 💡 Usage Examples\n\n### Example 1: **Basic Export with BoundingBox**\n```bash\nOptions: bbox basic\n```\n> Exports basic data with BoundingBox geometry included\n\n### Example 2: **Complete Export with Schedules**\n```bash\nOptions: complete schedule sheets2pdf\n```\n> Full data export including all schedules and sheets converted to PDF\n\n### Example 3: **Custom Mode with Category File**\n```bash\nOptions: custom categories.txt bbox\n```\n> Custom export using categories.txt file with BoundingBox geometry\n\n### Example 4: **Export Without Specific Formats**\n```bash\nOptions: standard -no-xlsx -no-collada\n```\n> Standard export excluding XLSX and Collada formats\n\n### Example 5: **Schedules Only Export**\n```bash\nOptions: schedule -no-collada\n```\n> Export only schedules without Collada format\n\n### Example 6: **Custom Output Path**\n```bash\nOptions: C:\\Output\\result.xlsx complete bbox\n```\n> Complete export with BoundingBox to specific output file\n\n---\n\n## ⚡ Quick Tips\n- **Combine multiple options** by separating them with spaces\n- **Order matters** - export mode should come before other options\n- **Custom mode** requires a category file to function properly\n- **Disable flags** can be used to exclude unwanted formats\n\n---\n\n## 📝 Notes\n- All options are case-sensitive\n- Invalid combinations will be ignored\n- Default behavior applies when no options are specified"
      },
      "typeVersion": 1
    },
    {
      "id": "2fe7360d-df50-4eb7-8799-441e7aed7a1a",
      "name": "Déclencheur manuel",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -80,
        704
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "614d8fe1-6c80-4cb0-b941-5aa3998a2321",
      "name": "Capturer l'heure de début du pipeline1",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        656
      ],
      "parameters": {
        "jsCode": "// Capture pipeline start time at the very beginning\nconst now = new Date();\nreturn [{\n  json: {\n    pipeline_start_time: now.toISOString(),\n    pipeline_start_timestamp: now.getTime(),\n    pipeline_start_message: `Pipeline started at ${now.toLocaleString()}`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e64288d1-9d53-4036-afde-437c9892a927",
      "name": "Fusionner le début du pipeline avec la configuration1",
      "type": "n8n-nodes-base.merge",
      "position": [
        688,
        672
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "7b214d29-a619-4a41-8cfa-67f074470b89",
      "name": "Définir les paramètres de configuration1",
      "type": "n8n-nodes-base.set",
      "position": [
        416,
        512
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "converter-path",
              "name": "converter_path",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\DDC_Converter_Revit\\datadrivenlibs\\RvtExporter.exe"
            },
            {
              "id": "source-folder",
              "name": "source_folder",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
            },
            {
              "id": "output-folder",
              "name": "output_folder",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
            },
            {
              "id": "include-subfolders",
              "name": "include_subfolders",
              "type": "boolean",
              "value": false
            },
            {
              "id": "file-extension",
              "name": "file_extension",
              "type": "string",
              "value": ".rvt"
            },
            {
              "id": "a811f4ba-b7a5-4774-a1fa-90d9c43a0de6",
              "name": "options",
              "type": "string",
              "value": "basic schedule  -no-collada"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "53648934-0bb7-49d3-83f9-ab4280bb3ec9",
      "name": "Rechercher les fichiers CAD1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        848,
        688
      ],
      "parameters": {
        "command": "=powershell -Command \"Get-ChildItem -LiteralPath '{{ $json.source_folder }}' -Filter '*{{ $json.file_extension }}' {{ $json.include_subfolders ? '-Recurse' : '' }} | Select-Object -ExpandProperty FullName\""
      },
      "typeVersion": 1
    },
    {
      "id": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
      "name": "Fusionner la configuration avec les résultats de recherche1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1008,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "cc6f17d1-4393-4c77-947a-53c42c01543c",
      "name": "Traiter la liste de fichiers1",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        576
      ],
      "parameters": {
        "jsCode": "// Process file list with configuration and pipeline start time\nconst configData = $input.first().json || {};\nconst searchData = $input.last().json || {};\n\nconst output = searchData.stdout || '';\nconst stderr = searchData.stderr || '';\n\n// Preserve pipeline start time\nconst pipeline_start_time = configData.pipeline_start_time || '';\nconst pipeline_start_timestamp = configData.pipeline_start_timestamp || Date.now();\n\nconsole.log('=== Processing File List ===');\nconsole.log('Pipeline start time:', pipeline_start_time);\nconsole.log('Config data:', configData);\nconsole.log('Raw output:', output);\nconsole.log('Files found:', output ? output.split(/\\r?\\n/).filter(x => x.trim()).length : 0);\n\nconst config = {\n  converter_path: configData.converter_path || '',\n  source_folder: configData.source_folder || '',\n  output_folder: configData.output_folder || '',\n  include_subfolders: configData.include_subfolders || false,\n  file_extension: configData.file_extension || '.rvt',\n  options: configData.options || ''\n};\n\nif (!output || !output.trim()) {\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `No files found with extension '${config.file_extension}' in ${config.source_folder}`,\n      error: stderr || 'No output from search command',\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nlet normalizedExtension = (config.file_extension || '').trim();\nif (normalizedExtension && !normalizedExtension.startsWith('.')) {\n  normalizedExtension = '.' + normalizedExtension;\n}\n\nlet rawPaths = [];\ntry {\n  rawPaths = output.trim().split(/\\r?\\n/)\n    .map(path => (path || '').trim())\n    .filter(path => path && path.length > 0);\n} catch (error) {\n  console.log('Error splitting output:', error);\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `Error processing search results: ${error.message}`,\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nconst filePaths = rawPaths.filter(path => {\n  if (!path || !normalizedExtension) return false;\n  const lowerPath = path.toLowerCase();\n  const lowerExt = normalizedExtension.toLowerCase();\n  return lowerPath.endsWith(lowerExt);\n});\n\nif (filePaths.length === 0) {\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `No files found with extension '${normalizedExtension}' in ${config.source_folder}`,\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nconst files = filePaths.map((filePath, index) => {\n  let fileName = 'unknown_file';\n  let fileNameWithoutExt = 'unknown_file';\n  \n  try {\n    if (filePath && typeof filePath === 'string') {\n      const normalizedPath = filePath.replace(/\\\\/g, '/');\n      const pathParts = normalizedPath.split('/');\n      fileName = pathParts[pathParts.length - 1] || 'unknown_file';\n      \n      console.log(`Processing file ${index + 1}: ${fileName} from path: ${filePath}`);\n      \n      const lastDotIndex = fileName.lastIndexOf('.');\n      if (lastDotIndex > 0) {\n        fileNameWithoutExt = fileName.substring(0, lastDotIndex);\n      } else {\n        fileNameWithoutExt = fileName;\n      }\n    }\n  } catch (error) {\n    console.log(`Error parsing file path ${filePath}:`, error);\n  }\n  \n  const extWithoutDot = normalizedExtension ? normalizedExtension.substring(1) : 'unknown';\n  \n  return {\n    index: index + 1,\n    file_path: filePath,\n    file_name: fileName,\n    file_name_without_ext: fileNameWithoutExt,\n    converter_path: config.converter_path,\n    expected_output: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.xlsx`,\n    expected_output_dae: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.dae`,\n    config: config,\n    options: config.options,\n    pipeline_start_time: pipeline_start_time,\n    pipeline_start_timestamp: pipeline_start_timestamp\n  };\n});\n\nconsole.log('Processed files:', files.map(f => f.file_name));\n\nreturn [{\n  json: {\n    files: files,\n    total_files: files.length,\n    message: `Found ${files.length} files to convert`,\n    config: config,\n    pipeline_start_time: pipeline_start_time,\n    pipeline_start_timestamp: pipeline_start_timestamp\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "357d21f1-ff0b-4308-99a7-8ab3140b6dde",
      "name": "Vérifier l'existence des fichiers1",
      "type": "n8n-nodes-base.if",
      "position": [
        1328,
        576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "files-exist-condition",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.files && $json.files.length || 0 }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea",
      "name": "Diviser les fichiers pour traitement1",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1520,
        560
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "files"
      },
      "typeVersion": 1
    },
    {
      "id": "0330e04c-9572-439d-b09f-aa932b5e81ad",
      "name": "Créer le répertoire de sortie1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        1696,
        656
      ],
      "parameters": {
        "command": "=mkdir \"{{ $json.config.output_folder }}\" 2>nul || echo Output directory ready",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
      "name": "Fusionner les données de fichiers1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1856,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "627097e5-5d07-4386-a9d7-22dffe7ba4ab",
      "name": "Capturer l'heure de début1",
      "type": "n8n-nodes-base.code",
      "position": [
        2032,
        576
      ],
      "parameters": {
        "jsCode": "// Capture conversion start time and preserve ALL data including pipeline start\nreturn $input.all().map(item => {\n  const json = {...item.json};\n  json.conversion_start_time = new Date().toISOString();\n  json.conversion_start_timestamp = Date.now();\n  console.log(`Starting conversion for: ${json.file_name} at ${json.conversion_start_time}`);\n  console.log(`Pipeline started at: ${json.pipeline_start_time}`);\n  return {json};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "edf42bdd-67ed-46d1-a522-cbc93538d894",
      "name": "Petit délai",
      "type": "n8n-nodes-base.wait",
      "position": [
        2192,
        576
      ],
      "webhookId": "5a1cb608-ec3e-4832-a289-f6a52ef8bc7a",
      "parameters": {
        "unit": "milliseconds"
      },
      "typeVersion": 1
    },
    {
      "id": "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18",
      "name": "Exécuter la conversion1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        2352,
        688
      ],
      "parameters": {
        "command": "=\"{{ $json.converter_path }}\" \"{{ $json.file_path }}\" \"{{ $json.expected_output }}\" \"{{ $json.expected_output_dae }}\" {{ $json.options }}",
        "executeOnce": false
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "f02f9f74-9b20-4857-92c3-4b0f4103f1b1",
      "name": "Calculer le temps de conversion1",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        688
      ],
      "parameters": {
        "jsCode": "// Calculate actual conversion time and rename outputs - preserve pipeline start time\nreturn $input.all().map(item => {\n  const json = {...item.json};\n  \n  // Rename stdout/stderr\n  json.conversion_stdout = json.stdout || '';\n  json.conversion_stderr = json.stderr || '';\n  json.conversion_exitCode = json.exitCode || 0;\n  delete json.stdout;\n  delete json.stderr;\n  delete json.exitCode;\n  \n  // Calculate actual processing time with decimal precision\n  if (json.conversion_start_timestamp) {\n    const endTime = Date.now();\n    const startTime = json.conversion_start_timestamp;\n    const processingTimeMs = endTime - startTime;\n    json.processing_time = processingTimeMs / 1000;\n    json.processing_time_ms = processingTimeMs;\n    json.conversion_end_time = new Date().toISOString();\n    json.conversion_end_timestamp = endTime;\n    console.log(`Conversion completed for: ${json.file_name} in ${json.processing_time.toFixed(2)} seconds`);\n  } else {\n    json.processing_time = 0;\n    json.processing_time_ms = 0;\n    console.warn(`No start timestamp found for: ${json.file_name}`);\n  }\n  \n  // Ensure pipeline start time is preserved\n  console.log(`Pipeline start preserved: ${json.pipeline_start_timestamp}`);\n  \n  return {json};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "9255e34b-be94-4630-8887-fc7a0aafd739",
      "name": "Fusionner les données avant vérification1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2672,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "0e3dc51e-4891-4094-ad20-341424dd04df",
      "name": "Vérifier les fichiers de sortie et obtenir les tailles1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        2832,
        576
      ],
      "parameters": {
        "command": "=powershell -Command \"$xlsx='{{ $json.expected_output }}'; $dae='{{ $json.expected_output_dae }}'; $revit='{{ $json.file_path }}'; $xlsxExists=Test-Path -LiteralPath $xlsx; $daeExists=Test-Path -LiteralPath $dae; $revitExists=Test-Path -LiteralPath $revit; $xlsxSize=if($xlsxExists){(Get-Item -LiteralPath $xlsx).Length}else{0}; $daeSize=if($daeExists){(Get-Item -LiteralPath $dae).Length}else{0}; $revitSize=if($revitExists){(Get-Item -LiteralPath $revit).Length}else{0}; Write-Output ('XLSX_EXISTS:'+$xlsxExists+'|XLSX_SIZE:'+$xlsxSize+'|DAE_EXISTS:'+$daeExists+'|DAE_SIZE:'+$daeSize+'|REVIT_EXISTS:'+$revitExists+'|REVIT_SIZE:'+$revitSize)\"",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "8d038788-44e7-49be-a999-b97ac4f96fac",
      "name": "Finaliser la vérification des fichiers1",
      "type": "n8n-nodes-base.code",
      "position": [
        2992,
        576
      ],
      "parameters": {
        "jsCode": "// Complete file verification with processing time - preserve pipeline start time\nconst allInputs = $input.all();\nconst mergedDataItems = $('Merge Data Before Verification1').all();\n\nconsole.log(`=== Complete File Verification ===`);\nconsole.log(`Total verification inputs: ${allInputs.length}`);\nconsole.log(`Total merged data items: ${mergedDataItems.length}`);\n\nconst results = [];\n\n// Process each file\nallInputs.forEach((input, index) => {\n  const verificationData = input.json || {};\n  \n  // Find corresponding merged data by matching index\n  const mergedData = mergedDataItems[index]?.json || {};\n  \n  // Extract all necessary data from merged node\n  const fileName = mergedData.file_name || 'unknown_file';\n  const filePath = mergedData.file_path || '';\n  const expectedOutput = mergedData.expected_output || '';\n  const expectedOutputDae = mergedData.expected_output_dae || '';\n  const fileIndex = mergedData.index || index + 1;\n  const processingTime = mergedData.processing_time || 0;\n  const config = mergedData.config || {};\n  const conversionStartTime = mergedData.conversion_start_time || '';\n  const conversionEndTime = mergedData.conversion_end_time || '';\n  const conversionEndTimestamp = mergedData.conversion_end_timestamp || Date.now();\n  \n  // IMPORTANT: Preserve pipeline start time\n  const pipelineStartTime = mergedData.pipeline_start_time || '';\n  const pipelineStartTimestamp = mergedData.pipeline_start_timestamp || 0;\n\n  console.log(`Processing verification for file ${fileIndex}: ${fileName}`);\n  console.log('Actual processing time:', processingTime, 'seconds');\n  console.log('Pipeline start timestamp:', pipelineStartTimestamp);\n\n  // Get verification output from the current input\n  let verificationOutput = verificationData.stdout || verificationData.verification_output || '';\n\n  // Parse PowerShell output\n  let successXlsx = false;\n  let successDae = false;\n  let successRevit = false;\n  let fileSizeXlsx = 0;\n  let fileSizeDae = 0;\n  let fileSizeRevit = 0;\n\n  try {\n    if (verificationOutput && verificationOutput.includes('|')) {\n      const parts = verificationOutput.split('|');\n      \n      parts.forEach(part => {\n        if (part && part.includes('XLSX_EXISTS:')) {\n          successXlsx = part.includes('XLSX_EXISTS:True');\n        }\n        if (part && part.includes('XLSX_SIZE:')) {\n          const sizeStr = part.replace('XLSX_SIZE:', '').trim();\n          fileSizeXlsx = parseInt(sizeStr) || 0;\n        }\n        if (part && part.includes('DAE_EXISTS:')) {\n          successDae = part.includes('DAE_EXISTS:True');\n        }\n        if (part && part.includes('DAE_SIZE:')) {\n          const sizeStr = part.replace('DAE_SIZE:', '').trim();\n          fileSizeDae = parseInt(sizeStr) || 0;\n        }\n        if (part && part.includes('REVIT_EXISTS:')) {\n          successRevit = part.includes('REVIT_EXISTS:True');\n        }\n        if (part && part.includes('REVIT_SIZE:')) {\n          const sizeStr = part.replace('REVIT_SIZE:', '').trim();\n          fileSizeRevit = parseInt(sizeStr) || 0;\n        }\n      });\n    }\n  } catch (error) {\n    console.log('Error parsing verification output:', error);\n  }\n\n  const finalSuccess = successXlsx && successDae && fileSizeXlsx > 0 && fileSizeDae > 0;\n\n  const statusMessage = finalSuccess \n    ? `✅ [${fileIndex}] Successfully converted: ${fileName} (XLSX: ${Math.round(fileSizeXlsx/1024)}KB, DAE: ${Math.round(fileSizeDae/1024)}KB, ${processingTime.toFixed(1)}s)`\n    : `❌ [${fileIndex}] Failed to convert: ${fileName} (XLSX: ${successXlsx && fileSizeXlsx > 0 ? '✓' : '✗'}, DAE: ${successDae && fileSizeDae > 0 ? '✓' : '✗'}, ${processingTime.toFixed(1)}s)`;\n\n  const result = {\n    file_name: fileName,\n    file_path: filePath,\n    expected_output: expectedOutput,\n    expected_output_dae: expectedOutputDae,\n    success: finalSuccess,\n    success_xlsx: successXlsx && fileSizeXlsx > 0,\n    success_dae: successDae && fileSizeDae > 0,\n    processing_time: processingTime,\n    file_size: fileSizeXlsx + fileSizeDae,\n    file_size_xlsx: fileSizeXlsx,\n    file_size_dae: fileSizeDae,\n    file_size_revit: fileSizeRevit,\n    index: fileIndex,\n    status: finalSuccess ? 'converted' : 'failed',\n    message: statusMessage,\n    timestamp: new Date().toISOString(),\n    verification_output: verificationOutput,\n    config: config,\n    conversion_start_time: conversionStartTime,\n    conversion_end_time: conversionEndTime,\n    conversion_end_timestamp: conversionEndTimestamp,\n    pipeline_start_time: pipelineStartTime,\n    pipeline_start_timestamp: pipelineStartTimestamp\n  };\n\n  console.log(`Verification complete for ${fileName}: ${result.message}`);\n  results.push({ json: result });\n});\n\nconsole.log(`=== Returning ${results.length} verification results ===`);\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "44336609-1655-4f5b-92b4-9b6165620036",
      "name": "Générer le rapport HTML1",
      "type": "n8n-nodes-base.code",
      "position": [
        3152,
        576
      ],
      "parameters": {
        "jsCode": "// Generate HTML Report with CORRECT pipeline timing and no-collada handling\nconst results = $input.all();\n\n// Get the current time as pipeline end\nconst pipelineEndTime = Date.now();\n\n// Find the pipeline start timestamp from the data\nlet pipelineStartTimestamp = null;\nlet config = {};\n\n// Extract timing and config from results\nfor (const result of results) {\n  if (result.json) {\n    // Get pipeline start timestamp\n    if (result.json.pipeline_start_timestamp && !pipelineStartTimestamp) {\n      pipelineStartTimestamp = result.json.pipeline_start_timestamp;\n      console.log('Found pipeline start timestamp:', pipelineStartTimestamp);\n    }\n    \n    // Get config\n    if (result.json.config && Object.keys(result.json.config).length > 0) {\n      config = result.json.config;\n    }\n  }\n}\n\n// Check if no-collada option is enabled\nconst noColladaEnabled = config.options && config.options.includes('no-collada');\nconsole.log('No-collada mode enabled:', noColladaEnabled);\n\n// Calculate total pipeline time in minutes\nlet totalPipelineMinutes = '0.00';\nif (pipelineStartTimestamp) {\n  const totalMs = pipelineEndTime - pipelineStartTimestamp;\n  totalPipelineMinutes = (totalMs / 60000).toFixed(2);\n  console.log('Pipeline timing:', {\n    start: pipelineStartTimestamp,\n    end: pipelineEndTime,\n    totalMs: totalMs,\n    totalMinutes: totalPipelineMinutes\n  });\n} else {\n  console.warn('No pipeline start timestamp found!');\n}\n\n// Use config output folder or fallback\nconst outputFolder = config.output_folder || 'C:\\\\temp';\n\n// Count successful and failed conversions\nlet totalFiles = 0;\nlet successfulConversions = 0;\nlet failedConversions = 0;\nlet totalInputSize = 0;\nlet totalOutputSize = 0;\n\nconst successfulFiles = [];\nconst failedFiles = [];\n\nresults.forEach((result, index) => {\n  const data = result.json || {};\n  totalFiles++;\n  \n  const processingTime = data.processing_time || 0;\n  const xlsxSuccess = data.success_xlsx || false;\n  const daeSuccess = data.success_dae || false;\n  const fileSizeRevit = data.file_size_revit || 0;\n  const fileSizeXlsx = data.file_size_xlsx || 0;\n  const fileSizeDae = data.file_size_dae || 0;\n  \n  totalInputSize += fileSizeRevit;\n  \n  // Modified success logic for no-collada mode\n  let isSuccess = false;\n  if (noColladaEnabled) {\n    // In no-collada mode, only XLSX matters\n    isSuccess = xlsxSuccess;\n    if (isSuccess) {\n      totalOutputSize += fileSizeXlsx;\n    }\n  } else {\n    // Normal mode: both XLSX and DAE must succeed\n    isSuccess = xlsxSuccess && daeSuccess;\n    if (isSuccess) {\n      totalOutputSize += fileSizeXlsx + fileSizeDae;\n    }\n  }\n  \n  if (isSuccess) {\n    successfulConversions++;\n    successfulFiles.push({\n      name: data.file_name || `File ${index + 1}`,\n      originalSize: Math.round(fileSizeRevit / 1024),\n      xlsxSize: Math.round(fileSizeXlsx / 1024),\n      daeSize: Math.round(fileSizeDae / 1024),\n      totalSize: Math.round((fileSizeXlsx + (noColladaEnabled ? 0 : fileSizeDae)) / 1024),\n      xlsxPath: data.expected_output || '',\n      daePath: data.expected_output_dae || '',\n      noCollada: noColladaEnabled\n    });\n  } else {\n    failedConversions++;\n    failedFiles.push({\n      name: data.file_name || `File ${index + 1}`,\n      originalSize: Math.round(fileSizeRevit / 1024),\n      xlsxStatus: xlsxSuccess ? '✓' : '✗',\n      daeStatus: daeSuccess ? '✓' : '✗'\n    });\n  }\n});\n\nconst successRate = totalFiles > 0 ? Math.round((successfulConversions / totalFiles) * 100) : 0;\n\n// Generate timestamp for filename\nconst now = new Date();\nconst timestamp = now.toISOString().slice(0,19).replace(/:/g, '-');\nconst fileName = `CAD_Conversion_Report_${timestamp}.html`;\nconst fullPath = `${outputFolder}\\\\${fileName}`.replace(/\\\\\\\\/g, '\\\\');\n\nconsole.log('Report summary:', {\n  totalFiles,\n  successfulConversions,\n  failedConversions,\n  successRate: successRate + '%',\n  totalPipelineTime: totalPipelineMinutes + ' minutes',\n  outputPath: fullPath,\n  noColladaMode: noColladaEnabled\n});\n\n// Create HTML content with professional styling\nconst htmlContent = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>CAD Project Batch Conversion Report</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n        \n        body { \n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; \n            background: #f5f6fa;\n            color: #2c3e50;\n            line-height: 1.6;\n        }\n        \n        .container { \n            max-width: 1400px; \n            margin: 0 auto; \n            background: white; \n            box-shadow: 0 0 40px rgba(0, 0, 0, 0.08); \n        }\n        \n        .header { \n            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);\n            color: white; \n            padding: 60px 40px;\n            position: relative;\n            overflow: hidden;\n        }\n        \n        .header::before {\n            content: '';\n            position: absolute;\n            top: -50%;\n            right: -10%;\n            width: 40%;\n            height: 200%;\n            background: rgba(255, 255, 255, 0.05);\n            transform: rotate(35deg);\n        }\n        \n        .header-content {\n            position: relative;\n            z-index: 1;\n        }\n        \n        .header h1 { \n            font-size: 2.8rem; \n            font-weight: 300;\n            letter-spacing: -1px;\n            margin-bottom: 15px;\n        }\n        \n        .header .subtitle { \n            font-size: 1.1rem;\n            opacity: 0.85;\n            font-weight: 400;\n        }\n        \n        .header .timestamp {\n            margin-top: 20px;\n            font-size: 0.95rem;\n            opacity: 0.7;\n        }\n        \n        .content {\n            padding: 40px;\n        }\n        \n        .metrics { \n            display: grid; \n            grid-template-columns: repeat(5, 1fr); \n            gap: 20px; \n            margin: -20px 0 40px 0;\n            position: relative;\n            z-index: 10;\n        }\n        \n        @media (max-width: 1200px) {\n            .metrics {\n                grid-template-columns: repeat(3, 1fr);\n            }\n        }\n        \n        @media (max-width: 768px) {\n            .metrics {\n                grid-template-columns: repeat(2, 1fr);\n            }\n        }\n        \n        @media (max-width: 480px) {\n            .metrics {\n                grid-template-columns: 1fr;\n            }\n        }\n        \n        .metric { \n            background: white;\n            padding: 30px 20px;\n            border-radius: 12px;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n            transition: all 0.3s ease;\n            border: 1px solid #e8ecf1;\n            position: relative;\n            overflow: hidden;\n            text-align: center;\n        }\n        \n        .metric::before {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 4px;\n            background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);\n        }\n        \n        .metric:hover {\n            transform: translateY(-3px);\n            box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n        }\n        \n        .metric-value { \n            font-size: 2.2rem; \n            font-weight: 600;\n            color: #1e3c72;\n            margin-bottom: 8px;\n            line-height: 1;\n        }\n        \n        .metric-label { \n            font-size: 0.85rem; \n            color: #64748b;\n            font-weight: 500;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .metric.highlight {\n            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n            border-color: #3b82f6;\n        }\n        \n        .metric.highlight::before {\n            background: linear-gradient(90deg, #10b981 0%, #059669 100%);\n        }\n        \n        .section { \n            margin: 40px 0;\n            background: white;\n            border-radius: 12px;\n            overflow: hidden;\n            box-shadow: 0 2px 15px rgba(0, 0, 0, 0.06);\n            border: 1px solid #e8ecf1;\n        }\n        \n        .section-header {\n            background: #f8fafc;\n            padding: 24px 32px;\n            border-bottom: 1px solid #e8ecf1;\n        }\n        \n        .section h2 { \n            color: #1e293b;\n            font-size: 1.4rem;\n            font-weight: 600;\n            display: flex;\n            align-items: center;\n            gap: 10px;\n        }\n        \n        .section-content {\n            padding: 0;\n        }\n        \n        table { \n            width: 100%; \n            border-collapse: collapse;\n        }\n        \n        th, td { \n            padding: 16px 24px;\n            text-align: left;\n            border-bottom: 1px solid #f1f5f9;\n        }\n        \n        th { \n            background: #f8fafc;\n            font-weight: 600;\n            color: #475569;\n            font-size: 0.875rem;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n            position: sticky;\n            top: 0;\n            z-index: 1;\n        }\n        \n        tr:last-child td {\n            border-bottom: none;\n        }\n        \n        tr:hover { \n            background: #f8fafc;\n        }\n        \n        .status-success { \n            background: #10b981;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .status-failed { \n            background: #ef4444;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .status-skipped { \n            background: #6b7280;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .config-section {\n            background: #f8fafc;\n            padding: 32px;\n            border-radius: 12px;\n            margin: 40px 0;\n        }\n        \n        .config-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));\n            gap: 24px;\n            margin-top: 20px;\n        }\n        \n        .config-item {\n            display: flex;\n            align-items: flex-start;\n            gap: 16px;\n        }\n        \n        .config-label {\n            font-weight: 600;\n            color: #475569;\n            min-width: 140px;\n            font-size: 0.9rem;\n        }\n        \n        .config-value {\n            color: #1e293b;\n            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n            background: white;\n            padding: 8px 12px;\n            border-radius: 6px;\n            font-size: 0.85rem;\n            word-break: break-all;\n            flex: 1;\n            border: 1px solid #e2e8f0;\n        }\n        \n        .footer { \n            background: #f8fafc;\n            padding: 40px;\n            text-align: center;\n            color: #64748b;\n            border-top: 1px solid #e8ecf1;\n        }\n        \n        .footer .company {\n            font-size: 1.1rem;\n            font-weight: 600;\n            color: #1e293b;\n            margin-bottom: 8px;\n        }\n        \n        .footer .version {\n            font-size: 0.85rem;\n            margin-top: 16px;\n            color: #94a3b8;\n        }\n        \n        .report-location {\n            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n            border: 1px solid #3b82f6;\n            border-radius: 12px;\n            padding: 20px 28px;\n            margin: 30px 0;\n            text-align: center;\n            color: #1e3c72;\n            font-size: 0.95rem;\n        }\n        \n        .report-location strong {\n            color: #1e3c72;\n            font-weight: 600;\n        }\n        \n        .size-cell {\n            font-family: 'SF Mono', Monaco, monospace;\n            font-size: 0.9rem;\n        }\n        \n        .file-link {\n            color: #3b82f6;\n            text-decoration: none;\n            font-weight: 500;\n            transition: all 0.2s ease;\n            position: relative;\n            display: inline-block;\n            padding: 2px 0;\n        }\n        \n        .file-link:hover {\n            color: #2563eb;\n            text-decoration: underline;\n        }\n        \n        .tooltip {\n            position: relative;\n            display: inline-block;\n        }\n        \n        .tooltip .tooltiptext {\n            visibility: hidden;\n            width: 280px;\n            background-color: #333;\n            color: #fff;\n            text-align: center;\n            border-radius: 8px;\n            padding: 12px 16px;\n            position: absolute;\n            z-index: 1000;\n            bottom: 125%;\n            left: 50%;\n            margin-left: -140px;\n            opacity: 0;\n            transition: opacity 0.3s;\n            font-size: 0.85rem;\n            line-height: 1.4;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n        }\n        \n        .tooltip .tooltiptext::after {\n            content: \"\";\n            position: absolute;\n            top: 100%;\n            left: 50%;\n            margin-left: -5px;\n            border-width: 5px;\n            border-style: solid;\n            border-color: #333 transparent transparent transparent;\n        }\n        \n        .tooltip:hover .tooltiptext {\n            visibility: visible;\n            opacity: 1;\n        }\n        \n        .no-collada-notice {\n            background: #fef3c7;\n            border: 1px solid #fbbf24;\n            border-radius: 8px;\n            padding: 16px;\n            margin: 20px 0;\n            color: #92400e;\n            font-size: 0.9rem;\n            text-align: center;\n        }\n        \n        .no-collada-notice strong {\n            color: #78350f;\n        }\n        \n        @media print {\n            body { background: white; }\n            .container { box-shadow: none; }\n            .metric { box-shadow: none; border: 1px solid #e5e7eb; }\n            .section { box-shadow: none; page-break-inside: avoid; }\n            .tooltip .tooltiptext { display: none; }\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"header\">\n            <div class=\"header-content\">\n                <h1>CAD Project Batch Conversion Report</h1>\n                <p class=\"subtitle\">Automated conversion pipeline execution summary</p>\n                <p class=\"timestamp\">${now.toLocaleString('en-US', { \n                    weekday: 'long', \n                    year: 'numeric', \n                    month: 'long', \n                    day: 'numeric', \n                    hour: '2-digit', \n                    minute: '2-digit' \n                })}</p>\n            </div>\n        </div>\n        \n        <div class=\"content\">\n            <div class=\"metrics\">\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${totalFiles}</div>\n                    <div class=\"metric-label\">Total Files</div>\n                </div>\n                <div class=\"metric highlight\">\n                    <div class=\"metric-value\">${successRate}%</div>\n                    <div class=\"metric-label\">Success Rate</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${totalPipelineMinutes}</div>\n                    <div class=\"metric-label\">Total Time (Minutes)</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${Math.round(totalInputSize / 1048576)} MB</div>\n                    <div class=\"metric-label\">Total Input Size</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${Math.round(totalOutputSize / 1048576)} MB</div>\n                    <div class=\"metric-label\">Total Output Size</div>\n                </div>\n            </div>\n\n            <div class=\"report-location\">\n                <strong>Report Location:</strong> ${fullPath}\n            </div>\n\n            ${noColladaEnabled ? `\n            <div class=\"no-collada-notice\">\n                <strong>Note:</strong> DAE (Collada) file generation was skipped as per configuration (no-collada option enabled).\n            </div>\n            ` : ''}\n\n            ${successfulFiles.length > 0 ? `\n            <div class=\"section\">\n                <div class=\"section-header\">\n                    <h2>✅ Successfully Converted Files (${successfulFiles.length})</h2>\n                </div>\n                <div class=\"section-content\">\n                    <table>\n                        <thead>\n                            <tr>\n                                <th>File Name</th>\n                                <th>Original Size</th>\n                                <th>Excel Output</th>\n                                ${noColladaEnabled ? '<th>DAE Output</th>' : '<th>DAE Output</th>'}\n                                <th>Total Output</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            ${successfulFiles.map(file => `\n                                <tr>\n                                    <td><strong>${file.name}</strong></td>\n                                    <td class=\"size-cell\">${file.originalSize} KB</td>\n                                    <td class=\"size-cell\">\n                                        <a href=\"file:///${file.xlsxPath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.xlsxSize} KB</a>\n                                    </td>\n                                    <td class=\"size-cell\">\n                                        ${noColladaEnabled ? \n                                            '<span class=\"status-skipped\">SKIPPED</span>' : \n                                            `<div class=\"tooltip\">\n                                                <a href=\"file:///${file.daePath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.daeSize} KB</a>\n                                                <span class=\"tooltiptext\">💡 Tip: Use the free CAD Assistant software for the best viewing experience of DAE files</span>\n                                            </div>`\n                                        }\n                                    </td>\n                                    <td class=\"size-cell\"><strong>${file.totalSize} KB</strong></td>\n                                </tr>\n                            `).join('')}\n                        </tbody>\n                    </table>\n                </div>\n            </div>\n            ` : ''}\n\n            ${failedFiles.length > 0 ? `\n            <div class=\"section\">\n                <div class=\"section-header\">\n                    <h2>❌ Failed Conversions (${failedFiles.length})</h2>\n                </div>\n                <div class=\"section-content\">\n                    <table>\n                        <thead>\n                            <tr>\n                                <th>File Name</th>\n                                <th>Original Size</th>\n                                <th>XLSX Status</th>\n                                <th>DAE Status</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            ${failedFiles.map(file => `\n                                <tr>\n                                    <td><strong>${file.name}</strong></td>\n                                    <td class=\"size-cell\">${file.originalSize} KB</td>\n                                    <td><span class=\"status-${file.xlsxStatus === '✓' ? 'success' : 'failed'}\">${file.xlsxStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span></td>\n                                    <td>${noColladaEnabled && file.xlsxStatus === '✓' ? \n                                        '<span class=\"status-skipped\">SKIPPED</span>' : \n                                        `<span class=\"status-${file.daeStatus === '✓' ? 'success' : 'failed'}\">${file.daeStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span>`\n                                    }</td>\n                                </tr>\n                            `).join('')}\n                        </tbody>\n                    </table>\n                </div>\n            </div>\n            ` : ''}\n\n            <div class=\"config-section\">\n                <h2 style=\"color: #1e293b; font-size: 1.4rem; font-weight: 600; margin-bottom: 20px;\">\n                    ⚙️ Configuration Details\n                </h2>\n                <div class=\"config-grid\">\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Source Folder:</span>\n                        <span class=\"config-value\">${config.source_folder || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Output Folder:</span>\n                        <span class=\"config-value\">${config.output_folder || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">File Extension:</span>\n                        <span class=\"config-value\">${config.file_extension || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Include Subfolders:</span>\n                        <span class=\"config-value\">${config.include_subfolders ? 'Yes' : 'No'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Converter Options:</span>\n                        <span class=\"config-value\">${config.options || 'None'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Generated:</span>\n                        <span class=\"config-value\">${now.toLocaleString()}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Workflow ID:</span>\n                        <span class=\"config-value\">FYFQhblt4gILLSpe</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"footer\">\n            <div class=\"company\">DataDrivenConstruction.io</div>\n            <div>Professional CAD Conversion Pipeline</div>\n            <div class=\"version\">Version 2.0 | Automated Batch Processing System</div>\n        </div>\n    </div>\n</body>\n</html>`;\n\n// Return data for saving and opening\nreturn [{\n    json: {\n        html_content: htmlContent,\n        file_name: fileName,\n        full_path: fullPath,\n        output_folder: outputFolder,\n        total_files: totalFiles,\n        successful_conversions: successfulConversions,\n        failed_conversions: failedConversions,\n        success_rate: successRate,\n        total_pipeline_time: totalPipelineMinutes,\n        config: config,\n        summary_message: `✅ Conversion Complete! ${successfulConversions}/${totalFiles} files converted successfully (${successRate}% success rate) in ${totalPipelineMinutes} minutes`\n    }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "21c9bd6d-e1a0-4697-b99d-c51e666af986",
      "name": "Préparer les données binaires1",
      "type": "n8n-nodes-base.code",
      "position": [
        3312,
        576
      ],
      "parameters": {
        "jsCode": "// Prepare binary data for file saving\nconst item = $input.first().json;\n\nconsole.log('Preparing binary data for file:', item.file_name);\nconsole.log('Full path:', item.full_path);\n\nif (!item.html_content) {\n  throw new Error('No HTML content found');\n}\n\nreturn [{\n  json: item,\n  binary: {\n    report: {\n      data: Buffer.from(item.html_content, 'utf-8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: item.file_name,\n      fileExtension: 'html'\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3ba8b465-5da4-4b79-83e2-2bdb63222b6b",
      "name": "Sauvegarder le fichier HTML1",
      "type": "n8n-nodes-base.writeBinaryFile",
      "position": [
        3472,
        576
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.full_path }}",
        "dataPropertyName": "report"
      },
      "typeVersion": 1
    },
    {
      "id": "94de0bbb-402b-4dea-bcaf-f98fdc678048",
      "name": "Vérifier et préparer le chemin1",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        576
      ],
      "parameters": {
        "jsCode": "// Verify file was saved and prepare for opening\nconst data = $input.first().json;\nconst fullPath = data.full_path || '';\n\nconsole.log('Preparing to open file:', fullPath);\n\n// Normalize path for Windows\nconst windowsPath = fullPath.replace(/\\\\\\\\/g, '\\\\').replace(/\\//g, '\\\\');\n\nreturn [{\n  json: {\n    ...data,\n    windows_path: windowsPath,\n    command_to_open: `cmd /c start \"\" \"${windowsPath}\"`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "6558ac19-23c1-44c9-8b7b-3615052cd4a5",
      "name": "Ouvrir le rapport HTML1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3792,
        576
      ],
      "parameters": {
        "command": "=cmd /c start \"\" \"{{ $json.windows_path }}\""
      },
      "typeVersion": 1
    },
    {
      "id": "cdf12580-ade1-4ae0-a699-980aca156c98",
      "name": "Notification d'achèvement final1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3952,
        576
      ],
      "parameters": {
        "command": "=echo 🏁 CAD Conversion Pipeline Completed Successfully! & echo 📋 Summary: {{ $json.summary_message }} & echo 📁 Report Location: {{ $json.windows_path }} & echo ✨ All processes finished successfully! & echo. & echo 🎯 Next Steps: & echo    1. Check the HTML report that opened in your browser & echo    2. Verify output files in the specified folder & echo    3. Review conversion results and metrics & echo. & echo ✅ Pipeline execution completed!",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "dc253ac4-c296-4a8c-897c-380e5e14b683",
      "name": "Réponse si aucun fichier trouvé1",
      "type": "n8n-nodes-base.set",
      "position": [
        1520,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "no-files-message",
              "name": "message",
              "type": "string",
              "value": "⚠️ No files found to convert with the specified parameters"
            },
            {
              "id": "no-files-details",
              "name": "details",
              "type": "string",
              "value": "Please check: 1) Source folder path exists, 2) File extension is correct, 3) Files exist in the specified location"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ee8bb887-c553-412b-9be5-c865f08fbd82",
      "name": "Note adhésive7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        112
      ],
      "parameters": {
        "color": 6,
        "width": 756,
        "height": 844,
        "content": "## 🏁 GROUP 1: INITIALIZATION\n\n**Nodes in this group:**\n• Manual Trigger\n• Capture Pipeline Start Time\n• Set Configuration Parameters\n• Merge Pipeline Start with Config\n\n**What happens here:**\n1️⃣ Pipeline starts manually or on schedule\n2️⃣ Current timestamp is captured for metrics\n3️⃣ All settings are loaded (paths, folders, etc.)\n4️⃣ Configuration merged with timing data\n\n💡 **Key:** This sets up everything needed for the conversion process"
      },
      "typeVersion": 1
    },
    {
      "id": "0ca6f619-b073-41b1-a248-0f2b62333773",
      "name": "Note adhésive9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 1024,
        "height": 840,
        "content": "## 🔍 GROUP 2: FILE DISCOVERY\n\n**Nodes in this group:**\n• Find CAD Files\n• Merge Config with Search Results\n• Process File List\n• Check if Files Exist\n\n**What happens here:**\n1️⃣ PowerShell scans the source folder\n2️⃣ Filters files by extension (.rvt, .ifc, etc.)\n3️⃣ Creates a list with full paths\n4️⃣ Validates that files were found\n\n📁 **Output:** Array of file objects with paths\n❌ **If no files:** Pipeline stops gracefully"
      },
      "typeVersion": 1
    },
    {
      "id": "8dcff587-4f09-4e86-a065-bdd056be456d",
      "name": "Note adhésive10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 316,
        "height": 844,
        "content": "## 🔄 GROUP 3: BATCH PREPARATION\n\n**Nodes in this group:**\n• Split Files for Processing\n• Create Output Directory\n• Merge File Data\n\n**What happens here:**\n1️⃣ File list is split into individual items\n2️⃣ Output directory is created (if needed)\n3️⃣ Each file gets conversion parameters\n\n🎯 **Purpose:** Enables parallel processing\n📊 **Data added:** Expected output paths for XLSX & DAE"
      },
      "typeVersion": 1
    },
    {
      "id": "ae685ee4-bbee-4b56-85e0-26f3da9053fd",
      "name": "Note adhésive11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 800,
        "height": 840,
        "content": "## ⚡ GROUP 4: CONVERSION EXECUTION\n\n**Nodes in this group:**\n• Capture Start Time\n• Small Delay\n• Execute Conversion\n• Calculate Conversion Time\n\n**What happens here:**\n1️⃣ Timestamp captured for each file\n2️⃣ Small delay prevents overload\n3️⃣ RvtExporter.exe runs the conversion\n4️⃣ Processing time calculated\n\n🔧 **Command:** \n`RvtExporter.exe [input] [output.xlsx] [output.dae]`\n\n⏱️ **Conversion Time:** 1 Minute per 100Mb."
      },
      "typeVersion": 1
    },
    {
      "id": "5fe7b6d6-7a36-41ae-a431-a93803227a50",
      "name": "Note adhésive15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2816,
        112
      ],
      "parameters": {
        "color": 6,
        "width": 284,
        "height": 836,
        "content": "## ✅ GROUP 5: VALIDATION\n\n**Nodes in this group:**\n• Merge Data Before Verification\n• Verify Output Files and Get Sizes\n• Complete File Verification\n\n**What happens here:**\n1️⃣ All conversion data is consolidated\n2️⃣ PowerShell checks if outputs exist\n3️⃣ File sizes are measured\n4️⃣ Success/failure status determined\n\n**Success criteria:**\n• Both XLSX and DAE files exist\n• File sizes > 0 bytes\n• No conversion errors"
      },
      "typeVersion": 1
    },
    {
      "id": "2a199a8f-7206-4b18-9e87-f9dc4db56d8d",
      "name": "Note adhésive16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3120,
        112
      ],
      "parameters": {
        "width": 456,
        "height": 832,
        "content": "## 📊 GROUP 6: REPORTING\n\n**Nodes in this group:**\n• Generate HTML Report\n• Prepare Binary Data\n• Save HTML File\n\n**What happens here:**\n1️⃣ All results compiled into statistics\n2️⃣ Professional HTML report generated\n3️⃣ Report saved to output folder\n\n**Report includes:**\n• Success rate & metrics\n• Processing times\n• File sizes & links\n• Detailed conversion log"
      },
      "typeVersion": 1
    },
    {
      "id": "565d9855-8447-4c4e-ab57-b652a8733040",
      "name": "Note adhésive17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        112
      ],
      "parameters": {
        "width": 484,
        "height": 828,
        "content": "## 🎯 GROUP 7: FINALIZATION\n\n**Nodes in this group:**\n• Verify and Prepare Path\n• Open HTML Report\n• Final Completion Notice\n\n**What happens here:**\n1️⃣ Report path normalized for Windows\n2️⃣ HTML report opens in browser\n3️⃣ Success message displayed\n\n**Final output:**\n• Report auto-opens\n• Summary in console\n• All files ready for use"
      },
      "typeVersion": 1
    },
    {
      "id": "32324229-f786-4023-bfc1-bc01ad05575d",
      "name": "Note adhésive30",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        976
      ],
      "parameters": {
        "color": 4,
        "width": 488,
        "height": 308,
        "content": "## 📝 Configuration Example\n\n```\nconverter_path: \nC:\\DDC_Converter\\RvtExporter.exe\nsource_folder: \nC:\\Projects\\Building_A\noutput_folder: \nC:\\Projects\\Converted\nfile_extension: .rvt\ninclude_subfolders: false\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "62e0570c-238d-40e2-8b1f-f1f71ca8f034",
      "name": "Déclencheur planifié1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -80,
        560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "637d319f-4c7d-4f14-8676-16e6a839812d",
      "name": "Note adhésive2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        304
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 384,
        "content": "## ⬇️ Only modify the variables here  \n— everything else works automatically"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "7eb4a013-6adf-4f71-b5d8-1c78163410dd",
  "connections": {
    "edf42bdd-67ed-46d1-a522-cbc93538d894": {
      "main": [
        [
          {
            "node": "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2fe7360d-df50-4eb7-8799-441e7aed7a1a": {
      "main": [
        [
          {
            "node": "614d8fe1-6c80-4cb0-b941-5aa3998a2321",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "53648934-0bb7-49d3-83f9-ab4280bb3ec9": {
      "main": [
        [
          {
            "node": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "3ba8b465-5da4-4b79-83e2-2bdb63222b6b": {
      "main": [
        [
          {
            "node": "94de0bbb-402b-4dea-bcaf-f98fdc678048",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1cbf1ee9-0710-4145-a54e-0c4c73e177a6": {
      "main": [
        [
          {
            "node": "627097e5-5d07-4386-a9d7-22dffe7ba4ab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6558ac19-23c1-44c9-8b7b-3615052cd4a5": {
      "main": [
        [
          {
            "node": "cdf12580-ade1-4ae0-a699-980aca156c98",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cc6f17d1-4393-4c77-947a-53c42c01543c": {
      "main": [
        [
          {
            "node": "357d21f1-ff0b-4308-99a7-8ab3140b6dde",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "627097e5-5d07-4386-a9d7-22dffe7ba4ab": {
      "main": [
        [
          {
            "node": "edf42bdd-67ed-46d1-a522-cbc93538d894",
            "type": "main",
            "index": 0
          },
          {
            "node": "9255e34b-be94-4630-8887-fc7a0aafd739",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18": {
      "main": [
        [
          {
            "node": "f02f9f74-9b20-4857-92c3-4b0f4103f1b1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "21c9bd6d-e1a0-4697-b99d-c51e666af986": {
      "main": [
        [
          {
            "node": "3ba8b465-5da4-4b79-83e2-2bdb63222b6b",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "357d21f1-ff0b-4308-99a7-8ab3140b6dde": {
      "main": [
        [
          {
            "node": "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "dc253ac4-c296-4a8c-897c-380e5e14b683",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "44336609-1655-4f5b-92b4-9b6165620036": {
      "main": [
        [
          {
            "node": "21c9bd6d-e1a0-4697-b99d-c51e666af986",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0330e04c-9572-439d-b09f-aa932b5e81ad": {
      "main": [
        [
          {
            "node": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "94de0bbb-402b-4dea-bcaf-f98fdc678048": {
      "main": [
        [
          {
            "node": "6558ac19-23c1-44c9-8b7b-3615052cd4a5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f02f9f74-9b20-4857-92c3-4b0f4103f1b1": {
      "main": [
        [
          {
            "node": "9255e34b-be94-4630-8887-fc7a0aafd739",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "8d038788-44e7-49be-a999-b97ac4f96fac": {
      "main": [
        [
          {
            "node": "44336609-1655-4f5b-92b4-9b6165620036",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea": {
      "main": [
        [
          {
            "node": "0330e04c-9572-439d-b09f-aa932b5e81ad",
            "type": "main",
            "index": 0
          },
          {
            "node": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "614d8fe1-6c80-4cb0-b941-5aa3998a2321": {
      "main": [
        [
          {
            "node": "7b214d29-a619-4a41-8cfa-67f074470b89",
            "type": "main",
            "index": 0
          },
          {
            "node": "e64288d1-9d53-4036-afde-437c9892a927",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7b214d29-a619-4a41-8cfa-67f074470b89": {
      "main": [
        [
          {
            "node": "e64288d1-9d53-4036-afde-437c9892a927",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "9255e34b-be94-4630-8887-fc7a0aafd739": {
      "main": [
        [
          {
            "node": "0e3dc51e-4891-4094-ad20-341424dd04df",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "894a4fae-cd2f-4418-b35b-fc6a06cda820": {
      "main": [
        [
          {
            "node": "cc6f17d1-4393-4c77-947a-53c42c01543c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e64288d1-9d53-4036-afde-437c9892a927": {
      "main": [
        [
          {
            "node": "53648934-0bb7-49d3-83f9-ab4280bb3ec9",
            "type": "main",
            "index": 0
          },
          {
            "node": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0e3dc51e-4891-4094-ad20-341424dd04df": {
      "main": [
        [
          {
            "node": "8d038788-44e7-49be-a999-b97ac4f96fac",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé - Extraction de documents, IA Multimodale

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds82
Catégorie2
Types de nœuds17
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur

Founder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34