Inspiration

Standing in front of a mirror ten minutes before something that matters, and not knowing whether to change anything.

Every appearance tool we looked at answers the same question — what should I wear? — and answers it with more: more outfits, more products, more combinations. None of them answered the question we actually had, which was narrower and harder:

Will this look work here?

That distinction became the whole product. Checking your look is observation. Deciding what is worth changing is a judgement, and it depends entirely on the moment you're walking into. The same outfit is right for a dinner and wrong for a wedding.

What it does

MIRROR OPS is a contextual appearance decision engine.

  1. The moment — occasion, goal, time available. Three taps.
  2. Your look — one photo, which pieces you're wearing, how dressed up it is.
  3. Contextual fit — the engine judges your look against that occasion, not against an abstract standard. Three verdicts:
    • FIT — You're good to go.
    • ALMOST THERE — Your outfit fits, but the shoes reduce the formality.
    • MISMATCH — This doesn't fit the moment.
  4. One change — the single highest-value intervention. Change a piece, add one that's missing, or remove one too many. Never a list.
  5. The proof — YouCam Apparel VTO shows the change on you, before you make it.
  6. Act — everything else stays exactly as it was.

The same casual look scores 69 for travel and 47 for a wedding — two different verdicts on an unchanged photo. That difference is the product.

MIRROR OPS can also decide that nothing is worth changing. NO CHANGE is a real outcome and consumes no try-on credit. A decision engine that cannot decide to do nothing isn't a decision engine — it's a recommendation generator.

How we built it

Next.js 14 + FastAPI + PostgreSQL, joined by a shared TypeScript contract that mirrors the Pydantic schemas.

The decision engine is a pure domain module: it imports no HTTP client, no ORM, no LLM. It is deterministic and testable without network, database or YouCam. Seven weighted features produce a 0–100 score for each candidate intervention — goal alignment (0.25), context fit (0.20), visual impact (0.20), current gap, time fit, data confidence, try-on feasibility. Every table is configurable, because they encode product heuristics meant to be calibrated, not scientific truths.

YouCam sits inside the decision loop, not beside it.

Skin AI informs the decision/s2s/v2.0/{file,task}/skin-analysis. Its influence is capped at 0.08 and only applies past explicit materiality thresholds. A test verifies that heavily marked skin does not change the clothing decision.

Apparel VTO proves the decision/s2s/v2.0/{file,task}/cloth. Candidates are scored; only the winner is rendered. One journey costs one skin analysis and one try-on, enforced by input-aware idempotency keys and by a test that counts provider calls.

Users can also photograph a garment they're actually considering and see it on themselves — which is the real moment of hesitation. Not a catalogue: a specific jacket.

Challenges we ran into

Ten occasions, one verdict. Our first working build returned ALMOST_THERE with a score between 60 and 65 for all ten occasions. A judge testing "wedding" then "travel" would have seen the same screen twice and concluded, rightly, that the occasion did nothing. The thesis was true in the engine and invisible on screen — because the interface only declared which pieces were present, so every look got the same neutral prior. One extra question, three answers, fixed it.

Skin AI needs a close-up; we photograph an outfit. Skin AI rejects images where the face fills less than 60% of the width. Asking for a second photo would have added a screen to a 90-second flow. So the server detects and crops the face from the same shot and sends only that; the original goes untouched to the try-on.

Health scores versus severity. YouCam returns health scores — higher is better. Our engine reasons in severity. Getting that backwards would have made flawless skin read as a problem, and pushed decisions the wrong way.

A screen that contradicted itself. One occasion produced a FIT verdict and "Change the jacket" — "you're ready", then "change the jacket". Two lines that disagree destroy credibility faster than any crash. The verdict and the action are now reconciled by construction, and a test walks every occasion and dressiness level to prove no screen can promise readiness while demanding a change.

Phone photos processed sideways. Browsers apply EXIF orientation; PIL does not. Users saw their photo upright while the server analysed it rotated 90° — which made faces undetectable and poses unreadable. It was invisible in tests until we fabricated EXIF data.

