aniketkumar1106 commited on
Commit
d80107f
·
verified ·
1 Parent(s): 8b5933d

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +26 -0
  2. orbiitt_engine.py +123 -0
  3. orbit_analytics.db +0 -0
  4. requirements.txt +10 -0
  5. server.py +32 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Base Image
2
+ FROM python:3.11-slim
3
+
4
+ # 2. Install Git LFS to handle the 10GB download
5
+ RUN apt-get update && apt-get install -y git git-lfs && git lfs install
6
+
7
+ # 3. Setup Hugging Face User
8
+ RUN useradd -m -u 1000 user
9
+ USER user
10
+ ENV PATH="/home/user/.local/bin:$PATH"
11
+ WORKDIR /app
12
+
13
+ # 4. Install Dependencies
14
+ COPY --chown=user requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # 5. DOWNLOAD YOUR DATA (The 10GB Part)
18
+ # We use the HF_TOKEN secret to clone your private dataset into the /app folder.
19
+ RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=true \
20
+ git clone https://user:$(cat /run/secrets/hf_token)@huggingface.co/datasets/aniketkumar1106/orbit-data .
21
+
22
+ # 6. EXPOSE PORT & RUN
23
+ # Hugging Face Spaces mandates port 7860.
24
+ # We override the port here so you don't have to change your server.py.
25
+ EXPOSE 7860
26
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
orbiitt_engine.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn.functional as F
4
+ from PIL import Image
5
+ from transformers import AutoModel, AutoProcessor
6
+ import chromadb
7
+ from tqdm import tqdm
8
+
9
+ class OrbiittEngine:
10
+ def __init__(self, db_path="./orbiitt_db"):
11
+ self.image_folder = "Productimages"
12
+ self.db_path = db_path
13
+
14
+ # 1. Device detection (Native Apple Silicon Support)
15
+ self.device = "mps" if torch.backends.mps.is_available() else "cpu"
16
+
17
+ # 2. Load SigLIP 2 (The Modern Champ)
18
+ print(f"🧠 Loading SigLIP 2 (google/siglip2-base-patch16-256) on {self.device}...")
19
+ self.model_name = "google/siglip2-base-patch16-256"
20
+ self.model = AutoModel.from_pretrained(self.model_name).to(self.device).eval()
21
+ self.processor = AutoProcessor.from_pretrained(self.model_name)
22
+
23
+ # 3. Get Expected Dimension (768 for Base)
24
+ self.expected_dim = self.model.config.vision_config.hidden_size
25
+
26
+ # 4. Connect to Database with Safety Logic
27
+ self.client = chromadb.PersistentClient(path=self.db_path)
28
+ self._check_db_compatibility()
29
+
30
+ # CRITICAL: Set space to 'cosine' for AI search
31
+ self.collection = self.client.get_or_create_collection(
32
+ name="product_catalog",
33
+ metadata={"hnsw:space": "cosine"}
34
+ )
35
+
36
+ def _check_db_compatibility(self):
37
+ """Ensures the stored vectors match SigLIP 2's 768 dimensions."""
38
+ try:
39
+ col = self.client.get_collection(name="product_catalog")
40
+ sample = col.peek(limit=1)
41
+ if sample and sample['embeddings']:
42
+ existing_dim = len(sample['embeddings'][0])
43
+ if existing_dim != self.expected_dim:
44
+ print(f"⚠️ Dimension Mismatch: DB is {existing_dim}, Model is {self.expected_dim}")
45
+ if input("Wipe DB and restart? (y/n): ").lower() == 'y':
46
+ self.client.delete_collection(name="product_catalog")
47
+ else:
48
+ exit()
49
+ except: pass
50
+
51
+ def get_image_embedding(self, image_path):
52
+ """Processes image and returns a normalized 768D vector."""
53
+ image = Image.open(image_path).convert("RGB")
54
+ inputs = self.processor(images=image, return_tensors="pt").to(self.device)
55
+ with torch.no_grad():
56
+ features = self.model.get_image_features(**inputs)
57
+ # Normalize to unit length (Unit Sphere)
58
+ features = F.normalize(features, p=2, dim=-1)
59
+ return features.squeeze().cpu().numpy().tolist()
60
+
61
+ def get_text_embedding(self, text):
62
+ """Processes text and returns a normalized 768D vector."""
63
+ # Use the SigLIP 2 standard prompt template
64
+ prompt = f"this is a photo of {text}"
65
+ inputs = self.processor(text=[prompt], padding="max_length", return_tensors="pt").to(self.device)
66
+ with torch.no_grad():
67
+ features = self.model.get_text_features(**inputs)
68
+ features = F.normalize(features, p=2, dim=-1)
69
+ return features.squeeze().cpu().numpy().tolist()
70
+
71
+ def index_images(self):
72
+ """Scans the Productimages folder and indexes them."""
73
+ if not os.path.exists(self.image_folder):
74
+ print(f"❌ Error: {self.image_folder} not found."); return
75
+
76
+ files = [f for f in os.listdir(self.image_folder) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp'))]
77
+ print(f"🏗️ Indexing {len(files)} products...")
78
+
79
+ for fname in tqdm(files, desc="SigLIP 2 Processing"):
80
+ path = os.path.join(self.image_folder, fname)
81
+ if len(self.collection.get(ids=[fname])['ids']) > 0: continue
82
+ try:
83
+ emb = self.get_image_embedding(path)
84
+ self.collection.add(ids=[fname], embeddings=[emb], metadatas=[{"path": path}])
85
+ except Exception as e:
86
+ tqdm.write(f"⚠️ Skipped {fname}: {e}")
87
+
88
+ def search(self, text_query=None, image_file=None, text_weight=0.5):
89
+ """Hybrid search blending visual and text embeddings."""
90
+ img_vec = None
91
+ txt_vec = None
92
+
93
+ if image_file:
94
+ img_vec = torch.tensor(self.get_image_embedding(image_file))
95
+ if text_query:
96
+ txt_vec = torch.tensor(self.get_text_embedding(text_query))
97
+
98
+ # BLENDING LOGIC
99
+ if img_vec is not None and txt_vec is not None:
100
+ # Combined and then re-normalized to maintain 1.0 length
101
+ combined = (img_vec * (1.0 - text_weight)) + (txt_vec * text_weight)
102
+ query_emb = F.normalize(combined, p=2, dim=0).tolist()
103
+ elif img_vec is not None:
104
+ query_emb = img_vec.tolist()
105
+ elif txt_vec is not None:
106
+ query_emb = txt_vec.tolist()
107
+ else:
108
+ return []
109
+
110
+ results = self.collection.query(query_embeddings=[query_emb], n_results=10)
111
+
112
+ output = []
113
+ for i in range(len(results['ids'][0])):
114
+ fname = results['ids'][0][i]
115
+ # distance for 'cosine' is 1 - similarity.
116
+ # 0 distance = perfect match.
117
+ score = round((1.0 - results['distances'][0][i]) * 100)
118
+ output.append({
119
+ "id": fname,
120
+ "url": f"http://localhost:8000/Productimages/{fname}",
121
+ "score": score
122
+ })
123
+ return output
orbit_analytics.db ADDED
Binary file (12.3 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ chromadb
5
+ sentence-transformers
6
+ torch
7
+ pillow
8
+ transformers
9
+ numpy
10
+ huggingface_hub
server.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Form
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from orbiitt_engine import OrbiittEngine
5
+ import shutil
6
+ import os
7
+
8
+ app = FastAPI()
9
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
10
+
11
+ # Mount your specific folder
12
+ app.mount("/Productimages", StaticFiles(directory="Productimages"), name="Productimages")
13
+
14
+ engine = OrbiittEngine()
15
+
16
+ @app.post("/search")
17
+ async def search_endpoint(text: str = Form(None), weight: float = Form(0.5), file: UploadFile = File(None)):
18
+ temp_path = None
19
+ if file:
20
+ temp_path = f"temp_{file.filename}"
21
+ with open(temp_path, "wb") as buffer:
22
+ shutil.copyfileobj(file.file, buffer)
23
+
24
+ results = engine.search(text_query=text, image_file=temp_path, text_weight=weight)
25
+
26
+ if temp_path and os.path.exists(temp_path):
27
+ os.remove(temp_path)
28
+ return {"results": results}
29
+
30
+ if __name__ == "__main__":
31
+ import uvicorn
32
+ uvicorn.run(app, host="0.0.0.0", port=8000)