Browse Source

不在本地留存base64的内容

feature/group
TianYun 1 month ago
parent
commit
83fdb89239
  1. 10
      src/app/api/generate/route.ts
  2. 36
      src/store/execution/generateAudioExecutor.ts
  3. 67
      src/store/execution/generateVideoExecutor.ts
  4. 135
      src/store/execution/nanoBananaExecutor.ts
  5. 40
      src/utils/mediaStorage.ts

10
src/app/api/generate/route.ts

@ -162,21 +162,19 @@ export async function buildMediaResponse(
} }
if (output.type === "video") { if (output.type === "video") {
const isLarge = !output.data && output.url; const videoUrl = output.url || output.data || "";
return NextResponse.json<GenerateResponse>({ return NextResponse.json<GenerateResponse>({
success: true, success: true,
video: isLarge ? undefined : output.data, videoUrl,
videoUrl: isLarge ? output.url : undefined,
contentType: "video", contentType: "video",
}); });
} }
if (output.type === "audio") { if (output.type === "audio") {
const isLarge = !output.data && output.url; const audioUrl = output.url || output.data || "";
return NextResponse.json<GenerateResponse>({ return NextResponse.json<GenerateResponse>({
success: true, success: true,
audio: isLarge ? undefined : output.data, audioUrl,
audioUrl: isLarge ? output.url : undefined,
contentType: "audio", contentType: "audio",
}); });
} }

36
src/store/execution/generateAudioExecutor.ts

