nexusbert's picture
push
016cbbb
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;
}