Spaces:
Running
Running
push
Browse files- fashionclip +1 -0
- package-lock.json +12 -0
- package.json +2 -0
- src/routes/upload.ts +10 -5
- src/utils/hfClient.ts +48 -36
- 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
|
| 6 |
|
| 7 |
-
export
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
"jewelry", "necklace", "bracelet", "earrings", "ring",
|
| 25 |
-
// Styles
|
| 26 |
-
"formal", "casual", "streetwear", "sportswear", "business", "elegant"
|
| 27 |
-
],
|
| 28 |
-
},
|
| 29 |
-
};
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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,
|
| 41 |
-
headers
|
| 42 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
continue;
|
| 53 |
}
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 1 |
+
Subproject commit b986d5b01afb828f04a613c5d521d90048813935
|