FayssalJ Claude Opus 4.6 (1M context) commited on
Commit
4f9bac9
·
1 Parent(s): fcd831f

feat: Add text embedding endpoint for semantic product search

Browse files

Add text_search Gradio endpoint alongside existing image search.
Uses model.encode_text() from Jina CLIP v2 for 512-dim text embeddings.
Backward compatible: image endpoint keeps api_name='predict'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +78 -18
app.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Visual Search API - HuggingFace Space
3
  Returns embedding vector for external Pinecone queries
 
4
  """
5
 
6
  import os
@@ -29,7 +30,7 @@ def load_model():
29
  return model
30
 
31
 
32
- def get_embedding(image: Image.Image) -> list:
33
  """Generate 512-dim embedding for an image."""
34
  m = load_model()
35
 
@@ -44,22 +45,35 @@ def get_embedding(image: Image.Image) -> list:
44
  return emb.tolist()
45
 
46
 
47
- def search(image):
48
- """Return embedding vector as JSON."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  if image is None:
50
  return json.dumps({"error": "No image provided"})
51
 
52
  try:
53
- print("Generating embedding...")
54
- embedding = get_embedding(image)
55
- print(f"Embedding generated: {len(embedding)} dimensions")
56
 
57
- # Return embedding as JSON
58
- result = {
59
  "embedding": embedding,
60
  "dimensions": len(embedding)
61
- }
62
- return json.dumps(result, indent=2)
63
 
64
  except Exception as e:
65
  import traceback
@@ -67,14 +81,60 @@ def search(image):
67
  return json.dumps({"error": str(e)})
68
 
69
 
70
- # Gradio interface - returns embedding as JSON
71
- demo = gr.Interface(
72
- fn=search,
73
- inputs=gr.Image(type="pil", label="Upload Image"),
74
- outputs=gr.Textbox(label="Embedding Vector (JSON)", lines=15),
75
- title="Visual Search - Embedding Generator",
76
- description="Upload an image to get its 512-dimensional CLIP embedding as JSON."
77
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  if __name__ == "__main__":
80
  demo.queue().launch()
 
1
  """
2
  Visual Search API - HuggingFace Space
3
  Returns embedding vector for external Pinecone queries
4
+ Supports both image and text inputs (Jina CLIP v2 multimodal)
5
  """
6
 
7
  import os
 
30
  return model
31
 
32
 
33
+ def get_image_embedding(image: Image.Image) -> list:
34
  """Generate 512-dim embedding for an image."""
35
  m = load_model()
36
 
 
45
  return emb.tolist()
46
 
47
 
48
+ def get_text_embedding(text: str) -> list:
49
+ """Generate 512-dim embedding for a text query."""
50
+ m = load_model()
51
+
52
+ with torch.no_grad():
53
+ emb = m.encode_text([text])
54
+ if hasattr(emb, 'cpu'):
55
+ emb = emb.cpu().numpy()
56
+ emb = emb.flatten()
57
+ emb = emb / np.linalg.norm(emb)
58
+ if len(emb) > 512:
59
+ emb = emb[:512]
60
+ return emb.tolist()
61
+
62
+
63
+ def image_search(image):
64
+ """Return image embedding vector as JSON."""
65
  if image is None:
66
  return json.dumps({"error": "No image provided"})
67
 
68
  try:
69
+ print("Generating image embedding...")
70
+ embedding = get_image_embedding(image)
71
+ print(f"Image embedding generated: {len(embedding)} dimensions")
72
 
73
+ return json.dumps({
 
74
  "embedding": embedding,
75
  "dimensions": len(embedding)
76
+ }, indent=2)
 
77
 
78
  except Exception as e:
79
  import traceback
 
81
  return json.dumps({"error": str(e)})
82
 
83
 
84
+ def text_search(text):
85
+ """Return text embedding vector as JSON."""
86
+ if not text or not text.strip():
87
+ return json.dumps({"error": "No text provided"})
88
+
89
+ try:
90
+ text = text.strip()[:200]
91
+ print(f"Generating text embedding for: {text}")
92
+ embedding = get_text_embedding(text)
93
+ print(f"Text embedding generated: {len(embedding)} dimensions")
94
+
95
+ return json.dumps({
96
+ "embedding": embedding,
97
+ "dimensions": len(embedding)
98
+ }, indent=2)
99
+
100
+ except Exception as e:
101
+ import traceback
102
+ traceback.print_exc()
103
+ return json.dumps({"error": str(e)})
104
+
105
+
106
+ # Gradio Blocks with explicit api_name for stable endpoints
107
+ # Image: /call/predict (backward compatible with existing image-search.py)
108
+ # Text: /call/text_search (new endpoint for text-search.py)
109
+ with gr.Blocks(title="Visual Search - Embedding Generator") as demo:
110
+ gr.Markdown("# Visual Search - Embedding Generator")
111
+ gr.Markdown("Upload an image or enter text to get a 512-dimensional CLIP embedding.")
112
+
113
+ with gr.Tab("Image Search"):
114
+ image_input = gr.Image(type="pil", label="Upload Image")
115
+ image_output = gr.Textbox(label="Embedding Vector (JSON)", lines=15)
116
+ image_btn = gr.Button("Generate Embedding")
117
+ image_btn.click(
118
+ image_search,
119
+ inputs=image_input,
120
+ outputs=image_output,
121
+ api_name="predict"
122
+ )
123
+
124
+ with gr.Tab("Text Search"):
125
+ text_input = gr.Textbox(
126
+ label="Search Query",
127
+ placeholder="e.g. boys underwear",
128
+ lines=1
129
+ )
130
+ text_output = gr.Textbox(label="Embedding Vector (JSON)", lines=15)
131
+ text_btn = gr.Button("Generate Embedding")
132
+ text_btn.click(
133
+ text_search,
134
+ inputs=text_input,
135
+ outputs=text_output,
136
+ api_name="text_search"
137
+ )
138
 
139
  if __name__ == "__main__":
140
  demo.queue().launch()