Skip to content
Back to all work
AI Systems2026

Style DNA

A working demonstration of visual fashion discovery — per-garment detection, an interpretable vector space, and a feed that says why each result surfaced.

Role
Architect & sole engineer
Next.js 16React 19TypeScriptPrismaSQLite

The application

Screens, captured from the running app

Booted against its seeded database and walked route by route — these are the real pages, not mockups.

Upload screen showing the image hash used as a cache key, per-garment detections with confidence and bounding boxes, and the extracted palette.
The image is hashed first, so the same photo is analysed once. Each garment is located separately — a three-piece look produces three queries rather than one blurred average.
Style DNA profiles for four looks, each showing eight named vector dimensions with values.
Eight interpretable dimensions instead of an opaque 512. Retrieval is only as explainable as the space it searches.
Retrieved feed of twelve garments, each with its cosine similarity, the reasons it matched, and a note where the brand cap moved its rank.
Every result carries why it surfaced and its real cosine score — and says so when the brand cap moved it up the list.
Pipeline stage timings with detection dominating, beside a before-and-after comparison of the ranking either side of the brand-diversity pass.
Detection dominates the budget, which is why the pipeline is async and polled. Beside it: the ranking before and after the per-brand cap.
Indexed catalogue of garments rendered from their colour attributes, with out-of-stock items marked.
Garments are drawn from their own colour attributes — there is no product photography, because inventing some would imply a licensing relationship that does not exist.

A demo, built to show the retrieval architecture behind a confidential fashion-discovery product without exposing it. The brands, garments and prices are invented. The retrieval maths is real and runs on the data you can see.

Why it exists

The production version pairs a Flutter app with a FastAPI vision service doing DETR/SegFormer detection and a Milvus index, and can't be shown. The parts worth showing aren't the models — plenty of people can call a detector — but four decisions about what happens around them.

160 indexed garments across 8 brands, 4 analysed looks, 48 retrieved results.

1. Detect per garment, not per photo

A look is three or four pieces. Embedding the whole photograph produces the average of a coat, a trouser and a boot, which resembles none of them.

So each garment is located separately and queried separately. The upload screen shows the detections with their confidence and boxes, because the number of things found is the first thing that tells you whether the rest of the pipeline had a chance.

2. Hash the image before anything else

The SHA-256 of the image bytes is the cache key. The same photo uploaded twice is analysed once. Detection is the expensive stage — around 70% of the end-to-end budget — so this is the difference between a product that costs pennies per user and one that doesn't.

3. An interpretable vector space

The demo embeds into eight named dimensions — palette warmth, lightness, saturation, silhouette structure, volume, fabric weight, occasion register, internal contrast — rather than an opaque 512.

That's a deliberate demo choice, and it earns two things. The app runs with no GPU and no API key. And the ranking becomes legible: you can look at a result and see which dimension pulled it in.

One detail worth naming, because it's the sort of thing that quietly ruins a demo: an all-positive vector space pins every cosine between 0.96 and 1.0. Technically correct, and useless — nothing in the ranking is distinguishable. Centring each dimension on zero before normalising spreads the same catalogue across 0.709 to 0.996, and the ordering suddenly means something.

The production shape swaps the encoder and the index and leaves the ranking below untouched. That's the point of putting a named space here rather than an opaque one.

4. Diversify, or the feed looks broken

Pure nearest-neighbour over a fashion catalogue returns whatever brand happens to sit closest in the embedding space — eight results from one label. That reads to a user as a broken feed, not a good match.

A greedy per-brand cap (two) re-ranks the results, backfilling from the overflow if the cap leaves the feed thin, because a strict cap is a preference rather than a correctness rule. In this seed it moves 19 of 48 results, and each moved item says so: "moved from #15 by the brand cap".

The pipeline screen shows the ranking either side of the pass, which is the only honest way to claim a re-ranker is doing anything.

Every result explains itself

Shared vibes, how close the palettes sit, whether the silhouette matches what was detected, and the raw cosine. A recommendation a user can't interrogate is one they stop trusting after the first bad row.

A note on the images

There is no product photography here. Each garment is rendered as a gradient built from its own colour attributes — which is also, conveniently, what the retrieval actually sees. Inventing catalogue shots would imply a licensing relationship that doesn't exist.

Stack

Next.js 16 App Router with React Server Components, Prisma over SQLite, five routes, deterministic seed. Cosine similarity, the diversity pass and the explanations are computed at seed time over the real catalogue — nothing on screen is hardcoded.

You have a process thatshould be a system.

Tell me what arrives, who has to act on it, and where it currently falls over. That conversation is usually enough to scope the build.