Spaces:
Running
Running
File size: 6,758 Bytes
dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 dbc2c2a f09b9f0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
/**
* 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 });
}
}
|