Browse Source

feat(05-02): convert large images to URLs with cleanup

- Import uploadImageForUrl, shouldUseImageUrl, deleteImages from @/lib/images
- Process images for Replicate/fal providers before generation
- Small images (<256KB) use data URIs directly
- Large images converted to temporary URLs via /api/images/[id]
- Cleanup uploaded images in finally block (success or failure)
- Gemini path unchanged (handles base64 directly)
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
2195a6ed9c
  1. 207
      src/app/api/generate/route.ts

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

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { GoogleGenAI } from "@google/genai"; import { GoogleGenAI } from "@google/genai";
import { GenerateRequest, GenerateResponse, ModelType, SelectedModel, ProviderType } from "@/types"; import { GenerateRequest, GenerateResponse, ModelType, SelectedModel, ProviderType } from "@/types";
import { GenerationInput, GenerationOutput, ProviderModel } from "@/lib/providers/types"; import { GenerationInput, GenerationOutput, ProviderModel } from "@/lib/providers/types";
import { uploadImageForUrl, shouldUseImageUrl, deleteImages } from "@/lib/images";
export const maxDuration = 300; // 5 minute timeout for API calls export const maxDuration = 300; // 5 minute timeout for API calls
export const dynamic = 'force-dynamic'; // Ensure this route is always dynamic export const dynamic = 'force-dynamic'; // Ensure this route is always dynamic
@ -567,96 +568,150 @@ export async function POST(request: NextRequest) {
); );
} }
// Build generation input // Process images: convert large images to URLs for provider to fetch
const genInput: GenerationInput = { const baseUrl = new URL(request.url).origin;
model: { const uploadedImageIds: string[] = [];
id: selectedModel!.modelId, const processedImages: string[] = [];
name: selectedModel!.displayName,
provider: "replicate", if (images && images.length > 0) {
capabilities: ["text-to-image"], for (const image of images) {
description: null, if (shouldUseImageUrl(image)) {
}, const { url, id } = uploadImageForUrl(image, baseUrl);
prompt, uploadedImageIds.push(id);
images, processedImages.push(url);
parameters, console.log(`[API:${requestId}] Converted large image to URL: ${url}`);
}; } else {
processedImages.push(image);
const result = await generateWithReplicate(requestId, replicateApiKey, genInput); console.log(`[API:${requestId}] Using base64 directly (small image)`);
}
if (!result.success) { }
return NextResponse.json<GenerateResponse>(
{
success: false,
error: result.error || "Generation failed",
},
{ status: 500 }
);
} }
// Return first output image try {
const outputImage = result.outputs?.[0]?.data; // Build generation input
if (!outputImage) { const genInput: GenerationInput = {
return NextResponse.json<GenerateResponse>( model: {
{ id: selectedModel!.modelId,
success: false, name: selectedModel!.displayName,
error: "No image in generation output", provider: "replicate",
capabilities: ["text-to-image"],
description: null,
}, },
{ status: 500 } prompt,
); images: processedImages,
} parameters,
};
const result = await generateWithReplicate(requestId, replicateApiKey, genInput);
if (!result.success) {
return NextResponse.json<GenerateResponse>(
{
success: false,
error: result.error || "Generation failed",
},
{ status: 500 }
);
}
// Return first output image
const outputImage = result.outputs?.[0]?.data;
if (!outputImage) {
return NextResponse.json<GenerateResponse>(
{
success: false,
error: "No image in generation output",
},
{ status: 500 }
);
}
return NextResponse.json<GenerateResponse>({ return NextResponse.json<GenerateResponse>({
success: true, success: true,
image: outputImage, image: outputImage,
}); });
} finally {
// Clean up uploaded images
if (uploadedImageIds.length > 0) {
deleteImages(uploadedImageIds);
console.log(`[API:${requestId}] Cleaned up ${uploadedImageIds.length} uploaded images`);
}
}
} }
if (provider === "fal") { if (provider === "fal") {
// Get fal.ai API key from request headers (optional - fal.ai works without key but rate limited) // Get fal.ai API key from request headers (optional - fal.ai works without key but rate limited)
const falApiKey = request.headers.get("X-Fal-API-Key"); const falApiKey = request.headers.get("X-Fal-API-Key");
// Build generation input // Process images: convert large images to URLs for provider to fetch
const genInput: GenerationInput = { const baseUrl = new URL(request.url).origin;
model: { const uploadedImageIds: string[] = [];
id: selectedModel!.modelId, const processedImages: string[] = [];
name: selectedModel!.displayName,
provider: "fal", if (images && images.length > 0) {
capabilities: ["text-to-image"], for (const image of images) {
description: null, if (shouldUseImageUrl(image)) {
}, const { url, id } = uploadImageForUrl(image, baseUrl);
prompt, uploadedImageIds.push(id);
images, processedImages.push(url);
parameters, console.log(`[API:${requestId}] Converted large image to URL: ${url}`);
}; } else {
processedImages.push(image);
const result = await generateWithFal(requestId, falApiKey, genInput); console.log(`[API:${requestId}] Using base64 directly (small image)`);
}
if (!result.success) { }
return NextResponse.json<GenerateResponse>(
{
success: false,
error: result.error || "Generation failed",
},
{ status: 500 }
);
} }
// Return first output image try {
const outputImage = result.outputs?.[0]?.data; // Build generation input
if (!outputImage) { const genInput: GenerationInput = {
return NextResponse.json<GenerateResponse>( model: {
{ id: selectedModel!.modelId,
success: false, name: selectedModel!.displayName,
error: "No image in generation output", provider: "fal",
capabilities: ["text-to-image"],
description: null,
}, },
{ status: 500 } prompt,
); images: processedImages,
} parameters,
};
const result = await generateWithFal(requestId, falApiKey, genInput);
if (!result.success) {
return NextResponse.json<GenerateResponse>(
{
success: false,
error: result.error || "Generation failed",
},
{ status: 500 }
);
}
// Return first output image
const outputImage = result.outputs?.[0]?.data;
if (!outputImage) {
return NextResponse.json<GenerateResponse>(
{
success: false,
error: "No image in generation output",
},
{ status: 500 }
);
}
return NextResponse.json<GenerateResponse>({ return NextResponse.json<GenerateResponse>({
success: true, success: true,
image: outputImage, image: outputImage,
}); });
} finally {
// Clean up uploaded images
if (uploadedImageIds.length > 0) {
deleteImages(uploadedImageIds);
console.log(`[API:${requestId}] Cleaned up ${uploadedImageIds.length} uploaded images`);
}
}
} }
// Default: Use Gemini // Default: Use Gemini

Loading…
Cancel
Save