import axios from "axios"; import FormData from "form-data"; import dotenv from "dotenv"; dotenv.config(); const FASHION_CLIP_URL = process.env.FASHION_CLIP_URL || "https://nexusbert-fashionclip.hf.space"; export interface ClassificationResult { label: string; score: number; } export interface IdentifyResult { label: string; score: number; type: "brand" | "item" | "color" | "accessory" | "combined"; } export interface IdentifyResponse { precise_identification: IdentifyResult[]; detected_components: { top_colors: Array<{ color: string; score: number }>; top_items: Array<{ item: string; score: number }>; top_accessories: Array<{ accessory: string; score: number }>; top_brands: Array<{ brand: string; score: number }>; }; } /** * Classify a fashion image using the FashionCLIP /classify endpoint * @param fileBuffer - The image file buffer * @param filename - The original filename * @param mimetype - The file MIME type * @returns Array of classification results sorted by score (highest first) */ export async function classifyFashionImage( fileBuffer: Buffer, filename: string, mimetype: string ): Promise { const endpoint = `${FASHION_CLIP_URL}/classify`; // Create form data with file const formData = new FormData(); formData.append("file", fileBuffer, { filename: filename, contentType: mimetype, }); let lastError: any = null; const maxAttempts = 2; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const response = await axios.post(endpoint, formData, { headers: { ...formData.getHeaders(), }, timeout: 60000, // 60 second timeout for model inference validateStatus: (status) => status >= 200 && status < 300, }); // Response is already an array of {label, score} objects return response.data; } catch (error: any) { lastError = error; const status = error?.response?.status; // Retry once for transient errors if (attempt < maxAttempts && (status === 429 || status === 503 || status === 502)) { console.log(`FashionCLIP classify API returned ${status}. Retrying...`); await new Promise((r) => setTimeout(r, 2000)); continue; } // Provide clearer error messages if (error?.response?.data?.detail) { throw new Error(`FashionCLIP classify API error: ${error.response.data.detail}`); } if (error?.code === "ECONNREFUSED" || error?.code === "ETIMEDOUT") { throw new Error("Unable to connect to FashionCLIP service. Please try again later."); } throw error; } } throw lastError; } export async function identifyFashionItem( fileBuffer: Buffer, filename: string, mimetype: string, topK: number = 5 ): Promise { const clampedTopK = Math.max(1, Math.min(20, topK)); const endpoint = `${FASHION_CLIP_URL}/identify?top_k=${clampedTopK}`; const formData = new FormData(); formData.append("file", fileBuffer, { filename: filename, contentType: mimetype, }); let lastError: any = null; const maxAttempts = 2; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const response = await axios.post(endpoint, formData, { headers: { ...formData.getHeaders(), }, timeout: 60000, validateStatus: (status) => status >= 200 && status < 300, }); return response.data; } catch (error: any) { lastError = error; const status = error?.response?.status; if (attempt < maxAttempts && (status === 429 || status === 503 || status === 502)) { console.log(`FashionCLIP identify API returned ${status}. Retrying...`); await new Promise((r) => setTimeout(r, 2000)); continue; } if (error?.response?.data?.detail) { throw new Error(`FashionCLIP identify API error: ${error.response.data.detail}`); } if (error?.code === "ECONNREFUSED" || error?.code === "ETIMEDOUT") { throw new Error("Unable to connect to FashionCLIP service. Please try again later."); } throw error; } } throw lastError; }