nexusbert commited on
Commit
cd39fb1
·
1 Parent(s): 9f9a4dd
Files changed (6) hide show
  1. fashionclip +1 -0
  2. package-lock.json +12 -0
  3. package.json +2 -0
  4. src/routes/upload.ts +10 -5
  5. src/utils/hfClient.ts +48 -36
  6. stylegptUI +1 -1
fashionclip ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 781e5c346788713ee8086ee68557c3b44909010d
package-lock.json CHANGED
@@ -15,6 +15,7 @@
15
  "cors": "^2.8.5",
16
  "dotenv": "^17.2.3",
17
  "express": "^5.1.0",
 
18
  "jsonwebtoken": "^9.0.2",
19
  "multer": "^1.4.5-lts.1",
20
  "pg": "^8.16.3",
@@ -27,6 +28,7 @@
27
  "@types/bcrypt": "^5.0.2",
28
  "@types/cors": "^2.8.19",
29
  "@types/express": "^5.0.5",
 
30
  "@types/jsonwebtoken": "^9.0.10",
31
  "@types/multer": "^1.4.12",
32
  "@types/node": "^24.9.2",
@@ -258,6 +260,16 @@
258
  "@types/send": "*"
259
  }
260
  },
 
 
 
 
 
 
 
 
 
 
261
  "node_modules/@types/http-errors": {
262
  "version": "2.0.5",
263
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
 
15
  "cors": "^2.8.5",
16
  "dotenv": "^17.2.3",
17
  "express": "^5.1.0",
18
+ "form-data": "^4.0.0",
19
  "jsonwebtoken": "^9.0.2",
20
  "multer": "^1.4.5-lts.1",
21
  "pg": "^8.16.3",
 
28
  "@types/bcrypt": "^5.0.2",
29
  "@types/cors": "^2.8.19",
30
  "@types/express": "^5.0.5",
31
+ "@types/form-data": "^2.5.0",
32
  "@types/jsonwebtoken": "^9.0.10",
33
  "@types/multer": "^1.4.12",
34
  "@types/node": "^24.9.2",
 
260
  "@types/send": "*"
261
  }
262
  },
