File size: 3,921 Bytes
370c714
06f9bb6
370c714
6371453
 
 
62ad79c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6371453
62ad79c
6371453
62ad79c
 
 
 
370c714
4b7f4e3
370c714
 
 
 
 
6371453
370c714
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6371453
370c714
06f9bb6
370c714
6371453
370c714
 
 
 
 
 
 
6371453
370c714
6371453
370c714
6371453
81c9042
6371453
 
 
62ad79c
370c714
6371453
 
b41929f
 
6371453
 
 
 
 
 
 
 
 
 
 
 
 
 
4b7f4e3
 
6371453
06f9bb6
 
370c714
 
6371453
4b7f4e3
6371453
 
 
06f9bb6
6371453
370c714
 
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
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;
  }
};