|
|
@ -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") { |
|
|
|