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. 22
      src/utils/mediaStorage.ts

10
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<GenerateResponse>({
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<GenerateResponse>({
success: true,
audio: isLarge ? undefined : output.data,
audioUrl: isLarge ? output.url : undefined,
audioUrl,
contentType: "audio",
});
}

36
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",

67
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<typeof readVideoGenerationConfig>,
@ -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<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 {
updateNodeData(node.id, {
status: "error",

135
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<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(
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<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) {
if (error instanceof DOMException && error.name === "AbortError") {

22
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 {
outputVideoRef = undefined;
outputVideo = d.outputVideo;
outputVideoStorageStatus = "remote-only";
}
} else if (d.outputVideoRef && isDataUrl(d.outputVideo)) {
// Already has ref, just clear base64
outputVideo = null;
@ -425,9 +415,8 @@ 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,
@ -439,6 +428,9 @@ async function externalizeNodeMedia(
} else {
cleanedVideoHistory.push(item);
}
} else {
cleanedVideoHistory.push(item);
}
}
}

Loading…
Cancel
Save