Spaces:
Running on Zero
Running on Zero
| """ | |
| Visual Search API - HuggingFace Space | |
| """ | |
| import os | |
| import gradio as gr | |
| import torch | |
| import numpy as np | |
| from PIL import Image | |
| # Pinecone config from HF Secrets | |
| PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY') | |
| PINECONE_HOST = os.environ.get('PINECONE_HOST') | |
| # Model (loaded on first use) | |
| model = None | |
| def load_model(): | |
| """Load Jina CLIP v2 model.""" | |
| global model | |
| if model is None: | |
| print("Loading Jina CLIP v2...") | |
| from transformers import AutoModel | |
| model = AutoModel.from_pretrained( | |
| "jinaai/jina-clip-v2", | |
| trust_remote_code=True | |
| ) | |
| model.eval() | |
| print("Model loaded!") | |
| return model | |
| def get_embedding(image: Image.Image) -> list: | |
| """Generate 512-dim embedding for an image.""" | |
| m = load_model() | |
| with torch.no_grad(): | |
| emb = m.encode_image(image) | |
| if hasattr(emb, 'cpu'): | |
| emb = emb.cpu().numpy() | |
| emb = emb.flatten() | |
| emb = emb / np.linalg.norm(emb) | |
| if len(emb) > 512: | |
| emb = emb[:512] | |
| return emb.tolist() | |
| def query_pinecone(embedding: list, top_k: int = 12) -> list: | |
| """Query Pinecone for similar products.""" | |
| if not PINECONE_API_KEY or not PINECONE_HOST: | |
| return [] | |
| import requests | |
| resp = requests.post( | |
| f"https://{PINECONE_HOST}/query", | |
| headers={ | |
| "Api-Key": PINECONE_API_KEY, | |
| "Content-Type": "application/json" | |
| }, | |
| json={ | |
| "vector": embedding, | |
| "topK": top_k, | |
| "includeMetadata": True | |
| }, | |
| timeout=15 | |
| ) | |
| if resp.status_code != 200: | |
| return [] | |
| matches = resp.json().get('matches', []) | |
| return [ | |
| { | |
| 'handle': m.get('metadata', {}).get('handle', m.get('id')), | |
| 'title': m.get('metadata', {}).get('title', ''), | |
| 'score': m.get('score', 0), | |
| } | |
| for m in matches | |
| ] | |
| def search(image): | |
| """Main search function.""" | |
| if image is None: | |
| return "No image provided" | |
| try: | |
| embedding = get_embedding(image) | |
| products = query_pinecone(embedding) | |
| if not products: | |
| return "No similar products found" | |
| result = "\n".join([ | |
| f"{i+1}. {p['title']} ({p['handle']}) - score: {p['score']:.3f}" | |
| for i, p in enumerate(products) | |
| ]) | |
| return result | |
| except Exception as e: | |
| return f"Error: {str(e)}" | |
| # Simple Gradio interface | |
| demo = gr.Interface( | |
| fn=search, | |
| inputs=gr.Image(type="pil", label="Upload Image"), | |
| outputs=gr.Textbox(label="Similar Products", lines=15), | |
| title="Visual Product Search", | |
| description="Upload an image to find similar products." | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |