Maaz Uddin commited on
Commit
e5eabef
·
1 Parent(s): 293a2a8

Add application file

Browse files
#jewelry_recommender_full.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ import torch
4
+ import torchvision.transforms as transforms
5
+
6
+ class Config:
7
+ """Configuration class for the Jewelry Recommender System."""
8
+
9
+ # Model settings
10
+ VECTOR_DIMENSION = 1280
11
+ INDEX_PATH = "rootdir/trained_models/jewelry_index.idx"
12
+ METADATA_PATH = "rootdir/trained_models/jewelry_metadata.pkl"
13
+
14
+ # Hardware settings
15
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+
17
+ # Image processing settings
18
+ IMAGE_SIZE = (640, 640)
19
+ NORMALIZATION_MEAN = [0.485, 0.456, 0.406]
20
+ NORMALIZATION_STD = [0.229, 0.224, 0.225]
21
+
22
+ # Recommendation settings
23
+ DEFAULT_NUM_RECOMMENDATIONS = 5
24
+ MAX_RECOMMENDATIONS = 20
25
+
26
+ @classmethod
27
+ def get_image_transform(cls):
28
+ """Returns the image transformation pipeline."""
29
+ from PIL import ImageOps
30
+ return transforms.Compose([
31
+ transforms.Lambda(lambda img: ImageOps.exif_transpose(img)),
32
+ transforms.Resize(cls.IMAGE_SIZE),
33
+ transforms.ToTensor(),
34
+ transforms.Normalize(
35
+ mean=cls.NORMALIZATION_MEAN,
36
+ std=cls.NORMALIZATION_STD
37
+ )
38
+ ])
39
+
40
+
41
+ # model_loader.py
42
+ import os
43
+ import pickle
44
+ import faiss
45
+ import torch
46
+ import torchvision.models as models
47
+ import warnings
48
+
49
+ class ModelLoader:
50
+ """Handles loading of the feature extraction model and FAISS index."""
51
+
52
+ @staticmethod
53
+ def load_feature_extraction_model():
54
+ """Loads and configures the EfficientNet model for feature extraction."""
55
+ print("Loading feature extraction model...")
56
+ model = models.efficientnet_b0(weights='EfficientNet_B0_Weights.DEFAULT')
57
+ model.eval()
58
+ # Remove the classification head
59
+ model = torch.nn.Sequential(*list(model.children())[:-1])
60
+ model = model.to(Config.DEVICE)
61
+ return model
62
+
63
+ @staticmethod
64
+ def load_index_and_metadata(index_path=None, metadata_path=None):
65
+ """Loads the FAISS index and metadata from files.
66
+
67
+ Args:
68
+ index_path (str): Path to the FAISS index file
69
+ metadata_path (str): Path to the metadata pickle file
70
+
71
+ Returns:
72
+ tuple: (index, metadata, success_flag)
73
+ """
74
+ warnings.filterwarnings("ignore")
75
+
76
+ index_path = index_path or Config.INDEX_PATH
77
+ metadata_path = metadata_path or Config.METADATA_PATH
78
+
79
+ try:
80
+ if os.path.exists(index_path) and os.path.exists(metadata_path):
81
+ index = faiss.read_index(index_path)
82
+ with open(metadata_path, "rb") as f:
83
+ metadata = pickle.load(f)
84
+ print(f"Index and metadata loaded successfully.")
85
+ return index, metadata, True
86
+ else:
87
+ print(f"Index file or metadata file not found.")
88
+ return None, {}, False
89
+ except Exception as e:
90
+ print(f"Error loading index or metadata: {e}")
91
+ return None, {}, False
92
+
93
+
94
+ # image_processor.py
95
+ import io
96
+ import torch
97
+ import numpy as np
98
+ from PIL import Image
99
+
100
+ class ImageProcessor:
101
+ """Handles processing and feature extraction from images."""
102
+
103
+ def __init__(self, model):
104
+ """Initialize with a pre-trained model.
105
+
106
+ Args:
107
+ model: The pre-trained model for feature extraction
108
+ """
109
+ self.model = model
110
+ self.transform = Config.get_image_transform()
111
+
112
+ def normalize_image_input(self, image):
113
+ """Normalize different image input types to a PIL Image.
114
+
115
+ Args:
116
+ image: Can be a PIL.Image, file path, byte stream, or numpy array
117
+
118
+ Returns:
119
+ PIL.Image: The normalized image
120
+ """
121
+ try:
122
+ if isinstance(image, str):
123
+ # If image is a file path
124
+ return Image.open(image).convert('RGB')
125
+ elif isinstance(image, bytes) or isinstance(image, io.BytesIO):
126
+ # If image is a byte stream
127
+ if isinstance(image, bytes):
128
+ image = io.BytesIO(image)
129
+ return Image.open(image).convert('RGB')
130
+ elif isinstance(image, np.ndarray):
131
+ # If image is a numpy array (as from gradio)
132
+ return Image.fromarray(image.astype('uint8')).convert('RGB')
133
+ elif isinstance(image, Image.Image):
134
+ # If image is already a PIL Image
135
+ return image.convert('RGB')
136
+ else:
137
+ raise ValueError(f"Unsupported image type: {type(image)}")
138
+ except Exception as e:
139
+ print(f"Error normalizing image: {str(e)}")
140
+ return None
141
+
142
+ def extract_embedding(self, image):
143
+ """Extract feature embedding from an image.
144
+
145
+ Args:
146
+ image: The image to extract features from (various formats accepted)
147
+
148
+ Returns:
149
+ numpy.ndarray: The feature embedding or None if extraction failed
150
+ """
151
+ try:
152
+ img = self.normalize_image_input(image)
153
+ if img is None:
154
+ return None
155
+
156
+ img_tensor = self.transform(img).unsqueeze(0).to(Config.DEVICE)
157
+ with torch.no_grad():
158
+ embedding = self.model(img_tensor).squeeze().cpu().numpy()
159
+ return embedding
160
+ except Exception as e:
161
+ print(f"Error extracting embedding: {str(e)}")
162
+ return None
163
+
164
+
165
+ # recommender.py - Already provided in the artifact above
166
+
167
+
168
+ # jewelry_recommender.py
169
+ import warnings
170
+
171
+ class JewelryRecommenderService:
172
+ """Main service class for the Jewelry Recommender System."""
173
+
174
+ def __init__(self,
175
+ index_path=None,
176
+ metadata_path=None):
177
+ """Initialize the jewelry recommender service.
178
+
179
+ Args:
180
+ index_path (str, optional): Path to FAISS index
181
+ metadata_path (str, optional): Path to metadata pickle file
182
+ """
183
+ warnings.filterwarnings("ignore")
184
+
185
+ # Load the model
186
+ self.model = ModelLoader.load_feature_extraction_model()
187
+
188
+ # Load index and metadata
189
+ self.index, self.metadata, success = ModelLoader.load_index_and_metadata(
190
+ index_path, metadata_path
191
+ )
192
+
193
+ # Initialize pipeline components
194
+ self.image_processor = ImageProcessor(self.model)
195
+ self.recommender = RecommenderEngine(self.index, self.metadata)
196
+
197
+ def get_recommendations(self, image, num_recommendations=None, skip_exact_match=True):
198
+ """Get recommendations for a query image.
199
+
200
+ Args:
201
+ image: Query image (various formats)
202
+ num_recommendations (int, optional): Number of recommendations
203
+ skip_exact_match (bool): Whether to skip the first/exact match
204
+
205
+ Returns:
206
+ list: Recommendation results
207
+ """
208
+ num_recommendations = num_recommendations or Config.DEFAULT_NUM_RECOMMENDATIONS
209
+
210
+ # Extract embedding from the image
211
+ embedding = self.image_processor.extract_embedding(image)
212
+
213
+ # Get similar items based on the embedding
214
+ recommendations = self.recommender.find_similar_items(
215
+ embedding, num_recommendations, skip_exact_match
216
+ )
217
+
218
+ return recommendations
219
+
220
+
221
+ # formatter.py
222
+ class ResultFormatter:
223
+ """Formats recommendation results for display."""
224
+
225
+ @staticmethod
226
+ def format_html(recommendations):
227
+ """Format recommendations as HTML for the Gradio interface.
228
+
229
+ Args:
230
+ recommendations (list): List of recommendation dictionaries
231
+
232
+ Returns:
233
+ str: HTML formatted results
234
+ """
235
+ if not recommendations:
236
+ return "No recommendations found."
237
+
238
+ result_html = "<h3>Recommended Jewelry Items:</h3>"
239
+ for i, rec in enumerate(recommendations, 1):
240
+ metadata = rec["metadata"]
241
+ result_html += f"<div style='margin-bottom:15px; padding:10px; border:1px solid #ddd; border-radius:5px;'>"
242
+ result_html += f"<h4>#{i}: {metadata.get('name', 'Unknown')}</h4>"
243
+ result_html += f"<p><b>Category:</b> {metadata.get('category', 'Unknown')}</p>"
244
+ result_html += f"<p><b>Description:</b> {metadata.get('description', 'No description available')}</p>"
245
+ result_html += f"<p><b>Price:</b> ${metadata.get('price', 'N/A')}</p>"
246
+ result_html += f"<p><b>Similarity Score:</b> {rec['similarity_score']:.4f}</p>"
247
+ if 'image_url' in metadata:
248
+ result_html += f"<p><img src='{metadata['image_url']}' style='max-width:200px; max-height:200px;'></p>"
249
+ result_html += "</div>"
250
+
251
+ return result_html
252
+
253
+ @staticmethod
254
+ def format_json(recommendations):
255
+ """Format recommendations as JSON.
256
+
257
+ Args:
258
+ recommendations (list): List of recommendation dictionaries
259
+
260
+ Returns:
261
+ list: Clean JSON-serializable results
262
+ """
263
+ if not recommendations:
264
+ return []
265
+
266
+ results = []
267
+ for rec in recommendations:
268
+ results.append({
269
+ "item": rec["metadata"].get("name", "Unknown"),
270
+ "category": rec["metadata"].get("category", "Unknown"),
271
+ "description": rec["metadata"].get("description", "No description"),
272
+ "price": rec["metadata"].get("price", "N/A"),
273
+ "similarity_score": round(rec["similarity_score"], 4),
274
+ "image_url": rec["metadata"].get("image_url", None)
275
+ })
276
+
277
+ return results
278
+
279
+
280
+ # input_handlers.py
281
+ import io
282
+ import base64
283
+ from PIL import Image
284
+
285
+ class InputHandlers:
286
+ """Handles different types of image inputs for recommendation."""
287
+
288
+ @staticmethod
289
+ def process_image(image, num_recommendations=5, skip_exact_match=True):
290
+ """Process direct image input.
291
+
292
+ Args:
293
+ image: The image (PIL, numpy array, etc.)
294
+ num_recommendations (int): Number of recommendations
295
+ skip_exact_match (bool): Whether to skip the first/exact match
296
+
297
+ Returns:
298
+ str: HTML formatted results
299
+ """
300
+ recommender = JewelryRecommenderService()
301
+ recommendations = recommender.get_recommendations(
302
+ image, num_recommendations, skip_exact_match
303
+ )
304
+ return ResultFormatter.format_html(recommendations)
305
+
306
+ @staticmethod
307
+ def process_url(url, num_recommendations=5, skip_exact_match=True):
308
+ """Process image from URL.
309
+
310
+ Args:
311
+ url (str): URL to the image
312
+ num_recommendations (int): Number of recommendations
313
+ skip_exact_match (bool): Whether to skip the first/exact match
314
+
315
+ Returns:
316
+ str: HTML formatted results
317
+ """
318
+ try:
319
+ import requests
320
+ response = requests.get(url)
321
+ image = Image.open(io.BytesIO(response.content))
322
+ return InputHandlers.process_image(image, num_recommendations, skip_exact_match)
323
+ except Exception as e:
324
+ return f"Error processing URL: {str(e)}"
325
+
326
+ # Base64 input handler is commented out
327
+ """
328
+ @staticmethod
329
+ def process_base64(base64_string, num_recommendations=5, skip_exact_match=True):
330
+ # Process base64-encoded image.
331
+ #
332
+ # Args:
333
+ # base64_string (str): Base64 encoded image
334
+ # num_recommendations (int): Number of recommendations
335
+ # skip_exact_match (bool): Whether to skip the first/exact match
336
+ #
337
+ # Returns:
338
+ # str: HTML formatted results
339
+
340
+ try:
341
+ # Remove data URL prefix if present
342
+ if ',' in base64_string:
343
+ base64_string = base64_string.split(',', 1)[1]
344
+
345
+ image_bytes = base64.b64decode(base64_string)
346
+ image = Image.open(io.BytesIO(image_bytes))
347
+ return InputHandlers.process_image(image, num_recommendations, skip_exact_match)
348
+ except Exception as e:
349
+ return f"Error processing base64 image: {str(e)}"
350
+ """
351
+
352
+
353
+ # gradio_app.py
354
+ import gradio as gr
355
+
356
+ def create_gradio_interface():
357
+ """Create and configure the Gradio web interface.
358
+
359
+ Returns:
360
+ gradio.Blocks: The configured Gradio interface
361
+ """
362
+ with gr.Blocks(title="Jewelry Recommender") as demo:
363
+ gr.Markdown("# Jewelry Recommendation System")
364
+ gr.Markdown("Upload an image of jewelry to get similar recommendations.")
365
+
366
+ with gr.Tab("Upload Image"):
367
+ with gr.Row():
368
+ image_input = gr.Image(type="pil", label="Upload Jewelry Image")
369
+ num_recs_slider = gr.Slider(
370
+ minimum=1,
371
+ maximum=Config.MAX_RECOMMENDATIONS,
372
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
373
+ step=1,
374
+ label="Number of Recommendations"
375
+ )
376
+ skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
377
+ submit_btn = gr.Button("Get Recommendations")
378
+ output_html = gr.HTML(label="Recommendations")
379
+ submit_btn.click(
380
+ InputHandlers.process_image,
381
+ inputs=[image_input, num_recs_slider, skip_exact],
382
+ outputs=output_html
383
+ )
384
+
385
+ with gr.Tab("Image URL"):
386
+ with gr.Row():
387
+ url_input = gr.Textbox(label="Enter Image URL")
388
+ url_num_recs = gr.Slider(
389
+ minimum=1,
390
+ maximum=Config.MAX_RECOMMENDATIONS,
391
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
392
+ step=1,
393
+ label="Number of Recommendations"
394
+ )
395
+ url_skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
396
+ url_btn = gr.Button("Get Recommendations from URL")
397
+ url_output = gr.HTML(label="Recommendations")
398
+ url_btn.click(
399
+ InputHandlers.process_url,
400
+ inputs=[url_input, url_num_recs, url_skip_exact],
401
+ outputs=url_output
402
+ )
403
+
404
+ # Base64 tab is commented out
405
+ """
406
+ with gr.Tab("Base64 Image"):
407
+ with gr.Row():
408
+ base64_input = gr.Textbox(label="Enter Base64 Image String")
409
+ base64_num_recs = gr.Slider(
410
+ minimum=1,
411
+ maximum=Config.MAX_RECOMMENDATIONS,
412
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
413
+ step=1,
414
+ label="Number of Recommendations"
415
+ )
416
+ base64_skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
417
+ base64_btn = gr.Button("Get Recommendations from Base64")
418
+ base64_output = gr.HTML(label="Recommendations")
419
+ base64_btn.click(
420
+ InputHandlers.process_base64,
421
+ inputs=[base64_input, base64_num_recs, base64_skip_exact],
422
+ outputs=base64_output
423
+ )
424
+ """
425
+
426
+ gr.Markdown("## How to Use")
427
+ gr.Markdown("""
428
+ 1. Upload an image of jewelry or provide an image URL
429
+ 2. Adjust the number of recommendations you want to see
430
+ 3. Check "Skip Exact Match" to exclude the identical or closest match from results
431
+ 4. Click the 'Get Recommendations' button
432
+ 5. View similar jewelry items based on visual similarity
433
+ """)
434
+
435
+ return demo
436
+
437
+
438
+ # main.py
439
+ def main():
440
+ """Main entry point to run the Jewelry Recommender application."""
441
+ print("Starting Jewelry Recommender System...")
442
+ demo = create_gradio_interface()
443
+ demo.launch()
444
+
445
+ if __name__ == "__main__":
446
+ main()
#temp_del/end to end proj/.yml ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CI/CD Pipeline Configuration for Jewelry Recommender
2
+
3
+ version: 2.1
4
+
5
+ jobs:
6
+ setup:
7
+ docker:
8
+ - image: python:3.9
9
+ steps:
10
+ - checkout
11
+ - restore_cache:
12
+ keys:
13
+ - v1-dependencies-{{ checksum "requirements.txt" }}
14
+ - run:
15
+ name: Install Dependencies
16
+ command: |
17
+ python -m pip install --upgrade pip
18
+ pip install -r requirements.txt
19
+ - save_cache:
20
+ paths:
21
+ - ./venv
22
+ key: v1-dependencies-{{ checksum "requirements.txt" }}
23
+
24
+ test:
25
+ docker:
26
+ - image: python:3.9
27
+ steps:
28
+ - checkout
29
+ - restore_cache:
30
+ keys:
31
+ - v1-dependencies-{{ checksum "requirements.txt" }}
32
+ - run:
33
+ name: Run Tests
34
+ command: |
35
+ python -m unittest discover tests
36
+
37
+ build_model:
38
+ docker:
39
+ - image: python:3.9
40
+ resource_class: large
41
+ steps:
42
+ - checkout
43
+ - restore_cache:
44
+ keys:
45
+ - v1-dependencies-{{ checksum "requirements.txt" }}
46
+ - run:
47
+ name: Download Dataset
48
+ command: python data_management.py download
49
+ - run:
50
+ name: Train Model and Build Index
51
+ command: python build_index.py
52
+ - persist_to_workspace:
53
+ root: .
54
+ paths:
55
+ - model_files/
56
+
57
+ deploy_staging:
58
+ docker:
59
+ - image: python:3.9
60
+ steps:
61
+ - checkout
62
+ - attach_workspace:
63
+ at: .
64
+ - run:
65
+ name: Deploy to Staging
66
+ command: |
67
+ # Setup GCP authentication
68
+ echo $GCLOUD_SERVICE_KEY | base64 -d > ${HOME}/gcloud-service-key.json
69
+ gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
70
+
71
+ # Deploy to GCP App Engine
72
+ gcloud app deploy app_staging.yaml --project $GCP_PROJECT_ID --quiet
73
+
74
+ deploy_production:
75
+ docker:
76
+ - image: python:3.9
77
+ steps:
78
+ - checkout
79
+ - attach_workspace:
80
+ at: .
81
+ - run:
82
+ name: Deploy to Production
83
+ command: |
84
+ # Setup GCP authentication
85
+ echo $GCLOUD_SERVICE_KEY | base64 -d > ${HOME}/gcloud-service-key.json
86
+ gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
87
+
88
+ # Deploy to GCP App Engine
89
+ gcloud app deploy app.yaml --project $GCP_PROJECT_ID --quiet
90
+
91
+ workflows:
92
+ version: 2
93
+ build-test-deploy:
94
+ jobs:
95
+ - setup
96
+ - test:
97
+ requires:
98
+ - setup
99
+ - build_model:
100
+ requires:
101
+ - test
102
+ filters:
103
+ branches:
104
+ only: main
105
+ - deploy_staging:
106
+ requires:
107
+ - build_model
108
+ filters:
109
+ branches:
110
+ only: main
111
+ - approve_production:
112
+ type: approval
113
+ requires:
114
+ - deploy_staging
115
+ - deploy_production:
116
+ requires:
117
+ - approve_production
#temp_del/end to end proj/clustering-module.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # clustering.py
2
+
3
+ import numpy as np
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Tuple, List, Optional
6
+ from sklearn.cluster import KMeans
7
+ from sklearn.metrics import silhouette_score, davies_bouldin_score
8
+ from sklearn.preprocessing import StandardScaler
9
+ from tqdm.auto import tqdm
10
+
11
+ @dataclass
12
+ class ClusterMetrics:
13
+ n_clusters: int
14
+ silhouette: float
15
+ davies_bouldin: float
16
+ cluster_sizes: Dict[int, int]
17
+ inertia: float
18
+
19
+ class EnhancedJewelryClusterer:
20
+ def __init__(self,
21
+ min_clusters: int = 10,
22
+ max_clusters: int = 50,
23
+ random_state: int = 42):
24
+ self.min_clusters = min_clusters
25
+ self.max_clusters = max_clusters
26
+ self.random_state = random_state
27
+ self.best_model = None
28
+ self.cluster_centers_ = None
29
+ self.scaler = StandardScaler()
30
+
31
+ def analyze_jewelry_types(self, metadata: List[Dict]) -> Dict[str, int]:
32
+ """Analyze distribution of jewelry types in dataset"""
33
+ type_counts = {}
34
+ for item in metadata:
35
+ j_type = item.get('jewelry_type', 'unknown')
36
+ type_counts[j_type] = type_counts.get(j_type, 0) + 1
37
+ return type_counts
38
+
39
+ def adjust_clusters_by_complexity(self, n_samples: int) -> Tuple[int, int]:
40
+ """Adjust cluster range based on dataset size and complexity"""
41
+ # Base calculation
42
+ suggested_min = max(10, n_samples // 1000)
43
+ suggested_max = min(50, n_samples // 100)
44
+
45
+ # Ensure reasonable bounds
46
+ final_min = max(self.min_clusters, suggested_min)
47
+ final_max = min(self.max_clusters, suggested_max)
48
+ final_min = min(final_min, final_max) # Ensure min_k <= max_k
49
+
50
+ return final_min, final_max
51
+
52
+ def evaluate_clustering(self,
53
+ embeddings: np.ndarray,
54
+ n_clusters: int) -> ClusterMetrics:
55
+ """Evaluate clustering for a specific number of clusters"""
56
+ kmeans = KMeans(n_clusters=n_clusters,
57
+ random_state=self.random_state,
58
+ n_init='auto')
59
+
60
+ # Fit and predict
61
+ labels = kmeans.fit_predict(embeddings)
62
+
63
+ # Calculate metrics
64
+ sil_score = silhouette_score(embeddings, labels)
65
+ db_score = davies_bouldin_score(embeddings, labels)
66
+
67
+ # Get cluster sizes
68
+ unique, counts = np.unique(labels, return_counts=True)
69
+ cluster_sizes = dict(zip(unique, counts))
70
+
71
+ return ClusterMetrics(
72
+ n_clusters=n_clusters,
73
+ silhouette=sil_score,
74
+ davies_bouldin=db_score,
75
+ cluster_sizes=cluster_sizes,
76
+ inertia=kmeans.inertia_
77
+ )
78
+
79
+ def find_optimal_clusters(self,
80
+ embeddings: np.ndarray,
81
+ metadata: Optional[List[Dict]] = None) -> Dict:
82
+ """Find optimal number of clusters using multiple metrics"""
83
+ print("Starting clustering analysis...")
84
+
85
+ # Scale the embeddings
86
+ scaled_embeddings = self.scaler.fit_transform(embeddings)
87
+
88
+ # Adjust cluster range based on dataset size
89
+ min_k, max_k = self.adjust_clusters_by_complexity(len(embeddings))
90
+ print(f"Analyzing cluster range: {min_k} to {max_k}")
91
+
92
+ # Analyze jewelry types if metadata available
93
+ if metadata:
94
+ type_distribution = self.analyze_jewelry_types(metadata)
95
+ print("\nJewelry Type Distribution:")
96
+ for j_type, count in type_distribution.items():
97
+ print(f"{j_type}: {count} items ({count/len(metadata)*100:.1f}%)")
98
+
99
+ # Evaluate different cluster counts
100
+ metrics_list = []
101
+ for k in tqdm(range(min_k, max_k + 1, 2), desc="Evaluating clusters"):
102
+ metrics = self.evaluate_clustering(scaled_embeddings, k)
103
+ metrics_list.append(metrics)
104
+
105
+ # Find best configuration using combined metric
106
+ best_metrics = max(metrics_list,
107
+ key=lambda x: x.silhouette - x.davies_bouldin * 0.5)
108
+
109
+ # Fit final model with optimal clusters
110
+ final_model = KMeans(n_clusters=best_metrics.n_clusters,
111
+ random_state=self.random_state,
112
+ n_init='auto')
113
+ final_labels = final_model.fit_predict(scaled_embeddings)
114
+
115
+ # Store best model and cluster centers
116
+ self.best_model = final_model
117
+ self.cluster_centers_ = final_model.cluster_centers_
118
+
119
+ # Prepare detailed report
120
+ report = {
121
+ 'optimal_clusters': best_metrics.n_clusters,
122
+ 'silhouette_score': best_metrics.silhouette,
123
+ 'davies_bouldin_score': best_metrics.davies_bouldin,
124
+ 'cluster_distribution': best_metrics.cluster_sizes,
125
+ 'cluster_labels': final_labels,
126
+ 'scaled_embeddings': scaled_embeddings
127
+ }
128
+
129
+ # Print summary
130
+ print("\nClustering Analysis Results:")
131
+ print(f"Optimal number of clusters: {report['optimal_clusters']}")
132
+ print(f"Silhouette Score: {report['silhouette_score']:.3f}")
133
+ print(f"Davies-Bouldin Score: {report['davies_bouldin_score']:.3f}")
134
+
135
+ print("\nCluster Size Distribution:")
136
+ for cluster, size in report['cluster_distribution'].items():
137
+ percentage = (size / len(embeddings)) * 100
138
+ print(f"Cluster {cluster}: {size} items ({percentage:.1f}%)")
139
+
140
+ return report
141
+
142
+ def predict(self, embeddings: np.ndarray) -> np.ndarray:
143
+ """Predict clusters for new embeddings"""
144
+ if self.best_model is None:
145
+ raise ValueError("Model not fitted. Run find_optimal_clusters first.")
146
+
147
+ scaled_embeddings = self.scaler.transform(embeddings)
148
+ return self.best_model.predict(scaled_embeddings)
#temp_del/end to end proj/data-management-module.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data_management.py
2
+
3
+ import os
4
+ import gdown
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ class DataManager:
9
+ """Handles dataset download, extraction, and validation"""
10
+
11
+ def __init__(self, base_dir="/content/extracted_jewellery_data"):
12
+ self.base_dir = base_dir
13
+ self.dataset_path = os.path.join(base_dir, "all images extracted")
14
+
15
+ def setup_dataset_from_drive(self, file_id="1z445s15uuZUysdpyOYjIbWcV0CQrO5fs"):
16
+ """
17
+ Downloads and sets up the jewelry dataset from Google Drive shared link
18
+ Returns the path to the dataset
19
+ """
20
+ # Create base directory
21
+ os.makedirs(self.base_dir, exist_ok=True)
22
+
23
+ # Construct the direct download URL
24
+ url = f"https://drive.google.com/uc?id={file_id}"
25
+
26
+ # Download location
27
+ zip_path = os.path.join(self.base_dir, "jewelry_dataset.zip")
28
+
29
+ print("Downloading dataset from Google Drive...")
30
+ try:
31
+ # Download the file
32
+ gdown.download(url, zip_path, quiet=False)
33
+
34
+ print("\nExtracting files...")
35
+ # Extract the zip file
36
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
37
+ zip_ref.extractall(self.base_dir)
38
+
39
+ # Remove the zip file to save space
40
+ os.remove(zip_path)
41
+
42
+ # Verify the dataset path exists
43
+ if os.path.exists(self.dataset_path):
44
+ print(f"\nDataset successfully downloaded and extracted to: {self.dataset_path}")
45
+ # Count images
46
+ image_count = len(list(Path(self.dataset_path).rglob("*.[jJ][pP][gG]")))
47
+ print(f"Found {image_count} images in the dataset")
48
+ return self.dataset_path
49
+ else:
50
+ print(f"\nError: Expected dataset path not found: {self.dataset_path}")
51
+ return None
52
+
53
+ except Exception as e:
54
+ print(f"\nError downloading or extracting dataset: {e}")
55
+ return None
56
+
57
+ def get_all_images(self):
58
+ """Return list of all image paths in the dataset"""
59
+ return list(Path(self.dataset_path).rglob("*.[jJ][pP][gG]"))
60
+
61
+ def validate_dataset(self):
62
+ """Validates that the dataset exists and contains images"""
63
+ if not os.path.exists(self.dataset_path):
64
+ print(f"Dataset path does not exist: {self.dataset_path}")
65
+ return False
66
+
67
+ images = self.get_all_images()
68
+ if len(images) == 0:
69
+ print("No images found in the dataset.")
70
+ return False
71
+
72
+ print(f"Dataset validated: {len(images)} images found.")
73
+ return True
#temp_del/end to end proj/feature-extraction-module.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # feature_extraction.py
2
+
3
+ import torch
4
+ import torchvision.models as models
5
+ import torchvision.transforms as transforms
6
+ from PIL import Image, ImageOps
7
+ import numpy as np
8
+ import logging
9
+ import warnings
10
+
11
+ class FeatureExtractor:
12
+ """Handles extraction of embeddings from images using a pre-trained model"""
13
+
14
+ def __init__(self, vector_dimension=1280):
15
+ self.vector_dimension = vector_dimension
16
+
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.ERROR)
19
+ self.logger = logging.getLogger(__name__)
20
+
21
+ # Load model
22
+ with warnings.catch_warnings():
23
+ warnings.filterwarnings("ignore", category=UserWarning)
24
+ self.model = models.efficientnet_b0(weights='EfficientNet_B0_Weights.DEFAULT')
25
+ self.model.eval()
26
+ self.model = torch.nn.Sequential(*list(self.model.children())[:-1])
27
+
28
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
29
+ self.model = self.model.to(self.device)
30
+
31
+ # Image transformation
32
+ self.transform = transforms.Compose([
33
+ transforms.Lambda(lambda img: ImageOps.exif_transpose(img)),
34
+ transforms.Resize((640, 640)),
35
+ transforms.ToTensor(),
36
+ transforms.Normalize(
37
+ mean=[0.485, 0.456, 0.406],
38
+ std=[0.229, 0.224, 0.225]
39
+ )
40
+ ])
41
+
42
+ def extract_embedding(self, image_path):
43
+ """Extract embedding vector from an image"""
44
+ try:
45
+ with Image.open(image_path).convert('RGB') as img:
46
+ img_tensor = self.transform(img).unsqueeze(0).to(self.device)
47
+ with torch.no_grad():
48
+ embedding = self.model(img_tensor).squeeze().cpu().numpy()
49
+ return embedding
50
+ except Exception as e:
51
+ self.logger.error(f"Error processing image {image_path}: {str(e)}")
52
+ return None
53
+
54
+ def batch_extract_embeddings(self, image_paths):
55
+ """Extract embeddings for a batch of images"""
56
+ embeddings_list = []
57
+ valid_paths = []
58
+
59
+ for path in image_paths:
60
+ embedding = self.extract_embedding(str(path))
61
+ if embedding is not None:
62
+ embeddings_list.append(embedding)
63
+ valid_paths.append(str(path))
64
+
65
+ if embeddings_list:
66
+ return np.vstack(embeddings_list), valid_paths
67
+ return None, []
#temp_del/end to end proj/index-storage-module.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # index_storage.py
2
+
3
+ import os
4
+ import faiss
5
+ import numpy as np
6
+ import pickle
7
+ import pandas as pd
8
+ from typing import List, Tuple, Dict, Optional
9
+
10
+ class IndexManager:
11
+ """Manages the FAISS index and metadata storage for efficient similarity search"""
12
+
13
+ def __init__(self,
14
+ vector_dimension: int = 1280,
15
+ index_path: str = "./model_files/jewelry_index.idx",
16
+ metadata_path: str = "./model_files/jewelry_metadata.pkl",
17
+ vectors_path: str = "./model_files/jewelry_vectors.parquet"):
18
+
19
+ self.vector_dimension = vector_dimension
20
+ self.index_path = index_path
21
+ self.metadata_path = metadata_path
22
+ self.vectors_path = vectors_path
23
+ self.metadata = {}
24
+
25
+ # Initialize FAISS index
26
+ self.index = faiss.IndexIVFFlat(
27
+ faiss.IndexFlatL2(vector_dimension),
28
+ vector_dimension,
29
+ min(100, max(10, int(vector_dimension * 0.1))),
30
+ faiss.METRIC_L2
31
+ )
32
+
33
+ def save_vectors_to_parquet(self, embeddings_array: np.ndarray, image_paths: List[str]):
34
+ """Save vectors to parquet file with columns for each dimension"""
35
+ # Create column names for each dimension
36
+ dim_cols = [f'dim_{i}' for i in range(embeddings_array.shape[1])]
37
+
38
+ # Create DataFrame with embeddings
39
+ df = pd.DataFrame(embeddings_array, columns=dim_cols)
40
+ df['image_path'] = image_paths
41
+
42
+ # Save to parquet
43
+ os.makedirs(os.path.dirname(self.vectors_path) or '.', exist_ok=True)
44
+ df.to_parquet(self.vectors_path, index=False)
45
+ print(f"Vectors saved to {self.vectors_path}")
46
+
47
+ def load_vectors_from_parquet(self) -> Tuple[Optional[np.ndarray], Optional[List[str]]]:
48
+ """Load vectors from parquet file"""
49
+ if not os.path.exists(self.vectors_path):
50
+ return None, None
51
+
52
+ df = pd.read_parquet(self.vectors_path)
53
+ image_paths = df['image_path'].tolist()
54
+ dim_cols = [col for col in df.columns if col.startswith('dim_')]
55
+ embeddings_array = df[dim_cols].values
56
+
57
+ return embeddings_array, image_paths
58
+
59
+ def load_index_and_metadata(self) -> bool:
60
+ """Load index and metadata from files"""
61
+ try:
62
+ if os.path.exists(self.index_path) and os.path.exists(self.metadata_path):
63
+ self.index = faiss.read_index(self.index_path)
64
+ with open(self.metadata_path, "rb") as f:
65
+ self.metadata = pickle.load(f)
66
+ print("Index and metadata loaded successfully.")
67
+ return True
68
+ except Exception as e:
69
+ print(f"Error loading index or metadata: {e}")
70
+ return False
71
+
72
+ def save_index_and_metadata(self):
73
+ """Save index and metadata to files"""
74
+ try:
75
+ os.makedirs(os.path.dirname(self.index_path) or '.', exist_ok=True)
76
+ faiss.write_index(self.index, self.index_path)
77
+ with open(self.metadata_path, "wb") as f:
78
+ pickle.dump(self.metadata, f)
79
+ print("Index and metadata saved successfully.")
80
+ except Exception as e:
81
+ print(f"Error saving index or metadata: {e}")
82
+
83
+ def build_index(self, embeddings_array: np.ndarray, metadata_list: List[Dict]):
84
+ """Build FAISS index from embeddings and metadata"""
85
+ print("Training the index...")
86
+ self.index.train(embeddings_array)
87
+
88
+ print("Adding images to the index...")
89
+ ids = np.arange(len(metadata_list))
90
+ self.index.add_with_ids(embeddings_array, ids)
91
+ self.metadata = {i: meta for i, meta in enumerate(metadata_list)}
92
+
93
+ self.save_index_and_metadata()
94
+ print(f"Successfully indexed {len(metadata_list)} images")
95
+
96
+ def search(self, query_embedding: np.ndarray, k: int = 5) -> Tuple[np.ndarray, np.ndarray]:
97
+ """Search the index for similar vectors"""
98
+ search_k = min(k, self.index.ntotal)
99
+ return self.index.search(query_embedding.reshape(1, -1), search_k)
#temp_del/end to end proj/main-module.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ import os
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from data_management import DataManager
8
+ from recommendation import JewelryRecommender
9
+ import ui
10
+
11
+ def parse_args():
12
+ """Parse command line arguments"""
13
+ parser = argparse.ArgumentParser(description="Jewelry Recommendation System")
14
+ parser.add_argument("--dataset", type=str, default="./extracted_jewellery_data",
15
+ help="Path to dataset directory")
16
+ parser.add_argument("--model-dir", type=str, default="./model_files",
17
+ help="Directory to store model files")
18
+ parser.add_argument("--rebuild-index", action="store_true",
19
+ help="Force rebuilding the index even if it exists")
20
+ parser.add_argument("--interface", type=str, default="colab",
21
+ choices=["colab", "gradio", "none"],
22
+ help="Type of interface to launch")
23
+ return parser.parse_args()
24
+
25
+ def main():
26
+ """Main function to run the Jewelry Recommendation System"""
27
+ args = parse_args()
28
+
29
+ # Setup paths
30
+ dataset_path = args.dataset
31
+ model_dir = args.model_dir
32
+ os.makedirs(model_dir, exist_ok=True)
33
+
34
+ index_path = os.path.join(model_dir, "jewelry_index.idx")
35
+ metadata_path = os.path.join(model_dir, "jewelry_metadata.pkl")
36
+ vectors_path = os.path.join(model_dir, "jewelry_vectors.parquet")
37
+
38
+ # Setup dataset if needed
39
+ if not os.path.exists(dataset_path) or not os.listdir(dataset_path):
40
+ print("Dataset not found or empty. Downloading dataset...")
41
+ data_manager = DataManager(dataset_path)
42
+ dataset_path = data_manager.setup_dataset_from_drive()
43
+ if not dataset_path:
44
+ print("Failed to download dataset. Exiting.")
45
+ return
46
+
47
+ # Initialize recommender
48
+ recommender = JewelryRecommender(
49
+ dataset_path=dataset_path,
50
+ index_path=index_path,
51
+ metadata_path=metadata_path,
52
+ vectors_path=vectors_path
53
+ )
54
+
55
+ # Build or load index
56
+ index_exists = os.path.exists(index_path) and os.path.exists(metadata_path)
57
+ if args.rebuild_index or not index_exists:
58
+ print("Building new index...")
59
+ recommender.build_index()
60
+ else:
61
+ print("Loading existing index...")
62
+ if not recommender.index_manager.load_index_and_metadata():
63
+ print("Failed to load existing index. Building new index...")
64
+ recommender.build_index()
65
+
66
+ # Launch interface
67
+ if args.interface == "colab":
68
+ try:
69
+ ui.create_colab_interface(recommender)
70
+ except Exception as e:
71
+ print(f"Failed to create Colab interface: {e}")
72
+ print("Are you running in a Colab environment?")
73
+ elif args.interface == "gradio":
74
+ try:
75
+ import gradio as gr
76
+ interface = ui.create_gradio_interface(recommender)
77
+ interface.launch()
78
+ except ImportError:
79
+ print("Gradio not installed. Install with: pip install gradio")
80
+ else:
81
+ print("No interface launched. System is ready for programmatic use.")
82
+ print("Example usage:")
83
+ print(" from recommendation import JewelryRecommender")
84
+ print(" recommender = JewelryRecommender()")
85
+ print(" recommender.get_recommendations('path/to/image.jpg')")
86
+
87
+ if __name__ == "__main__":
88
+ main()
#temp_del/end to end proj/recommendation-module.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # recommendation.py
2
+
3
+ import os
4
+ from typing import List, Dict, Optional
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from PIL import Image
8
+ from pathlib import Path
9
+
10
+ from feature_extraction import FeatureExtractor
11
+ from index_storage import IndexManager
12
+ from clustering import EnhancedJewelryClusterer
13
+ from data_management import DataManager
14
+
15
+ class JewelryRecommender:
16
+ """Jewelry recommendation system that combines feature extraction, clustering, and indexing"""
17
+
18
+ def __init__(self,
19
+ dataset_path: str = "",
20
+ vector_dimension: int = 1280,
21
+ index_path: str = "./model_files/jewelry_index.idx",
22
+ metadata_path: str = "./model_files/jewelry_metadata.pkl",
23
+ vectors_path: str = "./model_files/jewelry_vectors.parquet"):
24
+
25
+ self.dataset_path = dataset_path
26
+ self.extractor = FeatureExtractor(vector_dimension)
27
+ self.index_manager = IndexManager(
28
+ vector_dimension=vector_dimension,
29
+ index_path=index_path,
30
+ metadata_path=metadata_path,
31
+ vectors_path=vectors_path
32
+ )
33
+
34
+ # Load index and metadata if available
35
+ self.index_manager.load_index_and_metadata()
36
+
37
+ @property
38
+ def metadata(self):
39
+ return self.index_manager.metadata
40
+
41
+ @metadata.setter
42
+ def metadata(self, value):
43
+ self.index_manager.metadata = value
44
+
45
+ def enhanced_auto_categorize_images(self, embeddings_array: np.ndarray) -> np.ndarray:
46
+ """Auto-categorize images using enhanced clustering techniques"""
47
+ clusterer = EnhancedJewelryClusterer()
48
+ clustering_report = clusterer.find_optimal_clusters(
49
+ embeddings_array,
50
+ metadata=list(self.metadata.values()) if hasattr(self, 'metadata') else None
51
+ )
52
+
53
+ # Store clustering information in metadata
54
+ self.index_manager.metadata['clustering_info'] = {
55
+ 'optimal_clusters': clustering_report['optimal_clusters'],
56
+ 'silhouette_score': clustering_report['silhouette_score'],
57
+ 'davies_bouldin_score': clustering_report['davies_bouldin_score'],
58
+ 'cluster_centers': clusterer.cluster_centers_.tolist()
59
+ }
60
+
61
+ return clustering_report['cluster_labels']
62
+
63
+ def build_index(self):
64
+ """Build the search index from the dataset images"""
65
+ data_manager = DataManager(self.dataset_path)
66
+ all_images = data_manager.get_all_images()
67
+ total_images = len(all_images)
68
+
69
+ if total_images == 0:
70
+ print("No images found in the dataset. Exiting index building.")
71
+ return
72
+
73
+ # Extract features
74
+ print("Extracting features from images...")
75
+ embeddings_array, image_paths = self.extractor.batch_extract_embeddings(all_images)
76
+
77
+ if embeddings_array is None:
78
+ print("Failed to extract embeddings. Exiting index building.")
79
+ return
80
+
81
+ # Save vectors
82
+ self.index_manager.save_vectors_to_parquet(embeddings_array, image_paths)
83
+
84
+ # Create metadata
85
+ metadata_list = []
86
+ for path in image_paths:
87
+ path_obj = Path(path)
88
+ metadata = {
89
+ "full_path": str(path),
90
+ "filename": path_obj.name
91
+ }
92
+ metadata_list.append(metadata)
93
+
94
+ # Auto-categorize images
95
+ categories = self.enhanced_auto_categorize_images(embeddings_array)
96
+ for meta, cat in zip(metadata_list, categories):
97
+ meta["category"] = f"Category_{cat}"
98
+
99
+ # Build the index
100
+ self.index_manager.build_index(embeddings_array, metadata_list)
101
+
102
+ def get_recommendations(self, query_image_path: str, num_recommendations: int = 5) -> List[Dict]:
103
+ """Get recommendations for a query image"""
104
+ query_embedding = self.extractor.extract_embedding(query_image_path)
105
+ if query_embedding is None:
106
+ return []
107
+
108
+ distances, indices = self.index_manager.search(query_embedding, num_recommendations * 3)
109
+
110
+ results = []
111
+ seen_categories = set()
112
+
113
+ for dist, idx in zip(distances[0], indices[0]):
114
+ if idx != -1:
115
+ metadata = self.metadata[idx]
116
+ if metadata["full_path"] != query_image_path:
117
+ similarity_score = 1 / (1 + float(dist))
118
+ if metadata.get("category") not in seen_categories:
119
+ result = {
120
+ "metadata": metadata,
121
+ "distance": float(dist),
122
+ "similarity_score": similarity_score
123
+ }
124
+ results.append(result)
125
+ seen_categories.add(metadata.get("category"))
126
+
127
+ results.sort(key=lambda x: x["similarity_score"], reverse=True)
128
+ return results[:num_recommendations]
129
+
130
+ def display_recommendations(self, query_image_path: str, num_recommendations: int = 5):
131
+ """Display recommendations with visualization"""
132
+ recommendations = self.get_recommendations(query_image_path, num_recommendations)
133
+
134
+ if not recommendations:
135
+ print("No recommendations found.")
136
+ return
137
+
138
+ plt.figure(figsize=(20, 5))
139
+
140
+ plt.subplot(1, num_recommendations + 1, 1)
141
+ query_img = Image.open(query_image_path).convert('RGB')
142
+ plt.imshow(query_img)
143
+ plt.title('Query Image', fontsize=10)
144
+ plt.axis('off')
145
+
146
+ for idx, result in enumerate(recommendations, 2):
147
+ plt.subplot(1, num_recommendations + 1, idx)
148
+ img_path = result['metadata']['full_path']
149
+ img = Image.open(img_path).convert('RGB')
150
+ plt.imshow(img)
151
+
152
+ similarity = result['similarity_score']
153
+ plt.title(f"Match {idx-1}\nSimilarity: {similarity:.3f}\nCategory: {result['metadata'].get('category', 'N/A')}",
154
+ fontsize=8)
155
+ plt.axis('off')
156
+
157
+ plt.tight_layout()
158
+ plt.show()
159
+
160
+ print("\nDetailed Recommendations:")
161
+ for idx, result in enumerate(recommendations, 1):
162
+ print(f"\n{idx}. Category: {result['metadata'].get('category', 'N/A')}")
163
+ print(f" Similarity Score: {result['similarity_score']:.3f}")
164
+ print(f" File: {result['metadata']['filename']}")
#temp_del/end to end proj/requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # requirements.txt
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+ faiss-cpu>=1.7.0
5
+ scikit-learn>=1.0.0
6
+ numpy>=1.20.0
7
+ pandas>=1.3.0
8
+ pyarrow>=7.0.0
9
+ matplotlib>=3.5.0
10
+ Pillow>=9.0.0
11
+ tqdm>=4.60.0
12
+ ipywidgets>=7.7.0
13
+ gdown>=4.5.0
14
+ gradio>=3.0.0
15
+ concurrent-log-handler>=0.9.20
16
+ plotly>=5.10.0
#temp_del/end to end proj/ui-module.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui.py
2
+
3
+ import ipywidgets as widgets
4
+ from IPython.display import display, clear_output, HTML
5
+ from google.colab import files
6
+ from pathlib import Path
7
+
8
+ def create_colab_interface(recommender):
9
+ """Create an interactive interface for the recommender in Colab"""
10
+ output_area = widgets.Output()
11
+
12
+ def on_upload_button_clicked(b):
13
+ with output_area:
14
+ clear_output()
15
+ print("Upload an image to get recommendations")
16
+ uploaded = files.upload()
17
+
18
+ if uploaded:
19
+ filename = list(uploaded.keys())[0]
20
+ try:
21
+ recommender.display_recommendations(filename)
22
+ except Exception as e:
23
+ print(f"Error processing image: {e}")
24
+
25
+ def on_sample_button_clicked(b):
26
+ with output_area:
27
+ clear_output()
28
+ dataset_images = list(Path(recommender.dataset_path).rglob("*.[jJ][pP][gG]"))
29
+
30
+ if dataset_images:
31
+ sample_image = str(dataset_images[0])
32
+ print(f"Using sample image: {sample_image}")
33
+ recommender.display_recommendations(sample_image)
34
+ else:
35
+ print("No sample images found in the dataset.")
36
+
37
+ upload_button = widgets.Button(
38
+ description='Upload Image',
39
+ button_style='primary',
40
+ layout=widgets.Layout(width='200px')
41
+ )
42
+
43
+ sample_button = widgets.Button(
44
+ description='Use Sample Image',
45
+ button_style='success',
46
+ layout=widgets.Layout(width='200px')
47
+ )
48
+
49
+ upload_button.on_click(on_upload_button_clicked)
50
+ sample_button.on_click(on_sample_button_clicked)
51
+
52
+ box = widgets.VBox([
53
+ widgets.HTML("<h2>Jewelry Recommendation System</h2>"),
54
+ widgets.HBox([upload_button, sample_button]),
55
+ output_area
56
+ ])
57
+
58
+ display(box)
59
+
60
+ # For web-based deployments (not Colab)
61
+ try:
62
+ import gradio as gr
63
+ except ImportError:
64
+ pass
65
+ else:
66
+ def create_gradio_interface(recommender):
67
+ """Create a Gradio interface for web deployment"""
68
+ def process_image(image):
69
+ # Save the uploaded image temporarily
70
+ temp_path = "temp_upload.jpg"
71
+ image.save(temp_path)
72
+
73
+ # Get recommendations
74
+ recommendations = recommender.get_recommendations(temp_path)
75
+
76
+ # Format results
77
+ results = []
78
+ for idx, rec in enumerate(recommendations, 1):
79
+ img_path = rec["metadata"]["full_path"]
80
+ similarity = rec["similarity_score"]
81
+ category = rec["metadata"].get("category", "N/A")
82
+
83
+ results.append({
84
+ "image": img_path,
85
+ "similarity": f"{similarity:.3f}",
86
+ "category": category
87
+ })
88
+
89
+ return results
90
+
91
+ # Create Gradio interface
92
+ with gr.Blocks() as interface:
93
+ gr.Markdown("# Jewelry Recommendation System")
94
+
95
+ with gr.Row():
96
+ input_image = gr.Image(type="pil", label="Upload Jewelry Image")
97
+
98
+ submit_btn = gr.Button("Get Recommendations")
99
+
100
+ output_gallery = gr.Gallery(
101
+ label="Recommendations",
102
+ show_label=True,
103
+ columns=5,
104
+ object_fit="contain"
105
+ )
106
+
107
+ submit_btn.click(
108
+ fn=process_image,
109
+ inputs=input_image,
110
+ outputs=output_gallery
111
+ )
112
+
113
+ return interface
#temp_del/oldapp.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import faiss
2
+ # import numpy as np
3
+ # import pickle
4
+ # import torch
5
+ # import torchvision.transforms as transforms
6
+ # import torchvision.models as models
7
+ # from PIL import Image, ImageOps
8
+ # import os
9
+ # import warnings
10
+ # import io
11
+ # import base64
12
+ # import gradio as gr
13
+
14
+ # class JewelryRecommenderServing:
15
+ # def __init__(self,
16
+ # vector_dimension: int = 1280,
17
+ # index_path: str = "rootdir/trained_models/jewelry_index.idx",
18
+ # metadata_path: str = "rootdir/trained_models/jewelry_metadata.pkl"):
19
+
20
+ # warnings.filterwarnings("ignore")
21
+
22
+ # # Load index and metadata
23
+ # self.index_path = index_path
24
+ # self.metadata_path = metadata_path
25
+ # self.index = None
26
+ # self.metadata = {}
27
+
28
+ # # Load model for feature extraction
29
+ # self.model = models.efficientnet_b0(weights='EfficientNet_B0_Weights.DEFAULT')
30
+ # self.model.eval()
31
+ # self.model = torch.nn.Sequential(*list(self.model.children())[:-1])
32
+
33
+ # self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
+ # self.model = self.model.to(self.device)
35
+
36
+ # # Image transformation
37
+ # self.transform = transforms.Compose([
38
+ # transforms.Lambda(lambda img: ImageOps.exif_transpose(img)),
39
+ # transforms.Resize((640, 640)),
40
+ # transforms.ToTensor(),
41
+ # transforms.Normalize(
42
+ # mean=[0.485, 0.456, 0.406],
43
+ # std=[0.229, 0.224, 0.225]
44
+ # )
45
+ # ])
46
+
47
+ # # Load the existing index and metadata
48
+ # self.load_index_and_metadata()
49
+
50
+ # def load_index_and_metadata(self) -> bool:
51
+ # """Load the pre-built FAISS index and metadata from files"""
52
+ # try:
53
+ # if os.path.exists(self.index_path) and os.path.exists(self.metadata_path):
54
+ # self.index = faiss.read_index(self.index_path)
55
+ # with open(self.metadata_path, "rb") as f:
56
+ # self.metadata = pickle.load(f)
57
+ # print(f"Index and metadata loaded successfully from {self.index_path} and {self.metadata_path}.")
58
+ # return True
59
+ # else:
60
+ # print(f"Index file or metadata file not found at {self.index_path} or {self.metadata_path}")
61
+ # return False
62
+ # except Exception as e:
63
+ # print(f"Error loading index or metadata: {e}")
64
+ # return False
65
+
66
+ # def _extract_embedding(self, image) -> np.ndarray:
67
+ # """Extract embedding from an image using the pre-trained model
68
+
69
+ # Parameters:
70
+ # - image: Can be a PIL.Image object, file path, or byte stream
71
+ # """
72
+ # try:
73
+ # # Handle different input types
74
+ # if isinstance(image, str):
75
+ # # If image is a file path
76
+ # img = Image.open(image).convert('RGB')
77
+ # elif isinstance(image, bytes) or isinstance(image, io.BytesIO):
78
+ # # If image is a byte stream
79
+ # if isinstance(image, bytes):
80
+ # image = io.BytesIO(image)
81
+ # img = Image.open(image).convert('RGB')
82
+ # elif isinstance(image, np.ndarray):
83
+ # # If image is a numpy array (as from gradio)
84
+ # img = Image.fromarray(image.astype('uint8')).convert('RGB')
85
+ # elif isinstance(image, Image.Image):
86
+ # # If image is already a PIL Image
87
+ # img = image.convert('RGB')
88
+ # else:
89
+ # raise ValueError(f"Unsupported image type: {type(image)}")
90
+
91
+ # # Process image
92
+ # img_tensor = self.transform(img).unsqueeze(0).to(self.device)
93
+ # with torch.no_grad():
94
+ # embedding = self.model(img_tensor).squeeze().cpu().numpy()
95
+ # return embedding
96
+ # except Exception as e:
97
+ # print(f"Error processing image: {str(e)}")
98
+ # return None
99
+
100
+ # def get_recommendations(self, image, num_recommendations: int = 5):
101
+ # """Get recommendations for a query image based on similarity
102
+
103
+ # Parameters:
104
+ # - image: Can be a PIL.Image object, file path, or byte stream
105
+ # - num_recommendations: Number of recommendations to return
106
+ # """
107
+ # if self.index is None:
108
+ # print("Index not loaded. Please check that the index path is correct.")
109
+ # return []
110
+
111
+ # query_embedding = self._extract_embedding(image)
112
+ # if query_embedding is None:
113
+ # return []
114
+
115
+ # # Perform the similarity search
116
+ # search_k = min(num_recommendations * 3, self.index.ntotal)
117
+ # distances, indices = self.index.search(query_embedding.reshape(1, -1), search_k)
118
+
119
+ # results = []
120
+ # seen_categories = set()
121
+
122
+ # for dist, idx in zip(distances[0], indices[0]):
123
+ # if idx != -1:
124
+ # metadata = self.metadata[idx]
125
+ # # No need to check for query_image_path anymore since we're handling objects
126
+ # similarity_score = 1 / (1 + float(dist))
127
+ # if metadata.get("category") not in seen_categories:
128
+ # result = {
129
+ # "metadata": metadata,
130
+ # "distance": float(dist),
131
+ # "similarity_score": similarity_score
132
+ # }
133
+ # results.append(result)
134
+ # seen_categories.add(metadata.get("category"))
135
+
136
+ # results.sort(key=lambda x: x["similarity_score"], reverse=True)
137
+ # return results[:num_recommendations]
138
+
139
+ # def format_results(recommendations):
140
+ # """Format the recommendation results for display in the Gradio interface"""
141
+ # if not recommendations:
142
+ # return "No recommendations found."
143
+
144
+ # result_html = "<h3>Recommended Jewelry Items:</h3>"
145
+ # for i, rec in enumerate(recommendations, 1):
146
+ # metadata = rec["metadata"]
147
+ # result_html += f"<div style='margin-bottom:15px; padding:10px; border:1px solid #ddd; border-radius:5px;'>"
148
+ # result_html += f"<h4>#{i}: {metadata.get('name', 'Unknown')}</h4>"
149
+ # result_html += f"<p><b>Category:</b> {metadata.get('category', 'Unknown')}</p>"
150
+ # result_html += f"<p><b>Description:</b> {metadata.get('description', 'No description available')}</p>"
151
+ # result_html += f"<p><b>Price:</b> ${metadata.get('price', 'N/A')}</p>"
152
+ # result_html += f"<p><b>Similarity Score:</b> {rec['similarity_score']:.4f}</p>"
153
+ # if 'image_url' in metadata:
154
+ # result_html += f"<p><img src='{metadata['image_url']}' style='max-width:200px; max-height:200px;'></p>"
155
+ # result_html += "</div>"
156
+
157
+ # return result_html
158
+
159
+ # def process_image(image, num_recommendations=5):
160
+ # """Process the image and return recommendations"""
161
+ # recommender = JewelryRecommenderServing()
162
+ # recommendations = recommender.get_recommendations(image, num_recommendations)
163
+ # return format_results(recommendations)
164
+
165
+ # def process_url(url, num_recommendations=5):
166
+ # """Process an image from a URL and return recommendations"""
167
+ # try:
168
+ # import requests
169
+ # response = requests.get(url)
170
+ # image = Image.open(io.BytesIO(response.content))
171
+ # return process_image(image, num_recommendations)
172
+ # except Exception as e:
173
+ # return f"Error processing URL: {str(e)}"
174
+
175
+ # def process_base64(base64_string, num_recommendations=5):
176
+ # """Process a base64-encoded image and return recommendations"""
177
+ # try:
178
+ # # Remove data URL prefix if present
179
+ # if ',' in base64_string:
180
+ # base64_string = base64_string.split(',', 1)[1]
181
+
182
+ # image_bytes = base64.b64decode(base64_string)
183
+ # image = Image.open(io.BytesIO(image_bytes))
184
+ # return process_image(image, num_recommendations)
185
+ # except Exception as e:
186
+ # return f"Error processing base64 image: {str(e)}"
187
+
188
+ # # Create Gradio interface
189
+ # def create_gradio_interface():
190
+ # with gr.Blocks(title="Jewelry Recommender") as demo:
191
+ # gr.Markdown("# Jewelry Recommendation System")
192
+ # gr.Markdown("Upload an image of jewelry to get similar recommendations.")
193
+
194
+ # with gr.Tab("Upload Image"):
195
+ # with gr.Row():
196
+ # image_input = gr.Image(type="pil", label="Upload Jewelry Image")
197
+ # num_recs_slider = gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Number of Recommendations")
198
+ # submit_btn = gr.Button("Get Recommendations")
199
+ # output_html = gr.HTML(label="Recommendations")
200
+ # submit_btn.click(process_image, inputs=[image_input, num_recs_slider], outputs=output_html)
201
+
202
+ # with gr.Tab("Image URL"):
203
+ # with gr.Row():
204
+ # url_input = gr.Textbox(label="Enter Image URL")
205
+ # url_num_recs = gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Number of Recommendations")
206
+ # url_btn = gr.Button("Get Recommendations from URL")
207
+ # url_output = gr.HTML(label="Recommendations")
208
+ # url_btn.click(process_url, inputs=[url_input, url_num_recs], outputs=url_output)
209
+
210
+ # with gr.Tab("Base64 Image"):
211
+ # with gr.Row():
212
+ # base64_input = gr.Textbox(label="Enter Base64 Image String")
213
+ # base64_num_recs = gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Number of Recommendations")
214
+ # base64_btn = gr.Button("Get Recommendations from Base64")
215
+ # base64_output = gr.HTML(label="Recommendations")
216
+ # base64_btn.click(process_base64, inputs=[base64_input, base64_num_recs], outputs=base64_output)
217
+
218
+ # gr.Markdown("## How to Use")
219
+ # gr.Markdown("""
220
+ # 1. Upload an image of jewelry, provide an image URL, or paste a base64-encoded image
221
+ # 2. Adjust the number of recommendations you want to see
222
+ # 3. Click the 'Get Recommendations' button
223
+ # 4. View similar jewelry items based on visual similarity
224
+ # """)
225
+
226
+ # return demo
227
+
228
+ # # For Hugging Face Spaces deployment
229
+ # if __name__ == "__main__":
230
+ # demo = create_gradio_interface()
231
+ # demo.launch()
#temp_del/rawsnippet.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import numpy as np
3
+ import pickle
4
+ import torch
5
+ import torchvision.transforms as transforms
6
+ import torchvision.models as models
7
+ from PIL import Image, ImageOps
8
+ import os
9
+ import warnings
10
+
11
+ class JewelryRecommenderServing:
12
+ def __init__(self,
13
+ vector_dimension: int = 1280,
14
+ index_path: str = "/path/to/jewelry_index.idx",
15
+ metadata_path: str = "/path/to/jewelry_metadata.pkl"):
16
+
17
+ warnings.filterwarnings("ignore")
18
+
19
+ # Load index and metadata
20
+ self.index_path = index_path
21
+ self.metadata_path = metadata_path
22
+ self.index = None
23
+ self.metadata = {}
24
+
25
+ # Load model for feature extraction
26
+ self.model = models.efficientnet_b0(weights='EfficientNet_B0_Weights.DEFAULT')
27
+ self.model.eval()
28
+ self.model = torch.nn.Sequential(*list(self.model.children())[:-1])
29
+
30
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
31
+ self.model = self.model.to(self.device)
32
+
33
+ # Image transformation
34
+ self.transform = transforms.Compose([
35
+ transforms.Lambda(lambda img: ImageOps.exif_transpose(img)),
36
+ transforms.Resize((640, 640)),
37
+ transforms.ToTensor(),
38
+ transforms.Normalize(
39
+ mean=[0.485, 0.456, 0.406],
40
+ std=[0.229, 0.224, 0.225]
41
+ )
42
+ ])
43
+
44
+ # Load the existing index and metadata
45
+ self.load_index_and_metadata()
46
+
47
+ def load_index_and_metadata(self) -> bool:
48
+ """Load the pre-built FAISS index and metadata from files"""
49
+ try:
50
+ if os.path.exists(self.index_path) and os.path.exists(self.metadata_path):
51
+ self.index = faiss.read_index(self.index_path)
52
+ with open(self.metadata_path, "rb") as f:
53
+ self.metadata = pickle.load(f)
54
+ print("Index and metadata loaded successfully.")
55
+ return True
56
+ else:
57
+ print(f"Index file or metadata file not found at {self.index_path} or {self.metadata_path}")
58
+ return False
59
+ except Exception as e:
60
+ print(f"Error loading index or metadata: {e}")
61
+ return False
62
+
63
+ def _extract_embedding(self, image_path: str) -> np.ndarray:
64
+ """Extract embedding from an image using the pre-trained model"""
65
+ try:
66
+ with Image.open(image_path).convert('RGB') as img:
67
+ img_tensor = self.transform(img).unsqueeze(0).to(self.device)
68
+ with torch.no_grad():
69
+ embedding = self.model(img_tensor).squeeze().cpu().numpy()
70
+ return embedding
71
+ except Exception as e:
72
+ print(f"Error processing image {image_path}: {str(e)}")
73
+ return None
74
+
75
+ def get_recommendations(self, query_image_path: str, num_recommendations: int = 5):
76
+ """Get recommendations for a query image based on similarity"""
77
+ if self.index is None:
78
+ print("Index not loaded. Please check that the index path is correct.")
79
+ return []
80
+
81
+ query_embedding = self._extract_embedding(query_image_path)
82
+ if query_embedding is None:
83
+ return []
84
+
85
+ # Perform the similarity search
86
+ search_k = min(num_recommendations * 3, self.index.ntotal)
87
+ distances, indices = self.index.search(query_embedding.reshape(1, -1), search_k)
88
+
89
+ results = []
90
+ seen_categories = set()
91
+
92
+ for dist, idx in zip(distances[0], indices[0]):
93
+ if idx != -1:
94
+ metadata = self.metadata[idx]
95
+ if metadata["full_path"] != query_image_path:
96
+ similarity_score = 1 / (1 + float(dist))
97
+ if metadata.get("category") not in seen_categories:
98
+ result = {
99
+ "metadata": metadata,
100
+ "distance": float(dist),
101
+ "similarity_score": similarity_score
102
+ }
103
+ results.append(result)
104
+ seen_categories.add(metadata.get("category"))
105
+
106
+ results.sort(key=lambda x: x["similarity_score"], reverse=True)
107
+ return results[:num_recommendations]
108
+
109
+
110
+ # Usage example:
111
+ def serve_recommendations(image_path, num_recommendations=5):
112
+ # Initialize the recommender with paths to your saved files
113
+ recommender = JewelryRecommenderServing(
114
+ index_path="/path/to/jewelry_index.idx",
115
+ metadata_path="/path/to/jewelry_metadata.pkl"
116
+ )
117
+
118
+ # Get recommendations
119
+ recommendations = recommender.get_recommendations(image_path, num_recommendations)
120
+
121
+ return recommendations
app.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ from gradio_app import create_gradio_interface
3
+
4
+ def main():
5
+ """Main entry point to run the Jewelry Recommender application."""
6
+ print("Starting Jewelry Recommender System...")
7
+ demo = create_gradio_interface()
8
+ demo.launch()
9
+
10
+ if __name__ == "__main__":
11
+ main()
app.yml ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ title: Jewelry Recommender
2
+ emoji: 💎
3
+ colorFrom: purple
4
+ colorTo: pink
5
+ sdk: gradio
6
+ sdk_version: 3.50.2
7
+ app_file: updatedcode/app.py
8
+ pinned: false
9
+ license: mit
10
+ duplicated_from: null
11
+ models:
12
+ - efficientnet
13
+ - faiss
14
+ python_version: 3.9
15
+ datasets:
16
+ - None
17
+ tags:
18
+ - image-similarity
19
+ - jewelry
20
+ - recommendation-system
21
+ - computer-vision
22
+
23
+ # Gradio configuration
24
+ gradio:
25
+ theme: default
26
+ dark_background: False
27
+ live: False
28
+ capture_session: False
29
+ allow_flagging: never
30
+ queue_concurrency_count: 1
31
+ max_file_size: 10
32
+
33
+ # System dependencies
34
+ dependencies:
35
+ -torch>=2.0.0
36
+ -torchvision>=0.15.0
37
+ -faiss-cpu>=1.7.0
38
+ -scikit-learn>=1.0.0
39
+ -numpy>=1.20.0
40
+ -pandas>=1.3.0
41
+ -pyarrow>=7.0.0
42
+ -matplotlib>=3.5.0
43
+ -Pillow>=9.0.0
44
+ -tqdm>=4.60.0
45
+ -ipywidgets>=7.7.0
46
+ -gdown>=4.5.0
47
+ -gradio>=3.0.0
48
+ -concurrent-log-handler>=0.9.20
49
+ -plotly>=5.10.0
50
+
51
+
52
+ # Space hardware
53
+ hardware:
54
+ accelerator: cpu
55
+ cpu: 2
56
+ memory: 16GB
57
+
58
+ # Required files for the application
59
+ files:
60
+ - app.py
61
+ - jewelry_index.idx
62
+ - jewelry_metadata.pkl
63
+ - README.md
64
+
65
+ # Documentation
66
+ information:
67
+ description: >
68
+ This Jewelry Recommender app uses computer vision to find similar jewelry items
69
+ based on a reference image. Upload an image of jewelry, provide an image URL,
70
+ or paste a base64-encoded image to get visually similar recommendations.
71
+ The system uses an EfficientNet model for feature extraction and FAISS for fast similarity search.
72
+ license: MIT
73
+ author: Maazuddin
74
+ repository: https://github.com/Maazuddin1/jewelry-recommender
backend/jewelry_recomm_service.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jewelry_recommender.py
2
+ import warnings
3
+ from config import Config
4
+
5
+ from supportingfiles.model_loader import ModelLoader
6
+ from supportingfiles.image_processor import ImageProcessor
7
+ from supportingfiles.recommender import RecommenderEngine
8
+
9
+ class JewelryRecommenderService:
10
+ """Main service class for the Jewelry Recommender System."""
11
+
12
+ def __init__(self,
13
+ index_path=None,
14
+ metadata_path=None):
15
+ """Initialize the jewelry recommender service.
16
+
17
+ Args:
18
+ index_path (str, optional): Path to FAISS index
19
+ metadata_path (str, optional): Path to metadata pickle file
20
+ """
21
+ warnings.filterwarnings("ignore")
22
+
23
+ # Load the model
24
+ self.model = ModelLoader.load_feature_extraction_model()
25
+
26
+ # Load index and metadata
27
+ self.index, self.metadata, success = ModelLoader.load_index_and_metadata(
28
+ index_path, metadata_path
29
+ )
30
+
31
+ # Initialize pipeline components
32
+ self.image_processor = ImageProcessor(self.model)
33
+ self.recommender = RecommenderEngine(self.index, self.metadata)
34
+
35
+ def get_recommendations(self, image, num_recommendations=None):
36
+ """Get recommendations for a query image.
37
+
38
+ Args:
39
+ image: Query image (various formats)
40
+ num_recommendations (int, optional): Number of recommendations
41
+
42
+ Returns:
43
+ list: Recommendation results
44
+ """
45
+ num_recommendations = num_recommendations or Config.DEFAULT_NUM_RECOMMENDATIONS
46
+
47
+ # Extract embedding from the image
48
+ embedding = self.image_processor.extract_embedding(image)
49
+
50
+ # Get similar items based on the embedding
51
+ recommendations = self.recommender.find_similar_items(
52
+ embedding, num_recommendations
53
+ )
54
+
55
+ return recommendations
backend/supportingfiles/image_processor.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # image_processor.py
2
+ import io
3
+ import torch
4
+ import numpy as np
5
+ from PIL import Image
6
+ from config import Config
7
+
8
+ class ImageProcessor:
9
+ """Handles processing and feature extraction from images."""
10
+
11
+ def __init__(self, model):
12
+ """Initialize with a pre-trained model.
13
+
14
+ Args:
15
+ model: The pre-trained model for feature extraction
16
+ """
17
+ self.model = model
18
+ self.transform = Config.get_image_transform()
19
+
20
+ def normalize_image_input(self, image):
21
+ """Normalize different image input types to a PIL Image.
22
+
23
+ Args:
24
+ image: Can be a PIL.Image, file path, byte stream, or numpy array
25
+
26
+ Returns:
27
+ PIL.Image: The normalized image
28
+ """
29
+ try:
30
+ if isinstance(image, str):
31
+ # If image is a file path
32
+ return Image.open(image).convert('RGB')
33
+ elif isinstance(image, bytes) or isinstance(image, io.BytesIO):
34
+ # If image is a byte stream
35
+ if isinstance(image, bytes):
36
+ image = io.BytesIO(image)
37
+ return Image.open(image).convert('RGB')
38
+ elif isinstance(image, np.ndarray):
39
+ # If image is a numpy array (as from gradio)
40
+ return Image.fromarray(image.astype('uint8')).convert('RGB')
41
+ elif isinstance(image, Image.Image):
42
+ # If image is already a PIL Image
43
+ return image.convert('RGB')
44
+ else:
45
+ raise ValueError(f"Unsupported image type: {type(image)}")
46
+ except Exception as e:
47
+ print(f"Error normalizing image: {str(e)}")
48
+ return None
49
+
50
+ def extract_embedding(self, image):
51
+ """Extract feature embedding from an image.
52
+
53
+ Args:
54
+ image: The image to extract features from (various formats accepted)
55
+
56
+ Returns:
57
+ numpy.ndarray: The feature embedding or None if extraction failed
58
+ """
59
+ try:
60
+ img = self.normalize_image_input(image)
61
+ if img is None:
62
+ return None
63
+
64
+ img_tensor = self.transform(img).unsqueeze(0).to(Config.DEVICE)
65
+ with torch.no_grad():
66
+ embedding = self.model(img_tensor).squeeze().cpu().numpy()
67
+ return embedding
68
+ except Exception as e:
69
+ print(f"Error extracting embedding: {str(e)}")
70
+ return None
backend/supportingfiles/model_loader.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # model_loader.py
2
+ import os
3
+ import pickle
4
+ import faiss
5
+ import torch
6
+ import torchvision.models as models
7
+ import warnings
8
+ from config import Config
9
+
10
+ class ModelLoader:
11
+ """Handles loading of the feature extraction model and FAISS index."""
12
+
13
+ @staticmethod
14
+ def load_feature_extraction_model():
15
+ """Loads and configures the EfficientNet model for feature extraction."""
16
+ print("Loading feature extraction model...")
17
+ model = models.efficientnet_b0(weights='EfficientNet_B0_Weights.DEFAULT')
18
+ model.eval()
19
+ # Remove the classification head
20
+ model = torch.nn.Sequential(*list(model.children())[:-1])
21
+ model = model.to(Config.DEVICE)
22
+ return model
23
+
24
+ @staticmethod
25
+ def load_index_and_metadata(index_path=None, metadata_path=None):
26
+ """Loads the FAISS index and metadata from files.
27
+
28
+ Args:
29
+ index_path (str): Path to the FAISS index file
30
+ metadata_path (str): Path to the metadata pickle file
31
+
32
+ Returns:
33
+ tuple: (index, metadata, success_flag)
34
+ """
35
+ warnings.filterwarnings("ignore")
36
+
37
+ index_path = index_path or Config.INDEX_PATH
38
+ metadata_path = metadata_path or Config.METADATA_PATH
39
+
40
+ try:
41
+ if os.path.exists(index_path) and os.path.exists(metadata_path):
42
+ index = faiss.read_index(index_path)
43
+ with open(metadata_path, "rb") as f:
44
+ metadata = pickle.load(f)
45
+ print(f"Index and metadata loaded successfully.")
46
+ return index, metadata, True
47
+ else:
48
+ print(f"Index file or metadata file not found.")
49
+ return None, {}, False
50
+ except Exception as e:
51
+ print(f"Error loading index or metadata: {e}")
52
+ return None, {}, False
backend/supportingfiles/recommender.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # recommender.py
2
+ import numpy as np
3
+ from config import Config
4
+
5
+ class RecommenderEngine:
6
+ """Engine for finding similar jewelry items based on image embeddings."""
7
+
8
+ def __init__(self, index, metadata):
9
+ """Initialize with FAISS index and metadata.
10
+
11
+ Args:
12
+ index: FAISS index for similarity search
13
+ metadata (dict): Metadata for the indexed items
14
+ """
15
+ self.index = index
16
+ self.metadata = metadata
17
+
18
+ def find_similar_items(self, embedding, num_recommendations=None, skip_exact_match=True):
19
+ """Find similar items based on embedding vector.
20
+
21
+ Args:
22
+ embedding (numpy.ndarray): The query embedding vector
23
+ num_recommendations (int): Number of recommendations to return
24
+ skip_exact_match (bool): Whether to skip the first result (exact match)
25
+
26
+ Returns:
27
+ list: Sorted list of recommendation dictionaries
28
+ """
29
+ if self.index is None:
30
+ print("Error: Index not loaded")
31
+ return []
32
+
33
+ if embedding is None:
34
+ print("Error: Invalid embedding")
35
+ return []
36
+
37
+ num_recommendations = num_recommendations or Config.DEFAULT_NUM_RECOMMENDATIONS
38
+
39
+ # Calculate how many items to retrieve based on whether we're skipping the first match
40
+ search_k = num_recommendations
41
+ if skip_exact_match:
42
+ search_k += 1
43
+
44
+ # Get exact number of results we need
45
+ distances, indices = self.index.search(embedding.reshape(1, -1), search_k)
46
+
47
+ results = []
48
+
49
+ # Start from index 1 to skip the first result (closest match) if skip_exact_match is True
50
+ start_idx = 1 if skip_exact_match and len(indices[0]) > 1 else 0
51
+
52
+ for dist, idx in zip(distances[0][start_idx:], indices[0][start_idx:]):
53
+ if idx != -1:
54
+ metadata = self.metadata[idx]
55
+ similarity_score = 1 / (1 + float(dist))
56
+
57
+ # Add item to results without category filtering
58
+ result = {
59
+ "metadata": metadata,
60
+ "distance": float(dist),
61
+ "similarity_score": similarity_score
62
+ }
63
+ results.append(result)
64
+
65
+ # Sort by similarity score (highest first)
66
+ results.sort(key=lambda x: x["similarity_score"], reverse=True)
67
+ return results[:num_recommendations]
config.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ import torch
4
+ import torchvision.transforms as transforms
5
+
6
+ class Config:
7
+ """Configuration class for the Jewelry Recommender System."""
8
+
9
+ # Model settings
10
+ VECTOR_DIMENSION = 1280
11
+ INDEX_PATH = "rootdir/trained_models/jewelry_index.idx"
12
+ METADATA_PATH = "rootdir/trained_models/jewelry_metadata.pkl"
13
+
14
+ # Hardware settings
15
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+
17
+ # Image processing settings
18
+ IMAGE_SIZE = (640, 640)
19
+ NORMALIZATION_MEAN = [0.485, 0.456, 0.406]
20
+ NORMALIZATION_STD = [0.229, 0.224, 0.225]
21
+
22
+ # Recommendation settings
23
+ DEFAULT_NUM_RECOMMENDATIONS = 5
24
+ MAX_RECOMMENDATIONS = 20
25
+
26
+ @classmethod
27
+ def get_image_transform(cls):
28
+ """Returns the image transformation pipeline."""
29
+ from PIL import ImageOps
30
+ return transforms.Compose([
31
+ transforms.Lambda(lambda img: ImageOps.exif_transpose(img)),
32
+ transforms.Resize(cls.IMAGE_SIZE),
33
+ transforms.ToTensor(),
34
+ transforms.Normalize(
35
+ mean=cls.NORMALIZATION_MEAN,
36
+ std=cls.NORMALIZATION_STD
37
+ )
38
+ ])
frontend/gradio_app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gradio_app.py
2
+ import gradio as gr
3
+ from input_handlers import InputHandlers
4
+ from config import Config
5
+
6
+ def create_gradio_interface():
7
+ """Create and configure the Gradio web interface.
8
+
9
+ Returns:
10
+ gradio.Blocks: The configured Gradio interface
11
+ """
12
+ with gr.Blocks(title="Jewelry Recommender") as demo:
13
+ gr.Markdown("# Jewelry Recommendation System")
14
+ gr.Markdown("Upload an image of jewelry to get similar recommendations.")
15
+
16
+ with gr.Tab("Upload Image"):
17
+ with gr.Row():
18
+ image_input = gr.Image(type="pil", label="Upload Jewelry Image")
19
+ num_recs_slider = gr.Slider(
20
+ minimum=1,
21
+ maximum=Config.MAX_RECOMMENDATIONS,
22
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
23
+ step=1,
24
+ label="Number of Recommendations"
25
+ )
26
+ skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
27
+ submit_btn = gr.Button("Get Recommendations")
28
+ output_html = gr.HTML(label="Recommendations")
29
+ submit_btn.click(
30
+ InputHandlers.process_image,
31
+ inputs=[image_input, num_recs_slider, skip_exact],
32
+ outputs=output_html
33
+ )
34
+
35
+ with gr.Tab("Image URL"):
36
+ with gr.Row():
37
+ url_input = gr.Textbox(label="Enter Image URL")
38
+ url_num_recs = gr.Slider(
39
+ minimum=1,
40
+ maximum=Config.MAX_RECOMMENDATIONS,
41
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
42
+ step=1,
43
+ label="Number of Recommendations"
44
+ )
45
+ url_skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
46
+ url_btn = gr.Button("Get Recommendations from URL")
47
+ url_output = gr.HTML(label="Recommendations")
48
+ url_btn.click(
49
+ InputHandlers.process_url,
50
+ inputs=[url_input, url_num_recs, url_skip_exact],
51
+ outputs=url_output
52
+ )
53
+
54
+ with gr.Tab("Base64 Image"):
55
+ with gr.Row():
56
+ base64_input = gr.Textbox(label="Enter Base64 Image String")
57
+ base64_num_recs = gr.Slider(
58
+ minimum=1,
59
+ maximum=Config.MAX_RECOMMENDATIONS,
60
+ value=Config.DEFAULT_NUM_RECOMMENDATIONS,
61
+ step=1,
62
+ label="Number of Recommendations"
63
+ )
64
+ base64_skip_exact = gr.Checkbox(value=True, label="Skip Exact Match")
65
+ base64_btn = gr.Button("Get Recommendations from Base64")
66
+ base64_output = gr.HTML(label="Recommendations")
67
+ base64_btn.click(
68
+ InputHandlers.process_base64,
69
+ inputs=[base64_input, base64_num_recs, base64_skip_exact],
70
+ outputs=base64_output
71
+ )
72
+
73
+ gr.Markdown("## How to Use")
74
+ gr.Markdown("""
75
+ 1. Upload an image of jewelry, provide an image URL, or paste a base64-encoded image
76
+ 2. Adjust the number of recommendations you want to see
77
+ 3. Check "Skip Exact Match" to exclude the identical or closest match from results
78
+ 4. Click the 'Get Recommendations' button
79
+ 5. View similar jewelry items based on visual similarity
80
+ """)
81
+
82
+ return demo
frontend/input_handlers.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # input_handlers.py
2
+ import io
3
+ import base64
4
+ from PIL import Image
5
+ from backend.jewelry_recomm_service import JewelryRecommenderService
6
+ from utils.formatter import ResultFormatter
7
+
8
+ class InputHandlers:
9
+ """Handles different types of image inputs for recommendation."""
10
+
11
+ @staticmethod
12
+ def process_image(image, num_recommendations=5, skip_exact_match=True):
13
+ """Process direct image input.
14
+
15
+ Args:
16
+ image: The image (PIL, numpy array, etc.)
17
+ num_recommendations (int): Number of recommendations
18
+ skip_exact_match (bool): Whether to skip the first/exact match
19
+
20
+ Returns:
21
+ str: HTML formatted results
22
+ """
23
+ recommender = JewelryRecommenderService()
24
+ recommendations = recommender.get_recommendations(
25
+ image, num_recommendations, skip_exact_match
26
+ )
27
+ return ResultFormatter.format_html(recommendations)
28
+
29
+ @staticmethod
30
+ def process_url(url, num_recommendations=5, skip_exact_match=True):
31
+ """Process image from URL.
32
+
33
+ Args:
34
+ url (str): URL to the image
35
+ num_recommendations (int): Number of recommendations
36
+ skip_exact_match (bool): Whether to skip the first/exact match
37
+
38
+ Returns:
39
+ str: HTML formatted results
40
+ """
41
+ try:
42
+ import requests
43
+ response = requests.get(url)
44
+ image = Image.open(io.BytesIO(response.content))
45
+ return InputHandlers.process_image(image, num_recommendations, skip_exact_match)
46
+ except Exception as e:
47
+ return f"Error processing URL: {str(e)}"
48
+
49
+ @staticmethod
50
+ def process_base64(base64_string, num_recommendations=5, skip_exact_match=True):
51
+ """Process base64-encoded image.
52
+
53
+ Args:
54
+ base64_string (str): Base64 encoded image
55
+ num_recommendations (int): Number of recommendations
56
+ skip_exact_match (bool): Whether to skip the first/exact match
57
+
58
+ Returns:
59
+ str: HTML formatted results
60
+ """
61
+ try:
62
+ # Remove data URL prefix if present
63
+ if ',' in base64_string:
64
+ base64_string = base64_string.split(',', 1)[1]
65
+
66
+ image_bytes = base64.b64decode(base64_string)
67
+ image = Image.open(io.BytesIO(image_bytes))
68
+ return InputHandlers.process_image(image, num_recommendations, skip_exact_match)
69
+ except Exception as e:
70
+ return f"Error processing base64 image: {str(e)}"
models/jewelry_metadata.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ee0c0ff1ca72d10ede65576643059c5093daab4546892641ae46abc2fa96efd5
3
+ size 14415743
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # requirements.txt
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+ faiss-cpu>=1.7.0
5
+ scikit-learn>=1.0.0
6
+ numpy>=1.20.0
7
+ pandas>=1.3.0
8
+ pyarrow>=7.0.0
9
+ matplotlib>=3.5.0
10
+ Pillow>=9.0.0
11
+ tqdm>=4.60.0
12
+ ipywidgets>=7.7.0
13
+ gdown>=4.5.0
14
+ gradio>=3.0.0
15
+ concurrent-log-handler>=0.9.20
16
+ plotly>=5.10.0
17
+ requests
utils/formatter.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # formatter.py
2
+
3
+ class ResultFormatter:
4
+ """Formats recommendation results for display."""
5
+
6
+ @staticmethod
7
+ def format_html(recommendations):
8
+ """Format recommendations as HTML for the Gradio interface.
9
+
10
+ Args:
11
+ recommendations (list): List of recommendation dictionaries
12
+
13
+ Returns:
14
+ str: HTML formatted results
15
+ """
16
+ if not recommendations:
17
+ return "No recommendations found."
18
+
19
+ result_html = "<h3>Recommended Jewelry Items:</h3>"
20
+ for i, rec in enumerate(recommendations, 1):
21
+ metadata = rec["metadata"]
22
+ result_html += f"<div style='margin-bottom:15px; padding:10px; border:1px solid #ddd; border-radius:5px;'>"
23
+ result_html += f"<h4>#{i}: {metadata.get('name', 'Unknown')}</h4>"
24
+ result_html += f"<p><b>Category:</b> {metadata.get('category', 'Unknown')}</p>"
25
+ result_html += f"<p><b>Description:</b> {metadata.get('description', 'No description available')}</p>"
26
+ result_html += f"<p><b>Price:</b> ${metadata.get('price', 'N/A')}</p>"
27
+ result_html += f"<p><b>Similarity Score:</b> {rec['similarity_score']:.4f}</p>"
28
+ if 'image_url' in metadata:
29
+ result_html += f"<p><img src='{metadata['image_url']}' style='max-width:200px; max-height:200px;'></p>"
30
+ result_html += "</div>"
31
+
32
+ return result_html
33
+
34
+ @staticmethod
35
+ def format_json(recommendations):
36
+ """Format recommendations as JSON.
37
+
38
+ Args:
39
+ recommendations (list): List of recommendation dictionaries
40
+
41
+ Returns:
42
+ list: Clean JSON-serializable results
43
+ """
44
+ if not recommendations:
45
+ return []
46
+
47
+ results = []
48
+ for rec in recommendations:
49
+ results.append({
50
+ "item": rec["metadata"].get("name", "Unknown"),
51
+ "category": rec["metadata"].get("category", "Unknown"),
52
+ "description": rec["metadata"].get("description", "No description"),
53
+ "price": rec["metadata"].get("price", "N/A"),
54
+ "similarity_score": round(rec["similarity_score"], 4),
55
+ "image_url": rec["metadata"].get("image_url", None)
56
+ })
57
+
58
+ return results