Spaces:
Sleeping
Sleeping
| 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<ClassificationResult[]> { | |
| 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<ClassificationResult[]>(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<IdentifyResponse> { | |
| 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<IdentifyResponse>(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; | |
| } | |