Skip to content

About

πŸ† Top 3 @ Clozyt Hackathon β€” StyleNova AI: Vision-Language fashion recommender built with OpenAI CLIP (ViT-B/32), adaptive collaborative filtering & real-time swipe feedback loop. Next.js 15 + FastAPI + PyTorch.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

StyleNova AI Banner

StyleNova AI

Vision-Language AI Β· Adaptive Recommendation Β· Full-Stack ML System

Python PyTorch CLIP FastAPI scikit-learn Next.js

A fashion recommender that fuses OpenAI CLIP vision-language embeddings with a per-user feedback loop β€” an image/brand/category query is embedded with CLIP, blended against a preference vector learned from each user's own likes and dislikes, and served through a FastAPI backend to a Next.js swipe interface (with a rule-based scorer as an offline fallback).


At a Glance

Dimension What was built
Computer Vision Zero-shot visual semantic matching via CLIP (ViT-B/32) cross-modal embeddings
ML System Design Priority-weighted CLIP query fusion (image > brand > category, falling back to quiz text) blended against a per-user preference vector
Online Learning Preference vector rebuilt from liked/disliked item embeddings after every swipe, with exponential time-decay on dislikes
Full-Stack Integration FastAPI ML backend ↔ Next.js 15 TypeScript frontend with Zod-validated contracts
Cold Start Strategy 6-question preference quiz (categories, colors, brands, styles, size, budget) bootstraps a query before first swipe

Why CLIP for Fashion?

Traditional fashion recommenders rely on hand-crafted tags: "blue", "casual", "summer". Tags are sparse, inconsistent, and cannot capture visual gestalt.

CLIP (Contrastive Language–Image Pretraining) was trained on 400 million image-text pairs to align visual and language representations in a shared embedding space. This lets us:

  • Query by concept, not keyword β€” "bohemian flowy dress" matches visually similar items even if none share those exact tags
  • Bridge the vocabulary gap β€” two products described differently but looking alike become neighbors in embedding space
  • Zero-shot generalization β€” new product categories need no retraining; the embedding space already understands them
Text Encoder (Transformer)        Image Encoder (ViT-B/32)
      β”‚                                    β”‚
      β–Ό                                    β–Ό
 512-dim text embedding  ◄── cosine ──►  512-dim image embedding
      β”‚                   similarity             β”‚
      └─────────── shared latent space β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

"A bohemian dress" and a "flowy summer dress" share nearly identical CLIP embeddings despite zero keyword overlap β€” this is the representational power that makes visual semantic search possible.


Core ML Architecture

CLIP, PyTorch, and FastAPI provide the pretrained embedding model, tensor runtime, and serving framework. The query-fusion weighting, the feedback-driven preference vector and its decay math, and the exclusion rules below are what Brijesh designed and implemented on top of them.

1. Priority-Weighted CLIP Query Fusion

AdaptiveFashionRecommender.get_priority_adaptive_recommendations (fashion-reco/adaptive_recommender.py) builds one query embedding by combining whichever inputs are present, each CLIP-encoded and weighted by priority, then normalizes the result before scoring the catalog:

IMAGE_WEIGHT = 0.6      # highest priority β€” an uploaded/reference image
BRAND_WEIGHT = 0.3      # medium priority β€” a brand name query
CATEGORY_WEIGHT = 0.1   # lower priority β€” a category filter
# quiz features are used as a fallback (weight 1.0) only when none of the above are present

combined_embedding = (
    IMAGE_WEIGHT * image_embedding
    + BRAND_WEIGHT * brand_embedding
    + CATEGORY_WEIGHT * category_embedding
) / total_weight
combined_embedding /= np.linalg.norm(combined_embedding)

base_similarities = catalog_embeddings @ combined_embedding

