diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 606dbcb5..94acbd2e 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -162,21 +162,19 @@ export async function buildMediaResponse( } if (output.type === "video") { - const isLarge = !output.data && output.url; + const videoUrl = output.url || output.data || ""; return NextResponse.json({ success: true, - video: isLarge ? undefined : output.data, - videoUrl: isLarge ? output.url : undefined, + videoUrl, contentType: "video", }); } if (output.type === "audio") { - const isLarge = !output.data && output.url; + const audioUrl = output.url || output.data || ""; return NextResponse.json({ success: true, - audio: isLarge ? undefined : output.data, - audioUrl: isLarge ? output.url : undefined, + audioUrl, contentType: "audio", }); } diff --git a/src/store/execution/generateAudioExecutor.ts b/src/store/execution/generateAudioExecutor.ts index 1de17065..cd72a65e 100644 --- a/src/store/execution/generateAudioExecutor.ts +++ b/src/store/execution/generateAudioExecutor.ts @@ -38,9 +38,6 @@ export async function executeGenerateAudio( signal, providerSettings, addIncurredCost, - generationsPath, - getNodes, - trackSaveGeneration, } = ctx; void _options; @@ -180,39 +177,6 @@ export async function executeGenerateAudio( addIncurredCost(modelToUse.pricing.amount); } - // Auto-save to generations folder if configured - if (generationsPath) { - const savePromise = fetch("/api/save-generation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - directoryPath: generationsPath, - audio: audioData, - prompt: text, - imageId: audioId, - }), - }) - .then((res) => res.json()) - .then((saveResult) => { - if (saveResult.success && saveResult.imageId && saveResult.imageId !== audioId) { - const currentNode = getNodes().find((n) => n.id === node.id); - if (currentNode) { - const currentData = currentNode.data as GenerateAudioNodeData; - const histCopy = [...(currentData.audioHistory || [])]; - const entryIndex = histCopy.findIndex((h) => h.id === audioId); - if (entryIndex !== -1) { - histCopy[entryIndex] = { ...histCopy[entryIndex], id: saveResult.imageId }; - updateNodeData(node.id, { audioHistory: histCopy }); - } - } - } - }) - .catch((err) => { - console.error("Failed to save audio generation:", err); - }); - - trackSaveGeneration(audioId, savePromise); - } } else { updateNodeData(node.id, { status: "error", diff --git a/src/store/execution/generateVideoExecutor.ts b/src/store/execution/generateVideoExecutor.ts index d71fb5a6..eef6f3a2 100644 --- a/src/store/execution/generateVideoExecutor.ts +++ b/src/store/execution/generateVideoExecutor.ts @@ -38,6 +38,14 @@ export interface GenerateVideoOptions { useStoredFallback?: boolean; } +function getGeneratedImageUrl(value: unknown): string | undefined { + if (typeof value === "string" && value.length > 0) return value; + if (!value || typeof value !== "object") return undefined; + + const record = value as { url?: unknown }; + return typeof record.url === "string" && record.url.length > 0 ? record.url : undefined; +} + function buildVideoParameters( nodeData: GenerateVideoNodeData, nodeConfig: ReturnType, @@ -142,9 +150,6 @@ export async function executeGenerateVideo( providerSettings, providerEnvStatus, addIncurredCost, - generationsPath, - getNodes, - trackSaveGeneration, } = ctx; const { useStoredFallback = false } = options; @@ -335,7 +340,7 @@ export async function executeGenerateVideo( // Handle video response (video or videoUrl field) const videoData = result.video || result.videoUrl; - const imageData = result.image?.url; + const imageData = getGeneratedImageUrl(result.image); if (result.success && (videoData || imageData)) { const outputContent = videoData || imageData; const timestamp = Date.now(); @@ -357,9 +362,7 @@ export async function executeGenerateVideo( const isRemoteVideo = Boolean(videoData && isHttpMediaUrl(outputContent)); const storageStatus = isRemoteVideo - ? generationsPath - ? "localizing" - : "remote-only" + ? "remote-only" : undefined; updateNodeData(node.id, { @@ -379,56 +382,6 @@ export async function executeGenerateVideo( addIncurredCost(modelToUse.pricing.amount); } - // Auto-save to generations folder if configured - if (generationsPath) { - const saveContent = videoData - ? { video: videoData } - : { image: imageData }; - - const savePromise = fetch("/api/save-generation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - directoryPath: generationsPath, - ...saveContent, - prompt: text, - imageId: videoId, - }), - }) - .then((res) => res.json()) - .then((saveResult) => { - if (saveResult.success && saveResult.imageId) { - const currentNode = getNodes().find((n) => n.id === node.id); - if (currentNode) { - const currentData = currentNode.data as GenerateVideoNodeData; - const histCopy = [...(currentData.videoHistory || [])]; - const entryIndex = histCopy.findIndex((h) => h.id === videoId); - const updates: Partial = {}; - if (entryIndex !== -1 && saveResult.imageId !== videoId) { - histCopy[entryIndex] = { ...histCopy[entryIndex], id: saveResult.imageId }; - updates.videoHistory = histCopy; - } - if (videoData) { - updates.outputVideoRef = saveResult.imageId; - updates.outputVideoStorageStatus = "localized"; - const currentOutputVideo = currentData.outputVideo; - if (isHttpMediaUrl(currentOutputVideo) && currentOutputVideo) { - updates.outputVideoRemoteUrl = currentData.outputVideoRemoteUrl || currentOutputVideo; - } - } - updateNodeData(node.id, updates); - } - } - }) - .catch((err) => { - console.error("Failed to save video generation:", err); - if (videoData && isRemoteVideo) { - updateNodeData(node.id, { outputVideoStorageStatus: "remote-only" }); - } - }); - - trackSaveGeneration(videoId, savePromise); - } } else { updateNodeData(node.id, { status: "error", diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 31e890e2..4fbbff8f 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -23,11 +23,6 @@ import { pollGenerateTask } from "./pollTaskCompletion"; import { runWithFallback } from "./runWithFallback"; import type { NodeExecutionContext } from "./types"; import { buildPromptFromConnectedTextItems } from "./textInputPrompt"; -import { - isBrowserFileSystemPath, - joinBrowserFileSystemPath, - writeBrowserGenerationFile, -} from "@/utils/browserFileSystem"; import { isHttpMediaUrl } from "@/utils/mediaResolver"; import { uploadPopiserverGenerationInputsForGeneration, @@ -50,6 +45,21 @@ type GeneratedResultImage = { previewImg?: string; }; +function normalizeGeneratedResultImage(value: unknown): GeneratedResultImage | null { + if (typeof value === "string" && value.length > 0) { + return { url: value }; + } + if (!value || typeof value !== "object") return null; + + const record = value as { url?: unknown; previewImg?: unknown }; + if (typeof record.url !== "string" || record.url.length === 0) return null; + + return { + url: record.url, + previewImg: typeof record.previewImg === "string" ? record.previewImg : undefined, + }; +} + const GEMINI_RESOLUTIONS: Resolution[] = ["512", "1K", "2K", "4K"]; function normalizeGeminiResolution(value: string): Resolution { @@ -61,16 +71,6 @@ function isProviderOverloadError(error: unknown): boolean { return /provider is temporarily overloaded|system\s+disk\s+overloaded/i.test(message); } -async function loadSavedGenerationImage(directoryPath: string, imageId: string): Promise { - const response = await fetch("/api/load-generation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ directoryPath, imageId }), - }); - const result = await response.json(); - return result.success && typeof result.image === "string" ? result.image : null; -} - export async function executeNanoBanana( ctx: NodeExecutionContext, options: NanoBananaOptions = {} @@ -87,9 +87,6 @@ export async function executeNanoBanana( providerEnvStatus, addIncurredCost, addToGlobalHistory, - generationsPath, - saveDirectoryPath, - trackSaveGeneration, appendOutputGalleryImage, } = ctx; @@ -101,10 +98,6 @@ export async function executeNanoBanana( textItems: connectedTextItems = [], dynamicInputs, } = getConnectedInputs(node.id); - const effectiveGenerationsPath = generationsPath - ?? (saveDirectoryPath && isBrowserFileSystemPath(saveDirectoryPath) - ? joinBrowserFileSystemPath(saveDirectoryPath, "generations") - : null); // Get fresh node data from store const freshNode = getFreshNode(node.id); @@ -286,8 +279,12 @@ export async function executeNanoBanana( } const resultImages: GeneratedResultImage[] = result.success && Array.isArray(result.images) && result.images.length > 0 ? result.images - : result.success && result.image - ? [result.image] + .map((image: unknown) => normalizeGeneratedResultImage(image)) + .filter((image: GeneratedResultImage | null): image is GeneratedResultImage => image !== null) + : result.success + ? [normalizeGeneratedResultImage(result.image)].filter( + (image: GeneratedResultImage | null): image is GeneratedResultImage => image !== null + ) : []; if (resultImages.length > 0) { @@ -391,96 +388,6 @@ export async function executeNanoBanana( addIncurredCost(generationCost); } - // Auto-save to generations folder if configured - if (effectiveGenerationsPath && isBrowserFileSystemPath(effectiveGenerationsPath)) { - const savePromise = writeBrowserGenerationFile( - effectiveGenerationsPath, - item.id, - item.image - ) - .then((saveResult) => { - const currentNode = getNodes().find((n) => n.id === node.id); - if (!currentNode) return; - const currentData = currentNode.data as NanoBananaNodeData; - const histCopy = [...(currentData.imageHistory || [])]; - const entryIndex = histCopy.findIndex((h) => h.id === item.id); - const localizedImage = saveResult.imageData || item.image; - - if (entryIndex !== -1) { - histCopy[entryIndex] = { ...histCopy[entryIndex], image: localizedImage }; - } - - const update: Partial = { imageHistory: histCopy }; - if (currentData.outputImage === item.image || currentData.selectedHistoryIndex === entryIndex) { - update.outputImage = localizedImage; - update.outputImageRef = item.id; - update.outputImageStorageStatus = "localized"; - } - updateNodeData(node.id, update); - }) - .catch((err) => { - console.error("Failed to save browser-local generation:", err); - if (isHttpMediaUrl(item.image)) { - updateNodeData(node.id, { outputImageStorageStatus: "remote-only" }); - } - }); - - if (isHttpMediaUrl(item.image)) { - updateNodeData(node.id, { outputImageStorageStatus: "localizing" }); - } - trackSaveGeneration(item.id, savePromise); - } else if (effectiveGenerationsPath) { - const savePromise = fetch("/api/save-generation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - directoryPath: effectiveGenerationsPath, - image: item.image, - prompt: finalPrompt, - imageId: item.id, - }), - }) - .then((res) => res.json()) - .then(async (saveResult) => { - if (saveResult.success && saveResult.imageId) { - const savedImageId = String(saveResult.imageId); - const localizedImage = await loadSavedGenerationImage(effectiveGenerationsPath, savedImageId) - .catch(() => null); - const currentNode = getNodes().find((n) => n.id === node.id); - if (!currentNode) return; - - const currentData = currentNode.data as NanoBananaNodeData; - const histCopy = [...(currentData.imageHistory || [])]; - const entryIndex = histCopy.findIndex((h) => h.id === item.id); - if (entryIndex !== -1) { - histCopy[entryIndex] = { - ...histCopy[entryIndex], - id: savedImageId, - image: localizedImage || histCopy[entryIndex].image, - }; - } - - const update: Partial = { imageHistory: histCopy }; - if (currentData.outputImage === item.image || currentData.selectedHistoryIndex === entryIndex) { - if (localizedImage) update.outputImage = localizedImage; - update.outputImageRef = savedImageId; - update.outputImageStorageStatus = localizedImage ? "localized" : "remote-only"; - } - updateNodeData(node.id, update); - } - }) - .catch((err) => { - console.error("Failed to save generation:", err); - if (isHttpMediaUrl(item.image)) { - updateNodeData(node.id, { outputImageStorageStatus: "remote-only" }); - } - }); - - if (isHttpMediaUrl(item.image)) { - updateNodeData(node.id, { outputImageStorageStatus: "localizing" }); - } - trackSaveGeneration(item.id, savePromise); - } }); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { diff --git a/src/utils/mediaStorage.ts b/src/utils/mediaStorage.ts index da16de75..1cc74332 100644 --- a/src/utils/mediaStorage.ts +++ b/src/utils/mediaStorage.ts @@ -388,24 +388,14 @@ async function externalizeNodeMedia( } } - // Handle output video. Archive provider URLs locally when possible, while keeping the - // original remote URL as a fallback/re-download source. + // Handle output video. New generations keep provider/media URLs in workflow JSON; + // only data URLs are externalized as a compatibility fallback. if (d.outputVideo) { if (isHttpUrl(d.outputVideo)) { outputVideoRemoteUrl = d.outputVideoRemoteUrl || d.outputVideo; - if (!outputVideoRef) { - const selectedIndex = d.selectedVideoHistoryIndex || 0; - const expectedRef = d.videoHistory?.[selectedIndex]?.id; - const videoRef = await saveVideoAndGetRef(d.outputVideo, workflowPath, savedMediaIds, expectedRef); - outputVideoRef = videoRef || undefined; - } - if (outputVideoRef) { - outputVideo = null; - outputVideoStorageStatus = "localized"; - } else { - outputVideo = d.outputVideo; - outputVideoStorageStatus = "remote-only"; - } + outputVideoRef = undefined; + outputVideo = d.outputVideo; + outputVideoStorageStatus = "remote-only"; } else if (d.outputVideoRef && isDataUrl(d.outputVideo)) { // Already has ref, just clear base64 outputVideo = null; @@ -425,17 +415,19 @@ async function externalizeNodeMedia( for (const item of d.videoHistory) { const inlineVideo = item.video; if (inlineVideo) { - if (isDataUrl(inlineVideo) || isHttpUrl(inlineVideo)) { + if (isDataUrl(inlineVideo)) { await saveVideoAndGetRef(inlineVideo, workflowPath, savedMediaIds, item.id); + cleanedVideoHistory.push({ + id: item.id, + timestamp: item.timestamp, + prompt: item.prompt, + model: item.model, + modelDisplayName: item.modelDisplayName, + modelProvider: item.modelProvider, + }); + } else { + cleanedVideoHistory.push(item); } - cleanedVideoHistory.push({ - id: item.id, - timestamp: item.timestamp, - prompt: item.prompt, - model: item.model, - modelDisplayName: item.modelDisplayName, - modelProvider: item.modelProvider, - }); } else { cleanedVideoHistory.push(item); }