← back to Handbag Auth Nextjs

python-matcher/scripts/embed_handbags.py

129 lines

"""
Batch script to embed handbag catalog images
Processes all handbags with embedding IS NULL
"""
import os
import sys
import io
import psycopg2
import requests
from PIL import Image
from datetime import datetime

# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from models.handbag_embedder import image_to_vec, embed_batch

DB_DSN = os.environ.get("DATABASE_URL", "postgresql://localhost/handbags")

def get_conn():
    """Get database connection"""
    return psycopg2.connect(DB_DSN)


def fetch_unembedded_handbags(conn, limit=500):
    """Fetch handbags that need embeddings"""
    with conn.cursor() as cur:
        cur.execute("""
            SELECT id, sku, image_url, brand, model_name
            FROM handbags
            WHERE embedding IS NULL
              AND is_active = true
              AND image_url IS NOT NULL
            ORDER BY id
            LIMIT %s;
        """, (limit,))
        return cur.fetchall()


def update_embedding(conn, row_id, vec):
    """Update handbag with embedding vector"""
    with conn.cursor() as cur:
        # pgvector expects a string like '[0.1, 0.2, ...]'
        vec_str = "[" + ",".join(str(x) for x in vec) + "]"
        cur.execute("""
            UPDATE handbags
            SET embedding = %s,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = %s;
        """, (vec_str, row_id))


def download_image(url, timeout=10):
    """Download and open image from URL"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (compatible; HandbagEmbedder/1.0)'
    }
    resp = requests.get(url, timeout=timeout, headers=headers)
    resp.raise_for_status()
    img = Image.open(io.BytesIO(resp.content)).convert("RGB")
    return img


def main():
    """Main embedding loop"""
    print("🚀 Starting handbag embedding process...")
    print(f"📊 Database: {DB_DSN}\n")

    conn = get_conn()
    conn.autocommit = False

    total_processed = 0
    total_errors = 0

    try:
        while True:
            # Fetch batch of unembedded handbags
            rows = fetch_unembedded_handbags(conn, limit=100)

            if not rows:
                print("\n✅ No more handbags to embed!")
                break

            print(f"\n📦 Processing batch of {len(rows)} handbags...")

            for row_id, sku, image_url, brand, model in rows:
                try:
                    # Download image
                    img = download_image(image_url)

                    # Generate embedding
                    vec = image_to_vec(img)

                    # Update database
                    update_embedding(conn, row_id, vec)

                    total_processed += 1
                    print(f"✓ [{total_processed}] {brand} {model} (SKU: {sku})")

                except Exception as e:
                    total_errors += 1
                    print(f"✗ Error on {sku}: {e}")
                    # Continue with next item

            # Commit batch
            conn.commit()
            print(f"💾 Committed batch ({total_processed} total, {total_errors} errors)")

        # Final stats
        print(f"\n" + "="*60)
        print(f"🎉 Embedding complete!")
        print(f"   ✅ Processed: {total_processed}")
        print(f"   ❌ Errors: {total_errors}")
        print(f"   📊 Success rate: {(total_processed/(total_processed+total_errors)*100):.1f}%")
        print("="*60)

    except KeyboardInterrupt:
        print("\n⚠️  Interrupted by user")
        conn.rollback()
    except Exception as e:
        print(f"\n❌ Fatal error: {e}")
        conn.rollback()
        raise
    finally:
        conn.close()


if __name__ == "__main__":
    main()