Drawing no. QP-CS-03 / Sheet 2 - Engineering detail

How the AI stylist actually works

Sheet 1 says what was built. This sheet says how: the pipeline that keeps the stylist inside the user's own wardrobe, the two-stage trick that renders real garments rather than imagined ones, and the faults the process caught before users met them. Every figure on this page is read straight from the codebase.

← Back to sheet 1, the case study
Fig. 2 - From question to worn preview, stage by stage
FILTERPROMPTMODELSPLITVALIDATECOMPOSEIMG2IMGO4-MINIFLUX DEVAN ITEM THE USER DOES NOT OWN DOES NOT SURVIVE THE VALIDATE STAGE
1.0

The constraint that shaped everything

Ask a language model to dress you and it will, cheerfully, from a wardrobe that does not exist. The product promise here was the opposite: outfits assembled from the clothes actually hanging in the user's wardrobe, and previews showing those clothes, not lookalikes. So the design goal was fixed early. No suggestion that cannot be resolved to owned garments, and no preview that is not anchored to the user's own photos.

That splits into two problems. The first is containment: the model's output is treated as a proposal, and code decides whether each proposed item really exists in the wardrobe. The second is harder, because image models cannot be handed a wardrobe at all: a prompt can describe a black wool coat, but it cannot reference yours. The answer to that one is the two-stage pipeline in section 3.0.

Design rules
  • The model sees only a reduced projection of the user's own wardrobe, serialised into the prompt
  • Suggested item IDs are resolved against the wardrobe and role-checked in code after generation
  • Previews are generated over a composite of the user's real garment photos, never from text alone
  • The image generation token lives in Secret Manager and is read only by the Cloud Function
2.0

From question to outfit

Every styling request starts by shrinking the problem. The full wardrobe is fetched, then filtered by live weather through temperature-to-season bands: above 22°C the stylist sees summer and spring pieces, below 8°C winter and autumn, with the weather itself served from a cache invalidated after 120 minutes or a 5 km change in location, whichever comes first. Occasion filtering follows a defined fallback graph, work borrows from everyday, formal from evening, so a sparse wardrobe degrades to a sensible neighbour instead of an empty result. What survives is serialised into the prompt as a reduced projection, item ID, brand, category, colour, style tags, occasion and piece role, enough to style with and nothing more. The four agents themselves are explicit decision trees, not open-ended personas: ordered steps with hard reject rules such as at most one hero piece per outfit, no duplicated roles, and named conditions that force a regeneration.

The model streams its reply under a strict contract: prose for the user, a delimiter, then a JSON block naming the chosen item IDs. The two halves are split, and the JSON is put through the same compatibility rules used before the prompt: every ID is looked up in the user's wardrobe, and clashes such as two bottoms or two pairs of shoes are filtered out. Only the validated outfit is written to the conversation, and the chat renders from a live Firestore listener, so history, refresh and resume come for free. Conversation context is truncated to the system prompt plus the most recent nineteen messages, which keeps long sessions from dragging the whole transcript into every call.

3.0

Dressing users in their own clothes

3.1
Composite first, generate second

The suggested garments' real photos are laid out on a hidden 400 by 600 canvas, positioned off-screen and layered by piece role: shoes under bottoms under tops under outerwear under accessories. Three fixed layout templates cover the common outfit shapes, and a dynamic layout divides the canvas at runtime when tops and outerwear have to share it.

3.2
Readiness is counted, not assumed

Capture fires only when the image loader reports every garment photo loaded and the layout counter confirms each one has actually been placed. Nothing about the capture is timed; it is gated on counts agreeing. Section 7.0 tells the story of the version that slept instead.

3.3
Anchored generation

The captured composite is uploaded and a Cloud Function runs FLUX.1-dev image-to-image over it: strength 0.65, 30 steps, 512 by 768 portrait output. Strength is the whole trade: high enough to render a person wearing the outfit, low enough that the garments stay recognisably the user's.

3.4
Cached by content

The cache key is built from the sorted item IDs, plus the face photo when one is set, so the same outfit requested in any order hits the same entry and a repeat visualisation costs nothing. Results and failures are both persisted, the failures to their own Firestore collection for diagnosis.

4.0

Safety and enforcement

Moderation is wired where the app cannot route around it: a Cloud Function triggered by the storage bucket itself runs Google Cloud Vision SafeSearch on every uploaded image. A likely adult or violent result, or a strongly racy one, moves the file into quarantine and marks the item; an in-app queue lets an admin approve or reject, and rejection deletes the file outright. Because the trigger sits on the bucket, no client build, current or future, can skip the check.