263
+ "node_modules/@types/form-data": {
264
+ "version": "2.5.2",
265
+ "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.5.2.tgz",
266
+ "integrity": "sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==",
267
+ "deprecated": "This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.",
268
+ "dev": true,
269
+ "dependencies": {
270
+ "form-data": "*"
271
+ }
272
+ },
273
  "node_modules/@types/http-errors": {
274
  "version": "2.0.5",
275
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
package.json CHANGED
@@ -24,6 +24,7 @@
24
  "cors": "^2.8.5",
25
  "dotenv": "^17.2.3",
26
  "express": "^5.1.0",
 
27
  "jsonwebtoken": "^9.0.2",
28
  "multer": "^1.4.5-lts.1",
29
  "pg": "^8.16.3",
@@ -36,6 +37,7 @@
36
  "@types/bcrypt": "^5.0.2",
37
  "@types/cors": "^2.8.19",
38
  "@types/express": "^5.0.5",
 
39
  "@types/jsonwebtoken": "^9.0.10",
40
  "@types/multer": "^1.4.12",
41
  "@types/node": "^24.9.2",
 
24
  "cors": "^2.8.5",
25
  "dotenv": "^17.2.3",
26
  "express": "^5.1.0",
27
+ "form-data": "^4.0.0",
28
  "jsonwebtoken": "^9.0.2",
29
  "multer": "^1.4.5-lts.1",
30
  "pg": "^8.16.3",
 
37
  "@types/bcrypt": "^5.0.2",
38
  "@types/cors": "^2.8.19",
39
  "@types/express": "^5.0.5",
40
+ "@types/form-data": "^2.5.0",
41
  "@types/jsonwebtoken": "^9.0.10",
42
  "@types/multer": "^1.4.12",
43
  "@types/node": "^24.9.2",
src/routes/upload.ts CHANGED
@@ -101,19 +101,24 @@ router.post("/", authenticateToken, upload.array("image", 20), async (req: AuthR
101
  try {
102
  console.log(`Processing image ${i + 1}/${files.length}: ${file.originalname}`);
103
 
 
 
 
 
 
 
 
 
 
104
  // Convert buffer to data URI for Cloudinary
105
  const base64Image = `data:${file.mimetype};base64,${file.buffer.toString("base64")}`;
106
 
107
- // Upload image to Cloudinary
108
  const uploadResult = await cloudinary.uploader.upload(base64Image, {
109
  folder: "wardrobe",
110
  resource_type: "image",
111
  });
112
 
113
- // Classify using Fashion-CLIP (one by one)
114
- const fashionResult = await classifyFashionImage(uploadResult.secure_url);
115
- const bestLabel = fashionResult[0]?.label || "unknown";
116
-
117
  // Determine style based on category and label
118
  let style = "casual";
119
  const labelLower = bestLabel.toLowerCase();
 
101
  try {
102
  console.log(`Processing image ${i + 1}/${files.length}: ${file.originalname}`);
103
 
104
+ // Classify using Fashion-CLIP first (before uploading to Cloudinary)
105
+ const fashionResult = await classifyFashionImage(
106
+ file.buffer,
107
+ file.originalname,
108
+ file.mimetype
109
+ );
110
+ const bestLabel = fashionResult[0]?.label || "unknown";
111
+ console.log(`Classification result: ${bestLabel} (score: ${fashionResult[0]?.score?.toFixed(4)})`);
112
+
113
  // Convert buffer to data URI for Cloudinary
114
  const base64Image = `data:${file.mimetype};base64,${file.buffer.toString("base64")}`;
115
 
116
+ // Upload image to Cloudinary for storage
117
  const uploadResult = await cloudinary.uploader.upload(base64Image, {
118
  folder: "wardrobe",
119
  resource_type: "image",
120
  });
121
 
 
 
 
 
122
  // Determine style based on category and label
123
  let style = "casual";
124
  const labelLower = bestLabel.toLowerCase();
src/utils/hfClient.ts CHANGED
@@ -1,63 +1,75 @@
1
  import axios from "axios";
 
2
  import dotenv from "dotenv";
3
  dotenv.config();
4
 
5
- const HF_API_KEY = process.env.HF_API_KEY;
6
 
7
- export async function classifyFashionImage(imageUrl: string) {
8
- // Use new Router Inference API endpoint
9
- const endpoint = "https://router.huggingface.co/hf-inference/models/patrickjohncyh/fashion-clip";
 
10
 
11
- const payload = {
12
- inputs: imageUrl,
13
- parameters: {
14
- candidate_labels: [
15
- // Clothing
16
- "shirt", "pants", "dress", "jacket", "blazer", "t-shirt", "hoodie", "sweater",
17
- "jeans", "shorts", "skirt", "suit", "coat", "cardigan", "vest",
18
- // Footwear
19
- "shoes", "sneakers", "boots", "sandals", "heels", "flats", "loafers", "slippers",
20
- // Accessories
21
- "watch", "wristwatch", "glasses", "sunglasses", "eyeglasses",
22
- "belt", "bag", "handbag", "backpack", "purse", "wallet",
23
- "hat", "cap", "beanie", "scarf", "tie", "bow tie",
24
- "jewelry", "necklace", "bracelet", "earrings", "ring",
25
- // Styles
26
- "formal", "casual", "streetwear", "sportswear", "business", "elegant"
27
- ],
28
- },
29
- };
30
 
31
- const headers = {
32
- Authorization: `Bearer ${HF_API_KEY}`,
33
- "Content-Type": "application/json",
34
- };
 
 
35
 
36
  let lastError: any = null;
37
  const maxAttempts = 2;
 
38
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
39
  try {
40
- const response = await axios.post(endpoint, payload, {
41
- headers,
42
- timeout: 15000,
 
 
43
  validateStatus: (status) => status >= 200 && status < 300,
44
  });
 
 
45
  return response.data;
46
  } catch (error: any) {
47
  lastError = error;
48
  const status = error?.response?.status;
49
- // Retry once for transient/transition statuses
50
- if (attempt < maxAttempts && (status === 429 || status === 503)) {
51
- await new Promise((r) => setTimeout(r, 1500));
 
 
52
  continue;
53
  }
54
- // Provide clearer message when old endpoint deprecation surfaces from proxies
55
- if (status === 410 || error?.response?.data?.error?.includes("no longer supported")) {
56
- throw new Error("Hugging Face API endpoint changed. Please try again in a moment.");
 
 
 
 
 
57
  }
 
58
  throw error;
59
  }
60
  }
 
61
  throw lastError;
62
  }
63
 
 
1
  import axios from "axios";
2
+ import FormData from "form-data";
3
  import dotenv from "dotenv";
4
  dotenv.config();
5
 
6
+ const FASHION_CLIP_URL = process.env.FASHION_CLIP_URL || "https://nexusbert-fashionclip.hf.space";
7
 
8
+ export interface ClassificationResult {
9
+ label: string;
10
+ score: number;
11
+ }
12
 
13
+ /**
14
+ * Classify a fashion image using the FashionCLIP API
15
+ * @param fileBuffer - The image file buffer
16
+ * @param filename - The original filename
17
+ * @param mimetype - The file MIME type
18
+ * @returns Array of classification results sorted by score (highest first)
19
+ */
20
+ export async function classifyFashionImage(
21
+ fileBuffer: Buffer,
22
+ filename: string,
23
+ mimetype: string
24
+ ): Promise<ClassificationResult[]> {
25
+ const endpoint = `${FASHION_CLIP_URL}/classify`;
 
 
 
 
 
 
26
 
27
+ // Create form data with file
28
+ const formData = new FormData();
29
+ formData.append("file", fileBuffer, {
30
+ filename: filename,
31
+ contentType: mimetype,
32
+ });
33
 
34
  let lastError: any = null;
35
  const maxAttempts = 2;
36
+
37
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
38
  try {
39
+ const response = await axios.post<ClassificationResult[]>(endpoint, formData, {
40
+ headers: {
41
+ ...formData.getHeaders(),
42
+ },
43
+ timeout: 60000, // 60 second timeout for model inference
44
  validateStatus: (status) => status >= 200 && status < 300,
45
  });
46
+
47
+ // Response is already an array of {label, score} objects
48
  return response.data;
49
  } catch (error: any) {
50
  lastError = error;
51
  const status = error?.response?.status;
52
+
53
+ // Retry once for transient errors
54
+ if (attempt < maxAttempts && (status === 429 || status === 503 || status === 502)) {
55
+ console.log(`FashionCLIP API returned ${status}. Retrying...`);
56
+ await new Promise((r) => setTimeout(r, 2000));
57
  continue;
58
  }
59
+
60
+ // Provide clearer error messages
61
+ if (error?.response?.data?.detail) {
62
+ throw new Error(`FashionCLIP API error: ${error.response.data.detail}`);
63
+ }
64
+
65
+ if (error?.code === "ECONNREFUSED" || error?.code === "ETIMEDOUT") {
66
+ throw new Error("Unable to connect to FashionCLIP service. Please try again later.");
67
  }
68
+
69
  throw error;
70
  }
71
  }
72
+
73
  throw lastError;
74
  }
75
 
stylegptUI CHANGED
@@ -1 +1 @@
1
- Subproject commit cf8194c3d8c836c4a674be05d2fa2f00e3cce595
 
1
+ Subproject commit b986d5b01afb828f04a613c5d521d90048813935