An error code that meant something else. Skin AI kept answering error_src_face_too_small. We widened the crop, twice. Then we measured across three real photographs: the rejected one carried a 906 px face, the accepted one 289 px. Size was never the issue — the rejected photo had sunglasses, which defeat frontal face detection. We had been optimising the wrong variable for two days.

A dependency that imported and did not work. opencv-python-headless with no upper bound installed OpenCV 5.0, which removed Haar cascades outright. It imports cleanly and fails on every call, so nothing at startup reported it and face detection was silently dead.

Configuration that depended on where you stood. Our diagnostic probe reported "live mode" and then failed on a missing API key: the settings file was resolved relative to the working directory, so tooling launched from the repository root read a different file than the API did. Half our diagnostics had been worthless.

Going back changed nothing. After adding back-navigation, correcting the occasion or the outfit left the result identical. Two independent causes: a duplicate moment row tie-broken by a random UUID, and an idempotency key that depended only on the session. Both were optimisations that had stopped being keyed on everything the result depends on.

Accomplishments that we're proud of

The constraint held. Through every iteration, the product never became a list. When we added "Try another", we made it swap the piece and never the decision — and wrote two tests that fail if a listbox, a combobox or a selection verb ever appears on that screen.

Honesty is enforced by tests, not by intention. Offline previews are watermarked and flagged simulated: true all the way to the UI. The fit verdict names the weakest piece only when one genuinely stands out — when everything is equal, it says so rather than inventing a culprit. Undeclared garments get an explicit prior and lower confidence.

Every refusal has a remedy. We removed a refusal that had none: it told users to retake a perfectly good photo because their outfit was undescribed, which no amount of retaking could fix.

An audit that catches what unit tests can't. 324 backend tests and 38 frontend tests, plus an end-to-end audit of the assembled system — 38 checks in production. It verifies that a piece declared absent is never listed as "kept", that a FIT verdict never coexists with a demanded change, and that media URLs actually resolve — a class of failure that returns HTTP 201 while showing nothing.

Both integrations are verified live, not assumed. Skin AI returns redness, radiance, oiliness and texture on a real full-length photograph; Apparel VTO returns a genuine render with simulated: false. Getting there settled two things we could not have guessed: a photo cropped into a 1:3.12 strip breaks the try-on, and error_src_face_too_small usually means "I cannot find a face" — the rejected photo carried a 906 px face while the accepted one carried 289 px. The difference was sunglasses.

Every try-on failure now reports whether the garment came from the catalogue or from the user, so diagnosis takes one line instead of one afternoon. And because some references are refused for reasons no check of ours predicts, a failed render falls back once to the next piece in the same category — the decision never changes, only the piece used as proof.

What we learned

Measure before you claim. We believed the contextual thesis was working because the engine's tables said so. It took one measurement across all ten occasions to discover that users saw one identical answer. A property that is true in the code and invisible on screen doesn't exist.

A refusal without a remedy is a dead end. Error messages should name a gesture the user can actually perform. Anything else sends people in circles.

Any optimisation that memoises must be keyed on everything the result depends on — otherwise it silently turns a user's correction into an illusion.

A deliverable must never write where the user writes. Our project archive contained placeholder garment images, so updating the project deleted the user's imported photos. Their catalogue now lives outside the source tree.

Honesty is a feature, and it costs less than it looks. Saying "we don't know this" and lowering a confidence score is cheaper to build than pretending, and far cheaper than being caught.

What's next for MIRROR OPS

Feedback on the decision. The product rules and never listens. One button — "that wasn't the right piece" — would give the only signal that lets the scoring model be calibrated on evidence rather than on our judgement. It's also the first thing an enterprise buyer asks for.

Retail catalogues via URL manifest. A retailer already has product visuals online. Their catalogue becomes an id → URL list, imported in one command, with no further integration. The decision layer is the product; the catalogue is just an input.

A second look at the Skin AI framing. On a full-length shot the face is often too small even after cropping. That's a real constraint, and it deserves to be designed around rather than hidden — possibly by making the skin signal an explicit, optional close-up step rather than a silent best-effort.

Built With

Share this project:

Updates