This is a single-user query-fusion scheme, not collaborative filtering β€” there is no cross-user behavioral signal in the live serving path. (A separate, not-currently-wired module, hybrid_recommender.py, does use scikit-learn's NearestNeighbors for item-to-item visual similarity search over ResNet50 embeddings β€” see Tech Stack.)

2. Feedback-Driven Preference Vector, with Temporal Decay

Every like/dislike updates a per-user preference vector, which is then blended against the base query at request time via a client-supplied adaptation_strength (0–1):

positive_boost = 0.3      # weight applied to the mean of liked-item embeddings
negative_penalty = -0.4   # base weight applied to disliked-item embeddings
decay_rate = 0.1          # exponential decay applied per day since the dislike
min_negative_weight = -0.1  # a dislike's influence never decays to zero

decay_factor = exp(-decay_rate * days_since_feedback)
weight = max(min_negative_weight, negative_penalty * decay_factor)

preference_vector = positive_boost * mean(liked_embeddings) - mean(disliked_embeddings * |weight|)
preference_vector /= np.linalg.norm(preference_vector)

adapted_similarities = (1 - adaptation_strength) * base_similarities + adaptation_strength * (catalog_embeddings @ preference_vector)

On the frontend, adaptationStrength is itself increased as a session progresses (Math.min(0.9, 0.5 + totalSwipes * 0.05) in SwipeDeck.tsx), so later recommendations lean more heavily on the user's own feedback history.

3. CLIP Embedding Engine

# fashion-reco/clip_recommender.py β€” CLIPFashionRecommender
class CLIPFashionRecommender:
    def __init__(self, model_name: str = "ViT-B/32"):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load(model_name, device=self.device)

    def encode_text(self, text: str) -> np.ndarray:
        tokens = clip.tokenize([text]).to(self.device)
        with torch.no_grad():
            features = self.model.encode_text(tokens)
        features = features / features.norm(dim=-1, keepdim=True)
        return features.cpu().numpy().flatten()

Key optimization decisions:

  • Catalog embeddings are computed once when the FastAPI service starts up (build_catalog_index) and held in memory as a NumPy matrix for the life of the process β€” inference cost is paid once per process start, not per request
  • CPU inference for development portability; GPU swap is a single device change
  • L2 normalization at encode time so all similarity queries are pure dot-product batch ops

4. Client-Side Smart Exclusion

Permanently excluding every passed item degrades recommendation diversity over time, so exclusion is scoped to items the user has actively rejected. This logic lives in the Next.js swipe UI (app/recommendations/components/SwipeDeck.tsx), which computes the exclusion list before asking the backend for more recommendations:

const recentlyPassedIds = passedProducts.slice(-3).map(p => p.id)   // recency window

const dislikeCount = passedProducts.reduce((count, product) => {
  count[product.id] = (count[product.id] || 0) + 1
  return count
}, {} as Record<string, number>)

const excludeItemIds = passedProducts
  .filter(p => dislikeCount[p.id] >= 2 || recentlyPassedIds.includes(p.id))
  .map(p => p.id)

The exclusion list is sent to the backend as exclude_items; separately, the backend applies its own decaying score penalty (Section 2) to specific disliked items, so an old single pass naturally re-enters the pool as its penalty decays.


System Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         StyleNova AI β€” System Overview                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Next.js 15  (TypeScript)       β”‚   FastAPI + Python  (ML Backend)           β”‚
β”‚                                  β”‚                                            β”‚
β”‚  β”Œβ”€β”€β”€ Style Quiz (6 steps) ───┐  β”‚  β”Œβ”€β”€β”€ /recommend/adaptive ───────────┐    β”‚
β”‚  β”‚ categories Β· colors        β”‚  β”‚  β”‚  1. Encode image/brand/category    β”‚    β”‚
β”‚  β”‚ brands Β· styles            │──┼─►│     (or quiz text) with CLIP       β”‚    β”‚
β”‚  β”‚ sizing Β· budget            β”‚  β”‚  β”‚  2. Priority-weighted query fusion β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  3. Blend with user preference     β”‚    β”‚
β”‚                                  β”‚  β”‚     vector (adaptation_strength)   β”‚    β”‚
β”‚  β”Œβ”€β”€β”€ Swipe Interface ────────┐  β”‚  β”‚  4. Filter client-supplied         β”‚    β”‚
β”‚  β”‚  Like ──► POST /feedback   │──┼─►│     exclude_items                  β”‚    β”‚
β”‚  β”‚  Pass ──► POST /feedback   β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  β”‚  Client-side smart         │◄─┼──  Ranked recommendations returned         β”‚
β”‚  β”‚  exclusion (recency +      β”‚  β”‚                                            β”‚
β”‚  β”‚  dislike-count rules)      β”‚  β”‚                                            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                                            β”‚
β”‚                                  β”‚  β”Œβ”€β”€β”€ Catalog + Embeddings ──────────┐    β”‚
β”‚  β”Œβ”€β”€β”€ Zustand Store ──────────┐  β”‚  β”‚  catalog.csv β†’ CLIP ViT-B/32      β”‚    β”‚
β”‚  β”‚  quiz answers              β”‚  β”‚  β”‚  512-dim vectors held in-memory   β”‚    β”‚
β”‚  β”‚  session feedback history  β”‚  β”‚  β”‚  (NumPy matrix), computed once    β”‚    β”‚
β”‚  β”‚  current recommendations   β”‚  β”‚  β”‚  at FastAPI process startup       β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚          Prisma ORM  Β·  SQLite               β”‚
               β”‚   Users Β· Quizzes Β· Feedback Β· Recommendation β”‚
               β”‚   (separate from the Python service's         β”‚
               β”‚    in-memory catalog/embeddings above)         β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Computer Vision Skills Demonstrated

Skill Where Applied
Vision Transformer (ViT) CLIP ViT-B/32 backbone β€” patch tokenization, self-attention over 32Γ—32 patches
Contrastive Learning CLIP's InfoNCE training objective: align image–text pairs, push apart negatives
Cross-modal Embeddings Text queries retrieve visually similar images via shared 512-dim latent space
Zero-shot Recognition New fashion categories handled without any retraining
Embedding Similarity Search L2-normalized cosine similarity as efficient inner-product retrieval
Feature Caching / Precomputation Offline embedding generation β†’ low-latency online retrieval pipeline
Semantic Gap Bridging Visual similarity completely decoupled from textual tag quality

Tech Stack

ML / Backend

Technology Role
OpenAI CLIP (ViT-B/32) Vision-language embedding, zero-shot visual retrieval
PyTorch 2.0+ CLIP inference, tensor operations, GPU/CPU abstraction
scikit-learn NearestNeighbors item-to-item similarity search over ResNet50 embeddings (hybrid_recommender.py / indexing/sklearn_index.py) β€” a separate exploratory module, not the CLIP path the live API serves
NumPy / Pandas Embedding arithmetic, catalog preprocessing
FastAPI Async REST endpoints with automatic OpenAPI documentation
Pydantic Runtime data validation, type-safe request/response models

Frontend / Infrastructure

Technology Role
Next.js 15 + TypeScript Full-stack React with App Router
Zustand Lightweight global state (quiz answers, session feedback)
Framer Motion Physics-based swipe card animations
Tailwind CSS + shadcn/ui Accessible, consistent component system
Prisma 6 + SQLite Type-safe ORM, schema migrations
Zod Runtime schema validation β€” frontend/backend contract enforcement

Database Schema

The Next.js app persists quiz and feedback data through Prisma to SQLite. The actual models (prisma/schema.prisma):

CREATE TABLE "User" (
    "id"        TEXT PRIMARY KEY,
    "email"     TEXT NOT NULL UNIQUE,
    "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE "Quiz" (
    "id"        TEXT PRIMARY KEY,
    "userId"    TEXT NOT NULL REFERENCES "User"("id"),
    "answers"   TEXT NOT NULL,   -- JSON-encoded QuizAnswer[]
    "completed" BOOLEAN DEFAULT false,
    "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE "Recommendation" (
    "id"        TEXT PRIMARY KEY,
    "quizId"    TEXT NOT NULL,
    "items"     TEXT NOT NULL,   -- JSON-encoded top-N catalog subset with scores
    "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE "Feedback" (
    "id"        TEXT PRIMARY KEY,
    "userId"    TEXT NOT NULL REFERENCES "User"("id"),
    "productId" TEXT NOT NULL,
    "action"    TEXT NOT NULL,   -- 'like' | 'pass'
    "score"     INTEGER,
    "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP
);

The product catalog itself is a static TypeScript array (lib/catalog.ts) on the frontend and a separate CSV (fashion-reco/catalog.csv) loaded into memory by the Python service β€” neither the catalog nor CLIP embeddings are stored in this SQLite database.


Getting Started

Prerequisites

  • Node.js 18+ Β· Python 3.10+ Β· npm

Frontend Setup

git clone <repo-url>
cd stylenova-ai

npm install

# Push the Prisma schema and seed a demo user
# (the product catalog itself ships as a static array in lib/catalog.ts,
#  not as Prisma-seeded data)
npm run db:push
npm run db:seed

# Start Next.js dev server
npm run dev
# β†’ http://localhost:3000

ML Backend Setup

cd fashion-reco

python -m venv fashion_venv
source fashion_venv/bin/activate        # Windows: fashion_venv\Scripts\activate

pip install -r requirements.txt

# Start FastAPI with hot reload β€” must be port 8004, since the frontend
# (lib/fashion-api.ts) calls http://localhost:8004
uvicorn adaptive_api:app --reload --port 8004
# β†’ http://localhost:8004/docs  (auto-generated OpenAPI UI)

Project Structure

stylenova-ai/
β”œβ”€β”€ fashion-reco/                   # β—„ ML backend (live path: adaptive_api.py)
β”‚   β”œβ”€β”€ adaptive_api.py             #   FastAPI entrypoint (serves on :8004)
β”‚   β”œβ”€β”€ adaptive_recommender.py     #   Priority query fusion + preference-vector updates
β”‚   β”œβ”€β”€ clip_recommender.py         #   CLIP ViT-B/32 embedding engine
β”‚   β”œβ”€β”€ hybrid_recommender.py       #   Separate ResNet50 + sklearn exploration (not wired to adaptive_api.py)
β”‚   β”œβ”€β”€ catalog.csv                 #   Fashion product catalog
β”‚   β”œβ”€β”€ requirements.txt
β”‚   └── indexing/
β”‚       └── sklearn_index.py        #   scikit-learn kNN index (used by hybrid_recommender.py)
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ quiz/                       #   6-step preference quiz
β”‚   β”œβ”€β”€ recommendations/            #   Swipe card interface + client-side smart exclusion
β”‚   └── api/
β”‚       β”œβ”€β”€ quiz/                   #   Quiz submission β†’ Prisma
β”‚       └── feedback/               #   Like/pass β†’ Prisma (separate from the Python backend's own feedback loop)
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ fashion-api.ts              #   Typed client for the Python CLIP backend
β”‚   β”œβ”€β”€ store.ts                    #   Zustand session store
β”‚   β”œβ”€β”€ scoring.ts                  #   Rule-based fallback scorer (used when the Python backend is unreachable)
β”‚   └── validators.ts               #   Zod schemas
└── prisma/
    β”œβ”€β”€ schema.prisma
    └── seed.ts                     #   Seeds a demo user (catalog is not seeded into this DB β€” see lib/catalog.ts)

Key Engineering Decisions

Decision Alternatives Considered Why This Choice
CLIP ViT-B/32 over text-only TF-IDF ResNet, EfficientNet, pure TF-IDF Cross-modal: text queries β†’ visual results without image uploads
Boost/decay preference vector over gradient descent SGD on a learned preference vector No labels needed; works from binary like/dislike signals only
Catalog embeddings computed once at process startup, held in memory On-the-fly CLIP inference at query time Avoids re-running CLIP inference on every request
Soft exclusion (recency + dislike-count rules) over hard blacklist Remove all passed items permanently Preserves catalog diversity; matches real browsing behavior

Challenges & Solutions

Challenge Solution
Inconsistent catalog data β€” sparse tags on many items Pydantic validation + CLIP bridges gaps text tags cannot fill
CLIP memory overhead Catalog embeddings computed once and held in memory as a NumPy matrix; inference cost amortized across the process lifetime
ML adaptation invisible to users Swipe UI re-fetches recommendations every 3rd swipe and surfaces an "AI adapted" badge β€” visible proof of learning
Cold start before any feedback 6-question quiz bootstraps a CLIP query before any swipe feedback exists
Type contract drift between TS frontend and Python backend Zod schemas mirror Pydantic models; validated at both boundaries
Recommendation staleness after many passes Soft exclusion with a recency window; a hard exclude only after 2+ explicit passes

Advanced Product Engineering Decisions

Decision Constraint Result
Optimistic swipe updates Swipe feedback needs to feel instant even though the backend call takes real network + compute time The Zustand store advances to the next card immediately (swipeProduct) while the like/dislike is submitted to the FastAPI backend and to the Next.js /api/feedback route in the background; if the backend call fails, the local swipe is not rolled back β€” the UI stays responsive at the cost of that one interaction not reaching the adaptive backend
Velocity-based swipe gestures Fashion discovery should feel physical, not like button clicking Framer Motion's drag handler commits a swipe when either the drag distance exceeds 50px or the release velocity exceeds 300px/s β€” so a fast flick commits even under the distance threshold

What's Next

  • Fashion-specific fine-tuned CLIP β€” domain adaptation on FashionGen / DeepFashion datasets
  • Image upload query β€” encode user photo via CLIP image encoder, retrieve visually similar items
  • Seasonal & contextual signals β€” weather API for occasion-aware recommendations
  • Outfit completion β€” multi-item combinatorial recommendation with pairwise compatibility scoring
  • GPU model serving β€” TorchServe / Triton Inference Server for production throughput
  • A/B testing framework β€” compare recommendation strategies across user cohorts
  • Online evaluation β€” real-time Precision@K and NDCG tracking per session

Acknowledgments

  • OpenAI CLIP β€” Learning Transferable Visual Models From Natural Language Supervision (Radford et al., 2021)
  • FastAPI Β· Next.js Β· shadcn/ui Β· Prisma
  • The fashion recommendation research community

"The best recommendation system is one that users don't notice β€” it just works."

About

πŸ† Top 3 @ Clozyt Hackathon β€” StyleNova AI: Vision-Language fashion recommender built with OpenAI CLIP (ViT-B/32), adaptive collaborative filtering & real-time swipe feedback loop. Next.js 15 + FastAPI + PyTorch.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages