Spaces:
Running
Running
/** | |
* API ROUTE: /api/merge (DEPRECATED - functionality moved to /api/process) | |
* | |
* Legacy endpoint for merging multiple character images into cohesive group photos. | |
* This functionality is now handled by the main /api/process endpoint with type="MERGE". | |
* Kept for backwards compatibility. | |
* | |
* Input: JSON with array of image URLs/data and optional custom prompt | |
* Output: JSON with merged group photo as base64 data URL | |
*/ | |
import { NextRequest, NextResponse } from "next/server"; | |
import { GoogleGenAI } from "@google/genai"; | |
// Configure Next.js runtime for Node.js (required for Google AI SDK) | |
export const runtime = "nodejs"; | |
/** | |
* Parse base64 data URL into MIME type and data components | |
* Handles data URLs in the format: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA... | |
* | |
* @param dataUrl Complete data URL string | |
* @returns Object with mimeType and data, or null if invalid format | |
*/ | |
function parseDataUrl(dataUrl: string): { mimeType: string; data: string } | null { | |
const match = dataUrl.match(/^data:(.*?);base64,(.*)$/); // Extract MIME type and base64 data | |
if (!match) return null; // Invalid data URL format | |
return { | |
mimeType: match[1] || "image/png", // Use extracted MIME type or default to PNG | |
data: match[2] // Base64 encoded image data | |
}; | |
} | |
/** | |
* Convert various image URL formats to inline data format required by Gemini AI | |
* | |
* Supports: | |
* - Data URLs (data:image/png;base64,...) | |
* - HTTP/HTTPS URLs (fetches and converts to base64) | |
* | |
* @param url Image URL in any supported format | |
* @returns Promise resolving to inline data object or null on failure | |
*/ | |
async function toInlineData(url: string): Promise<{ mimeType: string; data: string } | null> { | |
try { | |
// Handle data URLs directly | |
if (url.startsWith('data:')) { | |
return parseDataUrl(url); | |
} | |
// Handle HTTP URLs by fetching and converting to base64 | |
if (url.startsWith('http')) { | |
const res = await fetch(url); // Fetch image from URL | |
const buf = await res.arrayBuffer(); // Get binary data | |
const base64 = Buffer.from(buf).toString('base64'); // Convert to base64 | |
const mimeType = res.headers.get('content-type') || 'image/jpeg'; // Get MIME type from headers | |
return { mimeType, data: base64 }; | |
} | |
return null; // Unsupported URL format | |
} catch (e) { | |
console.error('Failed to process image URL:', url.substring(0, 100), e); | |
return null; // Return null on any processing error | |
} | |
} | |
export async function POST(req: NextRequest) { | |
try { | |
const body = (await req.json()) as { | |
images?: string[]; // data URLs | |
prompt?: string; | |
apiToken?: string; | |
}; | |
const imgs = body.images?.filter(Boolean) ?? []; | |
if (imgs.length < 2) { | |
return NextResponse.json( | |
{ error: "MERGE requires at least two images" }, | |
{ status: 400 } | |
); | |
} | |
// Use user-provided API token or fall back to environment variable | |
const apiKey = body.apiToken || process.env.GOOGLE_API_KEY; | |
if (!apiKey || apiKey === 'your_api_key_here') { | |
return NextResponse.json( | |
{ error: "API key not provided. Please enter your Hugging Face API token in the top right corner or add GOOGLE_API_KEY to .env.local file. Get your key from: https://aistudio.google.com/app/apikey" }, | |
{ status: 500 } | |
); | |
} | |
const ai = new GoogleGenAI({ apiKey }); | |
// Build parts array: first the text prompt, then image inlineData parts | |
// If no custom prompt, use default extraction-focused prompt | |
let prompt = body.prompt; | |
if (!prompt) { | |
prompt = `MERGE TASK: Create a natural, cohesive group photo combining ALL subjects from ${imgs.length} provided images. | |
CRITICAL REQUIREMENTS: | |
1. Extract ALL people/subjects from EACH image exactly as they appear | |
2. Place them together in a SINGLE UNIFIED SCENE with: | |
- Consistent lighting direction and color temperature | |
- Matching shadows and ambient lighting | |
- Proper scale relationships (realistic relative sizes) | |
- Natural spacing as if they were photographed together | |
- Shared environment/background that looks cohesive | |
3. Composition guidelines: | |
- Arrange subjects at similar depth (not one far behind another) | |
- Use natural group photo positioning (slight overlap is ok) | |
- Ensure all faces are clearly visible | |
- Create visual balance in the composition | |
- Apply consistent color grading across all subjects | |
4. Environmental unity: | |
- Use a single, coherent background for all subjects | |
- Match the perspective as if taken with one camera | |
- Ensure ground plane continuity (all standing on same level) | |
- Apply consistent atmospheric effects (if any) | |
The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`; | |
} else { | |
// Even with custom prompt, append cohesion requirements | |
const enforcement = `\n\nIMPORTANT: Create a COHESIVE group photo where all subjects appear to be in the same scene with consistent lighting, scale, and environment. The result should look naturally photographed together, not composited.`; | |
prompt = `${prompt}${enforcement}`; | |
} | |
// Debug: Log what we're receiving | |
const parts: any[] = [{ text: prompt }]; | |
for (const url of imgs) { | |
const parsed = await toInlineData(url); | |
if (!parsed) { | |
console.error('[MERGE API] Failed to parse image:', url.substring(0, 100)); | |
continue; | |
} | |
parts.push({ inlineData: { mimeType: parsed.mimeType, data: parsed.data } }); | |
} | |
const response = await ai.models.generateContent({ | |
model: "gemini-2.5-flash-image-preview", | |
contents: parts, | |
}); | |
const outParts = (response as any)?.candidates?.[0]?.content?.parts ?? []; | |
const images: string[] = []; | |
const texts: string[] = []; | |
for (const p of outParts) { | |
if (p?.inlineData?.data) { | |
images.push(`data:image/png;base64,${p.inlineData.data}`); | |
} else if (p?.text) { | |
texts.push(p.text); | |
} | |
} | |
if (!images.length) { | |
return NextResponse.json( | |
{ error: "Model returned no image", text: texts.join("\n") }, | |
{ status: 500 } | |
); | |
} | |
return NextResponse.json({ images, text: texts.join("\n") }); | |
} catch (err) { | |
console.error("/api/merge error", err); | |
return NextResponse.json({ error: "Failed to merge" }, { status: 500 }); | |
} | |
} | |