Spaces:
Running
Running
| import { GoogleGenAI } from "@google/genai"; | |
| import { Product } from "../types"; | |
| // Micro-Resolution: 192px is extremely small but sufficient for many VLM tasks. | |
| // Using 0.15 quality to minimize the token payload significantly. | |
| const resizeImage = async (base64Str: string, maxWidth = 192): Promise<string> => { | |
| return new Promise((resolve) => { | |
| const img = new Image(); | |
| img.src = base64Str; | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > height) { | |
| if (width > maxWidth) { | |
| height *= maxWidth / width; | |
| width = maxWidth; | |
| } | |
| } else { | |
| if (height > maxWidth) { | |
| width *= maxWidth / height; | |
| height = maxWidth; | |
| } | |
| } | |
| canvas.width = width; | |
| canvas.height = height; | |
| const ctx = canvas.getContext('2d'); | |
| // Low quality (0.15) for maximum token savings | |
| ctx?.drawImage(img, 0, 0, width, height); | |
| resolve(canvas.toDataURL('image/jpeg', 0.15)); | |
| }; | |
| }); | |
| }; | |
| const cleanBase64 = (data: string) => { | |
| return data.replace(/^data:image\/\w+;base64,/i, ''); | |
| }; | |
| const urlToBase64 = async (url: string): Promise<string> => { | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`H:${response.status}`); | |
| const blob = await response.blob(); | |
| return await new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => resolve(reader.result as string); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| } catch (error) { | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.crossOrigin = "Anonymous"; | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) return reject(new Error("C-Fail")); | |
| ctx.drawImage(img, 0, 0); | |
| try { resolve(canvas.toDataURL('image/jpeg')); } catch (e) { reject(e); } | |
| }; | |
| img.onerror = () => reject(new Error("L-Fail")); | |
| img.src = url; | |
| }); | |
| } | |
| }; | |
| export const generateTryOn = async (userImageBase64: string, product: Product): Promise<string> => { | |
| const apiKey = process.env.API_KEY; | |
| if (!apiKey) throw new Error("API Key config error."); | |
| const ai = new GoogleGenAI({ apiKey: apiKey }); | |
| // 1. Process images to micro resolution | |
| const productRaw = await urlToBase64(product.imageUrl); | |
| const [processedUserImg, processedProductImg] = await Promise.all([ | |
| resizeImage(userImageBase64, 192), | |
| resizeImage(productRaw, 192) | |
| ]); | |
| // Prompt reduced to the absolute minimum to save input tokens | |
| const prompt = `Put ${product.name} from img 2 on person in img 1.`; | |
| try { | |
| const response = await ai.models.generateContent({ | |
| model: 'gemini-2.5-flash-image', | |
| contents: { | |
| parts: [ | |
| { text: prompt }, | |
| { inlineData: { mimeType: 'image/jpeg', data: cleanBase64(processedUserImg) } }, | |
| { inlineData: { mimeType: 'image/jpeg', data: cleanBase64(processedProductImg) } } | |
| ] | |
| } | |
| }); | |
| const candidate = response.candidates?.[0]; | |
| if (candidate?.finishReason && candidate.finishReason !== 'STOP') { | |
| throw new Error(`Fin:${candidate.finishReason}`); | |
| } | |
| const part = candidate?.content?.parts?.find(p => p.inlineData); | |
| if (part?.inlineData?.data) { | |
| return `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`; | |
| } | |
| throw new Error("Empty AI response."); | |
| } catch (error: any) { | |
| const msg = error.message || ""; | |
| if (msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED")) { | |
| throw new Error("FREE_LIMIT_HIT: Please wait for the cooldown timer."); | |
| } | |
| throw error; | |
| } | |
| }; |