coder1208 commited on
Commit
0200ff9
Β·
verified Β·
1 Parent(s): 9e59cc7

Upload app.py

Browse files

Initial commit

Files changed (1) hide show
  1. app.py +342 -0
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visual Product Matcher - Hugging Face Spaces Backend
3
+ AI-powered product similarity search using CLIP model
4
+ """
5
+
6
+ import gradio as gr
7
+ from flask import Flask, request, jsonify
8
+ from sentence_transformers import SentenceTransformer
9
+ from PIL import Image
10
+ import requests
11
+ import json
12
+ import numpy as np
13
+ import io
14
+ import base64
15
+ import os
16
+ import threading
17
+ import logging
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Initialize Flask app for API endpoints
24
+ flask_app = Flask(__name__)
25
+
26
+ # Load model
27
+ print("πŸ”„ Loading CLIP model...")
28
+ model = SentenceTransformer("sentence-transformers/clip-ViT-B-32")
29
+ print("βœ… Model loaded successfully!")
30
+
31
+ # Load products database
32
+ print("πŸ“‚ Loading products database...")
33
+ with open("server/products.json", "r", encoding="utf-8") as f:
34
+ products = json.load(f)
35
+ print(f"βœ… Loaded {len(products)} products")
36
+
37
+ # Configuration
38
+ IMAGE_FOLDER = "server/assets/product"
39
+
40
+ # ============================================================================
41
+ # HELPER FUNCTIONS
42
+ # ============================================================================
43
+
44
+ def cosine_similarity(a, b):
45
+ """Calculate cosine similarity between two vectors"""
46
+ a = np.array(a)
47
+ b = np.array(b)
48
+ return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
49
+
50
+
51
+ def load_image_from_base64(data_string):
52
+ """Load image from base64 string"""
53
+ try:
54
+ if "," in data_string:
55
+ image_bytes = base64.b64decode(data_string.split(",")[-1])
56
+ else:
57
+ image_bytes = base64.b64decode(data_string)
58
+ return Image.open(io.BytesIO(image_bytes)).convert("RGB")
59
+ except Exception as e:
60
+ raise ValueError(f"Failed to decode base64 image: {str(e)}")
61
+
62
+
63
+ def load_image_from_url(url):
64
+ """Load image from URL"""
65
+ if url.startswith("data:image"):
66
+ return load_image_from_base64(url)
67
+
68
+ headers = {'User-Agent': 'Mozilla/5.0'}
69
+ response = requests.get(url, timeout=15, headers=headers)
70
+ response.raise_for_status()
71
+ return Image.open(io.BytesIO(response.content)).convert("RGB")
72
+
73
+
74
+ def search_similar_products(image, top_k=20):
75
+ """
76
+ Core search function - finds similar products
77
+ Returns list of products sorted by similarity
78
+ """
79
+ query_embedding = model.encode(image)
80
+
81
+ results = []
82
+ for product in products:
83
+ if "embedding" not in product:
84
+ continue
85
+
86
+ score = cosine_similarity(query_embedding, product["embedding"])
87
+
88
+ # Use relative path for HF Spaces
89
+ image_url = f"/file={IMAGE_FOLDER}/{product['image']}"
90
+
91
+ results.append({
92
+ "id": product.get("id", ""),
93
+ "name": product["name"],
94
+ "price": product["price"],
95
+ "category": product.get("category", ""),
96
+ "description": product.get("description", ""),
97
+ "image": image_url,
98
+ "similarity": score
99
+ })
100
+
101
+ results.sort(key=lambda x: x["similarity"], reverse=True)
102
+ return results[:top_k]
103
+
104
+
105
+ # ============================================================================
106
+ # FLASK API ENDPOINTS (for Vercel integration)
107
+ # ============================================================================
108
+
109
+ @flask_app.route("/api/health", methods=["GET"])
110
+ def health_check():
111
+ """Health check endpoint"""
112
+ return jsonify({
113
+ "status": "ok",
114
+ "service": "Visual Product Matcher",
115
+ "model": "sentence-transformers/clip-ViT-B-32",
116
+ "products_loaded": len(products)
117
+ })
118
+
119
+
120
+ @flask_app.route("/api/match", methods=["POST", "OPTIONS"])
121
+ def match_products():
122
+ """
123
+ Main API endpoint for product matching
124
+ Accepts: {"image": "base64_data"} or {"imageUrl": "http://..."}
125
+ Returns: List of matching products
126
+ """
127
+ # Handle CORS preflight
128
+ if request.method == "OPTIONS":
129
+ response = jsonify({"status": "ok"})
130
+ response.headers.add("Access-Control-Allow-Origin", "*")
131
+ response.headers.add("Access-Control-Allow-Headers", "Content-Type")
132
+ response.headers.add("Access-Control-Allow-Methods", "POST, OPTIONS")
133
+ return response
134
+
135
+ try:
136
+ data = request.json
137
+
138
+ if not data:
139
+ return jsonify({"error": "No JSON data provided"}), 400
140
+
141
+ image_data = data.get("image")
142
+ image_url = data.get("imageUrl")
143
+
144
+ if not image_data and not image_url:
145
+ return jsonify({"error": "No image provided"}), 400
146
+
147
+ # Load image
148
+ if image_data:
149
+ image = load_image_from_base64(image_data)
150
+ else:
151
+ image = load_image_from_url(image_url)
152
+
153
+ # Search for similar products
154
+ results = search_similar_products(image, top_k=20)
155
+
156
+ # Add CORS headers
157
+ response = jsonify(results)
158
+ response.headers.add("Access-Control-Allow-Origin", "*")
159
+ response.headers.add("Access-Control-Allow-Headers", "Content-Type")
160
+
161
+ return response
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error in match_products: {str(e)}")
165
+ error_response = jsonify({"error": str(e)})
166
+ error_response.headers.add("Access-Control-Allow-Origin", "*")
167
+ return error_response, 400
168
+
169
+
170
+ # ============================================================================
171
+ # GRADIO UI (for web interface)
172
+ # ============================================================================
173
+
174
+ def gradio_search(image):
175
+ """Gradio interface function"""
176
+ if image is None:
177
+ return "⚠️ Please upload an image", []
178
+
179
+ try:
180
+ results = search_similar_products(image, top_k=6)
181
+
182
+ # Format text output
183
+ output_lines = ["# πŸ” Search Results\n"]
184
+
185
+ for i, product in enumerate(results, 1):
186
+ output_lines.append(f"### {i}. {product['name']}")
187
+ output_lines.append(f"**Price:** ${product['price']}")
188
+ output_lines.append(f"**Similarity:** {product['similarity']:.1%}")
189
+ output_lines.append(f"**Category:** {product['category']}")
190
+ if product['description']:
191
+ output_lines.append(f"*{product['description']}*")
192
+ output_lines.append("---")
193
+
194
+ text_output = "\n".join(output_lines)
195
+
196
+ # Get images for gallery
197
+ image_paths = []
198
+ for product in results:
199
+ img_path = f"{IMAGE_FOLDER}/{product['image'].split('/')[-1]}"
200
+ if os.path.exists(img_path):
201
+ image_paths.append(img_path)
202
+
203
+ return text_output, image_paths
204
+
205
+ except Exception as e:
206
+ logger.error(f"Error in gradio_search: {str(e)}")
207
+ return f"❌ Error: {str(e)}", []
208
+
209
+
210
+ # Create Gradio interface (moved theme and css to launch method for Gradio 6.0)
211
+ with gr.Blocks(title="πŸ” Visual Product Matcher") as gradio_interface:
212
+
213
+ gr.Markdown(
214
+ """
215
+ # πŸ” Visual Product Matcher
216
+
217
+ Upload a product image to find similar items!
218
+
219
+ **Features:**
220
+ - πŸ€– AI-powered similarity search using CLIP
221
+ - πŸ“Έ Upload images or use camera
222
+ - ⚑ Instant results
223
+
224
+ ---
225
+ """
226
+ )
227
+
228
+ with gr.Row():
229
+ with gr.Column(scale=1):
230
+ image_input = gr.Image(
231
+ type="pil",
232
+ label="πŸ“Έ Upload Product Image",
233
+ sources=["upload", "webcam", "clipboard"]
234
+ )
235
+
236
+ search_btn = gr.Button(
237
+ "πŸ” Search Similar Products",
238
+ variant="primary",
239
+ size="lg"
240
+ )
241
+
242
+ gr.Markdown(
243
+ """
244
+ ### πŸ’‘ Tips:
245
+ - Upload clear product images
246
+ - Works best with centered products
247
+ - Supported: JPG, PNG, WebP
248
+ """
249
+ )
250
+
251
+ with gr.Column(scale=1):
252
+ text_output = gr.Markdown(label="Results")
253
+
254
+ with gr.Row():
255
+ gallery = gr.Gallery(
256
+ label="🎨 Similar Products",
257
+ columns=3,
258
+ rows=2,
259
+ height="auto",
260
+ object_fit="contain"
261
+ )
262
+
263
+ search_btn.click(
264
+ fn=gradio_search,
265
+ inputs=[image_input],
266
+ outputs=[text_output, gallery]
267
+ )
268
+
269
+ # Example images
270
+ example_images = []
271
+ for i in [1, 2, 10]:
272
+ img_path = f"{IMAGE_FOLDER}/product_{i}.jpg"
273
+ if os.path.exists(img_path):
274
+ example_images.append([img_path])
275
+
276
+ if example_images:
277
+ gr.Markdown("---\n### πŸ“Œ Try These Examples:")
278
+ gr.Examples(examples=example_images, inputs=image_input)
279
+
280
+ gr.Markdown(
281
+ """
282
+ ---
283
+
284
+ ### πŸ“Œ API Access
285
+
286
+ **Endpoint:** `POST /api/match`
287
+
288
+ **Request:**
289
+ ```json
290
+ {
291
+ "image": "base64_image_data",
292
+ "imageUrl": "https://example.com/image.jpg"
293
+ }
294
+ ```
295
+
296
+ **Response:** Array of similar products with similarity scores
297
+
298
+ ---
299
+
300
+ Built with ❀️ using Sentence Transformers CLIP
301
+ """
302
+ )
303
+
304
+
305
+ # ============================================================================
306
+ # LAUNCH APP
307
+ # ============================================================================
308
+
309
+ def run_flask():
310
+ """Run Flask API in background thread on port 5000"""
311
+ try:
312
+ flask_app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
313
+ except Exception as e:
314
+ logger.error(f"Flask error: {str(e)}")
315
+
316
+
317
+ if __name__ == "__main__":
318
+ # Start Flask API in background on port 5000
319
+ flask_thread = threading.Thread(target=run_flask, daemon=True)
320
+ flask_thread.start()
321
+
322
+ print("\n" + "="*60)
323
+ print("πŸš€ Visual Product Matcher Started!")
324
+ print("="*60)
325
+ print(f"πŸ“Š Products loaded: {len(products)}")
326
+ print(f"πŸ€– Model: sentence-transformers/clip-ViT-B-32")
327
+ print("="*60)
328
+ print("\n🌐 Gradio UI: Will be available at Space URL")
329
+ print("πŸ”Œ Flask API Endpoints (port 5000):")
330
+ print(" - GET http://localhost:5000/api/health")
331
+ print(" - POST http://localhost:5000/api/match")
332
+ print("="*60 + "\n")
333
+
334
+ # Launch Gradio on port 7860 (HF Spaces default)
335
+ # Moved theme and css parameters here for Gradio 6.0 compatibility
336
+ gradio_interface.launch(
337
+ server_name="0.0.0.0",
338
+ server_port=7860,
339
+ share=False,
340
+ theme=gr.themes.Soft(),
341
+ css=".gradio-container {max-width: 1200px; margin: auto;}"
342
+ )