From 7360d9f8aa019363b8874731a8886592f62c2df9 Mon Sep 17 00:00:00 2001 From: Shrimbly Date: Thu, 26 Mar 2026 09:43:46 +1300 Subject: [PATCH] fix: handle outputGallery nodes in image externalization OutputGallery nodes were not being handled in externalizeWorkflowImages(), causing their images arrays (which can contain dozens of high-res base64 images) to remain embedded in workflow JSON even when "Embed images as base64" was disabled. This caused workflow files to balloon to 500MB+ and hit JavaScript's "Invalid string length" limit when saving. Changes: - Add outputGallery case to externalizeNodeImages() to clear images array - Add outputGallery case to hydrateNodeImages() for consistency - Add cleanup-workflow.js script for fixing existing affected workflows OutputGallery content is display-only and regenerated on workflow execution, so it doesn't need to be persisted. Co-Authored-By: Claude Sonnet 4.5 --- cleanup-workflow.js | 73 +++++++++++++++++++++++++++++++++++++++ src/utils/imageStorage.ts | 14 ++++++++ 2 files changed, 87 insertions(+) create mode 100644 cleanup-workflow.js diff --git a/cleanup-workflow.js b/cleanup-workflow.js new file mode 100644 index 00000000..8824d0ac --- /dev/null +++ b/cleanup-workflow.js @@ -0,0 +1,73 @@ +/** + * One-time script to clean up outputGallery nodes with embedded images + * This fixes workflows saved before the outputGallery externalization bug was fixed + */ + +const fs = require('fs'); +const path = require('path'); + +const workflowPath = process.argv[2]; + +if (!workflowPath) { + console.error('Usage: node cleanup-workflow.js '); + process.exit(1); +} + +console.log(`Reading workflow: ${workflowPath}`); +const workflow = JSON.parse(fs.readFileSync(workflowPath, 'utf8')); + +let cleaned = false; +let totalSizeBefore = 0; +let totalSizeAfter = 0; + +workflow.nodes.forEach((node, index) => { + if (node.type === 'outputGallery' && node.data.images && node.data.images.length > 0) { + const sizeBefore = JSON.stringify(node.data.images).length; + const imageCount = node.data.images.filter(img => img && img.startsWith('data:')).length; + + if (imageCount > 0) { + console.log(`\nNode ${index} (${node.id}):`); + console.log(` Found ${imageCount} embedded images`); + console.log(` Size: ${(sizeBefore / 1024 / 1024).toFixed(2)} MB`); + + node.data.images = []; + totalSizeBefore += sizeBefore; + cleaned = true; + } + } + + if (node.type === 'output' && node.data.image && node.data.image.startsWith('data:')) { + const sizeBefore = node.data.image.length; + console.log(`\nNode ${index} (${node.id}):`); + console.log(` Found embedded output image`); + console.log(` Size: ${(sizeBefore / 1024 / 1024).toFixed(2)} MB`); + + node.data.image = null; + node.data.imageRef = undefined; + if (node.data.video) { + node.data.video = null; + } + totalSizeBefore += sizeBefore; + cleaned = true; + } +}); + +if (cleaned) { + const backupPath = workflowPath.replace('.json', '.backup.json'); + console.log(`\nCreating backup: ${backupPath}`); + fs.copyFileSync(workflowPath, backupPath); + + console.log(`Writing cleaned workflow...`); + fs.writeFileSync(workflowPath, JSON.stringify(workflow, null, 2)); + + const finalSize = fs.statSync(workflowPath).size; + const originalSize = fs.statSync(backupPath).size; + + console.log(`\n✅ Done!`); + console.log(` Original size: ${(originalSize / 1024 / 1024).toFixed(2)} MB`); + console.log(` New size: ${(finalSize / 1024 / 1024).toFixed(2)} MB`); + console.log(` Saved: ${((originalSize - finalSize) / 1024 / 1024).toFixed(2)} MB`); + console.log(`\nBackup saved to: ${backupPath}`); +} else { + console.log('\n✓ No embedded images found - workflow is already clean!'); +} diff --git a/src/utils/imageStorage.ts b/src/utils/imageStorage.ts index 0a3fa6b8..5756c7b6 100644 --- a/src/utils/imageStorage.ts +++ b/src/utils/imageStorage.ts @@ -270,6 +270,14 @@ async function externalizeNodeImages( break; } + case "outputGallery": { + const d = data as import("@/types").OutputGalleryNodeData; + // OutputGallery content is regenerated on each workflow run + // Clear images array to keep workflow file small + newData = { ...d, images: [] }; + break; + } + case "splitGrid": { const d = data as import("@/types").SplitGridNodeData; // SplitGrid source is input content, save to inputs @@ -510,6 +518,12 @@ async function hydrateNodeImages( break; } + case "outputGallery": { + // OutputGallery content is not persisted - it's regenerated on each workflow run + newData = data; + break; + } + case "splitGrid": { const d = data as import("@/types").SplitGridNodeData; if (d.sourceImageRef && !d.sourceImage) {