Commercial limits get the same treatment as safety ones. The free tier allows 10 AI generations and 100 wardrobe items, and the usage counter is cached on the device and checked against a connectivity probe, so going offline does not grant free generations; the count syncs when the connection returns. On privacy, the defaults do the work: Mixpanel starts opted out, Firebase Analytics receives a randomised ID rather than the account ID while Crashlytics keeps the real one for support, marketing email is a separate opt-in recorded with a timestamp, and account deletion removes the data and the credentials together.

Enforced server-side
  • SafeSearch moderation triggered by the storage bucket, not the app
  • Quarantine path with an in-app human review queue
  • Free-tier limits that hold with the device offline
  • Consent recorded with timestamps; deletion removes data and credentials
  • Server secrets held in Google Secret Manager
5.0

The app around the stylist

5.1
Votes that feel instant and stay correct

Community voting mutates local state immediately, handling all three cases in the client: a new vote, a switched vote, a vote removed. A Firestore transaction then replays exactly the same three cases server-side. The UI never waits on the network, and concurrent voters cannot corrupt a count.

5.2
Sharing on real deep links

Wardrobe and wishlist sections are shareable by link, with gifting mode as a wishlist variant. The links are universal links, associated domains on iOS and verified intent filters on Android, so a shared wardrobe opens straight into the installed app rather than dead-ending in a browser.

5.3
Admin without a second system

Administration lives inside the app behind custom-claim role checks: granting and revoking admin runs through server-side functions, and the moderation queue is just another screen. There is no separate dashboard to build, host or secure.

5.4
Insights from data already there

The insights screen computes wardrobe value, wear frequency, never-worn counts and category, colour and brand breakdowns directly from the wardrobe items, with no extra tracking pipeline behind it.

6.0

The numbers, and where they come from

0.65
Image-to-image strength: the dial between preserving real garments and rendering a worn look
100%
Uploads moderated server-side: the trigger fires on the storage bucket, not in the app
120 min / 5 km
Weather cache invalidation, whichever of time or distance trips first
10 + 100
Free-tier caps on AI generations and wardrobe items, enforced offline

These are engineering constants read from the source, stated because each one records a decision: how much an image generation may transform the garments, where moderation sits, when cached weather is too stale to style with, and what the free tier includes.

Where other sheets in this series quote latency and load figures, this one does not. We hold no usage or performance measurements for Auddr.ai that we would defend in front of a prospect, and a figure we cannot defend does not go on the page.

7.0

Faults the process caught

7.1
A sleep is not a synchronisation

The first pass at capturing the hidden canvas waited 500 milliseconds and hoped the garment photos had loaded, which meant the capture raced the image loader and lost whenever a device or connection was slow enough, producing half-composed outfits. The fix replaced hope with a count: every image reports when it has loaded and when it has laid out, and capture fires only when the counts agree.

7.2
Three chats that should have been one

A system review found the three stylist modes had each grown a private chat interface with its own state and its own model calls, and none of those conversations persisted to history. Caught by walking the flows end to end rather than testing screens in isolation. The refactor cut the mode screens back to input forms feeding one chat screen with an agent dispatcher, and every conversation became durable.

7.3
The cache that was specified but never built

The plan specified caching of generated previews; a written gap analysis comparing the plan against the built system found it missing, so every repeat of the same outfit was paying for a fresh generation. Caught by the audit, not by a bill. The fix keys the cache on sorted item IDs, so the same garments in any order resolve to the same entry.

8.0

Deployment and operations

The app ships through EAS Build with five profiles, from simulator development builds to store production, and version numbers are managed remotely with auto-increment so a release cannot reuse a build number. Store compliance was treated as engineering rather than paperwork: the Android target SDK held at the level Play policy requires, Apple sign-in alongside Google, and permission strings and encryption declarations maintained in configuration instead of clicked through a console.

The serverless surface is deliberately small: eight Cloud Functions, capped at ten instances, with lint and compile run as a predeploy gate on every release. Failures write to dedicated Firestore collections for visualisation, moderation and email sync, so a support question starts from persisted evidence rather than log archaeology. Subscription state flows from the RevenueCat webhook into Firestore and on to Brevo, and the lifecycle email sequence hangs off that sync rather than off anything the app does.

Operational surface
  • Five EAS build profiles; store versioning managed remotely
  • Cloud Functions capped at ten instances; lint and build gate every deploy
  • Failures persisted to Firestore audit collections
  • RevenueCat webhook syncs subscription state to Firestore and Brevo
  • Server secrets in Google Secret Manager, never in the binary