Spaces:
Running
Running
File size: 4,541 Bytes
89b7f5b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | """
STEP 3 — Generate Embeddings
Run after generate_descriptions.py.
For each product WITHOUT embeddings:
- Encodes the ACTUAL IMAGE with CLIP (visual_embedding)
- Encodes the REAL visual_description + metadata with CLIP (text_embedding)
- Combines 60% visual + 40% text (combined_embedding)
- Inserts into product_embeddings table
Idempotent: skips products that already have embeddings.
Usage:
cd backend
.venv/Scripts/python scripts/generate_embeddings.py
"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from supabase import create_client
from PIL import Image
from io import BytesIO
from dotenv import load_dotenv
import os, requests, numpy as np
load_dotenv()
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
IMG_WEIGHT = float(os.getenv("IMAGE_WEIGHT", "0.6"))
TXT_WEIGHT = 1.0 - IMG_WEIGHT
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
def download_image(image_url: str) -> Image.Image:
"""Download image from URL and return as PIL Image."""
response = requests.get(image_url, timeout=30)
response.raise_for_status()
return Image.open(BytesIO(response.content)).convert("RGB")
def combine_embeddings(img_emb: np.ndarray, txt_emb: np.ndarray) -> np.ndarray:
"""Combine visual + text embeddings with 60/40 weighting, L2-normalized."""
combined = IMG_WEIGHT * img_emb + TXT_WEIGHT * txt_emb
norm = np.linalg.norm(combined)
if norm > 1e-8:
combined = combined / norm
return combined
def main():
print("=" * 60)
print(f" STEP 3 — Generate CLIP Embeddings ({IMG_WEIGHT:.0%} visual / {TXT_WEIGHT:.0%} text)")
print("=" * 60)
# Load CLIP service
print("\n🤖 Loading CLIP model (ViT-B-32)...")
from app.services.clip_service import get_clip_service
clip_svc = get_clip_service()
clip_svc._ensure_loaded()
print(" ✅ CLIP loaded")
# Get products that have descriptions but no embeddings yet
all_products = supabase.table("products").select(
"id, title, image_url, visual_description, color, category"
).not_.is_("description", "null").execute().data
existing_ids = {
row["product_id"]
for row in supabase.table("product_embeddings").select("product_id").execute().data
}
products = [p for p in all_products if p["id"] not in existing_ids]
print(f"\nGenerating embeddings for {len(products)} products...\n")
if not products:
print("All products already have embeddings. Nothing to do.")
return
success = 0
failed = 0
for i, p in enumerate(products):
try:
print(f"[{i+1}/{len(products)}] {p['title']}")
# Download image
pil_image = download_image(p["image_url"])
# Visual embedding: encode the ACTUAL IMAGE
visual_emb = clip_svc.encode_image(pil_image)
print(f" Visual embedding: {visual_emb.shape} (from image)")
# Text embedding: encode the REAL Gemini description + metadata
# Use visual_description (truth from Gemini) not title (can be corrupted later)
visual_text = p.get("visual_description") or p["title"]
search_text = f"{visual_text} {p['color']} {p['category']}"
text_emb = clip_svc.encode_text(search_text)
print(f" Text embedding: {text_emb.shape} (from Gemini description)")
# Combined embedding: 60% visual + 40% text
combined_emb = combine_embeddings(visual_emb, text_emb)
print(f" Combined: {combined_emb.shape} ({IMG_WEIGHT:.0%}/{TXT_WEIGHT:.0%})")
# Insert into product_embeddings
supabase.table("product_embeddings").insert({
"product_id": p["id"],
"visual_embedding": visual_emb.tolist(),
"text_embedding": text_emb.tolist(),
"combined_embedding": combined_emb.tolist(),
}).execute()
print(f" ✅ Embedding stored for product {p['id']}")
success += 1
except Exception as e:
print(f" ❌ ERROR: {e}")
failed += 1
continue
print(f"\n{'=' * 60}")
print(f" ✅ STEP 3 COMPLETE")
print(f" Success: {success} | Failed: {failed}")
print(f" Weights: {IMG_WEIGHT:.0%} visual / {TXT_WEIGHT:.0%} text")
print(f"{'=' * 60}")
print(" Next: run scripts/corrupt_products.py")
if __name__ == "__main__":
main()
|