Spaces:
Running
Running
Update src/services/geminiService.ts
Browse files- src/services/geminiService.ts +30 -69
src/services/geminiService.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { Product, ProductCategory } from "../types";
|
|
| 3 |
|
| 4 |
// Helper to clean base64 string
|
| 5 |
const cleanBase64 = (data: string) => {
|
| 6 |
-
|
|
|
|
| 7 |
};
|
| 8 |
|
| 9 |
const getMimeType = (data: string) => {
|
|
@@ -55,7 +56,7 @@ const urlToBase64 = async (url: string): Promise<string> => {
|
|
| 55 |
export const generateTryOn = async (userImageBase64: string, product: Product): Promise<string> => {
|
| 56 |
const apiKey = process.env.API_KEY;
|
| 57 |
if (!apiKey) {
|
| 58 |
-
throw new Error("API Key is missing. Please
|
| 59 |
}
|
| 60 |
|
| 61 |
const ai = new GoogleGenAI({ apiKey });
|
|
@@ -69,81 +70,28 @@ export const generateTryOn = async (userImageBase64: string, product: Product):
|
|
| 69 |
throw new Error("Could not load product image. Please try another item.");
|
| 70 |
}
|
| 71 |
|
| 72 |
-
//
|
| 73 |
-
//
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
const basePrompt = `Act as an advanced Virtual Try-On (VTON) AI model.
|
| 77 |
-
|
| 78 |
-
TASK:
|
| 79 |
-
Synthesize a photorealistic image of the USER (Image A) wearing the GARMENT (Image B).
|
| 80 |
-
|
| 81 |
-
STRICT PROCESSING STEPS:
|
| 82 |
-
1. SEGMENTATION: Identify the user's body parts relevant to the garment (e.g., torso for shirts, legs for pants, face for glasses).
|
| 83 |
-
2. EXTRACTION: Extract the garment from Image B, completely ignoring any white background, hangers, or mannequins.
|
| 84 |
-
3. WARPING: Warp the extracted garment to match the user's body pose, rotation, and shape in Image A.
|
| 85 |
-
- If the user is turning, the garment must turn.
|
| 86 |
-
- If the user has arms crossed, the garment must fold realistically.
|
| 87 |
-
4. COMPOSITING: Blend the warped garment onto the user.
|
| 88 |
-
- Apply lighting from the user's environment to the garment.
|
| 89 |
-
- Cast shadows from the garment onto the user's skin/body to create depth.
|
| 90 |
|
| 91 |
-
|
| 92 |
-
- PRESERVE FACE IDENTITY: Do not modify the user's facial features.
|
| 93 |
-
- PRESERVE BACKGROUND: Do not change the background behind the user.
|
| 94 |
-
- REALISM: The output must look like a photograph, not a photoshop cut-paste.
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
`;
|
| 98 |
|
| 99 |
-
let categoryInstruction = "";
|
| 100 |
-
switch (product.category) {
|
| 101 |
-
case ProductCategory.EYEWEAR:
|
| 102 |
-
categoryInstruction = `Category: EYEWEAR.
|
| 103 |
-
- Fit: Align perfectly with the eyes and nose bridge.
|
| 104 |
-
- Scale: Ensure the glasses width matches the face width.
|
| 105 |
-
- Reality: If the glasses are sunglasses, add reflections. If clear, show the eyes through the lenses.`;
|
| 106 |
-
break;
|
| 107 |
-
case ProductCategory.NECKLACE:
|
| 108 |
-
categoryInstruction = `Category: JEWELRY.
|
| 109 |
-
- Fit: Drape the necklace around the neck curve. Gravity is important.
|
| 110 |
-
- Layering: It must sit ON TOP of skin or the shirt the user is already wearing (unless it's a shirt try-on, then on the new shirt).`;
|
| 111 |
-
break;
|
| 112 |
-
case ProductCategory.HEADWEAR:
|
| 113 |
-
categoryInstruction = `Category: HAT.
|
| 114 |
-
- Fit: Sit naturally on the crown of the head.
|
| 115 |
-
- Occlusion: If the user has hair volume, the hat should compress it slightly or sit behind fringe.`;
|
| 116 |
-
break;
|
| 117 |
-
case ProductCategory.SHIRT:
|
| 118 |
-
categoryInstruction = `Category: UPPER BODY GARMENT.
|
| 119 |
-
- Action: Completely replace the user's current top.
|
| 120 |
-
- Sleeves: Match the sleeve length of the target garment. If the user's arm pose is complex, warp the sleeves accordingly.
|
| 121 |
-
- Tucking: Determine if a tuck is natural based on the user's pose.`;
|
| 122 |
-
break;
|
| 123 |
-
case ProductCategory.PANTS:
|
| 124 |
-
categoryInstruction = `Category: LOWER BODY GARMENT.
|
| 125 |
-
- Action: Replace the user's pants/skirt.
|
| 126 |
-
- Waist: Align with the natural waistline.`;
|
| 127 |
-
break;
|
| 128 |
-
default:
|
| 129 |
-
categoryInstruction = `Category: FASHION ITEM. Wear it naturally.`;
|
| 130 |
-
break;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
const fullPrompt = `${basePrompt}\n${categoryInstruction}`;
|
| 134 |
-
|
| 135 |
const userMime = getMimeType(userImageBase64);
|
| 136 |
const productMime = getMimeType(productBase64);
|
| 137 |
|
| 138 |
try {
|
| 139 |
-
// gemini-2.5-flash-image is the recommended model for high-fidelity image manipulation tasks
|
| 140 |
const response = await ai.models.generateContent({
|
| 141 |
model: 'gemini-2.5-flash-image',
|
| 142 |
contents: {
|
| 143 |
parts: [
|
| 144 |
-
{
|
| 145 |
-
text: fullPrompt
|
| 146 |
-
},
|
| 147 |
{
|
| 148 |
inlineData: {
|
| 149 |
mimeType: userMime,
|
|
@@ -160,19 +108,32 @@ export const generateTryOn = async (userImageBase64: string, product: Product):
|
|
| 160 |
}
|
| 161 |
});
|
| 162 |
|
| 163 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
if (parts) {
|
| 165 |
for (const part of parts) {
|
| 166 |
if (part.inlineData && part.inlineData.data) {
|
| 167 |
return `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`;
|
| 168 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
}
|
| 171 |
|
| 172 |
-
throw new Error("
|
| 173 |
|
| 174 |
-
} catch (error) {
|
| 175 |
console.error("Gemini API Error:", error);
|
| 176 |
-
|
|
|
|
| 177 |
}
|
| 178 |
};
|
|
|
|
| 3 |
|
| 4 |
// Helper to clean base64 string
|
| 5 |
const cleanBase64 = (data: string) => {
|
| 6 |
+
// Use regex with case-insensitive flag
|
| 7 |
+
return data.replace(/^data:image\/\w+;base64,/i, '');
|
| 8 |
};
|
| 9 |
|
| 10 |
const getMimeType = (data: string) => {
|
|
|
|
| 56 |
export const generateTryOn = async (userImageBase64: string, product: Product): Promise<string> => {
|
| 57 |
const apiKey = process.env.API_KEY;
|
| 58 |
if (!apiKey) {
|
| 59 |
+
throw new Error("API Key is missing. Please check your settings.");
|
| 60 |
}
|
| 61 |
|
| 62 |
const ai = new GoogleGenAI({ apiKey });
|
|
|
|
| 70 |
throw new Error("Could not load product image. Please try another item.");
|
| 71 |
}
|
| 72 |
|
| 73 |
+
// Simplified prompt focused on the visual task to avoid safety filter triggers
|
| 74 |
+
// and improve adherence to the "editing" capability of the model.
|
| 75 |
+
const prompt = `Update the first image to show the person wearing the ${product.name} shown in the second image.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
Product Category: ${product.category}
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
Instructions:
|
| 80 |
+
- The item should fit the person naturally based on their pose.
|
| 81 |
+
- Maintain the original lighting, shadows, and background.
|
| 82 |
+
- Do not change the person's identity or facial features (other than adding eyewear if applicable).
|
| 83 |
+
- High photorealism.
|
| 84 |
`;
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const userMime = getMimeType(userImageBase64);
|
| 87 |
const productMime = getMimeType(productBase64);
|
| 88 |
|
| 89 |
try {
|
|
|
|
| 90 |
const response = await ai.models.generateContent({
|
| 91 |
model: 'gemini-2.5-flash-image',
|
| 92 |
contents: {
|
| 93 |
parts: [
|
| 94 |
+
{ text: prompt },
|
|
|
|
|
|
|
| 95 |
{
|
| 96 |
inlineData: {
|
| 97 |
mimeType: userMime,
|
|
|
|
| 108 |
}
|
| 109 |
});
|
| 110 |
|
| 111 |
+
const candidate = response.candidates?.[0];
|
| 112 |
+
|
| 113 |
+
// Check for safety blocks or other finish reasons
|
| 114 |
+
if (candidate?.finishReason && candidate.finishReason !== 'STOP') {
|
| 115 |
+
throw new Error(`Generation blocked. Reason: ${candidate.finishReason}`);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const parts = candidate?.content?.parts;
|
| 119 |
if (parts) {
|
| 120 |
for (const part of parts) {
|
| 121 |
if (part.inlineData && part.inlineData.data) {
|
| 122 |
return `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`;
|
| 123 |
}
|
| 124 |
+
// If we got text back instead of an image, it might be an explanation of refusal
|
| 125 |
+
if (part.text) {
|
| 126 |
+
console.warn("Model returned text:", part.text);
|
| 127 |
+
// Don't throw immediately, look for other parts, but if only text remains...
|
| 128 |
+
}
|
| 129 |
}
|
| 130 |
}
|
| 131 |
|
| 132 |
+
throw new Error("The AI did not generate an image. Please try a different photo.");
|
| 133 |
|
| 134 |
+
} catch (error: any) {
|
| 135 |
console.error("Gemini API Error:", error);
|
| 136 |
+
// Propagate the specific error message to the UI
|
| 137 |
+
throw new Error(error.message || "Failed to connect to AI service");
|
| 138 |
}
|
| 139 |
};
|