diff --git a/src/app/api/generate/providers/gemini.ts b/src/app/api/generate/providers/gemini.ts index 6c333b9c..2f1c14c8 100644 --- a/src/app/api/generate/providers/gemini.ts +++ b/src/app/api/generate/providers/gemini.ts @@ -153,7 +153,7 @@ export async function generateWithGemini( const dataUrl = `data:${mimeType};base64,${imgData}`; - const responsePayload = { success: true, image: dataUrl }; + const responsePayload: GenerateResponse = { success: true, image: { url: dataUrl } }; const responseSize = JSON.stringify(responsePayload).length; const responseSizeMB = (responseSize / (1024 * 1024)).toFixed(2); diff --git a/src/app/api/generate/providers/popiserver.ts b/src/app/api/generate/providers/popiserver.ts index 895bd8b3..06dd9aed 100644 --- a/src/app/api/generate/providers/popiserver.ts +++ b/src/app/api/generate/providers/popiserver.ts @@ -88,6 +88,7 @@ type PopiTaskResult = { video?: string | null; voice?: string | null; cover?: string | null; + originImages?: string[] | null; }; type PopiTask = { @@ -557,17 +558,30 @@ function extractOutputFromTask(task: PopiTask, mediaType: string): GenerationOut ? { success: true, outputs: [{ type: "audio", data: "", url }] } : { success: false, error: "PopiServer audio task completed without output" }; } - const resultImages = collectStrings( - Array.isArray(task.resultList) ? task.resultList.map((result) => result.images) : [] - ); + const images: { image: string; previewImg?: string }[] = []; + task.resultList?.forEach((res) => { + if (Array.isArray(res?.originImages) && res.originImages.length > 0) { + images.push( + ...res.originImages + .filter((item): item is string => typeof item === "string" && item.length > 0) + .map((item, index) => ({ + image: item, + previewImg: res.images?.[index], + })), + ); + return; + } + + collectStrings(res?.images ?? []).forEach((image) => { + images.push({ image }); + }); + }); // task.images is the original input/reference image list in Popiserver task // payloads. Only use it as a defensive fallback for older responses that do // not put generated images under resultList. - const images = [ - ...new Set(resultImages.length > 0 ? resultImages : collectStrings(task.images ?? [])), - ]; + return images.length > 0 - ? { success: true, outputs: images.map((url) => ({ type: "image", data: "", url })) } + ? { success: true, outputs: images.map((img) => ({ type: "image", data: "", url: img.image, previewImg: img.previewImg })) } : { success: false, error: "PopiServer image task completed without output" }; } diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 8e6b1a6e..b7fb8fc6 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -12,7 +12,7 @@ */ import { NextRequest, NextResponse } from "next/server"; import { GenerateRequest, GenerateResponse, ModelType, SelectedModel, ProviderType } from "@/types"; -import { GenerationInput, ModelCapability } from "@/lib/providers/types"; +import { GenerationInput, GenerationOutput, GenerationOutputItem, ModelCapability } from "@/lib/providers/types"; import { generateWithGemini, generateWithGeminiVideo } from "./providers/gemini"; import { generateWithReplicate } from "./providers/replicate"; import { clearFalInputMappingCache as _clearFalInputMappingCache, generateWithFalQueue } from "./providers/fal"; @@ -111,8 +111,8 @@ function requireSelectedModel( } export function buildMediaResponse( - output: { type: string; data: string; url?: string }, - outputs?: Array<{ type: string; data: string; url?: string }> + output: GenerationOutputItem, + outputs?: GenerationOutput["outputs"] ): NextResponse { if (output.type === "3d") { return NextResponse.json({ @@ -142,15 +142,21 @@ export function buildMediaResponse( }); } - const image = output.data || output.url; + const image = output.data || output.url || ""; const images = outputs ?.filter((item) => item.type === "image") - .map((item) => item.data || item.url) - .filter((item): item is string => typeof item === "string" && item.length > 0); + .map((item) => ({ + url: item.data || item.url || "", + previewImg: item.previewImg, + })) + .filter((item) => item.url.length > 0); return NextResponse.json({ success: true, - image, + image: { + url: image, + previewImg: output.previewImg, + }, ...(images && images.length > 1 ? { images } : {}), contentType: "image", }); diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index c7535174..3143482b 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -122,7 +122,8 @@ type NanoBananaNodeType = Node; export function GenerateImageNode({ id, data, selected }: NodeProps) { const nodeData = data; const { t } = useI18n(); - const adaptiveOutputImage = useAdaptiveImageSrc(data.outputImage, id); + const displayImage = data.previewImg || data.outputImage; + const adaptiveOutputImage = useAdaptiveImageSrc(displayImage, id); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const popiserverImageModels = useModelStore((state) => state.byKind.image); const generationsPath = useWorkflowStore((state) => state.generationsPath); @@ -401,7 +402,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { - updateNodeData(id, { outputImage: null, status: "idle", error: null }); + updateNodeData(id, { outputImage: null, previewImg: undefined, status: "idle", error: null }); }, [id, updateNodeData]); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); @@ -464,6 +465,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps {/* Preview area */} - {nodeData.outputImage ? ( + {displayImage ? ( <> Generated preview setIsPreviewOpen(false)} diff --git a/src/lib/providers/types.ts b/src/lib/providers/types.ts index 5c84fd15..09bc44b7 100644 --- a/src/lib/providers/types.ts +++ b/src/lib/providers/types.ts @@ -109,18 +109,22 @@ export interface GenerationInput { /** * Unified output format for generation results */ -export interface GenerationOutput { - /** Whether the generation succeeded */ - success: boolean; - /** Generated outputs (images, videos, or audio) */ - outputs?: Array<{ + +export interface GenerationOutputItem { /** Type of output */ type: "image" | "video" | "3d" | "audio"; /** Base64 data URL of the output (empty string for 3D/large video URL-only responses) */ data: string; /** Original URL if applicable (e.g., from provider CDN) */ url?: string; - }>; + + previewImg?: string; // 为图片时封面图片 + } +export interface GenerationOutput { + /** Whether the generation succeeded */ + success: boolean; + /** Generated outputs (images, videos, or audio) */ + outputs?: Array; /** Error message if success is false */ error?: string; } diff --git a/src/store/execution/generateVideoExecutor.ts b/src/store/execution/generateVideoExecutor.ts index 4d6b04de..3d7d2368 100644 --- a/src/store/execution/generateVideoExecutor.ts +++ b/src/store/execution/generateVideoExecutor.ts @@ -336,8 +336,9 @@ export async function executeGenerateVideo( // Handle video response (video or videoUrl field) const videoData = result.video || result.videoUrl; - if (result.success && (videoData || result.image)) { - const outputContent = videoData || result.image; + const imageData = result.image?.url; + if (result.success && (videoData || imageData)) { + const outputContent = videoData || imageData; const timestamp = Date.now(); const videoId = `${timestamp}`; const usedModelId = modelToUse.modelId || ""; @@ -383,7 +384,7 @@ export async function executeGenerateVideo( if (generationsPath) { const saveContent = videoData ? { video: videoData } - : { image: result.image }; + : { image: imageData }; const savePromise = fetch("/api/save-generation", { method: "POST", diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 44144eca..b4d09c99 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -234,6 +234,7 @@ export async function executeNanoBanana( const generatedItems: Array<{ id: string; image: string; + previewImg?: string; timestamp: number; }> = []; let partialError: string | null = null; @@ -282,10 +283,9 @@ export async function executeNanoBanana( signal, }); } - - const resultImages: string[] = result.success && Array.isArray(result.images) && result.images.length > 0 - ? result.images.filter((image: unknown): image is string => typeof image === "string" && image.length > 0) - : result.success && typeof result.image === "string" && result.image.length > 0 + const resultImages = result.success && Array.isArray(result.images) && result.images.length > 0 + ? result.images + : result.success && result.image ? [result.image] : []; @@ -298,7 +298,8 @@ export async function executeNanoBanana( : `${batchTimestamp}-${itemIndex + 1}`; generatedItems.push({ id: imageId, - image, + image: image.url, + previewImg: image.previewImg, timestamp, }); }); @@ -334,6 +335,7 @@ export async function executeNanoBanana( const newHistoryItems = generatedItems.map((item) => ({ id: item.id, image: item.image, + previewImg: item.previewImg, timestamp: item.timestamp, prompt: finalPrompt, aspectRatio: nodeConfig.aspectRatio, @@ -345,6 +347,7 @@ export async function executeNanoBanana( updateNodeData(node.id, { outputImage: generatedItems[0].image, + previewImg: generatedItems[0].previewImg, outputImageRef: undefined, outputImageStorageStatus: isHttpMediaUrl(generatedItems[0].image) ? "remote-only" : "localized", status: "complete", diff --git a/src/types/api.ts b/src/types/api.ts index bbfc57a1..cc6ca93c 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -23,8 +23,14 @@ export interface GenerateRequest { export interface GenerateResponse { success: boolean; requestId?: string; - image?: string; - images?: string[]; + image?: { + url: string; // For large images, return URL directly + previewImg?: string; // Base64 data URL for smaller images + }; + images?: { + url: string; // For large images, return URL directly + previewImg?: string; // Base64 data URL for smaller images + }[]; video?: string; videoUrl?: string; // For large videos, return URL directly audio?: string; // Base64 audio data diff --git a/src/types/nodes.ts b/src/types/nodes.ts index ef0f54b1..8cbed1e7 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -162,6 +162,7 @@ export interface ImageHistoryItem { export interface CarouselImageItem { id: string; image?: string; // Current-session fallback; stripped when externalizing workflow media + previewImg?: string; timestamp: number; prompt: string; aspectRatio: AspectRatio; @@ -204,6 +205,7 @@ export interface NanoBananaNodeData extends BaseNodeData { inputImageRefs?: string[]; // External image references for storage optimization inputPrompt: string | null; outputImage: string | null; + previewImg?: string; outputImageRef?: string; // External image reference for storage optimization outputImageStorageStatus?: "remote-only" | "localizing" | "localized" | "missing"; aspectRatio: AspectRatio;