From 3d204bdf8d666e42d3878c2fab45c794d51f20d2 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 27 Mar 2026 20:45:40 +1300 Subject: [PATCH] fix: save video/audio to generations/ and strip imageHistory bloat Videos and audio were saving to the project root instead of the generations/ subfolder. Also strips legacy inline base64 image data from imageHistory items (~4.5MB each) that was inflating workflow JSON files. Co-Authored-By: Claude Opus 4.6 --- src/utils/mediaStorage.ts | 73 +++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/utils/mediaStorage.ts b/src/utils/mediaStorage.ts index 2e29672d..7791e7e4 100644 --- a/src/utils/mediaStorage.ts +++ b/src/utils/mediaStorage.ts @@ -240,12 +240,42 @@ async function externalizeNodeMedia( } } + // Strip inline image data from imageHistory items (legacy bloat) + // Each history item may carry a full base64 `image` field (~4.5MB each) + // The `image` field isn't in the CarouselImageItem type but exists at runtime + let cleanedHistory = d.imageHistory; + if (d.imageHistory?.length) { + cleanedHistory = []; + for (const item of d.imageHistory) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const legacyImage = (item as any).image as string | undefined; + if (legacyImage && isDataUrl(legacyImage)) { + // Save history image to generations if not already saved + await saveImageAndGetId( + legacyImage, + workflowPath, savedImageIds, "generations", item.id + ); + // Push clean item with only typed fields (strips legacy `image`) + cleanedHistory.push({ + id: item.id, + timestamp: item.timestamp, + prompt: item.prompt, + aspectRatio: item.aspectRatio, + model: item.model, + }); + } else { + cleanedHistory.push(item); + } + } + } + newData = { ...d, inputImages: inputImages.length > 0 && inputImages.every(i => i === "") ? [] : inputImages, inputImageRefs: inputImageRefs.length > 0 ? inputImageRefs : undefined, outputImage, outputImageRef, + imageHistory: cleanedHistory, }; break; } @@ -613,7 +643,8 @@ async function saveVideoAndGetRef( method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - directoryPath: workflowPath, + directoryPath: `${workflowPath}/generations`, + createDirectory: true, imageId: videoId, video: videoData, }), @@ -680,7 +711,8 @@ async function saveAudioAndGetRef( method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - directoryPath: workflowPath, + directoryPath: `${workflowPath}/generations`, + createDirectory: true, imageId: audioId, audio: audioData, }), @@ -1038,27 +1070,48 @@ async function loadMediaById( response = await fetch(`/api/workflow-images?${params.toString()}`); } else { // Use load-generation API for videos and audio + // Try generations/ subfolder first, fall back to root for legacy files response = await fetch("/api/load-generation", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - directoryPath: workflowPath, + directoryPath: `${workflowPath}/generations`, imageId: mediaId, }), }); + + let genResult = await response.json(); + + // Fallback to root directory for legacy files saved before generations/ fix + if (!genResult.success && genResult.notFound) { + response = await fetch("/api/load-generation", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + directoryPath: workflowPath, + imageId: mediaId, + }), + }); + genResult = await response.json(); + } + + const mediaData = mediaType === "video" ? genResult.video : genResult.audio; + if (!genResult.success) { + console.log(`${mediaType} not found: ${mediaId}`); + return ""; + } + loadedMedia.set(mediaId, mediaData); + return mediaData; } + // Only images reach here (video/audio return early above) const result = await response.json(); if (!result.success) { - // Missing media is expected when refs point to deleted/moved files console.log(`${mediaType} not found: ${mediaId}`); - return ""; // Return empty string to avoid breaking the workflow + return ""; } - const mediaData = mediaType === "image" ? result.image - : mediaType === "video" ? result.video - : result.audio; - loadedMedia.set(mediaId, mediaData); - return mediaData; + loadedMedia.set(mediaId, result.image); + return result.image; }