Try-on pipeline
This is the core of Drape: turning one body photo plus a garment into a render of
that garment on that body. The entry point is POST /api/try-on
(generate_try_on), with the heavy work in _run_tryon_background.
The full path
Gate checks
Before any compute, generate_try_on enforces, in order: the RENDER_ENABLED
kill switch, an allowlist bypass for testers, the rate limits (below), BIPA
consent (403 if consent_given is false), and body-photo ownership — if
body_photo_owner_id does not match the authenticated user, the request is
blocked with HTTP 409 so a contaminated photo never reaches the AI pipeline.
Model routing
determine_tryon_model picks the provider from the product's garment_style and
tags:
The tech notes describe "baggy/wide-leg → FLUX.2 LoRA", but the live routing
sends baggy garments to Fashn tryon-max. A flux2-lora branch exists in the
code, but no default rule selects it — a product must be explicitly tagged to
reach FLUX.2. Treat FLUX.2 as present-but-inactive until a product is deliberately
routed to it. This is flagged on the tech-debt page.
Garment masking & preprocessing
The Fashn path does real image work before the model sees anything:
- rembg (
fal-ai/imageutils/rembg) removes the background from the body photo, with a retry wrapper. - EVF-SAM (
fal-ai/evf-sam) produces upper/lower garment masks;apply_garment_maskfills the existing-garment region with a neutral#EBEBEBso the model has a clean canvas instead of fighting the old garment. - The cutout is reframed so the person fills roughly two-thirds of the
canvas, composited onto the neutral background, and uploaded to the private
body-photos-cutoutbucket as a signed URL. _call_fashn_tryonruns, capped by a semaphore (MAX_CONCURRENT_RENDERS = 3), with a 180-second timeout and one retry; bottoms fall back fromtryon-maxtotryon-v1.6.
The garment category is normalized from merchandising terms (tops / bottoms / one-pieces / auto) to what Fashn expects.
Caching
The Redis cache key is composed from the user, product, color, size, the body photo hash, and a hash of the garment URL:
cache_key = generate_cache_key(user_id, product_id, color, size, body_photo_hash, garment_url)
Because body_photo_hash is part of the key, uploading a new body photo
auto-busts stale renders with no manual cache clear. A cache hit returns
immediately with job_id = "cached" and the stored URL — no polling. Concurrent
identical requests are de-duplicated via an inflight: key that returns the
existing job id. Valid renders are cached for 24 hours.
Status polling & ownership
`GET /api/try-on/status/{job_id}` polls every 3 seconds up to a 180-second
timeout. The Redis job payload stores user_id at creation, and the poll handler
verifies the caller owns the job — a mismatch returns 403. This closed the
original IDOR where any authenticated user who guessed a job id could read another
user's render (derived from their body photo). Missing jobs report as expired.
Rate limiting
A render must pass all of these Redis-backed counters (fail-closed — a Redis outage returns 503, never unlimited):
| Limit | Value |
|---|---|
| Daily try-ons per user | 25 |
| Global daily render cap | 1000 (env-configurable) |
| Burst guard | 5 renders / 10 minutes |
| Per-garment cooldown | 8 seconds |
| Status poll | 60 / minute |
Allowlisted testers bypass the daily, burst, and cooldown limits.
Silent failure modes
Several pipeline steps can return 200 OK while producing a wrong result — these
are known and tracked for Sentry wiring:
rembgreturnsNone→ cutout missing, pipeline continues.body_photo_datawas never written → try-on finds no photo.- Fashn returns a source/marketing image → treated as a successful render.
- The analyze-body-photos pipeline throws → measurements are written but the photo is skipped.
is_valid_render guards the obvious cases (a result must be an https:// or
data:image/ URL, else the cache entry is deleted and a Sentry warning fires),
but semantic wrongness ("this isn't the right garment") is not something the
backend can detect. See the tech-debt page for the render
quality issues that stem from this.