@ -38,9 +38,6 @@ export async function executeGenerateAudio(
signal, signal,
providerSettings, providerSettings,
addIncurredCost, addIncurredCost,
generationsPath,
getNodes,
trackSaveGeneration,
} = ctx; } = ctx;
void _options; void _options;
@ -180,39 +177,6 @@ export async function executeGenerateAudio(
addIncurredCost(modelToUse.pricing.amount); 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 { } else {
updateNodeData(node.id, { updateNodeData(node.id, {
status: "error", status: "error",

67
src/store/execution/generateVideoExecutor.ts

@ -38,6 +38,14 @@ export interface GenerateVideoOptions {
useStoredFallback?: boolean; 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( function buildVideoParameters(
nodeData: GenerateVideoNodeData, nodeData: GenerateVideoNodeData,
nodeConfig: ReturnType<typeof readVideoGenerationConfig>, nodeConfig: ReturnType<typeof readVideoGenerationConfig>,
@ -142,9 +150,6 @@ export async function executeGenerateVideo(
providerSettings, providerSettings,
providerEnvStatus, providerEnvStatus,
addIncurredCost, addIncurredCost,
generationsPath,
getNodes,
trackSaveGeneration,
} = ctx; } = ctx;
const { useStoredFallback = false } = options; const { useStoredFallback = false } = options;
@ -335,7 +340,7 @@ export async function executeGenerateVideo(
// Handle video response (video or videoUrl field) // Handle video response (video or videoUrl field)
const videoData = result.video || result.videoUrl; const videoData = result.video || result.videoUrl;
const imageData = result.image?.url; const imageData = getGeneratedImageUrl(result.image);
if (result.success && (videoData || imageData)) { if (result.success && (videoData || imageData)) {
const outputContent = videoData || imageData; const outputContent = videoData || imageData;
const timestamp = Date.now(); const timestamp = Date.now();
@ -357,9 +362,7 @@ export async function executeGenerateVideo(
const isRemoteVideo = Boolean(videoData && isHttpMediaUrl(outputContent)); const isRemoteVideo = Boolean(videoData && isHttpMediaUrl(outputContent));
const storageStatus = isRemoteVideo const storageStatus = isRemoteVideo
? generationsPath ? "remote-only"
? "localizing"
: "remote-only"
: undefined; : undefined;
updateNodeData(node.id, { updateNodeData(node.id, {
@ -379,56 +382,6 @@ export async function executeGenerateVideo(
addIncurredCost(modelToUse.pricing.amount); 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<GenerateVideoNodeData> = {};
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 { } else {
updateNodeData(node.id, { updateNodeData(node.id, {
status: "error", status: "error",

135
src/store/execution/nanoBananaExecutor.ts

@ -23,11 +23,6 @@ import { pollGenerateTask } from "./pollTaskCompletion";
import { runWithFallback } from "./runWithFallback"; import { runWithFallback } from "./runWithFallback";
import type { NodeExecutionContext } from "./types"; import type { NodeExecutionContext } from "./types";
import { buildPromptFromConnectedTextItems } from "./textInputPrompt"; import { buildPromptFromConnectedTextItems } from "./textInputPrompt";
import {
isBrowserFileSystemPath,
joinBrowserFileSystemPath,
writeBrowserGenerationFile,
} from "@/utils/browserFileSystem";
import { isHttpMediaUrl } from "@/utils/mediaResolver"; import { isHttpMediaUrl } from "@/utils/mediaResolver";
import { import {
uploadPopiserverGenerationInputsForGeneration, uploadPopiserverGenerationInputsForGeneration,
@ -50,6 +45,21 @@ type GeneratedResultImage = {
previewImg?: string; 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"]; const GEMINI_RESOLUTIONS: Resolution[] = ["512", "1K", "2K", "4K"];
function normalizeGeminiResolution(value: string): Resolution { 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); return /provider is temporarily overloaded|system\s+disk\s+overloaded/i.test(message);
} }
async function loadSavedGenerationImage(directoryPath: string, imageId: string): Promise<string | null> {
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( export async function executeNanoBanana(
ctx: NodeExecutionContext, ctx: NodeExecutionContext,
options: NanoBananaOptions = {} options: NanoBananaOptions = {}
@ -87,9 +87,6 @@ export async function executeNanoBanana(
providerEnvStatus, providerEnvStatus,
addIncurredCost, addIncurredCost,
addToGlobalHistory, addToGlobalHistory,
generationsPath,
saveDirectoryPath,
trackSaveGeneration,
appendOutputGalleryImage, appendOutputGalleryImage,
} = ctx; } = ctx;
@ -101,10 +98,6 @@ export async function executeNanoBanana(
textItems: connectedTextItems = [], textItems: connectedTextItems = [],
dynamicInputs, dynamicInputs,
} = getConnectedInputs(node.id); } = getConnectedInputs(node.id);
const effectiveGenerationsPath = generationsPath
?? (saveDirectoryPath && isBrowserFileSystemPath(saveDirectoryPath)
? joinBrowserFileSystemPath(saveDirectoryPath, "generations")
: null);
// Get fresh node data from store // Get fresh node data from store
const freshNode = getFreshNode(node.id); 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 const resultImages: GeneratedResultImage[] = result.success && Array.isArray(result.images) && result.images.length > 0
? result.images ? result.images
: result.success && result.image .map((image: unknown) => normalizeGeneratedResultImage(image))
? [result.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) { if (resultImages.length > 0) {
@ -391,96 +388,6 @@ export async function executeNanoBanana(
addIncurredCost(generationCost); 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<NanoBananaNodeData> = { 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<NanoBananaNodeData> = { 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) { } catch (error) {
if (error instanceof DOMException && error.name === "AbortError") { if (error instanceof DOMException && error.name === "AbortError") {

40
src/utils/mediaStorage.ts

@ -388,24 +388,14 @@ async function externalizeNodeMedia(
} }
} }
// Handle output video. Archive provider URLs locally when possible, while keeping the // Handle output video. New generations keep provider/media URLs in workflow JSON;
// original remote URL as a fallback/re-download source. // only data URLs are externalized as a compatibility fallback.
if (d.outputVideo) { if (d.outputVideo) {
if (isHttpUrl(d.outputVideo)) { if (isHttpUrl(d.outputVideo)) {
outputVideoRemoteUrl = d.outputVideoRemoteUrl || d.outputVideo; outputVideoRemoteUrl = d.outputVideoRemoteUrl || d.outputVideo;
if (!outputVideoRef) { outputVideoRef = undefined;
const selectedIndex = d.selectedVideoHistoryIndex || 0; outputVideo = d.outputVideo;
const expectedRef = d.videoHistory?.[selectedIndex]?.id; outputVideoStorageStatus = "remote-only";
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";
}
} else if (d.outputVideoRef && isDataUrl(d.outputVideo)) { } else if (d.outputVideoRef && isDataUrl(d.outputVideo)) {
// Already has ref, just clear base64 // Already has ref, just clear base64
outputVideo = null; outputVideo = null;
@ -425,17 +415,19 @@ async function externalizeNodeMedia(
for (const item of d.videoHistory) { for (const item of d.videoHistory) {
const inlineVideo = item.video; const inlineVideo = item.video;
if (inlineVideo) { if (inlineVideo) {
if (isDataUrl(inlineVideo) || isHttpUrl(inlineVideo)) { if (isDataUrl(inlineVideo)) {
await saveVideoAndGetRef(inlineVideo, workflowPath, savedMediaIds, item.id); 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 { } else {
cleanedVideoHistory.push(item); cleanedVideoHistory.push(item);
} }

Loading…
Cancel
Save