FayssalJ commited on
Commit
da0fb08
·
verified ·
1 Parent(s): f6e0ca8

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +36 -6
  2. app.py +163 -0
  3. requirements.txt +8 -0
README.md CHANGED
@@ -1,13 +1,43 @@
1
  ---
2
- title: Visual Search
3
- emoji: 🦀
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.5.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Visual Product Search
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
12
 
13
+ # Visual Product Search API
14
+
15
+ AI-powered visual search using **Jina CLIP v2** embeddings.
16
+
17
+ ## Features
18
+ - Upload an image to find visually similar products
19
+ - Uses Jina CLIP v2 for state-of-the-art image embeddings
20
+ - Queries Pinecone vector database for similarity search
21
+
22
+ ## API Usage
23
+
24
+ ```python
25
+ from gradio_client import Client
26
+
27
+ client = Client("YOUR_USERNAME/visual-search")
28
+ result = client.predict(
29
+ "path/to/image.jpg",
30
+ api_name="/predict"
31
+ )
32
+ print(result)
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ Set these secrets in HuggingFace Space settings:
38
+ - `PINECONE_API_KEY`: Your Pinecone API key
39
+ - `PINECONE_HOST`: Your Pinecone index host (without https://)
40
+
41
+ ## Model
42
+
43
+ Uses [jinaai/jina-clip-v2](https://huggingface.co/jinaai/jina-clip-v2) - a multilingual multimodal embedding model.
app.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visual Search API - HuggingFace Space
3
+
4
+ Provides image embedding endpoint using Jina CLIP v2.
5
+ Queries Pinecone for similar products.
6
+
7
+ Deploy to HuggingFace Spaces with ZeroGPU (free).
8
+ """
9
+
10
+ import os
11
+ import gradio as gr
12
+ import torch
13
+ import numpy as np
14
+ from PIL import Image
15
+
16
+ # Pinecone config from HF Secrets
17
+ PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY')
18
+ PINECONE_HOST = os.environ.get('PINECONE_HOST')
19
+
20
+ # Model (loaded on first use)
21
+ model = None
22
+
23
+
24
+ def load_model():
25
+ """Load Jina CLIP v2 model."""
26
+ global model
27
+ if model is None:
28
+ print("Loading Jina CLIP v2...")
29
+ from transformers import AutoModel
30
+ model = AutoModel.from_pretrained(
31
+ "jinaai/jina-clip-v2",
32
+ trust_remote_code=True
33
+ )
34
+ if torch.cuda.is_available():
35
+ model = model.cuda()
36
+ model.eval()
37
+ print("Model loaded!")
38
+ return model
39
+
40
+
41
+ def get_embedding(image: Image.Image) -> list:
42
+ """Generate 512-dim embedding for an image."""
43
+ m = load_model()
44
+
45
+ with torch.no_grad():
46
+ emb = m.encode_image(image)
47
+ if hasattr(emb, 'cpu'):
48
+ emb = emb.cpu().numpy()
49
+ emb = emb.flatten()
50
+ emb = emb / np.linalg.norm(emb) # L2 normalize
51
+ if len(emb) > 512:
52
+ emb = emb[:512]
53
+ return emb.tolist()
54
+
55
+
56
+ def query_pinecone(embedding: list, top_k: int = 12) -> list:
57
+ """Query Pinecone for similar products."""
58
+ if not PINECONE_API_KEY or not PINECONE_HOST:
59
+ return []
60
+
61
+ import requests
62
+
63
+ resp = requests.post(
64
+ f"https://{PINECONE_HOST}/query",
65
+ headers={
66
+ "Api-Key": PINECONE_API_KEY,
67
+ "Content-Type": "application/json"
68
+ },
69
+ json={
70
+ "vector": embedding,
71
+ "topK": top_k,
72
+ "includeMetadata": True
73
+ },
74
+ timeout=15
75
+ )
76
+
77
+ if resp.status_code != 200:
78
+ return []
79
+
80
+ matches = resp.json().get('matches', [])
81
+ return [
82
+ {
83
+ 'handle': m.get('metadata', {}).get('handle', m.get('id')),
84
+ 'title': m.get('metadata', {}).get('title', ''),
85
+ 'score': m.get('score', 0),
86
+ 'image_url': m.get('metadata', {}).get('image_url', '')
87
+ }
88
+ for m in matches
89
+ ]
90
+
91
+
92
+ def search(image: Image.Image) -> dict:
93
+ """
94
+ Main search function.
95
+ Returns embedding and similar products.
96
+ """
97
+ if image is None:
98
+ return {"error": "No image provided"}
99
+
100
+ # Get embedding
101
+ embedding = get_embedding(image)
102
+
103
+ # Query Pinecone
104
+ products = query_pinecone(embedding)
105
+
106
+ return {
107
+ "embedding": embedding,
108
+ "products": products
109
+ }
110
+
111
+
112
+ def search_simple(image: Image.Image) -> str:
113
+ """Simple search returning product handles."""
114
+ if image is None:
115
+ return "No image"
116
+
117
+ embedding = get_embedding(image)
118
+ products = query_pinecone(embedding)
119
+
120
+ if not products:
121
+ return "No similar products found"
122
+
123
+ return "\n".join([
124
+ f"{i+1}. {p['title']} ({p['handle']}) - {p['score']:.2f}"
125
+ for i, p in enumerate(products)
126
+ ])
127
+
128
+
129
+ # Gradio Interface
130
+ with gr.Blocks(title="Visual Search API") as demo:
131
+ gr.Markdown("# Visual Product Search")
132
+ gr.Markdown("Upload an image to find similar products.")
133
+
134
+ with gr.Row():
135
+ with gr.Column():
136
+ image_input = gr.Image(type="pil", label="Upload Image")
137
+ search_btn = gr.Button("Search", variant="primary")
138
+
139
+ with gr.Column():
140
+ output = gr.Textbox(label="Results", lines=15)
141
+
142
+ search_btn.click(
143
+ fn=search_simple,
144
+ inputs=[image_input],
145
+ outputs=[output]
146
+ )
147
+
148
+ gr.Markdown("---")
149
+ gr.Markdown("### API Endpoint")
150
+ gr.Markdown("""
151
+ Use the `/api/predict` endpoint for programmatic access:
152
+
153
+ ```python
154
+ from gradio_client import Client
155
+
156
+ client = Client("YOUR_SPACE_URL")
157
+ result = client.predict(image_path, api_name="/predict")
158
+ ```
159
+ """)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=4.45.0,<4.48.0
3
+ pillow
4
+ numpy
5
+ requests
6
+ einops
7
+ timm
8
+ gradio